Merge pull request #3 from R0m1k3/1.1

1.1
This commit is contained in:
LogiFlow authored and GitHub committed 2025-08-12 12:18:26 +02:00
commit dce2e6296b
7 files changed
+1611 -954

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+108 -18
View File
@@ -20,7 +20,8 @@ import {
Package, Package,
Truck, Truck,
CheckCircle, CheckCircle,
XCircle XCircle,
Clock
} from "lucide-react"; } from "lucide-react";
import type { Supplier } from "@shared/schema"; import type { Supplier } from "@shared/schema";
@@ -37,7 +38,10 @@ export default function Suppliers() {
name: "", name: "",
contact: "", contact: "",
phone: "", phone: "",
email: "",
address: "",
hasDlc: false, hasDlc: false,
automaticReconciliation: false,
}); });
const { data: suppliers = [], isLoading } = useQuery<Supplier[]>({ const { data: suppliers = [], isLoading } = useQuery<Supplier[]>({
@@ -66,7 +70,15 @@ export default function Suppliers() {
}); });
queryClient.invalidateQueries({ queryKey: ['/api/suppliers'] }); queryClient.invalidateQueries({ queryKey: ['/api/suppliers'] });
setShowCreateModal(false); setShowCreateModal(false);
setFormData({ name: "", contact: "", phone: "", hasDlc: false }); setFormData({
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
}, },
onError: (error) => { onError: (error) => {
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
@@ -137,7 +149,15 @@ export default function Suppliers() {
setShowEditModal(false); setShowEditModal(false);
setSelectedSupplier(null); setSelectedSupplier(null);
setFormData({ name: "", contact: "", phone: "", hasDlc: false }); setFormData({
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
}, },
onError: (error, variables, context) => { onError: (error, variables, context) => {
// Rollback optimiste en cas d'erreur // Rollback optimiste en cas d'erreur
@@ -214,7 +234,15 @@ export default function Suppliers() {
}; };
const handleCreate = () => { const handleCreate = () => {
setFormData({ name: "", contact: "", phone: "", hasDlc: false }); setFormData({
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
setShowCreateModal(true); setShowCreateModal(true);
}; };
@@ -224,7 +252,10 @@ export default function Suppliers() {
name: supplier.name, name: supplier.name,
contact: supplier.contact || "", contact: supplier.contact || "",
phone: supplier.phone || "", phone: supplier.phone || "",
email: supplier.email || "",
address: supplier.address || "",
hasDlc: supplier.hasDlc || false, hasDlc: supplier.hasDlc || false,
automaticReconciliation: supplier.automaticReconciliation || false,
}); });
setShowEditModal(true); setShowEditModal(true);
}; };
@@ -400,19 +431,30 @@ export default function Suppliers() {
{supplier.phone} {supplier.phone}
</div> </div>
)} )}
<div className="flex items-center text-sm"> <div className="flex items-center justify-between text-sm">
{supplier.hasDlc ? ( <div className="flex items-center space-x-2">
<div className="flex items-center text-green-600"> {supplier.hasDlc && (
<CheckCircle className="w-4 h-4 mr-2" /> <div className="flex items-center text-green-600 bg-green-50 px-2 py-1 rounded-full">
DLC activé <Clock className="w-3 h-3 mr-1" />
</div> <span className="text-xs font-medium">DLC</span>
) : ( </div>
<div className="flex items-center text-gray-400"> )}
<XCircle className="w-4 h-4 mr-2" /> {supplier.automaticReconciliation && (
DLC désactivé <div className="flex items-center text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
</div> <CheckCircle className="w-3 h-3 mr-1" />
)} <span className="text-xs font-medium">Auto-rapprochement</span>
</div>
)}
</div>
</div> </div>
{!supplier.hasDlc && !supplier.automaticReconciliation && (
<div className="text-xs text-gray-500 mt-2">
Aucune option activée
</div>
)}
{/* Statistics section follows */}
</div> </div>
<div className="border-t pt-4"> <div className="border-t pt-4">
@@ -453,7 +495,15 @@ export default function Suppliers() {
setShowCreateModal(false); setShowCreateModal(false);
setShowEditModal(false); setShowEditModal(false);
setSelectedSupplier(null); setSelectedSupplier(null);
setFormData({ name: "", contact: "", phone: "", hasDlc: false }); setFormData({
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
}}> }}>
<DialogContent className="sm:max-w-md" aria-describedby="supplier-modal-description"> <DialogContent className="sm:max-w-md" aria-describedby="supplier-modal-description">
<DialogHeader> <DialogHeader>
@@ -497,6 +547,27 @@ export default function Suppliers() {
/> />
</div> </div>
<div>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
placeholder="Adresse email"
/>
</div>
<div>
<Label htmlFor="address">Adresse</Label>
<Input
id="address"
value={formData.address}
onChange={(e) => handleChange('address', e.target.value)}
placeholder="Adresse complète"
/>
</div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
id="hasDlc" id="hasDlc"
@@ -508,6 +579,17 @@ export default function Suppliers() {
</Label> </Label>
</div> </div>
<div className="flex items-center space-x-2">
<Checkbox
id="automaticReconciliation"
checked={formData.automaticReconciliation}
onCheckedChange={(checked) => handleChange('automaticReconciliation', checked === true)}
/>
<Label htmlFor="automaticReconciliation" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
Rapprochement automatique BL/Factures
</Label>
</div>
<div className="flex items-center space-x-3 pt-4"> <div className="flex items-center space-x-3 pt-4">
<Button <Button
type="button" type="button"
@@ -516,7 +598,15 @@ export default function Suppliers() {
setShowCreateModal(false); setShowCreateModal(false);
setShowEditModal(false); setShowEditModal(false);
setSelectedSupplier(null); setSelectedSupplier(null);
setFormData({ name: "", contact: "", phone: "", hasDlc: false }); setFormData({
name: "",
contact: "",
phone: "",
email: "",
address: "",
hasDlc: false,
automaticReconciliation: false
});
}} }}
> >
Annuler Annuler
+2
View File
@@ -30,6 +30,7 @@ Preferred communication style: Simple, everyday language.
### Core Business Entities ### Core Business Entities
- **Management**: Orders, Deliveries, Customer Orders, Suppliers, Publicities, DLC Products (limited shelf life), Tasks. - **Management**: Orders, Deliveries, Customer Orders, Suppliers, Publicities, DLC Products (limited shelf life), Tasks.
- **Automatic Reconciliation**: Dual-mode BL/Invoice reconciliation system with automatic validation for designated suppliers.
### Data Flow ### Data Flow
- **Request Flow**: Client requests via React Query, processed by Express middleware for authentication/security, then by route handlers using Drizzle ORM for database operations. - **Request Flow**: Client requests via React Query, processed by Express middleware for authentication/security, then by route handlers using Drizzle ORM for database operations.
@@ -41,6 +42,7 @@ Preferred communication style: Simple, everyday language.
- **Multi-Store Management**: Global store context, role-based access control (admin, manager, employee, directeur), and data isolation. - **Multi-Store Management**: Global store context, role-based access control (admin, manager, employee, directeur), and data isolation.
- **Universal Pagination**: Reusable client-side pagination component across all main tabular data pages. - **Universal Pagination**: Reusable client-side pagination component across all main tabular data pages.
- **Reconciliation Module**: For balancing and tracking financial discrepancies. - **Reconciliation Module**: For balancing and tracking financial discrepancies.
- **Automatic Reconciliation System**: Dual-mode BL/Invoice reconciliation with automatic validation for suppliers in automatic mode. Auto-validates deliveries when status = "delivered" AND BL number exists.
- **Permission System**: Granular, hardcoded permissions (54 permissions across 12 categories, assigned to 4 roles) for improved performance and maintenance. - **Permission System**: Granular, hardcoded permissions (54 permissions across 12 categories, assigned to 4 roles) for improved performance and maintenance.
- **User Management**: Comprehensive features including user deletion with ownership transfer, consistent name and email field handling, and robust password hashing. - **User Management**: Comprehensive features including user deletion with ownership transfer, consistent name and email field handling, and robust password hashing.
- **Calendar Synchronization**: Proper display of delivery dates and automatic synchronization of order statuses. - **Calendar Synchronization**: Proper display of delivery dates and automatic synchronization of order statuses.
+25
View File
@@ -702,6 +702,31 @@ export async function registerRoutes(app: Express): Promise<Server> {
console.error(`❌ Auto-sync failed for order #${updatedDelivery.orderId}:`, error); console.error(`❌ Auto-sync failed for order #${updatedDelivery.orderId}:`, error);
} }
} }
// AUTO-VALIDATION RAPPROCHEMENT AUTOMATIQUE : Si fournisseur en mode automatique, livraison delivered + BL → auto-valider
if (data.status === 'delivered' || data.blNumber) {
try {
// Récupérer le fournisseur pour vérifier le mode automatique
const supplier = await storage.getSupplier(updatedDelivery.supplierId);
if (supplier?.automaticReconciliation &&
updatedDelivery.status === 'delivered' &&
updatedDelivery.blNumber) {
console.log(`🤖 Auto-reconciliation: Delivery #${id} from automatic supplier ${supplier.name}, auto-validating...`);
// Auto-valider le rapprochement
await storage.updateDelivery(id, {
reconciled: true,
validatedAt: new Date()
});
console.log(`✅ Auto-reconciliation: Delivery #${id} automatically validated for supplier ${supplier.name}`);
}
} catch (error) {
console.error(`❌ Auto-reconciliation failed for delivery #${id}:`, error);
}
}
res.json(updatedDelivery); res.json(updatedDelivery);
} catch (error) { } catch (error) {
+5
View File
@@ -470,6 +470,11 @@ export class DatabaseStorage implements IStorage {
return await db.select().from(suppliers).orderBy(suppliers.name); return await db.select().from(suppliers).orderBy(suppliers.name);
} }
async getSupplier(id: number): Promise<Supplier | undefined> {
const [supplier] = await db.select().from(suppliers).where(eq(suppliers.id, id));
return supplier;
}
async createSupplier(supplier: InsertSupplier): Promise<Supplier> { async createSupplier(supplier: InsertSupplier): Promise<Supplier> {
const [newSupplier] = await db.insert(suppliers).values(supplier).returning(); const [newSupplier] = await db.insert(suppliers).values(supplier).returning();
return newSupplier; return newSupplier;
+67
View File
@@ -74,7 +74,10 @@ export const suppliers = pgTable("suppliers", {
name: varchar("name").notNull(), name: varchar("name").notNull(),
contact: varchar("contact"), contact: varchar("contact"),
phone: varchar("phone"), phone: varchar("phone"),
email: varchar("email"),
address: text("address"),
hasDlc: boolean("has_dlc").default(false), // Coche DLC pour la gestion DLC hasDlc: boolean("has_dlc").default(false), // Coche DLC pour la gestion DLC
automaticReconciliation: boolean("automatic_reconciliation").default(false), // Rapprochement automatique BL/Factures
createdAt: timestamp("created_at").defaultNow(), createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(), updatedAt: timestamp("updated_at").defaultNow(),
}); });
@@ -470,6 +473,70 @@ export const insertTaskSchema = createInsertSchema(tasks).omit({
dueDate: z.coerce.date().optional().nullable(), // Convertit automatiquement les chaînes en Date dueDate: z.coerce.date().optional().nullable(), // Convertit automatiquement les chaînes en Date
}); });
// 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 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 NocodbConfig = typeof nocodbConfig.$inferSelect;
export type InsertNocodbConfig = z.infer<typeof insertNocodbConfigSchema>;
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>;
// Complex types with relations
export type UserWithGroups = User & {
groups: Group[];
};
export type OrderWithRelations = Order & {
supplier: Supplier;
group: Group;
creator: User;
};
export type DeliveryWithRelations = Delivery & {
supplier: Supplier;
group: Group;
creator: User;
order?: Order;
};
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;
};
// Types // Types
export type UpsertUser = z.infer<typeof insertUserSchema>; export type UpsertUser = z.infer<typeof insertUserSchema>;
export type User = typeof users.$inferSelect; export type User = typeof users.$inferSelect;