feat: implement PouchDB storage layer with CRUD operations and add showcase UI component

This commit is contained in:
Firman Ramdhani
2026-05-29 15:35:14 +07:00
parent 86c02e7111
commit 6712558eaf
15 changed files with 1820 additions and 309 deletions
+3
View File
@@ -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
+2
View File
@@ -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",
+237
View File
@@ -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>
);
}
+238 -179
View File
@@ -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>
);
}
+57
View File
@@ -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'),
});
+47
View File
@@ -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;
}
+5 -1
View File
@@ -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;
+10
View File
@@ -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',
},
},
});