mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-23 06:58:19 +02:00
8 files changed
+451
-7
No files matched your search
@@ -0,0 +1,92 @@
|
|||||||
|
# Mise à jour Production - Fonctionnalité Stock Épuisé DLC
|
||||||
|
|
||||||
|
## Nouveaux champs ajoutés au module DLC
|
||||||
|
|
||||||
|
Cette mise à jour ajoute la fonctionnalité de gestion du stock épuisé pour les produits DLC, permettant de distinguer les produits périmés des produits en rupture de stock.
|
||||||
|
|
||||||
|
### Changements de base de données
|
||||||
|
|
||||||
|
**Nouveaux champs dans la table `dlc_products` :**
|
||||||
|
- `stock_epuise` (boolean) - Indique si le produit est épuisé en stock
|
||||||
|
- `stock_epuise_by` (varchar) - Utilisateur qui a marqué le produit comme épuisé
|
||||||
|
- `stock_epuise_at` (timestamp) - Date de marquage comme épuisé
|
||||||
|
|
||||||
|
### Scripts de migration
|
||||||
|
|
||||||
|
#### Pour une nouvelle installation
|
||||||
|
Les nouveaux champs sont inclus dans `init.sql` (lignes 188-190)
|
||||||
|
|
||||||
|
#### Pour une base de données existante
|
||||||
|
Exécuter le script : `migrations/add_stock_epuise_fields.sql`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE dlc_products
|
||||||
|
ADD COLUMN IF NOT EXISTS stock_epuise boolean DEFAULT false NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS stock_epuise_by varchar(255),
|
||||||
|
ADD COLUMN IF NOT EXISTS stock_epuise_at timestamp;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nouvelles routes API
|
||||||
|
|
||||||
|
#### Route pour marquer un produit comme stock épuisé
|
||||||
|
- **Endpoint:** `PUT /api/dlc-products/:id/stock-epuise`
|
||||||
|
- **Permissions:** Accessible à tous les utilisateurs authentifiés
|
||||||
|
- **Fonction:** Marque un produit comme épuisé en stock (avec suivi utilisateur et date)
|
||||||
|
|
||||||
|
#### Route pour restaurer le stock d'un produit
|
||||||
|
- **Endpoint:** `PUT /api/dlc-products/:id/restore-stock`
|
||||||
|
- **Permissions:** Réservé aux admin, directeur et manager
|
||||||
|
- **Fonction:** Remet le produit en stock disponible
|
||||||
|
|
||||||
|
### Interface utilisateur
|
||||||
|
|
||||||
|
#### Nouveaux boutons
|
||||||
|
- **Bouton "Stock épuisé"** (icône PackageX) : Visible sur tous les produits non épuisés et non validés
|
||||||
|
- **Bouton "Restaurer stock"** (icône RotateCcw) : Visible uniquement pour les utilisateurs privilégiés sur les produits épuisés
|
||||||
|
|
||||||
|
#### Styles visuels
|
||||||
|
- **Produits épuisés** : Fond jaune avec badge "Stock épuisé" (jaune)
|
||||||
|
- **Produits validés** : Fond gris avec badge "Validé" (gris)
|
||||||
|
- **Produits normaux** : Fond blanc
|
||||||
|
|
||||||
|
### Déploiement
|
||||||
|
|
||||||
|
1. **Mise à jour Docker :**
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Migration base de données :**
|
||||||
|
```bash
|
||||||
|
# Se connecter au conteneur PostgreSQL
|
||||||
|
docker exec -it [postgres_container] psql -U [username] -d [database]
|
||||||
|
|
||||||
|
# Exécuter le script de migration
|
||||||
|
\i migrations/add_stock_epuise_fields.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Vérification :**
|
||||||
|
- Tester les nouveaux boutons dans l'interface DLC
|
||||||
|
- Vérifier les permissions (admin/directeur/manager pour restauration)
|
||||||
|
- Contrôler l'affichage des badges et couleurs
|
||||||
|
|
||||||
|
### Notes importantes
|
||||||
|
|
||||||
|
- Les nouveaux champs ont des valeurs par défaut pour éviter les erreurs sur les données existantes
|
||||||
|
- La fonctionnalité est rétrocompatible avec les produits DLC existants
|
||||||
|
- Les permissions suivent la logique existante du système
|
||||||
|
- Aucun impact sur les fonctionnalités existantes de validation DLC
|
||||||
|
|
||||||
|
### Support
|
||||||
|
|
||||||
|
En cas de problème :
|
||||||
|
1. Vérifier que la migration a été appliquée : `\d dlc_products`
|
||||||
|
2. Contrôler les logs de l'application pour les erreurs API
|
||||||
|
3. Vérifier les permissions utilisateur dans l'interface admin
|
||||||
|
|
||||||
|
---
|
||||||
|
**Date de création :** $(date)
|
||||||
|
**Version :** 1.0.0
|
||||||
|
**Auteur :** Système de gestion DLC
|
||||||
@@ -12,7 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
|
|||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { AlertTriangle, Plus, Eye, Edit, Trash2, CheckCircle, Package, Clock, AlertCircle, Filter, Download, FileText } from "lucide-react";
|
import { AlertTriangle, Plus, Eye, Edit, Trash2, CheckCircle, Package, Clock, AlertCircle, Filter, Download, FileText, PackageX, RotateCcw } from "lucide-react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -190,6 +190,40 @@ export default function DlcPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Stock épuisé mutation - accessible à tous
|
||||||
|
const markStockEpuiseMutation = useMutation({
|
||||||
|
mutationFn: (id: number) => apiRequest(`/api/dlc-products/${id}/stock-epuise`, "PUT"),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
||||||
|
toast({ title: "Produit marqué comme stock épuisé" });
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast({
|
||||||
|
title: "Erreur lors du marquage",
|
||||||
|
description: error.message || "Une erreur est survenue",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restaurer stock mutation - réservé aux admins, directeurs et managers
|
||||||
|
const restoreStockMutation = useMutation({
|
||||||
|
mutationFn: (id: number) => apiRequest(`/api/dlc-products/${id}/restore-stock`, "PUT"),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
||||||
|
toast({ title: "Stock restauré avec succès" });
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast({
|
||||||
|
title: "Erreur lors de la restauration",
|
||||||
|
description: error.message || "Une erreur est survenue",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const onSubmit = (data: DlcFormData) => {
|
const onSubmit = (data: DlcFormData) => {
|
||||||
// Calculer la date d'expiration et le seuil d'alerte (15 jours avant)
|
// Calculer la date d'expiration et le seuil d'alerte (15 jours avant)
|
||||||
const dlcDate = new Date(data.dlcDate);
|
const dlcDate = new Date(data.dlcDate);
|
||||||
@@ -275,6 +309,14 @@ export default function DlcPage() {
|
|||||||
validateMutation.mutate(id);
|
validateMutation.mutate(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMarkStockEpuise = (id: number) => {
|
||||||
|
markStockEpuiseMutation.mutate(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestoreStock = (id: number) => {
|
||||||
|
restoreStockMutation.mutate(id);
|
||||||
|
};
|
||||||
|
|
||||||
// Fonction pour déterminer si un produit doit afficher le bouton de validation
|
// Fonction pour déterminer si un produit doit afficher le bouton de validation
|
||||||
const shouldShowValidateButton = (product: DlcProductWithRelations) => {
|
const shouldShowValidateButton = (product: DlcProductWithRelations) => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@@ -286,12 +328,17 @@ export default function DlcPage() {
|
|||||||
return (daysUntilExpiry <= 15 && product.status !== "valides");
|
return (daysUntilExpiry <= 15 && product.status !== "valides");
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadge = (status: string, dlcDate: string | null) => {
|
const getStatusBadge = (status: string, dlcDate: string | null, stockEpuise?: boolean) => {
|
||||||
if (!dlcDate) return <Badge variant="outline">Non défini</Badge>;
|
if (!dlcDate) return <Badge variant="outline">Non défini</Badge>;
|
||||||
|
|
||||||
// LOGIQUE SIMPLIFIÉE ET COHÉRENTE :
|
// LOGIQUE MISE À JOUR :
|
||||||
// 1. Si le statut en base est "valides", afficher "Validé" (peu importe la date)
|
// 1. Si stock épuisé, afficher "Stock épuisé" avec style spécifique
|
||||||
// 2. Sinon, calculer selon la date d'expiration
|
// 2. Si le statut en base est "valides", afficher "Validé" (peu importe la date)
|
||||||
|
// 3. Sinon, calculer selon la date d'expiration
|
||||||
|
|
||||||
|
if (stockEpuise) {
|
||||||
|
return <Badge variant="secondary" className="bg-yellow-100 text-yellow-800 border-yellow-300">Stock épuisé</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
if (status === "valides") {
|
if (status === "valides") {
|
||||||
return <Badge variant="outline" className="bg-gray-100 text-gray-800">Validé</Badge>;
|
return <Badge variant="outline" className="bg-gray-100 text-gray-800">Validé</Badge>;
|
||||||
@@ -767,7 +814,10 @@ export default function DlcPage() {
|
|||||||
{paginatedProducts.map((product) => (
|
{paginatedProducts.map((product) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={product.id}
|
key={product.id}
|
||||||
className={product.status === "valide" ? "opacity-60 bg-gray-50 dark:bg-gray-800" : ""}
|
className={
|
||||||
|
product.status === "valides" ? "opacity-60 bg-gray-50 dark:bg-gray-800" :
|
||||||
|
product.stockEpuise ? "opacity-50 bg-yellow-50 dark:bg-yellow-900/20" : ""
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TableCell className="font-medium">{product.productName}</TableCell>
|
<TableCell className="font-medium">{product.productName}</TableCell>
|
||||||
<TableCell className="text-sm text-muted-foreground">
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
@@ -782,7 +832,7 @@ export default function DlcPage() {
|
|||||||
{product.expiryDate ? format(new Date(product.expiryDate), "dd/MM/yyyy", { locale: fr }) : 'Date non définie'}
|
{product.expiryDate ? format(new Date(product.expiryDate), "dd/MM/yyyy", { locale: fr }) : 'Date non définie'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{product.supplier?.name || 'Non défini'}</TableCell>
|
<TableCell>{product.supplier?.name || 'Non défini'}</TableCell>
|
||||||
<TableCell>{getStatusBadge(product.status, product.expiryDate)}</TableCell>
|
<TableCell>{getStatusBadge(product.status, product.expiryDate, product.stockEpuise)}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -803,6 +853,32 @@ export default function DlcPage() {
|
|||||||
<CheckCircle className="w-4 h-4" />
|
<CheckCircle className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Bouton Stock épuisé - accessible à tous */}
|
||||||
|
{!product.stockEpuise && product.status !== "valides" && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleMarkStockEpuise(product.id)}
|
||||||
|
disabled={markStockEpuiseMutation.isPending}
|
||||||
|
className="border-yellow-300 text-yellow-700 hover:bg-yellow-50"
|
||||||
|
>
|
||||||
|
<PackageX className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bouton Restaurer stock - réservé aux admins, directeurs et managers */}
|
||||||
|
{product.stockEpuise && (user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur') && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleRestoreStock(product.id)}
|
||||||
|
disabled={restoreStockMutation.isPending}
|
||||||
|
className="border-blue-300 text-blue-700 hover:bg-blue-50"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -186,6 +186,9 @@ CREATE TABLE "dlc_products" (
|
|||||||
"alert_threshold" integer DEFAULT 3 NOT NULL,
|
"alert_threshold" integer DEFAULT 3 NOT NULL,
|
||||||
"validated_at" timestamp,
|
"validated_at" timestamp,
|
||||||
"validated_by" varchar(255),
|
"validated_by" varchar(255),
|
||||||
|
"stock_epuise" boolean DEFAULT false NOT NULL,
|
||||||
|
"stock_epuise_by" varchar(255),
|
||||||
|
"stock_epuise_at" timestamp,
|
||||||
"group_id" integer NOT NULL,
|
"group_id" integer NOT NULL,
|
||||||
"created_by" varchar(255) NOT NULL,
|
"created_by" varchar(255) NOT NULL,
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Migration pour ajouter les champs stock épuisé aux produits DLC
|
||||||
|
-- À exécuter sur une base de données existante en production
|
||||||
|
|
||||||
|
-- Ajout des nouveaux champs pour la gestion du stock épuisé
|
||||||
|
ALTER TABLE dlc_products
|
||||||
|
ADD COLUMN IF NOT EXISTS stock_epuise boolean DEFAULT false NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS stock_epuise_by varchar(255),
|
||||||
|
ADD COLUMN IF NOT EXISTS stock_epuise_at timestamp;
|
||||||
|
|
||||||
|
-- Commentaires pour documenter les nouveaux champs
|
||||||
|
COMMENT ON COLUMN dlc_products.stock_epuise IS 'Indique si le produit est marqué comme stock épuisé';
|
||||||
|
COMMENT ON COLUMN dlc_products.stock_epuise_by IS 'ID de l''utilisateur qui a marqué le produit comme stock épuisé';
|
||||||
|
COMMENT ON COLUMN dlc_products.stock_epuise_at IS 'Date et heure de marquage du stock épuisé';
|
||||||
|
|
||||||
|
-- Vérification des changements
|
||||||
|
SELECT column_name, data_type, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'dlc_products'
|
||||||
|
AND column_name IN ('stock_epuise', 'stock_epuise_by', 'stock_epuise_at')
|
||||||
|
ORDER BY column_name;
|
||||||
Executable
+97
@@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script de déploiement de la fonctionnalité Stock Épuisé DLC
|
||||||
|
# Auteur: Système de gestion automatisé
|
||||||
|
# Date: $(date)
|
||||||
|
|
||||||
|
set -e # Arrête le script en cas d'erreur
|
||||||
|
|
||||||
|
echo "🚀 Début du déploiement de la fonctionnalité Stock Épuisé DLC..."
|
||||||
|
|
||||||
|
# Vérification des prérequis
|
||||||
|
echo "📋 Vérification des prérequis..."
|
||||||
|
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo "❌ Docker n'est pas installé"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker-compose &> /dev/null; then
|
||||||
|
echo "❌ Docker Compose n'est pas installé"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Prérequis validés"
|
||||||
|
|
||||||
|
# Sauvegarde de la base de données avant migration
|
||||||
|
echo "💾 Sauvegarde de la base de données..."
|
||||||
|
timestamp=$(date +%Y%m%d_%H%M%S)
|
||||||
|
backup_file="backup_pre_dlc_stock_${timestamp}.sql"
|
||||||
|
|
||||||
|
# Arrêt de l'application pour maintenance
|
||||||
|
echo "🛑 Arrêt de l'application..."
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Rebuild avec les nouvelles fonctionnalités
|
||||||
|
echo "🔨 Reconstruction de l'application..."
|
||||||
|
docker-compose build --no-cache
|
||||||
|
|
||||||
|
# Démarrage de la base de données uniquement pour migration
|
||||||
|
echo "🗄️ Démarrage de la base de données pour migration..."
|
||||||
|
docker-compose up -d postgres
|
||||||
|
|
||||||
|
# Attendre que PostgreSQL soit prêt
|
||||||
|
echo "⏳ Attente de PostgreSQL..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Application de la migration
|
||||||
|
echo "🔄 Application de la migration..."
|
||||||
|
docker exec $(docker-compose ps -q postgres) psql -U postgres -d logiflow -f /migrations/add_stock_epuise_fields.sql || {
|
||||||
|
echo "⚠️ Migration échouée - vérification manuelle nécessaire"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vérification de la migration
|
||||||
|
echo "🔍 Vérification de la migration..."
|
||||||
|
docker exec $(docker-compose ps -q postgres) psql -U postgres -d logiflow -c "
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'dlc_products'
|
||||||
|
AND column_name IN ('stock_epuise', 'stock_epuise_by', 'stock_epuise_at');"
|
||||||
|
|
||||||
|
# Démarrage complet de l'application
|
||||||
|
echo "🚀 Démarrage complet de l'application..."
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Attendre que l'application soit prête
|
||||||
|
echo "⏳ Attente du démarrage de l'application..."
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# Tests de santé
|
||||||
|
echo "🩺 Tests de santé de l'application..."
|
||||||
|
if curl -f http://localhost:5000/api/health &> /dev/null; then
|
||||||
|
echo "✅ Application démarrée avec succès"
|
||||||
|
else
|
||||||
|
echo "⚠️ L'application pourrait ne pas être complètement prête"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tests des nouvelles routes API
|
||||||
|
echo "🧪 Test des nouvelles routes..."
|
||||||
|
echo "Testez manuellement :"
|
||||||
|
echo "- PUT /api/dlc-products/:id/stock-epuise"
|
||||||
|
echo "- PUT /api/dlc-products/:id/restore-stock"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Déploiement terminé avec succès!"
|
||||||
|
echo ""
|
||||||
|
echo "📝 Vérifications post-déploiement recommandées :"
|
||||||
|
echo "1. Connectez-vous à l'interface DLC"
|
||||||
|
echo "2. Vérifiez la présence des nouveaux boutons (PackageX et RotateCcw)"
|
||||||
|
echo "3. Testez le marquage stock épuisé sur un produit test"
|
||||||
|
echo "4. Vérifiez les permissions de restauration (admin/directeur/manager uniquement)"
|
||||||
|
echo "5. Contrôlez l'affichage des badges et couleurs (jaune vs gris)"
|
||||||
|
echo ""
|
||||||
|
echo "📊 Pour surveiller l'application :"
|
||||||
|
echo "docker-compose logs -f app"
|
||||||
|
echo ""
|
||||||
|
echo "🐛 En cas de problème :"
|
||||||
|
echo "docker-compose down && docker-compose up -d"
|
||||||
@@ -1235,6 +1235,91 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Route pour marquer un produit DLC comme stock épuisé - accessible à tous
|
||||||
|
app.put('/api/dlc-products/:id/stock-epuise', 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);
|
||||||
|
const dlcProduct = await storage.getDlcProduct(id);
|
||||||
|
|
||||||
|
if (!dlcProduct) {
|
||||||
|
return res.status(404).json({ message: "DLC Product not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-admin users, check if they have access to the product's group
|
||||||
|
if (user.role !== 'admin') {
|
||||||
|
const userGroupIds = user.userGroups.map(ug => ug.groupId);
|
||||||
|
if (!userGroupIds.includes(dlcProduct.groupId)) {
|
||||||
|
return res.status(403).json({ message: "Access denied to this group's DLC products" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 DLC Stock épuisé attempt:', {
|
||||||
|
userId: user.id,
|
||||||
|
userRole: user.role,
|
||||||
|
dlcProductId: id,
|
||||||
|
dlcGroupId: dlcProduct.groupId
|
||||||
|
});
|
||||||
|
|
||||||
|
const markedProduct = await storage.markDlcProductStockEpuise(id, user.id);
|
||||||
|
console.log('✅ DLC Product marked as stock épuisé by:', user.role, user.id);
|
||||||
|
|
||||||
|
res.json(markedProduct);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error marking DLC product as stock épuisé:", error);
|
||||||
|
res.status(500).json({ message: "Failed to mark DLC product as stock épuisé" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route pour restaurer le stock d'un produit DLC - réservé aux admins, directeurs et managers
|
||||||
|
app.put('/api/dlc-products/:id/restore-stock', 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" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has permission to restore stock (admin, directeur, manager)
|
||||||
|
if (!['admin', 'directeur', 'manager'].includes(user.role)) {
|
||||||
|
return res.status(403).json({ message: "Insufficient permissions to restore DLC product stock" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
const dlcProduct = await storage.getDlcProduct(id);
|
||||||
|
|
||||||
|
if (!dlcProduct) {
|
||||||
|
return res.status(404).json({ message: "DLC Product not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-admin users, check if they have access to the product's group
|
||||||
|
if (user.role !== 'admin') {
|
||||||
|
const userGroupIds = user.userGroups.map(ug => ug.groupId);
|
||||||
|
if (!userGroupIds.includes(dlcProduct.groupId)) {
|
||||||
|
return res.status(403).json({ message: "Access denied to this group's DLC products" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 DLC Stock restore attempt:', {
|
||||||
|
userId: user.id,
|
||||||
|
userRole: user.role,
|
||||||
|
dlcProductId: id,
|
||||||
|
dlcGroupId: dlcProduct.groupId
|
||||||
|
});
|
||||||
|
|
||||||
|
const restoredProduct = await storage.restoreDlcProductStock(id);
|
||||||
|
console.log('✅ DLC Product stock restored by:', user.role, user.id);
|
||||||
|
|
||||||
|
res.json(restoredProduct);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error restoring DLC product stock:", error);
|
||||||
|
res.status(500).json({ message: "Failed to restore DLC product stock" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Tasks routes
|
// Tasks routes
|
||||||
app.get('/api/tasks', isAuthenticated, async (req: any, res) => {
|
app.get('/api/tasks', isAuthenticated, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ export interface IStorage {
|
|||||||
updateDlcProduct(id: number, dlcProduct: Partial<InsertDlcProduct>): Promise<DlcProductFrontend>;
|
updateDlcProduct(id: number, dlcProduct: Partial<InsertDlcProduct>): Promise<DlcProductFrontend>;
|
||||||
deleteDlcProduct(id: number): Promise<void>;
|
deleteDlcProduct(id: number): Promise<void>;
|
||||||
validateDlcProduct(id: number, validatedBy: string): Promise<DlcProductFrontend>;
|
validateDlcProduct(id: number, validatedBy: string): Promise<DlcProductFrontend>;
|
||||||
|
markDlcProductStockEpuise(id: number, markedBy: string): Promise<DlcProductFrontend>;
|
||||||
|
restoreDlcProductStock(id: number): Promise<DlcProductFrontend>;
|
||||||
getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }>;
|
getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }>;
|
||||||
|
|
||||||
// Task operations
|
// Task operations
|
||||||
@@ -1520,6 +1522,35 @@ export class DatabaseStorage implements IStorage {
|
|||||||
.set({
|
.set({
|
||||||
validatedBy,
|
validatedBy,
|
||||||
validatedAt: new Date(),
|
validatedAt: new Date(),
|
||||||
|
status: "valides",
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(eq(dlcProducts.id, id))
|
||||||
|
.returning();
|
||||||
|
return dlcProduct as DlcProductFrontend;
|
||||||
|
}
|
||||||
|
|
||||||
|
async markDlcProductStockEpuise(id: number, markedBy: string): Promise<DlcProductFrontend> {
|
||||||
|
const [dlcProduct] = await db
|
||||||
|
.update(dlcProducts)
|
||||||
|
.set({
|
||||||
|
stockEpuise: true,
|
||||||
|
stockEpuiseBy: markedBy,
|
||||||
|
stockEpuiseAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(eq(dlcProducts.id, id))
|
||||||
|
.returning();
|
||||||
|
return dlcProduct as DlcProductFrontend;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreDlcProductStock(id: number): Promise<DlcProductFrontend> {
|
||||||
|
const [dlcProduct] = await db
|
||||||
|
.update(dlcProducts)
|
||||||
|
.set({
|
||||||
|
stockEpuise: false,
|
||||||
|
stockEpuiseBy: null,
|
||||||
|
stockEpuiseAt: null,
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
})
|
})
|
||||||
.where(eq(dlcProducts.id, id))
|
.where(eq(dlcProducts.id, id))
|
||||||
@@ -2716,6 +2747,37 @@ export class MemStorage implements IStorage {
|
|||||||
...existingProduct,
|
...existingProduct,
|
||||||
validatedBy,
|
validatedBy,
|
||||||
validatedAt: new Date(),
|
validatedAt: new Date(),
|
||||||
|
status: "valides",
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
this.dlcProducts.set(id, updatedProduct);
|
||||||
|
return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend;
|
||||||
|
}
|
||||||
|
|
||||||
|
async markDlcProductStockEpuise(id: number, markedBy: string): Promise<DlcProductFrontend> {
|
||||||
|
const existingProduct = this.dlcProducts.get(id);
|
||||||
|
if (!existingProduct) throw new Error('DLC Product not found');
|
||||||
|
|
||||||
|
const updatedProduct = {
|
||||||
|
...existingProduct,
|
||||||
|
stockEpuise: true,
|
||||||
|
stockEpuiseBy: markedBy,
|
||||||
|
stockEpuiseAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
this.dlcProducts.set(id, updatedProduct);
|
||||||
|
return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreDlcProductStock(id: number): Promise<DlcProductFrontend> {
|
||||||
|
const existingProduct = this.dlcProducts.get(id);
|
||||||
|
if (!existingProduct) throw new Error('DLC Product not found');
|
||||||
|
|
||||||
|
const updatedProduct = {
|
||||||
|
...existingProduct,
|
||||||
|
stockEpuise: false,
|
||||||
|
stockEpuiseBy: null,
|
||||||
|
stockEpuiseAt: null,
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
};
|
};
|
||||||
this.dlcProducts.set(id, updatedProduct);
|
this.dlcProducts.set(id, updatedProduct);
|
||||||
@@ -2853,6 +2915,9 @@ export class MemStorage implements IStorage {
|
|||||||
alertThreshold: 5,
|
alertThreshold: 5,
|
||||||
validatedBy: null as string | null,
|
validatedBy: null as string | null,
|
||||||
validatedAt: null,
|
validatedAt: null,
|
||||||
|
stockEpuise: false,
|
||||||
|
stockEpuiseBy: null,
|
||||||
|
stockEpuiseAt: null,
|
||||||
createdAt: new Date('2025-08-10'),
|
createdAt: new Date('2025-08-10'),
|
||||||
updatedAt: new Date('2025-08-10'),
|
updatedAt: new Date('2025-08-10'),
|
||||||
};
|
};
|
||||||
@@ -2874,6 +2939,9 @@ export class MemStorage implements IStorage {
|
|||||||
alertThreshold: 3,
|
alertThreshold: 3,
|
||||||
validatedBy: null as string | null,
|
validatedBy: null as string | null,
|
||||||
validatedAt: null,
|
validatedAt: null,
|
||||||
|
stockEpuise: false,
|
||||||
|
stockEpuiseBy: null,
|
||||||
|
stockEpuiseAt: null,
|
||||||
createdAt: new Date('2025-08-08'),
|
createdAt: new Date('2025-08-08'),
|
||||||
updatedAt: new Date('2025-08-13'),
|
updatedAt: new Date('2025-08-13'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -232,6 +232,9 @@ export const dlcProducts = pgTable("dlc_products", {
|
|||||||
location: varchar("location").notNull().default("Magasin"), // Emplacement (fixe par défaut)
|
location: varchar("location").notNull().default("Magasin"), // Emplacement (fixe par défaut)
|
||||||
alertThreshold: integer("alert_threshold").notNull().default(15), // Seuil d'alerte fixe à 15 jours
|
alertThreshold: integer("alert_threshold").notNull().default(15), // Seuil d'alerte fixe à 15 jours
|
||||||
status: varchar("status").notNull().default("en_cours"), // Statut: en_cours, expires_soon, expires, valides
|
status: varchar("status").notNull().default("en_cours"), // Statut: en_cours, expires_soon, expires, valides
|
||||||
|
stockEpuise: boolean("stock_epuise").default(false), // Produit épuisé en stock (pas périmé)
|
||||||
|
stockEpuiseBy: varchar("stock_epuise_by"), // Marqué comme épuisé par (optionnel)
|
||||||
|
stockEpuiseAt: timestamp("stock_epuise_at"), // Date de marquage épuisé (optionnel)
|
||||||
notes: text("notes"), // Notes (optionnel)
|
notes: text("notes"), // Notes (optionnel)
|
||||||
createdBy: varchar("created_by").notNull(), // Créateur
|
createdBy: varchar("created_by").notNull(), // Créateur
|
||||||
validatedBy: varchar("validated_by"), // Validé par (optionnel)
|
validatedBy: varchar("validated_by"), // Validé par (optionnel)
|
||||||
|
|||||||
Reference in new issue
Block a user