feat(currencies,preferences,export,misc): currencies, rates, attachments, link types, preferences, export, administrations, recurring and webhooks

This commit is contained in:
hh committed 2026-10-16 03:42:24 +00:00
1 parent 89bbf56062
commit f8a5fd6c6d
47 files changed
+4496 -8

No files matched your search

+186
View File
@@ -0,0 +1,186 @@
<script lang="ts">
import Download from "@lucide/svelte/icons/download";
import { createQuery } from "@tanstack/svelte-query";
import { lists } from "#lib/api/lists.js";
import Box from "#lib/components/Box.svelte";
import { Action } from "#lib/components/misc/action.svelte.js";
import { download } from "#lib/components/misc/download.js";
import Field from "#lib/components/misc/Field.svelte";
import { sizeText } from "#lib/components/misc/queries.js";
import Status from "#lib/components/misc/Status.svelte";
import { muted } from "#lib/components/misc/styles.js";
import { setQuery } from "#lib/components/misc/url.js";
import PageHeader from "#lib/components/shell/PageHeader.svelte";
import Button from "#lib/components/ui/button/button.svelte";
import Input from "#lib/components/ui/input/input.svelte";
import { today } from "#lib/dates.js";
import { useKeys } from "#lib/keys.svelte.js";
import { period } from "#lib/period.svelte.js";
import { page } from "$app/state";
const TYPES = [
["transactions", "Transactions"],
["accounts", "Accounts"],
["budgets", "Budgets"],
["categories", "Categories"],
["tags", "Tags"],
["bills", "Subscriptions"],
["piggy-banks", "Piggy banks"],
["recurring", "Recurring transactions"],
["rules", "Rules"],
];
const EARLIEST = "1970-01-02";
const params = $derived(page.url.searchParams);
const type = $derived(
TYPES.find(([key]) => key === params.get("type"))?.[0] ?? "transactions"
);
const from = $derived(params.get("from") ?? period.start);
const to = $derived(params.get("to") ?? period.end);
const picked = $derived(
(params.get("accounts") ?? "").split(",").filter(Boolean)
);
const assets = createQuery(() => lists.accounts("asset"));
const accounts = $derived(
(assets.data ?? []).filter((a) => a.attributes.active !== false)
);
function toggle(id: string) {
const next = picked.includes(id)
? picked.filter((p) => p !== id)
: [...picked, id];
setQuery({ accounts: next.join(",") });
}
const action = new Action();
let element: HTMLFormElement | undefined = $state();
async function submit(event: SubmitEvent) {
event.preventDefault();
let size = 0;
const ok = await action.run(async () => {
if (from >= to) {
throw new Error("The start date must be before the end date");
}
size = await download(
`/v1/data/export/${type}`,
{ accounts: picked.join(","), end: to, start: from, type: "csv" },
`${type}-export.csv`
);
});
if (ok) {
action.message = `Downloaded ${sizeText(size)}`;
}
}
useKeys(() => [
{
group: "Export",
input: true,
keys: "mod+Enter",
label: "Export",
run: () => element?.requestSubmit(),
},
]);
</script>
<PageHeader
crumbs={[{ label: "Export data from Firefly III" }]}
title="Export data from Firefly III"
/>
<form class="max-w-4xl" onsubmit={submit} bind:this={element}>
<Box title="Export data from Firefly III">
<div class="flex flex-col gap-4">
<Field label="Type of export">
<div class="grid gap-1.5 pt-1.5 sm:grid-cols-3">
{#each TYPES as [key, label] (key)}
<label class="flex items-center gap-2">
<input
checked={type === key}
class="size-3.5 accent-brand"
name="type"
onchange={() => setQuery({ type: key })}
type="radio"
value={key}
>
{label}
</label>
{/each}
</div>
</Field>
<Field label="Date range">
<div class="flex flex-wrap items-center gap-2">
<Input
aria-label="Start date"
class="num w-40"
onchange={(event) => setQuery({ from: event.currentTarget.value })}
type="date"
value={from}
/>
<span class="text-cladd-fg-soft">to</span>
<Input
aria-label="End date"
class="num w-40"
onchange={(event) => setQuery({ to: event.currentTarget.value })}
type="date"
value={to}
/>
<Button
onclick={() => setQuery({ from: null, to: null })}
size="sm"
variant="outline"
>This period</Button
>
<Button
onclick={() => setQuery({ from: EARLIEST, to: today() })}
size="sm"
variant="outline"
>Everything</Button
>
</div>
</Field>
{#if type === "transactions"}
<Field label="Accounts">
<div class="flex flex-col gap-1.5 pt-1.5">
<p class={muted}>
{picked.length === 0
? "Transactions from all asset accounts. Pick accounts to narrow it down."
: `Transactions from ${picked.length} of ${accounts.length} asset accounts.`}
{#if picked.length > 0}
<button
class="text-brand hover:underline"
onclick={() => setQuery({ accounts: null })}
type="button"
>
Use all
</button>
{/if}
</p>
<div class="grid gap-1.5 sm:grid-cols-2 lg:grid-cols-3">
{#each accounts as account (account.id)}
<label class="flex min-w-0 items-center gap-2">
<input
checked={picked.includes(account.id)}
class="size-3.5 accent-brand"
onchange={() => toggle(account.id)}
type="checkbox"
>
<span class="truncate">{account.attributes.name}</span>
</label>
{/each}
</div>
</div>
</Field>
{/if}
</div>
{#snippet footer()}
<Button disabled={action.busy} size="sm" type="submit">
<Download />
Export {TYPES.find(([key]) => key === type)?.[1].toLowerCase()} as CSV
</Button>
<Status {action} busy="Preparing the file" />
{/snippet}
</Box>
</form>