feat(invoice form): added new invoice form

This commit is contained in:
2025-11-05 21:58:28 -06:00
parent 6ce4d84fd0
commit 65304f61ce
13 changed files with 484 additions and 31 deletions

View File

@@ -37,7 +37,7 @@ export default function ForkliftSideBar() {
},
{
title: "Invoices",
url: "/lst/app/admin/settings",
url: "/lst/app/forklifts/invoices",
icon: ReceiptText,
role: ["systemAdmin", "admin", "manager"],
module: "forklifts",

View File

@@ -1,6 +1,7 @@
import { Link, useRouterState } from "@tanstack/react-router";
import { useState } from "react";
import NewCompanyForm from "@/routes/_app/_forklifts/-components/NewCompany";
import NewInvoice from "@/routes/_app/_forklifts/-components/NewInvoice";
import NewLeaseForm from "@/routes/_app/_forklifts/-components/NewLease";
import { useAuth, useLogout } from "../../lib/authClient";
import { ModeToggle } from "../mode-toggle";
@@ -23,6 +24,7 @@ export default function Nav() {
const currentPath = router.location.href;
const [openDialog, setOpenDialog] = useState(false);
const [openLeaseDialog, setOpenLeaseDialog] = useState(false);
const [openInvoiceDialog, setOpenInvoiceDialog] = useState(false);
return (
<nav className="flex justify-end w-full shadow ">
<div className="m-2 flex flex-row gap-1">
@@ -65,6 +67,14 @@ export default function Nav() {
>
New Lease
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
// just open the dialog when clicked
setOpenInvoiceDialog(true);
}}
>
New Invoice
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Company */}
@@ -85,6 +95,16 @@ export default function Nav() {
</DialogContent>
</Dialog>
)}
{openInvoiceDialog && (
<Dialog
open={openInvoiceDialog}
onOpenChange={setOpenInvoiceDialog}
>
<DialogContent className="sm:max-w-fit">
<NewInvoice setOpenInvoiceDialog={setOpenInvoiceDialog} />
</DialogContent>
</Dialog>
)}
</>
)}
</div>

View File

