added admin page dataset
This commit is contained in:
parent
d36f16d5bb
commit
ddbb81b400
@ -45,8 +45,9 @@ function App() {
|
|||||||
<Route path="reports/:reportType" element={<ReportPage />} />
|
<Route path="reports/:reportType" element={<ReportPage />} />
|
||||||
<Route path="sales-report" element={<DailySalesReportPage />} />
|
<Route path="sales-report" element={<DailySalesReportPage />} />
|
||||||
|
|
||||||
<Route path="admin/datasets" element={<DatasetsPage />} />
|
<Route path="admin/datasets" element={<DatasetsPage />}>
|
||||||
<Route path="admin/datasets/:id" element={<DatasetItemsPage />} />
|
<Route path=":id" element={<DatasetItemsPage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path="*" element={<Navigate to="/orders" replace />} />
|
<Route path="*" element={<Navigate to="/orders" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@ -21,31 +21,34 @@ export function DailySalesReportPage() {
|
|||||||
const [reportData, setReportData] = useState<any>(null);
|
const [reportData, setReportData] = useState<any>(null);
|
||||||
|
|
||||||
const downloadPDF = () => {
|
const downloadPDF = () => {
|
||||||
const doc = new jsPDF();
|
const doc = new jsPDF({ orientation: 'landscape' });
|
||||||
|
const pageWidth = doc.internal.pageSize.getWidth();
|
||||||
|
const centerX = pageWidth / 2;
|
||||||
|
const rightX = pageWidth - 14;
|
||||||
|
|
||||||
// Headers
|
// Headers
|
||||||
doc.setFontSize(16);
|
doc.setFontSize(16);
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.text('Krishna Flour Mills (Bangalore) Pvt. Limited', 105, 15, { align: 'center' });
|
doc.text('Krishna Flour Mills (Bangalore) Pvt. Limited', centerX, 15, { align: 'center' });
|
||||||
|
|
||||||
doc.setFontSize(11);
|
doc.setFontSize(11);
|
||||||
doc.setFont('helvetica', 'normal');
|
doc.setFont('helvetica', 'normal');
|
||||||
doc.text('19, Platform Road, Bengaluru - 560 020', 105, 22, { align: 'center' });
|
doc.text('19, Platform Road, Bengaluru - 560 020', centerX, 22, { align: 'center' });
|
||||||
|
|
||||||
doc.setFontSize(12);
|
doc.setFontSize(12);
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.text('DAILY ORDER REPORT', 105, 30, { align: 'center' });
|
doc.text('DAILY ORDER REPORT', centerX, 30, { align: 'center' });
|
||||||
|
|
||||||
// Simple underline for DAILY ORDER REPORT
|
// Simple underline for DAILY ORDER REPORT
|
||||||
const textWidth = doc.getTextWidth('DAILY ORDER REPORT');
|
const textWidth = doc.getTextWidth('DAILY ORDER REPORT');
|
||||||
doc.setLineWidth(0.5);
|
doc.setLineWidth(0.5);
|
||||||
doc.line(105 - textWidth / 2, 31, 105 + textWidth / 2, 31);
|
doc.line(centerX - textWidth / 2, 31, centerX + textWidth / 2, 31);
|
||||||
|
|
||||||
// Meta Info
|
// Meta Info
|
||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.text(`Distributor Name : ${distributor}`, 14, 40);
|
doc.text(`Distributor Name : ${distributor}`, 14, 40);
|
||||||
doc.text(`Date : ${date}`, 196, 40, { align: 'right' });
|
doc.text(`Date : ${date}`, rightX, 40, { align: 'right' });
|
||||||
doc.text(`SO Name : ${soName}`, 196, 45, { align: 'right' });
|
doc.text(`SO Name : ${soName}`, rightX, 45, { align: 'right' });
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
autoTable(doc, {
|
autoTable(doc, {
|
||||||
@ -60,31 +63,34 @@ export function DailySalesReportPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const printReport = async () => {
|
const printReport = async () => {
|
||||||
const doc = new jsPDF();
|
const doc = new jsPDF({ orientation: 'landscape' });
|
||||||
|
const pageWidth = doc.internal.pageSize.getWidth();
|
||||||
|
const centerX = pageWidth / 2;
|
||||||
|
const rightX = pageWidth - 14;
|
||||||
|
|
||||||
// Headers
|
// Headers
|
||||||
doc.setFontSize(16);
|
doc.setFontSize(16);
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.text('Krishna Flour Mills (Bangalore) Pvt. Limited', 105, 15, { align: 'center' });
|
doc.text('Krishna Flour Mills (Bangalore) Pvt. Limited', centerX, 15, { align: 'center' });
|
||||||
|
|
||||||
doc.setFontSize(11);
|
doc.setFontSize(11);
|
||||||
doc.setFont('helvetica', 'normal');
|
doc.setFont('helvetica', 'normal');
|
||||||
doc.text('19, Platform Road, Bengaluru - 560 020', 105, 22, { align: 'center' });
|
doc.text('19, Platform Road, Bengaluru - 560 020', centerX, 22, { align: 'center' });
|
||||||
|
|
||||||
doc.setFontSize(12);
|
doc.setFontSize(12);
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.text('DAILY ORDER REPORT', 105, 30, { align: 'center' });
|
doc.text('DAILY ORDER REPORT', centerX, 30, { align: 'center' });
|
||||||
|
|
||||||
// Simple underline for DAILY ORDER REPORT
|
// Simple underline for DAILY ORDER REPORT
|
||||||
const textWidth = doc.getTextWidth('DAILY ORDER REPORT');
|
const textWidth = doc.getTextWidth('DAILY ORDER REPORT');
|
||||||
doc.setLineWidth(0.5);
|
doc.setLineWidth(0.5);
|
||||||
doc.line(105 - textWidth / 2, 31, 105 + textWidth / 2, 31);
|
doc.line(centerX - textWidth / 2, 31, centerX + textWidth / 2, 31);
|
||||||
|
|
||||||
// Meta Info
|
// Meta Info
|
||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.text(`Distributor Name : ${distributor}`, 14, 40);
|
doc.text(`Distributor Name : ${distributor}`, 14, 40);
|
||||||
doc.text(`Date : ${date}`, 196, 40, { align: 'right' });
|
doc.text(`Date : ${date}`, rightX, 40, { align: 'right' });
|
||||||
doc.text(`SO Name : ${soName}`, 196, 45, { align: 'right' });
|
doc.text(`SO Name : ${soName}`, rightX, 45, { align: 'right' });
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
autoTable(doc, {
|
autoTable(doc, {
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export function DatasetItemsPage() {
|
|||||||
|
|
||||||
// Local state for items editing
|
// Local state for items editing
|
||||||
const [items, setItems] = useState<any[]>([]);
|
const [items, setItems] = useState<any[]>([]);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) fetchDataset(id);
|
if (id) fetchDataset(id);
|
||||||
@ -220,90 +221,113 @@ export function DatasetItemsPage() {
|
|||||||
const isString = dataset.data_type === 'string';
|
const isString = dataset.data_type === 'string';
|
||||||
const hasChanges = JSON.stringify(items) !== JSON.stringify(dataset.items || []);
|
const hasChanges = JSON.stringify(items) !== JSON.stringify(dataset.items || []);
|
||||||
|
|
||||||
|
const filteredIndices = items
|
||||||
|
.map((item, idx) => ({ item, idx }))
|
||||||
|
.filter(({ item }) => {
|
||||||
|
if (!searchTerm) return true;
|
||||||
|
const lowerSearch = searchTerm.toLowerCase();
|
||||||
|
if (isString) {
|
||||||
|
return (typeof item === 'string') && item.toLowerCase().includes(lowerSearch);
|
||||||
|
} else {
|
||||||
|
return dataset?.keys?.some(k =>
|
||||||
|
item[k] && typeof item[k] === 'string' && item[k].toLowerCase().includes(lowerSearch)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(({ idx }) => idx);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col h-full bg-transparent">
|
||||||
<header className="flex items-center justify-between">
|
<header className="flex items-center justify-between p-4 bg-[var(--z-block-bg)] border-b border-slate-200">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<h1 className="text-lg font-bold text-brand-700 m-0 flex items-center gap-2">
|
||||||
onClick={() => navigate('/admin/datasets')}
|
<Database size={20} /> {dataset.name}
|
||||||
className="p-2 -ml-2 rounded-full hover:bg-slate-200 text-slate-500 transition-colors"
|
<span className={`text-[10px] uppercase font-bold px-1.5 py-0.5 rounded flex items-center gap-1 ${
|
||||||
>
|
!isString ? 'bg-purple-100 text-purple-700' : 'bg-emerald-100 text-emerald-700'
|
||||||
<ArrowLeft size={20} />
|
}`}>
|
||||||
</button>
|
{!isString ? 'Table' : 'List'}
|
||||||
<div>
|
</span>
|
||||||
<h1 className="text-2xl font-bold text-slate-800 m-0 flex items-center gap-2">
|
<span className="text-sm font-normal text-slate-500 ml-2">
|
||||||
<Database className="text-brand-base" /> {dataset.name}
|
{items.length} items {dataset.keys?.length ? `- ${dataset.keys.length} cols` : ''}
|
||||||
<span className="text-sm font-normal text-slate-500 bg-slate-100 px-2.5 py-0.5 rounded-full border border-slate-200 ml-1">
|
|
||||||
{items.length} {isString ? 'Items' : 'Rows'}
|
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-slate-500 mt-1">Manage items for this dataset</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input type="file" accept=".csv" onChange={handleFileUpload} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" title="Import CSV" />
|
<input type="file" accept=".csv" onChange={handleFileUpload} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" title="Import CSV" />
|
||||||
<Button variant="outline" size="sm" className="rounded-full"><Upload size={14} className="mr-1.5" /> Import CSV</Button>
|
<Button variant="outline" size="sm" className="rounded font-medium"><Upload size={14} className="mr-1.5" /> Import</Button>
|
||||||
</div>
|
</div>
|
||||||
{selectedRows.size > 0 && (
|
{selectedRows.size > 0 && (
|
||||||
<Button variant="danger" size="sm" onClick={handleMultiDelete} className="rounded-full font-semibold px-3 flex items-center">
|
<Button variant="danger" size="sm" onClick={handleMultiDelete} className="rounded font-semibold px-3 flex items-center">
|
||||||
<Trash2 size={14} className="mr-1.5" /> Delete Selected ({selectedRows.size})
|
<Trash2 size={14} className="mr-1.5" /> Delete Selected ({selectedRows.size})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<Button variant="secondary" size="sm" onClick={handleReset} disabled={saving} className="rounded-full font-semibold">
|
<Button variant="secondary" size="sm" onClick={handleReset} disabled={saving} className="rounded font-semibold">
|
||||||
Reset Changes
|
Reset Changes
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button size="sm" onClick={handleSave} disabled={saving || !hasChanges} className="flex items-center gap-2 rounded-full">
|
<Button size="sm" onClick={handleSave} disabled={saving || !hasChanges} className="flex items-center gap-2 rounded bg-slate-100 text-slate-600 hover:bg-slate-200 border border-slate-200">
|
||||||
<Save size={16} /> {saving ? 'Saving...' : 'Save Changes'}
|
<Save size={16} /> {saving ? 'Saving...' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{error && <div className="p-4 bg-red-50 text-red-600 rounded-md border border-red-100 font-medium">{error}</div>}
|
{error && <div className="p-4 bg-red-50 text-red-600 rounded-md border border-red-100 font-medium">{error}</div>}
|
||||||
|
|
||||||
<div className="p-0">
|
<div className="flex-1 p-6">
|
||||||
{isString ? (
|
{isString ? (
|
||||||
<>
|
<div className="bg-[var(--z-block-bg)] border border-slate-200 rounded-lg shadow-sm">
|
||||||
<div className="px-4 py-3 border border-b-0 border-slate-300 bg-[#FFFBF4]">
|
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold text-slate-700 m-0">
|
<div className="relative w-64">
|
||||||
{dataset.name}
|
<input
|
||||||
</h3>
|
type="text"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Search rows..."
|
||||||
|
className="w-full text-sm pl-8 pr-4 py-2 border border-slate-200 rounded-md focus:outline-none focus:ring-1 focus:ring-brand-500"
|
||||||
|
/>
|
||||||
|
<svg className="absolute left-2.5 top-2.5 h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto border border-slate-300">
|
<div className="text-sm text-slate-500">{items.length} rows</div>
|
||||||
<table className="min-w-full table-auto text-left text-sm whitespace-nowrap border-collapse border-2 border-slate-300 bg-[#FFFBF4]">
|
</div>
|
||||||
<thead className="bg-emerald-900 border-b-2 border-emerald-950">
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full table-auto text-left text-sm whitespace-nowrap bg-[var(--z-block-bg)]">
|
||||||
|
<thead className="bg-slate-50 border-b border-slate-200">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 font-semibold text-white w-10 text-center border-r border-emerald-800">
|
<th className="px-4 py-3 font-semibold text-slate-600 w-10 text-center border-r border-slate-100">
|
||||||
<input type="checkbox" checked={selectedRows.size === items.length && items.length > 0} onChange={toggleSelectAll} className="rounded border-emerald-800 text-emerald-600 focus:ring-emerald-500 cursor-pointer" />
|
<input type="checkbox" checked={selectedRows.size === items.length && items.length > 0} onChange={toggleSelectAll} className="rounded border-slate-300 text-brand-600 focus:ring-brand-500 cursor-pointer" />
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 font-semibold text-white w-10 text-center border-r border-emerald-800">#</th>
|
<th className="px-4 py-3 font-bold text-xs uppercase text-slate-700 w-10 text-center border-r border-slate-100">#</th>
|
||||||
<th className="px-4 py-3 font-semibold text-white border-r border-emerald-800">Value</th>
|
<th className="px-4 py-3 font-bold text-xs uppercase text-slate-700 border-r border-slate-100">VALUE</th>
|
||||||
<th className="px-4 py-3 font-semibold text-white text-center w-16">Actions</th>
|
<th className="px-4 py-3 font-bold text-xs uppercase text-slate-700 text-center w-16">ACTIONS</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y-2 divide-slate-300">
|
<tbody className="divide-y divide-slate-100">
|
||||||
{items.map((val, idx) => (
|
{filteredIndices.map((idx) => {
|
||||||
<tr key={idx} className={`bg-[#FFFBF4] hover:bg-emerald-50 transition-colors ${selectedRows.has(idx) ? '!bg-emerald-100' : ''}`}>
|
const val = items[idx];
|
||||||
<td className="px-4 py-2 text-center border-r border-slate-300">
|
return (
|
||||||
|
<tr key={idx} className={`bg-[var(--z-block-bg)] hover:bg-slate-50 transition-colors ${selectedRows.has(idx) ? '!bg-brand-50' : ''}`}>
|
||||||
|
<td className="px-4 py-2 text-center border-r border-slate-100">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedRows.has(idx)}
|
checked={selectedRows.has(idx)}
|
||||||
onChange={() => toggleRowSelection(idx)}
|
onChange={() => toggleRowSelection(idx)}
|
||||||
className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500 cursor-pointer"
|
className="rounded border-slate-300 text-brand-600 focus:ring-brand-500 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-slate-500 font-mono text-center text-xs border-r border-slate-300">{idx + 1}</td>
|
<td className="px-4 py-2 text-slate-400 font-mono text-center text-xs border-r border-slate-100">{idx + 1}</td>
|
||||||
<td className="px-0 py-0 relative border-r border-slate-300 min-w-[150px]">
|
<td className="px-0 py-0 relative border-r border-slate-100 min-w-[150px]">
|
||||||
<div className="invisible px-4 py-2 text-sm whitespace-pre" aria-hidden="true">
|
<div className="invisible px-4 py-3 text-sm whitespace-pre" aria-hidden="true">
|
||||||
{val || 'Enter value'}
|
{val || 'Enter value'}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={val}
|
value={val}
|
||||||
onChange={(e) => handleStringChange(idx, e.target.value)}
|
onChange={(e) => 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"
|
className="absolute inset-0 w-full h-full px-4 py-2 border-none bg-transparent text-sm text-brand-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-brand-500 transition-colors"
|
||||||
placeholder="Enter value"
|
placeholder="Enter value"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@ -317,7 +341,8 @@ export function DatasetItemsPage() {
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
{items.length === 0 && (
|
{items.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4} className="px-5 py-10 text-center text-slate-500 italic font-medium">
|
<td colSpan={4} className="px-5 py-10 text-center text-slate-500 italic font-medium">
|
||||||
@ -328,59 +353,72 @@ export function DatasetItemsPage() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-4">
|
<div className="p-4 border-t border-slate-100 bg-[var(--z-block-bg)] flex justify-between items-center text-xs text-slate-500 rounded-b-lg">
|
||||||
<Button variant="primary" size="sm" onClick={handleAddString} className="rounded-full">
|
<Button variant="primary" size="sm" onClick={handleAddString} className="font-semibold px-4 flex items-center">
|
||||||
<Plus size={16} className="mr-1.5" /> Add Item
|
<Plus size={16} className="mr-1.5" /> Add Row
|
||||||
</Button>
|
</Button>
|
||||||
|
<div>{items.length} rows</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="bg-[var(--z-block-bg)] border border-slate-200 rounded-lg shadow-sm">
|
||||||
<div className="px-4 py-3 border border-b-0 border-slate-300 bg-[#FFFBF4]">
|
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold text-slate-700 m-0">
|
<div className="relative w-64">
|
||||||
{dataset.name}
|
<input
|
||||||
</h3>
|
type="text"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Search rows..."
|
||||||
|
className="w-full text-sm pl-8 pr-4 py-2 border border-slate-200 rounded-md focus:outline-none focus:ring-1 focus:ring-brand-500"
|
||||||
|
/>
|
||||||
|
<svg className="absolute left-2.5 top-2.5 h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto border border-slate-300">
|
<div className="text-sm text-slate-500">{items.length} rows</div>
|
||||||
<table className="min-w-full table-auto text-left text-sm whitespace-nowrap border-collapse border-2 border-slate-300 bg-[#FFFBF4]">
|
</div>
|
||||||
<thead className="bg-emerald-900 border-b-2 border-emerald-950">
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full table-auto text-left text-sm whitespace-nowrap bg-[var(--z-block-bg)]">
|
||||||
|
<thead className="bg-slate-50 border-b border-slate-200">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 font-semibold text-white w-10 text-center border-r border-emerald-800">
|
<th className="px-4 py-3 font-semibold text-slate-600 w-10 text-center border-r border-slate-100">
|
||||||
<input type="checkbox" checked={selectedRows.size === items.length && items.length > 0} onChange={toggleSelectAll} className="rounded border-emerald-800 text-emerald-600 focus:ring-emerald-500 cursor-pointer" />
|
<input type="checkbox" checked={selectedRows.size === items.length && items.length > 0} onChange={toggleSelectAll} className="rounded border-slate-300 text-brand-600 focus:ring-brand-500 cursor-pointer" />
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 font-semibold text-white w-10 text-center border-r border-emerald-800">#</th>
|
<th className="px-4 py-3 font-bold text-xs uppercase text-slate-700 w-10 text-center border-r border-slate-100">#</th>
|
||||||
{dataset.keys?.map(k => (
|
{dataset.keys?.map(k => (
|
||||||
<th key={k} className="px-4 py-3 font-semibold text-white border-r border-emerald-800">{k}</th>
|
<th key={k} className="px-4 py-3 font-bold text-xs uppercase text-slate-700 border-r border-slate-100">{k}</th>
|
||||||
))}
|
))}
|
||||||
<th className="px-4 py-3 font-semibold text-white text-center w-16">Actions</th>
|
<th className="px-4 py-3 font-bold text-xs uppercase text-slate-700 text-center w-16">ACTIONS</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y-2 divide-slate-300">
|
<tbody className="divide-y divide-slate-100">
|
||||||
{items.map((rowObj, idx) => (
|
{filteredIndices.map((idx) => {
|
||||||
<tr key={idx} className={`bg-[#FFFBF4] hover:bg-emerald-50 transition-colors ${selectedRows.has(idx) ? '!bg-emerald-100' : ''}`}>
|
const rowObj = items[idx];
|
||||||
<td className="px-4 py-2 text-center border-r border-slate-300">
|
return (
|
||||||
|
<tr key={idx} className={`bg-[var(--z-block-bg)] hover:bg-slate-50 transition-colors ${selectedRows.has(idx) ? '!bg-brand-50' : ''}`}>
|
||||||
|
<td className="px-4 py-2 text-center border-r border-slate-100">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedRows.has(idx)}
|
checked={selectedRows.has(idx)}
|
||||||
onChange={() => toggleRowSelection(idx)}
|
onChange={() => toggleRowSelection(idx)}
|
||||||
className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500 cursor-pointer"
|
className="rounded border-slate-300 text-brand-600 focus:ring-brand-500 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-slate-500 font-mono text-center text-xs border-r border-slate-300">{idx + 1}</td>
|
<td className="px-4 py-2 text-slate-400 font-mono text-center text-xs border-r border-slate-100">{idx + 1}</td>
|
||||||
{dataset.keys?.map(k => {
|
{dataset.keys?.map(k => {
|
||||||
const isInvalid = invalidCells.has(`${idx}-${k}`);
|
const isInvalid = invalidCells.has(`${idx}-${k}`);
|
||||||
return (
|
return (
|
||||||
<td key={k} className="px-0 py-0 relative border-r border-slate-300 min-w-[150px]">
|
<td key={k} className="px-0 py-0 relative border-r border-slate-100 min-w-[150px]">
|
||||||
<div className="invisible px-4 py-2 text-sm whitespace-pre" aria-hidden="true">
|
<div className="invisible px-4 py-3 text-sm whitespace-pre" aria-hidden="true">
|
||||||
{rowObj[k] || `Enter ${k}`}
|
{rowObj[k] || `Enter ${k}`}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={rowObj[k] || ''}
|
value={rowObj[k] || ''}
|
||||||
onChange={(e) => handleObjectChange(idx, k, e.target.value)}
|
onChange={(e) => 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
|
className={`absolute inset-0 w-full h-full px-4 py-2 border-none bg-transparent text-sm text-brand-700 transition-colors ${isInvalid
|
||||||
? 'ring-2 ring-inset ring-red-400 bg-red-50/50 focus:ring-red-500'
|
? 'ring-2 ring-inset ring-red-400 bg-red-50/50 focus:ring-red-500 focus:outline-none'
|
||||||
: 'focus:ring-2 focus:ring-inset focus:ring-emerald-500'
|
: 'focus:outline-none focus:ring-2 focus:ring-inset focus:ring-brand-500'
|
||||||
}`}
|
}`}
|
||||||
placeholder={`Enter ${k}`}
|
placeholder={`Enter ${k}`}
|
||||||
/>
|
/>
|
||||||
@ -397,7 +435,8 @@ export function DatasetItemsPage() {
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
{items.length === 0 && (
|
{items.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={(dataset.keys?.length || 0) + 3} className="px-5 py-10 text-center text-slate-500 italic font-medium">
|
<td colSpan={(dataset.keys?.length || 0) + 3} className="px-5 py-10 text-center text-slate-500 italic font-medium">
|
||||||
@ -408,12 +447,13 @@ export function DatasetItemsPage() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-4">
|
<div className="p-4 border-t border-slate-100 bg-[var(--z-block-bg)] flex justify-between items-center text-xs text-slate-500 rounded-b-lg">
|
||||||
<Button variant="primary" size="sm" onClick={handleAddObject} className="rounded-full">
|
<Button variant="primary" size="sm" onClick={handleAddObject} className="font-semibold px-4 flex items-center">
|
||||||
<Plus size={16} className="mr-1.5" /> Add Row
|
<Plus size={16} className="mr-1.5" /> Add Row
|
||||||
</Button>
|
</Button>
|
||||||
|
<div>{items.length} rows - {dataset.keys?.length || 0} cols</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { orderBookingClient } from '../../api/clients';
|
import { orderBookingClient } from '../../api/clients';
|
||||||
import type { Dataset } from '../../api/types';
|
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 { Spinner } from '../../components/reusable/Spinner';
|
||||||
import { Database, List } from 'lucide-react';
|
import { Database, LayoutList, Table2 } from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useLocation, Outlet, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
export function DatasetsPage() {
|
export function DatasetsPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@ -23,6 +24,11 @@ export function DatasetsPage() {
|
|||||||
const res = await orderBookingClient.getDatasets();
|
const res = await orderBookingClient.getDatasets();
|
||||||
setDatasets(res);
|
setDatasets(res);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
|
// Auto-select first dataset if none selected and on base path
|
||||||
|
if (res.length > 0 && location.pathname.endsWith('/admin/datasets')) {
|
||||||
|
navigate(`/admin/datasets/${res[0].id}`, { replace: true });
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Failed to fetch datasets');
|
setError(err.message || 'Failed to fetch datasets');
|
||||||
} finally {
|
} finally {
|
||||||
@ -30,56 +36,70 @@ export function DatasetsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="p-10 flex justify-center"><Spinner /></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="p-4 bg-red-50 text-red-600 rounded-md border border-red-100 font-medium">{error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex h-[calc(100vh-80px)] -m-4 bg-[var(--z-block-bg)] rounded-lg overflow-hidden">
|
||||||
<header className="flex items-center justify-between">
|
{/* Sidebar */}
|
||||||
<h1 className="text-2xl font-bold text-slate-800 m-0 flex items-center gap-2">
|
<div className="w-64 border-r border-slate-200 flex flex-col bg-[var(--z-block-bg)]">
|
||||||
<Database className="text-brand-base" /> Datasets
|
<div className="p-4 border-b border-slate-200 flex items-center gap-2">
|
||||||
</h1>
|
<Database size={18} className="text-slate-400" />
|
||||||
|
<h2 className="text-sm font-bold text-slate-700 tracking-wide">DATASETS</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<ul className="flex flex-col py-2">
|
||||||
|
{datasets.map((ds) => {
|
||||||
|
const isActive = id === String(ds.id);
|
||||||
|
const isTable = ds.data_type === 'object';
|
||||||
|
return (
|
||||||
|
<li key={ds.id}>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/admin/datasets/${ds.id}`)}
|
||||||
|
className={`w-full text-left px-4 py-3 flex flex-col gap-1 border-l-4 transition-colors ${
|
||||||
|
isActive
|
||||||
|
? 'border-brand-base bg-brand-50'
|
||||||
|
: 'border-transparent hover:bg-slate-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`font-medium text-sm truncate ${isActive ? 'text-brand-700' : 'text-slate-700'}`}>
|
||||||
|
{ds.name}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`text-[10px] uppercase font-bold px-1.5 py-0.5 rounded flex items-center gap-1 ${
|
||||||
|
isTable ? 'bg-purple-100 text-purple-700' : 'bg-emerald-100 text-emerald-700'
|
||||||
|
}`}>
|
||||||
|
{isTable ? 'Table' : 'List'}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-500 font-medium">
|
||||||
|
{ds.item_count || 0} items
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div className="h-px bg-slate-200 mx-4" />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</header>
|
{/* Main Content Area */}
|
||||||
|
<div className="flex-1 bg-[var(--z-block-bg)] overflow-y-auto">
|
||||||
{error && <div className="p-4 bg-red-50 text-red-600 rounded-md border border-red-100 font-medium">{error}</div>}
|
{id ? (
|
||||||
|
<Outlet />
|
||||||
<Card className="p-0 overflow-hidden shadow-sm border border-slate-200">
|
|
||||||
{loading ? (
|
|
||||||
<div className="p-10 flex justify-center"><Spinner /></div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="h-full flex items-center justify-center text-slate-400">
|
||||||
<table className="w-full text-left text-sm whitespace-nowrap">
|
Select a dataset to view its contents
|
||||||
<thead className="bg-slate-50 border-b border-slate-200">
|
|
||||||
<tr>
|
|
||||||
<th className="px-5 py-3 font-semibold text-slate-600">Name</th>
|
|
||||||
<th className="px-5 py-3 font-semibold text-slate-600">Items</th>
|
|
||||||
<th className="px-5 py-3 font-semibold text-slate-600 text-right">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-slate-100">
|
|
||||||
{datasets.map((ds) => (
|
|
||||||
<tr key={ds.id} className="hover:bg-slate-50/50 transition-colors">
|
|
||||||
<td className="px-5 py-4 text-slate-900 font-bold">{ds.name}</td>
|
|
||||||
<td className="px-5 py-4 text-slate-600 font-medium">{ds.item_count || 0}</td>
|
|
||||||
<td className="px-5 py-4 text-right flex items-center justify-end gap-2">
|
|
||||||
<Button variant="secondary" onClick={() => navigate(`/admin/datasets/${ds.id}`)} className="px-3 py-1.5 text-xs">
|
|
||||||
<List size={14} className="mr-1.5" /> Items
|
|
||||||
</Button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{datasets.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3} className="px-5 py-10 text-center text-slate-500 italic font-medium">No datasets found.</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user