Add account management features and integrate Tailwind CSS typography

- Implement setAccountInTotal and deleteBudget functions in db.js
- Update budget handling in the budget server API
- Add DELETE endpoint for budget removal
- Enhance account page with delete functionality and confirmation modal
- Include @tailwindcss/typography in package.json and app.css
This commit is contained in:
2025-07-07 21:43:36 -04:00
parent 3bc6b9ab7c
commit 61d2a258fc
12 changed files with 233 additions and 45 deletions

72
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/typography": "^0.5.16",
"daisyui": "^5.0.43",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
@ -1104,6 +1105,22 @@
"node": ">= 10"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
"integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2",
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz",
@ -1193,6 +1210,19 @@
"node": ">= 0.6"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/daisyui": {
"version": "5.0.43",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.43.tgz",
@ -1615,6 +1645,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@ -1751,6 +1802,20 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/postgres": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz",
@ -1958,6 +2023,13 @@
"node": ">=6"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",

View File

@ -15,6 +15,7 @@
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/typography": "^0.5.16",
"daisyui": "^5.0.43",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",

View File

@ -1,2 +1,3 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui";
@plugin "@tailwindcss/typography";

View File

@ -3,13 +3,30 @@ import postgres from 'postgres'
const db = postgres({ host:'192.168.1.126', username:'budget', password:'budget', database:'budget'}) // will use psql environment variables
export async function setAccountInTotal(accountId, total) {
return await db`
update account
set in_total = ${total}
where id = ${accountId}
`
}
export async function deleteBudget(id) {
return await db`
UPDATE budget
SET delete = true
WHERE id = ${id}
`
}
export async function createBudgetTable() {
return await db`
create table if not exists budget (
id serial primary key,
name text not null,
amount numeric(10,2) not null,
notes text
notes text,
delete boolean default false
)
`
}
@ -112,7 +129,8 @@ export async function getBudgets() {
budget.name as name,
budget.amount as amount,
budget.notes as notes
from budget
from budget
WHERE budget.delete is false
`
if (!budgets) {
await createBudgetTable();
@ -230,15 +248,15 @@ export async function updateAccounts(data) {
}
console.log(`Preparing to upsert transaction: ${txn.id} with data:`, txn);
await db`
insert into transaction (id, account_id, posted, amount, description, pending, extra_id)
insert into transaction (id, account_id, posted, amount, description, pending, transacted_at)
values (
${txn.id},
${account.id},
${txn.posted},
${txn.amount ?? null},
${txn.description ?? null},
${txn.pending},
${extraId}
${txn.pending ?? false},
${txn.transacted_at ?? 0}
)
on conflict (id) do update set
account_id = excluded.account_id,
@ -246,7 +264,7 @@ export async function updateAccounts(data) {
amount = excluded.amount,
description = excluded.description,
pending = excluded.pending,
extra_id = excluded.extra_id
transacted_at = excluded.transacted_at
`;
}
}

View File

@ -7,11 +7,11 @@
amount: 0,
notes: ''
});
async function update() {
let res = await fetch('/api/simplefin/update', {
method: 'POST'
});
}
async function update() {
let res = await fetch('/api/simplefin/update', {
method: 'POST'
});
}
async function saveBudget() {
let res = await fetch('/budget', {
method: 'POST',
@ -45,7 +45,7 @@
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-200 text-base-content min-h-full w-80 p-4">
<li>
<button onclick={update}>Update</button>
<button onclick={update}>Update</button>
</li>
<li><div class="divider">Budgets</div></li>
{#each budgets as budget}

View File

@ -31,6 +31,10 @@
}
</script>
<div class="flex justify-between mb-4">
<h1 class="text-2xl font-bold">Account: {data.account.name}</h1>
</div>
<h1>Transcations</h1>
<table class="table w-full">
<thead>

View File

@ -0,0 +1,25 @@
import { setAccountInTotal } from "$lib/db";
export async function POST({ request }) {
const body = await request.json();
const { slug, total } = body;
console.log(`Setting account ${slug} in total to ${total}`);
try {
const res = await setAccountInTotal(slug, total);
return new Response(JSON.stringify({ success: true, data: res }), {
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error('Error setting account in total:', error);
return new Response(JSON.stringify({ success: false, error: error.message }), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
});
}
}

View File

@ -0,0 +1,11 @@
import { deleteBudget } from '$lib/db.js';
export function DELETE({ params }) {
const { slug } = params;
console.log(`Deleting budget with slug: ${slug}`);
// Call the deleteBudget function from db.js
return deleteBudget(slug)
.then(() => new Response(`Budget with slug ${slug} deleted successfully.`))
.catch(err => new Response(`Error deleting budget: ${err.message}`, { status: 500 }));
}

View File

@ -5,7 +5,6 @@ import { updateAccounts } from '$lib/db' ;
const url = "https://19443E0E8171E175EC5DA0C69B35DD50197F234B9A74C00D27FD606121257ECF:DAA3702E2100CFFD3B544251E6D755E86B1EDDFBFCC7F6FA9CE77AB3677E60DE@beta-bridge.simplefin.org/simplefin";
export async function POST() {
const res = await fetchAccounts(url, new Date("2025-07-01"))
const res = await fetchAccounts(url, new Date("2025-07-03"))
return new Response(await updateAccounts(res));
}

View File

@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit';
import { addBudget } from '$lib/db';
import { addBudget, deleteBudget } from '$lib/db';
// In-memory store for demonstration (replace with a database in production)
let budgets = [];
@ -15,4 +15,4 @@ export async function POST({ request }) {
let res = await addBudget(data.name, data.amount, data.notes);
return json(res, { status: 201 });
}
}

View File

@ -3,6 +3,7 @@ import { getBudgetTransactions } from '$lib/db.js';
export async function load({ params }) {
const { slug } = params;
console.log(`Loading transactions for budget: ${slug}`);
const transactions = await getBudgetTransactions(slug);
return { transactions, slug };

View File

@ -1,35 +1,91 @@
<script>
let { data } = $props();
let budget = $state(data.budgets.find((b) => b.id == data.slug) || {});
let transactions = $state(data.transactions || []);
let { data } = $props();
let budget = $state(data.budgets.find((b) => b.id == data.slug) || {});
let transactions = $state(data.transactions || []);
let toDeleteBudget = $state(null);
async function deleteBudget() {
if (toDeleteBudget === budget.name) {
let res = await fetch(`/api/budget/${budget.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (res.ok) {
window.location.href = '/'; // Redirect to budgets list after deletion
} else {
console.error('Failed to delete budget');
}
} else {
alert('Please type the budget name correctly to confirm deletion.');
}
}
</script>
<div class="align-text-top">
<h1 class="text-2xl font-bold">{budget.name}</h1>
<p class="text-lg">Amount: ${budget.amount}</p>
<p class="text-sm">Notes: {budget.notes}</p>
<div class="flex mb-4">
<div class="w-32 flex-none justify-bottom"><h1 class="text-2xl font-bold">{budget.name}</h1></div>
<div class="w-64 grow">{budget.amount}</div>
<div class="w-14 flex-none text-right">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 30 30"
width="30px"
height="30px"
class="cursor-pointer hover:scale-110 transition"
onclick={()=> DeleteBudgetModal.showModal()}
>
<path d="M 14.984375 2.4863281 A 1.0001 1.0001 0 0 0 14 3.5 L 14 4 L 8.5 4 A 1.0001 1.0001 0 0 0 7.4863281 5 L 6 5 A 1.0001 1.0001 0 1 0 6 7 L 24 7 A 1.0001 1.0001 0 1 0 24 5 L 22.513672 5 A 1.0001 1.0001 0 0 0 21.5 4 L 16 4 L 16 3.5 A 1.0001 1.0001 0 0 0 14.984375 2.4863281 z M 6 9 L 7.7929688 24.234375 C 7.9109687 25.241375 8.7633438 26 9.7773438 26 L 20.222656 26 C 21.236656 26 22.088031 25.241375 22.207031 24.234375 L 24 9 L 6 9 z"/>
</svg>
</div>
</div>
<div class="mb-4">
<p class="text-sm">Notes: {budget.notes}</p>
</div>
<table class="min-w-full border border-gray-300">
<thead>
<tr class="">
<th class="px-4 py-2 border-b">Date</th>
<th class="px-4 py-2 border-b">Description</th>
<th class="px-4 py-2 border-b">Amount</th>
</tr>
</thead>
<tbody>
{#each transactions as txn}
<tr>
<td class="px-4 py-2 border-b">{txn.date}</td>
<td class="px-4 py-2 border-b">{txn.description}</td>
<td class="px-4 py-2 border-b">${txn.amount}</td>
</tr>
{/each}
{#if transactions.length === 0}
<tr>
<td class="px-4 py-2 border-b text-center" colspan="3">No transactions found.</td>
</tr>
{/if}
</tbody>
<thead>
<tr class="">
<th class="px-4 py-2 border-b">Date</th>
<th class="px-4 py-2 border-b">Description</th>
<th class="px-4 py-2 border-b">Amount</th>
</tr>
</thead>
<tbody>
{#each transactions as txn}
<tr>
<td class="px-4 py-2 border-b">{txn.date}</td>
<td class="px-4 py-2 border-b">{txn.description}</td>
<td class="px-4 py-2 border-b">${txn.amount}</td>
</tr>
{/each}
{#if transactions.length === 0}
<tr>
<td class="px-4 py-2 border-b text-center" colspan="3">No transactions found.</td>
</tr>
{/if}
</tbody>
</table>
<dialog id="DeleteBudgetModal" class="modal">
<div class="modal-box">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<p>Are you sure you want to delete <code>{budget.name}</code>? type its name in the box below if so.</p>
<div>
<input
bind:value={toDeleteBudget}
type="text"
placeholder="Budget Name"
class="input input-bordered w-full max-w-xs"
/>
</div>
<button onclick={()=>deleteBudget()} class="btn btn-primary mt-4">Delete</button>
</div>
</dialog>