diff --git a/src/App.tsx b/src/App.tsx index 1a5f7a8..16a757e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,8 @@ import { DailyLogsPage } from './screens/DailyLogsPage' import { MyDailyLogsPage } from './screens/MyDailyLogsPage' import { DailySalesReportPage } from './screens/DailySalesReportPage' import { ReportPage } from './screens/ReportPage' +import { DatasetsPage } from './screens/admin/DatasetsPage' +import { DatasetItemsPage } from './screens/admin/DatasetItemsPage' function App() { return ( @@ -42,6 +44,10 @@ function App() { } /> } /> + + } /> + } /> + } /> diff --git a/src/api/client.ts b/src/api/client.ts index 840769d..c5e7fa6 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -3,6 +3,7 @@ import type { AiDecision, ApiError, AuditEntry, + Dataset, FormScreenResponse, LoginResponse, RecordViewParams, @@ -301,6 +302,24 @@ export class ZinoClient { }); } + // --- Datasets --- + + getDatasets(): Promise { + return this.request('GET', `/app/${APP_ID}/datasets`); + } + + getDataset(id: number | string): Promise { + return this.request('GET', `/app/${APP_ID}/datasets/${id}`); + } + + createDataset(payload: { name: string; data_type: 'string' | 'object'; keys: string[]; description?: string }): Promise { + return this.request('POST', `/app/${APP_ID}/datasets`, payload); + } + + updateDataset(id: number | string, payload: Partial<{ name: string; data_type: 'string' | 'object'; keys: string[]; description: string; items: any[] }>): Promise { + return this.request('PUT', `/app/${APP_ID}/datasets/${id}`, payload); + } + // --- File upload + OCR (multipart, field-scoped) --- private fieldForm(file: File, ctx: FieldContext): FormData { diff --git a/src/api/types.ts b/src/api/types.ts index 2a1efbe..ea4e7a3 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -19,6 +19,19 @@ export interface LoginResponse { user: User; } +// --- Datasets --- + +export interface Dataset { + id: number; + uuid?: string; + name: string; + description?: string; + data_type: 'string' | 'object'; + keys: string[]; + item_count?: number; + items?: any[]; +} + // --- Record view (POST /app/{appId}/view/recordview) --- export interface RecordViewField { diff --git a/src/screens/ConsoleLayout.tsx b/src/screens/ConsoleLayout.tsx index 6e53bbb..bbe7cad 100644 --- a/src/screens/ConsoleLayout.tsx +++ b/src/screens/ConsoleLayout.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import { NavLink, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom'; -import { LogOut, ChevronDown } from 'lucide-react'; +import { LogOut, ChevronDown, Database } from 'lucide-react'; import { cn } from '../lib/cn'; import { useAuth } from '../auth/context'; import { onAuthErrorAll, orderBookingClient } from '../api/clients'; @@ -78,6 +78,7 @@ export function ConsoleLayout() { ))} +
+
+ cn("w-full text-left px-3 py-2 text-sm font-medium hover:bg-indigo-50 hover:text-indigo-700 rounded-md transition-colors flex items-center gap-2 cursor-pointer", isActive ? "text-indigo-700 bg-indigo-50" : "text-gray-700")} + > + + Manage Datasets + +
+
+

+ {dataset.name} + + {items.length} {isString ? 'Items' : 'Rows'} + +

+

Manage items for this dataset

+
+
+
+
+ + +
+ {selectedRows.size > 0 && ( + + )} + {hasChanges && ( + + )} + +
+ + + {error &&
{error}
} + +
+ {isString ? ( + <> +
+

+ {dataset.name} +

+
+
+ + + + + + + + + + + {items.map((val, idx) => ( + + + + + + + ))} + {items.length === 0 && ( + + + + )} + +
+ 0} onChange={toggleSelectAll} className="rounded border-emerald-800 text-emerald-600 focus:ring-emerald-500 cursor-pointer" /> + #ValueActions
+ toggleRowSelection(idx)} + className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500 cursor-pointer" + /> + {idx + 1} + + handleStringChange(idx, e.target.value)} + className="absolute inset-0 w-full h-full px-4 py-2 border-none bg-transparent text-sm focus:outline-none focus:ring-2 focus:ring-inset focus:ring-emerald-500 transition-colors" + placeholder="Enter value" + /> + + +
+ No string items exist. +
+
+
+ +
+ + ) : ( + <> +
+

+ {dataset.name} +

+
+
+ + + + + + {dataset.keys?.map(k => ( + + ))} + + + + + {items.map((rowObj, idx) => ( + + + + {dataset.keys?.map(k => { + const isInvalid = invalidCells.has(`${idx}-${k}`); + return ( + + ); + })} + + + ))} + {items.length === 0 && ( + + + + )} + +
+ 0} onChange={toggleSelectAll} className="rounded border-emerald-800 text-emerald-600 focus:ring-emerald-500 cursor-pointer" /> + #{k}Actions
+ toggleRowSelection(idx)} + className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500 cursor-pointer" + /> + {idx + 1} + + handleObjectChange(idx, k, e.target.value)} + className={`absolute inset-0 w-full h-full px-4 py-2 border-none bg-transparent text-sm focus:outline-none transition-colors ${isInvalid + ? 'ring-2 ring-inset ring-red-400 bg-red-50/50 focus:ring-red-500' + : 'focus:ring-2 focus:ring-inset focus:ring-emerald-500' + }`} + placeholder={`Enter ${k}`} + /> + + +
+ No data rows available. +
+
+
+ +
+ + )} +
+ + ); +} diff --git a/src/screens/admin/DatasetsPage.tsx b/src/screens/admin/DatasetsPage.tsx new file mode 100644 index 0000000..fd9616e --- /dev/null +++ b/src/screens/admin/DatasetsPage.tsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from 'react'; +import { orderBookingClient } from '../../api/clients'; +import type { Dataset } from '../../api/types'; +import { Card } from '../../components/reusable/Card'; +import { Button } from '../../components/buttons/Button'; +import { Spinner } from '../../components/reusable/Spinner'; +import { Database, List } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; + +export function DatasetsPage() { + const navigate = useNavigate(); + const [datasets, setDatasets] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + fetchDatasets(); + }, []); + + const fetchDatasets = async () => { + try { + setLoading(true); + const res = await orderBookingClient.getDatasets(); + setDatasets(res); + setError(''); + } catch (err: any) { + setError(err.message || 'Failed to fetch datasets'); + } finally { + setLoading(false); + } + }; + + + + return ( +
+
+

+ Datasets +

+ +
+ + {error &&
{error}
} + + + {loading ? ( +
+ ) : ( +
+ + + + + + + + + + {datasets.map((ds) => ( + + + + + + ))} + {datasets.length === 0 && ( + + + + )} + +
NameItemsActions
{ds.name}{ds.item_count || 0} + +
No datasets found.
+
+ )} +
+ + +
+ ); +}