feat: add mobile customer orders page with order creation and status update functionality

This commit is contained in:
Michael committed 2025-12-10 16:12:57 +01:00
1 parent 676d8cee96
commit 4a25c2040b
3 files changed
+165 -50

No files matched your search

+43 -2
View File
@@ -58,6 +58,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox";
// Schema for form validation // Schema for form validation
const customerOrderFormSchema = z.object({ const customerOrderFormSchema = z.object({
@@ -177,8 +178,8 @@ export default function MobileCustomerOrdersPage() {
productDesignation: data.productName, productDesignation: data.productName,
status: "En attente de Commande", status: "En attente de Commande",
groupId, groupId,
isPromotionalPrice: false, isPromotionalPrice: data.isPromotionalPrice,
customerNotified: false customerNotified: data.customerNotified
}; };
createMutation.mutate(submitData); createMutation.mutate(submitData);
@@ -422,6 +423,46 @@ export default function MobileCustomerOrdersPage() {
/> />
</div> </div>
<FormField
control={form.control}
name="isPromotionalPrice"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
Prix promotionnel / Pub
</FormLabel>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="customerNotified"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
Client déjà notifié
</FormLabel>
</div>
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="notes" name="notes"
+17 -1
View File
@@ -56,6 +56,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
const dlcFormSchema = z.object({ const dlcFormSchema = z.object({
@@ -65,6 +66,7 @@ const dlcFormSchema = z.object({
dateType: z.enum(["dlc", "ddm", "dluo"]).default("dlc"), dateType: z.enum(["dlc", "ddm", "dluo"]).default("dlc"),
supplierId: z.coerce.number().min(1, "Fournisseur requis"), supplierId: z.coerce.number().min(1, "Fournisseur requis"),
status: z.string().default("en_cours"), status: z.string().default("en_cours"),
notes: z.string().optional(),
}); });
export default function MobileDlcPage() { export default function MobileDlcPage() {
@@ -132,7 +134,8 @@ export default function MobileDlcPage() {
dlcDate: format(new Date(), 'yyyy-MM-dd'), dlcDate: format(new Date(), 'yyyy-MM-dd'),
dateType: "dlc", dateType: "dlc",
supplierId: 1, supplierId: 1,
status: "en_cours" status: "en_cours",
notes: ""
} }
}); });
@@ -389,6 +392,19 @@ export default function MobileDlcPage() {
)} )}
/> />
<FormField
control={form.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Notes internes</FormLabel>
<FormControl>
<Textarea placeholder="Remarques..." className="h-20" {...field} />
</FormControl>
</FormItem>
)}
/>
<Button type="submit" className="w-full bg-orange-600 hover:bg-orange-700 h-12 text-lg font-bold shadow-lg mt-4" disabled={createMutation.isPending}> <Button type="submit" className="w-full bg-orange-600 hover:bg-orange-700 h-12 text-lg font-bold shadow-lg mt-4" disabled={createMutation.isPending}>
{createMutation.isPending ? "Ajout..." : "Ajouter le produit"} {createMutation.isPending ? "Ajout..." : "Ajouter le produit"}
</Button> </Button>
+105 -47
View File
@@ -61,11 +61,15 @@ import {
// Schema // Schema
const savFormSchema = z.object({ const savFormSchema = z.object({
customerName: z.string().min(1, "Nom client requis"), clientName: z.string().optional(),
customerPhone: z.string().optional(), clientPhone: z.string().optional(),
productName: z.string().min(1, "Produit requis"), productGencode: z.string().min(1, "Gencode requis"),
issueDescription: z.string().min(1, "Description requise"), productReference: z.string().optional(),
priority: z.enum(["low", "medium", "high", "critical"]).default("medium"), productDesignation: z.string().min(1, "Désignation requise"),
supplierId: z.coerce.number().min(1, "Fournisseur requis"),
problemType: z.enum(["defectueux", "pieces_manquantes", "non_conforme", "autre"]).default("defectueux"),
problemDescription: z.string().min(1, "Description requise"),
priority: z.enum(["faible", "normale", "haute", "critique"]).default("normale"),
}); });
export default function MobileSavPage() { export default function MobileSavPage() {
@@ -88,6 +92,11 @@ export default function MobileSavPage() {
enabled: !!selectedStoreId && !!user, enabled: !!selectedStoreId && !!user,
}); });
const { data: suppliers = [] } = useQuery({
queryKey: ['/api/suppliers'],
queryFn: () => apiRequest('/api/suppliers')
});
const { data: groups = [] } = useQuery({ const { data: groups = [] } = useQuery({
queryKey: ['/api/groups'], queryKey: ['/api/groups'],
queryFn: () => apiRequest('/api/groups') queryFn: () => apiRequest('/api/groups')
@@ -118,11 +127,15 @@ export default function MobileSavPage() {
const form = useForm({ const form = useForm({
resolver: zodResolver(savFormSchema), resolver: zodResolver(savFormSchema),
defaultValues: { defaultValues: {
customerName: "", clientName: "",
customerPhone: "", clientPhone: "",
productName: "", productGencode: "",
issueDescription: "", productReference: "",
priority: "medium" productDesignation: "",
supplierId: 0,
problemType: "defectueux",
problemDescription: "",
priority: "normale"
} }
}); });
@@ -139,23 +152,23 @@ export default function MobileSavPage() {
createMutation.mutate({ createMutation.mutate({
...data, ...data,
groupId, groupId,
status: "open" status: "nouveau"
}); });
}; };
const filteredTickets = tickets.filter((t: any) => const filteredTickets = tickets.filter((t: any) =>
t.customerName.toLowerCase().includes(searchTerm.toLowerCase()) || t.clientName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
t.productName.toLowerCase().includes(searchTerm.toLowerCase()) || t.productDesignation?.toLowerCase().includes(searchTerm.toLowerCase()) ||
t.ticketNumber?.toLowerCase().includes(searchTerm.toLowerCase()) t.ticketNumber?.toLowerCase().includes(searchTerm.toLowerCase())
); );
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
switch (status) { switch (status) {
case "open": return <Badge className="bg-blue-100 text-blue-800 border-blue-200">Ouvert</Badge>; case "nouveau": return <Badge className="bg-blue-100 text-blue-800 border-blue-200">Nouveau</Badge>;
case "in_progress": return <Badge className="bg-yellow-100 text-yellow-800 border-yellow-200">En cours</Badge>; case "en_cours": return <Badge className="bg-yellow-100 text-yellow-800 border-yellow-200">En cours</Badge>;
case "waiting_parts": return <Badge className="bg-orange-100 text-orange-800 border-orange-200">Pièces</Badge>; case "attente_pieces": return <Badge className="bg-orange-100 text-orange-800 border-orange-200">Pièces</Badge>;
case "resolved": return <Badge className="bg-green-100 text-green-800 border-green-200">Résolu</Badge>; case "resolu": return <Badge className="bg-green-100 text-green-800 border-green-200">Résolu</Badge>;
case "closed": return <Badge className="bg-gray-100 text-gray-800 border-gray-200">Fermé</Badge>; case "ferme": return <Badge className="bg-gray-100 text-gray-800 border-gray-200">Fermé</Badge>;
default: return <Badge variant="outline">{status}</Badge>; default: return <Badge variant="outline">{status}</Badge>;
} }
}; };
@@ -203,8 +216,8 @@ export default function MobileSavPage() {
#{ticket.ticketNumber} • {format(new Date(ticket.createdAt), "dd/MM", { locale: fr })} #{ticket.ticketNumber} • {format(new Date(ticket.createdAt), "dd/MM", { locale: fr })}
</span> </span>
</div> </div>
<h3 className="font-bold text-gray-900">{ticket.customerName}</h3> <h3 className="font-bold text-gray-900">{ticket.clientName || 'Client inconnu'}</h3>
<p className="text-sm text-gray-600 font-medium">{ticket.productName}</p> <p className="text-sm text-gray-600 font-medium">{ticket.productDesignation}</p>
</div> </div>
<DropdownMenu> <DropdownMenu>
@@ -231,12 +244,12 @@ export default function MobileSavPage() {
</div> </div>
<div className="bg-gray-50 p-2 rounded text-xs text-gray-700 line-clamp-2 mb-3"> <div className="bg-gray-50 p-2 rounded text-xs text-gray-700 line-clamp-2 mb-3">
{ticket.issueDescription} {ticket.problemDescription}
</div> </div>
{ticket.customerPhone && ( {ticket.clientPhone && (
<div className="flex mt-2"> <div className="flex mt-2">
<a href={`tel:${ticket.customerPhone}`} className="flex items-center gap-2 text-sm text-blue-600 font-medium bg-blue-50 px-3 py-1.5 rounded-full"> <a href={`tel:${ticket.clientPhone}`} className="flex items-center gap-2 text-sm text-blue-600 font-medium bg-blue-50 px-3 py-1.5 rounded-full">
<Phone className="h-3.5 w-3.5" /> <Phone className="h-3.5 w-3.5" />
Appeler Appeler
</a> </a>
@@ -262,41 +275,66 @@ export default function MobileSavPage() {
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 pb-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 pb-8">
<div className="bg-gray-50 p-3 rounded-lg space-y-3"> <div className="bg-gray-50 p-3 rounded-lg space-y-3">
<h3 className="font-medium text-sm text-gray-500 uppercase">Client</h3> <h3 className="font-medium text-sm text-gray-500 uppercase">Produit</h3>
<FormField <FormField
control={form.control} control={form.control}
name="customerName" name="productGencode"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Nom</FormLabel> <FormLabel>Gencode (EAN13)</FormLabel>
<FormControl><Input {...field} /></FormControl> <FormControl><Input placeholder="Scanner..." {...field} /></FormControl>
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="customerPhone" name="productDesignation"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Téléphone</FormLabel> <FormLabel>Désignation</FormLabel>
<FormControl><Input type="tel" {...field} /></FormControl> <FormControl><Input placeholder="Nom du produit" {...field} /></FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="supplierId"
render={({ field }) => (
<FormItem>
<FormLabel>Fournisseur</FormLabel>
<Select onValueChange={(val) => field.onChange(parseInt(val))} defaultValue={field.value?.toString()}>
<FormControl><SelectTrigger><SelectValue placeholder="Choisir..." /></SelectTrigger></FormControl>
<SelectContent>
{suppliers.map((s: any) => (
<SelectItem key={s.id} value={s.id.toString()}>{s.name}</SelectItem>
))}
</SelectContent>
</Select>
</FormItem>
)}
/>
<FormField
control={form.control}
name="problemType"
render={({ field }) => (
<FormItem>
<FormLabel>Type de problème</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl><SelectTrigger><SelectValue /></SelectTrigger></FormControl>
<SelectContent>
<SelectItem value="defectueux">Défectueux</SelectItem>
<SelectItem value="pieces_manquantes">Pièces manquantes</SelectItem>
<SelectItem value="non_conforme">Non conforme</SelectItem>
<SelectItem value="autre">Autre</SelectItem>
</SelectContent>
</Select>
</FormItem> </FormItem>
)} )}
/> />
</div> </div>
<div className="bg-gray-50 p-3 rounded-lg space-y-3"> <div className="bg-gray-50 p-3 rounded-lg space-y-3">
<h3 className="font-medium text-sm text-gray-500 uppercase">Problème</h3> <h3 className="font-medium text-sm text-gray-500 uppercase">Description & Client</h3>
<FormField
control={form.control}
name="productName"
render={({ field }) => (
<FormItem>
<FormLabel>Produit</FormLabel>
<FormControl><Input placeholder="Marque, Modèle..." {...field} /></FormControl>
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="priority" name="priority"
@@ -306,10 +344,10 @@ export default function MobileSavPage() {
<Select onValueChange={field.onChange} defaultValue={field.value}> <Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl><SelectTrigger><SelectValue /></SelectTrigger></FormControl> <FormControl><SelectTrigger><SelectValue /></SelectTrigger></FormControl>
<SelectContent> <SelectContent>
<SelectItem value="low">Faible</SelectItem> <SelectItem value="faible">Faible</SelectItem>
<SelectItem value="medium">Moyenne</SelectItem> <SelectItem value="normale">Normale</SelectItem>
<SelectItem value="high">Haute</SelectItem> <SelectItem value="haute">Haute</SelectItem>
<SelectItem value="critical">Critique</SelectItem> <SelectItem value="critique">Critique</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</FormItem> </FormItem>
@@ -317,11 +355,31 @@ export default function MobileSavPage() {
/> />
<FormField <FormField
control={form.control} control={form.control}
name="issueDescription" name="problemDescription"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Description Panne</FormLabel> <FormLabel>Description Panne</FormLabel>
<FormControl><Textarea {...field} className="h-24" /></FormControl> <FormControl><Textarea {...field} className="h-20" /></FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientName"
render={({ field }) => (
<FormItem>
<FormLabel>Nom Client</FormLabel>
<FormControl><Input {...field} /></FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientPhone"
render={({ field }) => (
<FormItem>
<FormLabel>Téléphone</FormLabel>
<FormControl><Input type="tel" {...field} /></FormControl>
</FormItem> </FormItem>
)} )}
/> />