Sage100/src/components/page/facture/FactureContent.tsx
2026-01-20 11:05:50 +03:00

480 lines
No EOL
18 KiB
TypeScript

import React from "react";
import { Plus, Trash2, PenLine, Package, Lock, EyeOff } from "lucide-react";
import { cn } from "@/lib/utils";
import { Tooltip, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import { Textarea } from "@/components/ui/FormModal";
import ArticleAutocomplete from "@/components/molecules/ArticleAutocomplete";
import StickyTotals from "@/components/document-entry/StickyTotals";
import { Facture } from "@/types/factureType";
export interface LigneForm {
id: string;
article_code: string;
quantite: number;
prix_unitaire_ht: number;
total_taxes: number;
taux_taxe1: number;
montant_ligne_ht: number;
remise_pourcentage: number;
designation: string;
articles: any | null;
isManual: boolean;
}
export interface Note {
publique: string;
prive: string;
}
interface FactureContentProps {
// Données
facture: Facture;
editLignes: LigneForm[];
note: Note;
// États
isEditing: boolean;
isPdfPreviewVisible: boolean;
activeLineId: string | null;
description: string | null;
error: string | null;
// Totaux
editTotalHT: number;
editTotalTVA: number;
editTotalTTC: number;
// Handlers
onToggleLineMode: (lineId: string) => void;
onUpdateLigne: (lineId: string, field: keyof LigneForm, value: any) => void;
onAjouterLigne: () => void;
onSupprimerLigne: (lineId: string) => void;
onSetActiveLineId: (lineId: string | null) => void;
onSetDescription: (value: string | null) => void;
onSetNote: (note: Note) => void;
onOpenArticleModal: (lineId: string) => void;
// Fonction de calcul
calculerTotalLigne: (ligne: LigneForm) => number;
}
const FactureContent: React.FC<FactureContentProps> = ({
facture,
editLignes,
note,
isEditing,
isPdfPreviewVisible,
activeLineId,
description,
error,
editTotalHT,
editTotalTVA,
editTotalTTC,
onToggleLineMode,
onUpdateLigne,
onAjouterLigne,
onSupprimerLigne,
onSetActiveLineId,
onSetDescription,
onSetNote,
onOpenArticleModal,
calculerTotalLigne,
}) => {
const textSizeClass = isPdfPreviewVisible ? "text-xs" : "text-sm";
return (
<div className="flex flex-col h-full p-2 ">
{/* Zone scrollable - SEULEMENT ici le scroll s'active */}
<div className="flex-1 overflow-y-auto px-4 mx-auto w-full sm:px-6 lg:px-8 scroll-smooth">
<div className="mt-6 pb-4 w-full">
{/* Tableau des lignes */}
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm dark:bg-gray-950 dark:border-gray-800">
<div className="overflow-x-auto mb-2">
<table className="w-full">
<thead className="text-xs font-semibold tracking-wider text-gray-500 uppercase bg-gray-50 border-b dark:bg-gray-900/50">
<tr className={isEditing ? "text-[10px]" : ""}>
{isEditing && <th className="px-2 py-3 w-5 text-center">Mode</th>}
<th className="px-4 py-3 text-left w-[25%]">Désignation</th>
<th className="px-0 py-3 text-left w-[20%]">
Description {!isEditing && "détaillée"}
</th>
<th className="px-4 py-3 text-right w-[8%]">Qté</th>
<th className="px-4 py-3 text-right w-[15%]">P.U. HT</th>
<th className="px-4 py-3 text-right w-[8%]">Remise</th>
<th className="px-4 py-3 text-right w-[8%]">TVA</th>
<th className="px-4 py-3 text-right w-[15%]">Total HT</th>
{isEditing && <th className="px-2 py-3 w-12"></th>}
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{isEditing
? editLignes.map((item) => (
<EditableLigneRow
key={item.id}
item={item}
isPdfPreviewVisible={isPdfPreviewVisible}
activeLineId={activeLineId}
description={description}
onToggleLineMode={onToggleLineMode}
onUpdateLigne={onUpdateLigne}
onSupprimerLigne={onSupprimerLigne}
onSetActiveLineId={onSetActiveLineId}
onSetDescription={onSetDescription}
onOpenArticleModal={onOpenArticleModal}
calculerTotalLigne={calculerTotalLigne}
canDelete={editLignes.length > 1}
/>
))
: facture.lignes?.map((line, index) => (
<ReadOnlyLigneRow
key={index}
line={line}
textSizeClass={textSizeClass}
/>
))}
</tbody>
</table>
</div>
{/* Bouton ajouter ligne */}
{isEditing && (
<div className="m-6">
<button
onClick={onAjouterLigne}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-[#2A6F4F] bg-white border border-[#2A6F4F] rounded-lg hover:bg-green-50 transition-all shadow-sm"
>
<Plus className="w-4 h-4" /> Ajouter une ligne
</button>
</div>
)}
{/* Message d'erreur */}
{error && isEditing && (
<div className="p-4 m-4 text-sm text-red-800 bg-red-50 rounded-xl border border-red-200">
{error}
</div>
)}
</div>
{/* Section Notes */}
<NotesSection
note={note}
isEditing={isEditing}
onSetNote={onSetNote}
/>
</div>
</div>
{/* Footer Totaux - Reste fixe naturellement en bas */}
<div className="flex-none mt-5 bg-white dark:bg-gray-950 border-t border-gray-200 dark:border-gray-800 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)] z-20">
<div className="px-4 mx-auto w-full sm:px-6 lg:px-8">
<StickyTotals
total_ht_calcule={isEditing ? editTotalHT : facture.total_ht_calcule}
total_taxes_calcule={isEditing ? editTotalTVA : facture.total_taxes_calcule}
total_ttc_calcule={isEditing ? editTotalTTC : facture.total_ttc_calcule}
/>
</div>
</div>
</div>
);
};
// ============================================
// SOUS-COMPOSANTS
// ============================================
interface EditableLigneRowProps {
item: LigneForm;
isPdfPreviewVisible: boolean;
activeLineId: string | null;
description: string | null;
canDelete: boolean;
onToggleLineMode: (lineId: string) => void;
onUpdateLigne: (lineId: string, field: keyof LigneForm, value: any) => void;
onSupprimerLigne: (lineId: string) => void;
onSetActiveLineId: (lineId: string | null) => void;
onSetDescription: (value: string | null) => void;
onOpenArticleModal: (lineId: string) => void;
calculerTotalLigne: (ligne: LigneForm) => number;
}
const EditableLigneRow: React.FC<EditableLigneRowProps> = ({
item,
isPdfPreviewVisible,
activeLineId,
description,
canDelete,
onToggleLineMode,
onUpdateLigne,
onSupprimerLigne,
onSetActiveLineId,
onSetDescription,
onOpenArticleModal,
calculerTotalLigne,
}) => {
const textSizeClass = isPdfPreviewVisible ? "text-xs" : "text-sm";
return (
<tr className="group hover:bg-gray-50/50 dark:hover:bg-gray-900/20">
{/* Mode Toggle */}
<td className="px-2 py-3">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onToggleLineMode(item.id)}
className={cn(
"p-1.5 rounded-md transition-all duration-200",
item.isManual
? "text-amber-600 bg-amber-50 hover:bg-amber-100"
: "text-[#007E45] bg-green-50 hover:bg-green-100"
)}
>
{item.isManual ? (
<PenLine className="w-4 h-4" />
) : (
<Package className="w-4 h-4" />
)}
</button>
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
</td>
{/* Article / Désignation */}
<td className="px-4 py-3 min-w-[280px]">
{item.isManual ? (
<div className="relative">
<input
type="text"
value={item.designation}
onChange={(e) => onUpdateLigne(item.id, "designation", e.target.value)}
onFocus={() => onSetActiveLineId(item.id)}
onBlur={() => setTimeout(() => onSetActiveLineId(null), 150)}
className={cn(
`w-full border-0 ${textSizeClass} focus:outline-none bg-white text-gray-900`
)}
placeholder="Saisir une désignation..."
/>
{/* Menu dropdown */}
{activeLineId === item.id && item.designation && (
<ArticleCreationDropdown
designation={item.designation}
onCreateArticle={() => onOpenArticleModal(item.id)}
/>
)}
</div>
) : (
<div className="flex flex-col gap-1">
<ArticleAutocomplete
value={item.articles}
onChange={(article) => onUpdateLigne(item.id, "articles", article)}
required
className={textSizeClass}
/>
</div>
)}
</td>
{/* Description */}
<td className="px-1">
<input
type="text"
value={item.isManual ? description! : item.designation || ""}
onChange={(e) =>
item.isManual
? onSetDescription(e.target.value)
: onUpdateLigne(item.id, "designation", e.target.value)
}
placeholder="Description détaillée..."
className={cn(
`w-full border-0 ${textSizeClass} focus:outline-none bg-white text-gray-900 text-left`
)}
/>
</td>
{/* Quantité */}
<td className="px-1" style={{ width: "100px" }}>
<input
type="number"
value={item.quantite}
onChange={(e) =>
onUpdateLigne(item.id, "quantite", parseFloat(e.target.value) || 0)
}
min={0}
className={cn(
`w-full border-0 ${textSizeClass} focus:outline-none bg-white text-gray-900 text-right`
)}
/>
</td>
{/* Prix Unitaire */}
<td className="px-1 text-center" style={{ width: "60px" }}>
<input
type="number"
value={item.prix_unitaire_ht || item.articles?.prix_vente || 0}
onChange={(e) =>
onUpdateLigne(item.id, "prix_unitaire_ht", parseFloat(e.target.value) || 0)
}
min={0}
step={0.01}
className={cn(
`w-full border-0 ${textSizeClass} focus:outline-none bg-white text-gray-900 text-right`
)}
/>
</td>
{/* Remise */}
<td className="px-1 text-center" style={{ width: "60px" }}>
<input
type="number"
value={item.remise_pourcentage || 0}
onChange={(e) =>
onUpdateLigne(item.id, "remise_pourcentage", parseFloat(e.target.value) || 0)
}
min={0}
step={1}
className={cn(
`w-full border-0 ${textSizeClass} focus:outline-none bg-white text-gray-900 text-right`
)}
/>
</td>
{/* TVA */}
<td className={cn("px-1 text-center text-gray-500 text-right", textSizeClass)}>
{item.taux_taxe1}%
</td>
{/* Total */}
<td className="px-1 text-right">
<span className={cn("font-bold font-mono text-gray-900", textSizeClass)}>
{calculerTotalLigne(item).toFixed(2)}
</span>
</td>
{/* Delete */}
<td className="px-1 text-center">
<button
onClick={() => onSupprimerLigne(item.id)}
disabled={!canDelete}
className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors disabled:opacity-30"
>
<Trash2 className="w-4 h-4" />
</button>
</td>
</tr>
);
};
interface ReadOnlyLigneRowProps {
line: any;
textSizeClass: string;
}
const ReadOnlyLigneRow: React.FC<ReadOnlyLigneRowProps> = ({ line, textSizeClass }) => (
<tr className="group hover:bg-gray-50/50">
<td className={cn("px-4 py-3 font-bold font-mono", textSizeClass)}>
{line.article_code}
</td>
<td className={cn("px-4 py-3 text-gray-600", textSizeClass)}>
{line.designation}
</td>
<td className={cn("px-4 py-3 text-right", textSizeClass)}>
{line.quantite}
</td>
<td className={cn("px-4 py-3 text-right font-mono", textSizeClass)}>
{line.prix_unitaire_ht?.toFixed(2)}
</td>
<td className={cn("px-4 py-3 text-right text-gray-500", textSizeClass)}>
{line.remise_valeur1 || 0}%
</td>
<td className={cn("px-4 py-3 text-right text-gray-500", textSizeClass)}>
{line.taux_taxe1}%
</td>
<td className={cn("px-4 py-3 text-right font-bold font-mono", textSizeClass)}>
{line.montant_ligne_ht?.toFixed(2)}
</td>
</tr>
);
interface ArticleCreationDropdownProps {
designation: string;
onCreateArticle: () => void;
}
const ArticleCreationDropdown: React.FC<ArticleCreationDropdownProps> = ({
designation,
onCreateArticle,
}) => (
<div className="overflow-hidden mt-1 w-full bg-white rounded-xl border border-gray-200 shadow-lg">
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
onClick={onCreateArticle}
className="flex gap-3 items-center px-4 py-3 w-full text-left transition-colors hover:bg-green-50"
>
<div className="p-1.5 bg-[#007E45] rounded-lg text-white">
<Plus className="w-4 h-4" />
</div>
<span className="text-sm font-medium text-gray-900">
Créer l'article "<span className="text-[#007E45]">{designation}</span>"
</span>
</button>
<div className="border-t border-gray-100" />
<div className="flex flex-row gap-3 justify-center items-center py-2 w-full text-xs text-center text-gray-400">
<div className="p-1.5 rounded-lg">
<PenLine className="w-4 h-4" />
</div>
<span className="text-xs font-medium">Texte libre accepté</span>
</div>
</div>
);
interface NotesSectionProps {
note: Note;
isEditing: boolean;
onSetNote: (note: Note) => void;
}
const NotesSection: React.FC<NotesSectionProps> = ({ note, isEditing, onSetNote }) => (
<div className="grid grid-cols-1 gap-6 p-6 mt-8 bg-gray-50 rounded-2xl border border-gray-100 md:grid-cols-2 dark:bg-gray-900/30 dark:border-gray-800">
{/* Notes publiques */}
<div className="space-y-3">
<div className="flex justify-between items-center">
<label className="flex gap-2 items-center text-sm font-semibold text-gray-900 dark:text-white">
Notes publiques <Lock className="w-3 h-3 text-gray-400" />
</label>
<span className="text-xs text-gray-500">Visible sur le PDF</span>
</div>
<Textarea
rows={4}
disabled={!isEditing}
placeholder="Conditions de paiement, délais de livraison..."
className="bg-white resize-none dark:bg-gray-950"
value={note.publique}
onChange={(e) => onSetNote({ ...note, publique: e.target.value })}
/>
</div>
{/* Notes privées */}
<div className="space-y-3">
<div className="flex justify-between items-center">
<label className="flex gap-2 items-center text-sm font-semibold text-gray-900 dark:text-white">
Notes privées <EyeOff className="w-3 h-3 text-gray-400" />
</label>
<span className="text-xs text-gray-500">Interne uniquement</span>
</div>
<Textarea
rows={4}
disabled={!isEditing}
placeholder="Notes internes, marge de négociation..."
className="bg-white border-yellow-200 resize-none dark:bg-gray-950 focus:border-yellow-400"
value={note.prive}
onChange={(e) => onSetNote({ ...note, prive: e.target.value })}
/>
</div>
</div>
);
export default FactureContent;