From 4a046ea646aefd3c90d45e4ee6ed4c720a07b2ff Mon Sep 17 00:00:00 2001 From: sbellavoine Date: Mon, 4 May 2026 14:36:28 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=F0=9F=8F=A2=20Archive=20foster=20fami?= =?UTF-8?q?lies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/animals/status.tsx | 1 + apps/admin/src/core/data-display/chip.tsx | 11 +- apps/admin/src/core/data-display/helper.tsx | 6 +- apps/admin/src/foster-families/db.server.ts | 1 + .../route.tsx | 143 +++++++++++++++++- .../loader.server.ts | 1 + .../routes/_layout.foster-families._index.tsx | 10 ++ .../routes/resources.foster-family/route.tsx | 2 +- .../migration.sql | 2 + libs/prisma/schema.prisma | 1 + 10 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 libs/prisma/migrations/20260325102108_add_foster_family_is_archived/migration.sql diff --git a/apps/admin/src/animals/status.tsx b/apps/admin/src/animals/status.tsx index 395242b6..fd7d05cc 100644 --- a/apps/admin/src/animals/status.tsx +++ b/apps/admin/src/animals/status.tsx @@ -91,6 +91,7 @@ export function StatusIcon({ const CHIP_COLOR_STATUS_ICON_CLASS_NAMES: Record = { green: cn("text-green-600"), black: cn("text-gray-800"), + grey: cn("text-gray-500"), red: cn("text-red-500"), blue: cn("text-blue-500"), yellow: cn("text-yellow-400"), diff --git a/apps/admin/src/core/data-display/chip.tsx b/apps/admin/src/core/data-display/chip.tsx index d3729b90..8599c4b7 100644 --- a/apps/admin/src/core/data-display/chip.tsx +++ b/apps/admin/src/core/data-display/chip.tsx @@ -5,7 +5,14 @@ import { Icon } from "#i/generated/icon.js" export type ChipVariant = "primary" | "secondary" -export type ChipColor = "black" | "blue" | "green" | "orange" | "red" | "yellow" +export type ChipColor = + | "black" + | "blue" + | "green" + | "grey" + | "orange" + | "red" + | "yellow" export function Chip({ variant, @@ -42,6 +49,7 @@ const COLOR_CLASS_NAME: Record> = { black: cn("bg-gray-800 text-white"), blue: cn("bg-blue-500 text-white"), green: cn("bg-green-600 text-white"), + grey: cn("bg-gray-500 text-white"), orange: cn("bg-orange-500 text-white"), red: cn("bg-red-500 text-white"), yellow: cn("bg-yellow-400 text-gray-800"), @@ -51,6 +59,7 @@ const COLOR_CLASS_NAME: Record> = { black: cn("bg-gray-100 text-gray-800"), blue: cn("bg-blue-50 text-blue-500"), green: cn("bg-green-50 text-green-600"), + grey: cn("bg-gray-100 text-gray-500"), orange: cn("bg-orange-50 text-orange-500"), red: cn("bg-red-50 text-red-500"), yellow: cn("bg-yellow-50 text-yellow-700"), diff --git a/apps/admin/src/core/data-display/helper.tsx b/apps/admin/src/core/data-display/helper.tsx index 75b72d9c..e00635f3 100644 --- a/apps/admin/src/core/data-display/helper.tsx +++ b/apps/admin/src/core/data-display/helper.tsx @@ -5,7 +5,7 @@ import { Action } from "#i/core/actions.js" import type { IconName } from "#i/generated/icon.js" import { Icon } from "#i/generated/icon.js" -type HelperVariant = "error" | "info" | "success" | "warning" +type HelperVariant = "error" | "info" | "neutral" | "success" | "warning" type BlockHelperProps = { children?: React.ReactNode @@ -34,6 +34,7 @@ export function BlockHelper({ children, icon, variant }: BlockHelperProps) { const BLOCK_VARIANT_CLASS_NAME: Record = { error: cn("border-red-100"), info: cn("border-blue-100"), + neutral: cn("border-gray-100"), success: cn("border-green-100"), warning: cn("border-orange-100"), } @@ -76,6 +77,7 @@ export function InlineHelper({ const VARIANT_ACTION_COLOR: Record = { error: "red", info: "blue", + neutral: "gray", success: "green", warning: "orange", } @@ -113,6 +115,7 @@ export function DenseHelper({ const VARIANT_CLASS_NAME: Record = { error: cn("bg-red-50 text-red-500"), info: cn("bg-blue-50 text-blue-500"), + neutral: cn("bg-gray-200 text-gray-500"), success: cn("bg-green-50 text-green-600"), warning: cn("bg-orange-50 text-orange-500"), } @@ -120,6 +123,7 @@ const VARIANT_CLASS_NAME: Record = { const VARIANT_ICON: Record = { error: "icon-circle-exclamation-solid", info: "icon-circle-info-solid", + neutral: "icon-triangle-exclamation-solid", success: "icon-circle-check-solid", warning: "icon-triangle-exclamation-solid", } diff --git a/apps/admin/src/foster-families/db.server.ts b/apps/admin/src/foster-families/db.server.ts index 6f2f122d..e396230e 100644 --- a/apps/admin/src/foster-families/db.server.ts +++ b/apps/admin/src/foster-families/db.server.ts @@ -363,6 +363,7 @@ type DataUpdate = { email?: string garden?: FosterFamilyGarden housing?: FosterFamilyHousing + isArchived?: boolean isBanned?: boolean phone?: string speciesAlreadyPresent?: Species[] diff --git a/apps/admin/src/routes/_layout.foster-families.$id._index/route.tsx b/apps/admin/src/routes/_layout.foster-families.$id._index/route.tsx index c3471f0e..b6601717 100644 --- a/apps/admin/src/routes/_layout.foster-families.$id._index/route.tsx +++ b/apps/admin/src/routes/_layout.foster-families.$id._index/route.tsx @@ -78,6 +78,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { garden: true, housing: true, id: true, + isArchived: true, isBanned: true, phone: true, speciesAlreadyPresent: true, @@ -125,12 +126,24 @@ export const meta: MetaFunction = ({ data }) => { return [{ title: getPageTitle(fosterFamily.displayName) }] } +const SharedActionFormData = FormDataDelegate.create( + zu.object({ + action: zu.union([zu.literal("ARCHIVE"), zu.literal("BAN")]), + }), +) + const BanActionFormData = FormDataDelegate.create( zu.object({ isBanned: zu.checkbox(), }), ) +const ArchiveActionFormData = FormDataDelegate.create( + zu.object({ + isArchived: zu.checkbox(), + }), +) + export async function action({ request, params }: ActionFunctionArgs) { const currentUser = await db.currentUser.get(request, { select: { id: true, groups: true }, @@ -153,9 +166,23 @@ export async function action({ request, params }: ActionFunctionArgs) { }) } - return await actionBan({ + const rawFormData = await request.formData() + const formData = SharedActionFormData.safeParse(rawFormData) + if (!formData.success) { + throw badRequest() + } + + if (formData.data.action === "BAN") { + return await actionBan({ + fosterFamilyId: paramsResult.data.id, + formData: rawFormData, + currentUser, + }) + } + + return await actionArchive({ fosterFamilyId: paramsResult.data.id, - request, + formData: rawFormData, currentUser, }) } @@ -197,23 +224,55 @@ async function actionDelete({ throw redirect(Routes.fosterFamilies.toString()) } +async function actionArchive({ + fosterFamilyId, + currentUser, + formData, +}: { + fosterFamilyId: FosterFamily["id"] + currentUser: { id: string } + formData: FormData +}) { + const parsedData = ArchiveActionFormData.safeParse(formData) + if (!parsedData.success) { + throw badRequest() + } + + try { + await db.fosterFamily.update( + fosterFamilyId, + { isArchived: parsedData.data.isArchived }, + currentUser, + ) + } catch (error) { + if (error instanceof NotFoundError) { + throw notFound() + } + + throw error + } + + return json({}) +} + async function actionBan({ fosterFamilyId, currentUser, - request, -}: Pick & { + formData, +}: { fosterFamilyId: FosterFamily["id"] currentUser: { id: string } + formData: FormData }) { - const formData = BanActionFormData.safeParse(await request.formData()) - if (!formData.success) { + const parsedData = BanActionFormData.safeParse(formData) + if (!parsedData.success) { throw badRequest() } try { await db.fosterFamily.update( fosterFamilyId, - { isBanned: formData.data.isBanned }, + { isBanned: parsedData.data.isBanned }, currentUser, ) } catch (error) { @@ -242,6 +301,11 @@ export default function Route() { {fosterFamily.displayName} est actuellement banni. ) : null} + {fosterFamily.isArchived ? ( + + {fosterFamily.displayName} est actuellement archivé. + + ) : null}
@@ -426,6 +490,7 @@ function ActionsCard() {
+
@@ -475,6 +540,8 @@ function ActionBan() { Annuler + + {!fosterFamily.isBanned ? ( () + const fetcher = useFetcher() + const [isDialogOpened, setIsDialogOpened] = useState(false) + + const done = fetcher.state === "idle" && fetcher.data != null + useEffect(() => { + if (done) { + setIsDialogOpened(false) + } + }, [done]) + + return ( + + + + + {fosterFamily.isArchived ? "Désarchiver" : "Archiver"} + + + + + + {fosterFamily.isArchived ? "Désarchiver" : "Archiver"}{" "} + {fosterFamily.displayName} + + + + Êtes-vous sûr de vouloir{" "} + {fosterFamily.isArchived ? "désarchiver" : "archiver"}{" "} + + {fosterFamily.displayName} + + {" "}? + + + + + + Annuler + + + + + {!fosterFamily.isArchived ? ( + + ) : null} + + + Oui, {fosterFamily.isArchived ? "désarchiver" : "archiver"} + + + + + + ) +} + function ActionDelete() { const { fosterFamily, fosterAnimalCount } = useLoaderData() const canDelete = fosterAnimalCount === 0 diff --git a/apps/admin/src/routes/_layout.foster-families.$id.edit/loader.server.ts b/apps/admin/src/routes/_layout.foster-families.$id.edit/loader.server.ts index d2f0f9c7..95315b21 100644 --- a/apps/admin/src/routes/_layout.foster-families.$id.edit/loader.server.ts +++ b/apps/admin/src/routes/_layout.foster-families.$id.edit/loader.server.ts @@ -37,6 +37,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { email: true, garden: true, housing: true, + isArchived: true, isBanned: true, phone: true, speciesAlreadyPresent: true, diff --git a/apps/admin/src/routes/_layout.foster-families._index.tsx b/apps/admin/src/routes/_layout.foster-families._index.tsx index 7abf3a07..d5b0c327 100644 --- a/apps/admin/src/routes/_layout.foster-families._index.tsx +++ b/apps/admin/src/routes/_layout.foster-families._index.tsx @@ -70,6 +70,7 @@ export async function loader({ request }: LoaderFunctionArgs) { displayName: true, fosterAnimals: { select: { avatar: true, name: true, id: true } }, id: true, + isArchived: true, isBanned: true, zipCode: true, }, @@ -225,6 +226,15 @@ function FosterFamilyItem({ {getShortLocation(fosterFamily)} + {fosterFamily.isArchived ? ( + + ) : null} + {fosterFamily.isBanned ? (