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:
@@ -21,6 +21,7 @@ describe('MENU_ITEMS', () => {
|
|||||||
|
|
||||||
expect(childKeys(sales)).toEqual(['sales-data', 'sales-activities', 'sales-reports']);
|
expect(childKeys(sales)).toEqual(['sales-data', 'sales-activities', 'sales-reports']);
|
||||||
expect(childKeys(findItem(sales?.children ?? [], 'sales-data'))).toEqual(['sales-employees', 'sales-cycles']);
|
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([
|
expect(childKeys(findItem(sales?.children ?? [], 'sales-activities'))).toEqual([
|
||||||
'sales-requests',
|
'sales-requests',
|
||||||
'sales-orders',
|
'sales-orders',
|
||||||
@@ -38,6 +39,9 @@ describe('MENU_ITEMS', () => {
|
|||||||
'logistics-employees',
|
'logistics-employees',
|
||||||
'logistics-cycles',
|
'logistics-cycles',
|
||||||
]);
|
]);
|
||||||
|
expect(findItem(logistics?.children ?? [], 'logistics-data')?.children?.[0]?.path).toBe(
|
||||||
|
'/app/logistics/employees/index',
|
||||||
|
);
|
||||||
expect(childKeys(findItem(logistics?.children ?? [], 'logistics-activities'))).toEqual([
|
expect(childKeys(findItem(logistics?.children ?? [], 'logistics-activities'))).toEqual([
|
||||||
'logistics-packing-slips',
|
'logistics-packing-slips',
|
||||||
'logistics-plans',
|
'logistics-plans',
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
key: 'sales-employees',
|
key: 'sales-employees',
|
||||||
label: 'nav:configuration-employees',
|
label: 'nav:configuration-employees',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
path: '/app/configuration/employees/index',
|
path: '/app/sales/employees/index',
|
||||||
moduleKey: 'CONFIGURATION.EMPLOYEE',
|
moduleKey: 'CONFIGURATION.EMPLOYEE',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -129,7 +129,7 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
key: 'logistics-employees',
|
key: 'logistics-employees',
|
||||||
label: 'nav:configuration-employees',
|
label: 'nav:configuration-employees',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
path: '/app/configuration/employees/index',
|
path: '/app/logistics/employees/index',
|
||||||
moduleKey: 'CONFIGURATION.EMPLOYEE',
|
moduleKey: 'CONFIGURATION.EMPLOYEE',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+7
-1
@@ -1,6 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import type { AxiosInstance } from '@repo/core-api/http-client';
|
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';
|
import { EmployeesRemoteDataTransformer } from '../domain/transformers/employee.remote.transformer';
|
||||||
|
|
||||||
function createMockHttpClient(): AxiosInstance {
|
function createMockHttpClient(): AxiosInstance {
|
||||||
@@ -51,4 +51,10 @@ describe('EmployeesRemoteDataServices', () => {
|
|||||||
expect.objectContaining({ url: '/employees/bulk-delete', method: 'POST' }),
|
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',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+25
-1
@@ -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 type { DataServicesConfig } from '@repo/core-api/data-services';
|
||||||
import { TrackGoRemoteDataServices } from '../../../../../../core/lib/trackgo-remote-data-services';
|
import { TrackGoRemoteDataServices } from '../../../../../../core/lib/trackgo-remote-data-services';
|
||||||
import type { EmployeeEntity } from '../domain/entities';
|
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> {
|
export class EmployeesRemoteDataServices extends TrackGoRemoteDataServices<EmployeeEntity> {
|
||||||
constructor(httpClient: AxiosInstance, config: DataServicesConfig<EmployeeEntity>) {
|
constructor(httpClient: AxiosInstance, config: DataServicesConfig<EmployeeEntity>) {
|
||||||
super(httpClient, {
|
super(httpClient, {
|
||||||
@@ -10,4 +27,11 @@ export class EmployeesRemoteDataServices extends TrackGoRemoteDataServices<Emplo
|
|||||||
apiUrl: config.apiUrl ?? '/employees',
|
apiUrl: config.apiUrl ?? '/employees',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMany<T = unknown>(config?: AxiosRequestConfig) {
|
||||||
|
return super.getMany<T>({
|
||||||
|
...config,
|
||||||
|
paramsSerializer: serializeRepeatQuery,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
+11
-8
@@ -1,11 +1,14 @@
|
|||||||
import { ModuleConfigEntity } from '@repo/ui/foundations';
|
import { ModuleConfigEntity } from '@repo/ui/foundations';
|
||||||
|
import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose';
|
||||||
import type { EmployeeEntity } from '../entities';
|
import type { EmployeeEntity } from '../entities';
|
||||||
|
|
||||||
export const employeesModuleConfig: ModuleConfigEntity<EmployeeEntity> = {
|
export function createEmployeeModuleConfig(purpose: FieldPurpose): ModuleConfigEntity<EmployeeEntity> {
|
||||||
moduleKey: 'CONFIGURATION.EMPLOYEE',
|
return {
|
||||||
translationNamespace: 'EMPLOYEES',
|
moduleKey: 'CONFIGURATION.EMPLOYEE',
|
||||||
apiUrl: '/employees',
|
translationNamespace: 'EMPLOYEES',
|
||||||
webUrl: '/app/configuration/employees',
|
apiUrl: '/employees',
|
||||||
moduleCategory: 'FULL_PAGE',
|
webUrl: `/app/${purpose}/employees`,
|
||||||
moduleType: 'MASTER_DATA',
|
moduleCategory: 'FULL_PAGE',
|
||||||
} as const;
|
moduleType: 'MASTER_DATA',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
+9
@@ -1,9 +1,18 @@
|
|||||||
import { BaseEntity } from '@repo/core-api/data-services';
|
import { BaseEntity } from '@repo/core-api/data-services';
|
||||||
import type { ConfigurationStatus } from '../../../divisions/domain/entities';
|
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 const EMPLOYEE_POSITIONS = ['sales', 'driver', 'crew'] as const;
|
||||||
export type EmployeePosition = (typeof EMPLOYEE_POSITIONS)[number];
|
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 {
|
export interface EmployeeEntity extends BaseEntity {
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import { apiClient } from '../../../../../../../core/lib/api-client';
|
import { apiClient } from '../../../../../../../core/lib/api-client';
|
||||||
|
import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose';
|
||||||
import { EmployeesRemoteDataServices } from '../../data/employee.remote.service';
|
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';
|
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, {
|
export const employeesDataService = createEmployeeDataService();
|
||||||
apiUrl: employeesModuleConfig.apiUrl,
|
export const salesEmployeesDataService = createEmployeeDataService('sales');
|
||||||
moduleKey: employeesModuleConfig.moduleKey,
|
export const logisticsEmployeesDataService = createEmployeeDataService('logistics');
|
||||||
transformer: employeesDataTransformer,
|
|
||||||
});
|
|
||||||
|
|||||||
+49
-2
@@ -2,8 +2,6 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import { EmployeesRemoteDataTransformer } from './employee.remote.transformer';
|
import { EmployeesRemoteDataTransformer } from './employee.remote.transformer';
|
||||||
import type { EmployeeEntity } from '../entities';
|
import type { EmployeeEntity } from '../entities';
|
||||||
|
|
||||||
const transformer = new EmployeesRemoteDataTransformer();
|
|
||||||
|
|
||||||
const dto = {
|
const dto = {
|
||||||
id: 'emp-1',
|
id: 'emp-1',
|
||||||
code: 'EMP_01',
|
code: 'EMP_01',
|
||||||
@@ -18,6 +16,8 @@ const dto = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('EmployeesRemoteDataTransformer', () => {
|
describe('EmployeesRemoteDataTransformer', () => {
|
||||||
|
const transformer = new EmployeesRemoteDataTransformer();
|
||||||
|
|
||||||
it('maps dto fields onto the entity', () => {
|
it('maps dto fields onto the entity', () => {
|
||||||
const entity = transformer.transformToEntity(dto);
|
const entity = transformer.transformToEntity(dto);
|
||||||
expect(entity).toMatchObject({
|
expect(entity).toMatchObject({
|
||||||
@@ -52,4 +52,51 @@ describe('EmployeesRemoteDataTransformer', () => {
|
|||||||
expect(payload).not.toHaveProperty('status');
|
expect(payload).not.toHaveProperty('status');
|
||||||
expect(payload).not.toHaveProperty('id');
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+40
-3
@@ -1,7 +1,12 @@
|
|||||||
import { BaseDataTransformer } from '@repo/core-api/data-services';
|
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> {
|
export class EmployeesRemoteDataTransformer extends BaseDataTransformer<EmployeeEntity> {
|
||||||
|
constructor(private readonly allowedPositions?: readonly EmployeePosition[]) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
transformToEntity(dto: EmployeeDto | EmployeeEntity): EmployeeEntity {
|
transformToEntity(dto: EmployeeDto | EmployeeEntity): EmployeeEntity {
|
||||||
return {
|
return {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
@@ -26,7 +31,7 @@ export class EmployeesRemoteDataTransformer extends BaseDataTransformer<Employee
|
|||||||
code: entity.code,
|
code: entity.code,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
phone: entity.phone,
|
phone: entity.phone,
|
||||||
position: entity.position,
|
position: this.resolvePosition(entity.position),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +40,39 @@ export class EmployeesRemoteDataTransformer extends BaseDataTransformer<Employee
|
|||||||
code: entity.code,
|
code: entity.code,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
phone: entity.phone,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -22,4 +22,16 @@ describe('createEmployeeSchema', () => {
|
|||||||
it('rejects an unknown position', () => {
|
it('rejects an unknown position', () => {
|
||||||
expect(schema.safeParse({ ...valid, position: 'manager' }).success).toBe(false);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+7
-3
@@ -4,14 +4,18 @@ import {
|
|||||||
configNameSchema,
|
configNameSchema,
|
||||||
configPhoneSchema,
|
configPhoneSchema,
|
||||||
} from '../../../../../../../core/domain/configuration-field-validators';
|
} 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({
|
return z.object({
|
||||||
code: configCodeSchema(t),
|
code: configCodeSchema(t),
|
||||||
name: configNameSchema(t),
|
name: configNameSchema(t),
|
||||||
phone: configPhoneSchema(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') } }),
|
required_error: JSON.stringify({ key: 'validation:required', values: { field: t('common:fields.position') } }),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
+12
-10
@@ -1,8 +1,8 @@
|
|||||||
import { Box, FieldTextInput, FieldSelect, Paper, SimpleGrid, Text } from '@repo/ui/components';
|
import { Box, FieldTextInput, FieldSelect, Paper, SimpleGrid, Text } from '@repo/ui/components';
|
||||||
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
|
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 { formControl } = useFormPageContext();
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
|
||||||
@@ -37,14 +37,16 @@ export function FormGeneral() {
|
|||||||
required
|
required
|
||||||
radius="md"
|
radius="md"
|
||||||
/>
|
/>
|
||||||
<FieldSelect
|
{positions.length > 1 ? (
|
||||||
control={formControl.control}
|
<FieldSelect
|
||||||
name="position"
|
control={formControl.control}
|
||||||
label={t('common:fields.position')}
|
name="position"
|
||||||
data={EMPLOYEE_POSITIONS.map((value) => ({ value, label: t(`position_${value}`) }))}
|
label={t('common:fields.position')}
|
||||||
required
|
data={positions.map((value) => ({ value, label: t(`position_${value}`) }))}
|
||||||
radius="md"
|
required
|
||||||
/>
|
radius="md"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
+19
-9
@@ -2,9 +2,17 @@ import { SimpleGrid } from '@repo/ui/components';
|
|||||||
import { FieldTextInput, FieldSelect } from '@repo/ui/form';
|
import { FieldTextInput, FieldSelect } from '@repo/ui/form';
|
||||||
import { UseFormReturn } from 'react-hook-form';
|
import { UseFormReturn } from 'react-hook-form';
|
||||||
import { statusFilterOptions } from '../../../../shared/status-filter-options';
|
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 (
|
return (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||||
<FieldTextInput
|
<FieldTextInput
|
||||||
@@ -25,13 +33,15 @@ export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (k
|
|||||||
label={t('common:fields.phone')}
|
label={t('common:fields.phone')}
|
||||||
placeholder={`Enter ${t('common:fields.phone')}`}
|
placeholder={`Enter ${t('common:fields.phone')}`}
|
||||||
/>
|
/>
|
||||||
<FieldSelect
|
{positions.length > 1 ? (
|
||||||
control={form.control}
|
<FieldSelect
|
||||||
name="position"
|
control={form.control}
|
||||||
label={t('common:fields.position')}
|
name="position"
|
||||||
clearable
|
label={t('common:fields.position')}
|
||||||
data={EMPLOYEE_POSITIONS.map((value) => ({ value, label: t(`position_${value}`) }))}
|
clearable
|
||||||
/>
|
data={positions.map((value) => ({ value, label: t(`position_${value}`) }))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="status"
|
name="status"
|
||||||
|
|||||||
+17
-16
@@ -2,10 +2,11 @@ import { lazy } from 'react';
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { EnterpriseModuleProvider } from '@repo/ui/foundations';
|
import { EnterpriseModuleProvider } from '@repo/ui/foundations';
|
||||||
import { registerModuleNamespace } from '@repo/core-i18n';
|
import { registerModuleNamespace } from '@repo/core-i18n';
|
||||||
import { employeesModuleConfig } from '../../domain/constants';
|
import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose';
|
||||||
import { employeesDataService } from '../../domain/factories';
|
import { createEmployeeModuleConfig } from '../../domain/constants';
|
||||||
|
import { logisticsEmployeesDataService, salesEmployeesDataService } from '../../domain/factories';
|
||||||
import { EmployeeEntity } from '../../domain/entities';
|
import { EmployeeEntity } from '../../domain/entities';
|
||||||
import { employeesStore } from '../store';
|
import { logisticsEmployeesStore, salesEmployeesStore } from '../store';
|
||||||
|
|
||||||
import employeesId from '../languages/id/employees.json';
|
import employeesId from '../languages/id/employees.json';
|
||||||
import employeesEn from '../languages/en/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 FormPage = lazy(() => import('../pages/employee.page.form'));
|
||||||
const DetailPage = lazy(() => import('../pages/employee.page.detail'));
|
const DetailPage = lazy(() => import('../pages/employee.page.detail'));
|
||||||
|
|
||||||
registerModuleNamespace(employeesModuleConfig.translationNamespace, {
|
registerModuleNamespace('EMPLOYEES', {
|
||||||
id: employeesId,
|
id: employeesId,
|
||||||
en: employeesEn,
|
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 (
|
return (
|
||||||
<EnterpriseModuleProvider<EmployeeEntity>
|
<EnterpriseModuleProvider<EmployeeEntity> config={config} dataServices={dataService} store={store}>
|
||||||
config={employeesModuleConfig}
|
|
||||||
dataServices={employeesDataService}
|
|
||||||
store={employeesStore}
|
|
||||||
>
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/index" element={<IndexPage />} />
|
<Route path="/index" element={<IndexPage purpose={purpose} />} />
|
||||||
<Route path="/detail/:dataId" element={<DetailPage />} />
|
<Route path="/detail/:dataId" element={<DetailPage purpose={purpose} />} />
|
||||||
<Route path="/edit/:dataId" element={<FormPage formPageType="EDIT" />} />
|
<Route path="/edit/:dataId" element={<FormPage purpose={purpose} formPageType="EDIT" />} />
|
||||||
<Route path="/duplicate/:dataId" element={<FormPage formPageType="DUPLICATE" />} />
|
<Route path="/duplicate/:dataId" element={<FormPage purpose={purpose} formPageType="DUPLICATE" />} />
|
||||||
<Route path="/create" element={<FormPage formPageType="CREATE" />} />
|
<Route path="/create" element={<FormPage purpose={purpose} formPageType="CREATE" />} />
|
||||||
<Route path="/" element={<Navigate to={`${employeesModuleConfig.webUrl}/index`} replace={true} />} />
|
<Route path="/" element={<Navigate to={`${config.webUrl}/index`} replace={true} />} />
|
||||||
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</EnterpriseModuleProvider>
|
</EnterpriseModuleProvider>
|
||||||
|
|||||||
+6
-4
@@ -1,9 +1,11 @@
|
|||||||
import { EnterpriseDetailPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
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';
|
import { DetailGeneral } from '../components/detail-component/detail-general';
|
||||||
|
|
||||||
export default function EmployeePageDetail() {
|
export default function EmployeePageDetail({ purpose }: { purpose: FieldPurpose }) {
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
const moduleConfig = createEmployeeModuleConfig(purpose);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnterpriseDetailPageProvider
|
<EnterpriseDetailPageProvider
|
||||||
@@ -12,8 +14,8 @@ export default function EmployeePageDetail() {
|
|||||||
title: t('detail_page_title'),
|
title: t('detail_page_title'),
|
||||||
description: t('detail_page_description'),
|
description: t('detail_page_description'),
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{ label: t('nav:configuration'), type: 'text' },
|
{ label: t(`nav:${purpose}`), type: 'text' },
|
||||||
{ label: t('nav:configuration-employees'), type: 'link', href: `${employeesModuleConfig.webUrl}/index` },
|
{ label: t('nav:configuration-employees'), type: 'link', href: `${moduleConfig.webUrl}/index` },
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+20
-7
@@ -2,12 +2,22 @@ import { useMemo } from 'react';
|
|||||||
import { useEnterpriseModuleTranslationContext, EnterpriseFormPageProvider, FormPageType } from '@repo/ui/foundations';
|
import { useEnterpriseModuleTranslationContext, EnterpriseFormPageProvider, FormPageType } from '@repo/ui/foundations';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
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 { createEmployeeSchema } from '../../domain/validators/employee.validator';
|
||||||
import { FormGeneral } from '../components/form-component/form-general';
|
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 { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
const moduleConfig = createEmployeeModuleConfig(purpose);
|
||||||
|
const positions = employeePositionsForPurpose(purpose);
|
||||||
|
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
if (formPageType === 'CREATE') {
|
if (formPageType === 'CREATE') {
|
||||||
@@ -22,8 +32,11 @@ export default function EmployeePageForm({ formPageType }: { formPageType: FormP
|
|||||||
return { title: '', description: '' };
|
return { title: '', description: '' };
|
||||||
}, [formPageType, t]);
|
}, [formPageType, t]);
|
||||||
|
|
||||||
const validator = useMemo(() => createEmployeeSchema(t), [t]);
|
const validator = useMemo(() => createEmployeeSchema(t, positions), [positions, t]);
|
||||||
const formControl = useForm({ resolver: zodResolver(validator) });
|
const formControl = useForm({
|
||||||
|
resolver: zodResolver(validator),
|
||||||
|
defaultValues: positions.length === 1 ? { position: positions[0] } : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnterpriseFormPageProvider
|
<EnterpriseFormPageProvider
|
||||||
@@ -36,12 +49,12 @@ export default function EmployeePageForm({ formPageType }: { formPageType: FormP
|
|||||||
title: title?.title,
|
title: title?.title,
|
||||||
description: title?.description,
|
description: title?.description,
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{ label: t('nav:configuration'), type: 'text' },
|
{ label: t(`nav:${purpose}`), type: 'text' },
|
||||||
{ label: t('nav:configuration-employees'), type: 'link', href: `${employeesModuleConfig.webUrl}/index` },
|
{ label: t('nav:configuration-employees'), type: 'link', href: `${moduleConfig.webUrl}/index` },
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormGeneral />
|
<FormGeneral positions={positions} />
|
||||||
</EnterpriseFormPageProvider>
|
</EnterpriseFormPageProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-5
@@ -7,11 +7,13 @@ import {
|
|||||||
import { ColDef, Text } from '@repo/ui/components';
|
import { ColDef, Text } from '@repo/ui/components';
|
||||||
import { Trans } from '@repo/core-i18n';
|
import { Trans } from '@repo/core-i18n';
|
||||||
import { Users } from 'lucide-react';
|
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 { 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 { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
const positions = employeePositionsForPurpose(purpose);
|
||||||
|
|
||||||
const columnDefs: ColDef<EmployeeEntity>[] = useMemo(() => {
|
const columnDefs: ColDef<EmployeeEntity>[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
@@ -31,10 +33,10 @@ export default function EmployeePageIndex() {
|
|||||||
return {
|
return {
|
||||||
renderBody: (form: any) => {
|
renderBody: (form: any) => {
|
||||||
if (!form) return null;
|
if (!form) return null;
|
||||||
return <FilterFormContent form={form} t={t} />;
|
return <FilterFormContent form={form} t={t} positions={positions} />;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [t]);
|
}, [positions, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnterpriseIndexPageProvider
|
<EnterpriseIndexPageProvider
|
||||||
@@ -45,7 +47,7 @@ export default function EmployeePageIndex() {
|
|||||||
),
|
),
|
||||||
icon: Users,
|
icon: Users,
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{ label: t('nav:configuration'), type: 'text' },
|
{ label: t(`nav:${purpose}`), type: 'text' },
|
||||||
{ label: t('nav:configuration-employees'), type: 'text' },
|
{ label: t('nav:configuration-employees'), type: 'text' },
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
|
|||||||
+16
-15
@@ -4,19 +4,20 @@ import { EmployeeEntity } from '../../domain/entities';
|
|||||||
|
|
||||||
export interface EmployeesStoreState extends EnterpriseModuleState<EmployeeEntity> {}
|
export interface EmployeesStoreState extends EnterpriseModuleState<EmployeeEntity> {}
|
||||||
|
|
||||||
export const employeesStore = create<EmployeesStoreState>((set) => ({
|
export function createEmployeesStore() {
|
||||||
metaData: { limit: 15 },
|
return create<EmployeesStoreState>((set) => ({
|
||||||
setMetaData: (data) => set({ metaData: data }),
|
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: {},
|
export const salesEmployeesStore = createEmployeesStore();
|
||||||
setFilterData: (data) => set({ filterData: data }),
|
export const logisticsEmployeesStore = createEmployeesStore();
|
||||||
|
|
||||||
selectedRows: [],
|
|
||||||
setSelectedRows: (rows) => set({ selectedRows: rows }),
|
|
||||||
|
|
||||||
privileges: [],
|
|
||||||
setPrivileges: (privileges) => set({ privileges }),
|
|
||||||
|
|
||||||
tableConfig: null,
|
|
||||||
setTableConfig: (config) => set({ tableConfig: config }),
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
import { WEB_URL } from '../../../../core/constants/web-url';
|
||||||
|
|
||||||
const DivisionsModule = lazy(() => import('./divisions/presentation/factory'));
|
const DivisionsModule = lazy(() => import('./divisions/presentation/factory'));
|
||||||
const BranchesModule = lazy(() => import('./branches/presentation/factory'));
|
const BranchesModule = lazy(() => import('./branches/presentation/factory'));
|
||||||
const CustomersModule = lazy(() => import('./customers/presentation/factory'));
|
const CustomersModule = lazy(() => import('./customers/presentation/factory'));
|
||||||
const EmployeesModule = lazy(() => import('./employees/presentation/factory'));
|
|
||||||
const ProductsModule = lazy(() => import('./products/presentation/factory'));
|
const ProductsModule = lazy(() => import('./products/presentation/factory'));
|
||||||
|
|
||||||
export default function ConfigurationModule() {
|
export default function ConfigurationModule() {
|
||||||
@@ -13,8 +13,8 @@ export default function ConfigurationModule() {
|
|||||||
<Route path="/divisions/*" element={<DivisionsModule />} />
|
<Route path="/divisions/*" element={<DivisionsModule />} />
|
||||||
<Route path="/branches/*" element={<BranchesModule />} />
|
<Route path="/branches/*" element={<BranchesModule />} />
|
||||||
<Route path="/customers/*" element={<CustomersModule />} />
|
<Route path="/customers/*" element={<CustomersModule />} />
|
||||||
<Route path="/employees/*" element={<EmployeesModule />} />
|
|
||||||
<Route path="/products/*" element={<ProductsModule />} />
|
<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} />} />
|
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|||||||
+10
-3
@@ -1,12 +1,19 @@
|
|||||||
import { Box, FieldAsyncSelect, FieldNumberInput, Paper, SimpleGrid, Text } from '@repo/ui/components';
|
import { Box, FieldAsyncSelect, FieldNumberInput, Paper, SimpleGrid, Text } from '@repo/ui/components';
|
||||||
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
|
import {
|
||||||
import { loadEmployeeOptions } from '../../../../shared/load-employee-options';
|
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 { relationLabel } from '../../../../shared/relation-label';
|
||||||
import type { EmployeeEntity } from '../../../../../configuration/employees/domain/entities';
|
import type { EmployeeEntity } from '../../../../../configuration/employees/domain/entities';
|
||||||
|
|
||||||
export function FormGeneral() {
|
export function FormGeneral() {
|
||||||
const { formControl } = useFormPageContext();
|
const { formControl } = useFormPageContext();
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
const { config } = useEnterpriseModuleConfigContext();
|
||||||
|
const purpose = purposeFromModuleKey(config.moduleKey);
|
||||||
const employee = formControl.watch('employee') as EmployeeEntity | null | undefined;
|
const employee = formControl.watch('employee') as EmployeeEntity | null | undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +32,7 @@ export function FormGeneral() {
|
|||||||
labelKey="name"
|
labelKey="name"
|
||||||
required
|
required
|
||||||
searchable
|
searchable
|
||||||
loadOptions={loadEmployeeOptions}
|
loadOptions={purpose === 'sales' ? loadSalesEmployeeOptions : loadLogisticsEmployeeOptions}
|
||||||
defaultOptions={employee ? [employee] : []}
|
defaultOptions={employee ? [employee] : []}
|
||||||
renderLabel={relationLabel}
|
renderLabel={relationLabel}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+7
-2
@@ -1,12 +1,17 @@
|
|||||||
import { SimpleGrid } from '@repo/ui/components';
|
import { SimpleGrid } from '@repo/ui/components';
|
||||||
import { FieldAsyncSelect, FieldNumberInput, FieldSelect } from '@repo/ui/form';
|
import { FieldAsyncSelect, FieldNumberInput, FieldSelect } from '@repo/ui/form';
|
||||||
import { UseFormReturn } from 'react-hook-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 { 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 { relationLabel } from '../../../../shared/relation-label';
|
||||||
import type { EmployeeEntity } from '../../../../../configuration/employees/domain/entities';
|
import type { EmployeeEntity } from '../../../../../configuration/employees/domain/entities';
|
||||||
|
|
||||||
export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (key: string) => string }) => {
|
export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (key: string) => string }) => {
|
||||||
|
const { config } = useEnterpriseModuleConfigContext();
|
||||||
|
const purpose = purposeFromModuleKey(config.moduleKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||||
<FieldAsyncSelect<EmployeeEntity>
|
<FieldAsyncSelect<EmployeeEntity>
|
||||||
@@ -18,7 +23,7 @@ export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (k
|
|||||||
labelKey="name"
|
labelKey="name"
|
||||||
clearable
|
clearable
|
||||||
searchable
|
searchable
|
||||||
loadOptions={loadEmployeeOptions}
|
loadOptions={purpose === 'sales' ? loadSalesEmployeeOptions : loadLogisticsEmployeeOptions}
|
||||||
renderLabel={relationLabel}
|
renderLabel={relationLabel}
|
||||||
/>
|
/>
|
||||||
<FieldNumberInput
|
<FieldNumberInput
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { lazy } from 'react';
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { EmbeddedComingSoonPage } from '../../../../../core/components/coming-soon-page';
|
import { EmbeddedComingSoonPage } from '../../../../../core/components/coming-soon-page';
|
||||||
|
|
||||||
|
const EmployeesModule = lazy(() => import('../../configuration/employees/presentation/factory'));
|
||||||
const CyclesModule = lazy(() => import('../cycles/presentation/factory'));
|
const CyclesModule = lazy(() => import('../cycles/presentation/factory'));
|
||||||
const PlansModule = lazy(() => import('../plans/presentation/factory'));
|
const PlansModule = lazy(() => import('../plans/presentation/factory'));
|
||||||
|
|
||||||
export default function LogisticsFieldModule() {
|
export default function LogisticsFieldModule() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="/employees/*" element={<EmployeesModule purpose="logistics" />} />
|
||||||
<Route path="/cycles/*" element={<CyclesModule purpose="logistics" />} />
|
<Route path="/cycles/*" element={<CyclesModule purpose="logistics" />} />
|
||||||
<Route path="/plans/*" element={<PlansModule purpose="logistics" />} />
|
<Route path="/plans/*" element={<PlansModule purpose="logistics" />} />
|
||||||
<Route path="/packing-slips" element={<EmbeddedComingSoonPage />} />
|
<Route path="/packing-slips" element={<EmbeddedComingSoonPage />} />
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ import {
|
|||||||
useEnterpriseModuleConfigContext,
|
useEnterpriseModuleConfigContext,
|
||||||
} from '@repo/ui/foundations';
|
} from '@repo/ui/foundations';
|
||||||
import { purposeFromModuleKey } from '../../../../../../../../core/domain/field-purpose';
|
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 { loadBranchOptions } from '../../../../shared/load-branch-options';
|
||||||
import { loadCustomerOptions } from '../../../../shared/load-customer-options';
|
import { loadCustomerOptions } from '../../../../shared/load-customer-options';
|
||||||
import { loadSalesInvoiceOptions, loadPackingSlipOptions } from '../../../../shared/lookup.factories';
|
import { loadSalesInvoiceOptions, loadPackingSlipOptions } from '../../../../shared/lookup.factories';
|
||||||
@@ -42,7 +42,7 @@ export function FormGeneral() {
|
|||||||
labelKey="name"
|
labelKey="name"
|
||||||
required
|
required
|
||||||
searchable
|
searchable
|
||||||
loadOptions={loadEmployeeOptions}
|
loadOptions={purpose === 'sales' ? loadSalesEmployeeOptions : loadLogisticsEmployeeOptions}
|
||||||
defaultOptions={employee ? [employee] : []}
|
defaultOptions={employee ? [employee] : []}
|
||||||
renderLabel={relationLabel}
|
renderLabel={relationLabel}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+7
-2
@@ -1,12 +1,17 @@
|
|||||||
import { SimpleGrid } from '@repo/ui/components';
|
import { SimpleGrid } from '@repo/ui/components';
|
||||||
import { FieldAsyncSelect, FieldDatePicker, FieldSelect } from '@repo/ui/form';
|
import { FieldAsyncSelect, FieldDatePicker, FieldSelect } from '@repo/ui/form';
|
||||||
import { UseFormReturn } from 'react-hook-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 { 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 { relationLabel } from '../../../../shared/relation-label';
|
||||||
import type { EmployeeEntity } from '../../../../../configuration/employees/domain/entities';
|
import type { EmployeeEntity } from '../../../../../configuration/employees/domain/entities';
|
||||||
|
|
||||||
export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (key: string) => string }) => {
|
export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (key: string) => string }) => {
|
||||||
|
const { config } = useEnterpriseModuleConfigContext();
|
||||||
|
const purpose = purposeFromModuleKey(config.moduleKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||||
<FieldAsyncSelect<EmployeeEntity>
|
<FieldAsyncSelect<EmployeeEntity>
|
||||||
@@ -17,7 +22,7 @@ export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (k
|
|||||||
labelKey="name"
|
labelKey="name"
|
||||||
clearable
|
clearable
|
||||||
searchable
|
searchable
|
||||||
loadOptions={loadEmployeeOptions}
|
loadOptions={purpose === 'sales' ? loadSalesEmployeeOptions : loadLogisticsEmployeeOptions}
|
||||||
renderLabel={relationLabel}
|
renderLabel={relationLabel}
|
||||||
/>
|
/>
|
||||||
<FieldDatePicker control={form.control} name="date" label={t('common:fields.date')} clearable />
|
<FieldDatePicker control={form.control} name="date" label={t('common:fields.date')} clearable />
|
||||||
|
|||||||
+10
-3
@@ -2,8 +2,13 @@ import { useMemo } from 'react';
|
|||||||
import { Button, FieldAsyncSelect, FieldDatePicker, Group, Modal, Stack, notifications } from '@repo/ui/components';
|
import { Button, FieldAsyncSelect, FieldDatePicker, Group, Modal, Stack, notifications } from '@repo/ui/components';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useEnterpriseModuleTranslationContext, useEnterpriseModuleDataServiceContext } from '@repo/ui/foundations';
|
import {
|
||||||
import { loadEmployeeOptions } from '../../../../shared/load-employee-options';
|
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 { relationLabel } from '../../../../shared/relation-label';
|
||||||
import { createGeneratePlansSchema } from '../../../domain/validators/plan.validator';
|
import { createGeneratePlansSchema } from '../../../domain/validators/plan.validator';
|
||||||
import type { PlansRemoteDataServices } from '../../../data/plan.remote.service';
|
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 }) {
|
export function GeneratePlansModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
const { config } = useEnterpriseModuleConfigContext();
|
||||||
|
const purpose = purposeFromModuleKey(config.moduleKey);
|
||||||
const { dataServices } = useEnterpriseModuleDataServiceContext<PlanEntity, PlansRemoteDataServices>();
|
const { dataServices } = useEnterpriseModuleDataServiceContext<PlanEntity, PlansRemoteDataServices>();
|
||||||
const validator = useMemo(() => createGeneratePlansSchema(t), [t]);
|
const validator = useMemo(() => createGeneratePlansSchema(t), [t]);
|
||||||
const form = useForm({ resolver: zodResolver(validator) });
|
const form = useForm({ resolver: zodResolver(validator) });
|
||||||
@@ -45,7 +52,7 @@ export function GeneratePlansModal({ opened, onClose }: { opened: boolean; onClo
|
|||||||
labelKey="name"
|
labelKey="name"
|
||||||
required
|
required
|
||||||
searchable
|
searchable
|
||||||
loadOptions={loadEmployeeOptions}
|
loadOptions={purpose === 'sales' ? loadSalesEmployeeOptions : loadLogisticsEmployeeOptions}
|
||||||
renderLabel={relationLabel}
|
renderLabel={relationLabel}
|
||||||
/>
|
/>
|
||||||
<FieldDatePicker control={form.control as any} name="from" label={t('common:fields.from')} required />
|
<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 type { EmployeeEntity } from '../../configuration/employees/domain/entities';
|
||||||
import { createOptionLoader } from './create-option-loader';
|
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 RequestsModule = lazy(() => import('./requests/presentation/factory'));
|
||||||
const OrdersModule = lazy(() => import('./orders/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 CyclesModule = lazy(() => import('../field/cycles/presentation/factory'));
|
||||||
const PlansModule = lazy(() => import('../field/plans/presentation/factory'));
|
const PlansModule = lazy(() => import('../field/plans/presentation/factory'));
|
||||||
|
|
||||||
export default function SalesModule() {
|
export default function SalesModule() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="/employees/*" element={<EmployeesModule purpose="sales" />} />
|
||||||
<Route path="/requests/*" element={<RequestsModule />} />
|
<Route path="/requests/*" element={<RequestsModule />} />
|
||||||
<Route path="/orders/*" element={<OrdersModule />} />
|
<Route path="/orders/*" element={<OrdersModule />} />
|
||||||
<Route path="/cycles/*" element={<CyclesModule purpose="sales" />} />
|
<Route path="/cycles/*" element={<CyclesModule purpose="sales" />} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SimpleGrid } from '@repo/ui/components';
|
import { SimpleGrid } from '@repo/ui/components';
|
||||||
import { FieldAsyncSelect, FieldSelect, FieldTextInput } from '@repo/ui/form';
|
import { FieldAsyncSelect, FieldSelect, FieldTextInput } from '@repo/ui/form';
|
||||||
import { UseFormReturn } from 'react-hook-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 { loadBranchOptions } from '../../field/shared/load-branch-options';
|
||||||
import { loadCustomerOptions } from '../../field/shared/load-customer-options';
|
import { loadCustomerOptions } from '../../field/shared/load-customer-options';
|
||||||
import { loadDivisionOptions } from '../../configuration/shared/load-division-options';
|
import { loadDivisionOptions } from '../../configuration/shared/load-division-options';
|
||||||
@@ -50,7 +50,7 @@ export function SalesFilterFormContent({
|
|||||||
labelKey="name"
|
labelKey="name"
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
loadOptions={loadEmployeeOptions}
|
loadOptions={loadSalesEmployeeOptions}
|
||||||
renderLabel={relationLabel}
|
renderLabel={relationLabel}
|
||||||
/>
|
/>
|
||||||
<FieldAsyncSelect<BranchEntity>
|
<FieldAsyncSelect<BranchEntity>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box, FieldAsyncSelect, FieldDatePicker, FieldTextInput, Paper, SimpleGrid, Text } from '@repo/ui/components';
|
import { Box, FieldAsyncSelect, FieldDatePicker, FieldTextInput, Paper, SimpleGrid, Text } from '@repo/ui/components';
|
||||||
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
|
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 { loadBranchOptions } from '../../field/shared/load-branch-options';
|
||||||
import { loadCustomerOptions } from '../../field/shared/load-customer-options';
|
import { loadCustomerOptions } from '../../field/shared/load-customer-options';
|
||||||
import { loadDivisionOptions } from '../../configuration/shared/load-division-options';
|
import { loadDivisionOptions } from '../../configuration/shared/load-division-options';
|
||||||
@@ -47,7 +47,7 @@ export function FormGeneral() {
|
|||||||
labelKey="name"
|
labelKey="name"
|
||||||
required
|
required
|
||||||
searchable
|
searchable
|
||||||
loadOptions={loadEmployeeOptions}
|
loadOptions={loadSalesEmployeeOptions}
|
||||||
defaultOptions={salesPerson ? [salesPerson] : []}
|
defaultOptions={salesPerson ? [salesPerson] : []}
|
||||||
renderLabel={relationLabel}
|
renderLabel={relationLabel}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ export const WEB_URL = {
|
|||||||
DIVISIONS: '/app/configuration/divisions',
|
DIVISIONS: '/app/configuration/divisions',
|
||||||
BRANCHES: '/app/configuration/branches',
|
BRANCHES: '/app/configuration/branches',
|
||||||
CUSTOMERS: '/app/configuration/customers',
|
CUSTOMERS: '/app/configuration/customers',
|
||||||
EMPLOYEES: '/app/configuration/employees',
|
SALES_EMPLOYEES: '/app/sales/employees',
|
||||||
|
LOGISTICS_EMPLOYEES: '/app/logistics/employees',
|
||||||
PRODUCTS: '/app/configuration/products',
|
PRODUCTS: '/app/configuration/products',
|
||||||
SALES_REQUESTS: '/app/sales/requests',
|
SALES_REQUESTS: '/app/sales/requests',
|
||||||
SALES_ORDERS: '/app/sales/orders',
|
SALES_ORDERS: '/app/sales/orders',
|
||||||
|
|||||||
Reference in New Issue
Block a user