summaryrefslogtreecommitdiff
path: root/ping/frontend/src/lib/components/dashboard
diff options
context:
space:
mode:
Diffstat (limited to 'ping/frontend/src/lib/components/dashboard')
-rw-r--r--ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte10
-rw-r--r--ping/frontend/src/lib/components/dashboard/StockGraph.svelte136
-rw-r--r--ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte85
-rw-r--r--ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte165
4 files changed, 396 insertions, 0 deletions
diff --git a/ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte b/ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte
new file mode 100644
index 0000000..3844abc
--- /dev/null
+++ b/ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte
@@ -0,0 +1,10 @@
+<h2>Analyse de risque</h2>
+<i>tkt t safe c hardcodé chef</i>
+
+<style>
+ h2 {
+ font-weight: bold;
+ color: var(--text-lime);
+ font-size: 24px;
+ }
+</style>
diff --git a/ping/frontend/src/lib/components/dashboard/StockGraph.svelte b/ping/frontend/src/lib/components/dashboard/StockGraph.svelte
new file mode 100644
index 0000000..beefed9
--- /dev/null
+++ b/ping/frontend/src/lib/components/dashboard/StockGraph.svelte
@@ -0,0 +1,136 @@
+<script lang="ts">
+ import Chart from 'chart.js/auto';
+ import { onMount } from 'svelte';
+ import StockSelector from '../input/StockSelector.svelte';
+
+ async function fetchChartData(stock: string, startDate: string, endDate: string) {
+ const res = await fetch(
+ `/stocksapi/chart?query=${stock}&startDate=${startDate}&endDate=${endDate}&interval=${range}`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-cache'
+ }
+ }
+ );
+ return await res.json();
+ }
+
+ function updateGraph() {
+ fetchChartData(selectedStock, startDate, endDate).then((data) => {
+ if (chart) {
+ chart.destroy();
+ chart = null;
+ }
+
+ const { quotes, meta } = data;
+
+ validRanges = meta.validRanges;
+
+ const labels = quotes.map((item: any) => item.date.split('T')[0]);
+ const datasets = ['low', 'high', 'open', 'close'].map((key) => ({
+ label: key.charAt(0).toUpperCase() + key.slice(1),
+ data: quotes.map((item: any) => item[key]),
+ borderColor:
+ key === 'low'
+ ? 'rgb(255, 99, 132)'
+ : key === 'high'
+ ? 'rgb(54, 162, 235)'
+ : key === 'open'
+ ? 'rgb(255, 205, 86)'
+ : 'rgb(75, 192, 192)',
+ fill: false
+ }));
+
+ // @ts-ignore
+ chart = new Chart(document.getElementById('stockgraph'), {
+ type: 'line',
+ data: {
+ labels,
+ datasets
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: true,
+ plugins: {
+ title: {
+ display: true,
+ text: meta.longName
+ }
+ }
+ }
+ });
+ });
+ }
+
+ let today = $state(new Date().toISOString().split('T')[0]);
+
+ let selectedStock = $state('2223.SR');
+ let startDate = $state('');
+ let endDate = $state('');
+ let chart = $state<Chart | null>(null);
+ let range = $state('1d');
+ let validRanges = $state(['1d']);
+
+ onMount(() => {
+ endDate = new Date().toISOString().split('T')[0];
+ startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
+
+ updateGraph();
+ });
+
+ $effect(() => {
+ if (selectedStock && startDate && endDate && range) {
+ updateGraph();
+ }
+ });
+
+ $effect(() => {
+ if (startDate && endDate && startDate > endDate) {
+ let temp = startDate;
+ startDate = endDate;
+ endDate = temp;
+ }
+ });
+</script>
+
+<div>
+ <div class="header">
+ <h2>Vue d'ensemble : {selectedStock}</h2>
+ <div class="controls">
+ <input type="date" name="startDate" id="startDate" bind:value={startDate} max={today} />
+ <input type="date" name="endDate" id="endDate" bind:value={endDate} max={today} />
+ <select name="validRanges" id="validRanges" bind:value={range}>
+ {#each validRanges as r}
+ <option value={r}>
+ {r}
+ </option>
+ {/each}
+ </select>
+
+ <StockSelector bind:selectedStock />
+ </div>
+ </div>
+ <canvas id="stockgraph"></canvas>
+</div>
+
+<style>
+ .header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ max-height: 512px !important;
+ }
+
+ h2 {
+ font-weight: bold;
+ color: var(--text-lime);
+ font-size: 24px;
+ }
+
+ #stockgraph {
+ width: 100% !important;
+ max-height: 512px !important;
+ }
+</style>
diff --git a/ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte b/ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte
new file mode 100644
index 0000000..3d947ad
--- /dev/null
+++ b/ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte
@@ -0,0 +1,85 @@
+<script lang="ts">
+ import { Chart } from 'chart.js';
+ import { onMount } from 'svelte';
+
+ function updateGraph() {
+ getTrendingSymbols().then((trendingSymbols) => {
+ const labels = trendingSymbols.map((d) => d.longName);
+
+ const datasets = [
+ {
+ label: '50 day performance',
+ data: trendingSymbols.map((ts) => ts.fiftyDayAverageChange),
+ backgroundColor: 'green'
+ },
+ {
+ label: '52 week performance',
+ data: trendingSymbols.map((ts) => ts.fiftyTwoWeekLowChange),
+ backgroundColor: 'orange'
+ }
+ ];
+
+ const data = { labels, datasets };
+
+ // @ts-ignore
+ new Chart(document.getElementById('graph'), {
+ type: 'bar',
+ data: data,
+ options: {
+ indexAxis: 'y',
+ elements: {
+ bar: {
+ borderWidth: 2
+ }
+ },
+ responsive: true,
+ plugins: {
+ legend: {
+ position: 'bottom'
+ }
+ }
+ }
+ });
+ });
+ }
+
+ async function getTrendingSymbols() {
+ const trendingRes = await fetch('/stocksapi/trendingSymbols');
+ if (!trendingRes.ok) {
+ throw new Error('Failed to fetch trending symbols');
+ }
+ const trendingJson = await trendingRes.json();
+
+ const { quotes } = trendingJson;
+
+ const trendingSymbols = await Promise.all(
+ quotes.map(async (quote: any) => {
+ const quoteRes = await fetch(`/stocksapi/quote?query=${quote.symbol}`);
+ const quoteJson = await quoteRes.json();
+ return quoteJson;
+ })
+ );
+
+ return trendingSymbols;
+ }
+
+ onMount(() => {
+ updateGraph();
+ });
+</script>
+
+<h2>Tendances</h2>
+<canvas id="graph"></canvas>
+
+<style>
+ h2 {
+ font-weight: bold;
+ color: var(--text-lime);
+ font-size: 24px;
+ }
+
+ #graph {
+ max-height: 40vh;
+ max-width: 40vw;
+ }
+</style>
diff --git a/ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte b/ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte
new file mode 100644
index 0000000..b9f0224
--- /dev/null
+++ b/ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte
@@ -0,0 +1,165 @@
+<script lang="ts">
+ import { authFetch } from '$lib/stores/auth';
+ import { onMount } from 'svelte';
+
+ interface IProps {
+ isOpen: boolean;
+ onCreate: (transaction: any) => void;
+ }
+
+ let { isOpen = $bindable(false), onCreate = () => {} }: IProps = $props(); // Changed default to false
+
+ let amount: number = $state(0);
+ let currency: string = $state('USD');
+ let label: string = $state('');
+ let receiverLabel: string = $state('');
+ let receiverIban: string = $state('');
+ let operationDate: string = $state('');
+
+ function closeDialog() {
+ isOpen = false;
+ }
+
+ async function createTransaction(event: Event) {
+ event.preventDefault();
+
+ const transaction = {
+ amount,
+ currency,
+ label,
+ receiverLabel,
+ receiverIban,
+ operationDate
+ };
+
+ try {
+ const response = await authFetch('/api/transactions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(transaction)
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ alert('Erreur lors de la création de la transaction: ' + error);
+ return;
+ }
+
+ onCreate(await response.json());
+
+ closeDialog();
+ } catch (err) {
+ alert('Erreur réseau lors de la création de la transaction');
+ }
+ }
+</script>
+
+<!-- svelte-ignore a11y_click_events_have_key_events -->
+<!-- svelte-ignore a11y_no_static_element_interactions -->
+{#if isOpen}
+ <div class="backdrop" onclick={closeDialog}>
+ <dialog open onclick={(e) => e.stopPropagation()}>
+ <h2>Créer une nouvelle transaction</h2>
+ <form class="flex flex-col gap-2">
+ <label>
+ <span>Montant</span>
+ <input type="number" step="0.01" min="0" bind:value={amount} required />
+ </label>
+ <label>
+ <span>Devise</span>
+ <select bind:value={currency} required>
+ <option selected value="USD" data-symbol="$" data-name="Dollar américain"
+ >USD - Dollar américain</option
+ >
+ <option value="EUR" data-symbol="€" data-name="Euro">EUR - Euro</option>
+ <option value="GBP" data-symbol="£" data-name="Livre sterling"
+ >GBP - Livre sterling</option
+ >
+ <option value="JPY" data-symbol="¥" data-name="Yen japonais">JPY - Yen japonais</option>
+ <option value="CHF" data-symbol="Fr" data-name="Franc suisse">CHF - Franc suisse</option
+ >
+ <option value="CAD" data-symbol="$" data-name="Dollar canadien"
+ >CAD - Dollar canadien</option
+ >
+ <option value="AUD" data-symbol="$" data-name="Dollar australien"
+ >AUD - Dollar australien</option
+ >
+ <option value="CNY" data-symbol="¥" data-name="Yuan chinois">CNY - Yuan chinois</option>
+ </select>
+ </label>
+ <label>
+ <span>Libellé</span>
+ <input type="text" bind:value={label} required />
+ </label>
+ <label>
+ <span>Libellé du bénéficiaire</span>
+ <input type="text" bind:value={receiverLabel} required />
+ </label>
+ <label>
+ <span>IBAN du bénéficiaire</span>
+ <input type="text" bind:value={receiverIban} required />
+ </label>
+ <label>
+ <span>Date de l'opération</span>
+ <input type="datetime-local" bind:value={operationDate} required />
+ </label>
+ <button class="btn" onclick={createTransaction}>➕ Créer</button>
+ </form>
+ </dialog>
+ </div>
+{/if}
+
+<style>
+ .backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0, 0, 0, 0.4);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ }
+
+ dialog {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ max-width: 90vw;
+ max-height: 90vh;
+ border: none;
+ border-radius: 8px;
+ box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2);
+ padding: 2rem;
+ background: var(--bg-primary);
+ outline: none;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ }
+
+ h2 {
+ font-weight: 600;
+ font-size: 32px;
+ color: var(--text-lime);
+ }
+
+ form {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ label {
+ display: flex;
+ flex-direction: column;
+ }
+</style>