From 25b0fe1c05ae6a92c71dc1231a55e0b14255fed0 Mon Sep 17 00:00:00 2001 From: Wim Brand Date: Wed, 30 Jul 2025 18:53:10 +0200 Subject: [PATCH] Add page for calculating paypal invoice taxes --- res/static/style/layout.css | 5 +- svelte/src/admin_panel/InvoiceVAT.svelte | 302 ++++++++++++++-------- svelte/src/admin_panel/PayPalTaxes.svelte | 12 +- svelte/src/admin_panel/PayPalVAT.svelte | 184 +++++++++++++ svelte/src/admin_panel/Router.svelte | 2 +- svelte/src/layout/SortableTable.svelte | 135 ++++++++++ svelte/src/lib/AdminAPI.ts | 29 +++ svelte/src/lib/PixeldrainAPI.ts | 10 + svelte/src/login/Login.svelte | 10 +- svelte/src/util/TabMenu.svelte | 24 +- 10 files changed, 577 insertions(+), 136 deletions(-) create mode 100644 svelte/src/admin_panel/PayPalVAT.svelte create mode 100644 svelte/src/layout/SortableTable.svelte create mode 100644 svelte/src/lib/AdminAPI.ts diff --git a/res/static/style/layout.css b/res/static/style/layout.css index ec26dc6..de3c987 100644 --- a/res/static/style/layout.css +++ b/res/static/style/layout.css @@ -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 */ diff --git a/svelte/src/admin_panel/InvoiceVAT.svelte b/svelte/src/admin_panel/InvoiceVAT.svelte index 930fbaa..8aad009 100644 --- a/svelte/src/admin_panel/InvoiceVAT.svelte +++ b/svelte/src/admin_panel/InvoiceVAT.svelte @@ -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[] = []
-

{month_str}

+

{year + "-" + ("00"+(month)).slice(-2)}

-

Invoices per payment processor

