mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +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,
|
||||
Truck,
|
||||
CheckCircle,
|
||||
XCircle
|
||||
XCircle,
|
||||
Clock
|
||||
} from "lucide-react";
|
||||
import type { Supplier } from "@shared/schema";
|
||||
|
||||
@@ -37,7 +38,10 @@ export default function Suppliers() {
|
||||
name: "",
|
||||
contact: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
hasDlc: false,
|
||||
automaticReconciliation: false,
|
||||
});
|
||||
|
||||
const { data: suppliers = [], isLoading } = useQuery<Supplier[]>({
|
||||
@@ -66,7 +70,15 @@ export default function Suppliers() {
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/suppliers'] });
|
||||
setShowCreateModal(false);
|
||||
setFormData({ name: "", contact: "", phone: "", hasDlc: false });
|
||||
setFormData({
|
||||
name: "",
|
||||
contact: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
hasDlc: false,
|
||||
automaticReconciliation: false
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isUnauthorizedError(error)) {
|
||||
@@ -137,7 +149,15 @@ export default function Suppliers() {
|
||||
|
||||
setShowEditModal(false);
|
||||
setSelectedSupplier(null);
|
||||
setFormData({ name: "", contact: "", phone: "", hasDlc: false });
|
||||
setFormData({
|
||||
name: "",
|
||||
contact: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
hasDlc: false,
|
||||
automaticReconciliation: false
|
||||
});
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
// Rollback optimiste en cas d'erreur
|
||||
@@ -214,7 +234,15 @@ export default function Suppliers() {
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setFormData({ name: "", contact: "", phone: "", hasDlc: false });
|
||||
setFormData({
|
||||
name: "",
|
||||
contact: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
hasDlc: false,
|
||||
automaticReconciliation: false
|
||||
});
|
||||
setShowCreateModal(true);
|
||||
};
|
||||
|
||||
@@ -224,7 +252,10 @@ export default function Suppliers() {
|
||||
name: supplier.name,
|
||||
contact: supplier.contact || "",
|
||||
phone: supplier.phone || "",
|
||||
email: supplier.email || "",
|
||||
address: supplier.address || "",
|
||||
hasDlc: supplier.hasDlc || false,
|
||||
automaticReconciliation: supplier.automaticReconciliation || false,
|
||||
});
|
||||
setShowEditModal(true);
|
||||
};
|
||||
@@ -400,19 +431,30 @@ export default function Suppliers() {
|
||||
{supplier.phone}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center text-sm">
|
||||
{supplier.hasDlc ? (
|
||||
<div className="flex items-center text-green-600">
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
DLC activé
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center text-gray-400">
|
||||
<XCircle className="w-4 h-4 mr-2" />
|
||||
DLC désactivé
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
{supplier.hasDlc && (
|
||||
<div className="flex items-center text-green-600 bg-green-50 px-2 py-1 rounded-full">
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
<span className="text-xs font-medium">DLC</span>
|
||||
</div>
|
||||
)}
|
||||
{supplier.automaticReconciliation && (
|
||||
<div className="flex items-center text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
<span className="text-xs font-medium">Auto-rapprochement</span>
|
||||
</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 className="border-t pt-4">
|
||||
@@ -453,7 +495,15 @@ export default function Suppliers() {
|
||||
setShowCreateModal(false);
|
||||
setShowEditModal(false);
|
||||
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">
|
||||
<DialogHeader>
|
||||
@@ -497,6 +547,27 @@ export default function Suppliers() {
|
||||
/>
|
||||
</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">
|
||||
<Checkbox
|
||||
id="hasDlc"
|
||||
@@ -508,6 +579,17 @@ export default function Suppliers() {
|
||||
</Label>
|
||||
</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">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -516,7 +598,15 @@ export default function Suppliers() {
|
||||
setShowCreateModal(false);
|
||||
setShowEditModal(false);
|
||||
setSelectedSupplier(null);
|
||||
setFormData({ name: "", contact: "", phone: "", hasDlc: false });
|
||||
setFormData({
|
||||
name: "",
|
||||
contact: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
hasDlc: false,
|
||||
automaticReconciliation: false
|
||||
});
|
||||
}}
|
||||
>
|
||||
Annuler
|
||||
|
||||
@@ -30,6 +30,7 @@ Preferred communication style: Simple, everyday language.
|
||||
|
||||
### Core Business Entities
|
||||
- **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
|
||||
- **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.
|
||||
- **Universal Pagination**: Reusable client-side pagination component across all main tabular data pages.
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
@@ -702,6 +702,31 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
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);
|
||||
} catch (error) {
|
||||
|
||||
@@ -470,6 +470,11 @@ export class DatabaseStorage implements IStorage {
|
||||
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> {
|
||||
const [newSupplier] = await db.insert(suppliers).values(supplier).returning();
|
||||
return newSupplier;
|
||||
|
||||
@@ -74,7 +74,10 @@ export const suppliers = pgTable("suppliers", {
|
||||
name: varchar("name").notNull(),
|
||||
contact: varchar("contact"),
|
||||
phone: varchar("phone"),
|
||||
email: varchar("email"),
|
||||
address: text("address"),
|
||||
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(),
|
||||
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
|
||||
});
|
||||
|
||||
// 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
|
||||
export type UpsertUser = z.infer<typeof insertUserSchema>;
|
||||
export type User = typeof users.$inferSelect;
|
||||
|
||||
Reference in new issue
Block a user