From e382372096210ed17ff78346aec5baeb8caa7df2 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:18:11 +0000 Subject: [PATCH] feat(CC-068): add Data Sources registry page (CRUD, type breakdown, sync status) --- src/app/dashboard/data/sources/page.tsx | 187 ++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/app/dashboard/data/sources/page.tsx diff --git a/src/app/dashboard/data/sources/page.tsx b/src/app/dashboard/data/sources/page.tsx new file mode 100644 index 0000000..8d76d66 --- /dev/null +++ b/src/app/dashboard/data/sources/page.tsx @@ -0,0 +1,187 @@ +'use client'; + +import { useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../../lib/api'; +import { useSession } from '../../../../components/session-provider'; + +interface DataSource { + id: string; name: string; sourceType: string; description: string | null; + connectionInfo: string | null; status: string; + lastSyncAt: string | null; recordCount: number | null; createdAt: string; +} + +const TYPE_META: Record = { + manual: { label: 'Manual', icon: '✋' }, + api: { label: 'API', icon: '🔌' }, + file: { label: 'Fișier', icon: '📄' }, + database: { label: 'Bază de date', icon: '🗄️' }, + integration: { label: 'Integrare', icon: '🔗' }, + clickhouse: { label: 'ClickHouse', icon: '🏢' }, + sheet: { label: 'Spreadsheet', icon: '📊' }, +}; + +const STATUS_META: Record = { + active: { label: 'Activ', cls: 'text-signal-ok bg-signal-ok/10' }, + paused: { label: 'Pauză', cls: 'text-signal-warn bg-signal-warn/10' }, + error: { label: 'Eroare', cls: 'text-signal-danger bg-signal-danger/10' }, + archived: { label: 'Arhivat', cls: 'text-ink-faint bg-muted' }, +}; + +const SOURCE_TYPES = ['manual','api','file','database','integration','clickhouse','sheet']; + +export default function DataSourcesPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + + const [showCreate, setShowCreate] = useState(false); + const [form, setForm] = useState({ name: '', sourceType: 'manual', description: '', connectionInfo: '' }); + + const { data: sources = [], isLoading } = useQuery({ + queryKey: ['data-sources', tenantId], + queryFn: () => apiFetch('/v1/data-sources', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const createMut = useMutation({ + mutationFn: (body: Record) => + apiFetch('/v1/data-sources', { tenantId, method: 'POST', body }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['data-sources', tenantId] }); + setShowCreate(false); + setForm({ name: '', sourceType: 'manual', description: '', connectionInfo: '' }); + }, + }); + + const patchMut = useMutation({ + mutationFn: ({ id, ...body }: { id: string; status: string }) => + apiFetch(`/v1/data-sources/${id}`, { tenantId, method: 'PATCH', body }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['data-sources', tenantId] }), + }); + + const totalRecords = sources.reduce((s, ds) => s + (ds.recordCount ?? 0), 0); + const active = sources.filter((ds) => ds.status === 'active').length; + + return ( +
+
+
+

Surse de Date

+

+ {isLoading ? 'Se încarcă…' : `${sources.length} surse · ${active} active · ${totalRecords.toLocaleString('ro-RO')} înregistrări totale`} +

+
+ +
+ + {/* Type breakdown */} + {sources.length > 0 && ( +
+ {SOURCE_TYPES.map((t) => { + const count = sources.filter((s) => s.sourceType === t).length; + if (!count) return null; + const m = TYPE_META[t] ?? { label: t, icon: '❓' }; + return ( + + {m.icon} {m.label} ({count}) + + ); + })} +
+ )} + + {/* Create form */} + {showCreate && ( +
+

Sursă nouă

+
+
+ setForm({ ...form, name: e.target.value })} + className="w-full rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ + setForm({ ...form, description: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ setForm({ ...form, connectionInfo: e.target.value })} + className="w-full rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+
+
+ + +
+
+ )} + + {/* Sources list */} + {!isLoading && sources.length === 0 ? ( +
+

🗄️

+

Nicio sursă de date înregistrată.

+

Adaugă sursele de date pe care le utilizezi: baze de date, API-uri, fișiere, etc.

+
+ ) : ( +
+ {sources.map((ds) => { + const tm = TYPE_META[ds.sourceType] ?? { icon: '❓', label: ds.sourceType }; + const sm = STATUS_META[ds.status] ?? STATUS_META.active; + return ( +
+ {tm.icon} +
+
+ {ds.name} + {sm.label} + {tm.label} +
+ {ds.description &&

{ds.description}

} + {ds.connectionInfo && ( +

{ds.connectionInfo}

+ )} +
+ {ds.recordCount != null && ( + {ds.recordCount.toLocaleString('ro-RO')} înreg. + )} + {ds.lastSyncAt && ( + + Sync: {new Date(ds.lastSyncAt).toLocaleDateString('ro-RO')} + + )} +
+
+
+ {ds.status === 'active' && ( + + )} + {ds.status === 'paused' && ( + + )} +
+
+ ); + })} +
+ )} +
+ ); +}