Skip to content

Commit

Permalink
feat: diplomas download bulk
Browse files Browse the repository at this point in the history
  • Loading branch information
DJ1TJOO committed Aug 16, 2024
1 parent 269bd4e commit a46fba0
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use server";

import { redirect } from "next/navigation";
import { storeCertificateHandles } from "~/lib/nwd";

export async function kickOffGeneratePDF({
handles,
fileName,
sort,
}: {
handles: string[];
fileName: string;
sort: "student" | "instructor";
}) {
const uuid = await storeCertificateHandles({ handles, fileName, sort });

redirect(`/api/export/certificate/pdf/${uuid}`);
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,183 @@
import type { Row } from "@tanstack/react-table";
import { useState } from "react";
import {
Alert,
AlertBody,
AlertDescription,
AlertTitle,
} from "~/app/(dashboard)/_components/alert";
import {
Dropdown,
DropdownButton,
DropdownItem,
DropdownLabel,
DropdownMenu,
} from "~/app/(dashboard)/_components/dropdown";

import { useFormState as useActionState, useFormStatus } from "react-dom";

import {
Disclosure as HeadlessDisclosure,
DisclosureButton as HeadlessDisclosureButton,
DisclosurePanel as HeadlessDisclosurePanel,
} from "@headlessui/react";
import { ChevronRightIcon } from "@heroicons/react/16/solid";
import { toast } from "sonner";
import { z } from "zod";
import { AlertActions } from "~/app/(dashboard)/_components/alert";
import { Button } from "~/app/(dashboard)/_components/button";
import type { listCertificates } from "~/lib/nwd";
import {
Description,
Field,
Fieldset,
Label,
Legend,
} from "~/app/(dashboard)/_components/fieldset";
import { Subheading } from "~/app/(dashboard)/_components/heading";
import { Input } from "~/app/(dashboard)/_components/input";
import {
Radio,
RadioField,
RadioGroup,
} from "~/app/(dashboard)/_components/radio";
import { Text } from "~/app/(dashboard)/_components/text";
import Spinner from "~/app/_components/spinner";
import dayjs from "~/lib/dayjs";
import { kickOffGeneratePDF } from "../_actions/download";

interface Props {
rows: {
handle: string;
}[];
}

export function ActionButtons(props: Props) {
const [isDialogOpen, setIsDialogOpen] = useState<string | null>(null);

return (
<>
<Dropdown>
<DropdownButton aria-label="Bulk actie">Bulk actie</DropdownButton>
<DropdownMenu anchor="top">
<DropdownItem onClick={() => setIsDialogOpen("download")}>
<DropdownLabel>Diploma's downloaden</DropdownLabel>
</DropdownItem>
</DropdownMenu>
</Dropdown>

<DownloadCertificatesDialog
{...props}
isOpen={isDialogOpen === "download"}
setIsOpen={(value) => setIsDialogOpen(value ? "download" : null)}
/>
</>
);
}

function DownloadCertificatesDialog({
rows,
isOpen,
setIsOpen,
}: Props & {
isOpen: boolean;
setIsOpen: (value: boolean) => void;
}) {
const submit = async (_prevState: unknown, formData: FormData) => {
const advancedOptionsSchema = z.object({
filename: z.string().catch(`${dayjs().toISOString()}-export-diplomas`),
sort: z.enum(["student", "instructor"]).catch("student"),
});

const advancedOptions = advancedOptionsSchema.parse({
filename: formData.get("filename"),
sort: formData.get("sort"),
});

type Certificate = Awaited<ReturnType<typeof listCertificates>>[number];
export function Download({ rows }: { rows: Row<Certificate>[] }) {
const params = new URLSearchParams();
try {
await kickOffGeneratePDF({
handles: rows.map((row) => row.handle),
fileName: advancedOptions.filename,
sort: advancedOptions.sort,
});

rows.forEach((row) => {
if (row.getIsSelected()) {
params.append("certificate[]", row.original.handle);
toast.success("Bestand gedownload");
setIsOpen(false);
} catch (error) {
toast.error("Er is iets misgegaan");
}
});
};

const [_state, formAction] = useActionState(submit, undefined);

return (
<>
<Alert open={isOpen} onClose={setIsOpen} size="lg">
<AlertTitle>Diploma's downloaden</AlertTitle>
<AlertDescription>
Download een PDF-bestand met de diploma's van de geselecteerde
cursisten.
</AlertDescription>
<form action={formAction}>
<AlertBody>
<HeadlessDisclosure>
<HeadlessDisclosureButton className="flex">
<div className="mr-6 flex h-6 items-center justify-center">
<ChevronRightIcon className="h-3.5 w-3.5 shrink-0 transition-transform ui-open:rotate-90" />
</div>
<Subheading>Geavanceerde opties</Subheading>
</HeadlessDisclosureButton>
<HeadlessDisclosurePanel className="mt-2 pl-10">
<Field>
<Label>Bestandsnaam</Label>
<Input
name="filename"
type="text"
required
defaultValue={`${dayjs().toISOString()}-export-diplomas`}
/>
</Field>

<Fieldset className="mt-6">
<Legend>Sortering</Legend>
<Text>
Hoe moeten de diploma's in de PDF gesorteerd zijn?
</Text>
<RadioGroup name="sort" defaultValue="student">
<RadioField>
<Radio value="student" />
<Label>Naam cursist</Label>
<Description>Sortering op voornaam, A tot Z.</Description>
</RadioField>
<RadioField>
<Radio value="instructor" />
<Label>Naam instructeur</Label>
<Description>
Sortering op voornaam instructeur, A tot Z. Diploma's
zonder instructeur worden als laatste getoond.
</Description>
</RadioField>
</RadioGroup>
</Fieldset>
</HeadlessDisclosurePanel>
</HeadlessDisclosure>
</AlertBody>
<AlertActions>
<Button plain onClick={() => setIsOpen(false)}>
Annuleren
</Button>
<DownloadSubmitButton />
</AlertActions>
</form>
</Alert>
</>
);
}

function DownloadSubmitButton() {
const { pending } = useFormStatus();
return (
<Button
plain
href={`/api/export/certificate/pdf?${params.toString()}`}
target="_blank"
>
Download PDF
<Button color="branding-dark" disabled={pending} type="submit">
{pending ? <Spinner className="text-white" /> : null}
Download
</Button>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { ChevronDownIcon } from "@heroicons/react/16/solid";
import { XMarkIcon } from "@heroicons/react/16/solid";
import type { RowSelectionState } from "@tanstack/react-table";
import {
createColumnHelper,
Expand All @@ -11,15 +11,11 @@ import {
import clsx from "clsx";
import Link from "next/link";
import { useState } from "react";
import { Button } from "~/app/(dashboard)/_components/button";
import {
Checkbox,
CheckboxField,
} from "~/app/(dashboard)/_components/checkbox";
import {
Popover,
PopoverButton,
PopoverPanel,
} from "~/app/(dashboard)/_components/popover";
import {
Table,
TableBody,
Expand All @@ -36,7 +32,7 @@ import {
import { Code } from "~/app/(dashboard)/_components/text";
import dayjs from "~/lib/dayjs";
import type { listCertificates } from "~/lib/nwd";
import { Download } from "./table-actions";
import { ActionButtons } from "./table-actions";

type Certificate = Awaited<ReturnType<typeof listCertificates>>[number];

Expand Down Expand Up @@ -150,25 +146,14 @@ export default function CertificateTable({
},
});

const anyRowSelected =
table.getIsAllRowsSelected() || table.getIsSomeRowsSelected();
const selectedRows = Object.keys(rowSelection).length;

return (
<div className="mt-8 relative">
<Table
dense
className="[--gutter:theme(spacing.6)] lg:[--gutter:theme(spacing.10)]"
>
{anyRowSelected ? (
<Popover className="absolute left-12 top-0 flex items-center space-x-2">
<PopoverButton color="branding-orange">
Acties <ChevronDownIcon />
</PopoverButton>
<PopoverPanel anchor="bottom start">
<Download rows={table.getSelectedRowModel().rows} />
</PopoverPanel>
</Popover>
) : null}
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
Expand Down Expand Up @@ -231,6 +216,30 @@ export default function CertificateTable({
/>
<TablePagination totalItems={totalItems} />
</TableFooter>

<div
className={clsx(
"fixed inset-x-0 bottom-14 mx-auto flex w-fit items-center space-x-2 rounded-lg border border-gray-200 bg-white p-2 shadow-md dark:border-gray-800 dark:bg-gray-950",
selectedRows > 0 ? "" : "hidden",
)}
>
<p className="select-none text-sm">
<span className="rounded bg-branding-light/10 px-2 py-1.5 font-medium tabular-nums text-branding-dark">
{selectedRows}
</span>
<span className="ml-2 font-medium text-gray-900 dark:text-gray-50">
geselecteerd
</span>
</p>
<div className="flex items-center space-x-4">
<Button plain onClick={() => setRowSelection({})}>
<XMarkIcon />
</Button>
<ActionButtons
rows={table.getRowModel().rows.map((row) => row.original)}
/>
</div>
</div>
</div>
);
}

0 comments on commit a46fba0

Please sign in to comment.