From 2586c0eb0981d3ca824999e5b8f2989154ef1521 Mon Sep 17 00:00:00 2001 From: SylvainP1 <5533467-SylvainP1@users.noreply.replit.com> Date: Tue, 21 Apr 2026 10:47:21 +0000 Subject: [PATCH] Protect user data entry with an administrator lock feature Introduce an admin lock mechanism to prevent unauthorized modifications to timesheet entries and quick entry fields. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 55837015-10e9-4be9-b857-7f5e6be73772 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: d79c5670-4ff2-409b-85ef-fc3f2472208b Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1cc377db-7ea0-49f2-97ce-c3e87e0228cc/55837015-10e9-4be9-b857-7f5e6be73772/LZewR4B Replit-Helium-Checkpoint-Created: true --- .../cra-app/src/components/layout/sidebar.tsx | 114 ++++++++++++------ .../cra-app/src/components/quick-entry.tsx | 25 +++- artifacts/cra-app/src/lib/admin-mode.ts | 34 ++++++ .../cra-app/src/pages/timesheet-detail.tsx | 15 ++- attached_assets/image_1776768428924.png | Bin 0 -> 10264 bytes replit.md | 2 +- 6 files changed, 146 insertions(+), 44 deletions(-) create mode 100644 artifacts/cra-app/src/lib/admin-mode.ts create mode 100644 attached_assets/image_1776768428924.png diff --git a/artifacts/cra-app/src/components/layout/sidebar.tsx b/artifacts/cra-app/src/components/layout/sidebar.tsx index f9e3db0..28e3824 100644 --- a/artifacts/cra-app/src/components/layout/sidebar.tsx +++ b/artifacts/cra-app/src/components/layout/sidebar.tsx @@ -1,14 +1,14 @@ import { useRef, useState } from "react"; import { Link, useLocation } from "wouter"; -import { Download, FolderKanban, LayoutDashboard, Settings, Upload, Clock } from "lucide-react"; +import { Download, FolderKanban, LayoutDashboard, Lock, LockOpen, Settings, Upload, Clock } from "lucide-react"; import { cn } from "@/lib/utils"; import { QuickEntryButton } from "@/components/quick-entry"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { getAppInfo, saveAppInfo, type AppInfo } from "@/lib/app-info"; import { useToast } from "@/hooks/use-toast"; +import { isAdminUnlocked, lockAdmin, unlockAdmin, useAdminUnlocked } from "@/lib/admin-mode"; const navItems = [ { @@ -31,22 +31,38 @@ const navItems = [ export function AppSidebar() { const [location] = useLocation(); const [isAdminOpen, setIsAdminOpen] = useState(false); - const [info, setInfo] = useState(() => getAppInfo()); + const [adminCode, setAdminCode] = useState(""); const [isImporting, setIsImporting] = useState(false); + const adminUnlocked = useAdminUnlocked(); const fileInputRef = useRef(null); const { toast } = useToast(); const handleOpenAdmin = () => { - setInfo(getAppInfo()); + setAdminCode(""); setIsAdminOpen(true); }; - const handleSaveInfo = () => { - saveAppInfo(info); - toast({ title: "Informations mises à jour" }); + const handleUnlock = () => { + if (unlockAdmin(adminCode)) { + setAdminCode(""); + toast({ title: "Saisie déverrouillée" }); + return; + } + + toast({ title: "Code admin incorrect", variant: "destructive" }); + }; + + const handleLock = () => { + lockAdmin(); + toast({ title: "Saisie verrouillée" }); }; const handleExport = async () => { + if (!isAdminUnlocked()) { + toast({ title: "Déverrouillez d'abord le mode admin", variant: "destructive" }); + return; + } + const response = await fetch("/api/admin/export"); if (!response.ok) { toast({ title: "Export impossible", variant: "destructive" }); @@ -64,6 +80,10 @@ export function AppSidebar() { const handleImport = async (file: File | undefined) => { if (!file) return; + if (!isAdminUnlocked()) { + toast({ title: "Déverrouillez d'abord le mode admin", variant: "destructive" }); + return; + } if (!confirm("Importer ce fichier remplacera toutes les données actuelles. Continuer ?")) return; setIsImporting(true); @@ -123,21 +143,30 @@ export function AppSidebar() {
- {info.userInitials} + JD
- {info.userName} - {info.userRole} + Jean Dupont + Consultant
@@ -150,32 +179,47 @@ export function AppSidebar() {
-

Informations affichées

-
-
- - setInfo({ ...info, userName: e.target.value })} /> +

Protection de la saisie

+ {adminUnlocked ? ( +
+ La saisie est déverrouillée. Les cellules CRA et la saisie rapide sont modifiables.
-
- - setInfo({ ...info, userRole: e.target.value })} /> + ) : ( +
+ La saisie est verrouillée. Déverrouillez le mode admin pour modifier les heures.
-
- - setInfo({ ...info, userInitials: e.target.value })} /> + )} +
+
+ + setAdminCode(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleUnlock(); + }} + placeholder="Code admin" + disabled={adminUnlocked} + />
-
- - setInfo({ ...info, deployDate: e.target.value })} /> -
-
- - setInfo({ ...info, repoUrl: e.target.value })} /> +
+ {adminUnlocked ? ( + + ) : ( + + )}
- +

+ Code par défaut : 1234 +

@@ -189,7 +233,7 @@ export function AppSidebar() { variant="outline" size="sm" onClick={() => fileInputRef.current?.click()} - disabled={isImporting} + disabled={isImporting || !adminUnlocked} className="gap-2" > diff --git a/artifacts/cra-app/src/components/quick-entry.tsx b/artifacts/cra-app/src/components/quick-entry.tsx index 5b5d6e1..3f2707a 100644 --- a/artifacts/cra-app/src/components/quick-entry.tsx +++ b/artifacts/cra-app/src/components/quick-entry.tsx @@ -27,24 +27,39 @@ import { } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { useToast } from "@/hooks/use-toast"; -import { Zap, Clock, Check } from "lucide-react"; +import { Zap, Clock, Check, Lock } from "lucide-react"; import { format } from "date-fns"; import { fr } from "date-fns/locale"; +import { useAdminUnlocked } from "@/lib/admin-mode"; +import { cn } from "@/lib/utils"; -const HOUR_OPTIONS = [0.5, 1, 2, 3, 4, 5, 6, 7, 7.7]; +const HOUR_OPTIONS = [0.5, 1, 2, 3, 4, 5, 6, 7, 8]; const COLLABORATOR = "PHAM Sylvain"; export function QuickEntryButton() { const [open, setOpen] = useState(false); + const adminUnlocked = useAdminUnlocked(); + const { toast } = useToast(); return ( <> diff --git a/artifacts/cra-app/src/lib/admin-mode.ts b/artifacts/cra-app/src/lib/admin-mode.ts new file mode 100644 index 0000000..fdcfef0 --- /dev/null +++ b/artifacts/cra-app/src/lib/admin-mode.ts @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; + +export const ADMIN_MODE_EVENT = "cra-admin-mode-updated"; +const ADMIN_UNLOCKED_KEY = "cra-admin-unlocked"; +const ADMIN_CODE = "1234"; + +export function isAdminUnlocked() { + if (typeof window === "undefined") return false; + return window.sessionStorage.getItem(ADMIN_UNLOCKED_KEY) === "true"; +} + +export function unlockAdmin(code: string) { + if (code !== ADMIN_CODE) return false; + window.sessionStorage.setItem(ADMIN_UNLOCKED_KEY, "true"); + window.dispatchEvent(new Event(ADMIN_MODE_EVENT)); + return true; +} + +export function lockAdmin() { + window.sessionStorage.removeItem(ADMIN_UNLOCKED_KEY); + window.dispatchEvent(new Event(ADMIN_MODE_EVENT)); +} + +export function useAdminUnlocked() { + const [unlocked, setUnlocked] = useState(() => isAdminUnlocked()); + + useEffect(() => { + const handleUpdate = () => setUnlocked(isAdminUnlocked()); + window.addEventListener(ADMIN_MODE_EVENT, handleUpdate); + return () => window.removeEventListener(ADMIN_MODE_EVENT, handleUpdate); + }, []); + + return unlocked; +} \ No newline at end of file diff --git a/artifacts/cra-app/src/pages/timesheet-detail.tsx b/artifacts/cra-app/src/pages/timesheet-detail.tsx index b4774e6..0e74d73 100644 --- a/artifacts/cra-app/src/pages/timesheet-detail.tsx +++ b/artifacts/cra-app/src/pages/timesheet-detail.tsx @@ -18,7 +18,8 @@ import { Trash2, CheckCircle, AlertCircle, - MessageSquare + MessageSquare, + Lock } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -33,6 +34,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from " import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { useToast } from "@/hooks/use-toast"; import { formatMonthYear, STATUS_LABELS, STATUS_COLORS, cn } from "@/lib/utils"; +import { useAdminUnlocked } from "@/lib/admin-mode"; import { getDaysInMonth, isWeekend, format } from "date-fns"; import { fr } from "date-fns/locale"; @@ -49,6 +51,7 @@ export default function TimesheetDetailPage() { const { toast } = useToast(); const queryClient = useQueryClient(); + const adminUnlocked = useAdminUnlocked(); const { data: timesheet, isLoading } = useGetTimesheet( timesheetId, @@ -108,9 +111,9 @@ export default function TimesheetDetailPage() { return { days: daysCount, daysArray: arr, - isEditable: timesheet.status === "draft" + isEditable: timesheet.status === "draft" && adminUnlocked }; - }, [timesheet]); + }, [timesheet, adminUnlocked]); const HOUR_OPTIONS = [0, 0.5, 1, 2, 3, 4, 5, 6, 7, 8]; const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved">("idle"); @@ -346,6 +349,12 @@ export default function TimesheetDetailPage() { )} + {timesheet.status === "draft" && !adminUnlocked && ( + + + Saisie verrouillée + + )} {timesheet.status === "submitted" && ( diff --git a/attached_assets/image_1776768428924.png b/attached_assets/image_1776768428924.png new file mode 100644 index 0000000000000000000000000000000000000000..9ed614cae8afdbd408713e41e400cec1fbceddc7 GIT binary patch literal 10264 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 gm;e7&+<{9 literal 0 HcmV?d00001 diff --git a/replit.md b/replit.md index e4b1a45..7e8e7ac 100644 --- a/replit.md +++ b/replit.md @@ -26,7 +26,7 @@ A French timesheet management application (CRA - Compte Rendu d'Activité) built - **CRA Grid**: Interactive calendar grid where rows = projects, columns = days of month. Click cells to open popover with hour options [0, 0.5, 1, 2, 3, 4, 5, 6, 7, 8]. Includes optional description per cell (amber dot indicator). Auto-save with debounce. Weekend distinction, row/column totals - **Project Management**: CRUD for projects with code, name, client, category - **Timesheet Workflow**: Draft → Submitted → Validated status flow -- **Inline Administration**: Sidebar Admin dialog to edit displayed app/user metadata and export/import project, CRA, line, and time-entry data as JSON +- **Inline Administration**: Sidebar Admin dialog to lock/unlock time entry editing and export/import project, CRA, line, and time-entry data as JSON ## Database Schema