'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import { useParams, useRouter } from 'next/navigation'; import Link from 'next/link'; import { ArrowLeft, Library, Calendar, Hash, Save, Trash2, Loader2, CheckCircle2, AlertCircle, Download, Upload, Plus, X, Search, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; import { useUser } from '@/app/dashboard/useUser'; import { useGlossary, useGlossaries, } from '../useGlossaries'; import { exportGlossaryToCsv, parseFileToTerms, generateCsvContent, } from '../csvUtils'; import { SUPPORTED_LANGUAGES } from '../types'; import type { GlossaryTermInput } from '../types'; import { useToast } from '@/components/ui/toast'; import { ProUpgradePrompt } from '../ProUpgradePrompt'; const MAX_TERMS = 500; /** Source term in the chosen language. * Structure du terme : source=FR (fondation), target=EN (fondation), * translations = 11 autres langues (de, es, it, pt, nl, ru, ja, ko, zh, ar, fa). * - lang vide / 'multi' → '' (pas de choix) * - lang === 'fr' → term.source * - lang === 'en' → term.target (fallback EN depuis le champ legacy) * - autre lang → term.translations[lang] * - si manquant → '' (placeholder, JAMAIS une autre langue en fallback) */ function getDisplaySource( term: { source: string; target: string; translations?: Record | null }, lang: string, glossarySourceLang: string ): string { if (!lang || lang === 'multi') return ''; const normalizedLang = lang.toLowerCase(); const normalizedSourceLang = glossarySourceLang.toLowerCase(); if (normalizedLang === normalizedSourceLang) return term.source; const translations = term.translations || {}; return translations[normalizedLang] || ''; } /** Target term in the chosen language. */ function getDisplayTarget( term: { source: string; target: string; translations?: Record | null }, lang: string, glossaryTargetLang: string ): string { if (!lang) return ''; const normalizedLang = lang.toLowerCase(); const normalizedTargetLang = glossaryTargetLang.toLowerCase(); if (normalizedLang === 'multi' || normalizedLang === normalizedTargetLang) return term.target; const translations = term.translations || {}; return translations[normalizedLang] || ''; } export default function GlossaryDetailPage() { const params = useParams(); const router = useRouter(); const id = (params?.id as string) || ''; const { t } = useI18n(); const { data: user, isLoading: isLoadingUser } = useUser(); const { glossary, isLoading, error } = useGlossary(id); const { updateGlossary, deleteGlossary, isUpdating, isDeleting } = useGlossaries(); const { toast } = useToast(); const isPro = user?.tier === 'pro'; const fileInputRef = useRef(null); // Editable state const [name, setName] = useState(''); const [sourceLanguage, setSourceLanguage] = useState('fr'); const [targetLanguage, setTargetLanguage] = useState('multi'); const [terms, setTerms] = useState([]); const [initialized, setInitialized] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); const [searchQuery, setSearchQuery] = useState(''); // Initialize from glossary data useEffect(() => { if (glossary && !initialized) { setName(glossary.name); setSourceLanguage(glossary.source_language || 'fr'); setTargetLanguage(glossary.target_language || 'multi'); setTerms( glossary.terms.map((t) => ({ source: t.source, target: t.target, translations: t.translations || {}, })) ); setInitialized(true); } }, [glossary, initialized]); // Reset on id change useEffect(() => { setInitialized(false); }, [id]); const hasUnsavedChanges = useCallback(() => { if (!glossary) return false; if (name.trim() !== glossary.name) return true; if (sourceLanguage !== (glossary.source_language || 'fr')) return true; if (targetLanguage !== (glossary.target_language || 'multi')) return true; // Compare normalized term payloads (source + target + translations dict). const normalize = (termList: Array<{ source: string; target: string; translations?: Record | null }>) => termList .filter((t) => t.source.trim() && t.target.trim()) .map((t) => { const tr = t.translations || {}; const trKeys = Object.keys(tr).sort(); return `${t.source}|${t.target}|${trKeys.map((k) => `${k}=${tr[k]}`).join(',')}`; }) .sort() .join(';;'); return normalize(terms) !== normalize(glossary.terms); }, [glossary, name, sourceLanguage, targetLanguage, terms]); const validTerms = terms.filter((t) => t.source.trim() && t.target.trim()); const validTermsCount = validTerms.length; const isDirty = hasUnsavedChanges(); const handleAddTerm = () => { if (validTermsCount >= MAX_TERMS) { toast({ variant: 'destructive', title: t('glossaries.detail.maxTermsTitle') || 'Limite atteinte', description: t('glossaries.detail.maxTermsDesc', { max: String(MAX_TERMS) }) || `Maximum ${MAX_TERMS} termes par glossaire.`, }); return; } setTerms([...terms, { source: '', target: '' }]); }; const handleRemoveTerm = (index: number) => { setTerms(terms.filter((_, i) => i !== index)); }; const handleTermChange = (index: number, field: 'source' | 'target', value: string) => { if (!glossary) return; setTerms(terms.map((t, i) => { if (i !== index) return t; const translations = { ...(t.translations || {}) } as Record; const editLang = (field === 'source' ? sourceLanguage : targetLanguage).toLowerCase(); const srcLang = glossary.source_language.toLowerCase(); const tgtLang = glossary.target_language.toLowerCase(); if (field === 'source') { if (editLang === srcLang) { return { ...t, source: value }; } else { if (editLang && editLang !== 'multi') { translations[editLang] = value; } return { ...t, translations }; } } else { if (editLang === 'multi' || editLang === tgtLang) { return { ...t, target: value }; } else { if (editLang && editLang !== 'multi') { translations[editLang] = value; } return { ...t, translations }; } } })); }; const migrateTerms = ( currentTerms: GlossaryTermInput[], oldSrc: string, newSrc: string, oldTgt: string, newTgt: string ): GlossaryTermInput[] => { if (!glossary) return currentTerms; return currentTerms.map(t => { const languagesData: Record = { ...(t.translations || {}) }; const oldSrcVal = oldSrc === glossary.source_language ? t.source : (t.translations?.[oldSrc] || ''); const oldTgtVal = oldTgt === 'multi' || oldTgt === glossary.target_language ? t.target : (t.translations?.[oldTgt] || ''); if (oldSrc && oldSrc !== 'multi') { languagesData[oldSrc] = oldSrcVal; } if (oldTgt) { const langKey = oldTgt === 'multi' ? 'en' : oldTgt; languagesData[langKey] = oldTgtVal; } const newSourceVal = languagesData[newSrc] || ''; const targetLangKey = newTgt === 'multi' ? 'en' : newTgt; const newTargetVal = languagesData[targetLangKey] || ''; delete languagesData[newSrc]; delete languagesData[targetLangKey]; return { source: newSourceVal, target: newTargetVal, translations: languagesData }; }); }; const handleSourceLanguageChange = (newLang: string) => { // For multilingual glossaries, the selector is a VIEW filter only. // migrateTerms must not be called — it would destroy translation data. if (!glossary || glossary.target_language === 'multi') { setSourceLanguage(newLang); return; } const updated = migrateTerms(terms, sourceLanguage, newLang, targetLanguage, targetLanguage); setSourceLanguage(newLang); setTerms(updated); }; const handleTargetLanguageChange = (newLang: string) => { // For multilingual glossaries, the selector is a VIEW filter only. // migrateTerms must not be called — it would destroy translation data. if (!glossary || glossary.target_language === 'multi') { setTargetLanguage(newLang); return; } const updated = migrateTerms(terms, sourceLanguage, sourceLanguage, targetLanguage, newLang); setTargetLanguage(newLang); setTerms(updated); }; const handleSave = async () => { if (!glossary || !name.trim()) return; try { await updateGlossary(glossary.id, { name: name.trim(), source_language: sourceLanguage, target_language: targetLanguage, terms: validTerms, }); toast({ title: t('glossaries.detail.savedTitle') || 'Enregistré', description: t('glossaries.detail.savedDesc') || 'Le glossaire a été mis à jour.', }); } catch { toast({ variant: 'destructive', title: t('glossaries.toast.error') || 'Erreur', description: t('glossaries.toast.errorUpdate') || 'Impossible de mettre à jour le glossaire.', }); } }; const handleDelete = async () => { if (!glossary) return; try { await deleteGlossary(glossary.id); toast({ title: t('glossaries.toast.deleted') || 'Supprimé', description: t('glossaries.toast.deletedDesc') || 'Le glossaire a été supprimé.', }); router.push('/dashboard/glossaries'); } catch { toast({ variant: 'destructive', title: t('glossaries.toast.error') || 'Erreur', description: t('glossaries.toast.errorDelete') || 'Impossible de supprimer le glossaire.', }); } }; const handleExport = () => { if (!glossary) return; const csv = generateCsvContent(validTerms); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${name.replace(/[^a-z0-9]/gi, '_') || 'glossary'}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; const handleImportClick = () => fileInputRef.current?.click(); const handleFileChange = async (e: React.ChangeEvent) => { if (!glossary) return; const file = e.target.files?.[0]; if (!file) return; e.target.value = ''; try { const parsed = await parseFileToTerms(file); if (parsed.length === 0) { toast({ variant: 'destructive', title: t('glossaries.detail.importEmptyTitle') || 'Fichier vide', description: t('glossaries.detail.importEmptyDesc') || 'Aucun terme détecté dans ce fichier.', }); return; } if (parsed.length > MAX_TERMS) { toast({ variant: 'destructive', title: t('glossaries.detail.maxTermsTitle') || 'Trop de termes', description: t('glossaries.detail.maxTermsDesc', { max: String(MAX_TERMS) }) || `Maximum ${MAX_TERMS} termes par glossaire.`, }); return; } // Auto-save immediately to DB await updateGlossary(glossary.id, { name: name.trim(), source_language: sourceLanguage, target_language: targetLanguage, terms: parsed, }); setTerms(parsed); toast({ title: t('glossaries.detail.importedTitle') || 'Importé', description: t('glossaries.detail.importedDesc', { count: String(parsed.length) }) || `${parsed.length} termes importés et enregistrés.`, }); } catch { toast({ variant: 'destructive', title: t('glossaries.toast.error') || 'Erreur', description: t('glossaries.toast.errorUpdate') || 'Impossible de mettre à jour le glossaire.', }); } }; // Filter terms by search const filteredTerms = useCallback(() => { if (!searchQuery.trim()) return terms.map((t, i) => ({ ...t, _index: i })); const q = searchQuery.toLowerCase(); return terms .map((t, i) => ({ ...t, _index: i })) .filter((t) => t.source.toLowerCase().includes(q) || t.target.toLowerCase().includes(q)); }, [terms, searchQuery]); if (isLoadingUser) { return (
); } if (!isPro) { return ; } if (isLoading) { return (

{t('glossaries.loading') || 'Chargement…'}

); } if (error || !glossary) { return (
{t('glossaries.detail.backToList') || 'Retour aux glossaires'}

{t('glossaries.detail.notFoundTitle') || 'Glossaire introuvable'}

{t('glossaries.detail.notFoundDesc') || 'Ce glossaire n\'existe pas ou vous n\'y avez pas accès.'}

); } const srcInfo = SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language); const tgtInfo = SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language); return (
{/* Back link */} {t('glossaries.detail.backToList') || 'Retour aux glossaires'} {/* Header */}
setName(e.target.value)} disabled={isUpdating} className="w-full text-2xl md:text-3xl font-serif font-semibold text-brand-dark dark:text-white tracking-tight leading-tight bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-brand-accent/20 rounded px-2 py-1 -mx-2" />
{srcInfo?.flag ?? '🌐'} {srcInfo?.label ?? glossary.source_language} {tgtInfo?.flag ?? '🌐'} {tgtInfo?.label ?? glossary.target_language} {validTermsCount} {t('glossaries.defineTerms') || 'termes'} {new Date(glossary.created_at).toLocaleDateString()}
{/* Action bar */}
{isDirty && ( {t('glossaries.status.unsaved') || 'Non enregistré'} )}
{/* Settings card */}

