Refonte: Ksuite 6.58E/user + site 69E/an + domaines + Office + Calendrier + emails extras
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const KSUITE_PRICE = 6.58
|
||||
const SITE_PRICE = 69
|
||||
|
||||
export default function Dashboard({ token, user }) {
|
||||
const [domains, setDomains] = useState({ parent: null, children: [] })
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -12,17 +15,48 @@ export default function Dashboard({ token, user }) {
|
||||
|
||||
if (loading) return <div className="text-slate-400 text-center py-12">Chargement...</div>
|
||||
|
||||
const count = (d) => [d.has_cloud, d.has_website, d.has_emails, d.has_team].filter(Boolean).length
|
||||
const color = (n) => n === 0 ? 'bg-slate-600' : n < 3 ? 'bg-amber-500' : 'bg-emerald-500'
|
||||
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 (d.has_cloud) tags.push(<span key="c" className="px-2 py-0.5 bg-sky-500/10 text-sky-400 text-xs rounded-md">Cloud</span>)
|
||||
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_emails) tags.push(<span key="e" className="px-2 py-0.5 bg-amber-500/10 text-amber-400 text-xs rounded-md">{d.email_count} mail{d.email_count > 1 ? 's' : ''}</span>)
|
||||
if (d.has_team) { const m = JSON.parse(d.team_members || '[]'); tags.push(<span key="t" className="px-2 py-0.5 bg-emerald-500/10 text-emerald-400 text-xs rounded-md">{m.length} user{m.length > 1 ? 's' : ''}</span>) }
|
||||
if (n === 0) tags.push(<span key="x" className="text-slate-500 text-xs italic">Aucun besoin renseigne</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)
|
||||
|
||||
return (
|
||||
<Link to={'/domain/' + d.id}
|
||||
@@ -38,23 +72,15 @@ export default function Dashboard({ token, user }) {
|
||||
<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' && d.monthly_price > 0 && (
|
||||
{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">{d.monthly_price} EUR/mois HT</span>
|
||||
<span className="text-emerald-400 font-semibold text-sm">{cost.total.toFixed(2)} EUR/mois HT</span>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
// Admin cost summary
|
||||
const allDomains = [domains.parent, ...domains.children].filter(Boolean)
|
||||
const totalCloud = allDomains.reduce((s, d) => s + (d.has_cloud ? (d.cloud_cost || 0) : 0), 0)
|
||||
const totalWebsite = allDomains.reduce((s, d) => s + (d.has_website ? (d.website_cost || 0) : 0), 0)
|
||||
const totalEmail = allDomains.reduce((s, d) => s + (d.has_emails ? (d.email_count || 0) * (d.email_cost || 0) : 0), 0)
|
||||
const totalUsers = allDomains.reduce((s, d) => s + (d.has_team ? JSON.parse(d.team_members || '[]').length * (d.user_cost || 0) : 0), 0)
|
||||
const totalAll = totalCloud + totalWebsite + totalEmail + totalUsers
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
@@ -72,56 +98,57 @@ export default function Dashboard({ token, user }) {
|
||||
|
||||
{user.role === 'admin' && (
|
||||
<div className="mt-8 space-y-4">
|
||||
{/* Vue d'ensemble */}
|
||||
<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-4 gap-4 text-center">
|
||||
<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-sky-400">{allDomains.filter(d => d.has_cloud).length}</p><p className="text-slate-400 text-xs">Cloud</p></div>
|
||||
<div><p className="text-2xl font-bold text-violet-400">{allDomains.filter(d => d.has_website).length}</p><p className="text-slate-400 text-xs">Sites web</p></div>
|
||||
<div><p className="text-2xl font-bold text-emerald-400">{allDomains.filter(d => d.has_emails).length}</p><p className="text-slate-400 text-xs">Avec mails</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detail des couts */}
|
||||
<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-3">
|
||||
<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">Cloud</span>
|
||||
<span className="text-slate-500 text-xs">({allDomains.filter(d => d.has_cloud).length})</span>
|
||||
<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>
|
||||
</div>
|
||||
<span className="text-white font-mono text-sm">{totalCloud.toFixed(2)} EUR</span>
|
||||
<span className="text-white font-mono text-sm">{totals.ksuite.toFixed(2)} EUR</span>
|
||||
</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</span>
|
||||
<span className="text-slate-500 text-xs">({allDomains.filter(d => d.has_website).length})</span>
|
||||
<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">{totalWebsite.toFixed(2)} EUR</span>
|
||||
<span className="text-white font-mono text-sm">{totals.siteCost.toFixed(2)} EUR</span>
|
||||
</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-amber-400" />
|
||||
<span className="text-slate-300 text-sm">Adresses mail</span>
|
||||
<span className="text-slate-500 text-xs">({allDomains.reduce((s, d) => s + (d.has_emails ? d.email_count || 0 : 0), 0)})</span>
|
||||
{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>
|
||||
</div>
|
||||
<span className="text-white font-mono text-sm">{totalEmail.toFixed(2)} EUR</span>
|
||||
</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-emerald-400" />
|
||||
<span className="text-slate-300 text-sm">Utilisateurs</span>
|
||||
<span className="text-slate-500 text-xs">({allDomains.reduce((s, d) => s + (d.has_team ? JSON.parse(d.team_members || '[]').length : 0), 0)})</span>
|
||||
)}
|
||||
{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>
|
||||
<span className="text-white font-mono text-sm">{totalUsers.toFixed(2)} EUR</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-3 border-t border-slate-600/50">
|
||||
)}
|
||||
<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">{totalAll.toFixed(2)} EUR/mois</span>
|
||||
<span className="text-emerald-400 font-bold font-mono text-lg">{totals.total.toFixed(2)} EUR/mois</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user