Fix people deletion

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-12-22 14:30:25 +01:00
parent 0da25edc15
commit 233e197fdb
5 changed files with 186 additions and 36 deletions

View File

@@ -10,7 +10,7 @@ import {
IconBank, IconBank,
IconBook, IconBook,
IconBox, IconBox,
IconCalendar1, IconCalendar2,
IconCheckmark1, IconCheckmark1,
IconChevronGrabberVertical, IconChevronGrabberVertical,
IconCircleProgress, IconCircleProgress,
@@ -122,7 +122,7 @@ function MainLayoutContent({
{isAuthorized("Organization", "listMeetings") && ( {isAuthorized("Organization", "listMeetings") && (
<SidebarItem <SidebarItem
label={__("Meetings")} label={__("Meetings")}
icon={IconCalendar1} icon={IconCalendar2}
to={`${prefix}/meetings`} to={`${prefix}/meetings`}
/> />
)} )}

View File

@@ -11,6 +11,7 @@ import {
ActionDropdown, ActionDropdown,
DropdownItem, DropdownItem,
IconTrashCan, IconTrashCan,
IconCalendar2,
} from "@probo/ui"; } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import type { PeopleGraphPaginatedQuery } from "/hooks/graph/__generated__/PeopleGraphPaginatedQuery.graphql"; import type { PeopleGraphPaginatedQuery } from "/hooks/graph/__generated__/PeopleGraphPaginatedQuery.graphql";
@@ -22,9 +23,10 @@ import type { NodeOf } from "/types";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { getRole } from "@probo/helpers"; import { getRole } from "@probo/helpers";
import { CreatePeopleDialog } from "./dialogs/CreatePeopleDialog"; import { CreatePeopleDialog } from "./dialogs/CreatePeopleDialog";
import { SetEndOfContractDialog, type SetEndOfContractDialogRef } from "./dialogs/SetEndOfContractDialog";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { PermissionsContext } from "/providers/PermissionsContext"; import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react"; import { use, useRef } from "react";
type People = NodeOf<PeopleGraphPaginatedFragment$data["peoples"]>; type People = NodeOf<PeopleGraphPaginatedFragment$data["peoples"]>;
@@ -108,40 +110,56 @@ function PeopleRow({
const deletePeople = useDeletePeople(people, connectionId); const deletePeople = useDeletePeople(people, connectionId);
const contractEnded = isContractEnded(people); const contractEnded = isContractEnded(people);
const { isAuthorized } = use(PermissionsContext); const { isAuthorized } = use(PermissionsContext);
const dialogRef = useRef<SetEndOfContractDialogRef>(null);
return ( return (
<Tr <>
to={`/organizations/${organizationId}/people/${people.id}/tasks`} <SetEndOfContractDialog
className={contractEnded ? "opacity-50" : ""} peopleId={people.id}
> currentContractEndDate={people.contractEndDate}
<Td> ref={dialogRef}
<div className="flex gap-3 items-center"> />
<Avatar name={people.fullName} /> <Tr
<div> to={`/organizations/${organizationId}/people/${people.id}/profile`}
<div className="text-sm">{people.fullName}</div> className={contractEnded ? "opacity-50" : ""}
<div className="text-xs text-txt-tertiary"> >
{people.primaryEmailAddress} <Td>
<div className="flex gap-3 items-center">
<Avatar name={people.fullName} />
<div>
<div className="text-sm">{people.fullName}</div>
<div className="text-xs text-txt-tertiary">
{people.primaryEmailAddress}
</div>
</div> </div>
</div> </div>
</div>
</Td>
<Td className="text-sm">{getRole(__, people.kind)}</Td>
<Td className="text-sm">{people.position}</Td>
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
{isAuthorized("People", "deletePeople") && (
<DropdownItem
icon={IconTrashCan}
variant="danger"
onClick={deletePeople}
>
{__("Delete")}
</DropdownItem>
)}
</ActionDropdown>
</Td> </Td>
)} <Td className="text-sm">{getRole(__, people.kind)}</Td>
</Tr> <Td className="text-sm">{people.position}</Td>
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
{isAuthorized("People", "updatePeople") && (
<DropdownItem
icon={IconCalendar2}
onClick={() => dialogRef.current?.open()}
>
{__("Set end of contract")}
</DropdownItem>
)}
{isAuthorized("People", "deletePeople") && (
<DropdownItem
icon={IconTrashCan}
variant="danger"
onClick={deletePeople}
>
{__("Delete")}
</DropdownItem>
)}
</ActionDropdown>
</Td>
)}
</Tr>
</>
); );
} }

View File

@@ -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<SetEndOfContractDialogRef, Props>(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 (
<Dialog
title={__("Set End of Contract")}
ref={dialogRef}
className="max-w-lg"
onClose={handleClose}
>
<form onSubmit={onSubmit}>
<DialogContent padded className="space-y-4">
<Field label={__("End of contract")}>
<Input {...register("contractEndDate")} type="date" />
</Field>
</DialogContent>
<DialogFooter>
<Button
type="submit"
disabled={isSubmitting}
icon={isSubmitting ? Spinner : undefined}
>
{__("Update")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
});

View File

@@ -22,6 +22,7 @@ import (
"time" "time"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail" "go.probo.inc/probo/pkg/mail"
@@ -52,6 +53,10 @@ type (
ErrPeopleAlreadyExists struct { ErrPeopleAlreadyExists struct {
message string message string
} }
ErrPeopleReferenced struct {
message string
}
) )
func (e ErrPeopleNotFound) Error() string { func (e ErrPeopleNotFound) Error() string {
@@ -62,6 +67,10 @@ func (e ErrPeopleAlreadyExists) Error() string {
return e.message return e.message
} }
func (e ErrPeopleReferenced) Error() string {
return e.message
}
func (p People) CursorKey(orderBy PeopleOrderField) page.CursorKey { func (p People) CursorKey(orderBy PeopleOrderField) page.CursorKey {
switch orderBy { switch orderBy {
case PeopleOrderFieldCreatedAt: case PeopleOrderFieldCreatedAt:
@@ -349,7 +358,19 @@ DELETE FROM peoples WHERE %s AND id = @people_id
maps.Copy(args, scope.SQLArguments()) maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args) _, 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( func (p *Peoples) CountByOrganizationID(

View File

@@ -2126,6 +2126,10 @@ func (r *mutationResolver) DeletePeople(ctx context.Context, input types.DeleteP
err := prb.Peoples.Delete(ctx, input.PeopleID) err := prb.Peoples.Delete(ctx, input.PeopleID)
if err != nil { 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)) panic(fmt.Errorf("cannot delete people: %w", err))
} }