Add and update guard, site, and shift management functionality

Implement CRUD operations for guards, sites, and shifts, including PATCH and DELETE endpoints, and enhance shift assignment logic with skill validation and real-time synchronization.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 99f0fce6-9386-489a-9632-1d81223cab44
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/99f0fce6-9386-489a-9632-1d81223cab44/9lvKVew
This commit is contained in:
marco370 2025-10-11 11:09:25 +00:00
parent b3d0441306
commit 06c03be97c
4 changed files with 133 additions and 4 deletions

View File

@ -18,10 +18,6 @@ externalPort = 80
localPort = 33035 localPort = 33035
externalPort = 3001 externalPort = 3001
[[ports]]
localPort = 38805
externalPort = 3002
[[ports]] [[ports]]
localPort = 41343 localPort = 41343
externalPort = 3000 externalPort = 3000

View File

@ -193,6 +193,18 @@ All interactive elements have `data-testid` attributes for automated testing.
- Frontend: form validation client-side con messaggi errore chiari - Frontend: form validation client-side con messaggi errore chiari
- Backend: validazione ISO strings prima di conversione a Date - Backend: validazione ISO strings prima di conversione a Date
- Test e2e passati con successo ✅ - Test e2e passati con successo ✅
- **Sistema assegnazione guardie ai turni** ✅:
- Dialog assegnazione con validazione skills vs requisiti sito
- Mostra competenze guardie (Armato, Patente, Primo Soccorso, Antincendio)
- Solo guardie idonee possono essere assegnate
- Add/Remove assignments con sync real-time (refetchQueries)
- DELETE /api/shift-assignments/:id implementato
- **CRUD completo (Backend)** ✅:
- PATCH/DELETE /api/guards/:id
- PATCH/DELETE /api/sites/:id
- PATCH/DELETE /api/shifts/:id
- 404 handling quando risorse non esistono
- Storage methods restituiscono entità aggiornate/eliminate
- Aggiunto SEO completo (title, meta description, Open Graph) - Aggiunto SEO completo (title, meta description, Open Graph)
- Tutti i componenti testabili con data-testid attributes - Tutti i componenti testabili con data-testid attributes

View File

@ -76,6 +76,32 @@ export async function registerRoutes(app: Express): Promise<Server> {
} }
}); });
app.patch("/api/guards/:id", isAuthenticated, async (req, res) => {
try {
const updated = await storage.updateGuard(req.params.id, req.body);
if (!updated) {
return res.status(404).json({ message: "Guard not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating guard:", error);
res.status(500).json({ message: "Failed to update guard" });
}
});
app.delete("/api/guards/:id", isAuthenticated, async (req, res) => {
try {
const deleted = await storage.deleteGuard(req.params.id);
if (!deleted) {
return res.status(404).json({ message: "Guard not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Error deleting guard:", error);
res.status(500).json({ message: "Failed to delete guard" });
}
});
// ============= CERTIFICATION ROUTES ============= // ============= CERTIFICATION ROUTES =============
app.post("/api/certifications", isAuthenticated, async (req, res) => { app.post("/api/certifications", isAuthenticated, async (req, res) => {
try { try {
@ -108,6 +134,32 @@ export async function registerRoutes(app: Express): Promise<Server> {
} }
}); });
app.patch("/api/sites/:id", isAuthenticated, async (req, res) => {
try {
const updated = await storage.updateSite(req.params.id, req.body);
if (!updated) {
return res.status(404).json({ message: "Site not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating site:", error);
res.status(500).json({ message: "Failed to update site" });
}
});
app.delete("/api/sites/:id", isAuthenticated, async (req, res) => {
try {
const deleted = await storage.deleteSite(req.params.id);
if (!deleted) {
return res.status(404).json({ message: "Site not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Error deleting site:", error);
res.status(500).json({ message: "Failed to delete site" });
}
});
// ============= SHIFT ROUTES ============= // ============= SHIFT ROUTES =============
app.get("/api/shifts", isAuthenticated, async (req, res) => { app.get("/api/shifts", isAuthenticated, async (req, res) => {
try { try {
@ -236,6 +288,51 @@ export async function registerRoutes(app: Express): Promise<Server> {
} }
}); });
app.patch("/api/shifts/:id", isAuthenticated, async (req, res) => {
try {
const { startTime: startTimeStr, endTime: endTimeStr, ...rest } = req.body;
const updateData: any = { ...rest };
if (startTimeStr) {
const startTime = new Date(startTimeStr);
if (isNaN(startTime.getTime())) {
return res.status(400).json({ message: "Invalid start time format" });
}
updateData.startTime = startTime;
}
if (endTimeStr) {
const endTime = new Date(endTimeStr);
if (isNaN(endTime.getTime())) {
return res.status(400).json({ message: "Invalid end time format" });
}
updateData.endTime = endTime;
}
const updated = await storage.updateShift(req.params.id, updateData);
if (!updated) {
return res.status(404).json({ message: "Shift not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating shift:", error);
res.status(500).json({ message: "Failed to update shift" });
}
});
app.delete("/api/shifts/:id", isAuthenticated, async (req, res) => {
try {
const deleted = await storage.deleteShift(req.params.id);
if (!deleted) {
return res.status(404).json({ message: "Shift not found" });
}
res.json({ success: true });
} catch (error) {
console.error("Error deleting shift:", error);
res.status(500).json({ message: "Failed to delete shift" });
}
});
// ============= SHIFT ASSIGNMENT ROUTES ============= // ============= SHIFT ASSIGNMENT ROUTES =============
app.post("/api/shift-assignments", isAuthenticated, async (req, res) => { app.post("/api/shift-assignments", isAuthenticated, async (req, res) => {
try { try {

View File

@ -110,6 +110,11 @@ export class DatabaseStorage implements IStorage {
return updated; return updated;
} }
async deleteGuard(id: string): Promise<Guard | undefined> {
const [deleted] = await db.delete(guards).where(eq(guards.id, id)).returning();
return deleted;
}
// Certification operations // Certification operations
async getCertificationsByGuard(guardId: string): Promise<Certification[]> { async getCertificationsByGuard(guardId: string): Promise<Certification[]> {
return await db return await db
@ -158,6 +163,11 @@ export class DatabaseStorage implements IStorage {
return updated; return updated;
} }
async deleteSite(id: string): Promise<Site | undefined> {
const [deleted] = await db.delete(sites).where(eq(sites.id, id)).returning();
return deleted;
}
// Shift operations // Shift operations
async getAllShifts(): Promise<Shift[]> { async getAllShifts(): Promise<Shift[]> {
return await db.select().from(shifts).orderBy(desc(shifts.startTime)); return await db.select().from(shifts).orderBy(desc(shifts.startTime));
@ -191,6 +201,20 @@ export class DatabaseStorage implements IStorage {
.where(eq(shifts.id, id)); .where(eq(shifts.id, id));
} }
async updateShift(id: string, shiftData: Partial<InsertShift>): Promise<Shift | undefined> {
const [updated] = await db
.update(shifts)
.set({ ...shiftData, updatedAt: new Date() })
.where(eq(shifts.id, id))
.returning();
return updated;
}
async deleteShift(id: string): Promise<Shift | undefined> {
const [deleted] = await db.delete(shifts).where(eq(shifts.id, id)).returning();
return deleted;
}
// Shift Assignment operations // Shift Assignment operations
async getShiftAssignments(shiftId: string): Promise<ShiftAssignment[]> { async getShiftAssignments(shiftId: string): Promise<ShiftAssignment[]> {
return await db return await db