mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
Fix database storage to correctly check cache expiration and delete expired entries, preventing stale data from being served. Added logs for cache hits, misses, and expirations. Replit-Commit-Author: Agent Replit-Commit-Session-Id: e437ffed-c31b-44eb-9fd5-b6f9faa8e7ce Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/e437ffed-c31b-44eb-9fd5-b6f9faa8e7ce/6kmwY2j
3435 lines
115 KiB
TypeScript
3435 lines
115 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>;
|
|
getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }>;
|
|
|
|
// Task operations
|
|
getTasks(groupIds?: number[]): 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) {
|
|
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) {
|
|
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) {
|
|
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();
|
|
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));
|
|
}
|
|
|
|
const results = await query.orderBy(desc(publicities.createdAt));
|
|
|
|
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))
|
|
: [];
|
|
|
|
return results.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(),
|
|
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[]): Promise<TaskWithRelations[]> {
|
|
let query = db
|
|
.select({
|
|
task: tasks,
|
|
group: groups
|
|
})
|
|
.from(tasks)
|
|
.leftJoin(groups, eq(tasks.groupId, groups.id));
|
|
|
|
if (groupIds && groupIds.length > 0) {
|
|
query = query.where(inArray(tasks.groupId, groupIds));
|
|
}
|
|
|
|
const results = await query.orderBy(desc(tasks.createdAt));
|
|
return results.map((row: any) => ({
|
|
...row.task,
|
|
group: row.group
|
|
}));
|
|
}
|
|
|
|
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> {
|
|
const [task] = await db.insert(tasks).values(taskData).returning();
|
|
return task;
|
|
}
|
|
|
|
async updateTask(id: number, taskData: Partial<InsertTask>): Promise<Task> {
|
|
const [task] = await db
|
|
.update(tasks)
|
|
.set({ ...taskData, updatedAt: new Date() })
|
|
.where(eq(tasks.id, id))
|
|
.returning();
|
|
return task;
|
|
}
|
|
|
|
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 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);
|
|
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(): Promise<PublicityWithRelations[]> { return []; }
|
|
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(),
|
|
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[]): Promise<TaskWithRelations[]> {
|
|
let tasks = Array.from(this.tasks.values());
|
|
|
|
if (groupIds && groupIds.length > 0) {
|
|
tasks = tasks.filter(task => groupIds.includes(task.groupId));
|
|
}
|
|
|
|
return tasks.map(task => ({
|
|
...task,
|
|
group: this.groups.get(task.groupId)!,
|
|
creator: this.users.get(task.createdBy)!
|
|
}));
|
|
}
|
|
|
|
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,
|
|
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,
|
|
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;
|
|
|
|
// 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)');
|
|
} |