diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx index cdcf329..0a6fffc 100644 --- a/client/src/components/app-sidebar.tsx +++ b/client/src/components/app-sidebar.tsx @@ -56,7 +56,7 @@ const menuItems = [ roles: ["admin", "coordinator"], }, { - title: "Planning Generale", + title: "Planning Fissi", url: "/general-planning", icon: BarChart3, roles: ["admin", "coordinator"], diff --git a/client/src/pages/general-planning.tsx b/client/src/pages/general-planning.tsx index 29567bc..2a1f4d2 100644 --- a/client/src/pages/general-planning.tsx +++ b/client/src/pages/general-planning.tsx @@ -330,7 +330,7 @@ export default function GeneralPlanning() {

- Planning Generale + Planning Fissi

Vista settimanale turni con calcolo automatico guardie mancanti diff --git a/replit.md b/replit.md index a30d0b4..6d491d3 100644 --- a/replit.md +++ b/replit.md @@ -46,7 +46,7 @@ The database includes core tables for `users`, `guards`, `certifications`, `site 5. Assign resources and create shift - **Location-Based Filtering**: Backend endpoints use INNER JOIN with sites table to ensure complete resource isolation between locations - guards/vehicles in one sede remain available even when assigned to shifts in other sedi - **Site Management**: Added sede selection in site creation/editing forms with visual badges showing location in site listings -- **Planning Generale (October 18, 2025)**: New weekly planning overview feature showing all sites × 7 days in table format: +- **Planning Fissi (October 18, 2025)**: New weekly planning overview feature showing all sites × 7 days in table format: - **Contract filtering**: Shows only sites with active contracts in the week dates (`contractStartDate <= weekEnd AND contractEndDate >= weekStart`) - Backend endpoint `/api/general-planning?weekStart=YYYY-MM-DD&location=sede` with complex joins and location filtering - Automatic missing guards calculation: `ceil(totalShiftHours / maxHoursPerGuard) × minGuards - assignedGuards` (e.g., 24h shift, 2 guards min, 9h max = 6 total needed) diff --git a/server/routes.ts b/server/routes.ts index fb79ca9..7ae8682 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -4,9 +4,11 @@ import { storage } from "./storage"; import { setupAuth as setupReplitAuth, isAuthenticated as isAuthenticatedReplit } from "./replitAuth"; import { setupLocalAuth, isAuthenticated as isAuthenticatedLocal } from "./localAuth"; import { db } from "./db"; -import { guards, certifications, sites, shifts, shiftAssignments, users, insertShiftSchema, contractParameters, vehicles, serviceTypes, createMultiDayShiftSchema } from "@shared/schema"; +import { guards, certifications, sites, shifts, shiftAssignments, users, insertShiftSchema, contractParameters, vehicles, serviceTypes, createMultiDayShiftSchema, insertCustomerSchema } from "@shared/schema"; import { eq, and, gte, lte, desc, asc, ne, sql } from "drizzle-orm"; import { differenceInDays, differenceInHours, differenceInMinutes, startOfWeek, endOfWeek, startOfMonth, endOfMonth, isWithinInterval, startOfDay, isSameDay, parseISO, format, isValid, addDays } from "date-fns"; +import { z } from "zod"; +import { fromZodError } from "zod-validation-error"; // Determina quale sistema auth usare basandosi sull'ambiente const USE_LOCAL_AUTH = process.env.DOMAIN === "vt.alfacom.it" || !process.env.REPLIT_DOMAINS; @@ -1979,6 +1981,67 @@ export async function registerRoutes(app: Express): Promise { } }); + // ============= CUSTOMER ROUTES ============= + app.get("/api/customers", isAuthenticated, async (req, res) => { + try { + const customers = await storage.getAllCustomers(); + res.json(customers); + } catch (error) { + console.error("Error fetching customers:", error); + res.status(500).json({ message: "Failed to fetch customers" }); + } + }); + + app.post("/api/customers", isAuthenticated, async (req, res) => { + try { + const validatedData = insertCustomerSchema.parse(req.body); + const customer = await storage.createCustomer(validatedData); + res.json(customer); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + message: "Validation failed", + errors: fromZodError(error).message + }); + } + console.error("Error creating customer:", error); + res.status(500).json({ message: "Failed to create customer" }); + } + }); + + app.patch("/api/customers/:id", isAuthenticated, async (req, res) => { + try { + const validatedData = insertCustomerSchema.partial().parse(req.body); + const customer = await storage.updateCustomer(req.params.id, validatedData); + if (!customer) { + return res.status(404).json({ message: "Customer not found" }); + } + res.json(customer); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + message: "Validation failed", + errors: fromZodError(error).message + }); + } + console.error("Error updating customer:", error); + res.status(500).json({ message: "Failed to update customer" }); + } + }); + + app.delete("/api/customers/:id", isAuthenticated, async (req, res) => { + try { + const customer = await storage.deleteCustomer(req.params.id); + if (!customer) { + return res.status(404).json({ message: "Customer not found" }); + } + res.json(customer); + } catch (error) { + console.error("Error deleting customer:", error); + res.status(500).json({ message: "Failed to delete customer" }); + } + }); + // ============= SITE ROUTES ============= app.get("/api/sites", isAuthenticated, async (req, res) => { try { diff --git a/server/storage.ts b/server/storage.ts index 1437704..c0da60c 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -4,6 +4,7 @@ import { guards, certifications, vehicles, + customers, sites, shifts, shiftAssignments, @@ -26,6 +27,8 @@ import { type InsertCertification, type Vehicle, type InsertVehicle, + type Customer, + type InsertCustomer, type Site, type InsertSite, type Shift, @@ -85,6 +88,13 @@ export interface IStorage { updateServiceType(id: string, serviceType: Partial): Promise; deleteServiceType(id: string): Promise; + // Customer operations + getAllCustomers(): Promise; + getCustomer(id: string): Promise; + createCustomer(customer: InsertCustomer): Promise; + updateCustomer(id: string, customer: Partial): Promise; + deleteCustomer(id: string): Promise; + // Site operations getAllSites(): Promise; getSite(id: string): Promise; @@ -342,6 +352,35 @@ export class DatabaseStorage implements IStorage { return deleted; } + // Customer operations + async getAllCustomers(): Promise { + return await db.select().from(customers).orderBy(desc(customers.createdAt)); + } + + async getCustomer(id: string): Promise { + const [customer] = await db.select().from(customers).where(eq(customers.id, id)); + return customer; + } + + async createCustomer(customer: InsertCustomer): Promise { + const [newCustomer] = await db.insert(customers).values(customer).returning(); + return newCustomer; + } + + async updateCustomer(id: string, customerData: Partial): Promise { + const [updated] = await db + .update(customers) + .set({ ...customerData, updatedAt: new Date() }) + .where(eq(customers.id, id)) + .returning(); + return updated; + } + + async deleteCustomer(id: string): Promise { + const [deleted] = await db.delete(customers).where(eq(customers.id, id)).returning(); + return deleted; + } + // Site operations async getAllSites(): Promise { return await db.select().from(sites); diff --git a/shared/schema.ts b/shared/schema.ts index 9aac6d3..7ec9919 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -200,13 +200,35 @@ export const serviceTypes = pgTable("service_types", { updatedAt: timestamp("updated_at").defaultNow(), }); +// ============= CUSTOMERS ============= + +export const customers = pgTable("customers", { + id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), + name: varchar("name").notNull(), + businessName: varchar("business_name"), // Ragione sociale + vatNumber: varchar("vat_number"), // Partita IVA + fiscalCode: varchar("fiscal_code"), // Codice fiscale + address: varchar("address"), + city: varchar("city"), + province: varchar("province"), + zipCode: varchar("zip_code"), + phone: varchar("phone"), + email: varchar("email"), + pec: varchar("pec"), // PEC (Posta Elettronica Certificata) + contactPerson: varchar("contact_person"), // Referente + notes: text("notes"), + isActive: boolean("is_active").default(true), + createdAt: timestamp("created_at").defaultNow(), + updatedAt: timestamp("updated_at").defaultNow(), +}); + // ============= SITES & CONTRACTS ============= export const sites = pgTable("sites", { id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), name: varchar("name").notNull(), address: varchar("address").notNull(), - clientId: varchar("client_id").references(() => users.id), + customerId: varchar("customer_id").references(() => customers.id), location: locationEnum("location").notNull().default("roccapiemonte"), // Sede gestionale // Service requirements @@ -478,7 +500,6 @@ export const usersRelations = relations(users, ({ one, many }) => ({ fields: [users.id], references: [guards.userId], }), - managedSites: many(sites), notifications: many(notifications), })); @@ -510,10 +531,14 @@ export const certificationsRelations = relations(certifications, ({ one }) => ({ }), })); +export const customersRelations = relations(customers, ({ many }) => ({ + sites: many(sites), +})); + export const sitesRelations = relations(sites, ({ one, many }) => ({ - client: one(users, { - fields: [sites.clientId], - references: [users.id], + customer: one(customers, { + fields: [sites.customerId], + references: [customers.id], }), shifts: many(shifts), preferences: many(sitePreferences), @@ -680,6 +705,12 @@ export const insertServiceTypeSchema = createInsertSchema(serviceTypes).omit({ updatedAt: true, }); +export const insertCustomerSchema = createInsertSchema(customers).omit({ + id: true, + createdAt: true, + updatedAt: true, +}); + export const insertSiteSchema = createInsertSchema(sites).omit({ id: true, createdAt: true, @@ -794,6 +825,9 @@ export type Vehicle = typeof vehicles.$inferSelect; export type InsertServiceType = z.infer; export type ServiceType = typeof serviceTypes.$inferSelect; +export type InsertCustomer = z.infer; +export type Customer = typeof customers.$inferSelect; + export type InsertSite = z.infer; export type Site = typeof sites.$inferSelect;