From cb1a1a933c0f4a2823dbf6b69ee7bf65f02d1543 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Tue, 10 Feb 2026 09:37:02 +0100 Subject: [PATCH] fix(dlc): filter stockEpuise products from DLC stats and invalidate stats cache --- client/src/components/DlcAlertModal.tsx | 63 +-- server/storage.ts | 572 ++++++++++++------------ 2 files changed, 320 insertions(+), 315 deletions(-) diff --git a/client/src/components/DlcAlertModal.tsx b/client/src/components/DlcAlertModal.tsx index b595194..5a604ec 100644 --- a/client/src/components/DlcAlertModal.tsx +++ b/client/src/components/DlcAlertModal.tsx @@ -1,21 +1,21 @@ import { useState, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { - Dialog, - DialogContent, - DialogHeader, +import { + Dialog, + DialogContent, + DialogHeader, DialogTitle, - DialogDescription + DialogDescription } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { - AlertTriangle, - XCircle, - Clock, - PackageX, - Eye, - ArrowRight +import { + AlertTriangle, + XCircle, + Clock, + PackageX, + Eye, + ArrowRight } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; @@ -73,7 +73,7 @@ export function DlcAlertModal({ isOpen, onClose, dlcStats, selectedStoreId }: Dl // Mutation to mark product as stock depleted const markStockEpuiseMutation = useMutation({ - mutationFn: (productId: number) => + mutationFn: (productId: number) => apiRequest(`/api/dlc-products/${productId}/stock-epuise`, "PUT"), onSuccess: () => { toast({ @@ -82,6 +82,7 @@ export function DlcAlertModal({ isOpen, onClose, dlcStats, selectedStoreId }: Dl }); // Refresh DLC stats and products queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"] }); + queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"] }); }, onError: () => { toast({ @@ -164,11 +165,11 @@ export function DlcAlertModal({ isOpen, onClose, dlcStats, selectedStoreId }: Dl Produits Expirés ({dlcStats.expired}) - +
{expiredProducts.slice(0, 8).map((product) => ( -
@@ -177,7 +178,7 @@ export function DlcAlertModal({ isOpen, onClose, dlcStats, selectedStoreId }: Dl {product.supplier.name} • Expiré depuis {Math.abs(getDaysUntilExpiry(product.expiryDate))} jour(s)
- + {!product.stockEpuise && (
))} - + {expiredProducts.length > 8 && (
et {expiredProducts.length - 8} autre(s) produit(s) expiré(s)... @@ -218,13 +219,13 @@ export function DlcAlertModal({ isOpen, onClose, dlcStats, selectedStoreId }: Dl Expirent Bientôt ({dlcStats.expiringSoon})
- +
{expiringSoonProducts.slice(0, 8).map((product) => { const daysLeft = getDaysUntilExpiry(product.expiryDate); return ( -
@@ -233,9 +234,9 @@ export function DlcAlertModal({ isOpen, onClose, dlcStats, selectedStoreId }: Dl {product.supplier.name} • Expire dans {daysLeft} jour(s)
- - ); })} - + {expiringSoonProducts.length > 8 && (
et {expiringSoonProducts.length - 8} autre(s) produit(s) expirant bientôt... @@ -258,7 +259,7 @@ export function DlcAlertModal({ isOpen, onClose, dlcStats, selectedStoreId }: Dl {/* Action Buttons */}
- - -
diff --git a/server/storage.ts b/server/storage.ts index 7dee56f..86aaa4e 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -95,20 +95,20 @@ export interface IStorage { 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; @@ -116,7 +116,7 @@ export interface IStorage { 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; @@ -126,12 +126,12 @@ export interface IStorage { deleteDelivery(id: number): Promise; validateDelivery(id: number, blData?: { blNumber: string; blAmount: number }): Promise; markDeliveryControlValidated(id: number, userId: string): 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; @@ -156,11 +156,11 @@ export interface IStorage { 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; @@ -168,20 +168,20 @@ export interface IStorage { createNocodbConfig(config: InsertNocodbConfig): Promise; updateNocodbConfig(id: number, config: Partial): Promise; deleteNocodbConfig(id: number): Promise; - + // Invoice Verification Cache operations getInvoiceVerificationCache(cacheKey: string): Promise; saveInvoiceVerificationCache(cache: InsertInvoiceVerificationCache): Promise; createInvoiceVerificationCache(cache: InsertInvoiceVerificationCache): Promise; clearExpiredCache(): 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; - + // Client call tracking getPendingClientCalls(groupIds?: number[]): Promise; markClientCalled(customerOrderId: number, calledBy: string): Promise; @@ -222,12 +222,12 @@ export interface IStorage { deleteAvoir(id: number): Promise; updateAvoirWebhookStatus(id: number, webhookSent: boolean): Promise; updateAvoirNocodbVerification(id: number, verified: boolean): Promise; - + // SAV operations - getSavTickets(filters?: { - groupIds?: number[]; - status?: string; - supplierId?: number; + getSavTickets(filters?: { + groupIds?: number[]; + status?: string; + supplierId?: number; priority?: string; startDate?: string; endDate?: string; @@ -272,7 +272,7 @@ export interface IStorage { createReconciliationComment(comment: InsertReconciliationComment): Promise; updateReconciliationComment(id: number, comment: Partial): Promise; deleteReconciliationComment(id: number): Promise; - + // Analytics operations getAnalyticsSummary(filters: { startDate?: Date; @@ -289,7 +289,7 @@ export interface IStorage { topSuppliers: Array<{ id: number; name: string; count: number; amount: number }>; topStores: Array<{ id: number; name: string; orders: number; deliveries: number }>; }>; - + getAnalyticsTimeseries(filters: { startDate?: Date; endDate?: Date; @@ -297,13 +297,13 @@ export interface IStorage { groupIds?: number[]; granularity?: 'day' | 'week' | 'month'; }): Promise>; - + getAnalyticsBySupplier(filters: { startDate?: Date; endDate?: Date; groupIds?: number[]; }): Promise>; - + getAnalyticsByStore(filters: { startDate?: Date; endDate?: Date; @@ -792,7 +792,7 @@ export class DatabaseStorage implements IStorage { const deliveriesWithOrders = await Promise.all( baseDeliveries.map(async (delivery) => { let associatedOrder = undefined; - + if (delivery.orderId) { try { const [orderData] = await db @@ -845,7 +845,7 @@ export class DatabaseStorage implements IStorage { .select({ count: sql`count(*)` }) .from(reconciliationComments) .where(eq(reconciliationComments.deliveryId, delivery.id)); - + commentsCount = Number(countResult?.count || 0); } catch (error) { console.error(`Failed to count reconciliation comments for delivery #${delivery.id}:`, error); @@ -927,7 +927,7 @@ export class DatabaseStorage implements IStorage { const deliveriesWithOrders = await Promise.all( baseDeliveries.map(async (delivery) => { let associatedOrder = undefined; - + if (delivery.orderId) { try { const [orderData] = await db @@ -980,7 +980,7 @@ export class DatabaseStorage implements IStorage { .select({ count: sql`count(*)` }) .from(reconciliationComments) .where(eq(reconciliationComments.deliveryId, delivery.id)); - + commentsCount = Number(countResult?.count || 0); } catch (error) { console.error(`Failed to count reconciliation comments for delivery #${delivery.id}:`, error); @@ -1076,7 +1076,7 @@ export class DatabaseStorage implements IStorage { .select({ count: sql`count(*)` }) .from(reconciliationComments) .where(eq(reconciliationComments.deliveryId, id)); - + commentsCount = Number(countResult?.count || 0); } catch (error) { console.error(`Failed to count reconciliation comments for delivery #${id}:`, error); @@ -1096,36 +1096,36 @@ export class DatabaseStorage implements IStorage { ...deliveryData, status: deliveryData.status || 'planned' // Force planned status by default }; - + const [delivery] = await db.insert(deliveries).values(deliveryDataWithStatus).returning(); - + // Si une commande est liée, la marquer comme "planned" (pas delivered!) if (deliveryData.orderId) { try { console.log(`🔗 PRODUCTION: Delivery #${delivery.id} linked to order #${deliveryData.orderId}, updating order status to 'planned'`); - + // Récupérer la commande actuelle const [currentOrder] = await db .select() .from(orders) .where(eq(orders.id, deliveryData.orderId)); - + if (currentOrder && currentOrder.status === 'pending') { await db .update(orders) - .set({ + .set({ status: 'planned', - updatedAt: new Date() + updatedAt: new Date() }) .where(eq(orders.id, deliveryData.orderId)); - + console.log(`✅ PRODUCTION: Order #${deliveryData.orderId} status updated to 'planned'`); } } catch (error) { console.error(`❌ PRODUCTION: Failed to update order #${deliveryData.orderId} status to planned:`, error); } } - + return delivery; } @@ -1135,27 +1135,27 @@ export class DatabaseStorage implements IStorage { .set({ ...deliveryData, updatedAt: new Date() }) .where(eq(deliveries.id, id)) .returning(); - + // CRITICAL FIX: Si une commande est liée lors de l'édition, la marquer comme "planned" if (deliveryData.orderId) { try { console.log(`🔗 CALENDAR EDIT: Delivery #${delivery.id} linked to order #${deliveryData.orderId}, updating order status to 'planned'`); - + // Récupérer la commande actuelle const [currentOrder] = await db .select() .from(orders) .where(eq(orders.id, deliveryData.orderId)); - + if (currentOrder && currentOrder.status === 'pending') { await db .update(orders) - .set({ + .set({ status: 'planned', - updatedAt: new Date() + updatedAt: new Date() }) .where(eq(orders.id, deliveryData.orderId)); - + console.log(`✅ CALENDAR EDIT: Order #${deliveryData.orderId} status updated to 'planned'`); } else if (currentOrder) { console.log(`ℹ️ CALENDAR EDIT: Order #${deliveryData.orderId} status is '${currentOrder.status}', no update needed`); @@ -1164,7 +1164,7 @@ export class DatabaseStorage implements IStorage { console.error(`❌ CALENDAR EDIT: Failed to update order #${deliveryData.orderId} status to planned:`, error); } } - + return delivery; } @@ -1238,8 +1238,8 @@ export class DatabaseStorage implements IStorage { // En production, calculer les vraies statistiques const startDate = `${year}-${month.toString().padStart(2, '0')}-01`; - const endDate = month === 12 - ? `${year + 1}-01-01` + const endDate = month === 12 + ? `${year + 1}-01-01` : `${year}-${(month + 1).toString().padStart(2, '0')}-01`; try { @@ -1252,7 +1252,7 @@ export class DatabaseStorage implements IStorage { ); // Pour les livraisons, utiliser deliveredDate pour celles livrées, sinon scheduledDate let deliveriesWhereCondition = and( - gte(deliveries.scheduledDate, startDate), + gte(deliveries.scheduledDate, startDate), lt(deliveries.scheduledDate, endDate) ); @@ -1516,24 +1516,24 @@ export class DatabaseStorage implements IStorage { // SAFE: Use simple column ordering instead of SQL CAST which fails in production const results = await query.orderBy(publicities.pubNumber); - + // LOG: Debug des publicités récupérées console.log(`📋 PUBLICITES FETCHED: ${results.length} résultats pour année ${year || 'toutes'}`); if (results.length > 0) { - console.log('🔍 PREMIERS RESULTATS:', results.slice(0, 3).map((p, i) => `${i+1}. N°${p.pubNumber} - ${p.designation}`)); + console.log('🔍 PREMIERS RESULTATS:', results.slice(0, 3).map((p, i) => `${i + 1}. N°${p.pubNumber} - ${p.designation}`)); } const publicityIds = results.map((p: any) => p.id); - const participations = publicityIds.length > 0 + const participations = publicityIds.length > 0 ? await db - .select({ - publicityId: publicityParticipations.publicityId, - groupId: publicityParticipations.groupId, - group: groups, - }) - .from(publicityParticipations) - .leftJoin(groups, eq(publicityParticipations.groupId, groups.id)) - .where(inArray(publicityParticipations.publicityId, publicityIds)) + .select({ + publicityId: publicityParticipations.publicityId, + groupId: publicityParticipations.groupId, + group: groups, + }) + .from(publicityParticipations) + .leftJoin(groups, eq(publicityParticipations.groupId, groups.id)) + .where(inArray(publicityParticipations.publicityId, publicityIds)) : []; // Sort by pubNumber as integer on the server side for consistency @@ -1595,20 +1595,20 @@ export class DatabaseStorage implements IStorage { async deletePublicity(id: number): Promise { console.log(`🗑️ [DELETION] Starting deletion of publicity ID: ${id}`); - + try { // First, delete all participations (defensive approach for production DB constraints) const deletedParticipations = await db.delete(publicityParticipations).where(eq(publicityParticipations.publicityId, id)).returning(); console.log(`🗑️ [DELETION] Deleted ${deletedParticipations.length} participations for publicity ${id}`); - + // Then delete the publicity itself const deletedPublicity = await db.delete(publicities).where(eq(publicities.id, id)).returning(); console.log(`🗑️ [DELETION] Deleted publicity ${id}, found: ${deletedPublicity.length > 0 ? 'YES' : 'NO'}`); - + if (deletedPublicity.length === 0) { throw new Error(`Publicity with ID ${id} not found`); } - + console.log(`✅ [DELETION] Successfully deleted publicity ID: ${id}`); } catch (error) { console.error(`❌ [DELETION] Failed to delete publicity ID: ${id}`, error); @@ -1652,7 +1652,7 @@ export class DatabaseStorage implements IStorage { .from(nocodbConfig) .where(eq(nocodbConfig.isActive, true)) .limit(1); - + console.log('🔧 Configuration NocoDB active récupérée:', config); return config; } catch (error) { @@ -1685,23 +1685,23 @@ export class DatabaseStorage implements IStorage { .select() .from(invoiceVerificationCache) .where(eq(invoiceVerificationCache.cacheKey, cacheKey)); - + // ✅ Vérification d'expiration ajoutée pour production if (cache) { const now = new Date(); const expiresAt = new Date(cache.expiresAt); - + if (now < expiresAt) { - console.log('✅ [DATABASE-CACHE] Cache hit pour:', { - cacheKey, + console.log('✅ [DATABASE-CACHE] Cache hit pour:', { + cacheKey, expires: cache.expiresAt, hoursRemaining: Math.round((expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60)) }); return cache; // Cache valide } else { - console.log('⏰ [DATABASE-CACHE] Cache expiré, suppression:', { - cacheKey, - expiredAt: cache.expiresAt + console.log('⏰ [DATABASE-CACHE] Cache expiré, suppression:', { + cacheKey, + expiredAt: cache.expiresAt }); // Supprimer le cache expiré await db.delete(invoiceVerificationCache) @@ -1709,7 +1709,7 @@ export class DatabaseStorage implements IStorage { return undefined; // Cache expiré } } - + console.log('❌ [DATABASE-CACHE] Cache miss pour:', { cacheKey }); return undefined; // Pas de cache } @@ -1839,7 +1839,7 @@ export class DatabaseStorage implements IStorage { .leftJoin(groups, eq(customerOrders.groupId, groups.id)) .where(and(...conditions)) .orderBy(desc(customerOrders.createdAt)); - + return results.map(result => ({ ...result.customerOrder, supplier: result.supplier!, @@ -1851,13 +1851,13 @@ export class DatabaseStorage implements IStorage { async markClientCalled(customerOrderId: number, calledBy: string): Promise { const [updated] = await db .update(customerOrders) - .set({ + .set({ customerNotified: true, updatedAt: new Date() }) .where(eq(customerOrders.id, customerOrderId)) .returning(); - + return updated; } @@ -1883,7 +1883,7 @@ export class DatabaseStorage implements IStorage { if (filters.status && filters.status !== 'all') { const today = new Date(); today.setHours(0, 0, 0, 0); // Set to start of day for accurate comparison - + if (filters.status === 'expires_soon') { // Products expiring within 15 days but not expired yet // Ne pas afficher les produits déjà traités (processedUntilExpiry = true) @@ -1930,7 +1930,7 @@ export class DatabaseStorage implements IStorage { conditions.push(eq(dlcProducts.status, 'valides')); } } - + if (filters.supplierId) { conditions.push(eq(dlcProducts.supplierId, filters.supplierId)); } @@ -1963,7 +1963,7 @@ export class DatabaseStorage implements IStorage { // Priorité 2 : Date d'expiration (croissante) asc(dlcProducts.expiryDate) ); - + return results.map((row: any) => ({ ...row.dlcProduct, dlcDate: row.dlcProduct.expiryDate, // ✅ Mapping expiryDate vers dlcDate pour frontend @@ -2016,7 +2016,7 @@ export class DatabaseStorage implements IStorage { async validateDlcProduct(id: number, validatedBy: string): Promise { const [dlcProduct] = await db .update(dlcProducts) - .set({ + .set({ validatedBy, validatedAt: new Date(), status: "valides", @@ -2030,7 +2030,7 @@ export class DatabaseStorage implements IStorage { async markDlcProductStockEpuise(id: number, markedBy: string): Promise { const [dlcProduct] = await db .update(dlcProducts) - .set({ + .set({ stockEpuise: true, stockEpuiseBy: markedBy, stockEpuiseAt: new Date(), @@ -2044,7 +2044,7 @@ export class DatabaseStorage implements IStorage { async restoreDlcProductStock(id: number): Promise { const [dlcProduct] = await db .update(dlcProducts) - .set({ + .set({ stockEpuise: false, stockEpuiseBy: null, stockEpuiseAt: null, @@ -2060,7 +2060,7 @@ export class DatabaseStorage implements IStorage { // Essayer avec les nouveaux champs si ils existent const [dlcProduct] = await db .update(dlcProducts) - .set({ + .set({ processedAt: new Date(), processedBy: processedBy, processedUntilExpiry: true, @@ -2073,7 +2073,7 @@ export class DatabaseStorage implements IStorage { // Fallback : juste mettre à jour la date de modification const [dlcProduct] = await db .update(dlcProducts) - .set({ + .set({ updatedAt: new Date() }) .where(eq(dlcProducts.id, id)) @@ -2087,7 +2087,7 @@ export class DatabaseStorage implements IStorage { // Essayer avec les nouveaux champs si ils existent const [dlcProduct] = await db .update(dlcProducts) - .set({ + .set({ processedAt: null, processedBy: null, processedUntilExpiry: false, @@ -2100,7 +2100,7 @@ export class DatabaseStorage implements IStorage { // Fallback : juste mettre à jour la date de modification const [dlcProduct] = await db .update(dlcProducts) - .set({ + .set({ updatedAt: new Date() }) .where(eq(dlcProducts.id, id)) @@ -2127,16 +2127,19 @@ export class DatabaseStorage implements IStorage { WHEN ${dlcProducts.expiryDate} > ${today.toISOString().split('T')[0]} AND ${dlcProducts.status} != 'valides' AND (${dlcProducts.processedUntilExpiry} IS NULL OR ${dlcProducts.processedUntilExpiry} = false) + AND (${dlcProducts.stockEpuise} IS NULL OR ${dlcProducts.stockEpuise} = false) THEN 1 END)`, expiringSoon: sql`COUNT(CASE WHEN ${dlcProducts.expiryDate} BETWEEN ${today.toISOString().split('T')[0]} AND ${alertDate.toISOString().split('T')[0]} AND ${dlcProducts.status} != 'valides' AND (${dlcProducts.processedUntilExpiry} IS NULL OR ${dlcProducts.processedUntilExpiry} = false) + AND (${dlcProducts.stockEpuise} IS NULL OR ${dlcProducts.stockEpuise} = false) THEN 1 END)`, expired: sql`COUNT(CASE WHEN ${dlcProducts.expiryDate} <= ${today.toISOString().split('T')[0]} AND ${dlcProducts.status} != 'valides' AND (${dlcProducts.processedUntilExpiry} IS NULL OR ${dlcProducts.processedUntilExpiry} = false) + AND (${dlcProducts.stockEpuise} IS NULL OR ${dlcProducts.stockEpuise} = false) THEN 1 END)` }) .from(dlcProducts) @@ -2184,7 +2187,7 @@ export class DatabaseStorage implements IStorage { } const results = await query.orderBy(desc(tasks.createdAt)); - + console.log('📋 DatabaseStorage.getTasks - Raw results:', { resultCount: results.length, userRole, @@ -2203,7 +2206,7 @@ export class DatabaseStorage implements IStorage { try { const task = row.task; const group = row.group || null; - + // Calculer si la tâche est future de manière sécurisée let isFutureTask = false; if (task.startDate) { @@ -2254,23 +2257,23 @@ export class DatabaseStorage implements IStorage { dueDateType: typeof taskData.dueDate, dueDateISO: taskData.dueDate instanceof Date ? taskData.dueDate.toISOString() : taskData.dueDate }); - + // Convert string dates to Date objects for Drizzle PostgreSQL const processedTaskData = { ...taskData, startDate: taskData.startDate ? new Date(taskData.startDate) : null, dueDate: taskData.dueDate ? new Date(taskData.dueDate) : null, }; - + const [task] = await db.insert(tasks).values(processedTaskData).returning(); - + console.log('💾 DatabaseStorage.createTask - Result:', { taskId: task.id, title: task.title, dueDate: task.dueDate, dueDateType: typeof task.dueDate }); - + return task; } @@ -2290,13 +2293,13 @@ export class DatabaseStorage implements IStorage { // Nettoyer les données pour éviter les problèmes de type const cleanData: any = {}; - + if (taskData.title !== undefined) cleanData.title = taskData.title; if (taskData.description !== undefined) cleanData.description = taskData.description; if (taskData.priority !== undefined) cleanData.priority = taskData.priority; if (taskData.status !== undefined) cleanData.status = taskData.status; if (taskData.assignedTo !== undefined) cleanData.assignedTo = taskData.assignedTo; - + // Gestion spéciale des dates selon les types PostgreSQL if (taskData.startDate !== undefined) { if (taskData.startDate === '' || taskData.startDate === null) { @@ -2306,7 +2309,7 @@ export class DatabaseStorage implements IStorage { cleanData.startDate = new Date(taskData.startDate); } } - + if (taskData.dueDate !== undefined) { if (taskData.dueDate === '' || taskData.dueDate === null) { cleanData.dueDate = null; @@ -2315,30 +2318,30 @@ export class DatabaseStorage implements IStorage { cleanData.dueDate = new Date(taskData.dueDate); } } - + cleanData.updatedAt = new Date(); - + console.log('🧹 Cleaned data for database update:', { ...cleanData, startDateType: cleanData.startDate ? typeof cleanData.startDate : 'null', dueDateType: cleanData.dueDate ? typeof cleanData.dueDate : 'null', startDateIsDate: cleanData.startDate instanceof Date, }); - + try { const [task] = await db .update(tasks) .set(cleanData) .where(eq(tasks.id, id)) .returning(); - + console.log('✅ DatabaseStorage.updateTask - Success:', { taskId: task.id, title: task.title, dueDate: task.dueDate, dueDateType: typeof task.dueDate }); - + return task; } catch (error) { console.error('❌ DatabaseStorage.updateTask - Database Error:', { @@ -2359,7 +2362,7 @@ export class DatabaseStorage implements IStorage { async completeTask(id: number, completedBy: string): Promise { await db .update(tasks) - .set({ + .set({ status: 'completed', completedAt: new Date(), completedBy, @@ -2529,19 +2532,19 @@ export class DatabaseStorage implements IStorage { async updateAvoirNocodbVerification(id: number, verified: boolean): Promise { await db .update(avoirs) - .set({ - nocodbVerified: verified, + .set({ + nocodbVerified: verified, nocodbVerifiedAt: verified ? new Date() : null, - updatedAt: new Date() + updatedAt: new Date() }) .where(eq(avoirs.id, id)); } // SAV operations - async getSavTickets(filters?: { - groupIds?: number[]; - status?: string; - supplierId?: number; + async getSavTickets(filters?: { + groupIds?: number[]; + status?: string; + supplierId?: number; priority?: string; startDate?: string; endDate?: string; @@ -2559,7 +2562,7 @@ export class DatabaseStorage implements IStorage { .leftJoin(users, eq(savTickets.createdBy, users.id)); const conditions = []; - + if (filters?.groupIds?.length) { conditions.push(inArray(savTickets.groupId, filters.groupIds)); } @@ -2636,7 +2639,7 @@ export class DatabaseStorage implements IStorage { .select({ count: sql`count(*)` }) .from(savTickets) .where(sql`EXTRACT(year from created_at) = ${currentYear}`); - + const ticketNumber = `SAV-${currentYear}-${String((count[0]?.count || 0) + 1).padStart(4, '0')}`; const [ticket] = await db @@ -2661,7 +2664,7 @@ export class DatabaseStorage implements IStorage { .set({ ...ticketData, updatedAt: new Date() }) .where(eq(savTickets.id, id)) .returning(); - + return ticket; } @@ -2695,7 +2698,7 @@ export class DatabaseStorage implements IStorage { // .insert(savTicketHistory) // .values(historyData) // .returning(); - + // return history; return null; } @@ -2715,9 +2718,9 @@ export class DatabaseStorage implements IStorage { // Get status counts const statusResults = await db - .select({ - count: sql`count(*)`, - status: savTickets.status + .select({ + count: sql`count(*)`, + status: savTickets.status }) .from(savTickets) .where(groupIds?.length ? inArray(savTickets.groupId, groupIds) : undefined) @@ -2725,9 +2728,9 @@ export class DatabaseStorage implements IStorage { // Get priority counts const priorityResults = await db - .select({ - count: sql`count(*)`, - priority: savTickets.priority + .select({ + count: sql`count(*)`, + priority: savTickets.priority }) .from(savTickets) .where(groupIds?.length ? inArray(savTickets.groupId, groupIds) : undefined) @@ -2750,7 +2753,7 @@ export class DatabaseStorage implements IStorage { // Process status results statusResults.forEach(result => { const count = Number(result.count || 0); - + if (result.status === 'nouveau') { stats.newTickets = count; } else if (['en_cours', 'attente_pieces', 'attente_echange'].includes(result.status)) { @@ -2763,7 +2766,7 @@ export class DatabaseStorage implements IStorage { // Process priority results for critical tickets priorityResults.forEach(result => { const count = Number(result.count || 0); - + if (result.priority === 'critique') { stats.criticalTickets = count; } @@ -2821,11 +2824,11 @@ export class DatabaseStorage implements IStorage { async deleteOldWeatherData(daysToKeep: number): Promise { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - daysToKeep); - + await db .delete(weatherData) .where(lt(weatherData.createdAt, cutoffDate)); - + console.log(`🧹 Deleted weather data older than ${daysToKeep} days`); } @@ -3075,9 +3078,9 @@ export class DatabaseStorage implements IStorage { count: sql`COUNT(${deliveries.id})`, amount: sql`COALESCE(SUM(CAST(${deliveries.blAmount} AS NUMERIC)), 0)` }) - .from(deliveries) - .innerJoin(suppliers, eq(deliveries.supplierId, suppliers.id)); - + .from(deliveries) + .innerJoin(suppliers, eq(deliveries.supplierId, suppliers.id)); + if (deliveryConditions.length) supplierQuery.where(and(...deliveryConditions)); const topSuppliers = await supplierQuery .groupBy(suppliers.id, suppliers.name) @@ -3091,10 +3094,10 @@ export class DatabaseStorage implements IStorage { orders: sql`COUNT(DISTINCT ${orders.id})`, deliveries: sql`COUNT(DISTINCT ${deliveries.id})` }) - .from(groups) - .leftJoin(orders, eq(groups.id, orders.groupId)) - .leftJoin(deliveries, eq(groups.id, deliveries.groupId)); - + .from(groups) + .leftJoin(orders, eq(groups.id, orders.groupId)) + .leftJoin(deliveries, eq(groups.id, deliveries.groupId)); + if (filters.groupIds?.length) { storeQuery.where(inArray(groups.id, filters.groupIds)); } @@ -3109,17 +3112,17 @@ export class DatabaseStorage implements IStorage { reconciliationRate: deliveryStats.count ? (Number(deliveryStats.reconciled) / Number(deliveryStats.count)) * 100 : 0, totalAmount: Number(deliveryStats.totalAmount) || 0, avgDeliveryDelay: Number(deliveryStats.avgDelay) || 0, - topSuppliers: topSuppliers.map(s => ({ - id: s.id, - name: s.name, - count: Number(s.count), - amount: Number(s.amount) + topSuppliers: topSuppliers.map(s => ({ + id: s.id, + name: s.name, + count: Number(s.count), + amount: Number(s.amount) })), - topStores: topStores.map(s => ({ - id: s.id, - name: s.name, - orders: Number(s.orders), - deliveries: Number(s.deliveries) + topStores: topStores.map(s => ({ + id: s.id, + name: s.name, + orders: Number(s.orders), + deliveries: Number(s.deliveries) })) }; } catch (error) { @@ -3137,12 +3140,12 @@ export class DatabaseStorage implements IStorage { }): Promise> { const granularity = filters.granularity || 'day'; const dateFormat = granularity === 'day' ? 'YYYY-MM-DD' : - granularity === 'week' ? 'YYYY-IW' : 'YYYY-MM'; - + granularity === 'week' ? 'YYYY-IW' : 'YYYY-MM'; + // Build WHERE clauses with direct value substitution const orderConditions: string[] = []; const deliveryConditions: string[] = []; - + if (filters.startDate) { const startDate = filters.startDate.toISOString().split('T')[0]; orderConditions.push(`planned_date >= '${startDate}'`); @@ -3163,10 +3166,10 @@ export class DatabaseStorage implements IStorage { orderConditions.push(`group_id IN (${groupIds})`); deliveryConditions.push(`group_id IN (${groupIds})`); } - + const orderWhere = orderConditions.length > 0 ? `WHERE ${orderConditions.join(' AND ')}` : ''; const deliveryWhere = deliveryConditions.length > 0 ? `WHERE ${deliveryConditions.join(' AND ')}` : ''; - + // Get orders by date using raw SQL const ordersSql = ` SELECT TO_CHAR(planned_date, '${dateFormat}') as date, COUNT(*) as count @@ -3175,9 +3178,9 @@ export class DatabaseStorage implements IStorage { GROUP BY TO_CHAR(planned_date, '${dateFormat}') ORDER BY TO_CHAR(planned_date, '${dateFormat}') `; - + const ordersData = await db.execute(sql.raw(ordersSql)); - + // Get deliveries by date using raw SQL const deliveriesSql = ` SELECT TO_CHAR(scheduled_date, '${dateFormat}') as date, COUNT(*) as count @@ -3186,9 +3189,9 @@ export class DatabaseStorage implements IStorage { GROUP BY TO_CHAR(scheduled_date, '${dateFormat}') ORDER BY TO_CHAR(scheduled_date, '${dateFormat}') `; - + const deliveriesData = await db.execute(sql.raw(deliveriesSql)); - + // Merge data const dataMap = new Map(); (ordersData.rows as any[]).forEach(row => { @@ -3198,7 +3201,7 @@ export class DatabaseStorage implements IStorage { const existing = dataMap.get(row.date) || { orders: 0, deliveries: 0 }; dataMap.set(row.date, { ...existing, deliveries: Number(row.count) }); }); - + return Array.from(dataMap.entries()) .map(([date, data]) => ({ date, ...data })) .sort((a, b) => a.date.localeCompare(b.date)); @@ -3226,11 +3229,11 @@ export class DatabaseStorage implements IStorage { deliveries: sql`COUNT(${deliveries.id})`, amount: sql`COALESCE(SUM(CAST(${deliveries.blAmount} AS NUMERIC)), 0)` }) - .from(deliveries) - .innerJoin(suppliers, eq(deliveries.supplierId, suppliers.id)); - + .from(deliveries) + .innerJoin(suppliers, eq(deliveries.supplierId, suppliers.id)); + if (conditions.length) query.where(and(...conditions)); - + const result = await query .groupBy(suppliers.id, suppliers.name) .orderBy(desc(sql`COUNT(${deliveries.id})`)); @@ -3248,13 +3251,13 @@ export class DatabaseStorage implements IStorage { endDate?: Date; supplierIds?: number[]; }): Promise> { - + try { // Build date filter conditions as strings for raw SQL let orderDateFilter = '1=1'; let deliveryDateFilter = '1=1'; let supplierFilter = '1=1'; - + if (filters.startDate) { const startDate = filters.startDate.toISOString().split('T')[0]; orderDateFilter += ` AND planned_date >= '${startDate}'`; @@ -3299,8 +3302,8 @@ export class DatabaseStorage implements IStorage { orders: Number(row.orders), deliveries: Number(row.deliveries) })); - - + + return mappedResult; } catch (error) { console.error('Error in getAnalyticsByStore:', error); @@ -3368,7 +3371,7 @@ export class MemStorage implements IStorage { // Initialize group counter this.idCounters.group = 2; - + // Initialize supplier counter this.idCounters.supplier = 2; @@ -3471,7 +3474,7 @@ export class MemStorage implements IStorage { async updateUser(id: string, userData: Partial): Promise { const existingUser = this.users.get(id); if (!existingUser) throw new Error('User not found'); - + const updatedUser = { ...existingUser, ...userData, updatedAt: new Date() }; this.users.set(id, updatedUser); return updatedUser; @@ -3515,7 +3518,7 @@ export class MemStorage implements IStorage { async updateGroup(id: number, groupData: Partial): Promise { const existingGroup = this.groups.get(id); if (!existingGroup) throw new Error('Group not found'); - + const updatedGroup = { ...existingGroup, ...groupData, updatedAt: new Date() }; this.groups.set(id, updatedGroup); return updatedGroup; @@ -3549,7 +3552,7 @@ export class MemStorage implements IStorage { async updateSupplier(id: number, supplierData: Partial): Promise { const existingSupplier = this.suppliers.get(id); if (!existingSupplier) throw new Error('Supplier not found'); - + const updatedSupplier = { ...existingSupplier, ...supplierData, updatedAt: new Date() }; this.suppliers.set(id, updatedSupplier); return updatedSupplier; @@ -3582,7 +3585,7 @@ export class MemStorage implements IStorage { createdAt: new Date(), updatedAt: new Date(), }; - + console.log('🔧 Configuration NocoDB active:', { name: testConfig.name, baseUrl: testConfig.baseUrl, @@ -3668,7 +3671,7 @@ export class MemStorage implements IStorage { async saveInvoiceVerificationCache(cache: InsertInvoiceVerificationCache): Promise { const newCache = await this.createInvoiceVerificationCache(cache); // Sauver en mémoire pour le cache temporaire - console.log('💾 [MEMSTORAGE-CACHE] Sauvegarde en cache:', { + console.log('💾 [MEMSTORAGE-CACHE] Sauvegarde en cache:', { cacheKey: cache.cacheKey, exists: cache.exists, expiresAt: cache.expiresAt @@ -3715,11 +3718,11 @@ export class MemStorage implements IStorage { const orderDate = new Date(order.plannedDate); return orderDate >= start && orderDate <= end; }); - + if (groupIds && groupIds.length > 0) { orders = orders.filter(order => groupIds.includes(order.groupId)); } - + return orders.map(order => { // Récupérer les livraisons associées à cette commande (DEV RELATIONS) const associatedDeliveries = Array.from(this.deliveries.values()) @@ -3744,7 +3747,7 @@ export class MemStorage implements IStorage { async getOrder(id: number): Promise { const order = this.orders.get(id); if (!order) return undefined; - + // Récupérer les livraisons associées à cette commande const associatedDeliveries = Array.from(this.deliveries.values()) .filter(delivery => delivery.orderId === id) @@ -3754,7 +3757,7 @@ export class MemStorage implements IStorage { group: this.groups.get(delivery.groupId), creator: this.users.get(delivery.createdBy || '') })); - + return { ...order, supplier: this.suppliers.get(order.supplierId), @@ -3779,7 +3782,7 @@ export class MemStorage implements IStorage { async updateOrder(id: number, orderData: Partial): Promise { const existingOrder = this.orders.get(id); if (!existingOrder) throw new Error('Order not found'); - + const updatedOrder = { ...existingOrder, ...orderData, @@ -3835,11 +3838,11 @@ export class MemStorage implements IStorage { const deliveryDate = new Date(delivery.scheduledDate); return deliveryDate >= start && deliveryDate <= end; }); - + if (groupIds && groupIds.length > 0) { deliveries = deliveries.filter(delivery => groupIds.includes(delivery.groupId)); } - + return deliveries.map(delivery => { // Récupérer la commande associée à cette livraison si elle existe (DEV RELATIONS) let associatedOrder = undefined; @@ -3873,7 +3876,7 @@ export class MemStorage implements IStorage { async getDelivery(id: number): Promise { const delivery = this.deliveries.get(id); if (!delivery) return undefined; - + // Récupérer la commande associée si elle existe let associatedOrder = undefined; if (delivery.orderId) { @@ -3887,7 +3890,7 @@ export class MemStorage implements IStorage { }; } } - + // Compter les commentaires de rapprochement pour cette livraison const commentsCount = Array.from(this.reconciliationComments.values()) .filter(comment => comment.deliveryId === delivery.id).length; @@ -3912,7 +3915,7 @@ export class MemStorage implements IStorage { updatedAt: new Date(), }; this.deliveries.set(id, delivery); - + // Si une commande est liée, la marquer comme "planned" (pas delivered!) if (deliveryData.orderId) { try { @@ -3925,21 +3928,21 @@ export class MemStorage implements IStorage { console.error(`❌ Failed to update order #${deliveryData.orderId} status to planned:`, error); } } - + return delivery; } async updateDelivery(id: number, deliveryData: Partial): Promise { const existingDelivery = this.deliveries.get(id); if (!existingDelivery) throw new Error('Delivery not found'); - + const updatedDelivery = { ...existingDelivery, ...deliveryData, updatedAt: new Date(), }; this.deliveries.set(id, updatedDelivery); - + // CRITICAL FIX: Si une commande est liée lors de l'édition, la marquer comme "planned" if (deliveryData.orderId) { try { @@ -3955,7 +3958,7 @@ export class MemStorage implements IStorage { console.error(`❌ CALENDAR EDIT (DEV): Failed to update order #${deliveryData.orderId} status to planned:`, error); } } - + return updatedDelivery; } @@ -3966,7 +3969,7 @@ export class MemStorage implements IStorage { async validateDelivery(id: number, blData?: { blNumber: string; blAmount: number }): Promise { const delivery = this.deliveries.get(id); if (!delivery) throw new Error('Delivery not found'); - + const updatedDelivery = { ...delivery, status: 'delivered' as const, @@ -3980,9 +3983,9 @@ export class MemStorage implements IStorage { async getUserGroups(): Promise { return []; } async assignUserToGroup(): Promise { return {} as UserGroup; } - async removeUserFromGroup(): Promise {} + async removeUserFromGroup(): Promise { } - async getMonthlyStats(): Promise { + async getMonthlyStats(): Promise { return { ordersCount: 12, deliveriesCount: 8, @@ -3993,7 +3996,7 @@ export class MemStorage implements IStorage { }; } - async getYearlyStats(): Promise { + async getYearlyStats(): Promise { return { ordersCount: 144, // 12 mois * 12 deliveriesCount: 96, // 12 mois * 8 @@ -4006,21 +4009,21 @@ export class MemStorage implements IStorage { async getPublicities(year?: number, groupIds?: number[]): Promise { let publicities = Array.from(this.publicities.values()); - + // Filter by year if provided if (year) { publicities = publicities.filter(pub => pub.year === year); } - + // Sort by pubNumber in ASCENDING order (smallest first) publicities.sort((a, b) => a.pubNumber - b.pubNumber); - + // LOG: Debug du tri des publicités - console.log(`📋 PUBLICITES (DEV): Tri par pubNumber croissant - ${publicities.length} résultats:`, - publicities.slice(0, 3).map(p => `N°${p.pubNumber}(${p.designation})`).join(', ') + + console.log(`📋 PUBLICITES (DEV): Tri par pubNumber croissant - ${publicities.length} résultats:`, + publicities.slice(0, 3).map(p => `N°${p.pubNumber}(${p.designation})`).join(', ') + (publicities.length > 3 ? '...' : '') ); - + // Return with empty participations for now (since we're using MemStorage) return publicities.map(pub => ({ ...pub, @@ -4030,17 +4033,17 @@ export class MemStorage implements IStorage { async getPublicity(): Promise { return undefined; } async createPublicity(): Promise { return {} as Publicity; } async updatePublicity(): Promise { return {} as Publicity; } - async deletePublicity(): Promise {} + async deletePublicity(): Promise { } async getPublicityParticipations(): Promise { return []; } - async setPublicityParticipations(): Promise {} + async setPublicityParticipations(): Promise { } async getCustomerOrders(groupIds?: number[]): Promise { let orders = Array.from(this.customerOrders.values()); - + if (groupIds && groupIds.length > 0) { orders = orders.filter(order => groupIds.includes(order.groupId)); } - + return orders.map(order => ({ ...order, supplier: this.suppliers.get(order.supplierId)!, @@ -4052,7 +4055,7 @@ export class MemStorage implements IStorage { async getCustomerOrder(id: number): Promise { const order = this.customerOrders.get(id); if (!order) return undefined; - + return { ...order, supplier: this.suppliers.get(order.supplierId)!, @@ -4082,7 +4085,7 @@ export class MemStorage implements IStorage { async updateCustomerOrder(id: number, orderData: Partial): Promise { const existingOrder = this.customerOrders.get(id); if (!existingOrder) throw new Error('Customer order not found'); - + const updatedOrder = { ...existingOrder, ...orderData, updatedAt: new Date() }; this.customerOrders.set(id, updatedOrder); return updatedOrder; @@ -4094,20 +4097,20 @@ export class MemStorage implements IStorage { async getPendingClientCalls(groupIds?: number[]): Promise { let orders = Array.from(this.customerOrders.values()); - + // Filter by groupIds if provided if (groupIds && groupIds.length > 0) { orders = orders.filter(order => groupIds.includes(order.groupId)); } - + // Find orders where client not notified and status is 'Disponible' const pendingOrders = orders.filter(order => { - return !order.customerNotified && - (order.status === 'Disponible' || order.status === 'disponible' || - order.status === 'Arrivé' || order.status === 'arrivé' || - order.status === 'Prêt' || order.status === 'prêt'); + return !order.customerNotified && + (order.status === 'Disponible' || order.status === 'disponible' || + order.status === 'Arrivé' || order.status === 'arrivé' || + order.status === 'Prêt' || order.status === 'prêt'); }); - + return pendingOrders.map(order => ({ ...order, supplier: this.suppliers.get(order.supplierId)!, @@ -4121,24 +4124,24 @@ export class MemStorage implements IStorage { if (!order) { throw new Error(`CustomerOrder with id ${customerOrderId} not found`); } - + const updatedOrder = { ...order, customerNotified: true, updatedAt: new Date() }; - + this.customerOrders.set(customerOrderId, updatedOrder); return updatedOrder; } async getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; search?: string; }): Promise { let products = Array.from(this.dlcProducts.values()); - + if (groupIds && groupIds.length > 0) { products = products.filter(product => groupIds.includes(product.groupId)); } - + if (filters) { if (filters.status) { products = products.filter(product => product.status === filters.status); @@ -4151,11 +4154,11 @@ export class MemStorage implements IStorage { products = products.filter(product => { const supplier = this.suppliers.get(product.supplierId); return product.productName.toLowerCase().includes(searchTerm) || - (supplier && supplier.name.toLowerCase().includes(searchTerm)); + (supplier && supplier.name.toLowerCase().includes(searchTerm)); }); } } - + return products.map(product => ({ ...product, supplier: this.suppliers.get(product.supplierId)!, @@ -4167,7 +4170,7 @@ export class MemStorage implements IStorage { async getDlcProduct(id: number): Promise { const product = this.dlcProducts.get(id); if (!product) return undefined; - + return { ...product, supplier: this.suppliers.get(product.supplierId)!, @@ -4197,7 +4200,7 @@ export class MemStorage implements IStorage { async updateDlcProduct(id: number, productData: Partial): Promise { const existingProduct = this.dlcProducts.get(id); if (!existingProduct) throw new Error('DLC Product not found'); - + const updatedProduct = { ...existingProduct, ...productData, updatedAt: new Date() }; this.dlcProducts.set(id, updatedProduct); return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend; @@ -4210,13 +4213,13 @@ export class MemStorage implements IStorage { async validateDlcProduct(id: number, validatedBy: string): Promise { const existingProduct = this.dlcProducts.get(id); if (!existingProduct) throw new Error('DLC Product not found'); - - const updatedProduct = { - ...existingProduct, + + const updatedProduct = { + ...existingProduct, validatedBy, validatedAt: new Date(), status: "valides", - updatedAt: new Date() + updatedAt: new Date() }; this.dlcProducts.set(id, updatedProduct); return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend; @@ -4225,13 +4228,13 @@ export class MemStorage implements IStorage { async markDlcProductStockEpuise(id: number, markedBy: string): Promise { const existingProduct = this.dlcProducts.get(id); if (!existingProduct) throw new Error('DLC Product not found'); - - const updatedProduct = { - ...existingProduct, + + const updatedProduct = { + ...existingProduct, stockEpuise: true, stockEpuiseBy: markedBy, stockEpuiseAt: new Date(), - updatedAt: new Date() + updatedAt: new Date() }; this.dlcProducts.set(id, updatedProduct); return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend; @@ -4240,13 +4243,13 @@ export class MemStorage implements IStorage { async restoreDlcProductStock(id: number): Promise { const existingProduct = this.dlcProducts.get(id); if (!existingProduct) throw new Error('DLC Product not found'); - - const updatedProduct = { - ...existingProduct, + + const updatedProduct = { + ...existingProduct, stockEpuise: false, stockEpuiseBy: null, stockEpuiseAt: null, - updatedAt: new Date() + updatedAt: new Date() }; this.dlcProducts.set(id, updatedProduct); return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend; @@ -4255,13 +4258,13 @@ export class MemStorage implements IStorage { async markDlcProductAsProcessed(id: number, processedBy: string): Promise { const existingProduct = this.dlcProducts.get(id); if (!existingProduct) throw new Error('DLC Product not found'); - - const updatedProduct = { - ...existingProduct, + + const updatedProduct = { + ...existingProduct, processedAt: new Date(), processedBy: processedBy, processedUntilExpiry: true, - updatedAt: new Date() + updatedAt: new Date() }; this.dlcProducts.set(id, updatedProduct); return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend; @@ -4270,13 +4273,13 @@ export class MemStorage implements IStorage { async unmarkDlcProductAsProcessed(id: number): Promise { const existingProduct = this.dlcProducts.get(id); if (!existingProduct) throw new Error('DLC Product not found'); - - const updatedProduct = { - ...existingProduct, + + const updatedProduct = { + ...existingProduct, processedAt: null, processedBy: null, processedUntilExpiry: false, - updatedAt: new Date() + updatedAt: new Date() }; this.dlcProducts.set(id, updatedProduct); return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend; @@ -4288,28 +4291,29 @@ export class MemStorage implements IStorage { alertDate.setDate(today.getDate() + 15); let products = Array.from(this.dlcProducts.values()); - + if (groupIds && groupIds.length > 0) { products = products.filter(product => groupIds.includes(product.groupId)); } - + // IMPORTANT: Filtrer sur processedUntilExpiry pour ne pas réafficher les produits déjà traités // Les produits "traités" (processedUntilExpiry=true) ne doivent pas apparaître dans les alertes quotidiennes return { - active: products.filter(p => - new Date(p.expiryDate) > today && - p.status !== 'valides' && - !p.processedUntilExpiry + active: products.filter(p => + new Date(p.expiryDate) > today && + p.status !== 'valides' && + !p.processedUntilExpiry && + !p.stockEpuise ).length, expiringSoon: products.filter(p => { const expiry = new Date(p.expiryDate); const isExpiringSoon = expiry >= today && expiry <= alertDate; - return isExpiringSoon && p.status !== 'valides' && !p.processedUntilExpiry; + return isExpiringSoon && p.status !== 'valides' && !p.processedUntilExpiry && !p.stockEpuise; }).length, expired: products.filter(p => { const expiry = new Date(p.expiryDate); const isExpired = expiry <= today; - return isExpired && p.status !== 'valides' && !p.processedUntilExpiry; + return isExpired && p.status !== 'valides' && !p.processedUntilExpiry && !p.stockEpuise; }).length }; } @@ -4317,13 +4321,13 @@ export class MemStorage implements IStorage { async getTasks(groupIds?: number[], userRole?: string): Promise { const today = new Date(); today.setHours(0, 0, 0, 0); - + let tasks = Array.from(this.tasks.values()); - + if (groupIds && groupIds.length > 0) { tasks = tasks.filter(task => groupIds.includes(task.groupId)); } - + // Filtrage par rôle pour la date de départ if (userRole === 'manager' || userRole === 'employee') { // Managers et employés : seulement les tâches dont la date de départ est atteinte ou passée @@ -4334,7 +4338,7 @@ export class MemStorage implements IStorage { }); } // Admin et directeur voient toutes les tâches, y compris les tâches programmées - + return tasks.map(task => ({ ...task, group: this.groups.get(task.groupId)!, @@ -4346,7 +4350,7 @@ export class MemStorage implements IStorage { async getTask(id: number): Promise { const task = this.tasks.get(id); if (!task) return undefined; - + return { ...task, group: this.groups.get(task.groupId)!, @@ -4394,7 +4398,7 @@ export class MemStorage implements IStorage { createdAt: new Date('2025-01-15'), updatedAt: new Date('2025-01-15'), }; - + const testOrder2: CustomerOrder = { id: 2, supplierId: 1, @@ -4625,7 +4629,7 @@ export class MemStorage implements IStorage { async updateTask(id: number, taskData: Partial): Promise { const existingTask = this.tasks.get(id); if (!existingTask) throw new Error('Task not found'); - + const updatedTask = { ...existingTask, ...taskData, updatedAt: new Date() }; this.tasks.set(id, updatedTask); return updatedTask; @@ -4638,30 +4642,30 @@ export class MemStorage implements IStorage { async completeTask(id: number, completedBy: string): Promise { const existingTask = this.tasks.get(id); if (!existingTask) throw new Error('Task not found'); - - const updatedTask = { - ...existingTask, + + const updatedTask = { + ...existingTask, status: 'completed' as const, completedAt: new Date(), completedBy, - updatedAt: new Date() + updatedAt: new Date() }; this.tasks.set(id, updatedTask); } // SAV operations - async getSavTickets(filters?: { - groupIds?: number[]; - status?: string; - supplierId?: number; + async getSavTickets(filters?: { + groupIds?: number[]; + status?: string; + supplierId?: number; priority?: string; startDate?: string; endDate?: string; }): Promise { const tickets = Array.from(this.savTickets.values()); - + let filteredTickets = tickets; - + if (filters?.groupIds?.length) { filteredTickets = filteredTickets.filter(ticket => filters.groupIds!.includes(ticket.groupId)); } @@ -4676,13 +4680,13 @@ export class MemStorage implements IStorage { } if (filters?.startDate) { const startDate = new Date(filters.startDate); - filteredTickets = filteredTickets.filter(ticket => + filteredTickets = filteredTickets.filter(ticket => ticket.createdAt && ticket.createdAt >= startDate ); } if (filters?.endDate) { const endDate = new Date(filters.endDate); - filteredTickets = filteredTickets.filter(ticket => + filteredTickets = filteredTickets.filter(ticket => ticket.createdAt && ticket.createdAt <= endDate ); } @@ -4701,7 +4705,7 @@ export class MemStorage implements IStorage { const group = this.groups.get(ticket.groupId); const creator = this.users.get(ticket.createdBy); const history = await this.getSavTicketHistory(ticket.id); - + return { ...ticket, supplier: supplier!, @@ -4735,7 +4739,7 @@ export class MemStorage implements IStorage { async createSavTicket(ticketData: InsertSavTicket): Promise { const id = this.idCounters.savTicket++; - + // Generate ticket number const currentYear = new Date().getFullYear(); const ticketNumber = `SAV-${currentYear}-${String(id).padStart(4, '0')}`; @@ -4766,13 +4770,13 @@ export class MemStorage implements IStorage { async updateSavTicket(id: number, ticketData: Partial): Promise { const existingTicket = this.savTickets.get(id); if (!existingTicket) throw new Error('SAV ticket not found'); - - const updatedTicket = { - ...existingTicket, - ...ticketData, - updatedAt: new Date() + + const updatedTicket = { + ...existingTicket, + ...ticketData, + updatedAt: new Date() }; - + // Set resolved/closed dates based on status if (ticketData.status === 'resolu' && !updatedTicket.resolvedAt) { updatedTicket.resolvedAt = new Date(); @@ -4808,7 +4812,7 @@ export class MemStorage implements IStorage { criticalTickets: number; }> { let tickets = Array.from(this.savTickets.values()); - + if (groupIds?.length) { tickets = tickets.filter(ticket => groupIds.includes(ticket.groupId)); } @@ -4816,10 +4820,10 @@ export class MemStorage implements IStorage { const stats = { totalTickets: tickets.length, newTickets: tickets.filter(t => t.status === 'nouveau').length, - inProgressTickets: tickets.filter(t => + inProgressTickets: tickets.filter(t => ['en_cours', 'attente_pieces', 'attente_echange'].includes(t.status) ).length, - resolvedTickets: tickets.filter(t => + resolvedTickets: tickets.filter(t => ['resolu', 'ferme'].includes(t.status) ).length, criticalTickets: tickets.filter(t => t.priority === 'critique').length, @@ -4941,25 +4945,25 @@ export class MemStorage implements IStorage { const histories = [ // Ticket 1 history { ticketId: 1, action: 'created', description: 'Ticket créé avec le statut "nouveau"', createdBy: 'admin', createdAt: new Date('2025-08-10T08:30:00') }, - + // Ticket 2 history { ticketId: 2, action: 'created', description: 'Ticket créé avec le statut "nouveau"', createdBy: 'admin', createdAt: new Date('2025-08-09T14:15:00') }, { ticketId: 2, action: 'status_change', description: 'Statut changé de "nouveau" vers "en_cours"', createdBy: 'admin', createdAt: new Date('2025-08-12T10:20:00') }, { ticketId: 2, action: 'comment', description: 'Contact avec le client pour confirmer les pièces manquantes. Commande des accessoires en cours.', createdBy: 'admin', createdAt: new Date('2025-08-12T10:25:00') }, - + // Ticket 3 history { ticketId: 3, action: 'created', description: 'Ticket créé avec le statut "nouveau"', createdBy: 'admin', createdAt: new Date('2025-08-08T09:45:00') }, { ticketId: 3, action: 'status_change', description: 'Statut changé de "nouveau" vers "en_cours"', createdBy: 'admin', createdAt: new Date('2025-08-10T14:00:00') }, { ticketId: 3, action: 'status_change', description: 'Statut changé de "en_cours" vers "attente_pieces"', createdBy: 'admin', createdAt: new Date('2025-08-13T16:30:00') }, { ticketId: 3, action: 'comment', description: 'Écran de remplacement commandé. Délai annoncé de 5-7 jours ouvrables.', createdBy: 'admin', createdAt: new Date('2025-08-13T16:35:00') }, - + // Ticket 4 history { ticketId: 4, action: 'created', description: 'Ticket créé avec le statut "nouveau"', createdBy: 'admin', createdAt: new Date('2025-08-05T11:20:00') }, { ticketId: 4, action: 'status_change', description: 'Statut changé de "nouveau" vers "en_cours"', createdBy: 'admin', createdAt: new Date('2025-08-06T09:00:00') }, { ticketId: 4, action: 'comment', description: 'Diagnostic effectué : problème de connectivité Bluetooth. Réparation en cours.', createdBy: 'admin', createdAt: new Date('2025-08-07T15:30:00') }, { ticketId: 4, action: 'status_change', description: 'Statut changé de "en_cours" vers "resolu"', createdBy: 'admin', createdAt: new Date('2025-08-14T08:00:00') }, { ticketId: 4, action: 'comment', description: 'Casque réparé avec succès. Module Bluetooth remplacé. Tests complets effectués.', createdBy: 'admin', createdAt: new Date('2025-08-14T08:05:00') }, - + // Ticket 5 history { ticketId: 5, action: 'created', description: 'Ticket créé avec le statut "nouveau"', createdBy: 'admin', createdAt: new Date('2025-08-14T07:30:00') }, { ticketId: 5, action: 'status_change', description: 'Statut changé de "nouveau" vers "en_cours"', createdBy: 'admin', createdAt: new Date('2025-08-14T07:30:00') }, @@ -4973,7 +4977,7 @@ export class MemStorage implements IStorage { // id, // ...historyEntry, // }; - + // const existingHistories = this.savTicketHistories.get(historyEntry.ticketId) || []; // existingHistories.push(history); // this.savTicketHistories.set(historyEntry.ticketId, existingHistories); @@ -5150,7 +5154,7 @@ export class MemStorage implements IStorage { // Avoir operations async getAvoirs(groupIds?: number[]): Promise { let avoirs = Array.from(this.avoirs.values()); - + // Filter by groups if provided if (groupIds && groupIds.length > 0) { avoirs = avoirs.filter(avoir => groupIds.includes(avoir.groupId)); @@ -5169,7 +5173,7 @@ export class MemStorage implements IStorage { async getAvoir(id: number): Promise { const avoir = this.avoirs.get(id); if (!avoir) return undefined; - + return { ...avoir, supplier: this.suppliers.get(avoir.supplierId)!, @@ -5198,7 +5202,7 @@ export class MemStorage implements IStorage { async updateAvoir(id: number, avoirData: Partial): Promise { const existingAvoir = this.avoirs.get(id); if (!existingAvoir) throw new Error('Avoir not found'); - + const updatedAvoir = { ...existingAvoir, ...avoirData, @@ -5241,14 +5245,14 @@ export class MemStorage implements IStorage { const delivery = this.deliveries.get(comment.deliveryId); const author = this.users.get(comment.authorId); const group = this.groups.get(comment.groupId); - + if (!delivery || !author || !group) { throw new Error('Missing related data for reconciliation comment'); } const supplier = this.suppliers.get(delivery.supplierId); const creator = this.users.get(delivery.createdBy); - + if (!supplier || !creator) { throw new Error('Missing delivery related data for reconciliation comment'); } @@ -5274,14 +5278,14 @@ export class MemStorage implements IStorage { const delivery = this.deliveries.get(comment.deliveryId); const author = this.users.get(comment.authorId); const group = this.groups.get(comment.groupId); - + if (!delivery || !author || !group) { throw new Error('Missing related data for reconciliation comment'); } const supplier = this.suppliers.get(delivery.supplierId); const creator = this.users.get(delivery.createdBy); - + if (!supplier || !creator) { throw new Error('Missing delivery related data for reconciliation comment'); } @@ -5314,7 +5318,7 @@ export class MemStorage implements IStorage { async updateReconciliationComment(id: number, commentData: Partial): Promise { const existingComment = this.reconciliationComments.get(id); if (!existingComment) throw new Error('Reconciliation comment not found'); - + const updatedComment = { ...existingComment, ...commentData, @@ -5347,7 +5351,7 @@ export class MemStorage implements IStorage { // Simple implementation for development const orders = Array.from(this.orders.values()); const deliveries = Array.from(this.deliveries.values()); - + // Apply filters const filteredOrders = orders.filter(order => { if (filters.startDate) { @@ -5382,11 +5386,11 @@ export class MemStorage implements IStorage { // Calculate metrics const totalOrders = filteredOrders.length; const totalDeliveries = filteredDeliveries.length; - const reconciledDeliveries = filteredDeliveries.filter(d => + const reconciledDeliveries = filteredDeliveries.filter(d => d.reconciled === true || d.reconciled === 1 ).length; const reconciliationRate = totalDeliveries > 0 ? (reconciledDeliveries / totalDeliveries) * 100 : 0; - + const totalAmount = filteredDeliveries.reduce((sum, d) => { return sum + (parseFloat(d.blAmount as any) || 0) + (parseFloat(d.invoiceAmount as any) || 0); }, 0); @@ -5449,14 +5453,14 @@ export class MemStorage implements IStorage { granularity?: 'day' | 'week' | 'month'; }): Promise> { const dataMap = new Map(); - + // Process orders Array.from(this.orders.values()).forEach(order => { if (filters.startDate && order.plannedDate < filters.startDate) return; if (filters.endDate && order.plannedDate > filters.endDate) return; if (filters.supplierIds?.length && !filters.supplierIds.includes(order.supplierId)) return; if (filters.groupIds?.length && !filters.groupIds.includes(order.groupId)) return; - + const dateKey = order.plannedDate.toISOString().split('T')[0]; const existing = dataMap.get(dateKey) || { orders: 0, deliveries: 0 }; existing.orders++; @@ -5469,7 +5473,7 @@ export class MemStorage implements IStorage { if (filters.endDate && delivery.scheduledDate > filters.endDate) return; if (filters.supplierIds?.length && !filters.supplierIds.includes(delivery.supplierId)) return; if (filters.groupIds?.length && !filters.groupIds.includes(delivery.groupId)) return; - + const dateKey = delivery.scheduledDate.toISOString().split('T')[0]; const existing = dataMap.get(dateKey) || { orders: 0, deliveries: 0 }; existing.deliveries++; @@ -5487,12 +5491,12 @@ export class MemStorage implements IStorage { groupIds?: number[]; }): Promise> { const supplierStats = new Map(); - + Array.from(this.deliveries.values()).forEach(delivery => { if (filters.startDate && delivery.scheduledDate < filters.startDate) return; if (filters.endDate && delivery.scheduledDate > filters.endDate) return; if (filters.groupIds?.length && !filters.groupIds.includes(delivery.groupId)) return; - + const supplier = this.suppliers.get(delivery.supplierId); if (supplier) { const stats = supplierStats.get(delivery.supplierId) || { name: supplier.name, deliveries: 0, amount: 0 }; @@ -5513,13 +5517,13 @@ export class MemStorage implements IStorage { supplierIds?: number[]; }): Promise> { const storeStats = new Map(); - + // Count orders Array.from(this.orders.values()).forEach(order => { if (filters.startDate && order.plannedDate < filters.startDate) return; if (filters.endDate && order.plannedDate > filters.endDate) return; if (filters.supplierIds?.length && !filters.supplierIds.includes(order.supplierId)) return; - + const group = this.groups.get(order.groupId); if (group) { const stats = storeStats.get(order.groupId) || { name: group.name, orders: 0, deliveries: 0 }; @@ -5533,7 +5537,7 @@ export class MemStorage implements IStorage { if (filters.startDate && delivery.scheduledDate < filters.startDate) return; if (filters.endDate && delivery.scheduledDate > filters.endDate) return; if (filters.supplierIds?.length && !filters.supplierIds.includes(delivery.supplierId)) return; - + const group = this.groups.get(delivery.groupId); if (group) { const stats = storeStats.get(delivery.groupId) || { name: group.name, orders: 0, deliveries: 0 };