Fix people deletion
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -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`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,10 +110,17 @@ 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 (
|
||||||
|
<>
|
||||||
|
<SetEndOfContractDialog
|
||||||
|
peopleId={people.id}
|
||||||
|
currentContractEndDate={people.contractEndDate}
|
||||||
|
ref={dialogRef}
|
||||||
|
/>
|
||||||
<Tr
|
<Tr
|
||||||
to={`/organizations/${organizationId}/people/${people.id}/tasks`}
|
to={`/organizations/${organizationId}/people/${people.id}/profile`}
|
||||||
className={contractEnded ? "opacity-50" : ""}
|
className={contractEnded ? "opacity-50" : ""}
|
||||||
>
|
>
|
||||||
<Td>
|
<Td>
|
||||||
@@ -130,6 +139,14 @@ function PeopleRow({
|
|||||||
{hasAnyAction && (
|
{hasAnyAction && (
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
{isAuthorized("People", "updatePeople") && (
|
||||||
|
<DropdownItem
|
||||||
|
icon={IconCalendar2}
|
||||||
|
onClick={() => dialogRef.current?.open()}
|
||||||
|
>
|
||||||
|
{__("Set end of contract")}
|
||||||
|
</DropdownItem>
|
||||||
|
)}
|
||||||
{isAuthorized("People", "deletePeople") && (
|
{isAuthorized("People", "deletePeople") && (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
@@ -143,5 +160,6 @@ function PeopleRow({
|
|||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user