Compare commits
2 Commits
c12131e0c2
...
3e68f9af5b
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e68f9af5b | |||
| 9b2a0b63e3 |
28
package-lock.json
generated
28
package-lock.json
generated
@ -11,6 +11,7 @@
|
|||||||
"@auth/sveltekit": "^1.10.0",
|
"@auth/sveltekit": "^1.10.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"cron": "^4.3.2",
|
"cron": "^4.3.2",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
"patchright": "^1.52.5",
|
"patchright": "^1.52.5",
|
||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
"puppeteer": "^24.14.0",
|
"puppeteer": "^24.14.0",
|
||||||
@ -3934,6 +3935,20 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/echarts": {
|
||||||
|
"version": "5.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
|
||||||
|
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0",
|
||||||
|
"zrender": "5.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/echarts/node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||||
|
},
|
||||||
"node_modules/ejs": {
|
"node_modules/ejs": {
|
||||||
"version": "3.1.10",
|
"version": "3.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||||
@ -8475,6 +8490,19 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender": {
|
||||||
|
"version": "5.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
|
||||||
|
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender/node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
"@auth/sveltekit": "^1.10.0",
|
"@auth/sveltekit": "^1.10.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"cron": "^4.3.2",
|
"cron": "^4.3.2",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
"patchright": "^1.52.5",
|
"patchright": "^1.52.5",
|
||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
"puppeteer": "^24.14.0",
|
"puppeteer": "^24.14.0",
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export async function updateBudget(id, name, amount, notes) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBudgetTransactions(id) {
|
export async function getBudgetTransactionsByid(id) {
|
||||||
// Fetch all transactions associated with a specific budget
|
// Fetch all transactions associated with a specific budget
|
||||||
try {
|
try {
|
||||||
let transactions = await db`
|
let transactions = await db`
|
||||||
@ -258,6 +258,22 @@ export async function getBudgetTransactionsForAccount(accountID) {
|
|||||||
return transactions;
|
return transactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAllBudgetTransactions(accountID) {
|
||||||
|
const transactions = await db`
|
||||||
|
select budget_transaction.id as id,
|
||||||
|
budget_transaction.budget_id as budget_id,
|
||||||
|
budget_transaction.transaction_id as transaction_id,
|
||||||
|
budget_transaction.amount as amount,
|
||||||
|
budget_transaction.notes as notes,
|
||||||
|
budget.name as budget_name
|
||||||
|
from budget_transaction
|
||||||
|
join transaction on budget_transaction.transaction_id = transaction.id
|
||||||
|
join budget on budget_transaction.budget_id = budget.id
|
||||||
|
`;
|
||||||
|
return transactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getHiddenAccounts(age) {
|
export async function getHiddenAccounts(age) {
|
||||||
const accounts = await db`
|
const accounts = await db`
|
||||||
select
|
select
|
||||||
@ -514,4 +530,63 @@ export async function runRules() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUnallocatedTotal() {
|
||||||
|
const result = await db`
|
||||||
|
select sum(amount) as total
|
||||||
|
from budget_transaction
|
||||||
|
where transaction_id is null
|
||||||
|
`;
|
||||||
|
// result = Result [{ total: 1000.00 }]
|
||||||
|
return result[0].total;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUnallocatedTransactions() {
|
||||||
|
const result = await db`
|
||||||
|
SELECT TRANSACTION.id as id,
|
||||||
|
TRANSACTION.amount as amount,
|
||||||
|
TRANSACTION.description as description,
|
||||||
|
TRANSACTION.date as date,
|
||||||
|
TRANSACTION.account_id
|
||||||
|
FROM TRANSACTION
|
||||||
|
full join budget_transaction
|
||||||
|
ON TRANSACTION.id = budget_transaction.transaction_id
|
||||||
|
WHERE budget_transaction.amount IS NULL
|
||||||
|
AND TRANSACTION.out_of_budget = FALSE
|
||||||
|
`;
|
||||||
|
// result = Result [{ id: 1, amount: 100.00, notes: "Unallocated funds" }, ...]
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUnderallocatedTransactions() {
|
||||||
|
const result = await db`
|
||||||
|
SELECT *
|
||||||
|
FROM (SELECT TRANSACTION.id,
|
||||||
|
TRANSACTION.amount AS amount,
|
||||||
|
TRANSACTION.description,
|
||||||
|
TRANSACTION.DATE AS date,
|
||||||
|
TRANSACTION.account_id,
|
||||||
|
SUM(budget_transaction.amount) AS budget_total
|
||||||
|
FROM TRANSACTION
|
||||||
|
full join budget_transaction
|
||||||
|
ON TRANSACTION.id = budget_transaction.transaction_id
|
||||||
|
WHERE TRANSACTION.out_of_budget = FALSE
|
||||||
|
GROUP BY TRANSACTION.id) AS t
|
||||||
|
WHERE t.amount != t.budget_total
|
||||||
|
`;
|
||||||
|
// result = Result [{ id: 1, amount: 100.00, notes: "Unallocated funds" }, ...]
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLast30DaysTransactionsSums() {
|
||||||
|
let result = await db`
|
||||||
|
SELECT cast(date as date) as date, SUM(amount)
|
||||||
|
from transaction
|
||||||
|
where date >= (CURRENT_DATE - interval '30 days')
|
||||||
|
GROUP BY cast(date as date)
|
||||||
|
order by date desc;
|
||||||
|
`;
|
||||||
|
// result = Result [{ id: 1, amount: 100.00, notes: "Recent transaction" }, ...]
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
|
|||||||
6
src/lib/echarts.js
Normal file
6
src/lib/echarts.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import * as charts from 'echarts';
|
||||||
|
|
||||||
|
export function echarts(node, option) {
|
||||||
|
const chart = charts.init(node);
|
||||||
|
chart.setOption(option);
|
||||||
|
}
|
||||||
128
src/lib/editTransaction.svelte
Normal file
128
src/lib/editTransaction.svelte
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<script>
|
||||||
|
import { invalidateAll } from '$app/navigation';
|
||||||
|
let { transaction, close, budgets } = $props();
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
const addToast = getContext('addToast');
|
||||||
|
|
||||||
|
let id = $derived(transaction.id);
|
||||||
|
let description = $derived(transaction.description || 'No description');
|
||||||
|
let amount = $derived(transaction.amount || 0);
|
||||||
|
let date = $derived(new Date(transaction.date || Date.now()));
|
||||||
|
let out_of_budget = $derived(transaction.out_of_budget || false);
|
||||||
|
let budget_id = $derived(transaction.budget_id || null);
|
||||||
|
|
||||||
|
let loading = $state(false);
|
||||||
|
|
||||||
|
let notes = $state(transaction.notes || '');
|
||||||
|
|
||||||
|
async function saveNotes() {
|
||||||
|
loading = true;
|
||||||
|
let res = fetch(`/transcation/${id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
notes: $state.snapshot(notes),
|
||||||
|
out_of_budget: out_of_budget
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let result = await res;
|
||||||
|
if (result.ok) {
|
||||||
|
addToast('Notes saved successfully', 'success');
|
||||||
|
invalidateAll();
|
||||||
|
} else {
|
||||||
|
// Handle error case
|
||||||
|
addToast('Failed to save notes', 'error');
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog id="transactionEditModal" class="modal modal-open modal-top}">
|
||||||
|
<div class="modal-box">
|
||||||
|
{#if loading}
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<span class="loading loading-spinner loading-xl"></span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<form method="dialog">
|
||||||
|
<button
|
||||||
|
onclick={() => close()}
|
||||||
|
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<p class="label">{description}</p>
|
||||||
|
<p class="label">${amount}</p>
|
||||||
|
<p class="label">{date.toDateString()}</p>
|
||||||
|
<legend class="fieldset-legend">Notes</legend>
|
||||||
|
<textarea bind:value={notes} class="textarea w-100"></textarea>
|
||||||
|
<legend class="fieldset-legend">Transaction Properties</legend>
|
||||||
|
<label class="label">
|
||||||
|
<input type="checkbox" bind:checked={out_of_budget} class="toggle" />
|
||||||
|
Out of Budgets
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="btn btn-neutral" onclick={() => saveNotes()}>Save</button>
|
||||||
|
|
||||||
|
<legend class="fieldset-legend">Add to budget</legend>
|
||||||
|
<select bind:value={budget_id} class="select">
|
||||||
|
<option disabled selected>Pick a budget</option>
|
||||||
|
{#each budgets as budget}
|
||||||
|
<option value={budget.id}>{budget.name} - {budget.sum}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<legend class="fieldset-legend">Amount</legend>
|
||||||
|
<input
|
||||||
|
bind:value={amount}
|
||||||
|
type="number"
|
||||||
|
class="input validator"
|
||||||
|
required
|
||||||
|
placeholder="Amount"
|
||||||
|
title="Amount"
|
||||||
|
/>
|
||||||
|
<legend class="fieldset-legend">Notes</legend>
|
||||||
|
<textarea bind:value={notes} class="textarea w-100"></textarea>
|
||||||
|
<p class="validator-hint">Must a sensible number</p>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
onclick={() => {
|
||||||
|
if (budget_id) {
|
||||||
|
loading = true;
|
||||||
|
fetch(`/api/budget/${budget_id}/transaction`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
transactionId: id,
|
||||||
|
amount: amount,
|
||||||
|
notes: notes
|
||||||
|
})
|
||||||
|
}).then((res) => {
|
||||||
|
invalidateAll();
|
||||||
|
if (res.ok) {
|
||||||
|
// Optionally, you can refresh the UI or show a success message
|
||||||
|
addToast('Transaction added to budget', 'success');
|
||||||
|
console.log('Transaction added to budget successfully');
|
||||||
|
} else {
|
||||||
|
addToast('Failed to add transaction to budget', 'error');
|
||||||
|
console.error('Failed to add transaction to budget');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('No budget selected');
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
close();
|
||||||
|
}}>Add to Budget</button
|
||||||
|
>
|
||||||
|
</fieldset>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button onclick={() => close()}>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
80
src/lib/transactionList.svelte
Normal file
80
src/lib/transactionList.svelte
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<script>
|
||||||
|
import { EditSymbol } from '$lib/editSymbol.svelte';
|
||||||
|
import EditTransaction from '$lib/editTransaction.svelte';
|
||||||
|
let currentTransaction = $state({ budget_id: null, amount: 0, notes: '', out_of_budget: false });
|
||||||
|
let { transactions, budgetTransactions, budgets } = $props();
|
||||||
|
let editing = $state(false);
|
||||||
|
|
||||||
|
function editNotes(transaction, remaining) {
|
||||||
|
currentTransaction = transaction;
|
||||||
|
currentTransaction.amount = remaining;
|
||||||
|
editing = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#each transactions as transaction}
|
||||||
|
{@const applicableBudgets = budgetTransactions.filter(
|
||||||
|
(bt) => bt.transaction_id === transaction.id
|
||||||
|
)}
|
||||||
|
{@const budgetTotal = applicableBudgets.reduce(
|
||||||
|
(accumulator, currentValue) => accumulator + Number(currentValue.amount),
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
{@const remaining = transaction.amount - budgetTotal}
|
||||||
|
<div
|
||||||
|
class=" p-2 {remaining != 0 && !transaction.out_of_budget
|
||||||
|
? 'bg-warning-content'
|
||||||
|
: ''} {transaction.pending ? 'opacity-50' : ''}"
|
||||||
|
>
|
||||||
|
<div class="h-full flex flex-row justify-between items-center">
|
||||||
|
<div class="h-full flex flex-col md:flex-row justify-between md:items-center md:grow">
|
||||||
|
<div>
|
||||||
|
<div>{transaction.description}</div>
|
||||||
|
<div class="text-xs uppercase font-semibold opacity-60">
|
||||||
|
{transaction.date.toDateString()}
|
||||||
|
</div>
|
||||||
|
{#if !transaction.out_of_budget}
|
||||||
|
<div class="text-xs uppercase font-semibold text-left opacity-60">
|
||||||
|
In Budget: {budgetTotal.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs uppercase font-semibold text-left opacity-60">
|
||||||
|
Remaining {remaining.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-xs uppercase font-semibold text-left opacity-60">Out of budget</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if applicableBudgets.length > 0}
|
||||||
|
<div class="flex grow flex-col">
|
||||||
|
{#each applicableBudgets as budgetTransaction}
|
||||||
|
<div class="md:text-right">
|
||||||
|
{`${budgetTransaction.budget_name}: ${budgetTransaction.amount}`}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="md:text-right text-2xl md:p-4 md:w-35">
|
||||||
|
<div class="">
|
||||||
|
{transaction.amount}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<button
|
||||||
|
class="btn btn-square btn-ghost"
|
||||||
|
onclick={() => editNotes(transaction, remaining)}
|
||||||
|
>
|
||||||
|
{@render EditSymbol()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if editing}
|
||||||
|
<EditTransaction transaction={currentTransaction} close={() => (editing = false)} budgets />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@ -21,22 +21,23 @@ export function isNeedCode() {
|
|||||||
export function isRunning() {
|
export function isRunning() {
|
||||||
return running;
|
return running;
|
||||||
}
|
}
|
||||||
|
const browser = await chromium.launchPersistentContext('context', {
|
||||||
export async function pullData(amount = 100) {
|
|
||||||
running = true;
|
|
||||||
try {
|
|
||||||
state = [];
|
|
||||||
code = null;
|
|
||||||
needCode = false;
|
|
||||||
state.push('Starting Browser');
|
|
||||||
const browser = await chromium.launchPersistentContext('context', {
|
|
||||||
channel: 'chrome',
|
channel: 'chrome',
|
||||||
headless: true,
|
headless: false,
|
||||||
viewport: null
|
viewport: null
|
||||||
// do NOT add custom browser headers or userAgent
|
// do NOT add custom browser headers or userAgent
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export async function pullData(amount = 100) {
|
||||||
|
running = true;
|
||||||
|
state = [];
|
||||||
|
code = null;
|
||||||
|
needCode = false;
|
||||||
|
state.push('Starting Browser');
|
||||||
|
|
||||||
|
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
try {
|
||||||
|
|
||||||
state.push('Navigating to United FCU');
|
state.push('Navigating to United FCU');
|
||||||
await page.goto('https://online.unitedfcu.com/unitedfederalcredituniononline/uux.aspx#/login');
|
await page.goto('https://online.unitedfcu.com/unitedfederalcredituniononline/uux.aspx#/login');
|
||||||
@ -260,11 +261,10 @@ export async function pullData(amount = 100) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.push('Done');
|
state.push('Done');
|
||||||
await browser.close();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in pullData:', error);
|
console.error('Error in pullData:', error);
|
||||||
state.push(`Error: ${error.message}`);
|
state.push(`Error: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
page.close();
|
||||||
running = false;
|
running = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { getAccounts, getBudgets, getTotal } from '$lib/db';
|
import { getAccounts, getBudgets } from '$lib/db';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
export async function load({ params }) {
|
export async function load({ params }) {
|
||||||
let accounts = await getAccounts();
|
let accounts = await getAccounts();
|
||||||
let budgets = await getBudgets();
|
let budgets = await getBudgets();
|
||||||
let total = await getTotal();
|
|
||||||
|
|
||||||
return { accounts, budgets, total };
|
return { accounts, budgets };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
<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>
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<span class="text-lg font-bold">Total: {total}</span>
|
<span class="text-lg font-bold">Timm Budget</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li><div class="divider">Budgets</div></li>
|
<li><div class="divider">Budgets</div></li>
|
||||||
|
|||||||
20
src/routes/+page.server.js
Normal file
20
src/routes/+page.server.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
getLast30DaysTransactionsSums,
|
||||||
|
getUnallocatedTransactions,
|
||||||
|
getUnderallocatedTransactions,
|
||||||
|
getTotal,
|
||||||
|
getBudgets,
|
||||||
|
getAllBudgetTransactions
|
||||||
|
} from '$lib/db';
|
||||||
|
|
||||||
|
|
||||||
|
export async function load({ params }) {
|
||||||
|
const unallocatedTrans = await getUnallocatedTransactions();
|
||||||
|
const underAllocatedTrans = await getUnderallocatedTransactions();
|
||||||
|
const total = await getTotal();
|
||||||
|
const budgets = await getBudgets();
|
||||||
|
const budgetTransactions = await getAllBudgetTransactions();
|
||||||
|
const last30DaysTransactionsSums = await getLast30DaysTransactionsSums();
|
||||||
|
|
||||||
|
return { unallocatedTrans, underAllocatedTrans, total, budgets, budgetTransactions, last30DaysTransactionsSums };
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
<script>
|
||||||
|
import TransactionList from '$lib/transactionList.svelte';
|
||||||
|
import { echarts } from '$lib/echarts';
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
let total = $derived(data.total);
|
||||||
|
let unallocatedTrans = $derived(data.unallocatedTrans);
|
||||||
|
let underAllocatedTrans = $derived(data.underAllocatedTrans);
|
||||||
|
let budgets = $derived(data.budgets);
|
||||||
|
let budgetTransactions = $derived(data.budgetTransactions);
|
||||||
|
let last30days = $derived(data.last30DaysTransactionsSums);
|
||||||
|
|
||||||
|
let chartData = $derived(
|
||||||
|
last30days
|
||||||
|
.reduce((acc, curr) => [...acc, acc[acc.length - 1] + Number(curr.sum)], [Number(total)])
|
||||||
|
.reverse()
|
||||||
|
);
|
||||||
|
let chartDates = $derived(
|
||||||
|
[
|
||||||
|
'now',
|
||||||
|
...last30days.map((day) => `${day.date.getMonth() + 1}/${day.date.getDate()}`)
|
||||||
|
].reverse()
|
||||||
|
);
|
||||||
|
|
||||||
|
const option = $derived({
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: chartDates
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: chartData,
|
||||||
|
type: 'line'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="font-sans text-xl"
|
||||||
|
>Total Net Worth: <span class="{total > 0 ? 'bg-green-500' : 'bg-red-500'} pl-2 pr-2 rounded-lg"
|
||||||
|
>${total}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="container" use:echarts={option} />
|
||||||
|
|
||||||
|
<div class="text-xl divider">Unallocated Transactions</div>
|
||||||
|
<TransactionList {budgets} {budgetTransactions} transactions={unallocatedTrans} />
|
||||||
|
|
||||||
|
<div class="text-xl divider">Underallocated Transactions</div>
|
||||||
|
<TransactionList {budgets} {budgetTransactions} transactions={underAllocatedTrans} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
height: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -4,53 +4,30 @@
|
|||||||
import { invalidate, invalidateAll } from '$app/navigation';
|
import { invalidate, invalidateAll } from '$app/navigation';
|
||||||
import { loadingModal } from '$lib/loadingModal.svelte';
|
import { loadingModal } from '$lib/loadingModal.svelte';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
|
import TransactionList from '$lib/transactionList.svelte';
|
||||||
|
|
||||||
|
import EditTransaction from '$lib/editTransaction.svelte';
|
||||||
|
import { init } from 'echarts';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
const addToast = getContext('addToast');
|
const addToast = getContext('addToast');
|
||||||
let trans = $derived(data.transactions);
|
let transactions = $derived(data.transactions);
|
||||||
let budgets = $derived(data.budgets);
|
let budgets = $derived(data.budgets);
|
||||||
let budgetTransactions = $derived(data.budgetTransactions);
|
let budgetTransactions = $derived(data.budgetTransactions);
|
||||||
let notes = $state('');
|
|
||||||
let currentTransaction = $state({ budget_id: null, amount: 0, notes: '', out_of_budget: false });
|
|
||||||
let account = $derived(data.account);
|
let account = $derived(data.account);
|
||||||
let hide = $derived(account?.hide || false);
|
let hide = $derived(account?.hide || false);
|
||||||
let inTotal = $derived(account?.in_total || false);
|
let inTotal = $derived(account?.in_total || false);
|
||||||
let expanded = $state([]);
|
let expanded = $state([]);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
|
||||||
|
let editing = $state(false);
|
||||||
|
|
||||||
function editNotes(transaction, remaining) {
|
function editNotes(transaction, remaining) {
|
||||||
my_modal_3.showModal();
|
|
||||||
currentTransaction = transaction;
|
currentTransaction = transaction;
|
||||||
notes = transaction.notes;
|
|
||||||
currentTransaction.amount = remaining;
|
currentTransaction.amount = remaining;
|
||||||
|
editing = true;
|
||||||
}
|
}
|
||||||
async function saveNotes() {
|
|
||||||
my_modal_3.close();
|
|
||||||
loading = true;
|
|
||||||
let res = fetch(`/transcation/${currentTransaction.id}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
notes: $state.snapshot(notes),
|
|
||||||
out_of_budget: currentTransaction.out_of_budget
|
|
||||||
})
|
|
||||||
});
|
|
||||||
loading = false;
|
|
||||||
let result = await res;
|
|
||||||
if (result.ok) {
|
|
||||||
// Update the transaction in the data
|
|
||||||
currentTransaction.notes = notes;
|
|
||||||
// Optionally, you can also update the UI or show a success message
|
|
||||||
addToast('Notes saved successfully', 'success');
|
|
||||||
invalidateAll();
|
|
||||||
} else {
|
|
||||||
// Handle error case
|
|
||||||
console.error('Failed to save notes');
|
|
||||||
addToast('Failed to save notes', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
loading = true;
|
loading = true;
|
||||||
settings_modal.close();
|
settings_modal.close();
|
||||||
@ -86,146 +63,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1>Transcations</h1>
|
<h1>Transcations</h1>
|
||||||
<div>
|
|
||||||
{#each trans as transaction}
|
|
||||||
{@const applicableBudgets = budgetTransactions.filter(
|
|
||||||
(bt) => bt.transaction_id === transaction.id
|
|
||||||
)}
|
|
||||||
{@const budgetTotal = applicableBudgets.reduce(
|
|
||||||
(accumulator, currentValue) => accumulator + Number(currentValue.amount),
|
|
||||||
0
|
|
||||||
)}
|
|
||||||
{@const remaining = transaction.amount - budgetTotal}
|
|
||||||
<div
|
|
||||||
class=" p-2 {remaining != 0 && !transaction.out_of_budget
|
|
||||||
? 'bg-warning-content'
|
|
||||||
: ''} {transaction.pending ? 'opacity-50' : ''}"
|
|
||||||
>
|
|
||||||
<div class="h-full flex flex-row justify-between items-center">
|
|
||||||
<div class="h-full flex flex-col md:flex-row justify-between md:items-center md:grow">
|
|
||||||
<div>
|
|
||||||
<div>{transaction.description}</div>
|
|
||||||
<div class="text-xs uppercase font-semibold opacity-60">
|
|
||||||
{transaction.date.toDateString()}
|
|
||||||
</div>
|
|
||||||
{#if !transaction.out_of_budget}
|
|
||||||
<div class="text-xs uppercase font-semibold text-left opacity-60">
|
|
||||||
In Budget: {budgetTotal.toFixed(2)}
|
|
||||||
</div>
|
|
||||||
<div class="text-xs uppercase font-semibold text-left opacity-60">
|
|
||||||
Remaining {remaining.toFixed(2)}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="text-xs uppercase font-semibold text-left opacity-60">Out of budget</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if applicableBudgets.length > 0}
|
|
||||||
<div class="flex grow flex-col">
|
|
||||||
{#each applicableBudgets as budgetTransaction}
|
|
||||||
<div class="md:text-right">
|
|
||||||
{`${budgetTransaction.budget_name}: ${budgetTransaction.amount}`}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="md:text-right text-2xl md:p-4 md:w-35">
|
<TransactionList {budgets} {budgetTransactions} {transactions} />
|
||||||
<div class="">
|
|
||||||
{transaction.amount}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="">
|
|
||||||
<button
|
|
||||||
class="btn btn-square btn-ghost"
|
|
||||||
onclick={() => editNotes(transaction, remaining)}
|
|
||||||
>
|
|
||||||
{@render EditSymbol()}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dialog id="my_modal_3" 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>
|
|
||||||
<fieldset class="fieldset">
|
|
||||||
<p class="label">{currentTransaction?.description}</p>
|
|
||||||
<p class="label">${currentTransaction?.amount}</p>
|
|
||||||
<p class="label">{currentTransaction?.date?.toDateString()}</p>
|
|
||||||
<legend class="fieldset-legend">Notes</legend>
|
|
||||||
<textarea bind:value={notes} class="textarea w-100"></textarea>
|
|
||||||
<legend class="fieldset-legend">Login options</legend>
|
|
||||||
<label class="label">
|
|
||||||
<input type="checkbox" bind:checked={currentTransaction.out_of_budget} class="toggle" />
|
|
||||||
Out of Budgets
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button class="btn btn-neutral" onclick={() => saveNotes()}>Save</button>
|
|
||||||
|
|
||||||
<legend class="fieldset-legend">Add to budget</legend>
|
|
||||||
<select bind:value={currentTransaction.budget_id} class="select">
|
|
||||||
<option disabled selected>Pick a budget</option>
|
|
||||||
{#each budgets as budget}
|
|
||||||
<option value={budget.id}>{budget.name} - {budget.sum}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
<legend class="fieldset-legend">Amount</legend>
|
|
||||||
<input
|
|
||||||
bind:value={currentTransaction.amount}
|
|
||||||
type="number"
|
|
||||||
class="input validator"
|
|
||||||
required
|
|
||||||
placeholder="Amount"
|
|
||||||
title="Amount"
|
|
||||||
/>
|
|
||||||
<legend class="fieldset-legend">Notes</legend>
|
|
||||||
<textarea bind:value={currentTransaction.notes} class="textarea w-100"></textarea>
|
|
||||||
<p class="validator-hint">Must a sensible number</p>
|
|
||||||
<button
|
|
||||||
class="btn btn-primary"
|
|
||||||
onclick={() => {
|
|
||||||
if (currentTransaction.budget_id) {
|
|
||||||
loading = true;
|
|
||||||
my_modal_3.close();
|
|
||||||
fetch(`/api/budget/${currentTransaction.budget_id}/transaction`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
transactionId: currentTransaction.id,
|
|
||||||
amount: currentTransaction.amount,
|
|
||||||
notes: currentTransaction.notes
|
|
||||||
})
|
|
||||||
}).then((res) => {
|
|
||||||
loading = false;
|
|
||||||
invalidateAll();
|
|
||||||
if (res.ok) {
|
|
||||||
// Optionally, you can refresh the UI or show a success message
|
|
||||||
addToast('Transaction added to budget', 'success');
|
|
||||||
console.log('Transaction added to budget successfully');
|
|
||||||
} else {
|
|
||||||
addToast('Failed to add transaction to budget', 'error');
|
|
||||||
console.error('Failed to add transaction to budget');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('No budget selected');
|
|
||||||
}
|
|
||||||
}}>Add to Budget</button
|
|
||||||
>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button>close</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<dialog id="settings_modal" class="modal">
|
<dialog id="settings_modal" class="modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
|
|||||||
6
src/routes/api/transactions/unallocated/+server.js
Normal file
6
src/routes/api/transactions/unallocated/+server.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { getUnallocatedTransactions } from '$lib/db.js';
|
||||||
|
|
||||||
|
export async function GET({ url }) {
|
||||||
|
const transacations = await getUnallocatedTransactions();
|
||||||
|
return new Response(JSON.stringify(transacations));
|
||||||
|
}
|
||||||
6
src/routes/api/transactions/underallocated/+server.js
Normal file
6
src/routes/api/transactions/underallocated/+server.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { getUnderallocatedTransactions } from '$lib/db.js';
|
||||||
|
|
||||||
|
export async function GET({ url }) {
|
||||||
|
const transacations = await getUnderallocatedTransactions();
|
||||||
|
return new Response(JSON.stringify(transacations));
|
||||||
|
}
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { getBudget, getBudgetTransactions } from '$lib/db.js';
|
import { getBudget, getBudgetTransactionsByid } 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}`);
|
console.log(`Loading transactions for budget: ${slug}`);
|
||||||
const transactions = await getBudgetTransactions(slug);
|
const transactions = await getBudgetTransactionsByid(slug);
|
||||||
const budget = await getBudget(slug);
|
const budget = await getBudget(slug);
|
||||||
|
|
||||||
return { transactions, slug, budget };
|
return { transactions, slug, budget };
|
||||||
|
|||||||
Reference in New Issue
Block a user