Compare commits

...

6 Commits

6 changed files with 180 additions and 6 deletions

21
package-lock.json generated
View File

@ -7,6 +7,9 @@
"": { "": {
"name": "fin-check-front", "name": "fin-check-front",
"version": "0.0.1", "version": "0.0.1",
"dependencies": {
"chart.js": "^4.4.6"
},
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.0.0",
@ -511,6 +514,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1213,6 +1222,18 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/chart.js": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz",
"integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",

View File

@ -22,5 +22,8 @@
"tailwindcss": "^3.4.9", "tailwindcss": "^3.4.9",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vite": "^5.0.3" "vite": "^5.0.3"
},
"dependencies": {
"chart.js": "^4.4.6"
} }
} }

View File

@ -137,6 +137,17 @@ export interface Currency {
symbol: string; symbol: string;
} }
export interface StatsType {
value: number;
name: string;
color: string;
}
export interface StatsTypeCurrencyChart {
label: string;
elements: StatsType[];
}
export const EntityTypes = { export const EntityTypes = {
card: "Card", card: "Card",
type: "Type", type: "Type",
@ -148,6 +159,9 @@ export const EntityTypes = {
metric: "Metric", metric: "Metric",
currency: "Currency", currency: "Currency",
expense_bulk: "ExpenseBulk", expense_bulk: "ExpenseBulk",
// I don't know if I should put Statistics interfaces to Entities
stats_type: "StatsType",
stats_type_currency_chart: "StatsTypeCurrencyChart",
} as const; } as const;
export type EntityName = keyof typeof EntityTypes; export type EntityName = keyof typeof EntityTypes;
@ -162,6 +176,8 @@ export type EntityType<T extends EntityName> =
T extends "metric" ? Metric : T extends "metric" ? Metric :
T extends "currency" ? Currency : T extends "currency" ? Currency :
T extends "expense_bulk" ? ExpenseBulk : T extends "expense_bulk" ? ExpenseBulk :
T extends "stats_type" ? StatsType :
T extends "stats_type_currency_chart" ? StatsTypeCurrencyChart :
never; never;
// //
@ -357,3 +373,31 @@ export async function filter<F, R>(groupName: string, data: F, session?: string)
return { message: error.message }; return { message: error.message };
} }
} }
export async function get_stats_for<R>(groupName: string, session?: string): Promise<R | ErrorMessage> {
const url = `${BASE_API_URL}/statistics/${groupName}`
const defaultHeaders = {
'Content-Type': 'application/json',
};
const headers = session
? { ...defaultHeaders, Cookie: `session=${session}` }
: defaultHeaders
const config: RequestInit = {
method: 'GET',
headers,
};
try {
const response = await fetch(url, config);
if (!response.ok) {
const body = await response.json()
throw new Error(`Failed to update ${groupName}: ${body.message}`);
}
return await response.json() as R;
} catch (err) {
const error = err as Error
return { message: error.message };
}
}

View File

@ -1 +1,85 @@
<p>Hello, World!</p> <script lang="ts">
import { onMount, onDestroy } from "svelte";
import {
Chart,
DoughnutController,
ArcElement,
Tooltip,
Legend,
type ChartConfiguration,
} from "chart.js";
import type { StatsTypeCurrencyChart } from "$lib/entities";
Chart.register(DoughnutController, ArcElement, Tooltip, Legend);
let error: string | null = null;
let data: StatsTypeCurrencyChart[] = [];
let configs: ChartConfiguration[] = [];
let charts: Chart[] = [];
let canvases: HTMLCanvasElement[] = [];
async function fetchChartStats() {
try {
const result = await fetch("/api/statistics/type");
if (!result.ok) {
const obj = await result.json();
error = obj.message;
} else {
data = await result.json();
configs = data.map((chart) => ({
type: "doughnut",
data: {
labels: chart.elements.map((type) => type.name),
datasets: [
{
label: "Chart Dataset",
data: chart.elements.map((type) => type.value / 100),
backgroundColor: chart.elements.map((type) => type.color),
},
],
},
}));
}
} catch (e) {
console.error("Error fetching data:", e);
error = "Failed to fetch chart data.";
}
}
function createTypeCharts() {
charts.forEach((chart) => chart.destroy());
charts = configs.map((config, index) => new Chart(canvases[index], config));
}
onMount(async () => {
await fetchChartStats();
createTypeCharts();
});
onDestroy(() => {
charts.forEach((chart) => chart.destroy());
});
</script>
{#if error}
<p class="bg-red-100 text-red-700 p-4 rounded">{error}</p>
{/if}
<!-- Render canvas elements for charts -->
<div class="w-1/2 grid grid-cols-2 gap-4">
{#each configs as _, index}
<div class="flex flex-col">
<div class="type-chart">
<canvas bind:this={canvases[index]}></canvas>
<span class="flex justify-center">{data[index].label}</span>
</div>
</div>
{/each}
</div>
<style lang="postcss">
.type-chart {
width: 20vw;
height: 20vw;
}
</style>

View File

@ -0,0 +1,27 @@
import type { ErrorMessage } from "$lib/api";
import { get_stats_for, type StatsTypeCurrencyChart } from "$lib/entities";
import type { RequestHandler } from "./$types";
function isErrorMessage(value: any): value is ErrorMessage {
return value && typeof value.message === 'string';
}
export const GET: RequestHandler = async ({ cookies }): Promise<Response> => {
const session = cookies.get('session');
// const queryParams = url.searchParams.toString();
// Check if the entity is valid
if (!session) {
return new Response(JSON.stringify("no cookies"), { status: 401 });
}
// TypeScript type inference for entity
const result = await get_stats_for<StatsTypeCurrencyChart[]>("type", session);
if (isErrorMessage(result)) {
console.log("ERROR");
return new Response(JSON.stringify(result), { status: 500 });
}
return new Response(JSON.stringify(result), { status: 200 });
}

View File

@ -117,11 +117,6 @@
} }
// Helper function to get the name of the parent category // 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";
}
function getTypeColor(typeId: number) { function getTypeColor(typeId: number) {
if (typeId === 0) return "None"; if (typeId === 0) return "None";
const type = types.find((card) => card.id === typeId); const type = types.find((card) => card.id === typeId);