feat(contacts): add contacts page with per-store management and supplier email field

- New contacts table (group_id, name, role, phone, email, notes) per store
- Suppliers table: add email field with form and card display
- Page /contacts: two-column layout (suppliers read-only / free contacts CRUD)
- Permissions: read all roles, create/edit/delete admin + directeur + manager
- Admin can filter contacts by store; other roles see their own stores only
- Migration SQL: 20260515_add_contacts_and_supplier_email.sql

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Sonnet 4.6 committed 2026-05-15 10:44:39 +02:00
1 parent 40cf456a64
commit 805664dc32
8 files changed
+736 -6

No files matched your search

+71
View File
@@ -119,6 +119,7 @@ import {
insertUtilitiesSchema,
insertAvoirSchema,
insertReconciliationCommentSchema,
insertContactSchema,
users, groups, userGroups, suppliers, orders, deliveries, publicities, publicityParticipations,
customerOrders, nocodbConfig, dlcProducts, tasks, invoiceVerificationCache, dashboardMessages, webhookBapConfig,
utilities,
@@ -1191,6 +1192,76 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// Contacts routes
app.get('/api/contacts', isAuthenticated, async (req: any, res) => {
try {
const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);
if (!user) return res.status(404).json({ message: "User not found" });
let groupIds: number[] | undefined;
if (user.role !== 'admin') {
groupIds = (user.userGroups || []).map((ug: any) => ug.groupId);
} else if (req.query.groupId) {
groupIds = [parseInt(req.query.groupId as string)];
}
const result = await storage.getContacts(groupIds);
res.json(result);
} catch (error) {
console.error("Error fetching contacts:", error);
res.status(500).json({ message: "Failed to fetch contacts" });
}
});
app.post('/api/contacts', isAuthenticated, async (req: any, res) => {
try {
const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id);
if (!user || !['admin', 'directeur', 'manager'].includes(user.role)) {
return res.status(403).json({ message: "Insufficient permissions" });
}
const data = insertContactSchema.parse(req.body);
const contact = await storage.createContact(data);
res.json(contact);
} catch (error: any) {
if (error.name === 'ZodError') {
return res.status(400).json({ message: "Validation failed", errors: error.errors });
}
console.error("Error creating contact:", error);
res.status(500).json({ message: "Failed to create contact" });
}
});
app.put('/api/contacts/:id', isAuthenticated, async (req: any, res) => {
try {
const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id);
if (!user || !['admin', 'directeur', 'manager'].includes(user.role)) {
return res.status(403).json({ message: "Insufficient permissions" });
}
const id = parseInt(req.params.id);
const data = insertContactSchema.partial().parse(req.body);
const contact = await storage.updateContact(id, data);
res.json(contact);
} catch (error) {
console.error("Error updating contact:", error);
res.status(500).json({ message: "Failed to update contact" });
}
});
app.delete('/api/contacts/:id', isAuthenticated, async (req: any, res) => {
try {
const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id);
if (!user || !['admin', 'directeur', 'manager'].includes(user.role)) {
return res.status(403).json({ message: "Insufficient permissions" });
}
const id = parseInt(req.params.id);
await storage.deleteContact(id);
res.json({ message: "Contact deleted successfully" });
} catch (error) {
console.error("Error deleting contact:", error);
res.status(500).json({ message: "Failed to delete contact" });
}
});
// Orders routes
app.get('/api/orders', isAuthenticated, async (req: any, res) => {
try {
+75
View File
@@ -78,6 +78,9 @@ import {
type Avoir,
type InsertAvoir,
type AvoirWithRelations,
contacts,
type Contact,
type InsertContact,
} from "@shared/schema";
import { db } from "./db";
import { eq, and, inArray, desc, sql, gte, lte, lt, gt, or, isNull, isNotNull, asc, ne } from "drizzle-orm";
@@ -109,6 +112,12 @@ export interface IStorage {
updateSupplier(id: number, supplier: Partial<InsertSupplier>): Promise<Supplier>;
deleteSupplier(id: number): Promise<void>;
// Contact operations
getContacts(groupIds?: number[]): Promise<Contact[]>;
createContact(contact: InsertContact): Promise<Contact>;
updateContact(id: number, contact: Partial<InsertContact>): Promise<Contact>;
deleteContact(id: number): Promise<void>;
// Order operations
getOrders(groupIds?: number[]): Promise<OrderWithRelations[]>;
getOrdersByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise<OrderWithRelations[]>;
@@ -454,6 +463,32 @@ export class DatabaseStorage implements IStorage {
await db.delete(suppliers).where(eq(suppliers.id, id));
}
// Contact operations
async getContacts(groupIds?: number[]): Promise<Contact[]> {
if (groupIds && groupIds.length > 0) {
return await db.select().from(contacts).where(inArray(contacts.groupId, groupIds)).orderBy(contacts.name);
}
return await db.select().from(contacts).orderBy(contacts.name);
}
async createContact(contactData: InsertContact): Promise<Contact> {
const [contact] = await db.insert(contacts).values(contactData).returning();
return contact;
}
async updateContact(id: number, contactData: Partial<InsertContact>): Promise<Contact> {
const [contact] = await db
.update(contacts)
.set({ ...contactData, updatedAt: new Date() })
.where(eq(contacts.id, id))
.returning();
return contact;
}
async deleteContact(id: number): Promise<void> {
await db.delete(contacts).where(eq(contacts.id, id));
}
// Order operations
async getOrders(groupIds?: number[]): Promise<OrderWithRelations[]> {
let query = db
@@ -3330,6 +3365,7 @@ export class MemStorage implements IStorage {
private avoirs = new Map<number, Avoir>();
private savTickets = new Map<number, SavTicket>();
private invoiceVerificationCache = new Map<string, InvoiceVerificationCache>();
private contactsMap = new Map<number, Contact>();
private idCounters = {
group: 1,
@@ -3344,6 +3380,7 @@ export class MemStorage implements IStorage {
avoir: 1,
savTicket: 1,
reconciliationComment: 1,
contact: 1,
};
constructor() {
@@ -3562,6 +3599,44 @@ export class MemStorage implements IStorage {
this.suppliers.delete(id);
}
// Contact operations
async getContacts(groupIds?: number[]): Promise<Contact[]> {
const all = Array.from(this.contactsMap.values());
if (groupIds && groupIds.length > 0) {
return all.filter(c => groupIds.includes(c.groupId));
}
return all;
}
async createContact(contactData: InsertContact): Promise<Contact> {
const id = this.idCounters.contact++;
const contact: Contact = {
id,
groupId: contactData.groupId,
name: contactData.name,
role: contactData.role || null,
phone: contactData.phone || null,
email: contactData.email || null,
notes: contactData.notes || null,
createdAt: new Date(),
updatedAt: new Date(),
};
this.contactsMap.set(id, contact);
return contact;
}
async updateContact(id: number, contactData: Partial<InsertContact>): Promise<Contact> {
const existing = this.contactsMap.get(id);
if (!existing) throw new Error('Contact not found');
const updated = { ...existing, ...contactData, updatedAt: new Date() };
this.contactsMap.set(id, updated);
return updated;
}
async deleteContact(id: number): Promise<void> {
this.contactsMap.delete(id);
}
// NocoDB Configuration methods
async getNocodbConfigs(): Promise<NocodbConfig[]> {
return []; // Empty array for development