Compare commits

Author SHA1 Message Date
renovate Bot 0dac38abcc chore(deps): pin fireflyiii/core docker tag
renovate/stability-days Updates have not met minimum release age requirement
2026-10-17 04:03:28 +00:00
17 changed files with 88 additions and 244 deletions

No files matched your search

+2 -2
View File
@@ -1,4 +1,4 @@
FROM oven/bun:1-alpine@sha256:4cf82f5686b196504fe119db3fcd9185b191f82ece82c40680e597228ec6070a AS deps FROM oven/bun:1-alpine@sha256:629e17411f1f129dbec3af78d5af9c9f2a937435c80349437206c6b0b7422373 AS deps
ENV TERM=xterm-256color ENV TERM=xterm-256color
ENV COLORTERM=truecolor ENV COLORTERM=truecolor
@@ -19,7 +19,7 @@ RUN bun --bun svelte-kit sync
RUN bun run build RUN bun run build
FROM oven/bun:1-alpine@sha256:4cf82f5686b196504fe119db3fcd9185b191f82ece82c40680e597228ec6070a AS runtime FROM oven/bun:1-alpine@sha256:629e17411f1f129dbec3af78d5af9c9f2a937435c80349437206c6b0b7422373 AS runtime
ENV NODE_ENV=production ENV NODE_ENV=production
+15 -26
View File
@@ -1,39 +1,28 @@
const KEY = "ffui.budget-map";
export interface Learned { export interface Learned {
id: string; id: string;
name: string; name: string;
} }
function learnedMap(key: string) { function read(): Record<string, Learned> {
const read = (): Record<string, Learned> => {
try { try {
const value = JSON.parse(localStorage.getItem(key) ?? "{}"); const value = JSON.parse(localStorage.getItem(KEY) ?? "{}");
return value && typeof value === "object" ? value : {}; return value && typeof value === "object" ? value : {};
} catch { } catch {
return {}; return {};
} }
};
const get = (place: string): Learned | null => read()[place] ?? null;
const set = (place: string, learned: Learned | null) => {
const map = read();
if (learned) {
map[place] = { id: learned.id, name: learned.name };
} else {
delete map[place];
}
localStorage.setItem(key, JSON.stringify(map));
};
const remember = (place: string, learned: Learned) => {
const before = get(place);
set(place, learned);
return () => set(place, before);
};
return { get, remember };
} }
export const learnedBudgets = learnedMap("ffui.budget-map"); export const learnedBudget = (destinationId: string): Learned | null =>
read()[destinationId] ?? null;
export const learnedCategories = learnedMap("ffui.category-map"); export function learnBudget(destinationId: string, budget: Learned | null) {
const map = read();
if (budget) {
map[destinationId] = { id: budget.id, name: budget.name };
} else {
delete map[destinationId];
}
localStorage.setItem(KEY, JSON.stringify(map));
}
+16 -35
View File
@@ -75,26 +75,6 @@
function fill(past: Split, withCounterparty: boolean) { function fill(past: Split, withCounterparty: boolean) {
const parts: string[] = []; const parts: string[] = [];
const undo: (() => void)[] = []; const undo: (() => void)[] = [];
const put = (text: string, still: () => boolean, clear: () => void) => {
parts.push(text);
undo.push(() => {
if (still()) {
clear();
}
});
};
const putRef = (field: "bill" | "budget", refId: string, name: string) => {
if (withdrawal && !(split[field].id || split[field].name)) {
split[field] = { id: refId, name };
put(
`${field === "bill" ? "subscription" : field} ${name}`,
() => split[field].id === refId,
() => {
split[field] = emptyRef();
}
);
}
};
const side = counterparty; const side = counterparty;
if (withCounterparty && side && past.type === form.type) { if (withCounterparty && side && past.type === form.type) {
const pastId = past[`${side}_id`]; const pastId = past[`${side}_id`];
@@ -107,32 +87,33 @@
) { ) {
const next = toAccountRef(account); const next = toAccountRef(account);
split[side] = next; split[side] = next;
put( parts.push(next.name);
next.name, undo.push(() => {
() => split[side].id === next.id, if (split[side].id === next.id) {
() => {
split[side] = emptyAccount(); split[side] = emptyAccount();
} }
); });
} }
} }
const category = past.category_name ?? ""; const category = past.category_name ?? "";
if (category && !split.category.trim()) { if (category && !split.category.trim()) {
split.category = category; split.category = category;
put( parts.push(`category ${category}`);
`category ${category}`, undo.push(() => {
() => split.category === category, if (split.category === category) {
() => {
split.category = ""; split.category = "";
} }
); });
} }
if (past.budget_id) { const budget = past.budget_id;
putRef("budget", past.budget_id, past.budget_name ?? ""); if (withdrawal && budget && !(split.budget.id || split.budget.name)) {
split.budget = { id: budget, name: past.budget_name ?? "" };
parts.push(`budget ${past.budget_name ?? ""}`);
undo.push(() => {
if (split.budget.id === budget) {
split.budget = emptyRef();
} }
const bill = withCounterparty ? catalog.billById(past.bill_id) : undefined; });
if (bill) {
putRef("bill", bill.id, bill.attributes.name ?? "");
} }
if (parts.length) { if (parts.length) {
filled = { filled = {
+12 -36
View File
@@ -8,7 +8,7 @@ import type {
Page, Page,
TransactionGroup, TransactionGroup,
} from "#lib/api/types.js"; } from "#lib/api/types.js";
import { learnedBudgets, learnedCategories } from "#lib/budgets/map.js"; import { learnedBudget } from "#lib/budgets/map.js";
import type { Option } from "#lib/components/inputs/AutocompleteInput.svelte"; import type { Option } from "#lib/components/inputs/AutocompleteInput.svelte";
import { formatMoney } from "#lib/format.js"; import { formatMoney } from "#lib/format.js";
import { fuzzyFilter } from "#lib/fuzzy.js"; import { fuzzyFilter } from "#lib/fuzzy.js";
@@ -66,12 +66,6 @@ export class Catalog {
return (this.accounts.data ?? []).find((item) => item.id === id); return (this.accounts.data ?? []).find((item) => item.id === id);
} }
billById(id: string | null | undefined) {
return (this.bills.data ?? []).find(
(item) => item.id === id && item.attributes.active
);
}
accountOptions(kinds: string[], query: string): Option[] { accountOptions(kinds: string[], query: string): Option[] {
const items = rank( const items = rank(
this.accountsOf(kinds), this.accountsOf(kinds),
@@ -241,41 +235,23 @@ export class Catalog {
.catch(() => null); .catch(() => null);
this.history.set(key, found); this.history.set(key, found);
} }
const learnedBudget = learnedBudgets.get(accountId)?.id; const learned = learnedBudget(accountId)?.id;
const budget = const budget = this.budgets.data?.find(
type === "withdrawal" && side === "destination" (item) => item.id === learned && item.attributes.active
? this.budgets.data?.find( );
(item) => item.id === learnedBudget && item.attributes.active if (!budget || type !== "withdrawal" || side !== "destination") {
)
: undefined;
const ownSide = type === "deposit" ? "source" : "destination";
const learnedCategory = learnedCategories.get(`${type}:${accountId}`)?.id;
const category =
side === ownSide
? this.categories.data?.find((item) => item.id === learnedCategory)
: undefined;
if (!(budget || category)) {
return found; return found;
} }
return found.then((past) => { return found.then(
const split = past?.split; (past) =>
if (!split) { past && {
return past;
}
return {
split: { split: {
...split, ...past.split,
...(budget && {
budget_id: budget.id, budget_id: budget.id,
budget_name: budget.attributes.name, budget_name: budget.attributes.name,
}),
...(category && {
category_id: category.id,
category_name: category.attributes.name,
}),
}, },
}; }
}); );
} }
async fromJournal(journalId: string): Promise<Recent | null> { async fromJournal(journalId: string): Promise<Recent | null> {
+5 -11
View File
@@ -4,13 +4,13 @@
import ChevronRight from "@lucide/svelte/icons/chevron-right"; import ChevronRight from "@lucide/svelte/icons/chevron-right";
import { Popover } from "bits-ui"; import { Popover } from "bits-ui";
import { period } from "#lib/period.svelte.js"; import { period } from "#lib/period.svelte.js";
import { ui } from "#lib/ui.svelte.js";
let open = $state(false);
let start = $state(period.start); let start = $state(period.start);
let end = $state(period.end); let end = $state(period.end);
$effect(() => { $effect(() => {
if (ui.periodOpen) { if (open) {
({ start, end } = period); ({ start, end } = period);
} }
}); });
@@ -19,7 +19,7 @@
event.preventDefault(); event.preventDefault();
if (start && end && start <= end) { if (start && end && start <= end) {
period.set({ end, start }); period.set({ end, start });
ui.periodOpen = false; open = false;
} }
} }
</script> </script>
@@ -34,7 +34,7 @@
> >
<ChevronLeft class="size-4" /> <ChevronLeft class="size-4" />
</button> </button>
<Popover.Root bind:open={ui.periodOpen}> <Popover.Root bind:open>
<Popover.Trigger <Popover.Trigger
class="flex h-8 items-center gap-2 px-2 font-medium text-[13px] hover:bg-black/10" class="flex h-8 items-center gap-2 px-2 font-medium text-[13px] hover:bg-black/10"
title="Choose period" title="Choose period"
@@ -59,16 +59,11 @@
: ""}" : ""}"
onclick={() => { onclick={() => {
period.set(preset.range); period.set(preset.range);
ui.periodOpen = false; open = false;
}} }}
type="button" type="button"
> >
{preset.label} {preset.label}
{#if preset.key || preset.hint}
<span class="kbd"
>{preset.key ? `p ${preset.key}` : preset.hint}</span
>
{/if}
</button> </button>
{/each} {/each}
</div> </div>
@@ -81,7 +76,6 @@
Start Start
<input <input
class="cladd-cut cladd-focusable-input h-8 rounded-cladd-sm px-2 text-[13px] text-cladd-fg" class="cladd-cut cladd-focusable-input h-8 rounded-cladd-sm px-2 text-[13px] text-cladd-fg"
id="period-start"
type="date" type="date"
bind:value={start} bind:value={start}
> >
-33
View File
@@ -36,18 +36,6 @@
} }
}); });
function setPreset(key: string | undefined) {
const preset = period.presets().find((item) => item.key === key);
if (preset) {
period.set(preset.range);
}
}
function openCustomPeriod() {
ui.periodOpen = true;
setTimeout(() => document.getElementById("period-start")?.focus(), 50);
}
function openOld(sticky = false) { function openOld(sticky = false) {
location.assign(oldUiUrl(page.url.href, sticky)); location.assign(oldUiUrl(page.url.href, sticky));
} }
@@ -160,27 +148,6 @@
label: "Current period", label: "Current period",
run: () => period.reset(), run: () => period.reset(),
}, },
{
group: "Period",
keys: "p p",
label: "Choose a period",
run: () => (ui.periodOpen = true),
},
{
group: "Period",
keys: "p c",
label: "Custom period: type the dates",
run: openCustomPeriod,
},
...period
.presets()
.filter((preset) => preset.key)
.map((preset) => ({
group: "Period",
keys: `p ${preset.key}`,
label: `Period: ${preset.label}`,
run: () => setPreset(preset.key),
})),
{ {
group: "Global", group: "Global",
hidden: true, hidden: true,
@@ -122,7 +122,7 @@
onclick={() => byCurrency || onmode()} onclick={() => byCurrency || onmode()}
type="button" type="button"
> >
By currency<span class="kbd max-sm:hidden">v</span> By currency<span class="kbd max-sm:hidden">p</span>
</button> </button>
</fieldset> </fieldset>
</header> </header>
+5 -55
View File
@@ -118,50 +118,12 @@ export function periodOf(value: string, view: ViewRange): Range {
} }
} }
const UNITS: ViewRange[] = ["1M", "3M", "6M", "1Y"];
function unitOf(range: Range, view: ViewRange): ViewRange | null {
const units = isDynamic(view) ? UNITS : [view, ...UNITS];
return (
units.find((unit) => {
const aligned = periodOf(range.start, unit);
return aligned.start === range.start && range.end <= aligned.end;
}) ?? null
);
}
function wholeMonths(range: Range): number {
const after = addDays(range.end, 1);
if (!(range.start.endsWith("-01") && after.endsWith("-01"))) {
return 0;
}
const months = (value: string) =>
Number(value.slice(0, 4)) * 12 + Number(value.slice(5, 7));
return months(after) - months(range.start);
}
function calendarShift(
range: Range,
view: ViewRange,
dir: 1 | -1
): Range | null {
const unit = unitOf(range, view);
if (unit) {
const anchor = dir === 1 ? periodOf(range.start, unit).end : range.start;
return periodOf(addDays(anchor, dir), unit);
}
const months = wholeMonths(range);
if (months === 0) {
return null;
}
const start = addMonths(range.start, dir * months);
return { end: addDays(addMonths(start, months), -1), start };
}
export function shiftPeriod(range: Range, view: ViewRange, dir: 1 | -1): Range { export function shiftPeriod(range: Range, view: ViewRange, dir: 1 | -1): Range {
const calendar = calendarShift(range, view, dir); const fixed = isDynamic(view) ? "1M" : view;
if (calendar) { const aligned = periodOf(range.start, fixed);
return calendar; if (aligned.start === range.start && aligned.end === range.end) {
const anchor = dir === 1 ? addDays(range.end, 1) : addDays(range.start, -1);
return periodOf(anchor, fixed);
} }
const days = daysBetween(range.start, range.end) + 1; const days = daysBetween(range.start, range.end) + 1;
return { return {
@@ -170,18 +132,6 @@ export function shiftPeriod(range: Range, view: ViewRange, dir: 1 | -1): Range {
}; };
} }
export function stepPeriod(range: Range, view: ViewRange, dir: 1 | -1): Range {
const calendar = calendarShift(range, view, dir);
if (calendar) {
return calendar;
}
const unit = isDynamic(view) ? "1M" : view;
const edge = dir === 1 ? range.end : range.start;
const own = periodOf(edge, unit);
const reached = (dir === 1 ? own.end : own.start) === edge;
return reached ? periodOf(addDays(edge, dir), unit) : own;
}
export function daysBetween(a: string, b: string): number { export function daysBetween(a: string, b: string): number {
return Math.round((parse(b).getTime() - parse(a).getTime()) / 86_400_000); return Math.round((parse(b).getTime() - parse(a).getTime()) / 86_400_000);
} }
+11 -26
View File
@@ -5,7 +5,7 @@ import {
isDynamic, isDynamic,
periodOf, periodOf,
type Range, type Range,
stepPeriod, shiftPeriod,
today, today,
type ViewRange, type ViewRange,
} from "#lib/dates.js"; } from "#lib/dates.js";
@@ -15,15 +15,13 @@ const STORAGE_KEY = "ffui.period";
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/; const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
export interface Preset { export interface Preset {
hint?: string;
key?: string;
label: string; label: string;
range: Range; range: Range;
} }
function stored(): Range | null { function stored(): Range | null {
try { try {
const value = JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? "null"); const value = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "null");
return ISO_DATE.test(value?.start) && ISO_DATE.test(value?.end) return ISO_DATE.test(value?.start) && ISO_DATE.test(value?.end)
? value ? value
: null; : null;
@@ -71,7 +69,7 @@ class Period {
const range = rangeFromUrl(url) ?? stored() ?? periodOf(today(), view); const range = rangeFromUrl(url) ?? stored() ?? periodOf(today(), view);
this.start = range.start; this.start = range.start;
this.end = range.end; this.end = range.end;
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(range)); localStorage.setItem(STORAGE_KEY, JSON.stringify(range));
this.ready = true; this.ready = true;
} }
@@ -97,11 +95,11 @@ class Period {
} }
this.start = range.start; this.start = range.start;
this.end = range.end; this.end = range.end;
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(range)); localStorage.setItem(STORAGE_KEY, JSON.stringify(range));
} }
shift(dir: 1 | -1) { shift(dir: 1 | -1) {
this.set(stepPeriod(this.range, this.view, dir)); this.set(shiftPeriod(this.range, this.view, dir));
} }
reset() { reset() {
@@ -113,46 +111,33 @@ class Period {
const { view } = this; const { view } = this;
const presets: Preset[] = []; const presets: Preset[] = [];
if (!isDynamic(view)) { if (!isDynamic(view)) {
const prev = stepPeriod(this.range, view, -1); const prev = shiftPeriod(this.range, view, -1);
const next = stepPeriod(this.range, view, 1); const next = shiftPeriod(this.range, view, 1);
presets.push({ hint: "[", label: periodLabel(prev, view), range: prev }); presets.push({ label: periodLabel(prev, view), range: prev });
presets.push({ hint: "]", label: periodLabel(next, view), range: next }); presets.push({ label: periodLabel(next, view), range: next });
} }
const current = periodOf(now, view); const current = periodOf(now, view);
presets.push({ presets.push({
key: "t",
label: `Today (${periodLabel(current, view)})`, label: `Today (${periodLabel(current, view)})`,
range: current, range: current,
}); });
presets.push({ presets.push({
key: "7",
label: "Last seven days", label: "Last seven days",
range: { end: now, start: addDays(now, -7) }, range: { end: now, start: addDays(now, -7) },
}); });
presets.push({ presets.push({
key: "3",
label: "Last thirty days", label: "Last thirty days",
range: { end: now, start: addDays(now, -30) }, range: { end: now, start: addDays(now, -30) },
}); });
presets.push({ presets.push({ label: "Month to date", range: periodOf(now, "MTD") });
key: "m", presets.push({ label: "Year to date", range: periodOf(now, "YTD") });
label: "Month to date",
range: periodOf(now, "MTD"),
});
presets.push({
key: "y",
label: "Year to date",
range: periodOf(now, "YTD"),
});
const year = Number(now.slice(0, 4)) - 1; const year = Number(now.slice(0, 4)) - 1;
presets.push({ presets.push({
key: "l",
label: `Previous year (${year})`, label: `Previous year (${year})`,
range: { end: `${year}-12-31`, start: `${year}-01-01` }, range: { end: `${year}-12-31`, start: `${year}-01-01` },
}); });
if (this.first) { if (this.first) {
presets.push({ presets.push({
key: "a",
label: "Everything", label: "Everything",
range: { end: now, start: this.first }, range: { end: now, start: this.first },
}); });
-2
View File
@@ -28,12 +28,10 @@ export const runwayQueries = {
bills: (start: string, end: string) => ({ bills: (start: string, end: string) => ({
queryFn: () => all<Bill>("/v1/bills", { end, start }), queryFn: () => all<Bill>("/v1/bills", { end, start }),
queryKey: ["subscriptions", "runway", start, end], queryKey: ["subscriptions", "runway", start, end],
refetchOnWindowFocus: true,
}), }),
paid: () => ({ paid: () => ({
queryFn: async () => toPayments(await search("has_any_bill:true")), queryFn: async () => toPayments(await search("has_any_bill:true")),
queryKey: ["subscriptions", "payments"], queryKey: ["subscriptions", "payments"],
refetchOnWindowFocus: true,
}), }),
unlinked: (day: string) => { unlinked: (day: string) => {
const after = addDays(day, -UNLINKED_DAYS); const after = addDays(day, -UNLINKED_DAYS);
+5 -1
View File
@@ -208,7 +208,11 @@ function occurrences(bill: Bill, day: string) {
return (bill.pay_dates ?? []) return (bill.pay_dates ?? [])
.map((raw) => raw.slice(0, 10)) .map((raw) => raw.slice(0, 10))
.filter((date) => !end || date <= end) .filter((date) => !end || date <= end)
.filter((date) => !paid.some((p) => Math.abs(daysBetween(p, date)) < early)) .filter(
(date) =>
date < day ||
!paid.some((p) => p <= date && daysBetween(p, date) < early)
)
.map((date) => ({ date: date < day ? day : date, overdue: date < day })); .map((date) => ({ date: date < day ? day : date, overdue: date < day }));
} }
-1
View File
@@ -11,7 +11,6 @@ class Ui {
palette = $state(false); palette = $state(false);
paletteQuery = $state(""); paletteQuery = $state("");
quickEntry = $state(false); quickEntry = $state(false);
periodOpen = $state(false);
mobileNav = $state(false); mobileNav = $state(false);
collapsed = $state(stored(SIDEBAR_KEY)); collapsed = $state(stored(SIDEBAR_KEY));
notes = $state(stored(NOTES_KEY)); notes = $state(stored(NOTES_KEY));
@@ -4,7 +4,7 @@
import { api } from "#lib/api/client.js"; import { api } from "#lib/api/client.js";
import { invalidate } from "#lib/api/lists.js"; import { invalidate } from "#lib/api/lists.js";
import type { Budget, One } from "#lib/api/types.js"; import type { Budget, One } from "#lib/api/types.js";
import { learnedBudgets } from "#lib/budgets/map.js"; import { learnBudget, learnedBudget } from "#lib/budgets/map.js";
import { import {
type Place, type Place,
placesOf, placesOf,
@@ -269,6 +269,12 @@
return { id: made.data.id, name: made.data.attributes.name }; return { id: made.data.id, name: made.data.attributes.name };
} }
function remember(place: Place, option: { id: string; name: string }) {
const before = learnedBudget(place.accountId);
learnBudget(place.accountId, option);
return () => learnBudget(place.accountId, before);
}
function done(index: number, move: Move) { function done(index: number, move: Move) {
adding = null; adding = null;
const next = rows[index + move]; const next = rows[index + move];
@@ -733,12 +739,12 @@
? "Every expense has a budget." ? "Every expense has a budget."
: `Every expense in ${period.label} has a budget.`} : `Every expense in ${period.label} has a budget.`}
everything={scope === "all"} everything={scope === "all"}
hint={(place) => learnedBudgets.get(place.accountId)?.name ?? null} hint={(place) => learnedBudget(place.accountId)?.name ?? null}
ignoreKey="ffui.budget-ignored" ignoreKey="ffui.budget-ignored"
loading={triage.isPending} loading={triage.isPending}
noun="budget" noun="budget"
onactivate={() => (triaging = true)} onactivate={() => (triaging = true)}
onassign={(place, option) => learnedBudgets.remember(place.accountId, option)} onassign={remember}
onchange={() => refreshExcept(triageKey)} onchange={() => refreshExcept(triageKey)}
onleave={() => (triaging = false)} onleave={() => (triaging = false)}
onswitch={() => setParam("scope", scope === "all" ? "" : "all")} onswitch={() => setParam("scope", scope === "all" ? "" : "all")}
-3
View File
@@ -5,7 +5,6 @@
import { api } from "#lib/api/client.js"; import { api } from "#lib/api/client.js";
import { invalidate } from "#lib/api/lists.js"; import { invalidate } from "#lib/api/lists.js";
import type { Category, One } from "#lib/api/types.js"; import type { Category, One } from "#lib/api/types.js";
import { learnedCategories } from "#lib/budgets/map.js";
import { import {
placesOf, placesOf,
refreshExcept, refreshExcept,
@@ -415,12 +414,10 @@
? "Every transaction has a category." ? "Every transaction has a category."
: `Every transaction in ${period.label} has a category.`} : `Every transaction in ${period.label} has a category.`}
{everything} {everything}
hint={(place) => learnedCategories.get(place.key)?.name ?? null}
ignoreKey="ffui.category-ignored" ignoreKey="ffui.category-ignored"
loading={triage.isPending} loading={triage.isPending}
noun="category" noun="category"
onactivate={() => (triaging = true)} onactivate={() => (triaging = true)}
onassign={(place, option) => learnedCategories.remember(place.key, option)}
onchange={() => refreshExcept(triageKey)} onchange={() => refreshExcept(triageKey)}
onleave={() => (triaging = false)} onleave={() => (triaging = false)}
onswitch={() => setParam("scope", everything ? "" : "all")} onswitch={() => setParam("scope", everything ? "" : "all")}
+2 -2
View File
@@ -78,7 +78,7 @@
}, },
{ {
group: "Currencies", group: "Currencies",
keys: "P", keys: "p",
label: "Make primary currency", label: "Make primary currency",
run: () => nav.current && makePrimary(nav.current), run: () => nav.current && makePrimary(nav.current),
}, },
@@ -203,7 +203,7 @@
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem onSelect={() => makePrimary(item)}> <ContextMenuItem onSelect={() => makePrimary(item)}>
Make primary Make primary
<span class="ml-auto kbd">P</span> <span class="ml-auto kbd">p</span>
</ContextMenuItem> </ContextMenuItem>
<MenuDelete {arm} id={item.id} run={() => remove(item)} /> <MenuDelete {arm} id={item.id} run={() => remove(item)} />
{/if} {/if}
+3 -5
View File
@@ -296,7 +296,7 @@
})), })),
{ {
group: "Runway", group: "Runway",
keys: "v", keys: "p",
label: perCurrency ? "Group by card" : "Group by currency", label: perCurrency ? "Group by card" : "Group by currency",
run: toggleMode, run: toggleMode,
}, },
@@ -491,9 +491,7 @@
{@const ends = bill.end_date?.slice(0, 10)} {@const ends = bill.end_date?.slice(0, 10)}
{@const last = lastOf(item)} {@const last = lastOf(item)}
{@const who = payer(item)} {@const who = payer(item)}
{@const stale = paid.isSuccess {@const stale = staleSince(bill, last, day)}
? staleSince(bill, last, day)
: null}
{@const extra = found.get(item.id) ?? []} {@const extra = found.get(item.id) ?? []}
{@const armedHere = arm.key?.endsWith(`:${item.id}`)} {@const armedHere = arm.key?.endsWith(`:${item.id}`)}
<tr <tr
@@ -569,7 +567,7 @@
billIds={manualCards.picking.ids} billIds={manualCards.picking.ids}
cards={cardList} cards={cardList}
/> />
{:else if bill.active && paid.isSuccess} {:else if bill.active}
{#if who.card} {#if who.card}
<span class="text-cladd-fg-soft" <span class="text-cladd-fg-soft"
>{who.card.name}</span >{who.card.name}</span
+2 -2
View File
@@ -26,7 +26,7 @@ services:
- "127.0.0.1:${FFUI_V1_PORT:-30072}:8080" - "127.0.0.1:${FFUI_V1_PORT:-30072}:8080"
v3: v3:
image: fireflyiii/core:latest@sha256:d51ab6a3a7b91a5c892441578e0da7f2935f84f36bd1151fc3833acc6e060871 image: fireflyiii/core:latest@sha256:a2b53539338d1cd04d341b229c3806cb178cf64c72459060e3ed34bec4167bd8
environment: environment:
<<: *firefly-env <<: *firefly-env
APP_KEY: ffuistandv3ffuistandv3ffuistand3 APP_KEY: ffuistandv3ffuistandv3ffuistand3
@@ -52,7 +52,7 @@ services:
caddy: caddy:
profiles: [prodlike] profiles: [prodlike]
image: caddy:2-alpine@sha256:eb4d35b7452d1ab6171fbdbe8d9275b2301dad6dad92115d91e19fb9b24d510f image: caddy:2-alpine@sha256:d8542f48d34a9cf4e4c11a478865229840e87e4c96ea3f439101f31a5d35f75f
environment: environment:
FIREFLY_DOMAIN: ":8080" FIREFLY_DOMAIN: ":8080"
volumes: volumes: