mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-23 15:08:22 +02:00
6 files changed
+381
-3
No files matched your search
@@ -15,6 +15,10 @@ run = ["npm", "run", "start"]
|
|||||||
localPort = 5000
|
localPort = 5000
|
||||||
externalPort = 80
|
externalPort = 80
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 38145
|
||||||
|
externalPort = 3003
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 38451
|
localPort = 38451
|
||||||
externalPort = 3002
|
externalPort = 3002
|
||||||
@@ -23,6 +27,10 @@ externalPort = 3002
|
|||||||
localPort = 41343
|
localPort = 41343
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 43467
|
||||||
|
externalPort = 3000
|
||||||
|
|
||||||
[workflows]
|
[workflows]
|
||||||
runButton = "Project"
|
runButton = "Project"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useQueryClient, useMutation } from "@tanstack/react-query";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
|
import { Phone, User, Package, Building } from "lucide-react";
|
||||||
|
import type { CustomerOrderWithRelations } from "@shared/schema";
|
||||||
|
|
||||||
|
interface ClientCallsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
pendingCalls: CustomerOrderWithRelations[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClientCallsModal({ isOpen, onClose, pendingCalls }: ClientCallsModalProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [callingIds, setCallingIds] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
|
const markCalledMutation = useMutation({
|
||||||
|
mutationFn: async (customerOrderId: number) => {
|
||||||
|
return apiRequest(`/api/customer-orders/${customerOrderId}/mark-called`, 'PATCH');
|
||||||
|
},
|
||||||
|
onMutate: (customerOrderId) => {
|
||||||
|
setCallingIds(prev => new Set([...prev, customerOrderId]));
|
||||||
|
},
|
||||||
|
onSuccess: (_, customerOrderId) => {
|
||||||
|
toast({
|
||||||
|
title: "Client marqué comme appelé",
|
||||||
|
description: "L'appel a été enregistré avec succès",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Invalider les caches
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['/api/customer-orders/pending-calls'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['/api/customer-orders'] });
|
||||||
|
},
|
||||||
|
onError: (error, customerOrderId) => {
|
||||||
|
console.error("Error marking client as called:", error);
|
||||||
|
toast({
|
||||||
|
title: "Erreur",
|
||||||
|
description: "Impossible de marquer l'appel client",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSettled: (_, __, customerOrderId) => {
|
||||||
|
setCallingIds(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(customerOrderId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMarkCalled = (customerOrderId: number) => {
|
||||||
|
markCalledMutation.mutate(customerOrderId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Phone className="h-5 w-5 text-orange-600" />
|
||||||
|
Clients à Appeler
|
||||||
|
<Badge variant="secondary">{pendingCalls.length}</Badge>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-600 mb-4">
|
||||||
|
Produits disponibles - Les clients suivants n'ont pas encore été contactés :
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pendingCalls.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<Phone className="h-12 w-12 mx-auto mb-3 text-gray-300" />
|
||||||
|
<p>Aucun client à appeler actuellement</p>
|
||||||
|
<p className="text-xs">Tous les clients ont été contactés ou aucun produit n'est disponible</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ScrollArea className="max-h-[50vh]">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{pendingCalls.map((order) => (
|
||||||
|
<div
|
||||||
|
key={order.id}
|
||||||
|
className="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition-colors"
|
||||||
|
data-testid={`client-call-item-${order.id}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{/* Client Info */}
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<User className="h-4 w-4 text-blue-600 flex-shrink-0" />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="font-medium text-gray-900 truncate">
|
||||||
|
{order.customerName}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600 flex items-center gap-1">
|
||||||
|
<Phone className="h-3 w-3" />
|
||||||
|
{order.customerPhone}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Info */}
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Package className="h-4 w-4 text-green-600 flex-shrink-0" />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-sm text-gray-900 truncate">
|
||||||
|
{order.productDesignation}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Quantité: {order.quantity} • {order.supplier?.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Store Info */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Building className="h-4 w-4 text-purple-600 flex-shrink-0" />
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
{order.group?.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Call Button */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
onClick={() => handleMarkCalled(order.id)}
|
||||||
|
disabled={callingIds.has(order.id)}
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white w-full sm:w-auto"
|
||||||
|
data-testid={`button-mark-called-${order.id}`}
|
||||||
|
>
|
||||||
|
<Phone className="h-4 w-4 mr-2" />
|
||||||
|
{callingIds.has(order.id) ? "Marquage..." : "Marquer Appelé"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-4 border-t">
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
Fermer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Pagination, usePagination } from "@/components/ui/pagination";
|
import { Pagination, usePagination } from "@/components/ui/pagination";
|
||||||
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
@@ -21,14 +22,16 @@ import {
|
|||||||
Edit,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
Building,
|
Building,
|
||||||
User
|
User,
|
||||||
|
Phone
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { safeFormat } from "@/lib/dateUtils";
|
import { safeFormat } from "@/lib/dateUtils";
|
||||||
import CreateOrderModal from "@/components/modals/CreateOrderModal";
|
import CreateOrderModal from "@/components/modals/CreateOrderModal";
|
||||||
import EditOrderModal from "@/components/modals/EditOrderModal";
|
import EditOrderModal from "@/components/modals/EditOrderModal";
|
||||||
import OrderDetailModal from "@/components/modals/OrderDetailModal";
|
import OrderDetailModal from "@/components/modals/OrderDetailModal";
|
||||||
import ConfirmDeleteModal from "@/components/modals/ConfirmDeleteModal";
|
import ConfirmDeleteModal from "@/components/modals/ConfirmDeleteModal";
|
||||||
import type { OrderWithRelations } from "@shared/schema";
|
import ClientCallsModal from "@/components/modals/ClientCallsModal";
|
||||||
|
import type { OrderWithRelations, CustomerOrderWithRelations } from "@shared/schema";
|
||||||
|
|
||||||
export default function Orders() {
|
export default function Orders() {
|
||||||
const { user } = useAuthUnified();
|
const { user } = useAuthUnified();
|
||||||
@@ -65,6 +68,7 @@ export default function Orders() {
|
|||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [showClientCallsModal, setShowClientCallsModal] = useState(false);
|
||||||
const [selectedOrder, setSelectedOrder] = useState<OrderWithRelations | null>(null);
|
const [selectedOrder, setSelectedOrder] = useState<OrderWithRelations | null>(null);
|
||||||
const [orderToDelete, setOrderToDelete] = useState<OrderWithRelations | null>(null);
|
const [orderToDelete, setOrderToDelete] = useState<OrderWithRelations | null>(null);
|
||||||
|
|
||||||
@@ -72,6 +76,22 @@ export default function Orders() {
|
|||||||
// CRITICAL FIX: Appliquer le filtrage par storeId pour TOUS les rôles, pas seulement admin
|
// CRITICAL FIX: Appliquer le filtrage par storeId pour TOUS les rôles, pas seulement admin
|
||||||
const ordersUrl = `/api/orders${selectedStoreId ? `?storeId=${selectedStoreId}` : ''}`;
|
const ordersUrl = `/api/orders${selectedStoreId ? `?storeId=${selectedStoreId}` : ''}`;
|
||||||
|
|
||||||
|
// Query pour les appels clients en attente
|
||||||
|
const pendingCallsUrl = `/api/customer-orders/pending-calls${selectedStoreId ? `?storeId=${selectedStoreId}` : ''}`;
|
||||||
|
const { data: pendingCalls = [], isLoading: isPendingCallsLoading } = useQuery<CustomerOrderWithRelations[]>({
|
||||||
|
queryKey: [pendingCallsUrl, selectedStoreId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(pendingCallsUrl, { credentials: 'include' });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch pending calls');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('📞 Pending calls received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', 'items');
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
refetchInterval: 30000, // Refetch every 30 seconds
|
||||||
|
});
|
||||||
|
|
||||||
const { data: ordersData = [], isLoading } = useQuery<OrderWithRelations[]>({
|
const { data: ordersData = [], isLoading } = useQuery<OrderWithRelations[]>({
|
||||||
queryKey: [ordersUrl, selectedStoreId],
|
queryKey: [ordersUrl, selectedStoreId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -252,6 +272,23 @@ export default function Orders() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Alerte pour les appels clients en attente */}
|
||||||
|
{!isPendingCallsLoading && pendingCalls.length > 0 && (
|
||||||
|
<Alert
|
||||||
|
className="sticky top-0 z-50 bg-orange-50 border-orange-200 cursor-pointer hover:bg-orange-100 transition-colors"
|
||||||
|
onClick={() => setShowClientCallsModal(true)}
|
||||||
|
data-testid="alert-client-calls"
|
||||||
|
>
|
||||||
|
<Phone className="h-4 w-4 text-orange-600" />
|
||||||
|
<AlertDescription className="text-orange-700">
|
||||||
|
<strong>📞 {pendingCalls.length} client{pendingCalls.length > 1 ? 's' : ''} à appeler</strong>
|
||||||
|
<span className="block text-sm mt-1">
|
||||||
|
Produits disponibles - Cliquez pour voir la liste des clients à contacter
|
||||||
|
</span>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg">
|
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg">
|
||||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4">
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4">
|
||||||
@@ -465,6 +502,13 @@ export default function Orders() {
|
|||||||
itemName={orderToDelete ? `${orderToDelete.supplier?.name} - ${safeFormat(orderToDelete.plannedDate, 'dd/MM/yyyy')}` : undefined}
|
itemName={orderToDelete ? `${orderToDelete.supplier?.name} - ${safeFormat(orderToDelete.plannedDate, 'dd/MM/yyyy')}` : undefined}
|
||||||
isLoading={deleteMutation.isPending}
|
isLoading={deleteMutation.isPending}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Modal pour les appels clients */}
|
||||||
|
<ClientCallsModal
|
||||||
|
isOpen={showClientCallsModal}
|
||||||
|
onClose={() => setShowClientCallsModal(false)}
|
||||||
|
pendingCalls={pendingCalls}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2498,6 +2498,80 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Client call tracking routes
|
||||||
|
app.get('/api/customer-orders/pending-calls', 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" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let groupIds: number[] | undefined;
|
||||||
|
|
||||||
|
// Only admin, directeur, and manager can view pending calls
|
||||||
|
if (user.role === 'employee') {
|
||||||
|
return res.status(403).json({ message: "Insufficient permissions" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.role === 'admin') {
|
||||||
|
// Admin can optionally filter by store
|
||||||
|
const { storeId } = req.query;
|
||||||
|
if (storeId) {
|
||||||
|
groupIds = [parseInt(storeId as string)];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For directeur and manager, filter by their assigned groups
|
||||||
|
const userGroupIds = (user as any).userGroups?.map((ug: any) => ug.groupId) || [];
|
||||||
|
groupIds = userGroupIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingCalls = await storage.getPendingClientCalls(groupIds);
|
||||||
|
|
||||||
|
console.log(`📞 Pending client calls fetched: ${pendingCalls.length} calls for user ${user.role}`);
|
||||||
|
res.json(pendingCalls);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching pending client calls:", error);
|
||||||
|
res.status(500).json({ message: "Failed to fetch pending client calls" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.patch('/api/customer-orders/:id/mark-called', 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);
|
||||||
|
|
||||||
|
// Only admin, directeur, and manager can mark calls
|
||||||
|
if (user.role === 'employee') {
|
||||||
|
return res.status(403).json({ message: "Insufficient permissions" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has access to this customer order
|
||||||
|
const customerOrder = await storage.getCustomerOrder(id);
|
||||||
|
if (!customerOrder) {
|
||||||
|
return res.status(404).json({ message: "Customer order not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.role !== 'admin') {
|
||||||
|
const userGroupIds = (user as any).userGroups?.map((ug: any) => ug.groupId) || [];
|
||||||
|
if (!userGroupIds.includes(customerOrder.groupId)) {
|
||||||
|
return res.status(403).json({ message: "Access denied to this customer order" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedOrder = await storage.markClientCalled(id, user.id);
|
||||||
|
|
||||||
|
console.log(`📞 Client marked as called: order ${id} by user ${user.id} (${user.role})`);
|
||||||
|
res.json(updatedOrder);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error marking client as called:", error);
|
||||||
|
res.status(500).json({ message: "Failed to mark client as called" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Avoir routes
|
// Avoir routes
|
||||||
app.get('/api/avoirs', isAuthenticated, async (req: any, res) => {
|
app.get('/api/avoirs', isAuthenticated, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -180,6 +180,10 @@ export interface IStorage {
|
|||||||
updateCustomerOrder(id: number, customerOrder: Partial<InsertCustomerOrder>): Promise<CustomerOrder>;
|
updateCustomerOrder(id: number, customerOrder: Partial<InsertCustomerOrder>): Promise<CustomerOrder>;
|
||||||
deleteCustomerOrder(id: number): Promise<void>;
|
deleteCustomerOrder(id: number): Promise<void>;
|
||||||
|
|
||||||
|
// Client call tracking
|
||||||
|
getPendingClientCalls(groupIds?: number[]): Promise<CustomerOrderWithRelations[]>;
|
||||||
|
markClientCalled(customerOrderId: number, calledBy: string): Promise<CustomerOrder>;
|
||||||
|
|
||||||
// DLC Product operations
|
// DLC Product operations
|
||||||
getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; search?: string; }): 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>;
|
||||||
@@ -1676,6 +1680,50 @@ export class DatabaseStorage implements IStorage {
|
|||||||
await db.delete(customerOrders).where(eq(customerOrders.id, id));
|
await db.delete(customerOrders).where(eq(customerOrders.id, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPendingClientCalls(groupIds?: number[]): Promise<CustomerOrderWithRelations[]> {
|
||||||
|
const conditions = [
|
||||||
|
eq(dlcProducts.status, 'valides'),
|
||||||
|
eq(customerOrders.customerNotified, false)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (groupIds && groupIds.length > 0) {
|
||||||
|
conditions.push(inArray(customerOrders.groupId, groupIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await db
|
||||||
|
.select({
|
||||||
|
customerOrder: customerOrders,
|
||||||
|
supplier: suppliers,
|
||||||
|
group: groups
|
||||||
|
})
|
||||||
|
.from(customerOrders)
|
||||||
|
.innerJoin(dlcProducts, eq(customerOrders.gencode, dlcProducts.gencode))
|
||||||
|
.leftJoin(suppliers, eq(customerOrders.supplierId, suppliers.id))
|
||||||
|
.leftJoin(groups, eq(customerOrders.groupId, groups.id))
|
||||||
|
.where(and(...conditions))
|
||||||
|
.orderBy(desc(customerOrders.createdAt));
|
||||||
|
|
||||||
|
return results.map(result => ({
|
||||||
|
...result.customerOrder,
|
||||||
|
supplier: result.supplier!,
|
||||||
|
group: result.group!,
|
||||||
|
creator: null // Will be populated if needed
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async markClientCalled(customerOrderId: number, calledBy: string): Promise<CustomerOrder> {
|
||||||
|
const [updated] = await db
|
||||||
|
.update(customerOrders)
|
||||||
|
.set({
|
||||||
|
customerNotified: true,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(eq(customerOrders.id, customerOrderId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
async getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; search?: string; }): Promise<DlcProductWithRelations[]> {
|
async getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; search?: string; }): Promise<DlcProductWithRelations[]> {
|
||||||
let query = db
|
let query = db
|
||||||
.select({
|
.select({
|
||||||
@@ -3421,6 +3469,49 @@ export class MemStorage implements IStorage {
|
|||||||
this.customerOrders.delete(id);
|
this.customerOrders.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPendingClientCalls(groupIds?: number[]): Promise<CustomerOrderWithRelations[]> {
|
||||||
|
let orders = Array.from(this.customerOrders.values());
|
||||||
|
|
||||||
|
// Filter by groupIds if provided
|
||||||
|
if (groupIds && groupIds.length > 0) {
|
||||||
|
orders = orders.filter(order => groupIds.includes(order.groupId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find orders where client not notified and product is available
|
||||||
|
const pendingOrders = orders.filter(order => {
|
||||||
|
if (order.customerNotified) return false;
|
||||||
|
|
||||||
|
// Check if there's a DLC product with status 'valides' matching this gencode
|
||||||
|
const matchingDlcProduct = Array.from(this.dlcProducts.values())
|
||||||
|
.find(dlc => dlc.gencode === order.gencode && dlc.status === 'valides');
|
||||||
|
|
||||||
|
return !!matchingDlcProduct;
|
||||||
|
});
|
||||||
|
|
||||||
|
return pendingOrders.map(order => ({
|
||||||
|
...order,
|
||||||
|
supplier: this.suppliers.get(order.supplierId)!,
|
||||||
|
group: this.groups.get(order.groupId)!,
|
||||||
|
creator: this.users.get(order.createdBy)!
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async markClientCalled(customerOrderId: number, calledBy: string): Promise<CustomerOrder> {
|
||||||
|
const order = this.customerOrders.get(customerOrderId);
|
||||||
|
if (!order) {
|
||||||
|
throw new Error(`CustomerOrder with id ${customerOrderId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedOrder = {
|
||||||
|
...order,
|
||||||
|
customerNotified: true,
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.customerOrders.set(customerOrderId, updatedOrder);
|
||||||
|
return updatedOrder;
|
||||||
|
}
|
||||||
|
|
||||||
async getDlcProducts(groupIds?: number[], filters?: { status?: string; supplierId?: number; search?: string; }): 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());
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -14,7 +14,12 @@ export default {
|
|||||||
'p-3', 'p-4', 'px-3', 'px-4', 'py-2', 'py-3',
|
'p-3', 'p-4', 'px-3', 'px-4', 'py-2', 'py-3',
|
||||||
'gap-2', 'gap-3', 'gap-4', 'sm:gap-4',
|
'gap-2', 'gap-3', 'gap-4', 'sm:gap-4',
|
||||||
'order-1', 'order-2', 'sm:order-1', 'sm:order-2',
|
'order-1', 'order-2', 'sm:order-1', 'sm:order-2',
|
||||||
'flex-col', 'sm:flex-row', 'justify-stretch', 'sm:justify-end'
|
'flex-col', 'sm:flex-row', 'justify-stretch', 'sm:justify-end',
|
||||||
|
// Classes pour alertes appels clients (production-safe)
|
||||||
|
'bg-orange-50', 'border-orange-200', 'text-orange-700', 'bg-orange-600', 'hover:bg-orange-700',
|
||||||
|
'bg-green-600', 'hover:bg-green-700', 'bg-blue-600', 'hover:bg-blue-700',
|
||||||
|
'sticky', 'top-0', 'z-50', 'cursor-pointer',
|
||||||
|
'max-w-2xl', 'w-full', 'mx-4', 'max-h-[80vh]', 'overflow-y-auto'
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
|||||||
Reference in new issue
Block a user