From 233e197fdb5a42c015951de92da2b707ba2e88b6 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Mon, 22 Dec 2025 14:30:25 +0100 Subject: [PATCH] Fix people deletion Signed-off-by: Sacha Al Himdani --- apps/console/src/layouts/MainLayout.tsx | 8 +- .../organizations/people/PeopleListPage.tsx | 80 ++++++++----- .../people/dialogs/SetEndOfContractDialog.tsx | 107 ++++++++++++++++++ pkg/coredata/people.go | 23 +++- pkg/server/api/console/v1/v1_resolver.go | 4 + 5 files changed, 186 insertions(+), 36 deletions(-) create mode 100644 apps/console/src/pages/organizations/people/dialogs/SetEndOfContractDialog.tsx diff --git a/apps/console/src/layouts/MainLayout.tsx b/apps/console/src/layouts/MainLayout.tsx index 6c17788af..072331665 100644 --- a/apps/console/src/layouts/MainLayout.tsx +++ b/apps/console/src/layouts/MainLayout.tsx @@ -10,7 +10,7 @@ import { IconBank, IconBook, IconBox, - IconCalendar1, + IconCalendar2, IconCheckmark1, IconChevronGrabberVertical, IconCircleProgress, @@ -122,7 +122,7 @@ function MainLayoutContent({ {isAuthorized("Organization", "listMeetings") && ( )} @@ -441,7 +441,7 @@ function OrganizationSelector({ {isLoading ? __("Loading...") : currentOrganization?.name || ""} } - > + >
{ - e.stopPropagation(); + e.stopPropagation(); }} autoFocus /> diff --git a/apps/console/src/pages/organizations/people/PeopleListPage.tsx b/apps/console/src/pages/organizations/people/PeopleListPage.tsx index ba7f9b509..32a38b795 100644 --- a/apps/console/src/pages/organizations/people/PeopleListPage.tsx +++ b/apps/console/src/pages/organizations/people/PeopleListPage.tsx @@ -11,6 +11,7 @@ import { ActionDropdown, DropdownItem, IconTrashCan, + IconCalendar2, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import type { PeopleGraphPaginatedQuery } from "/hooks/graph/__generated__/PeopleGraphPaginatedQuery.graphql"; @@ -22,9 +23,10 @@ import type { NodeOf } from "/types"; import { usePageTitle } from "@probo/hooks"; import { getRole } from "@probo/helpers"; import { CreatePeopleDialog } from "./dialogs/CreatePeopleDialog"; +import { SetEndOfContractDialog, type SetEndOfContractDialogRef } from "./dialogs/SetEndOfContractDialog"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { PermissionsContext } from "/providers/PermissionsContext"; -import { use } from "react"; +import { use, useRef } from "react"; type People = NodeOf; @@ -108,40 +110,56 @@ function PeopleRow({ const deletePeople = useDeletePeople(people, connectionId); const contractEnded = isContractEnded(people); const { isAuthorized } = use(PermissionsContext); + const dialogRef = useRef(null); return ( - - -
- -
-
{people.fullName}
-
- {people.primaryEmailAddress} + <> + + + +
+ +
+
{people.fullName}
+
+ {people.primaryEmailAddress} +
-
- - {getRole(__, people.kind)} - {people.position} - {hasAnyAction && ( - - - {isAuthorized("People", "deletePeople") && ( - - {__("Delete")} - - )} - - )} - + {getRole(__, people.kind)} + {people.position} + {hasAnyAction && ( + + + {isAuthorized("People", "updatePeople") && ( + dialogRef.current?.open()} + > + {__("Set end of contract")} + + )} + {isAuthorized("People", "deletePeople") && ( + + {__("Delete")} + + )} + + + )} + + ); } diff --git a/apps/console/src/pages/organizations/people/dialogs/SetEndOfContractDialog.tsx b/apps/console/src/pages/organizations/people/dialogs/SetEndOfContractDialog.tsx new file mode 100644 index 000000000..af0cb4e47 --- /dev/null +++ b/apps/console/src/pages/organizations/people/dialogs/SetEndOfContractDialog.tsx @@ -0,0 +1,107 @@ +import { useTranslate } from "@probo/i18n"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + Field, + Input, + Spinner, + useDialogRef, +} from "@probo/ui"; +import { forwardRef, useImperativeHandle } from "react"; +import { z } from "zod"; +import { formatDatetime, toDateInput } from "@probo/helpers"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import { updatePeopleMutation } from "/hooks/graph/PeopleGraph"; + +const schema = z.object({ + contractEndDate: z.string().optional(), +}); + +export type SetEndOfContractDialogRef = { + open: () => void; + close: () => void; +}; + +type Props = { + peopleId: string; + currentContractEndDate?: string | null; +}; + +export const SetEndOfContractDialog = forwardRef(function SetEndOfContractDialog( + { + peopleId, + currentContractEndDate, + }, + ref +) { + const { __ } = useTranslate(); + const dialogRef = useDialogRef(); + + useImperativeHandle(ref, () => ({ + open: () => dialogRef.current?.open(), + close: () => dialogRef.current?.close(), + })); + + const { + register, + handleSubmit, + formState: { isSubmitting }, + reset, + } = useFormWithSchema(schema, { + defaultValues: { + contractEndDate: toDateInput(currentContractEndDate), + }, + }); + + const [mutate] = useMutationWithToasts(updatePeopleMutation, { + successMessage: __("End of contract updated successfully"), + errorMessage: __("Failed to update end of contract"), + }); + + const onSubmit = handleSubmit(async (data) => { + await mutate({ + variables: { + input: { + id: peopleId, + contractEndDate: formatDatetime(data.contractEndDate), + }, + }, + }); + + dialogRef.current?.close(); + }); + + const handleClose = () => { + reset(); + }; + + return ( + +
+ + + + + + + + + +
+
+ ); +}); diff --git a/pkg/coredata/people.go b/pkg/coredata/people.go index 7de3a3bdc..cc12ac52e 100644 --- a/pkg/coredata/people.go +++ b/pkg/coredata/people.go @@ -22,6 +22,7 @@ import ( "time" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/mail" @@ -52,6 +53,10 @@ type ( ErrPeopleAlreadyExists struct { message string } + + ErrPeopleReferenced struct { + message string + } ) func (e ErrPeopleNotFound) Error() string { @@ -62,6 +67,10 @@ func (e ErrPeopleAlreadyExists) Error() string { return e.message } +func (e ErrPeopleReferenced) Error() string { + return e.message +} + func (p People) CursorKey(orderBy PeopleOrderField) page.CursorKey { switch orderBy { case PeopleOrderFieldCreatedAt: @@ -349,7 +358,19 @@ DELETE FROM peoples WHERE %s AND id = @people_id maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) - return err + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + if pgErr.Code == "23503" { + return &ErrPeopleReferenced{ + message: fmt.Sprintf("person with id %s cannot be deleted because it is referenced by other records", p.ID), + } + } + } + return fmt.Errorf("cannot delete person: %w", err) + } + + return nil } func (p *Peoples) CountByOrganizationID( diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 6890d9e8f..8348f8454 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -2126,6 +2126,10 @@ func (r *mutationResolver) DeletePeople(ctx context.Context, input types.DeleteP err := prb.Peoples.Delete(ctx, input.PeopleID) if err != nil { + var errReferenced *coredata.ErrPeopleReferenced + if errors.As(err, &errReferenced) { + return nil, gqlutils.Conflict(errReferenced) + } panic(fmt.Errorf("cannot delete people: %w", err)) }