Añadir funcionalidades para eliminar personas y asignaciones en el repositorio de compañías
This commit is contained in:
parent
454e68d5c8
commit
f945e9a996
@ -12,4 +12,10 @@ export interface ICompanyRepository {
|
|||||||
id: UniqueID,
|
id: UniqueID,
|
||||||
change: (company: Company) => Result<Company, CompanyError>
|
change: (company: Company) => Result<Company, CompanyError>
|
||||||
): Promise<Result<Company, CompanyError>>;
|
): Promise<Result<Company, CompanyError>>;
|
||||||
|
removePerson(id: UniqueID, personId: UniqueID): Promise<Result<Company, CompanyError>>;
|
||||||
|
removeAssignment(
|
||||||
|
id: UniqueID,
|
||||||
|
personId: UniqueID,
|
||||||
|
assignmentId: UniqueID
|
||||||
|
): Promise<Result<Company, CompanyError>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,6 +97,14 @@ export class CompanyUseCases {
|
|||||||
company.updatePerson(child.data, props.data)
|
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) {
|
async addDepartment(id: string, dto: DepartmentRequestDTO) {
|
||||||
const parsed = this.input.id(id);
|
const parsed = this.input.id(id);
|
||||||
const props = this.input.department(dto);
|
const props = this.input.department(dto);
|
||||||
@ -148,6 +156,15 @@ export class CompanyUseCases {
|
|||||||
company.updateAssignment(person.data, child.data, values.data, new Date().toISOString())
|
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) {
|
async addSite(id: string, dto: SiteRequestDTO) {
|
||||||
const parsed = this.input.id(id);
|
const parsed = this.input.id(id);
|
||||||
const props = this.input.site(dto);
|
const props = this.input.site(dto);
|
||||||
|
|||||||
@ -4,9 +4,11 @@ export type CompanyErrorCode =
|
|||||||
| "CONTACT_NOT_FOUND"
|
| "CONTACT_NOT_FOUND"
|
||||||
| "SITE_NOT_FOUND"
|
| "SITE_NOT_FOUND"
|
||||||
| "PERSON_NOT_FOUND"
|
| "PERSON_NOT_FOUND"
|
||||||
|
| "PERSON_IN_USE"
|
||||||
| "DEPARTMENT_NOT_FOUND"
|
| "DEPARTMENT_NOT_FOUND"
|
||||||
| "DEPARTMENT_NAME_EXISTS"
|
| "DEPARTMENT_NAME_EXISTS"
|
||||||
| "ASSIGNMENT_NOT_FOUND"
|
| "ASSIGNMENT_NOT_FOUND"
|
||||||
|
| "ASSIGNMENT_IN_USE"
|
||||||
| "ASSIGNMENT_CONFLICT"
|
| "ASSIGNMENT_CONFLICT"
|
||||||
| "INACTIVE_ASSIGNMENT_TARGET";
|
| "INACTIVE_ASSIGNMENT_TARGET";
|
||||||
export class CompanyError extends Error {
|
export class CompanyError extends Error {
|
||||||
|
|||||||
@ -109,6 +109,20 @@ export class Company extends AggregateRoot<CompanyProps> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
removePerson(id: UniqueID): Result<Company, CompanyError> {
|
||||||
|
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 {
|
private departmentNameExists(name: string, except?: UniqueID): boolean {
|
||||||
const key = name.normalize("NFC").toLowerCase();
|
const key = name.normalize("NFC").toLowerCase();
|
||||||
return this.departments.some(
|
return this.departments.some(
|
||||||
@ -256,6 +270,20 @@ export class Company extends AggregateRoot<CompanyProps> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
removeAssignment(personId: UniqueID, id: UniqueID): Result<Company, CompanyError> {
|
||||||
|
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<Company, CompanyError> {
|
addSite(site: Site): Result<Company, CompanyError> {
|
||||||
if (!site.companyId.equals(this.id)) {
|
if (!site.companyId.equals(this.id)) {
|
||||||
return Result.fail(new CompanyError("INVALID_REQUEST"));
|
return Result.fail(new CompanyError("INVALID_REQUEST"));
|
||||||
|
|||||||
@ -126,6 +126,17 @@ export function registerCompanyRoutes(
|
|||||||
post("/:companyId/people/:id/update", PersonRequestSchema, ChildParamsSchema, (dto, params) =>
|
post("/:companyId/people/:id/update", PersonRequestSchema, ChildParamsSchema, (dto, params) =>
|
||||||
useCases.updatePerson(params.companyId, params.id, dto)
|
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(
|
post(
|
||||||
"/:companyId/departments",
|
"/:companyId/departments",
|
||||||
DepartmentRequestSchema,
|
DepartmentRequestSchema,
|
||||||
@ -146,6 +157,17 @@ export function registerCompanyRoutes(
|
|||||||
(dto, params) => useCases.addAssignment(params.companyId, params.personId, dto),
|
(dto, params) => useCases.addAssignment(params.companyId, params.personId, dto),
|
||||||
201
|
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(
|
post(
|
||||||
"/:companyId/people/:personId/assignments/:id/update",
|
"/:companyId/people/:personId/assignments/:id/update",
|
||||||
AssignmentUpdateRequestSchema,
|
AssignmentUpdateRequestSchema,
|
||||||
|
|||||||
@ -102,6 +102,55 @@ export class SequelizeCompanyRepository implements ICompanyRepository {
|
|||||||
await CompanyModel.create(this.mapper.toPersistence(company).company);
|
await CompanyModel.create(this.mapper.toPersistence(company).company);
|
||||||
return company;
|
return company;
|
||||||
}
|
}
|
||||||
|
async removePerson(
|
||||||
|
id: UniqueID,
|
||||||
|
personId: UniqueID
|
||||||
|
): Promise<DomainResult<Company, CompanyError>> {
|
||||||
|
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<DomainResult<Company, CompanyError>> {
|
||||||
|
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(
|
async update(
|
||||||
id: UniqueID,
|
id: UniqueID,
|
||||||
change: (company: Company) => DomainResult<Company, CompanyError>
|
change: (company: Company) => DomainResult<Company, CompanyError>
|
||||||
|
|||||||
@ -909,7 +909,15 @@
|
|||||||
"removed": "The logo will be removed on save.",
|
"removed": "The logo will be removed on save.",
|
||||||
"remove": "Remove logo",
|
"remove": "Remove logo",
|
||||||
"undo": "Undo logo change"
|
"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": {
|
"access": {
|
||||||
"helpTitle": "What is it and what is it for?",
|
"helpTitle": "What is it and what is it for?",
|
||||||
|
|||||||
@ -909,7 +909,15 @@
|
|||||||
"removed": "Se quitará el logotipo al guardar.",
|
"removed": "Se quitará el logotipo al guardar.",
|
||||||
"remove": "Quitar logotipo",
|
"remove": "Quitar logotipo",
|
||||||
"undo": "Deshacer cambio de 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": {
|
"access": {
|
||||||
"helpTitle": "¿Qué es y para qué sirve?",
|
"helpTitle": "¿Qué es y para qué sirve?",
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export { saveAssignment } from "./save-assignment.api";
|
export { deleteAssignment, saveAssignment } from "./save-assignment.api";
|
||||||
|
|||||||
@ -1,6 +1,14 @@
|
|||||||
import { CompanyResponseSchema } from "../../../../common";
|
import { CompanyResponseSchema } from "../../../../common";
|
||||||
import { companyRequest } from "../../../companies/shared/api/company-request";
|
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 = (
|
export const saveAssignment = (
|
||||||
companyId: string,
|
companyId: string,
|
||||||
personId: string,
|
personId: string,
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import type { CompanyDTO, PersonDTO } from "../../../../common";
|
import type { CompanyDTO, PersonDTO } from "../../../../common";
|
||||||
import { AssignmentEditorDialog, AssignmentHistoryDialog } from "../../../assignments";
|
import { AssignmentEditorDialog, AssignmentHistoryDialog } from "../../../assignments";
|
||||||
|
import { deleteAssignment } from "../../../assignments/shared/api";
|
||||||
|
import { deletePerson } from "../../shared/api";
|
||||||
|
|
||||||
export function PersonDetails({
|
export function PersonDetails({
|
||||||
company,
|
company,
|
||||||
@ -28,6 +30,8 @@ export function PersonDetails({
|
|||||||
const { t, i18n } = useTranslation("companies");
|
const { t, i18n } = useTranslation("companies");
|
||||||
const [editor, setEditor] = useState<string | undefined>();
|
const [editor, setEditor] = useState<string | undefined>();
|
||||||
const [historyId, setHistoryId] = useState<string | undefined>();
|
const [historyId, setHistoryId] = useState<string | undefined>();
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
const [deletingAssignment, setDeletingAssignment] = useState<string | undefined>();
|
||||||
const assignments = company.assignments.filter((item) => item.personId === person.id);
|
const assignments = company.assignments.filter((item) => item.personId === person.id);
|
||||||
const selected = assignments.find((item) => item.id === editor);
|
const selected = assignments.find((item) => item.id === editor);
|
||||||
const history = assignments.find((item) => item.id === historyId);
|
const history = assignments.find((item) => item.id === historyId);
|
||||||
@ -46,9 +50,31 @@ export function PersonDetails({
|
|||||||
{person.isContact ? ` · ${t("isContact")}` : ""}
|
{person.isContact ? ` · ${t("isContact")}` : ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={onEdit} variant="outline">
|
<div className="flex flex-wrap gap-2">
|
||||||
{t("editPerson")}
|
<Button onClick={onEdit} variant="outline">
|
||||||
</Button>
|
{t("editPerson")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={deleting}
|
||||||
|
onClick={async () => {
|
||||||
|
if (!window.confirm(t("deletePersonConfirm", { name: person.name }))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDeleting(true);
|
||||||
|
try {
|
||||||
|
await deletePerson(company.id, person.id, csrfToken);
|
||||||
|
onBack();
|
||||||
|
} catch {
|
||||||
|
window.alert(t("deletePersonError"));
|
||||||
|
} finally {
|
||||||
|
setDeleting(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
{t(deleting ? "deletingPerson" : "deletePerson")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="grid gap-4 pt-6 sm:grid-cols-3">
|
<CardContent className="grid gap-4 pt-6 sm:grid-cols-3">
|
||||||
@ -132,6 +158,32 @@ export function PersonDetails({
|
|||||||
<EntityAction onClick={() => setHistoryId(item.id)}>
|
<EntityAction onClick={() => setHistoryId(item.id)}>
|
||||||
{t("history")}
|
{t("history")}
|
||||||
</EntityAction>
|
</EntityAction>
|
||||||
|
<EntityAction
|
||||||
|
onClick={async () => {
|
||||||
|
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"
|
||||||
|
)}
|
||||||
|
</EntityAction>
|
||||||
</EntityRowActions>
|
</EntityRowActions>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export { savePerson } from "./save-person.api";
|
export { deletePerson, savePerson } from "./save-person.api";
|
||||||
|
|||||||
@ -13,3 +13,6 @@ export const savePerson = (
|
|||||||
body,
|
body,
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const deletePerson = (companyId: string, id: string, token: string) =>
|
||||||
|
companyRequest(`/${companyId}/people/${id}/delete`, CompanyResponseSchema, {}, token);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user