Suppression doublons monsieur-edgar.com + momentz.paris, onglets centres
This commit is contained in:
@@ -3,7 +3,6 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||
import Login from './components/Login'
|
||||
import Layout from './components/Layout'
|
||||
import Dashboard from './components/Dashboard'
|
||||
import DomainForm from './components/DomainForm'
|
||||
|
||||
function App() {
|
||||
const [token, setToken] = useState(localStorage.getItem('charles_token'))
|
||||
@@ -29,7 +28,6 @@ function App() {
|
||||
<Layout user={user} onLogout={handleLogout}>
|
||||
<Routes>
|
||||
<Route path='/' element={<Dashboard token={token} user={user} />} />
|
||||
<Route path='/domain/:id' element={<DomainForm token={token} user={user} />} />
|
||||
<Route path='*' element={<Navigate to='/' />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
|
||||
@@ -1,159 +1,318 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
const KSUITE_PRICE = 6.58
|
||||
const SITE_PRICE = 69
|
||||
|
||||
export default function Dashboard({ token, user }) {
|
||||
const [domains, setDomains] = useState({ parent: null, children: [] })
|
||||
const [domains, setDomains] = useState([])
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saved, setSaved] = useState(false)
|
||||
const saveTimer = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/domains', { headers: { Authorization: 'Bearer ' + token } })
|
||||
.then(r => r.json()).then(d => { setDomains(d); setLoading(false) })
|
||||
.then(r => r.json()).then(d => {
|
||||
const all = d.parent ? [d.parent, ...d.children] : d.children
|
||||
setDomains(all)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [token])
|
||||
|
||||
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
|
||||
setSaving(true)
|
||||
await fetch('/api/domains/' + id, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
|
||||
body: JSON.stringify(d)
|
||||
})
|
||||
setSaving(false)
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
}
|
||||
|
||||
const saveNow = () => { if (saveTimer.current) clearTimeout(saveTimer.current); saveDomain(domains[activeTab]?.id) }
|
||||
|
||||
const addToList = (id, field) => {
|
||||
const d = domains.find(x => x.id === id)
|
||||
const list = JSON.parse(d[field] || '[]')
|
||||
list.push(field === 'team_members' ? { first_name: '', last_name: '' } : { name: '', cost_per_year: 0, expiry: '', hoster: '' })
|
||||
update(id, field, JSON.stringify(list))
|
||||
}
|
||||
|
||||
const 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 allDomains = [domains.parent, ...domains.children].filter(Boolean)
|
||||
|
||||
const calcDomain = (d) => {
|
||||
const team = d.has_team ? JSON.parse(d.team_members || '[]').length : 0
|
||||
const ksuite = team * KSUITE_PRICE
|
||||
const site = d.has_website ? SITE_PRICE / 12 : 0
|
||||
const extra = (d.extra_email_count || 0) * (d.extra_email_cost || 0)
|
||||
const doms = JSON.parse(d.domains_list || '[]').reduce((s, dm) => s + (dm.cost_per_year || 0) / 12, 0)
|
||||
return { team, ksuite, site, extra, doms, total: ksuite + site + extra + doms }
|
||||
}
|
||||
|
||||
const totals = allDomains.reduce((acc, d) => {
|
||||
const c = calcDomain(d)
|
||||
return {
|
||||
users: acc.users + c.team,
|
||||
ksuite: acc.ksuite + c.ksuite,
|
||||
site: acc.site + (d.has_website ? 1 : 0),
|
||||
siteCost: acc.siteCost + c.site,
|
||||
extra: acc.extra + (d.extra_email_count || 0),
|
||||
extraCost: acc.extraCost + c.extra,
|
||||
doms: acc.doms + JSON.parse(d.domains_list || '[]').length,
|
||||
domsCost: acc.domsCost + c.doms,
|
||||
total: acc.total + c.total
|
||||
}
|
||||
}, { users: 0, ksuite: 0, site: 0, siteCost: 0, extra: 0, extraCost: 0, doms: 0, domsCost: 0, total: 0 })
|
||||
|
||||
const count = (d) => [d.has_website, d.has_team, d.has_office, d.has_calendar].filter(Boolean).length
|
||||
const color = (n) => n === 0 ? 'bg-slate-600' : n < 2 ? 'bg-amber-500' : 'bg-emerald-500'
|
||||
|
||||
const DomainCard = ({ d, badge }) => {
|
||||
const n = count(d)
|
||||
const team = d.has_team ? JSON.parse(d.team_members || '[]').length : 0
|
||||
const tags = []
|
||||
if (team > 0) tags.push(<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>)
|
||||
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 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 || '[]')
|
||||
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 isAdmin = user.role === 'admin'
|
||||
|
||||
const cost = calcDomain(d)
|
||||
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 (
|
||||
<Link to={'/domain/' + d.id}
|
||||
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">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{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>
|
||||
<h3 className="text-white font-semibold group-hover:text-blue-400 transition-colors">{d.company_name}</h3>
|
||||
<p className="text-slate-400 text-xs mt-0.5">{d.name}</p>
|
||||
<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>
|
||||
</div>
|
||||
<div className={'w-2.5 h-2.5 rounded-full mt-1 ' + color(n)} />
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">{tags}</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 (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-bold text-white mb-1">Domaines & Services</h2>
|
||||
<p className="text-slate-400 text-sm">
|
||||
{user.role === 'admin' ? 'Gerez les besoins de Charles pour chaque societe.' : 'Renseignez vos besoins pour chaque domaine.'}
|
||||
</p>
|
||||
{/* 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>
|
||||
|
||||
{domains.parent && <DomainCard d={domains.parent} badge={true} />}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
|
||||
{domains.children.map(d => <DomainCard key={d.id} d={d} />)}
|
||||
</div>
|
||||
|
||||
{user.role === 'admin' && (
|
||||
<div className="mt-8 space-y-4">
|
||||
<div className="p-5 bg-slate-800/30 rounded-2xl border border-slate-700/30">
|
||||
<h3 className="text-white font-semibold mb-3">Resume</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 text-center">
|
||||
<div><p className="text-2xl font-bold text-white">{allDomains.length}</p><p className="text-slate-400 text-xs">Domaines</p></div>
|
||||
<div><p className="text-2xl font-bold text-emerald-400">{totals.users}</p><p className="text-slate-400 text-xs">Utilisateurs</p></div>
|
||||
<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>
|
||||
<div><p className="text-2xl font-bold text-violet-400">{totals.site}</p><p className="text-slate-400 text-xs">Sites web</p></div>
|
||||
<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>
|
||||
{/* 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>
|
||||
|
||||
<div className="p-5 bg-slate-800/30 rounded-2xl border border-slate-700/30">
|
||||
<h3 className="text-white font-semibold mb-4">Detail des couts mensuels HT</h3>
|
||||
<div className="space-y-2">
|
||||
<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-sky-400" />
|
||||
<span className="text-slate-300 text-sm">Ksuite Pro ({totals.users} user{totals.users > 1 ? 's' : ''})</span>
|
||||
<span className="text-slate-500 text-xs">6,58 EUR/user</span>
|
||||
{/* Form */}
|
||||
<div className="bg-slate-800/30 rounded-2xl border border-slate-700/20 p-5">
|
||||
{/* Utilisateurs */}
|
||||
<div className="mb-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-white text-sm font-medium">Utilisateurs</span>
|
||||
<button onClick={() => update(d.id, 'has_team', d.has_team ? 0 : 1)}
|
||||
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>
|
||||
<span className="text-white font-mono text-sm">{totals.ksuite.toFixed(2)} EUR</span>
|
||||
{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>
|
||||
<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-violet-400" />
|
||||
<span className="text-slate-300 text-sm">Sites web ({totals.site})</span>
|
||||
<span className="text-slate-500 text-xs">69 EUR/an</span>
|
||||
</div>
|
||||
<span className="text-white font-mono text-sm">{totals.siteCost.toFixed(2)} EUR</span>
|
||||
</div>
|
||||
{totals.extra > 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-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>
|
||||
))}
|
||||
<button onClick={() => addToList(d.id, 'team_members')} className="text-blue-400 hover:text-blue-300 text-xs">+ Ajouter</button>
|
||||
</div>
|
||||
)}
|
||||
{totals.doms > 0 && (
|
||||
<div className="flex items-center justify-between py-2 border-b border-slate-700/30">
|
||||
</div>
|
||||
|
||||
<hr className="border-slate-700/30 my-4" />
|
||||
|
||||
{/* Site internet */}
|
||||
<div className="mb-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-white text-sm font-medium">Site internet <span className="text-slate-500 text-xs font-normal">69 EUR/an</span></span>
|
||||
<button onClick={() => update(d.id, 'has_website', d.has_website ? 0 : 1)}
|
||||
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>
|
||||
{d.has_website && (
|
||||
<div className="ml-0 space-y-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-slate-400 text-sm">WordPress</span>
|
||||
<button onClick={() => update(d.id, 'website_wordpress', d.website_wordpress ? 0 : 1)}
|
||||
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')}>
|
||||
{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)}
|
||||
options={[{v:'',l:'?'},{v:'charles',l:'Charles'},{v:'pierre',l:'Pierre'}]} />
|
||||
</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">
|
||||
<div className="w-2 h-2 rounded-full bg-teal-400" />
|
||||
<span className="text-slate-300 text-sm">Noms de domaine ({totals.doms})</span>
|
||||
<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>
|
||||
<span className="text-white font-mono text-sm">{totals.domsCost.toFixed(2)} EUR</span>
|
||||
<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>
|
||||
|
||||
<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 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>
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user