From 8cd9e7568206981980445d0ec0bb6c38f3639bb6 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 24 Aug 2026 12:59:36 +0000 Subject: [PATCH] Ajout couts par service (cloud, site, mail, user) + resume admin detaille HT --- backend/src/db.js | 14 ++++++ backend/src/index.js | 8 +-- frontend/src/components/Dashboard.jsx | 70 +++++++++++++++++++++++--- frontend/src/components/DomainForm.jsx | 33 +++++++++--- 4 files changed, 107 insertions(+), 18 deletions(-) diff --git a/backend/src/db.js b/backend/src/db.js index ed29a4c..9b21446 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -35,12 +35,26 @@ db.exec(` team_members TEXT DEFAULT '[]', notes TEXT DEFAULT '', monthly_price REAL DEFAULT 0, + cloud_cost REAL DEFAULT 0, + website_cost REAL DEFAULT 0, + email_cost REAL DEFAULT 0, + user_cost REAL DEFAULT 0, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (parent_id) REFERENCES domains(id) ); `); +// Migration: add cost columns if missing +const columns = db.prepare('PRAGMA table_info(domains)').all().map(c => c.name); +const addCostCol = (col) => { + if (!columns.includes(col)) db.exec('ALTER TABLE domains ADD COLUMN ' + col + ' REAL DEFAULT 0'); +}; +addCostCol('cloud_cost'); +addCostCol('website_cost'); +addCostCol('email_cost'); +addCostCol('user_cost'); + const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get().count; if (userCount === 0) { const insert = db.prepare('INSERT INTO users (username, password_hash, name, role) VALUES (?, ?, ?, ?)'); diff --git a/backend/src/index.js b/backend/src/index.js index 2c31350..7987384 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -23,7 +23,7 @@ app.get('/api/domains', authMiddleware, (req, res) => { const parent = domains.find(d => !d.parent_id); const children = domains.filter(d => d.parent_id); if (req.user.role === 'client') { - domains.forEach(d => { delete d.monthly_price; }); + domains.forEach(d => { delete d.monthly_price; delete d.cloud_cost; delete d.website_cost; delete d.email_cost; delete d.user_cost; }); } res.json({ parent, children }); }); @@ -31,7 +31,7 @@ app.get('/api/domains', authMiddleware, (req, res) => { app.get('/api/domains/:id', authMiddleware, (req, res) => { const domain = db.prepare('SELECT * FROM domains WHERE id = ?').get(req.params.id); if (!domain) return res.status(404).json({ error: 'Domaine non trouve' }); - if (req.user.role === 'client') delete domain.monthly_price; + if (req.user.role === 'client') { delete domain.monthly_price; delete domain.cloud_cost; delete domain.website_cost; delete domain.email_cost; delete domain.user_cost; } res.json(domain); }); @@ -44,7 +44,7 @@ app.put('/api/domains/:id', authMiddleware, (req, res) => { 'website_managed_by', 'website_details', 'has_emails', 'email_count', 'email_addresses', 'has_team', 'team_members', 'notes' ]; - if (req.user.role === 'admin') fields.push('monthly_price'); + if (req.user.role === 'admin') fields.push('monthly_price', 'cloud_cost', 'website_cost', 'email_cost', 'user_cost'); const updates = {}; fields.forEach(f => { if (req.body[f] !== undefined) updates[f] = req.body[f]; }); @@ -56,7 +56,7 @@ app.put('/api/domains/:id', authMiddleware, (req, res) => { db.prepare('UPDATE domains SET ' + setClause + ' WHERE id = ?').run(...values); const updated = db.prepare('SELECT * FROM domains WHERE id = ?').get(req.params.id); - if (req.user.role === 'client') delete updated.monthly_price; + if (req.user.role === 'client') { delete updated.monthly_price; delete updated.cloud_cost; delete updated.website_cost; delete updated.email_cost; delete updated.user_cost; } res.json(updated); }); diff --git a/frontend/src/components/Dashboard.jsx b/frontend/src/components/Dashboard.jsx index 411c3c3..89d3a7b 100644 --- a/frontend/src/components/Dashboard.jsx +++ b/frontend/src/components/Dashboard.jsx @@ -40,13 +40,21 @@ export default function Dashboard({ token, user }) {
{tags}
{user.role === 'admin' && d.monthly_price > 0 && (
- {d.monthly_price} EUR/mois + {d.monthly_price} EUR/mois HT
)} ) } + // Admin cost summary + const allDomains = [domains.parent, ...domains.children].filter(Boolean) + const totalCloud = allDomains.reduce((s, d) => s + (d.has_cloud ? (d.cloud_cost || 0) : 0), 0) + const totalWebsite = allDomains.reduce((s, d) => s + (d.has_website ? (d.website_cost || 0) : 0), 0) + const totalEmail = allDomains.reduce((s, d) => s + (d.has_emails ? (d.email_count || 0) * (d.email_cost || 0) : 0), 0) + const totalUsers = allDomains.reduce((s, d) => s + (d.has_team ? JSON.parse(d.team_members || '[]').length * (d.user_cost || 0) : 0), 0) + const totalAll = totalCloud + totalWebsite + totalEmail + totalUsers + return (
@@ -63,13 +71,59 @@ export default function Dashboard({ token, user }) {
{user.role === 'admin' && ( -
-

Resume

-
-

{domains.children.length + 1}

Domaines

-

{[domains.parent, ...domains.children].filter(d => d.has_cloud).length}

Cloud

-

{[domains.parent, ...domains.children].filter(d => d.has_website).length}

Sites web

-

{[domains.parent, ...domains.children].filter(d => d.has_emails).length}

Avec mails

+
+ {/* Vue d'ensemble */} +
+

Resume

+
+

{allDomains.length}

Domaines

+

{allDomains.filter(d => d.has_cloud).length}

Cloud

+

{allDomains.filter(d => d.has_website).length}

Sites web

+

{allDomains.filter(d => d.has_emails).length}

Avec mails

+
+
+ + {/* Detail des couts */} +
+

Detail des couts mensuels HT

+
+
+
+
+ Cloud + ({allDomains.filter(d => d.has_cloud).length}) +
+ {totalCloud.toFixed(2)} EUR +
+
+
+
+ Sites web + ({allDomains.filter(d => d.has_website).length}) +
+ {totalWebsite.toFixed(2)} EUR +
+
+
+
+ Adresses mail + ({allDomains.reduce((s, d) => s + (d.has_emails ? d.email_count || 0 : 0), 0)}) +
+ {totalEmail.toFixed(2)} EUR +
+
+
+
+ Utilisateurs + ({allDomains.reduce((s, d) => s + (d.has_team ? JSON.parse(d.team_members || '[]').length : 0), 0)}) +
+ {totalUsers.toFixed(2)} EUR +
+
+ Total mensuel HT + {totalAll.toFixed(2)} EUR/mois +
+
)} diff --git a/frontend/src/components/DomainForm.jsx b/frontend/src/components/DomainForm.jsx index 5511213..61b7f7b 100644 --- a/frontend/src/components/DomainForm.jsx +++ b/frontend/src/components/DomainForm.jsx @@ -39,6 +39,8 @@ export default function DomainForm({ token, user }) { if (loading) return
Chargement...
if (!d) return
Domaine non trouve
+ const isAdmin = user.role === 'admin' + const Toggle = ({ checked, onChange, label }) => (
{label} @@ -64,6 +66,19 @@ export default function DomainForm({ token, user }) {
) + const CostInput = ({ label, field }) => ( +
+ +
+ u(field, parseFloat(e.target.value) || 0)} placeholder="0" + className="w-24 px-3 py-2 bg-slate-900/50 border border-slate-600 rounded-lg text-emerald-400 text-sm text-right placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono" /> + EUR/mois HT +
+
+ ) + + const nbMembers = d.has_team ? JSON.parse(d.team_members || '[]').length : 0 + return (