import { useEffect, useState } from 'react' import api from '../api' import toast from 'react-hot-toast' import { useAuth } from '../context/AuthContext' const CURRENT_YEAR = new Date().getFullYear() function Modal({ title, onClose, children }) { return (

{title}

{children}
) } function CategoryBadge({ value, categories }) { const cat = categories.find(c => c.value === value) return {cat ? `${cat.icon} ${cat.label}` : value} } export default function Charges() { const { user } = useAuth() 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 [pdfLoadingLeaseId, setPdfLoadingLeaseId] = useState(null) const [closingLeaseId, setClosingLeaseId] = useState(null) const [cancelingLeaseId, setCancelingLeaseId] = useState(null) const [loading, setLoading] = useState(false) const [editCharge, setEditCharge] = useState(null) const [deductibleSummary, setDeductibleSummary] = useState(null) const [showDeductible, setShowDeductible] = useState(false) const [categories, setCategories] = useState([]) const [availableYears, setAvailableYears] = useState([CURRENT_YEAR]) const emptyForm = { property_id: '', category: 'eau', label: '', amount: '', date: new Date().toISOString().slice(0, 10), year: String(CURRENT_YEAR), notes: '', invoice: null, recoverable_type: 'recoverable', } 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) } const loadDeductibleSummary = async () => { if (!filterProperty) { setDeductibleSummary(null); return } try { const { data } = await api.get(`/charges/deductible-summary/${filterProperty}`) setDeductibleSummary(data) } catch { setDeductibleSummary(null) } } // Load all distinct years from charges (unfiltered by year) to populate year selectors const loadAvailableYears = async () => { const params = new URLSearchParams() if (filterProperty) params.append('property_id', filterProperty) try { const { data } = await api.get(`/charges?${params}`) const yearsFromData = [...new Set(data.map(c => c.year).filter(Boolean))] const merged = [...new Set([CURRENT_YEAR, ...yearsFromData])].sort((a, b) => b - a) setAvailableYears(merged) } catch {} } useEffect(() => { loadProperties(); api.get('/categories').then(r => setCategories(r.data)) }, []) useEffect(() => { if (filterProperty) { loadCharges(); loadDeductibleSummary(); loadAvailableYears() } }, [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, recoverable_type: c.recoverable_type || (c.recoverable !== 0 ? 'recoverable' : 'none'), }) 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 printBilan = () => { if (!bilan) return const catLabel = v => { const c = categories.find(c => c.value === v); return c ? `${c.icon} ${c.label}` : v } const fmtEur = v => `${Number(v).toFixed(2)} €` const solde = bilan.total_provisions_percues - bilan.total_charges_reelles const isPos = solde >= 0 const ownerName = (user?.first_name && user?.last_name) ? `${user.first_name} ${user.last_name}` : (user?.name || '') const ownerBlock = `
BAILLEUR ${ownerName} ${user?.address ? `
${user.address}` : ''} ${user?.email ? `
${user.email}` : ''} ${user?.phone ? `
${user.phone}` : ''}
` const tenantsRows = bilan.bilans.map(b => { const pos = b.trop_percu >= 0 return `
${b.tenant_name} ${b.tenant_email ? `
${b.tenant_email}` : ''}
${pos ? '↩ Remboursement' : '↑ Appel de fonds'}
Occupation ${b.occupied_days} / ${b.year_days} jours (${Number(b.ratio_percent || 0).toFixed(2)}%)
Provisions perçues ${fmtEur(b.provisions_percues)}
Quote-part des charges réelles (prorata occupation) ${fmtEur(b.quote_part_charges)}
${pos ? 'Trop-perçu à restituer' : 'Solde restant dû'} ${fmtEur(Math.abs(b.trop_percu))}
` }).join('') const catRows = bilan.charges_by_category.map(c => { const pct = bilan.total_charges_reelles > 0 ? (c.total / bilan.total_charges_reelles * 100).toFixed(1) : 0 return `${catLabel(c.category)}${fmtEur(c.total)}${pct}%` }).join('') const html = ` Bilan de charges ${bilan.year} — ${bilan.property?.name}

Bilan annuel de charges ${bilan.year}

${bilan.property?.name} — ${bilan.property?.address || ''}

${ownerBlock}
Charges réelles
${fmtEur(bilan.total_charges_reelles)}
${bilan.charges_count} facture${bilan.charges_count > 1 ? 's' : ''}
Provisions perçues
${fmtEur(bilan.total_provisions_percues)}
${isPos ? 'Trop-perçu global' : 'Solde insuffisant'}
${isPos ? '+' : '-'}${fmtEur(Math.abs(solde))}
${bilan.charges_by_category.length > 0 ? `

Répartition des charges par catégorie

${catRows}
CatégorieMontant%
Total${fmtEur(bilan.total_charges_reelles)}
` : ''} ${bilan.bilans.length > 0 ? `

Décompte par locataire

${tenantsRows}` : ''}