diff --git a/frontend/src/app/admin/files/reference/page.tsx b/frontend/src/app/admin/files/reference/page.tsx index ff276b5..f0756f6 100644 --- a/frontend/src/app/admin/files/reference/page.tsx +++ b/frontend/src/app/admin/files/reference/page.tsx @@ -1,11 +1,19 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { AppShell } from "@/components/layout/AppShell"; -import { FileUploader } from "@/components/admin/FileUploader"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Table, TableBody, @@ -21,21 +29,130 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { formatDate } from "@/lib/utils"; -import { Upload, Trash2, FileText, Download } from "lucide-react"; -import type { ReferenceFile } from "@/lib/types"; +import { Upload, Trash2, FileText, Download, Loader2 } from "lucide-react"; +import { + getReferenceFiles, + uploadReferenceFile, + deleteReferenceFile, + getClients, +} from "@/lib/api"; +import type { Client } from "@/lib/types"; -const mockRefFiles: ReferenceFile[] = [ - { id: "ref-001", name: "Brand_Guidelines_v3.pdf", file_type: "PDF", locale_code: undefined, client_id: undefined, file_path: "/files/ref/Brand_Guidelines_v3.pdf", uploaded_by: "Sarah Chen", uploaded_at: "2026-02-10T10:00:00Z" }, - { id: "ref-002", name: "Glossary_Master.xlsx", file_type: "XLSX", locale_code: undefined, client_id: undefined, file_path: "/files/ref/Glossary_Master.xlsx", uploaded_by: "Sarah Chen", uploaded_at: "2026-02-15T14:30:00Z" }, - { id: "ref-003", name: "StyleGuide_de-DE.pdf", file_type: "PDF", locale_code: "de-DE", client_id: undefined, file_path: "/files/ref/StyleGuide_de-DE.pdf", uploaded_by: "Emily Brown", uploaded_at: "2026-03-01T09:00:00Z" }, - { id: "ref-004", name: "StyleGuide_fr-FR.pdf", file_type: "PDF", locale_code: "fr-FR", client_id: undefined, file_path: "/files/ref/StyleGuide_fr-FR.pdf", uploaded_by: "Emily Brown", uploaded_at: "2026-03-01T09:05:00Z" }, - { id: "ref-005", name: "OMG_Voice_Profile.docx", file_type: "DOCX", locale_code: undefined, client_id: "client-001", file_path: "/files/ref/OMG_Voice_Profile.docx", uploaded_by: "James Miller", uploaded_at: "2026-03-10T11:00:00Z" }, - { id: "ref-006", name: "Prime_Terminology.xlsx", file_type: "XLSX", locale_code: undefined, client_id: undefined, file_path: "/files/ref/Prime_Terminology.xlsx", uploaded_by: "Sarah Chen", uploaded_at: "2026-03-20T15:45:00Z" }, +const LOCALES = [ + "de-DE", "fr-FR", "it-IT", "es-ES", "nl-NL", + "sv-SE", "pl-PL", "pt-PT", "de-AT", "fr-BE", "nl-BE", "ca-ES", ]; +const FILE_TYPES = [ + { value: "glossary", label: "Glossary" }, + { value: "blacklist", label: "Blacklist" }, + { value: "tov_global", label: "TOV Global" }, + { value: "tov_supplement", label: "TOV Supplement" }, + { value: "locale_considerations", label: "Locale Considerations" }, + { value: "date_pct_formats", label: "Date/Percent Formats" }, +]; + +const FILE_TYPE_LABELS: Record = { + glossary: "Glossary", + blacklist: "Blacklist", + tov_global: "TOV Global", + tov_supplement: "TOV Supplement", + locale_considerations: "Locale", + date_pct_formats: "Date/Pct", +}; + +interface RefFileRow { + id: string; + filename: string; + file_type: string; + locale_scope: string; + uploaded_at: string; +} + export default function ReferenceLibraryPage() { - const [files] = useState(mockRefFiles); + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(true); + const [clients, setClients] = useState([]); + const [clientId, setClientId] = useState(""); const [uploadOpen, setUploadOpen] = useState(false); + const [deleting, setDeleting] = useState(null); + + // Upload form state + const [uploadFile, setUploadFile] = useState(null); + const [uploadLocale, setUploadLocale] = useState("global"); + const [uploadFileType, setUploadFileType] = useState(""); + const [uploading, setUploading] = useState(false); + + useEffect(() => { + getClients().then((c) => { + setClients(c); + const amazon = c.find((cl) => cl.name.toLowerCase() === "amazon"); + if (amazon) setClientId(amazon.id); + else if (c.length > 0) setClientId(c[0].id); + }); + }, []); + + const fetchFiles = useCallback(async () => { + if (!clientId) return; + setLoading(true); + try { + const data = await getReferenceFiles(clientId); + setFiles(data as unknown as RefFileRow[]); + } catch { + setFiles([]); + } finally { + setLoading(false); + } + }, [clientId]); + + useEffect(() => { + fetchFiles(); + }, [fetchFiles]); + + const handleDelete = async (fileId: string) => { + if (!confirm("Delete this reference file?")) return; + setDeleting(fileId); + try { + await deleteReferenceFile(fileId); + setFiles((prev) => prev.filter((f) => f.id !== fileId)); + } catch { + // handled by interceptor + } finally { + setDeleting(null); + } + }; + + const handleDownload = (fileId: string, filename: string) => { + // Open download URL in new tab + const baseUrl = process.env.NEXT_PUBLIC_API_URL || ""; + const token = localStorage.getItem("access_token"); + window.open( + `${baseUrl}/api/v1/files/reference/${fileId}/download?token=${token}`, + "_blank" + ); + }; + + const handleUpload = async () => { + if (!uploadFile || !uploadFileType || !clientId) return; + setUploading(true); + try { + await uploadReferenceFile( + uploadFile, + clientId, + uploadFileType, + uploadLocale || "global" + ); + setUploadOpen(false); + setUploadFile(null); + setUploadLocale("global"); + setUploadFileType(""); + fetchFiles(); + } catch { + // handled by interceptor + } finally { + setUploading(false); + } + }; return ( @@ -51,74 +168,75 @@ export default function ReferenceLibraryPage() { - - - - File Name - Type - Locale - Client - Uploaded By - Date - - - - - {files.map((file) => ( - - -
- - - {file.name} - -
-
- - {file.file_type} - - - {file.locale_code ? ( - {file.locale_code} - ) : ( - Global - )} - - - {file.client_id ? ( - OMG - ) : ( - All - )} - - - {file.uploaded_by} - - - {formatDate(file.uploaded_at)} - - -
- - -
-
+ {loading ? ( +
+ +

Loading reference files...

+
+ ) : files.length === 0 ? ( +
+ +

No reference files uploaded yet

+
+ ) : ( +
+ + + File Name + Type + Locale + Date + - ))} - -
+ + + {files.map((file) => ( + + +
+ + + {file.filename} + +
+
+ + + {FILE_TYPE_LABELS[file.file_type] || file.file_type} + + + + {file.locale_scope === "global" ? ( + Global + ) : ( + {file.locale_scope} + )} + + + {formatDate(file.uploaded_at)} + + +
+ +
+
+
+ ))} +
+ + )}
@@ -126,11 +244,57 @@ export default function ReferenceLibraryPage() { Upload Reference File - setUploadOpen(false)} - /> +
+
+ + setUploadFile(e.target.files?.[0] || null)} + /> +
+
+ + +
+
+ + +
+ +
diff --git a/frontend/src/app/admin/files/tm/page.tsx b/frontend/src/app/admin/files/tm/page.tsx index a69d000..7877092 100644 --- a/frontend/src/app/admin/files/tm/page.tsx +++ b/frontend/src/app/admin/files/tm/page.tsx @@ -1,11 +1,19 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { AppShell } from "@/components/layout/AppShell"; -import { FileUploader } from "@/components/admin/FileUploader"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Table, TableBody, @@ -21,23 +29,99 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { formatDate } from "@/lib/utils"; -import { Upload, Trash2, Database } from "lucide-react"; -import type { TMFile } from "@/lib/types"; +import { Upload, Trash2, Database, Loader2 } from "lucide-react"; +import { getTMFiles, uploadTMFile, deleteTMFile, getClients } from "@/lib/api"; +import type { Client } from "@/lib/types"; -const mockTMFiles: TMFile[] = [ - { id: "tm-001", name: "TM_de-DE_Master.tmx", locale_code: "de-DE", client_id: undefined, file_path: "/files/tm/TM_de-DE_Master.tmx", entry_count: 15420, uploaded_by: "Sarah Chen", uploaded_at: "2026-03-15T10:30:00Z" }, - { id: "tm-002", name: "TM_fr-FR_Master.tmx", locale_code: "fr-FR", client_id: undefined, file_path: "/files/tm/TM_fr-FR_Master.tmx", entry_count: 12890, uploaded_by: "Sarah Chen", uploaded_at: "2026-03-15T10:35:00Z" }, - { id: "tm-003", name: "TM_it-IT_Master.tmx", locale_code: "it-IT", client_id: undefined, file_path: "/files/tm/TM_it-IT_Master.tmx", entry_count: 11230, uploaded_by: "Emily Brown", uploaded_at: "2026-03-20T14:00:00Z" }, - { id: "tm-004", name: "TM_es-ES_Master.tmx", locale_code: "es-ES", client_id: undefined, file_path: "/files/tm/TM_es-ES_Master.tmx", entry_count: 13670, uploaded_by: "Emily Brown", uploaded_at: "2026-03-20T14:05:00Z" }, - { id: "tm-005", name: "TM_de-DE_OMG.tmx", locale_code: "de-DE", client_id: "client-001", file_path: "/files/tm/TM_de-DE_OMG.tmx", entry_count: 3240, uploaded_by: "James Miller", uploaded_at: "2026-04-01T09:00:00Z" }, - { id: "tm-006", name: "TM_nl-NL_Master.tmx", locale_code: "nl-NL", client_id: undefined, file_path: "/files/tm/TM_nl-NL_Master.tmx", entry_count: 8450, uploaded_by: "Sarah Chen", uploaded_at: "2026-04-05T11:20:00Z" }, - { id: "tm-007", name: "TM_sv-SE_Master.tmx", locale_code: "sv-SE", client_id: undefined, file_path: "/files/tm/TM_sv-SE_Master.tmx", entry_count: 7120, uploaded_by: "Sarah Chen", uploaded_at: "2026-04-05T11:25:00Z" }, - { id: "tm-008", name: "TM_pl-PL_Master.tmx", locale_code: "pl-PL", client_id: undefined, file_path: "/files/tm/TM_pl-PL_Master.tmx", entry_count: 6890, uploaded_by: "Emily Brown", uploaded_at: "2026-04-06T16:00:00Z" }, +const LOCALES = [ + "de-DE", "fr-FR", "it-IT", "es-ES", "nl-NL", + "sv-SE", "pl-PL", "pt-PT", "de-AT", "fr-BE", "nl-BE", "ca-ES", ]; +const CHANNELS = [ + "MASS", "VALUE", "ONSITE", "OUTBOUND", "UEFA", + "PrimeDualBenefit", "PrimeGourmetGuard", "PrimeMidfunnel", + "PrimeSpeed", "TheKiss", "DoubleDonut", "EUSelection", "BDA", +]; + +interface TMFileRow { + id: string; + filename: string; + locale_code: string; + channel: string; + segment_count: number; + uploaded_at: string; +} + export default function TMRegistryPage() { - const [files] = useState(mockTMFiles); + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(true); + const [clients, setClients] = useState([]); + const [clientId, setClientId] = useState(""); const [uploadOpen, setUploadOpen] = useState(false); + const [deleting, setDeleting] = useState(null); + + // Upload form state + const [uploadFile, setUploadFile] = useState(null); + const [uploadLocale, setUploadLocale] = useState(""); + const [uploadChannel, setUploadChannel] = useState(""); + const [uploading, setUploading] = useState(false); + + useEffect(() => { + getClients().then((c) => { + setClients(c); + const amazon = c.find((cl) => cl.name.toLowerCase() === "amazon"); + if (amazon) setClientId(amazon.id); + else if (c.length > 0) setClientId(c[0].id); + }); + }, []); + + const fetchFiles = useCallback(async () => { + if (!clientId) return; + setLoading(true); + try { + const data = await getTMFiles(clientId); + setFiles(data as unknown as TMFileRow[]); + } catch { + setFiles([]); + } finally { + setLoading(false); + } + }, [clientId]); + + useEffect(() => { + fetchFiles(); + }, [fetchFiles]); + + const handleDelete = async (fileId: string) => { + if (!confirm("Delete this TM file?")) return; + setDeleting(fileId); + try { + await deleteTMFile(fileId); + setFiles((prev) => prev.filter((f) => f.id !== fileId)); + } catch { + // handled by interceptor + } finally { + setDeleting(null); + } + }; + + const handleUpload = async () => { + if (!uploadFile || !uploadLocale || !uploadChannel || !clientId) return; + setUploading(true); + try { + await uploadTMFile(uploadFile, clientId, uploadLocale, uploadChannel); + setUploadOpen(false); + setUploadFile(null); + setUploadLocale(""); + setUploadChannel(""); + fetchFiles(); + } catch { + // handled by interceptor + } finally { + setUploading(false); + } + }; return ( @@ -53,61 +137,71 @@ export default function TMRegistryPage() { - - - - File Name - Locale - Client - Entries - Uploaded By - Date - - - - - {files.map((file) => ( - - -
- - - {file.name} - -
-
- - {file.locale_code} - - - {file.client_id ? ( - OMG - ) : ( - Global - )} - - - {file.entry_count.toLocaleString()} - - - {file.uploaded_by} - - - {formatDate(file.uploaded_at)} - - - - + {loading ? ( +
+ +

Loading TM files...

+
+ ) : files.length === 0 ? ( +
+ +

No TM files uploaded yet

+
+ ) : ( +
+ + + File Name + Locale + Channel + Entries + Date + - ))} - -
+ + + {files.map((file) => ( + + +
+ + + {file.filename} + +
+
+ + {file.locale_code} + + + {file.channel} + + + {file.segment_count?.toLocaleString() ?? "—"} + + + {formatDate(file.uploaded_at)} + + + + +
+ ))} +
+ + )}
@@ -115,11 +209,54 @@ export default function TMRegistryPage() { Upload Translation Memory - setUploadOpen(false)} - /> +
+
+ + setUploadFile(e.target.files?.[0] || null)} + /> +
+
+ + +
+
+ + +
+ +
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index b13cef8..4b77f04 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -477,21 +477,23 @@ export async function updateClient( // ─── TM Files ──────────────────────────────────────────────────────── -export async function getTMFiles(): Promise { - const response = await api.get("/files/tm"); +export async function getTMFiles(clientId: string): Promise { + const response = await api.get("/files/tm", { + params: { client_id: clientId }, + }); return response.data; } export async function uploadTMFile( file: File, + clientId: string, localeCode: string, - clientId?: string -): Promise { + channel: string +): Promise { const formData = new FormData(); formData.append("file", file); - formData.append("locale_code", localeCode); - if (clientId) formData.append("client_id", clientId); - const response = await api.post("/files/tm", formData, { + const response = await api.post("/files/tm", formData, { + params: { client_id: clientId, locale_code: localeCode, channel }, headers: { "Content-Type": "multipart/form-data" }, }); return response.data; @@ -503,25 +505,31 @@ export async function deleteTMFile(fileId: string): Promise { // ─── Reference Files ───────────────────────────────────────────────── -export async function getReferenceFiles(): Promise { - const response = await api.get("/files/reference"); +export async function getReferenceFiles( + clientId: string +): Promise { + const response = await api.get("/files/reference", { + params: { client_id: clientId }, + }); return response.data; } export async function uploadReferenceFile( file: File, - localeCode?: string, - clientId?: string -): Promise { + clientId: string, + fileType: string, + localeScope: string +): Promise { const formData = new FormData(); formData.append("file", file); - if (localeCode) formData.append("locale_code", localeCode); - if (clientId) formData.append("client_id", clientId); - const response = await api.post( - "/files/reference", - formData, - { headers: { "Content-Type": "multipart/form-data" } } - ); + const response = await api.post("/files/reference", formData, { + params: { + client_id: clientId, + file_type: fileType, + locale_scope: localeScope, + }, + headers: { "Content-Type": "multipart/form-data" }, + }); return response.data; }