mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-12 01:37:40 +02:00
Adjust the advertisement retrieval to use direct column ordering instead of SQL CAST for better production compatibility and logging. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 0ec86cdf-a5be-4b0e-84a3-07b926f5cc80 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/0ec86cdf-a5be-4b0e-84a3-07b926f5cc80/0gylnen
3872 lines
130 KiB
TypeScript
3872 lines
130 KiB
TypeScript
import {
|
||
users,
|
||
groups,
|
||
suppliers,
|
||
orders,
|
||
deliveries,
|
||
userGroups,
|
||
publicities,
|
||
publicityParticipations,
|
||
customerOrders,
|
||
dlcProducts,
|
||
tasks,
|
||
dashboardMessages,
|
||
announcements,
|
||
nocodbConfig,
|
||
invoiceVerificationCache,
|
||
savTickets,
|
||
savTicketHistory,
|
||
weatherData,
|
||
weatherSettings,
|
||
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,
|
||
type CustomerOrder,
|
||
type InsertCustomerOrder,
|
||
type CustomerOrderWithRelations,
|
||
type DlcProduct,
|
||
type InsertDlcProduct,
|
||
type DlcProductFrontend,
|
||
type InsertDlcProductFrontend,
|
||
type DlcProductWithRelations,
|
||
type Task,
|
||
type InsertTask,
|
||
type TaskWithRelations,
|
||
type Announcement,
|
||
type InsertAnnouncement,
|
||
type AnnouncementWithRelations,
|
||
type NocodbConfig,
|
||
type InsertNocodbConfig,
|
||
type InvoiceVerificationCache,
|
||
type InsertInvoiceVerificationCache,
|
||
type SavTicket,
|
||
type InsertSavTicket,
|
||
type SavTicketHistory,
|
||
type InsertSavTicketHistory,
|
||
type SavTicketWithRelations,
|
||
type SavTicketHistoryWithCreator,
|
||
type WeatherData,
|
||
type InsertWeatherData,
|
||
type WeatherSettings,
|
||
type InsertWeatherSettings,
|
||
} from "@shared/schema";
|
||
import { db } from "./db";
|
||
import { eq, and, inArray, desc, sql, gte, lte, lt, or, isNull, isNotNull, asc } from "drizzle-orm";
|
||
import { getAnnouncementStorage } from "./announcementStorage";
|
||
|
||
|
||
export interface IStorage {
|
||
// User operations
|
||
getUser(id: string): Promise<User | undefined>;
|
||
getUserByEmail(email: string): Promise<User | undefined>;
|
||
getUserByUsername(username: string): Promise<User | undefined>;
|
||
upsertUser(user: UpsertUser): Promise<User>;
|
||
getUserWithGroups(id: string): Promise<UserWithGroups | undefined>;
|
||
getUsers(): Promise<User[]>;
|
||
createUser(user: UpsertUser): Promise<User>;
|
||
updateUser(id: string, user: Partial<UpsertUser>): Promise<User>;
|
||
deleteUser(id: string): Promise<void>;
|
||
|
||
// Group operations
|
||
getGroups(): Promise<Group[]>;
|
||
getGroup(id: number): Promise<Group | undefined>;
|
||
createGroup(group: InsertGroup): Promise<Group>;
|
||
updateGroup(id: number, group: Partial<InsertGroup>): Promise<Group>;
|
||
deleteGroup(id: number): Promise<void>;
|
||
|
||
// Supplier operations
|
||
getSuppliers(): Promise<Supplier[]>;
|
||
createSupplier(supplier: InsertSupplier): Promise<Supplier>;
|
||
updateSupplier(id: number, supplier: Partial<InsertSupplier>): Promise<Supplier>;
|
||
deleteSupplier(id: number): Promise<void>;
|
||
|
||
// Order operations
|
||
getOrders(groupIds?: number[]): Promise<OrderWithRelations[]>;
|
||
getOrdersByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise<OrderWithRelations[]>;
|
||
getOrder(id: number): Promise<OrderWithRelations | undefined>;
|
||
createOrder(order: InsertOrder): Promise<Order>;
|
||
updateOrder(id: number, order: Partial<InsertOrder>): Promise<Order>;
|
||
deleteOrder(id: number): Promise<void>;
|
||
|
||
// Delivery operations
|
||
getDeliveries(groupIds?: number[]): Promise<DeliveryWithRelations[]>;
|
||
getDeliveriesByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise<DeliveryWithRelations[]>;
|
||
getDelivery(id: number): Promise<DeliveryWithRelations | undefined>;
|
||
createDelivery(delivery: InsertDelivery): Promise<Delivery>;
|
||
updateDelivery(id: number, delivery: Partial<InsertDelivery>): Promise<Delivery>;
|
||
deleteDelivery(id: number): Promise<void>;
|
||
validateDelivery(id: number, blData?: { blNumber: string; blAmount: number }): Promise<void>;
|
||
|
||
// User-Group operations
|
||
getUserGroups(userId: string): Promise<UserGroup[]>;
|
||
assignUserToGroup(userGroup: InsertUserGroup): Promise<UserGroup>;
|
||
removeUserFromGroup(userId: string, groupId: number): Promise<void>;
|
||
|
||
// 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<PublicityWithRelations[]>;
|
||
getPublicity(id: number): Promise<PublicityWithRelations | undefined>;
|
||
createPublicity(publicity: InsertPublicity): Promise<Publicity>;
|
||
updatePublicity(id: number, publicity: Partial<InsertPublicity>): Promise<Publicity>;
|
||
deletePublicity(id: number): Promise<void>;
|
||
|
||
// Publicity participation operations
|
||
getPublicityParticipations(publicityId: number): Promise<PublicityParticipation[]>;
|
||
setPublicityParticipations(publicityId: number, groupIds: number[]): Promise<void>;
|
||
|
||
// NocoDB Configuration operations
|
||
getNocodbConfigs(): Promise<NocodbConfig[]>;
|
||
getNocodbConfig(id: number): Promise<NocodbConfig | undefined>;
|
||
getActiveNocodbConfig(): Promise<NocodbConfig | undefined>;
|
||
createNocodbConfig(config: InsertNocodbConfig): Promise<NocodbConfig>;
|
||
updateNocodbConfig(id: number, config: Partial<InsertNocodbConfig>): Promise<NocodbConfig>;
|
||
deleteNocodbConfig(id: number): Promise<void>;
|
||
|
||
// Invoice Verification Cache operations
|
||
getInvoiceVerificationCache(cacheKey: string): Promise<InvoiceVerificationCache | undefined>;
|
||
saveInvoiceVerificationCache(cache: InsertInvoiceVerificationCache): Promise<InvoiceVerificationCache>;
|
||
createInvoiceVerificationCache(cache: InsertInvoiceVerificationCache): Promise<InvoiceVerificationCache>;
|
||
clearExpiredCache(): Promise<void>;
|
||
|
||
// Customer Order operations
|
||
getCustomerOrders(groupIds?: number[]): Promise<CustomerOrderWithRelations[]>;
|
||
getCustomerOrder(id: number): Promise<CustomerOrderWithRelations | undefined>;
|
||
createCustomerOrder(customerOrder: InsertCustomerOrder): Promise<CustomerOrder>;
|
||
updateCustomerOrder(id: number, customerOrder: Partial<InsertCustomerOrder>): Promise<CustomerOrder>;
|
||
deleteCustomerOrder(id: number): Promise<void>;
|
||
|
||
// DLC Product operations
|
||
getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; search?: string; }): Promise<DlcProductWithRelations[]>;
|
||
getDlcProduct(id: number): Promise<DlcProductWithRelations | undefined>;
|
||
createDlcProduct(dlcProduct: InsertDlcProduct): Promise<DlcProductFrontend>;
|
||
updateDlcProduct(id: number, dlcProduct: Partial<InsertDlcProduct>): Promise<DlcProductFrontend>;
|
||
deleteDlcProduct(id: number): Promise<void>;
|
||
validateDlcProduct(id: number, validatedBy: string): Promise<DlcProductFrontend>;
|
||
markDlcProductStockEpuise(id: number, markedBy: string): Promise<DlcProductFrontend>;
|
||
restoreDlcProductStock(id: number): Promise<DlcProductFrontend>;
|
||
getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }>;
|
||
|
||
// Task operations
|
||
getTasks(groupIds?: number[], userRole?: string): Promise<any[]>;
|
||
getTask(id: number): Promise<any | undefined>;
|
||
createTask(task: InsertTask): Promise<Task>;
|
||
updateTask(id: number, task: Partial<InsertTask>): Promise<Task>;
|
||
deleteTask(id: number): Promise<void>;
|
||
completeTask(id: number, completedBy?: string): Promise<void>;
|
||
|
||
// Announcement operations
|
||
createAnnouncement(announcement: InsertAnnouncement): Promise<Announcement>;
|
||
getAnnouncements(groupIds?: number[]): Promise<AnnouncementWithRelations[]>;
|
||
getAnnouncement(id: number): Promise<AnnouncementWithRelations | undefined>;
|
||
updateAnnouncement(id: number, announcement: Partial<InsertAnnouncement>): Promise<AnnouncementWithRelations>;
|
||
deleteAnnouncement(id: number): Promise<boolean>;
|
||
|
||
// SAV operations
|
||
getSavTickets(filters?: {
|
||
groupIds?: number[];
|
||
status?: string;
|
||
supplierId?: number;
|
||
priority?: string;
|
||
startDate?: string;
|
||
endDate?: string;
|
||
}): Promise<SavTicketWithRelations[]>;
|
||
getSavTicket(id: number): Promise<SavTicketWithRelations | undefined>;
|
||
createSavTicket(ticket: InsertSavTicket): Promise<SavTicket>;
|
||
updateSavTicket(id: number, ticket: Partial<InsertSavTicket>): Promise<SavTicket>;
|
||
deleteSavTicket(id: number): Promise<void>;
|
||
getSavTicketHistory(ticketId: number): Promise<SavTicketHistoryWithCreator[]>;
|
||
addSavTicketHistory(history: InsertSavTicketHistory): Promise<SavTicketHistory>;
|
||
getSavTicketStats(groupIds?: number[]): Promise<{
|
||
totalTickets: number;
|
||
newTickets: number;
|
||
inProgressTickets: number;
|
||
resolvedTickets: number;
|
||
criticalTickets: number;
|
||
}>;
|
||
|
||
// Weather operations
|
||
getWeatherSettings(): Promise<WeatherSettings | undefined>;
|
||
createWeatherSettings(settings: InsertWeatherSettings): Promise<WeatherSettings>;
|
||
updateWeatherSettings(id: number, settings: Partial<InsertWeatherSettings>): Promise<WeatherSettings>;
|
||
getWeatherData(date: string, isCurrentYear: boolean): Promise<WeatherData | undefined>;
|
||
createWeatherData(data: InsertWeatherData): Promise<WeatherData>;
|
||
updateWeatherData(id: number, data: Partial<InsertWeatherData>): Promise<WeatherData>;
|
||
deleteOldWeatherData(daysToKeep: number): Promise<void>;
|
||
clearWeatherCache(): Promise<void>;
|
||
}
|
||
|
||
export class DatabaseStorage implements IStorage {
|
||
// User operations
|
||
async getUser(id: string): Promise<User | undefined> {
|
||
const [user] = await db.select().from(users).where(eq(users.id, id));
|
||
return user;
|
||
}
|
||
|
||
async getUserByEmail(email: string): Promise<User | undefined> {
|
||
const [user] = await db.select().from(users).where(eq(users.email, email));
|
||
return user;
|
||
}
|
||
|
||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||
const [user] = await db.select().from(users).where(eq(users.username, username));
|
||
return user;
|
||
}
|
||
|
||
async upsertUser(userData: UpsertUser): Promise<User> {
|
||
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<UserWithGroups | undefined> {
|
||
const user = await this.getUser(id);
|
||
if (!user) return undefined;
|
||
|
||
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.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 userGroups = result.rows.map((row: any) => ({
|
||
userId: row.user_id,
|
||
groupId: row.group_id,
|
||
group: {
|
||
id: row.group_id_ref,
|
||
name: row.group_name,
|
||
color: row.group_color,
|
||
createdAt: row.group_created_at,
|
||
updatedAt: row.group_updated_at,
|
||
}
|
||
}));
|
||
|
||
return { ...user, userGroups };
|
||
} catch (error) {
|
||
console.error('Error fetching user with groups:', error);
|
||
return { ...user, userGroups: [] };
|
||
}
|
||
}
|
||
|
||
async getUsers(): Promise<User[]> {
|
||
return await db.select().from(users);
|
||
}
|
||
|
||
async createUser(userData: UpsertUser): Promise<User> {
|
||
const [user] = await db.insert(users).values(userData).returning();
|
||
return user;
|
||
}
|
||
|
||
async updateUser(id: string, userData: Partial<UpsertUser>): Promise<User> {
|
||
const [user] = await db
|
||
.update(users)
|
||
.set({ ...userData, updatedAt: new Date() })
|
||
.where(eq(users.id, id))
|
||
.returning();
|
||
return user;
|
||
}
|
||
|
||
async deleteUser(id: string): Promise<void> {
|
||
await db.delete(users).where(eq(users.id, id));
|
||
}
|
||
|
||
// Group operations
|
||
async getGroups(): Promise<Group[]> {
|
||
return await db.select().from(groups).orderBy(groups.name);
|
||
}
|
||
|
||
async getGroup(id: number): Promise<Group | undefined> {
|
||
const [group] = await db.select().from(groups).where(eq(groups.id, id));
|
||
return group;
|
||
}
|
||
|
||
async createGroup(groupData: InsertGroup): Promise<Group> {
|
||
const [group] = await db.insert(groups).values(groupData).returning();
|
||
return group;
|
||
}
|
||
|
||
async updateGroup(id: number, groupData: Partial<InsertGroup>): Promise<Group> {
|
||
const [group] = await db
|
||
.update(groups)
|
||
.set({ ...groupData, updatedAt: new Date() })
|
||
.where(eq(groups.id, id))
|
||
.returning();
|
||
return group;
|
||
}
|
||
|
||
async deleteGroup(id: number): Promise<void> {
|
||
await db.delete(groups).where(eq(groups.id, id));
|
||
}
|
||
|
||
// Supplier operations
|
||
async getSuppliers(): Promise<Supplier[]> {
|
||
return await db.select().from(suppliers).orderBy(suppliers.name);
|
||
}
|
||
|
||
async createSupplier(supplierData: InsertSupplier): Promise<Supplier> {
|
||
const [supplier] = await db.insert(suppliers).values(supplierData).returning();
|
||
return supplier;
|
||
}
|
||
|
||
async updateSupplier(id: number, supplierData: Partial<InsertSupplier>): Promise<Supplier> {
|
||
const [supplier] = await db
|
||
.update(suppliers)
|
||
.set({ ...supplierData, updatedAt: new Date() })
|
||
.where(eq(suppliers.id, id))
|
||
.returning();
|
||
return supplier;
|
||
}
|
||
|
||
async deleteSupplier(id: number): Promise<void> {
|
||
await db.delete(suppliers).where(eq(suppliers.id, id));
|
||
}
|
||
|
||
// Order operations
|
||
async getOrders(groupIds?: number[]): Promise<OrderWithRelations[]> {
|
||
let query = db
|
||
.select({
|
||
id: orders.id,
|
||
supplierId: orders.supplierId,
|
||
groupId: orders.groupId,
|
||
plannedDate: orders.plannedDate,
|
||
status: orders.status,
|
||
quantity: orders.quantity,
|
||
unit: orders.unit,
|
||
notes: orders.notes,
|
||
createdBy: orders.createdBy,
|
||
createdAt: orders.createdAt,
|
||
updatedAt: orders.updatedAt,
|
||
supplier: suppliers,
|
||
group: groups,
|
||
creator: {
|
||
id: users.id,
|
||
firstName: users.firstName,
|
||
lastName: users.lastName,
|
||
username: users.username,
|
||
email: users.email
|
||
}
|
||
})
|
||
.from(orders)
|
||
.leftJoin(suppliers, eq(orders.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(orders.groupId, groups.id))
|
||
.leftJoin(users, eq(orders.createdBy, users.id));
|
||
|
||
if (groupIds && groupIds.length > 0) {
|
||
query = query.where(inArray(orders.groupId, groupIds));
|
||
}
|
||
|
||
const baseOrders = await query.orderBy(desc(orders.createdAt));
|
||
|
||
// Pour chaque commande, récupérer ses livraisons associées (PRODUCTION RELATIONS)
|
||
const ordersWithDeliveries = await Promise.all(
|
||
baseOrders.map(async (order) => {
|
||
const associatedDeliveries = await db
|
||
.select({
|
||
id: deliveries.id,
|
||
orderId: deliveries.orderId,
|
||
supplierId: deliveries.supplierId,
|
||
groupId: deliveries.groupId,
|
||
scheduledDate: deliveries.scheduledDate,
|
||
deliveredDate: deliveries.deliveredDate,
|
||
status: deliveries.status,
|
||
quantity: deliveries.quantity,
|
||
unit: deliveries.unit,
|
||
blNumber: deliveries.blNumber,
|
||
blAmount: deliveries.blAmount,
|
||
invoiceReference: deliveries.invoiceReference,
|
||
invoiceAmount: deliveries.invoiceAmount,
|
||
reconciled: deliveries.reconciled,
|
||
validatedAt: deliveries.validatedAt,
|
||
notes: deliveries.notes,
|
||
createdBy: deliveries.createdBy,
|
||
createdAt: deliveries.createdAt,
|
||
updatedAt: deliveries.updatedAt,
|
||
supplier: suppliers,
|
||
group: groups,
|
||
creator: {
|
||
id: users.id,
|
||
firstName: users.firstName,
|
||
lastName: users.lastName,
|
||
username: users.username,
|
||
email: users.email
|
||
}
|
||
})
|
||
.from(deliveries)
|
||
.leftJoin(suppliers, eq(deliveries.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(deliveries.groupId, groups.id))
|
||
.leftJoin(users, eq(deliveries.createdBy, users.id))
|
||
.where(eq(deliveries.orderId, order.id));
|
||
|
||
return {
|
||
...order,
|
||
deliveries: associatedDeliveries
|
||
};
|
||
})
|
||
);
|
||
|
||
console.log(`🔗 PRODUCTION: getOrders() récupéré ${ordersWithDeliveries.length} commandes avec relations`);
|
||
return ordersWithDeliveries as OrderWithRelations[];
|
||
}
|
||
|
||
async getOrdersByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise<OrderWithRelations[]> {
|
||
let query = db
|
||
.select({
|
||
id: orders.id,
|
||
supplierId: orders.supplierId,
|
||
groupId: orders.groupId,
|
||
plannedDate: orders.plannedDate,
|
||
status: orders.status,
|
||
quantity: orders.quantity,
|
||
unit: orders.unit,
|
||
notes: orders.notes,
|
||
createdBy: orders.createdBy,
|
||
createdAt: orders.createdAt,
|
||
updatedAt: orders.updatedAt,
|
||
supplier: suppliers,
|
||
group: groups,
|
||
creator: {
|
||
id: users.id,
|
||
firstName: users.firstName,
|
||
lastName: users.lastName,
|
||
username: users.username,
|
||
email: users.email
|
||
}
|
||
})
|
||
.from(orders)
|
||
.leftJoin(suppliers, eq(orders.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(orders.groupId, groups.id))
|
||
.leftJoin(users, eq(orders.createdBy, users.id))
|
||
.where(
|
||
and(
|
||
gte(orders.plannedDate, startDate),
|
||
lte(orders.plannedDate, endDate)
|
||
)
|
||
);
|
||
|
||
if (groupIds && groupIds.length > 0) {
|
||
query = query.where(
|
||
and(
|
||
gte(orders.plannedDate, startDate),
|
||
lte(orders.plannedDate, endDate),
|
||
inArray(orders.groupId, groupIds)
|
||
)
|
||
);
|
||
}
|
||
|
||
const baseOrders = await query.orderBy(desc(orders.plannedDate));
|
||
|
||
// Pour chaque commande, récupérer ses livraisons associées (PRODUCTION RELATIONS)
|
||
const ordersWithDeliveries = await Promise.all(
|
||
baseOrders.map(async (order) => {
|
||
const associatedDeliveries = await db
|
||
.select({
|
||
id: deliveries.id,
|
||
orderId: deliveries.orderId,
|
||
supplierId: deliveries.supplierId,
|
||
groupId: deliveries.groupId,
|
||
scheduledDate: deliveries.scheduledDate,
|
||
deliveredDate: deliveries.deliveredDate,
|
||
status: deliveries.status,
|
||
quantity: deliveries.quantity,
|
||
unit: deliveries.unit,
|
||
blNumber: deliveries.blNumber,
|
||
blAmount: deliveries.blAmount,
|
||
invoiceReference: deliveries.invoiceReference,
|
||
invoiceAmount: deliveries.invoiceAmount,
|
||
reconciled: deliveries.reconciled,
|
||
validatedAt: deliveries.validatedAt,
|
||
notes: deliveries.notes,
|
||
createdBy: deliveries.createdBy,
|
||
createdAt: deliveries.createdAt,
|
||
updatedAt: deliveries.updatedAt,
|
||
supplier: suppliers,
|
||
group: groups,
|
||
creator: {
|
||
id: users.id,
|
||
firstName: users.firstName,
|
||
lastName: users.lastName,
|
||
username: users.username,
|
||
email: users.email
|
||
}
|
||
})
|
||
.from(deliveries)
|
||
.leftJoin(suppliers, eq(deliveries.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(deliveries.groupId, groups.id))
|
||
.leftJoin(users, eq(deliveries.createdBy, users.id))
|
||
.where(eq(deliveries.orderId, order.id));
|
||
|
||
return {
|
||
...order,
|
||
deliveries: associatedDeliveries
|
||
};
|
||
})
|
||
);
|
||
|
||
return ordersWithDeliveries as OrderWithRelations[];
|
||
}
|
||
|
||
async getOrder(id: number): Promise<OrderWithRelations | undefined> {
|
||
const [order] = await db
|
||
.select({
|
||
id: orders.id,
|
||
supplierId: orders.supplierId,
|
||
groupId: orders.groupId,
|
||
plannedDate: orders.plannedDate,
|
||
status: orders.status,
|
||
quantity: orders.quantity,
|
||
unit: orders.unit,
|
||
notes: orders.notes,
|
||
createdBy: orders.createdBy,
|
||
createdAt: orders.createdAt,
|
||
updatedAt: orders.updatedAt,
|
||
supplier: suppliers,
|
||
group: groups,
|
||
creator: {
|
||
id: users.id,
|
||
firstName: users.firstName,
|
||
lastName: users.lastName,
|
||
username: users.username,
|
||
email: users.email
|
||
}
|
||
})
|
||
.from(orders)
|
||
.leftJoin(suppliers, eq(orders.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(orders.groupId, groups.id))
|
||
.leftJoin(users, eq(orders.createdBy, users.id))
|
||
.where(eq(orders.id, id));
|
||
|
||
if (!order) return undefined;
|
||
|
||
// Récupérer les livraisons associées à cette commande (PRODUCTION PostgreSQL)
|
||
const associatedDeliveries = await db
|
||
.select({
|
||
id: deliveries.id,
|
||
orderId: deliveries.orderId,
|
||
supplierId: deliveries.supplierId,
|
||
groupId: deliveries.groupId,
|
||
scheduledDate: deliveries.scheduledDate,
|
||
deliveredDate: deliveries.deliveredDate,
|
||
status: deliveries.status,
|
||
quantity: deliveries.quantity,
|
||
unit: deliveries.unit,
|
||
blNumber: deliveries.blNumber,
|
||
blAmount: deliveries.blAmount,
|
||
invoiceReference: deliveries.invoiceReference,
|
||
invoiceAmount: deliveries.invoiceAmount,
|
||
reconciled: deliveries.reconciled,
|
||
validatedAt: deliveries.validatedAt,
|
||
notes: deliveries.notes,
|
||
createdBy: deliveries.createdBy,
|
||
createdAt: deliveries.createdAt,
|
||
updatedAt: deliveries.updatedAt,
|
||
supplier: suppliers,
|
||
group: groups,
|
||
creator: {
|
||
id: users.id,
|
||
firstName: users.firstName,
|
||
lastName: users.lastName,
|
||
username: users.username,
|
||
email: users.email
|
||
}
|
||
})
|
||
.from(deliveries)
|
||
.leftJoin(suppliers, eq(deliveries.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(deliveries.groupId, groups.id))
|
||
.leftJoin(users, eq(deliveries.createdBy, users.id))
|
||
.where(eq(deliveries.orderId, id));
|
||
|
||
console.log(`🔗 PRODUCTION: getOrder #${id} found ${associatedDeliveries.length} associated deliveries`);
|
||
|
||
return {
|
||
...order,
|
||
deliveries: associatedDeliveries
|
||
} as OrderWithRelations;
|
||
}
|
||
|
||
async createOrder(orderData: InsertOrder): Promise<Order> {
|
||
const [order] = await db.insert(orders).values(orderData).returning();
|
||
return order;
|
||
}
|
||
|
||
async updateOrder(id: number, orderData: Partial<InsertOrder>): Promise<Order> {
|
||
const [order] = await db
|
||
.update(orders)
|
||
.set({ ...orderData, updatedAt: new Date() })
|
||
.where(eq(orders.id, id))
|
||
.returning();
|
||
return order;
|
||
}
|
||
|
||
async deleteOrder(id: number): Promise<void> {
|
||
await db.delete(orders).where(eq(orders.id, id));
|
||
}
|
||
|
||
// Delivery operations
|
||
async getDeliveries(groupIds?: number[]): Promise<DeliveryWithRelations[]> {
|
||
let query = db
|
||
.select({
|
||
id: deliveries.id,
|
||
orderId: deliveries.orderId,
|
||
supplierId: deliveries.supplierId,
|
||
groupId: deliveries.groupId,
|
||
scheduledDate: deliveries.scheduledDate,
|
||
deliveredDate: deliveries.deliveredDate,
|
||
status: deliveries.status,
|
||
quantity: deliveries.quantity,
|
||
unit: deliveries.unit,
|
||
blNumber: deliveries.blNumber,
|
||
blAmount: deliveries.blAmount,
|
||
invoiceReference: deliveries.invoiceReference,
|
||
invoiceAmount: deliveries.invoiceAmount,
|
||
reconciled: deliveries.reconciled,
|
||
validatedAt: deliveries.validatedAt,
|
||
notes: deliveries.notes,
|
||
createdBy: deliveries.createdBy,
|
||
createdAt: deliveries.createdAt,
|
||
updatedAt: deliveries.updatedAt,
|
||
supplier: suppliers,
|
||
group: groups,
|
||
creator: {
|
||
id: users.id,
|
||
firstName: users.firstName,
|
||
lastName: users.lastName,
|
||
username: users.username,
|
||
email: users.email
|
||
}
|
||
})
|
||
.from(deliveries)
|
||
.leftJoin(suppliers, eq(deliveries.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(deliveries.groupId, groups.id))
|
||
.leftJoin(users, eq(deliveries.createdBy, users.id));
|
||
|
||
if (groupIds && groupIds.length > 0) {
|
||
query = query.where(inArray(deliveries.groupId, groupIds));
|
||
}
|
||
|
||
const baseDeliveries = await query.orderBy(desc(deliveries.createdAt));
|
||
|
||
// Pour chaque livraison, récupérer sa commande associée si elle existe (PRODUCTION RELATIONS)
|
||
const deliveriesWithOrders = await Promise.all(
|
||
baseDeliveries.map(async (delivery) => {
|
||
let associatedOrder = undefined;
|
||
|
||
if (delivery.orderId) {
|
||
try {
|
||
const [orderData] = await db
|
||
.select({
|
||
id: orders.id,
|
||
supplierId: orders.supplierId,
|
||
groupId: orders.groupId,
|
||
plannedDate: orders.plannedDate,
|
||
status: orders.status,
|
||
quantity: orders.quantity,
|
||
unit: orders.unit,
|
||
notes: orders.notes,
|
||
createdBy: orders.createdBy,
|
||
createdAt: orders.createdAt,
|
||
updatedAt: orders.updatedAt,
|
||
supplier: suppliers,
|
||
group: groups,
|
||
creator: {
|
||
id: users.id,
|
||
firstName: users.firstName,
|
||
lastName: users.lastName,
|
||
username: users.username,
|
||
email: users.email
|
||
}
|
||
})
|
||
.from(orders)
|
||
.leftJoin(suppliers, eq(orders.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(orders.groupId, groups.id))
|
||
.leftJoin(users, eq(orders.createdBy, users.id))
|
||
.where(eq(orders.id, delivery.orderId));
|
||
|
||
if (orderData) {
|
||
// CRITICAL FIX: Vérifier que la commande appartient au même magasin que la livraison
|
||
if (orderData.groupId !== delivery.groupId) {
|
||
console.error(`❌ PRODUCTION: Delivery #${delivery.id} (store ${delivery.groupId}) linked to order #${delivery.orderId} (store ${orderData.groupId}) - STORE MISMATCH DETECTED!`);
|
||
// Ne pas inclure la commande si elle n'appartient pas au bon magasin
|
||
} else {
|
||
associatedOrder = orderData;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(`❌ PRODUCTION: Failed to retrieve associated order #${delivery.orderId} for delivery #${delivery.id}:`, error);
|
||
}
|
||
}
|
||
|
||
return {
|
||
...delivery,
|
||
order: associatedOrder
|
||
};
|
||
})
|
||
);
|
||
|
||
console.log(`🔗 PRODUCTION: getDeliveries() récupéré ${deliveriesWithOrders.length} livraisons avec relations`);
|
||
return deliveriesWithOrders as DeliveryWithRelations[];
|
||
}
|
||
|
||
async getDeliveriesByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise<DeliveryWithRelations[]> {
|
||
let query = db
|
||
.select({
|
||
id: deliveries.id,
|
||
orderId: deliveries.orderId,
|
||
supplierId: deliveries.supplierId,
|
||
groupId: deliveries.groupId,
|
||
scheduledDate: deliveries.scheduledDate,
|
||
deliveredDate: deliveries.deliveredDate,
|
||
status: deliveries.status,
|
||
quantity: deliveries.quantity,
|
||
unit: deliveries.unit,
|
||
blNumber: deliveries.blNumber,
|
||
blAmount: deliveries.blAmount,
|
||
invoiceReference: deliveries.invoiceReference,
|
||
invoiceAmount: deliveries.invoiceAmount,
|
||
reconciled: deliveries.reconciled,
|
||
validatedAt: deliveries.validatedAt,
|
||
notes: deliveries.notes,
|
||
createdBy: deliveries.createdBy,
|
||
createdAt: deliveries.createdAt,
|
||
updatedAt: deliveries.updatedAt,
|
||
supplier: suppliers,
|
||
group: groups,
|
||
creator: {
|
||
id: users.id,
|
||
firstName: users.firstName,
|
||
lastName: users.lastName,
|
||
username: users.username,
|
||
email: users.email
|
||
}
|
||
})
|
||
.from(deliveries)
|
||
.leftJoin(suppliers, eq(deliveries.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(deliveries.groupId, groups.id))
|
||
.leftJoin(users, eq(deliveries.createdBy, users.id))
|
||
.where(
|
||
and(
|
||
gte(deliveries.scheduledDate, startDate),
|
||
lte(deliveries.scheduledDate, endDate)
|
||
)
|
||
);
|
||
|
||
if (groupIds && groupIds.length > 0) {
|
||
query = query.where(
|
||
and(
|
||
gte(deliveries.scheduledDate, startDate),
|
||
lte(deliveries.scheduledDate, endDate),
|
||
inArray(deliveries.groupId, groupIds)
|
||
)
|
||
);
|
||
}
|
||
|
||
const baseDeliveries = await query.orderBy(desc(deliveries.scheduledDate));
|
||
|
||
// Pour chaque livraison, récupérer sa commande associée si elle existe (PRODUCTION RELATIONS)
|
||
const deliveriesWithOrders = await Promise.all(
|
||
baseDeliveries.map(async (delivery) => {
|
||
let associatedOrder = undefined;
|
||
|
||
if (delivery.orderId) {
|
||
try {
|
||
const [orderData] = await db
|
||
.select({
|
||
id: orders.id,
|
||
supplierId: orders.supplierId,
|
||
groupId: orders.groupId,
|
||
plannedDate: orders.plannedDate,
|
||
status: orders.status,
|
||
quantity: orders.quantity,
|
||
unit: orders.unit,
|
||
notes: orders.notes,
|
||
createdBy: orders.createdBy,
|
||
createdAt: orders.createdAt,
|
||
updatedAt: orders.updatedAt,
|
||
supplier: suppliers,
|
||
group: groups,
|
||
creator: {
|
||
id: users.id,
|
||
firstName: users.firstName,
|
||
lastName: users.lastName,
|
||
username: users.username,
|
||
email: users.email
|
||
}
|
||
})
|
||
.from(orders)
|
||
.leftJoin(suppliers, eq(orders.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(orders.groupId, groups.id))
|
||
.leftJoin(users, eq(orders.createdBy, users.id))
|
||
.where(eq(orders.id, delivery.orderId));
|
||
|
||
if (orderData) {
|
||
// CRITICAL FIX: Vérifier que la commande appartient au même magasin que la livraison
|
||
if (orderData.groupId !== delivery.groupId) {
|
||
console.error(`❌ PRODUCTION: Delivery #${delivery.id} (store ${delivery.groupId}) linked to order #${delivery.orderId} (store ${orderData.groupId}) - STORE MISMATCH DETECTED!`);
|
||
// Ne pas inclure la commande si elle n'appartient pas au bon magasin
|
||
} else {
|
||
associatedOrder = orderData;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(`❌ PRODUCTION: Failed to retrieve associated order #${delivery.orderId} for delivery #${delivery.id}:`, error);
|
||
}
|
||
}
|
||
|
||
return {
|
||
...delivery,
|
||
order: associatedOrder
|
||
};
|
||
})
|
||
);
|
||
|
||
return deliveriesWithOrders as DeliveryWithRelations[];
|
||
}
|
||
|
||
async getDelivery(id: number): Promise<DeliveryWithRelations | undefined> {
|
||
const [delivery] = await db
|
||
.select({
|
||
id: deliveries.id,
|
||
orderId: deliveries.orderId,
|
||
supplierId: deliveries.supplierId,
|
||
groupId: deliveries.groupId,
|
||
scheduledDate: deliveries.scheduledDate,
|
||
deliveredDate: deliveries.deliveredDate,
|
||
status: deliveries.status,
|
||
quantity: deliveries.quantity,
|
||
unit: deliveries.unit,
|
||
blNumber: deliveries.blNumber,
|
||
blAmount: deliveries.blAmount,
|
||
invoiceReference: deliveries.invoiceReference,
|
||
invoiceAmount: deliveries.invoiceAmount,
|
||
reconciled: deliveries.reconciled,
|
||
validatedAt: deliveries.validatedAt,
|
||
notes: deliveries.notes,
|
||
createdBy: deliveries.createdBy,
|
||
createdAt: deliveries.createdAt,
|
||
updatedAt: deliveries.updatedAt,
|
||
supplier: suppliers,
|
||
group: groups
|
||
})
|
||
.from(deliveries)
|
||
.leftJoin(suppliers, eq(deliveries.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(deliveries.groupId, groups.id))
|
||
.where(eq(deliveries.id, id));
|
||
|
||
if (!delivery) return undefined;
|
||
|
||
// Add creator info separately to avoid complex JOIN issues in production
|
||
let creator = null;
|
||
try {
|
||
const user = await this.getUser(delivery.createdBy);
|
||
if (user) {
|
||
creator = {
|
||
id: user.id,
|
||
firstName: user.firstName,
|
||
lastName: user.lastName,
|
||
username: user.username,
|
||
email: user.email
|
||
};
|
||
}
|
||
} catch (error) {
|
||
console.log('⚠️ Could not load creator info:', error);
|
||
}
|
||
|
||
// Récupérer la commande associée si elle existe (PRODUCTION PostgreSQL)
|
||
let associatedOrder = undefined;
|
||
if (delivery.orderId) {
|
||
try {
|
||
console.log(`🔗 PRODUCTION: getDelivery #${id} retrieving associated order #${delivery.orderId}`);
|
||
const orderData = await this.getOrder(delivery.orderId);
|
||
if (orderData) {
|
||
// CRITICAL FIX: Vérifier que la commande appartient au même magasin que la livraison
|
||
if (orderData.groupId !== delivery.groupId) {
|
||
console.error(`❌ PRODUCTION: getDelivery #${id} (store ${delivery.groupId}) linked to order #${delivery.orderId} (store ${orderData.groupId}) - STORE MISMATCH DETECTED!`);
|
||
// Ne pas inclure la commande si elle n'appartient pas au bon magasin
|
||
} else {
|
||
associatedOrder = orderData;
|
||
console.log(`✅ PRODUCTION: getDelivery #${id} found associated order #${delivery.orderId} with status: ${orderData.status}`);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(`❌ PRODUCTION: Failed to retrieve associated order #${delivery.orderId} for delivery #${id}:`, error);
|
||
}
|
||
}
|
||
|
||
return {
|
||
...delivery,
|
||
creator,
|
||
order: associatedOrder
|
||
} as DeliveryWithRelations;
|
||
}
|
||
|
||
async createDelivery(deliveryData: InsertDelivery): Promise<Delivery> {
|
||
// Créer la livraison avec statut "planned" par défaut
|
||
const deliveryDataWithStatus = {
|
||
...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({
|
||
status: 'planned',
|
||
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;
|
||
}
|
||
|
||
async updateDelivery(id: number, deliveryData: Partial<InsertDelivery>): Promise<Delivery> {
|
||
const [delivery] = await db
|
||
.update(deliveries)
|
||
.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({
|
||
status: 'planned',
|
||
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`);
|
||
}
|
||
} catch (error) {
|
||
console.error(`❌ CALENDAR EDIT: Failed to update order #${deliveryData.orderId} status to planned:`, error);
|
||
}
|
||
}
|
||
|
||
return delivery;
|
||
}
|
||
|
||
async deleteDelivery(id: number): Promise<void> {
|
||
await db.delete(deliveries).where(eq(deliveries.id, id));
|
||
}
|
||
|
||
async validateDelivery(id: number, blData?: { blNumber: string; blAmount: number }): Promise<void> {
|
||
const updateData: any = {
|
||
status: 'delivered',
|
||
deliveredDate: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
|
||
if (blData) {
|
||
updateData.blNumber = blData.blNumber;
|
||
if (blData.blAmount !== undefined) {
|
||
updateData.blAmount = blData.blAmount.toString();
|
||
}
|
||
}
|
||
|
||
await db.update(deliveries).set(updateData).where(eq(deliveries.id, id));
|
||
}
|
||
|
||
// User-Group operations
|
||
async getUserGroups(userId: string): Promise<UserGroup[]> {
|
||
return await db.select().from(userGroups).where(eq(userGroups.userId, userId));
|
||
}
|
||
|
||
async assignUserToGroup(userGroupData: InsertUserGroup): Promise<UserGroup> {
|
||
const [userGroup] = await db.insert(userGroups).values(userGroupData).returning();
|
||
return userGroup;
|
||
}
|
||
|
||
async removeUserFromGroup(userId: string, groupId: number): Promise<void> {
|
||
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;
|
||
}> {
|
||
// En mode développement avec MemStorage, retourner des statistiques simulées
|
||
if (process.env.NODE_ENV === 'development') {
|
||
console.log('📊 Mode développement - statistiques simulées');
|
||
return {
|
||
ordersCount: 12,
|
||
deliveriesCount: 8,
|
||
pendingOrdersCount: 3,
|
||
averageDeliveryTime: 2.5,
|
||
totalPalettes: 45,
|
||
totalPackages: 156,
|
||
};
|
||
}
|
||
|
||
// En production, calculer les vraies statistiques
|
||
const startDate = `${year}-${month.toString().padStart(2, '0')}-01`;
|
||
const endDate = month === 12
|
||
? `${year + 1}-01-01`
|
||
: `${year}-${(month + 1).toString().padStart(2, '0')}-01`;
|
||
|
||
try {
|
||
console.log('📊 Calcul statistiques mensuelles:', { year, month, startDate, endDate, groupIds });
|
||
|
||
// Construire les conditions pour le filtrage par groupe - UTILISER LE BON CHAMP DE DATE
|
||
let ordersWhereCondition = and(
|
||
gte(orders.plannedDate, startDate),
|
||
lt(orders.plannedDate, endDate)
|
||
);
|
||
// Pour les livraisons, utiliser deliveredDate pour celles livrées, sinon scheduledDate
|
||
let deliveriesWhereCondition = and(
|
||
gte(deliveries.scheduledDate, startDate),
|
||
lt(deliveries.scheduledDate, endDate)
|
||
);
|
||
|
||
// Ajouter le filtre par groupe si spécifié
|
||
if (groupIds && groupIds.length > 0) {
|
||
ordersWhereCondition = and(ordersWhereCondition, inArray(orders.groupId, groupIds));
|
||
deliveriesWhereCondition = and(deliveriesWhereCondition, inArray(deliveries.groupId, groupIds));
|
||
}
|
||
|
||
// Compter les commandes du mois
|
||
const ordersResult = await db
|
||
.select({ count: sql<number>`count(*)` })
|
||
.from(orders)
|
||
.where(ordersWhereCondition);
|
||
|
||
// Compter les livraisons du mois
|
||
const deliveriesResult = await db
|
||
.select({ count: sql<number>`count(*)` })
|
||
.from(deliveries)
|
||
.where(deliveriesWhereCondition);
|
||
|
||
// Commandes en attente (sans date de livraison ou pas encore livrées)
|
||
let pendingWhereCondition = eq(orders.status, 'pending');
|
||
if (groupIds && groupIds.length > 0) {
|
||
pendingWhereCondition = and(pendingWhereCondition, inArray(orders.groupId, groupIds)) as any;
|
||
}
|
||
|
||
const pendingResult = await db
|
||
.select({ count: sql<number>`count(*)` })
|
||
.from(orders)
|
||
.where(pendingWhereCondition);
|
||
|
||
// Calculer les totaux de palettes et colis du mois (SEULEMENT livraisons delivered)
|
||
// IMPORTANT: Utiliser deliveredDate pour filtrer par le mois de livraison effective
|
||
let deliveredWhereCondition = and(
|
||
gte(deliveries.deliveredDate, sql`${startDate}::timestamp`),
|
||
lt(deliveries.deliveredDate, sql`${endDate}::timestamp`),
|
||
eq(deliveries.status, 'delivered'),
|
||
isNotNull(deliveries.deliveredDate)
|
||
);
|
||
|
||
// Ajouter le filtre par groupe pour les livraisons delivered
|
||
if (groupIds && groupIds.length > 0) {
|
||
deliveredWhereCondition = and(deliveredWhereCondition, inArray(deliveries.groupId, groupIds));
|
||
}
|
||
|
||
const deliveriesStatsResult = await db
|
||
.select({
|
||
totalPalettes: sql<number>`COALESCE(SUM(CAST(${deliveries.quantity} as INTEGER)), 0)`,
|
||
totalPackages: sql<number>`COALESCE(COUNT(*), 0)`,
|
||
avgDelay: sql<number>`COALESCE(AVG(EXTRACT(DAY FROM (${deliveries.deliveredDate} - ${deliveries.scheduledDate}))), 0)`
|
||
})
|
||
.from(deliveries)
|
||
.where(deliveredWhereCondition);
|
||
|
||
const ordersCount = Number(ordersResult[0]?.count || 0);
|
||
const deliveriesCount = Number(deliveriesResult[0]?.count || 0);
|
||
const pendingOrdersCount = Number(pendingResult[0]?.count || 0);
|
||
const totalPalettes = Number(deliveriesStatsResult[0]?.totalPalettes || 0);
|
||
const totalPackages = Number(deliveriesStatsResult[0]?.totalPackages || 0);
|
||
const averageDeliveryTime = Number(deliveriesStatsResult[0]?.avgDelay || 0);
|
||
|
||
console.log('📊 Statistiques calculées:', {
|
||
ordersCount,
|
||
deliveriesCount,
|
||
pendingOrdersCount,
|
||
averageDeliveryTime,
|
||
totalPalettes,
|
||
totalPackages,
|
||
});
|
||
|
||
return {
|
||
ordersCount,
|
||
deliveriesCount,
|
||
pendingOrdersCount,
|
||
averageDeliveryTime,
|
||
totalPalettes,
|
||
totalPackages,
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur calcul statistiques:', error);
|
||
// Retourner des zéros en cas d'erreur pour éviter un crash
|
||
return {
|
||
ordersCount: 0,
|
||
deliveriesCount: 0,
|
||
pendingOrdersCount: 0,
|
||
averageDeliveryTime: 0,
|
||
totalPalettes: 0,
|
||
totalPackages: 0,
|
||
};
|
||
}
|
||
}
|
||
|
||
// Publicity operations
|
||
async getPublicities(year?: number, groupIds?: number[]): Promise<PublicityWithRelations[]> {
|
||
let 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,
|
||
})
|
||
.from(publicities);
|
||
|
||
if (year) {
|
||
query = query.where(eq(publicities.year, year));
|
||
}
|
||
|
||
// 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}`));
|
||
}
|
||
|
||
const publicityIds = results.map((p: any) => p.id);
|
||
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))
|
||
: [];
|
||
|
||
// Sort by pubNumber as integer on the server side for consistency
|
||
const sortedResults = results.sort((a, b) => {
|
||
const numA = parseInt(a.pubNumber) || 0;
|
||
const numB = parseInt(b.pubNumber) || 0;
|
||
return numA - numB;
|
||
});
|
||
|
||
return sortedResults.map((publicity: any) => ({
|
||
...publicity,
|
||
participations: participations
|
||
.filter((p: any) => p.publicityId === publicity.id)
|
||
.map((p: any) => ({
|
||
publicityId: p.publicityId,
|
||
groupId: p.groupId,
|
||
group: p.group!,
|
||
})),
|
||
}));
|
||
}
|
||
|
||
async getPublicity(id: number): Promise<PublicityWithRelations | undefined> {
|
||
const [publicity] = await db.select().from(publicities).where(eq(publicities.id, id));
|
||
if (!publicity) return undefined;
|
||
|
||
const participations = await db
|
||
.select({
|
||
publicityId: publicityParticipations.publicityId,
|
||
groupId: publicityParticipations.groupId,
|
||
group: groups,
|
||
})
|
||
.from(publicityParticipations)
|
||
.leftJoin(groups, eq(publicityParticipations.groupId, groups.id))
|
||
.where(eq(publicityParticipations.publicityId, id));
|
||
|
||
return {
|
||
...publicity,
|
||
participations: participations.map((p: any) => ({
|
||
publicityId: p.publicityId,
|
||
groupId: p.groupId,
|
||
group: p.group!,
|
||
})),
|
||
};
|
||
}
|
||
|
||
async createPublicity(publicityData: InsertPublicity): Promise<Publicity> {
|
||
const [publicity] = await db.insert(publicities).values(publicityData).returning();
|
||
return publicity;
|
||
}
|
||
|
||
async updatePublicity(id: number, publicityData: Partial<InsertPublicity>): Promise<Publicity> {
|
||
const [publicity] = await db
|
||
.update(publicities)
|
||
.set({ ...publicityData, updatedAt: new Date() })
|
||
.where(eq(publicities.id, id))
|
||
.returning();
|
||
return publicity;
|
||
}
|
||
|
||
async deletePublicity(id: number): Promise<void> {
|
||
await db.delete(publicityParticipations).where(eq(publicityParticipations.publicityId, id));
|
||
await db.delete(publicities).where(eq(publicities.id, id));
|
||
}
|
||
|
||
async getPublicityParticipations(publicityId: number): Promise<PublicityParticipation[]> {
|
||
return await db
|
||
.select()
|
||
.from(publicityParticipations)
|
||
.where(eq(publicityParticipations.publicityId, publicityId));
|
||
}
|
||
|
||
async setPublicityParticipations(publicityId: number, groupIds: number[]): Promise<void> {
|
||
await db.delete(publicityParticipations).where(eq(publicityParticipations.publicityId, publicityId));
|
||
|
||
if (groupIds.length > 0) {
|
||
const participationsData = groupIds.map(groupId => ({
|
||
publicityId,
|
||
groupId,
|
||
}));
|
||
await db.insert(publicityParticipations).values(participationsData);
|
||
}
|
||
}
|
||
|
||
// NocoDB Configuration operations
|
||
async getNocodbConfigs(): Promise<NocodbConfig[]> {
|
||
return await db.select().from(nocodbConfig).orderBy(desc(nocodbConfig.createdAt));
|
||
}
|
||
|
||
async getNocodbConfig(id: number): Promise<NocodbConfig | undefined> {
|
||
const [config] = await db.select().from(nocodbConfig).where(eq(nocodbConfig.id, id));
|
||
return config;
|
||
}
|
||
|
||
async getActiveNocodbConfig(): Promise<NocodbConfig | undefined> {
|
||
try {
|
||
const [config] = await db
|
||
.select()
|
||
.from(nocodbConfig)
|
||
.where(eq(nocodbConfig.isActive, true))
|
||
.limit(1);
|
||
|
||
console.log('🔧 Configuration NocoDB active récupérée:', config);
|
||
return config;
|
||
} catch (error) {
|
||
console.error('❌ Erreur récupération config NocoDB:', error);
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
async createNocodbConfig(configData: InsertNocodbConfig): Promise<NocodbConfig> {
|
||
const [config] = await db.insert(nocodbConfig).values(configData).returning();
|
||
return config;
|
||
}
|
||
|
||
async updateNocodbConfig(id: number, configData: Partial<InsertNocodbConfig>): Promise<NocodbConfig> {
|
||
const [config] = await db
|
||
.update(nocodbConfig)
|
||
.set({ ...configData, updatedAt: new Date() })
|
||
.where(eq(nocodbConfig.id, id))
|
||
.returning();
|
||
return config;
|
||
}
|
||
|
||
async deleteNocodbConfig(id: number): Promise<void> {
|
||
await db.delete(nocodbConfig).where(eq(nocodbConfig.id, id));
|
||
}
|
||
|
||
// Invoice Verification Cache operations
|
||
async getInvoiceVerificationCache(cacheKey: string): Promise<InvoiceVerificationCache | undefined> {
|
||
const [cache] = await db
|
||
.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,
|
||
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
|
||
});
|
||
// Supprimer le cache expiré
|
||
await db.delete(invoiceVerificationCache)
|
||
.where(eq(invoiceVerificationCache.cacheKey, cacheKey));
|
||
return undefined; // Cache expiré
|
||
}
|
||
}
|
||
|
||
console.log('❌ [DATABASE-CACHE] Cache miss pour:', { cacheKey });
|
||
return undefined; // Pas de cache
|
||
}
|
||
|
||
async createInvoiceVerificationCache(cacheData: InsertInvoiceVerificationCache): Promise<InvoiceVerificationCache> {
|
||
try {
|
||
// Utiliser UPSERT pour gérer les conflits de clés
|
||
const [cache] = await db
|
||
.insert(invoiceVerificationCache)
|
||
.values(cacheData)
|
||
.onConflictDoUpdate({
|
||
target: invoiceVerificationCache.cacheKey,
|
||
set: {
|
||
exists: cacheData.exists,
|
||
matchType: cacheData.matchType,
|
||
errorMessage: cacheData.errorMessage,
|
||
cacheHit: cacheData.cacheHit,
|
||
apiCallTime: cacheData.apiCallTime,
|
||
updatedAt: new Date()
|
||
}
|
||
})
|
||
.returning();
|
||
return cache;
|
||
} catch (error) {
|
||
console.error('❌ Erreur création cache:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async saveInvoiceVerificationCache(cacheData: InsertInvoiceVerificationCache): Promise<InvoiceVerificationCache> {
|
||
return this.createInvoiceVerificationCache(cacheData);
|
||
}
|
||
|
||
async clearExpiredCache(): Promise<void> {
|
||
await db.delete(invoiceVerificationCache).where(
|
||
sql`expires_at < NOW()`
|
||
);
|
||
}
|
||
|
||
// Real implementations for production operations
|
||
async getCustomerOrders(groupIds?: number[]): Promise<CustomerOrderWithRelations[]> {
|
||
let query = db
|
||
.select({
|
||
customerOrder: customerOrders,
|
||
supplier: suppliers,
|
||
group: groups
|
||
})
|
||
.from(customerOrders)
|
||
.leftJoin(suppliers, eq(customerOrders.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(customerOrders.groupId, groups.id));
|
||
|
||
if (groupIds && groupIds.length > 0) {
|
||
query = query.where(inArray(customerOrders.groupId, groupIds));
|
||
}
|
||
|
||
const results = await query.orderBy(desc(customerOrders.createdAt));
|
||
return results.map((row: any) => ({
|
||
...row.customerOrder,
|
||
supplier: row.supplier,
|
||
group: row.group,
|
||
creator: row.creator || { id: row.customerOrder.createdBy, username: 'unknown' }
|
||
}));
|
||
}
|
||
|
||
async getCustomerOrder(id: number): Promise<CustomerOrderWithRelations | undefined> {
|
||
const [result] = await db
|
||
.select({
|
||
customerOrder: customerOrders,
|
||
supplier: suppliers,
|
||
group: groups
|
||
})
|
||
.from(customerOrders)
|
||
.leftJoin(suppliers, eq(customerOrders.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(customerOrders.groupId, groups.id))
|
||
.where(eq(customerOrders.id, id));
|
||
|
||
if (!result) return undefined;
|
||
return {
|
||
...result.customerOrder,
|
||
supplier: result.supplier,
|
||
group: result.group,
|
||
creator: result.creator || { id: result.customerOrder.createdBy, username: 'unknown' }
|
||
};
|
||
}
|
||
|
||
async createCustomerOrder(orderData: InsertCustomerOrder): Promise<CustomerOrder> {
|
||
const [customerOrder] = await db.insert(customerOrders).values(orderData).returning();
|
||
return customerOrder;
|
||
}
|
||
|
||
async updateCustomerOrder(id: number, orderData: Partial<InsertCustomerOrder>): Promise<CustomerOrder> {
|
||
const [customerOrder] = await db
|
||
.update(customerOrders)
|
||
.set({ ...orderData, updatedAt: new Date() })
|
||
.where(eq(customerOrders.id, id))
|
||
.returning();
|
||
return customerOrder;
|
||
}
|
||
|
||
async deleteCustomerOrder(id: number): Promise<void> {
|
||
await db.delete(customerOrders).where(eq(customerOrders.id, id));
|
||
}
|
||
|
||
async getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; search?: string; }): Promise<DlcProductWithRelations[]> {
|
||
let query = db
|
||
.select({
|
||
dlcProduct: dlcProducts,
|
||
supplier: suppliers,
|
||
group: groups
|
||
})
|
||
.from(dlcProducts)
|
||
.leftJoin(suppliers, eq(dlcProducts.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(dlcProducts.groupId, groups.id));
|
||
|
||
const conditions = [];
|
||
|
||
if (groupIds && groupIds.length > 0) {
|
||
conditions.push(inArray(dlcProducts.groupId, groupIds));
|
||
}
|
||
|
||
if (filters) {
|
||
if (filters.status) {
|
||
conditions.push(eq(dlcProducts.status, filters.status));
|
||
}
|
||
if (filters.supplierId) {
|
||
conditions.push(eq(dlcProducts.supplierId, filters.supplierId));
|
||
}
|
||
if (filters.search) {
|
||
conditions.push(
|
||
or(
|
||
sql`LOWER(${dlcProducts.productName}) LIKE LOWER(${'%' + filters.search + '%'})`,
|
||
sql`LOWER(${suppliers.name}) LIKE LOWER(${'%' + filters.search + '%'})`
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
if (conditions.length > 0) {
|
||
query = query.where(and(...conditions));
|
||
}
|
||
|
||
const results = await query.orderBy(asc(dlcProducts.expiryDate));
|
||
return results.map((row: any) => ({
|
||
...row.dlcProduct,
|
||
dlcDate: row.dlcProduct.expiryDate, // ✅ Mapping expiryDate vers dlcDate pour frontend
|
||
supplier: row.supplier,
|
||
group: row.group,
|
||
creator: row.creator || { id: row.dlcProduct.createdBy, username: 'unknown' }
|
||
}));
|
||
}
|
||
|
||
async getDlcProduct(id: number): Promise<DlcProductWithRelations | undefined> {
|
||
const [result] = await db
|
||
.select({
|
||
dlcProduct: dlcProducts,
|
||
supplier: suppliers,
|
||
group: groups
|
||
})
|
||
.from(dlcProducts)
|
||
.leftJoin(suppliers, eq(dlcProducts.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(dlcProducts.groupId, groups.id))
|
||
.where(eq(dlcProducts.id, id));
|
||
|
||
if (!result) return undefined;
|
||
return {
|
||
...result.dlcProduct,
|
||
dlcDate: result.dlcProduct.expiryDate, // ✅ Mapping expiryDate vers dlcDate pour frontend
|
||
supplier: result.supplier,
|
||
group: result.group,
|
||
creator: result.creator || { id: result.dlcProduct.createdBy, username: 'unknown' }
|
||
};
|
||
}
|
||
|
||
async createDlcProduct(productData: InsertDlcProduct): Promise<DlcProductFrontend> {
|
||
const [dlcProduct] = await db.insert(dlcProducts).values(productData).returning();
|
||
return { ...dlcProduct, dlcDate: new Date(dlcProduct.expiryDate) } as DlcProductFrontend;
|
||
}
|
||
|
||
async updateDlcProduct(id: number, productData: Partial<InsertDlcProduct>): Promise<DlcProductFrontend> {
|
||
const [dlcProduct] = await db
|
||
.update(dlcProducts)
|
||
.set({ ...productData, updatedAt: new Date() })
|
||
.where(eq(dlcProducts.id, id))
|
||
.returning();
|
||
return dlcProduct as DlcProductFrontend;
|
||
}
|
||
|
||
async deleteDlcProduct(id: number): Promise<void> {
|
||
await db.delete(dlcProducts).where(eq(dlcProducts.id, id));
|
||
}
|
||
|
||
async validateDlcProduct(id: number, validatedBy: string): Promise<DlcProductFrontend> {
|
||
const [dlcProduct] = await db
|
||
.update(dlcProducts)
|
||
.set({
|
||
validatedBy,
|
||
validatedAt: new Date(),
|
||
status: "valides",
|
||
updatedAt: new Date()
|
||
})
|
||
.where(eq(dlcProducts.id, id))
|
||
.returning();
|
||
return dlcProduct as DlcProductFrontend;
|
||
}
|
||
|
||
async markDlcProductStockEpuise(id: number, markedBy: string): Promise<DlcProductFrontend> {
|
||
const [dlcProduct] = await db
|
||
.update(dlcProducts)
|
||
.set({
|
||
stockEpuise: true,
|
||
stockEpuiseBy: markedBy,
|
||
stockEpuiseAt: new Date(),
|
||
updatedAt: new Date()
|
||
})
|
||
.where(eq(dlcProducts.id, id))
|
||
.returning();
|
||
return dlcProduct as DlcProductFrontend;
|
||
}
|
||
|
||
async restoreDlcProductStock(id: number): Promise<DlcProductFrontend> {
|
||
const [dlcProduct] = await db
|
||
.update(dlcProducts)
|
||
.set({
|
||
stockEpuise: false,
|
||
stockEpuiseBy: null,
|
||
stockEpuiseAt: null,
|
||
updatedAt: new Date()
|
||
})
|
||
.where(eq(dlcProducts.id, id))
|
||
.returning();
|
||
return dlcProduct as DlcProductFrontend;
|
||
}
|
||
|
||
async getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }> {
|
||
const today = new Date();
|
||
const alertDate = new Date();
|
||
alertDate.setDate(today.getDate() + 15);
|
||
|
||
let whereCondition = sql`1 = 1`;
|
||
if (groupIds && groupIds.length > 0) {
|
||
whereCondition = inArray(dlcProducts.groupId, groupIds);
|
||
}
|
||
|
||
const [stats] = await db
|
||
.select({
|
||
active: sql<number>`COUNT(CASE WHEN ${dlcProducts.expiryDate} > ${today.toISOString().split('T')[0]} THEN 1 END)`,
|
||
expiringSoon: sql<number>`COUNT(CASE WHEN ${dlcProducts.expiryDate} BETWEEN ${today.toISOString().split('T')[0]} AND ${alertDate.toISOString().split('T')[0]} THEN 1 END)`,
|
||
expired: sql<number>`COUNT(CASE WHEN ${dlcProducts.expiryDate} <= ${today.toISOString().split('T')[0]} THEN 1 END)`
|
||
})
|
||
.from(dlcProducts)
|
||
.where(whereCondition);
|
||
|
||
return {
|
||
active: stats.active || 0,
|
||
expiringSoon: stats.expiringSoon || 0,
|
||
expired: stats.expired || 0
|
||
};
|
||
}
|
||
|
||
async getTasks(groupIds?: number[], userRole?: string): Promise<TaskWithRelations[]> {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
let query = db
|
||
.select({
|
||
task: tasks,
|
||
group: groups
|
||
})
|
||
.from(tasks)
|
||
.leftJoin(groups, eq(tasks.groupId, groups.id));
|
||
|
||
// Appliquer le filtrage par magasin si spécifié
|
||
let whereConditions = [];
|
||
if (groupIds && groupIds.length > 0) {
|
||
whereConditions.push(inArray(tasks.groupId, groupIds));
|
||
}
|
||
|
||
// 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
|
||
whereConditions.push(
|
||
or(
|
||
isNull(tasks.startDate), // Tâches sans date de départ (toujours visibles)
|
||
lte(tasks.startDate, today) // Tâches dont la date de départ est arrivée
|
||
)
|
||
);
|
||
}
|
||
// Admin et directeur voient toutes les tâches, y compris les tâches programmées
|
||
|
||
if (whereConditions.length > 0) {
|
||
query = query.where(and(...whereConditions));
|
||
}
|
||
|
||
const results = await query.orderBy(desc(tasks.createdAt));
|
||
|
||
console.log('📋 DatabaseStorage.getTasks - Raw results:', {
|
||
resultCount: results.length,
|
||
userRole,
|
||
sampleTasks: results.slice(0, 2).map(r => ({
|
||
taskExists: !!r.task,
|
||
taskId: r.task?.id,
|
||
title: r.task?.title,
|
||
startDate: r.task?.startDate,
|
||
groupExists: !!r.group
|
||
}))
|
||
});
|
||
|
||
return results
|
||
.filter((row: any) => row.task) // Filtrer les tâches nulles
|
||
.map((row: any) => {
|
||
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) {
|
||
try {
|
||
const startDate = new Date(task.startDate);
|
||
if (!isNaN(startDate.getTime())) {
|
||
isFutureTask = startDate > today;
|
||
}
|
||
} catch (error) {
|
||
console.warn('Error parsing startDate:', error, task.startDate);
|
||
}
|
||
}
|
||
|
||
return {
|
||
...task,
|
||
group,
|
||
isFutureTask
|
||
};
|
||
} catch (error) {
|
||
console.error('Error processing task row:', error, row);
|
||
return null;
|
||
}
|
||
})
|
||
.filter(Boolean);
|
||
}
|
||
|
||
async getTask(id: number): Promise<TaskWithRelations | undefined> {
|
||
const [result] = await db
|
||
.select({
|
||
task: tasks,
|
||
group: groups
|
||
})
|
||
.from(tasks)
|
||
.leftJoin(groups, eq(tasks.groupId, groups.id))
|
||
.where(eq(tasks.id, id));
|
||
|
||
if (!result) return undefined;
|
||
return {
|
||
...result.task,
|
||
group: result.group
|
||
};
|
||
}
|
||
|
||
async createTask(taskData: InsertTask): Promise<Task> {
|
||
console.log('💾 DatabaseStorage.createTask - Input:', {
|
||
taskData,
|
||
dueDate: taskData.dueDate,
|
||
dueDateType: typeof taskData.dueDate,
|
||
dueDateISO: taskData.dueDate instanceof Date ? taskData.dueDate.toISOString() : taskData.dueDate
|
||
});
|
||
|
||
const [task] = await db.insert(tasks).values(taskData).returning();
|
||
|
||
console.log('💾 DatabaseStorage.createTask - Result:', {
|
||
taskId: task.id,
|
||
title: task.title,
|
||
dueDate: task.dueDate,
|
||
dueDateType: typeof task.dueDate
|
||
});
|
||
|
||
return task;
|
||
}
|
||
|
||
async updateTask(id: number, taskData: Partial<InsertTask>): Promise<Task> {
|
||
console.log('💾 DatabaseStorage.updateTask - Input:', {
|
||
taskId: id,
|
||
taskData,
|
||
dueDate: taskData.dueDate,
|
||
dueDateType: typeof taskData.dueDate,
|
||
dueDateISO: taskData.dueDate instanceof Date ? taskData.dueDate.toISOString() : taskData.dueDate
|
||
});
|
||
|
||
// Validation des données avant update
|
||
if (!taskData || typeof taskData !== 'object') {
|
||
throw new Error('Invalid task data provided');
|
||
}
|
||
|
||
// 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) {
|
||
cleanData.startDate = null;
|
||
} else {
|
||
// start_date est un timestamp en PostgreSQL - on peut passer une date
|
||
cleanData.startDate = new Date(taskData.startDate);
|
||
}
|
||
}
|
||
|
||
if (taskData.dueDate !== undefined) {
|
||
if (taskData.dueDate === '' || taskData.dueDate === null) {
|
||
cleanData.dueDate = null;
|
||
} else {
|
||
// due_date est un date en PostgreSQL - on passe la chaîne directement
|
||
cleanData.dueDate = 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:', {
|
||
taskId: id,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
stack: error instanceof Error ? error.stack : undefined,
|
||
cleanData,
|
||
originalTaskData: taskData
|
||
});
|
||
throw new Error(`Failed to update task in database: ${error instanceof Error ? error.message : String(error)}`);
|
||
}
|
||
}
|
||
|
||
async deleteTask(id: number): Promise<void> {
|
||
await db.delete(tasks).where(eq(tasks.id, id));
|
||
}
|
||
|
||
async completeTask(id: number, completedBy: string): Promise<void> {
|
||
await db
|
||
.update(tasks)
|
||
.set({
|
||
status: 'completed',
|
||
completedAt: new Date(),
|
||
completedBy,
|
||
updatedAt: new Date()
|
||
})
|
||
.where(eq(tasks.id, id));
|
||
}
|
||
|
||
// Announcement operations
|
||
async createAnnouncement(announcementData: InsertAnnouncement): Promise<Announcement> {
|
||
const announcementStorage = getAnnouncementStorage(
|
||
() => this.getUsers(),
|
||
() => this.getGroups()
|
||
);
|
||
return await announcementStorage.createAnnouncement(announcementData);
|
||
}
|
||
|
||
async getAnnouncements(groupIds?: number[]): Promise<AnnouncementWithRelations[]> {
|
||
const announcementStorage = getAnnouncementStorage(
|
||
() => this.getUsers(),
|
||
() => this.getGroups()
|
||
);
|
||
return await announcementStorage.getAnnouncements(groupIds);
|
||
}
|
||
|
||
async getAnnouncement(id: number): Promise<AnnouncementWithRelations | undefined> {
|
||
const announcementStorage = getAnnouncementStorage(
|
||
() => this.getUsers(),
|
||
() => this.getGroups()
|
||
);
|
||
return await announcementStorage.getAnnouncement(id);
|
||
}
|
||
|
||
async updateAnnouncement(id: number, announcementData: Partial<InsertAnnouncement>): Promise<AnnouncementWithRelations> {
|
||
const announcementStorage = getAnnouncementStorage(
|
||
() => this.getUsers(),
|
||
() => this.getGroups()
|
||
);
|
||
return await announcementStorage.updateAnnouncement(id, announcementData);
|
||
}
|
||
|
||
async deleteAnnouncement(id: number): Promise<boolean> {
|
||
const announcementStorage = getAnnouncementStorage(
|
||
() => this.getUsers(),
|
||
() => this.getGroups()
|
||
);
|
||
return await announcementStorage.deleteAnnouncement(id);
|
||
}
|
||
|
||
// SAV operations
|
||
async getSavTickets(filters?: {
|
||
groupIds?: number[];
|
||
status?: string;
|
||
supplierId?: number;
|
||
priority?: string;
|
||
startDate?: string;
|
||
endDate?: string;
|
||
}): Promise<SavTicketWithRelations[]> {
|
||
let query = db
|
||
.select({
|
||
ticket: savTickets,
|
||
supplier: suppliers,
|
||
group: groups,
|
||
creator: users,
|
||
})
|
||
.from(savTickets)
|
||
.leftJoin(suppliers, eq(savTickets.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(savTickets.groupId, groups.id))
|
||
.leftJoin(users, eq(savTickets.createdBy, users.id));
|
||
|
||
const conditions = [];
|
||
|
||
if (filters?.groupIds?.length) {
|
||
conditions.push(inArray(savTickets.groupId, filters.groupIds));
|
||
}
|
||
if (filters?.status) {
|
||
conditions.push(eq(savTickets.status, filters.status));
|
||
}
|
||
if (filters?.supplierId) {
|
||
conditions.push(eq(savTickets.supplierId, filters.supplierId));
|
||
}
|
||
if (filters?.priority) {
|
||
conditions.push(eq(savTickets.priority, filters.priority));
|
||
}
|
||
if (filters?.startDate) {
|
||
conditions.push(gte(savTickets.createdAt, new Date(filters.startDate)));
|
||
}
|
||
if (filters?.endDate) {
|
||
conditions.push(lte(savTickets.createdAt, new Date(filters.endDate)));
|
||
}
|
||
|
||
if (conditions.length) {
|
||
query = query.where(and(...conditions));
|
||
}
|
||
|
||
const results = await query.orderBy(desc(savTickets.createdAt));
|
||
|
||
// Get history for each ticket
|
||
const ticketsWithHistory = await Promise.all(
|
||
results.map(async (result) => {
|
||
const history = await this.getSavTicketHistory(result.ticket.id);
|
||
return {
|
||
...result.ticket,
|
||
supplier: result.supplier!,
|
||
group: result.group!,
|
||
creator: result.creator!,
|
||
history,
|
||
};
|
||
})
|
||
);
|
||
|
||
return ticketsWithHistory;
|
||
}
|
||
|
||
async getSavTicket(id: number): Promise<SavTicketWithRelations | undefined> {
|
||
const [result] = await db
|
||
.select({
|
||
ticket: savTickets,
|
||
supplier: suppliers,
|
||
group: groups,
|
||
creator: users,
|
||
})
|
||
.from(savTickets)
|
||
.leftJoin(suppliers, eq(savTickets.supplierId, suppliers.id))
|
||
.leftJoin(groups, eq(savTickets.groupId, groups.id))
|
||
.leftJoin(users, eq(savTickets.createdBy, users.id))
|
||
.where(eq(savTickets.id, id));
|
||
|
||
if (!result) return undefined;
|
||
|
||
const history = await this.getSavTicketHistory(id);
|
||
|
||
return {
|
||
...result.ticket,
|
||
supplier: result.supplier!,
|
||
group: result.group!,
|
||
creator: result.creator!,
|
||
history,
|
||
};
|
||
}
|
||
|
||
async createSavTicket(ticketData: InsertSavTicket): Promise<SavTicket> {
|
||
// Generate ticket number
|
||
const currentYear = new Date().getFullYear();
|
||
const count = await db
|
||
.select({ count: sql<number>`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
|
||
.insert(savTickets)
|
||
.values({ ...ticketData, ticketNumber })
|
||
.returning();
|
||
|
||
// Add initial history entry
|
||
await this.addSavTicketHistory({
|
||
ticketId: ticket.id,
|
||
action: 'created',
|
||
description: `Ticket créé avec le statut "${ticket.status}"`,
|
||
createdBy: ticket.createdBy,
|
||
});
|
||
|
||
return ticket;
|
||
}
|
||
|
||
async updateSavTicket(id: number, ticketData: Partial<InsertSavTicket>): Promise<SavTicket> {
|
||
const [ticket] = await db
|
||
.update(savTickets)
|
||
.set({ ...ticketData, updatedAt: new Date() })
|
||
.where(eq(savTickets.id, id))
|
||
.returning();
|
||
|
||
return ticket;
|
||
}
|
||
|
||
async deleteSavTicket(id: number): Promise<void> {
|
||
// Delete history first
|
||
await db.delete(savTicketHistory).where(eq(savTicketHistory.ticketId, id));
|
||
// Delete ticket
|
||
await db.delete(savTickets).where(eq(savTickets.id, id));
|
||
}
|
||
|
||
async getSavTicketHistory(ticketId: number): Promise<SavTicketHistoryWithCreator[]> {
|
||
const results = await db
|
||
.select({
|
||
history: savTicketHistory,
|
||
creator: users,
|
||
})
|
||
.from(savTicketHistory)
|
||
.leftJoin(users, eq(savTicketHistory.createdBy, users.id))
|
||
.where(eq(savTicketHistory.ticketId, ticketId))
|
||
.orderBy(desc(savTicketHistory.createdAt));
|
||
|
||
return results.map(result => ({
|
||
...result.history,
|
||
creator: result.creator!,
|
||
}));
|
||
}
|
||
|
||
async addSavTicketHistory(historyData: InsertSavTicketHistory): Promise<SavTicketHistory> {
|
||
const [history] = await db
|
||
.insert(savTicketHistory)
|
||
.values(historyData)
|
||
.returning();
|
||
|
||
return history;
|
||
}
|
||
|
||
async getSavTicketStats(groupIds?: number[]): Promise<{
|
||
totalTickets: number;
|
||
newTickets: number;
|
||
inProgressTickets: number;
|
||
resolvedTickets: number;
|
||
criticalTickets: number;
|
||
}> {
|
||
let query = db.select({ count: sql<number>`count(*)`, status: savTickets.status, priority: savTickets.priority })
|
||
.from(savTickets);
|
||
|
||
if (groupIds?.length) {
|
||
query = query.where(inArray(savTickets.groupId, groupIds));
|
||
}
|
||
|
||
const results = await query.groupBy(savTickets.status, savTickets.priority);
|
||
|
||
const stats = {
|
||
totalTickets: 0,
|
||
newTickets: 0,
|
||
inProgressTickets: 0,
|
||
resolvedTickets: 0,
|
||
criticalTickets: 0,
|
||
};
|
||
|
||
results.forEach(result => {
|
||
const count = result.count || 0;
|
||
stats.totalTickets += count;
|
||
|
||
if (result.status === 'nouveau') {
|
||
stats.newTickets += count;
|
||
} else if (['en_cours', 'attente_pieces', 'attente_echange'].includes(result.status)) {
|
||
stats.inProgressTickets += count;
|
||
} else if (['resolu', 'ferme'].includes(result.status)) {
|
||
stats.resolvedTickets += count;
|
||
}
|
||
|
||
if (result.priority === 'critique') {
|
||
stats.criticalTickets += count;
|
||
}
|
||
});
|
||
|
||
return stats;
|
||
}
|
||
|
||
// Weather operations
|
||
async getWeatherSettings(): Promise<WeatherSettings | undefined> {
|
||
const [settings] = await db.select().from(weatherSettings).limit(1);
|
||
return settings;
|
||
}
|
||
|
||
async createWeatherSettings(settingsData: InsertWeatherSettings): Promise<WeatherSettings> {
|
||
const [settings] = await db.insert(weatherSettings).values(settingsData).returning();
|
||
return settings;
|
||
}
|
||
|
||
async updateWeatherSettings(id: number, settingsData: Partial<InsertWeatherSettings>): Promise<WeatherSettings> {
|
||
const [settings] = await db
|
||
.update(weatherSettings)
|
||
.set({ ...settingsData, updatedAt: new Date() })
|
||
.where(eq(weatherSettings.id, id))
|
||
.returning();
|
||
return settings;
|
||
}
|
||
|
||
async getWeatherData(date: string, isCurrentYear: boolean): Promise<WeatherData | undefined> {
|
||
const [data] = await db
|
||
.select()
|
||
.from(weatherData)
|
||
.where(and(
|
||
eq(weatherData.date, date),
|
||
eq(weatherData.isCurrentYear, isCurrentYear)
|
||
))
|
||
.limit(1);
|
||
return data;
|
||
}
|
||
|
||
async createWeatherData(data: InsertWeatherData): Promise<WeatherData> {
|
||
const [weatherInfo] = await db.insert(weatherData).values(data).returning();
|
||
return weatherInfo;
|
||
}
|
||
|
||
async updateWeatherData(id: number, data: Partial<InsertWeatherData>): Promise<WeatherData> {
|
||
const [weatherInfo] = await db
|
||
.update(weatherData)
|
||
.set({ ...data, updatedAt: new Date() })
|
||
.where(eq(weatherData.id, id))
|
||
.returning();
|
||
return weatherInfo;
|
||
}
|
||
|
||
async deleteOldWeatherData(daysToKeep: number): Promise<void> {
|
||
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`);
|
||
}
|
||
|
||
async clearWeatherCache(): Promise<void> {
|
||
await db.delete(weatherData);
|
||
console.log('🧹 Weather cache cleared due to location change');
|
||
}
|
||
}
|
||
|
||
// MemStorage class for development
|
||
export class MemStorage implements IStorage {
|
||
private users = new Map<string, User>();
|
||
private groups = new Map<number, Group>();
|
||
private suppliers = new Map<number, Supplier>();
|
||
private orders = new Map<number, Order>();
|
||
private deliveries = new Map<number, Delivery>();
|
||
private userGroups = new Map<string, UserGroup[]>();
|
||
private publicities = new Map<number, Publicity>();
|
||
private publicityParticipations = new Map<number, PublicityParticipation[]>();
|
||
private customerOrders = new Map<number, CustomerOrder>();
|
||
private dlcProducts = new Map<number, DlcProduct>();
|
||
private tasks = new Map<number, Task>();
|
||
private announcements = new Map<number, Announcement>();
|
||
private publicities = new Map<number, Publicity>();
|
||
private savTickets = new Map<number, SavTicket>();
|
||
private savTicketHistories = new Map<number, SavTicketHistory[]>();
|
||
private invoiceVerificationCache = new Map<string, InvoiceVerificationCache>();
|
||
|
||
private idCounters = {
|
||
group: 1,
|
||
supplier: 1,
|
||
order: 1,
|
||
delivery: 1,
|
||
publicity: 1,
|
||
customerOrder: 1,
|
||
dlcProduct: 1,
|
||
task: 1,
|
||
announcement: 1,
|
||
savTicket: 1,
|
||
savTicketHistory: 1,
|
||
};
|
||
|
||
constructor() {
|
||
this.initializeTestData();
|
||
this.initializeExtraTestData();
|
||
}
|
||
|
||
private initializeTestData() {
|
||
// Create admin user
|
||
const adminUser: User = {
|
||
id: 'admin',
|
||
username: 'admin',
|
||
email: 'admin@example.com',
|
||
name: 'Admin',
|
||
firstName: 'Admin',
|
||
lastName: 'User',
|
||
profileImageUrl: null,
|
||
password: 'admin', // In real app, this would be hashed
|
||
role: 'admin',
|
||
passwordChanged: false,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
this.users.set(adminUser.id, adminUser);
|
||
|
||
// Initialize group counter
|
||
this.idCounters.group = 2;
|
||
|
||
// Initialize supplier counter
|
||
this.idCounters.supplier = 2;
|
||
|
||
// Create test groups
|
||
const testGroup: Group = {
|
||
id: 1,
|
||
name: 'Magasin Test',
|
||
color: '#3B82F6',
|
||
nocodbConfigId: 1, // Référence vers la config NocoDB de test
|
||
nocodbTableName: 'invoices',
|
||
nocodbTableId: 'mrr733dfb8wtt9b', // ID réel de la table NocoDB
|
||
invoiceColumnName: 'invoice_reference',
|
||
nocodbBlColumnName: 'bl_number',
|
||
nocodbAmountColumnName: 'amount',
|
||
nocodbSupplierColumnName: 'supplier',
|
||
webhookUrl: 'https://webhook.test/invoice-upload',
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
this.groups.set(testGroup.id, testGroup);
|
||
this.idCounters.group = 2;
|
||
|
||
// Create test supplier
|
||
const testSupplier: Supplier = {
|
||
id: 1,
|
||
name: 'Fournisseur Test',
|
||
contact: 'test@supplier.com',
|
||
phone: '0123456789',
|
||
hasDlc: false,
|
||
automaticReconciliation: false,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
this.suppliers.set(testSupplier.id, testSupplier);
|
||
this.idCounters.supplier = 2;
|
||
|
||
console.log('✅ MemStorage initialized with admin user (admin/admin)');
|
||
}
|
||
|
||
// User operations
|
||
async getUser(id: string): Promise<User | undefined> {
|
||
return this.users.get(id);
|
||
}
|
||
|
||
async getUserByEmail(email: string): Promise<User | undefined> {
|
||
for (const user of this.users.values()) {
|
||
if (user.email === email) return user;
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||
for (const user of this.users.values()) {
|
||
if (user.username === username) return user;
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
async upsertUser(userData: UpsertUser): Promise<User> {
|
||
const existingUser = this.users.get(userData.id);
|
||
const user: User = {
|
||
id: userData.id,
|
||
username: userData.username,
|
||
email: userData.email || null,
|
||
name: userData.name || null,
|
||
firstName: userData.firstName || null,
|
||
lastName: userData.lastName || null,
|
||
profileImageUrl: userData.profileImageUrl || null,
|
||
password: userData.password || null,
|
||
role: userData.role || 'user',
|
||
passwordChanged: userData.passwordChanged || false,
|
||
createdAt: existingUser?.createdAt || new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
this.users.set(user.id, user);
|
||
return user;
|
||
}
|
||
|
||
async getUserWithGroups(id: string): Promise<UserWithGroups | undefined> {
|
||
const user = await this.getUser(id);
|
||
if (!user) return undefined;
|
||
|
||
const userGroupsList = this.userGroups.get(id) || [];
|
||
const userGroupsWithGroups = userGroupsList.map(ug => ({
|
||
...ug,
|
||
group: this.groups.get(ug.groupId)!,
|
||
}));
|
||
|
||
return { ...user, userGroups: userGroupsWithGroups };
|
||
}
|
||
|
||
async getUsers(): Promise<User[]> {
|
||
return Array.from(this.users.values());
|
||
}
|
||
|
||
async createUser(userData: UpsertUser): Promise<User> {
|
||
return this.upsertUser(userData);
|
||
}
|
||
|
||
async updateUser(id: string, userData: Partial<UpsertUser>): Promise<User> {
|
||
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;
|
||
}
|
||
|
||
async deleteUser(id: string): Promise<void> {
|
||
this.users.delete(id);
|
||
this.userGroups.delete(id);
|
||
}
|
||
|
||
// Group operations
|
||
async getGroups(): Promise<Group[]> {
|
||
return Array.from(this.groups.values());
|
||
}
|
||
|
||
async getGroup(id: number): Promise<Group | undefined> {
|
||
return this.groups.get(id);
|
||
}
|
||
|
||
async createGroup(groupData: InsertGroup): Promise<Group> {
|
||
const id = this.idCounters.group++;
|
||
const group: Group = {
|
||
id,
|
||
name: groupData.name,
|
||
color: groupData.color || null,
|
||
nocodbConfigId: groupData.nocodbConfigId || null,
|
||
nocodbTableName: groupData.nocodbTableName || null,
|
||
nocodbTableId: groupData.nocodbTableId || null,
|
||
invoiceColumnName: groupData.invoiceColumnName || null,
|
||
nocodbBlColumnName: groupData.nocodbBlColumnName || null,
|
||
nocodbAmountColumnName: groupData.nocodbAmountColumnName || null,
|
||
nocodbSupplierColumnName: groupData.nocodbSupplierColumnName || null,
|
||
webhookUrl: groupData.webhookUrl || null,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
this.groups.set(id, group);
|
||
return group;
|
||
}
|
||
|
||
async updateGroup(id: number, groupData: Partial<InsertGroup>): Promise<Group> {
|
||
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;
|
||
}
|
||
|
||
async deleteGroup(id: number): Promise<void> {
|
||
this.groups.delete(id);
|
||
}
|
||
|
||
// Supplier operations
|
||
async getSuppliers(): Promise<Supplier[]> {
|
||
return Array.from(this.suppliers.values());
|
||
}
|
||
|
||
async createSupplier(supplierData: InsertSupplier): Promise<Supplier> {
|
||
const id = this.idCounters.supplier++;
|
||
const supplier: Supplier = {
|
||
id,
|
||
name: supplierData.name,
|
||
contact: supplierData.contact || null,
|
||
phone: supplierData.phone || null,
|
||
hasDlc: supplierData.hasDlc || null,
|
||
automaticReconciliation: supplierData.automaticReconciliation || null,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
this.suppliers.set(id, supplier);
|
||
return supplier;
|
||
}
|
||
|
||
async updateSupplier(id: number, supplierData: Partial<InsertSupplier>): Promise<Supplier> {
|
||
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;
|
||
}
|
||
|
||
async deleteSupplier(id: number): Promise<void> {
|
||
this.suppliers.delete(id);
|
||
}
|
||
|
||
// NocoDB Configuration methods
|
||
async getNocodbConfigs(): Promise<NocodbConfig[]> {
|
||
return []; // Empty array for development
|
||
}
|
||
|
||
async getNocodbConfig(id: number): Promise<NocodbConfig | undefined> {
|
||
return undefined; // Not found in development
|
||
}
|
||
|
||
async getActiveNocodbConfig(): Promise<NocodbConfig | undefined> {
|
||
// Configuration corrigée pour votre serveur NocoDB
|
||
const testConfig: NocodbConfig = {
|
||
id: 1,
|
||
name: 'Configuration Production NocoDB FFNancy',
|
||
baseUrl: 'https://nocodb.ffnancy.fr', // URL corrigée
|
||
projectId: 'pcg4uw79ukvycxc', // Project ID de vos logs
|
||
apiToken: 'xc-token-production-invoice-system',
|
||
description: 'Configuration de production pour vérification des factures FFNancy',
|
||
isActive: true,
|
||
createdBy: 'admin',
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
|
||
console.log('🔧 Configuration NocoDB active:', {
|
||
name: testConfig.name,
|
||
baseUrl: testConfig.baseUrl,
|
||
projectId: testConfig.projectId,
|
||
hasToken: !!testConfig.apiToken
|
||
});
|
||
return testConfig;
|
||
}
|
||
|
||
async createNocodbConfig(config: InsertNocodbConfig): Promise<NocodbConfig> {
|
||
const newConfig: NocodbConfig = {
|
||
id: 1,
|
||
name: config.name,
|
||
baseUrl: config.baseUrl,
|
||
projectId: config.projectId,
|
||
apiToken: config.apiToken,
|
||
description: config.description || null,
|
||
isActive: config.isActive ?? true,
|
||
createdBy: config.createdBy || null,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
return newConfig;
|
||
}
|
||
|
||
async updateNocodbConfig(id: number, config: Partial<InsertNocodbConfig>): Promise<NocodbConfig> {
|
||
const updatedConfig: NocodbConfig = {
|
||
id,
|
||
name: config.name || 'Test Config',
|
||
baseUrl: config.baseUrl || 'https://nocodb.example.com',
|
||
projectId: config.projectId || 'test-project',
|
||
apiToken: config.apiToken || 'test-token',
|
||
description: config.description || null,
|
||
isActive: config.isActive ?? true,
|
||
createdBy: config.createdBy || null,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
return updatedConfig;
|
||
}
|
||
|
||
async deleteNocodbConfig(id: number): Promise<void> {
|
||
// No-op in development
|
||
}
|
||
|
||
async getInvoiceVerificationCache(cacheKey: string): Promise<InvoiceVerificationCache | undefined> {
|
||
// Implémentation temporaire d'un cache en mémoire pour tester
|
||
const cached = this.invoiceVerificationCache.get(cacheKey);
|
||
if (cached && new Date() < new Date(cached.expiresAt)) {
|
||
console.log('✅ [MEMSTORAGE-CACHE] Cache hit pour:', { cacheKey, expires: cached.expiresAt });
|
||
return cached;
|
||
}
|
||
if (cached && new Date() >= new Date(cached.expiresAt)) {
|
||
console.log('⏰ [MEMSTORAGE-CACHE] Cache expiré, suppression:', { cacheKey });
|
||
this.invoiceVerificationCache.delete(cacheKey);
|
||
}
|
||
console.log('❌ [MEMSTORAGE-CACHE] Cache miss pour:', { cacheKey });
|
||
return undefined;
|
||
}
|
||
|
||
async createInvoiceVerificationCache(cache: InsertInvoiceVerificationCache): Promise<InvoiceVerificationCache> {
|
||
const newCache: InvoiceVerificationCache = {
|
||
id: 1,
|
||
cacheKey: cache.cacheKey,
|
||
groupId: cache.groupId,
|
||
invoiceReference: cache.invoiceReference,
|
||
supplierName: cache.supplierName || null,
|
||
exists: cache.exists,
|
||
matchType: cache.matchType,
|
||
errorMessage: cache.errorMessage || null,
|
||
cacheHit: cache.cacheHit || false,
|
||
apiCallTime: cache.apiCallTime || null,
|
||
expiresAt: cache.expiresAt,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
return newCache;
|
||
}
|
||
|
||
async saveInvoiceVerificationCache(cache: InsertInvoiceVerificationCache): Promise<InvoiceVerificationCache> {
|
||
const newCache = await this.createInvoiceVerificationCache(cache);
|
||
// Sauver en mémoire pour le cache temporaire
|
||
console.log('💾 [MEMSTORAGE-CACHE] Sauvegarde en cache:', {
|
||
cacheKey: cache.cacheKey,
|
||
exists: cache.exists,
|
||
expiresAt: cache.expiresAt
|
||
});
|
||
this.invoiceVerificationCache.set(cache.cacheKey, newCache);
|
||
return newCache;
|
||
}
|
||
|
||
async clearExpiredCache(): Promise<void> {
|
||
// No-op en développement
|
||
}
|
||
|
||
// MemStorage implementations with actual data handling
|
||
async getOrders(groupIds?: number[]): Promise<OrderWithRelations[]> {
|
||
let orders = Array.from(this.orders.values());
|
||
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())
|
||
.filter(delivery => delivery.orderId === order.id)
|
||
.map(delivery => ({
|
||
...delivery,
|
||
supplier: this.suppliers.get(delivery.supplierId),
|
||
group: this.groups.get(delivery.groupId),
|
||
creator: this.users.get(delivery.createdBy || '')
|
||
}));
|
||
|
||
return {
|
||
...order,
|
||
supplier: this.suppliers.get(order.supplierId),
|
||
group: this.groups.get(order.groupId),
|
||
creator: this.users.get(order.createdBy),
|
||
deliveries: associatedDeliveries
|
||
};
|
||
});
|
||
}
|
||
|
||
async getOrdersByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise<OrderWithRelations[]> {
|
||
const start = new Date(startDate);
|
||
const end = new Date(endDate);
|
||
let orders = Array.from(this.orders.values()).filter(order => {
|
||
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())
|
||
.filter(delivery => delivery.orderId === order.id)
|
||
.map(delivery => ({
|
||
...delivery,
|
||
supplier: this.suppliers.get(delivery.supplierId),
|
||
group: this.groups.get(delivery.groupId),
|
||
creator: this.users.get(delivery.createdBy || '')
|
||
}));
|
||
|
||
return {
|
||
...order,
|
||
supplier: this.suppliers.get(order.supplierId),
|
||
group: this.groups.get(order.groupId),
|
||
creator: this.users.get(order.createdBy),
|
||
deliveries: associatedDeliveries
|
||
};
|
||
});
|
||
}
|
||
|
||
async getOrder(id: number): Promise<OrderWithRelations | undefined> {
|
||
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)
|
||
.map(delivery => ({
|
||
...delivery,
|
||
supplier: this.suppliers.get(delivery.supplierId),
|
||
group: this.groups.get(delivery.groupId),
|
||
creator: this.users.get(delivery.createdBy || '')
|
||
}));
|
||
|
||
return {
|
||
...order,
|
||
supplier: this.suppliers.get(order.supplierId),
|
||
group: this.groups.get(order.groupId),
|
||
creator: this.users.get(order.createdBy),
|
||
deliveries: associatedDeliveries
|
||
};
|
||
}
|
||
|
||
async createOrder(orderData: InsertOrder): Promise<Order> {
|
||
const id = this.idCounters.order++;
|
||
const order: Order = {
|
||
id,
|
||
...orderData,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
this.orders.set(id, order);
|
||
return order;
|
||
}
|
||
|
||
async updateOrder(id: number, orderData: Partial<InsertOrder>): Promise<Order> {
|
||
const existingOrder = this.orders.get(id);
|
||
if (!existingOrder) throw new Error('Order not found');
|
||
|
||
const updatedOrder = {
|
||
...existingOrder,
|
||
...orderData,
|
||
updatedAt: new Date(),
|
||
};
|
||
this.orders.set(id, updatedOrder);
|
||
return updatedOrder;
|
||
}
|
||
|
||
async deleteOrder(id: number): Promise<void> {
|
||
this.orders.delete(id);
|
||
}
|
||
|
||
async getDeliveries(groupIds?: number[]): Promise<DeliveryWithRelations[]> {
|
||
let deliveries = Array.from(this.deliveries.values());
|
||
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;
|
||
if (delivery.orderId) {
|
||
const order = this.orders.get(delivery.orderId);
|
||
if (order) {
|
||
associatedOrder = {
|
||
...order,
|
||
supplier: this.suppliers.get(order.supplierId),
|
||
group: this.groups.get(order.groupId),
|
||
creator: this.users.get(order.createdBy)
|
||
};
|
||
}
|
||
}
|
||
|
||
return {
|
||
...delivery,
|
||
supplier: this.suppliers.get(delivery.supplierId),
|
||
group: this.groups.get(delivery.groupId),
|
||
creator: this.users.get(delivery.createdBy || ''),
|
||
order: associatedOrder
|
||
};
|
||
});
|
||
}
|
||
|
||
async getDeliveriesByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise<DeliveryWithRelations[]> {
|
||
const start = new Date(startDate);
|
||
const end = new Date(endDate);
|
||
let deliveries = Array.from(this.deliveries.values()).filter(delivery => {
|
||
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;
|
||
if (delivery.orderId) {
|
||
const order = this.orders.get(delivery.orderId);
|
||
if (order) {
|
||
associatedOrder = {
|
||
...order,
|
||
supplier: this.suppliers.get(order.supplierId),
|
||
group: this.groups.get(order.groupId),
|
||
creator: this.users.get(order.createdBy)
|
||
};
|
||
}
|
||
}
|
||
|
||
return {
|
||
...delivery,
|
||
supplier: this.suppliers.get(delivery.supplierId),
|
||
group: this.groups.get(delivery.groupId),
|
||
creator: this.users.get(delivery.createdBy || ''),
|
||
order: associatedOrder
|
||
};
|
||
});
|
||
}
|
||
|
||
async getDelivery(id: number): Promise<DeliveryWithRelations | undefined> {
|
||
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) {
|
||
const order = this.orders.get(delivery.orderId);
|
||
if (order) {
|
||
associatedOrder = {
|
||
...order,
|
||
supplier: this.suppliers.get(order.supplierId),
|
||
group: this.groups.get(order.groupId),
|
||
creator: this.users.get(order.createdBy)
|
||
};
|
||
}
|
||
}
|
||
|
||
return {
|
||
...delivery,
|
||
supplier: this.suppliers.get(delivery.supplierId),
|
||
group: this.groups.get(delivery.groupId),
|
||
creator: this.users.get(delivery.createdBy || ''),
|
||
order: associatedOrder
|
||
};
|
||
}
|
||
|
||
async createDelivery(deliveryData: InsertDelivery): Promise<Delivery> {
|
||
const id = this.idCounters.delivery++;
|
||
const delivery: Delivery = {
|
||
id,
|
||
...deliveryData,
|
||
status: 'planned', // Livraisons créées sont automatiquement planifiées
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
this.deliveries.set(id, delivery);
|
||
|
||
// Si une commande est liée, la marquer comme "planned" (pas delivered!)
|
||
if (deliveryData.orderId) {
|
||
try {
|
||
const order = this.orders.get(deliveryData.orderId);
|
||
if (order && order.status === 'pending') {
|
||
console.log(`🔗 Delivery #${id} linked to order #${deliveryData.orderId}, updating order status to 'planned'`);
|
||
await this.updateOrder(deliveryData.orderId, { status: 'planned' });
|
||
}
|
||
} catch (error) {
|
||
console.error(`❌ Failed to update order #${deliveryData.orderId} status to planned:`, error);
|
||
}
|
||
}
|
||
|
||
return delivery;
|
||
}
|
||
|
||
async updateDelivery(id: number, deliveryData: Partial<InsertDelivery>): Promise<Delivery> {
|
||
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 {
|
||
const order = this.orders.get(deliveryData.orderId);
|
||
if (order && order.status === 'pending') {
|
||
console.log(`🔗 CALENDAR EDIT (DEV): Delivery #${id} linked to order #${deliveryData.orderId}, updating order status to 'planned'`);
|
||
await this.updateOrder(deliveryData.orderId, { status: 'planned' });
|
||
console.log(`✅ CALENDAR EDIT (DEV): Order #${deliveryData.orderId} status updated to 'planned'`);
|
||
} else if (order) {
|
||
console.log(`ℹ️ CALENDAR EDIT (DEV): Order #${deliveryData.orderId} status is '${order.status}', no update needed`);
|
||
}
|
||
} catch (error) {
|
||
console.error(`❌ CALENDAR EDIT (DEV): Failed to update order #${deliveryData.orderId} status to planned:`, error);
|
||
}
|
||
}
|
||
|
||
return updatedDelivery;
|
||
}
|
||
|
||
async deleteDelivery(id: number): Promise<void> {
|
||
this.deliveries.delete(id);
|
||
}
|
||
|
||
async validateDelivery(id: number, blData?: { blNumber: string; blAmount: number }): Promise<void> {
|
||
const delivery = this.deliveries.get(id);
|
||
if (!delivery) throw new Error('Delivery not found');
|
||
|
||
const updatedDelivery = {
|
||
...delivery,
|
||
status: 'delivered' as const,
|
||
deliveredDate: new Date(),
|
||
blNumber: blData?.blNumber || delivery.blNumber,
|
||
blAmount: blData?.blAmount || delivery.blAmount,
|
||
updatedAt: new Date(),
|
||
};
|
||
this.deliveries.set(id, updatedDelivery);
|
||
}
|
||
|
||
async getUserGroups(): Promise<UserGroup[]> { return []; }
|
||
async assignUserToGroup(): Promise<UserGroup> { return {} as UserGroup; }
|
||
async removeUserFromGroup(): Promise<void> {}
|
||
|
||
async getMonthlyStats(): Promise<any> { return {}; }
|
||
|
||
async getPublicities(year?: number, groupIds?: number[]): Promise<PublicityWithRelations[]> {
|
||
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(', ') +
|
||
(publicities.length > 3 ? '...' : '')
|
||
);
|
||
|
||
// Return with empty participations for now (since we're using MemStorage)
|
||
return publicities.map(pub => ({
|
||
...pub,
|
||
participations: []
|
||
}));
|
||
}
|
||
async getPublicity(): Promise<PublicityWithRelations | undefined> { return undefined; }
|
||
async createPublicity(): Promise<Publicity> { return {} as Publicity; }
|
||
async updatePublicity(): Promise<Publicity> { return {} as Publicity; }
|
||
async deletePublicity(): Promise<void> {}
|
||
async getPublicityParticipations(): Promise<PublicityParticipation[]> { return []; }
|
||
async setPublicityParticipations(): Promise<void> {}
|
||
|
||
async getCustomerOrders(groupIds?: number[]): Promise<CustomerOrderWithRelations[]> {
|
||
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)!,
|
||
group: this.groups.get(order.groupId)!,
|
||
creator: this.users.get(order.createdBy)!
|
||
}));
|
||
}
|
||
|
||
async getCustomerOrder(id: number): Promise<CustomerOrderWithRelations | undefined> {
|
||
const order = this.customerOrders.get(id);
|
||
if (!order) return undefined;
|
||
|
||
return {
|
||
...order,
|
||
supplier: this.suppliers.get(order.supplierId)!,
|
||
group: this.groups.get(order.groupId)!,
|
||
creator: this.users.get(order.createdBy)!
|
||
};
|
||
}
|
||
|
||
async createCustomerOrder(orderData: InsertCustomerOrder): Promise<CustomerOrder> {
|
||
const id = this.idCounters.customerOrder++;
|
||
const order: CustomerOrder = {
|
||
id,
|
||
...orderData,
|
||
notes: orderData.notes || null,
|
||
status: orderData.status || 'En attente de Commande',
|
||
quantity: orderData.quantity || 1,
|
||
deposit: orderData.deposit || "0.00" as any,
|
||
isPromotionalPrice: orderData.isPromotionalPrice || false,
|
||
customerNotified: orderData.customerNotified || false,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
this.customerOrders.set(id, order);
|
||
return order;
|
||
}
|
||
|
||
async updateCustomerOrder(id: number, orderData: Partial<InsertCustomerOrder>): Promise<CustomerOrder> {
|
||
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;
|
||
}
|
||
|
||
async deleteCustomerOrder(id: number): Promise<void> {
|
||
this.customerOrders.delete(id);
|
||
}
|
||
|
||
async getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; search?: string; }): Promise<DlcProductWithRelations[]> {
|
||
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);
|
||
}
|
||
if (filters.supplierId) {
|
||
products = products.filter(product => product.supplierId === filters.supplierId);
|
||
}
|
||
if (filters.search) {
|
||
const searchTerm = filters.search.toLowerCase();
|
||
products = products.filter(product => {
|
||
const supplier = this.suppliers.get(product.supplierId);
|
||
return product.productName.toLowerCase().includes(searchTerm) ||
|
||
(supplier && supplier.name.toLowerCase().includes(searchTerm));
|
||
});
|
||
}
|
||
}
|
||
|
||
return products.map(product => ({
|
||
...product,
|
||
supplier: this.suppliers.get(product.supplierId)!,
|
||
group: this.groups.get(product.groupId)!,
|
||
creator: this.users.get(product.createdBy)!
|
||
}));
|
||
}
|
||
|
||
async getDlcProduct(id: number): Promise<DlcProductWithRelations | undefined> {
|
||
const product = this.dlcProducts.get(id);
|
||
if (!product) return undefined;
|
||
|
||
return {
|
||
...product,
|
||
supplier: this.suppliers.get(product.supplierId)!,
|
||
group: this.groups.get(product.groupId)!,
|
||
creator: this.users.get(product.createdBy)!
|
||
};
|
||
}
|
||
|
||
async createDlcProduct(productData: InsertDlcProduct): Promise<DlcProductFrontend> {
|
||
const id = this.idCounters.dlcProduct++;
|
||
const product: DlcProduct = {
|
||
id,
|
||
...productData,
|
||
notes: productData.notes || null,
|
||
status: productData.status || 'en_cours',
|
||
quantity: productData.quantity || 1,
|
||
unit: productData.unit || 'unité',
|
||
location: productData.location || 'Magasin',
|
||
alertThreshold: productData.alertThreshold || 15,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
this.dlcProducts.set(id, product);
|
||
return { ...product, dlcDate: new Date(product.expiryDate) } as DlcProductFrontend;
|
||
}
|
||
|
||
async updateDlcProduct(id: number, productData: Partial<InsertDlcProduct>): Promise<DlcProductFrontend> {
|
||
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;
|
||
}
|
||
|
||
async deleteDlcProduct(id: number): Promise<void> {
|
||
this.dlcProducts.delete(id);
|
||
}
|
||
|
||
async validateDlcProduct(id: number, validatedBy: string): Promise<DlcProductFrontend> {
|
||
const existingProduct = this.dlcProducts.get(id);
|
||
if (!existingProduct) throw new Error('DLC Product not found');
|
||
|
||
const updatedProduct = {
|
||
...existingProduct,
|
||
validatedBy,
|
||
validatedAt: new Date(),
|
||
status: "valides",
|
||
updatedAt: new Date()
|
||
};
|
||
this.dlcProducts.set(id, updatedProduct);
|
||
return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend;
|
||
}
|
||
|
||
async markDlcProductStockEpuise(id: number, markedBy: string): Promise<DlcProductFrontend> {
|
||
const existingProduct = this.dlcProducts.get(id);
|
||
if (!existingProduct) throw new Error('DLC Product not found');
|
||
|
||
const updatedProduct = {
|
||
...existingProduct,
|
||
stockEpuise: true,
|
||
stockEpuiseBy: markedBy,
|
||
stockEpuiseAt: new Date(),
|
||
updatedAt: new Date()
|
||
};
|
||
this.dlcProducts.set(id, updatedProduct);
|
||
return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend;
|
||
}
|
||
|
||
async restoreDlcProductStock(id: number): Promise<DlcProductFrontend> {
|
||
const existingProduct = this.dlcProducts.get(id);
|
||
if (!existingProduct) throw new Error('DLC Product not found');
|
||
|
||
const updatedProduct = {
|
||
...existingProduct,
|
||
stockEpuise: false,
|
||
stockEpuiseBy: null,
|
||
stockEpuiseAt: null,
|
||
updatedAt: new Date()
|
||
};
|
||
this.dlcProducts.set(id, updatedProduct);
|
||
return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend;
|
||
}
|
||
|
||
async getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }> {
|
||
const today = new Date();
|
||
const alertDate = new Date();
|
||
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));
|
||
}
|
||
|
||
return {
|
||
active: products.filter(p => new Date(p.expiryDate) > today).length,
|
||
expiringSoon: products.filter(p => {
|
||
const expiry = new Date(p.expiryDate);
|
||
return expiry >= today && expiry <= alertDate;
|
||
}).length,
|
||
expired: products.filter(p => new Date(p.expiryDate) <= today).length
|
||
};
|
||
}
|
||
|
||
async getTasks(groupIds?: number[], userRole?: string): Promise<TaskWithRelations[]> {
|
||
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
|
||
tasks = tasks.filter(task => {
|
||
if (!task.startDate) return true; // Tâches sans date de départ (toujours visibles)
|
||
const startDate = new Date(task.startDate);
|
||
return startDate <= today; // Tâches dont la date de départ est arrivée
|
||
});
|
||
}
|
||
// 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)!,
|
||
creator: this.users.get(task.createdBy)!,
|
||
isFutureTask: task.startDate ? new Date(task.startDate) > today : false
|
||
}));
|
||
}
|
||
|
||
async getTask(id: number): Promise<TaskWithRelations | undefined> {
|
||
const task = this.tasks.get(id);
|
||
if (!task) return undefined;
|
||
|
||
return {
|
||
...task,
|
||
group: this.groups.get(task.groupId)!,
|
||
creator: this.users.get(task.createdBy)!
|
||
};
|
||
}
|
||
|
||
async createTask(taskData: InsertTask): Promise<Task> {
|
||
const id = this.idCounters.task++;
|
||
const task: Task = {
|
||
id,
|
||
...taskData,
|
||
description: taskData.description || null,
|
||
status: taskData.status || 'pending',
|
||
priority: taskData.priority || 'medium',
|
||
completedAt: null,
|
||
completedBy: null,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
this.tasks.set(id, task);
|
||
return task;
|
||
}
|
||
|
||
private initializeExtraTestData() {
|
||
// Add test customer orders
|
||
const testOrder1: CustomerOrder = {
|
||
id: 1,
|
||
supplierId: 1,
|
||
groupId: 1,
|
||
createdBy: 'admin',
|
||
orderTaker: 'Admin',
|
||
customerName: 'Marie Dupont',
|
||
customerPhone: '0123456789',
|
||
customerEmail: null,
|
||
productDesignation: 'Table de jardin',
|
||
productReference: null,
|
||
gencode: null,
|
||
quantity: 2,
|
||
deposit: '150.00' as any,
|
||
status: 'En attente de Commande',
|
||
isPromotionalPrice: false,
|
||
customerNotified: false,
|
||
notes: 'Livraison urgente demandée',
|
||
createdAt: new Date('2025-01-15'),
|
||
updatedAt: new Date('2025-01-15'),
|
||
};
|
||
|
||
const testOrder2: CustomerOrder = {
|
||
id: 2,
|
||
supplierId: 1,
|
||
groupId: 1,
|
||
createdBy: 'admin',
|
||
orderTaker: 'Admin',
|
||
customerName: 'Pierre Martin',
|
||
customerPhone: '0987654321',
|
||
customerEmail: null,
|
||
productDesignation: 'Chaises pliantes x4',
|
||
productReference: null,
|
||
gencode: null,
|
||
quantity: 1,
|
||
deposit: '80.00' as any,
|
||
status: 'Commande passée',
|
||
isPromotionalPrice: true,
|
||
customerNotified: true,
|
||
notes: null,
|
||
createdAt: new Date('2025-01-10'),
|
||
updatedAt: new Date('2025-01-12'),
|
||
};
|
||
|
||
this.customerOrders.set(1, testOrder1);
|
||
this.customerOrders.set(2, testOrder2);
|
||
this.idCounters.customerOrder = 3;
|
||
|
||
// Add test DLC products
|
||
const testDlc1: DlcProduct = {
|
||
id: 1,
|
||
supplierId: 1,
|
||
groupId: 1,
|
||
createdBy: 'admin',
|
||
productName: 'Yaourts bio',
|
||
gencode: 'YAO001',
|
||
dateType: 'DLC',
|
||
expiryDate: '2025-08-20',
|
||
status: 'en_cours',
|
||
quantity: 24,
|
||
unit: 'pièces',
|
||
notes: 'À vendre rapidement' as string | null,
|
||
location: 'Frigo principal',
|
||
alertThreshold: 5,
|
||
validatedBy: null as string | null,
|
||
validatedAt: null,
|
||
stockEpuise: false,
|
||
stockEpuiseBy: null,
|
||
stockEpuiseAt: null,
|
||
createdAt: new Date('2025-08-10'),
|
||
updatedAt: new Date('2025-08-10'),
|
||
};
|
||
|
||
const testDlc2: DlcProduct = {
|
||
id: 2,
|
||
supplierId: 1,
|
||
groupId: 1,
|
||
createdBy: 'admin',
|
||
productName: 'Pain de mie complet',
|
||
gencode: 'PAIN002',
|
||
dateType: 'DLC',
|
||
expiryDate: '2025-08-16',
|
||
status: 'alerte',
|
||
quantity: 8,
|
||
unit: 'unités',
|
||
notes: 'DLC proche, promotion conseillée' as string | null,
|
||
location: 'Rayonnage A3',
|
||
alertThreshold: 3,
|
||
validatedBy: null as string | null,
|
||
validatedAt: null,
|
||
stockEpuise: false,
|
||
stockEpuiseBy: null,
|
||
stockEpuiseAt: null,
|
||
createdAt: new Date('2025-08-08'),
|
||
updatedAt: new Date('2025-08-13'),
|
||
};
|
||
|
||
this.dlcProducts.set(1, testDlc1);
|
||
this.dlcProducts.set(2, testDlc2);
|
||
this.idCounters.dlcProduct = 3;
|
||
|
||
// Add test tasks
|
||
const testTask1: Task = {
|
||
id: 1,
|
||
groupId: 1,
|
||
createdBy: 'admin',
|
||
title: 'Vérifier stock yaourts',
|
||
description: 'Contrôler les DLC des yaourts et organiser la rotation',
|
||
status: 'pending',
|
||
priority: 'high',
|
||
assignedTo: 'admin',
|
||
dueDate: new Date('2025-08-15'),
|
||
completedAt: null,
|
||
completedBy: null,
|
||
createdAt: new Date('2025-08-13'),
|
||
updatedAt: new Date('2025-08-13'),
|
||
};
|
||
|
||
const testTask2: Task = {
|
||
id: 2,
|
||
groupId: 1,
|
||
createdBy: 'admin',
|
||
title: 'Contacter fournisseur',
|
||
description: 'Appeler le fournisseur pour confirmer la livraison de demain',
|
||
status: 'in_progress',
|
||
priority: 'medium',
|
||
assignedTo: 'admin',
|
||
dueDate: new Date('2025-08-14'),
|
||
completedAt: null,
|
||
completedBy: null,
|
||
createdAt: new Date('2025-08-12'),
|
||
updatedAt: new Date('2025-08-13'),
|
||
};
|
||
|
||
this.tasks.set(1, testTask1);
|
||
this.tasks.set(2, testTask2);
|
||
this.idCounters.task = 3;
|
||
|
||
// Add test publicity data
|
||
const testPublicity1: Publicity = {
|
||
id: 1,
|
||
pubNumber: 2517,
|
||
designation: 'Prix Fracassés',
|
||
startDate: new Date('2025-10-27'),
|
||
endDate: new Date('2025-11-02'),
|
||
year: 2025,
|
||
createdBy: 'admin',
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
|
||
const testPublicity2: Publicity = {
|
||
id: 2,
|
||
pubNumber: 2520,
|
||
designation: 'Halloween',
|
||
startDate: new Date('2025-10-08'),
|
||
endDate: new Date('2025-10-12'),
|
||
year: 2025,
|
||
createdBy: 'admin',
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
|
||
const testPublicity3: Publicity = {
|
||
id: 3,
|
||
pubNumber: 2521,
|
||
designation: 'Prix Fous',
|
||
startDate: new Date('2025-10-20'),
|
||
endDate: new Date('2025-10-26'),
|
||
year: 2025,
|
||
createdBy: 'admin',
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
|
||
const testPublicity4: Publicity = {
|
||
id: 4,
|
||
pubNumber: 2518,
|
||
designation: 'Le mois anniversaire 1',
|
||
startDate: new Date('2025-11-17'),
|
||
endDate: new Date('2025-11-23'),
|
||
year: 2025,
|
||
createdBy: 'admin',
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
|
||
const testPublicity5: Publicity = {
|
||
id: 5,
|
||
pubNumber: 2523,
|
||
designation: 'Noël',
|
||
startDate: new Date('2025-11-05'),
|
||
endDate: new Date('2025-11-09'),
|
||
year: 2025,
|
||
createdBy: 'admin',
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
|
||
const testPublicity6: Publicity = {
|
||
id: 6,
|
||
pubNumber: 2524,
|
||
designation: 'Cadeaux et noel',
|
||
startDate: new Date('2025-11-17'),
|
||
endDate: new Date('2025-11-23'),
|
||
year: 2025,
|
||
createdBy: 'admin',
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
|
||
const testPublicity7: Publicity = {
|
||
id: 7,
|
||
pubNumber: 2525,
|
||
designation: 'Deco et Table en Fete',
|
||
startDate: new Date('2025-11-24'),
|
||
endDate: new Date('2025-11-30'),
|
||
year: 2025,
|
||
createdBy: 'admin',
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
|
||
const testPublicity8: Publicity = {
|
||
id: 8,
|
||
pubNumber: 2526,
|
||
designation: 'Sous le Sapin',
|
||
startDate: new Date('2025-12-03'),
|
||
endDate: new Date('2025-12-07'),
|
||
year: 2025,
|
||
createdBy: 'admin',
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
};
|
||
|
||
this.publicities.set(1, testPublicity1);
|
||
this.publicities.set(2, testPublicity2);
|
||
this.publicities.set(3, testPublicity3);
|
||
this.publicities.set(4, testPublicity4);
|
||
this.publicities.set(5, testPublicity5);
|
||
this.publicities.set(6, testPublicity6);
|
||
this.publicities.set(7, testPublicity7);
|
||
this.publicities.set(8, testPublicity8);
|
||
this.idCounters.publicity = 9;
|
||
|
||
// Initialize test SAV data
|
||
this.initializeSavTestData();
|
||
}
|
||
|
||
async updateTask(id: number, taskData: Partial<InsertTask>): Promise<Task> {
|
||
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;
|
||
}
|
||
|
||
async deleteTask(id: number): Promise<void> {
|
||
this.tasks.delete(id);
|
||
}
|
||
|
||
async completeTask(id: number, completedBy: string): Promise<void> {
|
||
const existingTask = this.tasks.get(id);
|
||
if (!existingTask) throw new Error('Task not found');
|
||
|
||
const updatedTask = {
|
||
...existingTask,
|
||
status: 'completed' as const,
|
||
completedAt: new Date(),
|
||
completedBy,
|
||
updatedAt: new Date()
|
||
};
|
||
this.tasks.set(id, updatedTask);
|
||
}
|
||
|
||
// SAV operations
|
||
async getSavTickets(filters?: {
|
||
groupIds?: number[];
|
||
status?: string;
|
||
supplierId?: number;
|
||
priority?: string;
|
||
startDate?: string;
|
||
endDate?: string;
|
||
}): Promise<SavTicketWithRelations[]> {
|
||
const tickets = Array.from(this.savTickets.values());
|
||
|
||
let filteredTickets = tickets;
|
||
|
||
if (filters?.groupIds?.length) {
|
||
filteredTickets = filteredTickets.filter(ticket => filters.groupIds!.includes(ticket.groupId));
|
||
}
|
||
if (filters?.status) {
|
||
filteredTickets = filteredTickets.filter(ticket => ticket.status === filters.status);
|
||
}
|
||
if (filters?.supplierId) {
|
||
filteredTickets = filteredTickets.filter(ticket => ticket.supplierId === filters.supplierId);
|
||
}
|
||
if (filters?.priority) {
|
||
filteredTickets = filteredTickets.filter(ticket => ticket.priority === filters.priority);
|
||
}
|
||
if (filters?.startDate) {
|
||
const startDate = new Date(filters.startDate);
|
||
filteredTickets = filteredTickets.filter(ticket =>
|
||
ticket.createdAt && ticket.createdAt >= startDate
|
||
);
|
||
}
|
||
if (filters?.endDate) {
|
||
const endDate = new Date(filters.endDate);
|
||
filteredTickets = filteredTickets.filter(ticket =>
|
||
ticket.createdAt && ticket.createdAt <= endDate
|
||
);
|
||
}
|
||
|
||
// Sort by creation date (newest first)
|
||
filteredTickets.sort((a, b) => {
|
||
const dateA = a.createdAt || new Date(0);
|
||
const dateB = b.createdAt || new Date(0);
|
||
return dateB.getTime() - dateA.getTime();
|
||
});
|
||
|
||
// Add relations for each ticket
|
||
const ticketsWithRelations = await Promise.all(
|
||
filteredTickets.map(async (ticket) => {
|
||
const supplier = this.suppliers.get(ticket.supplierId);
|
||
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!,
|
||
group: group!,
|
||
creator: creator!,
|
||
history,
|
||
};
|
||
})
|
||
);
|
||
|
||
return ticketsWithRelations;
|
||
}
|
||
|
||
async getSavTicket(id: number): Promise<SavTicketWithRelations | undefined> {
|
||
const ticket = this.savTickets.get(id);
|
||
if (!ticket) return undefined;
|
||
|
||
const supplier = this.suppliers.get(ticket.supplierId);
|
||
const group = this.groups.get(ticket.groupId);
|
||
const creator = this.users.get(ticket.createdBy);
|
||
const history = await this.getSavTicketHistory(id);
|
||
|
||
return {
|
||
...ticket,
|
||
supplier: supplier!,
|
||
group: group!,
|
||
creator: creator!,
|
||
history,
|
||
};
|
||
}
|
||
|
||
async createSavTicket(ticketData: InsertSavTicket): Promise<SavTicket> {
|
||
const id = this.idCounters.savTicket++;
|
||
|
||
// Generate ticket number
|
||
const currentYear = new Date().getFullYear();
|
||
const ticketNumber = `SAV-${currentYear}-${String(id).padStart(4, '0')}`;
|
||
|
||
const ticket: SavTicket = {
|
||
id,
|
||
ticketNumber,
|
||
...ticketData,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
resolvedAt: null,
|
||
closedAt: null,
|
||
};
|
||
|
||
this.savTickets.set(id, ticket);
|
||
|
||
// Add initial history entry
|
||
await this.addSavTicketHistory({
|
||
ticketId: id,
|
||
action: 'created',
|
||
description: `Ticket créé avec le statut "${ticket.status}"`,
|
||
createdBy: ticket.createdBy,
|
||
});
|
||
|
||
return ticket;
|
||
}
|
||
|
||
async updateSavTicket(id: number, ticketData: Partial<InsertSavTicket>): Promise<SavTicket> {
|
||
const existingTicket = this.savTickets.get(id);
|
||
if (!existingTicket) throw new Error('SAV ticket not found');
|
||
|
||
const updatedTicket = {
|
||
...existingTicket,
|
||
...ticketData,
|
||
updatedAt: new Date()
|
||
};
|
||
|
||
// Set resolved/closed dates based on status
|
||
if (ticketData.status === 'resolu' && !updatedTicket.resolvedAt) {
|
||
updatedTicket.resolvedAt = new Date();
|
||
}
|
||
if (ticketData.status === 'ferme' && !updatedTicket.closedAt) {
|
||
updatedTicket.closedAt = new Date();
|
||
}
|
||
|
||
this.savTickets.set(id, updatedTicket);
|
||
return updatedTicket;
|
||
}
|
||
|
||
async deleteSavTicket(id: number): Promise<void> {
|
||
// Delete history first
|
||
this.savTicketHistories.delete(id);
|
||
// Delete ticket
|
||
this.savTickets.delete(id);
|
||
}
|
||
|
||
async getSavTicketHistory(ticketId: number): Promise<SavTicketHistoryWithCreator[]> {
|
||
const histories = this.savTicketHistories.get(ticketId) || [];
|
||
|
||
const historiesWithCreators = histories.map(history => {
|
||
const creator = this.users.get(history.createdBy);
|
||
return {
|
||
...history,
|
||
creator: creator!,
|
||
};
|
||
});
|
||
|
||
// Sort by creation date (newest first)
|
||
return historiesWithCreators.sort((a, b) => {
|
||
const dateA = a.createdAt || new Date(0);
|
||
const dateB = b.createdAt || new Date(0);
|
||
return dateB.getTime() - dateA.getTime();
|
||
});
|
||
}
|
||
|
||
async addSavTicketHistory(historyData: InsertSavTicketHistory): Promise<SavTicketHistory> {
|
||
const id = this.idCounters.savTicketHistory++;
|
||
|
||
const history: SavTicketHistory = {
|
||
id,
|
||
...historyData,
|
||
createdAt: new Date(),
|
||
};
|
||
|
||
const existingHistories = this.savTicketHistories.get(historyData.ticketId) || [];
|
||
existingHistories.push(history);
|
||
this.savTicketHistories.set(historyData.ticketId, existingHistories);
|
||
|
||
return history;
|
||
}
|
||
|
||
async getSavTicketStats(groupIds?: number[]): Promise<{
|
||
totalTickets: number;
|
||
newTickets: number;
|
||
inProgressTickets: number;
|
||
resolvedTickets: number;
|
||
criticalTickets: number;
|
||
}> {
|
||
let tickets = Array.from(this.savTickets.values());
|
||
|
||
if (groupIds?.length) {
|
||
tickets = tickets.filter(ticket => groupIds.includes(ticket.groupId));
|
||
}
|
||
|
||
const stats = {
|
||
totalTickets: tickets.length,
|
||
newTickets: tickets.filter(t => t.status === 'nouveau').length,
|
||
inProgressTickets: tickets.filter(t =>
|
||
['en_cours', 'attente_pieces', 'attente_echange'].includes(t.status)
|
||
).length,
|
||
resolvedTickets: tickets.filter(t =>
|
||
['resolu', 'ferme'].includes(t.status)
|
||
).length,
|
||
criticalTickets: tickets.filter(t => t.priority === 'critique').length,
|
||
};
|
||
|
||
return stats;
|
||
}
|
||
|
||
private initializeSavTestData() {
|
||
// Test SAV tickets
|
||
const testTicket1: SavTicket = {
|
||
id: 1,
|
||
ticketNumber: 'SAV-2025-0001',
|
||
groupId: 1,
|
||
supplierId: 1,
|
||
clientName: 'Jean Dupont',
|
||
clientPhone: '03 83 12 34 56',
|
||
problemDescription: 'Produit défaillant après 2 jours d\'utilisation. Écran cassé et boutons non fonctionnels.',
|
||
productGencode: '12345678901234',
|
||
productReference: 'REF-ABC-123',
|
||
productDesignation: 'Tablette Samsung Galaxy Tab A7',
|
||
status: 'nouveau',
|
||
priority: 'haute',
|
||
createdBy: 'admin',
|
||
createdAt: new Date('2025-08-10T08:30:00'),
|
||
updatedAt: new Date('2025-08-10T08:30:00'),
|
||
resolvedAt: null,
|
||
closedAt: null,
|
||
};
|
||
|
||
const testTicket2: SavTicket = {
|
||
id: 2,
|
||
ticketNumber: 'SAV-2025-0002',
|
||
groupId: 1,
|
||
supplierId: 2,
|
||
clientName: 'Marie Martin',
|
||
clientPhone: '06 12 34 56 78',
|
||
problemDescription: 'Pièces manquantes dans le colis. Manque le chargeur et les écouteurs.',
|
||
productGencode: '23456789012345',
|
||
productReference: 'REF-XYZ-456',
|
||
productDesignation: 'Smartphone iPhone 14',
|
||
status: 'en_cours',
|
||
priority: 'normale',
|
||
createdBy: 'admin',
|
||
createdAt: new Date('2025-08-09T14:15:00'),
|
||
updatedAt: new Date('2025-08-12T10:20:00'),
|
||
resolvedAt: null,
|
||
closedAt: null,
|
||
};
|
||
|
||
const testTicket3: SavTicket = {
|
||
id: 3,
|
||
ticketNumber: 'SAV-2025-0003',
|
||
groupId: 1,
|
||
supplierId: 1,
|
||
clientName: 'Pierre Durand',
|
||
clientPhone: '03 83 98 76 54',
|
||
problemDescription: 'En attente de la pièce de rechange. Écran de remplacement commandé chez le fournisseur.',
|
||
productGencode: '34567890123456',
|
||
productReference: 'REF-DEF-789',
|
||
productDesignation: 'Ordinateur portable Dell Inspiron',
|
||
status: 'attente_pieces',
|
||
priority: 'normale',
|
||
createdBy: 'admin',
|
||
createdAt: new Date('2025-08-08T09:45:00'),
|
||
updatedAt: new Date('2025-08-13T16:30:00'),
|
||
resolvedAt: null,
|
||
closedAt: null,
|
||
};
|
||
|
||
const testTicket4: SavTicket = {
|
||
id: 4,
|
||
ticketNumber: 'SAV-2025-0004',
|
||
groupId: 1,
|
||
supplierId: 3,
|
||
clientName: 'Sophie Moreau',
|
||
clientPhone: '06 98 76 54 32',
|
||
problemDescription: 'Problème résolu. Produit réparé et testé. Prêt pour le retour client.',
|
||
productGencode: '45678901234567',
|
||
productReference: 'REF-GHI-012',
|
||
productDesignation: 'Casque Sony WH-1000XM4',
|
||
status: 'resolu',
|
||
priority: 'faible',
|
||
createdBy: 'admin',
|
||
createdAt: new Date('2025-08-05T11:20:00'),
|
||
updatedAt: new Date('2025-08-14T08:00:00'),
|
||
resolvedAt: new Date('2025-08-14T08:00:00'),
|
||
closedAt: null,
|
||
};
|
||
|
||
const testTicket5: SavTicket = {
|
||
id: 5,
|
||
ticketNumber: 'SAV-2025-0005',
|
||
groupId: 1,
|
||
supplierId: 2,
|
||
clientName: 'Luc Bernard',
|
||
clientPhone: '03 83 45 67 89',
|
||
problemDescription: 'URGENT - Produit en panne critique pour activité professionnelle. Besoin intervention rapide.',
|
||
productGencode: '56789012345678',
|
||
productReference: 'REF-JKL-345',
|
||
productDesignation: 'Imprimante HP LaserJet Pro',
|
||
status: 'en_cours',
|
||
priority: 'critique',
|
||
createdBy: 'admin',
|
||
createdAt: new Date('2025-08-14T07:30:00'),
|
||
updatedAt: new Date('2025-08-14T07:30:00'),
|
||
resolvedAt: null,
|
||
closedAt: null,
|
||
};
|
||
|
||
// Store the tickets
|
||
this.savTickets.set(1, testTicket1);
|
||
this.savTickets.set(2, testTicket2);
|
||
this.savTickets.set(3, testTicket3);
|
||
this.savTickets.set(4, testTicket4);
|
||
this.savTickets.set(5, testTicket5);
|
||
|
||
// Add history entries for each ticket
|
||
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') },
|
||
{ ticketId: 5, action: 'comment', description: 'Priorité critique - Intervention d\'urgence programmée pour ce matin.', createdBy: 'admin', createdAt: new Date('2025-08-14T07:35:00') },
|
||
];
|
||
|
||
// Initialize history for each ticket
|
||
histories.forEach((historyEntry) => {
|
||
const id = this.idCounters.savTicketHistory++;
|
||
const history: SavTicketHistory = {
|
||
id,
|
||
...historyEntry,
|
||
};
|
||
|
||
const existingHistories = this.savTicketHistories.get(historyEntry.ticketId) || [];
|
||
existingHistories.push(history);
|
||
this.savTicketHistories.set(historyEntry.ticketId, existingHistories);
|
||
});
|
||
|
||
this.idCounters.savTicket = 6;
|
||
console.log('✅ SAV test data initialized with 5 tickets and comprehensive history');
|
||
}
|
||
|
||
// Weather operations
|
||
async getWeatherSettings(): Promise<WeatherSettings | undefined> {
|
||
// In memory storage, return a default setting for development
|
||
return {
|
||
id: 1,
|
||
apiKey: "6PSHYLBJB9EEYNZ7NDQ5BPGBZ", // Token fourni par l'utilisateur
|
||
location: "Nancy, France",
|
||
isActive: true,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date()
|
||
};
|
||
}
|
||
|
||
async createWeatherSettings(settings: InsertWeatherSettings): Promise<WeatherSettings> {
|
||
// For development, we'll just return the created settings with an ID
|
||
return {
|
||
id: 1,
|
||
...settings,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date()
|
||
};
|
||
}
|
||
|
||
async updateWeatherSettings(id: number, settings: Partial<InsertWeatherSettings>): Promise<WeatherSettings> {
|
||
const existing = await this.getWeatherSettings();
|
||
return {
|
||
...existing!,
|
||
...settings,
|
||
updatedAt: new Date()
|
||
};
|
||
}
|
||
|
||
async getWeatherData(date: string, isCurrentYear: boolean): Promise<WeatherData | undefined> {
|
||
// In development, return some mock data for testing
|
||
return undefined; // Will trigger API calls
|
||
}
|
||
|
||
async createWeatherData(data: InsertWeatherData): Promise<WeatherData> {
|
||
return {
|
||
id: 1,
|
||
...data,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date()
|
||
};
|
||
}
|
||
|
||
async updateWeatherData(id: number, data: Partial<InsertWeatherData>): Promise<WeatherData> {
|
||
const existing = await this.getWeatherData(data.date || "", data.isCurrentYear || true);
|
||
return {
|
||
...existing!,
|
||
...data,
|
||
updatedAt: new Date()
|
||
};
|
||
}
|
||
|
||
async deleteOldWeatherData(daysToKeep: number): Promise<void> {
|
||
// In development, this is a no-op
|
||
console.log(`🧹 DEV: Would delete weather data older than ${daysToKeep} days`);
|
||
}
|
||
|
||
async clearWeatherCache(): Promise<void> {
|
||
// In development, this is a no-op
|
||
console.log('🧹 DEV: Weather cache cleared due to location change');
|
||
}
|
||
|
||
// Announcement operations
|
||
async createAnnouncement(announcementData: InsertAnnouncement): Promise<Announcement> {
|
||
const announcementStorage = getAnnouncementStorage(
|
||
async () => Array.from(this.users.values()),
|
||
async () => Array.from(this.groups.values())
|
||
);
|
||
return await announcementStorage.createAnnouncement(announcementData);
|
||
}
|
||
|
||
async getAnnouncements(groupIds?: number[]): Promise<AnnouncementWithRelations[]> {
|
||
const announcementStorage = getAnnouncementStorage(
|
||
async () => Array.from(this.users.values()),
|
||
async () => Array.from(this.groups.values())
|
||
);
|
||
return await announcementStorage.getAnnouncements(groupIds);
|
||
}
|
||
|
||
async getAnnouncement(id: number): Promise<AnnouncementWithRelations | undefined> {
|
||
const announcementStorage = getAnnouncementStorage(
|
||
async () => Array.from(this.users.values()),
|
||
async () => Array.from(this.groups.values())
|
||
);
|
||
return await announcementStorage.getAnnouncement(id);
|
||
}
|
||
|
||
async updateAnnouncement(id: number, announcementData: Partial<InsertAnnouncement>): Promise<AnnouncementWithRelations> {
|
||
const announcementStorage = getAnnouncementStorage(
|
||
async () => Array.from(this.users.values()),
|
||
async () => Array.from(this.groups.values())
|
||
);
|
||
return await announcementStorage.updateAnnouncement(id, announcementData);
|
||
}
|
||
|
||
async deleteAnnouncement(id: number): Promise<boolean> {
|
||
const announcementStorage = getAnnouncementStorage(
|
||
async () => Array.from(this.users.values()),
|
||
async () => Array.from(this.groups.values())
|
||
);
|
||
return await announcementStorage.deleteAnnouncement(id);
|
||
}
|
||
}
|
||
|
||
// Use MemStorage in development, DatabaseStorage in production
|
||
const isProduction = process.env.NODE_ENV === 'production';
|
||
console.log('🔗 Database initialization:', {
|
||
NODE_ENV: process.env.NODE_ENV,
|
||
isProduction: isProduction,
|
||
hasDbUrl: !!process.env.DATABASE_URL,
|
||
dbHost: process.env.DATABASE_URL?.split('@')[1]?.split('/')[0] || 'unknown'
|
||
});
|
||
|
||
export const storage: IStorage = isProduction ? new DatabaseStorage() : new MemStorage();
|
||
if (isProduction) {
|
||
console.log('✅ Using DatabaseStorage (production PostgreSQL)');
|
||
} else {
|
||
console.log('🔧 DEV: Using MemStorage (development fallback)');
|
||
} |