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/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.0", "@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/typography": "^0.5.16",
"daisyui": "^5.0.43", "daisyui": "^5.0.43",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
@ -1104,6 +1105,22 @@
"node": ">= 10" "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": { "node_modules/@tailwindcss/vite": {
"version": "4.1.11", "version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz",
@ -1193,6 +1210,19 @@
"node": ">= 0.6" "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": { "node_modules/daisyui": {
"version": "5.0.43", "version": "5.0.43",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.43.tgz", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.43.tgz",
@ -1615,6 +1645,27 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@ -1751,6 +1802,20 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/postgres": {
"version": "3.4.7", "version": "3.4.7",
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz",
@ -1958,6 +2023,13 @@
"node": ">=6" "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": { "node_modules/vite": {
"version": "6.3.5", "version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",

View File

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

View File

@ -1,2 +1,3 @@
@import "tailwindcss"; @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 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() { export async function createBudgetTable() {
return await db` return await db`
create table if not exists budget ( create table if not exists budget (
id serial primary key, id serial primary key,
name text not null, name text not null,
amount numeric(10,2) not null, amount numeric(10,2) not null,
notes text notes text,
delete boolean default false
) )
` `
} }
@ -113,6 +130,7 @@ export async function getBudgets() {
budget.amount as amount, budget.amount as amount,
budget.notes as notes budget.notes as notes
from budget from budget
WHERE budget.delete is false
` `
if (!budgets) { if (!budgets) {
await createBudgetTable(); await createBudgetTable();
@ -230,15 +248,15 @@ export async function updateAccounts(data) {
} }
console.log(`Preparing to upsert transaction: ${txn.id} with data:`, txn); console.log(`Preparing to upsert transaction: ${txn.id} with data:`, txn);
await db` 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 ( values (
${txn.id}, ${txn.id},
${account.id}, ${account.id},
${txn.posted}, ${txn.posted},
${txn.amount ?? null}, ${txn.amount ?? null},
${txn.description ?? null}, ${txn.description ?? null},
${txn.pending}, ${txn.pending ?? false},
${extraId} ${txn.transacted_at ?? 0}
) )
on conflict (id) do update set on conflict (id) do update set
account_id = excluded.account_id, account_id = excluded.account_id,
@ -246,7 +264,7 @@ export async function updateAccounts(data) {
amount = excluded.amount, amount = excluded.amount,
description = excluded.description, description = excluded.description,
pending = excluded.pending, pending = excluded.pending,
extra_id = excluded.extra_id transacted_at = excluded.transacted_at
`; `;
} }
} }

View File

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

View File

@ -31,6 +31,10 @@
} }
</script> </script>
<div class="flex justify-between mb-4">
<h1 class="text-2xl font-bold">Account: {data.account.name}</h1>
</div>
<h1>Transcations</h1> <h1>Transcations</h1>
<table class="table w-full"> <table class="table w-full">
<thead> <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"; const url = "https://19443E0E8171E175EC5DA0C69B35DD50197F234B9A74C00D27FD606121257ECF:DAA3702E2100CFFD3B544251E6D755E86B1EDDFBFCC7F6FA9CE77AB3677E60DE@beta-bridge.simplefin.org/simplefin";
export async function POST() { 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)); return new Response(await updateAccounts(res));
} }

View File

@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit'; 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) // In-memory store for demonstration (replace with a database in production)
let budgets = []; let budgets = [];

View File

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

View File

@ -1,35 +1,91 @@
<script> <script>
let { data } = $props();
let budget = $state(data.budgets.find((b) => b.id == data.slug) || {}); let { data } = $props();
let transactions = $state(data.transactions || []); 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> </script>
<div class="align-text-top"> <div class="flex mb-4">
<h1 class="text-2xl font-bold">{budget.name}</h1> <div class="w-32 flex-none justify-bottom"><h1 class="text-2xl font-bold">{budget.name}</h1></div>
<p class="text-lg">Amount: ${budget.amount}</p> <div class="w-64 grow">{budget.amount}</div>
<p class="text-sm">Notes: {budget.notes}</p> <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> </div>
<table class="min-w-full border border-gray-300"> <table class="min-w-full border border-gray-300">
<thead> <thead>
<tr class=""> <tr class="">
<th class="px-4 py-2 border-b">Date</th> <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">Description</th>
<th class="px-4 py-2 border-b">Amount</th> <th class="px-4 py-2 border-b">Amount</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each transactions as txn} {#each transactions as txn}
<tr> <tr>
<td class="px-4 py-2 border-b">{txn.date}</td> <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.description}</td>
<td class="px-4 py-2 border-b">${txn.amount}</td> <td class="px-4 py-2 border-b">${txn.amount}</td>
</tr> </tr>
{/each} {/each}
{#if transactions.length === 0} {#if transactions.length === 0}
<tr> <tr>
<td class="px-4 py-2 border-b text-center" colspan="3">No transactions found.</td> <td class="px-4 py-2 border-b text-center" colspan="3">No transactions found.</td>
</tr> </tr>
{/if} {/if}
</tbody> </tbody>
</table> </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>