Compare commits

..

No commits in common. "bb50965ebaa9d4ab0c2032483e550fadb59ebc2f" and "1cdae8c4b315c97e0a34338fac027678655fb8e2" have entirely different histories.

7 changed files with 54 additions and 477 deletions

View File

@ -19,6 +19,10 @@ externalPort = 80
localPort = 33035
externalPort = 3001
[[ports]]
localPort = 39567
externalPort = 6000
[[ports]]
localPort = 41295
externalPort = 5173

View File

@ -241,16 +241,9 @@ export default function PlanningMobile() {
// Mutation per salvare patrol route
const savePatrolRouteMutation = useMutation({
mutationFn: async ({ data, existingRouteId }: { data: any; existingRouteId?: string }) => {
if (existingRouteId) {
// UPDATE: usa PUT se esiste già un patrol route
const response = await apiRequest("PUT", `/api/patrol-routes/${existingRouteId}`, data);
return response.json();
} else {
// CREATE: usa POST se è un nuovo patrol route
mutationFn: async (data: any) => {
const response = await apiRequest("POST", "/api/patrol-routes", data);
return response.json();
}
},
onSuccess: () => {
toast({
@ -302,15 +295,7 @@ export default function PlanningMobile() {
})),
};
// Controlla se esiste già un patrol route per questa guardia/data
const existingRoute = existingPatrolRoutes?.find(
(route: any) => route.guardId === selectedGuard.id
);
savePatrolRouteMutation.mutate({
data: patrolRouteData,
existingRouteId: existingRoute?.id,
});
savePatrolRouteMutation.mutate(patrolRouteData);
};
// Carica patrol route esistente quando si seleziona una guardia

View File

@ -2,7 +2,7 @@ import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { format, addWeeks, addDays, startOfWeek } from "date-fns";
import { it } from "date-fns/locale";
import { ChevronLeft, ChevronRight, Users, Building2, Navigation, Shield, Car as CarIcon, MapPin } from "lucide-react";
import { ChevronLeft, ChevronRight, Users, Building2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
@ -12,15 +12,13 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
type Location = "roccapiemonte" | "milano" | "roma";
interface FixedShiftDetail {
interface ShiftDetail {
shiftId: string;
date: string;
from: string;
to: string;
siteName: string;
siteAddress: string;
siteId: string;
isArmed: boolean;
vehicle?: {
licensePlate: string;
brand: string;
@ -29,42 +27,14 @@ interface FixedShiftDetail {
hours: number;
}
interface FixedGuardSchedule {
interface GuardSchedule {
guardId: string;
guardName: string;
badgeNumber: string;
shifts: FixedShiftDetail[];
shifts: ShiftDetail[];
totalHours: number;
}
interface PatrolRoute {
routeId: string;
guardId: string;
shiftDate: string;
startTime: string;
endTime: string;
isArmedRoute: boolean;
vehicle?: {
licensePlate: string;
brand: string;
model: string;
};
stops: {
siteId: string;
siteName: string;
siteAddress: string;
sequenceOrder: number;
}[];
}
interface MobileGuardSchedule {
guardId: string;
guardName: string;
badgeNumber: string;
routes: PatrolRoute[];
totalRoutes: number;
}
interface SiteSchedule {
siteId: string;
siteName: string;
@ -78,7 +48,6 @@ interface SiteSchedule {
guardName: string;
badgeNumber: string;
hours: number;
isArmed: boolean;
}[];
vehicle?: {
licensePlate: string;
@ -95,30 +64,20 @@ interface SiteSchedule {
export default function ServicePlanning() {
const [selectedLocation, setSelectedLocation] = useState<Location>("roccapiemonte");
const [weekStart, setWeekStart] = useState<Date>(startOfWeek(new Date(), { weekStartsOn: 1 }));
const [viewMode, setViewMode] = useState<"guard-fixed" | "guard-mobile" | "site">("guard-fixed");
const [viewMode, setViewMode] = useState<"guard" | "site">("guard");
const weekStartStr = format(weekStart, "yyyy-MM-dd");
const weekEndStr = format(addDays(weekStart, 6), "yyyy-MM-dd");
// Query per vista Agenti Fissi
const { data: fixedGuardSchedules, isLoading: isLoadingFixedGuards } = useQuery<FixedGuardSchedule[]>({
queryKey: ["/api/service-planning/guards-fixed", weekStartStr, selectedLocation],
// Query per vista Guardie
const { data: guardSchedules, isLoading: isLoadingGuards } = useQuery<GuardSchedule[]>({
queryKey: ["/api/service-planning/by-guard", weekStartStr, selectedLocation],
queryFn: async () => {
const response = await fetch(`/api/service-planning/guards-fixed?weekStart=${weekStartStr}&location=${selectedLocation}`);
if (!response.ok) throw new Error("Failed to fetch fixed guard schedules");
const response = await fetch(`/api/service-planning/by-guard?weekStart=${weekStartStr}&location=${selectedLocation}`);
if (!response.ok) throw new Error("Failed to fetch guard schedules");
return response.json();
},
enabled: viewMode === "guard-fixed",
});
// Query per vista Agenti Mobili
const { data: mobileGuardSchedules, isLoading: isLoadingMobileGuards } = useQuery<MobileGuardSchedule[]>({
queryKey: ["/api/service-planning/guards-mobile", weekStartStr, selectedLocation],
queryFn: async () => {
const response = await fetch(`/api/service-planning/guards-mobile?weekStart=${weekStartStr}&location=${selectedLocation}`);
if (!response.ok) throw new Error("Failed to fetch mobile guard schedules");
return response.json();
},
enabled: viewMode === "guard-mobile",
enabled: viewMode === "guard",
});
// Query per vista Siti
@ -140,9 +99,9 @@ export default function ServicePlanning() {
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Visione Servizi</h1>
<h1 className="text-3xl font-bold">Planning di Servizio</h1>
<p className="text-muted-foreground">
Visualizza orari e dotazioni per agente fisso, agente mobile o per sito
Visualizza orari e dotazioni per guardia o sito
</p>
</div>
</div>
@ -186,15 +145,11 @@ export default function ServicePlanning() {
</Card>
{/* Tabs per vista */}
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as "guard-fixed" | "guard-mobile" | "site")}>
<TabsList className="grid w-full max-w-2xl grid-cols-3">
<TabsTrigger value="guard-fixed" data-testid="tab-guard-fixed-view">
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as "guard" | "site")}>
<TabsList className="grid w-full max-w-md grid-cols-2">
<TabsTrigger value="guard" data-testid="tab-guard-view">
<Users className="h-4 w-4 mr-2" />
Agenti Fissi
</TabsTrigger>
<TabsTrigger value="guard-mobile" data-testid="tab-guard-mobile-view">
<Navigation className="h-4 w-4 mr-2" />
Agenti Mobili
Vista Agente
</TabsTrigger>
<TabsTrigger value="site" data-testid="tab-site-view">
<Building2 className="h-4 w-4 mr-2" />
@ -202,18 +157,18 @@ export default function ServicePlanning() {
</TabsTrigger>
</TabsList>
{/* Vista Agenti Fissi */}
<TabsContent value="guard-fixed" className="space-y-4 mt-6">
{isLoadingFixedGuards ? (
{/* Vista Agente */}
<TabsContent value="guard" className="space-y-4 mt-6">
{isLoadingGuards ? (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
) : fixedGuardSchedules && fixedGuardSchedules.length > 0 ? (
) : guardSchedules && guardSchedules.length > 0 ? (
<div className="grid gap-4">
{fixedGuardSchedules.map((guard) => (
<Card key={guard.guardId} data-testid={`card-guard-fixed-${guard.guardId}`}>
{guardSchedules.map((guard) => (
<Card key={guard.guardId} data-testid={`card-guard-${guard.guardId}`}>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">
@ -224,40 +179,25 @@ export default function ServicePlanning() {
</CardHeader>
<CardContent>
{guard.shifts.length === 0 ? (
<p className="text-sm text-muted-foreground">Nessun turno fisso assegnato</p>
<p className="text-sm text-muted-foreground">Nessun turno assegnato</p>
) : (
<div className="space-y-3">
{guard.shifts.map((shift) => (
<div
key={shift.shiftId}
className="p-3 rounded-md bg-muted/50 space-y-2"
className="flex items-start justify-between p-3 rounded-md bg-muted/50"
data-testid={`shift-${shift.shiftId}`}
>
<div className="flex items-start justify-between">
<div className="space-y-1 flex-1">
<div className="space-y-1">
<div className="font-medium">{shift.siteName}</div>
<div className="text-sm text-muted-foreground flex items-center gap-1">
<MapPin className="h-3 w-3" />
{shift.siteAddress}
</div>
<div className="text-sm text-muted-foreground">
{format(new Date(shift.date), "EEEE d MMM", { locale: it })} {shift.from} - {shift.to} ({shift.hours}h)
</div>
</div>
<div className="flex flex-col items-end gap-1">
{shift.isArmed && (
<Badge variant="outline" className="text-xs">
<Shield className="h-3 w-3 mr-1" />
Armato
</Badge>
)}
{shift.vehicle && (
<Badge variant="outline" className="text-xs">
<CarIcon className="h-3 w-3 mr-1" />
{shift.vehicle.licensePlate}
</Badge>
)}
<div className="text-xs text-muted-foreground">
🚗 {shift.vehicle.licensePlate} ({shift.vehicle.brand} {shift.vehicle.model})
</div>
)}
</div>
</div>
))}
@ -270,101 +210,7 @@ export default function ServicePlanning() {
) : (
<Card>
<CardContent className="pt-6">
<p className="text-center text-muted-foreground">Nessun agente con turni fissi assegnati</p>
</CardContent>
</Card>
)}
</TabsContent>
{/* Vista Agenti Mobili */}
<TabsContent value="guard-mobile" className="space-y-4 mt-6">
{isLoadingMobileGuards ? (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
) : mobileGuardSchedules && mobileGuardSchedules.length > 0 ? (
<div className="grid gap-4">
{mobileGuardSchedules.map((guard) => (
<Card key={guard.guardId} data-testid={`card-guard-mobile-${guard.guardId}`}>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">
{guard.guardName} <Badge variant="outline">{guard.badgeNumber}</Badge>
</CardTitle>
<Badge>{guard.totalRoutes} {guard.totalRoutes === 1 ? 'percorso' : 'percorsi'}</Badge>
</div>
</CardHeader>
<CardContent>
{guard.routes.length === 0 ? (
<p className="text-sm text-muted-foreground">Nessun percorso pattuglia assegnato</p>
) : (
<div className="space-y-4">
{guard.routes.map((route) => (
<div
key={route.routeId}
className="p-3 rounded-md bg-muted/50 space-y-3"
data-testid={`route-${route.routeId}`}
>
<div className="flex items-center justify-between">
<div className="font-medium">
{format(new Date(route.shiftDate), "EEEE d MMM yyyy", { locale: it })}
</div>
<div className="text-sm text-muted-foreground">
{route.startTime} - {route.endTime}
</div>
</div>
<div className="flex gap-2">
{route.isArmedRoute && (
<Badge variant="outline" className="text-xs">
<Shield className="h-3 w-3 mr-1" />
Armato
</Badge>
)}
{route.vehicle && (
<Badge variant="outline" className="text-xs">
<CarIcon className="h-3 w-3 mr-1" />
{route.vehicle.licensePlate}
</Badge>
)}
</div>
<div className="space-y-2">
<div className="text-sm font-medium flex items-center gap-1">
<Navigation className="h-4 w-4" />
Percorso ({route.stops.length} {route.stops.length === 1 ? 'tappa' : 'tappe'}):
</div>
<div className="space-y-1 pl-5">
{route.stops.map((stop) => (
<div key={stop.siteId} className="text-sm text-muted-foreground flex items-start gap-2">
<Badge variant="secondary" className="text-xs">
{stop.sequenceOrder}
</Badge>
<div className="flex-1">
<div className="font-medium text-foreground">{stop.siteName}</div>
<div className="text-xs flex items-center gap-1">
<MapPin className="h-3 w-3" />
{stop.siteAddress}
</div>
</div>
</div>
))}
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
))}
</div>
) : (
<Card>
<CardContent className="pt-6">
<p className="text-center text-muted-foreground">Nessun agente con percorsi pattuglia assegnati</p>
<p className="text-center text-muted-foreground">Nessuna guardia con turni assegnati</p>
</CardContent>
</Card>
)}
@ -406,29 +252,20 @@ export default function ServicePlanning() {
<div className="font-medium">
{format(new Date(shift.date), "EEEE d MMM", { locale: it })} {shift.from} - {shift.to}
</div>
<div className="flex gap-1">
<Badge variant="secondary">{shift.totalGuards} {shift.totalGuards === 1 ? "guardia" : "guardie"}</Badge>
{shift.vehicle && (
<Badge variant="outline" className="text-xs">
<CarIcon className="h-3 w-3 mr-1" />
{shift.vehicle.licensePlate}
</Badge>
)}
</div>
</div>
<div className="space-y-1">
{shift.guards.map((guard, idx) => (
<div key={idx} className="text-sm text-muted-foreground flex items-center justify-between">
<span>{guard.guardName} ({guard.badgeNumber}) - {guard.hours}h</span>
{guard.isArmed && (
<Badge variant="outline" className="text-xs">
<Shield className="h-3 w-3 mr-1" />
Armato
</Badge>
)}
<div key={idx} className="text-sm text-muted-foreground">
👤 {guard.guardName} ({guard.badgeNumber}) - {guard.hours}h
</div>
))}
</div>
{shift.vehicle && (
<div className="text-xs text-muted-foreground">
🚗 {shift.vehicle.licensePlate} ({shift.vehicle.brand} {shift.vehicle.model})
</div>
)}
</div>
))}
</div>

View File

@ -679,17 +679,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
)
);
// Ottieni patrol routes del giorno SOLO della sede selezionata
const dayPatrolRoutes = await db
.select()
.from(patrolRoutes)
.where(
and(
eq(patrolRoutes.shiftDate, dateStr),
eq(patrolRoutes.location, location as any)
)
);
// Calcola disponibilità agenti con report CCNL
const guardsWithAvailability = await Promise.all(
locationGuards.map(async (guard) => {
@ -697,10 +686,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
(assignment: any) => assignment.shift_assignments.guardId === guard.id
);
const assignedPatrolRoute = dayPatrolRoutes.find(
(route: any) => route.guardId === guard.id
);
// Calcola report disponibilità CCNL
const availabilityReport = await getGuardAvailabilityReport(
guard.id,
@ -710,18 +695,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
return {
...guard,
isAvailable: !assignedShift && !assignedPatrolRoute,
isAvailable: !assignedShift,
assignedShift: assignedShift ? {
id: assignedShift.shifts.id,
startTime: assignedShift.shifts.startTime,
endTime: assignedShift.shifts.endTime,
siteId: assignedShift.shifts.siteId
} : null,
assignedPatrolRoute: assignedPatrolRoute ? {
id: assignedPatrolRoute.id,
startTime: assignedPatrolRoute.startTime,
endTime: assignedPatrolRoute.endTime,
} : null,
availability: {
weeklyHours: availabilityReport.weeklyHours.current,
remainingWeeklyHours: availabilityReport.remainingWeeklyHours,
@ -1475,229 +1455,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
// ============= SERVICE PLANNING ROUTES =============
// Vista per Agente Fisso - mostra orari e dotazioni operative per turni fissi
app.get("/api/service-planning/guards-fixed", isAuthenticated, async (req, res) => {
try {
const rawWeekStart = req.query.weekStart as string || format(new Date(), "yyyy-MM-dd");
const normalizedWeekStart = rawWeekStart.split("/")[0];
const parsedWeekStart = parseISO(normalizedWeekStart);
if (!isValid(parsedWeekStart)) {
return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" });
}
const weekStartDate = format(parsedWeekStart, "yyyy-MM-dd");
const location = req.query.location as string || "roccapiemonte";
const weekEndDate = format(addDays(parsedWeekStart, 6), "yyyy-MM-dd");
const weekStartTimestamp = new Date(weekStartDate);
weekStartTimestamp.setHours(0, 0, 0, 0);
const weekEndTimestamp = new Date(weekEndDate);
weekEndTimestamp.setHours(23, 59, 59, 999);
// Ottieni tutte le guardie della sede
const allGuards = await db
.select()
.from(guards)
.where(eq(guards.location, location as any))
.orderBy(guards.fullName);
// Ottieni tutti i turni della settimana
const allWeekShifts = await db
.select({
shift: shifts,
site: sites,
vehicle: vehicles,
serviceType: serviceTypes,
})
.from(shifts)
.innerJoin(sites, eq(shifts.siteId, sites.id))
.leftJoin(vehicles, eq(shifts.vehicleId, vehicles.id))
.leftJoin(serviceTypes, eq(sites.serviceTypeId, serviceTypes.id))
.where(
and(
gte(shifts.startTime, weekStartTimestamp),
lte(shifts.startTime, weekEndTimestamp),
ne(shifts.status, "cancelled"),
eq(sites.location, location as any)
)
);
// Filtra solo turni FISSI in base alla classificazione del serviceType
const weekShifts = allWeekShifts.filter((s: any) =>
s.serviceType && s.serviceType.classification === "fisso"
);
// Ottieni tutte le assegnazioni per i turni della settimana
const shiftIds = weekShifts.map((s: any) => s.shift.id);
const assignments = shiftIds.length > 0 ? await db
.select({
assignment: shiftAssignments,
guard: guards,
})
.from(shiftAssignments)
.innerJoin(guards, eq(shiftAssignments.guardId, guards.id))
.where(
sql`${shiftAssignments.shiftId} IN (${sql.join(shiftIds.map((id: string) => sql`${id}`), sql`, `)})`
) : [];
// Costruisci dati per ogni guardia
const guardSchedules = allGuards.map((guard: any) => {
// Trova assegnazioni della guardia
const guardAssignments = assignments.filter((a: any) => a.guard.id === guard.id);
// Costruisci lista turni con dettagli
const shifts = guardAssignments.map((a: any) => {
const shiftData = weekShifts.find((s: any) => s.shift.id === a.assignment.shiftId);
if (!shiftData) return null;
const plannedStart = new Date(a.assignment.plannedStartTime);
const plannedEnd = new Date(a.assignment.plannedEndTime);
const minutes = differenceInMinutes(plannedEnd, plannedStart);
const hours = Math.round((minutes / 60) * 10) / 10;
return {
shiftId: shiftData.shift.id,
date: format(plannedStart, "yyyy-MM-dd"),
from: format(plannedStart, "HH:mm"),
to: format(plannedEnd, "HH:mm"),
siteName: shiftData.site.name,
siteAddress: shiftData.site.address,
siteId: shiftData.site.id,
isArmed: guard.isArmed,
vehicle: shiftData.vehicle ? {
licensePlate: shiftData.vehicle.licensePlate,
brand: shiftData.vehicle.brand,
model: shiftData.vehicle.model,
} : undefined,
hours,
};
}).filter(Boolean);
const totalHours = Math.round(shifts.reduce((sum: number, s: any) => sum + s.hours, 0) * 10) / 10;
return {
guardId: guard.id,
guardName: guard.fullName,
badgeNumber: guard.badgeNumber,
shifts,
totalHours,
};
});
// Filtra solo guardie con turni assegnati
const guardsWithShifts = guardSchedules.filter((g: any) => g.shifts.length > 0);
res.json(guardsWithShifts);
} catch (error) {
console.error("Error fetching fixed guard schedules:", error);
res.status(500).json({ message: "Failed to fetch fixed guard schedules", error: String(error) });
}
});
// Vista per Agente Mobile - mostra percorsi pattuglia con siti e indirizzi
app.get("/api/service-planning/guards-mobile", isAuthenticated, async (req, res) => {
try {
const rawWeekStart = req.query.weekStart as string || format(new Date(), "yyyy-MM-dd");
const normalizedWeekStart = rawWeekStart.split("/")[0];
const parsedWeekStart = parseISO(normalizedWeekStart);
if (!isValid(parsedWeekStart)) {
return res.status(400).json({ message: "Invalid date format. Use yyyy-MM-dd" });
}
const weekStartDate = format(parsedWeekStart, "yyyy-MM-dd");
const location = req.query.location as string || "roccapiemonte";
const weekEndDate = format(addDays(parsedWeekStart, 6), "yyyy-MM-dd");
// Ottieni tutte le guardie della sede
const allGuards = await db
.select()
.from(guards)
.where(eq(guards.location, location as any))
.orderBy(guards.fullName);
// Ottieni tutte le patrol routes della settimana per la sede
const weekRoutes = await db
.select({
route: patrolRoutes,
guard: guards,
vehicle: vehicles,
})
.from(patrolRoutes)
.innerJoin(guards, eq(patrolRoutes.guardId, guards.id))
.leftJoin(vehicles, eq(patrolRoutes.vehicleId, vehicles.id))
.where(
and(
gte(sql`${patrolRoutes.shiftDate}::date`, weekStartDate),
lte(sql`${patrolRoutes.shiftDate}::date`, weekEndDate),
eq(patrolRoutes.location, location as any)
)
);
// Per ogni route, ottieni le stops
const routesWithStops = await Promise.all(
weekRoutes.map(async (routeData: any) => {
const stops = await db
.select({
stop: patrolRouteStops,
site: sites,
})
.from(patrolRouteStops)
.innerJoin(sites, eq(patrolRouteStops.siteId, sites.id))
.where(eq(patrolRouteStops.routeId, routeData.route.id))
.orderBy(asc(patrolRouteStops.sequenceOrder));
return {
routeId: routeData.route.id,
guardId: routeData.guard.id,
shiftDate: routeData.route.shiftDate,
startTime: routeData.route.startTime,
endTime: routeData.route.endTime,
isArmedRoute: routeData.route.isArmedRoute,
vehicle: routeData.vehicle ? {
licensePlate: routeData.vehicle.licensePlate,
brand: routeData.vehicle.brand,
model: routeData.vehicle.model,
} : undefined,
stops: stops.map((s: any) => ({
siteId: s.site.id,
siteName: s.site.name,
siteAddress: s.site.address,
sequenceOrder: s.stop.sequenceOrder,
})),
};
})
);
// Costruisci dati per ogni guardia
const guardSchedules = allGuards.map((guard: any) => {
// Trova routes della guardia
const guardRoutes = routesWithStops.filter((r: any) => r.guardId === guard.id);
const totalRoutes = guardRoutes.length;
return {
guardId: guard.id,
guardName: guard.fullName,
badgeNumber: guard.badgeNumber,
routes: guardRoutes,
totalRoutes,
};
});
// Filtra solo guardie con routes assegnate
const guardsWithRoutes = guardSchedules.filter((g: any) => g.routes.length > 0);
res.json(guardsWithRoutes);
} catch (error) {
console.error("Error fetching mobile guard schedules:", error);
res.status(500).json({ message: "Failed to fetch mobile guard schedules", error: String(error) });
}
});
// Vista per Guardia - mostra orari e dotazioni per ogni guardia (LEGACY - manteniamo per compatibilità)
// Vista per Guardia - mostra orari e dotazioni per ogni guardia
app.get("/api/service-planning/by-guard", isAuthenticated, async (req, res) => {
try {
const rawWeekStart = req.query.weekStart as string || format(new Date(), "yyyy-MM-dd");
@ -1893,7 +1651,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
guardName: a.guard.fullName,
badgeNumber: a.guard.badgeNumber,
hours,
isArmed: a.guard.isArmed,
};
});

View File

@ -1,13 +1,7 @@
{
"version": "1.0.49",
"lastUpdate": "2025-10-23T17:04:52.044Z",
"version": "1.0.48",
"lastUpdate": "2025-10-23T16:03:23.268Z",
"changelog": [
{
"version": "1.0.49",
"date": "2025-10-23",
"type": "patch",
"description": "Deployment automatico v1.0.49"
},
{
"version": "1.0.48",
"date": "2025-10-23",