mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-19 21:18:25 +02:00
3 files changed
+77
-31
No files matched your search
@@ -82,6 +82,7 @@ export function DlcAlertModal({ isOpen, onClose, dlcStats, selectedStoreId }: Dl
|
|||||||
});
|
});
|
||||||
// Refresh DLC stats and products
|
// Refresh DLC stats and products
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"] });
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
+45
-4
@@ -5,7 +5,7 @@ import path from 'path';
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { eq, desc } from "drizzle-orm";
|
import { eq, desc } from "drizzle-orm";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { databaseBackups, utilities } from "@shared/schema";
|
import { databaseBackups, utilities, users } from "@shared/schema";
|
||||||
import type { DatabaseBackup, InsertDatabaseBackup } from "@shared/schema";
|
import type { DatabaseBackup, InsertDatabaseBackup } from "@shared/schema";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
@@ -70,6 +70,31 @@ export class BackupService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a valid user ID for system-initiated backups.
|
||||||
|
* Returns the first admin user ID, or the first user if no admin exists.
|
||||||
|
* This avoids FK violations on database_backups.created_by -> users.id.
|
||||||
|
*/
|
||||||
|
private async resolveSystemUserId(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
// Try to find an admin user first
|
||||||
|
const [adminUser] = await db.select({ id: users.id })
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.role, 'admin'))
|
||||||
|
.limit(1);
|
||||||
|
if (adminUser) return adminUser.id;
|
||||||
|
|
||||||
|
// Fallback: any existing user
|
||||||
|
const [anyUser] = await db.select({ id: users.id })
|
||||||
|
.from(users)
|
||||||
|
.limit(1);
|
||||||
|
return anyUser?.id ?? null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to resolve system user ID for backup:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async createBackup(type: 'manual' | 'automatic' = 'manual', createdBy: string = 'system'): Promise<DatabaseBackup> {
|
async createBackup(type: 'manual' | 'automatic' = 'manual', createdBy: string = 'system'): Promise<DatabaseBackup> {
|
||||||
try {
|
try {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
@@ -240,10 +265,20 @@ export class BackupService {
|
|||||||
const lastBackup = existingBackupToday[0];
|
const lastBackup = existingBackupToday[0];
|
||||||
const lastBackupDate = lastBackup ? lastBackup.createdAt.toISOString().split('T')[0] : null;
|
const lastBackupDate = lastBackup ? lastBackup.createdAt.toISOString().split('T')[0] : null;
|
||||||
|
|
||||||
// Si aucune sauvegarde automatique aujourd'hui, en créer une
|
// Only perform backup if no automatic backup exists for today
|
||||||
if (lastBackupDate !== today) {
|
if (lastBackupDate !== today) {
|
||||||
|
// Resolve a real user ID if called with default 'system'
|
||||||
|
let resolvedUserId = userId;
|
||||||
|
if (userId === 'system') {
|
||||||
|
const systemId = await this.resolveSystemUserId();
|
||||||
|
if (!systemId) {
|
||||||
|
console.warn('⚠️ No user found in database — skipping daily backup');
|
||||||
|
return { backupPerformed: false, message: 'Aucun utilisateur trouvé pour la sauvegarde' };
|
||||||
|
}
|
||||||
|
resolvedUserId = systemId;
|
||||||
|
}
|
||||||
console.log('🔄 Première connexion du jour - Création de la sauvegarde automatique...');
|
console.log('🔄 Première connexion du jour - Création de la sauvegarde automatique...');
|
||||||
await this.createBackup('automatic', userId);
|
await this.createBackup('automatic', resolvedUserId);
|
||||||
console.log('✅ Sauvegarde quotidienne effectuée avec succès');
|
console.log('✅ Sauvegarde quotidienne effectuée avec succès');
|
||||||
return {
|
return {
|
||||||
backupPerformed: true,
|
backupPerformed: true,
|
||||||
@@ -298,8 +333,14 @@ export class BackupService {
|
|||||||
|
|
||||||
// Only proceed if no automatic backup exists for today
|
// Only proceed if no automatic backup exists for today
|
||||||
if (lastBackupDate !== today) {
|
if (lastBackupDate !== today) {
|
||||||
|
// Resolve a real user ID to satisfy FK constraint
|
||||||
|
const systemUserId = await this.resolveSystemUserId();
|
||||||
|
if (!systemUserId) {
|
||||||
|
console.warn('⚠️ No user found in database — skipping automatic backup');
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log('🔄 Starting automatic backup...');
|
console.log('🔄 Starting automatic backup...');
|
||||||
await this.createBackup('automatic', 'system');
|
await this.createBackup('automatic', systemUserId);
|
||||||
this.lastAutomaticBackupDate = today;
|
this.lastAutomaticBackupDate = today;
|
||||||
console.log('✅ Automatic backup completed');
|
console.log('✅ Automatic backup completed');
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-27
@@ -1520,20 +1520,20 @@ export class DatabaseStorage implements IStorage {
|
|||||||
// LOG: Debug des publicités récupérées
|
// LOG: Debug des publicités récupérées
|
||||||
console.log(`📋 PUBLICITES FETCHED: ${results.length} résultats pour année ${year || 'toutes'}`);
|
console.log(`📋 PUBLICITES FETCHED: ${results.length} résultats pour année ${year || 'toutes'}`);
|
||||||
if (results.length > 0) {
|
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 publicityIds = results.map((p: any) => p.id);
|
||||||
const participations = publicityIds.length > 0
|
const participations = publicityIds.length > 0
|
||||||
? await db
|
? await db
|
||||||
.select({
|
.select({
|
||||||
publicityId: publicityParticipations.publicityId,
|
publicityId: publicityParticipations.publicityId,
|
||||||
groupId: publicityParticipations.groupId,
|
groupId: publicityParticipations.groupId,
|
||||||
group: groups,
|
group: groups,
|
||||||
})
|
})
|
||||||
.from(publicityParticipations)
|
.from(publicityParticipations)
|
||||||
.leftJoin(groups, eq(publicityParticipations.groupId, groups.id))
|
.leftJoin(groups, eq(publicityParticipations.groupId, groups.id))
|
||||||
.where(inArray(publicityParticipations.publicityId, publicityIds))
|
.where(inArray(publicityParticipations.publicityId, publicityIds))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Sort by pubNumber as integer on the server side for consistency
|
// Sort by pubNumber as integer on the server side for consistency
|
||||||
@@ -2127,16 +2127,19 @@ export class DatabaseStorage implements IStorage {
|
|||||||
WHEN ${dlcProducts.expiryDate} > ${today.toISOString().split('T')[0]}
|
WHEN ${dlcProducts.expiryDate} > ${today.toISOString().split('T')[0]}
|
||||||
AND ${dlcProducts.status} != 'valides'
|
AND ${dlcProducts.status} != 'valides'
|
||||||
AND (${dlcProducts.processedUntilExpiry} IS NULL OR ${dlcProducts.processedUntilExpiry} = false)
|
AND (${dlcProducts.processedUntilExpiry} IS NULL OR ${dlcProducts.processedUntilExpiry} = false)
|
||||||
|
AND (${dlcProducts.stockEpuise} IS NULL OR ${dlcProducts.stockEpuise} = false)
|
||||||
THEN 1 END)`,
|
THEN 1 END)`,
|
||||||
expiringSoon: sql<number>`COUNT(CASE
|
expiringSoon: sql<number>`COUNT(CASE
|
||||||
WHEN ${dlcProducts.expiryDate} BETWEEN ${today.toISOString().split('T')[0]} AND ${alertDate.toISOString().split('T')[0]}
|
WHEN ${dlcProducts.expiryDate} BETWEEN ${today.toISOString().split('T')[0]} AND ${alertDate.toISOString().split('T')[0]}
|
||||||
AND ${dlcProducts.status} != 'valides'
|
AND ${dlcProducts.status} != 'valides'
|
||||||
AND (${dlcProducts.processedUntilExpiry} IS NULL OR ${dlcProducts.processedUntilExpiry} = false)
|
AND (${dlcProducts.processedUntilExpiry} IS NULL OR ${dlcProducts.processedUntilExpiry} = false)
|
||||||
|
AND (${dlcProducts.stockEpuise} IS NULL OR ${dlcProducts.stockEpuise} = false)
|
||||||
THEN 1 END)`,
|
THEN 1 END)`,
|
||||||
expired: sql<number>`COUNT(CASE
|
expired: sql<number>`COUNT(CASE
|
||||||
WHEN ${dlcProducts.expiryDate} <= ${today.toISOString().split('T')[0]}
|
WHEN ${dlcProducts.expiryDate} <= ${today.toISOString().split('T')[0]}
|
||||||
AND ${dlcProducts.status} != 'valides'
|
AND ${dlcProducts.status} != 'valides'
|
||||||
AND (${dlcProducts.processedUntilExpiry} IS NULL OR ${dlcProducts.processedUntilExpiry} = false)
|
AND (${dlcProducts.processedUntilExpiry} IS NULL OR ${dlcProducts.processedUntilExpiry} = false)
|
||||||
|
AND (${dlcProducts.stockEpuise} IS NULL OR ${dlcProducts.stockEpuise} = false)
|
||||||
THEN 1 END)`
|
THEN 1 END)`
|
||||||
})
|
})
|
||||||
.from(dlcProducts)
|
.from(dlcProducts)
|
||||||
@@ -3075,8 +3078,8 @@ export class DatabaseStorage implements IStorage {
|
|||||||
count: sql<number>`COUNT(${deliveries.id})`,
|
count: sql<number>`COUNT(${deliveries.id})`,
|
||||||
amount: sql<number>`COALESCE(SUM(CAST(${deliveries.blAmount} AS NUMERIC)), 0)`
|
amount: sql<number>`COALESCE(SUM(CAST(${deliveries.blAmount} AS NUMERIC)), 0)`
|
||||||
})
|
})
|
||||||
.from(deliveries)
|
.from(deliveries)
|
||||||
.innerJoin(suppliers, eq(deliveries.supplierId, suppliers.id));
|
.innerJoin(suppliers, eq(deliveries.supplierId, suppliers.id));
|
||||||
|
|
||||||
if (deliveryConditions.length) supplierQuery.where(and(...deliveryConditions));
|
if (deliveryConditions.length) supplierQuery.where(and(...deliveryConditions));
|
||||||
const topSuppliers = await supplierQuery
|
const topSuppliers = await supplierQuery
|
||||||
@@ -3091,9 +3094,9 @@ export class DatabaseStorage implements IStorage {
|
|||||||
orders: sql<number>`COUNT(DISTINCT ${orders.id})`,
|
orders: sql<number>`COUNT(DISTINCT ${orders.id})`,
|
||||||
deliveries: sql<number>`COUNT(DISTINCT ${deliveries.id})`
|
deliveries: sql<number>`COUNT(DISTINCT ${deliveries.id})`
|
||||||
})
|
})
|
||||||
.from(groups)
|
.from(groups)
|
||||||
.leftJoin(orders, eq(groups.id, orders.groupId))
|
.leftJoin(orders, eq(groups.id, orders.groupId))
|
||||||
.leftJoin(deliveries, eq(groups.id, deliveries.groupId));
|
.leftJoin(deliveries, eq(groups.id, deliveries.groupId));
|
||||||
|
|
||||||
if (filters.groupIds?.length) {
|
if (filters.groupIds?.length) {
|
||||||
storeQuery.where(inArray(groups.id, filters.groupIds));
|
storeQuery.where(inArray(groups.id, filters.groupIds));
|
||||||
@@ -3137,7 +3140,7 @@ export class DatabaseStorage implements IStorage {
|
|||||||
}): Promise<Array<{ date: string; orders: number; deliveries: number }>> {
|
}): Promise<Array<{ date: string; orders: number; deliveries: number }>> {
|
||||||
const granularity = filters.granularity || 'day';
|
const granularity = filters.granularity || 'day';
|
||||||
const dateFormat = granularity === 'day' ? 'YYYY-MM-DD' :
|
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
|
// Build WHERE clauses with direct value substitution
|
||||||
const orderConditions: string[] = [];
|
const orderConditions: string[] = [];
|
||||||
@@ -3226,8 +3229,8 @@ export class DatabaseStorage implements IStorage {
|
|||||||
deliveries: sql<number>`COUNT(${deliveries.id})`,
|
deliveries: sql<number>`COUNT(${deliveries.id})`,
|
||||||
amount: sql<number>`COALESCE(SUM(CAST(${deliveries.blAmount} AS NUMERIC)), 0)`
|
amount: sql<number>`COALESCE(SUM(CAST(${deliveries.blAmount} AS NUMERIC)), 0)`
|
||||||
})
|
})
|
||||||
.from(deliveries)
|
.from(deliveries)
|
||||||
.innerJoin(suppliers, eq(deliveries.supplierId, suppliers.id));
|
.innerJoin(suppliers, eq(deliveries.supplierId, suppliers.id));
|
||||||
|
|
||||||
if (conditions.length) query.where(and(...conditions));
|
if (conditions.length) query.where(and(...conditions));
|
||||||
|
|
||||||
@@ -3980,7 +3983,7 @@ export class MemStorage implements IStorage {
|
|||||||
|
|
||||||
async getUserGroups(): Promise<UserGroup[]> { return []; }
|
async getUserGroups(): Promise<UserGroup[]> { return []; }
|
||||||
async assignUserToGroup(): Promise<UserGroup> { return {} as UserGroup; }
|
async assignUserToGroup(): Promise<UserGroup> { return {} as UserGroup; }
|
||||||
async removeUserFromGroup(): Promise<void> {}
|
async removeUserFromGroup(): Promise<void> { }
|
||||||
|
|
||||||
async getMonthlyStats(): Promise<any> {
|
async getMonthlyStats(): Promise<any> {
|
||||||
return {
|
return {
|
||||||
@@ -4030,9 +4033,9 @@ export class MemStorage implements IStorage {
|
|||||||
async getPublicity(): Promise<PublicityWithRelations | undefined> { return undefined; }
|
async getPublicity(): Promise<PublicityWithRelations | undefined> { return undefined; }
|
||||||
async createPublicity(): Promise<Publicity> { return {} as Publicity; }
|
async createPublicity(): Promise<Publicity> { return {} as Publicity; }
|
||||||
async updatePublicity(): Promise<Publicity> { return {} as Publicity; }
|
async updatePublicity(): Promise<Publicity> { return {} as Publicity; }
|
||||||
async deletePublicity(): Promise<void> {}
|
async deletePublicity(): Promise<void> { }
|
||||||
async getPublicityParticipations(): Promise<PublicityParticipation[]> { return []; }
|
async getPublicityParticipations(): Promise<PublicityParticipation[]> { return []; }
|
||||||
async setPublicityParticipations(): Promise<void> {}
|
async setPublicityParticipations(): Promise<void> { }
|
||||||
|
|
||||||
async getCustomerOrders(groupIds?: number[]): Promise<CustomerOrderWithRelations[]> {
|
async getCustomerOrders(groupIds?: number[]): Promise<CustomerOrderWithRelations[]> {
|
||||||
let orders = Array.from(this.customerOrders.values());
|
let orders = Array.from(this.customerOrders.values());
|
||||||
@@ -4103,9 +4106,9 @@ export class MemStorage implements IStorage {
|
|||||||
// Find orders where client not notified and status is 'Disponible'
|
// Find orders where client not notified and status is 'Disponible'
|
||||||
const pendingOrders = orders.filter(order => {
|
const pendingOrders = orders.filter(order => {
|
||||||
return !order.customerNotified &&
|
return !order.customerNotified &&
|
||||||
(order.status === 'Disponible' || order.status === 'disponible' ||
|
(order.status === 'Disponible' || order.status === 'disponible' ||
|
||||||
order.status === 'Arrivé' || order.status === 'arrivé' ||
|
order.status === 'Arrivé' || order.status === 'arrivé' ||
|
||||||
order.status === 'Prêt' || order.status === 'prêt');
|
order.status === 'Prêt' || order.status === 'prêt');
|
||||||
});
|
});
|
||||||
|
|
||||||
return pendingOrders.map(order => ({
|
return pendingOrders.map(order => ({
|
||||||
@@ -4151,7 +4154,7 @@ export class MemStorage implements IStorage {
|
|||||||
products = products.filter(product => {
|
products = products.filter(product => {
|
||||||
const supplier = this.suppliers.get(product.supplierId);
|
const supplier = this.suppliers.get(product.supplierId);
|
||||||
return product.productName.toLowerCase().includes(searchTerm) ||
|
return product.productName.toLowerCase().includes(searchTerm) ||
|
||||||
(supplier && supplier.name.toLowerCase().includes(searchTerm));
|
(supplier && supplier.name.toLowerCase().includes(searchTerm));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4299,17 +4302,18 @@ export class MemStorage implements IStorage {
|
|||||||
active: products.filter(p =>
|
active: products.filter(p =>
|
||||||
new Date(p.expiryDate) > today &&
|
new Date(p.expiryDate) > today &&
|
||||||
p.status !== 'valides' &&
|
p.status !== 'valides' &&
|
||||||
!p.processedUntilExpiry
|
!p.processedUntilExpiry &&
|
||||||
|
!p.stockEpuise
|
||||||
).length,
|
).length,
|
||||||
expiringSoon: products.filter(p => {
|
expiringSoon: products.filter(p => {
|
||||||
const expiry = new Date(p.expiryDate);
|
const expiry = new Date(p.expiryDate);
|
||||||
const isExpiringSoon = expiry >= today && expiry <= alertDate;
|
const isExpiringSoon = expiry >= today && expiry <= alertDate;
|
||||||
return isExpiringSoon && p.status !== 'valides' && !p.processedUntilExpiry;
|
return isExpiringSoon && p.status !== 'valides' && !p.processedUntilExpiry && !p.stockEpuise;
|
||||||
}).length,
|
}).length,
|
||||||
expired: products.filter(p => {
|
expired: products.filter(p => {
|
||||||
const expiry = new Date(p.expiryDate);
|
const expiry = new Date(p.expiryDate);
|
||||||
const isExpired = expiry <= today;
|
const isExpired = expiry <= today;
|
||||||
return isExpired && p.status !== 'valides' && !p.processedUntilExpiry;
|
return isExpired && p.status !== 'valides' && !p.processedUntilExpiry && !p.stockEpuise;
|
||||||
}).length
|
}).length
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user