From f5c0a45d5c726b2c318440ff5f767fd7bb04e65b Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 24 Aug 2026 13:57:53 +0000 Subject: [PATCH] Suppression doublons monsieur-edgar.com + momentz.paris, onglets centres --- frontend/src/App.jsx | 2 - frontend/src/components/Dashboard.jsx | 403 ++++++++++++++++++-------- 2 files changed, 281 insertions(+), 124 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d353fb4..745a8ae 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,7 +3,6 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import Login from './components/Login' import Layout from './components/Layout' import Dashboard from './components/Dashboard' -import DomainForm from './components/DomainForm' function App() { const [token, setToken] = useState(localStorage.getItem('charles_token')) @@ -29,7 +28,6 @@ function App() { } /> - } /> } /> diff --git a/frontend/src/components/Dashboard.jsx b/frontend/src/components/Dashboard.jsx index 06d8fe4..a48b501 100644 --- a/frontend/src/components/Dashboard.jsx +++ b/frontend/src/components/Dashboard.jsx @@ -1,159 +1,318 @@ -import { useState, useEffect } from 'react' -import { Link } from 'react-router-dom' +import { useState, useEffect, useRef } from 'react' const KSUITE_PRICE = 6.58 const SITE_PRICE = 69 export default function Dashboard({ token, user }) { - const [domains, setDomains] = useState({ parent: null, children: [] }) + const [domains, setDomains] = useState([]) + const [activeTab, setActiveTab] = useState(0) const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [saved, setSaved] = useState(false) + const saveTimer = useRef(null) useEffect(() => { fetch('/api/domains', { headers: { Authorization: 'Bearer ' + token } }) - .then(r => r.json()).then(d => { setDomains(d); setLoading(false) }) + .then(r => r.json()).then(d => { + const all = d.parent ? [d.parent, ...d.children] : d.children + setDomains(all) + setLoading(false) + }) }, [token]) - if (loading) return
Chargement...
- - const allDomains = [domains.parent, ...domains.children].filter(Boolean) - - const calcDomain = (d) => { - const team = d.has_team ? JSON.parse(d.team_members || '[]').length : 0 - const ksuite = team * KSUITE_PRICE - const site = d.has_website ? SITE_PRICE / 12 : 0 - const extra = (d.extra_email_count || 0) * (d.extra_email_cost || 0) - const doms = JSON.parse(d.domains_list || '[]').reduce((s, dm) => s + (dm.cost_per_year || 0) / 12, 0) - return { team, ksuite, site, extra, doms, total: ksuite + site + extra + doms } + const update = (id, field, val) => { + setDomains(prev => prev.map(d => d.id === id ? { ...d, [field]: val } : d)) + setSaved(false) + if (saveTimer.current) clearTimeout(saveTimer.current) + saveTimer.current = setTimeout(() => saveDomain(id), 1500) } - const totals = allDomains.reduce((acc, d) => { - const c = calcDomain(d) - return { - users: acc.users + c.team, - ksuite: acc.ksuite + c.ksuite, - site: acc.site + (d.has_website ? 1 : 0), - siteCost: acc.siteCost + c.site, - extra: acc.extra + (d.extra_email_count || 0), - extraCost: acc.extraCost + c.extra, - doms: acc.doms + JSON.parse(d.domains_list || '[]').length, - domsCost: acc.domsCost + c.doms, - total: acc.total + c.total - } - }, { users: 0, ksuite: 0, site: 0, siteCost: 0, extra: 0, extraCost: 0, doms: 0, domsCost: 0, total: 0 }) + const saveDomain = async (id) => { + const d = domains.find(x => x.id === id) + if (!d) return + setSaving(true) + await fetch('/api/domains/' + id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, + body: JSON.stringify(d) + }) + setSaving(false) + setSaved(true) + setTimeout(() => setSaved(false), 2000) + } - const count = (d) => [d.has_website, d.has_team, d.has_office, d.has_calendar].filter(Boolean).length - const color = (n) => n === 0 ? 'bg-slate-600' : n < 2 ? 'bg-amber-500' : 'bg-emerald-500' + const saveNow = () => { if (saveTimer.current) clearTimeout(saveTimer.current); saveDomain(domains[activeTab]?.id) } - const DomainCard = ({ d, badge }) => { - const n = count(d) - const team = d.has_team ? JSON.parse(d.team_members || '[]').length : 0 - const tags = [] - if (team > 0) tags.push({team} user{team > 1 ? 's' : ''}) - if (d.has_website) tags.push(Site{d.website_wordpress ? ' WP' : ''}) - if (d.has_office) tags.push(Office) - if (d.has_calendar) tags.push(Calendrier) - const doms = JSON.parse(d.domains_list || '[]') - if (doms.length > 0) tags.push({doms.length} domaine{doms.length > 1 ? 's' : ''}) - if (n === 0 && team === 0) tags.push(Aucun besoin renseigne) + const addToList = (id, field) => { + const d = domains.find(x => x.id === id) + const list = JSON.parse(d[field] || '[]') + list.push(field === 'team_members' ? { first_name: '', last_name: '' } : { name: '', cost_per_year: 0, expiry: '', hoster: '' }) + update(id, field, JSON.stringify(list)) + } - const cost = calcDomain(d) + const updateInList = (id, field, i, key, val) => { + const d = domains.find(x => x.id === id) + const list = JSON.parse(d[field] || '[]') + list[i][key] = val + update(id, field, JSON.stringify(list)) + } + + const removeFromList = (id, field, i) => { + const d = domains.find(x => x.id === id) + const list = JSON.parse(d[field] || '[]') + list.splice(i, 1) + update(id, field, JSON.stringify(list)) + } + + if (loading) return
Chargement...
+ if (domains.length === 0) return
Aucun domaine
+ + const d = domains[activeTab] + const team = JSON.parse(d.team_members || '[]') + const nbUsers = d.has_team ? team.length : 0 + const doms = JSON.parse(d.domains_list || '[]') + const isAdmin = user.role === 'admin' + + const ksuiteCost = nbUsers * KSUITE_PRICE + const siteCost = d.has_website ? SITE_PRICE / 12 : 0 + const extraCost = (d.extra_email_count || 0) * (d.extra_email_cost || 0) + const domsCost = doms.reduce((s, dm) => s + (dm.cost_per_year || 0) / 12, 0) + const total = ksuiteCost + siteCost + extraCost + domsCost + + const Row = ({ label, children }) => ( +
+ {label} +
{children}
+
+ ) + + const Txt = ({ value, onChange, placeholder }) => ( + onChange(e.target.value)} placeholder={placeholder} + className="w-full px-3 py-2 bg-slate-900/40 border border-slate-700/50 rounded-lg text-white text-sm placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-blue-500/50" /> + ) + + const Num = ({ value, onChange, placeholder, step }) => ( + onChange(parseFloat(e.target.value) || 0)} placeholder={placeholder} + className="w-28 px-3 py-2 bg-slate-900/40 border border-slate-700/50 rounded-lg text-white text-sm text-right placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-blue-500/50 font-mono" /> + ) + + const Toggle = ({ checked, onChange }) => ( + + ) + + const Select = ({ value, onChange, options }) => ( + + ) + + // Global summary for admin + const GlobalSummary = () => { + if (!isAdmin) return null + const all = domains.reduce((acc, dom) => { + const t = dom.has_team ? JSON.parse(dom.team_members || '[]').length : 0 + const dm = JSON.parse(dom.domains_list || '[]') + return { + users: acc.users + t, + sites: acc.sites + (dom.has_website ? 1 : 0), + doms: acc.doms + dm.length, + ksuite: acc.ksuite + t * KSUITE_PRICE, + site: acc.site + (dom.has_website ? SITE_PRICE / 12 : 0), + extra: acc.extra + (dom.extra_email_count || 0) * (dom.extra_email_cost || 0), + domCost: acc.domCost + dm.reduce((s, x) => s + (x.cost_per_year || 0) / 12, 0) + } + }, { users: 0, sites: 0, doms: 0, ksuite: 0, site: 0, extra: 0, domCost: 0 }) + const gTotal = all.ksuite + all.site + all.extra + all.domCost return ( - -
-
- {badge && Maitre} -
-

