diff options
Diffstat (limited to 'ping/frontend/src/routes/dashboard/analyses')
| -rw-r--r-- | ping/frontend/src/routes/dashboard/analyses/+page.svelte | 409 |
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> |
