import { useEffect, useState } from 'react'
import api from '../api'
import toast from 'react-hot-toast'
import { useAuth } from '../context/AuthContext'
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 (
)
}
function CategoryBadge({ value }) {
const cat = CATEGORIES.find(c => c.value === value)
return {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 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) }
}
useEffect(() => { loadProperties() }, [])
useEffect(() => { if (filterProperty) { loadCharges(); loadDeductibleSummary() } }, [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 => CATEGORIES.find(c => c.value === v)?.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 `
| 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
| Catégorie | Montant | % |
${catRows}
| Total | ${fmtEur(bilan.total_charges_reelles)} | |
` : ''}
${bilan.bilans.length > 0 ? `Décompte par locataire
${tenantsRows}` : ''}