-
- - - - - - - - - - - - {#each Object.entries(totals_provider) as [key, tot]} - - - - - - - - {/each} - -
ProviderCountAmountVATFee
{key}{tot.count}
-
+ +
Per payment processor
+ +
-

Invoices per country

-
- - - - - - - - - - - - {#each Object.entries(totals_country) as [key, tot]} - - - - - - - - {/each} - -
CountryCountAmountVATFee
{key}{tot.count}
-
+ +
Per country
+ 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 + /> +
+ + +
In European Union
+ 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 + /> +
+ + +
PayPal VAT
+ +

All invoices

+
+
+ Status:
+ {#each Object.keys(status_filter) as filter} + + +
+ {/each} +
+
+ Gateways:
+ {#each Object.keys(gateway_filter) as filter} + + +
+ {/each} +
+
+ Methods:
+ {#each Object.keys(method_filter) as filter} + + +
+ {/each} +
+
+ +
+ Total: {invoices.length} + Visible: {invoices.length-records_hidden} + Hidden: {records_hidden}
-
- - - - - - - - - - - - - - - - {#each invoices as row (row.id)} - - - - - - - - - - - - {/each} - -
TimeIDAmountVATFeeCountryGatewayMethodStatus
{formatDate(row.time)}{row.id}{row.country}{row.payment_gateway}{row.payment_method}{row.status}
-
+ + 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 +/> diff --git a/svelte/src/admin_panel/PayPalTaxes.svelte b/svelte/src/admin_panel/PayPalTaxes.svelte index 54ef0cb..70fe118 100644 --- a/svelte/src/admin_panel/PayPalTaxes.svelte +++ b/svelte/src/admin_panel/PayPalTaxes.svelte @@ -155,18 +155,18 @@ onMount(() => { 8040 - Omzet PayPal inkomsten - - - Geen BTW - - 1651 - BTW OSS - Geen BTW 8040 - Omzet PayPal inkomsten + + + Geen BTW + + 1651 - BTW OSS + {/if} diff --git a/svelte/src/admin_panel/PayPalVAT.svelte b/svelte/src/admin_panel/PayPalVAT.svelte new file mode 100644 index 0000000..668c283 --- /dev/null +++ b/svelte/src/admin_panel/PayPalVAT.svelte @@ -0,0 +1,184 @@ + + +
+ {#if per_country["NL"] && totals} +

Summary

+ + + + + + + + + + + + + +
Total PayPal earnings -fees
Total VAT NL
Total VAT OSS
+ +

Accounting information

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BedragBTW-codeBTWTegenrekening
BTW hoog 21%8040 - Omzet PayPal inkomsten
Geen BTW8040 - Omzet PayPal inkomsten
Geen BTW1651 - BTW OSS
+ +

Taxes per country

+
+ + + + + + + + + + + + + + + {#each Object.entries(per_country) as [country, row]} + {#if row.vat > 0} + + + + + + + + + + + {/if} + {/each} + + + + + + + + + + + + {#if per_country["NL"]} + + + + + + + + + + + {/if} + +
CountryPaymentsAmountVATVAT%TotalFeeTotal
{country_name(country)}{row.count}{row.vat_fraction*100}%
Total{totals.count}
Total ex NL{totals.count - per_country["NL"].count}
+
+ {/if} +
diff --git a/svelte/src/admin_panel/Router.svelte b/svelte/src/admin_panel/Router.svelte index 8316910..fc55518 100644 --- a/svelte/src/admin_panel/Router.svelte +++ b/svelte/src/admin_panel/Router.svelte @@ -14,7 +14,7 @@ import InvoiceVat from "./InvoiceVAT.svelte"; let pages = [ { path: "/admin", - title: "Status", + title: "Admin panel", icon: "home", component: Home, }, { diff --git a/svelte/src/layout/SortableTable.svelte b/svelte/src/layout/SortableTable.svelte new file mode 100644 index 0000000..74e82bd --- /dev/null +++ b/svelte/src/layout/SortableTable.svelte @@ -0,0 +1,135 @@ + + + +
+ + + + {#each columns as col (col.field)} + + {/each} + + + + {#each rows as row (row[index_field])} + + {#each columns as col (col.field)} + {#if col.type === FieldType.Text} + + {:else if col.type === FieldType.Bytes} + + {:else if col.type === FieldType.Bits} + + {:else if col.type === FieldType.Number} + + {:else if col.type === FieldType.Euro} + + {:else if col.type === FieldType.Func} + + {:else if col.type === FieldType.HTML} + + {/if} + {/each} + + {/each} + {#if totals === true} + + {#each columns as col (col.field)} + {#if col.field === index_field} + + {:else if col.type === FieldType.Bytes} + + {:else if col.type === FieldType.Bits} + + {:else if col.type === FieldType.Number} + + {:else if col.type === FieldType.Euro} + + {:else} + + {/if} + {/each} + + {/if} + +
+ + {col.label} + +
{row[col.field]}{formatDataVolume(row[col.field], 3)}{formatDataVolumeBits(row[col.field], 3)}{formatThousands(row[col.field])}{@html col.func(row[col.field])}{@html row[col.field]}
Total{formatDataVolume(rows.reduce((acc, val) => acc+val[col.field], 0), 3)}{formatDataVolumeBits(rows.reduce((acc, val) => acc+val[col.field], 0), 3)}{formatThousands(rows.reduce((acc, val) => acc+val[col.field], 0))} acc+val[col.field], 0)}/>
+
+ + diff --git a/svelte/src/lib/AdminAPI.ts b/svelte/src/lib/AdminAPI.ts new file mode 100644 index 0000000..a55b701 --- /dev/null +++ b/svelte/src/lib/AdminAPI.ts @@ -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[] +}; diff --git a/svelte/src/lib/PixeldrainAPI.ts b/svelte/src/lib/PixeldrainAPI.ts index 5b5bb22..d6e53bd 100644 --- a/svelte/src/lib/PixeldrainAPI.ts +++ b/svelte/src/lib/PixeldrainAPI.ts @@ -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 diff --git a/svelte/src/login/Login.svelte b/svelte/src/login/Login.svelte index cab4681..f408b71 100644 --- a/svelte/src/login/Login.svelte +++ b/svelte/src/login/Login.svelte @@ -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") { diff --git a/svelte/src/util/TabMenu.svelte b/svelte/src/util/TabMenu.svelte index 3d46556..3a48c7a 100644 --- a/svelte/src/util/TabMenu.svelte +++ b/svelte/src/util/TabMenu.svelte @@ -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()) {navigate(page.path, page.title)}}> + on:click|preventDefault={() => {navigate(page)}}> {page.icon} {page.title} @@ -95,7 +97,7 @@ onMount(() => get_page()) {navigate(page.path, page.title)}}> + on:click|preventDefault={() => {navigate(page)}}> {page.icon} {page.title}