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" });
{: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")}
+
+
{/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 @@