535 lines
25 KiB
TypeScript
535 lines
25 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { Plus, Trash2, X, Search, Edit2, Copy, CheckCircle2, Circle, FileSignature } from 'lucide-react'
|
|
import { formatCurrency } from '@/lib/utils'
|
|
import { format } from 'date-fns'
|
|
import { fr } from 'date-fns/locale'
|
|
|
|
interface Client {
|
|
id: string
|
|
name: string
|
|
email: string | null
|
|
phone: string | null
|
|
address: string | null
|
|
siret: string | null
|
|
isProfessional: boolean | null
|
|
}
|
|
|
|
interface SaleItem {
|
|
id: string
|
|
label: string
|
|
description: string | null
|
|
quantity: number
|
|
unitPrice: number
|
|
costPrice: number
|
|
}
|
|
|
|
interface Sale {
|
|
id: string
|
|
client: { id: string; name: string }
|
|
date: string
|
|
totalAmount: number
|
|
totalCost: number
|
|
paid: boolean
|
|
notes: string | null
|
|
items: SaleItem[]
|
|
}
|
|
|
|
export function SalesContent() {
|
|
const [sales, setSales] = useState<Sale[]>([])
|
|
const [clients, setClients] = useState<Client[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [showForm, setShowForm] = useState(false)
|
|
const [search, setSearch] = useState('')
|
|
|
|
const [selectedClient, setSelectedClient] = useState('')
|
|
const [clientName, setClientName] = useState('')
|
|
const [clientEmail, setClientEmail] = useState('')
|
|
const [clientPhone, setClientPhone] = useState('')
|
|
const [clientAddress, setClientAddress] = useState('')
|
|
const [clientSiret, setClientSiret] = useState('')
|
|
const [isProfessional, setIsProfessional] = useState(true)
|
|
const [saleDate, setSaleDate] = useState(format(new Date(), 'yyyy-MM-dd'))
|
|
const [notes, setNotes] = useState('')
|
|
const [items, setItems] = useState<{ id: string; label: string; description: string; quantity: number; unitPrice: number; costPrice: number }[]>([])
|
|
const [editingId, setEditingId] = useState<string | null>(null)
|
|
const [saving, setSaving] = useState(false)
|
|
const [saved, setSaved] = useState(false)
|
|
|
|
const isNewClient = !selectedClient
|
|
|
|
useEffect(() => { fetchSales(); fetchClients() }, [])
|
|
|
|
const fetchSales = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const res = await fetch('/api/sales')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setSales(data.sales)
|
|
}
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const fetchClients = async () => {
|
|
try {
|
|
const res = await fetch('/api/clients')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setClients(data.clients)
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
const openNew = () => {
|
|
setEditingId(null)
|
|
setSelectedClient('')
|
|
setClientName('')
|
|
setClientEmail('')
|
|
setClientPhone('')
|
|
setClientAddress('')
|
|
setClientSiret('')
|
|
setIsProfessional(true)
|
|
setSaleDate(format(new Date(), 'yyyy-MM-dd'))
|
|
setNotes('')
|
|
setItems([{ id: crypto.randomUUID(), label: '', description: '', quantity: 1, unitPrice: 0, costPrice: 0 }])
|
|
setSaved(false)
|
|
setShowForm(true)
|
|
}
|
|
|
|
const openEdit = async (sale: Sale) => {
|
|
setEditingId(sale.id)
|
|
setSelectedClient(sale.client.id)
|
|
setClientName('')
|
|
setClientEmail('')
|
|
setClientPhone('')
|
|
setClientAddress('')
|
|
setClientSiret('')
|
|
setIsProfessional(true)
|
|
setSaleDate(format(new Date(sale.date), 'yyyy-MM-dd'))
|
|
setNotes(sale.notes || '')
|
|
setItems(sale.items.map(i => ({ id: crypto.randomUUID(), label: i.label, description: i.description || '', quantity: i.quantity, unitPrice: i.unitPrice, costPrice: i.costPrice })))
|
|
setSaved(false)
|
|
setShowForm(true)
|
|
}
|
|
|
|
const addItem = () => {
|
|
setItems([...items, { id: crypto.randomUUID(), label: '', description: '', quantity: 1, unitPrice: 0, costPrice: 0 }])
|
|
}
|
|
|
|
const updateItem = (id: string, field: string, value: any) => {
|
|
setItems(items.map(item => item.id === id ? { ...item, [field]: value } : item))
|
|
}
|
|
|
|
const removeItem = (id: string) => {
|
|
setItems(items.filter(item => item.id !== id))
|
|
}
|
|
|
|
const totals = items.reduce((acc, item) => {
|
|
acc.amount += (item.unitPrice || 0) * (item.quantity || 1)
|
|
acc.cost += (item.costPrice || 0) * (item.quantity || 1)
|
|
return acc
|
|
}, { amount: 0, cost: 0 })
|
|
|
|
const handleSave = async () => {
|
|
if (!selectedClient && !clientName.trim()) return
|
|
if (items.length === 0 || !items.some(i => i.label.trim())) return
|
|
setSaving(true)
|
|
setSaved(false)
|
|
try {
|
|
const body: any = {
|
|
clientId: selectedClient || null,
|
|
clientName: isNewClient ? clientName.trim() : undefined,
|
|
clientEmail: isNewClient ? clientEmail || null : undefined,
|
|
clientPhone: isNewClient ? clientPhone || null : undefined,
|
|
clientAddress: isNewClient ? clientAddress || null : undefined,
|
|
clientSiret: isNewClient ? clientSiret || null : undefined,
|
|
isProfessional: isNewClient ? isProfessional : undefined,
|
|
date: saleDate,
|
|
totalAmount: totals.amount,
|
|
totalCost: totals.cost,
|
|
notes: notes || null,
|
|
items: items.filter(i => i.label.trim()).map(i => ({
|
|
label: i.label,
|
|
description: i.description || null,
|
|
quantity: i.quantity || 1,
|
|
unitPrice: i.unitPrice || 0,
|
|
costPrice: i.costPrice || 0,
|
|
})),
|
|
}
|
|
const method = editingId ? 'PUT' : 'POST'
|
|
const url = editingId ? `/api/sales/${editingId}` : '/api/sales'
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
if (res.ok) {
|
|
setSaved(true)
|
|
setShowForm(false)
|
|
setEditingId(null)
|
|
fetchSales()
|
|
fetchClients()
|
|
}
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const handleDuplicate = async (sale: Sale) => {
|
|
if (!confirm('Dupliquer cette vente ?')) return
|
|
try {
|
|
const body = {
|
|
clientId: sale.client.id,
|
|
date: format(new Date(), 'yyyy-MM-dd'),
|
|
totalAmount: sale.totalAmount,
|
|
totalCost: sale.totalCost,
|
|
notes: sale.notes,
|
|
items: sale.items.map(i => ({
|
|
label: i.label,
|
|
description: i.description,
|
|
quantity: i.quantity,
|
|
unitPrice: i.unitPrice,
|
|
costPrice: i.costPrice,
|
|
})),
|
|
}
|
|
await fetch('/api/sales', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
fetchSales()
|
|
} catch {}
|
|
}
|
|
|
|
const togglePaid = async (sale: Sale) => {
|
|
try {
|
|
await fetch(`/api/sales/${sale.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ paid: !sale.paid }),
|
|
})
|
|
fetchSales()
|
|
} catch {}
|
|
}
|
|
|
|
const convertToQuote = async (sale: Sale) => {
|
|
if (!confirm(`Convertir cette vente en devis ? La vente sera supprimée.`)) return
|
|
try {
|
|
const items = sale.items.map(i => ({
|
|
type: 'CUSTOM',
|
|
label: i.label,
|
|
description: i.description,
|
|
quantity: i.quantity,
|
|
unitPrice: i.unitPrice,
|
|
costPrice: i.costPrice,
|
|
isRecurring: false,
|
|
setupPrice: i.unitPrice,
|
|
setupCost: i.costPrice,
|
|
monthlyPrice: 0,
|
|
monthlyCost: 0,
|
|
}))
|
|
const res = await fetch('/api/quotes', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
clientId: sale.client.id,
|
|
name: `Devis - ${sale.client.name}`,
|
|
setupTotal: sale.totalAmount,
|
|
costTotal: sale.totalCost,
|
|
marginTotal: sale.totalAmount - sale.totalCost,
|
|
notes: sale.notes,
|
|
items,
|
|
}),
|
|
})
|
|
if (res.ok) {
|
|
await fetch(`/api/sales/${sale.id}`, { method: 'DELETE' })
|
|
fetchSales()
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (!confirm('Supprimer cette vente ?')) return
|
|
try {
|
|
await fetch(`/api/sales/${id}`, { method: 'DELETE' })
|
|
fetchSales()
|
|
} catch {}
|
|
}
|
|
|
|
const filtered = sales.filter(s =>
|
|
search === '' || s.client.name.toLowerCase().includes(search.toLowerCase())
|
|
)
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Ventes hors contrat</h2>
|
|
<button onClick={openNew}
|
|
className="inline-flex items-center gap-2 bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors">
|
|
<Plus className="h-4 w-4" />
|
|
Nouvelle vente
|
|
</button>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
|
<input type="text" placeholder="Rechercher..." value={search} onChange={(e) => setSearch(e.target.value)}
|
|
className="w-full pl-9 pr-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-white text-sm" />
|
|
</div>
|
|
|
|
{showForm && (
|
|
<div className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 p-4 pt-8 overflow-y-auto">
|
|
<div className="bg-white dark:bg-slate-800 rounded-xl w-full max-w-2xl p-6 shadow-xl">
|
|
<div className="flex items-center justify-between mb-5">
|
|
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">{editingId ? 'Modifier la vente' : 'Nouvelle vente'}</h3>
|
|
<button onClick={() => setShowForm(false)} className="p-1 rounded hover:bg-slate-100 dark:hover:bg-slate-700">
|
|
<X className="h-5 w-5 text-slate-500" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Client</label>
|
|
<select value={selectedClient} onChange={(e) => setSelectedClient(e.target.value)}
|
|
className="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white text-sm mb-2">
|
|
<option value="">Nouveau client...</option>
|
|
{clients.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
|
</select>
|
|
</div>
|
|
|
|
{isNewClient && (
|
|
<div className="p-4 rounded-lg bg-slate-50 dark:bg-slate-700/50 space-y-3">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-500 mb-1">Nom *</label>
|
|
<input type="text" value={clientName} onChange={(e) => setClientName(e.target.value)}
|
|
className="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-sm" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-500 mb-1">Email</label>
|
|
<input type="email" value={clientEmail} onChange={(e) => setClientEmail(e.target.value)}
|
|
className="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-sm" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-500 mb-1">Téléphone</label>
|
|
<input type="text" value={clientPhone} onChange={(e) => setClientPhone(e.target.value)}
|
|
className="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-sm" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-500 mb-1">Adresse</label>
|
|
<input type="text" value={clientAddress} onChange={(e) => setClientAddress(e.target.value)}
|
|
className="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-sm" />
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<label className="flex items-center gap-2">
|
|
<input type="checkbox" checked={isProfessional} onChange={(e) => setIsProfessional(e.target.checked)}
|
|
className="rounded border-slate-300 text-primary-600 focus:ring-primary-500" />
|
|
<span className="text-sm text-slate-700 dark:text-slate-300">Client professionnel</span>
|
|
</label>
|
|
{isProfessional && (
|
|
<div className="flex-1">
|
|
<label className="block text-xs font-medium text-slate-500 mb-1">SIRET</label>
|
|
<input type="text" value={clientSiret} onChange={(e) => setClientSiret(e.target.value)}
|
|
className="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-sm" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Date</label>
|
|
<input type="date" value={saleDate} onChange={(e) => setSaleDate(e.target.value)}
|
|
className="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-sm" />
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">Prestations</label>
|
|
<button onClick={addItem}
|
|
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded transition-colors">
|
|
<Plus className="h-3 w-3" /> Ajouter une ligne
|
|
</button>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{items.map((item, idx) => (
|
|
<div key={item.id} className="flex flex-wrap items-end gap-2 p-2 rounded bg-slate-50 dark:bg-slate-700/50">
|
|
<div className="flex-1 min-w-[120px]">
|
|
<label className="block text-xs text-slate-500 mb-0.5">Libellé</label>
|
|
<input type="text" value={item.label} onChange={(e) => updateItem(item.id, 'label', e.target.value)}
|
|
placeholder={`Prestation ${idx + 1}`}
|
|
className="w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-sm" />
|
|
</div>
|
|
<div className="w-16">
|
|
<label className="block text-xs text-slate-500 mb-0.5">Qté</label>
|
|
<input type="number" min={1} value={item.quantity} onChange={(e) => updateItem(item.id, 'quantity', parseInt(e.target.value) || 1)}
|
|
className="w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-sm text-center" />
|
|
</div>
|
|
<div className="w-24">
|
|
<label className="block text-xs text-slate-500 mb-0.5">Prix vente</label>
|
|
<input type="number" step="0.01" value={item.unitPrice} onChange={(e) => updateItem(item.id, 'unitPrice', parseFloat(e.target.value) || 0)}
|
|
className="w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-sm" />
|
|
</div>
|
|
<div className="w-24">
|
|
<label className="block text-xs text-slate-500 mb-0.5">Coût achat</label>
|
|
<input type="number" step="0.01" value={item.costPrice} onChange={(e) => updateItem(item.id, 'costPrice', parseFloat(e.target.value) || 0)}
|
|
className="w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-sm" />
|
|
</div>
|
|
<div className="w-20 text-right self-end pb-1">
|
|
<span className="text-xs text-slate-500">{formatCurrency((item.unitPrice || 0) * (item.quantity || 1))}</span>
|
|
</div>
|
|
<button onClick={() => removeItem(item.id)} className="p-1 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded self-end mb-0.5">
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-6 p-3 bg-slate-50 dark:bg-slate-700/50 rounded-lg">
|
|
<div className="text-right">
|
|
<p className="text-xs text-slate-500">Total vente</p>
|
|
<p className="text-lg font-bold text-green-600">{formatCurrency(totals.amount)}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-xs text-slate-500">Coût total</p>
|
|
<p className="text-lg font-bold text-red-500">{formatCurrency(totals.cost)}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-xs text-slate-500">Marge</p>
|
|
<p className={`text-lg font-bold ${totals.amount - totals.cost >= 0 ? 'text-primary-600' : 'text-red-500'}`}>
|
|
{formatCurrency(totals.amount - totals.cost)}
|
|
<span className="text-sm ml-1">
|
|
({totals.amount > 0 ? ((totals.amount - totals.cost) / totals.amount * 100).toFixed(0) : 0}%)
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Notes</label>
|
|
<textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={2}
|
|
className="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-sm" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-3 mt-6">
|
|
<button onClick={() => setShowForm(false)}
|
|
className="px-4 py-2 text-sm font-medium text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg">Annuler</button>
|
|
<button onClick={handleSave} disabled={saving || (!selectedClient && !clientName.trim()) || !items.some(i => i.label.trim())}
|
|
className="px-4 py-2 text-sm font-medium bg-primary-600 hover:bg-primary-700 text-white rounded-lg disabled:opacity-50">
|
|
{saving ? 'Enregistrement...' : editingId ? 'Enregistrer les modifications' : 'Enregistrer la vente'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="text-center py-12 text-slate-500">Chargement...</div>
|
|
) : filtered.length === 0 ? (
|
|
<div className="text-center py-12 text-slate-500">Aucune vente</div>
|
|
) : (
|
|
(() => {
|
|
const groupedByMonth = filtered.reduce((acc: Record<string, Sale[]>, sale) => {
|
|
const key = sale.date.substring(0, 7)
|
|
if (!acc[key]) acc[key] = []
|
|
acc[key].push(sale)
|
|
return acc
|
|
}, {} as Record<string, Sale[]>)
|
|
const months = Object.keys(groupedByMonth).sort().reverse()
|
|
return months.map(monthKey => {
|
|
const [year, month] = monthKey.split('-')
|
|
const totalMonth = groupedByMonth[monthKey].reduce((s, sale) => s + sale.totalAmount, 0)
|
|
const dateObj = new Date(parseInt(year), parseInt(month) - 1)
|
|
return (
|
|
<div key={monthKey} className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700">
|
|
<div className="px-5 py-3 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between">
|
|
<h3 className="font-semibold text-slate-900 dark:text-white">
|
|
{format(dateObj, 'MMMM yyyy', { locale: fr })}
|
|
</h3>
|
|
<span className="text-sm font-medium text-primary-600">{formatCurrency(totalMonth)}</span>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b border-slate-100 dark:border-slate-700 text-xs text-slate-500">
|
|
<th className="text-left px-4 py-2 font-medium">Date</th>
|
|
<th className="text-left px-4 py-2 font-medium">Client</th>
|
|
<th className="text-right px-4 py-2 font-medium">Montant</th>
|
|
<th className="text-right px-4 py-2 font-medium">Coût</th>
|
|
<th className="text-right px-4 py-2 font-medium">Marge</th>
|
|
<th className="text-center px-4 py-2 font-medium">Statut</th>
|
|
<th className="text-center px-4 py-2 font-medium">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100 dark:divide-slate-700">
|
|
{groupedByMonth[monthKey].map(sale => {
|
|
const margin = sale.totalAmount - sale.totalCost
|
|
const marginPct = sale.totalAmount > 0 ? (margin / sale.totalAmount) * 100 : 0
|
|
return (
|
|
<tr key={sale.id} className="hover:bg-slate-50 dark:hover:bg-slate-700/50">
|
|
<td className="px-4 py-2.5 text-sm text-slate-600 dark:text-slate-300">
|
|
{format(new Date(sale.date), 'dd/MM/yyyy', { locale: fr })}
|
|
</td>
|
|
<td className="px-4 py-2.5 font-medium text-slate-900 dark:text-white">{sale.client.name}</td>
|
|
<td className="px-4 py-2.5 text-right font-medium text-green-600">{formatCurrency(sale.totalAmount)}</td>
|
|
<td className="px-4 py-2.5 text-right text-sm text-slate-600 dark:text-slate-300">{formatCurrency(sale.totalCost)}</td>
|
|
<td className="px-4 py-2.5 text-right">
|
|
<span className={`font-medium ${margin >= 0 ? 'text-green-600' : 'text-red-500'}`}>
|
|
{formatCurrency(margin)}
|
|
<span className="text-xs ml-1">({marginPct.toFixed(0)}%)</span>
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-2.5 text-center">
|
|
<button onClick={() => togglePaid(sale)}
|
|
className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium transition-colors ${
|
|
sale.paid
|
|
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
|
|
}`}>
|
|
{sale.paid ? <CheckCircle2 className="h-3.5 w-3.5" /> : <Circle className="h-3.5 w-3.5" />}
|
|
{sale.paid ? 'Payé' : 'Non payé'}
|
|
</button>
|
|
</td>
|
|
<td className="px-4 py-2.5 text-center">
|
|
<div className="flex items-center justify-center gap-1">
|
|
<button onClick={() => openEdit(sale)}
|
|
className="p-1.5 rounded text-slate-500 hover:text-primary-600 hover:bg-primary-50 dark:hover:bg-primary-900/20">
|
|
<Edit2 className="h-4 w-4" />
|
|
</button>
|
|
<button onClick={() => handleDuplicate(sale)}
|
|
className="p-1.5 rounded text-slate-500 hover:text-green-600 hover:bg-green-50 dark:hover:bg-green-900/20">
|
|
<Copy className="h-4 w-4" />
|
|
</button>
|
|
<button onClick={() => convertToQuote(sale)}
|
|
className="p-1.5 rounded text-purple-600 hover:bg-purple-50 dark:hover:bg-purple-900/20"
|
|
title="Convertir en devis">
|
|
<FileSignature className="h-4 w-4" />
|
|
</button>
|
|
<button onClick={() => handleDelete(sale.id)}
|
|
className="p-1.5 rounded text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20">
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)
|
|
})
|
|
})()
|
|
)}
|
|
</div>
|
|
)
|
|
}
|