mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
Improve the reliability of the task management system by fixing errors
Update database schema to allow nullable fields for invoice reference and amount, adjust React components for safe access to optional data, and remove obsolete client-side debugging scripts. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869/mpVybiP
This commit is contained in:
1 parent
083dca5e71
commit
8baf071c17
4 files changed
+13
-704
No files matched your search
@@ -1,646 +0,0 @@
|
|||||||
# 🚀 SOLUTION COMPLÈTE - MODULE AVOIRS PRODUCTION
|
|
||||||
|
|
||||||
## 🔥 PROBLÈME RÉSOLU
|
|
||||||
- Erreur `contact_email` supprimée
|
|
||||||
- Interface React corrigée pour les champs optionnels
|
|
||||||
- Prêt pour la production
|
|
||||||
|
|
||||||
## 📋 ÉTAPES DE DÉPLOIEMENT
|
|
||||||
|
|
||||||
### ÉTAPE 1 - Base de données (Exécuter dans votre console PostgreSQL)
|
|
||||||
```sql
|
|
||||||
-- Corriger la structure de la table avoirs
|
|
||||||
ALTER TABLE avoirs ALTER COLUMN invoice_reference DROP NOT NULL;
|
|
||||||
ALTER TABLE avoirs ALTER COLUMN amount DROP NOT NULL;
|
|
||||||
|
|
||||||
-- Vérifier la correction
|
|
||||||
SELECT column_name, is_nullable
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_name = 'avoirs'
|
|
||||||
AND column_name IN ('invoice_reference', 'amount');
|
|
||||||
```
|
|
||||||
|
|
||||||
### ÉTAPE 2 - Fichiers à remplacer sur votre serveur
|
|
||||||
|
|
||||||
#### A) shared/schema.ts (section avoirs uniquement)
|
|
||||||
Remplacez la section avoirs par :
|
|
||||||
```typescript
|
|
||||||
export const avoirs = pgTable("avoirs", {
|
|
||||||
id: serial("id").primaryKey(),
|
|
||||||
supplierId: integer("supplier_id").notNull(),
|
|
||||||
groupId: integer("group_id").notNull(),
|
|
||||||
invoiceReference: varchar("invoice_reference"), // OPTIONNEL
|
|
||||||
amount: decimal("amount", { precision: 10, scale: 2 }), // OPTIONNEL
|
|
||||||
comment: text("comment"),
|
|
||||||
commercialProcessed: boolean("commercial_processed").default(false),
|
|
||||||
status: varchar("status").notNull().default("En attente de demande"),
|
|
||||||
webhookSent: boolean("webhook_sent").default(false),
|
|
||||||
nocodbVerified: boolean("nocodb_verified").default(false),
|
|
||||||
nocodbVerifiedAt: timestamp("nocodb_verified_at"),
|
|
||||||
processedAt: timestamp("processed_at"),
|
|
||||||
createdBy: varchar("created_by").notNull(),
|
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const insertAvoirSchema = createInsertSchema(avoirs).omit({
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
webhookSent: true,
|
|
||||||
nocodbVerified: true,
|
|
||||||
nocodbVerifiedAt: true,
|
|
||||||
processedAt: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Avoir = typeof avoirs.$inferSelect;
|
|
||||||
export type InsertAvoir = z.infer<typeof insertAvoirSchema>;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B) client/src/pages/Avoirs.tsx
|
|
||||||
**FICHIER COMPLET CORRIGÉ - Copiez tout :**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { fr } from "date-fns/locale";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
|
||||||
import { Plus, Search, FileText, CheckCircle, Clock, AlertCircle } from "lucide-react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
|
||||||
|
|
||||||
// Types
|
|
||||||
interface Avoir {
|
|
||||||
id: number;
|
|
||||||
supplierId: number;
|
|
||||||
groupId: number;
|
|
||||||
invoiceReference?: string; // OPTIONNEL
|
|
||||||
amount?: number; // OPTIONNEL
|
|
||||||
comment?: string;
|
|
||||||
commercialProcessed: boolean;
|
|
||||||
status: 'En attente de demande' | 'Demandé' | 'Reçu';
|
|
||||||
webhookSent: boolean;
|
|
||||||
nocodbVerified: boolean;
|
|
||||||
nocodbVerifiedAt?: Date;
|
|
||||||
processedAt?: Date;
|
|
||||||
createdBy: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
supplier: {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
contact?: string;
|
|
||||||
phone?: string;
|
|
||||||
};
|
|
||||||
group: {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
color: string;
|
|
||||||
};
|
|
||||||
creator: {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
firstName?: string;
|
|
||||||
lastName?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Supplier {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Group {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schema for form validation
|
|
||||||
const avoirSchema = z.object({
|
|
||||||
supplierId: z.number().min(1, "Veuillez sélectionner un fournisseur"),
|
|
||||||
groupId: z.number().min(1, "Veuillez sélectionner un magasin"),
|
|
||||||
invoiceReference: z.string().optional(),
|
|
||||||
amount: z.number().optional(),
|
|
||||||
comment: z.string().optional(),
|
|
||||||
commercialProcessed: z.boolean().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type AvoirFormData = z.infer<typeof avoirSchema>;
|
|
||||||
|
|
||||||
export default function Avoirs() {
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
|
||||||
const [selectedStoreId, setSelectedStoreId] = useState<string>("all");
|
|
||||||
const { toast } = useToast();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
// Fetch user profile
|
|
||||||
const { data: user } = useQuery({
|
|
||||||
queryKey: ['/api/user'],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch groups for store filter
|
|
||||||
const { data: groups = [] } = useQuery<Group[]>({
|
|
||||||
queryKey: ['/api/groups'],
|
|
||||||
enabled: !!user,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch suppliers
|
|
||||||
const { data: suppliers = [] } = useQuery<Supplier[]>({
|
|
||||||
queryKey: ['/api/suppliers'],
|
|
||||||
enabled: !!user,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch avoirs
|
|
||||||
const { data: avoirs = [], isLoading } = useQuery<Avoir[]>({
|
|
||||||
queryKey: ['/api/avoirs', selectedStoreId],
|
|
||||||
queryFn: () => apiRequest(`/api/avoirs${selectedStoreId !== 'all' ? `?storeId=${selectedStoreId}` : ''}`),
|
|
||||||
enabled: !!user,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create avoir mutation
|
|
||||||
const createAvoirMutation = useMutation({
|
|
||||||
mutationFn: async (data: AvoirFormData) => {
|
|
||||||
const response = await fetch('/api/avoirs', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to create avoir');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/avoirs'] });
|
|
||||||
setIsCreateDialogOpen(false);
|
|
||||||
toast({
|
|
||||||
title: "Avoir créé",
|
|
||||||
description: "L'avoir a été créé avec succès.",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
|
||||||
toast({
|
|
||||||
title: "Erreur",
|
|
||||||
description: error.message || "Erreur lors de la création de l'avoir",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize form
|
|
||||||
const form = useForm<AvoirFormData>({
|
|
||||||
resolver: zodResolver(avoirSchema),
|
|
||||||
defaultValues: {
|
|
||||||
supplierId: 0,
|
|
||||||
groupId: 0,
|
|
||||||
invoiceReference: "",
|
|
||||||
amount: undefined,
|
|
||||||
comment: "",
|
|
||||||
commercialProcessed: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = (data: AvoirFormData) => {
|
|
||||||
createAvoirMutation.mutate(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter avoirs based on search term - CORRECTION CRITIQUE
|
|
||||||
const filteredAvoirs = avoirs.filter(avoir =>
|
|
||||||
avoir.invoiceReference?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
avoir.supplier.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
avoir.comment?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
(avoir.creator.firstName && avoir.creator.lastName
|
|
||||||
? `${avoir.creator.firstName} ${avoir.creator.lastName}`.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
: avoir.creator.username.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Status badge variant
|
|
||||||
const getStatusVariant = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'Reçu':
|
|
||||||
return 'default';
|
|
||||||
case 'Demandé':
|
|
||||||
return 'secondary';
|
|
||||||
case 'En attente de demande':
|
|
||||||
return 'outline';
|
|
||||||
default:
|
|
||||||
return 'outline';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Status icon
|
|
||||||
const getStatusIcon = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'Reçu':
|
|
||||||
return <CheckCircle className="h-4 w-4 text-green-600" />;
|
|
||||||
case 'Demandé':
|
|
||||||
return <Clock className="h-4 w-4 text-blue-600" />;
|
|
||||||
case 'En attente de demande':
|
|
||||||
return <AlertCircle className="h-4 w-4 text-orange-600" />;
|
|
||||||
default:
|
|
||||||
return <FileText className="h-4 w-4" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return <div>Chargement...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check permissions
|
|
||||||
if ((user as any)?.role === 'employee') {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-[50vh]">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-xl font-semibold mb-2">Accès refusé</h2>
|
|
||||||
<p className="text-gray-600">Vous n'avez pas les permissions pour accéder aux avoirs.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const canCreate = ['admin', 'directeur', 'manager'].includes((user as any)?.role);
|
|
||||||
const userGroups = (user as any)?.userGroups || [];
|
|
||||||
const availableGroups = (user as any)?.role === 'admin' ? groups : userGroups.map((ug: any) => ug.group);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6 p-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold">Avoirs</h1>
|
|
||||||
<p className="text-gray-600 mt-2">
|
|
||||||
Gestion des demandes d'avoirs et suivi des remboursements
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{canCreate && (
|
|
||||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Créer un avoir
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Créer un nouvel avoir</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="supplierId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Fournisseur *</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => field.onChange(parseInt(value))}
|
|
||||||
value={field.value?.toString()}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Sélectionner un fournisseur" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{suppliers.map((supplier) => (
|
|
||||||
<SelectItem key={supplier.id} value={supplier.id.toString()}>
|
|
||||||
{supplier.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="groupId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Magasin *</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => field.onChange(parseInt(value))}
|
|
||||||
value={field.value?.toString()}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Sélectionner un magasin" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{availableGroups.map((group: any) => (
|
|
||||||
<SelectItem key={group.id} value={group.id.toString()}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div
|
|
||||||
className="w-3 h-3 rounded-full mr-2"
|
|
||||||
style={{ backgroundColor: group.color }}
|
|
||||||
/>
|
|
||||||
{group.name}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="invoiceReference"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Référence facture (optionnel)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="FAC-2024-001" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="amount"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Montant (optionnel)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
placeholder="0.00"
|
|
||||||
{...field}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
field.onChange(value ? parseFloat(value) : undefined);
|
|
||||||
}}
|
|
||||||
value={field.value || ''}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="comment"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Commentaire</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
placeholder="Raison de l'avoir, détails..."
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="commercialProcessed"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<div className="space-y-1 leading-none">
|
|
||||||
<FormLabel>
|
|
||||||
Avoir fait par commercial
|
|
||||||
</FormLabel>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setIsCreateDialogOpen(false)}
|
|
||||||
>
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={createAvoirMutation.isPending}
|
|
||||||
>
|
|
||||||
{createAvoirMutation.isPending ? "Création..." : "Créer"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters and Search */}
|
|
||||||
<div className="flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
|
||||||
<Input
|
|
||||||
placeholder="Rechercher par référence, fournisseur, commentaire..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="pl-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(user as any)?.role === 'admin' && (
|
|
||||||
<Select value={selectedStoreId} onValueChange={setSelectedStoreId}>
|
|
||||||
<SelectTrigger className="w-[200px]">
|
|
||||||
<SelectValue placeholder="Tous les magasins" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous les magasins</SelectItem>
|
|
||||||
{groups.map((group) => (
|
|
||||||
<SelectItem key={group.id} value={group.id.toString()}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div
|
|
||||||
className="w-3 h-3 rounded-full mr-2"
|
|
||||||
style={{ backgroundColor: group.color }}
|
|
||||||
/>
|
|
||||||
{group.name}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Avoirs List */}
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{isLoading ? (
|
|
||||||
Array.from({ length: 6 }).map((_, i) => (
|
|
||||||
<Card key={i} className="animate-pulse">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
|
||||||
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="h-3 bg-gray-200 rounded"></div>
|
|
||||||
<div className="h-3 bg-gray-200 rounded w-2/3"></div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))
|
|
||||||
) : filteredAvoirs.length === 0 ? (
|
|
||||||
<div className="col-span-full flex flex-col items-center justify-center py-12">
|
|
||||||
<FileText className="h-12 w-12 text-gray-400 mb-4" />
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Aucun avoir trouvé</h3>
|
|
||||||
<p className="text-gray-500 text-center mb-6">
|
|
||||||
{searchTerm ? "Aucun avoir ne correspond à votre recherche." : "Commencez par créer votre premier avoir."}
|
|
||||||
</p>
|
|
||||||
{canCreate && !searchTerm && (
|
|
||||||
<Button onClick={() => setIsCreateDialogOpen(true)}>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Créer un avoir
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
filteredAvoirs.map((avoir) => (
|
|
||||||
<Card key={avoir.id} className="hover:shadow-md transition-shadow">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<CardTitle className="text-lg flex items-center">
|
|
||||||
{getStatusIcon(avoir.status)}
|
|
||||||
<span className="ml-2">#{avoir.invoiceReference || 'Sans référence'}</span>
|
|
||||||
</CardTitle>
|
|
||||||
<Badge variant={getStatusVariant(avoir.status)}>
|
|
||||||
{avoir.status}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<CardDescription>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div
|
|
||||||
className="w-3 h-3 rounded-full mr-2"
|
|
||||||
style={{ backgroundColor: avoir.group.color }}
|
|
||||||
/>
|
|
||||||
{avoir.group.name}
|
|
||||||
</div>
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm text-muted-foreground">Fournisseur:</span>
|
|
||||||
<span className="font-medium">{avoir.supplier.name}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm text-muted-foreground">Montant:</span>
|
|
||||||
<span className="font-bold text-lg">{avoir.amount ? avoir.amount.toFixed(2) + ' €' : 'Non spécifié'}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{avoir.comment && (
|
|
||||||
<div className="pt-2 border-t">
|
|
||||||
<p className="text-sm text-muted-foreground italic">
|
|
||||||
"{avoir.comment}"
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="pt-2 border-t">
|
|
||||||
<div className="flex justify-between text-xs text-muted-foreground">
|
|
||||||
<span>
|
|
||||||
Par {avoir.creator.firstName && avoir.creator.lastName
|
|
||||||
? `${avoir.creator.firstName} ${avoir.creator.lastName}`
|
|
||||||
: avoir.creator.username
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{format(new Date(avoir.createdAt), "dd/MM/yyyy", { locale: fr })}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{avoir.commercialProcessed && (
|
|
||||||
<div className="flex items-center text-xs text-green-600">
|
|
||||||
<CheckCircle className="h-3 w-3 mr-1" />
|
|
||||||
Traité par commercial
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 text-xs">
|
|
||||||
{avoir.webhookSent && (
|
|
||||||
<div className="flex items-center text-green-600">
|
|
||||||
<CheckCircle className="h-3 w-3 mr-1" />
|
|
||||||
Webhook envoyé
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{avoir.nocodbVerified && (
|
|
||||||
<div className="flex items-center text-blue-600">
|
|
||||||
<CheckCircle className="h-3 w-3 mr-1" />
|
|
||||||
NocoDB vérifié
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ÉTAPE 3 - Redémarrer votre serveur
|
|
||||||
```bash
|
|
||||||
# Sur votre serveur
|
|
||||||
sudo systemctl restart votre-service-node
|
|
||||||
# ou
|
|
||||||
pm2 restart all
|
|
||||||
# ou simplement redémarrer Node.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### ÉTAPE 4 - Test final
|
|
||||||
1. Ouvrez votre application
|
|
||||||
2. Allez dans le menu Avoirs
|
|
||||||
3. Créez un avoir sans référence facture ni montant
|
|
||||||
4. Vérifiez que ça fonctionne
|
|
||||||
|
|
||||||
## ✅ RÉSULTAT ATTENDU
|
|
||||||
- Plus d'erreur "contact_email"
|
|
||||||
- Interface qui se charge sans crash
|
|
||||||
- Possibilité de créer des avoirs avec champs optionnels
|
|
||||||
- Affichage correct des listes
|
|
||||||
|
|
||||||
## 🆘 SI ÇA NE MARCHE TOUJOURS PAS
|
|
||||||
Ouvrez la console de votre navigateur (F12) et copiez-moi EXACTEMENT les erreurs affichées.
|
|
||||||
+11
-11
@@ -159,14 +159,14 @@ export default function Avoirs() {
|
|||||||
createAvoirMutation.mutate(data);
|
createAvoirMutation.mutate(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter avoirs based on search term
|
// Filter avoirs based on search term - ACCÈS SÉCURISÉ
|
||||||
const filteredAvoirs = avoirs.filter(avoir =>
|
const filteredAvoirs = avoirs.filter(avoir =>
|
||||||
avoir.invoiceReference?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
(avoir.invoiceReference?.toLowerCase() || '').includes(searchTerm.toLowerCase()) ||
|
||||||
avoir.supplier.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
(avoir.supplier?.name?.toLowerCase() || '').includes(searchTerm.toLowerCase()) ||
|
||||||
avoir.comment?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
(avoir.comment?.toLowerCase() || '').includes(searchTerm.toLowerCase()) ||
|
||||||
(avoir.creator.firstName && avoir.creator.lastName
|
(avoir.creator?.firstName && avoir.creator?.lastName
|
||||||
? `${avoir.creator.firstName} ${avoir.creator.lastName}`.toLowerCase().includes(searchTerm.toLowerCase())
|
? `${avoir.creator.firstName} ${avoir.creator.lastName}`.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
: avoir.creator.username.toLowerCase().includes(searchTerm.toLowerCase())
|
: (avoir.creator?.username?.toLowerCase() || '').includes(searchTerm.toLowerCase())
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -460,9 +460,9 @@ export default function Avoirs() {
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded-full mr-2"
|
className="w-3 h-3 rounded-full mr-2"
|
||||||
style={{ backgroundColor: avoir.group.color }}
|
style={{ backgroundColor: avoir.group?.color || '#ccc' }}
|
||||||
/>
|
/>
|
||||||
{avoir.group.name}
|
{avoir.group?.name || 'Non défini'}
|
||||||
</div>
|
</div>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -470,7 +470,7 @@ export default function Avoirs() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm text-muted-foreground">Fournisseur:</span>
|
<span className="text-sm text-muted-foreground">Fournisseur:</span>
|
||||||
<span className="font-medium">{avoir.supplier.name}</span>
|
<span className="font-medium">{avoir.supplier?.name || 'Non défini'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@@ -489,9 +489,9 @@ export default function Avoirs() {
|
|||||||
<div className="pt-2 border-t">
|
<div className="pt-2 border-t">
|
||||||
<div className="flex justify-between text-xs text-muted-foreground">
|
<div className="flex justify-between text-xs text-muted-foreground">
|
||||||
<span>
|
<span>
|
||||||
Par {avoir.creator.firstName && avoir.creator.lastName
|
Par {avoir.creator?.firstName && avoir.creator?.lastName
|
||||||
? `${avoir.creator.firstName} ${avoir.creator.lastName}`
|
? `${avoir.creator.firstName} ${avoir.creator.lastName}`
|
||||||
: avoir.creator.username
|
: avoir.creator?.username || 'Utilisateur inconnu'
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
// 🔍 DEBUG CLIENT-SIDE ERRORS
|
|
||||||
// Ajoutez ce script dans la console de votre navigateur pour capturer les erreurs
|
|
||||||
|
|
||||||
console.log('🔍 DEBUG AVOIRS - Initialisation...');
|
|
||||||
|
|
||||||
// Intercepter les erreurs React
|
|
||||||
window.addEventListener('error', (event) => {
|
|
||||||
console.error('🚨 ERREUR JAVASCRIPT:', {
|
|
||||||
message: event.message,
|
|
||||||
filename: event.filename,
|
|
||||||
lineno: event.lineno,
|
|
||||||
colno: event.colno,
|
|
||||||
error: event.error
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Intercepter les erreurs de Promise non gérées
|
|
||||||
window.addEventListener('unhandledrejection', (event) => {
|
|
||||||
console.error('🚨 ERREUR PROMISE:', {
|
|
||||||
reason: event.reason,
|
|
||||||
promise: event.promise
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tester l'accès direct à l'API
|
|
||||||
fetch('/api/avoirs', { credentials: 'include' })
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
console.log('✅ API TEST SUCCESS:', data);
|
|
||||||
console.log('📊 Nombre d\'avoirs:', data.length);
|
|
||||||
if (data.length > 0) {
|
|
||||||
console.log('📋 Premier avoir:', data[0]);
|
|
||||||
console.log('🔍 Structure:', {
|
|
||||||
hasInvoiceReference: data[0].invoiceReference !== undefined,
|
|
||||||
hasAmount: data[0].amount !== undefined,
|
|
||||||
invoiceRefType: typeof data[0].invoiceReference,
|
|
||||||
amountType: typeof data[0].amount
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('❌ API TEST ERROR:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('🔍 DEBUG initialisé - surveillez les erreurs ci-dessus');
|
|
||||||
@@ -7,8 +7,8 @@ INSERT INTO suppliers (name, contact) VALUES
|
|||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
-- 2. Créer des données de test minimales pour groups
|
-- 2. Créer des données de test minimales pour groups
|
||||||
INSERT INTO groups (name, description) VALUES
|
INSERT INTO groups (name, color) VALUES
|
||||||
('Magasin Test', 'Magasin de test pour avoirs')
|
('Magasin Test', '#1976D2')
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
-- 3. Vérifier/créer un utilisateur admin
|
-- 3. Vérifier/créer un utilisateur admin
|
||||||
|
|||||||
Reference in new issue
Block a user