diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9ba7f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.DS_Store +server/public +vite.config.ts.* +*.tar.gz \ No newline at end of file diff --git a/.replit b/.replit new file mode 100644 index 0000000..03709f0 --- /dev/null +++ b/.replit @@ -0,0 +1,50 @@ +modules = ["nodejs-20", "web", "postgresql-16"] +run = "npm run dev" +hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"] + +[nix] +channel = "stable-24_05" + +[deployment] +deploymentTarget = "autoscale" +build = ["npm", "run", "build"] +run = ["npm", "run", "start"] + +[[ports]] +localPort = 5000 +externalPort = 80 + +[[ports]] +localPort = 33035 +externalPort = 3001 + +[[ports]] +localPort = 41343 +externalPort = 3000 + +[env] +PORT = "5000" + +[workflows] +runButton = "Project" + +[[workflows.workflow]] +name = "Project" +mode = "parallel" +author = "agent" + +[[workflows.workflow.tasks]] +task = "workflow.run" +args = "Start application" + +[[workflows.workflow]] +name = "Start application" +author = "agent" + +[[workflows.workflow.tasks]] +task = "shell.exec" +args = "npm run dev" +waitForPort = 5000 + +[agent] +integrations = ["javascript_database:1.0.0", "javascript_log_in_with_replit:1.0.0"] diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..aae19fb --- /dev/null +++ b/client/index.html @@ -0,0 +1,26 @@ + + + + + + + + VigilanzaTurni - Sistema Gestione Turni Professionale per Istituti di Vigilanza + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..2c81685 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,84 @@ +import { Switch, Route } from "wouter"; +import { queryClient } from "./lib/queryClient"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { ThemeProvider } from "@/components/theme-provider"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { AppSidebar } from "@/components/app-sidebar"; +import { useAuth } from "@/hooks/useAuth"; +import NotFound from "@/pages/not-found"; +import Landing from "@/pages/landing"; +import Dashboard from "@/pages/dashboard"; +import Guards from "@/pages/guards"; +import Sites from "@/pages/sites"; +import Shifts from "@/pages/shifts"; +import Reports from "@/pages/reports"; +import Notifications from "@/pages/notifications"; + +function Router() { + const { isAuthenticated, isLoading } = useAuth(); + + return ( + + {isLoading || !isAuthenticated ? ( + + ) : ( + <> + + + + + + + + )} + + + ); +} + +function AppContent() { + const { isAuthenticated, isLoading } = useAuth(); + + // Sidebar style configuration for operational dashboard + const sidebarStyle = { + "--sidebar-width": "16rem", + "--sidebar-width-icon": "3rem", + } as React.CSSProperties; + + return ( + + + {!isLoading && isAuthenticated ? ( + +
+ +
+
+ +
+
+ +
+
+
+
+ ) : ( + + )} + +
+
+ ); +} + +function App() { + return ( + + + + ); +} + +export default App; diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx new file mode 100644 index 0000000..bc51cc8 --- /dev/null +++ b/client/src/components/app-sidebar.tsx @@ -0,0 +1,134 @@ +import { + Calendar, + Shield, + MapPin, + Users, + BarChart3, + Bell, + Settings, +} from "lucide-react"; +import { Link, useLocation } from "wouter"; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarHeader, + SidebarFooter, +} from "@/components/ui/sidebar"; +import { useAuth } from "@/hooks/useAuth"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { ThemeToggle } from "@/components/theme-toggle"; + +const menuItems = [ + { + title: "Dashboard", + url: "/", + icon: Shield, + roles: ["admin", "coordinator", "guard", "client"], + }, + { + title: "Turni", + url: "/shifts", + icon: Calendar, + roles: ["admin", "coordinator", "guard"], + }, + { + title: "Guardie", + url: "/guards", + icon: Users, + roles: ["admin", "coordinator"], + }, + { + title: "Siti", + url: "/sites", + icon: MapPin, + roles: ["admin", "coordinator", "client"], + }, + { + title: "Report", + url: "/reports", + icon: BarChart3, + roles: ["admin", "coordinator", "client"], + }, + { + title: "Notifiche", + url: "/notifications", + icon: Bell, + roles: ["admin", "coordinator", "guard"], + }, +]; + +export function AppSidebar() { + const { user } = useAuth(); + const [location] = useLocation(); + + const filteredItems = menuItems.filter( + (item) => user && item.roles.includes(user.role) + ); + + return ( + + +
+ +
+

VigilanzaTurni

+

Sistema Gestione

+
+
+
+ + + + Menu Principale + + + {filteredItems.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + + +
+
+ + + + {user?.firstName?.[0]}{user?.lastName?.[0]} + + +
+

+ {user?.firstName} {user?.lastName} +

+

+ {user?.role} +

+
+
+ +
+
+
+ ); +} diff --git a/client/src/components/kpi-card.tsx b/client/src/components/kpi-card.tsx new file mode 100644 index 0000000..2cbccc3 --- /dev/null +++ b/client/src/components/kpi-card.tsx @@ -0,0 +1,37 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { LucideIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface KPICardProps extends React.HTMLAttributes { + title: string; + value: string | number; + icon: LucideIcon; + trend?: { + value: string; + isPositive: boolean; + }; +} + +export function KPICard({ title, value, icon: Icon, trend, className, ...props }: KPICardProps) { + return ( + + + + {title} + + + + +
{value}
+ {trend && ( +

+ {trend.value} +

+ )} +
+
+ ); +} diff --git a/client/src/components/status-badge.tsx b/client/src/components/status-badge.tsx new file mode 100644 index 0000000..9bba669 --- /dev/null +++ b/client/src/components/status-badge.tsx @@ -0,0 +1,31 @@ +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; + +interface StatusBadgeProps { + status: "active" | "inactive" | "late" | "emergency" | "pending" | "completed"; + children: React.ReactNode; + className?: string; +} + +export function StatusBadge({ status, children, className }: StatusBadgeProps) { + const statusColors = { + active: "bg-[hsl(140,60%,45%)] text-white border-[hsl(140,60%,35%)]", + inactive: "bg-muted text-muted-foreground border-muted-border", + late: "bg-[hsl(25,90%,55%)] text-white border-[hsl(25,90%,45%)]", + emergency: "bg-destructive text-destructive-foreground border-destructive-border", + pending: "bg-[hsl(45,90%,55%)] text-white border-[hsl(45,90%,45%)]", + completed: "bg-[hsl(140,60%,45%)] text-white border-[hsl(140,60%,35%)]", + }; + + return ( + + {children} + + ); +} diff --git a/client/src/components/theme-provider.tsx b/client/src/components/theme-provider.tsx new file mode 100644 index 0000000..74c81c4 --- /dev/null +++ b/client/src/components/theme-provider.tsx @@ -0,0 +1,73 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +type Theme = "dark" | "light" | "system"; + +type ThemeProviderProps = { + children: React.ReactNode; + defaultTheme?: Theme; + storageKey?: string; +}; + +type ThemeProviderState = { + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const initialState: ThemeProviderState = { + theme: "system", + setTheme: () => null, +}; + +const ThemeProviderContext = createContext(initialState); + +export function ThemeProvider({ + children, + defaultTheme = "dark", + storageKey = "vite-ui-theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme + ); + + useEffect(() => { + const root = window.document.documentElement; + + root.classList.remove("light", "dark"); + + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; + + root.classList.add(systemTheme); + return; + } + + root.classList.add(theme); + }, [theme]); + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme); + setTheme(theme); + }, + }; + + return ( + + {children} + + ); +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext); + + if (context === undefined) + throw new Error("useTheme must be used within a ThemeProvider"); + + return context; +}; diff --git a/client/src/components/theme-toggle.tsx b/client/src/components/theme-toggle.tsx new file mode 100644 index 0000000..31a37ee --- /dev/null +++ b/client/src/components/theme-toggle.tsx @@ -0,0 +1,20 @@ +import { Moon, Sun } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useTheme } from "@/components/theme-provider"; + +export function ThemeToggle() { + const { theme, setTheme } = useTheme(); + + return ( + + ); +} diff --git a/client/src/components/ui/accordion.tsx b/client/src/components/ui/accordion.tsx new file mode 100644 index 0000000..e6a723d --- /dev/null +++ b/client/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..8722561 --- /dev/null +++ b/client/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/client/src/components/ui/alert.tsx b/client/src/components/ui/alert.tsx new file mode 100644 index 0000000..41fa7e0 --- /dev/null +++ b/client/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/client/src/components/ui/aspect-ratio.tsx b/client/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c4abbf3 --- /dev/null +++ b/client/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/client/src/components/ui/avatar.tsx b/client/src/components/ui/avatar.tsx new file mode 100644 index 0000000..fc7f964 --- /dev/null +++ b/client/src/components/ui/avatar.tsx @@ -0,0 +1,51 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx new file mode 100644 index 0000000..b59d7ad --- /dev/null +++ b/client/src/components/ui/badge.tsx @@ -0,0 +1,38 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + // Whitespace-nowrap: Badges should never wrap. + "whitespace-nowrap inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" + + " hover-elevate " , + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow-xs", + secondary: "border-transparent bg-secondary text-secondary-foreground", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow-xs", + + outline: " border [border-color:var(--badge-outline)] shadow-xs", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants } diff --git a/client/src/components/ui/breadcrumb.tsx b/client/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/client/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>