Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ Toutes les évolutions notables de **Dalibi** sont consignées ici.
Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le versionnage
respecte [SemVer](https://semver.org/lang/fr/).

## [1.0.1] — 2026-09-02

### Corrigé
- **Couleur des catégories dans les graphiques** : la couleur suivait la *position*
dans le tableau et non la catégorie. Lorsqu'une catégorie vide était filtrée, les
suivantes changeaient de couleur — les filles pouvaient hériter du bleu des garçons,
« Très bien » perdre son vert.

### Accessibilité
- **Palette revue et validée** (bande de luminosité, chroma, séparation en vision
déficiente, contraste) dans les modes clair *et* sombre. L'ancien couple bleu/violet
était indistinguable en deutéranopie.
- **Légendes ajoutées** aux camemberts qui n'en avaient pas : l'identité ne repose plus
sur la seule couleur.
- Les répartitions à plus de trois catégories (modes de paiement, régions) passent en
**barres étiquetées** ; les mentions, données ordonnées, en **rampe séquentielle**.
- **Mode sombre** : couleurs d'axes et de grille désormais adaptées au thème, au lieu de
valeurs claires codées en dur (la grille était quasi invisible sur fond sombre).
- L'animation d'entrée des camemberts est retirée : elle laissait le graphique **vide**
lorsque les images d'animation sont ralenties (onglet en arrière-plan, économie
d'énergie, `prefers-reduced-motion`).
## [1.0.0] — 2026-09-02

Première version stable. Dalibi couvre le cycle complet de gestion d'un établissement
Expand Down Expand Up @@ -59,4 +80,5 @@ scolaire, de l'inscription de l'élève au bulletin, en passant par l'écolage e
Laravel 12 (PHP 8.3+), React 19 + TypeScript via Inertia.js, Tailwind CSS 4, PostgreSQL,
identifiants UUID. **502 tests** automatisés.

[1.0.1]: https://github.com/wearedalibi/dalibi/releases/tag/v1.0.1
[1.0.0]: https://github.com/wearedalibi/dalibi/releases/tag/v1.0.0
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://www.schemastore.org/package.json",
"private": true,
"version": "1.0.0",
"version": "1.0.1",
"type": "module",
"scripts": {
"build": "vite build",
Expand Down
96 changes: 96 additions & 0 deletions resources/js/lib/chart-theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useMemo } from 'react';
import { useAppearance } from '@/hooks/use-appearance';

/**
* Thème des graphiques.
*
* Les couleurs de séries sont **validées** (bande de luminosité, plancher de
* chroma, séparation en vision déficiente, plancher en vision normale, contraste
* sur la surface) dans les deux modes. Ne pas les modifier sans revalider :
* bleu et violet, par exemple, sont indistinguables en deutéranopie (ΔE 0.4).
*
* ⚠️ Trois teintes au maximum lorsque toutes les paires sont comparables entre
* elles (camembert, nuage de points) : au-delà, aucun jeu ne tient dans les deux
* modes. Pour davantage de catégories, utiliser des **barres étiquetées en une
* seule teinte** — le travail est alors une magnitude, pas une identité.
*/
const SERIES = {
light: ['#2a78d6', '#eb6834', '#1baf7a', '#4a3aa7'],
dark: ['#3987e5', '#d95926', '#199e70', '#9085e9'],
} as const;

/** Rampe séquentielle (magnitude / donnée ordonnée) : une seule teinte, clair → foncé. */
const SEQUENTIAL = {
light: ['#bfdbfe', '#60a5fa', '#2a78d6', '#1e3a8a'],
dark: ['#1e3a8a', '#1d4ed8', '#3987e5', '#93c5fd'],
} as const;

/** Couleurs d'état — réservées, jamais réutilisées comme teinte de série. */
const STATUS = {
light: { good: '#1baf7a', warning: '#eda100', critical: '#e34948' },
dark: { good: '#199e70', warning: '#c98500', critical: '#e66767' },
} as const;

/** Habillage : grille et axes restent en retrait, jamais au premier plan. */
const CHROME = {
light: { grid: '#e5e7eb', axis: '#6b7280', tick: '#9ca3af', surface: '#ffffff', border: '#e5e7eb' },
dark: { grid: '#374151', axis: '#9ca3af', tick: '#9ca3af', surface: '#1f2937', border: '#374151' },
} as const;

export type ChartTheme = {
mode: 'light' | 'dark';
/** Teintes catégorielles, dans un ordre fixe — ne jamais cycler. */
series: readonly string[];
/** Première teinte : le défaut d'une série unique. */
primary: string;
sequential: readonly string[];
status: { good: string; warning: string; critical: string };
grid: string;
axis: string;
tick: string;
surface: string;
border: string;
/** Style de l'infobulle, accordé au mode. */
tooltip: { contentStyle: React.CSSProperties; itemStyle: React.CSSProperties };
};

export function useChartTheme(): ChartTheme {
const { resolvedAppearance } = useAppearance();

return useMemo<ChartTheme>(() => {
const mode = resolvedAppearance;
const chrome = CHROME[mode];

return {
mode,
series: SERIES[mode],
primary: SERIES[mode][0],
sequential: SEQUENTIAL[mode],
status: STATUS[mode],
...chrome,
tooltip: {
contentStyle: {
borderRadius: 12,
border: `1px solid ${chrome.border}`,
background: chrome.surface,
fontSize: 13,
color: mode === 'dark' ? '#f3f4f6' : '#111827',
},
itemStyle: { color: mode === 'dark' ? '#f3f4f6' : '#111827' },
},
};
}, [resolvedAppearance]);
}

/**
* Couleur d'une catégorie, dérivée de son **identité** et non de sa position.
*
* Indispensable quand la liste est filtrée ou triée : sans cela, retirer une
* catégorie vide décale toutes les couleurs (les filles héritent du bleu des
* garçons, « Très bien » perd son vert…).
*/
export function colorFor(key: string, keys: readonly string[], series: readonly string[]): string {
const index = keys.indexOf(key);

return series[index === -1 ? 0 : index % series.length];
}
26 changes: 14 additions & 12 deletions resources/js/pages/Comptabilite/Accounting/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Head, router } from '@inertiajs/react';
import { useMoney } from '@/helpers/money';
import {
TrendingUp, TrendingDown, Users, AlertTriangle,
CheckCircle2, Clock, AlertCircle, ChevronDown,
Eye, BookOpen, Banknote, Filter, XCircle,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { route } from '@/helpers/route';
import AppLayout from '@/layouts/app-layout';
import { useState } from 'react';
import {
Bar, CartesianGrid, ComposedChart, Legend, Line,
ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis,
} from 'recharts';
import { Button } from '@/components/ui/button';
import { useMoney } from '@/helpers/money';
import { route } from '@/helpers/route';
import AppLayout from '@/layouts/app-layout';
import { useChartTheme } from '@/lib/chart-theme';

/* ------------------------------------------------------------------ */
/* Types */
Expand Down Expand Up @@ -166,6 +167,7 @@ function SectionHeader({ icon, title, count }: { icon: React.ReactNode; title: s
export default function AccountingDashboard({
academicYears, classrooms, filters, globalStats, monthlyPayments, byClass, studentsUnpaid, studentsUnpaidTotal,
}: Readonly<DashboardProps>) {
const theme = useChartTheme();
const fmt = useMoney();
const [yearId, setYearId] = useState(filters.academic_year_id ?? '');
const [classId, setClassId] = useState(filters.class_id ?? '');
Expand Down Expand Up @@ -332,16 +334,16 @@ export default function AccountingDashboard({
<div className="h-72 mt-4 -ml-2">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={chartData} margin={{ top: 10, right: 12, left: 4, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" vertical={false} />
<XAxis dataKey="label" tick={{ fontSize: 12, fill: '#6b7280' }} axisLine={false} tickLine={false} />
<CartesianGrid strokeDasharray="3 3" stroke={theme.grid} vertical={false} />
<XAxis dataKey="label" tick={{ fontSize: 12, fill: theme.axis }} axisLine={false} tickLine={false} />
<YAxis
tick={{ fontSize: 11, fill: '#9ca3af' }} axisLine={false} tickLine={false} width={70}
tick={{ fontSize: 11, fill: theme.tick }} axisLine={false} tickLine={false} width={70}
tickFormatter={(v: number) => (v >= 1000 ? `${Math.round(v / 1000)}k` : String(v))}
/>
<Tooltip
formatter={(value, name) => [fmt(Number(value)), name === 'encaisse' ? 'Encaissé du mois' : 'Cumul encaissé']}
labelFormatter={(_label, payload) => payload?.[0]?.payload?.fullLabel ?? ''}
contentStyle={{ borderRadius: 12, border: '1px solid #e5e7eb', fontSize: 13 }}
contentStyle={theme.tooltip.contentStyle} itemStyle={theme.tooltip.itemStyle}
/>
<Legend
formatter={(value: string) =>
Expand All @@ -350,11 +352,11 @@ export default function AccountingDashboard({
wrapperStyle={{ fontSize: 12 }}
/>
{totalExpected > 0 && (
<ReferenceLine y={totalExpected} stroke="#f59e0b" strokeDasharray="5 5"
label={{ value: 'Attendu', position: 'right', fill: '#b45309', fontSize: 11 }} />
<ReferenceLine y={totalExpected} stroke={theme.status.warning} strokeDasharray="5 5"
label={{ value: 'Attendu', position: 'right', fill: theme.status.warning, fontSize: 11 }} />
)}
<Bar dataKey="encaisse" name="encaisse" fill="#3b82f6" radius={[6, 6, 0, 0]} maxBarSize={44} />
<Line type="monotone" dataKey="cumul" name="cumul" stroke="#10b981" strokeWidth={2.5} dot={{ r: 3 }} />
<Bar dataKey="encaisse" name="encaisse" fill={theme.primary} radius={[6, 6, 0, 0]} maxBarSize={44} />
<Line type="monotone" dataKey="cumul" name="cumul" stroke={theme.series[2]} strokeWidth={2.5} dot={{ r: 3 }} />
</ComposedChart>
</ResponsiveContainer>
</div>
Expand Down
Loading
Loading