@@ -13,6 +13,7 @@ import { FieldErrors } from "./FieldErrors";
type DateFieldProps = {
label: string;
required: boolean;
};
export const DateField = ({ label }: DateFieldProps) => {
const field = useFieldContext<any>();
@@ -37,6 +38,7 @@ export const DateField = ({ label }: DateFieldProps) => {
<Calendar
mode="single"
selected={date}
//required={required}
captionLayout="dropdown"
startMonth={new Date(new Date().getFullYear() - 10, 0)}
endMonth={new Date(new Date().getFullYear() + 20, 0)}

View File

@@ -0,0 +1,17 @@
import { queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function getInvoices() {
return queryOptions({
queryKey: ["getInvoices"],
queryFn: () => fetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
});
}
const fetch = async () => {
const { data } = await axios.get("/lst/api/forklifts/invoices");
return data.data;
};

View File

@@ -29,6 +29,7 @@ import { Route as MobileMobileLayoutMRelocateRouteImport } from './routes/_mobil
import { Route as MobileMobileLayoutMDeliveryRouteImport } from './routes/_mobile/_mobileLayout/m/delivery'
import { Route as MobileMobileLayoutMCyclecountsRouteImport } from './routes/_mobile/_mobileLayout/m/cyclecounts'
import { Route as AppForkliftsForkliftsLeasesRouteImport } from './routes/_app/_forklifts/forklifts/leases'
import { Route as AppForkliftsForkliftsInvoicesRouteImport } from './routes/_app/_forklifts/forklifts/invoices'
import { Route as AppForkliftsForkliftsCompaniesRouteImport } from './routes/_app/_forklifts/forklifts/companies'
import { Route as AppAdminLayoutAdminServersRouteImport } from './routes/_app/_adminLayout/admin/servers'
import { Route as ApplogisticsLogisticsDeliveryScheduleRouteImport } from './routes/_app/(logistics)/logistics/deliverySchedule'
@@ -152,6 +153,12 @@ const AppForkliftsForkliftsLeasesRoute =
path: '/forklifts/leases',
getParentRoute: () => AppForkliftsRouteRoute,
} as any)
const AppForkliftsForkliftsInvoicesRoute =
AppForkliftsForkliftsInvoicesRouteImport.update({
id: '/forklifts/invoices',
path: '/forklifts/invoices',
getParentRoute: () => AppForkliftsRouteRoute,
} as any)
const AppForkliftsForkliftsCompaniesRoute =
AppForkliftsForkliftsCompaniesRouteImport.update({
id: '/forklifts/companies',
@@ -287,6 +294,7 @@ export interface FileRoutesByFullPath {
'/logistics/deliverySchedule': typeof ApplogisticsLogisticsDeliveryScheduleRoute
'/admin/servers': typeof AppAdminLayoutAdminServersRoute
'/forklifts/companies': typeof AppForkliftsForkliftsCompaniesRoute
'/forklifts/invoices': typeof AppForkliftsForkliftsInvoicesRoute
'/forklifts/leases': typeof AppForkliftsForkliftsLeasesRoute
'/m/cyclecounts': typeof MobileMobileLayoutMCyclecountsRoute
'/m/delivery': typeof MobileMobileLayoutMDeliveryRoute
@@ -322,6 +330,7 @@ export interface FileRoutesByTo {
'/logistics/deliverySchedule': typeof ApplogisticsLogisticsDeliveryScheduleRoute
'/admin/servers': typeof AppAdminLayoutAdminServersRoute
'/forklifts/companies': typeof AppForkliftsForkliftsCompaniesRoute
'/forklifts/invoices': typeof AppForkliftsForkliftsInvoicesRoute
'/forklifts/leases': typeof AppForkliftsForkliftsLeasesRoute
'/m/cyclecounts': typeof MobileMobileLayoutMCyclecountsRoute
'/m/delivery': typeof MobileMobileLayoutMDeliveryRoute
@@ -365,6 +374,7 @@ export interface FileRoutesById {
'/_app/(logistics)/logistics/deliverySchedule': typeof ApplogisticsLogisticsDeliveryScheduleRoute
'/_app/_adminLayout/admin/servers': typeof AppAdminLayoutAdminServersRoute
'/_app/_forklifts/forklifts/companies': typeof AppForkliftsForkliftsCompaniesRoute
'/_app/_forklifts/forklifts/invoices': typeof AppForkliftsForkliftsInvoicesRoute
'/_app/_forklifts/forklifts/leases': typeof AppForkliftsForkliftsLeasesRoute
'/_mobile/_mobileLayout/m/cyclecounts': typeof MobileMobileLayoutMCyclecountsRoute
'/_mobile/_mobileLayout/m/delivery': typeof MobileMobileLayoutMDeliveryRoute
@@ -403,6 +413,7 @@ export interface FileRouteTypes {
| '/logistics/deliverySchedule'
| '/admin/servers'
| '/forklifts/companies'
| '/forklifts/invoices'
| '/forklifts/leases'
| '/m/cyclecounts'
| '/m/delivery'
@@ -438,6 +449,7 @@ export interface FileRouteTypes {
| '/logistics/deliverySchedule'
| '/admin/servers'
| '/forklifts/companies'
| '/forklifts/invoices'
| '/forklifts/leases'
| '/m/cyclecounts'
| '/m/delivery'
@@ -480,6 +492,7 @@ export interface FileRouteTypes {
| '/_app/(logistics)/logistics/deliverySchedule'
| '/_app/_adminLayout/admin/servers'
| '/_app/_forklifts/forklifts/companies'
| '/_app/_forklifts/forklifts/invoices'
| '/_app/_forklifts/forklifts/leases'
| '/_mobile/_mobileLayout/m/cyclecounts'
| '/_mobile/_mobileLayout/m/delivery'
@@ -645,6 +658,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppForkliftsForkliftsLeasesRouteImport
parentRoute: typeof AppForkliftsRouteRoute
}
'/_app/_forklifts/forklifts/invoices': {
id: '/_app/_forklifts/forklifts/invoices'
path: '/forklifts/invoices'
fullPath: '/forklifts/invoices'
preLoaderRoute: typeof AppForkliftsForkliftsInvoicesRouteImport
parentRoute: typeof AppForkliftsRouteRoute
}
'/_app/_forklifts/forklifts/companies': {
id: '/_app/_forklifts/forklifts/companies'
path: '/forklifts/companies'
@@ -860,12 +880,14 @@ const AppAdminLayoutRouteRouteWithChildren =
interface AppForkliftsRouteRouteChildren {
AppForkliftsForkliftsCompaniesRoute: typeof AppForkliftsForkliftsCompaniesRoute
AppForkliftsForkliftsInvoicesRoute: typeof AppForkliftsForkliftsInvoicesRoute
AppForkliftsForkliftsLeasesRoute: typeof AppForkliftsForkliftsLeasesRoute
AppForkliftsForkliftsIndexRoute: typeof AppForkliftsForkliftsIndexRoute
}
const AppForkliftsRouteRouteChildren: AppForkliftsRouteRouteChildren = {
AppForkliftsForkliftsCompaniesRoute: AppForkliftsForkliftsCompaniesRoute,
AppForkliftsForkliftsInvoicesRoute: AppForkliftsForkliftsInvoicesRoute,
AppForkliftsForkliftsLeasesRoute: AppForkliftsForkliftsLeasesRoute,
AppForkliftsForkliftsIndexRoute: AppForkliftsForkliftsIndexRoute,
}

View File

@@ -0,0 +1,264 @@
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { format } from "date-fns";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useAppForm } from "@/lib/formStuff";
import { getCompanies } from "@/lib/querys/forklifts/getCompanies";
import { getInvoices } from "@/lib/querys/forklifts/getInvoices";
type CompanyData = {
id: string;
name: string;
};
export default function NewInvoice({
setOpenInvoiceDialog,
}: {
setOpenInvoiceDialog: any;
}) {
const { refetch } = useQuery(getInvoices());
const form = useAppForm({
defaultValues: {
companyName: "",
leaseId: "",
invoiceNumber: "",
invoiceDate: "",
totalAmount: "",
forklifts: [{ forklift_id: "", serialNumber: "", amount: "" }],
},
onSubmit: async ({ value }) => {
const updatedForklifts = value.forklifts.map(
({ serialNumber, ...rest }) => rest,
);
const postData = {
leaseId: value.leaseId,
invoiceNumber: value.invoiceNumber,
invoiceDate: format(value.invoiceDate, "MM/dd/yyyy"),
totalAmount: value.totalAmount,
forklifts: updatedForklifts,
};
console.log(postData);
try {
await axios.post("/lst/api/forklifts/invoices", postData);
form.reset();
setOpenInvoiceDialog(false);
refetch();
toast.success(`${value.invoiceNumber} was just created `);
} catch (error) {
// @ts-ignore
console.log(error);
// @ts-ignore
if (!error.response.data.success) {
// @ts-ignore
toast.error(<span>{error?.response?.data.error}</span>);
} else {
// @ts-ignore
toast.error(error?.message);
}
}
},
});
const { data: c, isLoading: ce } = useQuery(getCompanies());
let companyName = form.getFieldValue("companyName");
const { data: l = [], refetch: lf } = useQuery({
queryKey: ["lease", companyName],
queryFn: async () => {
//if (!companyName) return [];
const { data } = await axios.get(
`/lst/api/forklifts/leases?companyId=${companyName}`,
);
return data.data;
},
enabled: !!companyName, // only run if nameId has value
});
if (ce) return <div>Loading Companies</div>;
// remap the companies to fit out select field
const companyMap = c.map((i: CompanyData) => {
return { value: i.id, label: i.name };
});
const leaseMap = l.map((i: any) => {
return { value: i.id, label: i.leaseNumber };
});
const onValueChange = (value: string) => {
companyName = value;
lf();
form.setFieldValue("leaseId", "");
};
let forkliftArray = [];
const onLeaseChange = (value: string) => {
const selectedLease = l.find((lease: any) => lease.id === value);
forkliftArray =
selectedLease?.forklifts.length > 0
? selectedLease.forklifts.map((f: any) => ({
forklift_id: f.forklift_id,
amount: 0,
serialNumber: f.serialNumber,
}))
: [
// {
// forklift_id: "missing",
// amount: 0,
// serialNumber: "Missing forklift",
// },
];
form.setFieldValue("forklifts", forkliftArray);
};
return (
<>
<DialogHeader>
<DialogTitle>Create New Lease</DialogTitle>
<DialogDescription>
Select the company this lease will be for, lease number, start and end
date
</DialogDescription>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.AppField
name="companyName"
listeners={{
onChange: ({ value }) => {
onValueChange(value);
},
}}
children={(field) => (
<field.SelectField
label="Select Company"
placeholder="Companies"
options={companyMap}
/>
)}
/>
<form.AppField
name="leaseId"
listeners={{
onChange: ({ value }) => {
onLeaseChange(value);
},
}}
children={(field) => (
<field.SelectField
label="Select Lease"
placeholder="LeaseNumber"
options={leaseMap}
/>
)}
/>
<div className="w-5/6 m-2">
<form.AppField
name="invoiceNumber"
children={(field) => (
<field.InputField
label="Enter Invoice Number"
inputType="string"
required={true}
/>
)}
/>
</div>
<div className="w-5/6 m-2">
<form.AppField
name="totalAmount"
children={(field) => (
<field.InputField
label="Enter Invoice Amount"
inputType="decimal"
required={true}
/>
)}
/>
</div>
<form.AppField
name="invoiceDate"
children={(field) => (
<field.DateField label="Invoice Date" required={true} />
)}
/>
<hr className="mt-2 mb-2" />
{/* Dynamic forklift section */}
<div className="space-y-3 mt-4">
<form.Field
name="forklifts"
mode="array"
children={(field) => (
<>
<Label>Forklifts</Label>
{field.state.value.map((fx, index) => (
<form.AppField
key={fx.forklift_id}
name={`forklifts[${index}].amount`}
children={(subField) => (
<div className="flex flex-row gap-2">
<Label htmlFor={subField.name}>{fx.serialNumber}</Label>
<Input
className="w-1/4"
id={subField.name}
value={subField.state.value ?? ""}
onChange={(e) => {
// update this subfields amount
subField.handleChange(e.target.value);
// if you also want to store the forklift_id with it
field.handleChange(
field.state.value.map((val, i) =>
i === index
? { ...val, amount: e.target.value }
: val,
),
);
}}
onBlur={subField.handleBlur}
type="number"
/>
</div>
)}
/>
))}
</>
)}
/>
</div>
{/* Map out the input filed based on the forklift id */}
<DialogFooter>
<DialogClose asChild>
<Button
variant="outline"
onClick={() => {
form.reset();
}}
>
Cancel
</Button>
</DialogClose>
<Button type="submit">Submit</Button>
</DialogFooter>
</form>
</>
);
}

View File

@@ -108,11 +108,13 @@ export default function NewLeaseForm({
<div className="flex flex-row gap-2 mt-2 mb-2">
<form.AppField
name="startDate"
children={(field) => <field.DateField label="Start Date" />}
children={(field) => (
<field.DateField label="Start Date" required />
)}
/>
<form.AppField
name="endDate"
children={(field) => <field.DateField label="End Date" />}
children={(field) => <field.DateField label="End Date" required />}
/>
</div>

View File

@@ -0,0 +1,115 @@
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import { format } from "date-fns";
import { ArrowDown, ArrowUp } from "lucide-react";
import { Button } from "@/components/ui/button";
import { getInvoices } from "@/lib/querys/forklifts/getInvoices";
import TableNoExpand from "@/lib/tableStuff/TableNoExpand";
type Invoices = {
id: string;
invoiceNumber: string;
invoiceDate: Date;
totalAmount: string;
add_date: Date;
};
export const Route = createFileRoute("/_app/_forklifts/forklifts/invoices")({
component: RouteComponent,
});
function RouteComponent() {
const { data: invoiceData = [], isLoading } = useQuery(getInvoices());
const columnHelper = createColumnHelper<Invoices>();
//const submitting = useRef(false);
console.log(invoiceData);
const columns = [
columnHelper.accessor("invoiceNumber", {
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="flex flex-row gap-2">Invoice Number</span>
{column.getIsSorted() === "asc" ? (
<ArrowUp className="ml-2 h-4 w-4" />
) : (
<ArrowDown className="ml-2 h-4 w-4" />
)}
</Button>
);
},
}),
columnHelper.accessor("totalAmount", {
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="flex flex-row gap-2">Total Amount</span>
{column.getIsSorted() === "asc" ? (
<ArrowUp className="ml-2 h-4 w-4" />
) : (
<ArrowDown className="ml-2 h-4 w-4" />
)}
</Button>
);
},
}),
columnHelper.accessor("invoiceDate", {
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="flex flex-row gap-2">Invoice Date</span>
{column.getIsSorted() === "asc" ? (
<ArrowUp className="ml-2 h-4 w-4" />
) : (
<ArrowDown className="ml-2 h-4 w-4" />
)}
</Button>
);
},
cell: ({ getValue }) => {
const raw = getValue() as string | Date;
const date = typeof raw === "string" ? new Date(raw) : (raw as Date);
if (isNaN(date.getTime())) return "Invalid date";
return <span>{format(date, "MM/dd/yyyy")}</span>;
},
}),
// columnHelper.accessor("add_date", {
// header: ({ column }) => {
// return (
// <Button
// variant="ghost"
// onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
// >
// <span className="flex flex-row gap-2">Start Date</span>
// {column.getIsSorted() === "asc" ? (
// <ArrowUp className="ml-2 h-4 w-4" />
// ) : (
// <ArrowDown className="ml-2 h-4 w-4" />
// )}
// </Button>
// );
// },
// cell: ({ getValue }) => {
// const raw = getValue() as string | Date;
// const date = typeof raw === "string" ? new Date(raw) : (raw as Date);
// if (isNaN(date.getTime())) return "Invalid date";
// return <span>{format(date, "MM/dd/yyyy")}</span>;
// },
//}),
];
if (isLoading) {
return <div className="m-auto">Loading user data</div>;
}
return <TableNoExpand data={invoiceData} columns={columns} />;
}

View File

@@ -65,7 +65,6 @@ let lotColumns = [
];
export default function Lots() {
const { data, isError, isLoading } = useQuery(getlots());
const { session } = useAuth();
//const { settings } = useSettingStore();

View File

@@ -40,7 +40,7 @@ export default function ManualPrintForm() {
const { settings } = useSettingStore();
const [open, setOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [printing, setPrinting] = useState(false);
// const serverPort = settings.filter((n) => n.name === "serverPort")[0]?.value;
// const serverUrl = `http://${server}:${serverPort}`;
@@ -59,6 +59,7 @@ export default function ManualPrintForm() {
// toast.success(`A new label was sent to printer: ${lot.PrinterName} for line ${lot.MachineDescription} `);
const logdataUrl = `/lst/old/api/ocp/manuallabellog`;
setIsSubmitting(true);
setPrinting(true);
axios
.post(logdataUrl, logData, {})
.then((d) => {
@@ -71,11 +72,13 @@ export default function ManualPrintForm() {
reset();
setOpen(false);
setIsSubmitting(false);
setPrinting(false);
})
.catch((e) => {
if (e.response.status === 500) {
toast.error(`Internal Server error please try again.`);
setIsSubmitting(false);
setPrinting(false);
return { sucess: false };
}
@@ -97,6 +100,7 @@ export default function ManualPrintForm() {
const closeForm = () => {
reset();
setOpen(false);
setPrinting(false);
};
return (
<Dialog
@@ -106,13 +110,14 @@ export default function ManualPrintForm() {
reset();
}
setOpen(isOpen);
setPrinting(true);
// toast.message("Model was something", {
// description: isOpen ? "Modal is open" : "Modal is closed",
// });
}}
>
<DialogTrigger asChild>
<Button variant="outline" size="icon">
<Button variant="outline" size="icon" disabled={printing}>
<Tag className="h-[16px] w-[16px]" />
</Button>
</DialogTrigger>