449 lines
15 KiB
TypeScript
449 lines
15 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import { useQuery } 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 { Download, FileText, Upload, Folder, Building2, ChevronDown, ChevronRight } from 'lucide-react'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import { usePageTitle } from '@/hooks/usePageTitle'
|
|
import { Tooltip } from '@/components/ui/tooltip'
|
|
|
|
type DocumentItem = {
|
|
id: string
|
|
title: string
|
|
url?: string
|
|
updatedAt?: string
|
|
sizeBytes?: number
|
|
meta?: Record<string, any>
|
|
period_label?: string | null
|
|
}
|
|
|
|
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 formatDateLast(dateStr?: string) {
|
|
if (!dateStr) return ''
|
|
const date = new Date(dateStr)
|
|
return date.toLocaleDateString('fr-FR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
})
|
|
}
|
|
|
|
function UploadPanel() {
|
|
const [description, setDescription] = React.useState('')
|
|
|
|
const handleSubmit = () => {
|
|
alert('Fonctionnalité de transmission de document en cours de développement')
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Upload className="h-4 w-4" />
|
|
Transmettre un document
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Envoyez-nous directement vos documents
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<Input type="file" />
|
|
<Textarea
|
|
placeholder="Description du document (optionnel)"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
/>
|
|
<Button onClick={handleSubmit} className="w-full">
|
|
Envoyer
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
function SectionGeneraux() {
|
|
const { data: documentsGeneraux, isLoading, error } = useQuery<DocumentItem[]>({
|
|
queryKey: ['documents', 'generaux'],
|
|
queryFn: async () => {
|
|
const res = await fetch('/api/documents?category=generaux')
|
|
const data = await res.json()
|
|
console.log('📄 Documents Généraux - Response:', data)
|
|
console.log('📄 Documents Généraux - Is Array:', Array.isArray(data))
|
|
|
|
// Si la réponse est un objet avec une propriété documents
|
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
console.log('📄 Documents Généraux - Keys:', Object.keys(data))
|
|
// Essayer différentes propriétés possibles
|
|
if (data.documents) return data.documents
|
|
if (data.data) return data.data
|
|
if (data.items) return data.items
|
|
}
|
|
|
|
return Array.isArray(data) ? data : []
|
|
}
|
|
})
|
|
|
|
console.log('📄 Documents Généraux - Final Data:', documentsGeneraux)
|
|
console.log('📄 Documents Généraux - Loading:', isLoading)
|
|
console.log('📄 Documents Généraux - Error:', error)
|
|
|
|
const handleDownload = (item: DocumentItem) => {
|
|
if (item.url) {
|
|
window.open(item.url, '_blank')
|
|
} else {
|
|
alert('Document non disponible')
|
|
}
|
|
}
|
|
|
|
if (isLoading) {
|
|
return <p className="text-center text-muted-foreground py-8">Chargement...</p>
|
|
}
|
|
|
|
if (error) {
|
|
return <p className="text-center text-red-500 py-8">Erreur: {String(error)}</p>
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
{documentsGeneraux && documentsGeneraux.length > 0 ? (
|
|
documentsGeneraux.map((item) => (
|
|
<div key={item.id} className="flex items-center justify-between p-3 border rounded-lg">
|
|
<div>
|
|
<h4 className="font-medium">{item.title}</h4>
|
|
<p className="text-sm text-muted-foreground">
|
|
{formatDateLast(item.updatedAt)} • {formatBytes(item.sizeBytes)}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => handleDownload(item)}
|
|
disabled={!item.url}
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Télécharger
|
|
</Button>
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-center text-muted-foreground py-8">
|
|
Aucun document disponible
|
|
</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SectionCaisses() {
|
|
const { data: documentsOrganismes, isLoading, error } = useQuery<DocumentItem[]>({
|
|
queryKey: ['documents', 'caisses'],
|
|
queryFn: async () => {
|
|
const res = await fetch('/api/documents?category=caisses')
|
|
const data = await res.json()
|
|
console.log('📄 Documents Caisses - Response:', data)
|
|
|
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
if (data.documents) return data.documents
|
|
if (data.data) return data.data
|
|
if (data.items) return data.items
|
|
}
|
|
|
|
return Array.isArray(data) ? data : []
|
|
}
|
|
})
|
|
|
|
const handleDownload = (item: DocumentItem) => {
|
|
if (item.url) {
|
|
window.open(item.url, '_blank')
|
|
} else {
|
|
alert('Document non disponible')
|
|
}
|
|
}
|
|
|
|
if (isLoading) {
|
|
return <p className="text-center text-muted-foreground py-8">Chargement...</p>
|
|
}
|
|
|
|
if (error) {
|
|
return <p className="text-center text-red-500 py-8">Erreur: {String(error)}</p>
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
{documentsOrganismes && documentsOrganismes.length > 0 ? (
|
|
documentsOrganismes.map((item) => (
|
|
<div key={item.id} className="flex items-center justify-between p-3 border rounded-lg">
|
|
<div>
|
|
<h4 className="font-medium">{item.title}</h4>
|
|
<p className="text-sm text-muted-foreground">
|
|
{formatDateLast(item.updatedAt)} • {formatBytes(item.sizeBytes)}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => handleDownload(item)}
|
|
disabled={!item.url}
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Télécharger
|
|
</Button>
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-center text-muted-foreground py-8">
|
|
Aucun document disponible
|
|
</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SectionComptables() {
|
|
const [expandedPeriods, setExpandedPeriods] = React.useState<Set<string>>(new Set())
|
|
|
|
const { data: documentsCompta, isLoading, error } = useQuery<DocumentItem[]>({
|
|
queryKey: ['documents', 'comptables'],
|
|
queryFn: async () => {
|
|
console.log('📄 Fetching comptables with category=docs_comptables')
|
|
const res = await fetch('/api/documents?category=docs_comptables')
|
|
const data = await res.json()
|
|
console.log('📄 Documents Comptables - Raw Response:', data)
|
|
console.log('📄 Documents Comptables - Is Array?', Array.isArray(data))
|
|
|
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
console.log('📄 Documents Comptables - Object keys:', Object.keys(data))
|
|
if (data.documents) {
|
|
console.log('📄 Using data.documents:', data.documents)
|
|
return data.documents
|
|
}
|
|
if (data.data) {
|
|
console.log('📄 Using data.data:', data.data)
|
|
return data.data
|
|
}
|
|
if (data.items) {
|
|
console.log('📄 Using data.items:', data.items)
|
|
return data.items
|
|
}
|
|
}
|
|
|
|
const result = Array.isArray(data) ? data : []
|
|
console.log('📄 Final result:', result)
|
|
return result
|
|
}
|
|
})
|
|
|
|
// Grouper les documents par période
|
|
const documentsByPeriod = React.useMemo(() => {
|
|
if (!documentsCompta || documentsCompta.length === 0) return new Map()
|
|
|
|
const grouped = new Map<string, DocumentItem[]>()
|
|
|
|
documentsCompta.forEach(doc => {
|
|
const period = doc.period_label || 'Sans période'
|
|
if (!grouped.has(period)) {
|
|
grouped.set(period, [])
|
|
}
|
|
grouped.get(period)!.push(doc)
|
|
})
|
|
|
|
// Trier les périodes par ordre décroissant (plus récent en premier)
|
|
const sortedEntries = Array.from(grouped.entries()).sort((a, b) => {
|
|
// Si "Sans période", mettre à la fin
|
|
if (a[0] === 'Sans période') return 1
|
|
if (b[0] === 'Sans période') return -1
|
|
|
|
// Sinon, tri décroissant (plus récent en premier)
|
|
return b[0].localeCompare(a[0])
|
|
})
|
|
|
|
return new Map(sortedEntries)
|
|
}, [documentsCompta])
|
|
|
|
const togglePeriod = (period: string) => {
|
|
setExpandedPeriods(prev => {
|
|
const next = new Set(prev)
|
|
if (next.has(period)) {
|
|
next.delete(period)
|
|
} else {
|
|
next.add(period)
|
|
}
|
|
return next
|
|
})
|
|
}
|
|
|
|
const handleDownload = (item: DocumentItem) => {
|
|
if (item.url) {
|
|
window.open(item.url, '_blank')
|
|
} else {
|
|
alert('Document non disponible')
|
|
}
|
|
}
|
|
|
|
if (isLoading) {
|
|
return <p className="text-center text-muted-foreground py-8">Chargement...</p>
|
|
}
|
|
|
|
if (error) {
|
|
return <p className="text-center text-red-500 py-8">Erreur: {String(error)}</p>
|
|
}
|
|
|
|
if (!documentsCompta || documentsCompta.length === 0) {
|
|
return (
|
|
<p className="text-center text-muted-foreground py-8">
|
|
Aucun document comptable disponible
|
|
</p>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{Array.from(documentsByPeriod.entries()).map(([period, docs]) => {
|
|
const isExpanded = expandedPeriods.has(period)
|
|
|
|
return (
|
|
<div key={period} className="border rounded-lg overflow-hidden">
|
|
{/* Header de la période - cliquable */}
|
|
<button
|
|
onClick={() => togglePeriod(period)}
|
|
className="w-full flex items-center justify-between p-4 bg-muted/30 hover:bg-muted/50 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{isExpanded ? (
|
|
<ChevronDown className="h-5 w-5 text-muted-foreground" />
|
|
) : (
|
|
<ChevronRight className="h-5 w-5 text-muted-foreground" />
|
|
)}
|
|
<div className="text-left">
|
|
<h3 className="font-semibold text-base">{period}</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{docs.length} document{docs.length > 1 ? 's' : ''}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Liste des documents - affichée si expanded */}
|
|
{isExpanded && (
|
|
<div className="p-2 space-y-2 bg-background">
|
|
{docs.map((item: DocumentItem) => (
|
|
<div
|
|
key={item.id}
|
|
className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/30 transition-colors"
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<h4 className="font-medium truncate">{item.title}</h4>
|
|
<p className="text-sm text-muted-foreground">
|
|
{formatDateLast(item.updatedAt)} • {formatBytes(item.sizeBytes)}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => handleDownload(item)}
|
|
disabled={!item.url}
|
|
className="ml-4"
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Télécharger
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function VosDocumentsPage() {
|
|
usePageTitle("Vos documents");
|
|
const [activeTab, setActiveTab] = React.useState('generaux');
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<header className="flex items-center justify-between">
|
|
<h2 className="text-2xl font-semibold tracking-tight">Vos documents</h2>
|
|
</header>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Colonne gauche : Documents disponibles */}
|
|
<div className="lg:col-span-2">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Documents disponibles</CardTitle>
|
|
<CardDescription>
|
|
{activeTab === 'generaux' && 'Téléchargez vos documents généraux'}
|
|
{activeTab === 'comptables' && 'Téléchargez vos documents comptables et sociaux'}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{activeTab === 'generaux' && <SectionGeneraux />}
|
|
{activeTab === 'comptables' && <SectionComptables />}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Colonne droite : Onglets + Transmettre un document */}
|
|
<div className="lg:col-span-1 space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Catégories</CardTitle>
|
|
<CardDescription>Sélectionnez une catégorie</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
<Button
|
|
variant={activeTab === 'generaux' ? 'default' : 'outline'}
|
|
className="w-full justify-start"
|
|
onClick={() => setActiveTab('generaux')}
|
|
>
|
|
<FileText className="h-4 w-4 mr-2" />
|
|
Documents généraux
|
|
</Button>
|
|
<Tooltip
|
|
content="Les documents des caisses seront de nouveau disponibles dans quelques jours"
|
|
side="left"
|
|
>
|
|
<Button
|
|
variant="outline"
|
|
className="w-full justify-start opacity-50 cursor-not-allowed"
|
|
disabled
|
|
>
|
|
<Building2 className="h-4 w-4 mr-2" />
|
|
Caisses & organismes
|
|
</Button>
|
|
</Tooltip>
|
|
<Button
|
|
variant={activeTab === 'comptables' ? 'default' : 'outline'}
|
|
className="w-full justify-start"
|
|
onClick={() => setActiveTab('comptables')}
|
|
>
|
|
<Folder className="h-4 w-4 mr-2" />
|
|
Documents comptables
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<UploadPanel />
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Besoin d'aide ?</CardTitle>
|
|
<CardDescription>
|
|
N'hésitez pas à nous contacter si vous avez besoin d'une attestation spécifique.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|