add delete modal
All checks were successful
Deploy Amap / deploy (push) Successful in 40s

This commit is contained in:
Julien Aldon
2026-03-06 15:19:07 +01:00
parent e970bb683a
commit 61710a0347
12 changed files with 216 additions and 15 deletions

View File

@@ -30,6 +30,31 @@ async def get_forms_filtered(
return service.get_all(session, seasons, productors, current_season, user) return service.get_all(session, seasons, productors, current_season, user)
@router.get(
'/{_id}/preview-delete',
response_model=list[models.DeleteDependency]
)
async def preview_delete(
_id: int,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user),
):
if not service.is_allowed(session, user, _id=_id):
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('forms', 'delete')
)
try:
result = service.get_delete_dependencies(
session,
_id
)
print(result)
except exceptions.FormNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result
@router.get('/{_id}', response_model=models.FormPublic) @router.get('/{_id}', response_model=models.FormPublic)
async def get_form( async def get_form(
_id: int, _id: int,
@@ -68,7 +93,7 @@ async def create_form(
@router.put('/{_id}', response_model=models.FormPublic) @router.put('/{_id}', response_model=models.FormPublic)
async def update_form( async def update_form(
_id: int, _id: int,
form: models.FormUpdate, form: models.FormUpdate,
user: models.User = Depends(get_current_user), user: models.User = Depends(get_current_user),
session: Session = Depends(get_session) session: Session = Depends(get_session)

View File

@@ -107,6 +107,44 @@ def delete_one(session: Session, _id: int) -> models.FormPublic:
return result return result
def get_delete_dependencies(
session: Session,
_id: int
) -> list[models.DeleteDependency]:
statement = select(models.Form).where(models.Form.id == _id)
result = session.exec(statement)
form = result.first()
if not form:
raise exceptions.FormNotFoundError(messages.Messages.not_found('form'))
print(_id)
statement_shipment = (
select(models.Shipment)
.where(models.Shipment.form_id == _id)
.distinct()
)
statement_contracts = (
select(models.Contract)
.where(models.Contract.form_id == _id)
.distinct()
)
shipments = session.exec(statement_shipment).all()
contracts = session.exec(statement_contracts).all()
result = [
models.DeleteDependency(
name=sh.name,
id=sh.id,
type='shipment'
) for sh in shipments
] + [
models.DeleteDependency(
name=f'{co.firstname} {co.lastname}',
id=co.id,
type='contract'
) for co in contracts
]
return result
def is_allowed( def is_allowed(
session: Session, session: Session,
user: models.User, user: models.User,

View File

@@ -5,6 +5,12 @@ from typing import Optional
from sqlmodel import Column, Field, LargeBinary, Relationship, SQLModel from sqlmodel import Column, Field, LargeBinary, Relationship, SQLModel
class DeleteDependency(SQLModel):
id: int
name: str
type: str
class ContractType(SQLModel, table=True): class ContractType(SQLModel, table=True):
id: int | None = Field( id: int | None = Field(
default=None, default=None,

View File

@@ -138,11 +138,7 @@ def is_allowed(
return False return False
if not _id: if not _id:
statement = ( statement = (
select(models.Shipment) select(models.Form)
.join(
models.Form,
models.Shipment.form_id == models.Form.id
)
.where(models.Form.id == shipment.form_id) .where(models.Form.id == shipment.form_id)
) )
form = session.exec(statement).first() form = session.exec(statement).first()
@@ -162,4 +158,3 @@ def is_allowed(
.distinct() .distinct()
) )
return len(session.exec(statement).all()) > 0 return len(session.exec(statement).all()) > 0

View File

@@ -84,6 +84,8 @@
"once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page": "once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page", "once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page": "once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page",
"by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form": "by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form", "by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form": "by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form",
"contracts": "contracts", "contracts": "contracts",
"contract": "contract",
"user": "user",
"hidden": "hidden", "hidden": "hidden",
"visible": "visible", "visible": "visible",
"minimum price for this shipment should be at least": "minimum price for this shipment should be at least", "minimum price for this shipment should be at least": "minimum price for this shipment should be at least",
@@ -179,6 +181,10 @@
"grams": "grams (g)", "grams": "grams (g)",
"kilo": "kilograms (kg)", "kilo": "kilograms (kg)",
"liter": "liters (L)", "liter": "liters (L)",
"are you sure you want to delete": "are you sure you want to delete",
"this will also delete": "this will also delete",
"delete entity": "delete {{entity}}",
"delete": "delete",
"success": "success", "success": "success",
"success edit": "{{entity}} correctly edited", "success edit": "{{entity}} correctly edited",
"success create": "{{entity}} correctly created", "success create": "{{entity}} correctly created",

View File

@@ -81,6 +81,8 @@
"there is": "il y a", "there is": "il y a",
"for this contract": "pour ce contrat.", "for this contract": "pour ce contrat.",
"remove shipment": "supprimer la livraison", "remove shipment": "supprimer la livraison",
"contract": "contrat",
"user": "utilisateur·trice",
"productors": "producteur·trices", "productors": "producteur·trices",
"products": "produits", "products": "produits",
"templates": "modèles", "templates": "modèles",
@@ -179,6 +181,10 @@
"grams": "grammes (g)", "grams": "grammes (g)",
"kilo": "kilogrammes (kg)", "kilo": "kilogrammes (kg)",
"liter": "litres (L)", "liter": "litres (L)",
"are you sure you want to delete": "êtes vous sûr de vouloir supprimer",
"this will also delete": "cette action supprimera aussi",
"delete entity": "supprimer un {{entity}}",
"delete": "supprimer",
"success": "succès", "success": "succès",
"success edit": "{{entity}} correctement édité", "success edit": "{{entity}} correctement édité",
"success create": "{{entity}} correctement créé", "success create": "{{entity}} correctement créé",

View File

@@ -0,0 +1,76 @@
import { t } from "@/config/i18n";
import type { DeleteDependencies } from "@/services/resources/common";
import { Button, Group, List, Modal, Text, type ModalBaseProps } from "@mantine/core";
import { IconCancel, IconCheck } from "@tabler/icons-react";
import { Link } from "react-router";
export type DeleteModalProps = ModalBaseProps & {
handleSubmit: (id: number) => void;
entityType: string;
entity: {name: string, id: number};
dependencies: DeleteDependencies[];
}
export function DeleteModal({
opened,
onClose,
handleSubmit,
entityType,
entity,
dependencies
}: DeleteModalProps) {
return (
<Modal
opened={opened}
onClose={onClose}
title={t("delete entity", { capfirst: true, entity: t(entityType)})}
>
<Text>{`${t("are you sure you want to delete", {capfirst: true})} : "${entity.name}"`}</Text>
<Text>{`${t("this will also delete", {capfirst: true})} :`}</Text>
{
<List>
{
dependencies?.map((dependency) => (
<List.Item
key={dependency.id}
>
{
dependency.type === 'contract' ? `${t(dependency.type, {capfirst: true})} - ${dependency.name}` :
<Link
to={`/dashboard/${dependency.type}s/${dependency.id}/edit`}
target="_blank"
rel="noopener noreferrer"
>
{`${t(dependency.type, {capfirst: true})} - ${dependency.name}`}
</Link>
}
</List.Item>
))
}
</List>
}
<Group mt="sm" justify="space-between">
<Button
variant="filled"
color="red"
aria-label={t("cancel", { capfirst: true })}
leftSection={<IconCancel />}
onClick={onClose}
>
{t("cancel", { capfirst: true })}
</Button>
<Button
variant="filled"
aria-label={t("delete entity", { capfirst: true, entity: t(entityType)})}
leftSection={<IconCheck />}
onClick={() => {
handleSubmit(entity.id);
}}
>
{t("delete", { capfirst: true})}
</Button>
</Group>
</Modal>
);
}

View File

@@ -1,9 +1,11 @@
import { ActionIcon, Badge, Table, Tooltip } from "@mantine/core"; import { ActionIcon, Badge, Table, Tooltip } from "@mantine/core";
import { useNavigate, useSearchParams } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
import { useDeleteForm } from "@/services/api"; import { useDeleteForm, useGetDeleteDependencies } from "@/services/api";
import { IconEdit, IconX } from "@tabler/icons-react"; import { IconEdit, IconX } from "@tabler/icons-react";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import type { Form } from "@/services/resources/forms"; import type { Form } from "@/services/resources/forms";
import { DeleteModal } from "@/components/DeleteModal";
import { useDisclosure } from "@mantine/hooks";
export type FormRowProps = { export type FormRowProps = {
form: Form; form: Form;
@@ -13,7 +15,8 @@ export default function FormRow({ form }: FormRowProps) {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const deleteMutation = useDeleteForm(); const deleteMutation = useDeleteForm();
const navigate = useNavigate(); const navigate = useNavigate();
const [deleteOpened, { open: deleteOpen, close: deleteClose }] = useDisclosure(false);
const {data: deleteDependencies} = useGetDeleteDependencies("form", form.id);
return ( return (
<Table.Tr key={form.id}> <Table.Tr key={form.id}>
<Table.Td> <Table.Td>
@@ -29,7 +32,7 @@ export default function FormRow({ form }: FormRowProps) {
<Table.Td>{form.productor.name}</Table.Td> <Table.Td>{form.productor.name}</Table.Td>
<Table.Td>{form.referer.name}</Table.Td> <Table.Td>{form.referer.name}</Table.Td>
<Table.Td> <Table.Td>
<Tooltip label={t("edit productor", { capfirst: true })}> <Tooltip label={t("edit form", { capfirst: true })}>
<ActionIcon <ActionIcon
size="sm" size="sm"
mr="5" mr="5"
@@ -43,14 +46,22 @@ export default function FormRow({ form }: FormRowProps) {
<IconEdit /> <IconEdit />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label={t("remove productor", { capfirst: true })}> <DeleteModal
opened={deleteOpened}
onClose={deleteClose}
handleSubmit={(id: number) => {
deleteMutation.mutate(id);
}}
entityType={"form"}
entity={form}
dependencies={deleteDependencies || []}
/>
<Tooltip label={t("remove form", { capfirst: true })}>
<ActionIcon <ActionIcon
color="red" color="red"
size="sm" size="sm"
mr="5" mr="5"
onClick={() => { onClick={deleteOpen}
deleteMutation.mutate(form.id);
}}
> >
<IconX /> <IconX />
</ActionIcon> </ActionIcon>

View File

@@ -36,12 +36,14 @@ export function Forms() {
const { data: allForms } = useGetReferentForms(); const { data: allForms } = useGetReferentForms();
const seasons = useMemo(() => { const seasons = useMemo(() => {
if (!allForms) return [];
return allForms return allForms
?.map((form: Form) => form.season) ?.map((form: Form) => form.season)
.filter((season, index, array) => array.indexOf(season) === index); .filter((season, index, array) => array.indexOf(season) === index);
}, [allForms]); }, [allForms]);
const productors = useMemo(() => { const productors = useMemo(() => {
if (!allForms) return [];
return allForms return allForms
?.map((form: Form) => form.productor.name) ?.map((form: Form) => form.productor.name)
.filter((productor, index, array) => array.indexOf(productor) === index); .filter((productor, index, array) => array.indexOf(productor) === index);

View File

@@ -151,7 +151,7 @@ export default function Shipments() {
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{shipments.map((shipment) => ( {shipments?.map((shipment) => (
<ShipmentRow shipment={shipment} key={shipment.id} /> <ShipmentRow shipment={shipment} key={shipment.id} />
))} ))}
</Table.Tbody> </Table.Tbody>

View File

@@ -24,6 +24,7 @@ import type { Product, ProductCreate, ProductEditPayload } from "./resources/pro
import type { Contract, ContractCreate } from "./resources/contracts"; import type { Contract, ContractCreate } from "./resources/contracts";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import type { DeleteDependencies, EntityName } from "./resources/common";
export async function refreshToken() { export async function refreshToken() {
return await fetch(`${Config.backend_uri}/auth/refresh`, {method: "POST", credentials: "include"}); return await fetch(`${Config.backend_uri}/auth/refresh`, {method: "POST", credentials: "include"});
@@ -321,6 +322,25 @@ export function useGetForm(
}); });
} }
export function useGetDeleteDependencies(
entity: EntityName,
id?: number,
) {
return useQuery<DeleteDependencies[]>({
queryKey: [`${entity}_delete_preview`],
queryFn: () =>
fetchWithAuth(`${Config.backend_uri}/${entity}s/${id}/preview-delete`, {
credentials: "include",
}).then((res) => {
const result = res.json()
console.log(result)
return result
}),
enabled: !!id,
});
}
export function useGetForms(filters?: URLSearchParams): UseQueryResult<Form[], Error> { export function useGetForms(filters?: URLSearchParams): UseQueryResult<Form[], Error> {
const queryString = filters?.toString(); const queryString = filters?.toString();
return useQuery<Form[]>({ return useQuery<Form[]>({

View File

@@ -0,0 +1,16 @@
export const ENTITY_NAMES = [
'contract',
'form',
'productor',
'product',
'shipment',
'user',
]
export type EntityName = (typeof ENTITY_NAMES)[number];
export type DeleteDependencies = {
name: string;
id: number;
type: EntityName;
}