Refonte UX: toggles, sections, recap couts Home

This commit is contained in:
2026-08-24 14:06:55 +00:00
parent f8789e148d
commit 91b1f28610
+160 -158
View File
@@ -3,6 +3,17 @@ import { useState, useEffect } from 'react'
const KSUITE_PRICE = 6.58 const KSUITE_PRICE = 6.58
const SITE_PRICE = 69 const SITE_PRICE = 69
function getCosts(d) {
const team = JSON.parse(d.team_members || '[]')
const nbUsers = d.has_team ? team.length : 0
const doms = JSON.parse(d.domains_list || '[]')
const ksuite = nbUsers * KSUITE_PRICE
const site = d.has_website ? SITE_PRICE / 12 : 0
const extra = (d.extra_email_count || 0) * (d.extra_email_cost || 0)
const domCost = doms.reduce((s, dm) => s + (dm.cost_per_year || 0) / 12, 0)
return { nbUsers, doms, ksuite, site, extra, domCost, total: ksuite + site + extra + domCost }
}
export default function Dashboard({ token, user }) { export default function Dashboard({ token, user }) {
const [domains, setDomains] = useState([]) const [domains, setDomains] = useState([])
const [activeTab, setActiveTab] = useState(-1) const [activeTab, setActiveTab] = useState(-1)
@@ -35,8 +46,8 @@ export default function Dashboard({ token, user }) {
const update = (id, field, val) => { const update = (id, field, val) => {
setDomains(prev => prev.map(d => d.id === id ? { ...d, [field]: val } : d)) setDomains(prev => prev.map(d => d.id === id ? { ...d, [field]: val } : d))
const timeout = setTimeout(() => saveDomain(id), 1200) const t = setTimeout(() => saveDomain(id), 1200)
return () => clearTimeout(timeout) return () => clearTimeout(t)
} }
const addToList = (id, field) => { const addToList = (id, field) => {
@@ -73,8 +84,10 @@ export default function Dashboard({ token, user }) {
doms.forEach(dm => allDomains.push({ ...dm, society: d.company_name })) doms.forEach(dm => allDomains.push({ ...dm, society: d.company_name }))
}) })
const grandTotal = domains.reduce((s, d) => s + getCosts(d).total, 0)
const getColor = (expiry) => { const getColor = (expiry) => {
if (!expiry) return '' if (!expiry) return 'text-slate-500'
const diff = (new Date(expiry) - new Date()) / (1000 * 60 * 60 * 24) const diff = (new Date(expiry) - new Date()) / (1000 * 60 * 60 * 24)
if (diff < 0) return 'text-red-400' if (diff < 0) return 'text-red-400'
if (diff < 30) return 'text-amber-400' if (diff < 30) return 'text-amber-400'
@@ -82,86 +95,91 @@ export default function Dashboard({ token, user }) {
return 'text-emerald-400' return 'text-emerald-400'
} }
const hosterColors = { const hosterBadge = (h) => {
'OVH': 'bg-sky-500/20 text-sky-300', const m = { 'OVH': 'bg-sky-500/20 text-sky-300', 'Infomaniak': 'bg-emerald-500/20 text-emerald-300' }
'Infomaniak': 'bg-emerald-500/20 text-emerald-300', return m[h] || 'bg-slate-500/20 text-slate-300'
'Other': 'bg-slate-500/20 text-slate-300'
} }
return ( return (
<div> <div>
<h2 className="text-lg font-bold text-white mb-4">Tableau de bord</h2> {/* Recap par societe */}
<div className="bg-slate-800/30 rounded-2xl border border-slate-700/20 overflow-hidden"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3 mb-6">
<table className="w-full text-sm"> {domains.map((d, i) => {
<thead> const c = getCosts(d)
<tr className="border-b border-slate-700/30"> return (
<th className="text-left px-4 py-3 text-slate-400 font-medium">Societe</th> <button key={d.id} onClick={() => setActiveTab(i)}
<th className="text-left px-4 py-3 text-slate-400 font-medium">Domaine</th> className="bg-slate-800/30 rounded-xl border border-slate-700/20 p-4 text-left hover:border-blue-500/30 hover:bg-slate-800/50 transition group">
<th className="text-left px-4 py-3 text-slate-400 font-medium">Hebergeur</th> <p className="text-white font-medium text-sm group-hover:text-blue-400 transition">{d.company_name}</p>
<th className="text-left px-4 py-3 text-slate-400 font-medium">Expiration</th> <p className="text-slate-500 text-xs mb-3">{d.name}</p>
</tr> <p className="text-2xl font-bold text-emerald-400 font-mono">{c.total.toFixed(2)}<span className="text-xs text-slate-500 ml-1">EUR/mois</span></p>
</thead> <div className="flex gap-3 mt-2 text-[11px] text-slate-500">
<tbody> {c.nbUsers > 0 && <span>{c.nbUsers} user{c.nbUsers > 1 ? 's' : ''}</span>}
{allDomains.length === 0 && ( {c.doms.length > 0 && <span>{c.doms.length} domaine{c.doms.length > 1 ? 's' : ''}</span>}
<tr><td colSpan={4} className="px-4 py-6 text-slate-500 text-center">Aucun domaine declare</td></tr> {d.has_website && <span>Site</span>}
)} </div>
{allDomains.map((dm, i) => ( </button>
<tr key={i} className="border-b border-slate-700/20 hover:bg-slate-800/40 transition"> )
<td className="px-4 py-3 text-white">{dm.society}</td> })}
<td className="px-4 py-3 text-slate-300 font-mono text-xs">{dm.name || '—'}</td>
<td className="px-4 py-3">
<span className={'px-2 py-0.5 rounded-md text-xs ' + (hosterColors[dm.hoster] || hosterColors['Other'])}>
{dm.hoster || '—'}
</span>
</td>
<td className={'px-4 py-3 font-mono text-xs ' + getColor(dm.expiry)}>
{dm.expiry || '—'}
</td>
</tr>
))}
</tbody>
</table>
</div> </div>
{/* Grand total */}
{isAdmin && (
<div className="bg-slate-800/30 rounded-xl border border-emerald-500/20 p-4 mb-6 text-center">
<p className="text-slate-400 text-sm">Total general</p>
<p className="text-3xl font-bold text-emerald-400 font-mono">{grandTotal.toFixed(2)}<span className="text-sm text-slate-500 ml-1">EUR/mois HT</span></p>
</div>
)}
{/* Tableau domaines */}
{allDomains.length > 0 && (
<div>
<h3 className="text-white text-sm font-semibold mb-3">Domaines</h3>
<div className="bg-slate-800/30 rounded-2xl border border-slate-700/20 overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-700/30">
<th className="text-left px-4 py-3 text-slate-400 font-medium">Societe</th>
<th className="text-left px-4 py-3 text-slate-400 font-medium">Domaine</th>
<th className="text-left px-4 py-3 text-slate-400 font-medium">Hebergeur</th>
<th className="text-left px-4 py-3 text-slate-400 font-medium">Expiration</th>
</tr>
</thead>
<tbody>
{allDomains.map((dm, i) => (
<tr key={i} className="border-b border-slate-700/20 hover:bg-slate-800/40 transition">
<td className="px-4 py-3 text-white">{dm.society}</td>
<td className="px-4 py-3 text-slate-300 font-mono text-xs">{dm.name || '—'}</td>
<td className="px-4 py-3"><span className={'px-2 py-0.5 rounded-md text-xs ' + hosterBadge(dm.hoster)}>{dm.hoster || '—'}</span></td>
<td className={'px-4 py-3 font-mono text-xs ' + getColor(dm.expiry)}>{dm.expiry || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div> </div>
) )
} }
// DOMAIN FORM VIEW // DOMAIN FORM
const DomainForm = ({ d }) => { const DomainForm = ({ d }) => {
const team = JSON.parse(d.team_members || '[]') const c = getCosts(d)
const nbUsers = d.has_team ? team.length : 0
const doms = JSON.parse(d.domains_list || '[]')
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 }) => (
<div className="flex items-center gap-4 py-2.5 border-b border-slate-700/20">
<span className="text-slate-400 text-sm w-44 shrink-0">{label}</span>
<div className="flex-1">{children}</div>
</div>
)
const Txt = ({ value, onChange, placeholder }) => ( const Txt = ({ value, onChange, placeholder }) => (
<input type="text" value={value || ''} onChange={e => onChange(e.target.value)} placeholder={placeholder} <input type="text" value={value || ''} onChange={e => 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" /> 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 }) => ( const Num = ({ value, onChange, placeholder, step }) => (
<input type="number" step={step || '1'} value={value || ''} onChange={e => onChange(parseFloat(e.target.value) || 0)} placeholder={placeholder} <input type="number" step={step || '1'} value={value || ''} onChange={e => 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" /> 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 Toggle = ({ checked, onChange, activeText, inactiveText, color }) => (
<button type="button" onClick={() => onChange(!checked ? 1 : 0)} <button type="button" onClick={() => onChange(!checked ? 1 : 0)}
className={'text-xs px-2 py-1 rounded-md transition ' + (checked ? 'bg-' + color + '-500/20 text-' + color + '-400' : 'bg-slate-700/50 text-slate-500')}> className={'relative inline-flex h-5 w-9 items-center rounded-full transition-colors ' + (checked ? 'bg-blue-600' : 'bg-slate-600')}>
{checked ? activeText : inactiveText} <span className={'inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform ' + (checked ? 'translate-x-4' : 'translate-x-1')} />
</button> </button>
) )
const Select = ({ value, onChange, options }) => ( const Select = ({ value, onChange, options }) => (
<select value={value || ''} onChange={e => onChange(e.target.value)} <select value={value || ''} onChange={e => onChange(e.target.value)}
className="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"> className="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">
@@ -170,109 +188,82 @@ export default function Dashboard({ token, user }) {
) )
return ( return (
<div className="bg-slate-800/30 rounded-2xl border border-slate-700/20 p-5"> <div className="space-y-6">
{/* Utilisateurs */} {/* Utilisateurs */}
<div className="mb-5"> <Section title="Utilisateurs" desc={c.nbUsers > 0 ? c.nbUsers + ' licence' + (c.nbUsers > 1 ? 's' : '') + ' Ksuite' : ''}>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between">
<span className="text-white text-sm font-medium">Utilisateurs</span> <span className="text-slate-300 text-sm">Activer les utilisateurs</span>
<button onClick={() => update(d.id, 'has_team', d.has_team ? 0 : 1)} <Toggle checked={d.has_team} onChange={v => update(d.id, 'has_team', v)} />
className={'text-xs px-2 py-1 rounded-md transition ' + (d.has_team ? 'bg-emerald-500/20 text-emerald-400' : 'bg-slate-700/50 text-slate-500')}>
{d.has_team ? 'Actif' : 'Inactif'}
</button>
</div> </div>
{d.has_team && ( {d.has_team && (
<div className="ml-0 space-y-1.5"> <div className="mt-3 space-y-2">
{team.map((m, i) => ( {c.team.map((m, i) => (
<div key={i} className="flex gap-2 items-center"> <div key={i} className="flex gap-2 items-center">
<input type="text" value={m.first_name} onChange={e => updateInList(d.id, 'team_members', i, 'first_name', e.target.value)} placeholder="Prenom" <input type="text" value={m.first_name} onChange={e => 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" /> 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" />
<input type="text" value={m.last_name} onChange={e => updateInList(d.id, 'team_members', i, 'last_name', e.target.value)} placeholder="Nom" <input type="text" value={m.last_name} onChange={e => 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" /> 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" />
<button onClick={() => removeFromList(d.id, 'team_members', i)} className="text-red-400/60 hover:text-red-400 p-1 transition">x</button> <button onClick={() => removeFromList(d.id, 'team_members', i)} className="text-slate-500 hover:text-red-400 text-lg px-1 transition">&times;</button>
</div> </div>
))} ))}
<button onClick={() => addToList(d.id, 'team_members')} className="text-blue-400 hover:text-blue-300 text-xs">+ Ajouter</button> <button onClick={() => addToList(d.id, 'team_members')} className="text-blue-400 hover:text-blue-300 text-sm">+ Ajouter un utilisateur</button>
</div> </div>
)} )}
</div> </Section>
<hr className="border-slate-700/30 my-4" />
{/* Site internet */} {/* Site internet */}
<div className="mb-5"> <Section title="Site internet" desc={d.has_website ? SITE_PRICE + ' EUR/an' : ''}>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between">
<span className="text-white text-sm font-medium">Site internet <span className="text-slate-500 text-xs font-normal">69 EUR/an</span></span> <span className="text-slate-300 text-sm">Site actif</span>
<button onClick={() => update(d.id, 'has_website', d.has_website ? 0 : 1)} <Toggle checked={d.has_website} onChange={v => update(d.id, 'has_website', v)} />
className={'text-xs px-2 py-1 rounded-md transition ' + (d.has_website ? 'bg-violet-500/20 text-violet-400' : 'bg-slate-700/50 text-slate-500')}>
{d.has_website ? 'Actif' : 'Inactif'}
</button>
</div> </div>
{d.has_website && ( {d.has_website && (
<div className="ml-0 space-y-2"> <div className="mt-3 space-y-3">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-slate-400 text-sm">WordPress</span> <span className="text-slate-400 text-sm">WordPress</span>
<button onClick={() => update(d.id, 'website_wordpress', d.website_wordpress ? 0 : 1)} <Toggle checked={d.website_wordpress} onChange={v => update(d.id, 'website_wordpress', v)} />
className={'text-xs px-2 py-1 rounded-md transition ' + (d.website_wordpress ? 'bg-violet-500/20 text-violet-400' : 'bg-slate-700/50 text-slate-500')}> <span className="text-slate-400 text-sm ml-2">Gere par</span>
{d.website_wordpress ? 'Oui' : 'Non'}
</button>
<span className="text-slate-400 text-sm ml-4">Gere par</span>
<Select value={d.website_managed_by} onChange={v => update(d.id, 'website_managed_by', v)} <Select value={d.website_managed_by} onChange={v => update(d.id, 'website_managed_by', v)}
options={[{v:'',l:'?'},{v:'charles',l:'Charles'},{v:'pierre',l:'Pierre'}]} /> options={[{v:'',l:'—'},{v:'charles',l:'Charles'},{v:'pierre',l:'Pierre'}]} />
</div> </div>
<Txt value={d.website_details} onChange={v => update(d.id, 'website_details', v)} placeholder="Details du site..." /> <Txt value={d.website_details} onChange={v => update(d.id, 'website_details', v)} placeholder="Details du site..." />
</div> </div>
)} )}
</div> </Section>
<hr className="border-slate-700/30 my-4" />
{/* Emails */} {/* Emails */}
<div className="mb-5"> <Section title="Emails" desc={c.nbUsers * 5 + ' inclus via Ksuite'}>
<span className="text-white text-sm font-medium">Emails <span className="text-slate-500 text-xs font-normal">{nbUsers * 5} inclus via Ksuite</span></span> <div className="flex items-center gap-4">
<div className="flex items-center gap-4 mt-2">
<span className="text-slate-400 text-sm">Supplementaires</span> <span className="text-slate-400 text-sm">Supplementaires</span>
<Num value={d.extra_email_count} onChange={v => update(d.id, 'extra_email_count', v)} placeholder="0" /> <Num value={d.extra_email_count} onChange={v => update(d.id, 'extra_email_count', v)} placeholder="0" />
<span className="text-slate-400 text-sm">Cout/unit EUR/mois</span> <span className="text-slate-400 text-sm">Cout/unit EUR/mois</span>
<Num value={d.extra_email_cost} onChange={v => update(d.id, 'extra_email_cost', v)} placeholder="0" step="0.01" /> <Num value={d.extra_email_cost} onChange={v => update(d.id, 'extra_email_cost', v)} placeholder="0" step="0.01" />
</div> </div>
</div> </Section>
<hr className="border-slate-700/30 my-4" />
{/* Office + Calendrier */} {/* Office + Calendrier */}
<div className="mb-5"> <Section title="Services complementaires" desc="">
<div className="flex items-center gap-6"> <div className="flex items-center gap-8">
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<span className="text-white text-sm font-medium">Pack Office</span> <span className="text-slate-300 text-sm">Pack Office</span>
<button onClick={() => update(d.id, 'has_office', d.has_office ? 0 : 1)} <Toggle checked={d.has_office} onChange={v => update(d.id, 'has_office', v)} />
className={'text-xs px-2 py-1 rounded-md transition ' + (d.has_office ? 'bg-rose-500/20 text-rose-400' : 'bg-slate-700/50 text-slate-500')}>
{d.has_office ? 'Oui' : 'Non'}
</button>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<span className="text-white text-sm font-medium">Calendrier partage</span> <span className="text-slate-300 text-sm">Calendrier partage</span>
<button onClick={() => update(d.id, 'has_calendar', d.has_calendar ? 0 : 1)} <Toggle checked={d.has_calendar} onChange={v => update(d.id, 'has_calendar', v)} />
className={'text-xs px-2 py-1 rounded-md transition ' + (d.has_calendar ? 'bg-teal-500/20 text-teal-400' : 'bg-slate-700/50 text-slate-500')}>
{d.has_calendar ? 'Oui' : 'Non'}
</button>
</div> </div>
</div> </div>
</div> </Section>
<hr className="border-slate-700/30 my-4" /> {/* Domaines */}
<Section title="Noms de domaine" desc={c.doms.length + ' domaine' + (c.doms.length > 1 ? 's' : '')}>
{/* Noms de domaine */} <button onClick={() => addToList(d.id, 'domains_list')} className="text-blue-400 hover:text-blue-300 text-sm mb-3">+ Ajouter un domaine</button>
<div className="mb-5"> {c.doms.length > 0 && (
<div className="flex items-center justify-between mb-2"> <div className="space-y-2">
<span className="text-white text-sm font-medium">Noms de domaine</span> <div className="flex gap-2 text-[10px] text-slate-600 uppercase tracking-wider px-1">
<button onClick={() => addToList(d.id, 'domains_list')} className="text-blue-400 hover:text-blue-300 text-xs">+ Ajouter</button>
</div>
{doms.length > 0 && (
<div className="space-y-1.5">
<div className="flex gap-2 text-[10px] text-slate-600 uppercase tracking-wider px-1 mb-1">
<span className="flex-1">Domaine</span><span className="w-24 text-right">Cout/an</span><span className="w-28">Expiration</span><span className="w-24">Hebergeur</span><span className="w-6"></span> <span className="flex-1">Domaine</span><span className="w-24 text-right">Cout/an</span><span className="w-28">Expiration</span><span className="w-24">Hebergeur</span><span className="w-6"></span>
</div> </div>
{doms.map((dm, i) => ( {c.doms.map((dm, i) => (
<div key={i} className="flex gap-2 items-center"> <div key={i} className="flex gap-2 items-center">
<input type="text" value={dm.name} onChange={e => updateInList(d.id, 'domains_list', i, 'name', e.target.value)} placeholder="domaine.fr" <input type="text" value={dm.name} onChange={e => 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" /> 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" />
@@ -282,37 +273,31 @@ export default function Dashboard({ token, user }) {
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" /> 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" />
<input type="text" value={dm.hoster || ''} onChange={e => updateInList(d.id, 'domains_list', i, 'hoster', e.target.value)} placeholder="Hebergeur" <input type="text" value={dm.hoster || ''} onChange={e => 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" /> 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" />
<button onClick={() => removeFromList(d.id, 'domains_list', i)} className="text-red-400/60 hover:text-red-400 p-1 transition text-sm">x</button> <button onClick={() => removeFromList(d.id, 'domains_list', i)} className="text-slate-500 hover:text-red-400 text-lg px-1 transition">&times;</button>
</div> </div>
))} ))}
</div> </div>
)} )}
</div> </Section>
<hr className="border-slate-700/30 my-4" />
{/* Notes */} {/* Notes */}
<div> <Section title="Notes" desc="">
<span className="text-white text-sm font-medium block mb-2">Notes</span>
<textarea value={d.notes} onChange={e => update(d.id, 'notes', e.target.value)} placeholder="Notes..." rows={3} <textarea value={d.notes} onChange={e => update(d.id, 'notes', e.target.value)} placeholder="Notes..." rows={3}
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 resize-none" /> 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 resize-none" />
</div> </Section>
{/* Admin: couts */} {/* Couts admin */}
{isAdmin && ( {isAdmin && (
<> <div className="bg-slate-800/30 rounded-2xl border border-emerald-500/20 p-5">
<hr className="border-slate-700/30 my-4" /> <h4 className="text-white text-sm font-semibold mb-3">Couts mensuels HT</h4>
<div className="bg-slate-900/30 rounded-xl p-4"> <div className="space-y-1.5 text-sm">
<h4 className="text-emerald-400 text-sm font-semibold mb-2">Couts mensuels HT</h4> {c.nbUsers > 0 && <CostRow label={'Ksuite (' + c.nbUsers + ' x 6,58)'} value={c.ksuite} />}
<div className="space-y-1 text-sm"> {d.has_website && <CostRow label='Site (69/an)' value={c.site} />}
{nbUsers > 0 && <div className="flex justify-between"><span className="text-slate-400">Ksuite ({nbUsers} x 6,58)</span><span className="text-white font-mono">{ksuiteCost.toFixed(2)} EUR</span></div>} {(d.extra_email_count || 0) > 0 && <CostRow label={'Emails extras (' + d.extra_email_count + ')'} value={c.extra} />}
{d.has_website && <div className="flex justify-between"><span className="text-slate-400">Site (69/an)</span><span className="text-white font-mono">{siteCost.toFixed(2)} EUR</span></div>} {c.doms.length > 0 && <CostRow label={'Domaines (' + c.doms.length + ')'} value={c.domCost} />}
{(d.extra_email_count || 0) > 0 && <div className="flex justify-between"><span className="text-slate-400">Emails extras ({d.extra_email_count})</span><span className="text-white font-mono">{extraCost.toFixed(2)} EUR</span></div>} <div className="flex justify-between pt-2 border-t border-slate-700/30 font-semibold"><span className="text-white">Total</span><span className="text-emerald-400 font-mono">{c.total.toFixed(2)} EUR/mois</span></div>
{doms.length > 0 && <div className="flex justify-between"><span className="text-slate-400">Domaines ({doms.length})</span><span className="text-white font-mono">{domsCost.toFixed(2)} EUR</span></div>}
<div className="flex justify-between pt-2 border-t border-slate-700/30 font-semibold"><span className="text-white">Total</span><span className="text-emerald-400 font-mono">{total.toFixed(2)} EUR/mois</span></div>
</div>
</div> </div>
</> </div>
)} )}
</div> </div>
) )
@@ -323,34 +308,51 @@ export default function Dashboard({ token, user }) {
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 pb-1 mb-6 justify-center flex-wrap"> <div className="flex gap-1 pb-1 mb-6 justify-center flex-wrap">
<button onClick={() => setActiveTab(-1)} <button onClick={() => setActiveTab(-1)}
className={'px-3 py-2 rounded-lg text-sm transition-colors shrink-0 ' + className={'px-4 py-2 rounded-lg text-sm transition-colors shrink-0 ' +
(activeTab === -1 ? 'bg-white text-slate-900 font-medium' : 'bg-slate-800/50 text-slate-400 hover:text-white hover:bg-slate-800')}> (activeTab === -1 ? 'bg-white text-slate-900 font-semibold' : 'bg-slate-800/50 text-slate-400 hover:text-white hover:bg-slate-800')}>
Home Home
</button> </button>
{domains.map((dom, i) => ( {domains.map((dom, i) => (
<button key={dom.id} onClick={() => { setActiveTab(i); if (isAdmin) saveDomain(dom.id) }} <button key={dom.id} onClick={() => { setActiveTab(i); if (isAdmin) saveDomain(dom.id) }}
className={'px-3 py-2 rounded-lg text-sm whitespace-nowrap transition-colors shrink-0 ' + className={'px-4 py-2 rounded-lg text-sm whitespace-nowrap transition-colors shrink-0 ' +
(i === activeTab ? 'bg-blue-600 text-white font-medium' : 'bg-slate-800/50 text-slate-400 hover:text-white hover:bg-slate-800')}> (i === activeTab ? 'bg-blue-600 text-white font-semibold' : 'bg-slate-800/50 text-slate-400 hover:text-white hover:bg-slate-800')}>
{dom.company_name} {dom.company_name}
</button> </button>
))} ))}
</div> </div>
{/* Save indicator */} {/* Header */}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
{activeTab === -1 ? ( {activeTab === -1 ? null : <h2 className="text-lg font-bold text-white">{domains[activeTab].company_name}</h2>}
<h2 className="text-lg font-bold text-white">Tableau de bord</h2> <div className="flex items-center gap-3 ml-auto">
) : (
<h2 className="text-lg font-bold text-white">{domains[activeTab].company_name}</h2>
)}
<div className="flex items-center gap-3">
{saving && <span className="text-slate-500 text-xs">Sauvegarde...</span>} {saving && <span className="text-slate-500 text-xs">Sauvegarde...</span>}
{saved && <span className="text-emerald-400 text-xs">Sauvegarde</span>} {saved && <span className="text-emerald-400 text-xs">Sauvegarde</span>}
</div> </div>
</div> </div>
{/* Content */}
{activeTab === -1 ? <HomeView /> : <DomainForm d={domains[activeTab]} key={domains[activeTab].id} />} {activeTab === -1 ? <HomeView /> : <DomainForm d={domains[activeTab]} key={domains[activeTab].id} />}
</div> </div>
) )
} }
// Reusable section
function Section({ title, desc, children }) {
return (
<div className="bg-slate-800/30 rounded-2xl border border-slate-700/20 p-5">
<div className="flex items-baseline gap-2 mb-3">
<h3 className="text-white text-sm font-semibold">{title}</h3>
{desc && <span className="text-slate-500 text-xs">{desc}</span>}
</div>
{children}
</div>
)
}
function CostRow({ label, value }) {
return (
<div className="flex justify-between">
<span className="text-slate-400">{label}</span>
<span className="text-white font-mono">{value.toFixed(2)} EUR</span>
</div>
)
}