mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-19 05:07:57 +02:00
9 files changed
+515
-7
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
@@ -25,6 +25,7 @@ import Avoirs from "@/pages/Avoirs";
|
|||||||
import WeatherSettings from "@/pages/WeatherSettings";
|
import WeatherSettings from "@/pages/WeatherSettings";
|
||||||
import Analytics from "@/pages/Analytics";
|
import Analytics from "@/pages/Analytics";
|
||||||
import SalesAnalysisPage from "@/pages/SalesAnalysisPage";
|
import SalesAnalysisPage from "@/pages/SalesAnalysisPage";
|
||||||
|
import PaymentSchedulePage from "@/pages/PaymentSchedulePage";
|
||||||
import Layout from "@/components/Layout";
|
import Layout from "@/components/Layout";
|
||||||
|
|
||||||
function RouterProduction() {
|
function RouterProduction() {
|
||||||
@@ -106,6 +107,7 @@ function RouterProduction() {
|
|||||||
<Route path="/avoirs" component={Avoirs} />
|
<Route path="/avoirs" component={Avoirs} />
|
||||||
<Route path="/analytics" component={Analytics} />
|
<Route path="/analytics" component={Analytics} />
|
||||||
<Route path="/sales-analysis" component={SalesAnalysisPage} />
|
<Route path="/sales-analysis" component={SalesAnalysisPage} />
|
||||||
|
<Route path="/payment-schedule" component={PaymentSchedulePage} />
|
||||||
|
|
||||||
{/* Routes de compatibilité - redirection vers utilities */}
|
{/* Routes de compatibilité - redirection vers utilities */}
|
||||||
<Route path="/backup" component={Utilities} />
|
<Route path="/backup" component={Utilities} />
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ import {
|
|||||||
FileUp,
|
FileUp,
|
||||||
Send,
|
Send,
|
||||||
Receipt,
|
Receipt,
|
||||||
TrendingUp
|
TrendingUp,
|
||||||
|
CreditCard
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
@@ -270,6 +271,12 @@ export default function Sidebar() {
|
|||||||
icon: TrendingUp,
|
icon: TrendingUp,
|
||||||
roles: ["admin", "directeur"]
|
roles: ["admin", "directeur"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/payment-schedule",
|
||||||
|
label: "Échéance",
|
||||||
|
icon: CreditCard,
|
||||||
|
roles: ["admin", "directeur"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/avoirs",
|
path: "/avoirs",
|
||||||
label: "Avoirs",
|
label: "Avoirs",
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export default function Groups() {
|
|||||||
nocodbBlColumnName: "",
|
nocodbBlColumnName: "",
|
||||||
nocodbAmountColumnName: "",
|
nocodbAmountColumnName: "",
|
||||||
nocodbSupplierColumnName: "",
|
nocodbSupplierColumnName: "",
|
||||||
|
nocodbDueDateColumnName: "",
|
||||||
webhookUrl: "",
|
webhookUrl: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,9 +102,13 @@ export default function Groups() {
|
|||||||
name: "",
|
name: "",
|
||||||
color: "#1976D2",
|
color: "#1976D2",
|
||||||
nocodbConfigId: "",
|
nocodbConfigId: "",
|
||||||
nocodbTableId: "",
|
|
||||||
nocodbTableName: "",
|
nocodbTableName: "",
|
||||||
invoiceColumnName: "Ref Facture",
|
invoiceColumnName: "",
|
||||||
|
nocodbBlColumnName: "",
|
||||||
|
nocodbAmountColumnName: "",
|
||||||
|
nocodbSupplierColumnName: "",
|
||||||
|
nocodbDueDateColumnName: "",
|
||||||
|
webhookUrl: "",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -142,9 +147,13 @@ export default function Groups() {
|
|||||||
name: "",
|
name: "",
|
||||||
color: "#1976D2",
|
color: "#1976D2",
|
||||||
nocodbConfigId: "",
|
nocodbConfigId: "",
|
||||||
nocodbTableId: "",
|
|
||||||
nocodbTableName: "",
|
nocodbTableName: "",
|
||||||
invoiceColumnName: "Ref Facture",
|
invoiceColumnName: "",
|
||||||
|
nocodbBlColumnName: "",
|
||||||
|
nocodbAmountColumnName: "",
|
||||||
|
nocodbSupplierColumnName: "",
|
||||||
|
nocodbDueDateColumnName: "",
|
||||||
|
webhookUrl: "",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -251,6 +260,7 @@ export default function Groups() {
|
|||||||
nocodbBlColumnName: "",
|
nocodbBlColumnName: "",
|
||||||
nocodbAmountColumnName: "",
|
nocodbAmountColumnName: "",
|
||||||
nocodbSupplierColumnName: "",
|
nocodbSupplierColumnName: "",
|
||||||
|
nocodbDueDateColumnName: "",
|
||||||
webhookUrl: "",
|
webhookUrl: "",
|
||||||
});
|
});
|
||||||
setShowCreateModal(true);
|
setShowCreateModal(true);
|
||||||
@@ -267,6 +277,7 @@ export default function Groups() {
|
|||||||
nocodbBlColumnName: group.nocodbBlColumnName || "",
|
nocodbBlColumnName: group.nocodbBlColumnName || "",
|
||||||
nocodbAmountColumnName: group.nocodbAmountColumnName || "",
|
nocodbAmountColumnName: group.nocodbAmountColumnName || "",
|
||||||
nocodbSupplierColumnName: group.nocodbSupplierColumnName || "",
|
nocodbSupplierColumnName: group.nocodbSupplierColumnName || "",
|
||||||
|
nocodbDueDateColumnName: group.nocodbDueDateColumnName || "",
|
||||||
webhookUrl: group.webhookUrl || "",
|
webhookUrl: group.webhookUrl || "",
|
||||||
});
|
});
|
||||||
setShowEditModal(true);
|
setShowEditModal(true);
|
||||||
@@ -631,6 +642,16 @@ export default function Groups() {
|
|||||||
placeholder="ex: Fournisseur"
|
placeholder="ex: Fournisseur"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="nocodbDueDateColumnName">Colonne Date d'Échéance</Label>
|
||||||
|
<Input
|
||||||
|
id="nocodbDueDateColumnName"
|
||||||
|
value={formData.nocodbDueDateColumnName}
|
||||||
|
onChange={(e) => handleChange('nocodbDueDateColumnName', e.target.value)}
|
||||||
|
placeholder="ex: Date_Echeance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -693,6 +714,7 @@ export default function Groups() {
|
|||||||
nocodbBlColumnName: "",
|
nocodbBlColumnName: "",
|
||||||
nocodbAmountColumnName: "",
|
nocodbAmountColumnName: "",
|
||||||
nocodbSupplierColumnName: "",
|
nocodbSupplierColumnName: "",
|
||||||
|
nocodbDueDateColumnName: "",
|
||||||
webhookUrl: "",
|
webhookUrl: "",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useStore } from "@/contexts/StoreContext";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Loader2, Calendar, DollarSign, Building, CreditCard, AlertCircle } from "lucide-react";
|
||||||
|
import { format, parseISO, startOfMonth, endOfMonth, eachMonthOfInterval, isWithinInterval } from "date-fns";
|
||||||
|
import { fr } from "date-fns/locale";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
interface PaymentSchedule {
|
||||||
|
id: string | number;
|
||||||
|
invoiceReference: string;
|
||||||
|
dueDate: string;
|
||||||
|
amount: number;
|
||||||
|
supplierName: string;
|
||||||
|
paymentMethod: string | null;
|
||||||
|
groupId: number;
|
||||||
|
groupName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaymentScheduleResponse {
|
||||||
|
schedules: PaymentSchedule[];
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PaymentSchedulePage() {
|
||||||
|
const { selectedStoreId } = useStore();
|
||||||
|
const [selectedMonth, setSelectedMonth] = useState<string>(() => {
|
||||||
|
const now = new Date();
|
||||||
|
return format(now, 'yyyy-MM');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Récupérer les échéances
|
||||||
|
const { data, isLoading, error } = useQuery<PaymentScheduleResponse>({
|
||||||
|
queryKey: ['/api/payment-schedule', selectedStoreId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`/api/payment-schedule?groupId=${selectedStoreId}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erreur ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
enabled: !!selectedStoreId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculer les mois disponibles (12 derniers mois + 12 prochains mois)
|
||||||
|
const availableMonths = useMemo(() => {
|
||||||
|
const now = new Date();
|
||||||
|
const startDate = new Date(now.getFullYear(), now.getMonth() - 12, 1);
|
||||||
|
const endDate = new Date(now.getFullYear(), now.getMonth() + 12, 1);
|
||||||
|
|
||||||
|
return eachMonthOfInterval({ start: startDate, end: endDate })
|
||||||
|
.map(date => ({
|
||||||
|
value: format(date, 'yyyy-MM'),
|
||||||
|
label: format(date, 'MMMM yyyy', { locale: fr })
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Filtrer les échéances par mois sélectionné
|
||||||
|
const filteredSchedules = useMemo(() => {
|
||||||
|
if (!data?.schedules) return [];
|
||||||
|
|
||||||
|
const monthStart = parseISO(`${selectedMonth}-01`);
|
||||||
|
const monthEnd = endOfMonth(monthStart);
|
||||||
|
|
||||||
|
return data.schedules
|
||||||
|
.filter(schedule => {
|
||||||
|
try {
|
||||||
|
const dueDate = parseISO(schedule.dueDate);
|
||||||
|
return isWithinInterval(dueDate, { start: monthStart, end: monthEnd });
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const dateA = parseISO(a.dueDate);
|
||||||
|
const dateB = parseISO(b.dueDate);
|
||||||
|
return dateA.getTime() - dateB.getTime();
|
||||||
|
});
|
||||||
|
}, [data?.schedules, selectedMonth]);
|
||||||
|
|
||||||
|
// Calculer le total du mois
|
||||||
|
const monthTotal = useMemo(() => {
|
||||||
|
return filteredSchedules.reduce((sum, schedule) => sum + schedule.amount, 0);
|
||||||
|
}, [filteredSchedules]);
|
||||||
|
|
||||||
|
// Grouper par mode de paiement
|
||||||
|
const paymentMethodTotals = useMemo(() => {
|
||||||
|
const totals: Record<string, number> = {};
|
||||||
|
filteredSchedules.forEach(schedule => {
|
||||||
|
const method = schedule.paymentMethod || 'Non défini';
|
||||||
|
totals[method] = (totals[method] || 0) + schedule.amount;
|
||||||
|
});
|
||||||
|
return totals;
|
||||||
|
}, [filteredSchedules]);
|
||||||
|
|
||||||
|
if (!selectedStoreId) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<Card className="w-96">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<AlertCircle className="w-12 h-12 text-orange-500 mx-auto mb-4" />
|
||||||
|
<CardTitle>Sélectionner un magasin</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Veuillez sélectionner un magasin pour afficher l'échéancier
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<Card className="w-96">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
|
||||||
|
<CardTitle>Erreur</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Une erreur est survenue lors du chargement des échéances
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.message) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<Card className="w-96">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<AlertCircle className="w-12 h-12 text-blue-500 mx-auto mb-4" />
|
||||||
|
<CardTitle>Configuration requise</CardTitle>
|
||||||
|
<CardDescription>{data.message}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* En-tête */}
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Échéancier des Paiements</h1>
|
||||||
|
<p className="text-gray-600 mt-1">Gestion des échéances fournisseurs</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-64">
|
||||||
|
<Select value={selectedMonth} onValueChange={setSelectedMonth}>
|
||||||
|
<SelectTrigger data-testid="select-month">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableMonths.map(month => (
|
||||||
|
<SelectItem key={month.value} value={month.value}>
|
||||||
|
{month.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Statistiques du mois */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total du Mois</CardTitle>
|
||||||
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold" data-testid="text-month-total">
|
||||||
|
{monthTotal.toFixed(2)} €
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{filteredSchedules.length} échéance{filteredSchedules.length > 1 ? 's' : ''}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Modes de Paiement</CardTitle>
|
||||||
|
<CreditCard className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
{Object.entries(paymentMethodTotals).map(([method, total]) => (
|
||||||
|
<div key={method} className="flex justify-between">
|
||||||
|
<span className="text-gray-600">{method}:</span>
|
||||||
|
<span className="font-semibold">{total.toFixed(2)} €</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Période</CardTitle>
|
||||||
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold capitalize">
|
||||||
|
{format(parseISO(`${selectedMonth}-01`), 'MMMM', { locale: fr })}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{format(parseISO(`${selectedMonth}-01`), 'yyyy', { locale: fr })}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Liste des échéances */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Échéances du Mois</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Liste détaillée des paiements à effectuer pour le mois sélectionné
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{filteredSchedules.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
Aucune échéance pour ce mois
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{filteredSchedules.map((schedule, index) => (
|
||||||
|
<div
|
||||||
|
key={`${schedule.id}-${index}`}
|
||||||
|
className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
data-testid={`schedule-item-${index}`}
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Calendar className="w-4 h-4 text-blue-600" />
|
||||||
|
<span className="font-semibold">
|
||||||
|
{format(parseISO(schedule.dueDate), 'dd/MM/yyyy', { locale: fr })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 space-y-1 text-sm text-gray-600">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Building className="w-3 h-3" />
|
||||||
|
<span>{schedule.supplierName}</span>
|
||||||
|
</div>
|
||||||
|
<div>Facture: {schedule.invoiceReference}</div>
|
||||||
|
{schedule.paymentMethod && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CreditCard className="w-3 h-3" />
|
||||||
|
<span>{schedule.paymentMethod}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-xl font-bold text-blue-600">
|
||||||
|
{schedule.amount.toFixed(2)} €
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Loader2, TrendingUp, AlertCircle } from "lucide-react";
|
import { Loader2, TrendingUp, AlertCircle, RefreshCw } from "lucide-react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
@@ -12,10 +13,18 @@ interface UtilitiesConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SalesAnalysisPage() {
|
export default function SalesAnalysisPage() {
|
||||||
|
// State pour forcer le rechargement de l'iframe
|
||||||
|
const [iframeKey, setIframeKey] = useState(0);
|
||||||
|
|
||||||
// Récupérer l'URL configurée
|
// Récupérer l'URL configurée
|
||||||
const { data: config, isLoading } = useQuery<UtilitiesConfig>({
|
const { data: config, isLoading } = useQuery<UtilitiesConfig>({
|
||||||
queryKey: ['/api/utilities']
|
queryKey: ['/api/utilities']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fonction pour recharger l'iframe
|
||||||
|
const handleRefresh = () => {
|
||||||
|
setIframeKey(prev => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -52,8 +61,24 @@ export default function SalesAnalysisPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-screen">
|
<div className="w-full h-screen relative">
|
||||||
|
{/* Bouton de rechargement flottant */}
|
||||||
|
<div className="absolute top-4 right-4 z-10">
|
||||||
|
<Button
|
||||||
|
onClick={handleRefresh}
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="shadow-lg"
|
||||||
|
data-testid="button-refresh-iframe"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
Recharger
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* iframe avec clé pour forcer le rechargement */}
|
||||||
<iframe
|
<iframe
|
||||||
|
key={iframeKey}
|
||||||
src={salesAnalysisUrl}
|
src={salesAnalysisUrl}
|
||||||
className="w-full h-full border-0"
|
className="w-full h-full border-0"
|
||||||
title="Analyse des Ventes"
|
title="Analyse des Ventes"
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
@@ -39,6 +46,7 @@ export default function Suppliers() {
|
|||||||
hasDlc: false,
|
hasDlc: false,
|
||||||
automaticReconciliation: false,
|
automaticReconciliation: false,
|
||||||
requiresControl: false,
|
requiresControl: false,
|
||||||
|
paymentMethod: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: suppliers = [], isLoading } = useQuery<Supplier[]>({
|
const { data: suppliers = [], isLoading } = useQuery<Supplier[]>({
|
||||||
@@ -193,6 +201,7 @@ export default function Suppliers() {
|
|||||||
hasDlc: false,
|
hasDlc: false,
|
||||||
automaticReconciliation: false,
|
automaticReconciliation: false,
|
||||||
requiresControl: false,
|
requiresControl: false,
|
||||||
|
paymentMethod: "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -230,6 +239,7 @@ export default function Suppliers() {
|
|||||||
hasDlc: supplier.hasDlc || false,
|
hasDlc: supplier.hasDlc || false,
|
||||||
automaticReconciliation: supplier.automaticReconciliation || false,
|
automaticReconciliation: supplier.automaticReconciliation || false,
|
||||||
requiresControl: supplier.requiresControl || false,
|
requiresControl: supplier.requiresControl || false,
|
||||||
|
paymentMethod: supplier.paymentMethod || "",
|
||||||
});
|
});
|
||||||
setShowEditModal(true);
|
setShowEditModal(true);
|
||||||
};
|
};
|
||||||
@@ -399,6 +409,13 @@ export default function Suppliers() {
|
|||||||
{supplier.phone}
|
{supplier.phone}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{supplier.paymentMethod && (
|
||||||
|
<div className="flex items-center text-sm text-gray-600">
|
||||||
|
<Package className="w-4 h-4 mr-2" />
|
||||||
|
<span className="font-medium">Mode de paiement:</span>
|
||||||
|
<span className="ml-1">{supplier.paymentMethod}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{supplier.hasDlc && (
|
{supplier.hasDlc && (
|
||||||
@@ -511,6 +528,25 @@ export default function Suppliers() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="paymentMethod">Mode de paiement</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.paymentMethod}
|
||||||
|
onValueChange={(value) => handleChange('paymentMethod', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="paymentMethod" data-testid="select-payment-method">
|
||||||
|
<SelectValue placeholder="Sélectionner un mode de paiement" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="">Aucun</SelectItem>
|
||||||
|
<SelectItem value="Virement">Virement</SelectItem>
|
||||||
|
<SelectItem value="Traite">Traite</SelectItem>
|
||||||
|
<SelectItem value="Traite Magnétique">Traite Magnétique</SelectItem>
|
||||||
|
<SelectItem value="Chèque">Chèque</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="hasDlc"
|
id="hasDlc"
|
||||||
|
|||||||
@@ -278,6 +278,132 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Route pour récupérer l'échéancier des paiements fournisseurs
|
||||||
|
app.get('/api/payment-schedule', isAuthenticated, async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.claims?.sub || req.user?.id;
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(401).json({ error: 'Utilisateur non authentifié' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await storage.getUser(userId);
|
||||||
|
if (!user || (user.role !== 'admin' && user.role !== 'directeur')) {
|
||||||
|
return res.status(403).json({ error: 'Accès refusé - Admin ou Directeur uniquement' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation du groupId avec Zod
|
||||||
|
const groupIdSchema = z.coerce.number().int().positive();
|
||||||
|
const validation = groupIdSchema.safeParse(req.query.groupId);
|
||||||
|
|
||||||
|
if (!validation.success) {
|
||||||
|
return res.status(400).json({ error: 'groupId invalide - doit être un entier positif' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupId = validation.data;
|
||||||
|
|
||||||
|
// Vérifier l'autorisation : directeur ne peut voir que ses groupes
|
||||||
|
if (user.role === 'directeur') {
|
||||||
|
const userGroupIds = await storage.getUserGroups(user.id);
|
||||||
|
if (!userGroupIds.includes(groupId)) {
|
||||||
|
return res.status(403).json({ error: 'Accès refusé - Vous ne pouvez accéder qu\'aux données de votre groupe' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer le groupe avec la configuration NocoDB
|
||||||
|
const group = await storage.getGroup(groupId);
|
||||||
|
if (!group || !group.nocodbConfigId || !group.nocodbTableId) {
|
||||||
|
return res.json({ schedules: [], message: 'Pas de configuration NocoDB pour ce groupe' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer la configuration NocoDB
|
||||||
|
const nocodbConfig = await storage.getNocodbConfig(group.nocodbConfigId);
|
||||||
|
if (!nocodbConfig) {
|
||||||
|
return res.json({ schedules: [], message: 'Configuration NocoDB non trouvée' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que les colonnes nécessaires sont configurées
|
||||||
|
if (!group.nocodbDueDateColumnName || !group.invoiceColumnName || !group.nocodbAmountColumnName) {
|
||||||
|
return res.json({
|
||||||
|
schedules: [],
|
||||||
|
message: 'Configuration incomplète: colonnes date échéance, facture ou montant manquantes'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer tous les enregistrements de NocoDB
|
||||||
|
const url = `${nocodbConfig.baseUrl}/api/v1/db/data/noco/${nocodbConfig.projectId}/${group.nocodbTableId}`;
|
||||||
|
|
||||||
|
console.log('📅 Récupération échéances NocoDB:', {
|
||||||
|
url,
|
||||||
|
groupId,
|
||||||
|
dueDateColumn: group.nocodbDueDateColumnName
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"xc-token": nocodbConfig.apiToken,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('❌ Erreur NocoDB:', response.status, response.statusText);
|
||||||
|
return res.status(500).json({ error: 'Erreur lors de la récupération des données NocoDB' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Récupérer tous les fournisseurs pour le mapping
|
||||||
|
const allSuppliers = await storage.getSuppliers();
|
||||||
|
|
||||||
|
// Fonction de normalisation pour comparer les noms de fournisseurs
|
||||||
|
const normalizeName = (name: string) => {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.normalize("NFD") // Décompose les accents
|
||||||
|
.replace(/[\u0300-\u036f]/g, "") // Supprime les diacritiques
|
||||||
|
.replace(/\s+/g, ' '); // Normalise les espaces multiples
|
||||||
|
};
|
||||||
|
|
||||||
|
// Créer une map avec noms normalisés
|
||||||
|
const supplierMap = new Map(allSuppliers.map(s => [normalizeName(s.name), s]));
|
||||||
|
|
||||||
|
// Traiter les données et filtrer les enregistrements avec une date d'échéance
|
||||||
|
const schedules = (data.list || [])
|
||||||
|
.map((record: any) => {
|
||||||
|
const dueDate = record[group.nocodbDueDateColumnName!];
|
||||||
|
const invoiceRef = record[group.invoiceColumnName!];
|
||||||
|
const amount = record[group.nocodbAmountColumnName!];
|
||||||
|
const supplierName = record[group.nocodbSupplierColumnName || 'Fournisseur'];
|
||||||
|
|
||||||
|
if (!dueDate) return null;
|
||||||
|
|
||||||
|
// Chercher le fournisseur correspondant avec normalisation
|
||||||
|
const normalizedSupplierName = normalizeName(supplierName || '');
|
||||||
|
const supplier = supplierMap.get(normalizedSupplierName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: record.Id || record.id,
|
||||||
|
invoiceReference: invoiceRef,
|
||||||
|
dueDate: dueDate,
|
||||||
|
amount: amount ? parseFloat(amount) : 0,
|
||||||
|
supplierName: supplierName,
|
||||||
|
paymentMethod: supplier?.paymentMethod || null,
|
||||||
|
groupId: group.id,
|
||||||
|
groupName: group.name
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((s: any) => s !== null);
|
||||||
|
|
||||||
|
res.json({ schedules });
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Erreur récupération échéances:', error);
|
||||||
|
res.status(500).json({ error: 'Erreur serveur', details: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Route BAP pour envoi webhook n8n
|
// Route BAP pour envoi webhook n8n
|
||||||
app.post('/api/bap/send-webhook', isAuthenticated, async (req: any, res) => {
|
app.post('/api/bap/send-webhook', isAuthenticated, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export const groups = pgTable("groups", {
|
|||||||
nocodbBlColumnName: varchar("nocodb_bl_column_name"), // Nom de la colonne BL dans leur table
|
nocodbBlColumnName: varchar("nocodb_bl_column_name"), // Nom de la colonne BL dans leur table
|
||||||
nocodbAmountColumnName: varchar("nocodb_amount_column_name"), // Nom de la colonne montant dans leur table
|
nocodbAmountColumnName: varchar("nocodb_amount_column_name"), // Nom de la colonne montant dans leur table
|
||||||
nocodbSupplierColumnName: varchar("nocodb_supplier_column_name"), // Nom de la colonne fournisseur dans leur table
|
nocodbSupplierColumnName: varchar("nocodb_supplier_column_name"), // Nom de la colonne fournisseur dans leur table
|
||||||
|
nocodbDueDateColumnName: varchar("nocodb_due_date_column_name"), // Nom de la colonne date d'échéance dans leur table
|
||||||
webhookUrl: varchar("webhook_url", { length: 500 }), // URL de webhook pour notifications par magasin
|
webhookUrl: varchar("webhook_url", { length: 500 }), // URL de webhook pour notifications par magasin
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
@@ -79,6 +80,7 @@ export const suppliers = pgTable("suppliers", {
|
|||||||
contact: varchar("contact"),
|
contact: varchar("contact"),
|
||||||
phone: varchar("phone"),
|
phone: varchar("phone"),
|
||||||
hasDlc: boolean("has_dlc").default(false), // Coche DLC pour la gestion DLC
|
hasDlc: boolean("has_dlc").default(false), // Coche DLC pour la gestion DLC
|
||||||
|
paymentMethod: varchar("payment_method"), // Mode de paiement: Virement, Traite, Traite Magnétique, Chèque
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
automaticReconciliation: boolean("automatic_reconciliation").default(false), // Rapprochement automatique BL/Factures
|
automaticReconciliation: boolean("automatic_reconciliation").default(false), // Rapprochement automatique BL/Factures
|
||||||
|
|||||||
Reference in new issue
Block a user