{t('glossaries.detail.settingsTitle') || 'Paramètres'}

{t('glossaries.detail.sourceLangNote') || 'La source originale est en français. Pour les autres langues, on lit le champ translations du terme (s\'il existe).'}

{t('glossaries.detail.targetLangNote') || 'Choisissez une langue pour voir les traductions correspondantes, ou « Multilingue » pour la valeur par défaut.'}

{/* Terms table card */}

{t('glossaries.detail.termsTitle') || 'Termes'}

{validTermsCount} / {MAX_TERMS} {t('glossaries.detail.terms') || 'termes'}

{terms.length > 5 && (
setSearchQuery(e.target.value)} placeholder={t('glossaries.detail.searchTerms') || 'Filtrer…'} className="pl-7 pr-2 py-1.5 text-[11px] w-32 sm:w-40 rounded-md border border-black/5 dark:border-white/10 bg-white dark:bg-[#141414] focus:outline-none focus:ring-2 focus:ring-brand-accent/20" />
)}
{terms.length === 0 ? (

{t('glossaries.detail.noTerms') || 'Aucun terme pour l\'instant.'}

) : (
{filteredTerms().map((term) => ( ))}
{t('glossaries.detail.source') || 'Source'} {t('glossaries.detail.target') || 'Cible'}
handleTermChange(term._index, 'source', e.target.value)} disabled={isUpdating} placeholder={t('glossaries.detail.sourcePlaceholder') || 'terme source'} className="w-full bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-brand-accent/20 rounded px-2 py-1.5 -mx-2 text-brand-dark dark:text-white" /> handleTermChange(term._index, 'target', e.target.value)} disabled={isUpdating} placeholder={t('glossaries.detail.targetPlaceholder') || 'terme cible'} className="w-full bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-brand-accent/20 rounded px-2 py-1.5 -mx-2 text-brand-dark dark:text-white" />
)} {/* Add term footer */} {terms.length > 0 && (
{validTermsCount >= MAX_TERMS && ( {t('glossaries.detail.maxReached') || 'Limite maximale atteinte'} )}
)}
{/* CSV + Danger zone */}

{t('glossaries.detail.csvTitle') || 'CSV'}

{t('glossaries.detail.csvDesc') || 'Exportez vos termes en CSV ou importez-en de nouveaux (remplace la liste actuelle).'}

{t('glossaries.detail.dangerTitle') || 'Zone danger'}

{t('glossaries.detail.dangerDesc') || 'La suppression est définitive. Tous les termes associés seront perdus.'}

{!confirmDelete ? ( ) : (

{t('glossaries.detail.confirmDelete') || 'Confirmer la suppression ?'}

)}
); }