Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2e7444d88 |
No files matched your search
@@ -1,4 +1,4 @@
|
||||
_commit: v0.7.0
|
||||
_commit: v0.8.0
|
||||
_src_path: https://git.kotikot.com/templates/frontend-svelte.git
|
||||
install_deps: true
|
||||
layout: root
|
||||
|
||||
+2
-2
@@ -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 COLORTERM=truecolor
|
||||
@@ -19,7 +19,7 @@ RUN bun --bun svelte-kit sync
|
||||
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
|
||||
|
||||
|
||||
+4
-5
@@ -7,20 +7,19 @@
|
||||
"!.svelte-kit",
|
||||
"!build",
|
||||
"!.mcp.json",
|
||||
<<<<<<< before updating
|
||||
"!.impeccable",
|
||||
"!src/lib/components/ui",
|
||||
"!src/routes/claddcn.css",
|
||||
"!!src/lib/api/schema.d.ts",
|
||||
"!api"
|
||||
=======
|
||||
"!src/lib/components/ui"
|
||||
>>>>>>> after updating
|
||||
]
|
||||
},
|
||||
"javascript": { "globals": ["Bun"] },
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["src/lib/components/ui/**"],
|
||||
"linter": { "enabled": false },
|
||||
"assist": { "enabled": false }
|
||||
},
|
||||
{
|
||||
"includes": ["src/lib/utils.ts"],
|
||||
"linter": { "rules": { "suspicious": { "noExplicitAny": "off" } } }
|
||||
|
||||
@@ -40,9 +40,13 @@
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-shape": "^3.2.0",
|
||||
"mode-watcher": "^1.1.0",
|
||||
<<<<<<< before updating
|
||||
"openapi-fetch": "^0.17.0",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
"shadcn-svelte": "^1.3.0",
|
||||
=======
|
||||
"shadcn-svelte": "^1.7.0",
|
||||
>>>>>>> after updating
|
||||
"svelte": "^5.57.1",
|
||||
"svelte-check": "^4.7.6",
|
||||
"svelte-sonner": "^1.1.0",
|
||||
|
||||
+15
-26
@@ -1,39 +1,28 @@
|
||||
const KEY = "ffui.budget-map";
|
||||
|
||||
export interface Learned {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function learnedMap(key: string) {
|
||||
const read = (): Record<string, Learned> => {
|
||||
function read(): Record<string, Learned> {
|
||||
try {
|
||||
const value = JSON.parse(localStorage.getItem(key) ?? "{}");
|
||||
const value = JSON.parse(localStorage.getItem(KEY) ?? "{}");
|
||||
return value && typeof value === "object" ? value : {};
|
||||
} catch {
|
||||
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));
|
||||
}
|
||||
@@ -75,26 +75,6 @@
|
||||
function fill(past: Split, withCounterparty: boolean) {
|
||||
const parts: string[] = [];
|
||||
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;
|
||||
if (withCounterparty && side && past.type === form.type) {
|
||||
const pastId = past[`${side}_id`];
|
||||
@@ -107,32 +87,33 @@
|
||||
) {
|
||||
const next = toAccountRef(account);
|
||||
split[side] = next;
|
||||
put(
|
||||
next.name,
|
||||
() => split[side].id === next.id,
|
||||
() => {
|
||||
parts.push(next.name);
|
||||
undo.push(() => {
|
||||
if (split[side].id === next.id) {
|
||||
split[side] = emptyAccount();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
const category = past.category_name ?? "";
|
||||
if (category && !split.category.trim()) {
|
||||
split.category = category;
|
||||
put(
|
||||
`category ${category}`,
|
||||
() => split.category === category,
|
||||
() => {
|
||||
parts.push(`category ${category}`);
|
||||
undo.push(() => {
|
||||
if (split.category === category) {
|
||||
split.category = "";
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
if (past.budget_id) {
|
||||
putRef("budget", past.budget_id, past.budget_name ?? "");
|
||||
const budget = past.budget_id;
|
||||
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) {
|
||||
filled = {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
Page,
|
||||
TransactionGroup,
|
||||
} 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 { formatMoney } from "#lib/format.js";
|
||||
import { fuzzyFilter } from "#lib/fuzzy.js";
|
||||
@@ -66,12 +66,6 @@ export class Catalog {
|
||||
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[] {
|
||||
const items = rank(
|
||||
this.accountsOf(kinds),
|
||||
@@ -241,41 +235,23 @@ export class Catalog {
|
||||
.catch(() => null);
|
||||
this.history.set(key, found);
|
||||
}
|
||||
const learnedBudget = learnedBudgets.get(accountId)?.id;
|
||||
const budget =
|
||||
type === "withdrawal" && side === "destination"
|
||||
? this.budgets.data?.find(
|
||||
(item) => item.id === learnedBudget && item.attributes.active
|
||||
)
|
||||
: 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)) {
|
||||
const learned = learnedBudget(accountId)?.id;
|
||||
const budget = this.budgets.data?.find(
|
||||
(item) => item.id === learned && item.attributes.active
|
||||
);
|
||||
if (!budget || type !== "withdrawal" || side !== "destination") {
|
||||
return found;
|
||||
}
|
||||
return found.then((past) => {
|
||||
const split = past?.split;
|
||||
if (!split) {
|
||||
return past;
|
||||
}
|
||||
return {
|
||||
return found.then(
|
||||
(past) =>
|
||||
past && {
|
||||
split: {
|
||||
...split,
|
||||
...(budget && {
|
||||
...past.split,
|
||||
budget_id: budget.id,
|
||||
budget_name: budget.attributes.name,
|
||||
}),
|
||||
...(category && {
|
||||
category_id: category.id,
|
||||
category_name: category.attributes.name,
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async fromJournal(journalId: string): Promise<Recent | null> {
|
||||
|
||||
+1
-40
@@ -130,39 +130,12 @@ function unitOf(range: Range, view: ViewRange): ViewRange | 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 {
|
||||
export function shiftPeriod(range: Range, view: ViewRange, dir: 1 | -1): Range {
|
||||
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 {
|
||||
const calendar = calendarShift(range, view, dir);
|
||||
if (calendar) {
|
||||
return calendar;
|
||||
}
|
||||
const days = daysBetween(range.start, range.end) + 1;
|
||||
return {
|
||||
end: addDays(range.end, dir * days),
|
||||
@@ -170,18 +143,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 {
|
||||
return Math.round((parse(b).getTime() - parse(a).getTime()) / 86_400_000);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
isDynamic,
|
||||
periodOf,
|
||||
type Range,
|
||||
stepPeriod,
|
||||
shiftPeriod,
|
||||
today,
|
||||
type ViewRange,
|
||||
} from "#lib/dates.js";
|
||||
@@ -23,7 +23,7 @@ export interface Preset {
|
||||
|
||||
function stored(): Range | null {
|
||||
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)
|
||||
? value
|
||||
: null;
|
||||
@@ -71,7 +71,7 @@ class Period {
|
||||
const range = rangeFromUrl(url) ?? stored() ?? periodOf(today(), view);
|
||||
this.start = range.start;
|
||||
this.end = range.end;
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(range));
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(range));
|
||||
this.ready = true;
|
||||
}
|
||||
|
||||
@@ -97,11 +97,11 @@ class Period {
|
||||
}
|
||||
this.start = range.start;
|
||||
this.end = range.end;
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(range));
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(range));
|
||||
}
|
||||
|
||||
shift(dir: 1 | -1) {
|
||||
this.set(stepPeriod(this.range, this.view, dir));
|
||||
this.set(shiftPeriod(this.range, this.view, dir));
|
||||
}
|
||||
|
||||
reset() {
|
||||
@@ -113,8 +113,8 @@ class Period {
|
||||
const { view } = this;
|
||||
const presets: Preset[] = [];
|
||||
if (!isDynamic(view)) {
|
||||
const prev = stepPeriod(this.range, view, -1);
|
||||
const next = stepPeriod(this.range, view, 1);
|
||||
const prev = shiftPeriod(this.range, view, -1);
|
||||
const next = shiftPeriod(this.range, view, 1);
|
||||
presets.push({ hint: "[", label: periodLabel(prev, view), range: prev });
|
||||
presets.push({ hint: "]", label: periodLabel(next, view), range: next });
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { api } from "#lib/api/client.js";
|
||||
import { invalidate } from "#lib/api/lists.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 {
|
||||
type Place,
|
||||
placesOf,
|
||||
@@ -269,6 +269,12 @@
|
||||
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) {
|
||||
adding = null;
|
||||
const next = rows[index + move];
|
||||
@@ -733,12 +739,12 @@
|
||||
? "Every expense has a budget."
|
||||
: `Every expense in ${period.label} has a budget.`}
|
||||
everything={scope === "all"}
|
||||
hint={(place) => learnedBudgets.get(place.accountId)?.name ?? null}
|
||||
hint={(place) => learnedBudget(place.accountId)?.name ?? null}
|
||||
ignoreKey="ffui.budget-ignored"
|
||||
loading={triage.isPending}
|
||||
noun="budget"
|
||||
onactivate={() => (triaging = true)}
|
||||
onassign={(place, option) => learnedBudgets.remember(place.accountId, option)}
|
||||
onassign={remember}
|
||||
onchange={() => refreshExcept(triageKey)}
|
||||
onleave={() => (triaging = false)}
|
||||
onswitch={() => setParam("scope", scope === "all" ? "" : "all")}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { api } from "#lib/api/client.js";
|
||||
import { invalidate } from "#lib/api/lists.js";
|
||||
import type { Category, One } from "#lib/api/types.js";
|
||||
import { learnedCategories } from "#lib/budgets/map.js";
|
||||
import {
|
||||
placesOf,
|
||||
refreshExcept,
|
||||
@@ -415,12 +414,10 @@
|
||||
? "Every transaction has a category."
|
||||
: `Every transaction in ${period.label} has a category.`}
|
||||
{everything}
|
||||
hint={(place) => learnedCategories.get(place.key)?.name ?? null}
|
||||
ignoreKey="ffui.category-ignored"
|
||||
loading={triage.isPending}
|
||||
noun="category"
|
||||
onactivate={() => (triaging = true)}
|
||||
onassign={(place, option) => learnedCategories.remember(place.key, option)}
|
||||
onchange={() => refreshExcept(triageKey)}
|
||||
onleave={() => (triaging = false)}
|
||||
onswitch={() => setParam("scope", everything ? "" : "all")}
|
||||
|
||||
@@ -13,7 +13,7 @@ x-firefly-env: &firefly-env
|
||||
|
||||
services:
|
||||
v1:
|
||||
image: fireflyiii/core:version-6.6.6@sha256:ae69fdd95cdef9038cd7a460a5aec731f14813973e4f096511d5a4ea9ff0e972
|
||||
image: fireflyiii/core:version-6.6.6
|
||||
environment:
|
||||
<<: *firefly-env
|
||||
APP_KEY: ffuistandv1ffuistandv1ffuistand1
|
||||
@@ -26,7 +26,7 @@ services:
|
||||
- "127.0.0.1:${FFUI_V1_PORT:-30072}:8080"
|
||||
|
||||
v3:
|
||||
image: fireflyiii/core:latest@sha256:d51ab6a3a7b91a5c892441578e0da7f2935f84f36bd1151fc3833acc6e060871
|
||||
image: fireflyiii/core:latest
|
||||
environment:
|
||||
<<: *firefly-env
|
||||
APP_KEY: ffuistandv3ffuistandv3ffuistand3
|
||||
@@ -52,7 +52,7 @@ services:
|
||||
|
||||
caddy:
|
||||
profiles: [prodlike]
|
||||
image: caddy:2-alpine@sha256:eb4d35b7452d1ab6171fbdbe8d9275b2301dad6dad92115d91e19fb9b24d510f
|
||||
image: caddy:2-alpine@sha256:d8542f48d34a9cf4e4c11a478865229840e87e4c96ea3f439101f31a5d35f75f
|
||||
environment:
|
||||
FIREFLY_DOMAIN: ":8080"
|
||||
volumes:
|
||||
|
||||
Reference in new issue
Block a user