Skip to content

Commit

Permalink
B 2740 create manual invoice panel (#653)
Browse files Browse the repository at this point in the history
  • Loading branch information
pixelfact authored Nov 18, 2024
1 parent 2a51299 commit 84c5819
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,29 +38,19 @@ export function useInvoiceUpload({ billingProfileId, invoiceId }: UseInvoiceUplo
},
});

function handleManualUpload(fileName: string) {
if (!fileName) return;
const params = new URLSearchParams({ fileName });
setQueryParams(params);
capture("invoice_submitted", { type: "manual" });
}

function handleAutoGeneratedUpload(fileBlob: Blob | undefined) {
function handleSendInvoice({ fileBlob, isManualUpload = false, fileName }: HandleSendInvoiceProps) {
if (isManualUpload && fileName) {
const params = new URLSearchParams();
params.append("fileName", fileName);
setQueryParams(params);
}
if (fileBlob) {
uploadInvoice(fileBlob);
capture("invoice_submitted", { type: "auto-generated" });
capture("invoice_submitted", { type: isManualUpload ? "manual" : "auto-generated" });
} else {
toast.error(<Translate token="features:invoices.invoiceSubmission.toaster.emptyFile" />);
}
}

function handleSendInvoice({ fileBlob, isManualUpload = false, fileName }: HandleSendInvoiceProps) {
if (isManualUpload && fileName) {
handleManualUpload(fileName);
} else {
handleAutoGeneratedUpload(fileBlob);
}
}

return { isPendingUploadInvoice, handleSendInvoice };
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function BillingProfileCard({
onClick={isDisabled ? undefined : onClick}
>
<div className="flex gap-lg">
{/*TODO @Mehdi use avatar label group*/}
<Avatar shape="squared" size="lg" iconProps={getIconProps()} />
<div className="flex flex-col gap-xs">
<Typo size={"sm"} weight="medium" color={"primary"}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { CloudUpload } from "lucide-react";
import { ChangeEvent, useRef } from "react";

import { Icon } from "@/design-system/atoms/icon";
import { Paper } from "@/design-system/atoms/paper";
import { Typo } from "@/design-system/atoms/typo";
import { toast } from "@/design-system/molecules/toaster";

import { UploadFileProps } from "@/shared/panels/_flows/request-payment-flow/_panels/_components/upload-file/upload-file.types";
import { Translate } from "@/shared/translation/components/translate/translate";

export function UploadFile({ setSelectedFile }: UploadFileProps) {
const inputRef = useRef<HTMLInputElement | null>(null);

function handleOnChange(event: ChangeEvent<HTMLInputElement>): void {
if (event.target.files) {
if (event.target.files[0].size > 3000000) {
toast.error(<Translate token={"panels:uploadInvoice.errorMaxSizeFile"} />);
} else {
setSelectedFile(event.target.files[0]);
}
}
}

return (
<Paper
classNames={{
base: "relative z-[0] flex flex-col items-center gap-lg border-border-primary border border-dashed !py-10",
}}
>
<Icon component={CloudUpload} />
<div className="flex flex-col gap-1 text-center">
<Typo color="brand-secondary-alt" translate={{ token: "panels:uploadInvoice.clickToUpload" }} />
<Typo size="sm" color="secondary" translate={{ token: "panels:uploadInvoice.fileType" }} />
</div>
<input
type="file"
ref={inputRef}
onChange={handleOnChange}
className="absolute h-full w-full cursor-pointer opacity-0"
accept="application/pdf"
/>
</Paper>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface UploadFileProps {
setSelectedFile: (file: File) => void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FileMinus, Trash2 } from "lucide-react";

import { Avatar } from "@/design-system/atoms/avatar";
import { Button } from "@/design-system/atoms/button/variants/button-default";
import { Paper } from "@/design-system/atoms/paper";
import { Typo } from "@/design-system/atoms/typo";

import { UploadedFileDisplayProps } from "@/shared/panels/_flows/request-payment-flow/_panels/_components/uploaded-file-display/uploaded-file-display.types";

export function UploadedFileDisplay({ fileName, onRemoveFile }: UploadedFileDisplayProps) {
return (
<Paper
background={"primary-alt"}
border={"primary"}
classNames={{ base: "relative z-[0] flex flex-row items-center gap-4" }}
>
<Avatar shape="squared" size="lg" iconProps={{ component: FileMinus }} />
<Typo size="sm" weight="medium" classNames={{ base: "flex-1" }}>
{fileName}
</Typo>
<Button
variant="secondary"
onClick={onRemoveFile}
isDisabled={!fileName}
iconOnly
theme="destructive"
startIcon={{ component: Trash2 }}
/>
</Paper>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface UploadedFileDisplayProps {
fileName: string;
onRemoveFile: () => void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"guidelinesTitle": "Guidelines",
"guidelineAll": {
"subtitle": "Please, make sure your invoice includes the following:",
"rule_1": "Total amount should be 1200 USDC excluding taxes",
"rule_2": "Make sure to send your invoice in US Dollars ($)",
"rule_3": "Apply taxes according to your local legislation"
},
"guidelineSender": {
"subtitle": "Sender",
"rule_1": "First name, Last name",
"rule_2": "10 Rue voltaire,75002, Paris, France"
},
"guidelineRecipient": {
"subtitle": "Recipient",
"rule_1": "Wagmi",
"rule_2": "60 rue François 1er, 75008 Paris, France"
},
"sample_to_download": "We highly recommend downloading our invoice sample to help you create your own correctly.",
"sample_link_label": "Download invoice sample",
"clickToUpload": "Click to upload or drag and drop",
"errorMaxSizeFile": "The file is too large, please upload a file smaller than 3MB",
"fileType": "Only PDF files smaller than 3MB are accepted",
"alert": {
"title": "Find some guidlines on how to properly generate your invoice",
"description": "We’ll have to reject it if it doesn’t match requirements."
},
"submission": {
"alert": {
"title": "Please double check that everything is correct",
"description": "Once approved it will trigger the payment process and can’t be edited."
},
"error": {
"title": "An error occurred",
"description": "We were unable to generate the invoice, please try again later"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useSinglePanelContext } from "@/shared/features/side-panels/side-panel/side-panel";

export function useUploadInvoice() {
return useSinglePanelContext("upload-invoice");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { Download, Info } from "lucide-react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";

import { Button } from "@/design-system/atoms/button/variants/button-default";
import { Icon } from "@/design-system/atoms/icon";
import { Paper } from "@/design-system/atoms/paper";
import { Spinner } from "@/design-system/atoms/spinner";
import { Typo } from "@/design-system/atoms/typo";
import { Alert } from "@/design-system/molecules/alert";

import { useInvoicePreview } from "@/shared/features/invoice/hooks/use-invoice-preview/use-invoice-preview";
import { useInvoiceUpload } from "@/shared/features/invoice/hooks/use-invoice-upload/use-invoice-upload";
import { SidePanelBody } from "@/shared/features/side-panels/side-panel-body/side-panel-body";
import { SidePanelFooter } from "@/shared/features/side-panels/side-panel-footer/side-panel-footer";
import { SidePanelHeader } from "@/shared/features/side-panels/side-panel-header/side-panel-header";
import { useSidePanel } from "@/shared/features/side-panels/side-panel/side-panel";
import { UploadFile } from "@/shared/panels/_flows/request-payment-flow/_panels/_components/upload-file/upload-file";
import { UploadedFileDisplay } from "@/shared/panels/_flows/request-payment-flow/_panels/_components/uploaded-file-display/uploaded-file-display";
import { useUploadInvoice } from "@/shared/panels/_flows/request-payment-flow/_panels/upload-invoice/upload-invoice.hooks";
import { useRequestPaymentFlow } from "@/shared/panels/_flows/request-payment-flow/request-payment-flow.context";
import { Translate } from "@/shared/translation/components/translate/translate";

function Content() {
const [selectedFileBlob, setSelectedFileBlob] = useState<File>();
const { t } = useTranslation();
const { billingProfileId = "", rewardIds } = useRequestPaymentFlow();

const {
isLoading: isLoadingInvoicePreview,
fileUrl,
invoiceId,
} = useInvoicePreview({
rewardIds,
billingProfileId,
isSample: true,
});

const { isPendingUploadInvoice, handleSendInvoice } = useInvoiceUpload({
billingProfileId,
invoiceId,
});

function removeFile() {
setSelectedFileBlob(undefined);
}

const renderGuidelineAll = useMemo(
() => (
<>
<Typo size="xs" color="secondary" translate={{ token: "panels:uploadInvoice.guidelineAll.subtitle" }} />
<Typo size="xs" color="secondary">
<ul>
{Array.from({ length: 3 }, (_, index) => {
const token = `panels:uploadInvoice.guidelineAll.rule_${index + 1}`;
return <li key={token}>{t(token)}</li>;
})}
</ul>
</Typo>
</>
),
[]
);

const renderGuidelineSender = useMemo(
() => (
<>
<Typo size="xs" color="secondary" translate={{ token: "panels:uploadInvoice.guidelineSender.subtitle" }} />
<Typo size="xs" color="secondary">
<ul>
{Array.from({ length: 2 }, (_, index) => {
const token = `panels:uploadInvoice.guidelineSender.rule_${index + 1}`;
return <li key={token}>{t(token)}</li>;
})}
</ul>
</Typo>
</>
),
[]
);

const renderGuidelineRecipient = useMemo(
() => (
<>
<Typo size="xs" color="secondary" translate={{ token: "panels:uploadInvoice.guidelineRecipient.subtitle" }} />
<Typo size="xs" color="secondary">
<ul>
{Array.from({ length: 2 }, (_, index) => {
const token = `panels:uploadInvoice.guidelineRecipient.rule_${index + 1}`;
return <li key={token}>{t(token)}</li>;
})}
</ul>
</Typo>
</>
),
[]
);

function renderUploadFile() {
if (selectedFileBlob) {
return <UploadedFileDisplay fileName={selectedFileBlob.name} onRemoveFile={removeFile} />;
}

return <UploadFile setSelectedFile={setSelectedFileBlob} />;
}

return (
<>
<SidePanelHeader
title={{
translate: {
// TODO update title
token: "panels:singleContributionSelection.title",
},
}}
canClose
/>

<SidePanelBody>
<Alert
color="grey"
title={<Translate token="panels:uploadInvoice.alert.title" />}
description={<Translate token="panels:uploadInvoice.alert.description" />}
icon={{ component: Info }}
/>
<Paper
background={"primary-alt"}
border={"primary"}
classNames={{ base: "prose leading-normal flex flex-col gap-lg" }}
>
<Typo as={"div"} size="sm" weight="medium" translate={{ token: "panels:uploadInvoice.guidelinesTitle" }} />
<div>
{renderGuidelineAll}
{renderGuidelineSender}
{renderGuidelineRecipient}
</div>
<Typo size="xs" color="secondary" translate={{ token: "panels:uploadInvoice.sample_to_download" }} />
</Paper>
{renderUploadFile()}
</SidePanelBody>
<SidePanelFooter>
<div className="flex w-full items-center justify-between">
{isLoadingInvoicePreview ? <Spinner /> : null}
{fileUrl ? (
<a
className="flex cursor-pointer items-center gap-md rounded-md border border-border-primary px-lg py-md effect-box-shadow-xs"
href={fileUrl}
download="invoice-sample.pdf"
>
<Icon component={Download} size="sm" />
<Typo size="sm" color="secondary" translate={{ token: "panels:uploadInvoice.sample_link_label" }} />
</a>
) : (
<div />
)}
<Button
variant={"secondary"}
size={"md"}
onClick={() =>
handleSendInvoice({
fileBlob: selectedFileBlob as Blob,
isManualUpload: true,
fileName: selectedFileBlob?.name,
})
}
isDisabled={!selectedFileBlob}
isLoading={isPendingUploadInvoice}
translate={{ token: "common:form.send" }}
/>
</div>
</SidePanelFooter>
</>
);
}

export function UploadInvoice() {
const { name } = useUploadInvoice();
const { Panel } = useSidePanel({ name });

return (
<Panel>
<Content />
</Panel>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export interface UploadInvoiceProps {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { BillingProfileSelection } from "@/shared/panels/_flows/request-payment-
import { useBillingProfileSelection } from "@/shared/panels/_flows/request-payment-flow/_panels/billing-profile-selection/billing-profile-selection.hooks";
import { GenerateInvoice } from "@/shared/panels/_flows/request-payment-flow/_panels/generate-invoice/generate-invoice";
import { RewardsSelection } from "@/shared/panels/_flows/request-payment-flow/_panels/rewards-selection/rewards-selection";
import { UploadInvoice } from "@/shared/panels/_flows/request-payment-flow/_panels/upload-invoice/upload-invoice";

import { AcceptInvoicingMandate } from "./_panels/accept-invoicing-mandate/accept-invoicing-mandate";
import { InvoicingMandate } from "./_panels/invoicing-mandate/invoicing-mandate";
Expand Down Expand Up @@ -69,6 +70,7 @@ export function RequestPaymentFlowProvider({ children }: PropsWithChildren) {
<AcceptInvoicingMandate />
<InvoicingMandate />
<GenerateInvoice />
<UploadInvoice />
</RequestPaymentFlowContext.Provider>
);
}
Expand Down
2 changes: 2 additions & 0 deletions shared/panels/_translations/panels.translate.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import enUploadInvoice from "@/shared/panels/_flows/request-payment-flow/_panels/upload-invoice/_translations/upload-invoice.en.json";
import enRequestPaymentFlow from "@/shared/panels/_flows/request-payment-flow/_translations/request-payment-flow.en.json";
import enBulkContributionSelection from "@/shared/panels/_flows/reward-flow/_panels/bulk-contribution-selection/_translations/bulk-contribution-selection.en.json";
import enBulkContributionValidation from "@/shared/panels/_flows/reward-flow/_panels/bulk-contribution-validation/_translations/bulk-contribution-validation.en.json";
Expand Down Expand Up @@ -45,5 +46,6 @@ export const enPanelsTranslation = {
singleContributionValidation: enSingleContributionValidation,
rewardFlow: enRewardFlow,
requestPaymentFlow: enRequestPaymentFlow,
uploadInvoice: enUploadInvoice,
},
};

0 comments on commit 84c5819

Please sign in to comment.