|
|
@@ -0,0 +1,434 @@
|
|
|
+import { useEffect, useState } from 'react'
|
|
|
+import api from '../api'
|
|
|
+import toast from 'react-hot-toast'
|
|
|
+
|
|
|
+const CATEGORIES = [
|
|
|
+ { value: 'eau', label: '💧 Eau' },
|
|
|
+ { value: 'gaz', label: '🔥 Gaz' },
|
|
|
+ { value: 'electricite', label: '⚡ Électricité' },
|
|
|
+ { value: 'entretien', label: '🔧 Entretien / Réparations' },
|
|
|
+ { value: 'ascenseur', label: '🛗 Ascenseur' },
|
|
|
+ { value: 'ordures', label: '🗑️ Ordures ménagères' },
|
|
|
+ { value: 'assurance', label: '🛡️ Assurance' },
|
|
|
+ { value: 'autre', label: '📦 Autre' },
|
|
|
+]
|
|
|
+
|
|
|
+const CURRENT_YEAR = new Date().getFullYear()
|
|
|
+const YEARS = Array.from({ length: 6 }, (_, i) => CURRENT_YEAR - i)
|
|
|
+
|
|
|
+function Modal({ title, onClose, children }) {
|
|
|
+ return (
|
|
|
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
|
+ <div className="bg-white rounded-2xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
|
|
+ <div className="flex items-center justify-between p-6 border-b sticky top-0 bg-white z-10">
|
|
|
+ <h2 className="text-lg font-semibold">{title}</h2>
|
|
|
+ <button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-2xl leading-none">×</button>
|
|
|
+ </div>
|
|
|
+ <div className="p-6">{children}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function CategoryBadge({ value }) {
|
|
|
+ const cat = CATEGORIES.find(c => c.value === value)
|
|
|
+ return <span className="inline-flex items-center gap-1 text-xs bg-blue-50 text-blue-700 px-2 py-0.5 rounded-full font-medium">{cat?.label || value}</span>
|
|
|
+}
|
|
|
+
|
|
|
+export default function Charges() {
|
|
|
+ const [properties, setProperties] = useState([])
|
|
|
+ const [charges, setCharges] = useState([])
|
|
|
+ const [filterProperty, setFilterProperty] = useState('')
|
|
|
+ const [filterYear, setFilterYear] = useState(String(CURRENT_YEAR))
|
|
|
+ const [showModal, setShowModal] = useState(false)
|
|
|
+ const [showBilan, setShowBilan] = useState(false)
|
|
|
+ const [bilan, setBilan] = useState(null)
|
|
|
+ const [bilanLoading, setBilanLoading] = useState(false)
|
|
|
+ const [loading, setLoading] = useState(false)
|
|
|
+ const [editCharge, setEditCharge] = useState(null)
|
|
|
+
|
|
|
+ const emptyForm = {
|
|
|
+ property_id: '',
|
|
|
+ category: 'eau',
|
|
|
+ label: '',
|
|
|
+ amount: '',
|
|
|
+ date: new Date().toISOString().slice(0, 10),
|
|
|
+ year: String(CURRENT_YEAR),
|
|
|
+ notes: '',
|
|
|
+ invoice: null,
|
|
|
+ }
|
|
|
+ const [form, setForm] = useState(emptyForm)
|
|
|
+
|
|
|
+ const loadProperties = async () => {
|
|
|
+ const { data } = await api.get('/properties')
|
|
|
+ setProperties(data)
|
|
|
+ if (data.length && !filterProperty) setFilterProperty(String(data[0].id))
|
|
|
+ }
|
|
|
+
|
|
|
+ const loadCharges = async () => {
|
|
|
+ const params = new URLSearchParams()
|
|
|
+ if (filterProperty) params.append('property_id', filterProperty)
|
|
|
+ if (filterYear) params.append('year', filterYear)
|
|
|
+ const { data } = await api.get(`/charges?${params}`)
|
|
|
+ setCharges(data)
|
|
|
+ }
|
|
|
+
|
|
|
+ useEffect(() => { loadProperties() }, [])
|
|
|
+ useEffect(() => { if (filterProperty) loadCharges() }, [filterProperty, filterYear])
|
|
|
+
|
|
|
+ const openAdd = () => {
|
|
|
+ setEditCharge(null)
|
|
|
+ setForm({ ...emptyForm, property_id: filterProperty, year: filterYear })
|
|
|
+ setShowModal(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ const openEdit = (c) => {
|
|
|
+ setEditCharge(c)
|
|
|
+ setForm({
|
|
|
+ property_id: String(c.property_id),
|
|
|
+ category: c.category,
|
|
|
+ label: c.label,
|
|
|
+ amount: String(c.amount),
|
|
|
+ date: c.date,
|
|
|
+ year: String(c.year),
|
|
|
+ notes: c.notes || '',
|
|
|
+ invoice: null,
|
|
|
+ })
|
|
|
+ setShowModal(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleSubmit = async e => {
|
|
|
+ e.preventDefault(); setLoading(true)
|
|
|
+ try {
|
|
|
+ const fd = new FormData()
|
|
|
+ Object.entries(form).forEach(([k, v]) => {
|
|
|
+ if (k === 'invoice') { if (v) fd.append('invoice', v) }
|
|
|
+ else fd.append(k, v)
|
|
|
+ })
|
|
|
+ if (editCharge) {
|
|
|
+ await api.put(`/charges/${editCharge.id}`, fd, { headers: { 'Content-Type': 'multipart/form-data' } })
|
|
|
+ toast.success('Charge mise à jour !')
|
|
|
+ } else {
|
|
|
+ await api.post('/charges', fd, { headers: { 'Content-Type': 'multipart/form-data' } })
|
|
|
+ toast.success('Charge ajoutée !')
|
|
|
+ }
|
|
|
+ setShowModal(false)
|
|
|
+ loadCharges()
|
|
|
+ } catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
|
|
|
+ finally { setLoading(false) }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleDelete = async (id) => {
|
|
|
+ if (!confirm('Supprimer cette charge ?')) return
|
|
|
+ try { await api.delete(`/charges/${id}`); toast.success('Charge supprimée'); loadCharges() }
|
|
|
+ catch { toast.error('Erreur lors de la suppression') }
|
|
|
+ }
|
|
|
+
|
|
|
+ const downloadInvoice = async (charge) => {
|
|
|
+ try {
|
|
|
+ const token = localStorage.getItem('token')
|
|
|
+ const resp = await fetch(`/api/charges/${charge.id}/invoice`, {
|
|
|
+ headers: { Authorization: `Bearer ${token}` }
|
|
|
+ })
|
|
|
+ if (!resp.ok) throw new Error()
|
|
|
+ const blob = await resp.blob()
|
|
|
+ const url = window.URL.createObjectURL(blob)
|
|
|
+ const a = document.createElement('a')
|
|
|
+ a.href = url; a.download = charge.invoice_name; a.click()
|
|
|
+ window.URL.revokeObjectURL(url)
|
|
|
+ } catch { toast.error('Impossible de télécharger la facture') }
|
|
|
+ }
|
|
|
+
|
|
|
+ const loadBilan = async () => {
|
|
|
+ if (!filterProperty || !filterYear) return
|
|
|
+ setBilanLoading(true)
|
|
|
+ try {
|
|
|
+ const { data } = await api.get(`/charges/bilan/${filterProperty}/${filterYear}`)
|
|
|
+ setBilan(data)
|
|
|
+ setShowBilan(true)
|
|
|
+ } catch (err) { toast.error(err.response?.data?.error || 'Erreur bilan') }
|
|
|
+ finally { setBilanLoading(false) }
|
|
|
+ }
|
|
|
+
|
|
|
+ const fmt = v => `${Number(v).toFixed(2)} €`
|
|
|
+ const fmtDate = d => d ? new Date(d + 'T00:00:00').toLocaleDateString('fr-FR') : '—'
|
|
|
+ const totalAmount = charges.reduce((s, c) => s + c.amount, 0)
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div>
|
|
|
+ <div className="flex items-center justify-between mb-6">
|
|
|
+ <div>
|
|
|
+ <h1 className="text-2xl font-bold text-gray-900">🧾 Gestion des charges</h1>
|
|
|
+ <p className="text-gray-500 text-sm mt-1">Charges réelles par immeuble et bilan annuel</p>
|
|
|
+ </div>
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <button onClick={loadBilan} disabled={!filterProperty || bilanLoading}
|
|
|
+ className="btn-secondary flex items-center gap-2">
|
|
|
+ {bilanLoading ? '...' : '📊 Bilan annuel'}
|
|
|
+ </button>
|
|
|
+ <button onClick={openAdd} className="btn-primary">+ Ajouter une charge</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Filters */}
|
|
|
+ <div className="card mb-6 flex flex-wrap gap-4">
|
|
|
+ <div className="flex-1 min-w-48">
|
|
|
+ <label className="label">Immeuble / Bien</label>
|
|
|
+ <select className="input" value={filterProperty} onChange={e => setFilterProperty(e.target.value)}>
|
|
|
+ <option value="">Tous les biens</option>
|
|
|
+ {properties.map(p => <option key={p.id} value={p.id}>{p.name} — {p.address}</option>)}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div className="w-36">
|
|
|
+ <label className="label">Année</label>
|
|
|
+ <select className="input" value={filterYear} onChange={e => setFilterYear(e.target.value)}>
|
|
|
+ {YEARS.map(y => <option key={y} value={y}>{y}</option>)}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ {filterProperty && filterYear && (
|
|
|
+ <div className="flex items-end">
|
|
|
+ <div className="bg-blue-50 border border-blue-200 rounded-lg px-4 py-2 text-sm">
|
|
|
+ <span className="text-blue-500">Total charges :</span>
|
|
|
+ <span className="text-blue-800 font-bold ml-2">{fmt(totalAmount)}</span>
|
|
|
+ <span className="text-blue-400 ml-2">({charges.length} ligne{charges.length > 1 ? 's' : ''})</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Charges list */}
|
|
|
+ {charges.length === 0 ? (
|
|
|
+ <div className="card text-center py-12">
|
|
|
+ <p className="text-4xl mb-3">🧾</p>
|
|
|
+ <p className="text-gray-500">Aucune charge enregistrée pour cette période.</p>
|
|
|
+ <button onClick={openAdd} className="btn-primary mt-4">Ajouter une première charge</button>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="card overflow-hidden p-0">
|
|
|
+ <table className="w-full">
|
|
|
+ <thead className="bg-gray-50 border-b">
|
|
|
+ <tr>
|
|
|
+ <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Date</th>
|
|
|
+ <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Catégorie</th>
|
|
|
+ <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Libellé</th>
|
|
|
+ <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Immeuble</th>
|
|
|
+ <th className="text-right px-6 py-3 text-sm font-semibold text-gray-600">Montant</th>
|
|
|
+ <th className="text-center px-6 py-3 text-sm font-semibold text-gray-600">Facture</th>
|
|
|
+ <th className="px-6 py-3"></th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody className="divide-y divide-gray-50">
|
|
|
+ {charges.map(c => (
|
|
|
+ <tr key={c.id} className="hover:bg-gray-50">
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-600">{fmtDate(c.date)}</td>
|
|
|
+ <td className="px-6 py-4"><CategoryBadge value={c.category} /></td>
|
|
|
+ <td className="px-6 py-4 text-gray-900 font-medium">{c.label}</td>
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-500">{c.property_name}</td>
|
|
|
+ <td className="px-6 py-4 text-right font-semibold text-gray-900">{fmt(c.amount)}</td>
|
|
|
+ <td className="px-6 py-4 text-center">
|
|
|
+ {c.invoice_path ? (
|
|
|
+ <button onClick={() => downloadInvoice(c)}
|
|
|
+ className="text-green-600 hover:text-green-800 text-sm font-medium" title={c.invoice_name}>
|
|
|
+ 📎 {c.invoice_name?.length > 20 ? c.invoice_name.slice(0, 17) + '…' : c.invoice_name}
|
|
|
+ </button>
|
|
|
+ ) : (
|
|
|
+ <span className="text-gray-300 text-xs">—</span>
|
|
|
+ )}
|
|
|
+ </td>
|
|
|
+ <td className="px-6 py-4 text-right space-x-2 whitespace-nowrap">
|
|
|
+ <button onClick={() => openEdit(c)} className="text-blue-500 hover:text-blue-700 text-sm">✏️</button>
|
|
|
+ <button onClick={() => handleDelete(c.id)} className="text-gray-400 hover:text-red-600 ml-1">🗑️</button>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ ))}
|
|
|
+ </tbody>
|
|
|
+ <tfoot className="bg-gray-50 border-t-2 border-gray-200">
|
|
|
+ <tr>
|
|
|
+ <td colSpan={4} className="px-6 py-3 text-sm font-semibold text-gray-700">Total</td>
|
|
|
+ <td className="px-6 py-3 text-right font-bold text-gray-900">{fmt(totalAmount)}</td>
|
|
|
+ <td colSpan={2}></td>
|
|
|
+ </tr>
|
|
|
+ </tfoot>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Add/Edit Modal */}
|
|
|
+ {showModal && (
|
|
|
+ <Modal title={editCharge ? 'Modifier la charge' : 'Ajouter une charge'} onClose={() => setShowModal(false)}>
|
|
|
+ <form onSubmit={handleSubmit} className="space-y-4">
|
|
|
+ <div>
|
|
|
+ <label className="label">Immeuble / Bien *</label>
|
|
|
+ <select className="input" required value={form.property_id}
|
|
|
+ onChange={e => setForm(f => ({ ...f, property_id: e.target.value }))}>
|
|
|
+ <option value="">— Sélectionner —</option>
|
|
|
+ {properties.map(p => <option key={p.id} value={p.id}>{p.name} — {p.address}</option>)}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div className="grid grid-cols-2 gap-4">
|
|
|
+ <div>
|
|
|
+ <label className="label">Catégorie *</label>
|
|
|
+ <select className="input" value={form.category}
|
|
|
+ onChange={e => setForm(f => ({ ...f, category: e.target.value }))}>
|
|
|
+ {CATEGORIES.map(c => <option key={c.value} value={c.value}>{c.label}</option>)}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="label">Année *</label>
|
|
|
+ <select className="input" value={form.year}
|
|
|
+ onChange={e => setForm(f => ({ ...f, year: e.target.value }))}>
|
|
|
+ {YEARS.map(y => <option key={y} value={y}>{y}</option>)}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="label">Libellé *</label>
|
|
|
+ <input className="input" required value={form.label}
|
|
|
+ onChange={e => setForm(f => ({ ...f, label: e.target.value }))}
|
|
|
+ placeholder="Ex : Facture eau T1 2024" />
|
|
|
+ </div>
|
|
|
+ <div className="grid grid-cols-2 gap-4">
|
|
|
+ <div>
|
|
|
+ <label className="label">Montant (€) *</label>
|
|
|
+ <input className="input" type="number" step="0.01" min="0" required value={form.amount}
|
|
|
+ onChange={e => setForm(f => ({ ...f, amount: e.target.value }))} />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="label">Date de la dépense *</label>
|
|
|
+ <input className="input" type="date" required value={form.date}
|
|
|
+ onChange={e => setForm(f => ({ ...f, date: e.target.value }))} />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="label">Notes (optionnel)</label>
|
|
|
+ <input className="input" value={form.notes}
|
|
|
+ onChange={e => setForm(f => ({ ...f, notes: e.target.value }))}
|
|
|
+ placeholder="Numéro de facture, prestataire…" />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="label">Facture (PDF, JPG, PNG — max 10 Mo)</label>
|
|
|
+ {editCharge?.invoice_name && !form.invoice && (
|
|
|
+ <p className="text-xs text-green-600 mb-1">📎 Fichier actuel : {editCharge.invoice_name}</p>
|
|
|
+ )}
|
|
|
+ <input className="input" type="file" accept=".pdf,.jpg,.jpeg,.png,.webp"
|
|
|
+ onChange={e => setForm(f => ({ ...f, invoice: e.target.files[0] || null }))} />
|
|
|
+ </div>
|
|
|
+ <div className="flex gap-3 pt-2">
|
|
|
+ <button type="button" onClick={() => setShowModal(false)} className="btn-secondary flex-1">Annuler</button>
|
|
|
+ <button type="submit" disabled={loading} className="btn-primary flex-1">
|
|
|
+ {loading ? '...' : editCharge ? 'Mettre à jour' : 'Ajouter'}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </Modal>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Bilan annuel Modal */}
|
|
|
+ {showBilan && bilan && (
|
|
|
+ <Modal title={`📊 Bilan annuel ${bilan.year} — ${bilan.property?.name}`} onClose={() => setShowBilan(false)}>
|
|
|
+ <div className="space-y-6">
|
|
|
+ {/* Summary cards */}
|
|
|
+ <div className="grid grid-cols-2 gap-3">
|
|
|
+ <div className="bg-red-50 border border-red-100 rounded-xl p-4 text-center">
|
|
|
+ <p className="text-xs text-red-500 font-medium mb-1">Charges réelles</p>
|
|
|
+ <p className="text-2xl font-bold text-red-700">{fmt(bilan.total_charges_reelles)}</p>
|
|
|
+ <p className="text-xs text-red-400 mt-1">{bilan.charges_count} facture{bilan.charges_count > 1 ? 's' : ''}</p>
|
|
|
+ </div>
|
|
|
+ <div className="bg-blue-50 border border-blue-100 rounded-xl p-4 text-center">
|
|
|
+ <p className="text-xs text-blue-500 font-medium mb-1">Provisions perçues</p>
|
|
|
+ <p className="text-2xl font-bold text-blue-700">{fmt(bilan.total_provisions_percues)}</p>
|
|
|
+ <p className="text-xs text-blue-400 mt-1">de tous les locataires</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Solde global */}
|
|
|
+ {(() => {
|
|
|
+ const solde = bilan.total_provisions_percues - bilan.total_charges_reelles
|
|
|
+ const isPositive = solde >= 0
|
|
|
+ return (
|
|
|
+ <div className={`rounded-xl p-4 text-center border ${isPositive ? 'bg-green-50 border-green-200' : 'bg-orange-50 border-orange-200'}`}>
|
|
|
+ <p className={`text-sm font-medium ${isPositive ? 'text-green-600' : 'text-orange-600'}`}>
|
|
|
+ {isPositive ? '✅ Trop-perçu global (à restituer)' : '⚠️ Solde insuffisant (à appeler)'}
|
|
|
+ </p>
|
|
|
+ <p className={`text-3xl font-bold mt-1 ${isPositive ? 'text-green-700' : 'text-orange-700'}`}>
|
|
|
+ {isPositive ? '+' : ''}{fmt(solde)}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ })()}
|
|
|
+
|
|
|
+ {/* Charges par catégorie */}
|
|
|
+ {bilan.charges_by_category.length > 0 && (
|
|
|
+ <div>
|
|
|
+ <h3 className="text-sm font-semibold text-gray-700 mb-2">Répartition par catégorie</h3>
|
|
|
+ <div className="space-y-1">
|
|
|
+ {bilan.charges_by_category.map(c => {
|
|
|
+ const cat = CATEGORIES.find(x => x.value === c.category)
|
|
|
+ const pct = bilan.total_charges_reelles > 0 ? (c.total / bilan.total_charges_reelles * 100).toFixed(1) : 0
|
|
|
+ return (
|
|
|
+ <div key={c.category} className="flex items-center justify-between text-sm py-1">
|
|
|
+ <span className="text-gray-600">{cat?.label || c.category}</span>
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ <div className="w-24 bg-gray-100 rounded-full h-1.5">
|
|
|
+ <div className="bg-blue-500 h-1.5 rounded-full" style={{ width: `${pct}%` }}></div>
|
|
|
+ </div>
|
|
|
+ <span className="text-gray-500 w-8 text-right text-xs">{pct}%</span>
|
|
|
+ <span className="font-medium text-gray-900 w-20 text-right">{fmt(c.total)}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Par locataire */}
|
|
|
+ {bilan.bilans.length > 0 ? (
|
|
|
+ <div>
|
|
|
+ <h3 className="text-sm font-semibold text-gray-700 mb-3">Décompte par locataire</h3>
|
|
|
+ <div className="space-y-3">
|
|
|
+ {bilan.bilans.map(b => {
|
|
|
+ const isPos = b.trop_percu >= 0
|
|
|
+ return (
|
|
|
+ <div key={b.lease_id} className="border rounded-xl p-4">
|
|
|
+ <div className="flex items-start justify-between mb-3">
|
|
|
+ <div>
|
|
|
+ <p className="font-semibold text-gray-900">{b.tenant_name}</p>
|
|
|
+ {b.tenant_email && <p className="text-xs text-gray-400">{b.tenant_email}</p>}
|
|
|
+ </div>
|
|
|
+ <span className={`text-sm font-bold px-2 py-0.5 rounded-full ${isPos ? 'bg-green-100 text-green-700' : 'bg-orange-100 text-orange-700'}`}>
|
|
|
+ {isPos ? '↩ Remboursement' : '↑ Appel de fonds'}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div className="grid grid-cols-3 gap-2 text-sm">
|
|
|
+ <div className="bg-gray-50 rounded-lg p-2 text-center">
|
|
|
+ <p className="text-xs text-gray-500">Provisions perçues</p>
|
|
|
+ <p className="font-semibold text-gray-800">{fmt(b.provisions_percues)}</p>
|
|
|
+ </div>
|
|
|
+ <div className="bg-gray-50 rounded-lg p-2 text-center">
|
|
|
+ <p className="text-xs text-gray-500">Quote-part réelle ({b.ratio_percent}%)</p>
|
|
|
+ <p className="font-semibold text-gray-800">{fmt(b.quote_part_charges)}</p>
|
|
|
+ </div>
|
|
|
+ <div className={`rounded-lg p-2 text-center ${isPos ? 'bg-green-50' : 'bg-orange-50'}`}>
|
|
|
+ <p className={`text-xs font-medium ${isPos ? 'text-green-600' : 'text-orange-600'}`}>
|
|
|
+ {isPos ? 'Trop-perçu' : 'Solde dû'}
|
|
|
+ </p>
|
|
|
+ <p className={`font-bold text-lg ${isPos ? 'text-green-700' : 'text-orange-700'}`}>
|
|
|
+ {fmt(Math.abs(b.trop_percu))}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <p className="text-center text-gray-400 text-sm py-4">Aucun locataire trouvé pour cette période.</p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </Modal>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|