Files
LogiFlow/shared/schema.ts
T
Claude 9de717387f feat(securite): encrypt SMTP passwords and NocoDB tokens at rest, log supplier mails
Encryption (server/crypto.ts):
- AES-256-GCM with a key derived from ENCRYPTION_KEY, falling back to
  SESSION_SECRET so existing deployments need no new configuration
- stored format enc:v1:<iv>:<tag>:<data>; encryption is idempotent and
  decryption passes legacy plaintext through unchanged, so nothing breaks
  mid-migration
- tampered data or a changed key raises an explicit error instead of
  returning garbage
- encrypt/decrypt is confined to the storage layer: group writes encrypt
  smtpPassword (decrypted only in emailService at connection time, never sent
  to the client), NocoDB config writes encrypt apiToken and reads decrypt it
  so the invoice verification and the admin page behave as before
- startup migration sweep encrypts secrets already stored in plaintext,
  idempotently; the active-config log line no longer prints the token

Mail history (supplier_mail_logs):
- every send attempt is recorded: delivery, store, supplier, recipient,
  subject, status sent/failed with error, message id, user id and name;
  a logging failure never fails the send itself
- GET /api/supplier-mail-logs restricted to the user's stores (admin may
  filter by store)
- on the reconciliation page the mail icon turns green once a request has
  been sent, with the date and sender in the tooltip; clicking again resends
- table created in init.sql, versioned migrations and the production
  startup migration, with delivery/group indexes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHdE9nEh8zHhQNGpCgrtYe
2026-08-14 16:02:55 +00:00

1033 lines
39 KiB
TypeScript

