Compare commits

...

4 Commits

Author SHA1 Message Date
Marco Lanzara
18e219e118 🚀 Release v1.0.24
- Tipo: patch
- Database backup: database-backups/vigilanzaturni_v1.0.24_20251018_102519.sql.gz
- Data: 2025-10-18 10:25:34
2025-10-18 10:25:35 +00:00
marco370
b4ca7d594e Add a way for users to safely log in to their accounts
Add a new API endpoint for user authentication.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/McZSLgC
2025-10-18 10:11:42 +00:00
marco370
ce1ba6ef33 Update guard details to show more relevant information
Refactors the Guards page to display guard's email, phone number, and location. Also updates initial fallback for Avatar component and changes formatting for CardDescription.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/McZSLgC
2025-10-18 10:11:23 +00:00
marco370
1dcb20400f Add missing guard details to the guard management form
Update client/src/pages/guards.tsx to include firstName, lastName, and email fields in the guard form, and update shared/schema.ts to reflect the new fields in the insertGuardSchema.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/2o9hx6y
2025-10-18 09:29:57 +00:00
21 changed files with 179 additions and 9 deletions

BIN
after-reload.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

View File

@ -7,6 +7,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Di
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { insertGuardSchema, insertCertificationSchema } from "@shared/schema";
@ -31,8 +32,12 @@ export default function Guards() {
const form = useForm<InsertGuard>({
resolver: zodResolver(insertGuardSchema),
defaultValues: {
firstName: "",
lastName: "",
email: "",
badgeNumber: "",
phoneNumber: "",
location: "roccapiemonte",
isArmed: false,
hasFireSafety: false,
hasFirstAid: false,
@ -44,8 +49,12 @@ export default function Guards() {
const editForm = useForm<InsertGuard>({
resolver: zodResolver(insertGuardSchema),
defaultValues: {
firstName: "",
lastName: "",
email: "",
badgeNumber: "",
phoneNumber: "",
location: "roccapiemonte",
isArmed: false,
hasFireSafety: false,
hasFirstAid: false,
@ -111,8 +120,12 @@ export default function Guards() {
const openEditDialog = (guard: GuardWithCertifications) => {
setEditingGuard(guard);
editForm.reset({
firstName: guard.firstName || "",
lastName: guard.lastName || "",
email: guard.email || "",
badgeNumber: guard.badgeNumber,
phoneNumber: guard.phoneNumber || "",
location: guard.location || "roccapiemonte",
isArmed: guard.isArmed,
hasFireSafety: guard.hasFireSafety,
hasFirstAid: guard.hasFirstAid,
@ -146,6 +159,50 @@ export default function Guards() {
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>Nome</FormLabel>
<FormControl>
<Input placeholder="Mario" {...field} data-testid="input-first-name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Cognome</FormLabel>
<FormControl>
<Input placeholder="Rossi" {...field} data-testid="input-last-name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="mario.rossi@esempio.it" type="email" {...field} value={field.value || ""} data-testid="input-email" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="badgeNumber"
@ -165,7 +222,7 @@ export default function Guards() {
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Telefono</FormLabel>
<FormLabel>Cellulare</FormLabel>
<FormControl>
<Input placeholder="+39 123 456 7890" {...field} value={field.value || ""} data-testid="input-phone" />
</FormControl>
@ -174,6 +231,29 @@ export default function Guards() {
)}
/>
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>Sede di Appartenenza</FormLabel>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger data-testid="select-location">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-3">
<p className="text-sm font-medium">Competenze</p>
<div className="grid grid-cols-2 gap-4">
@ -262,11 +342,55 @@ export default function Guards() {
<DialogHeader>
<DialogTitle>Modifica Guardia</DialogTitle>
<DialogDescription>
Modifica i dati della guardia {editingGuard?.user?.firstName} {editingGuard?.user?.lastName}
Modifica i dati della guardia {editingGuard?.firstName} {editingGuard?.lastName}
</DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit(onEditSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>Nome</FormLabel>
<FormControl>
<Input placeholder="Mario" {...field} data-testid="input-edit-first-name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Cognome</FormLabel>
<FormControl>
<Input placeholder="Rossi" {...field} data-testid="input-edit-last-name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={editForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="mario.rossi@esempio.it" type="email" {...field} value={field.value || ""} data-testid="input-edit-email" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="badgeNumber"
@ -286,7 +410,7 @@ export default function Guards() {
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Telefono</FormLabel>
<FormLabel>Cellulare</FormLabel>
<FormControl>
<Input placeholder="+39 123 456 7890" {...field} value={field.value || ""} data-testid="input-edit-phone" />
</FormControl>
@ -295,6 +419,29 @@ export default function Guards() {
)}
/>
<FormField
control={editForm.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>Sede di Appartenenza</FormLabel>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger data-testid="select-edit-location">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="roccapiemonte">Roccapiemonte</SelectItem>
<SelectItem value="milano">Milano</SelectItem>
<SelectItem value="roma">Roma</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-3">
<p className="text-sm font-medium">Competenze</p>
<div className="grid grid-cols-2 gap-4">
@ -391,15 +538,20 @@ export default function Guards() {
<Avatar>
<AvatarImage src={guard.user?.profileImageUrl || undefined} />
<AvatarFallback>
{guard.user?.firstName?.[0]}{guard.user?.lastName?.[0]}
{guard.firstName?.[0]}{guard.lastName?.[0]}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<CardTitle className="text-lg truncate">
{guard.user?.firstName} {guard.user?.lastName}
{guard.firstName} {guard.lastName}
</CardTitle>
<CardDescription className="font-mono text-xs">
{guard.badgeNumber}
<CardDescription className="space-y-0.5">
<div className="font-mono text-xs">{guard.badgeNumber}</div>
{guard.email && <div className="text-xs truncate">{guard.email}</div>}
{guard.phoneNumber && <div className="text-xs">{guard.phoneNumber}</div>}
<Badge variant="outline" className="text-xs mt-1">
{guard.location === "roccapiemonte" ? "Roccapiemonte" : guard.location === "milano" ? "Milano" : "Roma"}
</Badge>
</CardDescription>
</div>
<Button

BIN
final_reopen_edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
guards-nav.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
no-guard-link.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
no-login-button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

View File

@ -122,6 +122,11 @@ export const users = pgTable("users", {
export const guards = pgTable("guards", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
userId: varchar("user_id").references(() => users.id),
// Anagrafica
firstName: varchar("first_name").notNull(),
lastName: varchar("last_name").notNull(),
email: varchar("email"),
badgeNumber: varchar("badge_number").notNull().unique(),
phoneNumber: varchar("phone_number"),
location: locationEnum("location").notNull().default("roccapiemonte"), // Sede di appartenenza
@ -644,6 +649,13 @@ export const insertGuardSchema = createInsertSchema(guards).omit({
id: true,
createdAt: true,
updatedAt: true,
}).extend({
firstName: z.string().min(1, "Nome obbligatorio"),
lastName: z.string().min(1, "Cognome obbligatorio"),
email: z.string().email("Email non valida").optional().or(z.literal("")),
badgeNumber: z.string().min(1, "Matricola obbligatoria"),
phoneNumber: z.string().optional().or(z.literal("")),
location: z.enum(["roccapiemonte", "milano", "roma"]),
});
export const insertCertificationSchema = createInsertSchema(certifications).omit({

BIN
step11_toggled_switches.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
step14_after_submit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
step15_post_submit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
step4_page.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

BIN
step5_guards_nav.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
step8_add_guard_clicked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -1,7 +1,13 @@
{
"version": "1.0.23",
"lastUpdate": "2025-10-18T09:20:55.191Z",
"version": "1.0.24",
"lastUpdate": "2025-10-18T10:25:34.931Z",
"changelog": [
{
"version": "1.0.24",
"date": "2025-10-18",
"type": "patch",
"description": "Deployment automatico v1.0.24"
},
{
"version": "1.0.23",
"date": "2025-10-18",