From 8bb0386d1e7b36ca0081204aafcaf14eb7048c67 Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Thu, 23 Oct 2025 07:58:57 +0000 Subject: [PATCH] Add customer management features and rename planning view Implement CRUD operations for customers, including API endpoints and database schema. Rename the "Planning Generale" view to "Planning Fissi" and update related UI elements and documentation. 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/2w7P7NW --- client/src/components/app-sidebar.tsx | 2 +- client/src/pages/general-planning.tsx | 2 +- replit.md | 2 +- server/routes.ts | 65 ++++++++++++++++++++++++++- server/storage.ts | 39 ++++++++++++++++ shared/schema.ts | 44 +++++++++++++++--- 6 files changed, 145 insertions(+), 9 deletions(-) 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;