diff --git a/modules/companies/src/api/application/company.repository.ts b/modules/companies/src/api/application/company.repository.ts
index 9894a5a..bcfb711 100644
--- a/modules/companies/src/api/application/company.repository.ts
+++ b/modules/companies/src/api/application/company.repository.ts
@@ -12,4 +12,10 @@ export interface ICompanyRepository {
id: UniqueID,
change: (company: Company) => Result
): Promise>;
+ removePerson(id: UniqueID, personId: UniqueID): Promise>;
+ removeAssignment(
+ id: UniqueID,
+ personId: UniqueID,
+ assignmentId: UniqueID
+ ): Promise>;
}
diff --git a/modules/companies/src/api/application/company.use-cases.ts b/modules/companies/src/api/application/company.use-cases.ts
index a185ccd..dea64ab 100644
--- a/modules/companies/src/api/application/company.use-cases.ts
+++ b/modules/companies/src/api/application/company.use-cases.ts
@@ -97,6 +97,14 @@ export class CompanyUseCases {
company.updatePerson(child.data, props.data)
);
}
+ async removePerson(companyId: string, id: string) {
+ const parent = this.input.id(companyId);
+ const child = this.input.id(id);
+ if (!(parent.isSuccess && child.isSuccess)) {
+ return Result.fail(new CompanyError("INVALID_REQUEST"));
+ }
+ return this.companies.removePerson(parent.data, child.data);
+ }
async addDepartment(id: string, dto: DepartmentRequestDTO) {
const parsed = this.input.id(id);
const props = this.input.department(dto);
@@ -148,6 +156,15 @@ export class CompanyUseCases {
company.updateAssignment(person.data, child.data, values.data, new Date().toISOString())
);
}
+ async removeAssignment(companyId: string, personId: string, id: string) {
+ const parent = this.input.id(companyId);
+ const person = this.input.id(personId);
+ const assignment = this.input.id(id);
+ if (!(parent.isSuccess && person.isSuccess && assignment.isSuccess)) {
+ return Result.fail(new CompanyError("INVALID_REQUEST"));
+ }
+ return this.companies.removeAssignment(parent.data, person.data, assignment.data);
+ }
async addSite(id: string, dto: SiteRequestDTO) {
const parsed = this.input.id(id);
const props = this.input.site(dto);
diff --git a/modules/companies/src/api/domain/company-error.ts b/modules/companies/src/api/domain/company-error.ts
index bf97fd9..9f4d2e4 100644
--- a/modules/companies/src/api/domain/company-error.ts
+++ b/modules/companies/src/api/domain/company-error.ts
@@ -4,9 +4,11 @@ export type CompanyErrorCode =
| "CONTACT_NOT_FOUND"
| "SITE_NOT_FOUND"
| "PERSON_NOT_FOUND"
+ | "PERSON_IN_USE"
| "DEPARTMENT_NOT_FOUND"
| "DEPARTMENT_NAME_EXISTS"
| "ASSIGNMENT_NOT_FOUND"
+ | "ASSIGNMENT_IN_USE"
| "ASSIGNMENT_CONFLICT"
| "INACTIVE_ASSIGNMENT_TARGET";
export class CompanyError extends Error {
diff --git a/modules/companies/src/api/domain/company.aggregate.ts b/modules/companies/src/api/domain/company.aggregate.ts
index 5f8fb2d..90159d2 100644
--- a/modules/companies/src/api/domain/company.aggregate.ts
+++ b/modules/companies/src/api/domain/company.aggregate.ts
@@ -109,6 +109,20 @@ export class Company extends AggregateRoot {
)
);
}
+ removePerson(id: UniqueID): Result {
+ if (!this.people.some((person) => person.id.equals(id))) {
+ return Result.fail(new CompanyError("PERSON_NOT_FOUND"));
+ }
+ if (this.assignments.some((assignment) => assignment.personId.equals(id))) {
+ return Result.fail(new CompanyError("PERSON_IN_USE"));
+ }
+ return Result.ok(
+ new Company(
+ { ...this.props, people: this.people.filter((person) => !person.id.equals(id)) },
+ this.id
+ )
+ );
+ }
private departmentNameExists(name: string, except?: UniqueID): boolean {
const key = name.normalize("NFC").toLowerCase();
return this.departments.some(
@@ -256,6 +270,20 @@ export class Company extends AggregateRoot {
)
);
}
+ removeAssignment(personId: UniqueID, id: UniqueID): Result {
+ const assignment = this.assignments.find(
+ (item) => item.id.equals(id) && item.personId.equals(personId)
+ );
+ if (!assignment) {
+ return Result.fail(new CompanyError("ASSIGNMENT_NOT_FOUND"));
+ }
+ return Result.ok(
+ new Company(
+ { ...this.props, assignments: this.assignments.filter((item) => !item.id.equals(id)) },
+ this.id
+ )
+ );
+ }
addSite(site: Site): Result {
if (!site.companyId.equals(this.id)) {
return Result.fail(new CompanyError("INVALID_REQUEST"));
diff --git a/modules/companies/src/api/infrastructure/http/company.routes.ts b/modules/companies/src/api/infrastructure/http/company.routes.ts
index e79d91c..7fc64c7 100644
--- a/modules/companies/src/api/infrastructure/http/company.routes.ts
+++ b/modules/companies/src/api/infrastructure/http/company.routes.ts
@@ -126,6 +126,17 @@ export function registerCompanyRoutes(
post("/:companyId/people/:id/update", PersonRequestSchema, ChildParamsSchema, (dto, params) =>
useCases.updatePerson(params.companyId, params.id, dto)
);
+ router.post("/:companyId/people/:id/delete", (request, response, next) => {
+ const params = ChildParamsSchema.safeParse(request.params);
+ if (!(params.success && z.strictObject({}).safeParse(request.body).success)) {
+ sendError(response, "INVALID_REQUEST", 400);
+ return;
+ }
+ useCases
+ .removePerson(params.data.companyId, params.data.id)
+ .then((result) => sendResult(response, result))
+ .catch(next);
+ });
post(
"/:companyId/departments",
DepartmentRequestSchema,
@@ -146,6 +157,17 @@ export function registerCompanyRoutes(
(dto, params) => useCases.addAssignment(params.companyId, params.personId, dto),
201
);
+ router.post("/:companyId/people/:personId/assignments/:id/delete", (request, response, next) => {
+ const params = AssignmentParamsSchema.safeParse(request.params);
+ if (!(params.success && z.strictObject({}).safeParse(request.body).success)) {
+ sendError(response, "INVALID_REQUEST", 400);
+ return;
+ }
+ useCases
+ .removeAssignment(params.data.companyId, params.data.personId, params.data.id)
+ .then((result) => sendResult(response, result))
+ .catch(next);
+ });
post(
"/:companyId/people/:personId/assignments/:id/update",
AssignmentUpdateRequestSchema,
diff --git a/modules/companies/src/api/infrastructure/persistence/sequelize-company.repository.ts b/modules/companies/src/api/infrastructure/persistence/sequelize-company.repository.ts
index aedbfc7..4a40e81 100644
--- a/modules/companies/src/api/infrastructure/persistence/sequelize-company.repository.ts
+++ b/modules/companies/src/api/infrastructure/persistence/sequelize-company.repository.ts
@@ -102,6 +102,55 @@ export class SequelizeCompanyRepository implements ICompanyRepository {
await CompanyModel.create(this.mapper.toPersistence(company).company);
return company;
}
+ async removePerson(
+ id: UniqueID,
+ personId: UniqueID
+ ): Promise> {
+ return this.database.transaction(async (transaction) => {
+ const current = (await this.load(id, transaction, true)).getOrUndefined();
+ if (!current) {
+ return Result.fail(new CompanyError("COMPANY_NOT_FOUND"));
+ }
+ const changed = current.removePerson(personId);
+ if (!changed.isSuccess) {
+ return changed;
+ }
+ try {
+ await PersonModel.destroy({
+ where: { id: personId.toPrimitive(), companyId: id.toPrimitive() },
+ transaction,
+ });
+ } catch {
+ return Result.fail(new CompanyError("PERSON_IN_USE"));
+ }
+ return changed;
+ });
+ }
+ async removeAssignment(
+ id: UniqueID,
+ personId: UniqueID,
+ assignmentId: UniqueID
+ ): Promise> {
+ return this.database.transaction(async (transaction) => {
+ const current = (await this.load(id, transaction, true)).getOrUndefined();
+ if (!current) {
+ return Result.fail(new CompanyError("COMPANY_NOT_FOUND"));
+ }
+ const changed = current.removeAssignment(personId, assignmentId);
+ if (!changed.isSuccess) {
+ return changed;
+ }
+ await AssignmentRevisionModel.destroy({
+ where: { assignmentId: assignmentId.toPrimitive() },
+ transaction,
+ });
+ await AssignmentModel.destroy({
+ where: { id: assignmentId.toPrimitive(), companyId: id.toPrimitive() },
+ transaction,
+ });
+ return changed;
+ });
+ }
async update(
id: UniqueID,
change: (company: Company) => DomainResult
diff --git a/modules/companies/src/common/locales/en.json b/modules/companies/src/common/locales/en.json
index 1912e55..f8e4242 100644
--- a/modules/companies/src/common/locales/en.json
+++ b/modules/companies/src/common/locales/en.json
@@ -909,7 +909,15 @@
"removed": "The logo will be removed on save.",
"remove": "Remove logo",
"undo": "Undo logo change"
- }
+ },
+ "deletePerson": "Delete person",
+ "deletingPerson": "Deleting…",
+ "deletePersonConfirm": "Delete {{name}}? It can only be deleted when it has no registered relationships.",
+ "deletePersonError": "This person cannot be deleted because it has registered relationships.",
+ "deleteAssignment": "Delete assignment",
+ "deletingAssignment": "Deleting…",
+ "deleteAssignmentConfirm": "Delete the assignment to {{name}}?",
+ "deleteAssignmentError": "The assignment could not be deleted."
},
"access": {
"helpTitle": "What is it and what is it for?",
diff --git a/modules/companies/src/common/locales/es.json b/modules/companies/src/common/locales/es.json
index c13f19c..543bfe4 100644
--- a/modules/companies/src/common/locales/es.json
+++ b/modules/companies/src/common/locales/es.json
@@ -909,7 +909,15 @@
"removed": "Se quitará el logotipo al guardar.",
"remove": "Quitar logotipo",
"undo": "Deshacer cambio de logotipo"
- }
+ },
+ "deletePerson": "Eliminar persona",
+ "deletingPerson": "Eliminando…",
+ "deletePersonConfirm": "¿Eliminar a {{name}}? Solo se puede eliminar si no tiene relaciones registradas.",
+ "deletePersonError": "No se puede eliminar esta persona porque tiene relaciones registradas.",
+ "deleteAssignment": "Eliminar asignación",
+ "deletingAssignment": "Eliminando…",
+ "deleteAssignmentConfirm": "¿Eliminar la asignación al departamento {{name}}?",
+ "deleteAssignmentError": "No se puede eliminar la asignación."
},
"access": {
"helpTitle": "¿Qué es y para qué sirve?",
diff --git a/modules/companies/src/web/assignments/shared/api/index.ts b/modules/companies/src/web/assignments/shared/api/index.ts
index 9bddff1..e8df8ed 100644
--- a/modules/companies/src/web/assignments/shared/api/index.ts
+++ b/modules/companies/src/web/assignments/shared/api/index.ts
@@ -1 +1 @@
-export { saveAssignment } from "./save-assignment.api";
+export { deleteAssignment, saveAssignment } from "./save-assignment.api";
diff --git a/modules/companies/src/web/assignments/shared/api/save-assignment.api.ts b/modules/companies/src/web/assignments/shared/api/save-assignment.api.ts
index 5151716..47a0e54 100644
--- a/modules/companies/src/web/assignments/shared/api/save-assignment.api.ts
+++ b/modules/companies/src/web/assignments/shared/api/save-assignment.api.ts
@@ -1,6 +1,14 @@
import { CompanyResponseSchema } from "../../../../common";
import { companyRequest } from "../../../companies/shared/api/company-request";
+export const deleteAssignment = (companyId: string, personId: string, id: string, token: string) =>
+ companyRequest(
+ `/${companyId}/people/${personId}/assignments/${id}/delete`,
+ CompanyResponseSchema,
+ {},
+ token
+ );
+
export const saveAssignment = (
companyId: string,
personId: string,
diff --git a/modules/companies/src/web/people/detail/ui/person-details.tsx b/modules/companies/src/web/people/detail/ui/person-details.tsx
index b220175..48e12ed 100644
--- a/modules/companies/src/web/people/detail/ui/person-details.tsx
+++ b/modules/companies/src/web/people/detail/ui/person-details.tsx
@@ -11,6 +11,8 @@ import { useTranslation } from "react-i18next";
import type { CompanyDTO, PersonDTO } from "../../../../common";
import { AssignmentEditorDialog, AssignmentHistoryDialog } from "../../../assignments";
+import { deleteAssignment } from "../../../assignments/shared/api";
+import { deletePerson } from "../../shared/api";
export function PersonDetails({
company,
@@ -28,6 +30,8 @@ export function PersonDetails({
const { t, i18n } = useTranslation("companies");
const [editor, setEditor] = useState();
const [historyId, setHistoryId] = useState();
+ const [deleting, setDeleting] = useState(false);
+ const [deletingAssignment, setDeletingAssignment] = useState();
const assignments = company.assignments.filter((item) => item.personId === person.id);
const selected = assignments.find((item) => item.id === editor);
const history = assignments.find((item) => item.id === historyId);
@@ -46,9 +50,31 @@ export function PersonDetails({
{person.isContact ? ` · ${t("isContact")}` : ""}
-
+
+
+
+
@@ -132,6 +158,32 @@ export function PersonDetails({
setHistoryId(item.id)}>
{t("history")}
+ {
+ if (
+ !window.confirm(
+ t("deleteAssignmentConfirm", {
+ name: departmentName(item.departmentId),
+ })
+ )
+ ) {
+ return;
+ }
+ setDeletingAssignment(item.id);
+ try {
+ await deleteAssignment(company.id, person.id, item.id, csrfToken);
+ window.location.reload();
+ } catch {
+ window.alert(t("deleteAssignmentError"));
+ } finally {
+ setDeletingAssignment(undefined);
+ }
+ }}
+ >
+ {t(
+ deletingAssignment === item.id ? "deletingAssignment" : "deleteAssignment"
+ )}
+
),
},
diff --git a/modules/companies/src/web/people/shared/api/index.ts b/modules/companies/src/web/people/shared/api/index.ts
index e0dfd2d..0857bab 100644
--- a/modules/companies/src/web/people/shared/api/index.ts
+++ b/modules/companies/src/web/people/shared/api/index.ts
@@ -1 +1 @@
-export { savePerson } from "./save-person.api";
+export { deletePerson, savePerson } from "./save-person.api";
diff --git a/modules/companies/src/web/people/shared/api/save-person.api.ts b/modules/companies/src/web/people/shared/api/save-person.api.ts
index 637c728..d22fd6f 100644
--- a/modules/companies/src/web/people/shared/api/save-person.api.ts
+++ b/modules/companies/src/web/people/shared/api/save-person.api.ts
@@ -13,3 +13,6 @@ export const savePerson = (
body,
token
);
+
+export const deletePerson = (companyId: string, id: string, token: string) =>
+ companyRequest(`/${companyId}/people/${id}/delete`, CompanyResponseSchema, {}, token);