New and imporved!

This commit is contained in:
2025-07-25 16:22:57 -04:00
parent c12131e0c2
commit 9b2a0b63e3
16 changed files with 439 additions and 190 deletions

View File

@ -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
View File

@ -0,0 +1,6 @@
import * as charts from 'echarts';
export function echarts(node, option) {
const chart = charts.init(node);
chart.setOption(option);
}

View 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>

View 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>

View File

@ -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', {
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;
}