Compare commits
13 Commits
3791d4c9c9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ca5f67c1b | |||
| 416ef3bc37 | |||
| 82dbefa565 | |||
| 1d2e90a183 | |||
| ded6489edf | |||
| cf6fed50d3 | |||
| 9ec5fbf75a | |||
| d9b1a8430d | |||
| f4103953f6 | |||
| 63a6694507 | |||
| 3e68f9af5b | |||
| 9b2a0b63e3 | |||
| c12131e0c2 |
2
.gitignore
vendored
@ -21,3 +21,5 @@ Thumbs.db
|
|||||||
# Vite
|
# Vite
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
context
|
||||||
7
better-auth_migrations/2025-07-27T14-24-24.068Z.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" boolean not null, "image" text, "createdAt" timestamp not null, "updatedAt" timestamp not null);
|
||||||
|
|
||||||
|
create table "session" ("id" text not null primary key, "expiresAt" timestamp not null, "token" text not null unique, "createdAt" timestamp not null, "updatedAt" timestamp not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id"));
|
||||||
|
|
||||||
|
create table "auth_accounts" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id"), "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" timestamp, "refreshTokenExpiresAt" timestamp, "scope" text, "password" text, "createdAt" timestamp not null, "updatedAt" timestamp not null);
|
||||||
|
|
||||||
|
create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" timestamp not null, "createdAt" timestamp, "updatedAt" timestamp);
|
||||||
1249
package-lock.json
generated
10
package.json
@ -4,12 +4,13 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host",
|
"dev": "vite dev --host --port 3000",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prepare": "svelte-kit sync || echo ''",
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "prettier --check ."
|
"lint": "prettier --check .",
|
||||||
|
"generate-pwa-assets": "pwa-assets-generator"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-auto": "^6.0.0",
|
||||||
@ -17,6 +18,7 @@
|
|||||||
"@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",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"@vite-pwa/assets-generator": "^1.0.0",
|
||||||
"@vite-pwa/sveltekit": "^1.0.0",
|
"@vite-pwa/sveltekit": "^1.0.0",
|
||||||
"daisyui": "^5.0.43",
|
"daisyui": "^5.0.43",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
@ -27,7 +29,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/sveltekit": "^1.10.0",
|
"@auth/sveltekit": "^1.10.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"better-auth": "^1.3.4",
|
||||||
"cron": "^4.3.2",
|
"cron": "^4.3.2",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
|
"patchright": "^1.52.5",
|
||||||
|
"pg": "^8.16.3",
|
||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
"puppeteer": "^24.14.0",
|
"puppeteer": "^24.14.0",
|
||||||
"puppeteer-extra": "^3.3.6",
|
"puppeteer-extra": "^3.3.6",
|
||||||
|
|||||||
8
pwa-assets.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig, minimal2023Preset as preset } from '@vite-pwa/assets-generator/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
preset,
|
||||||
|
images: [
|
||||||
|
'static/favicon.svg',
|
||||||
|
]
|
||||||
|
})
|
||||||
13
src/app.html
@ -1,12 +1,15 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
|
<link rel="icon" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -1,5 +1,9 @@
|
|||||||
import { CronJob } from 'cron';
|
import { CronJob } from 'cron';
|
||||||
import { pullData } from './lib/united.js';
|
import { pullData } from './lib/united.js';
|
||||||
|
import { auth } from '$lib/auth'; // path to your auth file
|
||||||
|
import { svelteKitHandler } from 'better-auth/svelte-kit';
|
||||||
|
import { building } from '$app/environment';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
const job = new CronJob(
|
const job = new CronJob(
|
||||||
'0,30 * * * *', // cronTime
|
'0,30 * * * *', // cronTime
|
||||||
@ -10,3 +14,23 @@ const job = new CronJob(
|
|||||||
true, // start
|
true, // start
|
||||||
'America/Detroit' // timeZone
|
'America/Detroit' // timeZone
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export async function handle({ event, resolve }) {
|
||||||
|
console.log('Handling request:', event.request.method, event.url.pathname);
|
||||||
|
if (event.route.id?.startsWith('/(protected)/') && !event.url.pathname.startsWith('/api/auth')) {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: event.request.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
event.locals.session = session?.session;
|
||||||
|
event.locals.user = session?.user;
|
||||||
|
return svelteKitHandler({ event, resolve, auth });
|
||||||
|
} else {
|
||||||
|
redirect(307, '/sign-in');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Not a protected route or API auth request:', event.url.pathname);
|
||||||
|
return svelteKitHandler({ event, resolve, auth });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
7
src/lib/auth-client.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { createAuthClient } from "better-auth/svelte"
|
||||||
|
export const authClient = createAuthClient({
|
||||||
|
/** The base URL of the server (optional if you're using the same domain) */
|
||||||
|
baseURL: "https://budget.caseytimm.com",
|
||||||
|
|
||||||
|
})
|
||||||
|
export const { signIn, signUp, useSession } = authClient;
|
||||||
17
src/lib/auth.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import { sveltekitCookies } from "better-auth/svelte-kit";
|
||||||
|
import { getRequestEvent } from "$app/server";
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: new Pool({
|
||||||
|
connectionString: 'postgresql://budget:budget@sql.caseytimm.com:5432/budget',
|
||||||
|
}),
|
||||||
|
account: {
|
||||||
|
modelName: 'auth_accounts',
|
||||||
|
},
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
plugins: [sveltekitCookies(getRequestEvent)],
|
||||||
|
})
|
||||||
@ -1,14 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { EditSymbol } from '$lib/editSymbol.svelte';
|
import { EditSymbol } from '$lib/editSymbol.svelte';
|
||||||
import { TrashBin } from '$lib/trashbin.svelte';
|
import { TrashBin } from '$lib/trashbin.svelte';
|
||||||
import { loadingModal } from '$lib/loadingModal.svelte';
|
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { invalidate, invalidateAll } from '$app/navigation';
|
import { invalidate, invalidateAll } from '$app/navigation';
|
||||||
|
import { loadingModal } from '$lib/loadingModal.svelte';
|
||||||
let { data } = $props();
|
let { transactions } = $props();
|
||||||
const addToast = getContext('addToast');
|
const addToast = getContext('addToast');
|
||||||
let budget = $derived(data.budget);
|
|
||||||
let transactions = $derived(data.transactions.transactions || []);
|
|
||||||
let newData = $state({
|
let newData = $state({
|
||||||
amount: 0,
|
amount: 0,
|
||||||
notes: '',
|
notes: '',
|
||||||
@ -96,55 +94,27 @@
|
|||||||
newData.name = transaction.description;
|
newData.name = transaction.description;
|
||||||
newData.notes = transaction.notes || '';
|
newData.notes = transaction.notes || '';
|
||||||
newData.id = transaction.budget_transaction_id;
|
newData.id = transaction.budget_transaction_id;
|
||||||
|
searchString = transaction.notes || '';
|
||||||
|
search();
|
||||||
EditBudgetTransactionModal.showModal();
|
EditBudgetTransactionModal.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreBudget() {
|
|
||||||
let res = await fetch(`/api/budget/${budget.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ delete: false })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteTransaction(transaction) {
|
function deleteTransaction(transaction) {
|
||||||
toDelete = transaction.budget_transaction_id;
|
toDelete = transaction.budget_transaction_id;
|
||||||
DeleteTransactionModal.showModal();
|
DeleteTransactionModal.showModal();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if budget.delete}
|
|
||||||
<div role="alert" class="alert alert-error" onclick={() => RestoreModal.showModal()}>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-6 w-6 shrink-0 stroke-current"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>This budget has been deleted</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<ul class="list bg-base-100 rounded-box shadow-md">
|
<ul class="list bg-base-100 rounded-box shadow-md">
|
||||||
{#each transactions as tras}
|
{#each transactions as tras}
|
||||||
<li class="list-row">
|
<li class="list-row">
|
||||||
<div>
|
<div>
|
||||||
{#if tras.id == null}
|
{#if tras.id == null}
|
||||||
|
{#if tras.budget_name}
|
||||||
|
<div>{tras.budget_name}</div>
|
||||||
|
{:else}
|
||||||
<span class="badge badge-warning">Orphan</span>
|
<span class="badge badge-warning">Orphan</span>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div>{tras?.description}</div>
|
<div>{tras?.description}</div>
|
||||||
<div class="text-xs uppercase font-semibold opacity-60">
|
<div class="text-xs uppercase font-semibold opacity-60">
|
||||||
@ -173,9 +143,9 @@
|
|||||||
<dialog id="EditBudgetTransactionModal" class="modal">
|
<dialog id="EditBudgetTransactionModal" class="modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<h1>{newData.name}</h1>
|
<h1>{newData.name}</h1>
|
||||||
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
|
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-full border p-4">
|
||||||
<legend class="fieldset-legend">Reassign</legend>
|
<legend class="fieldset-legend">Reassign</legend>
|
||||||
<div class="join">
|
<div class="join w-full">
|
||||||
<label class="input">
|
<label class="input">
|
||||||
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
<g
|
<g
|
||||||
@ -195,7 +165,7 @@
|
|||||||
>Search</button
|
>Search</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="join">
|
<div class="join w-full">
|
||||||
<select class="select" bind:value={newTransaction.id} disabled={searching}>
|
<select class="select" bind:value={newTransaction.id} disabled={searching}>
|
||||||
<option value="" disabled selected>Select a Transaction</option>
|
<option value="" disabled selected>Select a Transaction</option>
|
||||||
{#each searchResults as res}
|
{#each searchResults as res}
|
||||||
@ -209,7 +179,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
|
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-full border p-4">
|
||||||
<legend class="fieldset-legend">Edit</legend>
|
<legend class="fieldset-legend">Edit</legend>
|
||||||
|
|
||||||
<label class="label">Amount</label>
|
<label class="label">Amount</label>
|
||||||
@ -217,11 +187,11 @@
|
|||||||
bind:value={newData.amount}
|
bind:value={newData.amount}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Amount"
|
placeholder="Amount"
|
||||||
class="input input-bordered w-full max-w-xs"
|
class="input input-bordered w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label class="label">Notes</label>
|
<label class="label">Notes</label>
|
||||||
<textarea bind:value={newData.notes} class="textarea w-100" placeholder="Budget Notes"
|
<textarea bind:value={newData.notes} class="textarea w-full" placeholder="Budget Notes"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<button onclick={() => saveTransaction()} class="btn btn-primary mt-4">Save</button>
|
<button onclick={() => saveTransaction()} class="btn btn-primary mt-4">Save</button>
|
||||||
@ -232,34 +202,13 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<dialog id="RestoreModal" class="modal">
|
|
||||||
<div class="modal-box">
|
|
||||||
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
|
|
||||||
<legend class="fieldset-legend">Restore?</legend>
|
|
||||||
|
|
||||||
<button onclick={() => restoreBudget()} class="btn btn-primary mt-4">Restore</button>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button>close</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<dialog id="DeleteTransactionModal" class="modal">
|
<dialog id="DeleteTransactionModal" class="modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<p>Are you sure you want to delete</p>
|
<p>Are you sure you want to delete</p>
|
||||||
<span>{toDelete}</span>
|
<span>{toDelete}</span>
|
||||||
<p>Type it in the box to confirm</p>
|
|
||||||
<input
|
|
||||||
bind:value={toDeleteName}
|
|
||||||
type="text"
|
|
||||||
placeholder="Type the name to confirm"
|
|
||||||
class="input input-bordered w-full max-w-xs"
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-error mt-4"
|
class="btn btn-error mt-4"
|
||||||
onclick={async () => {
|
onclick={async () => {
|
||||||
if (toDelete == toDeleteName) {
|
|
||||||
let res = await fetch(`/api/budget/${toDelete}/transaction/`, {
|
let res = await fetch(`/api/budget/${toDelete}/transaction/`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
@ -272,10 +221,6 @@
|
|||||||
console.error('Failed to delete transaction');
|
console.error('Failed to delete transaction');
|
||||||
addToast('Failed to delete transaction', 'error');
|
addToast('Failed to delete transaction', 'error');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.error('Name does not match');
|
|
||||||
addToast('Name does not match', 'warning');
|
|
||||||
}
|
|
||||||
}}>Delete</button
|
}}>Delete</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
116
src/lib/db.js
@ -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`
|
||||||
@ -138,24 +138,15 @@ export async function deleteBudgetTransaction(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function addBudgetTransaction(budgetId, transactionId, amount, notes, ruleId = null) {
|
export async function addBudgetTransaction(budgetId, transactionId, amount, notes, ruleId = null) {
|
||||||
const existingTransactions = await db`
|
|
||||||
select amount from budget_transaction
|
|
||||||
where transaction_id = ${transactionId}
|
|
||||||
`;
|
|
||||||
const realTransactionAmount = await db`
|
|
||||||
select amount from transaction
|
|
||||||
where id = ${transactionId}
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (existingTransactions.length > 0) {
|
const exsisting = await db`
|
||||||
if (
|
select id from budget_transaction
|
||||||
existingTransactions.reduce((acc, curr) => acc + curr.amount, 0) + amount >
|
where budget_id = ${budgetId} and transaction_id = ${transactionId}
|
||||||
realTransactionAmount
|
`;
|
||||||
) {
|
if (exsisting.length > 0) {
|
||||||
return -1;
|
// If the transaction already exists in the budget, update it
|
||||||
|
return updateBudgetTransaction(exsisting[0].id, amount, notes);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Add a transaction to a budget
|
// Add a transaction to a budget
|
||||||
const result = await db`
|
const result = await db`
|
||||||
insert into budget_transaction (budget_id, transaction_id, amount, notes, rule_id)
|
insert into budget_transaction (budget_id, transaction_id, amount, notes, rule_id)
|
||||||
@ -258,6 +249,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 +521,79 @@ 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 getOrphanedTransactions() {
|
||||||
|
const result = await db`
|
||||||
|
SELECT budget.name as budget_name,
|
||||||
|
budget_transaction.id as trans_id,
|
||||||
|
budget_transaction.budget_id as budget_id,
|
||||||
|
budget_transaction.transaction_id as transaction_id,
|
||||||
|
budget_transaction.amount as budget_amount,
|
||||||
|
budget_transaction.notes as notes
|
||||||
|
FROM budget_transaction
|
||||||
|
LEFT JOIN budget on budget_id = budget.id
|
||||||
|
WHERE transaction_id IS NULL
|
||||||
|
`;
|
||||||
|
// 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
@ -0,0 +1,6 @@
|
|||||||
|
import * as charts from 'echarts';
|
||||||
|
|
||||||
|
export function echarts(node, option) {
|
||||||
|
const chart = charts.init(node);
|
||||||
|
chart.setOption(option);
|
||||||
|
}
|
||||||
189
src/lib/editTransaction.svelte
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
<script>
|
||||||
|
import { invalidateAll } from '$app/navigation';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { EditSymbol } from '$lib/editSymbol.svelte';
|
||||||
|
import { TrashBin } from '$lib/trashbin.svelte';
|
||||||
|
const addToast = getContext('addToast');
|
||||||
|
|
||||||
|
let { transaction, close, budgets } = $props();
|
||||||
|
let id = $derived(transaction.id);
|
||||||
|
let description = $derived(transaction.description || 'No description');
|
||||||
|
let amount = $derived(transaction.amount.toFixed(2) || 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 deleting = $state(false);
|
||||||
|
let deletingText = $state('');
|
||||||
|
let deletebt = $state(null);
|
||||||
|
|
||||||
|
let notes = $state(transaction.notes || '');
|
||||||
|
|
||||||
|
function editBudget(budgetTransaction) {
|
||||||
|
budget_id = budgetTransaction.budget_id;
|
||||||
|
amount = budgetTransaction.amount;
|
||||||
|
notes = budgetTransaction.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveBudget() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBudgetTransaction(bt) {
|
||||||
|
deleting = true;
|
||||||
|
deletingText = `${bt.budget_name}: ${bt.amount}`;
|
||||||
|
deletebt = bt;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendDeletebt() {
|
||||||
|
loading = true;
|
||||||
|
if (deletebt) {
|
||||||
|
let res = await fetch(`/api/budget/${deletebt.id}/transaction`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
addToast('Budget transaction deleted successfully', 'success');
|
||||||
|
invalidateAll();
|
||||||
|
} else {
|
||||||
|
addToast('Failed to delete budget transaction', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deleting = false;
|
||||||
|
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 Transaction</button>
|
||||||
|
|
||||||
|
<legend class="fieldset-legend">Current Budgets</legend>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{#each transaction.budgetTransactions as budgetTransaction}
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-lg"
|
||||||
|
>{budgetTransaction.budget_name}: {budgetTransaction.amount}</span
|
||||||
|
>
|
||||||
|
<div class="grow justify-end flex">
|
||||||
|
<button
|
||||||
|
class="btn btn-square btn-ghost"
|
||||||
|
onclick={() => editBudget(budgetTransaction)}
|
||||||
|
>
|
||||||
|
{@render EditSymbol()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="btn btn-square btn-ghost"
|
||||||
|
onclick={() => deleteBudgetTransaction(budgetTransaction)}
|
||||||
|
>
|
||||||
|
{@render TrashBin()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if deleting}
|
||||||
|
<legend class="fieldset-legend">Delete Budget Transaction</legend>
|
||||||
|
<span class="text-xl">Deleting budget transaction - {deletingText}</span>
|
||||||
|
<span class="text-xl">Are you sure?</span>
|
||||||
|
<button class="text-xl btn btn-success" onclick={() => (deleting = false)}>Cancel</button>
|
||||||
|
<button class="text-xl btn btn-error" onclick={() => sendDeletebt()}>Delete</button>
|
||||||
|
{:else}
|
||||||
|
<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" required placeholder="Amount" title="Amount" />
|
||||||
|
<legend class="fieldset-legend">Notes</legend>
|
||||||
|
<textarea bind:value={notes} class="textarea w-100"></textarea>
|
||||||
|
<button class="btn btn-primary" onclick={() => saveBudget()}>Save Budget</button>
|
||||||
|
{/if}
|
||||||
|
</fieldset>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button onclick={() => close()}>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
26
src/lib/login.svelte
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script>
|
||||||
|
import { signIn, signUp } from '$lib/auth-client';
|
||||||
|
let email = $state('');
|
||||||
|
let password = $state('');
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
let res = await signIn.email({ email, password });
|
||||||
|
console.log('Login response:', res);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="flex justify-center items-center h-screen">
|
||||||
|
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
|
||||||
|
<legend class="fieldset-legend">Login</legend>
|
||||||
|
|
||||||
|
<label class="label">Username</label>
|
||||||
|
<input type="email" class="input" placeholder="Username" bind:value={email} />
|
||||||
|
|
||||||
|
<label class="label">Password</label>
|
||||||
|
<input type="password" class="input" placeholder="Password" bind:value={password} />
|
||||||
|
|
||||||
|
<button class="btn btn-neutral mt-4" onclick={() => handleLogin()}>Login</button>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
81
src/lib/transactionList.svelte
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<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, budgetTransactions) {
|
||||||
|
currentTransaction = transaction;
|
||||||
|
currentTransaction.amount = remaining;
|
||||||
|
currentTransaction.budgetTransactions = budgetTransactions;
|
||||||
|
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, applicableBudgets)}
|
||||||
|
>
|
||||||
|
{@render EditSymbol()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if editing}
|
||||||
|
<EditTransaction transaction={currentTransaction} close={() => (editing = false)} {budgets} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import puppeteer from 'puppeteer-extra';
|
|
||||||
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|
||||||
import db from './db.js';
|
import db from './db.js';
|
||||||
import { json } from '@sveltejs/kit';
|
import { chromium } from 'patchright';
|
||||||
|
|
||||||
puppeteer.use(StealthPlugin());
|
//const browser = await chromium.launch();
|
||||||
let state = [];
|
let state = [];
|
||||||
let code = null;
|
let code = null;
|
||||||
let needCode = false;
|
let needCode = false;
|
||||||
@ -24,74 +22,54 @@ export function isRunning() {
|
|||||||
return running;
|
return running;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const browser = await chromium.launchPersistentContext('context', {
|
||||||
|
channel: 'chrome',
|
||||||
|
headless: false,
|
||||||
|
viewport: null
|
||||||
|
// do NOT add custom browser headers or userAgent
|
||||||
|
});
|
||||||
|
|
||||||
export async function pullData(amount = 100) {
|
export async function pullData(amount = 100) {
|
||||||
running = true;
|
running = true;
|
||||||
try {
|
|
||||||
state = [];
|
state = [];
|
||||||
code = null;
|
code = null;
|
||||||
needCode = false;
|
needCode = false;
|
||||||
state.push('Starting Browser');
|
state.push('Starting Browser');
|
||||||
const browser = await puppeteer.launch({
|
|
||||||
args: ['--no-sandbox'],
|
|
||||||
headless: true
|
|
||||||
//defaultViewport: null,
|
|
||||||
//args: ['--disable-blink-features=PrettyPrintJSONDocument']
|
|
||||||
});
|
|
||||||
|
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
try {
|
||||||
state.push('Loading Cookies');
|
|
||||||
|
|
||||||
const cookiesdb = await db`SELECT
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
domain,
|
|
||||||
path,
|
|
||||||
expires,
|
|
||||||
size,
|
|
||||||
httpOnly,
|
|
||||||
secure,
|
|
||||||
session,
|
|
||||||
priority,
|
|
||||||
sameParty,
|
|
||||||
sourceScheme,
|
|
||||||
sourcePort
|
|
||||||
FROM cookies`;
|
|
||||||
|
|
||||||
cookiesdb.forEach((cookie) => {
|
|
||||||
cookie.expires = cookie.expires ? Number(cookie.expires) : undefined;
|
|
||||||
cookie.size = cookie.size ? Number(cookie.size) : undefined;
|
|
||||||
cookie.sourcePort = cookie.sourcePort ? Number(cookie.sourcePort) : undefined;
|
|
||||||
state.push('Loading cookie: ' + cookie.name);
|
|
||||||
browser.setCookie(cookie);
|
|
||||||
});
|
|
||||||
|
|
||||||
state.push('Navigating to United FCU');
|
state.push('Navigating to United FCU');
|
||||||
|
|
||||||
// Navigate the page to a URL.
|
|
||||||
await page.goto('https://online.unitedfcu.com/unitedfederalcredituniononline/uux.aspx#/login');
|
await page.goto('https://online.unitedfcu.com/unitedfederalcredituniononline/uux.aspx#/login');
|
||||||
|
|
||||||
state.push(`Current URL: ${page.url()}`);
|
state.push(`Current URL: ${page.url()}`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
|
|
||||||
if (page.url().includes('interstitial')) {
|
if (page.url().includes('interstitial')) {
|
||||||
await page.waitForNavigation();
|
await page.waitForLoadState();
|
||||||
state.push('Already logged in, navigating to dashboard');
|
}
|
||||||
|
while (page.url().includes('interstitial')) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
await page.waitForLoadState();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!page.url().includes('dashboard')) {
|
if (!page.url().includes('dashboard')) {
|
||||||
state.push('Logging in to United FCU');
|
state.push('Logging in to United FCU');
|
||||||
// Type into search box using accessible input name.
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
|
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
|
||||||
await page.locator('aria/Login ID').fill('92830');
|
await page.getByLabel('Login ID').fill('92830');
|
||||||
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
|
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
|
||||||
|
await page.getByLabel('Password').fill('Cmtjlt13');
|
||||||
await page.locator('aria/Password').fill('Cmtjlt13');
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
|
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
|
||||||
|
|
||||||
state.push('Submitting login form');
|
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
await page.waitForNavigation();
|
//await page.waitForLoadState();
|
||||||
|
//await new Promise((resolve) => setTimeout(resolve, 5 * 5000));
|
||||||
|
|
||||||
|
while (page.url().includes('interstitial')) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
await page.waitForLoadState();
|
||||||
|
}
|
||||||
|
|
||||||
const url = page.url();
|
const url = page.url();
|
||||||
state.push(`Current URL after login: ${url}`);
|
state.push(`Current URL after login: ${url}`);
|
||||||
|
|
||||||
@ -99,12 +77,9 @@ export async function pullData(amount = 100) {
|
|||||||
state.push('MFA required, selecting SMS option');
|
state.push('MFA required, selecting SMS option');
|
||||||
console.log('MFA required, please complete the authentication process.');
|
console.log('MFA required, please complete the authentication process.');
|
||||||
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
|
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
|
||||||
|
await page.getByText('SMS: (XXX) XXX-4029').click();
|
||||||
await page.locator('aria/SMS: (XXX) XXX-4029').click();
|
await page.waitForURL('**/mfa/entertarget');
|
||||||
await page.waitForNavigation();
|
await page.getByPlaceholder('Secure Access Code');
|
||||||
//need to do some stuff ehre
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
|
|
||||||
|
|
||||||
//await page.keyboard.press('Tab');
|
//await page.keyboard.press('Tab');
|
||||||
state.push('Waiting for code input');
|
state.push('Waiting for code input');
|
||||||
needCode = true;
|
needCode = true;
|
||||||
@ -112,38 +87,26 @@ export async function pullData(amount = 100) {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
if (code != null) break;
|
if (code != null) break;
|
||||||
}
|
}
|
||||||
if (code == null) {
|
|
||||||
needCode = false;
|
needCode = false;
|
||||||
|
if (code == null) {
|
||||||
state.push('Code not provided within 5 minutes');
|
state.push('Code not provided within 5 minutes');
|
||||||
throw new Error('Code not provided within 5 minutes');
|
throw new Error('Code not provided within 5 minutes');
|
||||||
}
|
}
|
||||||
state.push(`Got code: ${code}`);
|
state.push(`Got code: ${code}`);
|
||||||
|
await page.getByPlaceholder('Secure Access Code').fill(code);
|
||||||
await page.locator('>>> [placeholder="Secure Access Code"]').fill(code);
|
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000));
|
||||||
//await page.keyboard.type(code);
|
|
||||||
code = null;
|
|
||||||
needCode = false;
|
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
await page.locator('aria/Register Device').click();
|
|
||||||
await page.waitForNavigation();
|
await page.getByText('Access Code Accepted.').waitFor();
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.waitForURL('**/dashboard');
|
||||||
|
await page.screenshot({ path: 'united-login.png' });
|
||||||
|
state.push('Logged in successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
state.push('Saving cookies');
|
|
||||||
let cookies = await browser.cookies();
|
|
||||||
|
|
||||||
cookies.forEach(async (cookie) => {
|
|
||||||
state.push('Saving cookie: ' + cookie.name);
|
|
||||||
|
|
||||||
// Insert or update the cookie in the database
|
|
||||||
await db`DELETE FROM cookies WHERE name = ${cookie.name}`;
|
|
||||||
|
|
||||||
await db`INSERT INTO cookies (name, value, domain, path, expires, size, httpOnly, secure, session, priority, sameParty, sourceScheme, sourcePort)
|
|
||||||
VALUES (${cookie.name}, ${cookie.value}, ${cookie.domain}, ${cookie.path}, ${cookie.expires}, ${cookie.size}, ${cookie.httpOnly}, ${cookie.secure}, ${cookie.session}, ${cookie.priority}, ${cookie.sameParty}, ${cookie.sourceScheme}, ${cookie.sourcePort})`;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//await new Promise((resolve) => setTimeout(resolve, 60000));
|
|
||||||
|
|
||||||
state.push('Fetching q2token');
|
state.push('Fetching q2token');
|
||||||
const q2token = (await browser.cookies()).find((cookie) => cookie.name === 'q2token')?.value;
|
const q2token = (await browser.cookies()).find((cookie) => cookie.name === 'q2token')?.value;
|
||||||
|
|
||||||
@ -226,21 +189,19 @@ export async function pullData(amount = 100) {
|
|||||||
let currentPend =
|
let currentPend =
|
||||||
await db`SELECT id, amount, description, date from transaction where account_id = ${account.id} AND pending=true`;
|
await db`SELECT id, amount, description, date from transaction where account_id = ${account.id} AND pending=true`;
|
||||||
for (const pend of currentPend) {
|
for (const pend of currentPend) {
|
||||||
const found = transactions.find((t) => t.transactionId === pend.id);
|
const found = transactions.find((t) => `${t.description}:${t.amount}` === pend.id && t.transactionType == "Memo");
|
||||||
if (!found) {
|
|
||||||
const updated = transactions.find(
|
|
||||||
(t) => t.amount == pend.amount && new Date(t.postedDate) == pend.date
|
|
||||||
);
|
|
||||||
if (updated) {
|
|
||||||
state.push(
|
|
||||||
`I think I found an updated transaction: ${updated.statementDescription} ${updated.amount} for ${pend.description} ${pend.amount}`
|
|
||||||
);
|
|
||||||
|
|
||||||
await db`UPDATE budget_transaction SET transaction_id = ${updated.transactionId} WHERE transaction_id = ${pend.id}`;
|
if (found && found.transactionType != "Memo")
|
||||||
} else {
|
{state.push(
|
||||||
|
`I think I found an updated transaction: ${found.statementDescription} ${found.amount} for ${pend.description} ${pend.amount}`
|
||||||
|
);
|
||||||
|
await db`UPDATE budget_transaction SET transaction_id = ${found.transactionId} WHERE transaction_id = ${pend.id}`;
|
||||||
|
await db`DELETE FROM transaction WHERE id = ${pend.id}`;
|
||||||
|
} else if (!found)
|
||||||
|
{
|
||||||
state.push(`Orphaning no longer pending budget transaction with no new parent`);
|
state.push(`Orphaning no longer pending budget transaction with no new parent`);
|
||||||
await db`UPDATE budget_transaction SET transaction_id = null WHERE transaction_id = ${pend.id}`;
|
|
||||||
}
|
await db`UPDATE budget_transaction SET transaction_id = null, notes = notes || ${pend.description} WHERE transaction_id = ${pend.id}`;
|
||||||
state.push(`Removing pending transaction: ${pend.id}`);
|
state.push(`Removing pending transaction: ${pend.id}`);
|
||||||
await db`DELETE FROM transaction WHERE id = ${pend.id}`;
|
await db`DELETE FROM transaction WHERE id = ${pend.id}`;
|
||||||
}
|
}
|
||||||
@ -258,7 +219,7 @@ export async function pullData(amount = 100) {
|
|||||||
const accountId = transaction.accountId;
|
const accountId = transaction.accountId;
|
||||||
|
|
||||||
const id = pending
|
const id = pending
|
||||||
? `${transaction.postedDate}:${transaction.amount}`
|
? `${transaction.description}:${transaction.amount}`
|
||||||
: transaction.hostTranNumber;
|
: transaction.hostTranNumber;
|
||||||
|
|
||||||
await db`INSERT INTO transaction (
|
await db`INSERT INTO transaction (
|
||||||
@ -291,7 +252,7 @@ export async function pullData(amount = 100) {
|
|||||||
pending = EXCLUDED.pending`;
|
pending = EXCLUDED.pending`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
state.push('Orphaning transactions');
|
state.push('Orphaning transactions');
|
||||||
|
|
||||||
const orphaned = await db`SELECT bt.id as id
|
const orphaned = await db`SELECT bt.id as id
|
||||||
@ -302,10 +263,12 @@ export async function pullData(amount = 100) {
|
|||||||
state.push(`Orphaning transaction: ${orphan.id}`);
|
state.push(`Orphaning transaction: ${orphan.id}`);
|
||||||
await db`UPDATE budget_transaction set transaction_id = null where id = ${orphan.id}`;
|
await db`UPDATE budget_transaction set transaction_id = null where id = ${orphan.id}`;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
state.push('Done');
|
||||||
} 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 };
|
||||||
}
|
}
|
||||||
118
src/routes/(protected)/+layout.svelte
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<script>
|
||||||
|
import '../../app.css';
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
import { pwaInfo } from 'virtual:pwa-info';
|
||||||
|
import { authClient } from '$lib/auth-client';
|
||||||
|
import Login from '$lib/login.svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
const session = authClient.useSession();
|
||||||
|
|
||||||
|
console.log('PWA Info:', pwaInfo);
|
||||||
|
const webManifestLink = $state(pwaInfo ? pwaInfo.webManifest.linkTag : '');
|
||||||
|
$inspect(webManifestLink);
|
||||||
|
|
||||||
|
let { children, data } = $props();
|
||||||
|
let budgets = $derived(data.budgets);
|
||||||
|
let total = $derived(data.total);
|
||||||
|
let toast = $state([]);
|
||||||
|
|
||||||
|
function addToast(message, type = 'info') {
|
||||||
|
toast.push({ message, type });
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.pop();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setContext('addToast', addToast);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
{@html webManifestLink}
|
||||||
|
</svelte:head>
|
||||||
|
{#if $session.data}
|
||||||
|
<div class="drawer lg:drawer-open">
|
||||||
|
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
|
||||||
|
<div class="drawer-content flex flex-col m-5">
|
||||||
|
<div class="navbar bg-base-100 shadow-sm lg:hidden">
|
||||||
|
<div class="flex-none">
|
||||||
|
<label
|
||||||
|
for="my-drawer-2"
|
||||||
|
class="btn btn-primary drawer-button lg:hidden btn-square btn-ghost"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="inline-block h-5 w-5 stroke-current"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<a class="btn btn-ghost text-xl" href="/">Timm Budget</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
<div class="drawer-side">
|
||||||
|
<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>
|
||||||
|
<a href="/">
|
||||||
|
<span class="text-lg font-bold">Timm Budget</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><div class="divider">Budgets</div></li>
|
||||||
|
{#each budgets as budget}
|
||||||
|
<li>
|
||||||
|
<a href={`/budget/${budget.id}`}>
|
||||||
|
{budget.name} ({budget.sum})
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
<li><div class="divider">Accounts</div></li>
|
||||||
|
{#each data.accounts as account}
|
||||||
|
<li>
|
||||||
|
<a href={`/account/${account.id}`}>
|
||||||
|
{account.name} ({account.balance})
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
<li><div class="divider"></div></li>
|
||||||
|
<li><a href="/rules">Rules</a></li>
|
||||||
|
<li><a href="/settings">Settings</a></li>
|
||||||
|
<li><a href="/united">United</a></li>
|
||||||
|
<li><div class="divider"></div></li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-square w-70 grow"
|
||||||
|
onclick={() =>
|
||||||
|
authClient.signOut({
|
||||||
|
fetchOptions: {
|
||||||
|
onSuccess: () => goto('/')
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Logout</button
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toast toast-top toast-center">
|
||||||
|
{#each toast as t}
|
||||||
|
<div class="alert alert-{t.type}">
|
||||||
|
<span>{t.message}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Login></Login>
|
||||||
|
{/if}
|
||||||
22
src/routes/(protected)/+page.server.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
getLast30DaysTransactionsSums,
|
||||||
|
getUnallocatedTransactions,
|
||||||
|
getUnderallocatedTransactions,
|
||||||
|
getTotal,
|
||||||
|
getBudgets,
|
||||||
|
getAllBudgetTransactions,
|
||||||
|
getOrphanedTransactions
|
||||||
|
} 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();
|
||||||
|
const orphanedTransactions = await getOrphanedTransactions();
|
||||||
|
|
||||||
|
return { unallocatedTrans, underAllocatedTrans, total, budgets, budgetTransactions, last30DaysTransactionsSums, orphanedTransactions };
|
||||||
|
}
|
||||||
40
src/routes/(protected)/+page.svelte
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<script>
|
||||||
|
import TransactionList from '$lib/transactionList.svelte';
|
||||||
|
import Budgetlist from '$lib/budgetlist.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.reverse());
|
||||||
|
let orphanedTransactions = $derived(data.orphanedTransactions);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="font-sans text-3xl p-4"
|
||||||
|
>Total Net Worth: <span class="{total > 0 ? 'bg-green-500' : 'bg-red-500'} pl-2 pr-2 rounded-lg"
|
||||||
|
>${total}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
|
||||||
|
{#each budgets as budget}
|
||||||
|
<a
|
||||||
|
href="/budget/{budget.id}"
|
||||||
|
class="block p-4 mb-2 bg-base-200 rounded-lg hover:bg-base-300 transition duration-200"
|
||||||
|
><div class="flex flex-row justify-between items-center text-2xl">
|
||||||
|
<div>{budget.name}</div>
|
||||||
|
<div>{budget.sum}</div>
|
||||||
|
</div></a
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<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} />
|
||||||
|
|
||||||
|
<div class="text-xl divider">Orphans</div>
|
||||||
|
<Budgetlist transactions={orphanedTransactions} />
|
||||||
95
src/routes/(protected)/account/[slug]/+page.svelte
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<script>
|
||||||
|
import { EditSymbol } from '$lib/editSymbol.svelte';
|
||||||
|
import { settingsSymbol } from '$lib/settingsSymbol.svelte';
|
||||||
|
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 transactions = $derived(data.transactions);
|
||||||
|
let budgets = $derived(data.budgets);
|
||||||
|
let budgetTransactions = $derived(data.budgetTransactions);
|
||||||
|
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) {
|
||||||
|
currentTransaction = transaction;
|
||||||
|
currentTransaction.amount = remaining;
|
||||||
|
editing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSettings() {
|
||||||
|
loading = true;
|
||||||
|
settings_modal.close();
|
||||||
|
let res = await fetch(`/api/account/${account.id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
hide: $state.snapshot(hide),
|
||||||
|
in_total: $state.snapshot(inTotal)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
loading = false;
|
||||||
|
invalidateAll();
|
||||||
|
if (res.ok) {
|
||||||
|
// Optionally, you can refresh the account data or show a success message
|
||||||
|
} else {
|
||||||
|
console.error('Failed to save settings');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex mb-4">
|
||||||
|
<div class="w-128 flex-none justify-bottom">
|
||||||
|
<h1 class="text-lg font-semibold">{account?.name}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="w-64 grow text-lg uppercase font-semibold">{account?.balance}</div>
|
||||||
|
<div class="w-14 flex-none text-right">
|
||||||
|
<button class="btn btn-square btn-ghost" onclick={() => settings_modal.showModal()}
|
||||||
|
>{@render settingsSymbol()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1>Transcations</h1>
|
||||||
|
|
||||||
|
<TransactionList {budgets} {budgetTransactions} {transactions} />
|
||||||
|
|
||||||
|
<dialog id="settings_modal" 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>
|
||||||
|
<h3>{account?.name}</h3>
|
||||||
|
<fieldset class="fieldset bg-base-100 border-base-300 rounded-box w-64 border p-4">
|
||||||
|
<legend class="fieldset-legend">Options</legend>
|
||||||
|
<label class="label">
|
||||||
|
<input type="checkbox" bind:checked={hide} class="toggle" />
|
||||||
|
Hide
|
||||||
|
</label>
|
||||||
|
<label class="label">
|
||||||
|
<input type="checkbox" bind:checked={inTotal} class="toggle" />
|
||||||
|
Use in total
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<button onclick={() => saveSettings()} class="btn btn-primary mt-4">Save</button>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
{@render loadingModal()}
|
||||||
|
{/if}
|
||||||
@ -22,7 +22,6 @@ export async function PATCH({ params, request }) {
|
|||||||
const { amount, notes, transactionId } = body;
|
const { amount, notes, transactionId } = body;
|
||||||
console.log({ slug, transactionId, amount });
|
console.log({ slug, transactionId, amount });
|
||||||
|
|
||||||
// Call the deleteBudget function from db.js (budgetId, transactionId, amount)
|
|
||||||
return updateBudgetTransaction(transactionId, amount, notes)
|
return updateBudgetTransaction(transactionId, amount, notes)
|
||||||
.then(() => new Response(`Budget transaction updated successfully`, { status: 200 }))
|
.then(() => new Response(`Budget transaction updated successfully`, { status: 200 }))
|
||||||
.catch(
|
.catch(
|
||||||
@ -36,7 +35,6 @@ export async function DELETE({ params, request }) {
|
|||||||
const { transactionId } = slug;
|
const { transactionId } = slug;
|
||||||
console.log({ slug });
|
console.log({ slug });
|
||||||
|
|
||||||
// Call the deleteBudget function from db.js (budgetId, transactionId)
|
|
||||||
return deleteBudgetTransaction(slug)
|
return deleteBudgetTransaction(slug)
|
||||||
.then(() => new Response(`Budget transaction deleted successfully`, { status: 200 }))
|
.then(() => new Response(`Budget transaction deleted successfully`, { status: 200 }))
|
||||||
.catch(
|
.catch(
|
||||||
@ -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));
|
||||||
|
}
|
||||||
@ -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 };
|
||||||
62
src/routes/(protected)/budget/[slug]/+page.svelte
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<script>
|
||||||
|
import { EditSymbol } from '$lib/editSymbol.svelte';
|
||||||
|
import { TrashBin } from '$lib/trashbin.svelte';
|
||||||
|
import { loadingModal } from '$lib/loadingModal.svelte';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { invalidate, invalidateAll } from '$app/navigation';
|
||||||
|
import BudgetList from '$lib/budgetlist.svelte';
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
const addToast = getContext('addToast');
|
||||||
|
let budget = $derived(data.budget);
|
||||||
|
let transactions = $derived(data.transactions.transactions || []);
|
||||||
|
|
||||||
|
async function restoreBudget() {
|
||||||
|
let res = await fetch(`/api/budget/${budget.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ delete: false })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if budget.delete}
|
||||||
|
<div role="alert" class="alert alert-error" onclick={() => RestoreModal.showModal()}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>This budget has been deleted</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<BudgetList {transactions} />
|
||||||
|
|
||||||
|
<dialog id="RestoreModal" class="modal">
|
||||||
|
<div class="modal-box">
|
||||||
|
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
|
||||||
|
<legend class="fieldset-legend">Restore?</legend>
|
||||||
|
|
||||||
|
<button onclick={() => restoreBudget()} class="btn btn-primary mt-4">Restore</button>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
@ -53,6 +53,7 @@
|
|||||||
|
|
||||||
{#if needCode}
|
{#if needCode}
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
|
<form>
|
||||||
<span>Need Code</span>
|
<span>Need Code</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -74,5 +75,6 @@
|
|||||||
>
|
>
|
||||||
Send Code
|
Send Code
|
||||||
</button>
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -1,87 +0,0 @@
|
|||||||
<script>
|
|
||||||
import '../app.css';
|
|
||||||
import { setContext } from 'svelte';
|
|
||||||
let { children, data } = $props();
|
|
||||||
let budgets = $derived(data.budgets);
|
|
||||||
let total = $derived(data.total);
|
|
||||||
let toast = $state([]);
|
|
||||||
|
|
||||||
function addToast(message, type = 'info') {
|
|
||||||
toast.push({ message, type });
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.pop();
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
setContext('addToast', addToast);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="drawer lg:drawer-open">
|
|
||||||
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
|
|
||||||
<div class="drawer-content flex flex-col m-5">
|
|
||||||
<div class="navbar bg-base-100 shadow-sm lg:hidden">
|
|
||||||
<div class="flex-none">
|
|
||||||
<label
|
|
||||||
for="my-drawer-2"
|
|
||||||
class="btn btn-primary drawer-button lg:hidden btn-square btn-ghost"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
class="inline-block h-5 w-5 stroke-current"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M4 6h16M4 12h16M4 18h16"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<a class="btn btn-ghost text-xl" href="/">Timm Budget</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
<div class="drawer-side">
|
|
||||||
<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>
|
|
||||||
<a href="/">
|
|
||||||
<span class="text-lg font-bold">Total: {total}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li><div class="divider">Budgets</div></li>
|
|
||||||
{#each budgets as budget}
|
|
||||||
<li>
|
|
||||||
<a href={`/budget/${budget.id}`}>
|
|
||||||
{budget.name} ({budget.sum})
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
<li><div class="divider">Accounts</div></li>
|
|
||||||
{#each data.accounts as account}
|
|
||||||
<li>
|
|
||||||
<a href={`/account/${account.id}`}>
|
|
||||||
{account.name} ({account.balance})
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
<li><div class="divider"></div></li>
|
|
||||||
<li><a href="/rules">Rules</a></li>
|
|
||||||
<li><a href="/settings">Settings</a></li>
|
|
||||||
<li><a href="/united">United</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toast toast-top toast-center">
|
|
||||||
{#each toast as t}
|
|
||||||
<div class="alert alert-{t.type}">
|
|
||||||
<span>{t.message}</span>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
@ -1,256 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { EditSymbol } from '$lib/editSymbol.svelte';
|
|
||||||
import { settingsSymbol } from '$lib/settingsSymbol.svelte';
|
|
||||||
import { invalidate, invalidateAll } from '$app/navigation';
|
|
||||||
import { loadingModal } from '$lib/loadingModal.svelte';
|
|
||||||
import { getContext } from 'svelte';
|
|
||||||
|
|
||||||
let { data } = $props();
|
|
||||||
const addToast = getContext('addToast');
|
|
||||||
let trans = $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);
|
|
||||||
|
|
||||||
function editNotes(transaction, remaining) {
|
|
||||||
my_modal_3.showModal();
|
|
||||||
currentTransaction = transaction;
|
|
||||||
notes = transaction.notes;
|
|
||||||
currentTransaction.amount = remaining;
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
let res = await fetch(`/api/account/${account.id}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
hide: $state.snapshot(hide),
|
|
||||||
in_total: $state.snapshot(inTotal)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
loading = false;
|
|
||||||
invalidateAll();
|
|
||||||
if (res.ok) {
|
|
||||||
// Optionally, you can refresh the account data or show a success message
|
|
||||||
} else {
|
|
||||||
console.error('Failed to save settings');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex mb-4">
|
|
||||||
<div class="w-128 flex-none justify-bottom">
|
|
||||||
<h1 class="text-lg font-semibold">{account?.name}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="w-64 grow text-lg uppercase font-semibold">{account?.balance}</div>
|
|
||||||
<div class="w-14 flex-none text-right">
|
|
||||||
<button class="btn btn-square btn-ghost" onclick={() => settings_modal.showModal()}
|
|
||||||
>{@render settingsSymbol()}
|
|
||||||
</button>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<dialog id="settings_modal" 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>
|
|
||||||
<h3>{account?.name}</h3>
|
|
||||||
<fieldset class="fieldset bg-base-100 border-base-300 rounded-box w-64 border p-4">
|
|
||||||
<legend class="fieldset-legend">Options</legend>
|
|
||||||
<label class="label">
|
|
||||||
<input type="checkbox" bind:checked={hide} class="toggle" />
|
|
||||||
Hide
|
|
||||||
</label>
|
|
||||||
<label class="label">
|
|
||||||
<input type="checkbox" bind:checked={inTotal} class="toggle" />
|
|
||||||
Use in total
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
<button onclick={() => saveSettings()} class="btn btn-primary mt-4">Save</button>
|
|
||||||
</div>
|
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button>close</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
{#if loading}
|
|
||||||
{@render loadingModal()}
|
|
||||||
{/if}
|
|
||||||
BIN
static/apple-touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 282 B |
BIN
static/favicon.ico
Normal file
|
After Width: | Height: | Size: 277 B |
@ -1 +1,21 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="144"
|
||||||
|
height="144"
|
||||||
|
viewBox="0 0 38.099999 38.1"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
style="fill:#5599ff;stroke:none;stroke-width:1.05833"
|
||||||
|
d="M 14.365039,11.959191 H 1.9180591 L 2.0404884,3.5912594 35.259638,3.672879 35.300448,12 22.93509,12.04081 l 0.08162,23.163238 -8.753695,0.06121 z"
|
||||||
|
id="path1" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 606 B |
BIN
static/maskable-icon-512x512.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/pwa-192x192.png
Normal file
|
After Width: | Height: | Size: 344 B |
BIN
static/pwa-512x512.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/pwa-64x64.png
Normal file
|
After Width: | Height: | Size: 217 B |
@ -4,5 +4,9 @@ import tailwindcss from '@tailwindcss/vite';
|
|||||||
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
|
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss(), sveltekit(), SvelteKitPWA()]
|
plugins: [tailwindcss(), sveltekit(), SvelteKitPWA({registerType: 'autoUpdate', pwaAssets: { config: 'pwa-assets.config.js' }})],
|
||||||
|
server: {
|
||||||
|
allowedHosts: ['budget.caseytimm.com']
|
||||||
|
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||