diff --git a/apps/web/src/apps/main/layouts/data/menu.data.test.ts b/apps/web/src/apps/main/layouts/data/menu.data.test.ts index db28f99..1f7aaef 100644 --- a/apps/web/src/apps/main/layouts/data/menu.data.test.ts +++ b/apps/web/src/apps/main/layouts/data/menu.data.test.ts @@ -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', diff --git a/apps/web/src/apps/main/layouts/data/menu.data.ts b/apps/web/src/apps/main/layouts/data/menu.data.ts index a235059..f3686a3 100644 --- a/apps/web/src/apps/main/layouts/data/menu.data.ts +++ b/apps/web/src/apps/main/layouts/data/menu.data.ts @@ -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', }, { diff --git a/apps/web/src/apps/main/modules/configuration/employees/data/employee.remote.service.test.ts b/apps/web/src/apps/main/modules/configuration/employees/data/employee.remote.service.test.ts index bd3a616..6fff814 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/data/employee.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/configuration/employees/data/employee.remote.service.test.ts @@ -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', + ); + }); }); diff --git a/apps/web/src/apps/main/modules/configuration/employees/data/employee.remote.service.ts b/apps/web/src/apps/main/modules/configuration/employees/data/employee.remote.service.ts index 48f3299..5a23166 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/data/employee.remote.service.ts +++ b/apps/web/src/apps/main/modules/configuration/employees/data/employee.remote.service.ts @@ -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 { + 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 { constructor(httpClient: AxiosInstance, config: DataServicesConfig) { super(httpClient, { @@ -10,4 +27,11 @@ export class EmployeesRemoteDataServices extends TrackGoRemoteDataServices(config?: AxiosRequestConfig) { + return super.getMany({ + ...config, + paramsSerializer: serializeRepeatQuery, + }); + } } diff --git a/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.test.ts b/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.test.ts new file mode 100644 index 0000000..3bab4cb --- /dev/null +++ b/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.test.ts @@ -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'); + }); +}); diff --git a/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.ts b/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.ts index 41cd5e5..5a0438a 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.ts +++ b/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.ts @@ -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 = { - 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 { + return { + moduleKey: 'CONFIGURATION.EMPLOYEE', + translationNamespace: 'EMPLOYEES', + apiUrl: '/employees', + webUrl: `/app/${purpose}/employees`, + moduleCategory: 'FULL_PAGE', + moduleType: 'MASTER_DATA', + }; +} diff --git a/apps/web/src/apps/main/modules/configuration/employees/domain/entities/employee.entity.ts b/apps/web/src/apps/main/modules/configuration/employees/domain/entities/employee.entity.ts index 9b2b737..e7fcce3 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/domain/entities/employee.entity.ts +++ b/apps/web/src/apps/main/modules/configuration/employees/domain/entities/employee.entity.ts @@ -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; diff --git a/apps/web/src/apps/main/modules/configuration/employees/domain/factories/index.ts b/apps/web/src/apps/main/modules/configuration/employees/domain/factories/index.ts index 90a2389..2438e63 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/domain/factories/index.ts +++ b/apps/web/src/apps/main/modules/configuration/employees/domain/factories/index.ts @@ -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'); diff --git a/apps/web/src/apps/main/modules/configuration/employees/domain/transformers/employee.remote.transformer.test.ts b/apps/web/src/apps/main/modules/configuration/employees/domain/transformers/employee.remote.transformer.test.ts index caa7d78..2ff47db 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/domain/transformers/employee.remote.transformer.test.ts +++ b/apps/web/src/apps/main/modules/configuration/employees/domain/transformers/employee.remote.transformer.test.ts @@ -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(); + }); }); diff --git a/apps/web/src/apps/main/modules/configuration/employees/domain/transformers/employee.remote.transformer.ts b/apps/web/src/apps/main/modules/configuration/employees/domain/transformers/employee.remote.transformer.ts index 04e8ac5..31e27c2 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/domain/transformers/employee.remote.transformer.ts +++ b/apps/web/src/apps/main/modules/configuration/employees/domain/transformers/employee.remote.transformer.ts @@ -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 { + 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): Record { + 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; + } } diff --git a/apps/web/src/apps/main/modules/configuration/employees/domain/validators/employee.validator.test.ts b/apps/web/src/apps/main/modules/configuration/employees/domain/validators/employee.validator.test.ts index 3bacc02..5cfbad0 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/domain/validators/employee.validator.test.ts +++ b/apps/web/src/apps/main/modules/configuration/employees/domain/validators/employee.validator.test.ts @@ -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); + }); }); diff --git a/apps/web/src/apps/main/modules/configuration/employees/domain/validators/employee.validator.ts b/apps/web/src/apps/main/modules/configuration/employees/domain/validators/employee.validator.ts index c89df8c..558055c 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/domain/validators/employee.validator.ts +++ b/apps/web/src/apps/main/modules/configuration/employees/domain/validators/employee.validator.ts @@ -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') } }), }), }); diff --git a/apps/web/src/apps/main/modules/configuration/employees/presentation/components/form-component/form-general.tsx b/apps/web/src/apps/main/modules/configuration/employees/presentation/components/form-component/form-general.tsx index 7f2bfc2..f0ef527 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/presentation/components/form-component/form-general.tsx +++ b/apps/web/src/apps/main/modules/configuration/employees/presentation/components/form-component/form-general.tsx @@ -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" /> - ({ value, label: t(`position_${value}`) }))} - required - radius="md" - /> + {positions.length > 1 ? ( + ({ value, label: t(`position_${value}`) }))} + required + radius="md" + /> + ) : null} diff --git a/apps/web/src/apps/main/modules/configuration/employees/presentation/components/index-component/filter-content.tsx b/apps/web/src/apps/main/modules/configuration/employees/presentation/components/index-component/filter-content.tsx index b225bbe..e8c82b0 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/presentation/components/index-component/filter-content.tsx +++ b/apps/web/src/apps/main/modules/configuration/employees/presentation/components/index-component/filter-content.tsx @@ -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; t: (key: string) => string }) => { +export const FilterFormContent = ({ + form, + t, + positions, +}: { + form: UseFormReturn; + t: (key: string) => string; + positions: readonly EmployeePosition[]; +}) => { return ( ; t: (k label={t('common:fields.phone')} placeholder={`Enter ${t('common:fields.phone')}`} /> - ({ value, label: t(`position_${value}`) }))} - /> + {positions.length > 1 ? ( + ({ value, label: t(`position_${value}`) }))} + /> + ) : null} 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 ( - - config={employeesModuleConfig} - dataServices={employeesDataService} - store={employeesStore} - > + config={config} dataServices={dataService} store={store}> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> diff --git a/apps/web/src/apps/main/modules/configuration/employees/presentation/pages/employee.page.detail.tsx b/apps/web/src/apps/main/modules/configuration/employees/presentation/pages/employee.page.detail.tsx index 4a602c4..a73d053 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/presentation/pages/employee.page.detail.tsx +++ b/apps/web/src/apps/main/modules/configuration/employees/presentation/pages/employee.page.detail.tsx @@ -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 ( diff --git a/apps/web/src/apps/main/modules/configuration/employees/presentation/pages/employee.page.form.tsx b/apps/web/src/apps/main/modules/configuration/employees/presentation/pages/employee.page.form.tsx index d51bce5..78a4970 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/presentation/pages/employee.page.form.tsx +++ b/apps/web/src/apps/main/modules/configuration/employees/presentation/pages/employee.page.form.tsx @@ -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 ( - + ); } diff --git a/apps/web/src/apps/main/modules/configuration/employees/presentation/pages/employee.page.index.tsx b/apps/web/src/apps/main/modules/configuration/employees/presentation/pages/employee.page.index.tsx index 65a83c0..c88785b 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/presentation/pages/employee.page.index.tsx +++ b/apps/web/src/apps/main/modules/configuration/employees/presentation/pages/employee.page.index.tsx @@ -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[] = useMemo(() => { return [ @@ -31,10 +33,10 @@ export default function EmployeePageIndex() { return { renderBody: (form: any) => { if (!form) return null; - return ; + return ; }, }; - }, [t]); + }, [positions, t]); return ( {} -export const employeesStore = create((set) => ({ - metaData: { limit: 15 }, - setMetaData: (data) => set({ metaData: data }), +export function createEmployeesStore() { + return create((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(); diff --git a/apps/web/src/apps/main/modules/configuration/index.tsx b/apps/web/src/apps/main/modules/configuration/index.tsx index a4e0f32..97f3680 100644 --- a/apps/web/src/apps/main/modules/configuration/index.tsx +++ b/apps/web/src/apps/main/modules/configuration/index.tsx @@ -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() { } /> } /> } /> - } /> } /> + } /> } /> ); diff --git a/apps/web/src/apps/main/modules/field/cycles/presentation/components/form-component/form-general.tsx b/apps/web/src/apps/main/modules/field/cycles/presentation/components/form-component/form-general.tsx index b6e361b..6b9e828 100644 --- a/apps/web/src/apps/main/modules/field/cycles/presentation/components/form-component/form-general.tsx +++ b/apps/web/src/apps/main/modules/field/cycles/presentation/components/form-component/form-general.tsx @@ -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} /> diff --git a/apps/web/src/apps/main/modules/field/cycles/presentation/components/index-component/filter-content.tsx b/apps/web/src/apps/main/modules/field/cycles/presentation/components/index-component/filter-content.tsx index f7929a0..7ff8df5 100644 --- a/apps/web/src/apps/main/modules/field/cycles/presentation/components/index-component/filter-content.tsx +++ b/apps/web/src/apps/main/modules/field/cycles/presentation/components/index-component/filter-content.tsx @@ -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; t: (key: string) => string }) => { + const { config } = useEnterpriseModuleConfigContext(); + const purpose = purposeFromModuleKey(config.moduleKey); + return ( @@ -18,7 +23,7 @@ export const FilterFormContent = ({ form, t }: { form: UseFormReturn; t: (k labelKey="name" clearable searchable - loadOptions={loadEmployeeOptions} + loadOptions={purpose === 'sales' ? loadSalesEmployeeOptions : loadLogisticsEmployeeOptions} renderLabel={relationLabel} /> import('../../configuration/employees/presentation/factory')); const CyclesModule = lazy(() => import('../cycles/presentation/factory')); const PlansModule = lazy(() => import('../plans/presentation/factory')); export default function LogisticsFieldModule() { return ( + } /> } /> } /> } /> diff --git a/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/form-general.tsx b/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/form-general.tsx index 1887ba5..dbe5f50 100644 --- a/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/form-general.tsx +++ b/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/form-general.tsx @@ -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} /> diff --git a/apps/web/src/apps/main/modules/field/plans/presentation/components/index-component/filter-content.tsx b/apps/web/src/apps/main/modules/field/plans/presentation/components/index-component/filter-content.tsx index 39b3cdc..4872e8b 100644 --- a/apps/web/src/apps/main/modules/field/plans/presentation/components/index-component/filter-content.tsx +++ b/apps/web/src/apps/main/modules/field/plans/presentation/components/index-component/filter-content.tsx @@ -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; t: (key: string) => string }) => { + const { config } = useEnterpriseModuleConfigContext(); + const purpose = purposeFromModuleKey(config.moduleKey); + return ( @@ -17,7 +22,7 @@ export const FilterFormContent = ({ form, t }: { form: UseFormReturn; t: (k labelKey="name" clearable searchable - loadOptions={loadEmployeeOptions} + loadOptions={purpose === 'sales' ? loadSalesEmployeeOptions : loadLogisticsEmployeeOptions} renderLabel={relationLabel} /> diff --git a/apps/web/src/apps/main/modules/field/plans/presentation/components/index-component/generate-modal.tsx b/apps/web/src/apps/main/modules/field/plans/presentation/components/index-component/generate-modal.tsx index 15509b0..c2ca9e4 100644 --- a/apps/web/src/apps/main/modules/field/plans/presentation/components/index-component/generate-modal.tsx +++ b/apps/web/src/apps/main/modules/field/plans/presentation/components/index-component/generate-modal.tsx @@ -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(); 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} /> diff --git a/apps/web/src/apps/main/modules/field/shared/load-employee-options.ts b/apps/web/src/apps/main/modules/field/shared/load-employee-options.ts index c20b1a2..b744d25 100644 --- a/apps/web/src/apps/main/modules/field/shared/load-employee-options.ts +++ b/apps/web/src/apps/main/modules/field/shared/load-employee-options.ts @@ -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((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 { + const service = employeeServiceForPurpose(purpose); + return createOptionLoader((config) => service.getMany(config)); +} + +export const loadSalesEmployeeOptions = loadEmployeeOptionsForPurpose('sales'); +export const loadLogisticsEmployeeOptions = loadEmployeeOptionsForPurpose('logistics'); diff --git a/apps/web/src/apps/main/modules/sales/index.tsx b/apps/web/src/apps/main/modules/sales/index.tsx index 02bfcbf..dc75f77 100644 --- a/apps/web/src/apps/main/modules/sales/index.tsx +++ b/apps/web/src/apps/main/modules/sales/index.tsx @@ -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 ( + } /> } /> } /> } /> diff --git a/apps/web/src/apps/main/modules/sales/shared/filter-content.tsx b/apps/web/src/apps/main/modules/sales/shared/filter-content.tsx index eddf867..9ef8f37 100644 --- a/apps/web/src/apps/main/modules/sales/shared/filter-content.tsx +++ b/apps/web/src/apps/main/modules/sales/shared/filter-content.tsx @@ -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} /> diff --git a/apps/web/src/apps/main/modules/sales/shared/form-general.tsx b/apps/web/src/apps/main/modules/sales/shared/form-general.tsx index d3453f1..2c7d218 100644 --- a/apps/web/src/apps/main/modules/sales/shared/form-general.tsx +++ b/apps/web/src/apps/main/modules/sales/shared/form-general.tsx @@ -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} /> diff --git a/apps/web/src/core/constants/web-url.ts b/apps/web/src/core/constants/web-url.ts index 0c0ac68..45d2d26 100644 --- a/apps/web/src/core/constants/web-url.ts +++ b/apps/web/src/core/constants/web-url.ts @@ -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',