feat: implement PouchDB storage layer with CRUD operations and add showcase UI component
This commit is contained in:
@@ -1,2 +1,5 @@
|
||||
VITE_API_BASE_URL=http://localhost:8000/api
|
||||
VITE_APP_ENV=development
|
||||
VITE_COUCHDB_BASE_URL=http://202.146.229.134:7700
|
||||
VITE_COUCHDB_USERNAME=root
|
||||
VITE_COUCHDB_PASSWORD=password
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"@repo/utils": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"dayjs": "^1.11.19",
|
||||
"events": "^3.3.0",
|
||||
"i18next": "^24.2.2",
|
||||
"lucide-react": "^1.17.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-i18next": "^15.4.0",
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Title,
|
||||
Text,
|
||||
Table,
|
||||
Badge,
|
||||
} from '@repo/ui/components';
|
||||
import { itemDB, posConfigDB } from '../../core/db';
|
||||
import type { Item, POSConfiguration } from '../../core/db/types';
|
||||
|
||||
export default function PouchSample() {
|
||||
const [configs, setConfigs] = useState<POSConfiguration[]>([]);
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
|
||||
// Load initial data
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const allConfigs = await posConfigDB.find({ selector: {} });
|
||||
setConfigs(allConfigs);
|
||||
|
||||
const allItems = await itemDB.find({ selector: {} });
|
||||
setItems(allItems);
|
||||
console.log({allConfigs, allItems})
|
||||
} catch (err) {
|
||||
console.error('Failed to load PouchDB data', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 1. Initial Data Load
|
||||
loadData();
|
||||
|
||||
// 2. Setup Real-Time Listeners
|
||||
const unsubscribeItems = itemDB.onChange(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
const unsubscribePos = posConfigDB.onChange(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 3. CRITICAL: Cleanup to prevent memory leaks on unmount
|
||||
return () => {
|
||||
unsubscribeItems();
|
||||
unsubscribePos();
|
||||
};
|
||||
}, [loadData]);
|
||||
|
||||
// ─── POS Configuration Handlers ─────────────────────────────────
|
||||
|
||||
const handleSeedConfig = async () => {
|
||||
try {
|
||||
const id = `pos-${Date.now()}`;
|
||||
await posConfigDB.create({
|
||||
_id: id,
|
||||
pos_number: '1111111111666',
|
||||
pos_name: 'Premium Test POS TESTING COUNCH',
|
||||
items: items, // mapping current items
|
||||
payment_methods: [{ id: 'cash', name: 'Cash' }],
|
||||
});
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to seed config', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteConfig = async (id: string) => {
|
||||
try {
|
||||
await posConfigDB.delete(id);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete config', err);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Items Inventory Handlers ───────────────────────────────────
|
||||
|
||||
const handleAddItem = async () => {
|
||||
try {
|
||||
const id = `item-${Date.now()}`;
|
||||
await itemDB.create({
|
||||
_id: id,
|
||||
name: 'PLAYGROUND ALL DAY TESTING POUCH',
|
||||
base_price: '75000',
|
||||
item_type: 'wahana',
|
||||
usage_type: 'ticket',
|
||||
item_category: [{ name: 'Entertainment' }],
|
||||
item_rates: [
|
||||
{ season_period: 'weekday', price: 50000 },
|
||||
{ season_period: 'weekend', price: 75000 },
|
||||
],
|
||||
});
|
||||
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to add item', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteItem = async (id: string) => {
|
||||
try {
|
||||
await itemDB.delete(id);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete item', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearAll = async () => {
|
||||
try {
|
||||
await posConfigDB.cleanAllData();
|
||||
await itemDB.cleanAllData();
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to clear data', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<Group justify="space-between">
|
||||
<Title order={2}>Enterprise PouchDB Sync</Title>
|
||||
<Button color="error" variant="outline" onClick={handleClearAll}>
|
||||
Clear All Local Data
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Items Inventory Table */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Items Database</Title>
|
||||
<Button onClick={handleAddItem} color="success">
|
||||
Inject Mock ERP Item
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<div className="max-h-[400px] overflow-y-auto border border-gray-200 rounded-lg scrollbar-thin scrollbar-thumb-gray-300">
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">ID</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Name</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Type</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Base Price</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Rates Count</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{items.length > 0 ? (
|
||||
items.map((item) => (
|
||||
<Table.Tr key={item._id}>
|
||||
<Table.Td>{item._id}</Table.Td>
|
||||
<Table.Td>{item.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="brand" variant="light">
|
||||
{item.item_type}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>${Number(item.base_price).toFixed(2)}</Table.Td>
|
||||
<Table.Td>{item.item_rates?.length || 0}</Table.Td>
|
||||
<Table.Td>
|
||||
<Button size="xs" color="error" variant="subtle" onClick={() => handleDeleteItem(item._id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={6} align="center">
|
||||
<Text c="dimmed">No items found.</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* POS Configuration Table */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>POS Configurations</Title>
|
||||
<Button onClick={handleSeedConfig} variant="light" color="brand">
|
||||
Inject Mock POS Config
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<div className="max-h-[400px] overflow-y-auto border border-gray-200 rounded-lg scrollbar-thin scrollbar-thumb-gray-300">
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">ID</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">POS Name</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">POS Number</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Mapped Items</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{configs.length > 0 ? (
|
||||
configs.map((cfg) => (
|
||||
<Table.Tr key={cfg._id}>
|
||||
<Table.Td>{cfg._id}</Table.Td>
|
||||
<Table.Td>{cfg.pos_name}</Table.Td>
|
||||
<Table.Td>{cfg.pos_number}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="info" variant="outline">
|
||||
{cfg.items?.length || 0} Items
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Button size="xs" color="error" variant="subtle" onClick={() => handleDeleteConfig(cfg._id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={5} align="center">
|
||||
<Text c="dimmed">No configurations found.</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { ColorSchemeType, DensityType } from '@repo/ui/provider';
|
||||
import {
|
||||
Button,
|
||||
@@ -18,10 +19,15 @@ import {
|
||||
Table,
|
||||
Badge,
|
||||
Divider,
|
||||
Tabs,
|
||||
Box,
|
||||
Paper,
|
||||
} from '@repo/ui/components';
|
||||
import { ShieldCheck, Database, Lock, Layout, Activity, Printer } from 'lucide-react';
|
||||
import PrinterList from './printer-list';
|
||||
import ExamplePage from './example/example.page';
|
||||
import EventsDemoPage from './events-demo';
|
||||
import PouchSample from './pouch-sample';
|
||||
|
||||
interface ShowcaseViewProps {
|
||||
colorScheme: ColorSchemeType;
|
||||
@@ -31,6 +37,8 @@ interface ShowcaseViewProps {
|
||||
}
|
||||
|
||||
export default function ShowcaseView({ colorScheme, setColorScheme, density, setDensity }: ShowcaseViewProps) {
|
||||
const [activeTab, setActiveTab] = useState<string | null>('ui-components');
|
||||
|
||||
// Mock data for the table
|
||||
const tableData = [
|
||||
{ id: 'ORD-001', customer: 'John Doe', status: 'Shipped', total: '$120.00' },
|
||||
@@ -38,194 +46,245 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
|
||||
{ id: 'ORD-003', customer: 'Acme Corp', status: 'Delivered', total: '$1,250.00' },
|
||||
];
|
||||
|
||||
const getSubtitle = () => {
|
||||
switch (activeTab) {
|
||||
case 'rbac': return 'Role-Based Access Control and Permissions';
|
||||
case 'storage': return 'Offline-First PouchDB Synchronization';
|
||||
case 'auth': return 'Authentication & Security Layers';
|
||||
case 'ui-components': return 'Theme, Typography, Forms & Data Grids';
|
||||
case 'events': return 'Global Event Bus Synchronization';
|
||||
case 'hardware': return 'Hardware Integration & Printers';
|
||||
default: return 'Architecture Showcase';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size="lg" py="xl">
|
||||
<Stack gap="xl">
|
||||
<Title order={1}>Super App UI Showcase</Title>
|
||||
<Box className="min-h-screen" style={{ backgroundColor: 'var(--mantine-color-body)' }}>
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
placement="left"
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
variant="pills"
|
||||
radius="md"
|
||||
className="h-screen"
|
||||
styles={{
|
||||
root: { display: 'flex', height: '100vh', overflow: 'hidden' },
|
||||
list: {
|
||||
minWidth: 260,
|
||||
padding: '1rem',
|
||||
borderRight: '1px solid var(--mantine-color-default-border)',
|
||||
backgroundColor: 'var(--mantine-color-default-element-bg)'
|
||||
},
|
||||
panel: { flex: 1, display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' },
|
||||
tab: { justifyContent: 'flex-start' }, // Ensure the entire tab content aligns left
|
||||
tabLabel: { textAlign: 'left', flex: 1 }, // Ensure the text pushes to fill and aligns left
|
||||
}}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Box mb="xl" px="sm">
|
||||
<Title order={3} c="brand.7">Eigen ERP</Title>
|
||||
<Text size="xs" c="dimmed">Architecture Showcase</Text>
|
||||
</Box>
|
||||
|
||||
{/* =========================================
|
||||
CONTROL PANEL
|
||||
========================================= */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Theme Controls
|
||||
</Title>
|
||||
<Group grow align="flex-end">
|
||||
<Select
|
||||
label="Color Scheme"
|
||||
value={colorScheme}
|
||||
onChange={(val: string | null) => setColorScheme((val as ColorSchemeType) || 'light')}
|
||||
data={[
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Density (Spacing & Sizing)"
|
||||
value={density}
|
||||
onChange={(val: string | null) => setDensity((val as DensityType) || 'standard')}
|
||||
data={[
|
||||
{ value: 'compact', label: 'Compact (ERP Mode)' },
|
||||
{ value: 'standard', label: 'Standard (UI Mode)' },
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
</Card>
|
||||
<Tabs.Tab value="ui-components" leftSection={<Layout size={18} />}>
|
||||
UI Components
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="storage" leftSection={<Database size={18} />}>
|
||||
Offline Storage
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="rbac" leftSection={<ShieldCheck size={18} />}>
|
||||
RBAC Engine
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="auth" leftSection={<Lock size={18} />}>
|
||||
Auth & Security
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="events" leftSection={<Activity size={18} />}>
|
||||
Events
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="hardware" leftSection={<Printer size={18} />}>
|
||||
Hardware
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
{/* =========================================
|
||||
TAILWIND V4 BRIDGE TEST
|
||||
========================================= */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Tailwind v4 Synchronization
|
||||
</Title>
|
||||
{/* This div purely uses Tailwind classes to prove it inherits Mantine's variables */}
|
||||
<div className="bg-brand-500 text-brand-50 p-md rounded-md shadow-md text-base">
|
||||
<span className="font-bold">Tailwind works!</span> The padding (p-md), border-radius (rounded-md), text size
|
||||
(text-base), and background color of this box are entirely controlled by the ThemeProvider's current state.
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* =========================================
|
||||
TYPOGRAPHY & BUTTONS
|
||||
========================================= */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Title order={4} mb="xs">
|
||||
Typography & Badges
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
This is dimmed small text indicating a subtitle.
|
||||
</Text>
|
||||
<Text mb="md">
|
||||
This is standard text describing the components below. Watch how the font changes when you switch
|
||||
density.
|
||||
</Text>
|
||||
<Group>
|
||||
<Badge color="brand">Brand Badge</Badge>
|
||||
<Badge color="success" variant="light">
|
||||
Success Status
|
||||
</Badge>
|
||||
<Badge color="error" variant="outline">
|
||||
Error State
|
||||
</Badge>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div>
|
||||
<Title order={4} mb="md">
|
||||
Buttons
|
||||
</Title>
|
||||
<Group>
|
||||
<Button variant="filled" color="brand">
|
||||
Filled Button
|
||||
</Button>
|
||||
<Button variant="outline" color="brand">
|
||||
Outline Button
|
||||
</Button>
|
||||
<Button variant="light" color="info">
|
||||
Light Info
|
||||
</Button>
|
||||
<Button variant="subtle" color="error">
|
||||
Cancel
|
||||
</Button>
|
||||
</Group>
|
||||
</div>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* =========================================
|
||||
COMPLEX FORMS (ERP STYLE)
|
||||
========================================= */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Form Elements
|
||||
</Title>
|
||||
<Stack gap="md">
|
||||
<Group grow align="flex-start">
|
||||
<TextInput label="First Name" placeholder="Enter your first name" withAsterisk />
|
||||
<TextInput label="Last Name" placeholder="Enter your last name" />
|
||||
<Tabs.Panel value={activeTab as string}>
|
||||
{/* Header */}
|
||||
<Paper
|
||||
p="md"
|
||||
radius={0}
|
||||
withBorder
|
||||
style={{
|
||||
borderTop: 0,
|
||||
borderLeft: 0,
|
||||
borderRight: 0,
|
||||
zIndex: 10,
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={0}>
|
||||
<Title order={3}>Architecture Showcase</Title>
|
||||
<Text size="sm" c="dimmed">{getSubtitle()}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Group grow align="flex-start">
|
||||
<NumberInput label="Age" placeholder="25" min={0} max={100} />
|
||||
<PasswordInput label="Password" placeholder="Your secret password" withAsterisk />
|
||||
</Group>
|
||||
{/* Scrollable Content Area */}
|
||||
<Box className="flex-1 overflow-y-auto p-6" style={{ height: 'calc(100vh - 80px)' }}>
|
||||
<Container size="xl" m={0} p={0}>
|
||||
|
||||
{/* --- UI COMPONENTS TAB --- */}
|
||||
{activeTab === 'ui-components' && (
|
||||
<Stack gap="xl">
|
||||
{/* Control Panel */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">Theme Controls</Title>
|
||||
<Group grow align="flex-end">
|
||||
<Select
|
||||
label="Color Scheme"
|
||||
value={colorScheme}
|
||||
onChange={(val) => setColorScheme((val as ColorSchemeType) || 'light')}
|
||||
data={[{ value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' }]}
|
||||
/>
|
||||
<Select
|
||||
label="Density (Spacing & Sizing)"
|
||||
value={density}
|
||||
onChange={(val) => setDensity((val as DensityType) || 'standard')}
|
||||
data={[{ value: 'compact', label: 'Compact (ERP Mode)' }, { value: 'standard', label: 'Standard (UI Mode)' }]}
|
||||
/>
|
||||
</Group>
|
||||
</Card>
|
||||
|
||||
<Textarea label="Bio" placeholder="Tell us about yourself" minRows={3} />
|
||||
{/* Typography & Buttons */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Title order={4} mb="xs">Typography & Badges</Title>
|
||||
<Text size="sm" c="dimmed">This is dimmed small text indicating a subtitle.</Text>
|
||||
<Group mt="md">
|
||||
<Badge color="brand">Brand Badge</Badge>
|
||||
<Badge color="success" variant="light">Success Status</Badge>
|
||||
<Badge color="error" variant="outline">Error State</Badge>
|
||||
</Group>
|
||||
</div>
|
||||
<Divider />
|
||||
<div>
|
||||
<Title order={4} mb="md">Buttons</Title>
|
||||
<Group>
|
||||
<Button variant="filled" color="brand">Filled Button</Button>
|
||||
<Button variant="outline" color="brand">Outline Button</Button>
|
||||
<Button variant="light" color="info">Light Info</Button>
|
||||
<Button variant="subtle" color="error">Cancel</Button>
|
||||
</Group>
|
||||
</div>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Group mt="sm">
|
||||
<Checkbox label="I agree to the terms and conditions" defaultChecked />
|
||||
<Switch label="Enable notifications" defaultChecked />
|
||||
</Group>
|
||||
{/* Forms */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">Form Elements</Title>
|
||||
<Stack gap="md">
|
||||
<Group grow align="flex-start">
|
||||
<TextInput label="First Name" placeholder="Enter your first name" withAsterisk />
|
||||
<TextInput label="Last Name" placeholder="Enter your last name" />
|
||||
</Group>
|
||||
<Group grow align="flex-start">
|
||||
<NumberInput label="Age" placeholder="25" min={0} max={100} />
|
||||
<PasswordInput label="Password" placeholder="Your secret password" withAsterisk />
|
||||
</Group>
|
||||
<Textarea label="Bio" placeholder="Tell us about yourself" minRows={3} />
|
||||
<Group mt="sm">
|
||||
<Checkbox label="I agree to the terms and conditions" defaultChecked />
|
||||
<Switch label="Enable notifications" defaultChecked />
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Radio.Group name="favoriteFramework" label="Select your favorite framework" withAsterisk>
|
||||
<Group mt="xs">
|
||||
<Radio value="react" label="React" />
|
||||
<Radio value="svelte" label="Svelte" />
|
||||
<Radio value="vue" label="Vue" />
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
{/* Data Grid */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">Data Grid</Title>
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Order ID</Table.Th>
|
||||
<Table.Th>Customer</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Total</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{tableData.map((row) => (
|
||||
<Table.Tr key={row.id}>
|
||||
<Table.Td>{row.id}</Table.Td>
|
||||
<Table.Td>{row.customer}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm" color={row.status === 'Delivered' ? 'success' : row.status === 'Shipped' ? 'info' : 'warning'}>
|
||||
{row.status}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{row.total}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* =========================================
|
||||
DATA GRID / TABLE
|
||||
========================================= */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Data Grid
|
||||
</Title>
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Order ID</Table.Th>
|
||||
<Table.Th>Customer</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Total Amount</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{tableData.map((row) => (
|
||||
<Table.Tr key={row.id}>
|
||||
<Table.Td>{row.id}</Table.Td>
|
||||
<Table.Td>{row.customer}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
size="sm"
|
||||
color={row.status === 'Delivered' ? 'success' : row.status === 'Shipped' ? 'info' : 'warning'}
|
||||
>
|
||||
{row.status}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{row.total}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<PrinterList />
|
||||
</Card>
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Nested Showcase Example
|
||||
</Title>
|
||||
<Text>
|
||||
This is an example of a nested showcase component. You can create multiple layers of showcases to organize
|
||||
features by domain or complexity.
|
||||
</Text>
|
||||
<ExamplePage />
|
||||
</Card>
|
||||
{/* --- STORAGE TAB --- */}
|
||||
{activeTab === 'storage' && (
|
||||
<Stack gap="xl">
|
||||
<PouchSample />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* =========================================
|
||||
EVENT BUS SHOWCASE
|
||||
========================================= */}
|
||||
<EventsDemoPage />
|
||||
</Stack>
|
||||
</Container>
|
||||
{/* --- RBAC TAB --- */}
|
||||
{activeTab === 'rbac' && (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">RBAC Engine</Title>
|
||||
<Text c="dimmed">RBAC Demo Component Coming Soon...</Text>
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- AUTH TAB --- */}
|
||||
{activeTab === 'auth' && (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">Auth & Security</Title>
|
||||
<Text c="dimmed">Auth Demo Component Coming Soon...</Text>
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- EVENTS TAB --- */}
|
||||
{activeTab === 'events' && (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">Nested Showcase Example</Title>
|
||||
<Text mb="md">This is an example of a nested showcase component.</Text>
|
||||
<ExamplePage />
|
||||
</Card>
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<EventsDemoPage />
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- HARDWARE TAB --- */}
|
||||
{activeTab === 'hardware' && (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<PrinterList />
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
</Container>
|
||||
</Box>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Multi-Database PouchDB Configuration for apps/web.
|
||||
*
|
||||
* This module demonstrates the IoC pattern: the consuming app decides
|
||||
* which databases to create and where they sync to. The core engine
|
||||
* (`PouchDatabaseManager`) has zero knowledge of business domains.
|
||||
*/
|
||||
import { PouchDatabaseManager } from '@repo/core-storage';
|
||||
import { ENV } from '../../environment/env';
|
||||
import type { Item, POSConfiguration } from './types';
|
||||
|
||||
// ─── Manager Singleton ──────────────────────────────────────────
|
||||
|
||||
export const dbManager = new PouchDatabaseManager();
|
||||
|
||||
// ─── Helper: Build Secure Remote URL ────────────────────────────
|
||||
|
||||
function buildRemoteUrl(dbName: string): string | undefined {
|
||||
const { COUCHDB_BASE_URL, COUCHDB_USERNAME, COUCHDB_PASSWORD } = ENV;
|
||||
|
||||
if (!COUCHDB_BASE_URL || !COUCHDB_USERNAME || !COUCHDB_PASSWORD) {
|
||||
console.warn(`[DB Config] CouchDB credentials missing — "${dbName}" will run in offline-only mode.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 1. Tambahkan http:// secara otomatis jika DevOps hanya mengisi IP Address di .env
|
||||
const safeBaseUrl = COUCHDB_BASE_URL.startsWith('http')
|
||||
? COUCHDB_BASE_URL
|
||||
: `http://${COUCHDB_BASE_URL}`;
|
||||
|
||||
try {
|
||||
// 2. Gunakan URL parser yang aman dari karakter aneh pada password
|
||||
const url = new URL(safeBaseUrl);
|
||||
url.username = encodeURIComponent(COUCHDB_USERNAME);
|
||||
url.password = encodeURIComponent(COUCHDB_PASSWORD);
|
||||
url.pathname = `/${dbName}`;
|
||||
|
||||
return url.toString();
|
||||
} catch (error) {
|
||||
console.error(`[DB Config] Invalid URL format for CouchDB:`, safeBaseUrl);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Register Application Databases ─────────────────────────────
|
||||
|
||||
/** POS Configuration database — stores device settings, theme, etc. */
|
||||
export const posConfigDB = dbManager.register<POSConfiguration>({
|
||||
localName: 'pos_configuration',
|
||||
remoteUrl: buildRemoteUrl('pos_configuration'),
|
||||
});
|
||||
|
||||
/** Items database — products available for sale in POS. */
|
||||
export const itemDB = dbManager.register<Item>({
|
||||
localName: 'item',
|
||||
remoteUrl: buildRemoteUrl('item'),
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Enterprise ERP Data Domain Models
|
||||
* These types reflect the actual schema of the underlying CouchDB instances.
|
||||
*/
|
||||
|
||||
export interface ItemRate {
|
||||
season_period?: string | null;
|
||||
price: string | number;
|
||||
}
|
||||
|
||||
export interface ItemCategory {
|
||||
_id?: string;
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a sellable product or service.
|
||||
*/
|
||||
export interface Item {
|
||||
_id: string;
|
||||
_rev?: string;
|
||||
name: string;
|
||||
base_price: string | number;
|
||||
item_type: string;
|
||||
usage_type?: string;
|
||||
item_category?: ItemCategory[] | ItemCategory | string;
|
||||
item_rates?: ItemRate[];
|
||||
|
||||
// Allow for other ERP-specific fields
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the configuration and assigned data for a specific Point of Sale terminal.
|
||||
*/
|
||||
export interface POSConfiguration {
|
||||
_id: string;
|
||||
_rev?: string;
|
||||
pos_number: string;
|
||||
pos_name: string;
|
||||
items: Item[];
|
||||
payment_methods?: any[];
|
||||
|
||||
// Allow for other ERP-specific fields
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -6,5 +6,9 @@ export const ENV = {
|
||||
API_BASE_URL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api',
|
||||
APP_ENV: (import.meta.env.VITE_APP_ENV || 'development') as 'development' | 'staging' | 'production',
|
||||
IS_PROD: import.meta.env.VITE_APP_ENV === 'production',
|
||||
} as const;
|
||||
|
||||
// CouchDB Connection
|
||||
COUCHDB_BASE_URL: import.meta.env.VITE_COUCHDB_BASE_URL || 'http://localhost:5984',
|
||||
COUCHDB_USERNAME: import.meta.env.VITE_COUCHDB_USERNAME || '',
|
||||
COUCHDB_PASSWORD: import.meta.env.VITE_COUCHDB_PASSWORD || '',
|
||||
} as const;
|
||||
|
||||
@@ -8,4 +8,14 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
strictPort: true, // Fail if 5173 is in use. Electron NEEDS this exact port.
|
||||
},
|
||||
define: {
|
||||
// Crucial for PouchDB to not crash in the browser
|
||||
global: 'window',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// Force Vite to use the installed npm package for 'events'
|
||||
events: 'events',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user