Files
LogiFlow/server/storage.production.ts
T
2025-08-11 14:43:48 +00:00

3099 lines
106 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { pool } from "./db.production";
import { hashPassword } from './auth-utils.production';
import { nanoid } from 'nanoid';
import type { IStorage } from "./storage";
import type {
User,
UpsertUser,
Group,
InsertGroup,
Supplier,
InsertSupplier,
Order,
InsertOrder,
Delivery,
InsertDelivery,
UserGroup,
InsertUserGroup,
Publicity,
InsertPublicity,
PublicityParticipation,
Role,
InsertRole,
Permission,
InsertPermission,
RolePermission,
NocodbConfig,
InsertNocodbConfig,
CustomerOrder,
InsertCustomerOrder,
DlcProduct,
InsertDlcProduct,
DlcProductFrontend,
InsertDlcProductFrontend,
Task,
InsertTask
} from "../shared/schema";
// Production storage implementation using raw PostgreSQL queries
export class DatabaseStorage implements IStorage {
// Helper method to safely format dates to ISO strings
private formatDate(date: any): string | null {
if (!date) return null;
// If already a string, validate it's a proper date string
if (typeof date === 'string') {
try {
const parsedDate = new Date(date);
if (isNaN(parsedDate.getTime())) {
console.warn('Invalid date string:', date);
return null;
}
return parsedDate.toISOString();
} catch (error) {
console.warn('Failed to parse date string:', date, error);
return null;
}
}
// If it's a Date object
if (date instanceof Date) {
if (isNaN(date.getTime())) {
console.warn('Invalid Date object:', date);
return null;
}
return date.toISOString();
}
// Try to create a Date from the value
try {
const newDate = new Date(date);
if (isNaN(newDate.getTime())) {
console.warn('Cannot convert to valid date:', date, typeof date);
return null;
}
return newDate.toISOString();
} catch (error) {
console.warn('Failed to format date:', date, error);
return null;
}
}
async getUser(id: string): Promise<User | undefined> {
const result = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
return result.rows[0] || undefined;
}
async getUserByEmail(email: string): Promise<User | undefined> {
const result = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
return result.rows[0] || undefined;
}
async getUserByUsername(username: string): Promise<User | undefined> {
const result = await pool.query('SELECT * FROM users WHERE username = $1', [username]);
return result.rows[0] || undefined;
}
async upsertUser(userData: UpsertUser): Promise<User> {
const existing = await this.getUserByEmail(userData.email);
if (existing) {
return this.updateUser(existing.id, userData);
}
return this.createUser(userData);
}
async getUserWithGroups(id: string): Promise<any> {
const user = await this.getUser(id);
if (!user) return undefined;
// Récupérer les groupes de l'utilisateur
const groupsResult = await pool.query(`
SELECT g.*, ug.user_id, ug.group_id
FROM groups g
JOIN user_groups ug ON g.id = ug.group_id
WHERE ug.user_id = $1
`, [id]);
// Récupérer les rôles de l'utilisateur
const rolesResult = await pool.query(`
SELECT r.*, ur.assigned_by, ur.assigned_at
FROM roles r
JOIN user_roles ur ON r.id = ur.role_id
WHERE ur.user_id = $1
`, [id]);
return {
...user,
userGroups: groupsResult.rows.map(row => ({
userId: row.user_id,
groupId: row.group_id,
group: {
id: row.id,
name: row.name,
color: row.color,
createdAt: row.created_at,
updatedAt: row.updated_at
}
})),
userRoles: rolesResult.rows.map(row => ({
userId: user.id,
roleId: row.id,
assignedBy: row.assigned_by,
assignedAt: row.assigned_at,
role: {
id: row.id,
name: row.name,
displayName: row.display_name,
description: row.description,
color: row.color,
isSystem: row.is_system,
isActive: row.is_active
}
}))
};
}
async getUsers(): Promise<User[]> {
// Version complète récupérant les utilisateurs avec leurs rôles ET groupes
try {
// Récupérer tous les utilisateurs
const usersResult = await pool.query(`
SELECT
u.id,
u.username,
u.email,
u.name,
u.password,
u.role,
u.password_changed,
u.created_at,
u.updated_at
FROM users u
ORDER BY u.created_at DESC
`);
console.log(`✅ getUsers found ${usersResult.rows.length} users`);
// Pour chaque utilisateur, récupérer ses rôles et groupes
const usersWithData = await Promise.all(usersResult.rows.map(async (user) => {
// Récupérer les rôles
const rolesResult = await pool.query(`
SELECT r.*, ur.assigned_by, ur.assigned_at
FROM roles r
JOIN user_roles ur ON r.id = ur.role_id
WHERE ur.user_id = $1
`, [user.id]);
// Récupérer les groupes
const groupsResult = await pool.query(`
SELECT g.*, ug.user_id, ug.group_id
FROM groups g
JOIN user_groups ug ON g.id = ug.group_id
WHERE ug.user_id = $1
`, [user.id]);
return {
...user,
userRoles: rolesResult.rows.map(role => ({
userId: user.id,
roleId: role.id,
assignedBy: role.assigned_by,
assignedAt: role.assigned_at,
role: {
id: role.id,
name: role.name,
displayName: role.display_name,
description: role.description,
color: role.color,
isSystem: role.is_system,
isActive: role.is_active
}
})),
userGroups: groupsResult.rows.map(group => ({
userId: group.user_id,
groupId: group.group_id,
group: {
id: group.id,
name: group.name,
color: group.color,
createdAt: group.created_at,
updatedAt: group.updated_at
}
}))
};
}));
console.log(`✅ getUsers returned ${usersWithData.length} users with roles and groups`);
return usersWithData;
} catch (error) {
console.error('❌ Error in getUsers with roles and groups, falling back to simple query:', error);
// Fallback: requête simple sans rôles ni groupes si erreur
const simpleResult = await pool.query('SELECT * FROM users ORDER BY created_at DESC');
return simpleResult.rows.map(row => ({
...row,
userRoles: [],
userGroups: []
}));
}
}
async createUser(userData: UpsertUser): Promise<User> {
// Hash password if provided
const hashedPassword = userData.password ? await hashPassword(userData.password) : null;
// Ensure username is not null - use email prefix or generate from name
let username = userData.username;
if (!username) {
if (userData.email) {
username = userData.email.split('@')[0];
} else if (userData.firstName && userData.lastName) {
username = `${userData.firstName.toLowerCase()}.${userData.lastName.toLowerCase()}`;
} else if (userData.name) {
username = userData.name.toLowerCase().replace(/\s+/g, '.');
} else {
username = `user_${nanoid(8)}`;
}
}
console.log('🔍 PRODUCTION createUser - Generated username:', username, 'for data:', {
originalUsername: userData.username,
email: userData.email,
firstName: userData.firstName,
lastName: userData.lastName,
name: userData.name
});
const result = await pool.query(`
INSERT INTO users (id, username, email, name, first_name, last_name, profile_image_url, password, role, password_changed)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *
`, [
userData.id || nanoid(),
username,
userData.email,
userData.name,
userData.firstName,
userData.lastName,
userData.profileImageUrl,
hashedPassword,
userData.role || 'employee',
userData.passwordChanged || false
]);
return result.rows[0];
}
async updateUser(id: string, userData: Partial<UpsertUser>): Promise<User> {
console.log('🔄 updateUser called:', { id, userData });
try {
// Vérifier que l'utilisateur existe
const existingUser = await this.getUser(id);
if (!existingUser) {
throw new Error(`Utilisateur avec l'ID ${id} non trouvé`);
}
console.log('✅ User found:', existingUser.username);
// Validation des champs obligatoires seulement si fournis
if (userData.firstName !== undefined && (!userData.firstName || !userData.firstName.trim())) {
throw new Error('Le prénom ne peut pas être vide');
}
if (userData.lastName !== undefined && (!userData.lastName || !userData.lastName.trim())) {
throw new Error('Le nom ne peut pas être vide');
}
if (userData.email !== undefined && (!userData.email || !userData.email.trim())) {
throw new Error('L\'email ne peut pas être vide');
}
if (userData.email && !userData.email.includes('@')) {
throw new Error('L\'email doit être valide');
}
const fields = [];
const values = [];
let paramIndex = 1;
for (const [key, value] of Object.entries(userData)) {
// Ignorer les chaînes vides pour les champs texte, mais accepter false/true pour les booléens
const shouldSkip = value === undefined || value === null ||
(typeof value === 'string' && value.trim() === '') ||
(key === 'password' && (!value || value.trim() === ''));
if (!shouldSkip) {
if (key === 'password') {
// Hash password before storing et marquer comme changé
const hashedPassword = await hashPassword(value as string);
fields.push(`password = $${paramIndex}`);
values.push(hashedPassword);
paramIndex++;
// Marquer le mot de passe comme changé
fields.push(`password_changed = $${paramIndex}`);
values.push(true);
} else {
const dbKey = key === 'firstName' ? 'first_name' :
key === 'lastName' ? 'last_name' :
key === 'profileImageUrl' ? 'profile_image_url' :
key === 'passwordChanged' ? 'password_changed' : key;
fields.push(`${dbKey} = $${paramIndex}`);
values.push(value);
}
paramIndex++;
}
}
if (fields.length === 0) {
console.log('⚠️ No fields to update, returning existing user');
return existingUser;
}
values.push(id);
console.log('📝 SQL Query:', `UPDATE users SET ${fields.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = $${paramIndex}`);
console.log('📝 SQL Values:', values);
const result = await pool.query(`
UPDATE users SET ${fields.join(', ')}, updated_at = CURRENT_TIMESTAMP
WHERE id = $${paramIndex}
RETURNING *
`, values);
if (!result.rows[0]) {
throw new Error('Aucun utilisateur mis à jour - vérifiez l\'ID');
}
console.log('✅ updateUser success:', { id, fieldsUpdated: fields.length, updatedUser: result.rows[0] });
return result.rows[0];
} catch (error) {
console.error('❌ updateUser error:', error);
throw error;
}
}
async deleteUser(id: string): Promise<void> {
await pool.query('DELETE FROM users WHERE id = $1', [id]);
}
async getGroups(): Promise<Group[]> {
const result = await pool.query('SELECT * FROM groups ORDER BY name');
return result.rows;
}
async getGroup(id: number): Promise<Group | undefined> {
const result = await pool.query('SELECT * FROM groups WHERE id = $1', [id]);
return result.rows[0] || undefined;
}
async createGroup(group: InsertGroup): Promise<Group> {
console.log('🏪 Creating group with data:', {
name: group.name,
color: group.color,
fullData: group
});
try {
const result = await pool.query(`
INSERT INTO groups (name, color)
VALUES ($1, $2)
RETURNING *
`, [group.name, group.color]);
console.log('✅ Group created successfully:', result.rows[0]);
return result.rows[0];
} catch (error) {
console.error('❌ Failed to create group:', error);
console.error('📊 Error details:', {
message: error.message,
code: error.code,
detail: error.detail,
constraint: error.constraint
});
throw error;
}
}
async updateGroup(id: number, group: Partial<InsertGroup>): Promise<Group> {
const result = await pool.query(`
UPDATE groups SET name = $1, color = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $3
RETURNING *
`, [group.name, group.color, id]);
return result.rows[0];
}
async deleteGroup(id: number): Promise<void> {
await pool.query('DELETE FROM groups WHERE id = $1', [id]);
}
async getSuppliers(dlcOnly: boolean = false): Promise<Supplier[]> {
let query = 'SELECT * FROM suppliers';
let params = [];
if (dlcOnly) {
query += ' WHERE has_dlc = $1';
params = [true];
}
query += ' ORDER BY name';
const result = await pool.query(query, params);
// Map snake_case to camelCase for frontend compatibility
return result.rows.map(row => ({
id: row.id,
name: row.name,
contact: row.contact,
phone: row.phone,
hasDlc: row.has_dlc, // Convert snake_case to camelCase
createdAt: row.created_at,
updatedAt: row.updated_at
}));
}
async createSupplier(supplier: InsertSupplier): Promise<Supplier> {
console.log('🚚 Creating supplier with data:', {
name: supplier.name,
contact: supplier.contact,
phone: supplier.phone,
hasDlc: supplier.hasDlc,
fullData: supplier
});
try {
const result = await pool.query(`
INSERT INTO suppliers (name, contact, phone, has_dlc)
VALUES ($1, $2, $3, $4)
RETURNING *
`, [supplier.name, supplier.contact || '', supplier.phone || '', supplier.hasDlc || false]);
const createdSupplier = result.rows[0];
// Map snake_case to camelCase for frontend compatibility
const mappedSupplier = {
id: createdSupplier.id,
name: createdSupplier.name,
contact: createdSupplier.contact,
phone: createdSupplier.phone,
hasDlc: createdSupplier.has_dlc, // Convert snake_case to camelCase
createdAt: createdSupplier.created_at,
updatedAt: createdSupplier.updated_at
};
console.log('✅ Supplier created successfully:', mappedSupplier);
return mappedSupplier;
} catch (error) {
console.error('❌ Failed to create supplier:', error);
console.error('📊 Error details:', {
message: error.message,
code: error.code,
detail: error.detail,
constraint: error.constraint
});
throw error;
}
}
async updateSupplier(id: number, supplier: Partial<InsertSupplier>): Promise<Supplier> {
console.log('🔧 Updating supplier with data:', {
id,
supplier,
fieldsToUpdate: Object.keys(supplier)
});
try {
// Build dynamic update query based on provided fields
const fields = [];
const values = [];
let paramIndex = 1;
if (supplier.name !== undefined) {
fields.push(`name = $${paramIndex++}`);
values.push(supplier.name);
}
if (supplier.contact !== undefined) {
fields.push(`contact = $${paramIndex++}`);
values.push(supplier.contact || '');
}
if (supplier.phone !== undefined) {
fields.push(`phone = $${paramIndex++}`);
values.push(supplier.phone || '');
}
if (supplier.hasDlc !== undefined) {
fields.push(`has_dlc = $${paramIndex++}`);
values.push(supplier.hasDlc);
}
// Always update the updated_at field
fields.push(`updated_at = CURRENT_TIMESTAMP`);
values.push(id);
const query = `
UPDATE suppliers SET ${fields.join(', ')}
WHERE id = $${paramIndex}
RETURNING *
`;
console.log('🔧 SQL Query:', { query, values });
const result = await pool.query(query, values);
const updatedSupplier = result.rows[0];
// Map snake_case to camelCase for frontend compatibility
const mappedSupplier = {
id: updatedSupplier.id,
name: updatedSupplier.name,
contact: updatedSupplier.contact,
phone: updatedSupplier.phone,
hasDlc: updatedSupplier.has_dlc, // Convert snake_case to camelCase
createdAt: updatedSupplier.created_at,
updatedAt: updatedSupplier.updated_at
};
console.log('✅ Supplier updated successfully:', mappedSupplier);
return mappedSupplier;
} catch (error) {
console.error('❌ Failed to update supplier:', error);
console.error('📊 Error details:', {
message: error.message,
code: error.code,
detail: error.detail,
constraint: error.constraint,
supplierData: supplier,
supplierId: id
});
throw error;
}
}
async deleteSupplier(id: number): Promise<void> {
await pool.query('DELETE FROM suppliers WHERE id = $1', [id]);
}
// Simplified methods for production - implement core functionality only
async getOrders(groupIds?: number[]): Promise<any[]> {
console.log('📦 getOrders production called with groupIds:', groupIds);
let whereClause = '';
let params = [];
if (groupIds && groupIds.length > 0) {
whereClause = ' WHERE o.group_id = ANY($1)';
params = [groupIds];
}
console.log('📦 SQL Query:', {
whereClause,
params,
query: `SELECT o.* FROM orders o ${whereClause} ORDER BY o.created_at DESC`
});
const result = await pool.query(`
SELECT o.*,
s.id as supplier_id, s.name as supplier_name, s.contact as supplier_contact, s.phone as supplier_phone,
g.id as group_id, g.name as group_name, g.color as group_color,
u.id as creator_id, u.username as creator_username, u.email as creator_email, u.name as creator_name
FROM orders o
LEFT JOIN suppliers s ON o.supplier_id = s.id
LEFT JOIN groups g ON o.group_id = g.id
LEFT JOIN users u ON o.created_by = u.id
${whereClause}
ORDER BY o.created_at DESC
`, params);
console.log('📦 Query result:', {
rowCount: result.rows.length,
sampleRows: result.rows.slice(0, 2).map(row => ({
id: row.id,
groupId: row.group_id,
plannedDate: row.planned_date,
supplierName: row.supplier_name,
groupName: row.group_name
}))
});
// Transformer pour correspondre exactement à la structure Drizzle
return (result.rows || []).map(row => ({
id: row.id,
supplierId: row.supplier_id,
groupId: row.group_id,
plannedDate: row.planned_date,
quantity: row.quantity,
unit: row.unit,
status: row.status,
notes: row.notes,
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at,
supplier: row.supplier_name ? {
id: row.supplier_id,
name: row.supplier_name,
contact: row.supplier_contact || '',
phone: row.supplier_phone || '',
createdAt: row.created_at,
updatedAt: row.updated_at
} : null,
group: row.group_name ? {
id: row.group_id,
name: row.group_name,
color: row.group_color || '#666666',
createdAt: row.created_at,
updatedAt: row.updated_at
} : null,
creator: row.creator_username ? {
id: row.creator_id,
username: row.creator_username,
email: row.creator_email || '',
name: row.creator_name || row.creator_username,
firstName: row.creator_name || row.creator_username,
lastName: '',
role: 'admin'
} : null
}));
}
async getOrdersByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise<any[]> {
let whereClause = 'WHERE o.planned_date BETWEEN $1 AND $2';
let params = [startDate, endDate];
if (groupIds && groupIds.length > 0) {
whereClause += ' AND o.group_id = ANY($3)';
params.push(groupIds);
}
const result = await pool.query(`
SELECT o.*,
s.id as supplier_id, s.name as supplier_name, s.contact as supplier_contact, s.phone as supplier_phone,
g.id as group_id, g.name as group_name, g.color as group_color,
u.id as creator_id, u.username as creator_username, u.email as creator_email, u.name as creator_name
FROM orders o
LEFT JOIN suppliers s ON o.supplier_id = s.id
LEFT JOIN groups g ON o.group_id = g.id
LEFT JOIN users u ON o.created_by = u.id
${whereClause}
ORDER BY o.created_at DESC
`, params);
console.log('📅 getOrdersByDateRange debug:', { startDate, endDate, groupIds, orderCount: result.rows.length });
// Transformer pour correspondre exactement à la structure Drizzle
return (result.rows || []).map(row => ({
id: row.id,
supplierId: row.supplier_id,
groupId: row.group_id,
plannedDate: row.planned_date,
quantity: row.quantity,
unit: row.unit,
status: row.status,
notes: row.notes,
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at,
supplier: row.supplier_name ? {
id: row.supplier_id,
name: row.supplier_name,
contact: row.supplier_contact || '',
phone: row.supplier_phone || '',
createdAt: row.created_at,
updatedAt: row.updated_at
} : null,
group: row.group_name ? {
id: row.group_id,
name: row.group_name,
color: row.group_color || '#666666',
createdAt: row.created_at,
updatedAt: row.updated_at
} : null,
creator: row.creator_username ? {
id: row.creator_id,
username: row.creator_username,
email: row.creator_email || '',
name: row.creator_name || row.creator_username,
firstName: row.creator_name || row.creator_username,
lastName: '',
role: 'admin'
} : null,
deliveries: [] // Pas de deliveries dans cette méthode
}));
}
async getOrder(id: number): Promise<any> {
const result = await pool.query(`
SELECT o.*, s.name as supplier_name, g.name as group_name, g.color as group_color,
u.username as creator_username
FROM orders o
LEFT JOIN suppliers s ON o.supplier_id = s.id
LEFT JOIN groups g ON o.group_id = g.id
LEFT JOIN users u ON o.created_by = u.id
WHERE o.id = $1
`, [id]);
return result.rows[0] || undefined;
}
async createOrder(order: InsertOrder): Promise<Order> {
console.log('📦 createOrder production called with:', order);
const result = await pool.query(`
INSERT INTO orders (supplier_id, group_id, planned_date, quantity, unit, status, notes, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *
`, [
order.supplierId,
order.groupId,
order.plannedDate,
order.quantity,
order.unit,
order.status || 'pending',
order.notes,
order.createdBy
]);
console.log('✅ Order created in production DB:', {
id: result.rows[0].id,
groupId: result.rows[0].group_id,
plannedDate: result.rows[0].planned_date,
supplierId: result.rows[0].supplier_id,
status: result.rows[0].status
});
return result.rows[0];
}
async updateOrder(id: number, order: Partial<InsertOrder>): Promise<Order> {
const result = await pool.query(`
UPDATE orders SET
supplier_id = $1, group_id = $2, planned_date = $3, quantity = $4,
unit = $5, status = $6, notes = $7, updated_at = CURRENT_TIMESTAMP
WHERE id = $8
RETURNING *
`, [
order.supplierId,
order.groupId,
order.plannedDate,
order.quantity,
order.unit,
order.status,
order.notes,
id
]);
return result.rows[0];
}
async deleteOrder(id: number): Promise<void> {
console.log('🗑️ Production Storage - Deleting order:', id);
// Vérifier les livraisons liées avant suppression
const linkedDeliveries = await pool.query(
'SELECT id FROM deliveries WHERE order_id = $1',
[id]
);
if (linkedDeliveries.rows.length > 0) {
console.log('🔗 Order has linked deliveries:', linkedDeliveries.rows.length);
// Pour chaque livraison liée, supprimer la liaison
for (const delivery of linkedDeliveries.rows) {
await pool.query(
'UPDATE deliveries SET order_id = NULL WHERE id = $1',
[delivery.id]
);
}
}
const result = await pool.query('DELETE FROM orders WHERE id = $1', [id]);
console.log('✅ Production Storage - Order deleted, affected rows:', result.rowCount);
}
async getDeliveries(groupIds?: number[]): Promise<any[]> {
let whereClause = '';
let params = [];
if (groupIds && groupIds.length > 0) {
whereClause = ' WHERE d.group_id = ANY($1)';
params = [groupIds];
}
const result = await pool.query(`
SELECT d.*,
s.id as supplier_id, s.name as supplier_name, s.contact as supplier_contact, s.phone as supplier_phone,
g.id as group_id, g.name as group_name, g.color as group_color,
u.id as creator_id, u.username as creator_username, u.email as creator_email, u.name as creator_name,
o.id as order_id_rel, o.planned_date as order_planned_date, o.status as order_status
FROM deliveries d
LEFT JOIN suppliers s ON d.supplier_id = s.id
LEFT JOIN groups g ON d.group_id = g.id
LEFT JOIN users u ON d.created_by = u.id
LEFT JOIN orders o ON d.order_id = o.id
${whereClause}
ORDER BY d.created_at DESC
`, params);
// Transformer pour correspondre exactement à la structure Drizzle
return (result.rows || []).map(row => ({
id: row.id,
orderId: row.order_id,
supplierId: row.supplier_id,
groupId: row.group_id,
scheduledDate: row.scheduled_date,
quantity: row.quantity,
unit: row.unit,
status: row.status,
notes: row.notes,
blNumber: row.bl_number,
blAmount: row.bl_amount,
invoiceReference: row.invoice_reference,
invoiceAmount: row.invoice_amount,
reconciled: row.reconciled,
deliveredDate: row.delivered_date,
validatedAt: row.validated_at,
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at,
supplier: row.supplier_name ? {
id: row.supplier_id,
name: row.supplier_name,
contact: row.supplier_contact || '',
phone: row.supplier_phone || '',
createdAt: row.created_at,
updatedAt: row.updated_at
} : null,
group: row.group_name ? {
id: row.group_id,
name: row.group_name,
color: row.group_color || '#666666',
createdAt: row.created_at,
updatedAt: row.updated_at
} : null,
creator: row.creator_username ? {
id: row.creator_id,
username: row.creator_username,
email: row.creator_email || '',
name: row.creator_name || row.creator_username
} : null,
order: row.order_id_rel ? {
id: row.order_id_rel,
plannedDate: row.order_planned_date,
status: row.order_status
} : null
}));
}
async getDeliveriesByDateRange(startDate: string, endDate: string, groupIds?: number[]): Promise<any[]> {
let whereClause = 'WHERE d.scheduled_date BETWEEN $1 AND $2';
let params = [startDate, endDate];
if (groupIds && groupIds.length > 0) {
whereClause += ' AND d.group_id = ANY($3)';
params.push(groupIds);
}
const result = await pool.query(`
SELECT d.*,
s.id as supplier_id, s.name as supplier_name, s.contact as supplier_contact, s.phone as supplier_phone,
g.id as group_id, g.name as group_name, g.color as group_color,
u.id as creator_id, u.username as creator_username, u.email as creator_email, u.name as creator_name,
o.id as order_id_rel, o.planned_date as order_planned_date, o.status as order_status
FROM deliveries d
LEFT JOIN suppliers s ON d.supplier_id = s.id
LEFT JOIN groups g ON d.group_id = g.id
LEFT JOIN users u ON d.created_by = u.id
LEFT JOIN orders o ON d.order_id = o.id
${whereClause}
ORDER BY d.created_at DESC
`, params);
console.log('🚛 getDeliveriesByDateRange debug:', { startDate, endDate, groupIds, deliveryCount: result.rows.length });
// Transformer pour correspondre exactement à la structure Drizzle
return (result.rows || []).map(row => ({
id: row.id,
orderId: row.order_id,
supplierId: row.supplier_id,
groupId: row.group_id,
scheduledDate: row.scheduled_date,
quantity: row.quantity,
unit: row.unit,
status: row.status,
notes: row.notes,
blNumber: row.bl_number,
blAmount: row.bl_amount,
invoiceReference: row.invoice_reference,
invoiceAmount: row.invoice_amount,
reconciled: row.reconciled,
deliveredDate: row.delivered_date,
validatedAt: row.validated_at,
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at,
supplier: row.supplier_name ? {
id: row.supplier_id,
name: row.supplier_name,
contact: row.supplier_contact || '',
phone: row.supplier_phone || '',
createdAt: row.created_at,
updatedAt: row.updated_at
} : null,
group: row.group_name ? {
id: row.group_id,
name: row.group_name,
color: row.group_color || '#666666',
createdAt: row.created_at,
updatedAt: row.updated_at
} : null,
creator: row.creator_username ? {
id: row.creator_id,
username: row.creator_username,
email: row.creator_email || '',
name: row.creator_name || row.creator_username,
firstName: row.creator_name || row.creator_username,
lastName: '',
role: 'admin'
} : null,
order: row.order_id_rel ? {
id: row.order_id_rel,
plannedDate: row.order_planned_date,
status: row.order_status
} : null
}));
}
async getDelivery(id: number): Promise<any> {
const result = await pool.query(`
SELECT d.*, s.name as supplier_name, g.name as group_name, g.color as group_color,
u.username as creator_username, o.planned_date as order_planned_date
FROM deliveries d
JOIN suppliers s ON d.supplier_id = s.id
JOIN groups g ON d.group_id = g.id
JOIN users u ON d.created_by = u.id
LEFT JOIN orders o ON d.order_id = o.id
WHERE d.id = $1
`, [id]);
return result.rows[0] || undefined;
}
async createDelivery(delivery: InsertDelivery): Promise<Delivery> {
console.log('🚚 PRODUCTION createDelivery - Input data:', JSON.stringify(delivery, null, 2));
const finalStatus = delivery.status || 'pending';
console.log('🚚 PRODUCTION createDelivery - Final status will be:', finalStatus);
const params = [
delivery.orderId,
delivery.supplierId,
delivery.groupId,
delivery.scheduledDate,
delivery.quantity,
delivery.unit,
finalStatus,
delivery.notes,
delivery.createdBy
];
console.log('🚚 PRODUCTION createDelivery - SQL params:', params);
const result = await pool.query(`
INSERT INTO deliveries (order_id, supplier_id, group_id, scheduled_date, quantity, unit, status, notes, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *
`, params);
const newDelivery = result.rows[0];
console.log('✅ PRODUCTION createDelivery - Delivery inserted:', {
id: newDelivery.id,
status: newDelivery.status,
orderId: newDelivery.order_id
});
// Si la livraison est liée à une commande, mettre à jour le statut de la commande vers "planned"
if (newDelivery.order_id) {
console.log('🔄 PRODUCTION createDelivery - Updating linked order status to planned');
await pool.query(`
UPDATE orders SET status = 'planned', updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`, [newDelivery.order_id]);
console.log('✅ Order status updated to planned after delivery creation:', newDelivery.order_id);
}
return newDelivery;
}
async updateDelivery(id: number, delivery: Partial<InsertDelivery>): Promise<Delivery> {
// Récupérer l'ancienne livraison pour connaître l'ordre précédemment lié
const oldDeliveryResult = await pool.query('SELECT * FROM deliveries WHERE id = $1', [id]);
const oldDelivery = oldDeliveryResult.rows[0];
const fields = [];
const values = [];
let paramIndex = 1;
// Construire dynamiquement la requête pour éviter les erreurs de colonnes manquantes
for (const [key, value] of Object.entries(delivery)) {
if (value !== undefined) {
const dbKey = key === 'orderId' ? 'order_id' :
key === 'supplierId' ? 'supplier_id' :
key === 'groupId' ? 'group_id' :
key === 'scheduledDate' ? 'scheduled_date' :
key === 'blNumber' ? 'bl_number' :
key === 'blAmount' ? 'bl_amount' :
key === 'invoiceReference' ? 'invoice_reference' :
key === 'invoiceAmount' ? 'invoice_amount' :
key === 'deliveredDate' ? 'delivered_date' :
key === 'validatedAt' ? 'validated_at' :
key === 'createdBy' ? 'created_by' : key;
fields.push(`${dbKey} = $${paramIndex}`);
values.push(value);
paramIndex++;
}
}
if (fields.length === 0) {
throw new Error('No fields to update');
}
values.push(id);
const result = await pool.query(`
UPDATE deliveries SET ${fields.join(', ')}, updated_at = CURRENT_TIMESTAMP
WHERE id = $${paramIndex}
RETURNING *
`, values);
const updatedDelivery = result.rows[0];
// Gérer les changements d'association de commandes
if (delivery.orderId !== undefined) {
const newOrderId = delivery.orderId;
const oldOrderId = oldDelivery?.order_id;
// Si l'ordre a changé
if (newOrderId !== oldOrderId) {
// Remettre l'ancien ordre en "pending" s'il n'a plus de livraisons liées
if (oldOrderId) {
const remainingResult = await pool.query(
'SELECT COUNT(*) as count FROM deliveries WHERE order_id = $1 AND id != $2',
[oldOrderId, id]
);
const remainingCount = parseInt(remainingResult.rows[0].count);
if (remainingCount === 0) {
await pool.query(`
UPDATE orders SET status = 'pending', updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`, [oldOrderId]);
console.log('✅ Old order status reset to pending:', oldOrderId);
}
}
// Mettre le nouveau ordre en "planned"
if (newOrderId) {
await pool.query(`
UPDATE orders SET status = 'planned', updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`, [newOrderId]);
console.log('✅ New order status updated to planned:', newOrderId);
}
}
}
console.log('🔄 updateDelivery production:', { id, fieldsUpdated: fields.length, delivery });
return updatedDelivery;
}
async deleteDelivery(id: number): Promise<void> {
// Récupérer la livraison avant suppression pour connaître la commande liée
const deliveryResult = await pool.query('SELECT * FROM deliveries WHERE id = $1', [id]);
const delivery = deliveryResult.rows[0];
// Supprimer la livraison
await pool.query('DELETE FROM deliveries WHERE id = $1', [id]);
// Si la livraison était liée à une commande, gérer le statut de la commande
if (delivery?.order_id) {
// Vérifier s'il reste d'autres livraisons liées à cette commande
const remainingResult = await pool.query(
'SELECT COUNT(*) as count FROM deliveries WHERE order_id = $1',
[delivery.order_id]
);
const remainingCount = parseInt(remainingResult.rows[0].count);
if (remainingCount === 0) {
// Plus aucune livraison liée : remettre la commande en "pending"
await pool.query(`
UPDATE orders SET status = 'pending', updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`, [delivery.order_id]);
console.log('✅ Order status reset to pending after delivery deletion:', delivery.order_id);
}
}
}
async validateDelivery(id: number, blData?: { blNumber: string; blAmount: number }): Promise<void> {
try {
// Vérifier d'abord quelles colonnes existent dans la table deliveries
const columnsCheck = await pool.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'deliveries' AND table_schema = 'public'
`);
const existingColumns = columnsCheck.rows.map(row => row.column_name);
console.log('🔍 validateDelivery - Available columns:', existingColumns);
// Construire la requête en fonction des colonnes disponibles
const updates = ['status = $2', 'updated_at = CURRENT_TIMESTAMP'];
const params = [id, 'delivered'];
let paramIndex = 3;
if (existingColumns.includes('delivered_date')) {
updates.push(`delivered_date = $${paramIndex}`);
params.push(new Date().toISOString());
paramIndex++;
}
if (existingColumns.includes('validated_at')) {
updates.push(`validated_at = $${paramIndex}`);
params.push(new Date().toISOString());
paramIndex++;
}
if (blData?.blNumber && existingColumns.includes('bl_number')) {
updates.push(`bl_number = $${paramIndex}`);
params.push(blData.blNumber);
paramIndex++;
}
if (blData?.blAmount !== undefined && existingColumns.includes('bl_amount')) {
updates.push(`bl_amount = $${paramIndex}`);
params.push(blData.blAmount);
paramIndex++;
}
const result = await pool.query(`
UPDATE deliveries SET ${updates.join(', ')}
WHERE id = $1
RETURNING *
`, params);
console.log('✅ validateDelivery success:', { id, blData, updatedColumns: updates.length });
// Mettre à jour le statut de la commande liée si elle existe
if (result.rows[0]?.order_id) {
await pool.query(`
UPDATE orders SET status = 'delivered', updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`, [result.rows[0].order_id]);
console.log('✅ Order status updated to delivered:', result.rows[0].order_id);
}
} catch (error) {
console.error('❌ validateDelivery error:', error);
throw error;
}
}
async getUserGroups(userId: string): Promise<UserGroup[]> {
const result = await pool.query('SELECT * FROM user_groups WHERE user_id = $1', [userId]);
return result.rows;
}
async assignUserToGroup(userGroup: InsertUserGroup): Promise<UserGroup> {
console.log('🔄 assignUserToGroup appelé avec:', userGroup);
try {
// Vérifier que l'utilisateur existe
const userCheck = await pool.query('SELECT id, username FROM users WHERE id = $1', [userGroup.userId]);
if (userCheck.rows.length === 0) {
throw new Error(`Utilisateur non trouvé: ${userGroup.userId}`);
}
console.log('✅ Utilisateur vérifié:', userCheck.rows[0]);
// Vérifier que le groupe existe
const groupCheck = await pool.query('SELECT id, name FROM groups WHERE id = $1', [userGroup.groupId]);
if (groupCheck.rows.length === 0) {
throw new Error(`Groupe non trouvé: ${userGroup.groupId}`);
}
console.log('✅ Groupe vérifié:', groupCheck.rows[0]);
// Vérifier si l'assignation existe déjà
const existingCheck = await pool.query(
'SELECT * FROM user_groups WHERE user_id = $1 AND group_id = $2',
[userGroup.userId, userGroup.groupId]
);
if (existingCheck.rows.length > 0) {
console.log('ℹ️ Assignation déjà existante, retour de l\'existante');
return existingCheck.rows[0];
}
// Effectuer l'insertion
const result = await pool.query(`
INSERT INTO user_groups (user_id, group_id)
VALUES ($1, $2)
RETURNING *
`, [userGroup.userId, userGroup.groupId]);
console.log('✅ Assignation créée avec succès:', result.rows[0]);
return result.rows[0];
} catch (error) {
console.error('❌ Erreur dans assignUserToGroup:', error);
throw error;
}
}
async removeUserFromGroup(userId: string, groupId: number): Promise<void> {
await pool.query('DELETE FROM user_groups WHERE user_id = $1 AND group_id = $2', [userId, groupId]);
}
async getMonthlyStats(year: number, month: number, groupIds?: number[]): Promise<any> {
const startDate = `${year}-${month.toString().padStart(2, '0')}-01`;
const endDate = `${year}-${month.toString().padStart(2, '0')}-31`;
let whereClause = '';
let params = [startDate, endDate];
if (groupIds && groupIds.length > 0) {
whereClause = ' AND group_id = ANY($3)';
params.push(groupIds);
}
console.log('📊 Monthly stats query params:', { year, month, startDate, endDate, groupIds, whereClause });
const [ordersResult, deliveriesResult, orderStatsResult, deliveryStatsResult] = await Promise.all([
// Basic counts
pool.query(`SELECT COUNT(*) FROM orders WHERE planned_date BETWEEN $1 AND $2${whereClause}`, params),
pool.query(`SELECT COUNT(*) FROM deliveries WHERE scheduled_date BETWEEN $1 AND $2${whereClause}`, params),
// Order statistics with pending count and totals
pool.query(`
SELECT
COUNT(*) FILTER (WHERE status = 'pending') as pending_count,
COALESCE(SUM(CASE WHEN unit = 'palettes' THEN quantity ELSE 0 END), 0) as total_palettes,
COALESCE(SUM(CASE WHEN unit = 'colis' THEN quantity ELSE 0 END), 0) as total_packages
FROM orders
WHERE planned_date BETWEEN $1 AND $2${whereClause}
`, params),
// Delivery statistics
pool.query(`
SELECT
COALESCE(SUM(CASE WHEN unit = 'palettes' THEN quantity ELSE 0 END), 0) as total_palettes,
COALESCE(SUM(CASE WHEN unit = 'colis' THEN quantity ELSE 0 END), 0) as total_packages
FROM deliveries
WHERE scheduled_date BETWEEN $1 AND $2${whereClause}
`, params)
]);
console.log('📊 Query results:', {
ordersCount: ordersResult.rows[0].count,
deliveriesCount: deliveriesResult.rows[0].count,
orderStats: orderStatsResult.rows[0],
deliveryStats: deliveryStatsResult.rows[0]
});
// Calculate average delivery time
const deliveryTimeQuery = `
SELECT
o.planned_date,
d.delivered_date
FROM orders o
INNER JOIN deliveries d ON o.id = d.order_id
WHERE o.planned_date BETWEEN $1 AND $2
AND d.status = 'delivered'
AND d.delivered_date IS NOT NULL
${whereClause.replace('group_id', 'o.group_id')}
`;
const deliveryTimeResult = await pool.query(deliveryTimeQuery, params);
let averageDeliveryTime = 0;
if (deliveryTimeResult.rows.length > 0) {
const totalDelayDays = deliveryTimeResult.rows.reduce((sum, row) => {
const planned = new Date(row.planned_date);
const delivered = new Date(row.delivered_date);
const diffTime = delivered.getTime() - planned.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return sum + diffDays;
}, 0);
averageDeliveryTime = Math.round((totalDelayDays / deliveryTimeResult.rows.length) * 10) / 10;
}
const stats = {
ordersCount: parseInt(ordersResult.rows[0].count) || 0,
deliveriesCount: parseInt(deliveriesResult.rows[0].count) || 0,
pendingOrdersCount: parseInt(orderStatsResult.rows[0].pending_count) || 0,
averageDeliveryTime: averageDeliveryTime,
totalPalettes: (parseInt(orderStatsResult.rows[0].total_palettes) || 0) + (parseInt(deliveryStatsResult.rows[0].total_palettes) || 0),
totalPackages: (parseInt(orderStatsResult.rows[0].total_packages) || 0) + (parseInt(deliveryStatsResult.rows[0].total_packages) || 0)
};
console.log('📊 Final stats:', stats);
return stats;
}
// Publicities methods
async getPublicities(year?: number, groupIds?: number[]): Promise<any[]> {
// Filtre par année si spécifiée
let whereClause = '';
let params = [];
if (year) {
whereClause = 'WHERE year = $1';
params.push(year);
}
const publicities = await pool.query(`SELECT * FROM publicities ${whereClause} ORDER BY start_date DESC`, params);
console.log('🎯 getPublicities debug:', { year, whereClause, publicityCount: publicities.rows.length });
// Pour chaque publicité, récupérer ses participations
const publicityData = await Promise.all(
publicities.rows.map(async (pub) => {
// Use simplified query without created_at to avoid production issues
const participations = await pool.query(
'SELECT pp.group_id, g.name as group_name, g.color as group_color FROM publicity_participations pp LEFT JOIN groups g ON pp.group_id = g.id WHERE pp.publicity_id = $1',
[pub.id]
);
return {
id: pub.id,
pubNumber: pub.pub_number,
designation: pub.designation || pub.title, // Support ancien champ title
startDate: pub.start_date,
endDate: pub.end_date,
year: pub.year,
createdBy: pub.created_by,
createdAt: pub.created_at,
updatedAt: pub.updated_at,
participations: (participations.rows || []).map(p => ({
groupId: p.group_id,
group: {
id: p.group_id,
name: p.group_name,
color: p.group_color || '#666666'
}
}))
};
})
);
console.log('🎯 getPublicities result:', publicityData.length, 'publicités pour année', year);
return publicityData;
}
async getPublicity(id: number): Promise<any> {
// Get the publicity with creator information
const publicityResult = await pool.query(`
SELECT p.*, u.username, u.email, u.first_name, u.last_name, u.role
FROM publicities p
LEFT JOIN users u ON p.created_by = u.id
WHERE p.id = $1
`, [id]);
if (publicityResult.rows.length === 0) {
return undefined;
}
const publicity = publicityResult.rows[0];
// Get participations with group information - simplified for production compatibility
const participationsResult = await pool.query(`
SELECT pp.group_id, g.name as group_name, g.color as group_color
FROM publicity_participations pp
LEFT JOIN groups g ON pp.group_id = g.id
WHERE pp.publicity_id = $1
`, [id]);
return {
id: publicity.id,
pubNumber: publicity.pub_number,
designation: publicity.designation,
startDate: publicity.start_date,
endDate: publicity.end_date,
year: publicity.year,
createdBy: publicity.created_by,
createdAt: publicity.created_at,
updatedAt: publicity.updated_at,
creator: {
id: publicity.created_by,
username: publicity.username,
email: publicity.email,
firstName: publicity.first_name,
lastName: publicity.last_name,
role: publicity.role
},
participations: participationsResult.rows.map(p => ({
publicityId: id,
groupId: p.group_id,
group: {
id: p.group_id,
name: p.group_name,
color: p.group_color
},
createdAt: new Date().toISOString()
}))
};
}
async createPublicity(publicity: InsertPublicity): Promise<Publicity> {
const result = await pool.query(`
INSERT INTO publicities (pub_number, designation, start_date, end_date, year, created_by)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *
`, [
publicity.pubNumber,
publicity.designation,
publicity.startDate,
publicity.endDate,
publicity.year,
publicity.createdBy
]);
return result.rows[0];
}
async updatePublicity(id: number, publicity: Partial<InsertPublicity>): Promise<Publicity> {
const result = await pool.query(`
UPDATE publicities SET
pub_number = $1, designation = $2, start_date = $3,
end_date = $4, year = $5, updated_at = CURRENT_TIMESTAMP
WHERE id = $6
RETURNING *
`, [
publicity.pubNumber,
publicity.designation,
publicity.startDate,
publicity.endDate,
publicity.year,
id
]);
return result.rows[0];
}
async deletePublicity(id: number): Promise<void> {
await pool.query('DELETE FROM publicities WHERE id = $1', [id]);
}
async getPublicityParticipations(publicityId: number): Promise<PublicityParticipation[]> {
const result = await pool.query('SELECT * FROM publicity_participations WHERE publicity_id = $1', [publicityId]);
return result.rows;
}
async setPublicityParticipations(publicityId: number, groupIds: number[]): Promise<void> {
await pool.query('DELETE FROM publicity_participations WHERE publicity_id = $1', [publicityId]);
for (const groupId of groupIds) {
await pool.query(`
INSERT INTO publicity_participations (publicity_id, group_id)
VALUES ($1, $2)
`, [publicityId, groupId]);
}
}
// ===== ROLE MANAGEMENT METHODS =====
async getRoles(): Promise<Role[]> {
try {
const result = await pool.query(`
SELECT r.id,
r.name,
r.display_name,
r.description,
r.color,
r.is_system,
r.is_active,
r.created_at,
r.updated_at,
COALESCE(
JSON_AGG(
CASE WHEN p.id IS NOT NULL THEN
JSON_BUILD_OBJECT(
'id', p.id,
'name', p.name,
'displayName', p.display_name,
'description', p.description,
'category', p.category,
'action', p.action,
'resource', p.resource,
'isSystem', p.is_system,
'createdAt', p.created_at
)
END
) FILTER (WHERE p.id IS NOT NULL),
'[]'::json
) as permissions
FROM roles r
LEFT JOIN role_permissions rp ON r.id = rp.role_id
LEFT JOIN permissions p ON rp.permission_id = p.id
GROUP BY r.id, r.name, r.display_name, r.description, r.color, r.is_system, r.is_active, r.created_at, r.updated_at
ORDER BY r.name
`);
// 🔍 DIAGNOSTIC: Log détaillé pour identifier le problème
console.log('🔍 DIAGNOSTIC RÔLES PRODUCTION:');
result.rows.forEach((row, index) => {
console.log(`Role ${index + 1}:`, {
id: row.id,
name: row.name,
displayName: row.display_name,
color: row.color,
isGrayColor: row.color === '#6b7280',
expectedColors: {
admin: '#dc2626',
manager: '#2563eb',
employee: '#16a34a',
directeur: '#7c3aed'
}
});
});
return result.rows.map(row => ({
id: row.id,
name: row.name,
displayName: row.display_name || row.name,
description: row.description || '',
color: row.color || '#6b7280',
isSystem: row.is_system || false,
isActive: row.is_active !== false,
createdAt: row.created_at,
updatedAt: row.updated_at,
rolePermissions: Array.isArray(row.permissions) ? row.permissions.map(p => ({
id: p.id,
permissionId: p.id,
roleId: row.id,
createdAt: p.createdAt,
permission: {
id: p.id,
name: p.name,
displayName: p.displayName,
description: p.description,
category: p.category,
action: p.action,
resource: p.resource,
isSystem: p.isSystem,
createdAt: p.createdAt
}
})) : []
})) || [];
} catch (error) {
console.error("Error in getRoles:", error);
return [];
}
}
async getRoleWithPermissions(id: number): Promise<Role | undefined> {
try {
const result = await pool.query(`
SELECT r.id,
r.name,
r.display_name,
r.description,
r.color,
r.is_system,
r.is_active,
r.created_at,
r.updated_at,
COALESCE(
JSON_AGG(
CASE WHEN p.id IS NOT NULL THEN
JSON_BUILD_OBJECT(
'id', p.id,
'name', p.name,
'displayName', p.display_name,
'description', p.description,
'category', p.category,
'action', p.action,
'resource', p.resource,
'isSystem', p.is_system,
'createdAt', p.created_at
)
END
) FILTER (WHERE p.id IS NOT NULL),
'[]'::json
) as permissions
FROM roles r
LEFT JOIN role_permissions rp ON r.id = rp.role_id
LEFT JOIN permissions p ON rp.permission_id = p.id
WHERE r.id = $1
GROUP BY r.id, r.name, r.display_name, r.description, r.color, r.is_system, r.is_active, r.created_at, r.updated_at
`, [id]);
if (result.rows.length === 0) return undefined;
const row = result.rows[0];
return {
id: row.id,
name: row.name,
displayName: row.display_name || row.name,
description: row.description || '',
color: row.color || '#6b7280',
isSystem: row.is_system || false,
isActive: row.is_active !== false,
createdAt: row.created_at,
updatedAt: row.updated_at,
permissions: Array.isArray(row.permissions) ? row.permissions : []
};
} catch (error) {
console.error("Error in getRoleWithPermissions:", error);
return undefined;
}
}
async createRole(roleData: InsertRole): Promise<Role> {
try {
const result = await pool.query(`
INSERT INTO roles (name, display_name, description, color, is_system, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING *
`, [
roleData.name,
roleData.displayName || roleData.name,
roleData.description || '',
roleData.color || '#6b7280',
roleData.isSystem || false,
roleData.isActive !== false
]);
const row = result.rows[0];
return {
id: row.id,
name: row.name,
displayName: row.display_name || row.name,
description: row.description,
color: row.color,
isSystem: row.is_system,
isActive: row.is_active,
createdAt: row.created_at,
updatedAt: row.updated_at
};
} catch (error) {
console.error("Error in createRole:", error);
throw error;
}
}
async updateRole(id: number, roleData: Partial<InsertRole>): Promise<Role> {
try {
const setParts = [];
const values = [];
let paramCount = 1;
if (roleData.name !== undefined) {
setParts.push(`name = $${paramCount}`);
values.push(roleData.name);
paramCount++;
}
if (roleData.displayName !== undefined) {
setParts.push(`display_name = $${paramCount}`);
values.push(roleData.displayName);
paramCount++;
}
if (roleData.description !== undefined) {
setParts.push(`description = $${paramCount}`);
values.push(roleData.description);
paramCount++;
}
if (roleData.color !== undefined) {
setParts.push(`color = $${paramCount}`);
values.push(roleData.color);
paramCount++;
}
if (roleData.isActive !== undefined) {
setParts.push(`is_active = $${paramCount}`);
values.push(roleData.isActive);
paramCount++;
}
setParts.push(`updated_at = CURRENT_TIMESTAMP`);
values.push(id);
const result = await pool.query(`
UPDATE roles
SET ${setParts.join(', ')}
WHERE id = $${paramCount}
RETURNING *
`, values);
const row = result.rows[0];
return {
id: row.id,
name: row.name,
displayName: row.display_name || row.name,
description: row.description,
color: row.color,
isSystem: row.is_system,
isActive: row.is_active,
createdAt: row.created_at,
updatedAt: row.updated_at
};
} catch (error) {
console.error("Error in updateRole:", error);
throw error;
}
}
async deleteRole(id: number): Promise<void> {
try {
// Delete role permissions first
await pool.query('DELETE FROM role_permissions WHERE role_id = $1', [id]);
// Delete the role
await pool.query('DELETE FROM roles WHERE id = $1', [id]);
} catch (error) {
console.error("Error in deleteRole:", error);
throw error;
}
}
async getPermissions(): Promise<Permission[]> {
try {
console.log('🔍 PRODUCTION getPermissions() - Starting query...');
const result = await pool.query(`
SELECT id, name, display_name, description, category, action, resource, is_system, created_at
FROM permissions
ORDER BY category, name
`);
console.log('📊 PRODUCTION getPermissions() - Total permissions found:', result.rows.length);
if (result.rows.length === 0) {
console.log('⚠️ PRODUCTION getPermissions() - NO PERMISSIONS IN DATABASE!');
return [];
}
// Analyser les catégories spécifiques
const taskPerms = result.rows.filter(row => row.category === 'gestion_taches');
const adminPerms = result.rows.filter(row => row.category === 'administration');
console.log('📋 PRODUCTION Task permissions found:', taskPerms.length);
taskPerms.forEach(p => console.log(` - ID ${p.id}: ${p.name} -> "${p.display_name}"`));
console.log('🏛️ PRODUCTION Admin permissions found:', adminPerms.length);
adminPerms.forEach(p => console.log(` - ID ${p.id}: ${p.name} -> "${p.display_name}"`));
// Transformation des données snake_case vers camelCase pour cohérence TypeScript
const mappedResult = result.rows.map(row => ({
id: row.id,
name: row.name,
displayName: row.display_name || row.name,
description: row.description || '',
category: row.category,
action: row.action || 'read',
resource: row.resource,
isSystem: row.is_system || false,
createdAt: row.created_at
}));
console.log('✅ PRODUCTION getPermissions() - Returning mapped result:', mappedResult.length);
return mappedResult;
} catch (error) {
console.error("❌ CRITICAL ERROR in getPermissions PRODUCTION:", error);
return [];
}
}
async createPermission(permissionData: InsertPermission): Promise<Permission> {
try {
const result = await pool.query(`
INSERT INTO permissions (
name, display_name, description, category, action, resource, is_system
) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
`, [
permissionData.name,
permissionData.displayName || permissionData.name,
permissionData.description || '',
permissionData.category,
permissionData.action || '',
permissionData.resource || '',
permissionData.isSystem || false
]);
return {
id: result.rows[0].id,
name: result.rows[0].name,
displayName: result.rows[0].display_name || result.rows[0].name,
description: result.rows[0].description,
category: result.rows[0].category,
action: result.rows[0].action || 'read',
resource: result.rows[0].resource,
isSystem: result.rows[0].is_system,
createdAt: result.rows[0].created_at
};
} catch (error) {
console.error("Error in createPermission:", error);
throw error;
}
}
async getRolePermissions(roleId: number): Promise<any[]> {
try {
console.log('🔍 PRODUCTION getRolePermissions() - Starting for role ID:', roleId);
const result = await pool.query(`
SELECT
rp.role_id,
rp.permission_id,
p.id as p_id,
p.name as p_name,
p.display_name as p_display_name,
p.description as p_description,
p.category as p_category,
p.action as p_action,
p.resource as p_resource,
p.is_system as p_is_system,
p.created_at as p_created_at,
r.name as role_name
FROM role_permissions rp
JOIN permissions p ON rp.permission_id = p.id
JOIN roles r ON rp.role_id = r.id
WHERE rp.role_id = $1
ORDER BY p.category, p.name
`, [roleId]);
console.log('📊 PRODUCTION getRolePermissions() - SQL result:', result.rows.length, 'rows');
// Map to match the development storage format
const mappedResult = result.rows.map(row => ({
roleId: row.role_id,
permissionId: row.permission_id,
permission: {
id: row.p_id,
name: row.p_name,
displayName: row.p_display_name,
description: row.p_description,
category: row.p_category,
action: row.p_action,
resource: row.p_resource,
isSystem: row.p_is_system,
createdAt: this.formatDate(row.p_created_at)
}
}));
// Debug des permissions tâches spécifiquement
const taskPermissions = mappedResult.filter(rp => rp.permission.category === 'gestion_taches');
console.log('📋 PRODUCTION getRolePermissions() - Task permissions found:', taskPermissions.length);
taskPermissions.forEach(tp => {
console.log(` - Permission: ${tp.permission.name} (${tp.permission.displayName}) - Category: ${tp.permission.category}`);
});
console.log('✅ PRODUCTION getRolePermissions() - Returning', mappedResult.length, 'role permissions');
return mappedResult;
} catch (error) {
console.error("❌ Error in PRODUCTION getRolePermissions:", error);
return [];
}
}
async setRolePermissions(roleId: number, permissionIds: number[]): Promise<void> {
try {
console.log("🔄 PRODUCTION setRolePermissions called:", { roleId, permissionIds });
// Delete existing permissions for this role
await pool.query('DELETE FROM role_permissions WHERE role_id = $1', [roleId]);
console.log("✅ PRODUCTION Deleted existing role permissions for role:", roleId);
// Insert new permissions (without created_at column since it doesn't exist in production)
if (permissionIds.length > 0) {
const values = permissionIds.map((permId, index) =>
`($1, $${index + 2})`
).join(', ');
console.log("📝 PRODUCTION Inserting new permissions:", values);
await pool.query(`
INSERT INTO role_permissions (role_id, permission_id)
VALUES ${values}
`, [roleId, ...permissionIds]);
console.log("✅ PRODUCTION Successfully inserted", permissionIds.length, "permissions for role", roleId);
}
} catch (error) {
console.error("❌ PRODUCTION Error in setRolePermissions:", error);
throw error;
}
}
async setUserRoles(userId: string, roleIds: number[], assignedBy: string): Promise<void> {
try {
console.log(`🔧 setUserRoles called:`, { userId, roleIds, assignedBy });
// Vérifier que l'utilisateur existe
const userExists = await pool.query('SELECT id FROM users WHERE id = $1', [userId]);
if (userExists.rows.length === 0) {
console.error(`❌ User ${userId} not found`);
throw new Error(`User with ID ${userId} does not exist`);
}
console.log(`✅ User ${userId} exists`);
// Vérifier les rôles disponibles
const availableRoles = await pool.query('SELECT id, name FROM roles ORDER BY id');
console.log(`📋 Available roles:`, availableRoles.rows);
// Delete existing user roles
const deleteResult = await pool.query('DELETE FROM user_roles WHERE user_id = $1', [userId]);
console.log(`🗑️ Deleted ${deleteResult.rowCount} existing roles for user ${userId}`);
// Insert new user role (only one role per user)
if (roleIds.length > 0) {
const roleId = roleIds[0]; // Take only the first role
console.log(`🎯 Attempting to assign role ID: ${roleId}`);
// Vérifier que le rôle existe
const roleExists = await pool.query('SELECT id, name FROM roles WHERE id = $1', [roleId]);
if (roleExists.rows.length === 0) {
console.error(`❌ Role ${roleId} not found in available roles:`, availableRoles.rows.map(r => r.id));
throw new Error(`Role with ID ${roleId} does not exist. Available roles: ${availableRoles.rows.map(r => `${r.id}(${r.name})`).join(', ')}`);
}
console.log(`✅ Role ${roleId} (${roleExists.rows[0].name}) exists`);
const insertResult = await pool.query(`
INSERT INTO user_roles (user_id, role_id, assigned_by, assigned_at)
VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
RETURNING *
`, [userId, roleId, assignedBy]);
console.log(`✅ Successfully assigned role:`, insertResult.rows[0]);
} else {
console.log(`⚠️ No roles provided for user ${userId}`);
}
} catch (error) {
console.error("❌ Error in setUserRoles:", error);
throw error;
}
}
// NocoDB Config methods
async getNocodbConfigs(): Promise<NocodbConfig[]> {
try {
const result = await pool.query(`
SELECT id, name, base_url, project_id, api_token, description,
is_active, created_by, created_at, updated_at
FROM nocodb_config
ORDER BY created_at DESC
`);
// Transformation des données snake_case vers camelCase pour cohérence TypeScript
const transformedData = result.rows.map(row => ({
id: row.id,
name: row.name,
baseUrl: row.base_url,
projectId: row.project_id,
apiToken: row.api_token,
description: row.description,
isActive: row.is_active,
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at
}));
console.log('📊 getNocodbConfigs result:', {
rows: transformedData.length,
transformed: true,
sample: transformedData[0] || 'empty'
});
return Array.isArray(transformedData) ? transformedData : [];
} catch (error) {
console.error('❌ Error in getNocodbConfigs:', error);
return [];
}
}
async getNocodbConfig(id: number): Promise<NocodbConfig | undefined> {
const result = await pool.query(`
SELECT id, name, base_url, project_id, api_token, description,
is_active, created_by, created_at, updated_at
FROM nocodb_config
WHERE id = $1
`, [id]);
if (!result.rows[0]) return undefined;
// Transformation des données snake_case vers camelCase
const row = result.rows[0];
return {
id: row.id,
name: row.name,
baseUrl: row.base_url,
projectId: row.project_id,
apiToken: row.api_token,
description: row.description,
isActive: row.is_active,
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at
};
}
async createNocodbConfig(config: InsertNocodbConfig): Promise<NocodbConfig> {
console.log('📝 Creating NocoDB config with data:', config);
try {
// Première tentative avec la structure moderne
const result = await pool.query(`
INSERT INTO nocodb_config (
name, base_url, project_id, api_token, description, is_active, created_by
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
`, [
config.name,
config.baseUrl,
config.projectId || '',
config.apiToken,
config.description || '',
config.isActive !== undefined ? config.isActive : true,
config.createdBy
]);
console.log('✅ NocoDB config created:', result.rows[0]);
return result.rows[0];
} catch (error: any) {
// Si erreur de contrainte NOT NULL sur colonnes obsolètes
if (error.code === '23502' && (error.column === 'table_id' || error.column === 'table_name' || error.column === 'invoice_column_name')) {
console.log('🔧 Detected obsolete columns with NOT NULL constraints, attempting automatic fix...');
try {
// Essayer de supprimer les colonnes obsolètes automatiquement
await pool.query(`
ALTER TABLE nocodb_config
DROP COLUMN IF EXISTS table_id,
DROP COLUMN IF EXISTS table_name,
DROP COLUMN IF EXISTS invoice_column_name
`);
console.log('✅ Obsolete columns removed successfully');
// Réessayer l'insertion
const result = await pool.query(`
INSERT INTO nocodb_config (
name, base_url, project_id, api_token, description, is_active, created_by
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
`, [
config.name,
config.baseUrl,
config.projectId || '',
config.apiToken,
config.description || '',
config.isActive !== undefined ? config.isActive : true,
config.createdBy
]);
console.log('✅ NocoDB config created after automatic fix:', result.rows[0]);
return result.rows[0];
} catch (fixError) {
console.error('❌ Failed to automatically fix table structure:', fixError);
// Dernier recours : insertion avec valeurs par défaut pour les colonnes obsolètes
try {
const result = await pool.query(`
INSERT INTO nocodb_config (
name, base_url, project_id, api_token, description, is_active, created_by,
table_id, table_name, invoice_column_name
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *
`, [
config.name,
config.baseUrl,
config.projectId || '',
config.apiToken,
config.description || '',
config.isActive !== undefined ? config.isActive : true,
config.createdBy,
'', // table_id par défaut
'', // table_name par défaut
'' // invoice_column_name par défaut
]);
console.log('✅ NocoDB config created with legacy compatibility:', result.rows[0]);
return result.rows[0];
} catch (legacyError) {
console.error('❌ All insertion methods failed:', legacyError);
throw error; // Rethrow l'erreur originale
}
}
} else {
console.error('❌ Error creating NocoDB config:', error);
throw error;
}
}
}
async updateNocodbConfig(id: number, config: Partial<InsertNocodbConfig>): Promise<NocodbConfig> {
const result = await pool.query(`
UPDATE nocodb_config SET
name = $1, base_url = $2, project_id = $3, api_token = $4,
description = $5, is_active = $6, updated_at = CURRENT_TIMESTAMP
WHERE id = $7
RETURNING *
`, [
config.name,
config.baseUrl,
config.projectId,
config.apiToken,
config.description || '',
config.isActive,
id
]);
return result.rows[0];
}
async deleteNocodbConfig(id: number): Promise<void> {
await pool.query('DELETE FROM nocodb_config WHERE id = $1', [id]);
}
// Customer Orders methods
async getCustomerOrders(groupIds?: number[]): Promise<any[]> {
let whereClause = '';
let params = [];
if (groupIds && groupIds.length > 0) {
whereClause = ' WHERE co.group_id = ANY($1)';
params = [groupIds];
}
const result = await pool.query(`
SELECT co.*, s.name as supplier_name, g.name as group_name, g.color as group_color,
u.username as creator_username, u.name as creator_name
FROM customer_orders co
LEFT JOIN suppliers s ON co.supplier_id = s.id
LEFT JOIN groups g ON co.group_id = g.id
LEFT JOIN users u ON co.created_by = u.id
${whereClause}
ORDER BY co.created_at DESC
`, params);
return result.rows.map(row => ({
id: row.id,
orderTaker: row.order_taker,
customerName: row.customer_name,
customerPhone: row.customer_phone,
customerEmail: row.customer_email,
productDesignation: row.product_designation,
productReference: row.product_reference,
gencode: row.gencode,
quantity: row.quantity || 1,
supplierId: row.supplier_id,
status: row.status,
deposit: row.deposit,
isPromotionalPrice: row.is_promotional_price,
customerNotified: row.customer_notified,
notes: row.notes,
groupId: row.group_id,
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at,
creator: {
username: row.creator_username,
name: row.creator_name
},
supplier: {
id: row.supplier_id,
name: row.supplier_name
},
group: {
id: row.group_id,
name: row.group_name,
color: row.group_color
}
}));
}
async getCustomerOrder(id: number): Promise<any> {
const result = await pool.query(`
SELECT co.*, s.name as supplier_name, g.name as group_name, g.color as group_color,
u.username as creator_username, u.name as creator_name
FROM customer_orders co
LEFT JOIN suppliers s ON co.supplier_id = s.id
LEFT JOIN groups g ON co.group_id = g.id
LEFT JOIN users u ON co.created_by = u.id
WHERE co.id = $1
`, [id]);
if (!result.rows[0]) return undefined;
const row = result.rows[0];
return {
id: row.id,
orderTaker: row.order_taker,
customerName: row.customer_name,
customerPhone: row.customer_phone,
customerEmail: row.customer_email,
productDesignation: row.product_designation,
productReference: row.product_reference,
gencode: row.gencode,
quantity: row.quantity || 1,
supplierId: row.supplier_id,
status: row.status,
deposit: row.deposit,
isPromotionalPrice: row.is_promotional_price,
customerNotified: row.customer_notified,
notes: row.notes,
groupId: row.group_id,
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at,
creator: {
username: row.creator_username,
name: row.creator_name
},
supplier: {
id: row.supplier_id,
name: row.supplier_name
},
group: {
id: row.group_id,
name: row.group_name,
color: row.group_color
}
};
}
async createCustomerOrder(customerOrder: InsertCustomerOrder): Promise<CustomerOrder> {
const result = await pool.query(`
INSERT INTO customer_orders (
order_taker, customer_name, customer_phone, customer_email,
product_designation, product_reference, gencode, quantity,
supplier_id, status, deposit, is_promotional_price,
customer_notified, group_id, created_by
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
RETURNING *
`, [
customerOrder.orderTaker,
customerOrder.customerName,
customerOrder.customerPhone,
customerOrder.customerEmail || null,
customerOrder.productDesignation,
customerOrder.productReference || null,
customerOrder.gencode,
customerOrder.quantity || 1,
customerOrder.supplierId,
customerOrder.status || 'En attente de Commande',
customerOrder.deposit || '0.00',
customerOrder.isPromotionalPrice || false,
customerOrder.customerNotified || false,
customerOrder.groupId,
customerOrder.createdBy
]);
return result.rows[0];
}
async updateCustomerOrder(id: number, customerOrder: Partial<InsertCustomerOrder>): Promise<CustomerOrder> {
const result = await pool.query(`
UPDATE customer_orders SET
order_taker = COALESCE($1, order_taker),
customer_name = COALESCE($2, customer_name),
customer_phone = COALESCE($3, customer_phone),
customer_email = COALESCE($4, customer_email),
product_designation = COALESCE($5, product_designation),
product_reference = COALESCE($6, product_reference),
gencode = COALESCE($7, gencode),
quantity = COALESCE($8, quantity),
supplier_id = COALESCE($9, supplier_id),
status = COALESCE($10, status),
deposit = COALESCE($11, deposit),
is_promotional_price = COALESCE($12, is_promotional_price),
customer_notified = COALESCE($13, customer_notified),
notes = COALESCE($14, notes),
group_id = COALESCE($15, group_id),
updated_at = CURRENT_TIMESTAMP
WHERE id = $16
RETURNING *
`, [
customerOrder.orderTaker,
customerOrder.customerName,
customerOrder.customerPhone,
customerOrder.customerEmail,
customerOrder.productDesignation,
customerOrder.productReference,
customerOrder.gencode,
customerOrder.quantity,
customerOrder.supplierId,
customerOrder.status,
customerOrder.deposit,
customerOrder.isPromotionalPrice,
customerOrder.customerNotified,
customerOrder.notes,
customerOrder.groupId,
id
]);
return result.rows[0];
}
async deleteCustomerOrder(id: number): Promise<void> {
await pool.query('DELETE FROM customer_orders WHERE id = $1', [id]);
}
// 🔧 MÉTHODES MANQUANTES POUR L'AFFICHAGE DES RÔLES
async getUserWithRoles(userId: string): Promise<any> {
try {
const user = await this.getUser(userId);
if (!user) return undefined;
const result = await pool.query(`
SELECT
ur.user_id,
ur.role_id,
ur.assigned_by,
ur.assigned_at,
r.id as role_id,
r.name as role_name,
r.display_name as role_display_name,
r.description as role_description,
r.color as role_color,
r.is_system as role_is_system,
r.is_active as role_is_active,
r.created_at as role_created_at,
r.updated_at as role_updated_at
FROM user_roles ur
LEFT JOIN roles r ON ur.role_id = r.id
WHERE ur.user_id = $1
`, [userId]);
const userRoleData = result.rows.map(row => ({
userId: row.user_id,
roleId: row.role_id,
assignedBy: row.assigned_by,
assignedAt: row.assigned_at,
role: {
id: row.role_id,
name: row.role_name,
displayName: row.role_display_name,
description: row.role_description,
color: row.role_color,
isSystem: row.role_is_system,
isActive: row.role_is_active,
createdAt: row.role_created_at,
updatedAt: row.role_updated_at,
},
}));
console.log(`📊 getUserWithRoles(${userId}):`, { userRoleDataLength: userRoleData.length });
return {
...user,
userRoles: userRoleData,
};
} catch (error) {
console.error("Error in getUserWithRoles:", error);
return undefined;
}
}
async getUsersWithRolesAndGroups(): Promise<any[]> {
console.log('🔍 getUsersWithRolesAndGroups called');
try {
const baseUsers = await this.getUsers();
console.log('📊 Base users found:', baseUsers.length);
const usersWithRolesAndGroups = await Promise.all(
baseUsers.map(async (user) => {
console.log(`🔍 Processing user: ${user.username}`);
const userWithRoles = await this.getUserWithRoles(user.id);
const userWithGroups = await this.getUserWithGroups(user.id);
console.log(`📊 User ${user.username} groups:`, userWithGroups?.userGroups?.length || 0);
return {
...user,
userRoles: userWithRoles?.userRoles || [],
userGroups: userWithGroups?.userGroups || [],
roles: userWithRoles?.userRoles?.map(ur => ur.role) || []
};
})
);
console.log('🔍 Final users with roles and groups:', usersWithRolesAndGroups.length);
return usersWithRolesAndGroups;
} catch (error) {
console.error("Error in getUsersWithRolesAndGroups:", error);
return [];
}
}
// ===== DLC PRODUCTS METHODS =====
async getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number }): Promise<DlcProduct[]> {
try {
let query = `
SELECT dlc.*, g.name as group_name, s.name as supplier_name
FROM dlc_products dlc
LEFT JOIN groups g ON dlc.group_id = g.id
LEFT JOIN suppliers s ON dlc.supplier_id = s.id
`;
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (groupIds && groupIds.length > 0) {
conditions.push(`dlc.group_id = ANY($${paramIndex})`);
params.push(groupIds);
paramIndex++;
}
if (filters?.status) {
console.log(`🔍 DLC Filter Debug - Requested status: ${filters.status}`);
if (filters.status === 'expires_soon') {
// Produits qui expirent dans les 15 prochains jours (mais pas encore expirés)
conditions.push(`COALESCE(dlc.dlc_date, dlc.expiry_date) <= CURRENT_DATE + INTERVAL '15 days' AND COALESCE(dlc.dlc_date, dlc.expiry_date) > CURRENT_DATE`);
} else if (filters.status === 'expires') {
// Produits déjà expirés
conditions.push(`COALESCE(dlc.dlc_date, dlc.expiry_date) < CURRENT_DATE`);
} else if (filters.status === 'en_cours') {
// Produits encore valides (plus de 15 jours avant expiration)
conditions.push(`COALESCE(dlc.dlc_date, dlc.expiry_date) > CURRENT_DATE + INTERVAL '15 days'`);
} else if (filters.status === 'valides') {
// Produits avec statut exact "valides"
conditions.push(`dlc.status = 'valides'`);
} else {
// Filtrage par statut exact
conditions.push(`dlc.status = $${paramIndex}`);
params.push(filters.status);
paramIndex++;
}
}
if (filters?.supplierId) {
conditions.push(`dlc.supplier_id = $${paramIndex}`);
params.push(filters.supplierId);
paramIndex++;
}
if (conditions.length > 0) {
query += ` WHERE ${conditions.join(' AND ')}`;
}
query += ` ORDER BY dlc.created_at DESC`;
console.log('🔍 getDlcProducts query:', query);
console.log('🔍 getDlcProducts params:', params);
const result = await pool.query(query, params);
console.log(`🔍 getDlcProducts RESULT: Found ${result.rows.length} products`);
if (filters?.status) {
console.log(`🔍 Filter applied: ${filters.status} - Results:`, result.rows.map(r => ({
name: r.product_name,
status: r.status,
dlc_date: r.dlc_date,
calculated_days: r.dlc_date ? Math.ceil((new Date(r.dlc_date).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)) : 'N/A'
})));
}
return result.rows.map(row => ({
id: row.id,
productName: row.product_name || row.name,
gencode: row.gencode || row.product_code,
dlcDate: this.formatDate(row.dlc_date || row.expiry_date),
dateType: row.date_type || 'DLC',
quantity: row.quantity || 1,
unit: row.unit || 'unité',
location: row.location || 'Magasin',
status: row.status || 'en_attente',
notes: row.notes || '',
alertThreshold: row.alert_threshold || 15,
groupId: row.group_id,
supplierId: row.supplier_id,
createdBy: row.created_by,
validatedBy: row.validated_by,
validatedAt: this.formatDate(row.validated_at),
createdAt: this.formatDate(row.created_at),
updatedAt: this.formatDate(row.updated_at),
supplier: row.supplier_name ? {
id: row.supplier_id,
name: row.supplier_name
} : null,
group: row.group_name ? {
id: row.group_id,
name: row.group_name
} : null
}));
} catch (error) {
console.error("Error fetching DLC products:", error);
throw error;
}
}
async getDlcProduct(id: number): Promise<DlcProduct | undefined> {
try {
const result = await pool.query(`
SELECT dlc.*, g.name as group_name, s.name as supplier_name
FROM dlc_products dlc
LEFT JOIN groups g ON dlc.group_id = g.id
LEFT JOIN suppliers s ON dlc.supplier_id = s.id
WHERE dlc.id = $1
`, [id]);
if (result.rows.length === 0) return undefined;
const row = result.rows[0];
return {
id: row.id,
productName: row.product_name,
gencode: row.gencode,
dlcDate: this.formatDate(row.dlc_date),
dateType: row.date_type,
quantity: row.quantity,
unit: row.unit,
location: row.location,
status: row.status,
notes: row.notes,
alertThreshold: row.alert_threshold,
groupId: row.group_id,
supplierId: row.supplier_id,
createdBy: row.created_by,
validatedBy: row.validated_by,
validatedAt: this.formatDate(row.validated_at),
createdAt: this.formatDate(row.created_at),
updatedAt: this.formatDate(row.updated_at),
supplier: row.supplier_name ? {
id: row.supplier_id,
name: row.supplier_name
} : null,
group: row.group_name ? {
id: row.group_id,
name: row.group_name
} : null
};
} catch (error) {
console.error("Error fetching DLC product:", error);
throw error;
}
}
async createDlcProduct(dlcProductData: InsertDlcProductFrontend): Promise<DlcProductFrontend> {
try {
console.log('📨 Creating DLC product with data:', JSON.stringify(dlcProductData, null, 2));
console.log('📨 DLC data.name:', dlcProductData.name);
console.log('📨 DLC data.productCode:', dlcProductData.productCode);
console.log('📨 DLC data.dlcDate:', dlcProductData.dlcDate);
console.log('📨 DLC data.expiryDate:', (dlcProductData as any).expiryDate);
// Support à la fois dlcDate et expiryDate pour compatibilité
const finalExpiryDate = dlcProductData.dlcDate || (dlcProductData as any).expiryDate;
const finalProductName = dlcProductData.name || (dlcProductData as any).productName || 'Produit DLC';
const finalProductCode = dlcProductData.productCode || (dlcProductData as any).gencode || '';
console.log('📨 Using finalExpiryDate:', finalExpiryDate);
console.log('📨 Using finalProductName:', finalProductName);
// Vérifier que la date d'expiration n'est pas null/undefined
if (!finalExpiryDate) {
console.error('❌ Expiry date is missing! dlcDate:', dlcProductData.dlcDate, 'expiryDate:', (dlcProductData as any).expiryDate);
console.error('❌ Full data received:', dlcProductData);
throw new Error('Expiry date is required but was null or undefined');
}
console.log('📨 Using productName:', finalProductName);
const result = await pool.query(`
INSERT INTO dlc_products (
name, product_name, product_code, gencode, dlc_date, expiry_date,
quantity, status, group_id, supplier_id,
created_by, created_at, updated_at, date_type, unit,
location, alert_threshold, notes
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW(), NOW(), $12, $13, $14, $15, $16)
RETURNING *
`, [
finalProductName, // name
finalProductName, // product_name
finalProductCode, // product_code
finalProductCode, // gencode
finalExpiryDate, // dlc_date
finalExpiryDate, // expiry_date
dlcProductData.quantity || 1, // quantity
dlcProductData.status || 'en_attente', // status
dlcProductData.groupId, // group_id
dlcProductData.supplierId, // supplier_id
dlcProductData.createdBy, // created_by
dlcProductData.dateType || 'DLC', // date_type
dlcProductData.unit || 'unité', // unit
dlcProductData.location || 'Magasin', // location
dlcProductData.alertThreshold || 15, // alert_threshold
dlcProductData.notes || '' // notes
]);
const row = result.rows[0];
console.log('✅ DLC product created:', row.id);
return {
id: row.id,
name: row.name || row.product_name,
productCode: row.product_code || row.gencode,
dlcDate: this.formatDate(row.dlc_date || row.expiry_date),
quantity: row.quantity,
status: row.status,
groupId: row.group_id,
supplierId: row.supplier_id,
description: row.description || row.notes,
createdBy: row.created_by,
validatedBy: row.validated_by,
validatedAt: this.formatDate(row.validated_at),
createdAt: this.formatDate(row.created_at),
updatedAt: this.formatDate(row.updated_at)
};
} catch (error) {
console.error("❌ Error creating DLC product:", error);
throw error;
}
}
async updateDlcProduct(id: number, dlcProductData: Partial<InsertDlcProductFrontend>): Promise<DlcProductFrontend> {
try {
const fields: string[] = [];
const params: any[] = [];
let paramIndex = 1;
Object.entries(dlcProductData).forEach(([key, value]) => {
if (value !== undefined) {
if (key === 'name' || key === 'productName') {
fields.push(`name = $${paramIndex}`);
fields.push(`product_name = $${paramIndex}`);
params.push(value);
paramIndex++;
} else if (key === 'productCode' || key === 'gencode') {
fields.push(`product_code = $${paramIndex}`);
fields.push(`gencode = $${paramIndex}`);
params.push(value);
paramIndex++;
} else if (key === 'dlcDate') {
fields.push(`dlc_date = $${paramIndex}`);
fields.push(`expiry_date = $${paramIndex}`);
params.push(value);
paramIndex++;
} else if (key === 'description') {
fields.push(`description = $${paramIndex}`);
fields.push(`notes = $${paramIndex}`);
params.push(value);
paramIndex++;
} else if (key === 'groupId') {
fields.push(`group_id = $${paramIndex}`);
params.push(value);
paramIndex++;
} else if (key === 'supplierId') {
fields.push(`supplier_id = $${paramIndex}`);
params.push(value);
paramIndex++;
} else if (key === 'createdBy') {
fields.push(`created_by = $${paramIndex}`);
params.push(value);
paramIndex++;
} else if (key === 'dateType') {
fields.push(`date_type = $${paramIndex}`);
params.push(value);
paramIndex++;
} else if (key === 'alertThreshold') {
fields.push(`alert_threshold = $${paramIndex}`);
params.push(value);
paramIndex++;
} else if (key === 'validatedBy') {
fields.push(`validated_by = $${paramIndex}`);
params.push(value);
paramIndex++;
} else if (key === 'validatedAt') {
fields.push(`validated_at = $${paramIndex}`);
params.push(value);
paramIndex++;
} else {
// Pour les autres champs, utiliser le nom tel quel
const dbFieldName = key.replace(/([A-Z])/g, '_$1').toLowerCase();
fields.push(`${dbFieldName} = $${paramIndex}`);
params.push(value);
paramIndex++;
}
}
});
fields.push(`updated_at = NOW()`);
params.push(id);
const result = await pool.query(`
UPDATE dlc_products
SET ${fields.join(', ')}
WHERE id = $${paramIndex}
RETURNING *
`, params);
const row = result.rows[0];
return {
id: row.id,
name: row.name || row.product_name,
productCode: row.product_code || row.gencode,
dlcDate: this.formatDate(row.dlc_date || row.expiry_date),
quantity: row.quantity,
status: row.status,
groupId: row.group_id,
supplierId: row.supplier_id,
description: row.description || row.notes,
createdBy: row.created_by,
validatedBy: row.validated_by,
validatedAt: this.formatDate(row.validated_at),
createdAt: this.formatDate(row.created_at),
updatedAt: this.formatDate(row.updated_at)
};
} catch (error) {
console.error("Error updating DLC product:", error);
throw error;
}
}
async deleteDlcProduct(id: number): Promise<void> {
try {
console.log('🗑️ Deleting DLC product with ID:', id);
const result = await pool.query('DELETE FROM dlc_products WHERE id = $1', [id]);
console.log('🗑️ DLC product deleted, rows affected:', result.rowCount);
} catch (error) {
console.error("❌ Error deleting DLC product:", error);
throw error;
}
}
async validateDlcProduct(id: number, validatedBy: string): Promise<DlcProductFrontend> {
try {
console.log('✅ Validating DLC product:', { id, validatedBy });
const result = await pool.query(`
UPDATE dlc_products
SET status = 'valide', validated_by = $1, validated_at = NOW(), updated_at = NOW()
WHERE id = $2
RETURNING *
`, [validatedBy, id]);
if (result.rows.length === 0) {
throw new Error('DLC Product not found');
}
const row = result.rows[0];
console.log('✅ DLC product validated:', row.id);
return {
id: row.id,
productName: row.product_name || row.name,
gencode: row.gencode || row.product_code,
dlcDate: this.formatDate(row.dlc_date || row.expiry_date),
dateType: row.date_type || 'DLC',
quantity: row.quantity || 1,
unit: row.unit || 'unité',
location: row.location || 'Magasin',
status: row.status,
notes: row.notes || '',
alertThreshold: row.alert_threshold || 15,
groupId: row.group_id,
supplierId: row.supplier_id,
createdBy: row.created_by,
validatedBy: row.validated_by,
validatedAt: this.formatDate(row.validated_at),
createdAt: this.formatDate(row.created_at),
updatedAt: this.formatDate(row.updated_at)
};
} catch (error) {
console.error("❌ Error validating DLC product:", error);
throw error;
}
}
async getDlcStats(groupIds?: number[]): Promise<any> {
try {
console.log('📊 DLC Stats - groupIds:', groupIds);
// Query to get all products with date calculations
let query = `
SELECT
status,
dlc_date,
expiry_date,
COUNT(*) as count,
SUM(quantity) as total_quantity,
CASE
WHEN COALESCE(dlc_date, expiry_date) < CURRENT_DATE THEN 'expired'
WHEN COALESCE(dlc_date, expiry_date) <= CURRENT_DATE + INTERVAL '15 days' THEN 'expiring_soon'
ELSE 'active'
END as calculated_status
FROM dlc_products
`;
const params: any[] = [];
if (groupIds && groupIds.length > 0) {
query += ` WHERE group_id = ANY($1)`;
params.push(groupIds);
}
query += ` GROUP BY status, dlc_date, expiry_date, calculated_status`;
console.log('📊 DLC Stats query:', query);
console.log('📊 DLC Stats params:', params);
const result = await pool.query(query, params);
console.log('📊 DLC Stats raw result:', result.rows);
const stats = {
active: 0,
expiringSoon: 0,
expired: 0,
valide: 0,
en_cours: 0,
total: 0,
totalQuantity: 0
};
result.rows.forEach(row => {
const count = parseInt(row.count);
const quantity = parseInt(row.total_quantity || 0);
// Compter uniquement par statut calculé (basé sur les dates) pour l'affichage frontend
if (row.calculated_status === 'expired') {
stats.expired += count;
} else if (row.calculated_status === 'expiring_soon') {
stats.expiringSoon += count;
} else if (row.calculated_status === 'active') {
stats.active += count;
}
stats.total += count;
stats.totalQuantity += quantity;
});
console.log('📊 DLC Stats final:', stats);
return stats;
} catch (error) {
console.error("❌ Error fetching DLC stats:", error);
throw error;
}
}
// Tasks methods
async getTasks(groupIds?: number[]): Promise<any[]> {
let whereClause = '';
let params = [];
if (groupIds && groupIds.length > 0) {
whereClause = ' WHERE t.group_id = ANY($1)';
params = [groupIds];
}
const result = await pool.query(`
SELECT t.*,
g.name as group_name, g.color as group_color,
creator.username as creator_username, creator.first_name as creator_first_name, creator.last_name as creator_last_name,
completer.username as completer_username, completer.first_name as completer_first_name, completer.last_name as completer_last_name
FROM tasks t
LEFT JOIN groups g ON t.group_id = g.id
LEFT JOIN users creator ON t.created_by = creator.id
LEFT JOIN users completer ON t.completed_by = completer.id
${whereClause}
ORDER BY t.created_at DESC
`, params);
return result.rows.map(row => ({
id: row.id,
title: row.title,
description: row.description,
status: row.status,
priority: row.priority,
dueDate: this.formatDate(row.due_date),
assignedTo: row.assigned_to,
groupId: row.group_id,
createdBy: row.created_by,
createdAt: this.formatDate(row.created_at),
updatedAt: this.formatDate(row.updated_at),
completedAt: this.formatDate(row.completed_at),
completedBy: row.completed_by,
creator: {
id: row.created_by,
username: row.creator_username,
name: `${row.creator_first_name || ''} ${row.creator_last_name || ''}`.trim()
},
completer: row.completed_by ? {
id: row.completed_by,
username: row.completer_username,
name: `${row.completer_first_name || ''} ${row.completer_last_name || ''}`.trim()
} : null,
group: {
id: row.group_id,
name: row.group_name,
color: row.group_color
}
}));
}
async getTask(id: number): Promise<any> {
const result = await pool.query(`
SELECT t.*,
g.name as group_name, g.color as group_color,
creator.username as creator_username, creator.first_name as creator_first_name, creator.last_name as creator_last_name,
completer.username as completer_username, completer.first_name as completer_first_name, completer.last_name as completer_last_name
FROM tasks t
LEFT JOIN groups g ON t.group_id = g.id
LEFT JOIN users creator ON t.created_by = creator.id
LEFT JOIN users completer ON t.completed_by = completer.id
WHERE t.id = $1
`, [id]);
if (!result.rows[0]) return undefined;
const row = result.rows[0];
return {
id: row.id,
title: row.title,
description: row.description,
status: row.status,
priority: row.priority,
dueDate: this.formatDate(row.due_date),
assignedTo: row.assigned_to,
groupId: row.group_id,
createdBy: row.created_by,
createdAt: this.formatDate(row.created_at),
updatedAt: this.formatDate(row.updated_at),
completedAt: this.formatDate(row.completed_at),
completedBy: row.completed_by,
creator: {
id: row.created_by,
username: row.creator_username,
name: `${row.creator_first_name || ''} ${row.creator_last_name || ''}`.trim()
},
completer: row.completed_by ? {
id: row.completed_by,
username: row.completer_username,
name: `${row.completer_first_name || ''} ${row.completer_last_name || ''}`.trim()
} : null,
group: {
id: row.group_id,
name: row.group_name,
color: row.group_color
}
};
}
async createTask(task: InsertTask): Promise<Task> {
const result = await pool.query(`
INSERT INTO tasks (
title, description, status, priority, due_date,
assigned_to, group_id, created_by
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *
`, [
task.title,
task.description || null,
task.status || 'pending',
task.priority || 'medium',
this.formatDate(task.dueDate),
task.assignedTo,
task.groupId,
task.createdBy
]);
return result.rows[0];
}
async updateTask(id: number, task: Partial<InsertTask> & { completedAt?: string; completedBy?: string }): Promise<Task> {
const updateFields = [];
const values = [];
let paramCount = 1;
if (task.title !== undefined) {
updateFields.push(`title = $${paramCount++}`);
values.push(task.title);
}
if (task.description !== undefined) {
updateFields.push(`description = $${paramCount++}`);
values.push(task.description);
}
if (task.status !== undefined) {
updateFields.push(`status = $${paramCount++}`);
values.push(task.status);
// If status is completed, set completed_at only if not explicitly provided
if (task.status === 'completed' && task.completedAt === undefined) {
updateFields.push(`completed_at = CURRENT_TIMESTAMP`);
} else if (task.status !== 'completed' && task.completedAt === undefined) {
updateFields.push(`completed_at = NULL`);
}
}
if (task.priority !== undefined) {
updateFields.push(`priority = $${paramCount++}`);
values.push(task.priority);
}
if (task.dueDate !== undefined) {
updateFields.push(`due_date = $${paramCount++}`);
values.push(this.formatDate(task.dueDate));
}
if (task.assignedTo !== undefined) {
updateFields.push(`assigned_to = $${paramCount++}`);
values.push(task.assignedTo);
}
if (task.groupId !== undefined) {
updateFields.push(`group_id = $${paramCount++}`);
values.push(task.groupId);
}
// Add completedAt and completedBy for production routes
if (task.completedAt !== undefined) {
updateFields.push(`completed_at = $${paramCount++}`);
values.push(task.completedAt);
}
if (task.completedBy !== undefined) {
updateFields.push(`completed_by = $${paramCount++}`);
values.push(task.completedBy);
}
updateFields.push(`updated_at = CURRENT_TIMESTAMP`);
values.push(id);
const result = await pool.query(`
UPDATE tasks SET ${updateFields.join(', ')}
WHERE id = $${paramCount}
RETURNING *
`, values);
return result.rows[0];
}
async completeTask(id: number, completedBy?: string): Promise<void> {
console.log('🔄 Completing task using storage.completeTask...');
try {
if (completedBy) {
await pool.query(`
UPDATE tasks SET
status = 'completed',
completed_at = CURRENT_TIMESTAMP,
completed_by = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2
`, [completedBy, id]);
} else {
await pool.query(`
UPDATE tasks SET
status = 'completed',
completed_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`, [id]);
}
console.log('✅ Task completed successfully');
} catch (error) {
console.error('❌ Error completing task:', error);
throw error;
}
}
async deleteTask(id: number): Promise<void> {
await pool.query('DELETE FROM tasks WHERE id = $1', [id]);
}
}
export const storage = new DatabaseStorage();