refactor: restructure storage and environment modules; remove unused code
- Deleted unused PouchDB configuration and types from core storage. - Removed Electron-related hooks and utilities that are no longer needed. - Introduced new local storage management for application keys. - Created a new environment wrapper for landing application. - Added public HTTP client for landing application with minimal configuration. - Implemented new PouchDB entities for items and POS configurations. - Updated main application entry point to reflect new storage structure.
This commit is contained in:
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Type-safe Environment Wrapper for apps/web.
|
||||
* DO NOT use `import.meta.env` directly in components. Import this `ENV` object instead.
|
||||
*/
|
||||
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',
|
||||
|
||||
// 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;
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useIsElectron } from './use-is-electron';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────
|
||||
|
||||
export interface UseElectronPrinterReturn {
|
||||
/** List of available printers (populated after calling `refreshPrinters`) */
|
||||
printers: ElectronPrinterInfo[];
|
||||
/** Whether a printer operation is in progress */
|
||||
loading: boolean;
|
||||
/** Last error message, if any */
|
||||
error: string | null;
|
||||
/** Whether the app is running inside Electron */
|
||||
isElectron: boolean;
|
||||
/** Fetch the current list of available printers */
|
||||
refreshPrinters: () => Promise<ElectronPrinterInfo[]>;
|
||||
/** Print with the given options. Returns success/failure. */
|
||||
print: (options?: ElectronPrintOptions) => Promise<ElectronPrintResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook for Electron printer integration.
|
||||
*
|
||||
* Provides methods to list available printers and trigger print jobs
|
||||
* via the secure `window.electronAPI` bridge.
|
||||
*
|
||||
* Safe to use in both Electron and browser environments.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function PrintButton() {
|
||||
* const { printers, refreshPrinters, print, loading } = useElectronPrinter();
|
||||
*
|
||||
* useEffect(() => { refreshPrinters(); }, []);
|
||||
*
|
||||
* const handlePrint = async () => {
|
||||
* const result = await print({ silent: true, deviceName: printers[0]?.name });
|
||||
* if (!result.success) alert(`Print failed: ${result.failureReason}`);
|
||||
* };
|
||||
*
|
||||
* return (
|
||||
* <button onClick={handlePrint} disabled={loading || printers.length === 0}>
|
||||
* Print
|
||||
* </button>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useElectronPrinter(): UseElectronPrinterReturn {
|
||||
const [printers, setPrinters] = useState<ElectronPrinterInfo[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const isElectron = useIsElectron();
|
||||
|
||||
const refreshPrinters = useCallback(async (): Promise<ElectronPrinterInfo[]> => {
|
||||
if (!window.electronAPI) {
|
||||
setError('Not running in Electron');
|
||||
return [];
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await window.electronAPI.getPrinters();
|
||||
setPrinters(result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to get printers';
|
||||
setError(message);
|
||||
return [];
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const print = useCallback(
|
||||
async (options?: ElectronPrintOptions): Promise<ElectronPrintResult> => {
|
||||
if (!window.electronAPI) {
|
||||
return { success: false, failureReason: 'Not running in Electron' };
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await window.electronAPI.print(options);
|
||||
if (!result.success && result.failureReason) {
|
||||
setError(result.failureReason);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Print failed';
|
||||
setError(message);
|
||||
return { success: false, failureReason: message };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
printers,
|
||||
loading,
|
||||
error,
|
||||
isElectron,
|
||||
refreshPrinters,
|
||||
print,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useIsElectron } from './use-is-electron';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────
|
||||
|
||||
export type UpdateStatus =
|
||||
| 'idle'
|
||||
| 'checking'
|
||||
| 'available'
|
||||
| 'not-available'
|
||||
| 'downloading'
|
||||
| 'ready'
|
||||
| 'error';
|
||||
|
||||
export interface UseElectronUpdaterReturn {
|
||||
/** Current status of the auto-updater lifecycle */
|
||||
status: UpdateStatus;
|
||||
/** Download progress percentage (0–100) */
|
||||
progress: number;
|
||||
/** Download speed in bytes per second */
|
||||
bytesPerSecond: number;
|
||||
/** Information about the available/downloaded update */
|
||||
updateInfo: ElectronUpdateInfo | null;
|
||||
/** Error message if the updater encountered an issue */
|
||||
errorMessage: string | null;
|
||||
/** Whether the app is running inside Electron */
|
||||
isElectron: boolean;
|
||||
/** Trigger a manual update check */
|
||||
checkForUpdates: () => void;
|
||||
/** Quit the app and install the downloaded update */
|
||||
installUpdate: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook for the Electron auto-updater.
|
||||
*
|
||||
* Subscribes to all update lifecycle events via `window.electronAPI`
|
||||
* and provides reactive state for building an update notification UI.
|
||||
*
|
||||
* Safe to use in both Electron and browser environments — all
|
||||
* Electron-specific calls are gated behind `window.electronAPI` checks.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function UpdateBanner() {
|
||||
* const { status, progress, updateInfo, checkForUpdates, installUpdate } = useElectronUpdater();
|
||||
*
|
||||
* if (status === 'available') {
|
||||
* return <div>Update {updateInfo?.version} available! Downloading...</div>;
|
||||
* }
|
||||
* if (status === 'downloading') {
|
||||
* return <div>Downloading... {progress.toFixed(0)}%</div>;
|
||||
* }
|
||||
* if (status === 'ready') {
|
||||
* return <button onClick={installUpdate}>Restart to update</button>;
|
||||
* }
|
||||
* return <button onClick={checkForUpdates}>Check for updates</button>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useElectronUpdater(): UseElectronUpdaterReturn {
|
||||
const [status, setStatus] = useState<UpdateStatus>('idle');
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [bytesPerSecond, setBytesPerSecond] = useState(0);
|
||||
const [updateInfo, setUpdateInfo] = useState<ElectronUpdateInfo | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const isElectron = useIsElectron();
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.electronAPI) return;
|
||||
|
||||
const api = window.electronAPI;
|
||||
|
||||
const unsubChecking = api.onUpdateChecking(() => {
|
||||
setStatus('checking');
|
||||
setErrorMessage(null);
|
||||
});
|
||||
|
||||
const unsubAvailable = api.onUpdateAvailable((info) => {
|
||||
setStatus('available');
|
||||
setUpdateInfo(info);
|
||||
});
|
||||
|
||||
const unsubNotAvailable = api.onUpdateNotAvailable((info) => {
|
||||
setStatus('not-available');
|
||||
setUpdateInfo(info);
|
||||
});
|
||||
|
||||
const unsubProgress = api.onDownloadProgress((progressInfo) => {
|
||||
setStatus('downloading');
|
||||
setProgress(progressInfo.percent);
|
||||
setBytesPerSecond(progressInfo.bytesPerSecond);
|
||||
});
|
||||
|
||||
const unsubDownloaded = api.onUpdateDownloaded((info) => {
|
||||
setStatus('ready');
|
||||
setProgress(100);
|
||||
setUpdateInfo(info);
|
||||
});
|
||||
|
||||
const unsubError = api.onUpdateError((error) => {
|
||||
setStatus('error');
|
||||
setErrorMessage(error);
|
||||
});
|
||||
|
||||
// Cleanup all listeners on unmount
|
||||
return () => {
|
||||
unsubChecking();
|
||||
unsubAvailable();
|
||||
unsubNotAvailable();
|
||||
unsubProgress();
|
||||
unsubDownloaded();
|
||||
unsubError();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const checkForUpdates = useCallback(() => {
|
||||
if (!window.electronAPI) return;
|
||||
setStatus('checking');
|
||||
setErrorMessage(null);
|
||||
window.electronAPI.checkForUpdates();
|
||||
}, []);
|
||||
|
||||
const installUpdate = useCallback(() => {
|
||||
if (!window.electronAPI) return;
|
||||
window.electronAPI.installUpdate();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
status,
|
||||
progress,
|
||||
bytesPerSecond,
|
||||
updateInfo,
|
||||
errorMessage,
|
||||
isElectron,
|
||||
checkForUpdates,
|
||||
installUpdate,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* A utility hook to determine if the React application is running
|
||||
* inside the Electron desktop wrapper or a standard web browser.
|
||||
*/
|
||||
export function useIsElectron(): boolean {
|
||||
return typeof window !== 'undefined' && !!window.electronAPI;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createHttpClient } from '@repo/core-api/http-client';
|
||||
import { faroAdapter } from '@repo/core-api/observability';
|
||||
|
||||
/**
|
||||
* Enterprise HTTP client for `apps/web`.
|
||||
*
|
||||
* - Full Faro observability via the shared `faroAdapter`
|
||||
* - Automatic Bearer token injection from localStorage
|
||||
* - 401 redirect to `/auth/login`
|
||||
* - Supports per-request `telemetryContext` for custom spans/tags
|
||||
*
|
||||
* All interceptors (auth, observability, error normalization)
|
||||
* are baked into this instance. Import this singleton throughout
|
||||
* the web application — never create raw axios instances.
|
||||
*/
|
||||
export const apiClient = createHttpClient(
|
||||
{
|
||||
baseURL: import.meta.env.VITE_API_URL ?? 'http://localhost:8080/api/v1',
|
||||
timeout: 15000,
|
||||
observability: faroAdapter,
|
||||
},
|
||||
{
|
||||
// ── Auth Interceptor ──────────────────────────────────────────
|
||||
onRequest: async (config) => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
// ── Error Interceptor ─────────────────────────────────────────
|
||||
onResponseError: async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Clear stale token and redirect to login
|
||||
localStorage.removeItem('access_token');
|
||||
window.location.href = '/auth/login';
|
||||
}
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './item.pouchdb.entity';
|
||||
export * from './pos-configuration.pouchdb.entity';
|
||||
@@ -0,0 +1,27 @@
|
||||
interface ItemRateEntity {
|
||||
season_period?: string | null;
|
||||
price: string | number;
|
||||
}
|
||||
|
||||
interface ItemCategoryEntity {
|
||||
_id?: string;
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a sellable product or service.
|
||||
*/
|
||||
export interface ItemEntity {
|
||||
_id: string;
|
||||
_rev?: string;
|
||||
name: string;
|
||||
base_price: string | number;
|
||||
item_type: string;
|
||||
usage_type?: string;
|
||||
item_category?: ItemCategoryEntity[] | ItemCategoryEntity | string;
|
||||
item_rates?: ItemRateEntity[];
|
||||
|
||||
// Allow for other ERP-specific fields
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ItemEntity } from './item.pouchdb.entity';
|
||||
|
||||
/**
|
||||
* Represents the configuration and assigned data for a specific Point of Sale terminal.
|
||||
*/
|
||||
export interface POSConfigurationEntity {
|
||||
_id: string;
|
||||
_rev?: string;
|
||||
pos_number: string;
|
||||
pos_name: string;
|
||||
items: ItemEntity[];
|
||||
payment_methods?: any[];
|
||||
|
||||
// Allow for other ERP-specific fields
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -5,9 +5,10 @@
|
||||
* which databases to create and where they sync to. The core engine
|
||||
* (`PouchDatabaseManager`) has zero knowledge of business domains.
|
||||
*/
|
||||
import { ENV } from '../../environment';
|
||||
import { PouchDatabaseManager } from '@repo/core-storage';
|
||||
import { ENV } from '../../environment/env';
|
||||
import type { Item, POSConfiguration } from './types';
|
||||
|
||||
import { ItemEntity, POSConfigurationEntity } from './entities';
|
||||
|
||||
// ─── Manager Singleton ──────────────────────────────────────────
|
||||
|
||||
@@ -24,9 +25,7 @@ function buildRemoteUrl(dbName: string): string | 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}`;
|
||||
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
|
||||
@@ -45,13 +44,13 @@ function buildRemoteUrl(dbName: string): string | undefined {
|
||||
// ─── Register Application Databases ─────────────────────────────
|
||||
|
||||
/** POS Configuration database — stores device settings, theme, etc. */
|
||||
export const posConfigDB = dbManager.register<POSConfiguration>({
|
||||
export const posConfigDB = dbManager.register<POSConfigurationEntity>({
|
||||
localName: 'pos_configuration',
|
||||
remoteUrl: buildRemoteUrl('pos_configuration'),
|
||||
});
|
||||
|
||||
/** Items database — products available for sale in POS. */
|
||||
export const itemDB = dbManager.register<Item>({
|
||||
export const itemDB = dbManager.register<ItemEntity>({
|
||||
localName: 'item',
|
||||
remoteUrl: buildRemoteUrl('item'),
|
||||
});
|
||||
Reference in New Issue
Block a user