import { users, groups, suppliers, orders, deliveries, userGroups, publicities, publicityParticipations, customerOrders, dlcProducts, tasks, type User, type UpsertUser, type Group, type InsertGroup, type Supplier, type InsertSupplier, type Order, type InsertOrder, type Delivery, type InsertDelivery, type UserGroup, type InsertUserGroup, type OrderWithRelations, type DeliveryWithRelations, type UserWithGroups, type Publicity, type InsertPublicity, type PublicityParticipation, type InsertPublicityParticipation, type PublicityWithRelations, nocodbConfig, type NocodbConfig, type InsertNocodbConfig, type CustomerOrder, type InsertCustomerOrder, type CustomerOrderWithRelations, type DlcProduct, type InsertDlcProduct, type DlcProductFrontend, type InsertDlcProductFrontend, type DlcProductWithRelations, type Task, type InsertTask, } from "@shared/schema"; import { db } from "./db"; import { eq, and, inArray, desc, sql, gte, lte } from "drizzle-orm"; export interface IStorage { // User operations - supports both Replit Auth and local auth getUser(id: string): Promise; getUserByEmail(email: string): Promise; getUserByUsername(username: string): Promise; upsertUser(user: UpsertUser): Promise; getUserWithGroups(id: string): Promise; getUsers(): Promise; createUser(user: UpsertUser): Promise; updateUser(id: string, user: Partial): Promise; deleteUser(id: string): Promise; // Group operations getGroups(): Promise; getGroup(id: number): Promise; createGroup(group: InsertGroup): Promise; updateGroup(id: number, group: Partial): Promise; deleteGroup(id: number): Promise; // Supplier operations getSuppliers(): Promise; createSupplier(supplier: InsertSupplier): Promise; updateSupplier(id: number, supplier: Partial): Promise; deleteSupplier(id: number): Promise; // Order operations getOrders(groupIds?: number[]): Promise; getOrdersByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise; getOrder(id: number): Promise; createOrder(order: InsertOrder): Promise; updateOrder(id: number, order: Partial): Promise; deleteOrder(id: number): Promise; // Delivery operations getDeliveries(groupIds?: number[]): Promise; getDeliveriesByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise; getDelivery(id: number): Promise; createDelivery(delivery: InsertDelivery): Promise; updateDelivery(id: number, delivery: Partial): Promise; deleteDelivery(id: number): Promise; validateDelivery(id: number, blData?: { blNumber: string; blAmount: number }): Promise; // User-Group operations getUserGroups(userId: string): Promise; assignUserToGroup(userGroup: InsertUserGroup): Promise; removeUserFromGroup(userId: string, groupId: number): Promise; // Statistics getMonthlyStats(year: number, month: number, groupIds?: number[]): Promise<{ ordersCount: number; deliveriesCount: number; pendingOrdersCount: number; averageDeliveryTime: number; totalPalettes: number; totalPackages: number; }>; // Publicity operations getPublicities(year?: number, groupIds?: number[]): Promise; getPublicity(id: number): Promise; createPublicity(publicity: InsertPublicity): Promise; updatePublicity(id: number, publicity: Partial): Promise; deletePublicity(id: number): Promise; // Publicity participation operations getPublicityParticipations(publicityId: number): Promise; setPublicityParticipations(publicityId: number, groupIds: number[]): Promise; // NocoDB Configuration operations getNocodbConfigs(): Promise; getNocodbConfig(id: number): Promise; createNocodbConfig(config: InsertNocodbConfig): Promise; updateNocodbConfig(id: number, config: Partial): Promise; deleteNocodbConfig(id: number): Promise; // Customer Order operations getCustomerOrders(groupIds?: number[]): Promise; getCustomerOrder(id: number): Promise; createCustomerOrder(customerOrder: InsertCustomerOrder): Promise; updateCustomerOrder(id: number, customerOrder: Partial): Promise; deleteCustomerOrder(id: number): Promise; // DLC Product operations getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; }): Promise; getDlcProduct(id: number): Promise; createDlcProduct(dlcProduct: InsertDlcProductFrontend): Promise; updateDlcProduct(id: number, dlcProduct: Partial): Promise; deleteDlcProduct(id: number): Promise; validateDlcProduct(id: number, validatedBy: string): Promise; getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }>; // Task operations getTasks(groupIds?: number[]): Promise; getTasksByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise; getTask(id: number): Promise; createTask(task: InsertTask): Promise; updateTask(id: number, task: Partial): Promise; deleteTask(id: number): Promise; completeTask(id: number, completedBy?: string): Promise; } export class DatabaseStorage implements IStorage { // User operations async getUser(id: string): Promise { const [user] = await db.select().from(users).where(eq(users.id, id)); return user; } async getUserByEmail(email: string): Promise { const [user] = await db.select().from(users).where(eq(users.email, email)); return user; } async getUserByUsername(username: string): Promise { const [user] = await db.select().from(users).where(eq(users.username, username)); return user; } async upsertUser(userData: UpsertUser): Promise { const [user] = await db .insert(users) .values(userData) .onConflictDoUpdate({ target: users.id, set: { ...userData, updatedAt: new Date(), }, }) .returning(); return user; } async getUserWithGroups(id: string): Promise { // Get the user first const user = await this.getUser(id); if (!user) { return undefined; } // Fixed: Use manual SQL to avoid userGroups.createdAt column that doesn't exist try { const result = await db.execute(sql` SELECT ug.user_id, ug.group_id, g.id as group_id_ref, g.name as group_name, g.color as group_color, g.nocodb_config_id, g.nocodb_table_id, g.nocodb_table_name, g.invoice_column_name, g.created_at as group_created_at, g.updated_at as group_updated_at FROM user_groups ug INNER JOIN groups g ON ug.group_id = g.id WHERE ug.user_id = ${id} `); const userGroupsData = result.rows.map((row: any) => ({ userId: row.user_id, groupId: row.group_id, createdAt: null, // Avoid the problematic column group: { id: row.group_id_ref, name: row.group_name, color: row.group_color, nocodbConfigId: row.nocodb_config_id, nocodbTableId: row.nocodb_table_id, nocodbTableName: row.nocodb_table_name, invoiceColumnName: row.invoice_column_name, createdAt: row.group_created_at, updatedAt: row.group_updated_at, } })); return { ...user, userGroups: userGroupsData, }; } catch (error) { console.error('❌ getUserWithGroups error:', error); // Fallback: return user with empty groups return { ...user, userGroups: [], }; } } async getUsers(): Promise { return await db.select().from(users); } async getUsersWithRoles(): Promise { const baseUsers = await this.getUsers(); const usersWithRoles = await Promise.all( baseUsers.map(async (user) => { const userWithRoles = await this.getUserWithRoles(user.id); return userWithRoles || { ...user, userRoles: [] }; }) ); return usersWithRoles; } async getUsersWithRolesAndGroups(): Promise<(UserWithRoles & { userGroups: any[] })[]> { const baseUsers = await this.getUsers(); const usersWithRolesAndGroups = await Promise.all( baseUsers.map(async (user) => { const userWithRoles = await this.getUserWithRoles(user.id); const userWithGroups = await this.getUserWithGroups(user.id); return { ...user, userRoles: userWithRoles?.userRoles || [], userGroups: userWithGroups?.userGroups || [], roles: userWithRoles?.userRoles?.map(ur => ur.role) || [] }; }) ); return usersWithRolesAndGroups; } async createUser(userData: UpsertUser): Promise { try { const [user] = await db .insert(users) .values({ ...userData, createdAt: new Date(), updatedAt: new Date(), }) .returning(); return user; } catch (error) { console.error('❌ Storage createUser error:', error); throw error; } } async updateUser(id: string, userData: Partial): Promise { try { const [user] = await db .update(users) .set({ ...userData, updatedAt: new Date(), }) .where(eq(users.id, id)) .returning(); return user; } catch (error) { console.error('❌ Storage updateUser error:', error); throw error; } } async deleteUser(id: string): Promise { // Use a transaction to ensure atomicity await db.transaction(async (tx: typeof db) => { // Find admin_local or fallback to any admin user to transfer ownership to let adminUser = await tx .select({ id: users.id }) .from(users) .where(eq(users.id, "admin_local")) .limit(1); // If admin_local doesn't exist, find any admin user if (adminUser.length === 0) { adminUser = await tx .select({ id: users.id }) .from(users) .where(eq(users.role, "admin")) .limit(1); } if (adminUser.length === 0) { throw new Error("Cannot delete user: No admin user found to transfer ownership to"); } const adminUserId = adminUser[0].id; // Update customer orders await tx .update(customerOrders) .set({ createdBy: adminUserId }) .where(eq(customerOrders.createdBy, id)); // Update orders await tx .update(orders) .set({ createdBy: adminUserId }) .where(eq(orders.createdBy, id)); // Update deliveries await tx .update(deliveries) .set({ createdBy: adminUserId }) .where(eq(deliveries.createdBy, id)); // Update publicities await tx .update(publicities) .set({ createdBy: adminUserId }) .where(eq(publicities.createdBy, id)); // Update DLC products await tx .update(dlcProducts) .set({ createdBy: adminUserId }) .where(eq(dlcProducts.createdBy, id)); // Update tasks await tx .update(tasks) .set({ createdBy: adminUserId }) .where(eq(tasks.createdBy, id)); // Update NocoDB configs await tx .update(nocodbConfig) .set({ createdBy: adminUserId }) .where(eq(nocodbConfig.createdBy, id)); // Update database backups if the table exists (production feature) try { await tx.execute(sql` UPDATE database_backups SET created_by = ${adminUserId} WHERE created_by = ${id} `); } catch (error) { // Table might not exist in development, ignore the error console.log('Note: database_backups table not found or accessible'); } // Update role assignments made BY this user (assignedBy field) await tx .update(userRoles) .set({ assignedBy: adminUserId }) .where(eq(userRoles.assignedBy, id)); // Delete user-group assignments await tx.delete(userGroups).where(eq(userGroups.userId, id)); // Delete user-role assignments TO this user await tx.delete(userRoles).where(eq(userRoles.userId, id)); // Finally delete the user await tx.delete(users).where(eq(users.id, id)); }); } // Group operations async getGroups(): Promise { return await db.select().from(groups).orderBy(groups.name); } async getGroup(id: number): Promise { const [group] = await db.select().from(groups).where(eq(groups.id, id)); return group; } async createGroup(group: InsertGroup): Promise { const [newGroup] = await db.insert(groups).values(group).returning(); return newGroup; } async updateGroup(id: number, group: Partial): Promise { const [updatedGroup] = await db .update(groups) .set({ ...group, updatedAt: new Date() }) .where(eq(groups.id, id)) .returning(); return updatedGroup; } async deleteGroup(id: number): Promise { await db.delete(groups).where(eq(groups.id, id)); } // Supplier operations async getSuppliers(dlcOnly: boolean = false): Promise { if (dlcOnly) { return await db.select().from(suppliers).where(eq(suppliers.hasDlc, true)).orderBy(suppliers.name); } return await db.select().from(suppliers).orderBy(suppliers.name); } async getSupplier(id: number): Promise { const [supplier] = await db.select().from(suppliers).where(eq(suppliers.id, id)); return supplier; } async createSupplier(supplier: InsertSupplier): Promise { const [newSupplier] = await db.insert(suppliers).values(supplier).returning(); return newSupplier; } async updateSupplier(id: number, supplier: Partial): Promise { const [updatedSupplier] = await db .update(suppliers) .set({ ...supplier, updatedAt: new Date() }) .where(eq(suppliers.id, id)) .returning(); return updatedSupplier; } async deleteSupplier(id: number): Promise { await db.delete(suppliers).where(eq(suppliers.id, id)); } // Order operations async getOrders(groupIds?: number[]): Promise { const ordersQuery = db.query.orders.findMany({ with: { supplier: true, group: true, creator: true, deliveries: true, }, orderBy: [desc(orders.createdAt)], where: groupIds ? inArray(orders.groupId, groupIds) : undefined, }); return await ordersQuery; } async getOrdersByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise { const whereCondition = groupIds ? and( gte(orders.plannedDate, startDate), lte(orders.plannedDate, endDate), inArray(orders.groupId, groupIds) ) : and( gte(orders.plannedDate, startDate), lte(orders.plannedDate, endDate) ); return await db.query.orders.findMany({ with: { supplier: true, group: true, creator: true, deliveries: true, }, where: whereCondition, orderBy: [desc(orders.createdAt)], }); } async getOrder(id: number): Promise { return await db.query.orders.findFirst({ where: eq(orders.id, id), with: { supplier: true, group: true, creator: true, deliveries: true, }, }); } async createOrder(order: InsertOrder): Promise { const [newOrder] = await db.insert(orders).values(order).returning(); return newOrder; } async updateOrder(id: number, order: Partial): Promise { const [updatedOrder] = await db .update(orders) .set({ ...order, updatedAt: new Date() }) .where(eq(orders.id, id)) .returning(); return updatedOrder; } async deleteOrder(id: number): Promise { await db.delete(orders).where(eq(orders.id, id)); } // Delivery operations async getDeliveries(groupIds?: number[]): Promise { const results = await db.query.deliveries.findMany({ with: { supplier: true, group: { columns: { id: true, name: true, color: true, webhookUrl: true, } }, creator: true, order: true, }, orderBy: [desc(deliveries.createdAt)], where: groupIds ? inArray(deliveries.groupId, groupIds) : undefined, }); return results.map((result: any) => ({ ...result, order: result.order || undefined, })); } async getDeliveriesByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise { const whereCondition = groupIds ? and( gte(deliveries.scheduledDate, startDate), lte(deliveries.scheduledDate, endDate), inArray(deliveries.groupId, groupIds) ) : and( gte(deliveries.scheduledDate, startDate), lte(deliveries.scheduledDate, endDate) ); const results = await db.query.deliveries.findMany({ with: { supplier: true, group: { columns: { id: true, name: true, color: true, webhookUrl: true, } }, creator: true, order: true, }, where: whereCondition, orderBy: [desc(deliveries.createdAt)], }); return results.map((result: any) => ({ ...result, order: result.order || undefined, })); } async getDelivery(id: number): Promise { const result = await db.query.deliveries.findFirst({ where: eq(deliveries.id, id), with: { supplier: true, group: { columns: { id: true, name: true, color: true, webhookUrl: true, } }, creator: true, order: true, }, }); if (!result) return undefined; return { ...result, order: result.order || undefined, }; } async createDelivery(delivery: InsertDelivery): Promise { const [newDelivery] = await db.insert(deliveries).values(delivery).returning(); // Si la livraison est liée à une commande, mettre à jour le statut de la commande if (newDelivery.orderId) { await db .update(orders) .set({ status: 'planned' }) .where(eq(orders.id, newDelivery.orderId)); } return newDelivery; } async updateDelivery(id: number, delivery: Partial): Promise { const currentDelivery = await this.getDelivery(id); if (!currentDelivery) throw new Error("Delivery not found"); const [updatedDelivery] = await db .update(deliveries) .set({ ...delivery, updatedAt: new Date() }) .where(eq(deliveries.id, id)) .returning(); // Si la livraison est marquée comme livrée et qu'elle est liée à une commande if (delivery.status === 'delivered' && currentDelivery.orderId) { console.log('🔄 Delivery marked as delivered, updating linked order status'); await db .update(orders) .set({ status: 'delivered', updatedAt: new Date() }) .where(eq(orders.id, currentDelivery.orderId)); } return updatedDelivery; } async deleteDelivery(id: number): Promise { // Récupérer la livraison avant suppression pour connaître la commande liée const delivery = await this.getDelivery(id); // Supprimer la livraison await db.delete(deliveries).where(eq(deliveries.id, id)); // Si la livraison était liée à une commande, gérer le statut de la commande if (delivery?.orderId) { // Vérifier s'il reste d'autres livraisons liées à cette commande const remainingDeliveries = await db .select() .from(deliveries) .where(eq(deliveries.orderId, delivery.orderId)); if (remainingDeliveries.length === 0) { // Plus aucune livraison liée : remettre la commande en "pending" await db .update(orders) .set({ status: 'pending', updatedAt: new Date() }) .where(eq(orders.id, delivery.orderId)); } } } async validateDelivery(id: number, blData?: { blNumber: string; blAmount: number }): Promise { const delivery = await this.getDelivery(id); if (!delivery) throw new Error("Delivery not found"); const now = new Date(); // Prepare update data const updateData: any = { status: "delivered", deliveredDate: now, validatedAt: now, updatedAt: now }; // Add BL data if provided if (blData && blData.blNumber && blData.blAmount !== undefined) { updateData.blNumber = blData.blNumber; updateData.blAmount = blData.blAmount.toString(); } // Update delivery status and set delivered date await db .update(deliveries) .set(updateData) .where(eq(deliveries.id, id)); // Update linked order status if exists if (delivery.orderId) { await db .update(orders) .set({ status: "delivered", updatedAt: now }) .where(eq(orders.id, delivery.orderId)); } } // User-Group operations async getUserGroups(userId: string): Promise { return await db.select().from(userGroups).where(eq(userGroups.userId, userId)); } async assignUserToGroup(userGroup: InsertUserGroup): Promise { try { // Try with full schema first (development) const [newUserGroup] = await db.insert(userGroups).values(userGroup).returning(); return newUserGroup; } catch (error: any) { // If created_at column doesn't exist (production), use raw SQL if (error?.code === '42703' && error?.message?.includes('created_at')) { console.log('⚠️ Production mode: user_groups table missing created_at column, using raw SQL'); // Use raw SQL to insert without created_at column await db.execute(sql` INSERT INTO user_groups (user_id, group_id) VALUES (${userGroup.userId}, ${userGroup.groupId}) ON CONFLICT DO NOTHING `); // Return a UserGroup object with null created_at return { userId: userGroup.userId, groupId: userGroup.groupId, createdAt: null }; } throw error; } } async removeUserFromGroup(userId: string, groupId: number): Promise { await db.delete(userGroups).where( and( eq(userGroups.userId, userId), eq(userGroups.groupId, groupId) ) ); } // Statistics async getMonthlyStats(year: number, month: number, groupIds?: number[]): Promise<{ ordersCount: number; deliveriesCount: number; pendingOrdersCount: number; averageDeliveryTime: number; totalPalettes: number; totalPackages: number; }> { const startDate = `${year}-${month.toString().padStart(2, '0')}-01`; const endDate = `${year}-${month.toString().padStart(2, '0')}-31`; // Build where conditions const orderWhere = groupIds ? and( gte(orders.plannedDate, startDate), lte(orders.plannedDate, endDate), inArray(orders.groupId, groupIds) ) : and( gte(orders.plannedDate, startDate), lte(orders.plannedDate, endDate) ); const deliveryWhere = groupIds ? and( gte(deliveries.scheduledDate, startDate), lte(deliveries.scheduledDate, endDate), inArray(deliveries.groupId, groupIds) ) : and( gte(deliveries.scheduledDate, startDate), lte(deliveries.scheduledDate, endDate) ); // Get counts and totals - only count palettes/packages from orders with delivered deliveries const [orderStats] = await db .select({ count: sql`count(*)`, pendingCount: sql`count(*) filter (where status = 'pending')`, totalPalettes: sql`sum(case when unit = 'palettes' and exists( select 1 from deliveries d where d.order_id = orders.id and d.status = 'delivered' ) then quantity else 0 end)`, totalPackages: sql`sum(case when unit = 'colis' and exists( select 1 from deliveries d where d.order_id = orders.id and d.status = 'delivered' ) then quantity else 0 end)`, }) .from(orders) .where(orderWhere); const [deliveryStats] = await db .select({ count: sql`count(*)`, totalPalettes: sql`sum(case when unit = 'palettes' and status = 'delivered' then quantity else 0 end)`, totalPackages: sql`sum(case when unit = 'colis' and status = 'delivered' then quantity else 0 end)`, }) .from(deliveries) .where(deliveryWhere); // Calculate average delivery time const deliveredOrders = await db .select({ plannedDate: orders.plannedDate, deliveredDate: deliveries.deliveredDate, }) .from(orders) .innerJoin(deliveries, eq(orders.id, deliveries.orderId)) .where( and( orderWhere, eq(deliveries.status, 'delivered') ) ); let averageDeliveryTime = 0; if (deliveredOrders.length > 0) { const totalDelayDays = deliveredOrders.reduce((sum: any, order: any) => { if (order.deliveredDate && order.plannedDate) { const planned = new Date(order.plannedDate); const delivered = new Date(order.deliveredDate); const diffTime = delivered.getTime() - planned.getTime(); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return sum + diffDays; } return sum; }, 0); averageDeliveryTime = totalDelayDays / deliveredOrders.length; } return { ordersCount: Number(orderStats?.count || 0), deliveriesCount: Number(deliveryStats?.count || 0), pendingOrdersCount: Number(orderStats?.pendingCount || 0), averageDeliveryTime: Math.round(averageDeliveryTime * 10) / 10, totalPalettes: Number((orderStats?.totalPalettes || 0)) + Number((deliveryStats?.totalPalettes || 0)), totalPackages: Number((orderStats?.totalPackages || 0)) + Number((deliveryStats?.totalPackages || 0)), }; } // Publicity operations async getPublicities(year?: number, groupIds?: number[]): Promise { console.log('🔍 getPublicities called with:', { year, groupIds }); const query = db.select({ id: publicities.id, pubNumber: publicities.pubNumber, designation: publicities.designation, startDate: publicities.startDate, endDate: publicities.endDate, year: publicities.year, createdBy: publicities.createdBy, createdAt: publicities.createdAt, updatedAt: publicities.updatedAt, creator: { id: users.id, username: users.username, email: users.email, firstName: users.firstName, lastName: users.lastName, role: users.role } }) .from(publicities) .innerJoin(users, eq(publicities.createdBy, users.id)) .orderBy(desc(publicities.createdAt)); if (year) { query.where(eq(publicities.year, year)); } const results = await query; console.log('📢 Raw publicities found:', results.length); // Récupérer les participations séparément const publicityIds = results.map((pub: any) => pub.id); const participations = publicityIds.length > 0 ? await db.select({ publicityId: publicityParticipations.publicityId, groupId: publicityParticipations.groupId, group: { id: groups.id, name: groups.name, color: groups.color } }) .from(publicityParticipations) .innerJoin(groups, eq(publicityParticipations.groupId, groups.id)) .where(inArray(publicityParticipations.publicityId, publicityIds)) : []; console.log('🏪 Participations found:', participations.length); // Associer les participations aux publicités const finalResult = results.map((pub: any) => ({ ...pub, participations: participations .filter((p: any) => p.publicityId === pub.id) .map((p: any) => ({ publicityId: p.publicityId, groupId: p.groupId, group: p.group, createdAt: new Date() })) })); // Filter by groupIds if provided (for specific store selection or non-admin users) let filteredResult = finalResult; if (groupIds && groupIds.length > 0) { filteredResult = finalResult.filter(pub => pub.participations.some((p: any) => groupIds.includes(p.groupId)) ); console.log('🎯 Filtered publicities by groupIds:', { original: finalResult.length, filtered: filteredResult.length, groupIds }); } console.log('✅ Final publicities with participations:', filteredResult.length); if (filteredResult.length > 0) { console.log('📄 Sample publicity:', { id: filteredResult[0].id, pubNumber: filteredResult[0].pubNumber, participations: filteredResult[0].participations.map((p: any) => ({ groupId: p.groupId, groupName: p.group?.name })) }); } return filteredResult as PublicityWithRelations[]; } async getPublicity(id: number): Promise { const publicity = await db.select({ id: publicities.id, pubNumber: publicities.pubNumber, designation: publicities.designation, startDate: publicities.startDate, endDate: publicities.endDate, year: publicities.year, createdBy: publicities.createdBy, createdAt: publicities.createdAt, updatedAt: publicities.updatedAt, creator: { id: users.id, username: users.username, email: users.email, firstName: users.firstName, lastName: users.lastName, role: users.role } }) .from(publicities) .innerJoin(users, eq(publicities.createdBy, users.id)) .where(eq(publicities.id, id)) .limit(1); if (publicity.length === 0) return undefined; const participations = await db.select({ publicityId: publicityParticipations.publicityId, groupId: publicityParticipations.groupId, group: { id: groups.id, name: groups.name, color: groups.color } }) .from(publicityParticipations) .innerJoin(groups, eq(publicityParticipations.groupId, groups.id)) .where(eq(publicityParticipations.publicityId, id)); return { ...publicity[0], participations: participations.map((p: any) => ({ publicityId: p.publicityId, groupId: p.groupId, group: p.group, createdAt: new Date() })) } as PublicityWithRelations; } async createPublicity(publicity: InsertPublicity): Promise { const [newPublicity] = await db.insert(publicities).values(publicity).returning(); return newPublicity; } async updatePublicity(id: number, publicity: Partial): Promise { const [updatedPublicity] = await db.update(publicities) .set({ ...publicity, updatedAt: new Date() }) .where(eq(publicities.id, id)) .returning(); return updatedPublicity; } async deletePublicity(id: number): Promise { await db.delete(publicities).where(eq(publicities.id, id)); } async getPublicityParticipations(publicityId: number): Promise { return await db.select() .from(publicityParticipations) .where(eq(publicityParticipations.publicityId, publicityId)); } async setPublicityParticipations(publicityId: number, groupIds: number[]): Promise { // Supprimer les participations existantes await db.delete(publicityParticipations) .where(eq(publicityParticipations.publicityId, publicityId)); // Ajouter les nouvelles participations if (groupIds.length > 0) { const participations = groupIds.map(groupId => ({ publicityId, groupId })); await db.insert(publicityParticipations).values(participations); } } // NocoDB Configuration operations async getNocodbConfigs(): Promise { return await db.select({ id: nocodbConfig.id, name: nocodbConfig.name, baseUrl: nocodbConfig.baseUrl, projectId: nocodbConfig.projectId, apiToken: nocodbConfig.apiToken, description: nocodbConfig.description, isActive: nocodbConfig.isActive, createdBy: nocodbConfig.createdBy, createdAt: nocodbConfig.createdAt, updatedAt: nocodbConfig.updatedAt, }).from(nocodbConfig).orderBy(desc(nocodbConfig.createdAt)); } async getNocodbConfig(id: number): Promise { const configs = await db.select({ id: nocodbConfig.id, name: nocodbConfig.name, baseUrl: nocodbConfig.baseUrl, projectId: nocodbConfig.projectId, apiToken: nocodbConfig.apiToken, description: nocodbConfig.description, isActive: nocodbConfig.isActive, createdBy: nocodbConfig.createdBy, createdAt: nocodbConfig.createdAt, updatedAt: nocodbConfig.updatedAt, }).from(nocodbConfig).where(eq(nocodbConfig.id, id)); return configs[0]; } async createNocodbConfig(config: InsertNocodbConfig): Promise { const [newConfig] = await db.insert(nocodbConfig).values(config).returning(); return newConfig; } async updateNocodbConfig(id: number, config: Partial): Promise { const [updatedConfig] = await db.update(nocodbConfig) .set({ ...config, updatedAt: new Date() }) .where(eq(nocodbConfig.id, id)) .returning(); return updatedConfig; } async deleteNocodbConfig(id: number): Promise { await db.delete(nocodbConfig).where(eq(nocodbConfig.id, id)); } // Customer Order operations async getCustomerOrders(groupIds?: number[]): Promise { try { let whereClause = ""; if (groupIds && groupIds.length > 0) { whereClause = `WHERE co.group_id IN (${groupIds.join(',')})`; } const result = await db.execute(sql` SELECT co.*, g.id as group_id_ref, g.name as group_name, g.color as group_color, u.id as creator_id_ref, u.username as creator_username, u.email as creator_email, u.first_name, u.last_name, u.role as creator_role, s.id as supplier_id_ref, s.name as supplier_name, s.contact as supplier_contact, s.phone as supplier_phone FROM customer_orders co LEFT JOIN groups g ON co.group_id = g.id LEFT JOIN users u ON co.created_by = u.id LEFT JOIN suppliers s ON co.supplier_id = s.id ${whereClause ? sql.raw(whereClause) : sql.raw('')} ORDER BY co.created_at DESC `); return result.rows.map((row: any) => ({ id: row.id as number, orderTaker: row.order_taker as string, customerName: row.customer_name as string, customerPhone: row.customer_phone as string, customerEmail: row.customer_email as string | null, productDesignation: row.product_designation as string, productReference: row.product_reference as string | null, gencode: row.gencode as string, quantity: row.quantity as number, supplierId: row.supplier_id as number, status: row.status as string, deposit: row.deposit as number, isPromotionalPrice: row.is_promotional_price as boolean | null, customerNotified: row.customer_notified as boolean | null, groupId: row.group_id as number, createdBy: row.created_by as string, createdAt: row.created_at as Date | null, updatedAt: row.updated_at as Date | null, group: { id: row.group_id_ref as number, name: row.group_name as string, color: row.group_color as string, createdAt: null, updatedAt: null, nocodbConfigId: null, nocodbTableId: null, nocodbTableName: null, invoiceColumnName: null, }, creator: { id: row.creator_id_ref as string, username: row.creator_username as string | null, email: row.creator_email as string | null, firstName: row.first_name as string | null, lastName: row.last_name as string | null, role: row.creator_role as string, }, supplier: { id: row.supplier_id_ref as number, name: row.supplier_name as string, contact: row.supplier_contact as string, phone: row.supplier_phone as string, }, })) as unknown as CustomerOrderWithRelations[]; } catch (error) { console.error("Error in getCustomerOrders:", error); return []; } } async getCustomerOrder(id: number): Promise { try { const result = await db.execute(sql` SELECT co.*, g.id as group_id_ref, g.name as group_name, g.color as group_color, u.id as creator_id_ref, u.username as creator_username, u.email as creator_email, u.first_name, u.last_name, u.role as creator_role FROM customer_orders co LEFT JOIN groups g ON co.group_id = g.id LEFT JOIN users u ON co.created_by = u.id WHERE co.id = ${id} `); if (result.rows.length === 0) return undefined; const row = result.rows[0]; return { id: row.id as number, orderTaker: row.order_taker as string, customerName: row.customer_name as string, customerPhone: row.customer_phone as string, customerEmail: row.customer_email as string | null, productDesignation: row.product_designation as string, productReference: row.product_reference as string | null, gencode: row.gencode as string, quantity: row.quantity as number, supplierId: row.supplier_id as number, status: row.status as string, deposit: row.deposit as number, isPromotionalPrice: row.is_promotional_price as boolean | null, customerNotified: row.customer_notified as boolean | null, groupId: row.group_id as number, createdBy: row.created_by as string, createdAt: row.created_at as Date | null, updatedAt: row.updated_at as Date | null, group: { id: row.group_id_ref as number, name: row.group_name as string, color: row.group_color as string, createdAt: null, updatedAt: null, nocodbConfigId: null, nocodbTableId: null, nocodbTableName: null, invoiceColumnName: null, }, creator: { id: row.creator_id_ref as string, username: row.creator_username as string | null, email: row.creator_email as string | null, firstName: row.first_name as string | null, lastName: row.last_name as string | null, role: row.creator_role as string, }, supplier: { id: row.supplier_id as number, name: row.supplier_name as string, contact: row.supplier_contact as string, phone: row.supplier_phone as string, }, } as unknown as CustomerOrderWithRelations; } catch (error) { console.error("Error in getCustomerOrder:", error); return undefined; } } async createCustomerOrder(customerOrderData: InsertCustomerOrder): Promise { const insertData = { ...customerOrderData, createdAt: new Date(), updatedAt: new Date(), }; console.log("🔍 STORAGE DEBUG - Customer Order insertData:", insertData); console.log("🔍 STORAGE DEBUG - Customer Order insertData.groupId:", insertData.groupId, typeof insertData.groupId); const [customerOrder] = await db .insert(customerOrders) .values(insertData) .returning(); console.log("🔍 STORAGE DEBUG - Customer Order returned from DB:", customerOrder); console.log("🔍 STORAGE DEBUG - Customer Order returned groupId:", customerOrder.groupId, typeof customerOrder.groupId); return customerOrder; } async updateCustomerOrder(id: number, customerOrderData: Partial): Promise { const [customerOrder] = await db .update(customerOrders) .set({ ...customerOrderData, updatedAt: new Date(), }) .where(eq(customerOrders.id, id)) .returning(); return customerOrder; } async deleteCustomerOrder(id: number): Promise { await db.delete(customerOrders).where(eq(customerOrders.id, id)); } // ===== DLC PRODUCTS OPERATIONS ===== async getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; }): Promise { try { let whereClause = ""; const conditions: string[] = []; if (groupIds && groupIds.length > 0) { conditions.push(`dlc.group_id IN (${groupIds.join(',')})`); } if (filters?.status) { if (filters.status === 'expires_soon') { conditions.push(`dlc.status != 'valides' AND dlc.expiry_date <= NOW() + INTERVAL '15 days' AND dlc.expiry_date > NOW()`); } else if (filters.status === 'expires') { conditions.push(`dlc.status != 'valides' AND dlc.expiry_date <= NOW()`); } else if (filters.status === 'en_cours') { conditions.push(`dlc.status != 'valides' AND dlc.expiry_date > NOW() + INTERVAL '15 days'`); } else { conditions.push(`dlc.status = '${filters.status}'`); } } if (filters?.supplierId) { conditions.push(`dlc.supplier_id = ${filters.supplierId}`); } if (conditions.length > 0) { whereClause = `WHERE ${conditions.join(' AND ')}`; } const result = await db.execute(sql` SELECT dlc.*, g.id as group_id_ref, g.name as group_name, g.color as group_color, u.id as creator_id_ref, u.username as creator_username, u.email as creator_email, u.first_name, u.last_name, u.role as creator_role, v.id as validator_id_ref, v.username as validator_username, v.email as validator_email, v.first_name as validator_first_name, v.last_name as validator_last_name, s.id as supplier_id_ref, s.name as supplier_name, s.contact as supplier_contact, s.phone as supplier_phone FROM dlc_products dlc LEFT JOIN groups g ON dlc.group_id = g.id LEFT JOIN users u ON dlc.created_by = u.id LEFT JOIN users v ON dlc.validated_by = v.id LEFT JOIN suppliers s ON dlc.supplier_id = s.id ${whereClause ? sql.raw(whereClause) : sql.raw('')} ORDER BY dlc.expiry_date ASC, dlc.created_at DESC `); return result.rows.map((row: any) => ({ id: row.id as number, productName: row.product_name as string, gencode: row.gencode as string | null, dlcDate: row.expiry_date as Date, dateType: row.date_type as string, quantity: row.quantity as number, unit: row.unit as string, supplierId: row.supplier_id as number, location: row.location as string, status: row.status as string, notes: row.notes as string | null, alertThreshold: row.alert_threshold as number, validatedAt: row.validated_at as Date | null, validatedBy: row.validated_by as string | null, groupId: row.group_id as number, createdBy: row.created_by as string, createdAt: row.created_at as Date, updatedAt: row.updated_at as Date, group: { id: row.group_id_ref as number, name: row.group_name as string, color: row.group_color as string, createdAt: null, updatedAt: null, nocodbConfigId: null, nocodbTableId: null, nocodbTableName: null, invoiceColumnName: null, }, creator: { id: row.creator_id_ref as string, username: row.creator_username as string | null, email: row.creator_email as string | null, firstName: row.first_name as string | null, lastName: row.last_name as string | null, role: row.creator_role as string, }, validator: row.validator_id_ref ? { id: row.validator_id_ref as string, username: row.validator_username as string | null, email: row.validator_email as string | null, firstName: row.validator_first_name as string | null, lastName: row.validator_last_name as string | null, role: null, } : null, supplier: { id: row.supplier_id_ref as number, name: row.supplier_name as string, contact: row.supplier_contact as string, phone: row.supplier_phone as string, }, })) as unknown as DlcProductWithRelations[]; } catch (error) { console.error("Error in getDlcProducts:", error); return []; } } async getDlcProduct(id: number): Promise { try { const result = await db.execute(sql` SELECT dlc.*, g.id as group_id_ref, g.name as group_name, g.color as group_color, u.id as creator_id_ref, u.username as creator_username, u.email as creator_email, u.first_name, u.last_name, u.role as creator_role, v.id as validator_id_ref, v.username as validator_username, v.email as validator_email, v.first_name as validator_first_name, v.last_name as validator_last_name, s.id as supplier_id_ref, s.name as supplier_name, s.contact as supplier_contact, s.phone as supplier_phone FROM dlc_products dlc LEFT JOIN groups g ON dlc.group_id = g.id LEFT JOIN users u ON dlc.created_by = u.id LEFT JOIN users v ON dlc.validated_by = v.id LEFT JOIN suppliers s ON dlc.supplier_id = s.id WHERE dlc.id = ${id} `); if (result.rows.length === 0) return undefined; const row = result.rows[0]; return { id: row.id as number, productName: row.product_name as string, gencode: row.gencode as string | null, dlcDate: row.expiry_date as Date, dateType: row.date_type as string, quantity: row.quantity as number, unit: row.unit as string, supplierId: row.supplier_id as number, location: row.location as string, status: row.status as string, notes: row.notes as string | null, alertThreshold: row.alert_threshold as number, validatedAt: row.validated_at as Date | null, validatedBy: row.validated_by as string | null, groupId: row.group_id as number, createdBy: row.created_by as string, createdAt: row.created_at as Date, updatedAt: row.updated_at as Date, group: { id: row.group_id_ref as number, name: row.group_name as string, color: row.group_color as string, createdAt: null, updatedAt: null, nocodbConfigId: null, nocodbTableId: null, nocodbTableName: null, invoiceColumnName: null, }, creator: { id: row.creator_id_ref as string, username: row.creator_username as string | null, email: row.creator_email as string | null, firstName: row.first_name as string | null, lastName: row.last_name as string | null, role: row.creator_role as string, }, validator: row.validator_id_ref ? { id: row.validator_id_ref as string, username: row.validator_username as string | null, email: row.validator_email as string | null, firstName: row.validator_first_name as string | null, lastName: row.validator_last_name as string | null, role: null, } : null, supplier: { id: row.supplier_id_ref as number, name: row.supplier_name as string, contact: row.supplier_contact as string, phone: row.supplier_phone as string, }, } as unknown as DlcProductWithRelations; } catch (error) { console.error("Error in getDlcProduct:", error); return undefined; } } async createDlcProduct(dlcProductData: InsertDlcProductFrontend): Promise { // Convert dlcDate to expiryDate for Drizzle schema compatibility const { dlcDate, ...restData } = dlcProductData as any; const insertData = { ...restData, expiryDate: dlcDate, createdAt: new Date(), updatedAt: new Date(), }; console.log("🔍 STORAGE DEBUG - DLC insertData:", insertData); console.log("🔍 STORAGE DEBUG - DLC insertData.groupId:", insertData.groupId, typeof insertData.groupId); const [dlcProduct] = await db .insert(dlcProducts) .values(insertData) .returning(); console.log("🔍 STORAGE DEBUG - DLC returned from DB:", dlcProduct); console.log("🔍 STORAGE DEBUG - DLC returned groupId:", dlcProduct.groupId, typeof dlcProduct.groupId); // Return with dlcDate field for frontend compatibility const result = { ...dlcProduct, dlcDate: dlcProduct.expiryDate, } as any; console.log("🔍 STORAGE DEBUG - DLC final result:", result); return result; } async updateDlcProduct(id: number, dlcProductData: Partial): Promise { // Convert dlcDate to expiryDate for Drizzle schema compatibility const { dlcDate, ...restData } = dlcProductData as any; const updateData = { ...restData }; if (dlcDate) { updateData.expiryDate = dlcDate; } const [dlcProduct] = await db .update(dlcProducts) .set({ ...updateData, updatedAt: new Date(), }) .where(eq(dlcProducts.id, id)) .returning(); // Return with dlcDate field for frontend compatibility return { ...dlcProduct, dlcDate: dlcProduct.expiryDate, } as any; } async deleteDlcProduct(id: number): Promise { await db.delete(dlcProducts).where(eq(dlcProducts.id, id)); } async validateDlcProduct(id: number, validatedBy: string): Promise { const [dlcProduct] = await db .update(dlcProducts) .set({ status: 'valides', validatedAt: new Date(), validatedBy, updatedAt: new Date(), }) .where(eq(dlcProducts.id, id)) .returning(); // Return with dlcDate field for frontend compatibility return { ...dlcProduct, dlcDate: dlcProduct.expiryDate, } as any; } async getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }> { try { let whereClause = ""; if (groupIds && groupIds.length > 0) { whereClause = `WHERE group_id IN (${groupIds.join(',')})`; } const result = await db.execute(sql` SELECT COUNT(CASE WHEN status != 'valides' AND expiry_date > NOW() + INTERVAL '15 days' THEN 1 END) as active, COUNT(CASE WHEN status != 'valides' AND expiry_date <= NOW() + INTERVAL '15 days' AND expiry_date > NOW() THEN 1 END) as expiring_soon, COUNT(CASE WHEN status != 'valides' AND expiry_date <= NOW() THEN 1 END) as expired FROM dlc_products ${whereClause ? sql.raw(whereClause) : sql.raw('')} `); const row = result.rows[0] as any; return { active: parseInt(row.active) || 0, expiringSoon: parseInt(row.expiring_soon) || 0, expired: parseInt(row.expired) || 0, }; } catch (error) { console.error("Error in getDlcStats:", error); return { active: 0, expiringSoon: 0, expired: 0 }; } } // ===== ROLE MANAGEMENT OPERATIONS ===== // Role operations async getRoles(): Promise { return await db.select().from(roles).orderBy(roles.name); } async getRole(id: number): Promise { const [role] = await db.select().from(roles).where(eq(roles.id, id)); return role; } async createRole(roleData: InsertRole): Promise { const [role] = await db .insert(roles) .values({ ...roleData, createdAt: new Date(), updatedAt: new Date(), }) .returning(); return role; } async updateRole(id: number, roleData: Partial): Promise { const [role] = await db .update(roles) .set({ ...roleData, updatedAt: new Date(), }) .where(eq(roles.id, id)) .returning(); return role; } async deleteRole(id: number): Promise { // First remove all permissions from this role await db.delete(rolePermissions).where(eq(rolePermissions.roleId, id)); // Remove all user assignments to this role await db.delete(userRoles).where(eq(userRoles.roleId, id)); // Finally delete the role await db.delete(roles).where(eq(roles.id, id)); } async getRoleWithPermissions(id: number): Promise { const role = await this.getRole(id); if (!role) return undefined; const rolePerms = await db .select({ id: rolePermissions.roleId, permissionId: rolePermissions.permissionId, createdAt: rolePermissions.createdAt, permission: { id: permissions.id, name: permissions.name, displayName: permissions.displayName, description: permissions.description, category: permissions.category, action: permissions.action, resource: permissions.resource, isSystem: permissions.isSystem, createdAt: permissions.createdAt, }, }) .from(rolePermissions) .innerJoin(permissions, eq(rolePermissions.permissionId, permissions.id)) .where(eq(rolePermissions.roleId, id)); return { ...role, rolePermissions: rolePerms.map((rp: any) => ({ roleId: rp.id, permissionId: rp.permissionId, createdAt: rp.createdAt, permission: rp.permission, })), }; } // Permission operations async getPermissions(): Promise { console.log('🔍 DEV getPermissions() - Starting Drizzle query...'); const result = await db.select().from(permissions).orderBy(permissions.category, permissions.name); console.log('📊 DEV getPermissions() - Drizzle result:', result.length, 'permissions'); if (result.length === 0) { console.log('⚠️ DEV getPermissions() - NO PERMISSIONS FOUND IN DATABASE!'); return []; } // Log des catégories trouvées const categories = [...new Set(result.map((p: any) => p.category))]; console.log('🏷️ DEV getPermissions() - Categories found:', categories); // Log spécifique pour les permissions tâches const taskPerms = result.filter((p: any) => p.category === 'gestion_taches'); console.log('📋 DEV getPermissions() - Task permissions found:', taskPerms.length); if (taskPerms.length > 0) { taskPerms.forEach((p: any) => { console.log(` - ID: ${p.id}, Name: ${p.name}, DisplayName: "${p.displayName}", Category: ${p.category}`); }); } else { console.log('❌ DEV getPermissions() - NO TASK PERMISSIONS FOUND - This explains the issue!'); console.log('🔍 DEV getPermissions() - Sample permissions:', result.slice(0, 3).map((p: any) => ({ id: p.id, name: p.name, category: p.category }))); } console.log('✅ DEV getPermissions() - Returning', result.length, 'permissions'); return result; } async getPermission(id: number): Promise { const [permission] = await db.select().from(permissions).where(eq(permissions.id, id)); return permission; } async createPermission(permissionData: InsertPermission): Promise { const [permission] = await db .insert(permissions) .values({ ...permissionData, createdAt: new Date(), }) .returning(); return permission; } async updatePermission(id: number, permissionData: Partial): Promise { const [permission] = await db .update(permissions) .set(permissionData) .where(eq(permissions.id, id)) .returning(); return permission; } async deletePermission(id: number): Promise { // First remove this permission from all roles await db.delete(rolePermissions).where(eq(rolePermissions.permissionId, id)); // Then delete the permission await db.delete(permissions).where(eq(permissions.id, id)); } async getPermissionsByCategory(category: string): Promise { return await db .select() .from(permissions) .where(eq(permissions.category, category)) .orderBy(permissions.name); } // Role-Permission association operations async getRolePermissions(roleId: number): Promise { const results = await db .select({ roleId: rolePermissions.roleId, permissionId: rolePermissions.permissionId, createdAt: rolePermissions.createdAt, permission: { id: permissions.id, name: permissions.name, displayName: permissions.displayName, description: permissions.description, category: permissions.category, action: permissions.action, resource: permissions.resource, isSystem: permissions.isSystem, createdAt: permissions.createdAt } }) .from(rolePermissions) .innerJoin(permissions, eq(rolePermissions.permissionId, permissions.id)) .where(eq(rolePermissions.roleId, roleId)); return results; } async setRolePermissions(roleId: number, permissionIds: number[]): Promise { // First, remove existing permissions for this role await db.delete(rolePermissions).where(eq(rolePermissions.roleId, roleId)); // Then add new permissions if (permissionIds.length > 0) { const values = permissionIds.map(permissionId => ({ roleId, permissionId, createdAt: new Date(), })); await db.insert(rolePermissions).values(values); } } async addPermissionToRole(roleId: number, permissionId: number): Promise { const [rolePermission] = await db .insert(rolePermissions) .values({ roleId, permissionId, createdAt: new Date(), }) .returning(); return rolePermission; } async removePermissionFromRole(roleId: number, permissionId: number): Promise { await db .delete(rolePermissions) .where( and( eq(rolePermissions.roleId, roleId), eq(rolePermissions.permissionId, permissionId) ) ); } // User-Role association operations async getUserRoles(userId: string): Promise { return await db .select() .from(userRoles) .where(eq(userRoles.userId, userId)); } async setUserRoles(userId: string, roleIds: number[], assignedBy: string): Promise { // First, remove existing roles for this user await db.delete(userRoles).where(eq(userRoles.userId, userId)); // Then add new role (only one role per user) if (roleIds.length > 0) { const roleId = roleIds[0]; // Take only the first role await db.insert(userRoles).values({ userId, roleId, assignedBy, assignedAt: new Date(), }); } } async addRoleToUser(userId: string, roleId: number, assignedBy: string): Promise { const [userRole] = await db .insert(userRoles) .values({ userId, roleId, assignedBy, assignedAt: new Date(), }) .returning(); return userRole; } async removeRoleFromUser(userId: string, roleId: number): Promise { await db .delete(userRoles) .where( and( eq(userRoles.userId, userId), eq(userRoles.roleId, roleId) ) ); } async getUserWithRoles(userId: string): Promise { const user = await this.getUser(userId); if (!user) return undefined; const userRoleData = await db .select({ userId: userRoles.userId, roleId: userRoles.roleId, assignedBy: userRoles.assignedBy, assignedAt: userRoles.assignedAt, role: { id: roles.id, name: roles.name, displayName: roles.displayName, description: roles.description, color: roles.color, isSystem: roles.isSystem, isActive: roles.isActive, createdAt: roles.createdAt, updatedAt: roles.updatedAt, }, }) .from(userRoles) .innerJoin(roles, eq(userRoles.roleId, roles.id)) .where(eq(userRoles.userId, userId)); // Debug user roles console.log(`📊 getUserWithRoles(${userId}):`, { userRoleDataLength: userRoleData.length }); return { ...user, userRoles: userRoleData, }; } // Permission checking operations async userHasPermission(userId: string, permissionName: string): Promise { const result = await db .select({ exists: sql`1`, }) .from(userRoles) .innerJoin(rolePermissions, eq(userRoles.roleId, rolePermissions.roleId)) .innerJoin(permissions, eq(rolePermissions.permissionId, permissions.id)) .where( and( eq(userRoles.userId, userId), eq(permissions.name, permissionName) ) ) .limit(1); return result.length > 0; } async userHasRole(userId: string, roleName: string): Promise { const result = await db .select({ exists: sql`1`, }) .from(userRoles) .innerJoin(roles, eq(userRoles.roleId, roles.id)) .where( and( eq(userRoles.userId, userId), eq(roles.name, roleName) ) ) .limit(1); return result.length > 0; } async getUserEffectivePermissions(userId: string): Promise { return await db .select({ id: permissions.id, name: permissions.name, displayName: permissions.displayName, description: permissions.description, category: permissions.category, action: permissions.action, resource: permissions.resource, isSystem: permissions.isSystem, createdAt: permissions.createdAt, }) .from(userRoles) .innerJoin(rolePermissions, eq(userRoles.roleId, rolePermissions.roleId)) .innerJoin(permissions, eq(rolePermissions.permissionId, permissions.id)) .where(eq(userRoles.userId, userId)) .groupBy(permissions.id) .orderBy(permissions.category, permissions.name); } // Task operations async getTasks(groupIds?: number[]): Promise { const tasksQuery = db.query.tasks.findMany({ with: { assignedUser: true, creator: true, group: true, }, orderBy: [desc(tasks.createdAt)], where: groupIds ? inArray(tasks.groupId, groupIds) : undefined, }); return await tasksQuery; } async getTasksByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise { const start = new Date(startDate); const end = new Date(endDate); const whereCondition = groupIds ? and( gte(tasks.dueDate, start), lte(tasks.dueDate, end), inArray(tasks.groupId, groupIds) ) : and( gte(tasks.dueDate, start), lte(tasks.dueDate, end) ); return await db.query.tasks.findMany({ with: { assignedUser: true, creator: true, group: true, }, where: whereCondition, orderBy: [desc(tasks.createdAt)], }); } async getTask(id: number): Promise { return await db.query.tasks.findFirst({ where: eq(tasks.id, id), with: { assignedUser: true, creator: true, group: true, }, }); } async createTask(task: InsertTask): Promise { const [newTask] = await db.insert(tasks).values(task).returning(); return newTask; } async updateTask(id: number, task: Partial): Promise { const [updatedTask] = await db .update(tasks) .set({ ...task, updatedAt: new Date() }) .where(eq(tasks.id, id)) .returning(); return updatedTask; } async deleteTask(id: number): Promise { await db.delete(tasks).where(eq(tasks.id, id)); } async completeTask(id: number, completedBy?: string): Promise { await db .update(tasks) .set({ status: 'completed', completedAt: new Date(), completedBy: completedBy, updatedAt: new Date() }) .where(eq(tasks.id, id)); } } // Simple in-memory storage for development class MemStorage implements IStorage { private users = new Map(); private groups = new Map(); private suppliers = new Map(); private orders = new Map(); private deliveries = new Map(); private tasks = new Map(); private publicities = new Map(); private publicityParticipations = new Array(); private dlcProducts = new Map(); private dlcIdCounter = 1; constructor() { // Create default admin user for development const adminUser: User = { id: 'admin_dev', username: 'admin', email: 'admin@dev.local', name: 'Admin User', firstName: 'Admin', lastName: 'User', profileImageUrl: null, password: 'admin.salt', // Simple format for development role: 'admin', passwordChanged: true, createdAt: new Date(), updatedAt: new Date() }; this.users.set('admin_dev', adminUser); this.users.set('admin', adminUser); // Also set by username for easy lookup // Add test groups with NocoDB configuration const testGroups = [ { id: 1, name: 'Magasin A', color: '#FF5733', webhookUrl: 'https://webhook.site/test-facture-magasin-a', nocodbConfigId: 1, nocodbTableName: 'factures_magasin_a', invoiceColumnName: 'reference_facture', nocodbBlColumnName: 'numero_bl', nocodbAmountColumnName: 'montant', nocodbSupplierColumnName: 'fournisseur', createdAt: new Date(), updatedAt: new Date() }, { id: 2, name: 'Magasin B', color: '#33FF57', webhookUrl: 'https://webhook.site/test-facture-magasin-b', nocodbConfigId: 1, nocodbTableName: 'factures_magasin_b', invoiceColumnName: 'ref_facture', nocodbBlColumnName: 'bl_number', nocodbAmountColumnName: 'amount', nocodbSupplierColumnName: 'supplier_name', createdAt: new Date(), updatedAt: new Date() }, { id: 3, name: 'Magasin C', color: '#3357FF', createdAt: new Date(), updatedAt: new Date() } ]; testGroups.forEach(group => this.groups.set(group.id, group as Group)); // Add test publicities const currentYear = new Date().getFullYear(); const testPublicities = [ { id: 1, pubNumber: 'PUB001', designation: 'Promotion Été 2025', startDate: '2025-08-01', endDate: '2025-08-31', year: currentYear, createdBy: 'admin_dev', createdAt: new Date(), updatedAt: new Date() }, { id: 2, pubNumber: 'PUB002', designation: 'Back to School', startDate: '2025-08-15', endDate: '2025-09-15', year: currentYear, createdBy: 'admin_dev', createdAt: new Date(), updatedAt: new Date() } ]; testPublicities.forEach(pub => this.publicities.set(pub.id, pub)); // Add test publicity participations this.publicityParticipations.push( { publicityId: 1, groupId: 1, createdAt: new Date() }, { publicityId: 1, groupId: 2, createdAt: new Date() }, { publicityId: 2, groupId: 2, createdAt: new Date() }, { publicityId: 2, groupId: 3, createdAt: new Date() } ); // Add test suppliers const testSuppliers = [ { id: 1, name: 'Fournisseur Test A', automaticReconciliation: false, createdAt: new Date(), updatedAt: new Date() }, { id: 2, name: 'Fournisseur Test B', automaticReconciliation: true, createdAt: new Date(), updatedAt: new Date() } ]; testSuppliers.forEach(supplier => this.suppliers.set(supplier.id, supplier as Supplier)); // Add test deliveries const testDeliveries = [ { id: 1, supplierId: 1, groupId: 1, blNumber: 'BL001', status: 'delivered', scheduledDate: '2025-08-10', deliveredDate: '2025-08-10', blAmount: '150.50', reconciled: false, createdBy: 'admin_dev', createdAt: new Date(), updatedAt: new Date() }, { id: 2, supplierId: 2, groupId: 2, blNumber: 'BL002', status: 'delivered', scheduledDate: '2025-08-11', deliveredDate: '2025-08-11', blAmount: '89.99', invoiceReference: 'FACT202', invoiceAmount: '89.99', reconciled: false, createdBy: 'admin_dev', createdAt: new Date(), updatedAt: new Date() }, { id: 3, supplierId: 1, groupId: 3, blNumber: 'BL003', status: 'delivered', scheduledDate: '2025-08-12', deliveredDate: '2025-08-12', blAmount: '234.75', reconciled: true, validatedAt: new Date(), createdBy: 'admin_dev', createdAt: new Date(), updatedAt: new Date() } ]; testDeliveries.forEach(delivery => this.deliveries.set(delivery.id, delivery as Delivery)); console.log('✅ MemStorage initialized with admin user (admin/admin)'); console.log('📢 Test publicities and groups added for development'); } // User methods async getUser(id: string): Promise { return this.users.get(id); } async getUserByEmail(email: string): Promise { for (const user of this.users.values()) { if (user.email === email) return user; } return undefined; } async getUserByUsername(username: string): Promise { for (const user of this.users.values()) { if (user.username === username) return user; } return undefined; } async upsertUser(user: UpsertUser): Promise { const newUser = { ...user, createdAt: new Date(), updatedAt: new Date() } as User; this.users.set(user.id, newUser); return newUser; } async getUserWithGroups(id: string): Promise { const user = await this.getUser(id); if (!user) return undefined; return { ...user, userGroups: [] }; } async getUsers(): Promise { return Array.from(this.users.values()); } async getUsersWithRoles(): Promise { return Array.from(this.users.values()).map(user => ({ ...user, userRoles: [] })); } async getUsersWithRolesAndGroups(): Promise<(UserWithRoles & { userGroups: any[] })[]> { return Array.from(this.users.values()).map(user => ({ ...user, userRoles: [], userGroups: [] })); } async createUser(user: UpsertUser): Promise { return this.upsertUser(user); } async updateUser(id: string, user: Partial): Promise { const existingUser = this.users.get(id); if (!existingUser) throw new Error('User not found'); const updatedUser = { ...existingUser, ...user, updatedAt: new Date() }; this.users.set(id, updatedUser); return updatedUser; } async deleteUser(id: string): Promise { this.users.delete(id); } // Stub implementations for other required methods async getGroups(): Promise { return Array.from(this.groups.values()); } async getGroup(id: number): Promise { return this.groups.get(id); } async createGroup(group: InsertGroup): Promise { return {} as Group; } async updateGroup(id: number, group: Partial): Promise { return {} as Group; } async deleteGroup(id: number): Promise {} async getSuppliers(): Promise { return Array.from(this.suppliers.values()); } async createSupplier(supplier: InsertSupplier): Promise { return {} as Supplier; } async updateSupplier(id: number, supplier: Partial): Promise { return {} as Supplier; } async deleteSupplier(id: number): Promise {} async getOrders(groupIds?: number[]): Promise { return []; } async getOrdersByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise { return []; } async createOrder(order: InsertOrder): Promise { return {} as Order; } async updateOrder(id: number, order: Partial): Promise { return {} as Order; } async deleteOrder(id: number): Promise {} async getDeliveries(groupIds?: number[]): Promise { console.log('🚚 MemStorage getDeliveries called with:', { groupIds }); let deliveriesArray = Array.from(this.deliveries.values()); // Filter by groups if provided if (groupIds && groupIds.length > 0) { deliveriesArray = deliveriesArray.filter(d => groupIds.includes(d.groupId)); } // Add relations (supplier, group, creator) const deliveriesWithRelations = deliveriesArray.map(delivery => ({ ...delivery, supplier: this.suppliers.get(delivery.supplierId!), group: this.groups.get(delivery.groupId!), creator: this.users.get(delivery.createdBy!), order: undefined // No orders in test data for now })); console.log('🚚 MemStorage returning deliveries:', { total: deliveriesWithRelations.length, sample: deliveriesWithRelations[0] ? { id: deliveriesWithRelations[0].id, blNumber: deliveriesWithRelations[0].blNumber, supplier: deliveriesWithRelations[0].supplier?.name, group: { name: deliveriesWithRelations[0].group?.name, hasWebhook: !!deliveriesWithRelations[0].group?.webhookUrl } } : null }); return deliveriesWithRelations as DeliveryWithRelations[]; } async getDeliveriesByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise { return []; } async createDelivery(delivery: InsertDelivery): Promise { return {} as Delivery; } async updateDelivery(id: number, delivery: Partial): Promise { return {} as Delivery; } async deleteDelivery(id: number): Promise {} // Add missing required methods for IStorage interface async getOrder(id: number): Promise { return undefined; } async getDelivery(id: number): Promise { return undefined; } async validateDelivery(id: number, deliveryData: any): Promise {} async getUserGroups(userId: string): Promise { return []; } async getUserRoles(userId: string): Promise { return []; } async getUserWithRoles(userId: string): Promise { const user = await this.getUser(userId); return user ? { ...user, userRoles: [] } : undefined; } async getRoleWithPermissions(roleId: number): Promise { return undefined; } async getUserPermissions(userId: string): Promise { return []; } async getTasksByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise { return []; } async getOrderStats(): Promise { return {}; } async getDeliveryStats(): Promise { return {}; } async getPublicitiesWithParticipations(year?: number): Promise { const allPubs = Array.from(this.publicities.values()); const filtered = year ? allPubs.filter(pub => pub.year === year) : allPubs; return filtered.map(pub => ({ ...pub, participations: this.publicityParticipations .filter(pp => pp.publicityId === pub.id) .map(pp => ({ ...pp, group: this.groups.get(pp.groupId) })) })); } async setPublicityParticipations(publicityId: number, groupIds: number[]): Promise {} // Add placeholder methods for all other IStorage interface methods async getSavTickets(): Promise { return []; } async getSavTicket(): Promise { return undefined; } async createSavTicket(): Promise { return {}; } async updateSavTicket(): Promise { return {}; } async deleteSavTicket(): Promise {} async getSavTicketHistory(): Promise { return []; } async createSavTicketHistory(): Promise { return {}; } async updateSavTicketHistory(): Promise { return {}; } async deleteSavTicketHistory(): Promise {} // Publicity methods - properly implemented for MemStorage async getPublicities(year?: number, groupIds?: number[]): Promise { console.log('🔍 MemStorage getPublicities called with:', { year, groupIds }); let allPubs = Array.from(this.publicities.values()); // Filter by year if provided if (year) { allPubs = allPubs.filter(pub => pub.year === year); } // Add participations to each publicity const pubsWithParticipations = allPubs.map(pub => ({ ...pub, participations: this.publicityParticipations .filter(pp => pp.publicityId === pub.id) .map(pp => ({ ...pp, group: this.groups.get(pp.groupId) })) })); // Filter by groupIds if provided (for non-admin users or specific store selection) if (groupIds && groupIds.length > 0) { const filtered = pubsWithParticipations.filter(pub => pub.participations.some((p: any) => groupIds.includes(p.groupId)) ); console.log('📢 Filtered publicities by groups:', { total: pubsWithParticipations.length, filtered: filtered.length, groupIds }); return filtered; } console.log('📢 Returning all publicities:', pubsWithParticipations.length); return pubsWithParticipations; } async getPublicity(): Promise { return undefined; } async createPublicity(): Promise { return {}; } async updatePublicity(): Promise { return {}; } async deletePublicity(): Promise {} async getPublicityParticipations(): Promise { return []; } async createPublicityParticipation(): Promise { return {}; } async deletePublicityParticipation(): Promise {} async getCustomerOrders(): Promise { return []; } async getCustomerOrder(): Promise { return undefined; } async createCustomerOrder(): Promise { return {}; } async updateCustomerOrder(): Promise { return {}; } async deleteCustomerOrder(): Promise {} async getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; }): Promise { console.log("📋 MemStorage getDlcProducts:", { groupIds, dlcCount: this.dlcProducts.size }); let products = Array.from(this.dlcProducts.values()); // Filter by groups if provided if (groupIds && groupIds.length > 0) { products = products.filter(p => groupIds.includes(p.groupId)); console.log("🔍 Filtered by groups:", { original: this.dlcProducts.size, filtered: products.length, groupIds }); } // Filter by status if (filters?.status) { products = products.filter(p => p.status === filters.status); } // Filter by supplier if (filters?.supplierId) { products = products.filter(p => p.supplierId === filters.supplierId); } return products; } async getDlcProduct(id: number): Promise { return this.dlcProducts.get(id); } async createDlcProduct(dlcProduct: any): Promise { const id = this.dlcIdCounter++; const newProduct = { id, ...dlcProduct, createdAt: new Date(), updatedAt: new Date(), }; this.dlcProducts.set(id, newProduct); console.log("✅ MemStorage DLC créé:", { id, productName: newProduct.productName, groupId: newProduct.groupId }); return newProduct; } async updateDlcProduct(id: number, dlcProduct: any): Promise { const existing = this.dlcProducts.get(id); if (!existing) throw new Error("DLC product not found"); const updated = { ...existing, ...dlcProduct, updatedAt: new Date(), }; this.dlcProducts.set(id, updated); return updated; } async deleteDlcProduct(id: number): Promise { this.dlcProducts.delete(id); } async getTasks(): Promise { return Array.from(this.tasks.values()); } async getTask(): Promise { return undefined; } async createTask(): Promise { return {}; } async updateTask(): Promise { return {}; } async deleteTask(): Promise {} async completeTask(): Promise {} async getRoles(): Promise { return []; } async getRole(): Promise { return undefined; } async createRole(): Promise { return {}; } async updateRole(): Promise { return {}; } async deleteRole(): Promise {} async getPermissions(): Promise { return []; } async getPermission(): Promise { return undefined; } async createPermission(): Promise { return {}; } async updatePermission(): Promise { return {}; } async deletePermission(): Promise {} async getRolePermissions(): Promise { return []; } async setRolePermissions(): Promise {} async setUserRoles(): Promise {} async assignUserToGroup(userGroup: { groupId: number; userId: string; }): Promise<{ createdAt: Date | null; groupId: number; userId: string; }> { return { ...userGroup, createdAt: new Date() }; } async removeUserFromGroup(): Promise {} async getMonthlyStats(): Promise { return {}; } async getNocodbConfigs(): Promise { return []; } async getNocodbConfig(id: number): Promise { // Retourner une configuration de test pour le développement if (id === 1) { return { id: 1, name: 'Config Test NocoDB', baseUrl: 'https://nocodb-test.example.com', projectId: 'test-project', apiToken: 'test-token-123', isActive: true }; } return null; } async createNocodbConfig(): Promise { return {}; } async updateNocodbConfig(): Promise { return {}; } async deleteNocodbConfig(): Promise {} async getPermissionsByCategory(): Promise { return []; } async getUserEffectivePermissions(): Promise { return []; } async userHasPermission(): Promise { return false; } async userHasRole(): Promise { return false; } async getDlcStats(groupIds?: number[]): Promise { const products = Array.from(this.dlcProducts.values()); // Filter by groups if provided const filteredProducts = groupIds && groupIds.length > 0 ? products.filter(p => groupIds.includes(p.groupId)) : products; const now = new Date(); const in15Days = new Date(now.getTime() + 15 * 24 * 60 * 60 * 1000); const stats = { active: filteredProducts.filter(p => p.status !== 'valides' && new Date(p.dlcDate) > in15Days).length, expiringSoon: filteredProducts.filter(p => p.status !== 'valides' && new Date(p.dlcDate) <= in15Days && new Date(p.dlcDate) > now).length, expired: filteredProducts.filter(p => p.status !== 'valides' && new Date(p.dlcDate) <= now).length, }; console.log("📊 MemStorage DLC stats:", stats); return stats; } async validateDlcProduct(id: number, validatedBy: string): Promise { // MemStorage stub implementation - return a basic DlcProductFrontend object return { id, productName: "Sample Product", gencode: "1234567890123", supplierId: 1, groupId: 1, dlcDate: new Date("2025-12-31"), dateType: "dlc", quantity: 1, unit: "unité", location: "Magasin", alertThreshold: 15, status: "valides", notes: null, createdBy: validatedBy, validatedBy, validatedAt: new Date(), createdAt: new Date(), updatedAt: new Date(), } as DlcProductFrontend; } // Missing interface methods - stub implementations for MemStorage async addPermissionToRole(roleId: number, permissionId: number): Promise<{ createdAt: Date | null; roleId: number; permissionId: number; }> { return { roleId, permissionId, createdAt: new Date() }; } async removePermissionFromRole(): Promise {} async addRoleToUser(userId: string, roleId: number, assignedBy: string): Promise<{ userId: string; roleId: number; assignedBy: string; assignedAt: Date | null; }> { return { userId, roleId, assignedBy, assignedAt: new Date() }; } async removeRoleFromUser(): Promise {} } // Use MemStorage in development, DatabaseStorage in production const isProduction = process.env.NODE_ENV === 'production'; const dbUrl = process.env.DATABASE_URL; const isProductionDocker = isProduction && dbUrl && (dbUrl.includes('logiflow-db') || dbUrl.includes('localhost')); export const storage = isProductionDocker ? new DatabaseStorage() : new MemStorage(); console.log(isProductionDocker ? '🐳 PRODUCTION: Using PostgreSQL storage' : '🔧 DEV: Using in-memory storage');