Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unique spendings, installment spendings and recurring spendings #59

Merged
merged 2 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions my-app/src/lib/server/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type Opts = {
};

function stripTrailingSlash(url: string): string {
return url.endsWith('/') ? stripTrailingSlash(url.slice(0, -1)) : url;
return url?.endsWith('/') ? stripTrailingSlash(url.slice(0, -1)) : url;
}

const base = stripTrailingSlash(env.VITE_API_URL);
Expand Down Expand Up @@ -78,12 +78,29 @@ export const groupService = {
post(`group/${id}/member`, { user_identifier }, getAuthHeader(cookies))
};
export const spendingService = {
save: (data: Spending, cookies: Cookies) =>
list: (groupId: Id, cookies: Cookies) => get(`group/${groupId}/spending`, getAuthHeader(cookies)),

saveUniqueSpending: (data: UniqueSpending, cookies: Cookies) =>
post('unique-spending', data, getAuthHeader(cookies)),
listUniqueSpendings: (groupId: Id, cookies: Cookies) =>
get(`group/${groupId}/unique-spending`, getAuthHeader(cookies)),

saveInstallmentSpending: (data: InstallmentSpending, cookies: Cookies) =>
post('installment-spending', data, getAuthHeader(cookies)),
listInstallmentSpendings: (groupId: Id, cookies: Cookies) =>
get(`group/${groupId}/installment-spending`, getAuthHeader(cookies)),

saveRecurringSpending: (data: RecurringSpending, cookies: Cookies) =>
data.id > 0
? put(`spending/${data.id}`, data, getAuthHeader(cookies))
: post('spending', data, getAuthHeader(cookies)),
list: (groupId: Id, cookies: Cookies) => get(`group/${groupId}/spending`, getAuthHeader(cookies))
? put(`recurring-spending/${data.id}`, data, getAuthHeader(cookies))
: post('recurring-spending', data, getAuthHeader(cookies)),
listRecurringSpendings: (groupId: Id, cookies: Cookies) =>
get(`group/${groupId}/recurring-spending`, getAuthHeader(cookies)),

listAllSpendings: (groupId: Id, cookies: Cookies) =>
get(`group/${groupId}/spending`, getAuthHeader(cookies))
};

