summaryrefslogtreecommitdiff
path: root/ping/frontend/src/routes/dashboard/analyses/+page.svelte
diff options
context:
space:
mode:
authorMartial Simon <msimon_fr@hotmail.com>2025-09-15 01:07:58 +0200
committerMartial Simon <msimon_fr@hotmail.com>2025-09-15 01:07:58 +0200
commit967be9e750221ab2ab783f95df79bb26d290a45e (patch)
tree6802900a5e975f9f68b169f0f503f040056d6952 /ping/frontend/src/routes/dashboard/analyses/+page.svelte
add: added projectsHEADmain
Diffstat (limited to 'ping/frontend/src/routes/dashboard/analyses/+page.svelte')
-rw-r--r--ping/frontend/src/routes/dashboard/analyses/+page.svelte409
1 files changed, 409 insertions, 0 deletions
diff --git a/ping/frontend/src/routes/dashboard/analyses/+page.svelte b/ping/frontend/src/routes/dashboard/analyses/+page.svelte
new file mode 100644
index 0000000..1a8ed47
--- /dev/null
+++ b/ping/frontend/src/routes/dashboard/analyses/+page.svelte
@@ -0,0 +1,409 @@
+<script lang="ts">
+ import type { INumberStatList } from '$lib/components/NumberStatList.svelte';
+ import NumberStatList from '$lib/components/NumberStatList.svelte';
+ import SteppedLineChart from '$lib/components/SteppedLineChart.svelte';
+ import { onMount } from 'svelte';
+
+ interface Transaction {
+ id: number;
+ date: Date;
+ amount: number;
+ type: 'buy' | 'sell';
+ recipient: string;
+ company: string;
+ co2Impact: number;
+ }
+
+ const mockTransactions: Transaction[] = [
+ { id: 1, date: new Date('2025-06-20'), amount: 1200, type: 'buy', recipient: 'Microsoft Corp', company: 'MSFT', co2Impact: 45 },
+ { id: 2, date: new Date('2025-06-22'), amount: -800, type: 'sell', recipient: 'Apple Inc', company: 'AAPL', co2Impact: -30 },
+ { id: 3, date: new Date('2025-06-24'), amount: 2500, type: 'buy', recipient: 'Tesla Inc', company: 'TSLA', co2Impact: 15 },
+ { id: 4, date: new Date('2025-06-26'), amount: 950, type: 'buy', recipient: 'Amazon', company: 'AMZN', co2Impact: 60 },
+ { id: 5, date: new Date('2025-06-28'), amount: -1500, type: 'sell', recipient: 'Google', company: 'GOOGL', co2Impact: -40 },
+ { id: 6, date: new Date('2025-06-30'), amount: 750, type: 'buy', recipient: 'Netflix', company: 'NFLX', co2Impact: 25 },
+ { id: 7, date: new Date('2025-07-01'), amount: 1800, type: 'buy', recipient: 'Microsoft Corp', company: 'MSFT', co2Impact: 50 },
+ { id: 8, date: new Date('2025-06-15'), amount: -600, type: 'sell', recipient: 'Tesla Inc', company: 'TSLA', co2Impact: -10 },
+ { id: 9, date: new Date('2025-06-18'), amount: 3200, type: 'buy', recipient: 'NVIDIA', company: 'NVDA', co2Impact: 80 },
+ { id: 10, date: new Date('2025-06-25'), amount: 450, type: 'buy', recipient: 'Apple Inc', company: 'AAPL', co2Impact: 20 }
+ ];
+
+ let transactions = mockTransactions;
+ let selectedPeriod = 30;
+ let analysisData: any = {};
+
+ const thresholds = {
+ dailySpendingLimit: 1000,
+ monthlySpendingLimit: 5000,
+ co2Limit: 100,
+ profitTarget: 2000
+ };
+
+ function calculateStats() {
+ const now = new Date();
+ const cutoffDate = new Date(now.getTime() - selectedPeriod * 24 * 60 * 60 * 1000);
+ const filteredTransactions = transactions.filter(t => t.date >= cutoffDate);
+
+ const totalAmount = filteredTransactions.reduce((sum, t) => sum + t.amount, 0);
+ const averageAmount = filteredTransactions.length > 0 ? totalAmount / filteredTransactions.length : 0;
+ const uniqueRecipients = new Set(filteredTransactions.map(t => t.recipient)).size;
+ const totalCO2 = filteredTransactions.reduce((sum, t) => sum + t.co2Impact, 0);
+
+ const recipientTotals = filteredTransactions.reduce((acc, t) => {
+ acc[t.recipient] = (acc[t.recipient] || 0) + t.amount;
+ return acc;
+ }, {} as Record<string, number>);
+
+ const dailyData = filteredTransactions.reduce((acc, t) => {
+ const date = t.date.toISOString().split('T')[0];
+ acc[date] = (acc[date] || 0) + t.amount;
+ return acc;
+ }, {} as Record<string, number>);
+
+ const chartData = Object.entries(dailyData)
+ .map(([date, amount]) => ({
+ label: new Date(date).toLocaleDateString('fr-FR', { month: 'short', day: 'numeric' }),
+ value: amount
+ }))
+ .sort((a, b) => a.label.localeCompare(b.label));
+
+ const recipientChartData = Object.entries(recipientTotals).map(([recipient, total]) => ({
+ x: recipient,
+ y: Math.abs(total)
+ })).sort((a, b) => b.y - a.y);
+
+ const thresholdAlerts = {
+ dailySpending: Math.abs(totalAmount / selectedPeriod) > thresholds.dailySpendingLimit,
+ monthlySpending: Math.abs(totalAmount * (30 / selectedPeriod)) > thresholds.monthlySpendingLimit,
+ co2Impact: totalCO2 > thresholds.co2Limit,
+ profitTarget: totalAmount >= thresholds.profitTarget
+ };
+
+ analysisData = {
+ totalAmount,
+ averageAmount,
+ uniqueRecipients,
+ totalCO2,
+ chartData,
+ recipientChartData,
+ thresholdAlerts,
+ filteredTransactions
+ };
+ }
+
+ $: statsList = [
+ {
+ name: 'Total Transactions',
+ color: 'aqua',
+ value: `${analysisData.totalAmount ? analysisData.totalAmount.toFixed(0) : '0'}€`,
+ icon: '/icons/wallet.svg'
+ },
+ {
+ name: 'Moyenne',
+ color: '#1FCB4F',
+ value: `${analysisData.averageAmount ? analysisData.averageAmount.toFixed(0) : '0'}€`,
+ icon: '/icons/money-bills.svg'
+ },
+ {
+ name: 'Destinataires',
+ color: 'orange',
+ value: `${analysisData.uniqueRecipients || 0}`,
+ icon: '/icons/people.svg'
+ },
+ {
+ name: 'Impact CO2',
+ color: analysisData.totalCO2 > 0 ? '#ff6b6b' : '#1FCB4F',
+ value: `${analysisData.totalCO2 ? analysisData.totalCO2.toFixed(0) : '0'}g`,
+ icon: '/icons/leaf.svg'
+ }
+ ];
+
+ onMount(() => calculateStats());
+ $: selectedPeriod && calculateStats();
+</script>
+
+<section id="analyses">
+ <div class="controls">
+ <h1>Analyses des Transactions</h1>
+ <div class="period-selector">
+ <label for="period">Période d'analyse:</label>
+ <select bind:value={selectedPeriod} id="period">
+ <option value={7}>7 derniers jours</option>
+ <option value={30}>30 derniers jours</option>
+ <option value={90}>3 derniers mois</option>
+ <option value={365}>1 an</option>
+ </select>
+ </div>
+ </div>
+
+ <NumberStatList {statsList} />
+
+ {#if analysisData.thresholdAlerts}
+ <div class="threshold-alerts">
+ {#if analysisData.thresholdAlerts.dailySpending}
+ <div class="alert alert-warning">
+ ⚠️ Dépenses quotidiennes élevées ({(Math.abs(analysisData.totalAmount) / selectedPeriod).toFixed(0)}€/jour)
+ </div>
+ {/if}
+ {#if analysisData.thresholdAlerts.co2Impact}
+ <div class="alert alert-danger">
+ 🌍 Impact CO2 élevé ({analysisData.totalCO2}g)
+ </div>
+ {/if}
+ {#if analysisData.thresholdAlerts.profitTarget}
+ <div class="alert alert-success">
+ 🎯 Objectif de profit atteint ({analysisData.totalAmount.toFixed(0)}€)
+ </div>
+ {/if}
+ </div>
+ {/if}
+
+ <div id="analysis-grid">
+ <div class="card">
+ <h3>Transactions dans le temps</h3>
+ {#if analysisData.chartData?.length > 0}
+ <SteppedLineChart color="#1FCB4F" data={analysisData.chartData} title="Évolution" legend="Montant (€)" />
+ {:else}
+ <p style="color: #888; text-align: center; padding: 20px;">Aucune donnée</p>
+ {/if}
+ </div>
+
+ <div class="card">
+ <h3>Top destinataires</h3>
+ <div class="recipients-chart">
+ {#each (analysisData.recipientChartData || []).slice(0, 5) as recipient}
+ <div class="recipient-bar">
+ <span class="recipient-name">{recipient.x}</span>
+ <div class="bar-container">
+ {#if analysisData.recipientChartData?.length > 0}
+ {@const maxValue = Math.max(...analysisData.recipientChartData.map(r => r.y))}
+ <div class="bar" style="width: {(recipient.y / maxValue) * 100}%"></div>
+ {:else}
+ <div class="bar" style="width: 0%"></div>
+ {/if}
+ <span class="recipient-amount">{recipient.y.toFixed(0)}€</span>
+ </div>
+ </div>
+ {/each}
+ </div>
+ </div>
+
+ <div class="card">
+ <h3>Répartition des transactions</h3>
+ <div class="transaction-breakdown">
+ <div class="breakdown-item">
+ <span class="breakdown-label">Achats:</span>
+ <span class="breakdown-value positive">
+ {(analysisData.filteredTransactions || []).filter((t: Transaction) => t.type === 'buy').length}
+ </span>
+ </div>
+ <div class="breakdown-item">
+ <span class="breakdown-label">Ventes:</span>
+ <span class="breakdown-value negative">
+ {(analysisData.filteredTransactions || []).filter((t: Transaction) => t.type === 'sell').length}
+ </span>
+ </div>
+ <div class="breakdown-item">
+ <span class="breakdown-label">Volume total:</span>
+ <span class="breakdown-value">
+ {Math.abs(analysisData.totalAmount || 0).toFixed(0)}€
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div class="card">
+ <h3>Métriques CO2</h3>
+ <div class="co2-metrics">
+ <div class="co2-item">
+ <span class="co2-label">Impact total:</span>
+ <span class="co2-value {analysisData.totalCO2 > 0 ? 'negative' : 'positive'}">
+ {analysisData.totalCO2 || 0}g CO2
+ </span>
+ </div>
+ <div class="co2-item">
+ <span class="co2-label">Par transaction:</span>
+ <span class="co2-value">
+ {(analysisData.filteredTransactions?.length ? analysisData.totalCO2 / analysisData.filteredTransactions.length : 0).toFixed(1)}g
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+</section>
+
+<style>
+ .card {
+ margin: 0;
+ background-color: var(--bg-primary);
+ border-radius: 8px;
+ padding: 16px;
+ color: white;
+ }
+
+ .card h3 {
+ margin: 0 0 16px 0;
+ color: var(--text-lime);
+ font-size: 18px;
+ font-weight: 600;
+ }
+
+ #analyses {
+ display: flex;
+ flex-direction: column;
+ padding-right: 12px;
+ background-color: var(--bg-secondary);
+ min-height: calc(100vh - 72px);
+ color: white;
+ }
+
+ .controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 12px;
+ margin-bottom: 8px;
+ }
+
+ .controls h1 {
+ color: var(--text-lime);
+ font-size: 32px;
+ margin: 0;
+ font-weight: 600;
+ }
+
+ .period-selector {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .period-selector label {
+ font-size: 14px;
+ color: white;
+ }
+
+ .period-selector select {
+ background-color: var(--bg-primary);
+ color: white;
+ border: 1px solid #444;
+ border-radius: 4px;
+ padding: 8px 12px;
+ font-size: 14px;
+ }
+
+ .threshold-alerts {
+ padding: 0 12px;
+ margin-bottom: 16px;
+ }
+
+ .alert {
+ padding: 12px 16px;
+ border-radius: 8px;
+ margin-bottom: 8px;
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .alert-warning {
+ background-color: rgba(255, 193, 7, 0.2);
+ border-left: 4px solid #ffc107;
+ color: #ffc107;
+ }
+
+ .alert-danger {
+ background-color: rgba(220, 53, 69, 0.2);
+ border-left: 4px solid #dc3545;
+ color: #dc3545;
+ }
+
+ .alert-success {
+ background-color: rgba(40, 167, 69, 0.2);
+ border-left: 4px solid #28a745;
+ color: #28a745;
+ }
+
+ #analysis-grid {
+ padding: 0 12px;
+ width: 100%;
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1rem;
+ }
+
+ .recipients-chart {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .recipient-bar {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .recipient-name {
+ font-size: 12px;
+ color: #888;
+ font-weight: 500;
+ }
+
+ .bar-container {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ position: relative;
+ }
+
+ .bar {
+ height: 20px;
+ background: linear-gradient(90deg, var(--text-lime), #1FCB4F);
+ border-radius: 4px;
+ transition: width 0.3s ease;
+ }
+
+ .recipient-amount {
+ font-size: 12px;
+ color: white;
+ font-weight: 600;
+ min-width: 60px;
+ text-align: right;
+ }
+
+ .transaction-breakdown, .co2-metrics {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .breakdown-item, .co2-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 0;
+ border-bottom: 1px solid #333;
+ }
+
+ .breakdown-item:last-child, .co2-item:last-child {
+ border-bottom: none;
+ }
+
+ .breakdown-label, .co2-label {
+ font-size: 14px;
+ color: #888;
+ }
+
+ .breakdown-value, .co2-value {
+ font-size: 16px;
+ font-weight: 600;
+ color: white;
+ }
+
+ .breakdown-value.positive, .co2-value.positive {
+ color: var(--text-lime);
+ }
+
+ .breakdown-value.negative, .co2-value.negative {
+ color: #ff6b6b;
+ }
+</style>