Ui fixings
This commit is contained in:
@@ -22,7 +22,11 @@ import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
@@ -86,6 +90,14 @@ const STOCK_TITLES: Record<string, string> = {
|
||||
"/dashboard/stock/wastage/new": "Record Wastage",
|
||||
}
|
||||
|
||||
const PRODUCTION_TITLES: Record<string, string> = {
|
||||
"/dashboard/production": "Production",
|
||||
"/dashboard/production/templates": "Production Templates",
|
||||
"/dashboard/production/templates/new": "New Template",
|
||||
"/dashboard/production/runs": "Production Runs",
|
||||
"/dashboard/production/produced": "Produced Items",
|
||||
}
|
||||
|
||||
function titleFromPath(pathname: string) {
|
||||
if (pathname === "/dashboard/sales/invoices") return "Sales"
|
||||
if (pathname === "/dashboard/sales/invoices/new") return "New Sale"
|
||||
@@ -103,6 +115,10 @@ function titleFromPath(pathname: string) {
|
||||
if (/^\/dashboard\/stock\/transfers\/[^/]+$/.test(pathname)) return "Stock Transfer"
|
||||
if (/^\/dashboard\/stock\/counts\/[^/]+$/.test(pathname)) return "Stock Count"
|
||||
|
||||
if (PRODUCTION_TITLES[pathname]) return PRODUCTION_TITLES[pathname]
|
||||
if (/^\/dashboard\/production\/templates\/[^/]+$/.test(pathname)) return "Template Builder"
|
||||
if (/^\/dashboard\/production\/runs\/[^/]+$/.test(pathname)) return "Production Run"
|
||||
|
||||
if (PROCUREMENT_TITLES[pathname]) return PROCUREMENT_TITLES[pathname]
|
||||
if (/^\/dashboard\/procurement\/requisitions\/[^/]+$/.test(pathname)) return "Requisition"
|
||||
if (/^\/dashboard\/procurement\/rfqs\/[^/]+$/.test(pathname)) return "RFQ"
|
||||
@@ -277,12 +293,16 @@ export function Header() {
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
aria-label="Go back"
|
||||
className="flex size-9 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
className="flex size-8 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground sm:size-9"
|
||||
>
|
||||
<ArrowLeft className="size-5" />
|
||||
</button>
|
||||
)}
|
||||
<h1 className="min-w-0 truncate text-base font-bold tracking-tight text-foreground sm:text-xl">{title}</h1>
|
||||
{/* Phones wrap to two lines rather than truncating — at this width a one-line title
|
||||
is cut to a few letters ("Prod…"). One truncated line from sm, where it fits. */}
|
||||
<h1 className="line-clamp-2 min-w-0 text-base leading-tight font-bold tracking-tight wrap-break-word text-foreground sm:truncate sm:text-xl sm:leading-normal">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-1.5 sm:gap-3">
|
||||
@@ -377,7 +397,8 @@ export function Header() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ThemeToggle />
|
||||
{/* 108px wide — on phones it moves into the account menu so the title has room. */}
|
||||
<ThemeToggle className="hidden sm:flex" />
|
||||
|
||||
<Popover onOpenChange={(open) => open && loadNotifications()}>
|
||||
<PopoverTrigger
|
||||
@@ -443,7 +464,7 @@ export function Header() {
|
||||
</Popover>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-2 rounded-full py-1 pr-2 pl-1 outline-none hover:bg-muted">
|
||||
<DropdownMenuTrigger className="flex items-center gap-2 rounded-full p-1 outline-none hover:bg-muted sm:pr-2">
|
||||
<Avatar>
|
||||
<AvatarFallback className="bg-primary/10 font-semibold text-primary">
|
||||
{user ? initials(displayName(user)) : "?"}
|
||||
@@ -466,6 +487,39 @@ export function Header() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3 px-2 pb-2.5 sm:hidden">
|
||||
<span className="text-sm text-muted-foreground">Theme</span>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
{/* The header's branch picker is hidden on phones, so admins switch here. Radio
|
||||
items rather than a nested Select: a Select's portaled popup would read as an
|
||||
outside press and close this menu. */}
|
||||
{branchingEnabled && isAdmin && (
|
||||
<div className="sm:hidden">
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="flex items-center gap-1.5 px-2 text-sm">
|
||||
<Building2 className="size-3.5" />
|
||||
Branch
|
||||
</DropdownMenuLabel>
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
<DropdownMenuRadioGroup
|
||||
value={activeBranchId ? String(activeBranchId) : ALL_BRANCHES}
|
||||
onValueChange={(value: string) => handleBranchChange(value)}
|
||||
>
|
||||
<DropdownMenuRadioItem value={ALL_BRANCHES} closeOnClick className="py-2 text-base">
|
||||
All Branches
|
||||
</DropdownMenuRadioItem>
|
||||
{branches.map((b) => (
|
||||
<DropdownMenuRadioItem key={b.branchId} value={String(b.branchId)} closeOnClick className="py-2 text-base">
|
||||
{b.name}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</div>
|
||||
</DropdownMenuGroup>
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
render={<Link href="/dashboard/profile" />}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
/** Sign-in page chrome shared by the auth screens: illustration on the right, the form panel
|
||||
* (logo + content) overlaying the left 45%. */
|
||||
export function AuthShell({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden">
|
||||
{/* Full-page background photo — right side only. Dark mode swaps to the backdrop-free
|
||||
cutout (whithoutbackground.png) so there's no white scene box floating on the
|
||||
dark panel. */}
|
||||
<div className="absolute inset-y-0 left-[45%] right-0 bg-white dark:bg-background">
|
||||
<Image
|
||||
src="/images/loginerps.png"
|
||||
alt="ERP Illustration"
|
||||
fill
|
||||
className="object-contain object-center dark:hidden"
|
||||
priority
|
||||
/>
|
||||
<Image
|
||||
src="/images/whithoutbackground.png"
|
||||
alt="ERP Illustration"
|
||||
fill
|
||||
className="hidden object-contain object-center dark:block"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Form panel — overlays left side, no hard border with the photo */}
|
||||
<main className="relative z-10 flex min-h-screen w-full flex-col justify-center bg-white dark:bg-background px-6 py-12 sm:px-12 md:w-[45%]">
|
||||
<div className="mb-8 flex justify-center">
|
||||
<Link href="/" className="inline-flex items-center">
|
||||
<Image src="/images/hexa-erp-logo.png" alt="Hexa ERP" width={300} height={150} />
|
||||
</Link>
|
||||
</div>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
// (Branch, Designation, EmploymentType) — identical shape to each other, so one
|
||||
// component parameterized by the resource's api/labels replaces 3 near-duplicate pages.
|
||||
import { useEffect, useState } from "react"
|
||||
import { CheckCircle2, ChevronLeft, ChevronRight, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react"
|
||||
import { CheckCircle2, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react"
|
||||
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -16,6 +16,7 @@ import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
||||
import { PaginationFooter } from "@/components/ui/pagination-footer"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { toast } from "@/components/ui/toast"
|
||||
@@ -507,54 +508,13 @@ export function CodeNameMasterPage<T extends CodeNamed>({
|
||||
)}
|
||||
|
||||
{pagination && pagination.totalItems > 0 && (
|
||||
<div className="hidden items-center gap-3 md:flex md:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing <span className="font-medium text-foreground">{(pagination.page - 1) * pagination.pageSize + 1}</span>–
|
||||
<span className="font-medium text-foreground">
|
||||
{Math.min(pagination.page * pagination.pageSize, pagination.totalItems)}
|
||||
</span>{" "}
|
||||
of <span className="font-medium text-foreground">{pagination.totalItems}</span> {title.toLowerCase()}
|
||||
</p>
|
||||
<label className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
Rows per page
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={(e) => { setPage(1); setPageSize(Number(e.target.value)) }}
|
||||
className="h-8 rounded-md border border-black bg-white px-2 text-sm text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
||||
aria-label="Rows per page"
|
||||
>
|
||||
{PAGE_SIZE_OPTIONS.map((size) => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={pagination.page <= 1}
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
Previous
|
||||
</Button>
|
||||
<span className="min-w-20 text-center text-sm text-muted-foreground">
|
||||
Page {pagination.page} of {pagination.totalPages}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={pagination.page >= pagination.totalPages}
|
||||
onClick={() => setPage((p) => Math.min(pagination.totalPages, p + 1))}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
<PaginationFooter
|
||||
pagination={pagination}
|
||||
onPageChange={setPage}
|
||||
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
||||
onPageSizeChange={(size) => { setPage(1); setPageSize(size) }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -52,25 +52,16 @@ function fmt(n: number): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* One stage on a run's production line, positioned from the run's own `posX`/`posY` (copied
|
||||
* from the template at creation) and colored by its live status. Clicking it opens the stage
|
||||
* drawer — handled by the page via `onNodeClick`, not here.
|
||||
* The body of a stage card — everything but the frame. Shared by the canvas node below and the
|
||||
* run page's phone list, so the two views of a stage can't drift apart.
|
||||
*/
|
||||
function RunStageNode({ data, selected }: NodeProps & { data: RunStageData }) {
|
||||
export function RunStageCardBody({ data }: { data: RunStageData }) {
|
||||
const color = STAGE_STATUS_COLOR[data.state]
|
||||
const running = data.actualStartAt !== null && data.actualMinutes === null
|
||||
const intakeShort = data.intake !== null && data.intake.delivered < data.intake.planned
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-52 cursor-pointer rounded-2xl bg-card p-3 shadow-sm ring-2 transition-all hover:ring-primary/40",
|
||||
selected ? "ring-primary" : "ring-foreground/10"
|
||||
)}
|
||||
style={data.isActive && !selected ? { boxShadow: `0 0 0 3px ${color}` } : undefined}
|
||||
>
|
||||
<Handle type="target" position={Position.Left} className="!bg-primary !size-2.5" />
|
||||
|
||||
<>
|
||||
<div className="flex items-start gap-1.5">
|
||||
<span className="mt-1 size-2.5 shrink-0 rounded-full" style={{ backgroundColor: color }} />
|
||||
<p className="min-w-0 flex-1 truncate text-sm font-bold text-foreground">{data.name}</p>
|
||||
@@ -116,7 +107,26 @@ function RunStageNode({ data, selected }: NodeProps & { data: RunStageData }) {
|
||||
{fmt(data.availableToTransfer)} to transfer
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* One stage on a run's production line, positioned from the run's own `posX`/`posY` (copied
|
||||
* from the template at creation) and colored by its live status. Clicking it opens the stage
|
||||
* drawer — handled by the page via `onNodeClick`, not here.
|
||||
*/
|
||||
function RunStageNode({ data, selected }: NodeProps & { data: RunStageData }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-52 cursor-pointer rounded-2xl bg-card p-3 shadow-sm ring-2 transition-all hover:ring-primary/40",
|
||||
selected ? "ring-primary" : "ring-foreground/10"
|
||||
)}
|
||||
style={data.isActive && !selected ? { boxShadow: `0 0 0 3px ${STAGE_STATUS_COLOR[data.state]}` } : undefined}
|
||||
>
|
||||
<Handle type="target" position={Position.Left} className="!bg-primary !size-2.5" />
|
||||
<RunStageCardBody data={data} />
|
||||
<Handle type="source" position={Position.Right} className="!bg-primary !size-2.5" />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { Building2, ChevronLeft, ChevronRight, CheckCircle2, Eye, Pencil, Plus, Search, Trash2, X, XCircle } from "lucide-react"
|
||||
import { Building2, CheckCircle2, Eye, Pencil, Plus, Search, Trash2, X, XCircle } from "lucide-react"
|
||||
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
@@ -22,6 +22,7 @@ import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
||||
import { PaginationFooter } from "@/components/ui/pagination-footer"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { toast } from "@/components/ui/toast"
|
||||
@@ -477,54 +478,13 @@ export function BranchMasterPage() {
|
||||
)}
|
||||
|
||||
{pagination && pagination.totalItems > 0 && (
|
||||
<div className="hidden items-center gap-3 md:flex md:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing <span className="font-medium text-foreground">{(pagination.page - 1) * pagination.pageSize + 1}</span>–
|
||||
<span className="font-medium text-foreground">
|
||||
{Math.min(pagination.page * pagination.pageSize, pagination.totalItems)}
|
||||
</span>{" "}
|
||||
of <span className="font-medium text-foreground">{pagination.totalItems}</span> branches
|
||||
</p>
|
||||
<label className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
Rows per page
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={(e) => { setPage(1); setPageSize(Number(e.target.value)) }}
|
||||
className="h-8 rounded-md border border-black bg-white px-2 text-sm text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
||||
aria-label="Rows per page"
|
||||
>
|
||||
{PAGE_SIZE_OPTIONS.map((size) => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={pagination.page <= 1}
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
Previous
|
||||
</Button>
|
||||
<span className="min-w-20 text-center text-sm text-muted-foreground">
|
||||
Page {pagination.page} of {pagination.totalPages}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={pagination.page >= pagination.totalPages}
|
||||
onClick={() => setPage((p) => Math.min(pagination.totalPages, p + 1))}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
<PaginationFooter
|
||||
pagination={pagination}
|
||||
onPageChange={setPage}
|
||||
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
||||
onPageSizeChange={(size) => { setPage(1); setPageSize(size) }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function OtpInput({ length = 6, value = "", onChange }: OtpInputP
|
||||
value={char}
|
||||
onChange={(e) => handleChange(e.target.value, i)}
|
||||
onKeyDown={(e) => handleKeyDown(e, i)}
|
||||
className="w-14 h-14 mx-1 text-center text-lg font-semibold border-2 border-input rounded-lg bg-background text-foreground transition-all outline-none placeholder:text-muted-foreground focus:border-ring focus:ring-3 focus:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:border-input dark:bg-input/30 dark:focus:ring-ring/30"
|
||||
className="size-11 sm:size-14 mx-0.5 sm:mx-1 text-center text-lg font-semibold border-2 border-input rounded-lg bg-background text-foreground transition-all outline-none placeholder:text-muted-foreground focus:border-ring focus:ring-3 focus:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:border-input dark:bg-input/30 dark:focus:ring-ring/30"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user