mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-22 22:48:15 +02:00
Improve product search functionality and data caching
Add server-side search and filtering for DLC products, and implement client-side debouncing for search terms. Optimize data fetching with extended stale times for stores, suppliers, and DLC stats to improve performance. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 78d55416-09d3-4c00-b411-444299b0da00 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/78d55416-09d3-4c00-b411-444299b0da00/WSwf4nZ
This commit is contained in:
1 parent
b172870ef9
commit
48bfc884ed
3 files changed
+86
-66
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -44,58 +44,53 @@ export default function DlcPage() {
|
|||||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||||
const [supplierFilter, setSupplierFilter] = useState<string>("all");
|
const [supplierFilter, setSupplierFilter] = useState<string>("all");
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [editingProduct, setEditingProduct] = useState<DlcProductWithRelations | null>(null);
|
const [editingProduct, setEditingProduct] = useState<DlcProductWithRelations | null>(null);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [productToDelete, setProductToDelete] = useState<number | null>(null);
|
const [productToDelete, setProductToDelete] = useState<number | null>(null);
|
||||||
|
|
||||||
// Fetch stores/groups
|
// Debounce search term to reduce API calls
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedSearchTerm(searchTerm);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchTerm]);
|
||||||
|
|
||||||
|
// Fetch stores/groups - optimized cache
|
||||||
const { data: stores = [] } = useQuery({
|
const { data: stores = [] } = useQuery({
|
||||||
queryKey: ["/api/groups"],
|
queryKey: ["/api/groups"],
|
||||||
enabled: !authLoading,
|
enabled: !authLoading,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes cache
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch suppliers with DLC enabled only - optimized cache
|
||||||
|
|
||||||
// Fetch suppliers with DLC enabled only
|
|
||||||
const { data: suppliers = [] } = useQuery({
|
const { data: suppliers = [] } = useQuery({
|
||||||
queryKey: ["/api/suppliers", "dlc"],
|
queryKey: ["/api/suppliers", "dlc"],
|
||||||
queryFn: () => apiRequest("/api/suppliers?dlc=true"),
|
queryFn: () => apiRequest("/api/suppliers?dlc=true"),
|
||||||
enabled: !authLoading,
|
enabled: !authLoading,
|
||||||
|
staleTime: 10 * 60 * 1000, // 10 minutes cache
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch DLC products
|
// Fetch DLC products - optimized with server-side search
|
||||||
const { data: dlcProducts = [], isLoading: productsLoading } = useQuery({
|
const { data: dlcProducts = [], isLoading: productsLoading } = useQuery({
|
||||||
queryKey: ["/api/dlc-products", selectedStoreId, statusFilter, supplierFilter],
|
queryKey: ["/api/dlc-products", selectedStoreId, statusFilter, supplierFilter, debouncedSearchTerm],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (selectedStoreId && user?.role === 'admin') params.append("storeId", selectedStoreId.toString());
|
if (selectedStoreId && user?.role === 'admin') params.append("storeId", selectedStoreId.toString());
|
||||||
if (statusFilter && statusFilter !== "all") params.append("status", statusFilter);
|
if (statusFilter && statusFilter !== "all") params.append("status", statusFilter);
|
||||||
if (supplierFilter && supplierFilter !== "all") params.append("supplierId", supplierFilter);
|
if (supplierFilter && supplierFilter !== "all") params.append("supplierId", supplierFilter);
|
||||||
|
if (debouncedSearchTerm) params.append("search", debouncedSearchTerm);
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.log("🔍 Requête DLC products:", {
|
|
||||||
url: `/api/dlc-products?${params.toString()}`,
|
|
||||||
user: user?.role,
|
|
||||||
selectedStoreId,
|
|
||||||
userGroups: user?.userGroups?.map(ug => ({ groupId: ug.groupId, groupName: ug.group?.name }))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiRequest(`/api/dlc-products?${params.toString()}`);
|
return apiRequest(`/api/dlc-products?${params.toString()}`);
|
||||||
},
|
},
|
||||||
enabled: !authLoading,
|
enabled: !authLoading,
|
||||||
onSuccess: (data) => {
|
staleTime: 2 * 60 * 1000, // 2 minutes cache for DLC data
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.log("📋 DLC products reçus:", {
|
|
||||||
total: data.length,
|
|
||||||
sample: data.slice(0, 3).map(d => ({ id: d.id, productName: d.productName, groupId: d.groupId }))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch DLC stats
|
// Fetch DLC stats - optimized cache
|
||||||
const { data: stats = { active: 0, expiringSoon: 0, expired: 0 } } = useQuery({
|
const { data: stats = { active: 0, expiringSoon: 0, expired: 0 } } = useQuery({
|
||||||
queryKey: ["/api/dlc-products/stats", selectedStoreId],
|
queryKey: ["/api/dlc-products/stats", selectedStoreId],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
@@ -104,6 +99,7 @@ export default function DlcPage() {
|
|||||||
return apiRequest(`/api/dlc-products/stats?${params.toString()}`);
|
return apiRequest(`/api/dlc-products/stats?${params.toString()}`);
|
||||||
},
|
},
|
||||||
enabled: !authLoading,
|
enabled: !authLoading,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes cache for stats
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form setup
|
// Form setup
|
||||||
@@ -118,16 +114,13 @@ export default function DlcPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create mutation
|
// Create mutation - optimized cache invalidation
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: InsertDlcProduct) => apiRequest("/api/dlc-products", "POST", data),
|
mutationFn: (data: InsertDlcProduct) => apiRequest("/api/dlc-products", "POST", data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// Invalidate all DLC queries with any combination of parameters
|
// Selective cache invalidation
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.log("🔄 Cache DLC invalidé après création");
|
|
||||||
}
|
|
||||||
toast({ title: "Produit DLC créé avec succès" });
|
toast({ title: "Produit DLC créé avec succès" });
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
form.reset();
|
form.reset();
|
||||||
@@ -142,16 +135,13 @@ export default function DlcPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update mutation
|
// Update mutation - optimized cache invalidation
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number; data: Partial<InsertDlcProduct> }) =>
|
mutationFn: ({ id, data }: { id: number; data: Partial<InsertDlcProduct> }) =>
|
||||||
apiRequest(`/api/dlc-products/${id}`, "PUT", data),
|
apiRequest(`/api/dlc-products/${id}`, "PUT", data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.log("🔄 Cache DLC invalidé après mise à jour");
|
|
||||||
}
|
|
||||||
toast({ title: "Produit DLC mis à jour avec succès" });
|
toast({ title: "Produit DLC mis à jour avec succès" });
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
form.reset();
|
form.reset();
|
||||||
@@ -166,15 +156,12 @@ export default function DlcPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate mutation
|
// Validate mutation - optimized cache invalidation
|
||||||
const validateMutation = useMutation({
|
const validateMutation = useMutation({
|
||||||
mutationFn: (id: number) => apiRequest(`/api/dlc-products/${id}/validate`, "PUT"),
|
mutationFn: (id: number) => apiRequest(`/api/dlc-products/${id}/validate`, "PUT"),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.log("🔄 Cache DLC invalidé après validation");
|
|
||||||
}
|
|
||||||
toast({ title: "Produit validé avec succès" });
|
toast({ title: "Produit validé avec succès" });
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
@@ -186,13 +173,12 @@ export default function DlcPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete mutation
|
// Delete mutation - optimized cache invalidation
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: (id: number) => apiRequest(`/api/dlc-products/${id}`, "DELETE"),
|
mutationFn: (id: number) => apiRequest(`/api/dlc-products/${id}`, "DELETE"),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
||||||
console.log("🔄 Cache DLC invalidé après suppression");
|
|
||||||
toast({ title: "Produit supprimé avec succès" });
|
toast({ title: "Produit supprimé avec succès" });
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
@@ -325,13 +311,7 @@ export default function DlcPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const printExpiringSoon = () => {
|
const printExpiringSoon = () => {
|
||||||
const expiringSoon = filteredProducts.filter(product => {
|
const expiringSoon = expiringSoonProducts;
|
||||||
const today = new Date();
|
|
||||||
const expiry = new Date(product.dlcDate || new Date());
|
|
||||||
const diffTime = expiry.getTime() - today.getTime();
|
|
||||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
||||||
return diffDays > 0 && diffDays <= 15;
|
|
||||||
});
|
|
||||||
|
|
||||||
const printContent = `
|
const printContent = `
|
||||||
<html>
|
<html>
|
||||||
@@ -390,11 +370,7 @@ export default function DlcPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const printExpired = () => {
|
const printExpired = () => {
|
||||||
const expired = filteredProducts.filter(product => {
|
const expired = expiredProducts;
|
||||||
const today = new Date();
|
|
||||||
const expiry = new Date(product.dlcDate || new Date());
|
|
||||||
return expiry < today;
|
|
||||||
});
|
|
||||||
|
|
||||||
const printContent = `
|
const printContent = `
|
||||||
<html>
|
<html>
|
||||||
@@ -452,13 +428,27 @@ export default function DlcPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredProducts = (dlcProducts || []).filter(product => {
|
// Products are already filtered server-side, no client filtering needed
|
||||||
if (searchTerm) {
|
const filteredProducts = dlcProducts || [];
|
||||||
return product.productName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
(product.supplier?.name && product.supplier.name.toLowerCase().includes(searchTerm.toLowerCase()));
|
// Memoized calculations for print functions
|
||||||
}
|
const { expiringSoonProducts, expiredProducts } = useMemo(() => {
|
||||||
return true;
|
const today = new Date();
|
||||||
});
|
const expiringSoon = filteredProducts.filter(product => {
|
||||||
|
if (product.status === 'valides') return false;
|
||||||
|
const expiryDate = new Date(product.dlcDate || new Date());
|
||||||
|
const diffDays = Math.ceil((expiryDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
return diffDays <= 15 && diffDays > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const expired = filteredProducts.filter(product => {
|
||||||
|
if (product.status === 'valides') return false;
|
||||||
|
const expiryDate = new Date(product.dlcDate || new Date());
|
||||||
|
return expiryDate <= today;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { expiringSoonProducts: expiringSoon, expiredProducts: expired };
|
||||||
|
}, [filteredProducts]);
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
const {
|
const {
|
||||||
|
|||||||
+2
-3
@@ -986,7 +986,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
return res.status(404).json({ message: "User not found" });
|
return res.status(404).json({ message: "User not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { storeId, status, supplierId } = req.query;
|
const { storeId, status, supplierId, search } = req.query;
|
||||||
let groupIds: number[] | undefined;
|
let groupIds: number[] | undefined;
|
||||||
|
|
||||||
if (user.role === 'admin') {
|
if (user.role === 'admin') {
|
||||||
@@ -1003,10 +1003,9 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const filters: any = {};
|
const filters: any = {};
|
||||||
if (status && status !== 'all') filters.status = status as string;
|
if (status && status !== 'all') filters.status = status as string;
|
||||||
if (supplierId && supplierId !== 'all') filters.supplierId = parseInt(supplierId as string);
|
if (supplierId && supplierId !== 'all') filters.supplierId = parseInt(supplierId as string);
|
||||||
|
if (search) filters.search = search as string;
|
||||||
|
|
||||||
console.log('DLC Products API called with:', { groupIds, filters, userRole: user.role });
|
|
||||||
const dlcProducts = await storage.getDlcProducts(groupIds, filters);
|
const dlcProducts = await storage.getDlcProducts(groupIds, filters);
|
||||||
console.log('DLC Products returned:', dlcProducts.length, 'items');
|
|
||||||
res.json(dlcProducts);
|
res.json(dlcProducts);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching DLC products:", error);
|
console.error("Error fetching DLC products:", error);
|
||||||
|
|||||||
+35
-4
@@ -149,7 +149,7 @@ export interface IStorage {
|
|||||||
deleteCustomerOrder(id: number): Promise<void>;
|
deleteCustomerOrder(id: number): Promise<void>;
|
||||||
|
|
||||||
// DLC Product operations
|
// DLC Product operations
|
||||||
getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; }): Promise<DlcProductWithRelations[]>;
|
getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; search?: string; }): Promise<DlcProductWithRelations[]>;
|
||||||
getDlcProduct(id: number): Promise<DlcProductWithRelations | undefined>;
|
getDlcProduct(id: number): Promise<DlcProductWithRelations | undefined>;
|
||||||
createDlcProduct(dlcProduct: InsertDlcProduct): Promise<DlcProductFrontend>;
|
createDlcProduct(dlcProduct: InsertDlcProduct): Promise<DlcProductFrontend>;
|
||||||
updateDlcProduct(id: number, dlcProduct: Partial<InsertDlcProduct>): Promise<DlcProductFrontend>;
|
updateDlcProduct(id: number, dlcProduct: Partial<InsertDlcProduct>): Promise<DlcProductFrontend>;
|
||||||
@@ -1350,7 +1350,7 @@ export class DatabaseStorage implements IStorage {
|
|||||||
await db.delete(customerOrders).where(eq(customerOrders.id, id));
|
await db.delete(customerOrders).where(eq(customerOrders.id, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDlcProducts(groupIds?: number[]): Promise<DlcProductWithRelations[]> {
|
async getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; search?: string; }): Promise<DlcProductWithRelations[]> {
|
||||||
let query = db
|
let query = db
|
||||||
.select({
|
.select({
|
||||||
dlcProduct: dlcProducts,
|
dlcProduct: dlcProducts,
|
||||||
@@ -1361,8 +1361,31 @@ export class DatabaseStorage implements IStorage {
|
|||||||
.leftJoin(suppliers, eq(dlcProducts.supplierId, suppliers.id))
|
.leftJoin(suppliers, eq(dlcProducts.supplierId, suppliers.id))
|
||||||
.leftJoin(groups, eq(dlcProducts.groupId, groups.id));
|
.leftJoin(groups, eq(dlcProducts.groupId, groups.id));
|
||||||
|
|
||||||
|
const conditions = [];
|
||||||
|
|
||||||
if (groupIds && groupIds.length > 0) {
|
if (groupIds && groupIds.length > 0) {
|
||||||
query = query.where(inArray(dlcProducts.groupId, groupIds));
|
conditions.push(inArray(dlcProducts.groupId, groupIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters) {
|
||||||
|
if (filters.status) {
|
||||||
|
conditions.push(eq(dlcProducts.status, filters.status));
|
||||||
|
}
|
||||||
|
if (filters.supplierId) {
|
||||||
|
conditions.push(eq(dlcProducts.supplierId, filters.supplierId));
|
||||||
|
}
|
||||||
|
if (filters.search) {
|
||||||
|
conditions.push(
|
||||||
|
or(
|
||||||
|
sql`LOWER(${dlcProducts.productName}) LIKE LOWER(${'%' + filters.search + '%'})`,
|
||||||
|
sql`LOWER(${suppliers.name}) LIKE LOWER(${'%' + filters.search + '%'})`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.length > 0) {
|
||||||
|
query = query.where(and(...conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await query.orderBy(asc(dlcProducts.expiryDate));
|
const results = await query.orderBy(asc(dlcProducts.expiryDate));
|
||||||
@@ -2409,7 +2432,7 @@ export class MemStorage implements IStorage {
|
|||||||
this.customerOrders.delete(id);
|
this.customerOrders.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; }): Promise<DlcProductWithRelations[]> {
|
async getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; search?: string; }): Promise<DlcProductWithRelations[]> {
|
||||||
let products = Array.from(this.dlcProducts.values());
|
let products = Array.from(this.dlcProducts.values());
|
||||||
|
|
||||||
if (groupIds && groupIds.length > 0) {
|
if (groupIds && groupIds.length > 0) {
|
||||||
@@ -2423,6 +2446,14 @@ export class MemStorage implements IStorage {
|
|||||||
if (filters.supplierId) {
|
if (filters.supplierId) {
|
||||||
products = products.filter(product => product.supplierId === filters.supplierId);
|
products = products.filter(product => product.supplierId === filters.supplierId);
|
||||||
}
|
}
|
||||||
|
if (filters.search) {
|
||||||
|
const searchTerm = filters.search.toLowerCase();
|
||||||
|
products = products.filter(product => {
|
||||||
|
const supplier = this.suppliers.get(product.supplierId);
|
||||||
|
return product.productName.toLowerCase().includes(searchTerm) ||
|
||||||
|
(supplier && supplier.name.toLowerCase().includes(searchTerm));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return products.map(product => ({
|
return products.map(product => ({
|
||||||
|
|||||||
Reference in new issue
Block a user