stock / src /pages /StockManagement.tsx
Zelyanoth's picture
Upload 101 files
24d40b9 verified
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";
// Define the stock item type to ensure type safety
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;
}
// Example initial stock data tailored for informal commerce in Africa
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) => {
// Create a new item with explicit typing to ensure all required properties are present
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;