|
|
|
import { useState } from "react";
|
|
import { Plus, Search, Filter, ArrowUpDown, AlertTriangle, TrendingUp, TrendingDown } from "lucide-react";
|
|
import AppLayout from "@/components/layout/AppLayout";
|
|
import Header from "@/components/shared/Header";
|
|
import StockMenu from "@/components/stock/StockMenu";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
|
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
import { toast } from "sonner";
|
|
import { ThemeToggle } from "@/components/theme/ThemeToggle";
|
|
|
|
|
|
interface StockItem {
|
|
id: number;
|
|
name: string;
|
|
sku: string;
|
|
quantity: number;
|
|
price: number;
|
|
category: string;
|
|
lastUpdated: string;
|
|
trend?: 'up' | 'down' | 'stable';
|
|
demand?: 'high' | 'medium' | 'low';
|
|
reorderPoint?: number;
|
|
}
|
|
|
|
|
|
const initialStock: StockItem[] = [
|
|
{ id: 1, name: "Riz (25kg)", sku: "RIZ-25", quantity: 15, price: 12000, category: "Alimentation", lastUpdated: "2023-07-15", trend: 'up', demand: 'high', reorderPoint: 10 },
|
|
{ id: 2, name: "Lait en poudre (carton)", sku: "LAIT-C", quantity: 8, price: 15000, category: "Alimentation", lastUpdated: "2023-07-10", trend: 'stable', demand: 'medium', reorderPoint: 5 },
|
|
{ id: 3, name: "Huile de palme (bidon 5L)", sku: "HUILE-5L", quantity: 23, price: 4500, category: "Alimentation", lastUpdated: "2023-07-12", trend: 'down', demand: 'medium', reorderPoint: 15 },
|
|
{ id: 4, name: "Sucre (sac 50kg)", sku: "SUCRE-50", quantity: 5, price: 22000, category: "Alimentation", lastUpdated: "2023-07-18", trend: 'up', demand: 'high', reorderPoint: 3 },
|
|
{ id: 5, name: "Savon (carton 24)", sku: "SAVON-24", quantity: 12, price: 7500, category: "Hygiène", lastUpdated: "2023-07-14", trend: 'stable', demand: 'medium', reorderPoint: 8 },
|
|
{ id: 6, name: "Farine de blé (sac 25kg)", sku: "FARINE-25", quantity: 7, price: 11000, category: "Alimentation", lastUpdated: "2023-07-16", trend: 'down', demand: 'low', reorderPoint: 5 },
|
|
];
|
|
|
|
const stockFormSchema = z.object({
|
|
name: z.string().min(1, "Le nom est requis"),
|
|
sku: z.string().min(1, "SKU est requis"),
|
|
quantity: z.number().min(0, "La quantité doit être 0 ou plus"),
|
|
price: z.number().min(0, "Le prix doit être 0 ou plus"),
|
|
category: z.string().min(1, "La catégorie est requise"),
|
|
reorderPoint: z.number().min(0, "Le point de réapprovisionnement doit être 0 ou plus"),
|
|
});
|
|
|
|
type StockFormValues = z.infer<typeof stockFormSchema>;
|
|
|
|
const StockManagement = () => {
|
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
|
const [search, setSearch] = useState("");
|
|
const [stock, setStock] = useState<StockItem[]>(initialStock);
|
|
const [selectedCategory, setSelectedCategory] = useState<string>("all");
|
|
|
|
const form = useForm<StockFormValues>({
|
|
resolver: zodResolver(stockFormSchema),
|
|
defaultValues: {
|
|
name: "",
|
|
sku: "",
|
|
quantity: 0,
|
|
price: 0,
|
|
category: "",
|
|
reorderPoint: 5,
|
|
},
|
|
});
|
|
|
|
const onSubmit = (values: StockFormValues) => {
|
|
|
|
const newItem: StockItem = {
|
|
id: stock.length + 1,
|
|
name: values.name,
|
|
sku: values.sku,
|
|
quantity: values.quantity,
|
|
price: values.price,
|
|
category: values.category,
|
|
lastUpdated: new Date().toISOString().split("T")[0],
|
|
trend: 'stable',
|
|
demand: 'medium',
|
|
reorderPoint: values.reorderPoint,
|
|
};
|
|
|
|
setStock([...stock, newItem]);
|
|
toast.success("Produit ajouté à l'inventaire");
|
|
setIsAddDialogOpen(false);
|
|
form.reset();
|
|
};
|
|
|
|
const categories = ["all", ...Array.from(new Set(stock.map(item => item.category)))];
|
|
|
|
const filteredStock = stock.filter(item =>
|
|
(selectedCategory === "all" || item.category === selectedCategory) &&
|
|
(item.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
item.sku.toLowerCase().includes(search.toLowerCase()))
|
|
);
|
|
|
|
const lowStockItems = stock.filter(item => item.quantity <= (item.reorderPoint || 5));
|
|
|
|
return (
|
|
<AppLayout>
|
|
<div className="max-w-6xl mx-auto px-4">
|
|
<div className="flex justify-between items-center">
|
|
<Header
|
|
title="Gestion des Stocks"
|
|
subtitle="Suivez votre inventaire et gérez les niveaux de stock"
|
|
/>
|
|
<ThemeToggle />
|
|
</div>
|
|
|
|
{/* Add Stock Menu */}
|
|
<StockMenu />
|
|
|
|
{lowStockItems.length > 0 && (
|
|
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 mb-6 flex items-center gap-3">
|
|
<AlertTriangle className="text-yellow-500 h-5 w-5" />
|
|
<div>
|
|
<p className="text-sm font-medium">Alerte de stock faible</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{lowStockItems.length} produit(s) nécessitent un réapprovisionnement
|
|
</p>
|
|
</div>
|
|
<Button variant="outline" size="sm" className="ml-auto">
|
|
Voir tous
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between mb-6 flex-col md:flex-row gap-4">
|
|
<div className="relative w-full md:w-auto flex-1 max-w-md">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
|
<Input
|
|
placeholder="Rechercher des produits..."
|
|
className="pl-10"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2 w-full md:w-auto justify-end">
|
|
<select
|
|
className="bg-background dark:bg-violet-darker border border-input rounded-md h-9 px-3 text-sm"
|
|
value={selectedCategory}
|
|
onChange={(e) => setSelectedCategory(e.target.value)}
|
|
>
|
|
{categories.map(category => (
|
|
<option key={category} value={category}>
|
|
{category === "all" ? "Toutes les catégories" : category}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
<Button onClick={() => setIsAddDialogOpen(true)}>
|
|
<Plus size={16} className="mr-1" />
|
|
Ajouter
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-card rounded-lg border shadow-sm overflow-hidden">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Produit</TableHead>
|
|
<TableHead>SKU</TableHead>
|
|
<TableHead>Quantité</TableHead>
|
|
<TableHead>Prix (FCFA)</TableHead>
|
|
<TableHead>Catégorie</TableHead>
|
|
<TableHead>Tendance</TableHead>
|
|
<TableHead>Dernière mise à jour</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredStock.length > 0 ? (
|
|
filteredStock.map((item) => (
|
|
<TableRow key={item.id} className="cursor-pointer hover:bg-muted/50">
|
|
<TableCell className="font-medium">{item.name}</TableCell>
|
|
<TableCell>{item.sku}</TableCell>
|
|
<TableCell className={
|
|
item.quantity <= (item.reorderPoint || 5) ? "text-destructive" :
|
|
item.quantity <= (item.reorderPoint || 5) * 2 ? "text-yellow-500" : ""
|
|
}>
|
|
{item.quantity}
|
|
</TableCell>
|
|
<TableCell>{item.price.toLocaleString()}</TableCell>
|
|
<TableCell>{item.category}</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center">
|
|
{item.trend === 'up' && <TrendingUp size={16} className="text-green-500 mr-1" />}
|
|
{item.trend === 'down' && <TrendingDown size={16} className="text-red-500 mr-1" />}
|
|
{item.demand === 'high' && <span className="text-xs text-green-500 font-medium">Forte demande</span>}
|
|
{item.demand === 'medium' && <span className="text-xs text-amber-500 font-medium">Demande moyenne</span>}
|
|
{item.demand === 'low' && <span className="text-xs text-blue-500 font-medium">Faible demande</span>}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>{item.lastUpdated}</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="h-24 text-center">
|
|
Aucun produit trouvé.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
|
<DialogContent className="sm:max-w-[425px]">
|
|
<DialogHeader>
|
|
<DialogTitle>Ajouter un nouveau produit</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 pt-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="name"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Nom du produit</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="Entrez le nom du produit" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="sku"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>SKU</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="Entrez le SKU" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="flex gap-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="quantity"
|
|
render={({ field }) => (
|
|
<FormItem className="flex-1">
|
|
<FormLabel>Quantité</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="number"
|
|
placeholder="0"
|
|
{...field}
|
|
onChange={e => field.onChange(parseInt(e.target.value) || 0)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="price"
|
|
render={({ field }) => (
|
|
<FormItem className="flex-1">
|
|
<FormLabel>Prix (FCFA)</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="number"
|
|
step="1"
|
|
placeholder="0"
|
|
{...field}
|
|
onChange={e => field.onChange(parseFloat(e.target.value) || 0)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="category"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Catégorie</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="Entrez la catégorie" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="reorderPoint"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Seuil d'alerte</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="number"
|
|
placeholder="5"
|
|
{...field}
|
|
onChange={e => field.onChange(parseInt(e.target.value) || 0)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<DialogFooter className="pt-4">
|
|
<Button type="submit">Ajouter le produit</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
};
|
|
|
|
export default StockManagement;
|
|
|