Refonte UX: toggles, sections, recap couts Home
This commit is contained in:
@@ -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,15 +95,45 @@ 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="grid grid-cols-2 md:grid-cols-3 gap-3 mb-6">
|
||||||
|
{domains.map((d, i) => {
|
||||||
|
const c = getCosts(d)
|
||||||
|
return (
|
||||||
|
<button key={d.id} onClick={() => setActiveTab(i)}
|
||||||
|
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">
|
||||||
|
<p className="text-white font-medium text-sm group-hover:text-blue-400 transition">{d.company_name}</p>
|
||||||
|
<p className="text-slate-500 text-xs mb-3">{d.name}</p>
|
||||||
|
<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>
|
||||||
|
<div className="flex gap-3 mt-2 text-[11px] text-slate-500">
|
||||||
|
{c.nbUsers > 0 && <span>{c.nbUsers} user{c.nbUsers > 1 ? 's' : ''}</span>}
|
||||||
|
{c.doms.length > 0 && <span>{c.doms.length} domaine{c.doms.length > 1 ? 's' : ''}</span>}
|
||||||
|
{d.has_website && <span>Site</span>}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</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">
|
<div className="bg-slate-800/30 rounded-2xl border border-slate-700/20 overflow-hidden">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -102,66 +145,41 @@ export default function Dashboard({ token, user }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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) => (
|
{allDomains.map((dm, i) => (
|
||||||
<tr key={i} className="border-b border-slate-700/20 hover:bg-slate-800/40 transition">
|
<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-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 text-slate-300 font-mono text-xs">{dm.name || '—'}</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3"><span className={'px-2 py-0.5 rounded-md text-xs ' + hosterBadge(dm.hoster)}>{dm.hoster || '—'}</span></td>
|
||||||
<span className={'px-2 py-0.5 rounded-md text-xs ' + (hosterColors[dm.hoster] || hosterColors['Other'])}>
|
<td className={'px-4 py-3 font-mono text-xs ' + getColor(dm.expiry)}>{dm.expiry || '—'}</td>
|
||||||
{dm.hoster || '—'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className={'px-4 py-3 font-mono text-xs ' + getColor(dm.expiry)}>
|
|
||||||
{dm.expiry || '—'}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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">×</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 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 className="flex items-center gap-3">
|
||||||
|
<span className="text-slate-300 text-sm">Calendrier partage</span>
|
||||||
|
<Toggle checked={d.has_calendar} onChange={v => update(d.id, 'has_calendar', v)} />
|
||||||
</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">×</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user