- Créer hook useStaffOrgSelection avec persistence localStorage - Ajouter badge StaffOrgBadge dans Sidebar - Synchroniser filtres org dans toutes les pages (contrats, cotisations, facturation, etc.) - Fix calcul cachets: utiliser totalQuantities au lieu de dates.length - Fix structure field bug: ne plus écraser avec production_name - Ajouter création note lors modification contrat - Implémenter montants personnalisés pour virements salaires - Migrations SQL: custom_amount + fix_structure_field - Réorganiser boutons ContractEditor en carte flottante droite
851 lines
29 KiB
TypeScript
851 lines
29 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, Loader2 } from 'lucide-react'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import { usePageTitle } from '@/hooks/usePageTitle'
|
|
import { Tooltip } from '@/components/ui/tooltip'
|
|
import { supabase } from '@/lib/supabaseClient'
|
|
import { DocumentViewModal } from '@/components/DocumentViewModal'
|
|
import Cookies from 'js-cookie'
|
|
import { useStaffOrgSelection } from '@/hooks/useStaffOrgSelection'
|
|
|
|
type DocumentItem = {
|
|
id: string
|
|
title: string
|
|
url?: string
|
|
updatedAt?: string
|
|
sizeBytes?: number
|
|
meta?: Record<string, any>
|
|
period_label?: string | null
|
|
}
|
|
|
|
type Organization = {
|
|
id: string
|
|
name: string
|
|
key: string
|
|
}
|
|
|
|
type GeneralDocument = {
|
|
type: string
|
|
label: string
|
|
available: boolean
|
|
key?: string
|
|
name?: string
|
|
size?: number
|
|
lastModified?: string
|
|
downloadUrl?: string
|
|
}
|
|
|
|
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({ selectedOrgId }: { selectedOrgId?: string }) {
|
|
const [selectedDoc, setSelectedDoc] = React.useState<GeneralDocument | null>(null);
|
|
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
|
const [orgId, setOrgId] = React.useState<string | null>(null);
|
|
const [isLoadingOrgId, setIsLoadingOrgId] = React.useState(true);
|
|
|
|
// Récupérer l'orgId depuis les props (staff) ou depuis l'API /me (client)
|
|
React.useEffect(() => {
|
|
const fetchOrgId = async () => {
|
|
if (selectedOrgId) {
|
|
// Staff: utiliser l'org sélectionnée
|
|
setOrgId(selectedOrgId);
|
|
setIsLoadingOrgId(false);
|
|
} else {
|
|
// Client: récupérer depuis l'API /me
|
|
try {
|
|
const response = await fetch('/api/me');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setOrgId(data.active_org_id || null);
|
|
} else {
|
|
setOrgId(null);
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur récupération org_id:', error);
|
|
setOrgId(null);
|
|
} finally {
|
|
setIsLoadingOrgId(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
fetchOrgId();
|
|
}, [selectedOrgId]);
|
|
|
|
const { data: documentsGeneraux, isLoading, error } = useQuery<GeneralDocument[]>({
|
|
queryKey: ['documents', 'generaux', orgId],
|
|
queryFn: async () => {
|
|
if (!orgId) {
|
|
throw new Error('Aucune organisation sélectionnée');
|
|
}
|
|
|
|
const res = await fetch(`/api/documents/generaux?org_id=${encodeURIComponent(orgId)}`);
|
|
|
|
if (!res.ok) {
|
|
const errorText = await res.text();
|
|
throw new Error('Erreur lors de la récupération des documents');
|
|
}
|
|
|
|
const data = await res.json();
|
|
return data.documents || [];
|
|
},
|
|
enabled: !!orgId
|
|
});
|
|
|
|
const handleViewDocument = (doc: GeneralDocument) => {
|
|
if (doc.available && doc.downloadUrl) {
|
|
setSelectedDoc(doc);
|
|
setIsModalOpen(true);
|
|
}
|
|
};
|
|
|
|
if (isLoadingOrgId) {
|
|
return (
|
|
<div className="text-center py-8">
|
|
<p className="text-muted-foreground">Chargement...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!orgId) {
|
|
return (
|
|
<div className="text-center py-8">
|
|
<p className="text-muted-foreground">
|
|
Aucune organisation associée
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="text-center py-8">
|
|
<p className="text-muted-foreground">Chargement des documents...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="text-center py-8">
|
|
<p className="text-red-500">Erreur: {String(error)}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!documentsGeneraux || documentsGeneraux.length === 0) {
|
|
return (
|
|
<div className="text-center py-8">
|
|
<p className="text-muted-foreground">Aucun document trouvé</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{documentsGeneraux && documentsGeneraux.map((doc) => (
|
|
<Card
|
|
key={doc.type}
|
|
className={`cursor-pointer transition-all ${
|
|
doc.available
|
|
? 'hover:shadow-md hover:border-blue-300'
|
|
: 'opacity-60 cursor-not-allowed'
|
|
}`}
|
|
onClick={() => doc.available && handleViewDocument(doc)}
|
|
>
|
|
<CardHeader>
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<FileText className="h-4 w-4" />
|
|
{doc.label}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{doc.available ? (
|
|
<div className="space-y-1">
|
|
{doc.size && (
|
|
<p className="text-sm text-muted-foreground">
|
|
{formatBytes(doc.size)}
|
|
</p>
|
|
)}
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="w-full mt-2"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleViewDocument(doc);
|
|
}}
|
|
>
|
|
<FileText className="h-4 w-4 mr-2" />
|
|
Consulter
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="text-sm text-muted-foreground">
|
|
Non concerné ou indisponible
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{selectedDoc && (
|
|
<DocumentViewModal
|
|
isOpen={isModalOpen}
|
|
onClose={() => {
|
|
setIsModalOpen(false);
|
|
setSelectedDoc(null);
|
|
}}
|
|
document={selectedDoc.available && selectedDoc.downloadUrl ? {
|
|
name: selectedDoc.name || '',
|
|
label: selectedDoc.label,
|
|
downloadUrl: selectedDoc.downloadUrl,
|
|
size: selectedDoc.size
|
|
} : null}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
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()
|
|
|
|
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 [expandedYears, setExpandedYears] = React.useState<Set<string>>(new Set())
|
|
const [expandedPeriods, setExpandedPeriods] = React.useState<Set<string>>(new Set())
|
|
const [loadedPeriods, setLoadedPeriods] = React.useState<Set<string>>(new Set())
|
|
|
|
// Récupérer uniquement les métadonnées (sans URLs pré-signées)
|
|
const { data: documentsCompta, isLoading, error } = useQuery<DocumentItem[]>({
|
|
queryKey: ['documents', 'comptables', 'metadata'],
|
|
queryFn: async () => {
|
|
const res = await fetch('/api/documents?category=docs_comptables&metadata_only=true')
|
|
const data = await res.json()
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
const result = Array.isArray(data) ? data : []
|
|
return result
|
|
}
|
|
})
|
|
|
|
// Charger les URLs pour une période spécifique
|
|
const { data: periodUrls = {} } = useQuery<Record<string, DocumentItem[]>>({
|
|
queryKey: ['documents', 'comptables', 'urls', Array.from(loadedPeriods)],
|
|
queryFn: async () => {
|
|
if (loadedPeriods.size === 0) return {}
|
|
|
|
const urls: Record<string, DocumentItem[]> = {}
|
|
|
|
for (const period of Array.from(loadedPeriods)) {
|
|
const res = await fetch(`/api/documents?category=docs_comptables&period=${encodeURIComponent(period)}`)
|
|
const data = await res.json()
|
|
|
|
let docs = []
|
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
docs = data.documents || data.data || data.items || []
|
|
} else {
|
|
docs = Array.isArray(data) ? data : []
|
|
}
|
|
|
|
urls[period] = docs
|
|
}
|
|
|
|
return urls
|
|
},
|
|
enabled: loadedPeriods.size > 0
|
|
})
|
|
|
|
// Extraire l'année depuis le period_label (format: "2507-juillet-2025")
|
|
const extractYear = (periodLabel: string): string => {
|
|
const match = periodLabel.match(/-(\d{4})$/)
|
|
return match ? match[1] : 'Année inconnue'
|
|
}
|
|
|
|
// Grouper les documents par année puis par période
|
|
const documentsByYear = React.useMemo((): Map<string, Map<string, DocumentItem[]>> => {
|
|
if (!documentsCompta || documentsCompta.length === 0) return new Map()
|
|
|
|
const grouped = new Map<string, Map<string, DocumentItem[]>>()
|
|
|
|
documentsCompta.forEach(doc => {
|
|
const period = doc.period_label || 'Sans période'
|
|
const year = period === 'Sans période' ? 'Sans année' : extractYear(period)
|
|
|
|
if (!grouped.has(year)) {
|
|
grouped.set(year, new Map())
|
|
}
|
|
|
|
const yearGroup = grouped.get(year)!
|
|
if (!yearGroup.has(period)) {
|
|
yearGroup.set(period, [])
|
|
}
|
|
|
|
yearGroup.get(period)!.push(doc)
|
|
})
|
|
|
|
// Trier les années par ordre décroissant
|
|
const sortedYears = Array.from(grouped.entries()).sort((a, b) => {
|
|
if (a[0] === 'Sans année') return 1
|
|
if (b[0] === 'Sans année') return -1
|
|
return b[0].localeCompare(a[0])
|
|
})
|
|
|
|
// Pour chaque année, trier les périodes
|
|
sortedYears.forEach(([_, periods]) => {
|
|
const sortedPeriods = Array.from(periods.entries()).sort((a, b) => {
|
|
if (a[0] === 'Sans période') return 1
|
|
if (b[0] === 'Sans période') return -1
|
|
return b[0].localeCompare(a[0])
|
|
})
|
|
periods.clear()
|
|
sortedPeriods.forEach(([period, docs]) => periods.set(period, docs))
|
|
})
|
|
|
|
return new Map(sortedYears)
|
|
}, [documentsCompta])
|
|
|
|
const toggleYear = (year: string) => {
|
|
setExpandedYears(prev => {
|
|
const next = new Set(prev)
|
|
if (next.has(year)) {
|
|
next.delete(year)
|
|
} else {
|
|
next.add(year)
|
|
}
|
|
return next
|
|
})
|
|
}
|
|
|
|
const togglePeriod = (period: string) => {
|
|
setExpandedPeriods(prev => {
|
|
const next = new Set(prev)
|
|
if (next.has(period)) {
|
|
next.delete(period)
|
|
} else {
|
|
next.add(period)
|
|
// Charger les URLs pour cette période
|
|
setLoadedPeriods(loaded => new Set([...loaded, period]))
|
|
}
|
|
return next
|
|
})
|
|
}
|
|
|
|
const handleDownload = (item: DocumentItem, period: string) => {
|
|
// Chercher l'URL dans les données chargées
|
|
const docsWithUrls = periodUrls[period]
|
|
if (docsWithUrls) {
|
|
const docWithUrl = docsWithUrls.find(d => d.id === item.id)
|
|
if (docWithUrl?.url) {
|
|
window.open(docWithUrl.url, '_blank')
|
|
return
|
|
}
|
|
}
|
|
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(documentsByYear.entries()).map(([year, periods]) => {
|
|
const isYearExpanded = expandedYears.has(year)
|
|
const totalDocs = Array.from(periods.values()).reduce((sum, docs: DocumentItem[]) => sum + docs.length, 0)
|
|
|
|
return (
|
|
<div key={year} className="border rounded-lg overflow-hidden">
|
|
{/* Header de l'année - cliquable */}
|
|
<button
|
|
onClick={() => toggleYear(year)}
|
|
className="w-full flex items-center justify-between p-4 bg-muted/50 hover:bg-muted/70 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{isYearExpanded ? (
|
|
<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-bold text-lg">{year}</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{totalDocs} document{totalDocs > 1 ? 's' : ''} • {periods.size} période{periods.size > 1 ? 's' : ''}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Liste des périodes pour cette année */}
|
|
{isYearExpanded && (
|
|
<div className="bg-background">
|
|
{Array.from(periods.entries()).map(([period, docs]: [string, DocumentItem[]]) => {
|
|
const isPeriodExpanded = expandedPeriods.has(period)
|
|
const isLoading = isPeriodExpanded && loadedPeriods.has(period) && !periodUrls[period]
|
|
|
|
return (
|
|
<div key={period} className="border-t">
|
|
{/* Header de la période */}
|
|
<button
|
|
onClick={() => togglePeriod(period)}
|
|
className="w-full flex items-center justify-between p-3 pl-12 hover:bg-muted/30 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{isPeriodExpanded ? (
|
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
)}
|
|
<div className="text-left">
|
|
<h4 className="font-semibold text-sm">{period}</h4>
|
|
<p className="text-xs text-muted-foreground">
|
|
{docs.length} document{docs.length > 1 ? 's' : ''}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Liste des documents */}
|
|
{isPeriodExpanded && (
|
|
<div className="p-2 pl-12 space-y-2">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="h-6 w-6 text-blue-600 animate-spin" />
|
|
</div>
|
|
) : (
|
|
docs.map((item: DocumentItem) => (
|
|
<div
|
|
key={item.id}
|
|
className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/20 transition-colors"
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<h5 className="font-medium text-sm truncate">{item.title}</h5>
|
|
<p className="text-xs text-muted-foreground">
|
|
{formatDateLast(item.updatedAt)} • {formatBytes(item.sizeBytes)}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => handleDownload(item, period)}
|
|
disabled={isLoading}
|
|
className="ml-4"
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Télécharger
|
|
</Button>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function VosDocumentsPage() {
|
|
usePageTitle("Vos documents");
|
|
|
|
// Helper pour valider les UUIDs
|
|
const isValidUUID = (str: string | null): boolean => {
|
|
if (!str) return false;
|
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
|
|
};
|
|
|
|
// Zustand store pour la sélection d'organisation (staff)
|
|
const {
|
|
selectedOrgId: globalSelectedOrgId,
|
|
setSelectedOrg: setGlobalSelectedOrg
|
|
} = useStaffOrgSelection();
|
|
|
|
const [activeTab, setActiveTab] = React.useState('comptables');
|
|
// État local initialisé avec la valeur globale si elle est un UUID valide
|
|
const [selectedOrgId, setSelectedOrgId] = React.useState<string>(
|
|
isValidUUID(globalSelectedOrgId) ? globalSelectedOrgId : ''
|
|
);
|
|
const [isStaff, setIsStaff] = React.useState(false);
|
|
const [isCheckingStaff, setIsCheckingStaff] = React.useState(true);
|
|
|
|
// Récupérer les informations de l'utilisateur et vérifier s'il est staff via API
|
|
React.useEffect(() => {
|
|
const checkStaffStatus = async () => {
|
|
setIsCheckingStaff(true);
|
|
try {
|
|
const response = await fetch('/api/me');
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setIsStaff(data.is_staff === true);
|
|
} else {
|
|
setIsStaff(false);
|
|
}
|
|
} catch (error) {
|
|
setIsStaff(false);
|
|
} finally {
|
|
setIsCheckingStaff(false);
|
|
}
|
|
};
|
|
|
|
checkStaffStatus();
|
|
}, []);
|
|
|
|
// Récupérer la liste des organisations (pour staff uniquement)
|
|
const { data: organizations, isLoading: isLoadingOrgs } = useQuery<Organization[]>({
|
|
queryKey: ['organizations', 'all'],
|
|
queryFn: async () => {
|
|
const response = await fetch('/api/organizations');
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch organizations');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// L'API retourne { items: [...] }
|
|
const items = data.items || [];
|
|
|
|
// Trier par nom et mapper au bon format
|
|
const sorted = items
|
|
.map((org: any) => ({
|
|
id: org.id,
|
|
name: org.name,
|
|
key: org.structure_api || org.key
|
|
}))
|
|
.sort((a: Organization, b: Organization) =>
|
|
a.name.localeCompare(b.name)
|
|
);
|
|
|
|
return sorted;
|
|
},
|
|
enabled: isStaff && !isCheckingStaff
|
|
});
|
|
|
|
// Synchronisation bidirectionnelle : global → local
|
|
React.useEffect(() => {
|
|
if (isStaff && isValidUUID(globalSelectedOrgId)) {
|
|
setSelectedOrgId(globalSelectedOrgId);
|
|
}
|
|
}, [globalSelectedOrgId, isStaff]);
|
|
|
|
// Mettre à jour le cookie active_org_id quand l'organisation sélectionnée change
|
|
React.useEffect(() => {
|
|
if (selectedOrgId && isStaff) {
|
|
const selectedOrg = organizations?.find(org => org.id === selectedOrgId);
|
|
if (selectedOrg) {
|
|
// Mettre à jour les cookies
|
|
document.cookie = `active_org_id=${selectedOrgId}; path=/; max-age=31536000`;
|
|
document.cookie = `active_org_name=${encodeURIComponent(selectedOrg.name)}; path=/; max-age=31536000`;
|
|
document.cookie = `active_org_key=${selectedOrg.key}; path=/; max-age=31536000`;
|
|
}
|
|
}
|
|
}, [selectedOrgId, isStaff, organizations]);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<header className="flex items-center justify-between">
|
|
<h2 className="text-2xl font-semibold tracking-tight">Vos documents</h2>
|
|
{isStaff && (
|
|
<span className="text-sm bg-blue-100 text-blue-800 px-3 py-1 rounded-full">
|
|
Mode Staff
|
|
</span>
|
|
)}
|
|
</header>
|
|
|
|
{/* Sélecteur d'organisation pour le staff */}
|
|
{isStaff && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<Building2 className="h-4 w-4" />
|
|
Sélectionner une organisation
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Choisissez l'organisation dont vous souhaitez consulter les documents
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoadingOrgs ? (
|
|
<p className="text-sm text-gray-500">Chargement des organisations...</p>
|
|
) : organizations && organizations.length > 0 ? (
|
|
<select
|
|
value={selectedOrgId}
|
|
onChange={(e) => {
|
|
const newOrgId = e.target.value;
|
|
setSelectedOrgId(newOrgId);
|
|
|
|
// Synchronisation bidirectionnelle : local → global
|
|
if (newOrgId) {
|
|
const selectedOrg = organizations.find(org => org.id === newOrgId);
|
|
if (selectedOrg) {
|
|
setGlobalSelectedOrg(newOrgId, selectedOrg.name);
|
|
}
|
|
} else {
|
|
setGlobalSelectedOrg(null, null);
|
|
}
|
|
}}
|
|
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
|
|
>
|
|
<option value="">-- Sélectionner une organisation --</option>
|
|
{organizations.map((org) => (
|
|
<option key={org.id} value={org.id}>
|
|
{org.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<p className="text-sm text-amber-600">Aucune organisation trouvée</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Message si staff sans organisation sélectionnée */}
|
|
{isStaff && !selectedOrgId && !isLoadingOrgs && (
|
|
<Card className="border-amber-200 bg-amber-50">
|
|
<CardContent className="pt-6">
|
|
<p className="text-sm text-amber-800">
|
|
⚠️ Veuillez sélectionner une organisation pour afficher ses documents
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
<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'}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{activeTab === 'generaux' && <SectionGeneraux selectedOrgId={selectedOrgId} />}
|
|
{activeTab === 'comptables' && <SectionComptables />}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Colonne droite : Onglets + Transmettre un document */}
|
|
<div className="lg:col-span-1 space-y-4">
|
|
<Card className="border-2 border-blue-200 shadow-md">
|
|
<CardHeader className="bg-gradient-to-r from-blue-50 to-indigo-50 pb-3">
|
|
<CardTitle className="text-lg font-bold text-blue-900 flex items-center gap-2">
|
|
<Folder className="h-5 w-5 text-blue-600" />
|
|
Catégories de documents
|
|
</CardTitle>
|
|
<CardDescription className="text-blue-700 font-medium">
|
|
Choisissez le type de document
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3 pt-5">
|
|
<Button
|
|
variant={activeTab === 'comptables' ? 'default' : 'outline'}
|
|
className={`w-full justify-start h-auto py-4 text-base font-semibold transition-all ${
|
|
activeTab === 'comptables'
|
|
? 'bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg scale-105'
|
|
: 'bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-200 text-blue-900 hover:from-blue-100 hover:to-indigo-100 hover:border-blue-300 hover:scale-102'
|
|
}`}
|
|
onClick={() => setActiveTab('comptables')}
|
|
>
|
|
<Folder className="h-5 w-5 mr-3" />
|
|
<span className="flex-1 text-left">Documents comptables</span>
|
|
{activeTab === 'comptables' && (
|
|
<span className="ml-2 text-xs bg-white/20 px-2 py-1 rounded-full">✓ Actif</span>
|
|
)}
|
|
</Button>
|
|
<Button
|
|
variant={activeTab === 'generaux' ? 'default' : 'outline'}
|
|
className={`w-full justify-start h-auto py-4 text-base font-semibold transition-all ${
|
|
activeTab === 'generaux'
|
|
? 'bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg scale-105'
|
|
: 'bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-200 text-blue-900 hover:from-blue-100 hover:to-indigo-100 hover:border-blue-300 hover:scale-102'
|
|
}`}
|
|
onClick={() => setActiveTab('generaux')}
|
|
>
|
|
<FileText className="h-5 w-5 mr-3" />
|
|
<span className="flex-1 text-left">Documents généraux</span>
|
|
{activeTab === 'generaux' && (
|
|
<span className="ml-2 text-xs bg-white/20 px-2 py-1 rounded-full">✓ Actif</span>
|
|
)}
|
|
</Button>
|
|
<Tooltip
|
|
content="Les documents des caisses seront de nouveau disponibles dans quelques jours"
|
|
side="left"
|
|
>
|
|
<Button
|
|
variant="outline"
|
|
className="w-full justify-start h-auto py-4 text-base font-semibold opacity-50 cursor-not-allowed bg-gradient-to-r from-gray-50 to-slate-50 border-gray-200"
|
|
disabled
|
|
>
|
|
<Building2 className="h-5 w-5 mr-3" />
|
|
<span className="flex-1 text-left">Caisses & organismes</span>
|
|
<span className="ml-2 text-xs bg-gray-100 px-2 py-1 rounded-full">Bientôt</span>
|
|
</Button>
|
|
</Tooltip>
|
|
</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>
|
|
)
|
|
}
|