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 {