feat: enhance employee management with purpose-based configurations and improved routing

- Refactored employee management components to support purpose-based configurations for sales and logistics.
- Updated routing to dynamically load employee modules based on the selected purpose, improving modularity and code organization.
- Introduced new utility functions for handling employee positions and module configurations, enhancing flexibility in employee data handling.
- Enhanced form components to conditionally render based on the purpose, improving user experience and data entry accuracy.
- Added unit tests to validate new functionalities and ensure reliability across employee management features.

These changes improve the employee management experience by providing a more tailored approach to handling employee data based on specific business needs.
This commit is contained in:
shancheas
2026-08-27 15:10:42 +07:00
parent b21d647c8b
commit 133e73a070
31 changed files with 370 additions and 114 deletions
@@ -21,6 +21,7 @@ describe('MENU_ITEMS', () => {
expect(childKeys(sales)).toEqual(['sales-data', 'sales-activities', 'sales-reports']);
expect(childKeys(findItem(sales?.children ?? [], 'sales-data'))).toEqual(['sales-employees', 'sales-cycles']);
expect(findItem(sales?.children ?? [], 'sales-data')?.children?.[0]?.path).toBe('/app/sales/employees/index');
expect(childKeys(findItem(sales?.children ?? [], 'sales-activities'))).toEqual([
'sales-requests',
'sales-orders',
@@ -38,6 +39,9 @@ describe('MENU_ITEMS', () => {
'logistics-employees',
'logistics-cycles',
]);
expect(findItem(logistics?.children ?? [], 'logistics-data')?.children?.[0]?.path).toBe(
'/app/logistics/employees/index',
);
expect(childKeys(findItem(logistics?.children ?? [], 'logistics-activities'))).toEqual([
'logistics-packing-slips',
'logistics-plans',
@@ -49,7 +49,7 @@ export const MENU_ITEMS: MenuItemType[] = [
key: 'sales-employees',
label: 'nav:configuration-employees',
icon: Users,
path: '/app/configuration/employees/index',
path: '/app/sales/employees/index',
moduleKey: 'CONFIGURATION.EMPLOYEE',
},
{
@@ -129,7 +129,7 @@ export const MENU_ITEMS: MenuItemType[] = [
key: 'logistics-employees',
label: 'nav:configuration-employees',
icon: Users,
path: '/app/configuration/employees/index',
path: '/app/logistics/employees/index',
moduleKey: 'CONFIGURATION.EMPLOYEE',
},
{
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { AxiosInstance } from '@repo/core-api/http-client';
import { EmployeesRemoteDataServices } from './employee.remote.service';
import { EmployeesRemoteDataServices, serializeRepeatQuery } from './employee.remote.service';
import { EmployeesRemoteDataTransformer } from '../domain/transformers/employee.remote.transformer';
function createMockHttpClient(): AxiosInstance {
@@ -51,4 +51,10 @@ describe('EmployeesRemoteDataServices', () => {
expect.objectContaining({ url: '/employees/bulk-delete', method: 'POST' }),
);
});
it('serializes position arrays as repeated query keys', () => {
expect(serializeRepeatQuery({ page: 1, position: ['driver', 'crew'] })).toBe(
'page=1&position=driver&position=crew',
);
});
});
@@ -1,8 +1,25 @@
import type { AxiosInstance } from '@repo/core-api/http-client';
import type { AxiosInstance, AxiosRequestConfig } from '@repo/core-api/http-client';
import type { DataServicesConfig } from '@repo/core-api/data-services';
import { TrackGoRemoteDataServices } from '../../../../../../core/lib/trackgo-remote-data-services';
import type { EmployeeEntity } from '../domain/entities';
export function serializeRepeatQuery(params: Record<string, unknown>): string {
const search = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null || value === '') {
continue;
}
if (Array.isArray(value)) {
for (const item of value) {
search.append(key, String(item));
}
continue;
}
search.append(key, String(value));
}
return search.toString();
}
export class EmployeesRemoteDataServices extends TrackGoRemoteDataServices<EmployeeEntity> {
constructor(httpClient: AxiosInstance, config: DataServicesConfig<EmployeeEntity>) {
super(httpClient, {
@@ -10,4 +27,11 @@ export class EmployeesRemoteDataServices extends TrackGoRemoteDataServices<Emplo
apiUrl: config.apiUrl ?? '/employees',
});
}
async getMany<T = unknown>(config?: AxiosRequestConfig) {
return super.getMany<T>({
...config,
paramsSerializer: serializeRepeatQuery,
});
}
}
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { createEmployeeModuleConfig } from './employee.constants';
import { employeePositionsForPurpose } from '../entities';
describe('employee purpose helpers', () => {
it('maps sales to the sales position and logistics to crew and driver', () => {
expect(employeePositionsForPurpose('sales')).toEqual(['sales']);
expect(employeePositionsForPurpose('logistics')).toEqual(['driver', 'crew']);
});
it('uses distinct web urls per purpose', () => {
expect(createEmployeeModuleConfig('sales').webUrl).toBe('/app/sales/employees');
expect(createEmployeeModuleConfig('logistics').webUrl).toBe('/app/logistics/employees');
});
it('keeps a shared employee privilege key', () => {
expect(createEmployeeModuleConfig('sales').moduleKey).toBe('CONFIGURATION.EMPLOYEE');
expect(createEmployeeModuleConfig('logistics').moduleKey).toBe('CONFIGURATION.EMPLOYEE');
});
});
@@ -1,11 +1,14 @@
import { ModuleConfigEntity } from '@repo/ui/foundations';
import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose';
import type { EmployeeEntity } from '../entities';
export const employeesModuleConfig: ModuleConfigEntity<EmployeeEntity> = {
moduleKey: 'CONFIGURATION.EMPLOYEE',
translationNamespace: 'EMPLOYEES',
apiUrl: '/employees',
webUrl: '/app/configuration/employees',
moduleCategory: 'FULL_PAGE',
moduleType: 'MASTER_DATA',
} as const;
export function createEmployeeModuleConfig(purpose: FieldPurpose): ModuleConfigEntity<EmployeeEntity> {
return {
moduleKey: 'CONFIGURATION.EMPLOYEE',
translationNamespace: 'EMPLOYEES',
apiUrl: '/employees',
webUrl: `/app/${purpose}/employees`,
moduleCategory: 'FULL_PAGE',
moduleType: 'MASTER_DATA',
};
}
@@ -1,9 +1,18 @@
import { BaseEntity } from '@repo/core-api/data-services';
import type { ConfigurationStatus } from '../../../divisions/domain/entities';
import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose';
export const EMPLOYEE_POSITIONS = ['sales', 'driver', 'crew'] as const;
export type EmployeePosition = (typeof EMPLOYEE_POSITIONS)[number];
export const SALES_EMPLOYEE_POSITIONS = ['sales'] as const satisfies readonly EmployeePosition[];
export const LOGISTICS_EMPLOYEE_POSITIONS = ['driver', 'crew'] as const satisfies readonly EmployeePosition[];
export function employeePositionsForPurpose(purpose: FieldPurpose): readonly EmployeePosition[] {
return purpose === 'logistics' ? LOGISTICS_EMPLOYEE_POSITIONS : SALES_EMPLOYEE_POSITIONS;
}
export interface EmployeeEntity extends BaseEntity {
code: string;
name: string;
@@ -1,12 +1,20 @@
import { apiClient } from '../../../../../../../core/lib/api-client';
import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose';
import { EmployeesRemoteDataServices } from '../../data/employee.remote.service';
import { employeesModuleConfig } from '../constants/employee.constants';
import { createEmployeeModuleConfig } from '../constants/employee.constants';
import { employeePositionsForPurpose } from '../entities';
import { EmployeesRemoteDataTransformer } from '../transformers/employee.remote.transformer';
export const employeesDataTransformer = new EmployeesRemoteDataTransformer();
export function createEmployeeDataService(purpose?: FieldPurpose) {
const config = createEmployeeModuleConfig(purpose ?? 'sales');
const transformer = new EmployeesRemoteDataTransformer(purpose ? employeePositionsForPurpose(purpose) : undefined);
return new EmployeesRemoteDataServices(apiClient, {
apiUrl: config.apiUrl,
moduleKey: config.moduleKey,
transformer,
});
}
export const employeesDataService = new EmployeesRemoteDataServices(apiClient, {
apiUrl: employeesModuleConfig.apiUrl,
moduleKey: employeesModuleConfig.moduleKey,
transformer: employeesDataTransformer,
});
export const employeesDataService = createEmployeeDataService();
export const salesEmployeesDataService = createEmployeeDataService('sales');
export const logisticsEmployeesDataService = createEmployeeDataService('logistics');
@@ -2,8 +2,6 @@ import { describe, expect, it } from 'vitest';
import { EmployeesRemoteDataTransformer } from './employee.remote.transformer';
import type { EmployeeEntity } from '../entities';
const transformer = new EmployeesRemoteDataTransformer();
const dto = {
id: 'emp-1',
code: 'EMP_01',
@@ -18,6 +16,8 @@ const dto = {
};
describe('EmployeesRemoteDataTransformer', () => {
const transformer = new EmployeesRemoteDataTransformer();
it('maps dto fields onto the entity', () => {
const entity = transformer.transformToEntity(dto);
expect(entity).toMatchObject({
@@ -52,4 +52,51 @@ describe('EmployeesRemoteDataTransformer', () => {
expect(payload).not.toHaveProperty('status');
expect(payload).not.toHaveProperty('id');
});
it('leaves list filters unchanged when no positions are scoped', () => {
const filter = transformer.transformPayloadFilter({ name: 'Ada', status: 'active' });
expect(filter).toEqual({ name: 'Ada', status: 'active' });
});
it('injects the sales position array into list filters', () => {
const salesTransformer = new EmployeesRemoteDataTransformer(['sales']);
expect(salesTransformer.transformPayloadFilter({ name: 'Ada' })).toEqual({
name: 'Ada',
position: ['sales'],
});
});
it('narrows a selected logistics position to an array still inside the allowed set', () => {
const logisticsTransformer = new EmployeesRemoteDataTransformer(['driver', 'crew']);
expect(logisticsTransformer.transformPayloadFilter({ position: 'crew' })).toEqual({
position: ['crew'],
});
});
it('falls back to the allowed logistics positions when the selected position is invalid', () => {
const logisticsTransformer = new EmployeesRemoteDataTransformer(['driver', 'crew']);
expect(logisticsTransformer.transformPayloadFilter({ position: 'sales' })).toEqual({
position: ['driver', 'crew'],
});
});
it('forces sales position on create when the module is sales-scoped', () => {
const salesTransformer = new EmployeesRemoteDataTransformer(['sales']);
expect(salesTransformer.transformCreatePayload({ ...dto, position: 'driver' })).toEqual({
code: 'EMP_01',
name: 'Ada Lovelace',
phone: '+6281234567890',
position: 'sales',
});
});
it('keeps a logistics position on create when it is allowed', () => {
const logisticsTransformer = new EmployeesRemoteDataTransformer(['driver', 'crew']);
expect(logisticsTransformer.transformCreatePayload({ ...dto, position: 'crew' }).position).toBe('crew');
});
it('drops a disallowed position on logistics create instead of forwarding it', () => {
const logisticsTransformer = new EmployeesRemoteDataTransformer(['driver', 'crew']);
expect(logisticsTransformer.transformCreatePayload({ ...dto, position: 'sales' }).position).toBeUndefined();
});
});
@@ -1,7 +1,12 @@
import { BaseDataTransformer } from '@repo/core-api/data-services';
import type { EmployeeDto, EmployeeEntity } from '../entities';
import { omitEmptyFields } from '../../../../../../../core/domain/configuration-field-validators';
import type { EmployeeDto, EmployeeEntity, EmployeePosition } from '../entities';
export class EmployeesRemoteDataTransformer extends BaseDataTransformer<EmployeeEntity> {
constructor(private readonly allowedPositions?: readonly EmployeePosition[]) {
super();
}
transformToEntity(dto: EmployeeDto | EmployeeEntity): EmployeeEntity {
return {
id: dto.id,
@@ -26,7 +31,7 @@ export class EmployeesRemoteDataTransformer extends BaseDataTransformer<Employee
code: entity.code,
name: entity.name,
phone: entity.phone,
position: entity.position,
position: this.resolvePosition(entity.position),
};
}
@@ -35,7 +40,39 @@ export class EmployeesRemoteDataTransformer extends BaseDataTransformer<Employee
code: entity.code,
name: entity.name,
phone: entity.phone,
position: entity.position,
position: this.resolvePosition(entity.position),
};
}
transformPayloadFilter(filter: Record<string, any>): Record<string, any> {
if (!this.allowedPositions) {
return omitEmptyFields(filter);
}
return omitEmptyFields({ ...filter, position: this.resolveFilterPositions(filter.position) });
}
private resolvePosition(position?: EmployeePosition): EmployeePosition | undefined {
if (!this.allowedPositions) {
return position;
}
if (position && this.allowedPositions.includes(position)) {
return position;
}
return this.allowedPositions.length === 1 ? this.allowedPositions[0] : undefined;
}
private resolveFilterPositions(selected: unknown): EmployeePosition[] {
const allowed = [...this.allowedPositions!];
if (typeof selected === 'string' && allowed.includes(selected as EmployeePosition)) {
return [selected as EmployeePosition];
}
if (Array.isArray(selected)) {
const intersection = selected.filter((value): value is EmployeePosition => allowed.includes(value));
if (intersection.length > 0) {
return intersection;
}
}
return allowed;
}
}
@@ -22,4 +22,16 @@ describe('createEmployeeSchema', () => {
it('rejects an unknown position', () => {
expect(schema.safeParse({ ...valid, position: 'manager' }).success).toBe(false);
});
it('rejects a logistics position when the schema is sales-scoped', () => {
const salesSchema = createEmployeeSchema(t, ['sales']);
expect(salesSchema.safeParse({ ...valid, position: 'driver' }).success).toBe(false);
expect(salesSchema.safeParse(valid).success).toBe(true);
});
it('rejects a sales position when the schema is logistics-scoped', () => {
const logisticsSchema = createEmployeeSchema(t, ['driver', 'crew']);
expect(logisticsSchema.safeParse(valid).success).toBe(false);
expect(logisticsSchema.safeParse({ ...valid, position: 'crew' }).success).toBe(true);
});
});
@@ -4,14 +4,18 @@ import {
configNameSchema,
configPhoneSchema,
} from '../../../../../../../core/domain/configuration-field-validators';
import { EMPLOYEE_POSITIONS } from '../entities';
import { EMPLOYEE_POSITIONS, type EmployeePosition } from '../entities';
export const createEmployeeSchema = (t: (key: string) => string) => {
export const createEmployeeSchema = (
t: (key: string) => string,
positions: readonly EmployeePosition[] = EMPLOYEE_POSITIONS,
) => {
const allowed = positions as [EmployeePosition, ...EmployeePosition[]];
return z.object({
code: configCodeSchema(t),
name: configNameSchema(t),
phone: configPhoneSchema(t),
position: z.enum(EMPLOYEE_POSITIONS, {
position: z.enum(allowed, {
required_error: JSON.stringify({ key: 'validation:required', values: { field: t('common:fields.position') } }),
}),
});
@@ -1,8 +1,8 @@
import { Box, FieldTextInput, FieldSelect, Paper, SimpleGrid, Text } from '@repo/ui/components';
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
import { EMPLOYEE_POSITIONS } from '../../../domain/entities';
import type { EmployeePosition } from '../../../domain/entities';
export function FormGeneral() {
export function FormGeneral({ positions }: { positions: readonly EmployeePosition[] }) {
const { formControl } = useFormPageContext();
const { t } = useEnterpriseModuleTranslationContext();
@@ -37,14 +37,16 @@ export function FormGeneral() {
required
radius="md"
/>
<FieldSelect
control={formControl.control}
name="position"
label={t('common:fields.position')}
data={EMPLOYEE_POSITIONS.map((value) => ({ value, label: t(`position_${value}`) }))}
required
radius="md"
/>
{positions.length > 1 ? (
<FieldSelect
control={formControl.control}
name="position"
label={t('common:fields.position')}
data={positions.map((value) => ({ value, label: t(`position_${value}`) }))}
required
radius="md"
/>
) : null}
</SimpleGrid>
</Box>
</Paper>
@@ -2,9 +2,17 @@ import { SimpleGrid } from '@repo/ui/components';
import { FieldTextInput, FieldSelect } from '@repo/ui/form';
import { UseFormReturn } from 'react-hook-form';
import { statusFilterOptions } from '../../../../shared/status-filter-options';
import { EMPLOYEE_POSITIONS } from '../../../domain/entities';
import type { EmployeePosition } from '../../../domain/entities';
export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (key: string) => string }) => {
export const FilterFormContent = ({
form,
t,
positions,
}: {
form: UseFormReturn<any>;
t: (key: string) => string;
positions: readonly EmployeePosition[];
}) => {
return (
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<FieldTextInput
@@ -25,13 +33,15 @@ export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (k
label={t('common:fields.phone')}
placeholder={`Enter ${t('common:fields.phone')}`}
/>
<FieldSelect
control={form.control}
name="position"
label={t('common:fields.position')}
clearable
data={EMPLOYEE_POSITIONS.map((value) => ({ value, label: t(`position_${value}`) }))}
/>
{positions.length > 1 ? (
<FieldSelect
control={form.control}
name="position"
label={t('common:fields.position')}
clearable
data={positions.map((value) => ({ value, label: t(`position_${value}`) }))}
/>
) : null}
<FieldSelect
control={form.control}
name="status"
@@ -2,10 +2,11 @@ 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 { employeesModuleConfig } from '../../domain/constants';
import { employeesDataService } from '../../domain/factories';
import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose';
import { createEmployeeModuleConfig } from '../../domain/constants';
import { logisticsEmployeesDataService, salesEmployeesDataService } from '../../domain/factories';
import { EmployeeEntity } from '../../domain/entities';
import { employeesStore } from '../store';
import { logisticsEmployeesStore, salesEmployeesStore } from '../store';
import employeesId from '../languages/id/employees.json';
import employeesEn from '../languages/en/employees.json';
@@ -14,25 +15,25 @@ const IndexPage = lazy(() => import('../pages/employee.page.index'));
const FormPage = lazy(() => import('../pages/employee.page.form'));
const DetailPage = lazy(() => import('../pages/employee.page.detail'));
registerModuleNamespace(employeesModuleConfig.translationNamespace, {
registerModuleNamespace('EMPLOYEES', {
id: employeesId,
en: employeesEn,
});
export default function EmployeesModule() {
export default function EmployeesModule({ purpose }: { purpose: FieldPurpose }) {
const config = createEmployeeModuleConfig(purpose);
const dataService = purpose === 'sales' ? salesEmployeesDataService : logisticsEmployeesDataService;
const store = purpose === 'sales' ? salesEmployeesStore : logisticsEmployeesStore;
return (
<EnterpriseModuleProvider<EmployeeEntity>
config={employeesModuleConfig}
dataServices={employeesDataService}
store={employeesStore}
>
<EnterpriseModuleProvider<EmployeeEntity> config={config} dataServices={dataService} store={store}>
<Routes>
<Route path="/index" element={<IndexPage />} />
<Route path="/detail/:dataId" element={<DetailPage />} />
<Route path="/edit/:dataId" element={<FormPage formPageType="EDIT" />} />
<Route path="/duplicate/:dataId" element={<FormPage formPageType="DUPLICATE" />} />
<Route path="/create" element={<FormPage formPageType="CREATE" />} />
<Route path="/" element={<Navigate to={`${employeesModuleConfig.webUrl}/index`} replace={true} />} />
<Route path="/index" element={<IndexPage purpose={purpose} />} />
<Route path="/detail/:dataId" element={<DetailPage purpose={purpose} />} />
<Route path="/edit/:dataId" element={<FormPage purpose={purpose} formPageType="EDIT" />} />
<Route path="/duplicate/:dataId" element={<FormPage purpose={purpose} formPageType="DUPLICATE" />} />
<Route path="/create" element={<FormPage purpose={purpose} formPageType="CREATE" />} />
<Route path="/" element={<Navigate to={`${config.webUrl}/index`} replace={true} />} />
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
</Routes>
</EnterpriseModuleProvider>
@@ -1,9 +1,11 @@
import { EnterpriseDetailPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
import { employeesModuleConfig } from '../../domain/constants';
import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose';
import { createEmployeeModuleConfig } from '../../domain/constants';
import { DetailGeneral } from '../components/detail-component/detail-general';
export default function EmployeePageDetail() {
export default function EmployeePageDetail({ purpose }: { purpose: FieldPurpose }) {
const { t } = useEnterpriseModuleTranslationContext();
const moduleConfig = createEmployeeModuleConfig(purpose);
return (
<EnterpriseDetailPageProvider
@@ -12,8 +14,8 @@ export default function EmployeePageDetail() {
title: t('detail_page_title'),
description: t('detail_page_description'),
breadcrumbs: [
{ label: t('nav:configuration'), type: 'text' },
{ label: t('nav:configuration-employees'), type: 'link', href: `${employeesModuleConfig.webUrl}/index` },
{ label: t(`nav:${purpose}`), type: 'text' },
{ label: t('nav:configuration-employees'), type: 'link', href: `${moduleConfig.webUrl}/index` },
],
}}
>
@@ -2,12 +2,22 @@ import { useMemo } from 'react';
import { useEnterpriseModuleTranslationContext, EnterpriseFormPageProvider, FormPageType } from '@repo/ui/foundations';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { employeesModuleConfig } from '../../domain/constants';
import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose';
import { createEmployeeModuleConfig } from '../../domain/constants';
import { employeePositionsForPurpose } from '../../domain/entities';
import { createEmployeeSchema } from '../../domain/validators/employee.validator';
import { FormGeneral } from '../components/form-component/form-general';
export default function EmployeePageForm({ formPageType }: { formPageType: FormPageType }) {
export default function EmployeePageForm({
formPageType,
purpose,
}: {
formPageType: FormPageType;
purpose: FieldPurpose;
}) {
const { t } = useEnterpriseModuleTranslationContext();
const moduleConfig = createEmployeeModuleConfig(purpose);
const positions = employeePositionsForPurpose(purpose);
const title = useMemo(() => {
if (formPageType === 'CREATE') {
@@ -22,8 +32,11 @@ export default function EmployeePageForm({ formPageType }: { formPageType: FormP
return { title: '', description: '' };
}, [formPageType, t]);
const validator = useMemo(() => createEmployeeSchema(t), [t]);
const formControl = useForm({ resolver: zodResolver(validator) });
const validator = useMemo(() => createEmployeeSchema(t, positions), [positions, t]);
const formControl = useForm({
resolver: zodResolver(validator),
defaultValues: positions.length === 1 ? { position: positions[0] } : undefined,
});
return (
<EnterpriseFormPageProvider
@@ -36,12 +49,12 @@ export default function EmployeePageForm({ formPageType }: { formPageType: FormP
title: title?.title,
description: title?.description,
breadcrumbs: [
{ label: t('nav:configuration'), type: 'text' },
{ label: t('nav:configuration-employees'), type: 'link', href: `${employeesModuleConfig.webUrl}/index` },
{ label: t(`nav:${purpose}`), type: 'text' },
{ label: t('nav:configuration-employees'), type: 'link', href: `${moduleConfig.webUrl}/index` },
],
}}
>
<FormGeneral />
<FormGeneral positions={positions} />
</EnterpriseFormPageProvider>
);
}
@@ -7,11 +7,13 @@ import {
import { ColDef, Text } from '@repo/ui/components';
import { Trans } from '@repo/core-i18n';
import { Users } from 'lucide-react';
import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose';
import { employeePositionsForPurpose, type EmployeeEntity } from '../../domain/entities';
import { FilterFormContent } from '../components/index-component/filter-content';
import type { EmployeeEntity } from '../../domain/entities';
export default function EmployeePageIndex() {
export default function EmployeePageIndex({ purpose }: { purpose: FieldPurpose }) {
const { t } = useEnterpriseModuleTranslationContext();
const positions = employeePositionsForPurpose(purpose);
const columnDefs: ColDef<EmployeeEntity>[] = useMemo(() => {
return [
@@ -31,10 +33,10 @@ export default function EmployeePageIndex() {
return {
renderBody: (form: any) => {
if (!form) return null;
return <FilterFormContent form={form} t={t} />;
return <FilterFormContent form={form} t={t} positions={positions} />;
},
};
}, [t]);
}, [positions, t]);
return (
<EnterpriseIndexPageProvider
@@ -45,7 +47,7 @@ export default function EmployeePageIndex() {
),
icon: Users,
breadcrumbs: [
{ label: t('nav:configuration'), type: 'text' },
{ label: t(`nav:${purpose}`), type: 'text' },
{ label: t('nav:configuration-employees'), type: 'text' },
],
}}
@@ -4,19 +4,20 @@ import { EmployeeEntity } from '../../domain/entities';
export interface EmployeesStoreState extends EnterpriseModuleState<EmployeeEntity> {}
export const employeesStore = create<EmployeesStoreState>((set) => ({
metaData: { limit: 15 },
setMetaData: (data) => set({ metaData: data }),
export function createEmployeesStore() {
return create<EmployeesStoreState>((set) => ({
metaData: { limit: 15 },
setMetaData: (data) => set({ metaData: data }),
filterData: {},
setFilterData: (data) => set({ filterData: data }),
selectedRows: [],
setSelectedRows: (rows) => set({ selectedRows: rows }),
privileges: [],
setPrivileges: (privileges) => set({ privileges }),
tableConfig: null,
setTableConfig: (config) => set({ tableConfig: config }),
}));
}
filterData: {},
setFilterData: (data) => set({ filterData: data }),
selectedRows: [],
setSelectedRows: (rows) => set({ selectedRows: rows }),
privileges: [],
setPrivileges: (privileges) => set({ privileges }),
tableConfig: null,
setTableConfig: (config) => set({ tableConfig: config }),
}));
export const salesEmployeesStore = createEmployeesStore();
export const logisticsEmployeesStore = createEmployeesStore();
@@ -1,10 +1,10 @@
import { lazy } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { WEB_URL } from '../../../../core/constants/web-url';
const DivisionsModule = lazy(() => import('./divisions/presentation/factory'));
const BranchesModule = lazy(() => import('./branches/presentation/factory'));
const CustomersModule = lazy(() => import('./customers/presentation/factory'));
const EmployeesModule = lazy(() => import('./employees/presentation/factory'));
const ProductsModule = lazy(() => import('./products/presentation/factory'));
export default function ConfigurationModule() {
@@ -13,8 +13,8 @@ export default function ConfigurationModule() {
<Route path="/divisions/*" element={<DivisionsModule />} />
<Route path="/branches/*" element={<BranchesModule />} />
<Route path="/customers/*" element={<CustomersModule />} />
<Route path="/employees/*" element={<EmployeesModule />} />
<Route path="/products/*" element={<ProductsModule />} />
<Route path="/employees/*" element={<Navigate to={`${WEB_URL.SALES_EMPLOYEES}/index`} replace={true} />} />
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
</Routes>
);
@@ -1,12 +1,19 @@
import { Box, FieldAsyncSelect, FieldNumberInput, Paper, SimpleGrid, Text } from '@repo/ui/components';
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
import { loadEmployeeOptions } from '../../../../shared/load-employee-options';
import {
useEnterpriseModuleTranslationContext,
useEnterpriseModuleConfigContext,
useFormPageContext,
} from '@repo/ui/foundations';
import { purposeFromModuleKey } from '../../../../../../../../core/domain/field-purpose';
import { loadLogisticsEmployeeOptions, loadSalesEmployeeOptions } from '../../../../shared/load-employee-options';
import { relationLabel } from '../../../../shared/relation-label';
import type { EmployeeEntity } from '../../../../../configuration/employees/domain/entities';
export function FormGeneral() {
const { formControl } = useFormPageContext();
const { t } = useEnterpriseModuleTranslationContext();
const { config } = useEnterpriseModuleConfigContext();
const purpose = purposeFromModuleKey(config.moduleKey);
const employee = formControl.watch('employee') as EmployeeEntity | null | undefined;
return (
@@ -25,7 +32,7 @@ export function FormGeneral() {
labelKey="name"
required
searchable
loadOptions={loadEmployeeOptions}
loadOptions={purpose === 'sales' ? loadSalesEmployeeOptions : loadLogisticsEmployeeOptions}
defaultOptions={employee ? [employee] : []}
renderLabel={relationLabel}
/>
@@ -1,12 +1,17 @@
import { SimpleGrid } from '@repo/ui/components';
import { FieldAsyncSelect, FieldNumberInput, FieldSelect } from '@repo/ui/form';
import { UseFormReturn } from 'react-hook-form';
import { useEnterpriseModuleConfigContext } from '@repo/ui/foundations';
import { purposeFromModuleKey } from '../../../../../../../../core/domain/field-purpose';
import { statusFilterOptions } from '../../../../../configuration/shared/status-filter-options';
import { loadEmployeeOptions } from '../../../../shared/load-employee-options';
import { loadLogisticsEmployeeOptions, loadSalesEmployeeOptions } from '../../../../shared/load-employee-options';
import { relationLabel } from '../../../../shared/relation-label';
import type { EmployeeEntity } from '../../../../../configuration/employees/domain/entities';
export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (key: string) => string }) => {
const { config } = useEnterpriseModuleConfigContext();
const purpose = purposeFromModuleKey(config.moduleKey);
return (
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<FieldAsyncSelect<EmployeeEntity>
@@ -18,7 +23,7 @@ export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (k
labelKey="name"
clearable
searchable
loadOptions={loadEmployeeOptions}
loadOptions={purpose === 'sales' ? loadSalesEmployeeOptions : loadLogisticsEmployeeOptions}
renderLabel={relationLabel}
/>
<FieldNumberInput
@@ -2,12 +2,14 @@ import { lazy } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { EmbeddedComingSoonPage } from '../../../../../core/components/coming-soon-page';
const EmployeesModule = lazy(() => import('../../configuration/employees/presentation/factory'));
const CyclesModule = lazy(() => import('../cycles/presentation/factory'));
const PlansModule = lazy(() => import('../plans/presentation/factory'));
export default function LogisticsFieldModule() {
return (
<Routes>
<Route path="/employees/*" element={<EmployeesModule purpose="logistics" />} />
<Route path="/cycles/*" element={<CyclesModule purpose="logistics" />} />
<Route path="/plans/*" element={<PlansModule purpose="logistics" />} />
<Route path="/packing-slips" element={<EmbeddedComingSoonPage />} />
@@ -5,7 +5,7 @@ import {
useEnterpriseModuleConfigContext,
} from '@repo/ui/foundations';
import { purposeFromModuleKey } from '../../../../../../../../core/domain/field-purpose';
import { loadEmployeeOptions } from '../../../../shared/load-employee-options';
import { loadLogisticsEmployeeOptions, loadSalesEmployeeOptions } from '../../../../shared/load-employee-options';
import { loadBranchOptions } from '../../../../shared/load-branch-options';
import { loadCustomerOptions } from '../../../../shared/load-customer-options';
import { loadSalesInvoiceOptions, loadPackingSlipOptions } from '../../../../shared/lookup.factories';
@@ -42,7 +42,7 @@ export function FormGeneral() {
labelKey="name"
required
searchable
loadOptions={loadEmployeeOptions}
loadOptions={purpose === 'sales' ? loadSalesEmployeeOptions : loadLogisticsEmployeeOptions}
defaultOptions={employee ? [employee] : []}
renderLabel={relationLabel}
/>
@@ -1,12 +1,17 @@
import { SimpleGrid } from '@repo/ui/components';
import { FieldAsyncSelect, FieldDatePicker, FieldSelect } from '@repo/ui/form';
import { UseFormReturn } from 'react-hook-form';
import { useEnterpriseModuleConfigContext } from '@repo/ui/foundations';
import { purposeFromModuleKey } from '../../../../../../../../core/domain/field-purpose';
import { statusFilterOptions } from '../../../../../configuration/shared/status-filter-options';
import { loadEmployeeOptions } from '../../../../shared/load-employee-options';
import { loadLogisticsEmployeeOptions, loadSalesEmployeeOptions } from '../../../../shared/load-employee-options';
import { relationLabel } from '../../../../shared/relation-label';
import type { EmployeeEntity } from '../../../../../configuration/employees/domain/entities';
export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (key: string) => string }) => {
const { config } = useEnterpriseModuleConfigContext();
const purpose = purposeFromModuleKey(config.moduleKey);
return (
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<FieldAsyncSelect<EmployeeEntity>
@@ -17,7 +22,7 @@ export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (k
labelKey="name"
clearable
searchable
loadOptions={loadEmployeeOptions}
loadOptions={purpose === 'sales' ? loadSalesEmployeeOptions : loadLogisticsEmployeeOptions}
renderLabel={relationLabel}
/>
<FieldDatePicker control={form.control} name="date" label={t('common:fields.date')} clearable />
@@ -2,8 +2,13 @@ import { useMemo } from 'react';
import { Button, FieldAsyncSelect, FieldDatePicker, Group, Modal, Stack, notifications } from '@repo/ui/components';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useEnterpriseModuleTranslationContext, useEnterpriseModuleDataServiceContext } from '@repo/ui/foundations';
import { loadEmployeeOptions } from '../../../../shared/load-employee-options';
import {
useEnterpriseModuleConfigContext,
useEnterpriseModuleDataServiceContext,
useEnterpriseModuleTranslationContext,
} from '@repo/ui/foundations';
import { purposeFromModuleKey } from '../../../../../../../../core/domain/field-purpose';
import { loadLogisticsEmployeeOptions, loadSalesEmployeeOptions } from '../../../../shared/load-employee-options';
import { relationLabel } from '../../../../shared/relation-label';
import { createGeneratePlansSchema } from '../../../domain/validators/plan.validator';
import type { PlansRemoteDataServices } from '../../../data/plan.remote.service';
@@ -12,6 +17,8 @@ import type { PlanEntity } from '../../../domain/entities';
export function GeneratePlansModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
const { t } = useEnterpriseModuleTranslationContext();
const { config } = useEnterpriseModuleConfigContext();
const purpose = purposeFromModuleKey(config.moduleKey);
const { dataServices } = useEnterpriseModuleDataServiceContext<PlanEntity, PlansRemoteDataServices>();
const validator = useMemo(() => createGeneratePlansSchema(t), [t]);
const form = useForm({ resolver: zodResolver(validator) });
@@ -45,7 +52,7 @@ export function GeneratePlansModal({ opened, onClose }: { opened: boolean; onClo
labelKey="name"
required
searchable
loadOptions={loadEmployeeOptions}
loadOptions={purpose === 'sales' ? loadSalesEmployeeOptions : loadLogisticsEmployeeOptions}
renderLabel={relationLabel}
/>
<FieldDatePicker control={form.control as any} name="from" label={t('common:fields.from')} required />
@@ -1,5 +1,27 @@
import { employeesDataService } from '../../configuration/employees/domain/factories';
import type { LoadOptionsFn } from '@repo/ui/form';
import type { FieldPurpose } from '../../../../../core/domain/field-purpose';
import {
employeesDataService,
logisticsEmployeesDataService,
salesEmployeesDataService,
} from '../../configuration/employees/domain/factories';
import type { EmployeeEntity } from '../../configuration/employees/domain/entities';
import { createOptionLoader } from './create-option-loader';
export const loadEmployeeOptions = createOptionLoader<EmployeeEntity>((config) => employeesDataService.getMany(config));
function employeeServiceForPurpose(purpose?: FieldPurpose) {
if (purpose === 'sales') {
return salesEmployeesDataService;
}
if (purpose === 'logistics') {
return logisticsEmployeesDataService;
}
return employeesDataService;
}
export function loadEmployeeOptionsForPurpose(purpose?: FieldPurpose): LoadOptionsFn<EmployeeEntity> {
const service = employeeServiceForPurpose(purpose);
return createOptionLoader<EmployeeEntity>((config) => service.getMany(config));
}
export const loadSalesEmployeeOptions = loadEmployeeOptionsForPurpose('sales');
export const loadLogisticsEmployeeOptions = loadEmployeeOptionsForPurpose('logistics');
@@ -4,12 +4,14 @@ import { EmbeddedComingSoonPage } from '../../../../core/components/coming-soon-
const RequestsModule = lazy(() => import('./requests/presentation/factory'));
const OrdersModule = lazy(() => import('./orders/presentation/factory'));
const EmployeesModule = lazy(() => import('../configuration/employees/presentation/factory'));
const CyclesModule = lazy(() => import('../field/cycles/presentation/factory'));
const PlansModule = lazy(() => import('../field/plans/presentation/factory'));
export default function SalesModule() {
return (
<Routes>
<Route path="/employees/*" element={<EmployeesModule purpose="sales" />} />
<Route path="/requests/*" element={<RequestsModule />} />
<Route path="/orders/*" element={<OrdersModule />} />
<Route path="/cycles/*" element={<CyclesModule purpose="sales" />} />
@@ -1,7 +1,7 @@
import { SimpleGrid } from '@repo/ui/components';
import { FieldAsyncSelect, FieldSelect, FieldTextInput } from '@repo/ui/form';
import { UseFormReturn } from 'react-hook-form';
import { loadEmployeeOptions } from '../../field/shared/load-employee-options';
import { loadSalesEmployeeOptions } from '../../field/shared/load-employee-options';
import { loadBranchOptions } from '../../field/shared/load-branch-options';
import { loadCustomerOptions } from '../../field/shared/load-customer-options';
import { loadDivisionOptions } from '../../configuration/shared/load-division-options';
@@ -50,7 +50,7 @@ export function SalesFilterFormContent({
labelKey="name"
searchable
clearable
loadOptions={loadEmployeeOptions}
loadOptions={loadSalesEmployeeOptions}
renderLabel={relationLabel}
/>
<FieldAsyncSelect<BranchEntity>
@@ -1,6 +1,6 @@
import { Box, FieldAsyncSelect, FieldDatePicker, FieldTextInput, Paper, SimpleGrid, Text } from '@repo/ui/components';
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
import { loadEmployeeOptions } from '../../field/shared/load-employee-options';
import { loadSalesEmployeeOptions } from '../../field/shared/load-employee-options';
import { loadBranchOptions } from '../../field/shared/load-branch-options';
import { loadCustomerOptions } from '../../field/shared/load-customer-options';
import { loadDivisionOptions } from '../../configuration/shared/load-division-options';
@@ -47,7 +47,7 @@ export function FormGeneral() {
labelKey="name"
required
searchable
loadOptions={loadEmployeeOptions}
loadOptions={loadSalesEmployeeOptions}
defaultOptions={salesPerson ? [salesPerson] : []}
renderLabel={relationLabel}
/>
+2 -1
View File
@@ -3,7 +3,8 @@ export const WEB_URL = {
DIVISIONS: '/app/configuration/divisions',
BRANCHES: '/app/configuration/branches',
CUSTOMERS: '/app/configuration/customers',
EMPLOYEES: '/app/configuration/employees',
SALES_EMPLOYEES: '/app/sales/employees',
LOGISTICS_EMPLOYEES: '/app/logistics/employees',
PRODUCTS: '/app/configuration/products',
SALES_REQUESTS: '/app/sales/requests',
SALES_ORDERS: '/app/sales/orders',