diff --git a/src/lib/components/subscriptions/Calendar.svelte b/src/lib/components/subscriptions/Calendar.svelte index 3c6b2b9..c6fe746 100644 --- a/src/lib/components/subscriptions/Calendar.svelte +++ b/src/lib/components/subscriptions/Calendar.svelte @@ -128,7 +128,7 @@ : "text-cladd-fg"}" > {#each row.need.totals as total, i (total.code)} - {i > 0 ? "+" : ""} + {i > 0 ? "+" : ""} {/each} {/each} diff --git a/src/lib/components/subscriptions/CardMenu.svelte b/src/lib/components/subscriptions/CardMenu.svelte index 0a8cdd3..6c625fd 100644 --- a/src/lib/components/subscriptions/CardMenu.svelte +++ b/src/lib/components/subscriptions/CardMenu.svelte @@ -10,27 +10,32 @@ import { manualCards } from "#lib/subscriptions/manual.svelte.js"; import type { Card } from "#lib/subscriptions/runway.js"; - let { billId, cards }: { billId: string; cards: Card[] } = $props(); + let { billIds, cards }: { billIds: string[]; cards: Card[] } = $props(); - const chosen = $derived(manualCards.get(billId)); + const chosen = $derived(billIds.map((id) => manualCards.get(id))); + const shared = $derived(chosen.every((id) => id === chosen[0]) && chosen[0]); - Paying card + {billIds.length > 1 + ? `Paying card for ${billIds.length}` + : "Paying card"} {#each cards as card (card.id)} - manualCards.set(billId, card.id)}> + manualCards.set(billIds, card.id)}> {card.name} {card.code} - {#if chosen === card.id} + {#if shared === card.id} {/if} {/each} manualCards.set(billId, null)} + disabled={!chosen.some(Boolean)} + onSelect={() => manualCards.set(billIds, null)} >Card of the last payment diff --git a/src/lib/components/subscriptions/CardPicker.svelte b/src/lib/components/subscriptions/CardPicker.svelte index 812ee4d..aaead27 100644 --- a/src/lib/components/subscriptions/CardPicker.svelte +++ b/src/lib/components/subscriptions/CardPicker.svelte @@ -7,7 +7,7 @@ const AUTO = "\u0000"; - let { billId, cards }: { billId: string; cards: Card[] } = $props(); + let { billIds, cards }: { billIds: string[]; cards: Card[] } = $props(); let text = $state(""); let input = $state(); @@ -31,21 +31,23 @@ input?.focus(); }); - const cancel = () => (manualCards.picking = ""); + const cancel = () => (manualCards.picking = null); { - manualCards.set(billId, item.insert === AUTO ? null : item.insert); + manualCards.set(billIds, item.insert === AUTO ? null : item.insert); return true; }} onblur={cancel} oncancel={cancel} onsubmit={cancel} - placeholder="Paying card" + placeholder={billIds.length > 1 + ? `Paying card for ${billIds.length}` + : "Paying card"} {suggestions} bind:input bind:value={text} diff --git a/src/lib/components/subscriptions/Money.svelte b/src/lib/components/subscriptions/Money.svelte index 627141e..8aeffb8 100644 --- a/src/lib/components/subscriptions/Money.svelte +++ b/src/lib/components/subscriptions/Money.svelte @@ -1,23 +1,52 @@ -{approx ? "~" : ""}{symbol ?? ""}{formatNumber(Math.abs(toNumber(amount)), decimals ?? 2)}{mark}{shown.symbol ?? ""}{formatNumber(shown.amount, shown.decimals ?? 2)} diff --git a/src/lib/components/subscriptions/Runway.svelte b/src/lib/components/subscriptions/Runway.svelte index ee2b2d9..0694a2e 100644 --- a/src/lib/components/subscriptions/Runway.svelte +++ b/src/lib/components/subscriptions/Runway.svelte @@ -24,6 +24,7 @@ import Calendar from "./Calendar.svelte"; import CardMenu from "./CardMenu.svelte"; import Money from "./Money.svelte"; + import Sums from "./Sums.svelte"; let { needs, @@ -69,8 +70,20 @@ needs.flatMap((n) => n.dues).find((d) => d.bill.id === id)?.bill ?? null; } + function chosen(due: Due): string { + const card = byCurrency && due.card ? `${due.card.name}: ` : ""; + if (due.manual) { + return `${card}set by you`; + } + if (!due.lastPaid) { + return "never paid"; + } + const where = byCurrency ? "" : " here"; + return `${card}last paid${where} ${formatDate(due.lastPaid, "short")}`; + } + const segment = (on: boolean) => - buttonVariants({ size: "xs", variant: on ? "default" : "outline" }); + buttonVariants({ size: "sm", variant: on ? "default" : "outline" });
@@ -122,7 +135,7 @@

