- 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;