export const paymentService = {
save: (data: Payment, cookies: Cookies) =>
data.id > 0
Expand Down
4 changes: 3 additions & 1 deletion my-app/src/routes/groups/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
<summary role="button">Opciones</summary>
<ul>
<li><a href="/groups/details">Añadir grupo</a></li>
<li><a href="/spendings/details">Añadir gasto</a></li>
<li><a href="/unique_spendings/details">Añadir gasto unico</a></li>
<li><a href="/installment_spendings/details">Añadir gasto en cuotas</a></li>
<li><a href="/recurring_spendings/details">Añadir gasto recurrente</a></li>
<li><a href="/budgets/details">Añadir presupuesto</a></li>
<li><a href="/categories/details">Añadir categoría</a></li>
</ul>
Expand Down
9 changes: 8 additions & 1 deletion my-app/src/routes/groups/movements/[id=integer]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,14 @@
<!-- svelte-ignore a11y-no-redundant-roles -->
<summary role="button">Añadir</summary>
<ul>
<li><a href="/spendings/details?groupId={data.group.id}">Añadir gasto</a></li>
<li><a href="/unique_spendings/details?groupId={data.group.id}">Añadir gasto unico</a></li>
<li>
<a href="/installment_spendings/details?groupId={data.group.id}">Añadir gasto en cuotas</a
>
</li>
<li>
<a href="/recurring_spendings/details?groupId={data.group.id}">Añadir gasto recurrente</a>
</li>
<li><a href="/budgets/details?groupId={data.group.id}">Añadir presupuesto</a></li>
<li><a href="/categories/details?groupId={data.group.id}">Añadir categoría</a></li>
<li><a href="/payments/details?groupId={data.group.id}">Añadir pago</a></li>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { error, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { groupService, spendingService } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { fixDateString } from '$lib/formatter';
import { getUserId } from '$lib/auth';

export const load: PageServerLoad = async ({ params, url, cookies }) => {
const group_id = Number(url.searchParams.get('groupId'));
const id = Number(params.id) || 0;
const spending: InstallmentSpending = {
id,
description: '',
amount: 0,
owner_id: 0,
date: new Date().toJSON(),
category_id: 0,
amount_of_installments: 0,
group_id
};
const groups: Group[] = await groupService.list(cookies);
return { spending, groups };
};

export const actions: Actions = {
default: async ({ cookies, request, params }) => {
const id = Number(params.id) || 0;
const data = await request.formData();
const description = data.get('description')?.toString();
const amount = Number(data.get('amount'));
const dateString = data.get('date')?.toString();
const group_id = Number(data.get('groupId'));
const category_id = Number(data.get('categoryId'));
const amount_of_installments = Number(data.get('amountOfInstallments'));
const owner_id = getUserId(cookies);

if (!description) {
throw error(400, 'Description is required');
}
if (Number.isNaN(amount)) {
throw error(400, 'Amount is required');
}
if (!dateString) {
throw error(400, 'Date is required');
}

const timezoneOffset = Number(data.get('timezoneOffset')) || 0;
const date = fixDateString(dateString, timezoneOffset);
const spending: InstallmentSpending = {
id,
amount,
description,
date,
group_id,
category_id,
amount_of_installments,
owner_id
};
try {
await spendingService.saveInstallmentSpending(spending, cookies);
} catch {
return { success: false };
}

redirect(302, `/groups/movements/${group_id}`);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<script lang="ts">
import { title } from '$lib';
import { onMount } from 'svelte';
import type { PageServerData } from './$types';
import { fixDateString, formatMoney } from '$lib/formatter';

export let data: PageServerData;
let timezoneOffset = 0;
let suggestions: Map<string, Spending> = new Map();
let categories: Category[] = [];

async function onGroupUpdate(groupId: number) {
await Promise.all([updateSuggestions(groupId), updateCategories(groupId)]);
}

async function updateSuggestions(groupId: number) {
const newSuggestions: Map<string, Spending> = new Map();
if (groupId != 0) {
try {
const response = await fetch(`/api/spendings?groupId=${groupId}`);
const body: Spending[] = await response.json();
body.forEach((spending) => {
newSuggestions.set(spending.description, spending);
});
} catch {}
}
suggestions = newSuggestions;
}

function autocomplete(value: string) {
const spending = suggestions.get(value);
if (!spending) return;
data.spending.amount = spending.amount;
data.spending.description = spending.description;
data.spending.category_id = spending.category_id;
}

async function updateCategories(groupId: number) {
if (groupId != 0) {
try {
const response = await fetch(`/api/categories?groupId=${groupId}`);
categories = await response.json();
return;
} catch {}
}
categories = [];
}

onMount(async () => {
timezoneOffset = new Date().getTimezoneOffset();
data.spending.date = fixDateString(data.spending.date, timezoneOffset).slice(0, 16);
await onGroupUpdate(data.spending.group_id);
});
</script>

<svelte:head>
<title>{title} - Nuevo Gasto</title>
</svelte:head>

<nav aria-label="breadcrumb">
<ul>
<li><a href="/groups">Grupos</a></li>
<li>Gastos</li>
</ul>
</nav>

<h2>Nuevo Gasto en Cuotas</h2>
<form method="POST" autocomplete="off">
<fieldset>
<input type="hidden" name="timezoneOffset" value={timezoneOffset} required />
<label>
Ingrese el grupo al que pertenece el gasto en cuotas
<select
name="groupId"
required
value={data.spending.group_id}
on:change={(e) => onGroupUpdate(+e.currentTarget.value)}
>
{#each data.groups as group}
<option value={group.id}>{group.name}</option>
{/each}
</select>
</label>
<label>
Ingrese la categoría a la que pertenece el gasto en cuotas
<select name="categoryId" required value={data.spending.category_id}>
{#each categories as category}
<option value={category.id}>{category.name}</option>
{/each}
</select>
</label>
<label>
Ingrese una descripción para el gasto en cuotas
<input
type="text"
name="description"
placeholder="Descripción"
list="description-list"
required
value={data.spending.description}
on:change={(e) => autocomplete(e.currentTarget.value)}
/>
<datalist id="description-list">
{#each suggestions.values() as spending}
<option value={spending.description}>{formatMoney(spending.amount)}</option>
{/each}
</datalist>
</label>
<label>
Ingrese un el monto de la cuota
<input type="text" name="amount" placeholder="Monto" required value={data.spending.amount} />
</label>
<label>
Ingrese la cantidad de cuotas
<input
type="text"
name="amountOfInstallments"
placeholder="Cuotas"
required
value={data.spending.amount_of_installments}
/>
</label>
<label>
Fecha del gasto en cuotas
<input
type="datetime-local"
name="date"
placeholder="Fecha"
required
readonly
value={data.spending.date}
/>
</label>
<button>Crear</button>
<button type="button" class="outline" on:click={() => history.back()}>Cancelar</button>
</fieldset>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { error, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { groupService, spendingService } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { fixDateString } from '$lib/formatter';
import { getUserId } from '$lib/auth';

export const load: PageServerLoad = async ({ params, url, cookies }) => {
const group_id = Number(url.searchParams.get('groupId'));
const id = Number(params.id) || 0;
const spending: RecurringSpending = {
id,
description: '',
amount: 0,
owner_id: 0,
date: new Date().toJSON(),
category_id: 0,
group_id
};
const groups: Group[] = await groupService.list(cookies);
return { spending, groups };
};

export const actions: Actions = {
default: async ({ cookies, request, params }) => {
const id = Number(params.id) || 0;
const data = await request.formData();
const description = data.get('description')?.toString();
const amount = Number(data.get('amount'));
const dateString = data.get('date')?.toString();
const group_id = Number(data.get('groupId'));
const category_id = Number(data.get('categoryId'));
const owner_id = getUserId(cookies);

if (!description) {
throw error(400, 'Description is required');
}
if (Number.isNaN(amount)) {
throw error(400, 'Amount is required');
}
if (!dateString) {
throw error(400, 'Date is required');
}

const timezoneOffset = Number(data.get('timezoneOffset')) || 0;
const date = fixDateString(dateString, timezoneOffset);
const spending: RecurringSpending = {
id,
amount,
description,
date,
group_id,
category_id,
owner_id
};
try {
await spendingService.saveRecurringSpending(spending, cookies);
} catch {
return { success: false };
}

redirect(302, `/groups/movements/${group_id}`);
}
};
Loading