Compare commits

..

8 Commits

7 changed files with 342 additions and 30 deletions

View File

@ -35,6 +35,14 @@ export interface Expense {
date: string; date: string;
} }
export interface Income {
id: number;
card_id: number;
value: number;
comment: string;
date: string;
}
export interface Transfer { export interface Transfer {
id: number; id: number;
from_card_id: number; from_card_id: number;
@ -48,6 +56,7 @@ export const EntityTypes = {
type: "Type", type: "Type",
category: "Category", category: "Category",
expense: "Expense", expense: "Expense",
income: "Income",
transfer: "Transfer", transfer: "Transfer",
} as const; } as const;
@ -57,6 +66,7 @@ export type EntityType<T extends EntityName> =
T extends "type" ? Type : T extends "type" ? Type :
T extends "category" ? Category : T extends "category" ? Category :
T extends "expense" ? Expense : T extends "expense" ? Expense :
T extends "income" ? Income :
T extends "transfer" ? Transfer : T extends "transfer" ? Transfer :
never; never;

5
src/lib/util/fpa.ts Normal file
View File

@ -0,0 +1,5 @@
export const NumberToFPA = (value: number): string => {
return (value / 100).toFixed(2);
}

View File

@ -7,18 +7,6 @@
import UserIcon from '$lib/icons/UserIcon.svelte'; import UserIcon from '$lib/icons/UserIcon.svelte';
const paths = [ const paths = [
{
Path: "/ping",
Name: "ping",
},
{
Path: "/test",
Name: "test",
},
{
Path: "/test2",
Name: "test2",
},
{ {
Path: "/card", Path: "/card",
Name: "Cards", Name: "Cards",
@ -36,6 +24,9 @@
Name: "Expense", Name: "Expense",
}, },
{ {
Path: "/income",
Name: "Income",
},
Path: "/transfer", Path: "/transfer",
Name: "Transfer", Name: "Transfer",
} }

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { type Card } from "$lib/entities"; import { type Card } from "$lib/entities";
import { NumberToFPA } from "$lib/util/fpa";
let cards: Card[] = $state([]); let cards: Card[] = $state([]);
let error: string | null = $state(null); let error: string | null = $state(null);
@ -57,6 +58,12 @@
function editCard(card: Card) { function editCard(card: Card) {
editingCard = { ...card }; editingCard = { ...card };
if (balanceRef) {
balanceRef.value = NumberToFPA(card.balance);
}
if (creditLineRef) {
creditLineRef.value = NumberToFPA(card.credit_line);
}
} }
async function deleteCard(id: number) { async function deleteCard(id: number) {
@ -73,7 +80,27 @@
} }
} }
let balanceRef: HTMLInputElement | null = $state(null);
let creditLineRef: HTMLInputElement | null = $state(null);
function handleBalanceInput(
event: Event & { currentTarget: EventTarget & HTMLInputElement },
): void {
const target = event.target as HTMLInputElement;
const rawValue = target.value.replace(/[^0-9]/g, "");
currentCard.balance = parseInt(rawValue || "0");
target.value = NumberToFPA(currentCard.balance);
}
function handleCreditLineInput(
event: Event & { currentTarget: EventTarget & HTMLInputElement },
): void {
const target = event.target as HTMLInputElement;
const rawValue = target.value.replace(/[^0-9]/g, "");
currentCard.credit_line = parseInt(rawValue || "0");
target.value = NumberToFPA(currentCard.credit_line);
}
const currentCard = $derived(editingCard ?? newCard); const currentCard = $derived(editingCard ?? newCard);
$inspect("currentCard = ", currentCard);
</script> </script>
{#if error} {#if error}
@ -100,22 +127,13 @@
<label class="block"> <label class="block">
<span class="text-gray-700">Balance:</span> <span class="text-gray-700">Balance:</span>
<input <input
type="number" type="text"
bind:value={currentCard.balance} oninput={handleBalanceInput}
bind:this={balanceRef}
required required
class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md focus:ring focus:ring-indigo-200 focus:border-indigo-500" class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md focus:ring focus:ring-indigo-200 focus:border-indigo-500"
/> />
</label> </label>
{#if currentCard.have_credit_line}
<label class="block">
<span class="text-gray-700">Credit Line:</span>
<input
type="number"
bind:value={currentCard.credit_line}
class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md focus:ring focus:ring-indigo-200 focus:border-indigo-500"
/>
</label>
{/if}
<label class="flex items-center"> <label class="flex items-center">
<input <input
type="checkbox" type="checkbox"
@ -124,6 +142,17 @@
/> />
<span class="ml-2 text-gray-700">Have Credit Line</span> <span class="ml-2 text-gray-700">Have Credit Line</span>
</label> </label>
{#if currentCard.have_credit_line}
<label class="block">
<span class="text-gray-700">Credit Line:</span>
<input
type="number"
oninput={handleCreditLineInput}
bind:this={creditLineRef}
class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md focus:ring focus:ring-indigo-200 focus:border-indigo-500"
/>
</label>
{/if}
<div class="flex space-x-2"> <div class="flex space-x-2">
<button <button
type="submit" type="submit"
@ -153,8 +182,14 @@
<div> <div>
<strong class="block text-lg">{card.name}</strong> <strong class="block text-lg">{card.name}</strong>
<div class="text-sm text-gray-600"> <div class="text-sm text-gray-600">
Balance: {card.balance}, Credit Line: {card.credit_line},{" "} <span>
Have Credit Line: {card.have_credit_line ? "Yes" : "No"} Balance: {NumberToFPA(card.balance)},
</span>
{#if card.have_credit_line}
Credit Line: {NumberToFPA(card.credit_line)}
{:else}
No Credit Line
{/if}
</div> </div>
</div> </div>
<div class="flex space-x-2"> <div class="flex space-x-2">

View File

@ -2,6 +2,7 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { type Card, type Expense, type Type } from "$lib/entities"; import { type Card, type Expense, type Type } from "$lib/entities";
import { selectedDate } from "$lib/stores/dateStore"; import { selectedDate } from "$lib/stores/dateStore";
import { NumberToFPA } from "$lib/util/fpa";
let mutateDate = $state($selectedDate); let mutateDate = $state($selectedDate);
let selectedTime = $state("00:00:00"); let selectedTime = $state("00:00:00");
@ -92,6 +93,9 @@
const parts = expense.date.split("T"); const parts = expense.date.split("T");
mutateDate = parts[0]; mutateDate = parts[0];
selectedTime = parts[1].split("Z")[0]; selectedTime = parts[1].split("Z")[0];
if (valueRef) {
valueRef.value = NumberToFPA(expense.value);
}
} }
async function deleteExpense(id: number) { async function deleteExpense(id: number) {
@ -125,6 +129,16 @@
return type ? type.name : "Unknown"; return type ? type.name : "Unknown";
} }
let valueRef: HTMLInputElement | null = $state(null);
function handleValueInput(
event: Event & { currentTarget: EventTarget & HTMLInputElement },
): void {
const target = event.target as HTMLInputElement;
const rawValue = target.value.replace(/[^0-9]/g, "");
currentExpense.value = parseInt(rawValue || "0");
target.value = NumberToFPA(currentExpense.value);
}
const constructedTime = $derived(`${mutateDate}T${selectedTime}Z`); const constructedTime = $derived(`${mutateDate}T${selectedTime}Z`);
const currentExpense = $derived(editingExpense ?? newExpense); const currentExpense = $derived(editingExpense ?? newExpense);
const selectedType = $derived( const selectedType = $derived(
@ -152,7 +166,8 @@
<span class="text-gray-700">Value:</span> <span class="text-gray-700">Value:</span>
<input <input
type="text" type="text"
bind:value={currentExpense.value} oninput={handleValueInput}
bind:this={valueRef}
required required
class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md focus:ring focus:ring-indigo-200 focus:border-indigo-500" class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md focus:ring focus:ring-indigo-200 focus:border-indigo-500"
/> />
@ -230,7 +245,7 @@
class="bg-white p-4 rounded-lg shadow-md flex justify-between items-center" class="bg-white p-4 rounded-lg shadow-md flex justify-between items-center"
> >
<div> <div>
<strong class="block text-lg">{expense.value}</strong> <strong class="block text-lg">{NumberToFPA(expense.value)}</strong>
<div class="text-sm text-gray-600"> <div class="text-sm text-gray-600">
<span class="font-bold">Card:</span> <span class="font-bold">Card:</span>
{getCardName(expense.card_id)} {getCardName(expense.card_id)}

View File

@ -0,0 +1,245 @@
<script lang="ts">
import { onMount } from "svelte";
import { type Card, type Income, type Type } from "$lib/entities";
import { selectedDate } from "$lib/stores/dateStore";
import { NumberToFPA } from "$lib/util/fpa";
let mutateDate = $state($selectedDate);
let selectedTime = $state("00:00:00");
let incomes: Income[] = $state([]);
let cards: Card[] = $state([]);
let types: Type[] = $state([]);
let error: string | null = $state(null);
let editingIncome: Income | null = $state(null);
let newIncome: Partial<Income> = $state({
card_id: 0,
value: 0,
comment: "",
date: "2006-01-02T15:04:05Z",
});
onMount(async () => {
await fetchCategories();
await fetchCards();
await fetchTypes();
});
async function fetchTypes() {
const result = await fetch("/api/type/all");
if (!result.ok) {
const obj = await result.json();
error = obj.message;
} else {
types = await result.json();
}
}
async function fetchCards() {
const result = await fetch("/api/card/all");
if (!result.ok) {
const obj = await result.json();
error = obj.message;
} else {
cards = await result.json();
}
}
async function fetchCategories() {
const result = await fetch("/api/income/all");
if (!result.ok) {
const obj = await result.json();
error = obj.message;
} else {
incomes = await result.json();
}
}
async function saveIncome() {
const endpoint = editingIncome
? `/api/income/update`
: "/api/income/create";
const method = editingIncome ? "PUT" : "POST";
const data = {
...currentIncome,
card_id: Number(currentIncome.card_id),
value: Number(currentIncome.value),
date: constructedTime,
};
const response = await fetch(endpoint, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!response.ok) {
const { message } = await response.json();
error = message;
} else {
await fetchCategories();
if (editingIncome) {
editingIncome = null;
} else {
newIncome = {
card_id: 0,
value: 0,
comment: "",
date: "",
};
}
}
}
function editIncome(income: Income) {
editingIncome = { ...income };
const parts = income.date.split("T");
mutateDate = parts[0];
selectedTime = parts[1].split("Z")[0];
if (valueRef) {
valueRef.value = NumberToFPA(income.value);
}
}
async function deleteIncome(id: number) {
const response = await fetch(`/api/income/delete`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
});
if (!response.ok) {
const { message } = await response.json();
error = message;
} else {
await fetchCategories();
}
}
// Helper function to get the name of the parent category
function getCardName(cardId: number) {
if (cardId === 0) return "None";
const card = cards.find((card) => card.id === cardId);
return card ? card.name : "Unknown";
}
let valueRef: HTMLInputElement | null = $state(null);
function handleValueInput(
event: Event & { currentTarget: EventTarget & HTMLInputElement },
): void {
const target = event.target as HTMLInputElement;
const rawValue = target.value.replace(/[^0-9]/g, "");
currentIncome.value = parseInt(rawValue || "0");
target.value = NumberToFPA(currentIncome.value);
}
const constructedTime = $derived(`${mutateDate}T${selectedTime}Z`);
const currentIncome = $derived(editingIncome ?? newIncome);
$inspect(currentIncome);
$inspect("constructedTime = ", constructedTime);
</script>
{#if error}
<p class="bg-red-100 text-red-700 p-4 rounded">{error}</p>
{/if}
<div class="container mx-auto my-8 p-6 bg-gray-100 rounded-lg shadow-lg">
<h1 class="text-2xl font-bold mb-4 text-center">Manage Incomes</h1>
<div class="mb-8 p-6 bg-white rounded-lg shadow-md">
<h2 class="text-xl font-semibold mb-4">
{editingIncome ? "Edit Income" : "Add New Income"}
</h2>
<form onsubmit={saveIncome} class="space-y-4">
<label class="block">
<span class="text-gray-700">Value:</span>
<input
type="text"
oninput={handleValueInput}
bind:this={valueRef}
required
class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md focus:ring focus:ring-indigo-200 focus:border-indigo-500"
/>
</label>
<label class="block">
<span class="text-gray-700">Card:</span>
<select
bind:value={currentIncome.card_id}
class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md focus:ring focus:ring-indigo-200 focus:border-indigo-500"
>
{#each cards as card}
<option value={card.id}>{card.name}</option>
{/each}
</select>
</label>
<div class="flex items-center space-x-4">
<label>
Date:
<input
type="date"
bind:value={mutateDate}
class="bg-gray-800 text-white p-2 rounded-md border border-gray-700"
/>
</label>
<label>
Time:
<input
type="time"
bind:value={selectedTime}
class="bg-gray-800 text-white p-2 rounded-md border border-gray-700"
/>
</label>
</div>
<div class="flex space-x-2">
<button
type="submit"
class="px-4 py-2 bg-indigo-500 text-white rounded-md hover:bg-indigo-600"
>
{editingIncome ? "Update Income" : "Add Income"}
</button>
{#if editingIncome}
<button
type="button"
onclick={() => (editingIncome = null)}
class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400"
>
Cancel
</button>
{/if}
</div>
</form>
</div>
<h2 class="text-xl font-semibold mb-4 text-center">Incomes List</h2>
<ul class="space-y-4">
{#each incomes as income}
<li
class="bg-white p-4 rounded-lg shadow-md flex justify-between items-center"
>
<div>
<strong class="block text-lg">{NumberToFPA(income.value)}</strong>
<div class="text-sm text-gray-600">
<span class="font-bold">Card:</span>
{getCardName(income.card_id)}
<span class="font-bold">Date:</span>
{income.date}
</div>
</div>
<div class="flex space-x-2">
<button
onclick={() => editIncome(income)}
class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
>
Edit
</button>
<button
onclick={() => deleteIncome(income.id)}
class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600"
>
Delete
</button>
</div>
</li>
{/each}
</ul>
</div>

View File

@ -2,6 +2,7 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { type Card, type Transfer } from "$lib/entities"; import { type Card, type Transfer } from "$lib/entities";
import { selectedDate } from "$lib/stores/dateStore"; import { selectedDate } from "$lib/stores/dateStore";
import { NumberToFPA } from "$lib/util/fpa";
let mutateDate = $state($selectedDate); let mutateDate = $state($selectedDate);
let selectedTime = $state("00:00:00"); let selectedTime = $state("00:00:00");
@ -29,6 +30,7 @@
cards = await result.json(); cards = await result.json();
} }
} }
async function fetchCategories() { async function fetchCategories() {
const result = await fetch("/api/transfer/all"); const result = await fetch("/api/transfer/all");
if (!result.ok) { if (!result.ok) {
@ -104,6 +106,15 @@
return card ? card.name : "Unknown"; return card ? card.name : "Unknown";
} }
function handleValueInput(
event: Event & { currentTarget: EventTarget & HTMLInputElement },
): void {
const target = event.target as HTMLInputElement;
const rawValue = target.value.replace(/[^0-9]/g, "");
currentTransfer.value = parseInt(rawValue || "0");
target.value = NumberToFPA(currentTransfer.value);
}
const constructedTime = $derived(`${mutateDate}T${selectedTime}Z`); const constructedTime = $derived(`${mutateDate}T${selectedTime}Z`);
const currentTransfer = $derived(editingTransfer ?? newTransfer); const currentTransfer = $derived(editingTransfer ?? newTransfer);
$inspect(currentTransfer); $inspect(currentTransfer);
@ -154,7 +165,7 @@
<span class="text-gray-700">Value:</span> <span class="text-gray-700">Value:</span>
<input <input
type="text" type="text"
bind:value={currentTransfer.value} oninput={handleValueInput}
required required
class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md focus:ring focus:ring-indigo-200 focus:border-indigo-500" class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md focus:ring focus:ring-indigo-200 focus:border-indigo-500"
/> />
@ -207,7 +218,7 @@
class="bg-white p-4 rounded-lg shadow-md flex justify-between items-center" class="bg-white p-4 rounded-lg shadow-md flex justify-between items-center"
> >
<div> <div>
<strong class="block text-lg">{transfer.value}</strong> <strong class="block text-lg">{NumberToFPA(transfer.value)}</strong>
<div class="text-sm text-gray-600"> <div class="text-sm text-gray-600">
<span class="font-bold">From:</span> <span class="font-bold">From:</span>
{getCardName(transfer.from_card_id)} {getCardName(transfer.from_card_id)}