feat(leases): added in leases and move table to reuseable component

This commit is contained in:
2025-11-04 20:16:14 -06:00
parent b1c56ee4bb
commit bd7bea8db6
30 changed files with 5788 additions and 601 deletions

View File

@@ -1,6 +1,6 @@
meta { meta {
name: companies name: companies
seq: 1 seq: 2
} }
auth { auth {

View File

@@ -0,0 +1,22 @@
meta {
name: Get lease
type: http
seq: 2
}
get {
url: {{url}}/lst/api/forklifts/leases
body: none
auth: inherit
}
body:json {
{
"name":"Delage DLL"
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,27 @@
meta {
name: Update lease
type: http
seq: 3
}
patch {
url: {{url}}/lst/api/forklifts/leases/:id
body: json
auth: inherit
}
params:path {
id: de10c8ee-5756-4efb-9664-3c55338b2b60
}
body:json {
{
"endDate": "3/25/2029"
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,25 @@
meta {
name: add lease
type: http
seq: 1
}
post {
url: {{url}}/lst/api/forklifts/leases
body: json
auth: inherit
}
body:json {
{
"leaseNumber":"Delage DLL",
"startDate": "",
"endDate": "",
"companyId": ""
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,8 @@
meta {
name: lease
seq: 1
}
auth {
mode: inherit
}

View File

@@ -33,7 +33,16 @@ router.post("/", async (req: Request, res: Response) => {
upd_user: req.user?.username, upd_user: req.user?.username,
upd_date: sql`NOW()`, upd_date: sql`NOW()`,
}) })
//.onConflictDoNothing() .onConflictDoUpdate({
target: forkliftCompanies.name,
set: {
...parsed.data,
add_user: req.user?.username,
add_date: sql`NOW()`,
upd_user: req.user?.username,
upd_date: sql`NOW()`,
},
})
.returning({ .returning({
name: forkliftCompanies.name, name: forkliftCompanies.name,
}), }),

View File

@@ -0,0 +1,114 @@
import axios from "axios";
import { type DrizzleError, sql } from "drizzle-orm";
import type { Request, Response } from "express";
import { Router } from "express";
import https from "https";
import { db } from "../../../../pkg/db/db.js";
import {
insertLeasesCompanySchema,
leases,
} from "../../../../pkg/db/schema/forkliftLeases.js";
import { createLogger } from "../../../../pkg/logger/logger.js";
import { tryCatch } from "../../../../pkg/utils/tryCatch.js";
const router = Router();
router.post("/", async (req: Request, res: Response) => {
// when a new server is posted from localhost or 127.0.0.1 we also want to post it to the test server so we can see it from there
//res.status(200).json({ message: "Server added", ip: req.hostname });
const log = createLogger({ module: "forklift", subModule: "add lease" });
const parsed = insertLeasesCompanySchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.flatten() });
}
const { data, error } = await tryCatch(
db
.insert(leases)
.values({
...parsed.data,
add_user: req.user?.username,
add_date: sql`NOW()`,
upd_user: req.user?.username,
upd_date: sql`NOW()`,
})
.onConflictDoUpdate({
target: leases.leaseNumber,
set: {
...parsed.data,
add_user: req.user?.username,
add_date: sql`NOW()`,
upd_user: req.user?.username,
upd_date: sql`NOW()`,
},
})
.returning({
leaseNumber: leases.leaseNumber,
}),
);
if (error) {
const err: DrizzleError = error;
return res.status(400).json({
message: `Error adding lease`,
error: err.cause,
});
}
if (req.hostname === "localhost" && process.env.MAIN_SERVER) {
log.info({}, "Running in dev server about to add in a new server");
const axiosInstance = axios.create({
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
baseURL: process.env.MAIN_SERVER, // e.g. "https://example.com"
withCredentials: true,
});
const loginRes = (await axiosInstance.post(
`${process.env.MAIN_SERVER}/lst/api/auth/sign-in/username`,
{
username: process.env.MAIN_SERVER_USERNAME,
password: process.env.MAIN_SERVER_PASSWORD,
},
{
headers: { "Content-Type": "application/json" },
},
)) as any;
const setCookie = loginRes.headers["set-cookie"][0];
if (!setCookie) {
throw new Error("Did not receive a Set-Cookie header from login");
}
const { data, error } = await tryCatch(
axios.post(
`${process.env.MAIN_SERVER}/lst/api/forklifts/leases`,
parsed.data,
{
headers: {
"Content-Type": "application/json",
Cookie: setCookie.split(";")[0],
},
withCredentials: true,
},
),
);
if (error) {
log.error(
{ stack: error },
"There was an error adding the company to Main Server",
);
}
log.info(
{ stack: data?.data },
"A new Company was just added to the server.",
);
}
return res
.status(201)
.json({ message: `lease ${data[0]?.leaseNumber} added`, data: data });
});
export default router;

View File

@@ -0,0 +1,48 @@
import { and, asc, eq } from "drizzle-orm";
import type { Request, Response } from "express";
import { Router } from "express";
import { db } from "../../../../pkg/db/db.js";
import { forkliftCompanies } from "../../../../pkg/db/schema/forkliftLeaseCompanys.js";
import { leases } from "../../../../pkg/db/schema/forkliftLeases.js";
import { tryCatch } from "../../../../pkg/utils/tryCatch.js";
const router = Router();
router.get("/", async (req: Request, res: Response) => {
const lease = req.query.lease;
const conditions = [];
if (lease !== undefined) {
conditions.push(eq(leases.leaseNumber, `${lease}`));
}
//conditions.push(eq(forkliftCompanies.active, true));
const { data, error } = await tryCatch(
db
.select({
id: leases.id,
leaseNumber: leases.leaseNumber,
startDate: leases.startDate,
endDate: leases.endDate,
leaseLink: leases.leaseLink,
companyName: forkliftCompanies.name,
add_user: leases.add_user,
add_date: leases.add_date,
upd_user: leases.upd_user,
upd_date: leases.upd_date,
})
.from(leases)
.innerJoin(forkliftCompanies, eq(forkliftCompanies.id, leases.companyId))
.where(and(...conditions))
.orderBy(asc(leases.leaseNumber)),
);
if (error) {
return res.status(400).json({ error: error });
}
res.status(200).json({ message: "Current Leases", data: data });
});
export default router;

View File

@@ -0,0 +1,18 @@
import { Router } from "express";
import { requireAuth } from "../../../../pkg/middleware/authMiddleware.js";
import addLeases from "./addLease.js";
import getLeases from "./getLeases.js";
import updateLeases from "./updateLease.js";
const router = Router();
router.use("/", requireAuth("forklifts", ["systemAdmin", "admin"]), getLeases);
router.use("/", requireAuth("forklifts", ["systemAdmin", "admin"]), addLeases);
router.use(
"/",
requireAuth("forklifts", ["systemAdmin", "admin"]),
updateLeases,
);
export default router;

View File

@@ -0,0 +1,114 @@
import axios from "axios";
import { eq, sql } from "drizzle-orm";
import type { Request, Response } from "express";
import { Router } from "express";
import https from "https";
import { db } from "../../../../pkg/db/db.js";
import { forkliftCompanies } from "../../../../pkg/db/schema/forkliftLeaseCompanys.js";
import { leases } from "../../../../pkg/db/schema/forkliftLeases.js";
import { serverData } from "../../../../pkg/db/schema/servers.js";
import { createLogger } from "../../../../pkg/logger/logger.js";
import { tryCatch } from "../../../../pkg/utils/tryCatch.js";
const router = Router();
router.patch("/:id", async (req: Request, res: Response) => {
const log = createLogger({
module: "forklifts",
subModule: "update leases",
});
// when a server is updated and is posted from localhost or 127.0.0.1 we also want to post it to the test server so we can see it from there, we want to insert with update on conflict.
const id = req.params.id;
const updates: Record<string, any> = {};
console.log(req.body);
if (req.body?.leaseNumber !== undefined) {
updates.leaseNumber = req.body.leaseNumber;
}
if (req.body?.startDate !== undefined) {
updates.startDate = req.body.startDate;
}
if (req.body?.endDate !== undefined) {
updates.endDate = req.body.endDate;
}
if (req.body?.companyId !== undefined) {
updates.companyId = req.body.companyId;
}
if (req.body?.leaseLink !== undefined) {
updates.leaseLink = req.body.leaseLink;
}
updates.upd_user = req.user!.username || "lst_user";
updates.upd_date = sql`NOW()`;
console.log(updates);
try {
if (Object.keys(updates).length > 0) {
await db.update(leases).set(updates).where(eq(leases.id, id));
}
if (req.hostname === "localhost" && process.env.MAIN_SERVER) {
log.info({}, "Running in dev server about to add in a new server");
const axiosInstance = axios.create({
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
baseURL: process.env.MAIN_SERVER,
withCredentials: true,
});
const loginRes = (await axiosInstance.post(
`${process.env.MAIN_SERVER}/lst/api/auth/sign-in/username`,
{
username: process.env.MAIN_SERVER_USERNAME,
password: process.env.MAIN_SERVER_PASSWORD,
},
{
headers: { "Content-Type": "application/json" },
},
)) as any;
const setCookie = loginRes?.headers["set-cookie"][0];
//console.log(setCookie.split(";")[0].replace("__Secure-", ""));
if (!setCookie) {
throw new Error("Did not receive a Set-Cookie header from login");
}
const { data, error } = await tryCatch(
axios.patch(
`${process.env.MAIN_SERVER}/lst/api/forklifts/leases/${id}`,
updates,
{
headers: {
"Content-Type": "application/json",
Cookie: setCookie.split(";")[0],
},
withCredentials: true,
},
),
);
if (error) {
//console.log(error);
log.error(
{ stack: error },
"There was an error updating the lease to Main Server",
);
}
log.info(
{ stack: data?.data },
"A new lease was just updated to the server.",
);
}
res.status(200).json({ message: `${id} was just updated` });
} catch (error) {
//console.log(error);
res.status(200).json({ message: "Error updating lease", error });
}
});
export default router;

View File

@@ -1,9 +1,14 @@
import type { Express, Request, Response } from "express"; import type { Express, Request, Response } from "express";
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js"; import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
import companies from "./companies/companiesRoutes.js"; import companies from "./companies/companiesRoutes.js";
import leases from "./leases/leaseRoutes.js";
export const setupForkliftRoutes = (app: Express, basePath: string) => { export const setupForkliftRoutes = (app: Express, basePath: string) => {
app.use( app.use(
basePath + "/api/forklifts/companies", // will pass bc system admin but this is just telling us we need this basePath + "/api/forklifts/companies", // will pass bc system admin but this is just telling us we need this
companies, companies,
); );
app.use(
basePath + "/api/forklifts/leases", // will pass bc system admin but this is just telling us we need this
leases,
);
}; };

View File

@@ -1,13 +1,26 @@
import { date, pgTable, text, uuid } from "drizzle-orm/pg-core"; import { date, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createSelectSchema } from "drizzle-zod"; import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import z from "zod";
import { forkliftCompanies } from "./forkliftLeaseCompanys.js"; import { forkliftCompanies } from "./forkliftLeaseCompanys.js";
export const leases = pgTable("leases", { export const leases = pgTable("leases", {
id: uuid("id").defaultRandom().primaryKey(), id: uuid("id").defaultRandom().primaryKey(),
leaseNumber: text("lease_number").notNull(), leaseNumber: text("lease_number").unique().notNull(),
companyId: uuid("company_id").references(() => forkliftCompanies.id), companyId: uuid("company_id").references(() => forkliftCompanies.id),
startDate: date("start_date"), startDate: date("start_date"),
endDate: date("end_date"), endDate: date("end_date"),
leaseLink: text("lease_link"), leaseLink: text("lease_link"),
add_date: timestamp("add_date").defaultNow(),
add_user: text("add_user").default("LST"),
upd_date: timestamp("upd_date").defaultNow(),
upd_user: text("upd_user").default("LST"),
}); });
export const selectLeasesDataSchema = createSelectSchema(leases); export const selectLeasesDataSchema = createSelectSchema(leases);
export const insertLeasesCompanySchema = createInsertSchema(leases).extend({
leaseNumber: z.string().min(3),
// zipcode: z
// .string()
// .regex(/^\d{5}$/)
// .optional(),
});

View File

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

View File

@@ -1,6 +1,7 @@
import { Link, useRouterState } from "@tanstack/react-router"; import { Link, useRouterState } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
import NewCompanyForm from "@/routes/_app/_forklifts/-components/NewCompany"; import NewCompanyForm from "@/routes/_app/_forklifts/-components/NewCompany";
import NewLeaseForm from "@/routes/_app/_forklifts/-components/NewLease";
import { useAuth, useLogout } from "../../lib/authClient"; import { useAuth, useLogout } from "../../lib/authClient";
import { ModeToggle } from "../mode-toggle"; import { ModeToggle } from "../mode-toggle";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
@@ -21,6 +22,7 @@ export default function Nav() {
const router = useRouterState(); const router = useRouterState();
const currentPath = router.location.href; const currentPath = router.location.href;
const [openDialog, setOpenDialog] = useState(false); const [openDialog, setOpenDialog] = useState(false);
const [openLeaseDialog, setOpenLeaseDialog] = useState(false);
return ( return (
<nav className="flex justify-end w-full shadow "> <nav className="flex justify-end w-full shadow ">
<div className="m-2 flex flex-row gap-1"> <div className="m-2 flex flex-row gap-1">
@@ -41,7 +43,7 @@ export default function Nav() {
{location.pathname.includes("forklifts") && ( {location.pathname.includes("forklifts") && (
<> <>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger asChild>
<Button>Forklifts</Button> <Button>Forklifts</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
@@ -55,9 +57,17 @@ export default function Nav() {
> >
New Company New Company
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
// just open the dialog when clicked
setOpenLeaseDialog(true);
}}
>
New Lease
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{/* Dialog mounted outside the menu */} {/* Company */}
{openDialog && ( {openDialog && (
<Dialog open={openDialog} onOpenChange={setOpenDialog}> <Dialog open={openDialog} onOpenChange={setOpenDialog}>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
@@ -65,6 +75,16 @@ export default function Nav() {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}
{openLeaseDialog && (
<Dialog
open={openLeaseDialog}
onOpenChange={setOpenLeaseDialog}
>
<DialogContent className="sm:max-w-fit">
<NewLeaseForm setOpenDialog={setOpenLeaseDialog} />
</DialogContent>
</Dialog>
)}
</> </>
)} )}
</div> </div>

View File

@@ -1,58 +1,60 @@
import * as React from "react"; import * as React from "react"
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../../lib/utils"; import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
default: default: "bg-primary text-primary-foreground hover:bg-primary/90",
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive:
destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline:
outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary:
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost:
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9", icon: "size-9",
}, "icon-sm": "size-8",
}, "icon-lg": "size-10",
defaultVariants: { },
variant: "default", },
size: "default", defaultVariants: {
}, variant: "default",
} size: "default",
); },
}
)
function Button({ function Button({
className, className,
variant, variant,
size, size,
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"button"> & }: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean; asChild?: boolean
}) { }) {
const Comp = asChild ? Slot : "button"; const Comp = asChild ? Slot : "button"
return ( return (
<Comp <Comp
data-slot="button" data-slot="button"
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
{...props} {...props}
/> />
); )
} }
export { Button, buttonVariants }; export { Button, buttonVariants }

