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 0000000..9ed614c Binary files /dev/null and b/attached_assets/image_1776768428924.png differ 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