{d.company_name}

-

{d.name}

-
-
-
+
+

Resume global

+
+

{all.users}

Users

+

{all.ksuite.toFixed(0)}

Ksuite EUR/mois

+

{all.sites}

Sites

+

{all.doms}

Domaines

+

{gTotal.toFixed(2)}

Total EUR/mois HT

-
{tags}
- {user.role === 'admin' && cost.total > 0 && ( -
- {cost.total.toFixed(2)} EUR/mois HT -
- )} - +
) } return (
-
-

Domaines & Services

-

- {user.role === 'admin' ? 'Gerez les besoins de Charles pour chaque societe.' : 'Renseignez vos besoins pour chaque domaine.'} -

+ {/* Tabs */} +
+ {domains.map((dom, i) => ( + + ))}
- {domains.parent && } - -
- {domains.children.map(d => )} + {/* Save indicator */} +
+

{d.company_name} {d.name}

+
+ {saving && Sauvegarde...} + {saved && Sauvegarde} + {isAdmin && } +
- {user.role === 'admin' && ( -
-
-

Resume

-
-

{allDomains.length}

Domaines

-

{totals.users}

Utilisateurs

-

{totals.ksuite.toFixed(0)}

Licences Ksuite

-

{totals.site}

Sites web

-

{totals.doms}

Noms de domaine

-
+ {/* Form */} +
+ {/* Utilisateurs */} +
+
+ Utilisateurs +
+ {d.has_team && ( +
+ {team.map((m, i) => ( +
+ updateInList(d.id, 'team_members', i, 'first_name', e.target.value)} placeholder="Prenom" + className="flex-1 px-3 py-2 bg-slate-900/40 border border-slate-700/50 rounded-lg text-white text-sm placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-blue-500/50" /> + updateInList(d.id, 'team_members', i, 'last_name', e.target.value)} placeholder="Nom" + className="flex-1 px-3 py-2 bg-slate-900/40 border border-slate-700/50 rounded-lg text-white text-sm placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-blue-500/50" /> + +
+ ))} + +
+ )} +
-
-

Detail des couts mensuels HT

-
-
-
-
- Ksuite Pro ({totals.users} user{totals.users > 1 ? 's' : ''}) - 6,58 EUR/user -
- {totals.ksuite.toFixed(2)} EUR -
-
-
-
- Sites web ({totals.site}) - 69 EUR/an -
- {totals.siteCost.toFixed(2)} EUR -
- {totals.extra > 0 && ( -
-
-
- Emails extras ({totals.extra}) -
- {totals.extraCost.toFixed(2)} EUR -
- )} - {totals.doms > 0 && ( -
-
-
- Noms de domaine ({totals.doms}) -
- {totals.domsCost.toFixed(2)} EUR -
- )} -
- Total mensuel HT - {totals.total.toFixed(2)} EUR/mois +
+ + {/* Site internet */} +
+
+ Site internet 69 EUR/an + +
+ {d.has_website && ( +
+
+ WordPress + + Gere par + updateInList(d.id, 'domains_list', i, 'name', e.target.value)} placeholder="domaine.fr" + className="flex-1 px-3 py-2 bg-slate-900/40 border border-slate-700/50 rounded-lg text-white text-sm placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-blue-500/50" /> + updateInList(d.id, 'domains_list', i, 'cost_per_year', parseFloat(e.target.value) || 0)} placeholder="0" + className="w-24 px-3 py-2 bg-slate-900/40 border border-slate-700/50 rounded-lg text-white text-sm text-right placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-blue-500/50 font-mono" /> + updateInList(d.id, 'domains_list', i, 'expiry', e.target.value)} + className="w-28 px-3 py-2 bg-slate-900/40 border border-slate-700/50 rounded-lg text-white text-sm focus:outline-none focus:ring-1 focus:ring-blue-500/50" /> + updateInList(d.id, 'domains_list', i, 'hoster', e.target.value)} placeholder="Hebergeur" + className="w-24 px-3 py-2 bg-slate-900/40 border border-slate-700/50 rounded-lg text-white text-sm placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-blue-500/50" /> + +
+ ))} +
+ )} +
+ +
+ + {/* Notes */} +
+ Notes +