Add user and employee management enhancements with database schema updates

- Introduced new columns `status`, `created_by`, and `updated_by` in the `users` table to track user status and ownership.
- Updated the `employees` table to include a foreign key reference to the `users` table via `user_id`.
- Created migration script `0012_users_primary.sql` to apply these changes to the database schema.
- Enhanced the `EmployeesService` and `EmployeesRepository` to support user assignments and related data retrieval.
- Updated DTOs and service methods to reflect the new user and employee relationships.
- Added unit tests to validate the new functionality and ensure data integrity.
- Modified existing controllers to accommodate the new fields and relationships in user and employee management.
This commit is contained in:
shancheas
2026-08-26 15:29:18 +07:00
parent f635ebeda0
commit 8a61c94078
50 changed files with 2175 additions and 401 deletions
@@ -20,6 +20,7 @@ import {
parseCsvRecord,
type EmployeePosition,
} from './employee-fields';
import { UsersService } from '../../users/users.service';
import { EmployeesRepository } from './employees.repository';
export type ListEmployeesQuery = {
@@ -28,6 +29,7 @@ export type ListEmployeesQuery = {
readonly phone?: string;
readonly position?: string;
readonly status?: string;
readonly userId?: string;
readonly search?: string;
readonly page?: number;
readonly limit?: number;
@@ -45,13 +47,17 @@ const VISIBLE_FIELDS = [
'updatedAt',
'createdBy',
'updatedBy',
'user',
] as const;
const CSV_REQUIRED_HEADERS = ['code', 'name', 'phone', 'position'] as const;
@Injectable()
export class EmployeesService {
constructor(private readonly employeesRepository: EmployeesRepository) {}
constructor(
private readonly employeesRepository: EmployeesRepository,
private readonly usersService: UsersService,
) {}
async list(
query: ListEmployeesQuery,
@@ -63,6 +69,7 @@ export class EmployeesService {
phone: query.phone,
position: query.position,
status: query.status,
userId: query.userId,
search: query.search,
limit: page.limit,
offset: page.offset,
@@ -100,9 +107,10 @@ export class EmployeesService {
position: string;
status?: string;
userId: string;
assignedUserId?: string | null;
}): Promise<ReturnType<EmployeesService['toListItem']>> {
const created = await this.employeesRepository.create(
this.toCreateInput(input),
await this.toCreateInput(input),
);
return this.toListItem(created);
}
@@ -116,6 +124,7 @@ export class EmployeesService {
position?: string;
status?: unknown;
userId: string;
assignedUserId?: string | null;
},
): Promise<ReturnType<EmployeesService['toListItem']>> {
if (input.status !== undefined) {
@@ -131,6 +140,7 @@ export class EmployeesService {
? this.assertPosition(input.position)
: undefined,
userId: input.userId,
assignedUserId: await this.assertAssignedUserId(input.assignedUserId),
};
const updated = await this.employeesRepository.update(id, payload);
return this.toListItem(updated);
@@ -201,14 +211,16 @@ export class EmployeesService {
const rowNum = filled[i].lineNo;
try {
const statusRaw = idx('status') >= 0 ? cols[idx('status')] : '';
const assignedRaw = idx('userid') >= 0 ? cols[idx('userid')] : '';
rows.push(
this.toCreateInput({
await this.toCreateInput({
code: cols[idx('code')] ?? '',
name: cols[idx('name')] ?? '',
phone: cols[idx('phone')] ?? '',
position: cols[idx('position')] ?? '',
status: statusRaw || undefined,
userId,
assignedUserId: assignedRaw || undefined,
}),
);
} catch (error) {
@@ -241,6 +253,7 @@ export class EmployeesService {
updatedAt: employee.updatedAt.value,
createdBy: employee.createdBy,
updatedBy: employee.updatedBy,
user: employee.user,
};
}
@@ -248,14 +261,15 @@ export class EmployeesService {
return VISIBLE_FIELDS;
}
private toCreateInput(input: {
private async toCreateInput(input: {
code: string;
name: string;
phone: string;
position: string;
status?: string;
userId: string;
}): CreateEmployeeInput {
assignedUserId?: string | null;
}): Promise<CreateEmployeeInput> {
return {
code: this.assertCode(input.code),
name: this.assertName(input.name),
@@ -265,9 +279,26 @@ export class EmployeesService {
? Status.create(input.status)
: Status.create(Status.DEFAULT),
userId: input.userId,
assignedUserId: await this.assertAssignedUserId(input.assignedUserId),
};
}
private async assertAssignedUserId(
userId?: string | null,
): Promise<string | null | undefined> {
if (userId === undefined) {
return undefined;
}
if (userId === null || userId === '') {
return null;
}
const user = await this.usersService.findById(userId);
if (!user) {
throw new NotFoundException('User not found');
}
return userId;
}
private assertName(raw: string): string {
const name = raw.trim();
if (!isValidEmployeeName(name)) {