907 lines
33 KiB
TypeScript
907 lines
33 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import {
|
|
Upload, FileText, Folder, Building2, Search, Plus,
|
|
Trash2, Edit3, Download, Loader2, X, Check, ChevronDown, ChevronRight
|
|
} from 'lucide-react'
|
|
import { usePageTitle } from '@/hooks/usePageTitle'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
|
|
type Organization = {
|
|
id: string
|
|
name: string
|
|
structure_api: string
|
|
}
|
|
|
|
type GeneralDocument = {
|
|
type: string
|
|
label: string
|
|
available: boolean
|
|
key?: string
|
|
name?: string
|
|
size?: number
|
|
downloadUrl?: string
|
|
}
|
|
|
|
type DocumentItem = {
|
|
id: string
|
|
title: string
|
|
filename: string
|
|
category: string
|
|
period_label?: string | null
|
|
size_bytes: number
|
|
date_added: string
|
|
storage_path: string
|
|
download_url?: string
|
|
}
|
|
|
|
type Period = {
|
|
label: string
|
|
count: number
|
|
}
|
|
|
|
const DOC_TYPES = {
|
|
"contrat-odentas": "Contrat Odentas",
|
|
"licence-spectacles": "Licence de spectacles",
|
|
"rib": "RIB",
|
|
"kbis-jo": "KBIS / Journal Officiel",
|
|
"delegation-signature": "Délégation de signature"
|
|
}
|
|
|
|
function formatBytes(bytes?: number) {
|
|
if (!bytes && bytes !== 0) return ''
|
|
const sizes = ['o', 'Ko', 'Mo', 'Go', 'To']
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024))
|
|
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`
|
|
}
|
|
|
|
function formatDate(dateStr: string) {
|
|
return new Date(dateStr).toLocaleDateString('fr-FR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
})
|
|
}
|
|
|
|
function slugify(text: string): string {
|
|
return text
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.toLowerCase()
|
|
.trim()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
}
|
|
|
|
// Modal pour uploader un document général
|
|
function UploadGeneralDocModal({
|
|
isOpen,
|
|
onClose,
|
|
orgId,
|
|
orgKey,
|
|
docType
|
|
}: {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
orgId: string
|
|
orgKey: string
|
|
docType: string
|
|
}) {
|
|
const [file, setFile] = React.useState<File | null>(null)
|
|
const [uploading, setUploading] = React.useState(false)
|
|
const queryClient = useQueryClient()
|
|
|
|
const handleUpload = async () => {
|
|
if (!file) return
|
|
|
|
setUploading(true)
|
|
try {
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
formData.append('org_id', orgId)
|
|
formData.append('org_key', slugify(orgKey))
|
|
formData.append('doc_type', docType)
|
|
formData.append('category', 'docs_generaux')
|
|
|
|
const response = await fetch('/api/staff/documents/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
|
|
if (!response.ok) throw new Error('Erreur lors de l\'upload')
|
|
|
|
toast.success('Document uploadé avec succès')
|
|
queryClient.invalidateQueries({ queryKey: ['documents', 'generaux', orgId] })
|
|
setFile(null)
|
|
onClose()
|
|
} catch (error) {
|
|
toast.error('Erreur lors de l\'upload du document')
|
|
console.error(error)
|
|
} finally {
|
|
setUploading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Uploader {DOC_TYPES[docType as keyof typeof DOC_TYPES]}</DialogTitle>
|
|
<DialogDescription>
|
|
Sélectionnez un fichier PDF à uploader pour ce type de document.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Fichier PDF</label>
|
|
<Input
|
|
type="file"
|
|
accept=".pdf"
|
|
onChange={(e) => setFile(e.target.files?.[0] || null)}
|
|
/>
|
|
{file && (
|
|
<p className="text-sm text-muted-foreground mt-2">
|
|
{file.name} ({formatBytes(file.size)})
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<Button
|
|
onClick={handleUpload}
|
|
disabled={!file || uploading}
|
|
className="flex-1"
|
|
>
|
|
{uploading ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
Upload en cours...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Upload className="h-4 w-4 mr-2" />
|
|
Uploader
|
|
</>
|
|
)}
|
|
</Button>
|
|
<Button onClick={onClose} variant="outline" disabled={uploading}>
|
|
Annuler
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
// Modal pour uploader un ou plusieurs documents comptables
|
|
function UploadComptableModal({
|
|
isOpen,
|
|
onClose,
|
|
orgId,
|
|
orgKey
|
|
}: {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
orgId: string
|
|
orgKey: string
|
|
}) {
|
|
type FileEntry = { id: number; file: File | null }
|
|
const [files, setFiles] = React.useState<FileEntry[]>([{ id: 1, file: null }])
|
|
const [period, setPeriod] = React.useState('')
|
|
const [uploading, setUploading] = React.useState(false)
|
|
const queryClient = useQueryClient()
|
|
const nextId = React.useRef(2)
|
|
|
|
const addFileRow = () => {
|
|
setFiles(prev => [...prev, { id: nextId.current++, file: null }])
|
|
}
|
|
|
|
const removeFileRow = (id: number) => {
|
|
if (files.length === 1) return
|
|
setFiles(prev => prev.filter(f => f.id !== id))
|
|
}
|
|
|
|
const updateFile = (id: number, file: File | null) => {
|
|
setFiles(prev => prev.map(f => f.id === id ? { ...f, file } : f))
|
|
}
|
|
|
|
const handleUpload = async () => {
|
|
const validFiles = files.filter(f => f.file !== null).map(f => f.file!)
|
|
if (validFiles.length === 0 || !period) return
|
|
|
|
setUploading(true)
|
|
try {
|
|
// Upload chaque fichier séparément
|
|
for (const file of validFiles) {
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
formData.append('org_id', orgId)
|
|
formData.append('org_key', slugify(orgKey))
|
|
formData.append('period', period)
|
|
formData.append('category', 'docs_comptables')
|
|
|
|
const response = await fetch('/api/staff/documents/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
|
|
if (!response.ok) throw new Error(`Erreur lors de l'upload de ${file.name}`)
|
|
}
|
|
|
|
toast.success(`${validFiles.length} document(s) uploadé(s) avec succès`)
|
|
queryClient.invalidateQueries({ queryKey: ['documents', 'comptables'] })
|
|
setFiles([{ id: 1, file: null }])
|
|
setPeriod('')
|
|
onClose()
|
|
} catch (error) {
|
|
toast.error('Erreur lors de l\'upload des documents')
|
|
console.error(error)
|
|
} finally {
|
|
setUploading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="sm:max-w-2xl max-h-[80vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Uploader des documents comptables</DialogTitle>
|
|
<DialogDescription>
|
|
Ajoutez un ou plusieurs fichiers pour une période donnée.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
Période <span className="text-red-500">*</span>
|
|
</label>
|
|
<Input
|
|
type="text"
|
|
placeholder="ex: 2507-juillet-2025"
|
|
value={period}
|
|
onChange={(e) => setPeriod(e.target.value)}
|
|
/>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Format: YYMM-mois-YYYY
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<label className="block text-sm font-medium">
|
|
Fichiers <span className="text-red-500">*</span>
|
|
</label>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={addFileRow}
|
|
disabled={uploading}
|
|
>
|
|
<Plus className="h-4 w-4 mr-1" />
|
|
Ajouter un fichier
|
|
</Button>
|
|
</div>
|
|
|
|
{files.map((entry, index) => (
|
|
<div key={entry.id} className="flex gap-2 items-start">
|
|
<div className="flex-1">
|
|
<Input
|
|
type="file"
|
|
accept=".pdf,.xlsx,.xls,.doc,.docx"
|
|
onChange={(e) => updateFile(entry.id, e.target.files?.[0] || null)}
|
|
disabled={uploading}
|
|
/>
|
|
{entry.file && (
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{entry.file.name} ({formatBytes(entry.file.size)})
|
|
</p>
|
|
)}
|
|
</div>
|
|
{files.length > 1 && (
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => removeFileRow(entry.id)}
|
|
disabled={uploading}
|
|
className="text-red-600 hover:text-red-700 hover:bg-red-50 px-2"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex gap-3 pt-4">
|
|
<Button
|
|
onClick={handleUpload}
|
|
disabled={!period || files.every(f => !f.file) || uploading}
|
|
className="flex-1"
|
|
>
|
|
{uploading ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
Upload en cours...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Upload className="h-4 w-4 mr-2" />
|
|
Uploader {files.filter(f => f.file).length > 0 && `(${files.filter(f => f.file).length})`}
|
|
</>
|
|
)}
|
|
</Button>
|
|
<Button onClick={onClose} variant="outline" disabled={uploading}>
|
|
Annuler
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
// Modal pour visualiser un document
|
|
function ViewDocumentModal({
|
|
isOpen,
|
|
onClose,
|
|
document
|
|
}: {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
document: { name: string; downloadUrl: string; size?: number } | null
|
|
}) {
|
|
if (!document) return null
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="sm:max-w-4xl max-h-[90vh]">
|
|
<DialogHeader>
|
|
<DialogTitle>{document.name}</DialogTitle>
|
|
<DialogDescription>
|
|
{document.size && `Taille: ${formatBytes(document.size)}`}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="w-full h-[70vh] bg-gray-100 rounded-lg overflow-hidden">
|
|
<iframe
|
|
src={document.downloadUrl}
|
|
className="w-full h-full border-0"
|
|
title={document.name}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-3 justify-end">
|
|
<Button
|
|
onClick={() => window.open(document.downloadUrl, '_blank')}
|
|
variant="outline"
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Télécharger
|
|
</Button>
|
|
<Button onClick={onClose}>
|
|
Fermer
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
export default function StaffDocumentsPage() {
|
|
usePageTitle("Gestion des documents")
|
|
|
|
const [selectedOrgId, setSelectedOrgId] = React.useState<string>('')
|
|
const [searchTerm, setSearchTerm] = React.useState('')
|
|
const [activeTab, setActiveTab] = React.useState<'generaux' | 'comptables'>('generaux')
|
|
const [uploadGeneralModal, setUploadGeneralModal] = React.useState<string | null>(null)
|
|
const [uploadComptableModal, setUploadComptableModal] = React.useState(false)
|
|
const [expandedPeriods, setExpandedPeriods] = React.useState<Set<string>>(new Set())
|
|
const [viewDocument, setViewDocument] = React.useState<{ name: string; downloadUrl: string; size?: number } | null>(null)
|
|
const [deleteConfirm, setDeleteConfirm] = React.useState<{ type: 'general' | 'comptable'; data: any } | null>(null)
|
|
|
|
const queryClient = useQueryClient()
|
|
|
|
// Récupérer les organisations
|
|
const { data: organizations, isLoading: isLoadingOrgs } = useQuery<Organization[]>({
|
|
queryKey: ['organizations', 'all'],
|
|
queryFn: async () => {
|
|
const response = await fetch('/api/organizations')
|
|
if (!response.ok) throw new Error('Erreur chargement organisations')
|
|
const data = await response.json()
|
|
const items = data.items || []
|
|
return items.sort((a: Organization, b: Organization) =>
|
|
a.name.localeCompare(b.name)
|
|
)
|
|
}
|
|
})
|
|
|
|
// Filtrer les organisations selon la recherche
|
|
const filteredOrgs = React.useMemo(() => {
|
|
if (!organizations) return []
|
|
if (!searchTerm) return organizations
|
|
return organizations.filter(org =>
|
|
org.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
)
|
|
}, [organizations, searchTerm])
|
|
|
|
const selectedOrg = organizations?.find(org => org.id === selectedOrgId)
|
|
|
|
// Récupérer les documents généraux
|
|
const { data: documentsGeneraux } = useQuery<GeneralDocument[]>({
|
|
queryKey: ['documents', 'generaux', selectedOrgId],
|
|
queryFn: async () => {
|
|
const res = await fetch(`/api/documents/generaux?org_id=${selectedOrgId}`)
|
|
if (!res.ok) throw new Error('Erreur chargement documents')
|
|
const data = await res.json()
|
|
return data.documents || []
|
|
},
|
|
enabled: !!selectedOrgId && activeTab === 'generaux'
|
|
})
|
|
|
|
// Récupérer les documents comptables
|
|
const { data: documentsComptables } = useQuery<DocumentItem[]>({
|
|
queryKey: ['documents', 'comptables', selectedOrgId],
|
|
queryFn: async () => {
|
|
const res = await fetch(`/api/staff/documents/list?org_id=${selectedOrgId}&category=docs_comptables`)
|
|
if (!res.ok) throw new Error('Erreur chargement documents')
|
|
const data = await res.json()
|
|
return data.documents || []
|
|
},
|
|
enabled: !!selectedOrgId && activeTab === 'comptables'
|
|
})
|
|
|
|
// Grouper par période
|
|
const documentsByPeriod = React.useMemo(() => {
|
|
if (!documentsComptables) return new Map()
|
|
|
|
const grouped = new Map<string, DocumentItem[]>()
|
|
documentsComptables.forEach(doc => {
|
|
const period = doc.period_label || 'Sans période'
|
|
if (!grouped.has(period)) grouped.set(period, [])
|
|
grouped.get(period)!.push(doc)
|
|
})
|
|
|
|
return new Map(
|
|
Array.from(grouped.entries()).sort((a, b) => b[0].localeCompare(a[0]))
|
|
)
|
|
}, [documentsComptables])
|
|
|
|
// Supprimer un document général
|
|
const deleteGeneralDoc = useMutation({
|
|
mutationFn: async ({ docType, orgKey }: { docType: string, orgKey: string }) => {
|
|
const response = await fetch('/api/staff/documents/delete-general', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ org_key: slugify(orgKey), doc_type: docType })
|
|
})
|
|
if (!response.ok) throw new Error('Erreur suppression')
|
|
},
|
|
onSuccess: () => {
|
|
toast.success('Document supprimé')
|
|
queryClient.invalidateQueries({ queryKey: ['documents', 'generaux'] })
|
|
},
|
|
onError: () => toast.error('Erreur lors de la suppression')
|
|
})
|
|
|
|
// Supprimer un document comptable
|
|
const deleteComptableDoc = useMutation({
|
|
mutationFn: async (docId: string) => {
|
|
const response = await fetch(`/api/staff/documents/delete?doc_id=${docId}`, {
|
|
method: 'DELETE'
|
|
})
|
|
if (!response.ok) throw new Error('Erreur suppression')
|
|
},
|
|
onSuccess: () => {
|
|
toast.success('Document supprimé')
|
|
queryClient.invalidateQueries({ queryKey: ['documents', 'comptables'] })
|
|
},
|
|
onError: () => toast.error('Erreur lors de la suppression')
|
|
})
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<header>
|
|
<h1 className="text-3xl font-bold tracking-tight">Gestion des documents</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
Gérez les documents généraux et comptables de vos clients
|
|
</p>
|
|
</header>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
{/* Sidebar: Liste des organisations */}
|
|
<div className="lg:col-span-1">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<Building2 className="h-4 w-4" />
|
|
Organisations
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
|
<Input
|
|
placeholder="Rechercher..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-9"
|
|
/>
|
|
</div>
|
|
|
|
<div className="max-h-[600px] overflow-y-auto space-y-1">
|
|
{isLoadingOrgs ? (
|
|
<div className="flex justify-center py-8">
|
|
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
|
|
</div>
|
|
) : filteredOrgs.length > 0 ? (
|
|
filteredOrgs.map((org) => (
|
|
<button
|
|
key={org.id}
|
|
onClick={() => setSelectedOrgId(org.id)}
|
|
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${
|
|
selectedOrgId === org.id
|
|
? 'bg-blue-50 text-blue-700 font-medium'
|
|
: 'hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
{org.name}
|
|
</button>
|
|
))
|
|
) : (
|
|
<p className="text-sm text-gray-500 text-center py-8">
|
|
Aucune organisation trouvée
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Main content */}
|
|
<div className="lg:col-span-3">
|
|
{!selectedOrgId ? (
|
|
<Card>
|
|
<CardContent className="flex items-center justify-center py-16">
|
|
<div className="text-center">
|
|
<Building2 className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
|
<p className="text-gray-500">
|
|
Sélectionnez une organisation pour gérer ses documents
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{/* Header avec nom organisation */}
|
|
<Card className="bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-200">
|
|
<CardHeader>
|
|
<CardTitle className="text-xl">{selectedOrg?.name}</CardTitle>
|
|
<CardDescription>
|
|
Clé: {selectedOrg?.structure_api}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
</Card>
|
|
|
|
{/* Onglets */}
|
|
<div className="flex gap-2 border-b">
|
|
<button
|
|
onClick={() => setActiveTab('generaux')}
|
|
className={`px-4 py-2 font-medium transition-colors border-b-2 ${
|
|
activeTab === 'generaux'
|
|
? 'border-blue-600 text-blue-600'
|
|
: 'border-transparent text-gray-600 hover:text-gray-900'
|
|
}`}
|
|
>
|
|
<FileText className="h-4 w-4 inline mr-2" />
|
|
Documents généraux
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('comptables')}
|
|
className={`px-4 py-2 font-medium transition-colors border-b-2 ${
|
|
activeTab === 'comptables'
|
|
? 'border-blue-600 text-blue-600'
|
|
: 'border-transparent text-gray-600 hover:text-gray-900'
|
|
}`}
|
|
>
|
|
<Folder className="h-4 w-4 inline mr-2" />
|
|
Documents comptables
|
|
</button>
|
|
</div>
|
|
|
|
{/* Documents généraux */}
|
|
{activeTab === 'generaux' && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{Object.entries(DOC_TYPES).map(([type, label]) => {
|
|
const doc = documentsGeneraux?.find(d => d.type === type)
|
|
return (
|
|
<Card key={type} className="hover:shadow-md transition-shadow">
|
|
<CardHeader>
|
|
<CardTitle className="text-base flex items-center justify-between">
|
|
<span className="flex items-center gap-2">
|
|
<FileText className="h-4 w-4" />
|
|
{label}
|
|
</span>
|
|
{doc?.available ? (
|
|
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">
|
|
✓ Disponible
|
|
</span>
|
|
) : (
|
|
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full">
|
|
Non uploadé
|
|
</span>
|
|
)}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{doc?.available ? (
|
|
<div className="space-y-2">
|
|
<p className="text-sm text-gray-600">
|
|
{formatBytes(doc.size)}
|
|
</p>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => setViewDocument({
|
|
name: doc.name || DOC_TYPES[type as keyof typeof DOC_TYPES],
|
|
downloadUrl: doc.downloadUrl!,
|
|
size: doc.size
|
|
})}
|
|
className="flex-1"
|
|
>
|
|
<FileText className="h-3 w-3 mr-1" />
|
|
Voir
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => setUploadGeneralModal(type)}
|
|
className="flex-1"
|
|
>
|
|
<Edit3 className="h-3 w-3 mr-1" />
|
|
Remplacer
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setDeleteConfirm({
|
|
type: 'general',
|
|
data: {
|
|
docType: type,
|
|
orgKey: selectedOrg?.structure_api || '',
|
|
label: DOC_TYPES[type as keyof typeof DOC_TYPES]
|
|
}
|
|
})
|
|
}}
|
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<Button
|
|
size="sm"
|
|
onClick={() => setUploadGeneralModal(type)}
|
|
className="w-full"
|
|
>
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
Uploader
|
|
</Button>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* Documents comptables */}
|
|
{activeTab === 'comptables' && (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-end">
|
|
<Button onClick={() => setUploadComptableModal(true)}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Uploader un document
|
|
</Button>
|
|
</div>
|
|
|
|
{documentsByPeriod.size === 0 ? (
|
|
<Card>
|
|
<CardContent className="flex items-center justify-center py-16">
|
|
<div className="text-center">
|
|
<Folder className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
|
<p className="text-gray-500">Aucun document comptable</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{Array.from(documentsByPeriod.entries()).map(([period, docs]) => {
|
|
const isExpanded = expandedPeriods.has(period)
|
|
return (
|
|
<Card key={period}>
|
|
<button
|
|
onClick={() => {
|
|
const next = new Set(expandedPeriods)
|
|
if (next.has(period)) next.delete(period)
|
|
else next.add(period)
|
|
setExpandedPeriods(next)
|
|
}}
|
|
className="w-full p-4 flex items-center justify-between hover:bg-gray-50 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{isExpanded ? (
|
|
<ChevronDown className="h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4" />
|
|
)}
|
|
<div className="text-left">
|
|
<h3 className="font-semibold">{period}</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{docs.length} document{docs.length > 1 ? 's' : ''}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{isExpanded && (
|
|
<div className="border-t p-4 space-y-2">
|
|
{docs.map((doc: DocumentItem) => (
|
|
<div
|
|
key={doc.id}
|
|
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium truncate">{doc.filename}</p>
|
|
<p className="text-sm text-gray-500">
|
|
{formatDate(doc.date_added)} • {formatBytes(doc.size_bytes)}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
{doc.download_url && (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => setViewDocument({
|
|
name: doc.filename,
|
|
downloadUrl: doc.download_url!,
|
|
size: doc.size_bytes
|
|
})}
|
|
>
|
|
<FileText className="h-3 w-3 mr-1" />
|
|
Voir
|
|
</Button>
|
|
)}
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setDeleteConfirm({
|
|
type: 'comptable',
|
|
data: {
|
|
id: doc.id,
|
|
filename: doc.filename
|
|
}
|
|
})
|
|
}}
|
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modals */}
|
|
{uploadGeneralModal && selectedOrg && (
|
|
<UploadGeneralDocModal
|
|
isOpen={true}
|
|
onClose={() => setUploadGeneralModal(null)}
|
|
orgId={selectedOrgId}
|
|
orgKey={selectedOrg.structure_api}
|
|
docType={uploadGeneralModal}
|
|
/>
|
|
)}
|
|
|
|
{uploadComptableModal && selectedOrg && (
|
|
<UploadComptableModal
|
|
isOpen={true}
|
|
onClose={() => setUploadComptableModal(false)}
|
|
orgId={selectedOrgId}
|
|
orgKey={selectedOrg.structure_api}
|
|
/>
|
|
)}
|
|
|
|
<ViewDocumentModal
|
|
isOpen={!!viewDocument}
|
|
onClose={() => setViewDocument(null)}
|
|
document={viewDocument}
|
|
/>
|
|
|
|
{/* Modale de confirmation de suppression */}
|
|
<Dialog open={!!deleteConfirm} onOpenChange={() => setDeleteConfirm(null)}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2 text-red-600">
|
|
<Trash2 className="h-5 w-5" />
|
|
Confirmer la suppression
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Cette action est irréversible. Le document sera définitivement supprimé.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="py-4">
|
|
<p className="text-sm text-muted-foreground">
|
|
{deleteConfirm?.type === 'general' ? (
|
|
<>Voulez-vous vraiment supprimer <strong>{deleteConfirm.data.label}</strong> ?</>
|
|
) : (
|
|
<>Voulez-vous vraiment supprimer <strong>{deleteConfirm?.data.filename}</strong> ?</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex gap-3 justify-end">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setDeleteConfirm(null)}
|
|
>
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
if (deleteConfirm?.type === 'general') {
|
|
deleteGeneralDoc.mutate({
|
|
docType: deleteConfirm.data.docType,
|
|
orgKey: deleteConfirm.data.orgKey
|
|
})
|
|
} else if (deleteConfirm?.type === 'comptable') {
|
|
deleteComptableDoc.mutate(deleteConfirm.data.id)
|
|
}
|
|
setDeleteConfirm(null)
|
|
}}
|
|
className="bg-red-600 text-white hover:bg-red-700 border-red-600"
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
Supprimer
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
)
|
|
}
|