mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-19 21:18:25 +02:00
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
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user