mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
Add payment schedule view with filtering and supplier payment methods
Adds a new payment schedule page to display due dates and amounts, incorporating supplier payment methods and date filtering. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 1e4433b5-47ab-464c-b663-fea2e53367dd Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: e9c65355-546d-41f7-885b-728c905f4497 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/1e4433b5-47ab-464c-b663-fea2e53367dd/9uIte90
This commit is contained in:
1 parent
eeb9b5b34b
commit
6f298c4df7
7 files changed
+488
-5
No files matched your search
@@ -25,6 +25,7 @@ import Avoirs from "@/pages/Avoirs";
|
||||
import WeatherSettings from "@/pages/WeatherSettings";
|
||||
import Analytics from "@/pages/Analytics";
|
||||
import SalesAnalysisPage from "@/pages/SalesAnalysisPage";
|
||||
import PaymentSchedulePage from "@/pages/PaymentSchedulePage";
|
||||
import Layout from "@/components/Layout";
|
||||
|
||||
function RouterProduction() {
|
||||
@@ -106,6 +107,7 @@ function RouterProduction() {
|
||||
<Route path="/avoirs" component={Avoirs} />
|
||||
<Route path="/analytics" component={Analytics} />
|
||||
<Route path="/sales-analysis" component={SalesAnalysisPage} />
|
||||
<Route path="/payment-schedule" component={PaymentSchedulePage} />
|
||||
|
||||
{/* Routes de compatibilité - redirection vers utilities */}
|
||||
<Route path="/backup" component={Utilities} />
|
||||
|
||||
@@ -35,7 +35,8 @@ import {
|
||||
FileUp,
|
||||
Send,
|
||||
Receipt,
|
||||
TrendingUp
|
||||
TrendingUp,
|
||||
CreditCard
|
||||
} from "lucide-react";
|
||||
|
||||
export default function Sidebar() {
|
||||
@@ -270,6 +271,12 @@ export default function Sidebar() {
|
||||
icon: TrendingUp,
|
||||
roles: ["admin", "directeur"]
|
||||
},
|
||||
{
|
||||
path: "/payment-schedule",
|
||||
label: "Échéance",
|
||||
icon: CreditCard,
|
||||
roles: ["admin", "directeur"]
|
||||
},
|
||||
{
|
||||
path: "/avoirs",
|
||||
label: "Avoirs",
|
||||
|
||||
@@ -51,6 +51,7 @@ export default function Groups() {
|
||||
nocodbBlColumnName: "",
|
||||
nocodbAmountColumnName: "",
|
||||
nocodbSupplierColumnName: "",
|
||||
nocodbDueDateColumnName: "",
|
||||
webhookUrl: "",
|
||||
});
|
||||
|
||||
@@ -101,9 +102,13 @@ export default function Groups() {
|
||||
name: "",
|
||||
color: "#1976D2",
|
||||
nocodbConfigId: "",
|
||||
nocodbTableId: "",
|
||||
nocodbTableName: "",
|
||||
invoiceColumnName: "Ref Facture",
|
||||
invoiceColumnName: "",
|
||||
nocodbBlColumnName: "",
|
||||
nocodbAmountColumnName: "",
|
||||
nocodbSupplierColumnName: "",
|
||||
nocodbDueDateColumnName: "",
|
||||
webhookUrl: "",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -142,9 +147,13 @@ export default function Groups() {
|
||||
name: "",
|
||||
color: "#1976D2",
|
||||
nocodbConfigId: "",
|
||||
nocodbTableId: "",
|
||||
nocodbTableName: "",
|
||||
invoiceColumnName: "Ref Facture",
|
||||
invoiceColumnName: "",
|
||||
nocodbBlColumnName: "",
|
||||
nocodbAmountColumnName: "",
|
||||
nocodbSupplierColumnName: "",
|
||||
nocodbDueDateColumnName: "",
|
||||
webhookUrl: "",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -251,6 +260,7 @@ export default function Groups() {
|
||||
nocodbBlColumnName: "",
|
||||
nocodbAmountColumnName: "",
|
||||
nocodbSupplierColumnName: "",
|
||||
nocodbDueDateColumnName: "",
|
||||
webhookUrl: "",
|
||||
});
|
||||
setShowCreateModal(true);
|
||||
@@ -267,6 +277,7 @@ export default function Groups() {
|
||||
nocodbBlColumnName: group.nocodbBlColumnName || "",
|
||||
nocodbAmountColumnName: group.nocodbAmountColumnName || "",
|
||||
nocodbSupplierColumnName: group.nocodbSupplierColumnName || "",
|
||||
nocodbDueDateColumnName: group.nocodbDueDateColumnName || "",
|
||||
webhookUrl: group.webhookUrl || "",
|
||||
});
|
||||
setShowEditModal(true);
|
||||
@@ -631,6 +642,16 @@ export default function Groups() {
|
||||
placeholder="ex: Fournisseur"
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -693,6 +714,7 @@ export default function Groups() {
|
||||
nocodbBlColumnName: "",
|
||||
nocodbAmountColumnName: "",
|
||||
nocodbSupplierColumnName: "",
|
||||
nocodbDueDateColumnName: "",
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,13 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
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 { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
@@ -39,6 +46,7 @@ export default function Suppliers() {
|
||||
hasDlc: false,
|
||||
automaticReconciliation: false,
|
||||
requiresControl: false,
|
||||
paymentMethod: "",
|
||||
});
|
||||
|
||||
const { data: suppliers = [], isLoading } = useQuery<Supplier[]>({
|
||||
@@ -193,6 +201,7 @@ export default function Suppliers() {
|
||||
hasDlc: false,
|
||||
automaticReconciliation: false,
|
||||
requiresControl: false,
|
||||
paymentMethod: "",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -230,6 +239,7 @@ export default function Suppliers() {
|
||||
hasDlc: supplier.hasDlc || false,
|
||||
automaticReconciliation: supplier.automaticReconciliation || false,
|
||||
requiresControl: supplier.requiresControl || false,
|
||||
paymentMethod: supplier.paymentMethod || "",
|
||||
});
|
||||
setShowEditModal(true);
|
||||
};
|
||||
@@ -399,6 +409,13 @@ export default function Suppliers() {
|
||||
{supplier.phone}
|
||||
</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 space-x-2">
|
||||
{supplier.hasDlc && (
|
||||
@@ -511,6 +528,25 @@ export default function Suppliers() {
|
||||
/>
|
||||
</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">
|
||||
<Checkbox
|
||||
id="hasDlc"
|
||||
|
||||
Reference in new issue
Block a user