Suppression doublons monsieur-edgar.com + momentz.paris, onglets centres

This commit is contained in:
2026-08-24 13:57:53 +00:00
parent 6a3b16f1e9
commit f5c0a45d5c
2 changed files with 281 additions and 124 deletions
-2
View File
@@ -3,7 +3,6 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import Login from './components/Login' import Login from './components/Login'
import Layout from './components/Layout' import Layout from './components/Layout'
import Dashboard from './components/Dashboard' import Dashboard from './components/Dashboard'
import DomainForm from './components/DomainForm'
function App() { function App() {
const [token, setToken] = useState(localStorage.getItem('charles_token')) const [token, setToken] = useState(localStorage.getItem('charles_token'))
@@ -29,7 +28,6 @@ function App() {
<Layout user={user} onLogout={handleLogout}> <Layout user={user} onLogout={handleLogout}>
<Routes> <Routes>
<Route path='/' element={<Dashboard token={token} user={user} />} /> <Route path='/' element={<Dashboard token={token} user={user} />} />
<Route path='/domain/:id' element={<DomainForm token={token} user={user} />} />
<Route path='*' element={<Navigate to='/' />} /> <Route path='*' element={<Navigate to='/' />} />
</Routes> </Routes>
</Layout> </Layout>
+281 -122
View File
@@ -1,159 +1,318 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useRef } from 'react'
import { Link } from 'react-router-dom'
const KSUITE_PRICE = 6.58 const KSUITE_PRICE = 6.58
const SITE_PRICE = 69 const SITE_PRICE = 69
export default function Dashboard({ token, user }) { 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 [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const saveTimer = useRef(null)
useEffect(() => { useEffect(() => {
fetch('/api/domains', { headers: { Authorization: 'Bearer ' + token } }) 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]) }, [token])
if (loading) return <div className="text-slate-400 text-center py-12">Chargement...</div> const update = (id, field, val) => {
setDomains(prev => prev.map(d => d.id === id ? { ...d, [field]: val } : d))
const allDomains = [domains.parent, ...domains.children].filter(Boolean) setSaved(false)
if (saveTimer.current) clearTimeout(saveTimer.current)
const calcDomain = (d) => { saveTimer.current = setTimeout(() => saveDomain(id), 1500)
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 totals = allDomains.reduce((acc, d) => { const saveDomain = async (id) => {
const c = calcDomain(d) const d = domains.find(x => x.id === id)
return { if (!d) return
users: acc.users + c.team, setSaving(true)
ksuite: acc.ksuite + c.ksuite, await fetch('/api/domains/' + id, {
site: acc.site + (d.has_website ? 1 : 0), method: 'PUT',
siteCost: acc.siteCost + c.site, headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
extra: acc.extra + (d.extra_email_count || 0), body: JSON.stringify(d)
extraCost: acc.extraCost + c.extra, })
doms: acc.doms + JSON.parse(d.domains_list || '[]').length, setSaving(false)
domsCost: acc.domsCost + c.doms, setSaved(true)
total: acc.total + c.total setTimeout(() => setSaved(false), 2000)
} }
}, { users: 0, ksuite: 0, site: 0, siteCost: 0, extra: 0, extraCost: 0, doms: 0, domsCost: 0, total: 0 })
const count = (d) => [d.has_website, d.has_team, d.has_office, d.has_calendar].filter(Boolean).length const saveNow = () => { if (saveTimer.current) clearTimeout(saveTimer.current); saveDomain(domains[activeTab]?.id) }
const color = (n) => n === 0 ? 'bg-slate-600' : n < 2 ? 'bg-amber-500' : 'bg-emerald-500'
const DomainCard = ({ d, badge }) => { const addToList = (id, field) => {
const n = count(d) const d = domains.find(x => x.id === id)
const team = d.has_team ? JSON.parse(d.team_members || '[]').length : 0 const list = JSON.parse(d[field] || '[]')
const tags = [] list.push(field === 'team_members' ? { first_name: '', last_name: '' } : { name: '', cost_per_year: 0, expiry: '', hoster: '' })
if (team > 0) tags.push(<span key="t" className="px-2 py-0.5 bg-emerald-500/10 text-emerald-400 text-xs rounded-md">{team} user{team > 1 ? 's' : ''}</span>) update(id, field, JSON.stringify(list))
if (d.has_website) tags.push(<span key="w" className="px-2 py-0.5 bg-violet-500/10 text-violet-400 text-xs rounded-md">Site{d.website_wordpress ? ' WP' : ''}</span>) }
if (d.has_office) tags.push(<span key="o" className="px-2 py-0.5 bg-rose-500/10 text-rose-400 text-xs rounded-md">Office</span>)
if (d.has_calendar) tags.push(<span key="c" className="px-2 py-0.5 bg-teal-500/10 text-teal-400 text-xs rounded-md">Calendrier</span>)
const doms = JSON.parse(d.domains_list || '[]')
if (doms.length > 0) tags.push(<span key="d" className="px-2 py-0.5 bg-sky-500/10 text-sky-400 text-xs rounded-md">{doms.length} domaine{doms.length > 1 ? 's' : ''}</span>)
if (n === 0 && team === 0) tags.push(<span key="x" className="text-slate-500 text-xs italic">Aucun besoin renseigne</span>)
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 <div className="text-slate-400 text-center py-12">Chargement...</div>
if (domains.length === 0) return <div className="text-slate-400 text-center py-12">Aucun domaine</div>
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 }) => (
<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 }) => (
<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" />
)
const Num = ({ value, onChange, placeholder, step }) => (
<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" />
)
const Toggle = ({ checked, onChange }) => (
<button type="button" onClick={() => onChange(!checked ? 1 : 0)}
className={'relative w-10 h-5.5 rounded-full transition-colors ' + (checked ? 'bg-blue-600' : 'bg-slate-600')}>
<div className={'absolute top-0.5 left-0.5 w-4.5 h-4.5 rounded-full bg-white transition-transform ' + (checked ? 'translate-x-4.5' : '')} />
</button>
)
const Select = ({ value, onChange, options }) => (
<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">
{options.map(o => <option key={o.v} value={o.v}>{o.l}</option>)}
</select>
)
// 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 ( return (
<Link to={'/domain/' + d.id} <div className="mt-6 p-4 bg-slate-800/30 rounded-xl border border-slate-700/20">
className="block group p-5 bg-slate-800/40 hover:bg-slate-800/60 rounded-2xl border border-slate-700/30 hover:border-slate-600/50 transition-all duration-200 hover:shadow-lg hover:shadow-black/20"> <h4 className="text-white text-sm font-semibold mb-3">Resume global</h4>
<div className="flex items-start justify-between mb-4"> <div className="grid grid-cols-2 md:grid-cols-5 gap-3 text-center text-xs">
<div className="flex items-center gap-2"> <div><p className="text-lg font-bold text-white">{all.users}</p><p className="text-slate-500">Users</p></div>
{badge && <span className="px-2 py-0.5 bg-blue-500/10 text-blue-400 text-[10px] font-medium rounded-md uppercase tracking-wider">Maitre</span>} <div><p className="text-lg font-bold text-sky-400">{all.ksuite.toFixed(0)}</p><p className="text-slate-500">Ksuite EUR/mois</p></div>
<div> <div><p className="text-lg font-bold text-violet-400">{all.sites}</p><p className="text-slate-500">Sites</p></div>
<h3 className="text-white font-semibold group-hover:text-blue-400 transition-colors">{d.company_name}</h3> <div><p className="text-lg font-bold text-teal-400">{all.doms}</p><p className="text-slate-500">Domaines</p></div>
<p className="text-slate-400 text-xs mt-0.5">{d.name}</p> <div><p className="text-lg font-bold text-emerald-400">{gTotal.toFixed(2)}</p><p className="text-slate-500">Total EUR/mois HT</p></div>
</div>
</div>
<div className={'w-2.5 h-2.5 rounded-full mt-1 ' + color(n)} />
</div> </div>
<div className="flex flex-wrap gap-1.5">{tags}</div> </div>
{user.role === 'admin' && cost.total > 0 && (
<div className="mt-3 pt-3 border-t border-slate-700/30">
<span className="text-emerald-400 font-semibold text-sm">{cost.total.toFixed(2)} EUR/mois HT</span>
</div>
)}
</Link>
) )
} }
return ( return (
<div> <div>
<div className="mb-8"> {/* Tabs */}
<h2 className="text-xl font-bold text-white mb-1">Domaines & Services</h2> <div className="flex gap-1 pb-1 mb-6 justify-center">
<p className="text-slate-400 text-sm"> {domains.map((dom, i) => (
{user.role === 'admin' ? 'Gerez les besoins de Charles pour chaque societe.' : 'Renseignez vos besoins pour chaque domaine.'} <button key={dom.id} onClick={() => { setActiveTab(i); if (isAdmin) saveNow() }}
</p> className={'px-3 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')}>
{dom.company_name}
</button>
))}
</div> </div>
{domains.parent && <DomainCard d={domains.parent} badge={true} />} {/* Save indicator */}
<div className="flex items-center justify-between mb-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-4"> <h2 className="text-lg font-bold text-white">{d.company_name} <span className="text-slate-500 text-sm font-normal">{d.name}</span></h2>
{domains.children.map(d => <DomainCard key={d.id} d={d} />)} <div className="flex items-center gap-3">
{saving && <span className="text-slate-500 text-xs">Sauvegarde...</span>}
{saved && <span className="text-emerald-400 text-xs">Sauvegarde</span>}
{isAdmin && <button onClick={saveNow} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs rounded-lg transition">Sauvegarder</button>}
</div>
</div> </div>
{user.role === 'admin' && ( {/* Form */}
<div className="mt-8 space-y-4"> <div className="bg-slate-800/30 rounded-2xl border border-slate-700/20 p-5">
<div className="p-5 bg-slate-800/30 rounded-2xl border border-slate-700/30"> {/* Utilisateurs */}
<h3 className="text-white font-semibold mb-3">Resume</h3> <div className="mb-5">
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 text-center"> <div className="flex items-center justify-between mb-2">
<div><p className="text-2xl font-bold text-white">{allDomains.length}</p><p className="text-slate-400 text-xs">Domaines</p></div> <span className="text-white text-sm font-medium">Utilisateurs</span>
<div><p className="text-2xl font-bold text-emerald-400">{totals.users}</p><p className="text-slate-400 text-xs">Utilisateurs</p></div> <button onClick={() => update(d.id, 'has_team', d.has_team ? 0 : 1)}
<div><p className="text-2xl font-bold text-sky-400">{totals.ksuite.toFixed(0)}</p><p className="text-slate-400 text-xs">Licences Ksuite</p></div> 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')}>
<div><p className="text-2xl font-bold text-violet-400">{totals.site}</p><p className="text-slate-400 text-xs">Sites web</p></div> {d.has_team ? 'Actif' : 'Inactif'}
<div><p className="text-2xl font-bold text-teal-400">{totals.doms}</p><p className="text-slate-400 text-xs">Noms de domaine</p></div> </button>
</div>
</div> </div>
{d.has_team && (
<div className="ml-0 space-y-1.5">
{team.map((m, i) => (
<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"
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"
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>
</div>
))}
<button onClick={() => addToList(d.id, 'team_members')} className="text-blue-400 hover:text-blue-300 text-xs">+ Ajouter</button>
</div>
)}
</div>
<div className="p-5 bg-slate-800/30 rounded-2xl border border-slate-700/30"> <hr className="border-slate-700/30 my-4" />
<h3 className="text-white font-semibold mb-4">Detail des couts mensuels HT</h3>
<div className="space-y-2"> {/* Site internet */}
<div className="flex items-center justify-between py-2 border-b border-slate-700/30"> <div className="mb-5">
<div className="flex items-center gap-2"> <div className="flex items-center justify-between mb-2">
<div className="w-2 h-2 rounded-full bg-sky-400" /> <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">Ksuite Pro ({totals.users} user{totals.users > 1 ? 's' : ''})</span> <button onClick={() => update(d.id, 'has_website', d.has_website ? 0 : 1)}
<span className="text-slate-500 text-xs">6,58 EUR/user</span> 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')}>
</div> {d.has_website ? 'Actif' : 'Inactif'}
<span className="text-white font-mono text-sm">{totals.ksuite.toFixed(2)} EUR</span> </button>
</div> </div>
<div className="flex items-center justify-between py-2 border-b border-slate-700/30"> {d.has_website && (
<div className="flex items-center gap-2"> <div className="ml-0 space-y-2">
<div className="w-2 h-2 rounded-full bg-violet-400" /> <div className="flex items-center gap-4">
<span className="text-slate-300 text-sm">Sites web ({totals.site})</span> <span className="text-slate-400 text-sm">WordPress</span>
<span className="text-slate-500 text-xs">69 EUR/an</span> <button onClick={() => update(d.id, 'website_wordpress', d.website_wordpress ? 0 : 1)}
</div> 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-white font-mono text-sm">{totals.siteCost.toFixed(2)} EUR</span> {d.website_wordpress ? 'Oui' : 'Non'}
</div> </button>
{totals.extra > 0 && ( <span className="text-slate-400 text-sm ml-4">Gere par</span>
<div className="flex items-center justify-between py-2 border-b border-slate-700/30"> <Select value={d.website_managed_by} onChange={v => update(d.id, 'website_managed_by', v)}
<div className="flex items-center gap-2"> options={[{v:'',l:'?'},{v:'charles',l:'Charles'},{v:'pierre',l:'Pierre'}]} />
<div className="w-2 h-2 rounded-full bg-amber-400" />
<span className="text-slate-300 text-sm">Emails extras ({totals.extra})</span>
</div>
<span className="text-white font-mono text-sm">{totals.extraCost.toFixed(2)} EUR</span>
</div>
)}
{totals.doms > 0 && (
<div className="flex items-center justify-between py-2 border-b border-slate-700/30">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-teal-400" />
<span className="text-slate-300 text-sm">Noms de domaine ({totals.doms})</span>
</div>
<span className="text-white font-mono text-sm">{totals.domsCost.toFixed(2)} EUR</span>
</div>
)}
<div className="flex items-center justify-between pt-3 border-t border-slate-600/50">
<span className="text-white font-semibold">Total mensuel HT</span>
<span className="text-emerald-400 font-bold font-mono text-lg">{totals.total.toFixed(2)} EUR/mois</span>
</div> </div>
<Txt value={d.website_details} onChange={v => update(d.id, 'website_details', v)} placeholder="Details du site..." />
</div>
)}
</div>
<hr className="border-slate-700/30 my-4" />
{/* Emails */}
<div className="mb-5">
<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 mt-2">
<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" />
<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" />
</div>
</div>
<hr className="border-slate-700/30 my-4" />
{/* Office + Calendrier */}
<div className="mb-5">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-white text-sm font-medium">Pack Office</span>
<button onClick={() => update(d.id, 'has_office', d.has_office ? 0 : 1)}
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 className="flex items-center gap-2">
<span className="text-white text-sm font-medium">Calendrier partage</span>
<button onClick={() => update(d.id, 'has_calendar', d.has_calendar ? 0 : 1)}
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> </div>
)}
<hr className="border-slate-700/30 my-4" />
{/* Noms de domaine */}
<div className="mb-5">
<div className="flex items-center justify-between mb-2">
<span className="text-white text-sm font-medium">Noms de domaine</span>
<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>
</div>
{doms.map((dm, i) => (
<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"
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="number" step="0.01" value={dm.cost_per_year || ''} onChange={e => 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" />
<input type="date" value={dm.expiry || ''} onChange={e => 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" />
<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" />
<button onClick={() => removeFromList(d.id, 'domains_list', i)} className="text-red-400/60 hover:text-red-400 p-1 transition text-sm">x</button>
</div>
))}
</div>
)}
</div>
<hr className="border-slate-700/30 my-4" />
{/* Notes */}
<div>
<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}
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>
{/* Admin: couts */}
{isAdmin && (
<>
<hr className="border-slate-700/30 my-4" />
<div className="bg-slate-900/30 rounded-xl p-4">
<h4 className="text-emerald-400 text-sm font-semibold mb-2">Couts mensuels HT</h4>
<div className="space-y-1 text-sm">
{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.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>}
{(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>}
{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>
<GlobalSummary />
</div> </div>
) )
} }