mirror of
https://github.com/VladSez/easy-invoice-pdf
synced 2026-04-21 13:37:40 +00:00
* feat: Add language attribute to date input fields in invoice form components - Include `lang="en"` attribute in date input fields of `InvoiceForm` and `GeneralInformation` components for improved accessibility and localization support. * fix: Update language attribute for date input fields in invoice form components * refactor: Improve layout and organization of invoice components - Remove unnecessary margin from the main container in the Home component. - Wrap the share invoice button and PDF download link in a fragment for better structure. - Adjust margins for the ProjectInfo and action button container for improved spacing. - Update the InvoicePDFViewer height to use full height for better responsiveness. - Remove the deprecated RegenerateInvoiceButton component to streamline the codebase. - Update the InvoiceClientPage to accept handleShareInvoice prop for better functionality. - Clean up unused language attributes in date input fields across invoice form components. * feat: Integrate Playwright for end-to-end testing and enhance invoice form components - Add Playwright configuration and dependencies for E2E testing. - Create GitHub Actions workflow for automated E2E tests on deployment. - Implement initial E2E tests for the Invoice Generator Page, verifying UI elements and form functionality. - Refactor invoice form components to include data-testid attributes for better testability. - Update .gitignore to exclude Playwright-related files and directories. * chore: Update GitHub Actions workflow for E2E testing and enhance test coverage - Upgrade pnpm version from 8 to 10 in the E2E workflow for improved package management. - Add new test case to verify header buttons and links on the Invoice Generator Page, ensuring UI elements are displayed correctly and have the expected attributes. * chore: Enhance ESLint configuration for Playwright integration - Add Playwright ESLint plugin to package.json for improved E2E testing support. - Update .eslintrc.json to include overrides for E2E test files. - Clean up GitHub Actions workflow by removing unnecessary pnpm version specification. * chore: Update Playwright configuration and improve test assertions - Increase timeout for expect assertions and test execution from 15 seconds to 30 seconds for better stability in E2E tests. - Comment out mobile viewport tests to streamline configuration and focus on desktop testing. * chore: Update configuration and refactor invoice form components - Add compiler options to remove console logs in production and enhance logging for fetch requests in next.config.mjs. - Update package.json to include new type definitions for ua-parser-js and add ua-parser-js as a dependency. - Refactor invoice form components to remove form prefix IDs, simplifying data-testid attributes for better testability. - Introduce DeviceContext for managing device type state and improve responsiveness in invoice form components. - Implement server-side device detection using user agent parsing for better rendering on mobile and desktop views. - Update media query hooks to streamline device type checks across components. * chore: Update Playwright configuration and enhance invoice form tests - Reduce timeout for expect assertions from 30 seconds to 15 seconds for improved test performance. - Add new test for handling currency switching in the Invoice Generator Page, verifying correct currency display and calculations. - Refactor buyer and seller information components to include tooltip messages and improve accessibility with aria attributes. - Update BuyerDialog and BuyerManagement components to enhance user experience with better visibility and edit functionality for buyer details. * chore: Update Playwright installation command in GitHub Actions workflow - Modify Playwright installation command to remove explicit browser specification, allowing for default browser installation with dependencies. * chore: Update GitHub Actions E2E workflow for Playwright report handling - Change condition for uploading Playwright report to ensure it uploads regardless of test outcome. - Reduce retention days for uploaded reports from 5 to 3 for better resource management. * chore: Update Playwright installation command in GitHub Actions workflow - Specify installation of Chromium and WebKit browsers along with dependencies for enhanced testing capabilities. * chore: Enhance E2E tests for seller and buyer management functionality - Add tests to verify the deletion process for sellers and buyers, including confirmation dialogs and success messages. - Ensure localStorage data is correctly saved and parsed for both seller and buyer information. - Introduce default data constants for sellers and buyers to streamline test setup. - Improve accessibility by adding screen reader text for delete buttons in the seller management component. * chore: Pin versions of GitHub Actions in E2E workflow for stability - Update actions/checkout, pnpm/action-setup, actions/setup-node, and actions/upload-artifact to specific versions for improved reliability and security. - Comment added to clarify the rationale for using pinned versions. * chore: Add E2E test for accordion items visibility and localStorage state management - Implement test to verify that accordion items are visible, collapsible, and their state is correctly saved in localStorage. - Ensure state persistence across page reloads and validate updated states after toggling sections. - Introduce ACCORDION_STATE_LOCAL_STORAGE_KEY and AccordionState type for better type safety and clarity. * chore: Update Playwright configuration and add comprehensive E2E tests for seller and buyer management - Increase timeout for expect assertions and test execution from 30 seconds to 60 seconds for improved stability in E2E tests. - Introduce new E2E tests for seller and buyer management, covering creation, editing, and deletion processes, including confirmation dialogs and success messages. - Ensure localStorage data is correctly saved and parsed for both seller and buyer information. - Implement detailed validation for form fields and visibility toggles in seller and buyer management dialogs. - Enhance accessibility by adding screen reader text for buttons and tooltips in the management components. * chore: Refactor Playwright configuration and enhance invoice item validation tests - Introduce a constant for timeout values in Playwright configuration for consistency and maintainability. - Add comprehensive validation tests for amount, net price, and VAT fields in the invoice items section, ensuring proper error messages for invalid inputs. - Update expected error messages in the schema to match the new formatting for better clarity. - Improve test structure by utilizing descriptive variable names and modularizing input handling for better readability. * chore: add pdf e2e tests - Add `pdf-parse` and its type definitions to package.json for PDF handling capabilities. - Increase Playwright timeout from 30 seconds to 60 seconds for improved test stability. - Introduce comprehensive E2E tests for PDF generation, verifying content in both English and Polish. - Implement cleanup procedures for test downloads to ensure a clean testing environment. - Validate invoice data updates in the generated PDF, ensuring accurate content reflects user inputs. * chore: add eslint, knip, lint-staged * chore: run prettier * chore: minor improvements * chore: add more test and improved e2e config * minor fixes * minor fixes * chore: add new test
386 lines
12 KiB
TypeScript
386 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogClose,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { buyerSchema, type BuyerData } from "@/app/schema";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { CustomTooltip } from "@/components/ui/tooltip";
|
|
import { toast } from "sonner";
|
|
import { BUYERS_LOCAL_STORAGE_KEY } from "./buyer-management";
|
|
import { z } from "zod";
|
|
import { useState, useEffect } from "react";
|
|
import * as Sentry from "@sentry/nextjs";
|
|
|
|
const BUYER_FORM_ID = "buyer-form";
|
|
|
|
interface BuyerDialogProps {
|
|
isOpen: boolean;
|
|
onClose: React.Dispatch<React.SetStateAction<boolean>>;
|
|
handleBuyerAdd?: (
|
|
buyer: BuyerData,
|
|
{ shouldApplyNewBuyerToInvoice }: { shouldApplyNewBuyerToInvoice: boolean }
|
|
) => void;
|
|
handleBuyerEdit?: (buyer: BuyerData) => void;
|
|
initialData: BuyerData | null;
|
|
isEditMode: boolean;
|
|
formValues?: Partial<BuyerData>;
|
|
}
|
|
|
|
export function BuyerDialog({
|
|
isOpen,
|
|
onClose,
|
|
handleBuyerAdd,
|
|
handleBuyerEdit,
|
|
initialData,
|
|
isEditMode,
|
|
formValues,
|
|
}: BuyerDialogProps) {
|
|
const form = useForm<BuyerData>({
|
|
resolver: zodResolver(buyerSchema),
|
|
defaultValues: {
|
|
id: initialData?.id ?? "",
|
|
name: initialData?.name ?? "",
|
|
address: initialData?.address ?? "",
|
|
vatNo: initialData?.vatNo ?? "",
|
|
email: initialData?.email ?? "",
|
|
vatNoFieldIsVisible: initialData?.vatNoFieldIsVisible ?? true,
|
|
},
|
|
});
|
|
|
|
// by default, we want to apply the new buyer to the current invoice
|
|
const [shouldApplyNewBuyerToInvoice, setShouldApplyNewBuyerToInvoice] =
|
|
useState(true);
|
|
|
|
const [shouldApplyFormValues, setShouldApplyFormValues] = useState(false);
|
|
|
|
// Effect to update form values when switch is toggled
|
|
useEffect(() => {
|
|
// if the switch is on and we have form values, we want to apply the form values to the form
|
|
if (shouldApplyFormValues && formValues && !isEditMode) {
|
|
form.reset({
|
|
...form.getValues(),
|
|
...formValues,
|
|
});
|
|
}
|
|
|
|
// if the switch is off and we have initial data, we want to apply the initial data to the form
|
|
else if (!shouldApplyFormValues && !isEditMode) {
|
|
form.reset(
|
|
initialData ?? {
|
|
id: "",
|
|
name: "",
|
|
address: "",
|
|
vatNo: "",
|
|
email: "",
|
|
vatNoFieldIsVisible: true,
|
|
}
|
|
);
|
|
}
|
|
}, [shouldApplyFormValues, formValues, initialData, isEditMode, form]);
|
|
|
|
function onSubmit(formValues: BuyerData) {
|
|
try {
|
|
// **RUNNING SOME VALIDATIONS FIRST**
|
|
|
|
// Get existing buyers or initialize empty array
|
|
const buyers = localStorage.getItem(BUYERS_LOCAL_STORAGE_KEY);
|
|
const existingBuyers: unknown = buyers ? JSON.parse(buyers) : [];
|
|
|
|
// Validate existing buyers array with Zod
|
|
const existingBuyersValidationResult = z
|
|
.array(buyerSchema)
|
|
.safeParse(existingBuyers);
|
|
|
|
if (!existingBuyersValidationResult.success) {
|
|
console.error(
|
|
"Invalid existing buyers data:",
|
|
existingBuyersValidationResult.error
|
|
);
|
|
|
|
// Show error toast
|
|
toast.error("Error loading existing buyers", {
|
|
richColors: true,
|
|
description: "Please try again",
|
|
});
|
|
|
|
// Reset localStorage if validation fails
|
|
localStorage.setItem(BUYERS_LOCAL_STORAGE_KEY, JSON.stringify([]));
|
|
|
|
return;
|
|
}
|
|
|
|
// Validate buyer data against existing buyers
|
|
const isDuplicateName = existingBuyersValidationResult.data.some(
|
|
(buyer: BuyerData) =>
|
|
buyer.name === formValues.name && buyer.id !== formValues.id
|
|
);
|
|
|
|
if (isDuplicateName) {
|
|
form.setError("name", {
|
|
type: "manual",
|
|
message: "A buyer with this name already exists",
|
|
});
|
|
|
|
// Focus on the name input field for user to fix the error
|
|
form.setFocus("name");
|
|
|
|
// Show error toast
|
|
toast.error("A buyer with this name already exists", {
|
|
richColors: true,
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
if (isEditMode) {
|
|
// Edit buyer
|
|
handleBuyerEdit?.(formValues);
|
|
} else {
|
|
// Add new buyer
|
|
handleBuyerAdd?.(formValues, { shouldApplyNewBuyerToInvoice });
|
|
}
|
|
|
|
// Close dialog
|
|
onClose(false);
|
|
|
|
// Reset form
|
|
form.reset();
|
|
} catch (error) {
|
|
console.error("Failed to save buyer:", error);
|
|
|
|
toast.error("Failed to save buyer", {
|
|
description: "Please try again",
|
|
richColors: true,
|
|
});
|
|
|
|
Sentry.captureException(error);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog
|
|
open={isOpen}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
onClose(false);
|
|
form.reset();
|
|
}
|
|
}}
|
|
>
|
|
<DialogContent
|
|
className="flex flex-col gap-0 overflow-y-visible p-0 sm:max-w-lg [&>button:last-child]:top-3.5"
|
|
data-testid={`manage-buyer-dialog`}
|
|
>
|
|
<DialogHeader className="border-b border-slate-200 px-6 py-4 dark:border-slate-800">
|
|
<DialogTitle className="text-base">
|
|
{isEditMode ? "Edit Buyer" : "Add New Buyer"}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{isEditMode
|
|
? "Edit the buyer details"
|
|
: "Add a new buyer to use later in your invoices"}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="overflow-y-auto px-6 py-4">
|
|
{/* Add Use Current Form Values switch */}
|
|
{!isEditMode && (
|
|
<div className="mb-4 flex items-center gap-2">
|
|
<Switch
|
|
checked={shouldApplyFormValues}
|
|
onCheckedChange={setShouldApplyFormValues}
|
|
id="apply-form-values-switch"
|
|
/>
|
|
<CustomTooltip
|
|
trigger={
|
|
<Label
|
|
htmlFor="apply-form-values-switch"
|
|
className="cursor-pointer"
|
|
>
|
|
Use Current Form Values
|
|
</Label>
|
|
}
|
|
content="Pre-fill with values from the current invoice form"
|
|
className="z-[1000]"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
className="space-y-4"
|
|
id={BUYER_FORM_ID}
|
|
>
|
|
<FormField
|
|
control={form.control}
|
|
name="name"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Name</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
{...field}
|
|
rows={3}
|
|
placeholder="Enter buyer name"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="address"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Address</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
{...field}
|
|
rows={3}
|
|
placeholder="Enter buyer address"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{/* VAT Number */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-end justify-between">
|
|
<FormField
|
|
control={form.control}
|
|
name="vatNo"
|
|
render={({ field }) => (
|
|
<FormItem className="flex-1">
|
|
<FormLabel>VAT Number</FormLabel>
|
|
<FormControl>
|
|
<Input {...field} placeholder="Enter VAT number" />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{/* Show/Hide VAT Number Field in PDF */}
|
|
<div className="ml-4 flex items-center gap-2">
|
|
<FormField
|
|
control={form.control}
|
|
name="vatNoFieldIsVisible"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
id="vatNoFieldIsVisible"
|
|
/>
|
|
<CustomTooltip
|
|
trigger={
|
|
<Label htmlFor="vatNoFieldIsVisible">
|
|
Show in PDF
|
|
</Label>
|
|
}
|
|
content='Show/Hide the "VAT Number" field in the PDF'
|
|
className="z-[1000]"
|
|
/>
|
|
</div>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="email"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Email</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
type="email"
|
|
placeholder="buyer@email.com"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</form>
|
|
</Form>
|
|
|
|
{/* Apply to Current Invoice switch remains at bottom */}
|
|
{!isEditMode && (
|
|
<div className="mt-4 flex items-center gap-2 border-t pt-4">
|
|
<Switch
|
|
checked={shouldApplyNewBuyerToInvoice}
|
|
onCheckedChange={setShouldApplyNewBuyerToInvoice}
|
|
id="apply-buyer-to-current-invoice-switch"
|
|
/>
|
|
<CustomTooltip
|
|
trigger={
|
|
<Label
|
|
htmlFor="apply-buyer-to-current-invoice-switch"
|
|
className="cursor-pointer"
|
|
>
|
|
Apply to Current Invoice
|
|
</Label>
|
|
}
|
|
content="When enabled, the newly created buyer will be automatically applied to your current invoice form"
|
|
className="z-[1000]"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<DialogFooter className="border-border border-t px-6 py-4">
|
|
<DialogClose asChild>
|
|
<Button type="button" _variant="outline">
|
|
Cancel
|
|
</Button>
|
|
</DialogClose>
|
|
<Button
|
|
type="button"
|
|
onClick={async () => {
|
|
// validate the form
|
|
const isValid = await form.trigger();
|
|
if (!isValid) return;
|
|
|
|
// submit the form
|
|
onSubmit(form.getValues());
|
|
}}
|
|
form={BUYER_FORM_ID}
|
|
>
|
|
Save Buyer
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|