mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-22 22:48:15 +02:00
7 files changed
+625
-260
No files matched your search
@@ -28,21 +28,13 @@ localPort = 38451
|
|||||||
externalPort = 3002
|
externalPort = 3002
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 39283
|
localPort = 41077
|
||||||
externalPort = 6000
|
externalPort = 5000
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 41343
|
localPort = 41343
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 41735
|
|
||||||
externalPort = 4200
|
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 46559
|
|
||||||
externalPort = 5173
|
|
||||||
|
|
||||||
[workflows]
|
[workflows]
|
||||||
runButton = "Project"
|
runButton = "Project"
|
||||||
|
|
||||||
|
|||||||
+325
-242
File diff suppressed because it is too large.
Load diff
@@ -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, PackageX, RotateCcw } from "lucide-react";
|
import { AlertTriangle, Plus, Eye, Edit, Trash2, CheckCircle, CheckCircle2, Package, Clock, AlertCircle, Filter, Download, FileText, PackageX, RotateCcw, X } from "lucide-react";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -225,6 +225,40 @@ export default function DlcPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Marquer comme traité mutation - accessible à tous
|
||||||
|
const markProcessedMutation = useMutation({
|
||||||
|
mutationFn: (id: number) => apiRequest(`/api/dlc-products/${id}/mark-processed`, "PUT"),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
||||||
|
toast({ title: "Produit marqué comme traité" });
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast({
|
||||||
|
title: "Erreur lors du marquage",
|
||||||
|
description: error.message || "Une erreur est survenue",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Annuler traitement mutation - réservé aux admins, directeurs et managers
|
||||||
|
const unmarkProcessedMutation = useMutation({
|
||||||
|
mutationFn: (id: number) => apiRequest(`/api/dlc-products/${id}/unmark-processed`, "PUT"),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
||||||
|
toast({ title: "Traitement annulé avec succès" });
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast({
|
||||||
|
title: "Erreur lors de l'annulation",
|
||||||
|
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);
|
||||||
@@ -319,6 +353,14 @@ export default function DlcPage() {
|
|||||||
restoreStockMutation.mutate(id);
|
restoreStockMutation.mutate(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMarkProcessed = (id: number) => {
|
||||||
|
markProcessedMutation.mutate(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnmarkProcessed = (id: number) => {
|
||||||
|
unmarkProcessedMutation.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();
|
||||||
@@ -330,13 +372,40 @@ export default function DlcPage() {
|
|||||||
return (daysUntilExpiry <= 15 && product.status !== "valides");
|
return (daysUntilExpiry <= 15 && product.status !== "valides");
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadge = (status: string, dlcDate: string | null, stockEpuise?: boolean) => {
|
// Fonction pour déterminer si un produit doit afficher le bouton "Marquer comme traité"
|
||||||
|
const shouldShowMarkProcessedButton = (product: DlcProductWithRelations) => {
|
||||||
|
const today = new Date();
|
||||||
|
const expiry = new Date(product.expiryDate || new Date());
|
||||||
|
const daysUntilExpiry = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
// Afficher le bouton si :
|
||||||
|
// - Le produit expire bientôt (dans 15 jours ou moins) ET pas encore expiré
|
||||||
|
// - Il n'est pas déjà traité
|
||||||
|
// - Il n'est pas validé et pas en stock épuisé
|
||||||
|
return (daysUntilExpiry <= 15 && daysUntilExpiry > 0 &&
|
||||||
|
!product.processedUntilExpiry &&
|
||||||
|
product.status !== "valides" &&
|
||||||
|
!product.stockEpuise);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fonction pour déterminer si un produit doit afficher le bouton "Annuler traitement"
|
||||||
|
const shouldShowUnmarkProcessedButton = (product: DlcProductWithRelations) => {
|
||||||
|
const today = new Date();
|
||||||
|
const expiry = new Date(product.expiryDate || new Date());
|
||||||
|
const daysUntilExpiry = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
// Afficher le bouton si le produit est traité ET pas encore expiré
|
||||||
|
return (product.processedUntilExpiry && daysUntilExpiry > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string, dlcDate: string | null, stockEpuise?: boolean, processedAt?: string | null, processedUntilExpiry?: boolean) => {
|
||||||
if (!dlcDate) return <Badge variant="outline">Non défini</Badge>;
|
if (!dlcDate) return <Badge variant="outline">Non défini</Badge>;
|
||||||
|
|
||||||
// LOGIQUE MISE À JOUR :
|
// LOGIQUE MISE À JOUR :
|
||||||
// 1. Si stock épuisé, afficher "Stock épuisé" avec style spécifique
|
// 1. Si stock épuisé, afficher "Stock épuisé" avec style spécifique
|
||||||
// 2. Si le statut en base est "valides", afficher "Validé" (peu importe la date)
|
// 2. Si le statut en base est "valides", afficher "Validé" (peu importe la date)
|
||||||
// 3. Sinon, calculer selon la date d'expiration
|
// 3. Si traité temporairement ET pas encore expiré, afficher "Traité - Expire dans X jours"
|
||||||
|
// 4. Sinon, calculer selon la date d'expiration
|
||||||
|
|
||||||
if (stockEpuise) {
|
if (stockEpuise) {
|
||||||
return <Badge variant="secondary" className="bg-yellow-100 text-yellow-800 border-yellow-300">Stock épuisé</Badge>;
|
return <Badge variant="secondary" className="bg-yellow-100 text-yellow-800 border-yellow-300">Stock épuisé</Badge>;
|
||||||
@@ -351,6 +420,13 @@ export default function DlcPage() {
|
|||||||
const expiry = new Date(dlcDate);
|
const expiry = new Date(dlcDate);
|
||||||
const daysUntilExpiry = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
const daysUntilExpiry = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
// Si traité temporairement et pas encore expiré
|
||||||
|
if (processedAt && processedUntilExpiry && daysUntilExpiry > 0) {
|
||||||
|
return <Badge variant="outline" className="bg-blue-100 text-blue-800 border-blue-300">
|
||||||
|
Traité - Expire dans {daysUntilExpiry} jour{daysUntilExpiry !== 1 ? 's' : ''}
|
||||||
|
</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
if (daysUntilExpiry < 0) {
|
if (daysUntilExpiry < 0) {
|
||||||
return <Badge variant="destructive">Expiré</Badge>;
|
return <Badge variant="destructive">Expiré</Badge>;
|
||||||
} else if (daysUntilExpiry <= 15) {
|
} else if (daysUntilExpiry <= 15) {
|
||||||
@@ -834,7 +910,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, product.stockEpuise)}</TableCell>
|
<TableCell>{getStatusBadge(product.status, product.expiryDate, product.stockEpuise, product.processedAt, product.processedUntilExpiry)}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -851,10 +927,63 @@ export default function DlcPage() {
|
|||||||
onClick={() => handleValidate(product.id)}
|
onClick={() => handleValidate(product.id)}
|
||||||
disabled={validateMutation.isPending}
|
disabled={validateMutation.isPending}
|
||||||
className="bg-green-600 hover:bg-green-700"
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
data-testid={`button-validate-${product.id}`}
|
||||||
|
title="Valider définitivement ce produit"
|
||||||
>
|
>
|
||||||
<CheckCircle className="w-4 h-4" />
|
<CheckCircle className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Bouton Marquer comme traité - accessible à tous, pour produits expirant bientôt non traités */}
|
||||||
|
{shouldShowMarkProcessedButton(product) && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleMarkProcessed(product.id)}
|
||||||
|
disabled={markProcessedMutation.isPending}
|
||||||
|
className="border-blue-300 text-blue-700 hover:bg-blue-50"
|
||||||
|
data-testid={`button-mark-processed-${product.id}`}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Marquer comme traité</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Indique que vous vous êtes occupé de ce produit (réapparaîtra à expiration)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bouton Annuler traitement - réservé aux admins, directeurs et managers */}
|
||||||
|
{shouldShowUnmarkProcessedButton(product) && (user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur') && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleUnmarkProcessed(product.id)}
|
||||||
|
disabled={unmarkProcessedMutation.isPending}
|
||||||
|
className="border-orange-300 text-orange-700 hover:bg-orange-50"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Annuler le traitement</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Retire le marquage "traité" de ce produit
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Bouton Stock épuisé - accessible à tous, SEULEMENT pour produits en cours qui ne sont ni expirent bientôt ni expirés */}
|
{/* Bouton Stock épuisé - accessible à tous, SEULEMENT pour produits en cours qui ne sont ni expirent bientôt ni expirés */}
|
||||||
{!product.stockEpuise &&
|
{!product.stockEpuise &&
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 275 KiB |
@@ -2051,6 +2051,91 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Route pour marquer un produit DLC comme traité temporairement (expire bientôt) - accessible à tous
|
||||||
|
app.put('/api/dlc-products/:id/mark-processed', 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 Product mark as processed attempt:', {
|
||||||
|
userId: user.id,
|
||||||
|
userRole: user.role,
|
||||||
|
dlcProductId: id,
|
||||||
|
dlcGroupId: dlcProduct.groupId
|
||||||
|
});
|
||||||
|
|
||||||
|
const processedProduct = await storage.markDlcProductAsProcessed(id, user.id);
|
||||||
|
console.log('✅ DLC Product marked as processed by:', user.role, user.id);
|
||||||
|
|
||||||
|
res.json(processedProduct);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error marking DLC product as processed:", error);
|
||||||
|
res.status(500).json({ message: "Failed to mark DLC product as processed" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route pour annuler le traitement temporaire d'un produit DLC - réservé aux admins, directeurs et managers
|
||||||
|
app.put('/api/dlc-products/:id/unmark-processed', 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 unmark processed (admin, directeur, manager)
|
||||||
|
if (!['admin', 'directeur', 'manager'].includes(user.role)) {
|
||||||
|
return res.status(403).json({ message: "Insufficient permissions to unmark DLC product as processed" });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Product unmark processed attempt:', {
|
||||||
|
userId: user.id,
|
||||||
|
userRole: user.role,
|
||||||
|
dlcProductId: id,
|
||||||
|
dlcGroupId: dlcProduct.groupId
|
||||||
|
});
|
||||||
|
|
||||||
|
const unprocessedProduct = await storage.unmarkDlcProductAsProcessed(id);
|
||||||
|
console.log('✅ DLC Product unmarked as processed by:', user.role, user.id);
|
||||||
|
|
||||||
|
res.json(unprocessedProduct);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error unmarking DLC product as processed:", error);
|
||||||
|
res.status(500).json({ message: "Failed to unmark DLC product as processed" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Tasks routes
|
// Tasks routes
|
||||||
app.get('/api/tasks', isAuthenticated, async (req: any, res) => {
|
app.get('/api/tasks', isAuthenticated, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+77
-4
@@ -193,6 +193,8 @@ export interface IStorage {
|
|||||||
validateDlcProduct(id: number, validatedBy: string): Promise<DlcProductFrontend>;
|
validateDlcProduct(id: number, validatedBy: string): Promise<DlcProductFrontend>;
|
||||||
markDlcProductStockEpuise(id: number, markedBy: string): Promise<DlcProductFrontend>;
|
markDlcProductStockEpuise(id: number, markedBy: string): Promise<DlcProductFrontend>;
|
||||||
restoreDlcProductStock(id: number): Promise<DlcProductFrontend>;
|
restoreDlcProductStock(id: number): Promise<DlcProductFrontend>;
|
||||||
|
markDlcProductAsProcessed(id: number, processedBy: string): Promise<DlcProductFrontend>;
|
||||||
|
unmarkDlcProductAsProcessed(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
|
||||||
@@ -1990,6 +1992,34 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return dlcProduct as DlcProductFrontend;
|
return dlcProduct as DlcProductFrontend;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async markDlcProductAsProcessed(id: number, processedBy: string): Promise<DlcProductFrontend> {
|
||||||
|
const [dlcProduct] = await db
|
||||||
|
.update(dlcProducts)
|
||||||
|
.set({
|
||||||
|
processedAt: new Date(),
|
||||||
|
processedBy: processedBy,
|
||||||
|
processedUntilExpiry: true,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(eq(dlcProducts.id, id))
|
||||||
|
.returning();
|
||||||
|
return dlcProduct as DlcProductFrontend;
|
||||||
|
}
|
||||||
|
|
||||||
|
async unmarkDlcProductAsProcessed(id: number): Promise<DlcProductFrontend> {
|
||||||
|
const [dlcProduct] = await db
|
||||||
|
.update(dlcProducts)
|
||||||
|
.set({
|
||||||
|
processedAt: null,
|
||||||
|
processedBy: null,
|
||||||
|
processedUntilExpiry: false,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(eq(dlcProducts.id, id))
|
||||||
|
.returning();
|
||||||
|
return dlcProduct as DlcProductFrontend;
|
||||||
|
}
|
||||||
|
|
||||||
async getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }> {
|
async getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }> {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const alertDate = new Date();
|
const alertDate = new Date();
|
||||||
@@ -2003,8 +2033,14 @@ export class DatabaseStorage implements IStorage {
|
|||||||
const [stats] = await db
|
const [stats] = await db
|
||||||
.select({
|
.select({
|
||||||
active: sql<number>`COUNT(CASE WHEN ${dlcProducts.expiryDate} > ${today.toISOString().split('T')[0]} THEN 1 END)`,
|
active: sql<number>`COUNT(CASE WHEN ${dlcProducts.expiryDate} > ${today.toISOString().split('T')[0]} THEN 1 END)`,
|
||||||
expiringSoon: sql<number>`COUNT(CASE WHEN ${dlcProducts.expiryDate} BETWEEN ${today.toISOString().split('T')[0]} AND ${alertDate.toISOString().split('T')[0]} THEN 1 END)`,
|
expiringSoon: sql<number>`COUNT(CASE
|
||||||
expired: sql<number>`COUNT(CASE WHEN ${dlcProducts.expiryDate} <= ${today.toISOString().split('T')[0]} THEN 1 END)`
|
WHEN ${dlcProducts.expiryDate} BETWEEN ${today.toISOString().split('T')[0]} AND ${alertDate.toISOString().split('T')[0]}
|
||||||
|
AND (${dlcProducts.processedUntilExpiry} IS NULL OR ${dlcProducts.processedUntilExpiry} = false)
|
||||||
|
THEN 1 END)`,
|
||||||
|
expired: sql<number>`COUNT(CASE
|
||||||
|
WHEN ${dlcProducts.expiryDate} <= ${today.toISOString().split('T')[0]}
|
||||||
|
AND (${dlcProducts.processedUntilExpiry} IS NULL OR ${dlcProducts.processedUntilExpiry} = false OR ${dlcProducts.expiryDate} < ${today.toISOString().split('T')[0]})
|
||||||
|
THEN 1 END)`
|
||||||
})
|
})
|
||||||
.from(dlcProducts)
|
.from(dlcProducts)
|
||||||
.where(whereCondition);
|
.where(whereCondition);
|
||||||
@@ -4096,6 +4132,36 @@ export class MemStorage implements IStorage {
|
|||||||
return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend;
|
return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async markDlcProductAsProcessed(id: number, processedBy: string): Promise<DlcProductFrontend> {
|
||||||
|
const existingProduct = this.dlcProducts.get(id);
|
||||||
|
if (!existingProduct) throw new Error('DLC Product not found');
|
||||||
|
|
||||||
|
const updatedProduct = {
|
||||||
|
...existingProduct,
|
||||||
|
processedAt: new Date(),
|
||||||
|
processedBy: processedBy,
|
||||||
|
processedUntilExpiry: true,
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
this.dlcProducts.set(id, updatedProduct);
|
||||||
|
return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend;
|
||||||
|
}
|
||||||
|
|
||||||
|
async unmarkDlcProductAsProcessed(id: number): Promise<DlcProductFrontend> {
|
||||||
|
const existingProduct = this.dlcProducts.get(id);
|
||||||
|
if (!existingProduct) throw new Error('DLC Product not found');
|
||||||
|
|
||||||
|
const updatedProduct = {
|
||||||
|
...existingProduct,
|
||||||
|
processedAt: null,
|
||||||
|
processedBy: null,
|
||||||
|
processedUntilExpiry: false,
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
this.dlcProducts.set(id, updatedProduct);
|
||||||
|
return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend;
|
||||||
|
}
|
||||||
|
|
||||||
async getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }> {
|
async getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }> {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const alertDate = new Date();
|
const alertDate = new Date();
|
||||||
@@ -4111,9 +4177,16 @@ export class MemStorage implements IStorage {
|
|||||||
active: products.filter(p => new Date(p.expiryDate) > today).length,
|
active: products.filter(p => new Date(p.expiryDate) > today).length,
|
||||||
expiringSoon: products.filter(p => {
|
expiringSoon: products.filter(p => {
|
||||||
const expiry = new Date(p.expiryDate);
|
const expiry = new Date(p.expiryDate);
|
||||||
return expiry >= today && expiry <= alertDate;
|
const isExpiringSoon = expiry >= today && expiry <= alertDate;
|
||||||
|
const isProcessed = p.processedUntilExpiry && expiry > today;
|
||||||
|
return isExpiringSoon && !isProcessed;
|
||||||
}).length,
|
}).length,
|
||||||
expired: products.filter(p => new Date(p.expiryDate) <= today).length
|
expired: products.filter(p => {
|
||||||
|
const expiry = new Date(p.expiryDate);
|
||||||
|
const isExpired = expiry <= today;
|
||||||
|
const isProcessed = p.processedUntilExpiry && expiry > today;
|
||||||
|
return isExpired && !isProcessed;
|
||||||
|
}).length
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -248,6 +248,9 @@ export const dlcProducts = pgTable("dlc_products", {
|
|||||||
stockEpuise: boolean("stock_epuise").default(false), // Produit épuisé en stock (pas périmé)
|
stockEpuise: boolean("stock_epuise").default(false), // Produit épuisé en stock (pas périmé)
|
||||||
stockEpuiseBy: varchar("stock_epuise_by"), // Marqué comme épuisé par (optionnel)
|
stockEpuiseBy: varchar("stock_epuise_by"), // Marqué comme épuisé par (optionnel)
|
||||||
stockEpuiseAt: timestamp("stock_epuise_at"), // Date de marquage épuisé (optionnel)
|
stockEpuiseAt: timestamp("stock_epuise_at"), // Date de marquage épuisé (optionnel)
|
||||||
|
processedAt: timestamp("processed_at"), // Date de traitement "expire bientôt" (optionnel)
|
||||||
|
processedBy: varchar("processed_by"), // Qui a marqué comme traité (optionnel)
|
||||||
|
processedUntilExpiry: boolean("processed_until_expiry").default(false), // Traité jusqu'à expiration
|
||||||
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