- Implemented Person entity with validation and update methods. - Created CalendarDate value object for date validation. - Developed migration script to transition company_contacts to person and department structures. - Established Sequelize models for departments and assignments. - Introduced request DTOs for person and department management. - Added shared DTO schemas for validation. - Built assignment and department forms with validation and submission handling. - Created people section to manage and display company personnel. - Implemented person form for adding and editing personnel details.
286 lines
9.5 KiB
TypeScript
286 lines
9.5 KiB
TypeScript
import type { Name, UniqueID } from "@repo/rdx-ddd";
|
|
import { AggregateRoot } from "@repo/rdx-ddd";
|
|
import { Result } from "@repo/rdx-utils";
|
|
|
|
import { CompanyError } from "./company-error";
|
|
import type { Department, DepartmentProps } from "./department.entity";
|
|
import type { AssignmentValues } from "./department-assignment.entity";
|
|
import { DepartmentAssignment } from "./department-assignment.entity";
|
|
import type { Person, PersonProps } from "./person.entity";
|
|
import type { Site, SiteProps } from "./site.entity";
|
|
|
|
export type CompanyProps = Readonly<{
|
|
name: Name;
|
|
active: boolean;
|
|
people: readonly Person[];
|
|
departments: readonly Department[];
|
|
assignments: readonly DepartmentAssignment[];
|
|
sites: readonly Site[];
|
|
}>;
|
|
export class Company extends AggregateRoot<CompanyProps> {
|
|
protected constructor(props: CompanyProps, id?: UniqueID) {
|
|
super(
|
|
{
|
|
...props,
|
|
people: Object.freeze([...props.people]),
|
|
departments: Object.freeze([...props.departments]),
|
|
assignments: Object.freeze([...props.assignments]),
|
|
sites: Object.freeze([...props.sites]),
|
|
},
|
|
id
|
|
);
|
|
}
|
|
static create(props: Pick<CompanyProps, "name" | "active">): Result<Company, CompanyError> {
|
|
if (props.name.toPrimitive().length > 100) {
|
|
return Result.fail(new CompanyError("INVALID_REQUEST"));
|
|
}
|
|
return Result.ok(
|
|
new Company({ ...props, people: [], departments: [], assignments: [], sites: [] })
|
|
);
|
|
}
|
|
static rehydrate(props: CompanyProps, id: UniqueID): Company {
|
|
return new Company(props, id);
|
|
}
|
|
get name() {
|
|
return this.props.name;
|
|
}
|
|
get active() {
|
|
return this.props.active;
|
|
}
|
|
get people() {
|
|
return this.props.people;
|
|
}
|
|
get contacts() {
|
|
return this.people.filter((person) => person.isContact);
|
|
}
|
|
get departments() {
|
|
return this.props.departments;
|
|
}
|
|
get assignments() {
|
|
return this.props.assignments;
|
|
}
|
|
get sites() {
|
|
return this.props.sites;
|
|
}
|
|
update(values: Pick<CompanyProps, "name" | "active">): Result<Company, CompanyError> {
|
|
if (values.name.toPrimitive().length > 100) {
|
|
return Result.fail(new CompanyError("INVALID_REQUEST"));
|
|
}
|
|
return this.name.equals(values.name) && this.active === values.active
|
|
? Result.ok(this)
|
|
: Result.ok(new Company({ ...this.props, ...values }, this.id));
|
|
}
|
|
addPerson(contact: Person): Result<Company, CompanyError> {
|
|
if (!contact.companyId.equals(this.id)) {
|
|
return Result.fail(new CompanyError("INVALID_REQUEST"));
|
|
}
|
|
if (this.people.some((existing) => existing.id.equals(contact.id))) {
|
|
return Result.ok(this);
|
|
}
|
|
return Result.ok(new Company({ ...this.props, people: [...this.people, contact] }, this.id));
|
|
}
|
|
updatePerson(
|
|
id: UniqueID,
|
|
values: Omit<PersonProps, "companyId">
|
|
): Result<Company, CompanyError> {
|
|
const current = this.people.find((contact) => contact.id.equals(id));
|
|
if (!current) {
|
|
return Result.fail(new CompanyError("PERSON_NOT_FOUND"));
|
|
}
|
|
const updated = current.update(values);
|
|
if (!updated.isSuccess) {
|
|
return updated;
|
|
}
|
|
return updated.data === current
|
|
? Result.ok(this)
|
|
: Result.ok(
|
|
new Company(
|
|
{
|
|
...this.props,
|
|
people: this.people.map((contact) =>
|
|
contact.id.equals(id) ? updated.data : contact
|
|
),
|
|
},
|
|
this.id
|
|
)
|
|
);
|
|
}
|
|
private departmentNameExists(name: string, except?: UniqueID): boolean {
|
|
const key = name.normalize("NFC").toLowerCase();
|
|
return this.departments.some(
|
|
(department) =>
|
|
!department.id.equals(except) &&
|
|
department.name.toPrimitive().normalize("NFC").toLowerCase() === key
|
|
);
|
|
}
|
|
addDepartment(department: Department): Result<Company, CompanyError> {
|
|
if (!department.companyId.equals(this.id)) {
|
|
return Result.fail(new CompanyError("INVALID_REQUEST"));
|
|
}
|
|
if (this.departments.some((current) => current.id.equals(department.id))) {
|
|
return Result.ok(this);
|
|
}
|
|
if (this.departmentNameExists(department.name.toPrimitive())) {
|
|
return Result.fail(new CompanyError("DEPARTMENT_NAME_EXISTS"));
|
|
}
|
|
return Result.ok(
|
|
new Company({ ...this.props, departments: [...this.departments, department] }, this.id)
|
|
);
|
|
}
|
|
updateDepartment(
|
|
id: UniqueID,
|
|
values: Omit<DepartmentProps, "companyId">
|
|
): Result<Company, CompanyError> {
|
|
const current = this.departments.find((department) => department.id.equals(id));
|
|
if (!current) {
|
|
return Result.fail(new CompanyError("DEPARTMENT_NOT_FOUND"));
|
|
}
|
|
if (this.departmentNameExists(values.name.toPrimitive(), id)) {
|
|
return Result.fail(new CompanyError("DEPARTMENT_NAME_EXISTS"));
|
|
}
|
|
const changed = current.update(values);
|
|
if (!changed.isSuccess) {
|
|
return changed;
|
|
}
|
|
return changed.data === current
|
|
? Result.ok(this)
|
|
: Result.ok(
|
|
new Company(
|
|
{
|
|
...this.props,
|
|
departments: this.departments.map((department) =>
|
|
department.id.equals(id) ? changed.data : department
|
|
),
|
|
},
|
|
this.id
|
|
)
|
|
);
|
|
}
|
|
private validateAssignment(
|
|
candidate: DepartmentAssignment,
|
|
previous?: DepartmentAssignment
|
|
): Result<DepartmentAssignment, CompanyError> {
|
|
const person = this.people.find((item) => item.id.equals(candidate.personId));
|
|
const department = this.departments.find((item) => item.id.equals(candidate.departmentId));
|
|
if (!person) {
|
|
return Result.fail(new CompanyError("PERSON_NOT_FOUND"));
|
|
}
|
|
if (!department) {
|
|
return Result.fail(new CompanyError("DEPARTMENT_NOT_FOUND"));
|
|
}
|
|
const end = candidate.values.endDate.getOrUndefined();
|
|
// Una baja o corrección histórica sigue permitida tras desactivar la persona o el departamento.
|
|
const createsOrReopens =
|
|
!previous || (!end && previous.values.endDate.getOrUndefined() !== undefined);
|
|
if (createsOrReopens && !(person.active && department.active)) {
|
|
return Result.fail(new CompanyError("INACTIVE_ASSIGNMENT_TARGET"));
|
|
}
|
|
const conflict = this.assignments.some((other) => {
|
|
if (other.id.equals(candidate.id) || !other.personId.equals(candidate.personId)) {
|
|
return false;
|
|
}
|
|
const otherEnd = other.values.endDate.getOrUndefined();
|
|
if (!(end || otherEnd) && candidate.values.isPrimary && other.values.isPrimary) {
|
|
return true;
|
|
}
|
|
if (!other.departmentId.equals(candidate.departmentId)) {
|
|
return false;
|
|
}
|
|
if (!(end || otherEnd)) {
|
|
return true;
|
|
}
|
|
// Periodos semiabiertos: se puede cerrar un puesto e iniciar otro el mismo día.
|
|
return (
|
|
(!otherEnd || candidate.values.startDate.toPrimitive() < otherEnd.toPrimitive()) &&
|
|
(!end || other.values.startDate.toPrimitive() < end.toPrimitive())
|
|
);
|
|
});
|
|
return conflict ? Result.fail(new CompanyError("ASSIGNMENT_CONFLICT")) : Result.ok(candidate);
|
|
}
|
|
addAssignment(
|
|
personId: UniqueID,
|
|
departmentId: UniqueID,
|
|
values: AssignmentValues,
|
|
recordedAt: string
|
|
): Result<Company, CompanyError> {
|
|
const created = DepartmentAssignment.create(
|
|
{ ...values, companyId: this.id, personId, departmentId },
|
|
recordedAt
|
|
);
|
|
if (!created.isSuccess) {
|
|
return created;
|
|
}
|
|
const validated = this.validateAssignment(created.data);
|
|
if (!validated.isSuccess) {
|
|
return validated;
|
|
}
|
|
return Result.ok(
|
|
new Company({ ...this.props, assignments: [...this.assignments, created.data] }, this.id)
|
|
);
|
|
}
|
|
updateAssignment(
|
|
personId: UniqueID,
|
|
id: UniqueID,
|
|
values: AssignmentValues,
|
|
recordedAt: string
|
|
): Result<Company, CompanyError> {
|
|
const current = this.assignments.find(
|
|
(item) => item.id.equals(id) && item.personId.equals(personId)
|
|
);
|
|
if (!current) {
|
|
return Result.fail(new CompanyError("ASSIGNMENT_NOT_FOUND"));
|
|
}
|
|
const changed = current.update(values, recordedAt);
|
|
if (!changed.isSuccess) {
|
|
return changed;
|
|
}
|
|
const validated = this.validateAssignment(changed.data, current);
|
|
if (!validated.isSuccess) {
|
|
return validated;
|
|
}
|
|
return changed.data === current
|
|
? Result.ok(this)
|
|
: Result.ok(
|
|
new Company(
|
|
{
|
|
...this.props,
|
|
assignments: this.assignments.map((item) =>
|
|
item.id.equals(id) ? changed.data : item
|
|
),
|
|
},
|
|
this.id
|
|
)
|
|
);
|
|
}
|
|
addSite(site: Site): Result<Company, CompanyError> {
|
|
if (!site.companyId.equals(this.id)) {
|
|
return Result.fail(new CompanyError("INVALID_REQUEST"));
|
|
}
|
|
if (this.sites.some((existing) => existing.id.equals(site.id))) {
|
|
return Result.ok(this);
|
|
}
|
|
return Result.ok(new Company({ ...this.props, sites: [...this.sites, site] }, this.id));
|
|
}
|
|
updateSite(id: UniqueID, values: Omit<SiteProps, "companyId">): Result<Company, CompanyError> {
|
|
const current = this.sites.find((site) => site.id.equals(id));
|
|
if (!current) {
|
|
return Result.fail(new CompanyError("SITE_NOT_FOUND"));
|
|
}
|
|
const updated = current.update(values);
|
|
if (!updated.isSuccess) {
|
|
return updated;
|
|
}
|
|
return updated.data === current
|
|
? Result.ok(this)
|
|
: Result.ok(
|
|
new Company(
|
|
{
|
|
...this.props,
|
|
sites: this.sites.map((site) => (site.id.equals(id) ? updated.data : site)),
|
|
},
|
|
this.id
|
|
)
|
|
);
|
|
}
|
|
}
|