View File

@@ -1,225 +1,214 @@
import * as React from "react"; import * as React from "react"
import { import {
ChevronDownIcon, ChevronDownIcon,
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
} from "lucide-react"; } from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"; import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { buttonVariants, Button } from "./button";
import { cn } from "../../lib/utils"; import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({ function Calendar({
className, className,
classNames, classNames,
showOutsideDays = true, showOutsideDays = true,
captionLayout = "label", captionLayout = "label",
buttonVariant = "ghost", buttonVariant = "ghost",
formatters, formatters,
components, components,
...props ...props
}: React.ComponentProps<typeof DayPicker> & { }: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]; buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) { }) {
const defaultClassNames = getDefaultClassNames(); const defaultClassNames = getDefaultClassNames()
return ( return (
<DayPicker <DayPicker
showOutsideDays={showOutsideDays} showOutsideDays={showOutsideDays}
className={cn( className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent", "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className className
)} )}
captionLayout={captionLayout} captionLayout={captionLayout}
formatters={{ formatters={{
formatMonthDropdown: (date) => formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }), date.toLocaleString("default", { month: "short" }),
...formatters, ...formatters,
}} }}
classNames={{ classNames={{
root: cn("w-fit", defaultClassNames.root), root: cn("w-fit", defaultClassNames.root),
months: cn( months: cn(
"flex gap-4 flex-col md:flex-row relative", "flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months defaultClassNames.months
), ),
month: cn( month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
"flex flex-col w-full gap-4", nav: cn(
defaultClassNames.month "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
), defaultClassNames.nav
nav: cn( ),
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", button_previous: cn(
defaultClassNames.nav buttonVariants({ variant: buttonVariant }),
), "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
button_previous: cn( defaultClassNames.button_previous
buttonVariants({ variant: buttonVariant }), ),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", button_next: cn(
defaultClassNames.button_previous buttonVariants({ variant: buttonVariant }),
), "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
button_next: cn( defaultClassNames.button_next
buttonVariants({ variant: buttonVariant }), ),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", month_caption: cn(
defaultClassNames.button_next "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
), defaultClassNames.month_caption
month_caption: cn( ),
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", dropdowns: cn(
defaultClassNames.month_caption "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
), defaultClassNames.dropdowns
dropdowns: cn( ),
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", dropdown_root: cn(
defaultClassNames.dropdowns "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
), defaultClassNames.dropdown_root
dropdown_root: cn( ),
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", dropdown: cn(
defaultClassNames.dropdown_root "absolute bg-popover inset-0 opacity-0",
), defaultClassNames.dropdown
dropdown: cn( ),
"absolute bg-popover inset-0 opacity-0", caption_label: cn(
defaultClassNames.dropdown "select-none font-medium",
), captionLayout === "label"
caption_label: cn( ? "text-sm"
"select-none font-medium", : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
captionLayout === "label" defaultClassNames.caption_label
? "text-sm" ),
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", table: "w-full border-collapse",
defaultClassNames.caption_label weekdays: cn("flex", defaultClassNames.weekdays),
), weekday: cn(
table: "w-full border-collapse", "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
weekdays: cn("flex", defaultClassNames.weekdays), defaultClassNames.weekday
weekday: cn( ),
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", week: cn("flex w-full mt-2", defaultClassNames.week),
defaultClassNames.weekday week_number_header: cn(
), "select-none w-(--cell-size)",
week: cn("flex w-full mt-2", defaultClassNames.week), defaultClassNames.week_number_header
week_number_header: cn( ),
"select-none w-(--cell-size)", week_number: cn(
defaultClassNames.week_number_header "text-[0.8rem] select-none text-muted-foreground",
), defaultClassNames.week_number
week_number: cn( ),
"text-[0.8rem] select-none text-muted-foreground", day: cn(
defaultClassNames.week_number "relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
), props.showWeekNumber
day: cn( ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", : "[&:first-child[data-selected=true]_button]:rounded-l-md",
defaultClassNames.day defaultClassNames.day
), ),
range_start: cn( range_start: cn(
"rounded-l-md bg-accent", "rounded-l-md bg-accent",
defaultClassNames.range_start defaultClassNames.range_start
), ),
range_middle: cn( range_middle: cn("rounded-none", defaultClassNames.range_middle),
"rounded-none", range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
defaultClassNames.range_middle today: cn(
), "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
range_end: cn( defaultClassNames.today
"rounded-r-md bg-accent", ),
defaultClassNames.range_end outside: cn(
), "text-muted-foreground aria-selected:text-muted-foreground",
today: cn( defaultClassNames.outside
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", ),
defaultClassNames.today disabled: cn(
), "text-muted-foreground opacity-50",
outside: cn( defaultClassNames.disabled
"text-muted-foreground aria-selected:text-muted-foreground", ),
defaultClassNames.outside hidden: cn("invisible", defaultClassNames.hidden),
), ...classNames,
disabled: cn( }}
"text-muted-foreground opacity-50", components={{
defaultClassNames.disabled Root: ({ className, rootRef, ...props }) => {
), return (
hidden: cn("invisible", defaultClassNames.hidden), <div
...classNames, data-slot="calendar"
}} ref={rootRef}
components={{ className={cn(className)}
Root: ({ className, rootRef, ...props }) => { {...props}
return ( />
<div )
data-slot="calendar" },
ref={rootRef} Chevron: ({ className, orientation, ...props }) => {
className={cn(className)} if (orientation === "left") {
{...props} return (
/> <ChevronLeftIcon className={cn("size-4", className)} {...props} />
); )
}, }
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon
className={cn("size-4", className)}
{...props}
/>
);
}
if (orientation === "right") { if (orientation === "right") {
return ( return (
<ChevronRightIcon <ChevronRightIcon
className={cn("size-4", className)} className={cn("size-4", className)}
{...props} {...props}
/> />
); )
} }
return ( return (
<ChevronDownIcon <ChevronDownIcon className={cn("size-4", className)} {...props} />
className={cn("size-4", className)} )
{...props} },
/> DayButton: CalendarDayButton,
); WeekNumber: ({ children, ...props }) => {
}, return (
DayButton: CalendarDayButton, <td {...props}>
WeekNumber: ({ children, ...props }) => { <div className="flex size-(--cell-size) items-center justify-center text-center">
return ( {children}
<td {...props}> </div>
<div className="flex size-(--cell-size) items-center justify-center text-center"> </td>
{children} )
</div> },
</td> ...components,
); }}
}, {...props}
...components, />
}} )
{...props}
/>
);
} }
function CalendarDayButton({ function CalendarDayButton({
className, className,
day, day,
modifiers, modifiers,
...props ...props
}: React.ComponentProps<typeof DayButton>) { }: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames(); const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null); const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => { React.useEffect(() => {
if (modifiers.focused) ref.current?.focus(); if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused]); }, [modifiers.focused])
return ( return (
<Button <Button
ref={ref} ref={ref}
variant="ghost" variant="ghost"
size="icon" size="icon"
data-day={day.date.toLocaleDateString()} data-day={day.date.toLocaleDateString()}
data-selected-single={ data-selected-single={
modifiers.selected && modifiers.selected &&
!modifiers.range_start && !modifiers.range_start &&
!modifiers.range_end && !modifiers.range_end &&
!modifiers.range_middle !modifiers.range_middle
} }
data-range-start={modifiers.range_start} data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end} data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle} data-range-middle={modifiers.range_middle}
className={cn( className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70", "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day, defaultClassNames.day,
className className
)} )}
{...props} {...props}
/> />
); )
} }
export { Calendar, CalendarDayButton }; export { Calendar, CalendarDayButton }

