|
|
@@ -3,7 +3,19 @@ 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 (
|
|
|
@@ -19,9 +31,9 @@ function Modal({ title, onClose, children }) {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function CategoryBadge({ value, categories }) {
|
|
|
- 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 ? `${cat.icon} ${cat.label}` : value}</span>
|
|
|
+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() {
|
|
|
@@ -41,8 +53,6 @@ export default function Charges() {
|
|
|
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: '',
|
|
|
@@ -79,20 +89,8 @@ export default function Charges() {
|
|
|
} 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])
|
|
|
+ useEffect(() => { loadProperties() }, [])
|
|
|
+ useEffect(() => { if (filterProperty) { loadCharges(); loadDeductibleSummary() } }, [filterProperty, filterYear])
|
|
|
|
|
|
const openAdd = () => {
|
|
|
setEditCharge(null)
|
|
|
@@ -171,7 +169,7 @@ export default function Charges() {
|
|
|
|
|
|
const printBilan = () => {
|
|
|
if (!bilan) return
|
|
|
- const catLabel = v => { const c = categories.find(c => c.value === v); return c ? `${c.icon} ${c.label}` : v }
|
|
|
+ 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
|
|
|
@@ -413,7 +411,7 @@ export default function Charges() {
|
|
|
<div className="w-36">
|
|
|
<label className="label">Année</label>
|
|
|
<select className="input" value={filterYear} onChange={e => setFilterYear(e.target.value)}>
|
|
|
- {availableYears.map(y => <option key={y} value={y}>{y}</option>)}
|
|
|
+ {YEARS.map(y => <option key={y} value={y}>{y}</option>)}
|
|
|
</select>
|
|
|
</div>
|
|
|
{filterProperty && filterYear && (
|
|
|
@@ -437,7 +435,7 @@ export default function Charges() {
|
|
|
<div className="flex items-center gap-2">
|
|
|
<span className="text-lg">💼</span>
|
|
|
<div>
|
|
|
- <h3 className="text-sm font-semibold text-purple-800">Charges déductibles des impôts — 11 ans</h3>
|
|
|
+ <h3 className="text-sm font-semibold text-purple-800">Charges déductibles des impôts — 10 ans</h3>
|
|
|
<p className="text-xs text-purple-500">
|
|
|
Total cumulé : <span className="font-bold">{fmt(deductibleSummary.grand_total)}</span>
|
|
|
</p>
|
|
|
@@ -451,8 +449,7 @@ export default function Charges() {
|
|
|
<thead>
|
|
|
<tr className="bg-purple-50 border-b border-purple-100">
|
|
|
<th className="text-left px-4 py-2 text-purple-700 font-semibold">Année</th>
|
|
|
- <th className="text-right px-4 py-2 text-purple-700 font-semibold">Investissement</th>
|
|
|
- <th className="text-right px-4 py-2 text-purple-700 font-semibold">Amortissement /10 ans</th>
|
|
|
+ <th className="text-right px-4 py-2 text-purple-700 font-semibold">Montant déductible</th>
|
|
|
<th className="text-right px-4 py-2 text-purple-700 font-semibold">Nb factures</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
@@ -463,9 +460,6 @@ export default function Charges() {
|
|
|
<td className={`px-4 py-2 text-right ${y.total > 0 ? 'text-purple-700' : 'text-gray-300'}`}>
|
|
|
{y.total > 0 ? fmt(y.total) : '—'}
|
|
|
</td>
|
|
|
- <td className={`px-4 py-2 text-right ${y.amortization > 0 ? 'text-blue-700 font-medium' : 'text-gray-300'}`}>
|
|
|
- {y.amortization > 0 ? fmt(y.amortization) : '—'}
|
|
|
- </td>
|
|
|
<td className={`px-4 py-2 text-right ${y.count > 0 ? 'text-gray-600' : 'text-gray-300'}`}>
|
|
|
{y.count > 0 ? y.count : '—'}
|
|
|
</td>
|
|
|
@@ -474,9 +468,8 @@ export default function Charges() {
|
|
|
</tbody>
|
|
|
<tfoot className="bg-purple-50 border-t-2 border-purple-200">
|
|
|
<tr>
|
|
|
- <td className="px-4 py-2 font-bold text-purple-800">Total (11 ans)</td>
|
|
|
+ <td className="px-4 py-2 font-bold text-purple-800">Total (10 ans)</td>
|
|
|
<td className="px-4 py-2 text-right font-bold text-purple-800">{fmt(deductibleSummary.grand_total)}</td>
|
|
|
- <td className="px-4 py-2 text-right font-bold text-blue-800">{fmt(deductibleSummary.grand_amortization)}</td>
|
|
|
<td className="px-4 py-2 text-right font-bold text-gray-600">
|
|
|
{deductibleSummary.years.reduce((s, y) => s + y.count, 0)}
|
|
|
</td>
|
|
|
@@ -516,7 +509,7 @@ export default function Charges() {
|
|
|
{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} categories={categories} /></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>
|
|
|
@@ -559,7 +552,7 @@ export default function Charges() {
|
|
|
<div className="flex-1 min-w-0">
|
|
|
<p className="font-semibold text-gray-900 truncate">{c.label}</p>
|
|
|
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
|
|
- <CategoryBadge value={c.category} categories={categories} />
|
|
|
+ <CategoryBadge value={c.category} />
|
|
|
{c.recoverable_type === 'recoverable'
|
|
|
? <span className="text-xs bg-green-100 text-green-700 px-1.5 py-0.5 rounded-full font-medium">Locataire</span>
|
|
|
: c.recoverable_type === 'deductible'
|
|
|
@@ -606,14 +599,14 @@ export default function Charges() {
|
|
|
<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.icon} {c.label}</option>)}
|
|
|
+ {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 }))}>
|
|
|
- {availableYears.map(y => <option key={y} value={y}>{y}</option>)}
|
|
|
+ {YEARS.map(y => <option key={y} value={y}>{y}</option>)}
|
|
|
</select>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -742,11 +735,11 @@ export default function Charges() {
|
|
|
<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 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 ? `${cat.icon} ${cat.label}` : c.category}</span>
|
|
|
+ <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>
|