)}
+
+ {/* Bouton Marquer comme traité - accessible à tous, pour produits expirant bientôt non traités */}
+ {shouldShowMarkProcessedButton(product) && (
+
+
+
+
+
+
+ Marquer comme traité
+
+ Indique que vous vous êtes occupé de ce produit (réapparaîtra à expiration)
+
+
+
+
+ )}
+
+ {/* Bouton Annuler traitement - réservé aux admins, directeurs et managers */}
+ {shouldShowUnmarkProcessedButton(product) && (user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur') && (
+
+
+
+
+
+
+ Annuler le traitement
+
+ Retire le marquage "traité" de ce produit
+
+
+
+
+ )}
{/* Bouton Stock épuisé - accessible à tous, SEULEMENT pour produits en cours qui ne sont ni expirent bientôt ni expirés */}
{!product.stockEpuise &&
diff --git a/server/routes.ts b/server/routes.ts
index 36f7786..454ebd7 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -2051,6 +2051,91 @@ export async function registerRoutes(app: Express): Promise
{
}
});
+ // 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
app.get('/api/tasks', isAuthenticated, async (req: any, res) => {
try {
diff --git a/server/storage.ts b/server/storage.ts
index a071a15..aac2628 100644
--- a/server/storage.ts
+++ b/server/storage.ts
@@ -193,6 +193,8 @@ export interface IStorage {
validateDlcProduct(id: number, validatedBy: string): Promise;
markDlcProductStockEpuise(id: number, markedBy: string): Promise;
restoreDlcProductStock(id: number): Promise;
+ markDlcProductAsProcessed(id: number, processedBy: string): Promise;
+ unmarkDlcProductAsProcessed(id: number): Promise;
getDlcStats(groupIds?: number[]): Promise<{ active: number; expiringSoon: number; expired: number; }>;
// Task operations
@@ -1990,6 +1992,34 @@ export class DatabaseStorage implements IStorage {
return dlcProduct as DlcProductFrontend;
}
+ async markDlcProductAsProcessed(id: number, processedBy: string): Promise {
+ 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 {
+ 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; }> {
const today = new Date();
const alertDate = new Date();
@@ -2003,8 +2033,14 @@ export class DatabaseStorage implements IStorage {
const [stats] = await db
.select({
active: sql`COUNT(CASE WHEN ${dlcProducts.expiryDate} > ${today.toISOString().split('T')[0]} THEN 1 END)`,
- expiringSoon: sql`COUNT(CASE WHEN ${dlcProducts.expiryDate} BETWEEN ${today.toISOString().split('T')[0]} AND ${alertDate.toISOString().split('T')[0]} THEN 1 END)`,
- expired: sql`COUNT(CASE WHEN ${dlcProducts.expiryDate} <= ${today.toISOString().split('T')[0]} THEN 1 END)`
+ expiringSoon: sql`COUNT(CASE
+ 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`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)
.where(whereCondition);
@@ -4096,6 +4132,36 @@ export class MemStorage implements IStorage {
return { ...updatedProduct, dlcDate: new Date(updatedProduct.expiryDate) } as DlcProductFrontend;
}
+ async markDlcProductAsProcessed(id: number, processedBy: string): Promise {
+ 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 {
+ 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; }> {
const today = 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,
expiringSoon: products.filter(p => {
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,
- 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
};
}
diff --git a/shared/schema.ts b/shared/schema.ts
index 20d90bc..b184555 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -248,6 +248,9 @@ export const dlcProducts = pgTable("dlc_products", {
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)
+ 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)
createdBy: varchar("created_by").notNull(), // Créateur
validatedBy: varchar("validated_by"), // Validé par (optionnel)