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
This commit is contained in:
parent
ba0bd4d36f
commit
8bb0386d1e
@ -56,7 +56,7 @@ const menuItems = [
|
|||||||
roles: ["admin", "coordinator"],
|
roles: ["admin", "coordinator"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Planning Generale",
|
title: "Planning Fissi",
|
||||||
url: "/general-planning",
|
url: "/general-planning",
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
roles: ["admin", "coordinator"],
|
roles: ["admin", "coordinator"],
|
||||||
|
|||||||
@ -330,7 +330,7 @@ export default function GeneralPlanning() {
|
|||||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight" data-testid="text-page-title">
|
<h1 className="text-3xl font-bold tracking-tight" data-testid="text-page-title">
|
||||||
Planning Generale
|
Planning Fissi
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Vista settimanale turni con calcolo automatico guardie mancanti
|
Vista settimanale turni con calcolo automatico guardie mancanti
|
||||||
|
|||||||
@ -46,7 +46,7 @@ The database includes core tables for `users`, `guards`, `certifications`, `site
|
|||||||
5. Assign resources and create shift
|
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
|
- **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
|
- **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`)
|
- **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
|
- 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)
|
- Automatic missing guards calculation: `ceil(totalShiftHours / maxHoursPerGuard) × minGuards - assignedGuards` (e.g., 24h shift, 2 guards min, 9h max = 6 total needed)
|
||||||
|
|||||||
@ -4,9 +4,11 @@ import { storage } from "./storage";
|
|||||||
import { setupAuth as setupReplitAuth, isAuthenticated as isAuthenticatedReplit } from "./replitAuth";
|
import { setupAuth as setupReplitAuth, isAuthenticated as isAuthenticatedReplit } from "./replitAuth";
|
||||||
import { setupLocalAuth, isAuthenticated as isAuthenticatedLocal } from "./localAuth";
|
import { setupLocalAuth, isAuthenticated as isAuthenticatedLocal } from "./localAuth";
|
||||||
import { db } from "./db";
|
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 { 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 { 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
|
// Determina quale sistema auth usare basandosi sull'ambiente
|
||||||
const USE_LOCAL_AUTH = process.env.DOMAIN === "vt.alfacom.it" || !process.env.REPLIT_DOMAINS;
|
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<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============= 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 =============
|
// ============= SITE ROUTES =============
|
||||||
app.get("/api/sites", isAuthenticated, async (req, res) => {
|
app.get("/api/sites", isAuthenticated, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
guards,
|
guards,
|
||||||
certifications,
|
certifications,
|
||||||
vehicles,
|
vehicles,
|
||||||
|
customers,
|
||||||
sites,
|
sites,
|
||||||
shifts,
|
shifts,
|
||||||
shiftAssignments,
|
shiftAssignments,
|
||||||
@ -26,6 +27,8 @@ import {
|
|||||||
type InsertCertification,
|
type InsertCertification,
|
||||||
type Vehicle,
|
type Vehicle,
|
||||||
type InsertVehicle,
|
type InsertVehicle,
|
||||||
|
type Customer,
|
||||||
|
type InsertCustomer,
|
||||||
type Site,
|
type Site,
|
||||||
type InsertSite,
|
type InsertSite,
|
||||||
type Shift,
|
type Shift,
|
||||||
@ -85,6 +88,13 @@ export interface IStorage {
|
|||||||
updateServiceType(id: string, serviceType: Partial<InsertServiceType>): Promise<ServiceType | undefined>;
|
updateServiceType(id: string, serviceType: Partial<InsertServiceType>): Promise<ServiceType | undefined>;
|
||||||
deleteServiceType(id: string): Promise<ServiceType | undefined>;
|
deleteServiceType(id: string): Promise<ServiceType | undefined>;
|
||||||
|
|
||||||
|
// Customer operations
|
||||||
|
getAllCustomers(): Promise<Customer[]>;
|
||||||
|
getCustomer(id: string): Promise<Customer | undefined>;
|
||||||
|
createCustomer(customer: InsertCustomer): Promise<Customer>;
|
||||||
|
updateCustomer(id: string, customer: Partial<InsertCustomer>): Promise<Customer | undefined>;
|
||||||
|
deleteCustomer(id: string): Promise<Customer | undefined>;
|
||||||
|
|
||||||
// Site operations
|
// Site operations
|
||||||
getAllSites(): Promise<Site[]>;
|
getAllSites(): Promise<Site[]>;
|
||||||
getSite(id: string): Promise<Site | undefined>;
|
getSite(id: string): Promise<Site | undefined>;
|
||||||
@ -342,6 +352,35 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Customer operations
|
||||||
|
async getAllCustomers(): Promise<Customer[]> {
|
||||||
|
return await db.select().from(customers).orderBy(desc(customers.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomer(id: string): Promise<Customer | undefined> {
|
||||||
|
const [customer] = await db.select().from(customers).where(eq(customers.id, id));
|
||||||
|
return customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCustomer(customer: InsertCustomer): Promise<Customer> {
|
||||||
|
const [newCustomer] = await db.insert(customers).values(customer).returning();
|
||||||
|
return newCustomer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCustomer(id: string, customerData: Partial<InsertCustomer>): Promise<Customer | undefined> {
|
||||||
|
const [updated] = await db
|
||||||
|
.update(customers)
|
||||||
|
.set({ ...customerData, updatedAt: new Date() })
|
||||||
|
.where(eq(customers.id, id))
|
||||||
|
.returning();
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCustomer(id: string): Promise<Customer | undefined> {
|
||||||
|
const [deleted] = await db.delete(customers).where(eq(customers.id, id)).returning();
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
// Site operations
|
// Site operations
|
||||||
async getAllSites(): Promise<Site[]> {
|
async getAllSites(): Promise<Site[]> {
|
||||||
return await db.select().from(sites);
|
return await db.select().from(sites);
|
||||||
|
|||||||
@ -200,13 +200,35 @@ export const serviceTypes = pgTable("service_types", {
|
|||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
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 =============
|
// ============= SITES & CONTRACTS =============
|
||||||
|
|
||||||
export const sites = pgTable("sites", {
|
export const sites = pgTable("sites", {
|
||||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
address: varchar("address").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
|
location: locationEnum("location").notNull().default("roccapiemonte"), // Sede gestionale
|
||||||
|
|
||||||
// Service requirements
|
// Service requirements
|
||||||
@ -478,7 +500,6 @@ export const usersRelations = relations(users, ({ one, many }) => ({
|
|||||||
fields: [users.id],
|
fields: [users.id],
|
||||||
references: [guards.userId],
|
references: [guards.userId],
|
||||||
}),
|
}),
|
||||||
managedSites: many(sites),
|
|
||||||
notifications: many(notifications),
|
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 }) => ({
|
export const sitesRelations = relations(sites, ({ one, many }) => ({
|
||||||
client: one(users, {
|
customer: one(customers, {
|
||||||
fields: [sites.clientId],
|
fields: [sites.customerId],
|
||||||
references: [users.id],
|
references: [customers.id],
|
||||||
}),
|
}),
|
||||||
shifts: many(shifts),
|
shifts: many(shifts),
|
||||||
preferences: many(sitePreferences),
|
preferences: many(sitePreferences),
|
||||||
@ -680,6 +705,12 @@ export const insertServiceTypeSchema = createInsertSchema(serviceTypes).omit({
|
|||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const insertCustomerSchema = createInsertSchema(customers).omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
});
|
||||||
|
|
||||||
export const insertSiteSchema = createInsertSchema(sites).omit({
|
export const insertSiteSchema = createInsertSchema(sites).omit({
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
@ -794,6 +825,9 @@ export type Vehicle = typeof vehicles.$inferSelect;
|
|||||||
export type InsertServiceType = z.infer<typeof insertServiceTypeSchema>;
|
export type InsertServiceType = z.infer<typeof insertServiceTypeSchema>;
|
||||||
export type ServiceType = typeof serviceTypes.$inferSelect;
|
export type ServiceType = typeof serviceTypes.$inferSelect;
|
||||||
|
|
||||||
|
export type InsertCustomer = z.infer<typeof insertCustomerSchema>;
|
||||||
|
export type Customer = typeof customers.$inferSelect;
|
||||||
|
|
||||||
export type InsertSite = z.infer<typeof insertSiteSchema>;
|
export type InsertSite = z.infer<typeof insertSiteSchema>;
|
||||||
export type Site = typeof sites.$inferSelect;
|
export type Site = typeof sites.$inferSelect;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user