diff --git a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.index.tsx b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.index.tsx index 480e528..5296d42 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.index.tsx +++ b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.index.tsx @@ -1,9 +1,6 @@ import { Table, Box, Title, Paper } from '@repo/ui/components'; import { PageActions } from '@repo/ui/components'; -import { - useEnterpriseModuleTranslationContext, - useEnterpriseModuleNavigationContext, -} from '@repo/ui/foundations'; +import { useEnterpriseModuleTranslationContext, useEnterpriseModuleNavigationContext } from '@repo/ui/foundations'; import { FullPageEntity } from '../../domain/entities'; import { Edit2, Eye, Copy, Trash2 } from 'lucide-react'; import { useCallback } from 'react'; @@ -22,7 +19,7 @@ const MOCK_DATA: FullPageEntity[] = [ ]; export default function FullPagePageIndex() { - // Translation is scoped to ['full-page', 'common'] — no prefix needed for module keys + // Translation is scoped to ['FULL_PAGE', 'common'] — no prefix needed for module keys const { t } = useEnterpriseModuleTranslationContext(); const { navigateToDetail, navigateToEdit, navigateToDuplicate } = useEnterpriseModuleNavigationContext(); diff --git a/apps/web/src/apps/modules/example/single-page/data/single-page.remote.service.ts b/apps/web/src/apps/modules/example/single-page/data/single-page.remote.service.ts new file mode 100644 index 0000000..5ad4ea8 --- /dev/null +++ b/apps/web/src/apps/modules/example/single-page/data/single-page.remote.service.ts @@ -0,0 +1,25 @@ +import { BaseRemoteDataServices } from '@repo/core-api/data-services'; +import { SinglePageDTO, SinglePageEntity } from '../domain/entities'; + +/** + * Full Page Remote Data Services + * + * Provides core data services for the single-page module by extending the base remote data services. + * While this class automatically handles standard CRUD operations and data transformations out-of-the-box, + * implementers can freely extend it by adding custom methods to support domain-specific API endpoints + * or complex business logic as needed. + * + * @example + * ```ts + * export class SinglePageRemoteDataServices extends BaseRemoteDataServices { + * // Example of adding a custom method tailored to specific module needs + * public async getDashboardMetrics(status: string): Promise { + * const response = await this.httpClient.get(`${this.apiUrl}/metrics`, { + * params: { status } + * }); + * return response.data; + * } + * } + * ``` + */ +export class SinglePageRemoteDataServices extends BaseRemoteDataServices {} diff --git a/apps/web/src/apps/modules/example/single-page/domain/constants/index.ts b/apps/web/src/apps/modules/example/single-page/domain/constants/index.ts new file mode 100644 index 0000000..8779928 --- /dev/null +++ b/apps/web/src/apps/modules/example/single-page/domain/constants/index.ts @@ -0,0 +1 @@ +export * from './single-page.constants'; diff --git a/apps/web/src/apps/modules/example/single-page/domain/constants/single-page.constants.ts b/apps/web/src/apps/modules/example/single-page/domain/constants/single-page.constants.ts new file mode 100644 index 0000000..e1a8bb9 --- /dev/null +++ b/apps/web/src/apps/modules/example/single-page/domain/constants/single-page.constants.ts @@ -0,0 +1,22 @@ +import { ModuleConfigEntity } from '@repo/ui/foundations'; + +/** + * Core configuration and constants for the Full Page module. + * Used across Domain, Data, and Presentation layers. + */ +export const SinglePageModuleConfig: ModuleConfigEntity = { + /** Unique identifier for permissions, caching, and i18n */ + moduleKey: 'EXAMPLE_SINGLE_PAGE', + + /** Translation namespace — must match the namespace used in registerModuleNamespace() */ + translationNamespace: 'single-page', + + /** Base API endpoint for remote data services */ + apiUrl: '/single-page', + + /** Base Web Router URL for UI navigation */ + webUrl: 'apps/example/single-page', + + /** Architectural category of the module, used for rendering and routing logic */ + moduleCategory: 'SINGLE_PAGE', +} as const; diff --git a/apps/web/src/apps/modules/example/single-page/domain/entities/index.ts b/apps/web/src/apps/modules/example/single-page/domain/entities/index.ts new file mode 100644 index 0000000..a0b332c --- /dev/null +++ b/apps/web/src/apps/modules/example/single-page/domain/entities/index.ts @@ -0,0 +1 @@ +export * from './single-page.entity'; diff --git a/apps/web/src/apps/modules/example/single-page/domain/entities/single-page.entity.ts b/apps/web/src/apps/modules/example/single-page/domain/entities/single-page.entity.ts new file mode 100644 index 0000000..ccf05dd --- /dev/null +++ b/apps/web/src/apps/modules/example/single-page/domain/entities/single-page.entity.ts @@ -0,0 +1,26 @@ +import { BaseEntity } from '@repo/core-api/data-services'; + +/** + * Represents a single-page entity in the frontend domain model. + * + * This entity is derived from the API's `SinglePageDTO` via the + * {@link SinglePageTransformer}, which handles field name mapping + * and computed field derivation. + * + */ +export interface SinglePageEntity extends BaseEntity { + status?: string; + name?: string; + code?: string; + description?: string; +} + +/** + * Represents the raw data structure returned by the API for a single-page resource. + * + * This DTO uses snake_case field names matching the backend's JSON serialization. + * It is transformed into a {@link SinglePageEntity} by the {@link SinglePageTransformer}. + */ +export interface SinglePageDTO extends SinglePageEntity { + [key: string]: any; +} diff --git a/apps/web/src/apps/modules/example/single-page/domain/factories/index.ts b/apps/web/src/apps/modules/example/single-page/domain/factories/index.ts new file mode 100644 index 0000000..30cfe38 --- /dev/null +++ b/apps/web/src/apps/modules/example/single-page/domain/factories/index.ts @@ -0,0 +1,30 @@ +/** + * Full Page Factory + * + * This module acts as the dependency injection and configuration center for the single-page feature. + * It pre-configures and exports singleton instances of the data transformer and data service. + * By centralizing the instantiation here, it ensures that all UI components and hooks + * within the module share the same API client, configuration, and transformation logic. + */ + +import { apiClient } from '../../../../../../core/lib/api-client'; +import { SinglePageRemoteDataServices } from '../../data/single-page.remote.service'; +import { SinglePageModuleConfig } from '../constants/single-page.constants'; +import { SinglePageRemoteDataTransformer } from '../transformers/single-page.remote.transformer'; + +/** + * Singleton instance of the SinglePageRemoteDataTransformer. + * Exported for potential direct usage if manual data mapping is required outside the standard API flow. + */ +export const singlePageDataTransformer = new SinglePageRemoteDataTransformer(); + +/** + * Pre-configured singleton instance of the SinglePageRemoteDataServices. + * Ready to be consumed by UI components, state managers, or module providers. + * It is fully wired with the HTTP client and automatically handles data mapping via the injected transformer. + */ +export const singlePageDataService = new SinglePageRemoteDataServices(apiClient, { + apiUrl: SinglePageModuleConfig.apiUrl, + moduleKey: SinglePageModuleConfig.moduleKey, + transformer: singlePageDataTransformer, +}); diff --git a/apps/web/src/apps/modules/example/single-page/domain/transformers/single-page.remote.transformer.ts b/apps/web/src/apps/modules/example/single-page/domain/transformers/single-page.remote.transformer.ts new file mode 100644 index 0000000..a95ec8c --- /dev/null +++ b/apps/web/src/apps/modules/example/single-page/domain/transformers/single-page.remote.transformer.ts @@ -0,0 +1,45 @@ +import { BaseDataTransformer } from '@repo/core-api/data-services'; +import { SinglePageEntity, SinglePageDTO } from '../entities'; + +/** + * Full Page Remote Data Transformer + * + * Responsible for transforming data between the raw API data transfer objects (DTOs) + * and the frontend domain entities for the single-page module. By extending the base transformer, + * it ensures strict type safety and decouples data parsing logic from the API service layer. + * + * Implementers must define the core mapping rules (`transformToEntity`, `transformToDTO`) + * and can freely add custom transformation methods for specific API responses. + * + * @example + * ```ts + * export class SinglePageRemoteDataTransformer extends BaseDataTransformer { + * // Map snake_case API payload to camelCase frontend entity + * public transformToEntity(dto: SinglePageDTO): SinglePageEntity { + * return { + * id: dto.id, + * documentNumber: dto.document_number, + * createdAt: new Date(dto.created_at), + * // ... other property mappings + * }; + * } + * + * // Map camelCase frontend entity back to snake_case API payload + * public transformToDTO(entity: SinglePageEntity): SinglePageDTO { + * return { + * id: entity.id, + * document_number: entity.documentNumber, + * // ... other property mappings + * }; + * } + * + * // Example of adding a custom transformation method for a specific feature + * public transformMetrics(rawData: any): MetricsEntity { + * return { + * totalActive: rawData.total_active_count ?? 0, + * }; + * } + * } + * ``` + */ +export class SinglePageRemoteDataTransformer extends BaseDataTransformer {} diff --git a/apps/web/src/apps/modules/example/single-page/presentation/factory/index.tsx b/apps/web/src/apps/modules/example/single-page/presentation/factory/index.tsx new file mode 100644 index 0000000..6958ade --- /dev/null +++ b/apps/web/src/apps/modules/example/single-page/presentation/factory/index.tsx @@ -0,0 +1,37 @@ +import { lazy } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { EnterpriseModuleProvider } from '@repo/ui/foundations'; +import { registerModuleNamespace } from '@repo/core-i18n'; +import { SinglePageModuleConfig } from '../../domain/constants'; +import { singlePageDataService } from '../../domain/factories'; +import { SinglePageEntity } from '../../domain/entities'; + +import singlePageId from '../locales/id/single-page.json'; +import singlePageEn from '../locales/en/single-page.json'; + +const IndexPage = lazy(() => import('../pages/single-page.page.index')); + +// --------------------------------------------------------------------------- +// Namespace Registration (Module Scope) +// --------------------------------------------------------------------------- +// Called once at import time — safe, idempotent, outside React render cycle. +// The namespace 'single-page' must match config.translationNamespace. +registerModuleNamespace(SinglePageModuleConfig.translationNamespace, { + id: singlePageId, + en: singlePageEn, +}); + +// --------------------------------------------------------------------------- +// Module Factory +// --------------------------------------------------------------------------- +export default function SinglePageModule() { + return ( + config={SinglePageModuleConfig} dataServices={singlePageDataService}> + + } /> + } /> + } /> + + + ); +} diff --git a/apps/web/src/apps/modules/example/single-page/presentation/locales/en/single-page.json b/apps/web/src/apps/modules/example/single-page/presentation/locales/en/single-page.json new file mode 100644 index 0000000..e72dbb8 --- /dev/null +++ b/apps/web/src/apps/modules/example/single-page/presentation/locales/en/single-page.json @@ -0,0 +1,9 @@ +{ + "title": "Single Page Management", + "fields": { + "status": "Status", + "name": "Name", + "code": "Code", + "description": "Description" + } +} \ No newline at end of file diff --git a/apps/web/src/apps/modules/example/single-page/presentation/locales/id/single-page.json b/apps/web/src/apps/modules/example/single-page/presentation/locales/id/single-page.json new file mode 100644 index 0000000..81dacdb --- /dev/null +++ b/apps/web/src/apps/modules/example/single-page/presentation/locales/id/single-page.json @@ -0,0 +1,9 @@ +{ + "title": "Manajemen Halaman Tunggal", + "fields": { + "status": "Status", + "name": "Nama", + "code": "Kode", + "description": "Deskripsi" + } +} \ No newline at end of file diff --git a/apps/web/src/apps/modules/example/single-page/presentation/pages/single-page.page.index.tsx b/apps/web/src/apps/modules/example/single-page/presentation/pages/single-page.page.index.tsx new file mode 100644 index 0000000..5e5973f --- /dev/null +++ b/apps/web/src/apps/modules/example/single-page/presentation/pages/single-page.page.index.tsx @@ -0,0 +1,99 @@ +import { Table, Box, Title, Paper } from '@repo/ui/components'; +import { PageActions } from '@repo/ui/components'; +import { useEnterpriseModuleTranslationContext, useEnterpriseModuleNavigationContext } from '@repo/ui/foundations'; +import { SinglePageEntity } from '../../domain/entities'; +import { Edit2, Eye, Copy, Trash2 } from 'lucide-react'; +import { useCallback } from 'react'; + +// TODO: Replace this mock data with real API data loaded from DataService via useEnterpriseModuleDataServiceContext +const MOCK_DATA: SinglePageEntity[] = [ + { id: '1', name: 'Dashboard Widget', code: 'WID-001', status: 'ACTIVE', description: 'Main dashboard widget' }, + { id: '2', name: 'Report Generator', code: 'REP-002', status: 'INACTIVE', description: 'Generates monthly reports' }, + { + id: '3', + name: 'User Management', + code: 'USR-003', + status: 'ACTIVE', + description: 'Manages user roles and permissions', + }, +]; + +export default function SinglePagePageIndex() { + // Translation is scoped to ['SINGLE_PAGE', 'common'] — no prefix needed for module keys + const { t } = useEnterpriseModuleTranslationContext(); + + const { navigateToDetail, navigateToEdit, navigateToDuplicate } = useEnterpriseModuleNavigationContext(); + + const getRowActions = useCallback( + (row: SinglePageEntity) => [ + { + key: 'view', + label: t('common:view'), + icon: , + onClick: () => navigateToDetail(row.id as string), + }, + { + key: 'edit', + label: t('common:edit'), + icon: , + onClick: () => navigateToEdit(row.id as string), + }, + { + key: 'duplicate', + label: t('common:duplicate'), + icon: , + onClick: () => navigateToDuplicate(row.id as string), + }, + { + type: 'divider' as const, + key: 'div-1', + }, + { + key: 'delete', + label: t('common:delete'), + icon: , + intent: 'destructive' as const, + onClick: () => { + // Mock delete action + alert(`Delete ${row.name}`); + }, + }, + ], + [t, navigateToDetail, navigateToEdit, navigateToDuplicate], + ); + + const rows = MOCK_DATA.map((item) => ( + + {item.code} + {item.name} + {item.status} + {item.description} + + + + + )); + + return ( + + + {t('title')} + + + + + + + {t('fields.code')} + {t('fields.name')} + {t('fields.status')} + {t('fields.description')} + {t('common:edit')} + + + {rows} +
+
+
+ ); +} diff --git a/apps/web/src/apps/modules/example/single-page/presentation/states/index.ts b/apps/web/src/apps/modules/example/single-page/presentation/states/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/web/src/apps/modules/index.tsx b/apps/web/src/apps/modules/index.tsx index dba5012..e38586a 100644 --- a/apps/web/src/apps/modules/index.tsx +++ b/apps/web/src/apps/modules/index.tsx @@ -3,12 +3,14 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import ModuleLayout from './layouts/module.layout'; const FullPageModule = lazy(() => import('./example/full-page/presentation/factory')); +const SinglePageModule = lazy(() => import('./example/single-page/presentation/factory')); export default function AppModule() { return ( } /> + } /> } /> } />