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",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"cron": "^4.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"patchright": "^1.52.5",
|
||||
"postgres": "^3.4.7",
|
||||
"puppeteer": "^24.14.0",
|
||||
@ -3934,6 +3935,20 @@
|
||||
"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": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||
@ -8475,6 +8490,19 @@
|
||||
"funding": {
|
||||
"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",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"cron": "^4.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"patchright": "^1.52.5",
|
||||
"postgres": "^3.4.7",
|
||||
"puppeteer": "^24.14.0",
|
||||
|
||||
@ -86,7 +86,7 @@ export async function updateBudget(id, name, amount, notes) {
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getBudgetTransactions(id) {
|
||||
export async function getBudgetTransactionsByid(id) {
|
||||
// Fetch all transactions associated with a specific budget
|
||||
try {
|
||||
let transactions = await db`
|
||||
@ -258,6 +258,22 @@ export async function getBudgetTransactionsForAccount(accountID) {
|
||||
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) {
|
||||
const accounts = await db`
|
||||
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;
|
||||
|
||||
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() {
|
||||
return running;
|
||||
}
|
||||
|
||||
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',
|
||||
headless: true,
|
||||
headless: false,
|
||||
viewport: null
|
||||
// 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();
|
||||
try {
|
||||
|
||||
state.push('Navigating to United FCU');
|
||||
await page.goto('https://online.unitedfcu.com/unitedfederalcredituniononline/uux.aspx#/login');
|
||||
@ -260,11 +261,10 @@ export async function pullData(amount = 100) {
|
||||
}
|
||||
|
||||
state.push('Done');
|
||||
await browser.close();
|
||||
} catch (error) {
|
||||
console.error('Error in pullData:', error);
|
||||
state.push(`Error: ${error.message}`);
|
||||
}
|
||||
|
||||
page.close();
|
||||
running = false;
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { getAccounts, getBudgets, getTotal } from '$lib/db';
|
||||
import { getAccounts, getBudgets } from '$lib/db';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function load({ params }) {
|
||||
let accounts = await getAccounts();
|
||||
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">
|
||||
<li>
|
||||
<a href="/">
|
||||
<span class="text-lg font-bold">Total: {total}</span>
|
||||
<span class="text-lg font-bold">Timm Budget</span>
|
||||
</a>
|
||||
</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 { loadingModal } from '$lib/loadingModal.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import TransactionList from '$lib/transactionList.svelte';
|
||||
|
||||
import EditTransaction from '$lib/editTransaction.svelte';
|
||||
import { init } from 'echarts';
|
||||
|
||||
let { data } = $props();
|
||||
const addToast = getContext('addToast');
|
||||
let trans = $derived(data.transactions);
|
||||
let transactions = $derived(data.transactions);
|
||||
let budgets = $derived(data.budgets);
|
||||
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 hide = $derived(account?.hide || false);
|
||||
let inTotal = $derived(account?.in_total || false);
|
||||
let expanded = $state([]);
|
||||
let loading = $state(false);
|
||||
|
||||
let editing = $state(false);
|
||||
|
||||
function editNotes(transaction, remaining) {
|
||||
my_modal_3.showModal();
|
||||
currentTransaction = transaction;
|
||||
notes = transaction.notes;
|
||||
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() {
|
||||
loading = true;
|
||||
settings_modal.close();
|
||||
@ -86,146 +63,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
<TransactionList {budgets} {budgetTransactions} {transactions} />
|
||||
|
||||
<dialog id="settings_modal" class="modal">
|
||||
<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 { getBudget, getBudgetTransactions } from '$lib/db.js';
|
||||
import { getBudget, getBudgetTransactionsByid } from '$lib/db.js';
|
||||
|
||||
export async function load({ params }) {
|
||||
const { slug } = params;
|
||||
console.log(`Loading transactions for budget: ${slug}`);
|
||||
const transactions = await getBudgetTransactions(slug);
|
||||
const transactions = await getBudgetTransactionsByid(slug);
|
||||
const budget = await getBudget(slug);
|
||||
|
||||
return { transactions, slug, budget };
|
||||
|
||||
Reference in New Issue
Block a user