mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-21 05:58:10 +02:00
feat: hit-parade date range + fournisseur filter; fix create user button layout
- Replace month dropdown with date picker (Du/Au) + Appliquer button - Add fournisseur filter dropdown (client-side, all suppliers from current data) - Totals update live when fournisseur filter changes - SQL: use BETWEEN date range instead of TO_CHAR month match - Create user button: proper 3-col form layout so button stays on one line Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
05354a0e74
commit
868886db87
4 files changed
+160
-119
No files matched your search
@@ -22,24 +22,25 @@ function formatPct(ca: number, marge: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mois: string;
|
dateDebut: string;
|
||||||
currentMois: string;
|
dateFin: string;
|
||||||
pivotted: HitParadePivotRow[];
|
pivotted: HitParadePivotRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HitParadeClient({ mois, pivotted }: Props) {
|
export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [sortKey, setSortKey] = useState<SortKey>(DEFAULT_SORT);
|
const [sortKey, setSortKey] = useState<SortKey>(DEFAULT_SORT);
|
||||||
const [sortDir, setSortDir] = useState<SortDir>(DEFAULT_DIR);
|
const [sortDir, setSortDir] = useState<SortDir>(DEFAULT_DIR);
|
||||||
|
const [filterFournisseur, setFilterFournisseur] = useState<string>("Tous");
|
||||||
|
|
||||||
const months: { value: string; label: string }[] = [];
|
// Dates locales pour les inputs (évite rechargement à chaque frappe)
|
||||||
const now = new Date();
|
const [localDebut, setLocalDebut] = useState(dateDebut);
|
||||||
for (let i = 0; i < 24; i++) {
|
const [localFin, setLocalFin] = useState(dateFin);
|
||||||
const d = new Date(now.getFullYear(), now.getMonth() - i);
|
|
||||||
const value = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
const fournisseurs = useMemo(() => {
|
||||||
const label = new Intl.DateTimeFormat("fr-FR", { year: "numeric", month: "long" }).format(d);
|
const set = new Set(pivotted.map(r => r.fournisseur));
|
||||||
months.push({ value, label: label.charAt(0).toUpperCase() + label.slice(1) });
|
return ["Tous", ...Array.from(set).sort((a, b) => a.localeCompare(b, "fr"))];
|
||||||
}
|
}, [pivotted]);
|
||||||
|
|
||||||
function handleSort(key: SortKey) {
|
function handleSort(key: SortKey) {
|
||||||
if (sortKey === key) {
|
if (sortKey === key) {
|
||||||
@@ -55,25 +56,37 @@ export function HitParadeClient({ mois, pivotted }: Props) {
|
|||||||
setSortDir(DEFAULT_DIR);
|
setSortDir(DEFAULT_DIR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyDates() {
|
||||||
|
if (localDebut && localFin) {
|
||||||
|
router.push(`/hit-parade?debut=${localDebut}&fin=${localFin}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isSorted = sortKey !== DEFAULT_SORT || sortDir !== DEFAULT_DIR;
|
const isSorted = sortKey !== DEFAULT_SORT || sortDir !== DEFAULT_DIR;
|
||||||
|
|
||||||
const sorted = useMemo(() => {
|
const filtered = useMemo(() =>
|
||||||
return [...pivotted].sort((a, b) => {
|
filterFournisseur === "Tous" ? pivotted : pivotted.filter(r => r.fournisseur === filterFournisseur),
|
||||||
|
[pivotted, filterFournisseur]
|
||||||
|
);
|
||||||
|
|
||||||
|
const sorted = useMemo(() =>
|
||||||
|
[...filtered].sort((a, b) => {
|
||||||
const diff = a[sortKey] - b[sortKey];
|
const diff = a[sortKey] - b[sortKey];
|
||||||
return sortDir === "desc" ? -diff : diff;
|
return sortDir === "desc" ? -diff : diff;
|
||||||
});
|
}),
|
||||||
}, [pivotted, sortKey, sortDir]);
|
[filtered, sortKey, sortDir]
|
||||||
|
);
|
||||||
|
|
||||||
const totals = useMemo(() => pivotted.reduce(
|
const totals = useMemo(() => sorted.reduce(
|
||||||
(acc, r) => ({
|
(acc, r) => ({
|
||||||
qte292: acc.qte292 + r.qte292, ca292: acc.ca292 + r.ca292, marge292: acc.marge292 + r.marge292,
|
qte292: acc.qte292 + r.qte292, ca292: acc.ca292 + r.ca292, marge292: acc.marge292 + r.marge292,
|
||||||
qte579: acc.qte579 + r.qte579, ca579: acc.ca579 + r.ca579, marge579: acc.marge579 + r.marge579,
|
qte579: acc.qte579 + r.qte579, ca579: acc.ca579 + r.ca579, marge579: acc.marge579 + r.marge579,
|
||||||
qteTotal: acc.qteTotal + r.qteTotal, caTotal: acc.caTotal + r.caTotal, margeTotal: acc.margeTotal + r.margeTotal,
|
qteTotal: acc.qteTotal + r.qteTotal, caTotal: acc.caTotal + r.caTotal, margeTotal: acc.margeTotal + r.margeTotal,
|
||||||
}),
|
}),
|
||||||
{ qte292: 0, ca292: 0, marge292: 0, qte579: 0, ca579: 0, marge579: 0, qteTotal: 0, caTotal: 0, margeTotal: 0 }
|
{ qte292: 0, ca292: 0, marge292: 0, qte579: 0, ca579: 0, marge579: 0, qteTotal: 0, caTotal: 0, margeTotal: 0 }
|
||||||
), [pivotted]);
|
), [sorted]);
|
||||||
|
|
||||||
function ColHeader({ label, sortable, k }: { label: string; sortable?: SortKey; k?: string }) {
|
function ColHeader({ label, sortable, k }: { label: string; sortable?: SortKey; k: string }) {
|
||||||
const isActive = sortable && sortKey === sortable;
|
const isActive = sortable && sortKey === sortable;
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
@@ -98,45 +111,71 @@ export function HitParadeClient({ mois, pivotted }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Barre de contrôles */}
|
{/* Barre de contrôles */}
|
||||||
<div className="flex flex-wrap items-center gap-4 rounded-xl bg-white px-5 py-4 shadow-sm border border-gray-100">
|
<div className="flex flex-wrap items-end gap-4 rounded-xl bg-white px-5 py-4 shadow-sm border border-gray-100">
|
||||||
<div className="flex items-center gap-2">
|
{/* Période */}
|
||||||
<span className="text-sm font-semibold text-gray-600">Mois</span>
|
<div className="flex items-end gap-3">
|
||||||
<select
|
<div className="flex flex-col gap-1">
|
||||||
value={mois}
|
<label className="text-xs font-semibold text-gray-500">Du</label>
|
||||||
onChange={e => router.push(`/hit-parade?mois=${e.target.value}`)}
|
<input
|
||||||
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
|
type="date"
|
||||||
|
value={localDebut}
|
||||||
|
onChange={e => setLocalDebut(e.target.value)}
|
||||||
|
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-xs font-semibold text-gray-500">Au</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={localFin}
|
||||||
|
onChange={e => setLocalFin(e.target.value)}
|
||||||
|
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={applyDates}
|
||||||
|
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500 transition-colors"
|
||||||
>
|
>
|
||||||
{months.map(m => (
|
Appliquer
|
||||||
<option key={m.value} value={m.value}>{m.label}</option>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-8 w-px bg-gray-200 self-center" />
|
||||||
|
|
||||||
|
{/* Filtre fournisseur */}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-xs font-semibold text-gray-500">Fournisseur</label>
|
||||||
|
<select
|
||||||
|
value={filterFournisseur}
|
||||||
|
onChange={e => setFilterFournisseur(e.target.value)}
|
||||||
|
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400 min-w-[200px]"
|
||||||
|
>
|
||||||
|
{fournisseurs.map(f => (
|
||||||
|
<option key={f} value={f}>{f}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-6 w-px bg-gray-200" />
|
<div className="h-8 w-px bg-gray-200 self-center" />
|
||||||
|
|
||||||
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm font-medium text-gray-600">
|
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm font-medium text-gray-600 self-center">
|
||||||
{sorted.length} produits
|
{sorted.length} produits
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{isSorted && (
|
{isSorted && (
|
||||||
<button
|
<button
|
||||||
onClick={resetSort}
|
onClick={resetSort}
|
||||||
className="flex items-center gap-1.5 rounded-lg border border-orange-200 bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-700 hover:bg-orange-100 transition-colors"
|
className="flex items-center gap-1.5 rounded-lg border border-orange-200 bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-700 hover:bg-orange-100 transition-colors self-center"
|
||||||
>
|
>
|
||||||
<span>✕</span> Annuler le tri
|
<span>✕</span> Annuler le tri
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isSorted && (
|
|
||||||
<span className="text-xs text-gray-400 italic">Trié par CA Total ↓ — cliquez sur une colonne pour trier</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tableau */}
|
{/* Tableau */}
|
||||||
<div className="overflow-x-auto rounded-xl bg-white shadow-sm border border-gray-100">
|
<div className="overflow-x-auto rounded-xl bg-white shadow-sm border border-gray-100">
|
||||||
<table className="w-full border-collapse text-sm">
|
<table className="w-full border-collapse text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
{/* Ligne 1 — groupes */}
|
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2} className="w-24 border-b-2 border-gray-200 bg-gray-50 px-4 py-3 text-left text-xs font-semibold text-gray-600 align-bottom">Code</th>
|
<th rowSpan={2} className="w-24 border-b-2 border-gray-200 bg-gray-50 px-4 py-3 text-left text-xs font-semibold text-gray-600 align-bottom">Code</th>
|
||||||
<th rowSpan={2} className="border-b-2 border-gray-200 bg-gray-50 px-4 py-3 text-left text-xs font-semibold text-gray-600 align-bottom">Désignation</th>
|
<th rowSpan={2} className="border-b-2 border-gray-200 bg-gray-50 px-4 py-3 text-left text-xs font-semibold text-gray-600 align-bottom">Désignation</th>
|
||||||
@@ -151,7 +190,6 @@ export function HitParadeClient({ mois, pivotted }: Props) {
|
|||||||
Total Réseau
|
Total Réseau
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
{/* Ligne 2 — sous-colonnes */}
|
|
||||||
<tr className="border-b-2 border-gray-200">
|
<tr className="border-b-2 border-gray-200">
|
||||||
<ColHeader label="Qté" sortable="qte292" k="qte292" />
|
<ColHeader label="Qté" sortable="qte292" k="qte292" />
|
||||||
<ColHeader label="CA TTC" sortable="ca292" k="ca292" />
|
<ColHeader label="CA TTC" sortable="ca292" k="ca292" />
|
||||||
@@ -165,46 +203,40 @@ export function HitParadeClient({ mois, pivotted }: Props) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{sorted.map((row, idx) => {
|
{sorted.map((row, idx) => (
|
||||||
const highlight = sortKey.includes("292") ? "bg-blue-50/30" : sortKey.includes("579") ? "bg-violet-50/30" : "";
|
<tr key={row.codein} className={`border-b border-gray-100 ${idx % 2 === 0 ? "bg-white" : "bg-gray-50/60"} hover:bg-yellow-50/40 transition-colors`}>
|
||||||
return (
|
<td className="px-4 py-2.5 font-mono text-xs text-gray-400">{row.codein}</td>
|
||||||
<tr key={row.codein} className={`border-b border-gray-100 ${idx % 2 === 0 ? "bg-white" : "bg-gray-50/60"} hover:bg-yellow-50/40 transition-colors`}>
|
<td className="px-4 py-2.5 text-gray-900 font-medium max-w-[260px] truncate" title={row.libelle.trim()}>{row.libelle.trim()}</td>
|
||||||
<td className="px-4 py-2.5 font-mono text-xs text-gray-400">{row.codein}</td>
|
<td className="px-4 py-2.5 text-xs text-gray-500 whitespace-nowrap">{row.fournisseur}</td>
|
||||||
<td className="px-4 py-2.5 text-gray-900 font-medium max-w-[260px] truncate" title={row.libelle.trim()}>{row.libelle.trim()}</td>
|
<td className={`border-l-2 border-blue-100 px-4 py-2.5 text-right tabular-nums text-gray-700 ${sortKey === "qte292" ? "bg-blue-50/50 font-semibold" : ""}`}>
|
||||||
<td className="px-4 py-2.5 text-xs text-gray-500 whitespace-nowrap">{row.fournisseur}</td>
|
{row.qte292 > 0 ? formatQte(row.qte292) : <span className="text-gray-300">—</span>}
|
||||||
{/* 292 */}
|
</td>
|
||||||
<td className={`border-l-2 border-blue-100 px-4 py-2.5 text-right tabular-nums text-gray-700 ${sortKey === "qte292" ? "bg-blue-50/50 font-semibold" : ""}`}>
|
<td className={`px-4 py-2.5 text-right tabular-nums text-gray-800 ${sortKey === "ca292" ? "bg-blue-50/50 font-semibold" : ""}`}>
|
||||||
{row.qte292 > 0 ? formatQte(row.qte292) : <span className="text-gray-300">—</span>}
|
{row.ca292 > 0 ? formatCA(row.ca292) : <span className="text-gray-300">—</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className={`px-4 py-2.5 text-right tabular-nums text-gray-800 ${sortKey === "ca292" ? "bg-blue-50/50 font-semibold" : ""}`}>
|
<td className="px-4 py-2.5 text-right text-xs tabular-nums text-blue-500">
|
||||||
{row.ca292 > 0 ? formatCA(row.ca292) : <span className="text-gray-300">—</span>}
|
{row.ca292 > 0 ? formatPct(row.ca292, row.marge292) : ""}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2.5 text-right text-xs tabular-nums text-blue-500">
|
<td className={`border-l-2 border-violet-100 px-4 py-2.5 text-right tabular-nums text-gray-700 ${sortKey === "qte579" ? "bg-violet-50/50 font-semibold" : ""}`}>
|
||||||
{row.ca292 > 0 ? formatPct(row.ca292, row.marge292) : ""}
|
{row.qte579 > 0 ? formatQte(row.qte579) : <span className="text-gray-300">—</span>}
|
||||||
</td>
|
</td>
|
||||||
{/* 579 */}
|
<td className={`px-4 py-2.5 text-right tabular-nums text-gray-800 ${sortKey === "ca579" ? "bg-violet-50/50 font-semibold" : ""}`}>
|
||||||
<td className={`border-l-2 border-violet-100 px-4 py-2.5 text-right tabular-nums text-gray-700 ${sortKey === "qte579" ? "bg-violet-50/50 font-semibold" : ""}`}>
|
{row.ca579 > 0 ? formatCA(row.ca579) : <span className="text-gray-300">—</span>}
|
||||||
{row.qte579 > 0 ? formatQte(row.qte579) : <span className="text-gray-300">—</span>}
|
</td>
|
||||||
</td>
|
<td className="px-4 py-2.5 text-right text-xs tabular-nums text-violet-500">
|
||||||
<td className={`px-4 py-2.5 text-right tabular-nums text-gray-800 ${sortKey === "ca579" ? "bg-violet-50/50 font-semibold" : ""}`}>
|
{row.ca579 > 0 ? formatPct(row.ca579, row.marge579) : ""}
|
||||||
{row.ca579 > 0 ? formatCA(row.ca579) : <span className="text-gray-300">—</span>}
|
</td>
|
||||||
</td>
|
<td className={`border-l-2 border-emerald-100 px-4 py-2.5 text-right tabular-nums font-semibold text-gray-900 ${sortKey === "qteTotal" ? "bg-emerald-50/50" : ""}`}>
|
||||||
<td className="px-4 py-2.5 text-right text-xs tabular-nums text-violet-500">
|
{row.qteTotal > 0 ? formatQte(row.qteTotal) : <span className="text-gray-300">—</span>}
|
||||||
{row.ca579 > 0 ? formatPct(row.ca579, row.marge579) : ""}
|
</td>
|
||||||
</td>
|
<td className={`px-4 py-2.5 text-right tabular-nums font-semibold text-gray-900 ${sortKey === "caTotal" ? "bg-emerald-50/50" : ""}`}>
|
||||||
{/* Total */}
|
{row.caTotal > 0 ? formatCA(row.caTotal) : <span className="text-gray-300">—</span>}
|
||||||
<td className={`border-l-2 border-emerald-100 px-4 py-2.5 text-right tabular-nums font-semibold text-gray-900 ${sortKey === "qteTotal" ? "bg-emerald-50/50" : ""}`}>
|
</td>
|
||||||
{row.qteTotal > 0 ? formatQte(row.qteTotal) : <span className="text-gray-300">—</span>}
|
<td className="px-4 py-2.5 text-right text-xs tabular-nums text-emerald-600">
|
||||||
</td>
|
{row.caTotal > 0 ? formatPct(row.caTotal, row.margeTotal) : ""}
|
||||||
<td className={`px-4 py-2.5 text-right tabular-nums font-semibold text-gray-900 ${sortKey === "caTotal" ? "bg-emerald-50/50" : ""}`}>
|
</td>
|
||||||
{row.caTotal > 0 ? formatCA(row.caTotal) : <span className="text-gray-300">—</span>}
|
</tr>
|
||||||
</td>
|
))}
|
||||||
<td className="px-4 py-2.5 text-right text-xs tabular-nums text-emerald-600">
|
|
||||||
{row.caTotal > 0 ? formatPct(row.caTotal, row.margeTotal) : ""}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr className="border-t-2 border-gray-300 bg-gray-100">
|
<tr className="border-t-2 border-gray-300 bg-gray-100">
|
||||||
|
|||||||
@@ -53,15 +53,22 @@ function pivotHitParade(
|
|||||||
return Array.from(map.values());
|
return Array.from(map.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultDates() {
|
||||||
|
const today = new Date();
|
||||||
|
const firstOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||||
|
const fmt = (d: Date) => d.toISOString().slice(0, 10);
|
||||||
|
return { debut: fmt(firstOfMonth), fin: fmt(today) };
|
||||||
|
}
|
||||||
|
|
||||||
export default async function HitParadePage(props: {
|
export default async function HitParadePage(props: {
|
||||||
searchParams: Promise<Record<string, string | string[]>>;
|
searchParams: Promise<Record<string, string | string[]>>;
|
||||||
}) {
|
}) {
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
const today = new Date();
|
const defaults = defaultDates();
|
||||||
const currentMois = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}`;
|
const dateDebut = (searchParams.debut as string) || defaults.debut;
|
||||||
const mois = (searchParams.mois as string) || currentMois;
|
const dateFin = (searchParams.fin as string) || defaults.fin;
|
||||||
|
|
||||||
const rows = await pgGetHitParade(mois);
|
const rows = await pgGetHitParade(dateDebut, dateFin);
|
||||||
const pivotted = pivotHitParade(rows);
|
const pivotted = pivotHitParade(rows);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -69,8 +76,8 @@ export default async function HitParadePage(props: {
|
|||||||
<div className="mx-auto max-w-screen-2xl">
|
<div className="mx-auto max-w-screen-2xl">
|
||||||
<h1 className="mb-6 text-3xl font-bold text-gray-900">Hit Parade</h1>
|
<h1 className="mb-6 text-3xl font-bold text-gray-900">Hit Parade</h1>
|
||||||
<HitParadeClient
|
<HitParadeClient
|
||||||
mois={mois}
|
dateDebut={dateDebut}
|
||||||
currentMois={currentMois}
|
dateFin={dateFin}
|
||||||
pivotted={pivotted}
|
pivotted={pivotted}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -111,43 +111,45 @@ export function UserManagement() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleCreate} className="grid grid-cols-1 md:grid-cols-4 gap-6 items-end">
|
<form onSubmit={handleCreate} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">Utilisateur</label>
|
<div className="space-y-2">
|
||||||
<input
|
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">Utilisateur</label>
|
||||||
required
|
<input
|
||||||
value={newUsername}
|
required
|
||||||
onChange={(e) => setNewUsername(e.target.value)}
|
value={newUsername}
|
||||||
className="apple-input"
|
onChange={(e) => setNewUsername(e.target.value)}
|
||||||
placeholder="ex: jean.dupont"
|
className="apple-input"
|
||||||
/>
|
placeholder="ex: jean.dupont"
|
||||||
</div>
|
/>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">Mot de passe</label>
|
<div className="space-y-2">
|
||||||
<input
|
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">Mot de passe</label>
|
||||||
required
|
<input
|
||||||
type="password"
|
required
|
||||||
value={newPassword}
|
type="password"
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
value={newPassword}
|
||||||
className="apple-input"
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
placeholder="••••••••"
|
className="apple-input"
|
||||||
/>
|
placeholder="••••••••"
|
||||||
</div>
|
/>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">Rôle</label>
|
<div className="space-y-2">
|
||||||
<select
|
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">Rôle</label>
|
||||||
value={newRole}
|
<select
|
||||||
onChange={(e) => setNewRole(e.target.value as any)}
|
value={newRole}
|
||||||
className="apple-input"
|
onChange={(e) => setNewRole(e.target.value as any)}
|
||||||
>
|
className="apple-input"
|
||||||
<option value="user">Utilisateur</option>
|
>
|
||||||
<option value="admin">Administrateur</option>
|
<option value="user">Utilisateur</option>
|
||||||
</select>
|
<option value="admin">Administrateur</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isCreating}
|
disabled={isCreating}
|
||||||
className="apple-btn-primary w-full justify-center !h-auto py-2.5"
|
className="apple-btn-primary"
|
||||||
>
|
>
|
||||||
{isCreating ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
|
{isCreating ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
|
||||||
Créer le compte
|
Créer le compte
|
||||||
|
|||||||
@@ -709,9 +709,9 @@ export interface HitParadeRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne les ventes produit pour un mois donné, avec fournisseur, qté, CA TTC et marge par site.
|
* Retourne les ventes produit sur une période, avec fournisseur, qté, CA TTC et marge par site.
|
||||||
*/
|
*/
|
||||||
export async function pgGetHitParade(mois: string): Promise<HitParadeRow[]> {
|
export async function pgGetHitParade(dateDebut: string, dateFin: string): Promise<HitParadeRow[]> {
|
||||||
const result = await pgNoParallel(sql`
|
const result = await pgNoParallel(sql`
|
||||||
SELECT
|
SELECT
|
||||||
a.codein::text AS codein,
|
a.codein::text AS codein,
|
||||||
@@ -725,13 +725,13 @@ export async function pgGetHitParade(mois: string): Promise<HitParadeRow[]> {
|
|||||||
JOIN articles a ON a.no_id = m.artnoid
|
JOIN articles a ON a.no_id = m.artnoid
|
||||||
LEFT JOIN artfou1 af ON af.art_no_id = a.no_id AND af.preference = 1
|
LEFT JOIN artfou1 af ON af.art_no_id = a.no_id AND af.preference = 1
|
||||||
LEFT JOIN fouident fi ON fi.code = af.code
|
LEFT JOIN fouident fi ON fi.code = af.code
|
||||||
WHERE TO_CHAR(m.datmvt, 'YYYY-MM') = ${mois}
|
WHERE m.datmvt BETWEEN ${dateDebut}::date AND ${dateFin}::date
|
||||||
AND m.site IN ('292', '579')
|
AND m.site IN ('292', '579')
|
||||||
AND m.genremvt = 3
|
AND m.genremvt = 3
|
||||||
GROUP BY a.codein, a.libelle1, fi.nom, af.code, m.site
|
GROUP BY a.codein, a.libelle1, fi.nom, af.code, m.site
|
||||||
ORDER BY ca_ttc DESC
|
ORDER BY ca_ttc DESC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
console.log(`[pg-ff] HitParade: ${result.rows.length} lignes pour ${mois}`);
|
console.log(`[pg-ff] HitParade: ${result.rows.length} lignes pour ${dateDebut}→${dateFin}`);
|
||||||
return result.rows as unknown as HitParadeRow[];
|
return result.rows as unknown as HitParadeRow[];
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user