View File

@@ -1,31 +1,31 @@
import { format } from "date-fns"; import { format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react"; import { Calendar as CalendarIcon } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
import { Button } from "./button"; import { Button } from "./button";
import { Calendar } from "./calendar"; import { Calendar } from "./calendar";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
export function DatePicker({ export function DatePicker({
date, date,
onChange, onChange,
}: { }: {
date?: Date; date?: Date;
onChange?: (d: Date | undefined) => void; onChange?: (d: Date | undefined) => void;
}) { }) {
return ( return (
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
data-empty={!date} data-empty={!date}
className="data-[empty=true]:text-muted-foreground w-[200px] justify-start text-left font-normal" className="data-[empty=true]:text-muted-foreground w-[200px] justify-start text-left font-normal"
> >
<CalendarIcon className="mr-2 h-4 w-4" /> <CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : <span>Pick a date</span>} {date ? format(date, "PPP") : <span>Pick a date</span>}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-0"> <PopoverContent className="w-auto p-0">
<Calendar mode="single" selected={date} onSelect={onChange} /> <Calendar mode="single" selected={date} onSelect={onChange} />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
); );
} }

View File

@@ -1,263 +1,255 @@
import * as React from "react"; import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "../../lib/utils"; import { cn } from "@/lib/utils"
function DropdownMenu({ function DropdownMenu({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />; return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
} }
function DropdownMenuPortal({ function DropdownMenuPortal({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return ( return (
<DropdownMenuPrimitive.Portal <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
data-slot="dropdown-menu-portal" )
{...props}
/>
);
} }
function DropdownMenuTrigger({ function DropdownMenuTrigger({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return ( return (
<DropdownMenuPrimitive.Trigger <DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger" data-slot="dropdown-menu-trigger"
{...props} {...props}
/> />
); )
} }
function DropdownMenuContent({ function DropdownMenuContent({
className, className,
sideOffset = 4, sideOffset = 4,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return ( return (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content" data-slot="dropdown-menu-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className className
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
); )
} }
function DropdownMenuGroup({ function DropdownMenuGroup({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return ( return (
<DropdownMenuPrimitive.Group <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
data-slot="dropdown-menu-group" )
{...props}
/>
);
} }
function DropdownMenuItem({ function DropdownMenuItem({
className, className,
inset, inset,
variant = "default", variant = "default",
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean; inset?: boolean
variant?: "default" | "destructive"; variant?: "default" | "destructive"
}) { }) {
return ( return (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item" data-slot="dropdown-menu-item"
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
/> />
); )
} }
function DropdownMenuCheckboxItem({ function DropdownMenuCheckboxItem({
className, className,
children, children,
checked, checked,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return ( return (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" /> <CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
); )
} }
function DropdownMenuRadioGroup({ function DropdownMenuRadioGroup({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return ( return (
<DropdownMenuPrimitive.RadioGroup <DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group" data-slot="dropdown-menu-radio-group"
{...props} {...props}
/> />
); )
} }
function DropdownMenuRadioItem({ function DropdownMenuRadioItem({
className, className,
children, children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return ( return (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" /> <CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
); )
} }
function DropdownMenuLabel({ function DropdownMenuLabel({
className, className,
inset, inset,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean; inset?: boolean
}) { }) {
return ( return (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label" data-slot="dropdown-menu-label"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className className
)} )}
{...props} {...props}
/> />
); )
} }
function DropdownMenuSeparator({ function DropdownMenuSeparator({
className, className,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return ( return (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator" data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
); )
} }
function DropdownMenuShortcut({ function DropdownMenuShortcut({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="dropdown-menu-shortcut" data-slot="dropdown-menu-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className
)} )}
{...props} {...props}
/> />
); )
} }
function DropdownMenuSub({ function DropdownMenuSub({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return ( return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
<DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
);
} }
function DropdownMenuSubTrigger({ function DropdownMenuSubTrigger({
className, className,
inset, inset,
children, children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean; inset?: boolean
}) { }) {
return ( return (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger" data-slot="dropdown-menu-sub-trigger"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8", "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto size-4" /> <ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
); )
} }
function DropdownMenuSubContent({ function DropdownMenuSubContent({
className, className,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return ( return (
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content" data-slot="dropdown-menu-sub-content"
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className className
)} )}
{...props} {...props}
/> />
); )
} }
export { export {
DropdownMenu, DropdownMenu,
DropdownMenuPortal, DropdownMenuPortal,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuRadioGroup, DropdownMenuRadioGroup,
DropdownMenuRadioItem, DropdownMenuRadioItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuShortcut,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuSubContent, DropdownMenuSubContent,
}; }

View File

@@ -0,0 +1,53 @@
import { ChevronDownIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Label } from "../../../components/ui/label";
import { useFieldContext } from "..";
import { FieldErrors } from "./FieldErrors";
type DateFieldProps = {
label: string;
};
export const DateField = ({ label }: DateFieldProps) => {
const field = useFieldContext<any>();
const [open, setOpen] = useState(false);
const date = field.state.value;
return (
<div className="grid gap-3">
<Label htmlFor={field.name}>{label}</Label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
id="date"
className="w-48 justify-between font-normal"
>
{date ? date.toLocaleDateString() : "Select date"}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={date}
captionLayout="dropdown"
startMonth={new Date(new Date().getFullYear() - 10, 0)}
endMonth={new Date(new Date().getFullYear() + 20, 0)}
onSelect={(selected) => {
field.handleChange(selected ?? undefined);
setOpen(false);
}}
/>
</PopoverContent>
</Popover>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -1,21 +1,23 @@
import { createFormHook, createFormHookContexts } from "@tanstack/react-form"; import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
import { SubmitButton } from "./components/SubmitButton"; import { DateField } from "./components/CalenderSelect";
import { InputField } from "./components/InputField";
import { SelectField } from "./components/SelectField";
import { CheckboxField } from "./components/CheckBox"; import { CheckboxField } from "./components/CheckBox";
import { InputField } from "./components/InputField";
import { InputPasswordField } from "./components/InputPasswordField"; import { InputPasswordField } from "./components/InputPasswordField";
import { SelectField } from "./components/SelectField";
import { SubmitButton } from "./components/SubmitButton";
export const { fieldContext, useFieldContext, formContext, useFormContext } = export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts(); createFormHookContexts();
export const { useAppForm } = createFormHook({ export const { useAppForm } = createFormHook({
fieldComponents: { fieldComponents: {
InputField, InputField,
InputPasswordField, InputPasswordField,
SelectField, SelectField,
CheckboxField, CheckboxField,
}, DateField,
formComponents: { SubmitButton }, },
fieldContext, formComponents: { SubmitButton },
formContext, fieldContext,
formContext,
}); });

View File

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

View File

@@ -0,0 +1,112 @@
import {
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
type SortingState,
useReactTable,
} from "@tanstack/react-table";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export default function TableNoExpand({
data,
columns,
}: {
data: any;
columns: any;
}) {
const [sorting, setSorting] = useState<SortingState>([]);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
//renderSubComponent: ({ row }: { row: any }) => <ExpandedRow row={row} />,
//getRowCanExpand: () => true,
state: {
sorting,
},
});
return (
<div className="p-4">
<div className="w-fit">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<React.Fragment key={row.id}>
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
{/* {row.getIsExpanded() && (
<TableRow>
<TableCell colSpan={row.getVisibleCells().length}>
{renderSubComponent({ row })}
</TableCell>
</TableRow>
)} */}
</React.Fragment>
))}
</TableBody>
</Table>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,128 @@
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 { getCompanies } from "@/lib/querys/forklifts/getCompanies";
import { getLeases } from "@/lib/querys/forklifts/getLeases";
import { useAppForm } from "../../../../lib/formStuff";
type CompanyData = {
id: string;
name: string;
};
export default function NewLeaseForm({
setOpenDialog,
}: {
setOpenDialog: any;
}) {
//const search = useSearch({ from: "/_app/(auth)/login" });
const { data, isLoading } = useQuery(getCompanies());
const { refetch } = useQuery(getLeases());
const form = useAppForm({
defaultValues: {
companyId: "",
leaseNumber: "",
startDate: "",
endDate: "",
},
onSubmit: async ({ value }) => {
const data = {
leaseNumber: value.leaseNumber.trimStart().trimEnd(),
startDate: format(value.startDate, "MM/dd/yyyy"),
endDate: format(value.endDate, "MM/dd/yyyy"),
companyId: value.companyId,
};
console.log(data);
try {
await axios.post("/lst/api/forklifts/leases", data);
form.reset();
setOpenDialog(false);
refetch();
toast.success(`${value.leaseNumber} was just created `);
} catch (error) {
// @ts-ignore
if (!error.response.data.success) {
// @ts-ignore
toast.error(error?.response?.data.message);
} else {
// @ts-ignore
toast.error(error?.message);
}
}
},
});
if (isLoading) return <div>Loading Companies</div>;
// remap the companies to fit out select field
const companyMap = data.map((i: CompanyData) => {
return { value: i.id, label: i.name };
});
//const currentYear = new Date().getFullYear();
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="companyId"
children={(field) => (
<field.SelectField
label="Select Company"
placeholder="Companies"
options={companyMap}
/>
)}
/>
<form.AppField
name="leaseNumber"
children={(field) => (
<field.InputField
label="Lease Number"
inputType="string"
required={false}
/>
)}
/>
<div className="flex flex-row gap-2 mt-2 mb-2">
<form.AppField
name="startDate"
children={(field) => <field.DateField label="Start Date" />}
/>
<form.AppField
name="endDate"
children={(field) => <field.DateField label="End Date" />}
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button type="submit">Submit</Button>
</DialogFooter>
</form>
</>
);
}

View File

@@ -1,17 +1,9 @@
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { import { createColumnHelper } from "@tanstack/react-table";
createColumnHelper,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
type SortingState,
useReactTable,
} from "@tanstack/react-table";
import axios from "axios"; import axios from "axios";
import { Activity, ArrowDown, ArrowUp } from "lucide-react"; import { Activity, ArrowDown, ArrowUp } from "lucide-react";
import React, { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -22,15 +14,9 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { getCompanies } from "@/lib/querys/forklifts/getCompanies"; import { getCompanies } from "@/lib/querys/forklifts/getCompanies";
import TableNoExpand from "@/lib/tableStuff/TableNoExpand";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
type Company = { type Company = {
@@ -64,7 +50,6 @@ function RouteComponent() {
isLoading, isLoading,
refetch, refetch,
} = useQuery(getCompanies()); } = useQuery(getCompanies());
const [sorting, setSorting] = useState<SortingState>([]);
const columnHelper = createColumnHelper<Company>(); const columnHelper = createColumnHelper<Company>();
const submitting = useRef(false); const submitting = useRef(false);
@@ -192,91 +177,8 @@ function RouteComponent() {
}), }),
]; ];
const table = useReactTable({
data: companyData,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
//renderSubComponent: ({ row }: { row: any }) => <ExpandedRow row={row} />,
//getRowCanExpand: () => true,
state: {
sorting,
},
});
if (isLoading) { if (isLoading) {
return <div className="m-auto">Loading user data</div>; return <div className="m-auto">Loading user data</div>;
} }
return ( return <TableNoExpand data={companyData} columns={columns} />;
<div className="p-4">
<div className="w-fit">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<React.Fragment key={row.id}>
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
{/* {row.getIsExpanded() && (
<TableRow>
<TableCell colSpan={row.getVisibleCells().length}>
{renderSubComponent({ row })}
</TableCell>
</TableRow>
)} */}
</React.Fragment>
))}
</TableBody>
</Table>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
);
} }

