From e75fa65945eb6db00f44e6988a00bc2e098d464f Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Mon, 8 Sep 2025 13:36:45 +0000 Subject: [PATCH 1/2] Add a system for managing customer credit notes and their associated permissions Define new 'avoir' module for permissions and create a new 'avoirs' table in the database schema with detailed fields for credit note management, including supplier, group, invoice details, status, and processing flags. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869/0C3uBiZ --- shared/permissions.ts | 11 +++++++- shared/schema.ts | 61 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/shared/permissions.ts b/shared/permissions.ts index c978306..6d96070 100644 --- a/shared/permissions.ts +++ b/shared/permissions.ts @@ -12,7 +12,8 @@ export type Module = | 'publicity' | 'customer-orders' | 'dlc' - | 'tasks'; + | 'tasks' + | 'avoir'; // Définition des permissions par module et par rôle const PERMISSIONS: Record> = { @@ -86,6 +87,14 @@ const PERMISSIONS: Record> = { directeur: ['view', 'create', 'edit', 'delete', 'validate'], manager: ['view', 'create', 'edit', 'validate'], employee: ['view'] + }, + + // Avoirs - Admin/Directeur tout, Manager create + view seulement, Employé pas d'accès + avoir: { + admin: ['view', 'create', 'edit', 'delete', 'validate'], + directeur: ['view', 'create', 'edit', 'delete', 'validate'], + manager: ['view', 'create'], + employee: [] } }; diff --git a/shared/schema.ts b/shared/schema.ts index c3c6c2f..fd4de86 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -262,6 +262,25 @@ export const tasks = pgTable("tasks", { 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 }).notNull(), // Référence facture + amount: decimal("amount", { precision: 10, scale: 2 }).notNull(), // Montant + 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(), @@ -287,6 +306,7 @@ export const usersRelations = relations(users, ({ many }) => ({ createdTasks: many(tasks), assignedTasks: many(tasks), createdAnnouncements: many(dashboardMessages), + createdAvoirs: many(avoirs), })); export const groupsRelations = relations(groups, ({ one, many }) => ({ @@ -298,6 +318,7 @@ export const groupsRelations = relations(groups, ({ one, many }) => ({ dlcProducts: many(dlcProducts), tasks: many(tasks), announcements: many(dashboardMessages), + avoirs: many(avoirs), })); export const userGroupsRelations = relations(userGroups, ({ one }) => ({ @@ -315,6 +336,7 @@ 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 }) => ({ @@ -418,6 +440,21 @@ export const tasksRelations = relations(tasks, ({ one }) => ({ }), })); +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], @@ -538,6 +575,22 @@ export const insertTaskSchema = createInsertSchema(tasks).omit({ 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().positive(), // Montant doit être positif + invoiceReference: z.string().min(1, "La référence facture est obligatoire"), + comment: z.string().optional(), + commercialProcessed: z.boolean().default(false), +}); + export const insertDashboardMessageSchema = createInsertSchema(dashboardMessages).omit({ id: true, createdAt: true, @@ -601,6 +654,8 @@ export type DlcProductFrontend = Omit & { dlcDate: Dat export type InsertDlcProductFrontend = z.infer; export type Task = typeof tasks.$inferSelect; export type InsertTask = z.infer; +export type Avoir = typeof avoirs.$inferSelect; +export type InsertAvoir = z.infer; export type DashboardMessage = typeof dashboardMessages.$inferSelect; export type InsertDashboardMessage = z.infer; @@ -656,6 +711,12 @@ export type TaskWithRelations = Task & { 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; From f3565df01f18a5cd1e8a988f9b8bf7737c778810 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Mon, 8 Sep 2025 13:48:14 +0000 Subject: [PATCH 2/2] Add functionality to manage and track customer credit notes Introduces the "Avoirs" feature, including database schema, API endpoints for CRUD operations, and frontend components for viewing and managing credit notes. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869/0C3uBiZ --- client/src/components/RouterProduction.tsx | 2 + client/src/components/Sidebar.tsx | 9 +- client/src/pages/Avoirs.tsx | 530 +++++++++++++++++++++ server/routes.ts | 253 +++++++++- server/storage.ts | 228 ++++++++- sql/create_avoirs_table.sql | 50 ++ 6 files changed, 1069 insertions(+), 3 deletions(-) create mode 100644 client/src/pages/Avoirs.tsx create mode 100644 sql/create_avoirs_table.sql diff --git a/client/src/components/RouterProduction.tsx b/client/src/components/RouterProduction.tsx index 16c6445..3081448 100644 --- a/client/src/components/RouterProduction.tsx +++ b/client/src/components/RouterProduction.tsx @@ -21,6 +21,7 @@ import BackupManager from "@/pages/BackupManager"; import Utilities from "@/pages/Utilities"; import Tasks from "@/pages/Tasks"; import SavTickets from "@/pages/SavTickets"; +import Avoirs from "@/pages/Avoirs"; import WeatherSettings from "@/pages/WeatherSettings"; import Layout from "@/components/Layout"; @@ -96,6 +97,7 @@ function RouterProduction() { + {/* Routes de compatibilité - redirection vers utilities */} diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 46f64ea..eb20bfc 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -32,7 +32,8 @@ import { Settings, Wrench, FileUp, - Send + Send, + Receipt } from "lucide-react"; export default function Sidebar() { @@ -284,6 +285,12 @@ export default function Sidebar() { icon: Wrench, roles: ["admin", "directeur", "manager", "employee"] }, + { + path: "/avoirs", + label: "Avoirs", + icon: Receipt, + roles: ["admin", "directeur", "manager"] + }, ]; const adminItems = [ diff --git a/client/src/pages/Avoirs.tsx b/client/src/pages/Avoirs.tsx new file mode 100644 index 0000000..17d3718 --- /dev/null +++ b/client/src/pages/Avoirs.tsx @@ -0,0 +1,530 @@ +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Plus, Search, FileText, CheckCircle, AlertCircle, Clock } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { format } from "date-fns"; +import { fr } from "date-fns/locale"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest } from "@/lib/queryClient"; + +// Types +interface Avoir { + id: number; + supplierId: number; + groupId: number; + invoiceReference: string; + amount: number; + comment?: string; + commercialProcessed: boolean; + status: 'En attente de demande' | 'Demandé' | 'Reçu'; + webhookSent: boolean; + nocodbVerified: boolean; + nocodbVerifiedAt?: Date; + processedAt?: Date; + createdBy: string; + createdAt: Date; + updatedAt: Date; + supplier: { + id: number; + name: string; + contact?: string; + }; + group: { + id: number; + name: string; + color: string; + }; + creator: { + id: string; + firstName?: string; + lastName?: string; + username: string; + }; +} + +interface Supplier { + id: number; + name: string; +} + +interface Group { + id: number; + name: string; + color: string; +} + +// Schema for form validation +const avoirSchema = z.object({ + supplierId: z.number().min(1, "Veuillez sélectionner un fournisseur"), + groupId: z.number().min(1, "Veuillez sélectionner un magasin"), + invoiceReference: z.string().min(1, "La référence facture est requise"), + amount: z.number().min(0.01, "Le montant doit être supérieur à 0"), + comment: z.string().optional(), + commercialProcessed: z.boolean().optional(), +}); + +type AvoirFormData = z.infer; + +export default function Avoirs() { + const [searchTerm, setSearchTerm] = useState(""); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [selectedStoreId, setSelectedStoreId] = useState("all"); + const { toast } = useToast(); + const queryClient = useQueryClient(); + + // Fetch user profile + const { data: user } = useQuery({ + queryKey: ['/api/user'], + }); + + // Fetch groups for store filter + const { data: groups = [] } = useQuery({ + queryKey: ['/api/groups'], + enabled: !!user, + }); + + // Fetch suppliers + const { data: suppliers = [] } = useQuery({ + queryKey: ['/api/suppliers'], + enabled: !!user, + }); + + // Fetch avoirs + const { data: avoirs = [], isLoading } = useQuery({ + queryKey: ['/api/avoirs', selectedStoreId], + queryFn: () => apiRequest(`/api/avoirs${selectedStoreId !== 'all' ? `?storeId=${selectedStoreId}` : ''}`), + enabled: !!user, + }); + + // Create avoir mutation + const createAvoirMutation = useMutation({ + mutationFn: async (data: AvoirFormData) => { + const response = await fetch('/api/avoirs', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + credentials: 'include', + }); + if (!response.ok) { + throw new Error('Failed to create avoir'); + } + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/avoirs'] }); + setIsCreateDialogOpen(false); + toast({ + title: "Avoir créé", + description: "L'avoir a été créé avec succès.", + }); + }, + onError: (error: any) => { + toast({ + title: "Erreur", + description: error.message || "Erreur lors de la création de l'avoir", + variant: "destructive", + }); + } + }); + + // Initialize form + const form = useForm({ + resolver: zodResolver(avoirSchema), + defaultValues: { + supplierId: 0, + groupId: 0, + invoiceReference: "", + amount: 0, + comment: "", + commercialProcessed: false, + }, + }); + + // Handle form submission + const onSubmit = (data: AvoirFormData) => { + createAvoirMutation.mutate(data); + }; + + // Filter avoirs based on search term + const filteredAvoirs = avoirs.filter(avoir => + avoir.invoiceReference.toLowerCase().includes(searchTerm.toLowerCase()) || + avoir.supplier.name.toLowerCase().includes(searchTerm.toLowerCase()) || + avoir.comment?.toLowerCase().includes(searchTerm.toLowerCase()) || + (avoir.creator.firstName && avoir.creator.lastName + ? `${avoir.creator.firstName} ${avoir.creator.lastName}`.toLowerCase().includes(searchTerm.toLowerCase()) + : avoir.creator.username.toLowerCase().includes(searchTerm.toLowerCase()) + ) + ); + + // Status badge variant + const getStatusVariant = (status: string) => { + switch (status) { + case 'Reçu': + return 'default'; + case 'Demandé': + return 'secondary'; + case 'En attente de demande': + return 'outline'; + default: + return 'outline'; + } + }; + + // Status icon + const getStatusIcon = (status: string) => { + switch (status) { + case 'Reçu': + return ; + case 'Demandé': + return ; + case 'En attente de demande': + return ; + default: + return ; + } + }; + + if (!user) { + return
Chargement...
; + } + + return ( +
+
+
+

Avoirs

+

+ Gestion des demandes d'avoirs et de remboursements +

+
+ + + + + + + + Créer un nouvel avoir + + Remplissez les informations pour créer une nouvelle demande d'avoir. + + + +
+ + ( + + Fournisseur + + + + )} + /> + + ( + + Magasin + + + + )} + /> + + ( + + Référence facture + + + + + + )} + /> + + ( + + Montant (€) + + field.onChange(parseFloat(e.target.value) || 0)} + /> + + + + )} + /> + + ( + + Commentaire + +