Add page for calculating paypal invoice taxes
This commit is contained in:
@@ -452,9 +452,8 @@ tr {
|
||||
border-bottom: 1px var(--separator) solid;
|
||||
}
|
||||
|
||||
tr>td,
|
||||
tr>th {
|
||||
padding: 0.2em;
|
||||
tr>td {
|
||||
padding: 0.2em 0.5em;
|
||||
}
|
||||
|
||||
/* API documentation markup */
|
||||
|
@@ -2,27 +2,16 @@
|
||||
import { onMount } from "svelte";
|
||||
import { formatDate } from "util/Formatting";
|
||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
||||
import Euro from "util/Euro.svelte";
|
||||
import { get_endpoint } from "lib/PixeldrainAPI";
|
||||
|
||||
type Invoice = {
|
||||
id: string
|
||||
time: string
|
||||
amount: number
|
||||
vat: number
|
||||
processing_fee: number
|
||||
country: string
|
||||
payment_gateway: string
|
||||
payment_method: string
|
||||
status: string
|
||||
}
|
||||
import Expandable from "util/Expandable.svelte";
|
||||
import SortableTable, { FieldType } from "layout/SortableTable.svelte";
|
||||
import { country_name, get_admin_invoices, type Invoice } from "lib/AdminAPI";
|
||||
import PayPalVat from "./PayPalVAT.svelte";
|
||||
|
||||
let loading = true
|
||||
let invoices: Invoice[] = []
|
||||
|
||||
let year = 0
|
||||
let month = 0
|
||||
let month_str = ""
|
||||
|
||||
type Total = {
|
||||
count: number
|
||||
@@ -49,19 +38,35 @@ const add_total = (i: Invoice) => {
|
||||
totals_country[i.country].vat += i.vat
|
||||
totals_country[i.country].fee += i.processing_fee
|
||||
}
|
||||
const obj_to_list = (obj: {[id: string]: Total}) => {
|
||||
let list: ({id: string} & Total)[] = []
|
||||
for (const key in obj) {
|
||||
list.push({id: key, ...obj[key]})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
const eu_countries = [
|
||||
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
|
||||
"DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
|
||||
"PL", "PT", "RO", "SK", "SI", "ES", "SE",
|
||||
]
|
||||
const obj_to_list_eu = (obj: {[id: string]: Total}) => {
|
||||
let list: ({id: string} & Total)[] = []
|
||||
for (const key in obj) {
|
||||
if (eu_countries.includes(key)) {
|
||||
list.push({id: key, ...obj[key]})
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
const get_invoices = async () => {
|
||||
loading = true;
|
||||
month_str = year + "-" + ("00"+(month)).slice(-2)
|
||||
try {
|
||||
const resp = await fetch(get_endpoint()+"/admin/invoices/"+month_str);
|
||||
if(resp.status >= 400) {
|
||||
throw new Error(await resp.text());
|
||||
}
|
||||
const resp = await get_admin_invoices(year, month)
|
||||
|
||||
let resp_json = await resp.json() as Invoice[];
|
||||
|
||||
resp_json.sort((a, b) => {
|
||||
resp.sort((a, b) => {
|
||||
if (a.status !== b.status) {
|
||||
return a.status.localeCompare(b.status)
|
||||
}
|
||||
@@ -73,7 +78,7 @@ const get_invoices = async () => {
|
||||
|
||||
totals_provider = {}
|
||||
totals_country = {}
|
||||
resp_json.forEach(row => {
|
||||
resp.forEach(row => {
|
||||
if (row.status === "paid") {
|
||||
add_total(row)
|
||||
}
|
||||
@@ -82,7 +87,8 @@ const get_invoices = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
invoices = resp_json
|
||||
invoices = resp
|
||||
filter_invoices()
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
} finally {
|
||||
@@ -96,7 +102,6 @@ const last_month = () => {
|
||||
month = 12
|
||||
year--
|
||||
}
|
||||
|
||||
get_invoices()
|
||||
}
|
||||
const next_month = () => {
|
||||
@@ -105,7 +110,6 @@ const next_month = () => {
|
||||
month = 1
|
||||
year++
|
||||
}
|
||||
|
||||
get_invoices()
|
||||
}
|
||||
|
||||
@@ -115,12 +119,49 @@ onMount(() => {
|
||||
month = now.getMonth()+1
|
||||
get_invoices()
|
||||
})
|
||||
|
||||
let status_filter = {
|
||||
canceled: {checked: false},
|
||||
expired: {checked: false},
|
||||
open: {checked: false},
|
||||
paid: {checked: true},
|
||||
}
|
||||
let gateway_filter = {}
|
||||
let method_filter = {}
|
||||
|
||||
const filter_invoices = () => {
|
||||
records_hidden = 0
|
||||
invoices_filtered = invoices.filter(inv => {
|
||||
if (status_filter[inv.status] === undefined) {
|
||||
status_filter[inv.status] = {checked: true}
|
||||
}
|
||||
if (gateway_filter[inv.payment_gateway] === undefined) {
|
||||
gateway_filter[inv.payment_gateway] = {checked: true}
|
||||
}
|
||||
if (method_filter[inv.payment_method] === undefined) {
|
||||
method_filter[inv.payment_method] = {checked: true}
|
||||
}
|
||||
|
||||
if(
|
||||
status_filter[inv.status].checked === true &&
|
||||
gateway_filter[inv.payment_gateway].checked === true &&
|
||||
method_filter[inv.payment_method].checked === true
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
records_hidden++
|
||||
return false
|
||||
})
|
||||
}
|
||||
let records_hidden = 0
|
||||
let invoices_filtered: Invoice[] = []
|
||||
</script>
|
||||
|
||||
<LoadingIndicator loading={loading}/>
|
||||
|
||||
<section>
|
||||
<h3>{month_str}</h3>
|
||||
<h3>{year + "-" + ("00"+(month)).slice(-2)}</h3>
|
||||
<div class="toolbar">
|
||||
<button on:click={last_month}>
|
||||
<i class="icon">chevron_left</i>
|
||||
@@ -133,93 +174,123 @@ onMount(() => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h4>Invoices per payment processor</h4>
|
||||
<div class="table_scroll" style="text-align: initial;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Provider</td>
|
||||
<td>Count</td>
|
||||
<td>Amount</td>
|
||||
<td>VAT</td>
|
||||
<td>Fee</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each Object.entries(totals_provider) as [key, tot]}
|
||||
<tr>
|
||||
<td>{key}</td>
|
||||
<td>{tot.count}</td>
|
||||
<td><Euro amount={tot.amount}/></td>
|
||||
<td><Euro amount={tot.vat}/></td>
|
||||
<td><Euro amount={tot.fee}/></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Expandable click_expand>
|
||||
<div slot="header" class="header">Per payment processor</div>
|
||||
<SortableTable
|
||||
index_field="id"
|
||||
rows={obj_to_list(totals_provider)}
|
||||
columns={[
|
||||
{field: "id", label: "Provider", type: FieldType.Text},
|
||||
{field: "count", label: "Count", type: FieldType.Number},
|
||||
{field: "amount", label: "Amount", type: FieldType.Euro},
|
||||
{field: "vat", label: "VAT", type: FieldType.Euro},
|
||||
{field: "fee", label: "Fee", type: FieldType.Euro},
|
||||
]}
|
||||
totals
|
||||
/>
|
||||
</Expandable>
|
||||
|
||||
<h4>Invoices per country</h4>
|
||||
<div class="table_scroll" style="text-align: initial;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Country</td>
|
||||
<td>Count</td>
|
||||
<td>Amount</td>
|
||||
<td>VAT</td>
|
||||
<td>Fee</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each Object.entries(totals_country) as [key, tot]}
|
||||
<tr>
|
||||
<td>{key}</td>
|
||||
<td>{tot.count}</td>
|
||||
<td><Euro amount={tot.amount}/></td>
|
||||
<td><Euro amount={tot.vat}/></td>
|
||||
<td><Euro amount={tot.fee}/></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Expandable click_expand>
|
||||
<div slot="header" class="header">Per country</div>
|
||||
<SortableTable
|
||||
index_field="id"
|
||||
rows={obj_to_list(totals_country)}
|
||||
columns={[
|
||||
{field: "id", label: "Country", type: FieldType.Func, func: val => country_name(val)},
|
||||
{field: "count", label: "Count", type: FieldType.Number},
|
||||
{field: "amount", label: "Amount", type: FieldType.Euro},
|
||||
{field: "vat", label: "VAT", type: FieldType.Euro},
|
||||
{field: "fee", label: "Fee", type: FieldType.Euro},
|
||||
]}
|
||||
totals
|
||||
/>
|
||||
</Expandable>
|
||||
|
||||
<Expandable click_expand>
|
||||
<div slot="header" class="header">In European Union</div>
|
||||
<SortableTable
|
||||
index_field="id"
|
||||
rows={obj_to_list_eu(totals_country)}
|
||||
columns={[
|
||||
{field: "id", label: "Country", type: FieldType.Func, func: val => country_name(val)},
|
||||
{field: "count", label: "Count", type: FieldType.Number},
|
||||
{field: "amount", label: "Amount", type: FieldType.Euro},
|
||||
{field: "vat", label: "VAT", type: FieldType.Euro},
|
||||
{field: "fee", label: "Fee", type: FieldType.Euro},
|
||||
]}
|
||||
totals
|
||||
/>
|
||||
</Expandable>
|
||||
|
||||
<Expandable click_expand>
|
||||
<div slot="header" class="header">PayPal VAT</div>
|
||||
<PayPalVat invoices={invoices}/>
|
||||
</Expandable>
|
||||
|
||||
<h4>All invoices</h4>
|
||||
<div class="filters">
|
||||
<div class="filter">
|
||||
Status:<br/>
|
||||
{#each Object.keys(status_filter) as filter}
|
||||
<input
|
||||
type="checkbox"
|
||||
id="status_{filter}"
|
||||
bind:checked={status_filter[filter].checked}
|
||||
on:change={filter_invoices}>
|
||||
<label for="status_{filter}">{filter}</label>
|
||||
<br/>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="filter">
|
||||
Gateways:<br/>
|
||||
{#each Object.keys(gateway_filter) as filter}
|
||||
<input
|
||||
type="checkbox"
|
||||
id="gateway_{filter}"
|
||||
bind:checked={gateway_filter[filter].checked}
|
||||
on:change={filter_invoices}>
|
||||
<label for="gateway_{filter}">{filter}</label>
|
||||
<br/>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="filter">
|
||||
Methods:<br/>
|
||||
{#each Object.keys(method_filter) as filter}
|
||||
<input
|
||||
type="checkbox"
|
||||
id="method_{filter}"
|
||||
bind:checked={method_filter[filter].checked}
|
||||
on:change={filter_invoices}>
|
||||
<label for="method_{filter}">{filter}</label>
|
||||
<br/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
Total: {invoices.length}
|
||||
Visible: {invoices.length-records_hidden}
|
||||
Hidden: {records_hidden}
|
||||
</section>
|
||||
|
||||
<div class="table_scroll" style="text-align: initial;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Time</td>
|
||||
<td>ID</td>
|
||||
<td>Amount</td>
|
||||
<td>VAT</td>
|
||||
<td>Fee</td>
|
||||
<td>Country</td>
|
||||
<td>Gateway</td>
|
||||
<td>Method</td>
|
||||
<td>Status</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each invoices as row (row.id)}
|
||||
<tr>
|
||||
<td>{formatDate(row.time)}</td>
|
||||
<td>{row.id}</td>
|
||||
<td><Euro amount={row.amount}/></td>
|
||||
<td><Euro amount={row.vat}/></td>
|
||||
<td><Euro amount={row.processing_fee}/></td>
|
||||
<td>{row.country}</td>
|
||||
<td>{row.payment_gateway}</td>
|
||||
<td>{row.payment_method}</td>
|
||||
<td>{row.status}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<SortableTable
|
||||
index_field="id"
|
||||
sort_field="time"
|
||||
rows={invoices_filtered}
|
||||
columns={[
|
||||
{field: "time", label: "Time", type: FieldType.Func, func: val => formatDate(val)},
|
||||
{field: "id", label: "ID", type: FieldType.Text},
|
||||
{field: "amount", label: "Amount", type: FieldType.Euro},
|
||||
{field: "vat", label: "VAT", type: FieldType.Euro},
|
||||
{field: "processing_fee", label: "Fee", type: FieldType.Euro},
|
||||
{field: "country", label: "Country", type: FieldType.Func, func: val => country_name(val)},
|
||||
{field: "payment_gateway", label: "Gateway", type: FieldType.Text},
|
||||
{field: "payment_method", label: "Method", type: FieldType.Text},
|
||||
{field: "status", label: "Status", type: FieldType.Text},
|
||||
]}
|
||||
totals
|
||||
/>
|
||||
|
||||
<style>
|
||||
.toolbar {
|
||||
@@ -229,4 +300,15 @@ onMount(() => {
|
||||
}
|
||||
.toolbar > * { flex: 0 0 auto; }
|
||||
.toolbar_spacer { flex: 1 1 auto; }
|
||||
.header {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
}
|
||||
</style>
|
||||
|
@@ -155,18 +155,18 @@ onMount(() => {
|
||||
<td><Euro amount={per_country.NL.vat}/></td>
|
||||
<td>8040 - Omzet PayPal inkomsten</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><Euro amount={totals.vat-per_country.NL.vat}/></td>
|
||||
<td>Geen BTW</td>
|
||||
<td><Euro amount={0}/></td>
|
||||
<td>1651 - BTW OSS</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><Euro amount={totals.amount-totals.fee-per_country.NL.amount}/></td>
|
||||
<td>Geen BTW</td>
|
||||
<td><Euro amount={0}/></td>
|
||||
<td>8040 - Omzet PayPal inkomsten</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><Euro amount={totals.vat-per_country.NL.vat}/></td>
|
||||
<td>Geen BTW</td>
|
||||
<td><Euro amount={0}/></td>
|
||||
<td>1651 - BTW OSS</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
184
svelte/src/admin_panel/PayPalVAT.svelte
Normal file
184
svelte/src/admin_panel/PayPalVAT.svelte
Normal file
@@ -0,0 +1,184 @@
|
||||
<script lang="ts">
|
||||
import { country_name, type Invoice } from "lib/AdminAPI";
|
||||
import Euro from "util/Euro.svelte";
|
||||
|
||||
export let invoices: Invoice[] = []
|
||||
|
||||
type Country = {
|
||||
vat: number,
|
||||
amount: number,
|
||||
fee: number,
|
||||
count: number,
|
||||
vat_fraction: number,
|
||||
}
|
||||
|
||||
let totals: {
|
||||
count: number,
|
||||
vat: number,
|
||||
amount: number,
|
||||
fee: number,
|
||||
}
|
||||
|
||||
$: per_country = update_countries(invoices)
|
||||
|
||||
const update_countries = (invoices: Invoice[]) => {
|
||||
let per_country: {[id: string]: Country} = {
|
||||
NL: {
|
||||
vat: 0,
|
||||
amount: 0,
|
||||
fee: 0,
|
||||
count: 0,
|
||||
vat_fraction: .21,
|
||||
}
|
||||
}
|
||||
totals = {
|
||||
count: 0,
|
||||
vat: 0,
|
||||
amount: 0,
|
||||
fee: 0,
|
||||
}
|
||||
|
||||
invoices.forEach(row => {
|
||||
if (row.status !== "paid" || row.payment_method !== "paypal") {
|
||||
return
|
||||
}
|
||||
if (!per_country[row.country]) {
|
||||
per_country[row.country] = {
|
||||
vat: 0,
|
||||
amount: 0,
|
||||
fee: 0,
|
||||
count: 0,
|
||||
vat_fraction: row.vat/row.amount,
|
||||
}
|
||||
}
|
||||
per_country[row.country].vat += row.vat
|
||||
per_country[row.country].amount += row.amount
|
||||
per_country[row.country].fee += row.processing_fee
|
||||
per_country[row.country].count++
|
||||
totals.vat += row.vat
|
||||
totals.amount += row.amount
|
||||
totals.fee += row.processing_fee
|
||||
totals.count++
|
||||
})
|
||||
|
||||
// Sort the countries. Don't ask me how it works
|
||||
per_country = Object.keys(per_country).sort().reduce(
|
||||
(obj, key) => {
|
||||
obj[key] = per_country[key]
|
||||
return obj
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
return per_country
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
{#if per_country["NL"] && totals}
|
||||
<h2>Summary</h2>
|
||||
<table style="width: auto;">
|
||||
<tr>
|
||||
<td>Total PayPal earnings -fees</td>
|
||||
<td><Euro amount={totals.vat+totals.amount-totals.fee}/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total VAT NL</td>
|
||||
<td><Euro amount={per_country["NL"].vat}/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total VAT OSS</td>
|
||||
<td><Euro amount={totals.vat-per_country["NL"].vat}/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Accounting information</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Bedrag</td>
|
||||
<td>BTW-code</td>
|
||||
<td>BTW</td>
|
||||
<td>Tegenrekening</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><Euro amount={per_country["NL"].amount + per_country["NL"].vat}/></td>
|
||||
<td>BTW hoog 21%</td>
|
||||
<td><Euro amount={per_country["NL"].vat}/></td>
|
||||
<td>8040 - Omzet PayPal inkomsten</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><Euro amount={totals.amount-totals.fee-per_country["NL"].amount}/></td>
|
||||
<td>Geen BTW</td>
|
||||
<td><Euro amount={0}/></td>
|
||||
<td>8040 - Omzet PayPal inkomsten</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><Euro amount={totals.vat-per_country["NL"].vat}/></td>
|
||||
<td>Geen BTW</td>
|
||||
<td><Euro amount={0}/></td>
|
||||
<td>1651 - BTW OSS</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Taxes per country</h2>
|
||||
<div class="table_scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Country</td>
|
||||
<td>Payments</td>
|
||||
<td>Amount</td>
|
||||
<td>VAT</td>
|
||||
<td>VAT%</td>
|
||||
<td>Total</td>
|
||||
<td>Fee</td>
|
||||
<td>Total</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each Object.entries(per_country) as [country, row]}
|
||||
{#if row.vat > 0}
|
||||
<tr>
|
||||
<td>{country_name(country)}</td>
|
||||
<td>{row.count}</td>
|
||||
<td><Euro amount={row.amount}/></td>
|
||||
<td><Euro amount={row.vat}/></td>
|
||||
<td>{row.vat_fraction*100}%</td>
|
||||
<td><Euro amount={row.vat+row.amount}/></td>
|
||||
<td><Euro amount={-row.fee}/></td>
|
||||
<td><Euro amount={row.vat+row.amount-row.fee}/></td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<tr>
|
||||
<td>Total</td>
|
||||
<td>{totals.count}</td>
|
||||
<td><Euro amount={totals.amount}/></td>
|
||||
<td><Euro amount={totals.vat}/></td>
|
||||
<td></td>
|
||||
<td><Euro amount={totals.vat+totals.amount}/></td>
|
||||
<td><Euro amount={-totals.fee}/></td>
|
||||
<td><Euro amount={(totals.vat+totals.amount)-totals.fee}/></td>
|
||||
</tr>
|
||||
{#if per_country["NL"]}
|
||||
<tr>
|
||||
<td>Total ex NL</td>
|
||||
<td>{totals.count - per_country["NL"].count}</td>
|
||||
<td><Euro amount={totals.amount-per_country["NL"].amount}/></td>
|
||||
<td><Euro amount={totals.vat-per_country["NL"].vat}/></td>
|
||||
<td></td>
|
||||
<td><Euro amount={(totals.vat-per_country["NL"].vat)+(totals.amount-per_country["NL"].amount)}/></td>
|
||||
<td><Euro amount={-(totals.fee-per_country["NL"].fee)}/></td>
|
||||
<td><Euro amount={(totals.vat-per_country["NL"].vat)+(totals.amount-per_country["NL"].amount)-(totals.fee-per_country["NL"].fee)}/></td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
@@ -14,7 +14,7 @@ import InvoiceVat from "./InvoiceVAT.svelte";
|
||||
let pages = [
|
||||
{
|
||||
path: "/admin",
|
||||
title: "Status",
|
||||
title: "Admin panel",
|
||||
icon: "home",
|
||||
component: Home,
|
||||
}, {
|
||||
|
135
svelte/src/layout/SortableTable.svelte
Normal file
135
svelte/src/layout/SortableTable.svelte
Normal file
@@ -0,0 +1,135 @@
|
||||
<script lang="ts" context="module">
|
||||
export enum FieldType {
|
||||
Text,
|
||||
Bytes,
|
||||
Bits,
|
||||
Number,
|
||||
Euro,
|
||||
HTML,
|
||||
Func,
|
||||
}
|
||||
export type SortColumn = {
|
||||
field: string,
|
||||
label: string,
|
||||
type: FieldType,
|
||||
func?: (val: any) => string
|
||||
};
|
||||
</script>
|
||||
<script lang="ts">
|
||||
import SortButton from "layout/SortButton.svelte";
|
||||
import Euro from "util/Euro.svelte";
|
||||
import { formatDataVolume, formatDataVolumeBits, formatThousands } from "util/Formatting";
|
||||
|
||||
export let index_field = ""
|
||||
export let rows = [];
|
||||
export let columns: SortColumn[] = []
|
||||
export let sort_field = index_field
|
||||
export let totals = false
|
||||
|
||||
let asc = true
|
||||
const sort = (field: string) => {
|
||||
if (field !== "" && field === sort_field) {
|
||||
asc = !asc
|
||||
}
|
||||
if (field === "") {
|
||||
field = sort_field
|
||||
}
|
||||
sort_field = field
|
||||
|
||||
console.log("sorting by", field, "asc", asc)
|
||||
rows.sort((a, b) => {
|
||||
if (typeof (a[field]) === "number") {
|
||||
// Sort ints from high to low
|
||||
if (asc) {
|
||||
return a[field] - b[field]
|
||||
} else {
|
||||
return b[field] - a[field]
|
||||
}
|
||||
} else {
|
||||
// Sort strings alphabetically
|
||||
if (asc) {
|
||||
return a[field].localeCompare(b[field])
|
||||
} else {
|
||||
return b[field].localeCompare(a[field])
|
||||
}
|
||||
}
|
||||
})
|
||||
rows = rows
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="table_scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{#each columns as col (col.field)}
|
||||
<td>
|
||||
<SortButton field={col.field} active_field={sort_field} asc={asc} sort_func={sort}>
|
||||
{col.label}
|
||||
</SortButton>
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each rows as row (row[index_field])}
|
||||
<tr>
|
||||
{#each columns as col (col.field)}
|
||||
{#if col.type === FieldType.Text}
|
||||
<td>{row[col.field]}</td>
|
||||
{:else if col.type === FieldType.Bytes}
|
||||
<td class="number_cell">{formatDataVolume(row[col.field], 3)}</td>
|
||||
{:else if col.type === FieldType.Bits}
|
||||
<td class="number_cell">{formatDataVolumeBits(row[col.field], 3)}</td>
|
||||
{:else if col.type === FieldType.Number}
|
||||
<td class="number_cell">{formatThousands(row[col.field])}</td>
|
||||
{:else if col.type === FieldType.Euro}
|
||||
<td class="number_cell"><Euro amount={row[col.field]}/></td>
|
||||
{:else if col.type === FieldType.Func}
|
||||
<td>{@html col.func(row[col.field])}</td>
|
||||
{:else if col.type === FieldType.HTML}
|
||||
<td>{@html row[col.field]}</td>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
{#if totals === true}
|
||||
<tr>
|
||||
{#each columns as col (col.field)}
|
||||
{#if col.field === index_field}
|
||||
<td>Total</td>
|
||||
{:else if col.type === FieldType.Bytes}
|
||||
<td class="number_cell">{formatDataVolume(rows.reduce((acc, val) => acc+val[col.field], 0), 3)}</td>
|
||||
{:else if col.type === FieldType.Bits}
|
||||
<td class="number_cell">{formatDataVolumeBits(rows.reduce((acc, val) => acc+val[col.field], 0), 3)}</td>
|
||||
{:else if col.type === FieldType.Number}
|
||||
<td class="number_cell">{formatThousands(rows.reduce((acc, val) => acc+val[col.field], 0))}</td>
|
||||
{:else if col.type === FieldType.Euro}
|
||||
<td class="number_cell"><Euro amount={rows.reduce((acc, val) => acc+val[col.field], 0)}/></td>
|
||||
{:else}
|
||||
<td></td>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
table {
|
||||
width: auto;
|
||||
min-width: auto;
|
||||
}
|
||||
.table_scroll {
|
||||
margin: auto;
|
||||
text-align: initial;
|
||||
}
|
||||
tr {
|
||||
text-align: initial;
|
||||
}
|
||||
.number_cell {
|
||||
text-align: right;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
29
svelte/src/lib/AdminAPI.ts
Normal file
29
svelte/src/lib/AdminAPI.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { countries } from "country-data-list"
|
||||
import { check_response, get_endpoint } from "./PixeldrainAPI"
|
||||
|
||||
export const country_name = (country: string) => {
|
||||
if (country !== "" && countries[country] !== undefined) {
|
||||
return countries[country].emoji + " " + country + " (" + countries[country].name + ")"
|
||||
}
|
||||
return "🌐 Other"
|
||||
}
|
||||
|
||||
export type Invoice = {
|
||||
id: string
|
||||
time: string
|
||||
amount: number
|
||||
vat: number
|
||||
processing_fee: number
|
||||
country: string
|
||||
payment_gateway: string
|
||||
payment_method: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export const get_admin_invoices = async (year: number, month: number) => {
|
||||
return await check_response(
|
||||
await fetch(
|
||||
get_endpoint() + "/admin/invoices/" + year + "-" + ("00" + (month)).slice(-2)
|
||||
)
|
||||
) as Invoice[]
|
||||
};
|
@@ -96,6 +96,16 @@ export const dict_to_form = (dict: Object) => {
|
||||
// API methods
|
||||
// ===========
|
||||
|
||||
export type UserSession = {
|
||||
auth_key: string,
|
||||
creation_ip_address: string,
|
||||
user_agent: string,
|
||||
app_name: string,
|
||||
creation_time: string,
|
||||
last_used_time: string,
|
||||
valid_domains: string[],
|
||||
}
|
||||
|
||||
export const get_user = async () => {
|
||||
if ((window as any).user !== undefined) {
|
||||
return (window as any).user as User
|
||||
|
@@ -83,13 +83,13 @@ const login = async (e?: SubmitEvent) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete any existing auth cookie to prevent it from interfering with
|
||||
// the request
|
||||
document.cookie = "pd_auth_key=; Max-Age=0;"
|
||||
|
||||
const resp = await check_response(await fetch(
|
||||
get_endpoint() + "/user/login",
|
||||
{method: "POST", body: fd},
|
||||
{
|
||||
method: "POST",
|
||||
body: fd,
|
||||
credentials: "omit", // Dont send existing session cookies
|
||||
},
|
||||
))
|
||||
|
||||
if (resp.value !== undefined && resp.value === "login_link_sent") {
|
||||
|
@@ -17,18 +17,16 @@ import Footer from "layout/Footer.svelte";
|
||||
export let title = ""
|
||||
export let pages: Tab[] = []
|
||||
|
||||
let navigate = (path: string, title: string) => {
|
||||
window.document.title = title+" ~ pixeldrain"
|
||||
window.history.pushState({}, window.document.title, path)
|
||||
|
||||
const navigate = (page: Tab) => {
|
||||
window.history.pushState({}, window.document.title, page.path)
|
||||
get_page()
|
||||
}
|
||||
|
||||
let get_page = () => {
|
||||
const get_page = () => {
|
||||
current_page = null
|
||||
current_subpage = null
|
||||
|
||||
pages.forEach(page => {
|
||||
for (const page of pages) {
|
||||
if (window.location.pathname.endsWith(page.path)) {
|
||||
current_page = page
|
||||
}
|
||||
@@ -41,19 +39,23 @@ let get_page = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// If no page is active, default to home
|
||||
if (!current_page) {
|
||||
current_page = pages[0]
|
||||
}
|
||||
|
||||
// If no subpage is active, default to the first subpage, if there are any
|
||||
if (!current_subpage && current_page.subpages) {
|
||||
current_subpage = current_page.subpages[0]
|
||||
}
|
||||
|
||||
console.log("Page", current_page)
|
||||
console.log("Subpage", current_subpage)
|
||||
title = current_subpage === null ? current_page.title : current_subpage.title
|
||||
window.document.title = title+" ~ pixeldrain"
|
||||
|
||||
console.debug("Page", current_page)
|
||||
console.debug("Subpage", current_subpage)
|
||||
|
||||
pages = pages
|
||||
}
|
||||
@@ -76,7 +78,7 @@ onMount(() => get_page())
|
||||
<a class="button"
|
||||
href="{page.path}"
|
||||
class:button_highlight={current_page && page.path === current_page.path}
|
||||
on:click|preventDefault={() => {navigate(page.path, page.title)}}>
|
||||
on:click|preventDefault={() => {navigate(page)}}>
|
||||
<i class="icon">{page.icon}</i>
|
||||
<span>{page.title}</span>
|
||||
</a>
|
||||
@@ -95,7 +97,7 @@ onMount(() => get_page())
|
||||
<a class="button"
|
||||
href="{page.path}"
|
||||
class:button_highlight={current_subpage && page.path === current_subpage.path}
|
||||
on:click|preventDefault={() => {navigate(page.path, page.title)}}>
|
||||
on:click|preventDefault={() => {navigate(page)}}>
|
||||
<i class="icon">{page.icon}</i>
|
||||
<span>{page.title}</span>
|
||||
</a>
|
||||
|
Reference in New Issue
Block a user