feat: restrict gamme/export/exports-page to admin; fix create user button style

- Non-admin users see gamme as read-only badge (no select)
- Export button hidden for non-admin in grid
- Save drafts button hidden for non-admin
- Exports page hidden in sidebar for non-admin
- Create user button: explicit blue style (was unreadable with apple-btn-primary)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Sonnet 4.6 committed 2026-04-01 13:29:52 +02:00
1 parent f52532343f
commit ead53db496
4 files changed
+29 -7

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@ const NAV_ITEMS = [
{ icon: TrendingUp, label: "Analytics", href: "/analytics" },
{ icon: Trophy, label: "Hit Parade", href: "/hit-parade" },
{ icon: Camera, label: "Snapshots", href: "/snapshots" },
{ icon: FileDown, label: "Exports", href: "/exports" },
{ icon: FileDown, label: "Exports", href: "/exports", adminOnly: true },
{ icon: Settings, label: "Paramètres", href: "/settings", adminOnly: true },
];
@@ -147,7 +147,7 @@ export function UserManagement() {
<button
type="submit"
disabled={isCreating}
className="apple-btn-primary w-full justify-center"
className="flex items-center gap-2 justify-center w-full px-4 py-2.5 rounded-xl bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white text-sm font-semibold transition-colors"
>
{isCreating ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
Créer le compte
+6 -2
View File
@@ -10,6 +10,7 @@ import { useGridStore } from "@/features/grid/store/use-grid-store";
import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts";
import type { ProductRow } from "@/types/grid";
import { CheckCircle, AlertCircle, Loader2 } from "lucide-react";
import { useSession } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { computeProductScores } from "@/lib/score-engine";
@@ -24,6 +25,8 @@ interface GridClientProps {
}
export function GridClient({ initialRows, codeFournisseur, nomFournisseur, fournisseurs, magasins, magasin }: GridClientProps) {
const { data: session } = useSession();
const isAdmin = (session?.user as any)?.role === "admin";
const setRows = useGridStore((s) => s.setRows);
const setActiveGridQuery = useGridStore((s) => s.setActiveGridQuery);
const setFilter = useGridStore((s) => s.setFilter);
@@ -98,8 +101,8 @@ export function GridClient({ initialRows, codeFournisseur, nomFournisseur, fourn
</div>
<div className="flex items-center gap-3">
<div id="grid-toolbar-actions"></div>
<ExportDropdown nomFournisseur={nomFournisseur} />
{hasDrafts && (
{isAdmin && <ExportDropdown nomFournisseur={nomFournisseur} />}
{isAdmin && hasDrafts && (
<button
onClick={handleSave}
disabled={isPending}
@@ -141,6 +144,7 @@ export function GridClient({ initialRows, codeFournisseur, nomFournisseur, fourn
<div className="flex-1 min-h-0 min-w-0">
<HeatmapGrid
onSelectionChange={setSelectedCodeins}
isAdmin={isAdmin}
/>
</div>
+21 -3
View File
@@ -83,6 +83,7 @@ function getStoreConfig(name: string) {
interface HeatmapGridProps {
onSelectionChange?: (codeins: string[]) => void;
isAdmin?: boolean;
}
// =========================================================================
@@ -90,7 +91,7 @@ interface HeatmapGridProps {
// =========================================================================
// 1. Composant isolé pour la Cellule Gamme (évite le re-render des colonnes)
const GammeCell = React.memo(({ row }: { row: ProductRow }) => {
const GammeCell = React.memo(({ row, isAdmin }: { row: ProductRow; isAdmin?: boolean }) => {
// Abonnement ultra-ciblé : la cellule ne re-render que si SA valeur change
const codein = row.codein;
const isDraft = useGridStore((s) => s.draftChanges[codein] !== undefined);
@@ -98,6 +99,23 @@ const GammeCell = React.memo(({ row }: { row: ProductRow }) => {
const setDraftGamme = useGridStore((s) => s.setDraftGamme);
const isModified = row.codeGamme !== row.codeGammeInit && row.codeGammeInit !== null;
const displayValue = (!effectiveGamme || effectiveGamme.trim() === "") ? "Aucune" : effectiveGamme;
if (!isAdmin) {
return (
<div className={[
"w-full text-xs font-bold rounded-lg py-1.5 px-2 text-center",
displayValue === "A" ? "bg-emerald-500/10 text-emerald-700" :
displayValue === "B" ? "bg-blue-500/10 text-blue-700" :
displayValue === "C" ? "bg-amber-500/10 text-amber-700" :
displayValue === "Y" ? "bg-violet-500/10 text-violet-700" :
displayValue === "Z" ? "bg-rose-500/10 text-rose-700" :
"bg-slate-500/10 text-slate-500"
].join(" ")}>
{displayValue === "Aucune" ? "—" : displayValue}
</div>
);
}
return (
<div className="relative group/gamme">
@@ -249,7 +267,7 @@ function CellDetailModal({ d, activeMagasin, onClose }: { d: CellDetailData; act
// =========================================================================
export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
// L'abonnement doit être minimal ici ! PAS de draftChanges ni de setDraftGamme.
const { rows, filters, displayDensity, draftChanges, activeMagasin } = useGridStore();
@@ -626,7 +644,7 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
id: "gamme",
header: () => <div className="text-center w-full">Gamme</div>,
size: 110,
cell: ({ row }) => <GammeCell row={row.original} />,
cell: ({ row }) => <GammeCell row={row.original} isAdmin={isAdmin} />,
},
{
id: "ai",