mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-21 05:58:23 +02:00
Add quality control process for supplier deliveries
Introduce a new 'requires_control' flag for suppliers, enabling a quality control workflow for deliveries. This includes UI updates, database schema changes, and API endpoints to manage and track control validation. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 1e4433b5-47ab-464c-b663-fea2e53367dd Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/1e4433b5-47ab-464c-b663-fea2e53367dd/e7P7sOc
This commit is contained in:
1 parent
b643ea5fa8
commit
a2ab38a412
7 files changed
+97
-6
No files matched your search
@@ -39,6 +39,10 @@ externalPort = 5000
|
|||||||
localPort = 41343
|
localPort = 41343
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 42551
|
||||||
|
externalPort = 8000
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 43315
|
localPort = 43315
|
||||||
externalPort = 6000
|
externalPort = 6000
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
import { isUnauthorizedError } from "@/lib/authUtils";
|
import { isUnauthorizedError } from "@/lib/authUtils";
|
||||||
import { Check, X } from "lucide-react";
|
import { Check, X, AlertTriangle } from "lucide-react";
|
||||||
|
|
||||||
const validateDeliverySchema = z.object({
|
const validateDeliverySchema = z.object({
|
||||||
blNumber: z.string().min(1, "Le numéro de bon de livraison est obligatoire"),
|
blNumber: z.string().min(1, "Le numéro de bon de livraison est obligatoire"),
|
||||||
@@ -110,6 +110,21 @@ export default function ValidateDeliveryModal({
|
|||||||
</p>
|
</p>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Alerte si le fournisseur nécessite un contrôle */}
|
||||||
|
{delivery.supplier?.requiresControl && (
|
||||||
|
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3 flex items-start space-x-3">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-orange-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-sm font-semibold text-orange-800">
|
||||||
|
⚠️ Contrôle à effectuer sur cette livraison
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-orange-700 mt-1">
|
||||||
|
Ce fournisseur nécessite un contrôle qualité lors de la réception de la livraison.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
{/* Informations de la livraison */}
|
{/* Informations de la livraison */}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export default function Suppliers() {
|
|||||||
phone: "",
|
phone: "",
|
||||||
hasDlc: false,
|
hasDlc: false,
|
||||||
automaticReconciliation: false,
|
automaticReconciliation: false,
|
||||||
|
requiresControl: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: suppliers = [], isLoading } = useQuery<Supplier[]>({
|
const { data: suppliers = [], isLoading } = useQuery<Supplier[]>({
|
||||||
@@ -191,6 +192,7 @@ export default function Suppliers() {
|
|||||||
phone: "",
|
phone: "",
|
||||||
hasDlc: false,
|
hasDlc: false,
|
||||||
automaticReconciliation: false,
|
automaticReconciliation: false,
|
||||||
|
requiresControl: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -227,6 +229,7 @@ export default function Suppliers() {
|
|||||||
phone: supplier.phone || "",
|
phone: supplier.phone || "",
|
||||||
hasDlc: supplier.hasDlc || false,
|
hasDlc: supplier.hasDlc || false,
|
||||||
automaticReconciliation: supplier.automaticReconciliation || false,
|
automaticReconciliation: supplier.automaticReconciliation || false,
|
||||||
|
requiresControl: supplier.requiresControl || false,
|
||||||
});
|
});
|
||||||
setShowEditModal(true);
|
setShowEditModal(true);
|
||||||
};
|
};
|
||||||
@@ -410,10 +413,16 @@ export default function Suppliers() {
|
|||||||
<span className="text-xs font-medium">Auto-rapprochement</span>
|
<span className="text-xs font-medium">Auto-rapprochement</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{supplier.requiresControl && (
|
||||||
|
<div className="flex items-center text-orange-600 bg-orange-50 px-2 py-1 rounded-full">
|
||||||
|
<CheckCircle className="w-3 h-3 mr-1" />
|
||||||
|
<span className="text-xs font-medium">Contrôle requis</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!supplier.hasDlc && !supplier.automaticReconciliation && (
|
{!supplier.hasDlc && !supplier.automaticReconciliation && !supplier.requiresControl && (
|
||||||
<div className="text-xs text-gray-500 mt-2">
|
<div className="text-xs text-gray-500 mt-2">
|
||||||
Aucune option activée
|
Aucune option activée
|
||||||
</div>
|
</div>
|
||||||
@@ -524,6 +533,18 @@ export default function Suppliers() {
|
|||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="requiresControl"
|
||||||
|
checked={formData.requiresControl}
|
||||||
|
onCheckedChange={(checked) => handleChange('requiresControl', checked === true)}
|
||||||
|
data-testid="checkbox-requires-control"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="requiresControl" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||||
|
Fournisseur à contrôler
|
||||||
|
</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"
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ CREATE TABLE "deliveries" (
|
|||||||
"scheduled_date" date NOT NULL,
|
"scheduled_date" date NOT NULL,
|
||||||
"notes" text,
|
"notes" text,
|
||||||
"validated_at" timestamp,
|
"validated_at" timestamp,
|
||||||
|
"control_validated" boolean DEFAULT false,
|
||||||
|
"control_validated_by" varchar,
|
||||||
|
"control_validated_at" timestamp,
|
||||||
CONSTRAINT "deliveries_status_check_fixed" CHECK ((status)::text = ANY ((ARRAY['planned'::character varying, 'delivered'::character varying])::text[]))
|
CONSTRAINT "deliveries_status_check_fixed" CHECK ((status)::text = ANY ((ARRAY['planned'::character varying, 'delivered'::character varying])::text[]))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -66,7 +69,9 @@ CREATE TABLE "suppliers" (
|
|||||||
"phone" varchar,
|
"phone" varchar,
|
||||||
"created_at" timestamp DEFAULT now(),
|
"created_at" timestamp DEFAULT now(),
|
||||||
"updated_at" timestamp DEFAULT now(),
|
"updated_at" timestamp DEFAULT now(),
|
||||||
"has_dlc" boolean DEFAULT false
|
"has_dlc" boolean DEFAULT false,
|
||||||
|
"automatic_reconciliation" boolean DEFAULT false,
|
||||||
|
"requires_control" boolean DEFAULT false
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE "user_groups" (
|
CREATE TABLE "user_groups" (
|
||||||
|
|||||||
@@ -1655,6 +1655,40 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Marquer le contrôle d'une livraison comme effectué
|
||||||
|
app.put('/api/deliveries/:id/control', isAuthenticated, async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ message: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return res.status(400).json({ message: "Invalid delivery ID" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que la livraison existe
|
||||||
|
const delivery = await storage.getDeliveryById(id);
|
||||||
|
if (!delivery) {
|
||||||
|
return res.status(404).json({ message: "Delivery not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier les permissions
|
||||||
|
if (!hasPermission(user.role, 'deliveries', 'edit')) {
|
||||||
|
return res.status(403).json({ message: "Insufficient permissions" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour le contrôle
|
||||||
|
await storage.markDeliveryControlValidated(id, user.id);
|
||||||
|
|
||||||
|
res.json({ message: "Delivery control validated successfully" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error validating delivery control:", error);
|
||||||
|
res.status(500).json({ message: "Failed to validate delivery control" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Route pour diagnostiquer le cache des livraisons
|
// Route pour diagnostiquer le cache des livraisons
|
||||||
app.get('/api/cache/diagnosis', isAuthenticated, async (req: any, res) => {
|
app.get('/api/cache/diagnosis', isAuthenticated, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1165,6 +1165,15 @@ export class DatabaseStorage implements IStorage {
|
|||||||
await db.update(deliveries).set(updateData).where(eq(deliveries.id, id));
|
await db.update(deliveries).set(updateData).where(eq(deliveries.id, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async markDeliveryControlValidated(id: number, userId: string): Promise<void> {
|
||||||
|
await db.update(deliveries).set({
|
||||||
|
controlValidated: true,
|
||||||
|
controlValidatedBy: userId,
|
||||||
|
controlValidatedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}).where(eq(deliveries.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
// User-Group operations
|
// User-Group operations
|
||||||
async getUserGroups(userId: string): Promise<UserGroup[]> {
|
async getUserGroups(userId: string): Promise<UserGroup[]> {
|
||||||
return await db.select().from(userGroups).where(eq(userGroups.userId, userId));
|
return await db.select().from(userGroups).where(eq(userGroups.userId, userId));
|
||||||
|
|||||||
+6
-3
@@ -82,6 +82,7 @@ export const suppliers = pgTable("suppliers", {
|
|||||||
createdAt: timestamp("created_at").defaultNow(),
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
automaticReconciliation: boolean("automatic_reconciliation").default(false), // Rapprochement automatique BL/Factures
|
automaticReconciliation: boolean("automatic_reconciliation").default(false), // Rapprochement automatique BL/Factures
|
||||||
|
requiresControl: boolean("requires_control").default(false), // Fournisseur nécessite un contrôle à la livraison
|
||||||
});
|
});
|
||||||
|
|
||||||
// Orders
|
// Orders
|
||||||
@@ -118,6 +119,10 @@ export const deliveries = pgTable("deliveries", {
|
|||||||
invoiceAmount: decimal("invoice_amount", { precision: 10, scale: 2 }), // Montant facture
|
invoiceAmount: decimal("invoice_amount", { precision: 10, scale: 2 }), // Montant facture
|
||||||
reconciled: boolean("reconciled").default(false), // Rapprochement effectué
|
reconciled: boolean("reconciled").default(false), // Rapprochement effectué
|
||||||
validatedAt: timestamp("validated_at"), // Date de validation de la livraison
|
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(),
|
createdBy: varchar("created_by").notNull(),
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
@@ -553,9 +558,7 @@ export const insertPublicitySchema = createInsertSchema(publicities).omit({
|
|||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const insertPublicityParticipationSchema = createInsertSchema(publicityParticipations).omit({
|
export const insertPublicityParticipationSchema = createInsertSchema(publicityParticipations);
|
||||||
createdAt: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user