import {
pgTable,
text,
varchar,
timestamp,
jsonb,
index,
serial,
integer,
boolean,
date,
decimal,
primaryKey,
bigint,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
// Session storage table - mandatory for Replit Auth
export const sessions = pgTable(
"sessions",
{
sid: varchar("sid").primaryKey(),
sess: jsonb("sess").notNull(),
expire: timestamp("expire").notNull(),
},
(table) => [index("IDX_session_expire").on(table.expire)],
);
// User storage table - supports both Replit Auth and local auth
export const users = pgTable("users", {
id: varchar("id").primaryKey().notNull(),
username: varchar("username").unique().notNull(), // For simple login
email: varchar("email").unique(),
name: varchar("name"), // Single name field for compatibility
firstName: varchar("first_name"),
lastName: varchar("last_name"),
profileImageUrl: varchar("profile_image_url"),
password: varchar("password"), // For local auth only
role: varchar("role").notNull().default("employee"), // Legacy role field for compatibility
passwordChanged: boolean("password_changed").default(false), // Track if default password was changed
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Store/Group management
export const groups = pgTable("groups", {
id: serial("id").primaryKey(),
name: varchar("name").notNull(),
color: varchar("color").default("#1976D2"), // hex color code
// Configuration NocoDB pour vérification automatique des factures
nocodbConfigId: integer("nocodb_config_id"), // Référence vers la configuration NocoDB globale
nocodbTableName: varchar("nocodb_table_name"), // Nom de la table spécifique au magasin
nocodbTableId: varchar("nocodb_table_id"), // ID réel de la table dans NocoDB (ex: mrr733dfb8wtt9b)
// Mapping des colonnes par magasin dans leur table NocoDB
invoiceColumnName: varchar("invoice_column_name"), // Nom de la colonne facture dans leur table
nocodbBlColumnName: varchar("nocodb_bl_column_name"), // Nom de la colonne BL dans leur table
nocodbAmountColumnName: varchar("nocodb_amount_column_name"), // Nom de la colonne montant HT dans leur table
nocodbInvoiceAmountTTCColumnName: varchar("nocodb_invoice_amount_ttc_column_name"), // Nom de la colonne montant TTC dans leur table
nocodbSupplierColumnName: varchar("nocodb_supplier_column_name"), // Nom de la colonne fournisseur dans leur table
nocodbDueDateColumnName: varchar("nocodb_due_date_column_name"), // Nom de la colonne date d'échéance dans leur table
webhookUrl: varchar("webhook_url", { length: 500 }), // URL de webhook pour notifications par magasin
// Coordonnées du magasin, reprises dans la signature des mails fournisseurs
address: text("address"), // Adresse postale
phone: varchar("phone", { length: 50 }), // Téléphone
logo: text("logo"), // Logo en data URI (data:image/png;base64,...), intégré aux mails
// Configuration SMTP propre au magasin (envoi des mails fournisseurs)
smtpEnabled: boolean("smtp_enabled").default(false),
smtpHost: varchar("smtp_host", { length: 255 }),
smtpPort: integer("smtp_port"),
smtpSecure: boolean("smtp_secure").default(false), // true = SSL/TLS direct (465), false = STARTTLS
smtpUser: varchar("smtp_user", { length: 255 }),
smtpPassword: varchar("smtp_password", { length: 255 }), // jamais renvoyé au client
smtpSenderEmail: varchar("smtp_sender_email", { length: 255 }), // adresse expéditeur "De :"
smtpSenderName: varchar("smtp_sender_name", { length: 255 }), // nom affiché, par défaut le nom du magasin
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// User-Group assignments (many-to-many)
export const userGroups = pgTable("user_groups", {
userId: varchar("user_id").notNull(),
groupId: integer("group_id").notNull(),
createdAt: timestamp("created_at").defaultNow(),
});
// Suppliers - Schema exact correspondant à la production
export const suppliers = pgTable("suppliers", {
id: serial("id").primaryKey(),
name: varchar("name").notNull(),
contact: varchar("contact"),
phone: varchar("phone"),
codefou: varchar("codefou"), // Code fournisseur dans l'API ffnancy
email: varchar("email"),
hasDlc: boolean("has_dlc").default(false), // Coche DLC pour la gestion DLC
paymentMethod: varchar("payment_method"), // Mode de paiement: Virement, Traite, Traite Magnétique, Chèque
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
automaticReconciliation: boolean("automatic_reconciliation").default(false), // Rapprochement automatique BL/Factures
requiresControl: boolean("requires_control").default(false), // Fournisseur nécessite un contrôle à la livraison
});
// Contacts libres (hors fournisseurs), rattachés à un magasin
export const contacts = pgTable("contacts", {
id: serial("id").primaryKey(),
groupId: integer("group_id").notNull(),
name: varchar("name").notNull(),
company: varchar("company"),
role: varchar("role"),
phone: varchar("phone"),
email: varchar("email"),
notes: text("notes"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Orders
export const orders = pgTable("orders", {
id: serial("id").primaryKey(),
supplierId: integer("supplier_id").notNull(),
groupId: integer("group_id").notNull(),
plannedDate: date("planned_date").notNull(),
quantity: integer("quantity"), // Optional - will be set when delivery is linked
unit: varchar("unit"), // Optional - 'palettes' or 'colis'
status: varchar("status").notNull().default("pending"), // pending, planned, delivered
notes: text("notes"),
createdBy: varchar("created_by").notNull(),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Deliveries
export const deliveries = pgTable("deliveries", {
id: serial("id").primaryKey(),
orderId: integer("order_id"), // optional link to order
supplierId: integer("supplier_id").notNull(),
groupId: integer("group_id").notNull(),
scheduledDate: date("scheduled_date").notNull(),
deliveredDate: timestamp("delivered_date"),
quantity: integer("quantity").notNull(),
unit: varchar("unit").notNull(), // 'palettes' or 'colis'
status: varchar("status").notNull().default("planned"), // planned, delivered
notes: text("notes"),
// Champs pour le rapprochement BL/Factures
blNumber: varchar("bl_number"), // Numéro de Bon de Livraison
blAmount: decimal("bl_amount", { precision: 10, scale: 2 }), // Montant BL
invoiceReference: varchar("invoice_reference"), // Référence facture
invoiceAmount: decimal("invoice_amount", { precision: 10, scale: 2 }), // Montant facture HT
invoiceAmountTTC: decimal("invoice_amount_ttc", { precision: 10, scale: 2 }), // Montant facture TTC
dueDate: timestamp("due_date"), // Date d'échéance de paiement (récupérée depuis NocoDB)
reconciled: boolean("reconciled").default(false), // Rapprochement effectué
validatedAt: timestamp("validated_at"), // Date de validation de la livraison
// Champs pour le contrôle des livraisons (fournisseurs nécessitant un contrôle)
controlValidated: boolean("control_validated").default(false), // Contrôle effectué
controlValidatedBy: varchar("control_validated_by"), // Utilisateur ayant validé le contrôle
controlValidatedAt: timestamp("control_validated_at"), // Date de validation du contrôle
createdBy: varchar("created_by").notNull(),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Publicities
export const publicities = pgTable("publicities", {
id: serial("id").primaryKey(),
pubNumber: varchar("pub_number").notNull().unique(),
designation: text("designation").notNull(),
startDate: date("start_date").notNull(),
endDate: date("end_date").notNull(),
year: integer("year").notNull(),
createdBy: varchar("created_by").notNull(),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Publicity Participations
export const publicityParticipations = pgTable("publicity_participations", {
publicityId: integer("publicity_id").notNull(),
groupId: integer("group_id").notNull(),
}, (table) => ({
pk: primaryKey({ columns: [table.publicityId, table.groupId] })
}));
// Configuration NocoDB globale pour la vérification des factures
export const nocodbConfig = pgTable("nocodb_config", {
id: serial("id").primaryKey(),
name: varchar("name").notNull(),
baseUrl: varchar("base_url").notNull(),
projectId: varchar("project_id").notNull(),
apiToken: varchar("api_token").notNull(),
description: text("description"),
isActive: boolean("is_active").default(true),
createdBy: varchar("created_by"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Cache de vérification des factures
export const invoiceVerificationCache = pgTable("invoice_verification_cache", {
id: serial("id").primaryKey(),
cacheKey: varchar("cache_key", { length: 255 }).notNull().unique(),
groupId: integer("group_id").notNull(),
invoiceReference: varchar("invoice_reference", { length: 255 }).notNull(),
supplierName: varchar("supplier_name", { length: 255 }),
invoiceAmount: decimal("invoice_amount", { precision: 10, scale: 2 }), // Montant de la facture HT
invoiceAmountTTC: decimal("invoice_amount_ttc", { precision: 10, scale: 2 }), // Montant de la facture TTC
dueDate: timestamp("due_date"), // Date d'échéance
exists: boolean("exists").notNull(),
matchType: varchar("match_type", { length: 50 }).notNull(),
errorMessage: text("error_message"),
cacheHit: boolean("cache_hit").default(false),
apiCallTime: integer("api_call_time"),
isReconciled: boolean("is_reconciled").default(false), // Si true, cache permanent
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Commentaires de rapprochement - Système indépendant pour le module de rapprochement
export const reconciliationComments = pgTable("reconciliation_comments", {
id: serial("id").primaryKey(),
deliveryId: integer("delivery_id").notNull(), // Référence vers la livraison concernée
groupId: integer("group_id").notNull(), // Magasin/groupe pour filtrage
content: text("content").notNull(), // Contenu du commentaire
type: varchar("type").notNull().default("info"), // Type du commentaire: info, warning, error, success
authorId: varchar("author_id").notNull(), // Utilisateur auteur du commentaire
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Customer Orders (Commandes Client)
export const customerOrders = pgTable("customer_orders", {
id: serial("id").primaryKey(),
// Information de commande
orderTaker: varchar("order_taker").notNull(), // Qui a pris la commande
customerName: varchar("customer_name").notNull(), // Nom du client
customerPhone: varchar("customer_phone").notNull(), // N° de téléphone
customerEmail: varchar("customer_email"), // Email du client (optionnel)
// Information produit
productDesignation: text("product_designation").notNull(), // Désignation du produit
productReference: varchar("product_reference"), // Référence
gencode: varchar("gencode").notNull(), // Code à barres (obligatoire)
quantity: integer("quantity").notNull().default(1), // Quantité commandée
supplierId: integer("supplier_id").notNull(), // Fournisseur
// Statuts
status: varchar("status").notNull().default("En attente de Commande"), // Statut du produit
// Options financières
deposit: decimal("deposit", { precision: 10, scale: 2 }).default("0.00"), // Acompte
isPromotionalPrice: boolean("is_promotional_price").default(false), // Prix publicité
// Communication client
customerNotified: boolean("customer_notified").default(false), // Client appelé
notifiedAt: timestamp("notified_at"), // Date/heure du contact
notifiedComment: text("notified_comment"), // Commentaire lors de l'appel
// Notes additionnelles
notes: text("notes"), // Notes sur la commande
// Métadonnées
groupId: integer("group_id").notNull(), // Magasin
createdBy: varchar("created_by").notNull(), // Créateur
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// DLC Products (Date Limite de Consommation)
export const dlcProducts = pgTable("dlc_products", {
id: serial("id").primaryKey(),
name: varchar("name").notNull(), // Nom du produit (requis dans la base de données)
productName: varchar("product_name"), // Nom du produit (champ alternatif)
gencode: varchar("gencode"), // Code-barres/gencode EAN13 (optionnel)
supplierId: integer("supplier_id").notNull(), // ID du fournisseur
groupId: integer("group_id").notNull(), // ID du magasin/groupe
expiryDate: date("expiry_date").notNull(), // Date limite de consommation
dateType: varchar("date_type").notNull().default("dlc"), // Type: dlc, ddm, dluo
quantity: integer("quantity").notNull().default(1), // Quantité
unit: varchar("unit").notNull().default("unité"), // Unité (fixe par défaut)
location: varchar("location").notNull().default("Magasin"), // Emplacement (fixe par défaut)
alertThreshold: integer("alert_threshold").notNull().default(15), // Seuil d'alerte fixe à 15 jours
status: varchar("status").notNull().default("en_cours"), // Statut: en_cours, expires_soon, expires, valides
stockEpuise: boolean("stock_epuise").default(false), // Produit épuisé en stock (pas périmé)
stockEpuiseBy: varchar("stock_epuise_by"), // Marqué comme épuisé par (optionnel)
stockEpuiseAt: timestamp("stock_epuise_at"), // Date de marquage épuisé (optionnel)
processedAt: timestamp("processed_at"), // Date de traitement "expire bientôt" (optionnel)
processedBy: varchar("processed_by"), // Qui a marqué comme traité (optionnel)
processedUntilExpiry: boolean("processed_until_expiry").default(false), // Traité jusqu'à expiration
notes: text("notes"), // Notes (optionnel)
createdBy: varchar("created_by").notNull(), // Créateur
validatedBy: varchar("validated_by"), // Validé par (optionnel)
validatedAt: timestamp("validated_at"), // Date de validation (optionnel)
createdAt: timestamp("created_at").defaultNow(), // Date de création
updatedAt: timestamp("updated_at").defaultNow(), // Date de mise à jour
});
// Tasks table
export const tasks = pgTable("tasks", {
id: serial("id").primaryKey(),
title: varchar("title").notNull(), // Titre de la tâche (requis)
description: text("description"), // Description de la tâche (optionnel)
startDate: timestamp("start_date"), // Date de départ (optionnel) - aligné avec production
dueDate: date("due_date"), // Date d'échéance (optionnel)
priority: varchar("priority").notNull().default("medium"), // low, medium, high
status: varchar("status").notNull().default("pending"), // pending, completed
assignedTo: text("assigned_to").notNull(), // Utilisateur responsable (champ libre)
createdBy: varchar("created_by").notNull(), // Utilisateur créateur
groupId: integer("group_id").notNull(), // Magasin/groupe associé
completedAt: timestamp("completed_at"), // Date de completion
completedBy: varchar("completed_by"), // Utilisateur qui a complété la tâche
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Avoirs table
export const avoirs = pgTable("avoirs", {
id: serial("id").primaryKey(),
supplierId: integer("supplier_id").notNull(), // Fournisseur
groupId: integer("group_id").notNull(), // Magasin/groupe associé - OBLIGATOIRE pour filtrage
invoiceReference: varchar("invoice_reference", { length: 255 }), // Référence facture (OPTIONNEL)
amount: decimal("amount", { precision: 10, scale: 2 }), // Montant (OPTIONNEL)
comment: text("comment"), // Commentaire (optionnel)
commercialProcessed: boolean("commercial_processed").default(false), // Avoir fait par commercial
status: varchar("status", { length: 50 }).notNull().default("En attente de demande"), // En attente de demande, Demandé, Reçu
webhookSent: boolean("webhook_sent").default(false), // Webhook envoyé
nocodbVerified: boolean("nocodb_verified").default(false), // Vérifié dans NocoDB (coche verte)
nocodbVerifiedAt: timestamp("nocodb_verified_at"), // Date de vérification
processedAt: timestamp("processed_at"), // Date de passage au statut "Reçu"
createdBy: varchar("created_by").notNull(), // Utilisateur créateur
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Dashboard Messages - Utilise la table existante DASHBOARD_MESSAGES
export const dashboardMessages = pgTable("dashboard_messages", {
id: serial("id").primaryKey(),
title: varchar("title", { length: 255 }).notNull(), // Titre du message
content: text("content").notNull(), // Contenu du message
type: varchar("type", { length: 50 }).notNull().default("info"), // info, warning, error, success
storeId: integer("store_id"), // Filtrage par magasin (null = global)
createdBy: varchar("created_by", { length: 255 }).notNull(), // Référence vers l'utilisateur créateur
createdAt: timestamp("created_at").defaultNow(),
});
// Alias pour la compatibilité avec le code existant
export const announcements = dashboardMessages;
// Relations
export const usersRelations = relations(users, ({ many }) => ({
userGroups: many(userGroups),
createdOrders: many(orders),
createdDeliveries: many(deliveries),
createdPublicities: many(publicities),
createdCustomerOrders: many(customerOrders),
createdDlcProducts: many(dlcProducts),
createdTasks: many(tasks),
assignedTasks: many(tasks),
createdAnnouncements: many(dashboardMessages),
createdAvoirs: many(avoirs),
createdReconciliationComments: many(reconciliationComments),
}));
export const groupsRelations = relations(groups, ({ one, many }) => ({
userGroups: many(userGroups),
orders: many(orders),
deliveries: many(deliveries),
publicityParticipations: many(publicityParticipations),
customerOrders: many(customerOrders),
dlcProducts: many(dlcProducts),
tasks: many(tasks),
announcements: many(dashboardMessages),
avoirs: many(avoirs),
reconciliationComments: many(reconciliationComments),
}));
export const userGroupsRelations = relations(userGroups, ({ one }) => ({
user: one(users, {
fields: [userGroups.userId],
references: [users.id],
}),
group: one(groups, {
fields: [userGroups.groupId],
references: [groups.id],
}),
}));
export const suppliersRelations = relations(suppliers, ({ many }) => ({
orders: many(orders),
deliveries: many(deliveries),
dlcProducts: many(dlcProducts),
avoirs: many(avoirs),
}));
export const ordersRelations = relations(orders, ({ one, many }) => ({
supplier: one(suppliers, {
fields: [orders.supplierId],
references: [suppliers.id],
}),
group: one(groups, {
fields: [orders.groupId],
references: [groups.id],
}),
creator: one(users, {
fields: [orders.createdBy],
references: [users.id],
}),
deliveries: many(deliveries),
}));
export const deliveriesRelations = relations(deliveries, ({ one, many }) => ({
order: one(orders, {
fields: [deliveries.orderId],
references: [orders.id],
}),
supplier: one(suppliers, {
fields: [deliveries.supplierId],
references: [suppliers.id],
}),
group: one(groups, {
fields: [deliveries.groupId],
references: [groups.id],
}),
creator: one(users, {
fields: [deliveries.createdBy],
references: [users.id],
}),
reconciliationComments: many(reconciliationComments),
}));
export const publicitiesRelations = relations(publicities, ({ one, many }) => ({
creator: one(users, {
fields: [publicities.createdBy],
references: [users.id],
}),
participations: many(publicityParticipations),
}));
export const publicityParticipationsRelations = relations(publicityParticipations, ({ one }) => ({
publicity: one(publicities, {
fields: [publicityParticipations.publicityId],
references: [publicities.id],
}),
group: one(groups, {
fields: [publicityParticipations.groupId],
references: [groups.id],
}),
}));
export const customerOrdersRelations = relations(customerOrders, ({ one }) => ({
group: one(groups, {
fields: [customerOrders.groupId],
references: [groups.id],
}),
creator: one(users, {
fields: [customerOrders.createdBy],
references: [users.id],
}),
supplier: one(suppliers, {
fields: [customerOrders.supplierId],
references: [suppliers.id],
}),
}));
export const dlcProductsRelations = relations(dlcProducts, ({ one }) => ({
supplier: one(suppliers, {
fields: [dlcProducts.supplierId],
references: [suppliers.id],
}),
group: one(groups, {
fields: [dlcProducts.groupId],
references: [groups.id],
}),
creator: one(users, {
fields: [dlcProducts.createdBy],
references: [users.id],
}),
validator: one(users, {
fields: [dlcProducts.validatedBy],
references: [users.id],
}),
}));
export const tasksRelations = relations(tasks, ({ one }) => ({
creator: one(users, {
fields: [tasks.createdBy],
references: [users.id],
}),
group: one(groups, {
fields: [tasks.groupId],
references: [groups.id],
}),
}));
export const avoirsRelations = relations(avoirs, ({ one }) => ({
supplier: one(suppliers, {
fields: [avoirs.supplierId],
references: [suppliers.id],
}),
group: one(groups, {
fields: [avoirs.groupId],
references: [groups.id],
}),
creator: one(users, {
fields: [avoirs.createdBy],
references: [users.id],
}),
}));
export const dashboardMessagesRelations = relations(dashboardMessages, ({ one }) => ({
author: one(users, {
fields: [dashboardMessages.createdBy],
references: [users.id],
}),
group: one(groups, {
fields: [dashboardMessages.storeId],
references: [groups.id],
}),
}));
// Alias pour compatibilité
export const announcementsRelations = dashboardMessagesRelations;
// Relations pour les commentaires de rapprochement
export const reconciliationCommentsRelations = relations(reconciliationComments, ({ one }) => ({
delivery: one(deliveries, {
fields: [reconciliationComments.deliveryId],
references: [deliveries.id],
}),
group: one(groups, {
fields: [reconciliationComments.groupId],
references: [groups.id],
}),
author: one(users, {
fields: [reconciliationComments.authorId],
references: [users.id],
}),
}));
// Zod schemas
export const insertUserSchema = createInsertSchema(users).pick({
id: true,
username: true,
email: true,
firstName: true,
lastName: true,
name: true,
profileImageUrl: true,
password: true,
role: true,
passwordChanged: true,
}).extend({
email: z.union([z.string().email(), z.literal("")]).optional(),
});
export const insertGroupSchema = createInsertSchema(groups).omit({
id: true,
createdAt: true,
updatedAt: true,
}).extend({
// Champs optionnels : le formulaire envoie "" quand ils ne sont pas renseignés
address: z.string().optional().nullable(),
phone: z.string().optional().nullable(),
logo: z.string().optional().nullable(),
smtpHost: z.string().optional().nullable(),
smtpPort: z.coerce.number().int().min(1).max(65535).optional().nullable(),
smtpUser: z.string().optional().nullable(),
smtpPassword: z.string().optional().nullable(),
smtpSenderEmail: z.union([z.string().email(), z.literal("")]).optional().nullable(),
smtpSenderName: z.string().optional().nullable(),
});
export const insertSupplierSchema = createInsertSchema(suppliers).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertContactSchema = createInsertSchema(contacts).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertOrderSchema = createInsertSchema(orders).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertDeliverySchema = createInsertSchema(deliveries).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertUserGroupSchema = createInsertSchema(userGroups).omit({
createdAt: true,
});
export const insertPublicitySchema = createInsertSchema(publicities).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertPublicityParticipationSchema = createInsertSchema(publicityParticipations);
export const insertCustomerOrderSchema = createInsertSchema(customerOrders).omit({
id: true,
createdAt: true,
updatedAt: true,
});
// Frontend-compatible schema with different field names
export const insertCustomerOrderFrontendSchema = z.object({
customerName: z.string().min(1, "Customer name is required"),
contactNumber: z.string().min(1, "Contact number is required"), // Maps to customerPhone
productName: z.string().min(1, "Product name is required"), // Maps to productDesignation
productDescription: z.string().optional(), // Optional description
quantity: z.coerce.number().int().positive().default(1),
groupId: z.coerce.number().int().positive(),
isPickup: z.boolean().default(false),
notes: z.string().optional(),
// Optional fields with defaults
orderTaker: z.string().optional(), // Will be set to creator name if not provided
gencode: z.string().optional().default(""), // Will be empty if not provided
supplierId: z.coerce.number().int().positive().optional().default(1), // Default supplier ID
deposit: z.coerce.number().optional().default(0),
isPromotionalPrice: z.boolean().default(false),
customerEmail: z.string().email().optional().or(z.literal("")),
productReference: z.string().optional(),
});
export const insertDlcProductSchema = createInsertSchema(dlcProducts).omit({
id: true,
createdAt: true,
updatedAt: true,
});
// Frontend-compatible schema with dlcDate instead of expiryDate
export const insertDlcProductFrontendSchema = insertDlcProductSchema
.omit({ expiryDate: true })
.extend({ dlcDate: z.coerce.date() });
export const insertTaskSchema = createInsertSchema(tasks).omit({
id: true,
createdAt: true,
updatedAt: true,
completedAt: true,
completedBy: true,
}).extend({
startDate: z.coerce.date().optional().nullable(), // Date de départ (optionnel)
dueDate: z.coerce.date().optional().nullable(), // Date d'échéance (optionnel)
});
export const insertAvoirSchema = createInsertSchema(avoirs).omit({
id: true,
createdAt: true,
updatedAt: true,
webhookSent: true,
nocodbVerified: true,
nocodbVerifiedAt: true,
processedAt: true,
}).extend({
status: z.enum(["En attente de demande", "Demandé", "Reçu"]).default("En attente de demande"),
amount: z.coerce.number().optional().nullable(), // Montant optionnel, peut être vide, zéro, positif ou négatif
invoiceReference: z.string().optional(), // Référence facture optionnelle
comment: z.string().optional(),
commercialProcessed: z.boolean().default(false),
});
export const insertDashboardMessageSchema = createInsertSchema(dashboardMessages).omit({
id: true,
createdAt: true,
});
export const insertAnnouncementSchema = insertDashboardMessageSchema;
export const insertReconciliationCommentSchema = createInsertSchema(reconciliationComments).omit({
id: true,
createdAt: true,
updatedAt: true,
}).extend({
type: z.enum(["info", "warning", "error", "success"]).default("info"),
});
export const insertNocodbConfigSchema = createInsertSchema(nocodbConfig).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertInvoiceVerificationCacheSchema = createInsertSchema(invoiceVerificationCache).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const databaseBackups = pgTable("database_backups", {
id: varchar("id", { length: 255 }).primaryKey(),
filename: varchar("filename", { length: 255 }).notNull(),
description: text("description"),
size: bigint("size", { mode: "number" }).default(0),
createdAt: timestamp("created_at").defaultNow(),
createdBy: varchar("created_by", { length: 255 }).notNull(),
tablesCount: integer("tables_count").default(0),
status: varchar("status", { length: 50 }).default("creating"),
backupType: varchar("backup_type", { length: 10 }).default("manual"),
});
export const insertDatabaseBackupSchema = createInsertSchema(databaseBackups);
// Utilities - Configuration générale de l'application
export const utilities = pgTable("utilities", {
id: serial("id").primaryKey(),
salesAnalysisUrl: varchar("sales_analysis_url", { length: 500 }), // URL pour l'analyse des ventes
automaticBackupsEnabled: boolean("automatic_backups_enabled").default(true), // Activer/désactiver les backups automatiques
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
export const insertUtilitiesSchema = createInsertSchema(utilities).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export type InsertNocodbConfig = z.infer<typeof insertNocodbConfigSchema>;
export type InsertInvoiceVerificationCache = z.infer<typeof insertInvoiceVerificationCacheSchema>;
export type DatabaseBackup = typeof databaseBackups.$inferSelect;
export type InsertDatabaseBackup = z.infer<typeof insertDatabaseBackupSchema>;
export type Utilities = typeof utilities.$inferSelect;
export type InsertUtilities = z.infer<typeof insertUtilitiesSchema>;
// Types
export type User = typeof users.$inferSelect;
export type UpsertUser = z.infer<typeof insertUserSchema>;
export type Group = typeof groups.$inferSelect;
export type InsertGroup = z.infer<typeof insertGroupSchema>;
export type Supplier = typeof suppliers.$inferSelect;
export type InsertSupplier = z.infer<typeof insertSupplierSchema>;
export type Contact = typeof contacts.$inferSelect;
export type InsertContact = z.infer<typeof insertContactSchema>;
export type Order = typeof orders.$inferSelect;
export type InsertOrder = z.infer<typeof insertOrderSchema>;
export type Delivery = typeof deliveries.$inferSelect;
export type InsertDelivery = z.infer<typeof insertDeliverySchema>;
export type UserGroup = typeof userGroups.$inferSelect;
export type InsertUserGroup = z.infer<typeof insertUserGroupSchema>;
export type Publicity = typeof publicities.$inferSelect;
export type InsertPublicity = z.infer<typeof insertPublicitySchema>;
export type PublicityParticipation = typeof publicityParticipations.$inferSelect;
export type InsertPublicityParticipation = z.infer<typeof insertPublicityParticipationSchema>;
export type CustomerOrder = typeof customerOrders.$inferSelect;
export type InsertCustomerOrder = z.infer<typeof insertCustomerOrderSchema>;
export type DlcProduct = typeof dlcProducts.$inferSelect;
export type InsertDlcProduct = z.infer<typeof insertDlcProductSchema>;
export type DlcProductFrontend = Omit<DlcProduct, 'expiryDate'> & { dlcDate: Date };
export type InsertDlcProductFrontend = z.infer<typeof insertDlcProductFrontendSchema>;
export type Task = typeof tasks.$inferSelect;
export type InsertTask = z.infer<typeof insertTaskSchema>;
export type Avoir = typeof avoirs.$inferSelect;
export type InsertAvoir = z.infer<typeof insertAvoirSchema>;
export type DashboardMessage = typeof dashboardMessages.$inferSelect;
export type InsertDashboardMessage = z.infer<typeof insertDashboardMessageSchema>;
export type ReconciliationComment = typeof reconciliationComments.$inferSelect;
export type InsertReconciliationComment = z.infer<typeof insertReconciliationCommentSchema>;
// Alias pour compatibilité
export type Announcement = DashboardMessage;
export type InsertAnnouncement = InsertDashboardMessage;
// Complex types with relations
export type OrderWithRelations = Order & {
supplier: Supplier;
group: Group;
creator: User;
deliveries?: DeliveryWithRelations[];
};
// Comptages commandes/livraisons par magasin ou par fournisseur, agrégés en base.
// Évite de rapatrier tout l'historique côté client pour n'en compter que les lignes.
export type EntityActivityStats = {
id: number;
orders: number;
deliveries: number;
delivered: number;
};
export type DeliveryWithRelations = Delivery & {
supplier: Supplier;
group: Group;
creator: User;
order?: OrderWithRelations;
reconciliationCommentsCount?: number;
};
export type PublicityWithRelations = Publicity & {
creator: User;
participations: (PublicityParticipation & { group: Group })[];
};
export type CustomerOrderWithRelations = CustomerOrder & {
supplier: Supplier;
group: Group;
creator: User;
};
export type DlcProductWithRelations = DlcProduct & {
supplier: Supplier;
group: Group;
creator: User;
validator?: User;
};
export type UserWithGroups = User & {
userGroups: (UserGroup & { group: Group })[];
};
export type NocodbConfig = typeof nocodbConfig.$inferSelect;
export type InvoiceVerificationCache = typeof invoiceVerificationCache.$inferSelect;
// Task with relations type
export type TaskWithRelations = Task & {
creator?: User;
group?: Group;
isFutureTask?: boolean; // Pour distinguer les tâches futures (admin/directeur)
};
export type AvoirWithRelations = Avoir & {
supplier: Supplier;
group: Group;
creator: User;
};
// Dashboard Message with relations type
export type DashboardMessageWithRelations = DashboardMessage & {
author: User;
group?: Group;
};
// Alias pour compatibilité
export type AnnouncementWithRelations = DashboardMessageWithRelations;
// Reconciliation Comment with relations type
export type ReconciliationCommentWithRelations = ReconciliationComment & {
delivery: DeliveryWithRelations;
group: Group;
author: User;
};
// SAV (Service Après-Vente) Tables
export const savTickets = pgTable("sav_tickets", {
id: serial("id").primaryKey(),
ticketNumber: varchar("ticket_number", { length: 50 }).notNull(),
supplierId: integer("supplier_id").notNull(),
groupId: integer("group_id").notNull(),
productGencode: varchar("product_gencode", { length: 255 }).notNull(),
productReference: varchar("product_reference", { length: 255 }),
productDesignation: varchar("product_designation", { length: 500 }).notNull(),
problemType: varchar("problem_type", { length: 100 }).notNull(),
problemDescription: text("problem_description").notNull(),
resolutionDescription: text("resolution_description"),
status: varchar("status", { length: 50 }).notNull().default("nouveau"),
priority: varchar("priority", { length: 50 }).notNull().default("normale"), // normale, haute, critique
clientName: varchar("client_name", { length: 255 }),
clientPhone: varchar("client_phone", { length: 255 }),
createdBy: varchar("created_by", { length: 255 }).notNull(),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
resolvedAt: timestamp("resolved_at"),
closedAt: timestamp("closed_at"),
});
// SAV Relations
export const savTicketRelations = relations(savTickets, ({ one, many }) => ({
supplier: one(suppliers, {
fields: [savTickets.supplierId],
references: [suppliers.id],
}),
group: one(groups, {
fields: [savTickets.groupId],
references: [groups.id],
}),
creator: one(users, {
fields: [savTickets.createdBy],
references: [users.id],
}),
}));
// SAV Zod Schemas
export const insertSavTicketSchema = createInsertSchema(savTickets).omit({
id: true,
ticketNumber: true, // Auto-generated server-side
createdAt: true,
updatedAt: true,
resolvedAt: true,
closedAt: true,
}).extend({
priority: z.enum(["faible", "normale", "haute", "critique"]).default("normale"),
status: z.enum(["nouveau", "en_cours", "attente_pieces", "attente_echange", "resolu", "ferme"]).default("nouveau"),
problemType: z.enum(["defectueux", "pieces_manquantes", "non_conforme", "autre"]).default("defectueux"),
});
// export const insertSavTicketHistorySchema = createInsertSchema(savTicketHistory).omit({
// id: true,
// createdAt: true,
// });
// SAV Types
export type SavTicket = typeof savTickets.$inferSelect;
export type InsertSavTicket = z.infer<typeof insertSavTicketSchema>;
// export type SavTicketHistory = typeof savTicketHistory.$inferSelect;
// export type InsertSavTicketHistory = z.infer<typeof insertSavTicketHistorySchema>;
export type InsertSavTicketHistory = any;
export type SavTicketHistory = any;
export type SavTicketWithRelations = SavTicket & {
supplier: Supplier;
group: Group;
creator: User;
history: any[];
};
export type SavTicketHistoryWithCreator = any;
// Weather Tables
export const weatherData = pgTable("weather_data", {
id: serial("id").primaryKey(),
date: date("date").notNull(),
location: text("location").notNull(),
tempMax: text("temp_max").notNull(),
tempMin: text("temp_min").notNull(),
icon: text("icon").notNull(),
conditions: text("conditions").notNull(),
isCurrentYear: boolean("is_current_year").notNull().default(true),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
export const weatherSettings = pgTable("weather_settings", {
id: serial("id").primaryKey(),
apiKey: text("api_key").notNull(),
location: text("location").notNull().default("Paris, France"),
isActive: boolean("is_active").notNull().default(true),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Weather Schemas
export const insertWeatherDataSchema = createInsertSchema(weatherData).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertWeatherSettingsSchema = createInsertSchema(weatherSettings).omit({
id: true,
createdAt: true,
updatedAt: true,
});
// Weather Types
export type WeatherData = typeof weatherData.$inferSelect;
export type InsertWeatherData = z.infer<typeof insertWeatherDataSchema>;
export type WeatherSettings = typeof weatherSettings.$inferSelect;
export type InsertWeatherSettings = z.infer<typeof insertWeatherSettingsSchema>;
// Configuration Webhook BAP
// Historique des mails de relance envoyés aux fournisseurs (rapprochement)
export const supplierMailLogs = pgTable("supplier_mail_logs", {
id: serial("id").primaryKey(),
deliveryId: integer("delivery_id").notNull(),
groupId: integer("group_id").notNull(),
supplierId: integer("supplier_id"),
supplierName: varchar("supplier_name", { length: 255 }), // figé au moment de l'envoi
sentTo: varchar("sent_to", { length: 255 }).notNull(), // adresse destinataire
subject: text("subject"),
status: varchar("status", { length: 20 }).notNull(), // 'sent' | 'failed'
errorMessage: text("error_message"),
messageId: varchar("message_id", { length: 255 }),
sentBy: varchar("sent_by").notNull(), // id utilisateur
sentByName: varchar("sent_by_name", { length: 255 }), // nom lisible, figé
createdAt: timestamp("created_at").defaultNow(),
});
export const webhookBapConfig = pgTable("webhook_bap_config", {
id: serial("id").primaryKey(),
name: varchar("name", { length: 100 }).notNull().default("Configuration BAP"),
webhookUrl: text("webhook_url").notNull(),
description: text("description"),
isActive: boolean("is_active").notNull().default(true),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Webhook BAP Schemas
export const insertWebhookBapConfigSchema = createInsertSchema(webhookBapConfig).omit({
id: true,
createdAt: true,
updatedAt: true,
});
// Webhook BAP Types
export type WebhookBapConfig = typeof webhookBapConfig.$inferSelect;
export type InsertWebhookBapConfig = z.infer<typeof insertWebhookBapConfigSchema>;
// Supplier mail logs
export const insertSupplierMailLogSchema = createInsertSchema(supplierMailLogs).omit({
id: true,
createdAt: true,
});
export type SupplierMailLog = typeof supplierMailLogs.$inferSelect;
export type InsertSupplierMailLog = z.infer<typeof insertSupplierMailLogSchema>;