common components extraction.
This commit is contained in:
parent
e4da88b98b
commit
3fd769a36f
12 changed files with 218 additions and 153 deletions
19
apps/pharmanager/src/components/ui/BackLink.tsx
Normal file
19
apps/pharmanager/src/components/ui/BackLink.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
|
||||||
|
interface BackLinkProps {
|
||||||
|
to: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BackLink({ to, label }: BackLinkProps) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={to}
|
||||||
|
className="inline-flex items-center gap-1.5 text-sm text-blue-600 hover:underline mb-5"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Back to {label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
apps/pharmanager/src/components/ui/DetailRow.tsx
Normal file
29
apps/pharmanager/src/components/ui/DetailRow.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { cn } from "#/lib/cn";
|
||||||
|
|
||||||
|
interface DetailRowProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
valueClass?: string;
|
||||||
|
last?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DetailRow({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
valueClass = "",
|
||||||
|
last = false,
|
||||||
|
}: DetailRowProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex justify-between items-center py-2.5 text-sm",
|
||||||
|
!last && "border-b border-slate-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-slate-600 text-[13px]">{label}</span>
|
||||||
|
<span className={cn("font-medium text-right", valueClass)}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/pharmanager/src/components/ui/EmptyState.tsx
Normal file
38
apps/pharmanager/src/components/ui/EmptyState.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { cn } from "#/lib/cn";
|
||||||
|
import { buttonVariants } from "./Button";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
icon: LucideIcon;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
actionLabel?: string;
|
||||||
|
actionTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyState({
|
||||||
|
icon: Icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actionLabel,
|
||||||
|
actionTo,
|
||||||
|
}: EmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center py-16 px-6 text-slate-600">
|
||||||
|
<Icon className="w-12 h-12 mb-4 opacity-40" />
|
||||||
|
<h3 className="text-base font-semibold text-slate-900 mb-1.5">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm">{description}</p>
|
||||||
|
{actionLabel && actionTo && (
|
||||||
|
<Link
|
||||||
|
to={actionTo}
|
||||||
|
className={cn(buttonVariants({ variant: "primary" }), "mt-4")}
|
||||||
|
>
|
||||||
|
{actionLabel}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
apps/pharmanager/src/components/ui/FormField.tsx
Normal file
29
apps/pharmanager/src/components/ui/FormField.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { cn } from "#/lib/cn";
|
||||||
|
|
||||||
|
interface FormFieldProps {
|
||||||
|
label: string;
|
||||||
|
required?: boolean;
|
||||||
|
error?: string;
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormField({
|
||||||
|
label,
|
||||||
|
required = false,
|
||||||
|
error,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: FormFieldProps) {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-600 ml-0.5">*</span>}
|
||||||
|
</label>
|
||||||
|
{children}
|
||||||
|
{error && <p className="text-sm text-red-600 mt-1">{error}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/pharmanager/src/components/ui/SearchToolbar.tsx
Normal file
38
apps/pharmanager/src/components/ui/SearchToolbar.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { Search, Plus } from "lucide-react";
|
||||||
|
import { buttonVariants } from "./Button";
|
||||||
|
|
||||||
|
interface SearchToolbarProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder: string;
|
||||||
|
addLink: string;
|
||||||
|
addLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchToolbar({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
addLink,
|
||||||
|
addLabel,
|
||||||
|
}: SearchToolbarProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 mb-5 flex-wrap">
|
||||||
|
<div className="flex items-center gap-2 flex-1 min-w-[200px] max-w-[480px] px-3.5 py-2 bg-white rounded-md border border-slate-200 transition-all duration-200 focus-within:border-blue-600 focus-within:shadow-[0_0_0_3px_rgba(37,99,235,0.1)]">
|
||||||
|
<Search className="w-4 h-4 text-slate-600 shrink-0" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="bg-transparent border-none outline-none text-sm text-slate-900 w-full placeholder:text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Link to={addLink} className={buttonVariants({ variant: "primary" })}>
|
||||||
|
<Plus className="w-[15px] h-[15px]" />
|
||||||
|
{addLabel}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
export { Button, type ButtonProps, buttonVariants } from "./Button";
|
export { Button, type ButtonProps, buttonVariants } from "./Button";
|
||||||
export { Input, type InputProps, inputVariants } from "./Input";
|
export { Input, type InputProps, inputVariants } from "./Input";
|
||||||
export { Textarea, type TextareaProps, textareaVariants } from "./Textarea";
|
export { Textarea, type TextareaProps, textareaVariants } from "./Textarea";
|
||||||
export { Combobox } from "./Combobox";
|
|
||||||
export type { ComboboxOption } from "./Combobox";
|
|
||||||
export { Checkbox } from "./Checkbox";
|
export { Checkbox } from "./Checkbox";
|
||||||
export type { CheckboxProps } from "./Checkbox";
|
export type { CheckboxProps } from "./Checkbox";
|
||||||
|
export { Combobox } from "./Combobox";
|
||||||
|
export type { ComboboxOption } from "./Combobox";
|
||||||
|
export { DetailRow } from "./DetailRow";
|
||||||
|
export { BackLink } from "./BackLink";
|
||||||
|
export { FormField } from "./FormField";
|
||||||
|
export { EmptyState } from "./EmptyState";
|
||||||
|
export { SearchToolbar } from "./SearchToolbar";
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useState, useMemo, useCallback } from "react";
|
import { useState, useMemo, useCallback } from "react";
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
import { Search, Plus, Pencil, Trash2, Truck } from "lucide-react";
|
import { Pencil, Trash2, Truck } from "lucide-react";
|
||||||
import { GridTable } from "#/components/GridTable";
|
import { GridTable } from "#/components/GridTable";
|
||||||
import type { GridTableColumn } from "#/components/GridTable";
|
import type { GridTableColumn } from "#/components/GridTable";
|
||||||
import { Button, buttonVariants } from "#/components/ui";
|
import { Button, SearchToolbar } from "#/components/ui";
|
||||||
import {
|
import {
|
||||||
useListDistributors,
|
useListDistributors,
|
||||||
useRemoveDistributor,
|
useRemoveDistributor,
|
||||||
|
|
@ -149,25 +149,13 @@ function DistributorsIndexPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-5 flex-wrap">
|
<SearchToolbar
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-[200px] max-w-[400px] px-3.5 py-2 bg-white rounded-md border border-slate-200 transition-all duration-200 focus-within:border-blue-600 focus-within:shadow-[0_0_0_3px_rgba(37,99,235,0.1)]">
|
|
||||||
<Search className="w-4 h-4 text-slate-600 shrink-0" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={setSearchQuery}
|
||||||
placeholder="Search distributors..."
|
placeholder="Search distributors..."
|
||||||
className="bg-transparent border-none outline-none text-sm text-slate-900 w-full placeholder:text-slate-400"
|
addLink="/distributors/add"
|
||||||
|
addLabel="Add Distributor"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
to="/distributors/add"
|
|
||||||
className={buttonVariants({ variant: "primary" })}
|
|
||||||
>
|
|
||||||
<Plus className="w-[15px] h-[15px]" />
|
|
||||||
Add Distributor
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<GridTable
|
<GridTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { ArrowLeft, Pencil, Trash2, Pill, Package, Layers, DollarSign, EyeOff } from "lucide-react";
|
import { Pencil, Trash2, Pill, Package, Layers, DollarSign, EyeOff } from "lucide-react";
|
||||||
import { Button } from "#/components/ui";
|
import { Button, DetailRow, BackLink, EmptyState } from "#/components/ui";
|
||||||
import { useGetProductById, useRemoveProduct, trpc } from "shared-react";
|
import { useGetProductById, useRemoveProduct, trpc } from "shared-react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/products/$id")({
|
export const Route = createFileRoute("/products/$id")({
|
||||||
|
|
@ -38,17 +38,13 @@ function ProductDetailsPage() {
|
||||||
|
|
||||||
if (error || !product) {
|
if (error || !product) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center py-16 px-6 text-slate-600">
|
<EmptyState
|
||||||
<Pill className="w-12 h-12 mb-4 opacity-40" />
|
icon={Pill}
|
||||||
<h3 className="text-base font-semibold text-slate-900 mb-1.5">Product not found</h3>
|
title="Product not found"
|
||||||
<p className="text-sm mb-4">The product you're looking for doesn't exist.</p>
|
description="The product you're looking for doesn't exist."
|
||||||
<Link
|
actionLabel="Back to Products"
|
||||||
to="/products"
|
actionTo="/products"
|
||||||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"
|
/>
|
||||||
>
|
|
||||||
Back to Products
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,16 +60,9 @@ function ProductDetailsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<BackLink to="/products" label="Products" />
|
||||||
to="/products"
|
|
||||||
className="inline-flex items-center gap-1.5 text-sm text-blue-600 hover:underline mb-5"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
Back to Products
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-6 max-w-[960px]">
|
<div className="grid grid-cols-2 gap-6 max-w-[960px]">
|
||||||
{/* Basic Info */}
|
|
||||||
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
||||||
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
||||||
<Pill className="w-[14px] h-[14px] text-slate-600" />
|
<Pill className="w-[14px] h-[14px] text-slate-600" />
|
||||||
|
|
@ -89,7 +78,6 @@ function ProductDetailsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Inventory */}
|
|
||||||
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
||||||
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
||||||
<Package className="w-[14px] h-[14px] text-slate-600" />
|
<Package className="w-[14px] h-[14px] text-slate-600" />
|
||||||
|
|
@ -101,7 +89,6 @@ function ProductDetailsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Composition */}
|
|
||||||
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
||||||
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
||||||
<Layers className="w-[14px] h-[14px] text-slate-600" />
|
<Layers className="w-[14px] h-[14px] text-slate-600" />
|
||||||
|
|
@ -126,7 +113,6 @@ function ProductDetailsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pricing */}
|
|
||||||
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
||||||
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
||||||
<DollarSign className="w-[14px] h-[14px] text-slate-600" />
|
<DollarSign className="w-[14px] h-[14px] text-slate-600" />
|
||||||
|
|
@ -140,7 +126,6 @@ function ProductDetailsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Visibility */}
|
|
||||||
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden col-span-full">
|
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden col-span-full">
|
||||||
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
||||||
<EyeOff className="w-[14px] h-[14px] text-slate-600" />
|
<EyeOff className="w-[14px] h-[14px] text-slate-600" />
|
||||||
|
|
@ -153,7 +138,6 @@ function ProductDetailsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex items-center gap-2.5 py-5 mt-2 max-w-[960px]">
|
<div className="flex items-center gap-2.5 py-5 mt-2 max-w-[960px]">
|
||||||
<Button variant="primary">
|
<Button variant="primary">
|
||||||
<Pencil className="w-[15px] h-[15px]" />
|
<Pencil className="w-[15px] h-[15px]" />
|
||||||
|
|
@ -167,22 +151,3 @@ function ProductDetailsPage() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailRow({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
valueClass = "",
|
|
||||||
last = false,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
valueClass?: string;
|
|
||||||
last?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className={`flex justify-between items-center py-2.5 text-sm ${last ? "" : "border-b border-slate-200"}`}>
|
|
||||||
<span className="text-slate-600 text-[13px]">{label}</span>
|
|
||||||
<span className={`font-medium text-right ${valueClass}`}>{value}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useState, useMemo, useCallback } from "react";
|
import { useState, useMemo, useCallback } from "react";
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
import { Search, Plus, Pencil, Trash2, Pill } from "lucide-react";
|
import { Pencil, Trash2, Pill } from "lucide-react";
|
||||||
import { GridTable } from "#/components/GridTable";
|
import { GridTable } from "#/components/GridTable";
|
||||||
import type { GridTableColumn } from "#/components/GridTable";
|
import type { GridTableColumn } from "#/components/GridTable";
|
||||||
import { Button, buttonVariants } from "#/components/ui";
|
import { Button, SearchToolbar } from "#/components/ui";
|
||||||
import { useListProducts, useRemoveProduct, trpc } from "shared-react";
|
import { useListProducts, useRemoveProduct, trpc } from "shared-react";
|
||||||
|
|
||||||
interface ProductRow {
|
interface ProductRow {
|
||||||
|
|
@ -200,25 +200,13 @@ function ProductsIndexPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-5 flex-wrap">
|
<SearchToolbar
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-[200px] max-w-[480px] px-3.5 py-2 bg-white rounded-md border border-slate-200 transition-all duration-200 focus-within:border-blue-600 focus-within:shadow-[0_0_0_3px_rgba(37,99,235,0.1)]">
|
|
||||||
<Search className="w-4 h-4 text-slate-600 shrink-0" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={setSearchQuery}
|
||||||
placeholder="Search by product name, brand, or distributor..."
|
placeholder="Search by product name, brand, or distributor..."
|
||||||
className="bg-transparent border-none outline-none text-sm text-slate-900 w-full placeholder:text-slate-400"
|
addLink="/products/add"
|
||||||
|
addLabel="Add Product"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
to="/products/add"
|
|
||||||
className={buttonVariants({ variant: "primary" })}
|
|
||||||
>
|
|
||||||
<Plus className="w-[15px] h-[15px]" />
|
|
||||||
Add Product
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<GridTable
|
<GridTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { ArrowLeft, Pencil, Trash2, Package, Calendar, MapPin, Truck } from "lucide-react";
|
import { Pencil, Trash2, Package, Calendar, MapPin, Truck } from "lucide-react";
|
||||||
import { Button } from "#/components/ui";
|
import { Button, DetailRow, BackLink, EmptyState } from "#/components/ui";
|
||||||
import { useGetStockBatchById, useRemoveStockBatch, trpc } from "shared-react";
|
import { useGetStockBatchById, useRemoveStockBatch, trpc } from "shared-react";
|
||||||
|
|
||||||
function daysUntil(expiry: string): number {
|
function daysUntil(expiry: string): number {
|
||||||
|
|
@ -47,14 +47,13 @@ function StockDetailsPage() {
|
||||||
if (isLoading) return <div className="text-sm text-slate-600 py-8">Loading batch details...</div>;
|
if (isLoading) return <div className="text-sm text-slate-600 py-8">Loading batch details...</div>;
|
||||||
if (error || !batch) {
|
if (error || !batch) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center py-16 px-6 text-slate-600">
|
<EmptyState
|
||||||
<Package className="w-12 h-12 mb-4 opacity-40" />
|
icon={Package}
|
||||||
<h3 className="text-base font-semibold text-slate-900 mb-1.5">Batch not found</h3>
|
title="Batch not found"
|
||||||
<p className="text-sm mb-4">The batch you're looking for doesn't exist.</p>
|
description="The batch you're looking for doesn't exist."
|
||||||
<Link to="/stock" className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors">
|
actionLabel="Back to Stock"
|
||||||
Back to Stock
|
actionTo="/stock"
|
||||||
</Link>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,10 +65,7 @@ function StockDetailsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link to="/stock" className="inline-flex items-center gap-1.5 text-sm text-blue-600 hover:underline mb-5">
|
<BackLink to="/stock" label="Stock" />
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
Back to Stock
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-6 max-w-[800px]">
|
<div className="grid grid-cols-2 gap-6 max-w-[800px]">
|
||||||
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
||||||
|
|
@ -146,12 +142,3 @@ function StockDetailsPage() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailRow({ label, value, valueClass = "", last = false }: { label: string; value: string; valueClass?: string; last?: boolean }) {
|
|
||||||
return (
|
|
||||||
<div className={`flex justify-between items-center py-2.5 text-sm ${last ? "" : "border-b border-slate-200"}`}>
|
|
||||||
<span className="text-slate-600 text-[13px]">{label}</span>
|
|
||||||
<span className={`font-medium text-right ${valueClass}`}>{value}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useState, useMemo, useCallback } from "react";
|
import { useState, useMemo, useCallback } from "react";
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
import { Search, Plus, Pencil, Trash2, Package, Star } from "lucide-react";
|
import { Pencil, Trash2, Package, Star } from "lucide-react";
|
||||||
import { GridTable } from "#/components/GridTable";
|
import { GridTable } from "#/components/GridTable";
|
||||||
import type { GridTableColumn } from "#/components/GridTable";
|
import type { GridTableColumn } from "#/components/GridTable";
|
||||||
import { Button, buttonVariants } from "#/components/ui";
|
import { Button, SearchToolbar } from "#/components/ui";
|
||||||
import { useListStockBatches, useRemoveStockBatch, trpc } from "shared-react";
|
import { useListStockBatches, useRemoveStockBatch, trpc } from "shared-react";
|
||||||
|
|
||||||
interface StockRow {
|
interface StockRow {
|
||||||
|
|
@ -186,22 +186,13 @@ function StockIndexPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-5 flex-wrap">
|
<SearchToolbar
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-[200px] max-w-[480px] px-3.5 py-2 bg-white rounded-md border border-slate-200 transition-all duration-200 focus-within:border-blue-600 focus-within:shadow-[0_0_0_3px_rgba(37,99,235,0.1)]">
|
|
||||||
<Search className="w-4 h-4 text-slate-600 shrink-0" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={setSearchQuery}
|
||||||
placeholder="Search by product, brand, batch, or rack..."
|
placeholder="Search by product, brand, batch, or rack..."
|
||||||
className="bg-transparent border-none outline-none text-sm text-slate-900 w-full placeholder:text-slate-400"
|
addLink="/stock/add"
|
||||||
|
addLabel="Add Stock"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<Link to="/stock/add" className={buttonVariants({ variant: "primary" })}>
|
|
||||||
<Plus className="w-[15px] h-[15px]" />
|
|
||||||
Add Stock
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<GridTable
|
<GridTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useState, useMemo, useCallback } from "react";
|
import { useState, useMemo, useCallback } from "react";
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
import { Search, Plus, Pencil, Trash2, Warehouse } from "lucide-react";
|
import { Pencil, Trash2, Warehouse } from "lucide-react";
|
||||||
import { GridTable } from "#/components/GridTable";
|
import { GridTable } from "#/components/GridTable";
|
||||||
import type { GridTableColumn } from "#/components/GridTable";
|
import type { GridTableColumn } from "#/components/GridTable";
|
||||||
import { Button, buttonVariants } from "#/components/ui";
|
import { Button, SearchToolbar } from "#/components/ui";
|
||||||
import { useListStorage, useRemoveStorage, trpc } from "shared-react";
|
import { useListStorage, useRemoveStorage, trpc } from "shared-react";
|
||||||
|
|
||||||
interface Rack {
|
interface Rack {
|
||||||
|
|
@ -141,25 +141,13 @@ function StorageIndexPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-5 flex-wrap">
|
<SearchToolbar
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-[200px] max-w-[400px] px-3.5 py-2 bg-white rounded-md border border-slate-200 transition-all duration-200 focus-within:border-blue-600 focus-within:shadow-[0_0_0_3px_rgba(37,99,235,0.1)]">
|
|
||||||
<Search className="w-4 h-4 text-slate-600 shrink-0" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={setSearchQuery}
|
||||||
placeholder="Search by name, alias, or description..."
|
placeholder="Search by name, alias, or description..."
|
||||||
className="bg-transparent border-none outline-none text-sm text-slate-900 w-full placeholder:text-slate-400"
|
addLink="/storage/add"
|
||||||
|
addLabel="Add Rack"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
to="/storage/add"
|
|
||||||
className={buttonVariants({ variant: "primary" })}
|
|
||||||
>
|
|
||||||
<Plus className="w-[15px] h-[15px]" />
|
|
||||||
Add Rack
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<GridTable
|
<GridTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue