Update vendor compliance report UI

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-03-10 14:51:32 +01:00
parent 40e71489d8
commit 66a7d6c3e8
10 changed files with 379 additions and 383 deletions

View File

@@ -0,0 +1,175 @@
import { formatDatetime, todayAsDateInput } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
Dropzone,
Field,
Input,
Spinner,
useDialogRef,
} from "@probo/ui";
import { useState } from "react";
import { graphql } from "react-relay";
import { z } from "zod";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const uploadComplianceReportMutation = graphql`
mutation UploadComplianceReportDialogMutation(
$input: UploadVendorComplianceReportInput!
$connections: [ID!]!
) {
uploadVendorComplianceReport(input: $input) {
vendorComplianceReportEdge @appendEdge(connections: $connections) {
node {
id
reportName
reportDate
validUntil
file {
fileName
mimeType
size
downloadUrl
}
canDelete: permission(action: "core:vendor-compliance-report:delete")
}
}
}
}
`;
const schema = z.object({
reportDate: z.string().min(1, "Report date is required"),
validUntil: z.string().optional(),
});
type Props = {
children: React.ReactNode;
vendorId: string;
connectionId: string;
onSuccess?: () => void;
};
export function UploadComplianceReportDialog({
children,
vendorId,
connectionId,
onSuccess,
}: Props) {
const { __ } = useTranslate();
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const ref = useDialogRef();
const {
register,
handleSubmit,
formState: { isSubmitting },
reset,
} = useFormWithSchema(schema, {
defaultValues: {
reportDate: todayAsDateInput(),
validUntil: "",
},
});
const [mutate] = useMutationWithToasts(uploadComplianceReportMutation, {
successMessage: __("Compliance report uploaded successfully"),
errorMessage: __("Failed to upload compliance report"),
});
const handleDrop = (files: File[]) => {
if (files.length > 0) {
setUploadedFile(files[0]);
}
};
const onSubmit = async (data: z.infer<typeof schema>) => {
if (!uploadedFile) {
return;
}
await mutate({
variables: {
connections: [connectionId],
input: {
vendorId,
reportName: uploadedFile.name,
reportDate: `${data.reportDate}T00:00:00Z`,
validUntil: formatDatetime(data.validUntil),
file: null,
},
},
uploadables: {
"input.file": uploadedFile,
},
});
reset();
setUploadedFile(null);
onSuccess?.();
ref.current?.close();
};
const handleClose = () => {
reset();
setUploadedFile(null);
};
return (
<Dialog
title={__("Upload Compliance Report")}
ref={ref}
trigger={children}
className="max-w-lg"
onClose={handleClose}
>
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
<DialogContent padded className="space-y-4">
<Dropzone
description={__("Only PDF files up to 10MB are allowed")}
isUploading={isSubmitting}
onDrop={handleDrop}
accept={{
"application/pdf": [".pdf"],
}}
maxSize={10}
/>
{uploadedFile && (
<div className="p-3 bg-tertiary-subtle rounded-lg">
<p className="text-sm font-medium">
{__("Selected file")}
:
</p>
<p className="text-sm text-txt-secondary">{uploadedFile.name}</p>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<Field label={__("Report date")} required>
<Input {...register("reportDate")} type="date" required />
</Field>
<Field label={__("Valid until")}>
<Input {...register("validUntil")} type="date" />
</Field>
</div>
</DialogContent>
<DialogFooter>
<Button
type="submit"
disabled={isSubmitting || !uploadedFile}
icon={isSubmitting ? Spinner : undefined}
>
{__("Upload")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}

View File

@@ -1,11 +1,14 @@
import { fileSize, formatDate, sprintf } from "@probo/helpers";
import { downloadFile, fileSize, formatDate, sprintf } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import {
ActionDropdown,
Button,
DropdownItem,
Dropzone,
IconArrowDown,
IconPlusLarge,
IconTrashCan,
PageHeader,
Tbody,
Td,
Th,
@@ -14,7 +17,7 @@ import {
useConfirm,
} from "@probo/ui";
import type { ComponentProps } from "react";
import { useFragment, useMutation, useRefetchableFragment } from "react-relay";
import { useFragment, useRefetchableFragment } from "react-relay";
import { useOutletContext, useParams } from "react-router";
import { graphql } from "relay-runtime";
@@ -22,8 +25,10 @@ import type { ComplianceReportListQuery } from "#/__generated__/core/ComplianceR
import type { VendorComplianceTabFragment$key } from "#/__generated__/core/VendorComplianceTabFragment.graphql";
import type { VendorComplianceTabFragment_report$key } from "#/__generated__/core/VendorComplianceTabFragment_report.graphql";
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql";
import { SortableTable, SortableTh } from "#/components/SortableTable";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { UploadComplianceReportDialog } from "../dialogs/UploadComplianceReportDialog";
export const complianceReportsFragment = graphql`
fragment VendorComplianceTabFragment on Vendor
@@ -64,27 +69,12 @@ const complianceReportFragment = graphql`
fileName
mimeType
size
downloadUrl
}
canDelete: permission(action: "core:vendor-compliance-report:delete")
}
`;
const uploadReportMutation = graphql`
mutation VendorComplianceTabUploadReportMutation(
$input: UploadVendorComplianceReportInput!
$connections: [ID!]!
) {
uploadVendorComplianceReport(input: $input) {
vendorComplianceReportEdge @appendEdge(connections: $connections) {
node {
id
...VendorComplianceTabFragment_report
}
}
}
}
`;
const deleteReportMutation = graphql`
mutation VendorComplianceTabDeleteReportMutation(
$input: DeleteVendorComplianceReportInput!
@@ -109,45 +99,24 @@ export default function VendorComplianceTab() {
const { __ } = useTranslate();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const canDeleteSomeReport = reports.some(({ canDelete }) => canDelete);
usePageTitle(vendor.name + " - " + __("Compliance reports"));
const [mutate, isMutating] = useMutation(uploadReportMutation);
const handleDrop = (files: File[]) => {
const reportDate = new Date().toISOString();
for (const file of files) {
mutate({
variables: {
connections: [connectionId],
input: {
vendorId: vendor.id,
reportDate,
reportName: file.name,
file: null,
},
},
uploadables: {
"input.file": file,
},
});
}
};
return (
<div className="space-y-6">
{!isSnapshotMode && vendor.canUploadComplianceReport && (
<Dropzone
description={__("Only PDF files up to 10MB are allowed")}
isUploading={isMutating}
onDrop={handleDrop}
accept={{
"application/pdf": [".pdf"],
}}
maxSize={10}
/>
)}
<PageHeader
title={__("Compliance reports")}
description={__("Track vendor compliance certifications and reports.")}
>
{!isSnapshotMode && vendor.canUploadComplianceReport && (
<UploadComplianceReportDialog
vendorId={vendor.id}
connectionId={connectionId}
>
<Button icon={IconPlusLarge}>{__("Add report")}</Button>
</UploadComplianceReportDialog>
)}
</PageHeader>
<SortableTable
refetch={refetch as ComponentProps<typeof SortableTable>["refetch"]}
>
@@ -157,7 +126,7 @@ export default function VendorComplianceTab() {
<SortableTh field="REPORT_DATE">{__("Report date")}</SortableTh>
<Th>{__("Valid until")}</Th>
<Th>{__("File size")}</Th>
{!isSnapshotMode && canDeleteSomeReport && <Th>{__("Actions")}</Th>}
{!isSnapshotMode && reports.length > 0 && <Th>{__("Actions")}</Th>}
</Tr>
</Thead>
<Tbody>
@@ -221,16 +190,30 @@ function ReportRow(props: ReportRowProps) {
<Td>{formatDate(report.reportDate)}</Td>
<Td>{formatDate(report.validUntil)}</Td>
<Td>{fileSize(__, report.file?.size ?? 0)}</Td>
{!props.isSnapshotMode && report.canDelete && (
{!props.isSnapshotMode && (
<Td width={50} className="text-end">
<ActionDropdown>
<DropdownItem
icon={IconTrashCan}
onClick={handleDelete}
variant="danger"
>
{__("Delete")}
</DropdownItem>
{report.file?.downloadUrl && (
<DropdownItem
icon={IconArrowDown}
onClick={() =>
downloadFile(
report.file!.downloadUrl,
report.file!.fileName,
)}
>
{__("Download")}
</DropdownItem>
)}
{report.canDelete && (
<DropdownItem
icon={IconTrashCan}
onClick={handleDelete}
variant="danger"
>
{__("Delete")}
</DropdownItem>
)}
</ActionDropdown>
</Td>
)}