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
@@ -10,6 +10,7 @@ import { PhoneNumber } from '../../../common/value-objects/phone-number/phone-nu
import { Status } from '../../../common/value-objects/status/status';
import { DRIZZLE, type DrizzleDB } from '../../../database/database.module';
import { employees, type EmployeeRow } from '../../../database/employees-table';
import { users } from '../../../database/schema';
import type { EmployeePosition } from './employee-fields';
import type {
CreateEmployeeInput,
@@ -41,7 +42,11 @@ export class EmployeesRepository {
.offset(filters.offset);
return {
data: rows.map((row) => this.toDomain(row)),
data: await Promise.all(
rows.map(async (row) =>
this.toDomain(row, await this.loadAssignedUser(row.userId)),
),
),
total: Number(totalRow?.total ?? 0),
};
}
@@ -55,13 +60,13 @@ export class EmployeesRepository {
}
async findById(id: string): Promise<Employee | null> {
const rows: EmployeeRow[] = await this.db
const rows = await this.db
.select()
.from(employees)
.where(eq(employees.id, id))
.limit(1);
const row = rows[0];
return row ? this.toDomain(row) : null;
return row ? this.toDomain(row, await this.loadAssignedUser(row.userId)) : null;
}
async findByCode(code: string): Promise<Employee | null> {
@@ -71,7 +76,7 @@ export class EmployeesRepository {
.where(eq(employees.code, code))
.limit(1);
const row = rows[0];
return row ? this.toDomain(row) : null;
return row ? this.toDomain(row, await this.loadAssignedUser(row.userId)) : null;
}
async create(input: CreateEmployeeInput): Promise<Employee> {
@@ -83,7 +88,7 @@ export class EmployeesRepository {
.values(this.toInsertValues(input, status, now, input.userId))
.returning();
const row = inserted[0];
return this.toDomain(row);
return this.toDomain(row, await this.loadAssignedUser(row.userId));
} catch (error) {
this.rethrowUniqueViolation(error);
}
@@ -125,11 +130,14 @@ export class EmployeesRepository {
position: input.position ?? existing.position,
updatedAt: now.value,
updatedBy: input.userId,
...(input.assignedUserId !== undefined
? { userId: input.assignedUserId }
: {}),
})
.where(eq(employees.id, id))
.returning();
const row = updated[0];
return this.toDomain(row);
return this.toDomain(row, await this.loadAssignedUser(row.userId));
} catch (error) {
this.rethrowUniqueViolation(error);
}
@@ -154,7 +162,7 @@ export class EmployeesRepository {
if (!row) {
throw new NotFoundException('Employee not found');
}
return this.toDomain(row);
return this.toDomain(row, await this.loadAssignedUser(row.userId));
}
async bulkUpdateStatus(
@@ -216,6 +224,9 @@ export class EmployeesRepository {
if (filters.status) {
parts.push(eq(employees.status, filters.status));
}
if (filters.userId) {
parts.push(eq(employees.userId, filters.userId));
}
if (filters.search) {
const search = or(
ilike(employees.code, `%${filters.search}%`),
@@ -247,10 +258,41 @@ export class EmployeesRepository {
updatedAt: now.value,
createdBy: userId,
updatedBy: userId,
userId: input.assignedUserId ?? null,
};
}
private toDomain(row: EmployeeRow): Employee {
private selectWithUser() {
return this.db
.select({
employee: employees,
user: {
id: users.id,
username: users.username,
},
})
.from(employees)
.leftJoin(users, eq(employees.userId, users.id));
}
private async loadAssignedUser(
userId: string | null,
): Promise<{ id: string; username: string } | null> {
if (!userId) {
return null;
}
const rows = await this.db
.select({ id: users.id, username: users.username })
.from(users)
.where(eq(users.id, userId))
.limit(1);
return rows[0] ?? null;
}
private toDomain(
row: EmployeeRow,
user: { id: string; username: string } | null,
): Employee {
return {
id: row.id,
code: row.code,
@@ -262,29 +304,39 @@ export class EmployeesRepository {
updatedAt: DateTime.fromUnixMs(row.updatedAt),
createdBy: row.createdBy,
updatedBy: row.updatedBy,
userId: row.userId ?? null,
user: user?.id ? { id: user.id, username: user.username } : null,
};
}
private rethrowUniqueViolation(error: unknown): never {
const err = this.unwrapDbError(error);
if (err.code === '23505') {
const constraint = err.constraint ?? '';
if (constraint.includes('user_id')) {
throw new ConflictException('User is already assigned to an employee');
}
throw new ConflictException('Employee code already exists');
}
throw error;
}
private unwrapDbError(error: unknown): { code?: string } {
private unwrapDbError(error: unknown): { code?: string; constraint?: string } {
let current: unknown = error;
for (let i = 0; i < 5; i++) {
if (!current || typeof current !== 'object') {
break;
}
const obj = current as { code?: string; cause?: unknown };
const obj = current as {
code?: string;
constraint?: string;
cause?: unknown;
};
if (obj.code === '23505' || obj.code === '23503') {
return { code: obj.code };
return { code: obj.code, constraint: obj.constraint };
}
current = obj.cause;
}
return error as { code?: string };
return error as { code?: string; constraint?: string };
}
}