diff options
Diffstat (limited to 'ping/frontend/src/routes/dashboard')
| -rw-r--r-- | ping/frontend/src/routes/dashboard/+layout.svelte | 44 | ||||
| -rw-r--r-- | ping/frontend/src/routes/dashboard/+page.svelte | 81 | ||||
| -rw-r--r-- | ping/frontend/src/routes/dashboard/analyses/+page.svelte | 409 | ||||
| -rw-r--r-- | ping/frontend/src/routes/dashboard/messages/+page.svelte | 403 | ||||
| -rw-r--r-- | ping/frontend/src/routes/dashboard/models/+page.svelte | 568 | ||||
| -rw-r--r-- | ping/frontend/src/routes/dashboard/personnel/+page.svelte | 290 | ||||
| -rw-r--r-- | ping/frontend/src/routes/dashboard/settings/+page.svelte | 452 | ||||
| -rw-r--r-- | ping/frontend/src/routes/dashboard/transactions/+page.svelte | 360 |
8 files changed, 2607 insertions, 0 deletions
diff --git a/ping/frontend/src/routes/dashboard/+layout.svelte b/ping/frontend/src/routes/dashboard/+layout.svelte new file mode 100644 index 0000000..a2f7efe --- /dev/null +++ b/ping/frontend/src/routes/dashboard/+layout.svelte @@ -0,0 +1,44 @@ +<script lang="ts"> + import ToastList from '$lib/components/ToastList.svelte'; + import Avatar from '$lib/components/Avatar.svelte'; + import NavBar from '$lib/components/NavBar.svelte'; + import NumberStatList from '$lib/components/NumberStatList.svelte'; + import SideBar from '$lib/components/SideBar.svelte'; + + import '../../app.css'; + import { getPageIndex, pages } from '$lib/pages'; + import { onMount } from 'svelte'; + import { page } from '$app/stores'; + import { user } from '$lib/stores/auth'; + + let { children } = $props(); + + let selectedIndex = $state(0); + + $effect(() => { + selectedIndex = getPageIndex($page.url.pathname); + }); +</script> + +<ToastList /> +<NavBar pageTitle={pages[selectedIndex].name}> + {#snippet rightComponent()} + <div class="flex items-center gap-4"> + <form> + <input type="search" name="search" id="search" placeholder="Search..." /> + </form> + <Avatar username={$user?.login ?? 'No auth'} url={$user?.avatar} /> + </div> + {/snippet} +</NavBar> +<SideBar bind:selectedIndex /> +<section class="dashboard">{@render children()}</section> + +<style> + section.dashboard { + padding-left: 208px; + padding-top: 72px; + width: 100%; + height: calc(100vh); + } +</style> diff --git a/ping/frontend/src/routes/dashboard/+page.svelte b/ping/frontend/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000..c38e281 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/+page.svelte @@ -0,0 +1,81 @@ +<script lang="ts"> + import NumberStatList from '$lib/components/NumberStatList.svelte'; + import type { INumberStatList } from '$lib/components/NumberStatList.svelte'; + import RiskAnalysis from '$lib/components/dashboard/RiskAnalysis.svelte'; + import StockGraph from '$lib/components/dashboard/StockGraph.svelte'; + import TrendingSymbols from '$lib/components/dashboard/TrendingSymbols.svelte'; + import { onMount } from 'svelte'; + + const statsList: INumberStatList[] = [ + { + name: 'Portefeuille', + color: 'aqua', + value: `${Math.random().toFixed(3) * 1000}€`, + icon: '/icons/wallet.svg' + }, + { + name: 'Revenus', + color: '#1FCB4F', + value: `${Math.random().toFixed(3) * 1000}€`, + icon: '/icons/money-bills.svg' + }, + { + name: 'Depenses', + color: 'orange', + value: `${Math.random().toFixed(3) * 1000}€`, + icon: '/icons/credit-card.svg' + }, + { + name: 'Eco score', + color: '#1FCB4F', + value: `${Math.random().toFixed(3) * 1000}`, + icon: '/icons/leaf.svg' + } + ]; + + onMount(() => {}); +</script> + +<section class="dashboard-home"> + <NumberStatList {statsList} /> + <div class="home-grid"> + <div class="card overviewGraph"> + <StockGraph /> + </div> + <!-- <div class="card riskAnalysis"> + <RiskAnalysis /> + </div> --> + <!-- <div class="card activity">c</div> --> + <div class="card tendencies"> + <TrendingSymbols /> + </div> + <!-- <div class="card recentInvestments">e</div> --> + </div> +</section> + +<style> + .home-grid { + display: grid; + grid-template-areas: + 'overviewGraph overviewGraph overviewGraph' + 'tendencies tendencies tendencies'; + gap: 16px; + width: calc(100% - 32px); + } + + .overviewGraph { + grid-area: overviewGraph; + } + /* .riskAnalysis { + grid-area: riskAnalysis; + } + .activity { + grid-area: activity; + } */ + .tendencies { + grid-area: tendencies; + } + /* .recentInvestments { + grid-area: recentInvestments; + } */ +</style> 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> diff --git a/ping/frontend/src/routes/dashboard/messages/+page.svelte b/ping/frontend/src/routes/dashboard/messages/+page.svelte new file mode 100644 index 0000000..52869e4 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/messages/+page.svelte @@ -0,0 +1,403 @@ +<script lang="ts"> + import type { PageData } from './$types'; + + let { data }: { data: PageData } = $props(); + + let searchQuery = ''; + let selectedMessage = null; + let newMessage = ''; + let messages = [ + { + id: 1, + sender: 'Équipe Ping', + subject: 'Nouvelle analyse de portefeuille disponible', + content: 'Votre analyse mensuelle de performance ESG est maintenant disponible. Consultez les dernières métriques de votre portefeuille.', + date: new Date('2024-06-28'), + unread: true + }, + { + id: 2, + sender: 'Système', + subject: 'Alerte: Score ESG en baisse', + content: 'Le score ESG de votre portefeuille a diminué de 5% ce mois-ci. Nous recommandons de réviser vos investissements.', + date: new Date('2024-06-25'), + unread: true + }, + { + id: 3, + sender: 'Support', + subject: 'Mise à jour des modèles prédictifs', + content: 'Nos modèles d\'analyse ont été mis à jour avec les dernières données du marché. Vos prédictions sont maintenant plus précises.', + date: new Date('2024-06-20'), + unread: false + } + ]; + + function handleSearch() { + console.log('Searching messages:', searchQuery); + } + + function selectMessage(message: any) { + selectedMessage = message; + if (message.unread) { + message.unread = false; + messages = [...messages]; + } + } + + function sendMessage() { + if (newMessage.trim()) { + console.log('Sending message:', newMessage); + newMessage = ''; + } + } + + function deleteMessage(messageId: number) { + messages = messages.filter(m => m.id !== messageId); + if (selectedMessage?.id === messageId) { + selectedMessage = null; + } + } +</script> + +<section id="messages"> + <div class="messages-container"> + <!-- Header --> + <div class="messages-header"> + <h1>Messages</h1> + <div class="search-bar"> + <input + type="text" + placeholder="Rechercher dans les messages..." + bind:value={searchQuery} + on:input={handleSearch} + /> + <button class="search-btn">🔍</button> + </div> + </div> + + <div class="messages-layout"> + <!-- Messages List --> + <div class="messages-list card"> + <h2>Boîte de réception</h2> + <div class="message-items"> + {#each messages as message} + <div + class="message-item {message.unread ? 'unread' : ''} {selectedMessage?.id === message.id ? 'selected' : ''}" + on:click={() => selectMessage(message)} + > + <div class="message-header"> + <span class="message-sender">{message.sender}</span> + <span class="message-date">{message.date.toLocaleDateString()}</span> + </div> + <div class="message-subject">{message.subject}</div> + <div class="message-preview">{message.content.substring(0, 80)}...</div> + {#if message.unread} + <div class="unread-indicator"></div> + {/if} + </div> + {/each} + </div> + </div> + + <!-- Message Detail --> + <div class="message-detail card"> + {#if selectedMessage} + <div class="message-detail-header"> + <h3>{selectedMessage.subject}</h3> + <div class="message-actions"> + <button class="btn" on:click={() => deleteMessage(selectedMessage.id)}> + 🗑️ Supprimer + </button> + </div> + </div> + <div class="message-meta"> + <span>De: {selectedMessage.sender}</span> + <span>Date: {selectedMessage.date.toLocaleDateString()}</span> + </div> + <div class="message-content"> + {selectedMessage.content} + </div> + + <!-- Reply Section --> + <div class="reply-section"> + <h4>Répondre</h4> + <textarea + bind:value={newMessage} + placeholder="Tapez votre réponse..." + rows="4" + ></textarea> + <button class="btn" on:click={sendMessage}> + 📤 Envoyer + </button> + </div> + {:else} + <div class="no-message-selected"> + <h3>Sélectionnez un message</h3> + <p>Choisissez un message dans la liste pour le lire</p> + </div> + {/if} + </div> + </div> + </div> +</section> + +<style> + #messages { + padding: 16px; + background-color: var(--bg-secondary); + color: white; + min-height: calc(100vh - 72px); + } + + .messages-container { + max-width: 1400px; + } + + /* Header */ + .messages-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + } + + h1 { + color: var(--text-lime); + font-size: 32px; + margin: 0; + font-weight: 600; + } + + h2 { + color: var(--text-lime); + font-size: 24px; + margin: 0 0 16px 0; + font-weight: 600; + } + + .search-bar { + display: flex; + align-items: center; + background-color: var(--bg-primary); + border-radius: 8px; + padding: 4px; + gap: 8px; + } + + .search-bar input { + background: transparent; + border: none; + color: white; + padding: 12px 16px; + font-size: 14px; + outline: none; + width: 300px; + } + + .search-bar input::placeholder { + color: #888888; + } + + .search-btn { + background: transparent; + border: none; + color: #888888; + padding: 8px; + cursor: pointer; + font-size: 16px; + } + + /* Layout */ + .messages-layout { + display: flex; + gap: 16px; + height: calc(100vh - 200px); + } + + .card { + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 16px; + color: white; + } + + /* Messages List */ + .messages-list { + flex: 1; + max-width: 400px; + } + + .message-items { + display: flex; + flex-direction: column; + gap: 8px; + max-height: calc(100vh - 300px); + overflow-y: auto; + } + + .message-item { + padding: 12px; + border-radius: 8px; + background-color: var(--bg-secondary); + cursor: pointer; + transition-duration: 0.2s; + position: relative; + border-left: 3px solid transparent; + } + + .message-item:hover { + background-color: var(--btn-primary); + transform: scale(1.02); + } + + .message-item.selected { + border-left-color: var(--text-lime); + background-color: var(--btn-primary); + } + + .message-item.unread { + background-color: rgba(0, 255, 119, 0.1); + } + + .message-header { + display: flex; + justify-content: space-between; + margin-bottom: 4px; + } + + .message-sender { + font-weight: 600; + color: var(--text-lime); + } + + .message-date { + font-size: 12px; + color: #888888; + } + + .message-subject { + font-weight: 500; + margin-bottom: 4px; + font-size: 14px; + } + + .message-preview { + font-size: 13px; + color: #888888; + } + + .unread-indicator { + position: absolute; + top: 8px; + right: 8px; + width: 8px; + height: 8px; + background-color: var(--text-lime); + border-radius: 50%; + } + + /* Message Detail */ + .message-detail { + flex: 2; + } + + .message-detail-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 16px; + border-bottom: 1px solid var(--btn-primary); + } + + .message-detail-header h3 { + margin: 0; + color: white; + font-size: 20px; + } + + .message-actions { + display: flex; + gap: 8px; + } + + .message-meta { + display: flex; + gap: 20px; + margin-bottom: 20px; + font-size: 14px; + color: #888888; + } + + .message-content { + line-height: 1.6; + margin-bottom: 30px; + padding: 20px; + background-color: var(--bg-secondary); + border-radius: 8px; + } + + .reply-section { + border-top: 1px solid var(--btn-primary); + padding-top: 20px; + } + + .reply-section h4 { + margin: 0 0 12px 0; + color: var(--text-lime); + } + + textarea { + width: 100%; + background-color: var(--bg-secondary); + border: none; + border-bottom: 2px solid var(--text-lime); + padding: 8px; + border-radius: 8px 8px 0 0; + color: white; + font-size: 14px; + resize: vertical; + box-sizing: border-box; + margin-bottom: 12px; + } + + textarea::placeholder { + color: #888888; + } + + textarea:focus { + outline: none; + border-bottom-color: var(--text-lime); + } + + .no-message-selected { + text-align: center; + padding: 60px 20px; + color: #888888; + } + + .no-message-selected h3 { + margin: 0 0 12px 0; + color: #888888; + } + + .btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + text-decoration: none; + transition-duration: 0.2s; + font-weight: 600; + } + + .btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); + } +</style> diff --git a/ping/frontend/src/routes/dashboard/models/+page.svelte b/ping/frontend/src/routes/dashboard/models/+page.svelte new file mode 100644 index 0000000..3be6f00 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/models/+page.svelte @@ -0,0 +1,568 @@ +<script lang="ts"> + import Avatar from '$lib/components/Avatar.svelte'; + import UserSelector from '$lib/components/input/UserSelector.svelte'; + import { authFetch, getUser, type IUser } from '$lib/stores/auth'; + import { addToast } from '$lib/stores/toast'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + interface IModel { + id: string; + members: IUser[]; + name: string; + owner: IUser; + } + + let modelName = $state(''); + let sources = $state([]); + let models = $state<IModel[]>([]); + let modelToEdit = $state<number | null>(null); + let modelOwner = $state<IUser | null>(null); + + let users = $state<IUser[]>([]); + + function addSource() { + sources = [...sources, '']; + } + + function removeSource(index: number) { + sources = sources.filter((_, i) => i !== index); + } + + function handleSubmit(e: SubmitEvent) { + e.preventDefault(); + if (modelToEdit === null) { + createModel(); + } else { + updateModel(); + } + } + + async function createModel() { + try { + const response = await authFetch('/api/projects', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json' + }, + body: JSON.stringify({ + name: modelName + }) + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + models = [...models, data]; + await Promise.all( + users.map((user) => + authFetch(`/api/projects/${data.id}/add-user`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + accept: '*/*' + }, + body: JSON.stringify({ userId: user.id }) + }) + .then((res) => { + if (!res.ok) { + addToast({ + color: 'red', + title: 'Erreur', + message: `Impossible d'ajouter l'utilisateur ${user.displayName} au modèle "${modelName}".` + }); + console.error(`HTTP error! status: ${res.status}`); + } + return res.json().catch(() => null); + }) + .then((memberData) => { + console.log('Member added:', memberData); + }) + .catch((err) => { + console.error('Error adding member:', err); + }) + ) + ); + addToast({ + color: 'green', + title: 'Modèle créé', + message: `Le modèle "${modelName}" a été créé avec succès.` + }); + modelName = ''; + return data; + } catch (error) { + console.error('Error creating model:', error); + addToast({ + color: 'red', + title: 'Erreur', + message: `Une erreur est survenue lors de la création du modèle "${modelName}".` + }); + throw error; + } + } + + async function updateModel() { + try { + if (modelToEdit === null || models.length <= modelToEdit) { + addToast({ + color: 'red', + title: 'Erreur', + message: 'Aucun modèle sélectionné pour la mise à jour.' + }); + return; + } + const model = models[modelToEdit]; + console.log('Updating model:', model); + const response = await authFetch(`/api/projects/${model.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json' + }, + body: JSON.stringify({ + name: modelName + }) + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const updatedModel = await response.json(); + models = models.map((m, i) => (i === modelToEdit ? updatedModel : m)); + modelToEdit = null; + modelName = ''; + addToast({ + color: 'green', + title: 'Modèle mis à jour', + message: `Le modèle "${updatedModel.name}" a été mis à jour avec succès.` + }); + } catch (error) { + console.error('Error updating model:', error); + addToast({ + color: 'red', + title: 'Erreur', + message: `Une erreur est survenue lors de la mise à jour du modèle "${modelName}".` + }); + throw error; + } + } + + async function getMyModels() { + try { + const response = await authFetch('/api/projects', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json' + } + }); + const data = await response.json(); + console.log('My models:', data); + return data; + } catch (error) { + console.error('Error fetching models:', error); + addToast({ + color: 'red', + title: 'Erreur', + message: 'Impossible de charger les modèles.' + }); + throw error; + } + } + + function deleteModel(i: number) { + if (models.length <= i || i < 0) { + addToast({ + color: 'red', + title: 'Erreur', + message: 'Modèle introuvable.' + }); + return; + } + + modelToEdit = null; + + const modelToDelete = models[i]; + authFetch(`/api/projects/${modelToDelete.id}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json' + } + }) + .then(() => { + models = models.filter((_, index) => index !== i); + addToast({ + color: 'green', + title: 'Modèle supprimé', + message: `Le modèle "${modelToDelete.name}" a été supprimé avec succès.` + }); + }) + .catch((err) => { + console.error('Error deleting model:', err); + addToast({ + color: 'red', + title: 'Erreur', + message: `Impossible de supprimer le modèle "${modelToDelete.name}".` + }); + }); + } + + function onEditButtonPressed(i: number) { + if (modelToEdit === i) { + modelToEdit = null; + modelName = ''; + users = []; + modelOwner = null; + } else { + modelToEdit = i; + modelName = models[i].name; + users = models[i].members || []; + modelOwner = models[i].owner || null; + } + } + + let currentUser = $state<IUser | null>(null); + + onMount(() => { + getMyModels() + .then((lsModels) => { + models = lsModels; + console.log('Models loaded:', models); + }) + .catch((err) => { + console.error('Error loading models:', err); + }); + getUser() + .then((user) => { + currentUser = user; + console.log('Current user:', currentUser); + }) + .catch((err) => { + console.error('Error fetching current user:', err); + }); + }); +</script> + +<section id="models"> + <div class="models-container"> + <div class="active-models"> + <h2>Modèles actifs</h2> + <div class="models-list"> + {#each models as m, i} + <div class="model-item" class:selected={modelToEdit === i}> + <div class="model-name"> + <span>{m.name}</span><br /> + <code style="font-size:8px">{m.id}</code> + </div> + <div class="model-metrics"> + <div class="metric"> + <span class="metric-label">Eco score</span> + <span class="metric-value green">{69} %</span> + </div> + <div class="metric"> + <span class="metric-label">Efficacité</span> + <span class="metric-value green">{69} %</span> + </div> + </div> + <button class="modify-btn" onclick={() => onEditButtonPressed(i)}>✏️</button> + <button class="delete-btn btn" onclick={() => deleteModel(i)}>🗑️</button> + </div> + {/each} + </div> + </div> + + <div class="new-model"> + <form onsubmit={handleSubmit}> + <div class="new-model-header"> + {#if modelToEdit !== null} + <h2>Modifier un modèle</h2> + {:else} + <h2>Nouveau modèle</h2> + {/if} + <button class="register-btn" type="submit">📁 Enregistrer</button> + </div> + <div class="form-group"> + <label for="name">Nom du modèle</label> + <input type="text" id="name" bind:value={modelName} placeholder="Modèle X" /> + <div class="flex gap-2 p-2"> + {#if modelToEdit !== null} + <Avatar username={modelOwner?.displayName} url={modelOwner?.avatar} /> + {:else if currentUser} + <Avatar username={currentUser.displayName} url={currentUser.avatar} /> + {:else} + Chargement en cours... + {/if} + <UserSelector bind:users /> + </div> + </div> + + <div class="sources-section"> + <div class="sources-header"> + <span>Sources de données</span> + <button type="button" class="add-source-btn" onclick={addSource}> + ➕ Ajouter une source de données + </button> + </div> + + {#each sources as source, index} + <div class="source-item"> + <!-- svelte-ignore a11y_label_has_associated_control --> + <label>Source {index + 1}</label> + <div class="source-input-group"> + <input type="text" bind:value={sources[index]} placeholder="Source" /> + <button type="button" class="remove-source-btn" onclick={() => removeSource(index)}> + 🗑️ + </button> + </div> + </div> + {/each} + </div> + </form> + </div> + </div> +</section> + +<style> + #models { + padding: 16px; + background-color: var(--bg-secondary); + color: white; + min-height: calc(100vh - 72px); + } + + .models-container { + display: flex; + gap: 16px; + max-width: 1400px; + } + + h2 { + color: var(--text-lime); + font-size: 24px; + margin: 0 0 16px 0; + font-weight: 600; + } + + /* Modèles actifs */ + .active-models { + flex: 1; + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 16px; + color: white; + } + + .models-list { + display: flex; + flex-direction: column; + gap: 15px; + } + + .model-item { + background-color: var(--bg-secondary); + padding: 16px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: space-between; + border: 2px solid transparent; + } + + .model-item.selected { + border: 2px solid var(--text-lime); + } + + .model-name { + font-weight: 500; + font-size: 16px; + color: white; + } + + .model-metrics { + display: flex; + gap: 30px; + } + + .metric { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + } + + .metric-label { + font-size: 12px; + color: #888888; + } + + .metric-value { + font-size: 18px; + font-weight: bold; + } + + .metric-value.green { + color: var(--text-lime); + } + + .modify-btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition-duration: 0.2s; + } + + .modify-btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); + } + + /* Nouveau modèle */ + .new-model { + flex: 1; + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 16px; + color: white; + } + + .new-model-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + } + + .register-btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-weight: 600; + font-size: 14px; + transition-duration: 0.2s; + } + + .register-btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); + } + + .form-group { + margin-bottom: 16px; + } + + label { + display: block; + margin-bottom: 8px; + font-size: 14px; + color: white; + font-weight: 500; + } + + input[type='text'] { + width: 100%; + background-color: var(--bg-secondary); + border: none; + border-bottom: 2px solid var(--text-lime); + padding: 8px; + border-radius: 8px 8px 0 0; + color: white; + font-size: 14px; + box-sizing: border-box; + } + + input[type='text']:focus { + outline: none; + border-bottom-color: var(--text-lime); + } + + input[type='text']::placeholder { + color: #888888; + } + + /* Sources section */ + .sources-section { + margin-top: 16px; + } + + .sources-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + } + + .sources-header span { + font-size: 14px; + font-weight: 500; + color: white; + } + + .add-source-btn { + background-color: var(--btn-secondary); + color: var(--text-lime); + border: 1px solid var(--text-lime); + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-weight: 600; + transition-duration: 0.2s; + } + + .add-source-btn:hover { + background-color: var(--text-lime); + color: var(--bg-primary); + transform: scale(1.025); + } + + .source-item { + margin-bottom: 15px; + } + + .source-item label { + margin-bottom: 6px; + font-size: 12px; + color: #888888; + } + + .source-input-group { + display: flex; + gap: 10px; + align-items: center; + } + + .source-input-group input { + flex: 1; + } + + .remove-source-btn { + background-color: var(--btn-secondary); + color: #888888; + border: none; + padding: 8px; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + transition-duration: 0.2s; + min-width: 40px; + } + + .remove-source-btn:hover { + color: #ef4444; + background-color: var(--btn-primary); + transform: scale(1.025); + } +</style> diff --git a/ping/frontend/src/routes/dashboard/personnel/+page.svelte b/ping/frontend/src/routes/dashboard/personnel/+page.svelte new file mode 100644 index 0000000..a1722c1 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/personnel/+page.svelte @@ -0,0 +1,290 @@ +<script lang="ts"> + import Avatar from '$lib/components/Avatar.svelte'; + import UserItem from '$lib/components/UserItem.svelte'; + import { authFetch, getUpdatedUser, getUser, type IUser } from '$lib/stores/auth'; + import { addToast } from '$lib/stores/toast'; + import { onMount } from 'svelte'; + + let user = $state<IUser | null>(null); + let allUsers = $state<IUser[]>([]); + + let idValue = $state<string>(''); + let urlValue = $state<string>(''); + let nameValue = $state<string>(''); + let passwordValue = $state<string>(''); + + async function loadUser() { + try { + user = await getUser(); + if (!user) { + addToast({ title: 'Erreur', message: 'Utilisateur non trouvé.' }); + return null; + } + idValue = user.id; + urlValue = user.avatar || ''; + nameValue = user.displayName || ''; + return user; + } catch (error) { + user = null; + addToast({ title: 'Erreur', message: 'Impossible de charger les informations utilisateur.' }); + return null; + } + } + + async function loadAllUsers() { + try { + const response = await authFetch('/api/user/all'); + if (!response.ok) { + throw new Error('Erreur lors du chargement des utilisateurs'); + } + const users = await response.json(); + return users; + } catch (error) { + addToast({ title: 'Erreur', message: 'Impossible de charger la liste des utilisateurs.' }); + return []; + } + } + + function onEditUser(event: Event) { + event.preventDefault(); + const form = event.target as HTMLFormElement; + const formData = new FormData(form); + + const { id, avatar, username, password } = Object.fromEntries(formData.entries()); + + authFetch(`/api/user/${id}`, { + method: 'PUT', + body: JSON.stringify({ + avatar: avatar || user?.avatar, + displayName: username || user?.displayName, + password: password || undefined + }), + headers: { + 'Content-Type': 'application/json' + } + }) + .then((res) => res.json()) + .then((data) => { + getUpdatedUser().then((updatedUser) => { + user = updatedUser; + }); + console.log('Utilisateur mis à jour', data); + addToast({ + title: 'Succès', + message: 'Informations utilisateur mises à jour avec succès.', + color: 'green' + }); + }) + .catch((error) => { + addToast({ + title: 'Erreur', + message: 'Échec de la mise à jour des informations utilisateur.' + }); + }); + } + + function onCreateUser(event: Event) { + event.preventDefault(); + const form = event.target as HTMLFormElement; + const formData = new FormData(form); + const { login, password, isAdmin } = Object.fromEntries(formData.entries()); + + authFetch('/api/user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + login, + password, + isAdmin: isAdmin === 'on' + }) + }) + .then(async (res) => { + if (!res.ok) { + addToast({ + title: 'Erreur', + message: "Impossible de créer l'utilisateur." + }); + throw new Error("Erreur lors de la création de l'utilisateur"); + } + const newUser = await res.json(); + allUsers = [...allUsers, newUser]; + addToast({ + title: 'Succès', + message: 'Nouvel utilisateur créé avec succès.', + color: 'green' + }); + form.reset(); + }) + .catch(() => { + addToast({ + title: 'Erreur', + message: "Impossible de créer l'utilisateur." + }); + }); + } + + onMount(async () => { + loadUser().then((user) => { + if (user && user.isAdmin) + loadAllUsers().then((users) => { + allUsers = users; + }); + }); + }); +</script> + +{#if user} + <div id="personnel"> + <div> + <section id="myUser" class="card"> + <h2>Mon compte</h2> + <Avatar username={user.displayName} url={user.avatar || '/img/default-avatar.png'} /> + <p><b>Nom</b>: {user.displayName}</p> + <p><b>Login</b>: {user.login}</p> + <p><b>Role</b>: {user.isAdmin ? 'ADMIN' : 'USER'}</p> + <p><b>Id</b>: {user.id}</p> + </section> + <section id="userEdit" class="card"> + <h2>Modifier</h2> + <i + >Laisser vide pour ne pas modifier<br /> + {#if user.isAdmin} + <p>Vous pouvez modifier les informations de n'importe quel utilisateur.</p> + {:else} + <p>Vous ne pouvez modifier que vos propres informations.</p> + {/if} + </i><br /> + <form onsubmit={onEditUser}> + {#if user.isAdmin} + <label for="id">ID de l'utilisateur</label> + <input type="text" placeholder="ID de l'utilisateur" name="id" bind:value={idValue} /> + {/if} + <label for="id">URL de l'avatar</label> + <input type="text" placeholder="URL de l'avatar" name="avatar" bind:value={urlValue} /> + <label for="id">Nom d'utilisateur</label> + <input + type="text" + placeholder="Nom d'utilisateur" + name="username" + bind:value={nameValue} + /> + <label for="id">Mot de passe</label> + <input + type="password" + placeholder="Mot de passe" + name="password" + bind:value={passwordValue} + /> + <button type="submit" class="btn">Modifier</button> + </form> + </section> + </div> + + {#if user.isAdmin} + <section id="adminActions"> + <div class="card"> + <h2>Créer un nouvel utilisateur</h2> + <form class="flex flex-col gap-2" onsubmit={onCreateUser}> + <label for="new-login">Login</label> + <input type="text" id="new-login" name="login" placeholder="Login" required /> + + <label for="new-password">Mot de passe</label> + <input + type="password" + id="new-password" + name="password" + placeholder="Mot de passe" + required + /> + + <label for="new-isAdmin"> + <input type="checkbox" id="new-isAdmin" name="isAdmin" /> + Administrateur + </label> + + <button type="submit" class="btn">Créer</button> + </form> + </div> + <div class="card"> + <h2>Liste des utilisateurs</h2> + <div> + {#each allUsers as u} + <UserItem + user={u} + onEdit={() => { + idValue = u.id; + urlValue = u.avatar || ''; + nameValue = u.displayName || ''; + }} + onDelete={() => { + authFetch(`/api/user/${u.id}`, { + method: 'DELETE' + }) + .then(async (res) => { + if (!res.ok) { + addToast({ + title: 'Erreur', + message: "Impossible de supprimer l'utilisateur." + }); + throw new Error("Erreur lors de la suppression de l'utilisateur"); + } + allUsers = allUsers.filter((user) => user.id !== u.id); + addToast({ + title: 'Succès', + message: 'Utilisateur supprimé avec succès.', + color: 'green' + }); + }) + .catch(() => { + addToast({ + title: 'Erreur', + message: "Impossible de supprimer l'utilisateur." + }); + }); + }} + /> + {/each} + </div> + </div> + </section> + {/if} + </div> +{:else} + <p>Chargement des informations utilisateur...</p> +{/if} + +<style> + * { + color: white; + } + + #personnel { + width: calc(100% - 32px); + display: flex; + gap: 16px; + } + + #personnel > * { + flex: 1; + } + + #myUser { + display: flex; + flex-direction: column; + } + + #userEdit form { + display: flex; + flex-direction: column; + gap: 8px; + } + + h2 { + margin: 0; + font-size: 1.5em; + font-weight: 600; + color: var(--text-lime); + } +</style> diff --git a/ping/frontend/src/routes/dashboard/settings/+page.svelte b/ping/frontend/src/routes/dashboard/settings/+page.svelte new file mode 100644 index 0000000..99c5d8f --- /dev/null +++ b/ping/frontend/src/routes/dashboard/settings/+page.svelte @@ -0,0 +1,452 @@ +<script lang="ts"> + import type { PageData } from './$types'; + + let { data }: { data: PageData } = $props(); + + let userProfile = { + name: 'Baptiste', + email: 'baptiste@ping.com', + company: 'Ping Analytics', + phone: '+33 6 12 34 56 78' + }; + + let notifications = { + emailAlerts: true, + portfolioUpdates: true, + esgAlerts: true, + marketNews: false, + weeklyReports: true + }; + + let security = { + twoFactorAuth: false, + sessionTimeout: '30', + dataEncryption: true + }; + + let display = { + theme: 'dark', + language: 'fr', + currency: 'EUR', + dateFormat: 'DD/MM/YYYY' + }; + + let currentPassword = ''; + let newPassword = ''; + let confirmPassword = ''; + + function saveProfile() { + console.log('Saving profile:', userProfile); + } + + function saveNotifications() { + console.log('Saving notifications:', notifications); + } + + function saveSecurity() { + console.log('Saving security settings:', security); + } + + function saveDisplay() { + console.log('Saving display settings:', display); + } + + function changePassword() { + if (newPassword !== confirmPassword) { + alert('Les mots de passe ne correspondent pas'); + return; + } + console.log('Changing password'); + currentPassword = ''; + newPassword = ''; + confirmPassword = ''; + } + + function exportData() { + console.log('Exporting user data'); + } + + function deleteAccount() { + if (confirm('Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est irréversible.')) { + console.log('Deleting account'); + } + } +</script> + +<section id="settings"> + <div class="settings-container"> + <div class="settings-header"> + <h1>Paramètres</h1> + </div> + + <div class="settings-layout"> + <div class="settings-section card"> + <h2>👤 Profil utilisateur</h2> + <form on:submit|preventDefault={saveProfile}> + <div class="form-row"> + <div class="form-group"> + <label for="name">Nom complet</label> + <input type="text" id="name" bind:value={userProfile.name} /> + </div> + <div class="form-group"> + <label for="email">Email</label> + <input type="email" id="email" bind:value={userProfile.email} /> + </div> + </div> + <div class="form-row"> + <div class="form-group"> + <label for="company">Entreprise</label> + <input type="text" id="company" bind:value={userProfile.company} /> + </div> + <div class="form-group"> + <label for="phone">Téléphone</label> + <input type="tel" id="phone" bind:value={userProfile.phone} /> + </div> + </div> + <button type="submit" class="btn">💾 Sauvegarder le profil</button> + </form> + </div> + + <div class="settings-section card"> + <h2>🔔 Notifications</h2> + <form on:submit|preventDefault={saveNotifications}> + <div class="checkbox-group"> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={notifications.emailAlerts} /> + <span>Alertes par email</span> + </label> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={notifications.portfolioUpdates} /> + <span>Mises à jour du portefeuille</span> + </label> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={notifications.esgAlerts} /> + <span>Alertes ESG</span> + </label> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={notifications.marketNews} /> + <span>Actualités du marché</span> + </label> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={notifications.weeklyReports} /> + <span>Rapports hebdomadaires</span> + </label> + </div> + <button type="submit" class="btn">💾 Sauvegarder les notifications</button> + </form> + </div> + + <div class="settings-section card"> + <h2>🔒 Sécurité</h2> + <form on:submit|preventDefault={saveSecurity}> + <div class="form-group"> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={security.twoFactorAuth} /> + <span>Authentification à deux facteurs</span> + </label> + </div> + <div class="form-group"> + <label for="sessionTimeout">Délai d'expiration de session (minutes)</label> + <select id="sessionTimeout" bind:value={security.sessionTimeout}> + <option value="15">15 minutes</option> + <option value="30">30 minutes</option> + <option value="60">1 heure</option> + <option value="120">2 heures</option> + </select> + </div> + <div class="form-group"> + <label class="checkbox-label"> + <input type="checkbox" bind:checked={security.dataEncryption} disabled /> + <span>Chiffrement des données (toujours activé)</span> + </label> + </div> + <button type="submit" class="btn">💾 Sauvegarder la sécurité</button> + </form> + + <div class="password-section"> + <h3>Changer le mot de passe</h3> + <form on:submit|preventDefault={changePassword}> + <div class="form-group"> + <label for="currentPassword">Mot de passe actuel</label> + <input type="password" id="currentPassword" bind:value={currentPassword} /> + </div> + <div class="form-group"> + <label for="newPassword">Nouveau mot de passe</label> + <input type="password" id="newPassword" bind:value={newPassword} /> + </div> + <div class="form-group"> + <label for="confirmPassword">Confirmer le nouveau mot de passe</label> + <input type="password" id="confirmPassword" bind:value={confirmPassword} /> + </div> + <button type="submit" class="btn">🔑 Changer le mot de passe</button> + </form> + </div> + </div> + + <div class="settings-section card"> + <h2>🎨 Affichage</h2> + <form on:submit|preventDefault={saveDisplay}> + <div class="form-row"> + <div class="form-group"> + <label for="theme">Thème</label> + <select id="theme" bind:value={display.theme}> + <option value="dark">Sombre</option> + <option value="light">Clair</option> + <option value="auto">Automatique</option> + </select> + </div> + <div class="form-group"> + <label for="language">Langue</label> + <select id="language" bind:value={display.language}> + <option value="fr">Français</option> + <option value="en">English</option> + <option value="es">Español</option> + </select> + </div> + </div> + <div class="form-row"> + <div class="form-group"> + <label for="currency">Devise</label> + <select id="currency" bind:value={display.currency}> + <option value="EUR">EUR (€)</option> + <option value="USD">USD ($)</option> + <option value="GBP">GBP (£)</option> + </select> + </div> + <div class="form-group"> + <label for="dateFormat">Format de date</label> + <select id="dateFormat" bind:value={display.dateFormat}> + <option value="DD/MM/YYYY">DD/MM/YYYY</option> + <option value="MM/DD/YYYY">MM/DD/YYYY</option> + <option value="YYYY-MM-DD">YYYY-MM-DD</option> + </select> + </div> + </div> + <button type="submit" class="btn">💾 Sauvegarder l'affichage</button> + </form> + </div> + + <div class="settings-section card"> + <h2>📊 Gestion des données</h2> + <div class="data-actions"> + <div class="action-item"> + <div class="action-info"> + <h3>Exporter mes données</h3> + <p>Téléchargez toutes vos données personnelles dans un fichier JSON</p> + </div> + <button class="btn" on:click={exportData}>📥 Exporter</button> + </div> + <div class="action-item danger"> + <div class="action-info"> + <h3>Supprimer mon compte</h3> + <p>Supprimez définitivement votre compte et toutes vos données</p> + </div> + <button class="btn danger" on:click={deleteAccount}>🗑️ Supprimer</button> + </div> + </div> + </div> + </div> + </div> +</section> + +<style> + #settings { + padding: 16px; + background-color: var(--bg-secondary); + color: white; + min-height: calc(100vh - 72px); + } + + .settings-container { + max-width: 1200px; + } + + .settings-header { + margin-bottom: 16px; + } + + h1 { + color: var(--text-lime); + font-size: 32px; + margin: 0; + font-weight: 600; + } + + h2 { + color: var(--text-lime); + font-size: 24px; + margin: 0 0 20px 0; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; + } + + h3 { + color: white; + font-size: 18px; + margin: 20px 0 12px 0; + font-weight: 500; + } + + .settings-layout { + display: flex; + flex-direction: column; + gap: 0; + } + + .settings-section { + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 16px; + color: white; + width: calc(100% - 32px); + } + + .form-row { + display: flex; + gap: 20px; + margin-bottom: 16px; + } + + .form-group { + flex: 1; + margin-bottom: 16px; + } + + label { + display: block; + margin-bottom: 8px; + font-size: 14px; + color: white; + font-weight: 500; + } + + input[type="text"], + input[type="email"], + input[type="tel"], + input[type="password"], + select { + width: 100%; + background-color: var(--bg-secondary); + border: none; + border-bottom: 2px solid var(--text-lime); + padding: 8px; + border-radius: 8px 8px 0 0; + color: white; + font-size: 14px; + box-sizing: border-box; + } + + input:focus, + select:focus { + outline: none; + border-bottom-color: var(--text-lime); + } + + input::placeholder { + color: #888888; + } + + .checkbox-group { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 20px; + } + + .checkbox-label { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + font-size: 14px; + } + + input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--text-lime); + } + + input[type="checkbox"]:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + text-decoration: none; + transition-duration: 0.2s; + font-weight: 600; + font-size: 14px; + } + + .btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); + } + + .btn.danger { + background-color: #dc2626; + } + + .btn.danger:hover { + background-color: #b91c1c; + } + + .password-section { + border-top: 1px solid var(--btn-primary); + padding-top: 20px; + margin-top: 20px; + } + + .data-actions { + display: flex; + flex-direction: column; + gap: 20px; + } + + .action-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + background-color: var(--bg-secondary); + border-radius: 8px; + } + + .action-item.danger { + border-left: 4px solid #dc2626; + } + + .action-info h3 { + margin: 0 0 4px 0; + font-size: 16px; + } + + .action-info p { + margin: 0; + font-size: 14px; + color: #888888; + } + + @media (max-width: 768px) { + .form-row { + flex-direction: column; + } + + .action-item { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + } +</style> diff --git a/ping/frontend/src/routes/dashboard/transactions/+page.svelte b/ping/frontend/src/routes/dashboard/transactions/+page.svelte new file mode 100644 index 0000000..86f9292 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/transactions/+page.svelte @@ -0,0 +1,360 @@ +<script lang="ts"> + import TransactionModal from '$lib/components/dashboard/transactions/TransactionModal.svelte'; + import { authFetch } from '$lib/stores/auth'; + import { addToast } from '$lib/stores/toast'; + import { onMount } from 'svelte'; + + interface ITransaction { + id: string; + label: string | null; + amount: number; + currency: string; + receiverLabel: string | null; + receiverIban: string; + operationDate: Date; + eco_score: number; + creationDate: Date; + createrId: string; + } + + let searchQuery = $state(''); + let sortBy = $state('Date'); + let sortOrder = $state('Montant'); + let transactions: ITransaction[] = $state([]); + let isCreateDialogOpened = $state(false); + + let visibleTransactions = $state<ITransaction[]>([]); + + $effect(() => { + const query = searchQuery.toLowerCase(); + visibleTransactions = transactions.filter((t) => { + const matchesLabel = t.label?.toLowerCase().includes(query); + const matchesReceiver = t.receiverLabel?.toLowerCase().includes(query); + const receiverIban = t.receiverIban?.toLowerCase().includes(query); + return matchesLabel || matchesReceiver; + }); + }); + + function handleSearch() { + console.log('Searching for:', searchQuery); + } + + function applyFilters() { + console.log('Applying filters:', { sortBy, sortOrder }); + } + + function analyzeTransaction(transaction: any) { + console.log('Analyzing transaction', transaction); + } + + async function getAllTransactions(): Promise<ITransaction[]> { + const res = await authFetch('/api/transactions', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + const data = await res.json(); + + if (!res.ok) { + addToast({ message: data.message, color: 'red', title: 'Erreur' }); + return []; + } + + return data.transactions as ITransaction[]; + } + + function onCreate(tr: ITransaction) { + getAllTransactions().then((transactionsData) => { + if (transactionsData) { + transactions = transactionsData; + } + }); + } + + onMount(() => { + getAllTransactions().then((transactionsData) => { + if (transactionsData) { + transactions = transactionsData; + visibleTransactions = transactionsData; + console.log(transactions); + } + }); + }); +</script> + +<section id="transactions"> + <div class="transactions-container"> + <div class="transactions-header"> + <h1>Transactions</h1> + <div class="search-bar"> + <input + type="text" + placeholder="Recherche..." + bind:value={searchQuery} + oninput={handleSearch} + /> + <button class="search-btn">🔍</button> + </div> + </div> + + <div class="filters-section"> + <div class="filters-left"> + <span class="filter-label">Trier</span> + <div class="filter-group"> + <select bind:value={sortBy} class="filter-select"> + <option value="Date">Date</option> + <option value="Amount">Montant</option> + <option value="Company">Entreprise</option> + </select> + <select bind:value={sortOrder} class="filter-select"> + <option value="Montant">Montant</option> + <option value="ASC">Croissant</option> + <option value="DESC">Décroissant</option> + </select> + </div> + </div> + <div> + <button + class="apply-btn" + onclick={() => { + isCreateDialogOpened = true; + }} + > + ➕ Créer + </button> + <button class="apply-btn" onclick={applyFilters}> 📁 Appliquer </button> + </div> + </div> + + <div class="transactions-list"> + {#each visibleTransactions as transaction} + <div class="transaction-card"> + <div class="transaction-main"> + <div class="transaction-info"> + <h3 class="transaction-type">{transaction.label}</h3> + <p class="transaction-company"> + {transaction.receiverIban} • {transaction.receiverLabel} + </p> + </div> + <div class="transaction-metrics"> + <div class="metric"> + <span class="metric-label">CO2</span> + <span class="metric-value co2">{transaction.eco_score}</span> + </div> + <div class="metric"> + <span class="metric-label">{transaction.currency}</span> + <span class="metric-value amount">{transaction.amount}</span> + </div> + </div> + <button class="analyze-btn" onclick={() => analyzeTransaction(transaction)}> + ✏️ Analyser + </button> + </div> + </div> + {/each} + </div> + </div> +</section> +<TransactionModal bind:isOpen={isCreateDialogOpened} {onCreate} /> + +<style> + #transactions { + padding: 16px; + background-color: var(--bg-secondary); + color: white; + min-height: calc(100vh - 72px); + } + + .transactions-container { + max-width: 1200px; + } + + .transactions-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + } + + h1 { + color: var(--text-lime); + font-size: 32px; + margin: 0; + font-weight: 600; + } + + .search-bar { + display: flex; + align-items: center; + background-color: var(--bg-primary); + border-radius: 8px; + padding: 4px; + gap: 8px; + } + + .search-bar input { + background: transparent; + border: none; + color: white; + padding: 12px 16px; + font-size: 14px; + outline: none; + width: 300px; + } + + .search-bar input::placeholder { + color: #888888; + } + + .search-btn { + background: transparent; + border: none; + color: #888888; + padding: 8px; + cursor: pointer; + font-size: 16px; + } + + /* Filters */ + .filters-section { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding: 16px; + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .filters-left { + display: flex; + align-items: center; + gap: 15px; + } + + .filter-label { + color: white; + font-weight: 500; + font-size: 14px; + } + + .filter-group { + display: flex; + gap: 10px; + } + + .filter-select { + background-color: var(--bg-secondary); + color: white; + border: none; + border-bottom: 2px solid var(--text-lime); + border-radius: 8px 8px 0 0; + padding: 8px 12px; + font-size: 14px; + cursor: pointer; + } + + .filter-select:focus { + outline: none; + border-bottom-color: var(--text-lime); + } + + .apply-btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-weight: 600; + font-size: 14px; + transition-duration: 0.2s; + } + + .apply-btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); + } + + .transactions-list { + display: flex; + flex-direction: column; + gap: 0; + } + + .transaction-card { + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 16px; + color: white; + width: calc(100% - 32px); + } + + .transaction-main { + display: flex; + align-items: center; + justify-content: space-between; + } + + .transaction-info h3 { + margin: 0 0 5px 0; + font-size: 18px; + font-weight: 600; + color: white; + } + + .transaction-company { + margin: 0; + font-size: 14px; + color: #888888; + } + + .transaction-metrics { + display: flex; + gap: 40px; + } + + .metric { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + } + + .metric-label { + font-size: 12px; + color: #888888; + font-weight: 500; + } + + .metric-value { + font-size: 24px; + font-weight: bold; + } + + .metric-value.co2, + .metric-value.amount { + color: var(--text-lime); + } + + .analyze-btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition-duration: 0.2s; + } + + .analyze-btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); + } +</style> |
