Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/admin/src/core/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ export const Routes = {
applications: {
toString: () => "/downloads/show/applications" as const,
},
exhibitors: {
toString: () => "/downloads/show/exhibitors" as const,
},
},
},
login: { toString: () => "/login" as const },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const ActionSchema = zu
message: "Veuillez entrer un nombre valide",
})
.int({ message: "Veuillez entrer un nombre entier" })
.min(1, "Veuillez entrer un nombre supérieur ou égal à 1"),
.min(0, "Veuillez entrer un nombre supérieur ou égal à 0"),

dividerCount: zu.coerce
.number({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { BaseLink } from "#i/core/base-link";
import { Paginator } from "#i/core/controllers/paginator";
import { SimpleEmpty } from "#i/core/data-display/empty";
import { Card } from "#i/core/layout/card";
import { DownloadExhibitorsTrigger } from "#i/routes/downloads.show.exhibitors/trigger.js";
import { ExhibitorSearchParams } from "#i/show/exhibitors/search-params";
import { useOptimisticSearchParams } from "@animeaux/search-params-io";
import { useLoaderData } from "@remix-run/react";
import type { loader } from "./loader.server";
import { Row, Rows } from "./rows";

export function CardList() {
const { totalCount, pageCount, exhibitors } = useLoaderData<typeof loader>();
const { totalCount, pageCount, exhibitors, canExport } =
useLoaderData<typeof loader>();
const [searchParams] = useOptimisticSearchParams();

return (
Expand All @@ -19,6 +21,15 @@ export function CardList() {
<Card.Title>
{totalCount} {totalCount > 1 ? "exposants" : "exposant"}
</Card.Title>

{canExport ? (
<DownloadExhibitorsTrigger asChild>
<Action variant="text" color="gray">
<Action.Icon href="icon-download-solid" />
Exporter
</Action>
</DownloadExhibitorsTrigger>
) : null}
</Card.Header>

<Card.Content hasListItems>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { db } from "#i/core/db.server";
import { PageSearchParams } from "#i/core/search-params";
import { assertCurrentUserHasGroups } from "#i/current-user/groups.server";
import { ExhibitorSearchParams } from "#i/show/exhibitors/search-params";
import { hasGroups } from "#i/users/groups.js";
import { UserGroup } from "@animeaux/prisma/server";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
Expand Down Expand Up @@ -56,5 +57,14 @@ export async function loader({ request }: LoaderFunctionArgs) {

const pageCount = Math.ceil(totalCount / EXHIBITOR_COUNT_PER_PAGE);

return json({ totalCount, pageCount, exhibitors, dividerTypes, standSizes });
const canExport = hasGroups(currentUser, [UserGroup.ADMIN]);

return json({
totalCount,
pageCount,
exhibitors,
dividerTypes,
standSizes,
canExport,
});
}
97 changes: 97 additions & 0 deletions apps/admin/src/routes/downloads.show.exhibitors/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { db } from "#i/core/db.server.js";
import { assertCurrentUserHasGroups } from "#i/current-user/groups.server.js";
import { ExhibitorSearchParams } from "#i/show/exhibitors/search-params.js";
import type { Prisma } from "@animeaux/prisma";
import { UserGroup } from "@animeaux/prisma";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { csvFormatRows } from "d3-dsv";

const exhibitorSelect = {
appetizerPeopleCount: true,
breakfastPeopleCountSaturday: true,
breakfastPeopleCountSunday: true,
chairCount: true,
dividerCount: true,
dividerType: { select: { label: true } },
dogs: { select: { id: true } },
hasTableCloths: true,
name: true,
tableCount: true,
} satisfies Prisma.ShowExhibitorSelect;

export async function loader({ request }: LoaderFunctionArgs) {
const currentUser = await db.currentUser.get(request, {
select: { groups: true },
});

assertCurrentUserHasGroups(currentUser, [UserGroup.ADMIN]);

const searchParams = new URL(request.url).searchParams;

const { exhibitors } = await db.show.exhibitor.findMany({
searchParams: ExhibitorSearchParams.io.parse(searchParams),
select: exhibitorSelect,
});

return new Response(
csvFormatRows([
columns.map((column) => column.label),

...exhibitors.map((exhibitors) =>
columns.map((column) => column.accessor(exhibitors)),
),
]),
);
}

type ColumnDefinition = {
label: string;
accessor: (
exhibitors: Prisma.ShowExhibitorGetPayload<{
select: typeof exhibitorSelect;
}>,
) => string;
};

const columns: ColumnDefinition[] = [
{
label: "Nom",
accessor: (exhibitor) => exhibitor.name,
},
{
label: "Type de cloisons",
accessor: (exhibitor) => exhibitor.dividerType?.label ?? "",
},
{
label: "Nombre de cloisons",
accessor: (exhibitor) => String(exhibitor.dividerCount),
},
{
label: "Nombre de tables",
accessor: (exhibitor) => String(exhibitor.tableCount),
},
{
label: "Nappage",
accessor: (exhibitor) => (exhibitor.hasTableCloths ? "Oui" : "Non"),
},
{
label: "Nombre de chaises",
accessor: (exhibitor) => String(exhibitor.chairCount),
},
{
label: "Chiens sur stand",
accessor: (exhibitor) => (exhibitor.dogs.length > 0 ? "Oui" : "Non"),
},
{
label: "Nombre de pdj samedi",
accessor: (exhibitor) => String(exhibitor.breakfastPeopleCountSaturday),
},
{
label: "Nombre de pdj dimanche",
accessor: (exhibitor) => String(exhibitor.breakfastPeopleCountSunday),
},
{
label: "Nombre d’apéro",
accessor: (exhibitor) => String(exhibitor.appetizerPeopleCount),
},
];
34 changes: 34 additions & 0 deletions apps/admin/src/routes/downloads.show.exhibitors/trigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { DownloadTrigger } from "#i/core/actions/download-trigger.js";
import { Routes } from "#i/core/navigation";
import { ExhibitorSearchParams } from "#i/show/exhibitors/search-params.js";
import { useOptimisticSearchParams } from "@animeaux/search-params-io";
import { createPath } from "history";
import { forwardRef } from "react";
import type { Except } from "type-fest";

export const DownloadExhibitorsTrigger = forwardRef<
React.ComponentRef<typeof DownloadTrigger>,
Except<
React.ComponentPropsWithoutRef<typeof DownloadTrigger>,
"url" | "fileName"
>
>(function DownloadExhibitorsTrigger(props, ref) {
const [searchParams] = useOptimisticSearchParams();

const exhibitorSearchParams = ExhibitorSearchParams.io.copy(
searchParams,
new URLSearchParams(),
);

return (
<DownloadTrigger
{...props}
ref={ref}
fileName="exposants.csv"
url={createPath({
pathname: Routes.downloads.show.exhibitors.toString(),
search: exhibitorSearchParams.toString(),
})}
/>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function createActionSchema({
message: "Veuillez entrer un nombre valide",
})
.int({ message: "Veuillez entrer un nombre entier" })
.min(1, "Veuillez entrer un nombre supérieur ou égal à 1"),
.min(0, "Veuillez entrer un nombre supérieur ou égal à 0"),
dividerCount: zu.coerce
.number({ message: "Veuillez entrer un nombre valide" })
.int({ message: "Veuillez entrer un nombre entier" })
Expand Down
Loading