View File

@@ -0,0 +1,124 @@
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 { useRef } from "react";
import { Button } from "@/components/ui/button";
import { getLeases } from "@/lib/querys/forklifts/getLeases";
import TableNoExpand from "@/lib/tableStuff/TableNoExpand";
type Leases = {
id: string;
leaseNumber: string;
startDate: Date;
endDate: Date;
leaseLink: string | null;
companyName: string;
add_user: string;
add_date: Date;
upd_user: string;
upd_date: Date;
};
export const Route = createFileRoute("/_app/_forklifts/forklifts/leases")({
component: RouteComponent,
});
function RouteComponent() {
const { data: leaseData = [], isLoading } = useQuery(getLeases());
const columnHelper = createColumnHelper<Leases>();
//const submitting = useRef(false);
const columns = [
columnHelper.accessor("companyName", {
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="flex flex-row gap-2">Company</span>
{column.getIsSorted() === "asc" ? (
<ArrowUp className="ml-2 h-4 w-4" />
) : (
<ArrowDown className="ml-2 h-4 w-4" />
)}
</Button>
);
},
}),
columnHelper.accessor("leaseNumber", {
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="flex flex-row gap-2">Lease Number</span>
{column.getIsSorted() === "asc" ? (
<ArrowUp className="ml-2 h-4 w-4" />
) : (
<ArrowDown className="ml-2 h-4 w-4" />
)}
</Button>
);
},
cell: ({ getValue }) => {
return <span>{getValue()}</span>;
},
}),
columnHelper.accessor("startDate", {
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>;
},
}),
columnHelper.accessor("endDate", {
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="flex flex-row gap-2">End 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={leaseData} columns={columns} />;
}

View File

@@ -0,0 +1,4 @@
ALTER TABLE "leases" ADD COLUMN "add_date" timestamp DEFAULT now();--> statement-breakpoint
ALTER TABLE "leases" ADD COLUMN "add_user" text DEFAULT 'LST';--> statement-breakpoint
ALTER TABLE "leases" ADD COLUMN "upd_date" timestamp DEFAULT now();--> statement-breakpoint
ALTER TABLE "leases" ADD COLUMN "upd_user" text DEFAULT 'LST';

View File

@@ -0,0 +1 @@
ALTER TABLE "leases" ADD CONSTRAINT "leases_lease_number_unique" UNIQUE("lease_number");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -260,6 +260,20 @@
"when": 1762112909957, "when": 1762112909957,
"tag": "0036_sticky_brood", "tag": "0036_sticky_brood",
"breakpoints": true "breakpoints": true
},
{
"idx": 37,
"version": "7",
"when": 1762298425546,
"tag": "0037_cold_blur",
"breakpoints": true
},
{
"idx": 38,
"version": "7",
"when": 1762298736944,
"tag": "0038_secret_luminals",
"breakpoints": true
} }
] ]
} }