Home view: tableau recap + onglets centres, nettoyage affichage

This commit is contained in:
2026-08-24 14:01:11 +00:00
parent f5c0a45d5c
commit f8789e148d
+136 -98
View File
@@ -1,15 +1,14 @@
import { useState, useEffect, useRef } from 'react'
import { useState, useEffect } from 'react'
const KSUITE_PRICE = 6.58
const SITE_PRICE = 69
export default function Dashboard({ token, user }) {
const [domains, setDomains] = useState([])
const [activeTab, setActiveTab] = useState(0)
const [activeTab, setActiveTab] = useState(-1)
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 } })
@@ -20,13 +19,6 @@ export default function Dashboard({ token, user }) {
})
}, [token])
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 saveDomain = async (id) => {
const d = domains.find(x => x.id === id)
if (!d) return
@@ -41,7 +33,11 @@ export default function Dashboard({ token, user }) {
setTimeout(() => setSaved(false), 2000)
}
const saveNow = () => { if (saveTimer.current) clearTimeout(saveTimer.current); saveDomain(domains[activeTab]?.id) }
const update = (id, field, val) => {
setDomains(prev => prev.map(d => d.id === id ? { ...d, [field]: val } : d))
const timeout = setTimeout(() => saveDomain(id), 1200)
return () => clearTimeout(timeout)
}
const addToList = (id, field) => {
const d = domains.find(x => x.id === id)
@@ -67,105 +63,113 @@ export default function Dashboard({ token, user }) {
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
// HOME VIEW
const HomeView = () => {
const allDomains = []
domains.forEach(d => {
const doms = JSON.parse(d.domains_list || '[]')
doms.forEach(dm => allDomains.push({ ...dm, society: d.company_name }))
})
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 getColor = (expiry) => {
if (!expiry) return ''
const diff = (new Date(expiry) - new Date()) / (1000 * 60 * 60 * 24)
if (diff < 0) return 'text-red-400'
if (diff < 30) return 'text-amber-400'
if (diff < 90) return 'text-orange-300'
return 'text-emerald-400'
}
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
const hosterColors = {
'OVH': 'bg-sky-500/20 text-sky-300',
'Infomaniak': 'bg-emerald-500/20 text-emerald-300',
'Other': 'bg-slate-500/20 text-slate-300'
}
return (
<div className="mt-6 p-4 bg-slate-800/30 rounded-xl border border-slate-700/20">
<h4 className="text-white text-sm font-semibold mb-3">Resume global</h4>
<div className="grid grid-cols-2 md:grid-cols-5 gap-3 text-center text-xs">
<div><p className="text-lg font-bold text-white">{all.users}</p><p className="text-slate-500">Users</p></div>
<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><p className="text-lg font-bold text-violet-400">{all.sites}</p><p className="text-slate-500">Sites</p></div>
<div><p className="text-lg font-bold text-teal-400">{all.doms}</p><p className="text-slate-500">Domaines</p></div>
<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>
<h2 className="text-lg font-bold text-white mb-4">Tableau de bord</h2>
<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.length === 0 && (
<tr><td colSpan={4} className="px-4 py-6 text-slate-500 text-center">Aucun domaine declare</td></tr>
)}
{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 ' + (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>
)
}
return (
<div>
{/* Tabs */}
<div className="flex gap-1 pb-1 mb-6 justify-center">
{domains.map((dom, i) => (
<button key={dom.id} onClick={() => { setActiveTab(i); if (isAdmin) saveNow() }}
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>
// DOMAIN FORM VIEW
const DomainForm = ({ d }) => {
const team = JSON.parse(d.team_members || '[]')
const nbUsers = d.has_team ? team.length : 0
const doms = JSON.parse(d.domains_list || '[]')
{/* Save indicator */}
<div className="flex items-center justify-between mb-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>
<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>
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
{/* Form */}
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, activeText, inactiveText, color }) => (
<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')}>
{checked ? activeText : inactiveText}
</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>
)
return (
<div className="bg-slate-800/30 rounded-2xl border border-slate-700/20 p-5">
{/* Utilisateurs */}
<div className="mb-5">
@@ -311,8 +315,42 @@ export default function Dashboard({ token, user }) {
</>
)}
</div>
)
}
<GlobalSummary />
return (
<div>
{/* Tabs */}
<div className="flex gap-1 pb-1 mb-6 justify-center flex-wrap">
<button onClick={() => setActiveTab(-1)}
className={'px-3 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')}>
Home
</button>
{domains.map((dom, i) => (
<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 ' +
(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>
{/* Save indicator */}
<div className="flex items-center justify-between mb-4">
{activeTab === -1 ? (
<h2 className="text-lg font-bold text-white">Tableau de bord</h2>
) : (
<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>}
{saved && <span className="text-emerald-400 text-xs">Sauvegarde</span>}
</div>
</div>
{/* Content */}
{activeTab === -1 ? <HomeView /> : <DomainForm d={domains[activeTab]} key={domains[activeTab].id} />}
</div>
)
}