{:else} - +
@@ -143,7 +156,7 @@
- +
{/each} {#if need.short && need.currency} @@ -170,28 +183,33 @@ {/if}
    {#each need.dues as due, i (`${due.bill.id}:${due.date}:${i}`)} -
  • - {due.overdue - ? "overdue" - : formatDate(due.date, "short")} +
    + {due.overdue + ? "overdue" + : formatDate(due.date, "short")} + {due.bill.attributes.name} + +
    +
    - {due.bill.attributes.name} - + {chosen(due)} +
  • {/each}
@@ -211,7 +229,7 @@ >Edit - + {/if} @@ -225,10 +243,7 @@ >Annual renewals in {RENEWAL_DAYS} days: {renewals.length}, - {#each renewalTotals as total, i (total.code)} - {i > 0 ? " + " : ""} - {/each}{renewals.length}, {#each renewals as due (due.bill.id)} + import type { Money as Part } from "#lib/format.js"; + import { display } from "#lib/money/display.svelte.js"; + import Money from "./Money.svelte"; + + let { parts, inline = false }: { parts: Part[]; inline?: boolean } = $props(); + + const total = $derived(display.total(parts)); + + +{#if total} + +{:else if inline} + {#each parts as part, i (part.code ?? i)} + {i > 0 ? " + " : ""} + {/each} +{:else} + {#each parts as part, i (part.code ?? i)} +
+ {/each} +{/if} diff --git a/src/lib/subscriptions/manual.svelte.ts b/src/lib/subscriptions/manual.svelte.ts index cee005c..721b385 100644 --- a/src/lib/subscriptions/manual.svelte.ts +++ b/src/lib/subscriptions/manual.svelte.ts @@ -1,39 +1,79 @@ -const PREFIX = "ffui.bill-card."; +import { errorText } from "#lib/components/misc/action.svelte.js"; +import { session } from "#lib/session.svelte.js"; -function read(): Record { - const ids: Record = {}; - if (typeof localStorage === "undefined") { - return ids; - } - for (let i = 0; i < localStorage.length; i += 1) { - const key = localStorage.key(i); - if (key?.startsWith(PREFIX)) { - ids[key.slice(PREFIX.length)] = localStorage.getItem(key) ?? ""; - } - } - return ids; +const PREF = "ffui_bill_cards"; +const LEGACY = "ffui.bill-card."; + +type Choices = Record; + +function stored(): Choices { + const data = session.prefs[PREF]; + return data && typeof data === "object" && !Array.isArray(data) + ? (data as Choices) + : {}; +} + +function legacyKeys(): string[] { + return Array.from( + { length: localStorage.length }, + (_, i) => localStorage.key(i) ?? "" + ).filter((key) => key.startsWith(LEGACY)); } class ManualCards { - ids = $state>(read()); - picking = $state(""); + pending = $state.raw(null); + picking = $state<{ at: string; ids: string[] } | null>(null); + failure = $state(""); + ids = $derived(this.pending ?? stored()); get(billId: string): string | null { return this.ids[billId] || null; } - set(billId: string, cardId: string | null) { - if (cardId) { - localStorage.setItem(PREFIX + billId, cardId); - } else { - localStorage.removeItem(PREFIX + billId); + async set(billIds: string[], cardId: string | null) { + const changes = Object.fromEntries(billIds.map((id) => [id, cardId ?? ""])); + this.picking = null; + await this.save({ ...this.ids, ...changes }); + } + + async migrate() { + const keys = legacyKeys(); + if (keys.length === 0) { + return; } - this.ids = Object.fromEntries( - Object.entries({ ...this.ids, [billId]: cardId ?? "" }).filter( - ([, id]) => id - ) + const old = Object.fromEntries( + keys.map((key) => [ + key.slice(LEGACY.length), + localStorage.getItem(key) ?? "", + ]) ); - this.picking = ""; + if (await this.save({ ...old, ...this.ids })) { + for (const key of keys) { + localStorage.removeItem(key); + } + } + } + + private async save(choices: Choices): Promise { + const next = Object.fromEntries( + Object.entries(choices).filter(([, id]) => id) + ); + this.pending = next; + this.failure = ""; + try { + await session.setPreference( + PREF, + Object.keys(next).length > 0 ? next : false + ); + return true; + } catch (e) { + this.failure = errorText(e); + return false; + } finally { + if (this.pending === next) { + this.pending = null; + } + } } } diff --git a/src/lib/subscriptions/runway.ts b/src/lib/subscriptions/runway.ts index 663de11..f9417da 100644 --- a/src/lib/subscriptions/runway.ts +++ b/src/lib/subscriptions/runway.ts @@ -52,6 +52,8 @@ export interface Due { bill: Item; card: Card | null; date: string; + lastPaid: string; + manual: boolean; native: Sum; overdue: boolean; pay: Sum; @@ -238,6 +240,8 @@ export function dues( bill, card: who.card, date, + lastPaid: who.last?.date ?? "", + manual: who.manual, native, overdue, pay, diff --git a/src/routes/subscriptions/+page.svelte b/src/routes/subscriptions/+page.svelte index a8083b8..2dddd3c 100644 --- a/src/routes/subscriptions/+page.svelte +++ b/src/routes/subscriptions/+page.svelte @@ -1,8 +1,8 @@