added Model Box and Item

added Box function for admin
This commit is contained in:
2026-03-09 10:04:38 +07:00
parent 4f2f5b5694
commit c2981ed7d8
35 changed files with 1284 additions and 241 deletions

View File

@@ -22,6 +22,8 @@
"common_time_ago_week": "{value} tuần trước",
"common_time_ago_month": "{value} tháng trước",
"common_time_ago_year": "{value} năm trước",
"common_search_placeholder": "Search...",
"common_search_placeholder_for_box": "Search with name and tags...",
"role_tags": [
{
"match": {
@@ -81,7 +83,8 @@
"nav_houses": "Houses",
"nav_account": "Account",
"nav_profile": "Profile",
"nav_boxes": "Hộp chứa",
"nav_boxes": "Boxes",
"nav_items": "Items",
"login_page_form_email": "Email",
"login_page_form_password": "Password",
"login_page_ui_welcome_back": "Welcome back",
@@ -223,7 +226,22 @@
],
"notification_page_message_invitation_success": "You have been invited to join house!",
"notification_page_message_invitation_rejected": "You have been rejected to join house!",
"boxes_pages_ui_title": "Boxes",
"boxes_page_ui_title": "Boxes",
"boxes_page_form_name": "Box name",
"boxes_page_form_description": "Box description",
"boxes_page_form_color": "Màu sắc",
"boxes_page_form_house": "House",
"boxes_page_form_tag": "Tags",
"boxes_page_form_house_select_placeholder": "Please select house for box",
"boxes_page_message_box_not_found": "Box not found!",
"boxes_page_form_tag_placeholder": "Add a tag",
"boxes_page_ui_table_header_name": "Box name",
"boxes_page_ui_table_header_item_count": "Item count",
"boxes_page_ui_table_header_create_at": "Create date",
"boxes_page_ui_table_header_private": "Private?",
"boxes_page_ui_table_header_tags": "Tags",
"boxes_page_ui_table_header_user": "Creater",
"boxes_page_ui_table_header_house": "House",
"backend_message": [
{
"match": {

View File

@@ -22,6 +22,8 @@
"common_time_ago_week": "{value} tuần trước",
"common_time_ago_month": "{value} tháng trước",
"common_time_ago_year": "{value} năm trước",
"common_search_placeholder": "Tìm kiếm...",
"common_search_placeholder_for_box": "Tìm kiếm với tên and nhãn dán...",
"role_tags": [
{
"match": {
@@ -85,6 +87,7 @@
"nav_account": "Tài khoản",
"nav_profile": "Hồ sơ",
"nav_boxes": "Hộp chứa",
"nav_items": "Vật phẩm",
"login_page_form_email": "Email",
"login_page_form_password": "Mật khẩu",
"login_page_ui_welcome_back": "Chào mừng trở lại",
@@ -227,7 +230,22 @@
],
"notification_page_message_invitation_success": "Bạn đã đồng ý tham gia nhà!",
"notification_page_message_invitation_rejected": "Bạn đã từ chối tham gia nhà!",
"boxes_pages_ui_title": "Hộp chứa",
"boxes_page_ui_title": "Hộp chứa",
"boxes_page_form_name": "Tên hộp chứa",
"boxes_page_form_description": "Mô tả hộp chứa",
"boxes_page_form_color": "Màu sắc",
"boxes_page_form_house": "Nhà",
"boxes_page_form_tag": "Nhãn",
"boxes_page_form_house_select_placeholder": "Chọn nhà tạo hộp chứa",
"boxes_page_message_box_not_found": "Không tìm thấy hộp chứa!",
"boxes_page_form_tag_placeholder": "Thêm nhãn cho hộp",
"boxes_page_ui_table_header_name": "Tên hộp chứa",
"boxes_page_ui_table_header_item_count": "Số lượng vật phẩm",
"boxes_page_ui_table_header_create_at": "Ngày tạo",
"boxes_page_ui_table_header_private": "Hộp cá nhân",
"boxes_page_ui_table_header_tags": "Nhãn dán",
"boxes_page_ui_table_header_user": "Người tạo",
"boxes_page_ui_table_header_house": "Thuộc nhà",
"backend_message": [
{
"match": {

View File

@@ -1,16 +1,15 @@
-- CreateTable
CREATE TABLE "box" (
"id" TEXT NOT NULL,
"houseId" TEXT,
"icon" TEXT NOT NULL,
"color" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
"color" TEXT DEFAULT '#000000',
"houseId" TEXT,
"createrId" TEXT NOT NULL,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ NOT NULL,
"deletedAt" TIMESTAMP(3),
"deletedAt" TIMESTAMPTZ,
"isPrivate" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "box_pkey" PRIMARY KEY ("id")
@@ -30,7 +29,7 @@ CREATE TABLE "item" (
"expiresAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ NOT NULL,
"deletedAt" TIMESTAMP(3),
"deletedAt" TIMESTAMPTZ,
CONSTRAINT "item_pkey" PRIMARY KEY ("id")
);

View File

@@ -184,17 +184,16 @@ model Notification {
model Box {
id String @id @default(uuid())
houseId String?
icon String
color String
name String
description String?
tags String[] @default([])
color String? @default("#000000")
houseId String?
createrId String
createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt @db.Timestamptz
deletedAt DateTime?
deletedAt DateTime? @db.Timestamptz
isPrivate Boolean @default(false)
items Item[]
@@ -221,7 +220,7 @@ model Item {
expiresAt DateTime @default(now()) @db.Timestamptz
createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt @db.Timestamptz
deletedAt DateTime?
deletedAt DateTime? @db.Timestamptz
user User @relation(fields: [createrId], references: [id], onDelete: Cascade)
box Box? @relation(fields: [boxId], references: [id], onDelete: SetNull)

View File

@@ -114,7 +114,7 @@ const DataTable = <TData, TValue>({
</TableBody>
</Table>
</div>
<div className="grid grid-cols-2 gap-4 lg:hidden">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:hidden">
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<Card key={row.id}>

View File

@@ -1,28 +1,33 @@
import { cn } from '@lib/utils';
import { Avatar, AvatarFallback, AvatarImage } from '@ui/avatar';
import { useAuth } from '../auth/auth-provider';
import RoleRing from './role-ring';
export type AvatarUserType = {
name: string;
image?: string;
role: string;
};
interface AvatarUserProps {
user: AvatarUserType;
className?: string;
textSize?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
}
const AvatarUser = ({ className, textSize = 'md' }: AvatarUserProps) => {
const { session } = useAuth();
const imagePath = session?.user?.image
? new URL(`../../../data/avatar/${session?.user?.image}`, import.meta.url)
.href
const AvatarUser = ({ user, className, textSize = 'md' }: AvatarUserProps) => {
const { image, name, role } = user;
const imagePath = image
? new URL(`../../../data/avatar/${image}`, import.meta.url).href
: undefined;
const shortName = session?.user?.name
const shortName = name
?.split(' ')
.slice(0, 2)
.map((name) => name[0])
.join('');
return (
<RoleRing type={session?.user?.role}>
<RoleRing type={role}>
<Avatar className={className}>
<AvatarImage src={imagePath} title={shortName} />
<AvatarFallback

View File

@@ -0,0 +1,45 @@
import { m } from '@/paraglide/messages';
import { formatters } from '@/utils/formatters';
import { ColumnDef } from '@tanstack/react-table';
import ViewDetailBoxAction from './view-detail-dialog';
export const boxColumns: ColumnDef<BoxWithCount>[] = [
{
accessorKey: 'name',
header: m.boxes_page_ui_table_header_name(),
meta: {
thClass: 'w-1/6',
mLabel: m.boxes_page_ui_table_header_name(),
},
},
{
accessorFn: (row) => row._count.items,
header: m.boxes_page_ui_table_header_item_count(),
meta: {
thClass: 'w-1/6',
mLabel: m.boxes_page_ui_table_header_item_count(),
},
},
{
accessorKey: 'createdAt',
header: m.boxes_page_ui_table_header_create_at(),
meta: {
thClass: 'w-2/6',
mLabel: m.boxes_page_ui_table_header_create_at(),
},
cell: ({ row }) => {
return formatters.dateTime(new Date(row.original.createdAt));
},
},
{
id: 'actions',
meta: {
thClass: 'w-1/6',
},
cell: ({ row }) => (
<div className="flex justify-end w-full">
<ViewDetailBoxAction data={row.original} />
</div>
),
},
];

View File

@@ -0,0 +1,62 @@
import { m } from '@/paraglide/messages';
import useHasPermission from '@hooks/use-has-permission';
import usePreventAutoFocus from '@hooks/use-prevent-auto-focus';
import { cn } from '@lib/utils';
import { PlusIcon } from '@phosphor-icons/react';
import { Button } from '@ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@ui/dialog';
import { Skeleton } from '@ui/skeleton';
import { useState } from 'react';
import CreateNewBoxForm from '../form/box/create-new-box-form';
type CreateNewBoxProps = {
className?: string;
};
const CreateBoxAction = ({ className }: CreateNewBoxProps) => {
const { hasPermission, isLoading } = useHasPermission('box', 'create');
const [_open, _setOpen] = useState(false);
const prevent = usePreventAutoFocus();
if (isLoading) {
return <Skeleton className={cn('h-7 w-23', className)} />;
}
if (!hasPermission) return null;
return (
<Dialog open={_open} onOpenChange={_setOpen}>
<DialogTrigger asChild>
<Button type="button" variant="default" className={cn(className)}>
<PlusIcon />
{m.nav_add_new()}
</Button>
</DialogTrigger>
<DialogContent
className="max-w-80 xl:max-w-xl"
{...prevent}
onPointerDownOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-primary">
<PlusIcon size={16} />
{m.nav_add_new()}
</DialogTitle>
<DialogDescription className="sr-only">
{m.nav_add_new()}
</DialogDescription>
</DialogHeader>
<CreateNewBoxForm />
</DialogContent>
</Dialog>
);
};
export default CreateBoxAction;

View File

@@ -0,0 +1,177 @@
import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
import { m } from '@/paraglide/messages';
import { formatters } from '@/utils/formatters';
import { EyeIcon } from '@phosphor-icons/react';
import { Badge } from '@ui/badge';
import { Button } from '@ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@ui/dialog';
import { Label } from '@ui/label';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@ui/table';
import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/tooltip';
import TrueFalse from '@ui/true-false';
import AvatarUser, { AvatarUserType } from '../avatar/avatar-user';
import {
Item,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from '../ui/item';
type ActionProps = {
data: BoxWithCount;
};
const ViewDetailBoxAction = ({ data }: ActionProps) => {
const prevent = usePreventAutoFocus();
return (
<Dialog>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="rounded-full cursor-pointer text-blue-500 hover:bg-blue-100 hover:text-blue-600"
>
<EyeIcon size={16} />
<span className="sr-only">{m.ui_view_btn()}</span>
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent
side="left"
className="bg-blue-500 [&_svg]:bg-blue-500 [&_svg]:fill-blue-500 text-white"
>
<Label>{m.ui_view_btn()}</Label>
</TooltipContent>
</Tooltip>
<DialogContent
className="max-w-100 xl:max-w-2xl"
{...prevent}
onPointerDownOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-blue-600">
<EyeIcon size={20} />
{m.ui_dialog_view_title({ type: m.nav_boxes() })}
</DialogTitle>
<DialogDescription className="sr-only">
{m.ui_dialog_view_title({ type: m.nav_boxes() })}
</DialogDescription>
</DialogHeader>
<div className="overflow-hidden rounded-md border">
<Table className="bg-white">
<TableHeader>
<TableRow>
<TableHead className="w-2/5 text-white bg-primary">
{m.boxes_page_form_name()}
</TableHead>
<TableHead className="w-3/5 text-white bg-primary">
{data.name}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>{m.boxes_page_form_description()}</TableCell>
<TableCell>{data.description}</TableCell>
</TableRow>
<TableRow>
<TableCell>{m.boxes_page_form_color()}</TableCell>
<TableCell>
<div
className="bg-(--box-color) w-10 h-4 border"
style={
{
'--box-color': data.color,
} as React.CSSProperties
}
></div>
</TableCell>
</TableRow>
{data.tags.length && (
<TableRow>
<TableCell>{m.boxes_page_ui_table_header_tags()}</TableCell>
<TableCell className="flex">
{data.tags.map((tag) => (
<Badge
key={tag}
variant="outline"
className="flex items-center gap-1 bg-primary text-white"
>
{tag}
</Badge>
))}
</TableCell>
</TableRow>
)}
<TableRow>
<TableCell>{m.boxes_page_ui_table_header_private()}</TableCell>
<TableCell className="flex">
<TrueFalse value={data.isPrivate} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>{m.boxes_page_ui_table_header_user()}</TableCell>
<TableCell className="flex">
<Item className="p-0">
<ItemMedia>
<AvatarUser
className="h-8 w-8"
user={data.user as AvatarUserType}
/>
</ItemMedia>
<ItemContent>
<ItemTitle>{data.user.name}</ItemTitle>
<ItemDescription>{data.user.email}</ItemDescription>
</ItemContent>
</Item>
</TableCell>
</TableRow>
<TableRow>
<TableCell>{m.boxes_page_ui_table_header_house()}</TableCell>
<TableCell className="flex">
<div
className="text-(--house-color)"
style={
{
'--house-color': data.house?.color,
} as React.CSSProperties
}
>
{data.house?.name}
</div>
</TableCell>
</TableRow>
<TableRow>
<TableCell>{m.boxes_page_form_description()}</TableCell>
<TableCell>
{formatters.dateTime(new Date(data.createdAt))}
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</DialogContent>
</Dialog>
);
};
export default ViewDetailBoxAction;

View File

@@ -1,7 +1,7 @@
import { Skeleton } from '@/components/ui/skeleton';
import { updateProfile } from '@/service/profile.api';
import { useAuth } from '@components/auth/auth-provider';
import AvatarUser from '@components/avatar/avatar-user';
import AvatarUser, { AvatarUserType } from '@components/avatar/avatar-user';
import RoleBadge from '@components/avatar/role-badge';
import { useAppForm } from '@hooks/use-app-form';
import { m } from '@paraglide/messages';
@@ -92,7 +92,11 @@ const ProfileForm = () => {
>
<FieldGroup>
<div className="grid grid-cols-3 gap-3">
<AvatarUser className="h-20 w-20" textSize="2xl" />
<AvatarUser
className="h-20 w-20"
textSize="2xl"
user={session.user as AvatarUserType}
/>
<form.AppField name="image">
{(field) => (
<field.FileField

View File

@@ -0,0 +1,137 @@
import useDebounced from '@/hooks/use-debounced';
import { m } from '@/paraglide/messages';
import { createBox } from '@/service/box.api';
import { createBoxSchema } from '@/service/box.schema';
import { boxQueries, housesQueries } from '@/service/queries';
import { useAppForm } from '@hooks/use-app-form';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Button } from '@ui/button';
import { DialogClose, DialogFooter } from '@ui/dialog';
import { Field, FieldGroup } from '@ui/field';
import { useState } from 'react';
import { toast } from 'sonner';
type CreateBoxProp = {
onSubmit: (open: boolean) => void;
};
const defaultValues: {
name: string;
description: string;
color: string;
houseId: string;
tags: string[];
} = {
name: '',
description: '',
color: '#000000',
houseId: '',
tags: [],
};
const CreateNewBoxForm = ({ onSubmit }: CreateBoxProp) => {
const [houseKeyword, setHouseKeyword] = useState('');
const debouncedHouseKeyword = useDebounced(houseKeyword, 300);
const { data: house } = useQuery(
housesQueries.select({ keyword: debouncedHouseKeyword }),
);
const queryClient = useQueryClient();
const { mutate: createBoxMutation } = useMutation({
mutationFn: createBox,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [...boxQueries.all, 'list'],
});
onSubmit(false);
toast.success(m.houses_page_message_invite_member_success(), {
richColors: true,
});
},
onError: (error: ReturnError) => {
console.error(error);
const code = error.code as Parameters<
typeof m.backend_message
>[0]['code'];
toast.error(m.backend_message({ code }), {
richColors: true,
});
},
});
const form = useAppForm({
defaultValues,
validators: {
onSubmit: createBoxSchema,
onChange: createBoxSchema,
},
onSubmit: ({ value }) => {
createBoxMutation({ data: value });
},
});
return (
<form
id="admin-create-box-form"
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<FieldGroup>
<form.AppField name="name">
{(field) => <field.TextField label={m.boxes_page_form_name()} />}
</form.AppField>
<form.AppField name="description">
{(field) => (
<field.TextArea label={m.boxes_page_form_description()} />
)}
</form.AppField>
<form.AppField name="color">
{(field) => (
<field.TextField type="color" label={m.boxes_page_form_color()} />
)}
</form.AppField>
<form.AppField name="houseId">
{(field) => (
<field.SelectHouse
label={m.boxes_page_form_house()}
values={house || []}
placeholder={m.boxes_page_form_house_select_placeholder()}
searchPlaceholder={m.boxes_page_form_house_select_placeholder()}
keyword={houseKeyword}
onKeywordChange={setHouseKeyword}
/>
)}
</form.AppField>
<form.AppField name="tags">
{(field) => (
<field.TagInput
label={m.boxes_page_form_tag()}
placeholder={m.boxes_page_form_tag_placeholder()}
/>
)}
</form.AppField>
<Field>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" type="button">
{m.ui_cancel_btn()}
</Button>
</DialogClose>
<form.AppForm>
<form.SubscribeButton
label={m.ui_confirm_btn()}
// disabled={isPending}
/>
</form.AppForm>
</DialogFooter>
</Field>
</FieldGroup>
</form>
);
};
export default CreateNewBoxForm;

View File

@@ -4,10 +4,14 @@ import { Button, buttonVariants } from '@ui/button';
import { Field, FieldError, FieldLabel } from '@ui/field';
import { Input } from '@ui/input';
import * as ShadcnSelect from '@ui/select';
import { SelectUser as SelectUserUI } from '@ui/select-user';
import {
SelectHouse as SelectHouseUI,
SelectUser as SelectUserUI,
} from '@ui/select-user';
import { Textarea } from '@ui/textarea';
import { type VariantProps } from 'class-variance-authority';
import { Spinner } from '../ui/spinner';
import { TagInput as TagInputUI } from '../ui/tag-input';
export function SubscribeButton({
label,
@@ -232,7 +236,13 @@ export function SelectUser({
selectKey = 'id',
}: {
label: string;
values: Array<{ id: string; name: string; email: string }>;
values: Array<{
id: string;
name: string;
email: string;
image: string | null;
role: string | null;
}>;
placeholder?: string;
/** Khi truyền cùng onKeywordChange: tìm kiếm theo API (keyword gửi lên server) */
keyword?: string;
@@ -264,3 +274,75 @@ export function SelectUser({
</Field>
);
}
export function SelectHouse({
label,
values,
placeholder,
keyword,
onKeywordChange,
searchPlaceholder = 'Tìm theo tên...',
}: {
label: string;
values: Array<{
id: string;
name: string;
}>;
placeholder?: string;
keyword?: string;
onKeywordChange?: (value: string) => void;
searchPlaceholder?: string;
}) {
const field = useFieldContext<string>();
const errors = useStore(field.store, (state) => state.meta.errors);
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
return (
<Field data-invalid={isInvalid} className="col-span-2">
<FieldLabel htmlFor={field.name}>{label}:</FieldLabel>
<SelectHouseUI
name={field.name}
id={field.name}
value={field.state.value}
onValueChange={(id) => field.handleChange(id)}
values={values}
placeholder={placeholder}
keyword={keyword}
onKeywordChange={onKeywordChange}
searchPlaceholder={searchPlaceholder}
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={errors} />}
</Field>
);
}
export function TagInput({
label,
placeholder,
max = 10,
}: {
label: string;
placeholder?: string;
max?: number;
}) {
const field = useFieldContext<string[]>();
const errors = useStore(field.store, (state) => state.meta.errors);
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>{label}:</FieldLabel>
<TagInputUI
id={field.name}
name={field.name}
value={field.state.value}
onChange={(value) => field.handleChange(value)}
maxTags={max}
placeholder={placeholder}
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={errors} />}
</Field>
);
}

View File

@@ -15,15 +15,12 @@ export const houseColumns: ColumnDef<HouseWithMembers>[] = [
},
},
{
accessorKey: 'members',
accessorFn: (row) => row.members.length ?? '',
header: m.houses_page_ui_table_header_members(),
meta: {
thClass: 'w-1/6',
mLabel: m.houses_page_ui_table_header_members(),
},
cell: ({ row }) => {
return row.original.members.length;
},
},
{
accessorKey: 'createdAt',

View File

@@ -1,5 +1,6 @@
import { m } from '@paraglide/messages';
import {
CarrotIcon,
CircuitryIcon,
GaugeIcon,
GearIcon,
@@ -76,6 +77,11 @@ const NAV_MAIN = [
path: '/kanri/boxes',
icon: PackageIcon,
},
{
title: m.nav_items(),
path: '/kanri/items',
icon: CarrotIcon,
},
{
title: m.nav_logs(),
path: '/kanri/logs',
@@ -118,7 +124,7 @@ const NavMain = () => {
<SidebarMenuButtonLink
type="button"
to={item.path}
className="cursor-pointer"
className="cursor-pointer hover:bg-primary/80 hover:text-white"
tooltip={`${nav.title} - ${item.title}`}
activeProps={{
className: 'bg-primary text-white',

View File

@@ -26,7 +26,7 @@ import {
} from '@ui/sidebar';
import { toast } from 'sonner';
import { useAuth } from '../auth/auth-provider';
import AvatarUser from '../avatar/avatar-user';
import AvatarUser, { AvatarUserType } from '../avatar/avatar-user';
import RoleBadge from '../avatar/role-badge';
const SidebarMenuButtonLink = createLink(SidebarMenuButton);
@@ -83,7 +83,10 @@ const NavUser = () => {
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground cursor-pointer"
tooltip={session.user.name}
>
<AvatarUser className="h-8 w-8" />
<AvatarUser
className="h-8 w-8"
user={session.user as AvatarUserType}
/>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">
{session.user.name}
@@ -102,7 +105,10 @@ const NavUser = () => {
{/* Dropdown menu content */}
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<AvatarUser className="h-8 w-8" />
<AvatarUser
className="h-8 w-8"
user={session.user as AvatarUserType}
/>
<div className="grid flex-1 text-left text-sm leading-tight">
<div className="flex gap-2 items-center">
<span className="truncate font-medium">

View File

@@ -1,3 +1,4 @@
import { m } from '@/paraglide/messages';
import { MagnifyingGlassIcon, XIcon } from '@phosphor-icons/react';
import { Button } from './button';
import { InputGroup, InputGroupAddon, InputGroupInput } from './input-group';
@@ -6,9 +7,15 @@ type SearchInputProps = {
keywords: string;
setKeyword: (value: string) => void;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
placeholder?: string;
};
const SearchInput = ({ keywords, setKeyword, onChange }: SearchInputProps) => {
const SearchInput = ({
keywords,
setKeyword,
onChange,
placeholder,
}: SearchInputProps) => {
const onClearSearch = () => {
setKeyword('');
};
@@ -17,7 +24,7 @@ const SearchInput = ({ keywords, setKeyword, onChange }: SearchInputProps) => {
<InputGroup className="w-70 bg-white">
<InputGroupInput
id="keywords"
placeholder="Search...."
placeholder={placeholder || m.common_search_placeholder()}
value={keywords}
onChange={onChange}
/>

View File

@@ -2,23 +2,27 @@
import { cn } from '@lib/utils';
import { CaretDownIcon, MagnifyingGlassIcon } from '@phosphor-icons/react';
import type { ReactNode } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import AvatarUser, { AvatarUserType } from '../avatar/avatar-user';
import { Button } from './button';
import {
Item,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from './item';
type SelectUserItem = {
id: string;
name: string;
email: string;
};
const userLabel = (u: { name: string; email: string }) =>
`${u.name} - ${u.email}`;
type SelectUserProps = {
export type SelectGenericProps<T> = {
value: string;
onValueChange: (userId: string) => void;
values: SelectUserItem[];
onValueChange: (value: string) => void;
values: T[];
getValue: (item: T) => string;
getItemLabel: (item: T) => string;
renderOption?: (item: T) => ReactNode;
filterOption?: (item: T, query: string) => boolean;
placeholder?: string;
/** Khi truyền cùng onKeywordChange: tìm kiếm theo API (keyword gửi lên server) */
keyword?: string;
onKeywordChange?: (value: string) => void;
searchPlaceholder?: string;
@@ -27,24 +31,33 @@ type SelectUserProps = {
'aria-invalid'?: boolean;
disabled?: boolean;
className?: string;
selectKey?: 'id' | 'email';
};
export function SelectUser({
const defaultFilter =
<T,>(getItemLabel: (item: T) => string) =>
(item: T, query: string) => {
const q = query.trim().toLowerCase();
return q === '' || getItemLabel(item).toLowerCase().includes(q);
};
export function SelectGeneric<T>({
value,
onValueChange,
values,
getValue,
getItemLabel,
renderOption,
filterOption,
placeholder,
keyword,
onKeywordChange,
searchPlaceholder = 'Tìm theo tên hoặc email...',
searchPlaceholder = 'Tìm kiếm...',
name,
id,
'aria-invalid': ariaInvalid,
disabled = false,
className,
selectKey = 'id',
}: SelectUserProps) {
}: SelectGenericProps<T>) {
const [open, setOpen] = useState(false);
const [localQuery, setLocalQuery] = useState('');
const wrapperRef = useRef<HTMLDivElement>(null);
@@ -54,24 +67,16 @@ export function SelectUser({
const searchValue = useServerSearch ? keyword : localQuery;
const setSearchValue = useServerSearch ? onKeywordChange! : setLocalQuery;
const selectedUser =
const selectedItem =
value != null && value !== ''
? values.find((u) => u[selectKey] === value)
? values.find((item) => getValue(item) === value)
: null;
const displayValue = selectedUser ? userLabel(selectedUser) : '';
const displayValue = selectedItem ? getItemLabel(selectedItem) : '';
const filterFn = filterOption ?? defaultFilter(getItemLabel);
const filtered = useServerSearch
? values
: (() => {
const q = localQuery.trim().toLowerCase();
return q === ''
? values
: values.filter(
(u) =>
u.name.toLowerCase().includes(q) ||
u.email.toLowerCase().includes(q),
);
})();
: values.filter((item) => filterFn(item, localQuery));
const close = useCallback(() => {
setOpen(false);
@@ -97,8 +102,8 @@ export function SelectUser({
return () => document.removeEventListener('mousedown', onMouseDown);
}, [open, close]);
const handleSelect = (userId: string) => {
onValueChange(userId);
const handleSelect = (itemValue: string) => {
onValueChange(itemValue);
close();
};
@@ -160,26 +165,31 @@ export function SelectUser({
Không kết quả
</div>
) : (
filtered.map((u) => (
<button
key={u.id}
type="button"
role="option"
aria-selected={value === u[selectKey]}
className={cn(
'hover:bg-accent hover:text-accent-foreground flex w-full cursor-pointer items-center rounded-md px-2 py-1.5 text-left text-xs/relaxed outline-none',
value === u[selectKey] &&
'bg-accent text-accent-foreground',
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleSelect(u[selectKey]);
}}
>
{userLabel(u)}
</button>
))
filtered.map((item) => {
const itemValue = getValue(item);
return (
<Button
key={itemValue}
variant="ghost"
role="option"
aria-selected={value === itemValue}
className={cn(
'flex w-full justify-start cursor-pointer h-auto px-2 py-2',
{
'bg-accent text-accent-foreground': value === itemValue,
'p-0': renderOption,
},
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleSelect(itemValue);
}}
>
{renderOption ? renderOption(item) : getItemLabel(item)}
</Button>
);
})
)}
</div>
</div>
@@ -187,3 +197,74 @@ export function SelectUser({
</div>
);
}
export type SelectUserItem = {
id: string;
name: string;
email: string;
image: string | null;
role: string | null;
};
const userLabel = (u: SelectUserItem) => `${u.name} - ${u.email}`;
type SelectUserProps = Omit<
SelectGenericProps<SelectUserItem>,
'getValue' | 'getItemLabel'
> & {
values: SelectUserItem[];
selectKey?: 'id' | 'email';
};
/** Select dành cho User, tương thích code cũ. Dùng SelectGeneric cho đối tượng khác. */
export function SelectUser({
selectKey = 'id',
searchPlaceholder = 'Tìm theo tên hoặc email...',
...rest
}: SelectUserProps) {
return (
<SelectGeneric<SelectUserItem>
{...rest}
getValue={(u) => u[selectKey]}
getItemLabel={userLabel}
renderOption={(item) => (
<Item key={item.id}>
<ItemMedia>
<AvatarUser className="h-8 w-8" user={item as AvatarUserType} />
</ItemMedia>
<ItemContent>
<ItemTitle>{item.name}</ItemTitle>
<ItemDescription>{item.email}</ItemDescription>
</ItemContent>
</Item>
)}
searchPlaceholder={searchPlaceholder}
/>
);
}
export type SelectHouseItem = {
id: string;
name: string;
};
type SelectHouseProps = Omit<
SelectGenericProps<SelectHouseItem>,
'getValue' | 'getItemLabel'
> & {
values: SelectHouseItem[];
};
export function SelectHouse({
searchPlaceholder = 'Tìm theo tên hoặc email...',
...rest
}: SelectHouseProps) {
return (
<SelectGeneric<SelectHouseItem>
{...rest}
getValue={(h) => h.id}
getItemLabel={(h: SelectHouseItem) => h.name}
searchPlaceholder={searchPlaceholder}
/>
);
}

View File

@@ -0,0 +1,123 @@
'use client';
import { XIcon } from '@phosphor-icons/react';
import * as React from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { cn } from '@lib/utils';
interface TagInputProps extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'onChange' | 'value'
> {
value?: string[];
onChange?: (tags: string[]) => void;
maxTags?: number;
placeholder?: string;
'aria-invalid'?: boolean;
disabled?: boolean;
}
const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
(
{
value: tags = [],
onChange,
maxTags,
placeholder = 'Add a tag',
disabled,
className,
'aria-invalid': ariaInvalid,
...props
},
ref,
) => {
const [inputValue, setInputValue] = React.useState('');
const inputRef = React.useRef<HTMLInputElement>(null);
const handleAddTag = () => {
if (inputValue.trim() !== '' && !tags.includes(inputValue.trim())) {
if (maxTags && tags.length >= maxTags) {
// Optionally show a toast or message that max tags limit is reached
return;
}
onChange?.([...tags, inputValue.trim()]);
setInputValue('');
}
};
const handleRemoveTag = (tagToRemove: string) => {
onChange?.(tags.filter((tag) => tag !== tagToRemove));
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddTag();
} else if (
e.key === 'Backspace' &&
inputValue === '' &&
tags.length > 0
) {
e.preventDefault();
handleRemoveTag(tags[tags.length - 1]);
}
};
return (
<div
aria-invalid={ariaInvalid}
className={cn(
'flex flex-wrap items-center gap-2 rounded-md border border-input bg-input/20 p-1 text-xs transition-colors focus-within:ring-ring/30 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30',
disabled && 'cursor-not-allowed opacity-50',
className,
)}
>
{tags.map((tag) => (
<Badge
key={tag}
variant="outline"
className="flex items-center gap-1 bg-primary text-white"
>
{tag}
{!disabled && (
<Button
type="button"
variant="ghost"
size="icon-xs"
className="size-4 rounded-full"
onClick={() => handleRemoveTag(tag)}
disabled={disabled}
>
<XIcon className="size-3" />
<span className="sr-only">Remove tag</span>
</Button>
)}
</Badge>
))}
<Input
ref={ref || inputRef}
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder={
maxTags && tags.length >= maxTags ? 'Max tags reached' : placeholder
}
className="flex-1 border-none bg-transparent shadow-none focus-visible:ring-0 px-1 h-6 min-w-20"
disabled={disabled || !!(maxTags && tags.length >= maxTags)}
{...props}
/>
</div>
);
},
);
TagInput.displayName = 'TagInput';
export { TagInput };

View File

@@ -0,0 +1,37 @@
import { cn } from '@/lib/utils';
import { ChecksIcon, XIcon } from '@phosphor-icons/react';
type ComProps = {
value: boolean;
size?: 'xs' | 'sm' | 'md' | 'lg';
};
const SIZE = {
xs: 10,
sm: 15,
md: 20,
lg: 30,
};
const TrueFalse = ({ value, size = 'sm' }: ComProps) => {
return (
<div className="flex flex-row [&>*:not(:first-child)]:border-l *:p-1 *:px-3 border rounded-lg overflow-hidden">
<div
className={cn('bg-green-100 text-gray-300', {
'bg-green-500 text-white': value,
})}
>
<ChecksIcon size={SIZE[size]} weight="bold" />
</div>
<div
className={cn('bg-red-200 text-gray-300', {
'bg-red-500 text-white': !value,
})}
>
<XIcon size={SIZE[size]} weight="bold" />
</div>
</div>
);
};
export default TrueFalse;

File diff suppressed because one or more lines are too long

View File

@@ -1488,12 +1488,11 @@ export type NotificationScalarFieldEnum = (typeof NotificationScalarFieldEnum)[k
export const BoxScalarFieldEnum = {
id: 'id',
houseId: 'houseId',
icon: 'icon',
color: 'color',
name: 'name',
description: 'description',
tags: 'tags',
color: 'color',
houseId: 'houseId',
createrId: 'createrId',
createdAt: 'createdAt',
updatedAt: 'updatedAt',

View File

@@ -227,12 +227,11 @@ export type NotificationScalarFieldEnum = (typeof NotificationScalarFieldEnum)[k
export const BoxScalarFieldEnum = {
id: 'id',
houseId: 'houseId',
icon: 'icon',
color: 'color',
name: 'name',
description: 'description',
tags: 'tags',
color: 'color',
houseId: 'houseId',
createrId: 'createrId',
createdAt: 'createdAt',
updatedAt: 'updatedAt',

View File

@@ -26,11 +26,10 @@ export type AggregateBox = {
export type BoxMinAggregateOutputType = {
id: string | null
houseId: string | null
icon: string | null
color: string | null
name: string | null
description: string | null
color: string | null
houseId: string | null
createrId: string | null
createdAt: Date | null
updatedAt: Date | null
@@ -40,11 +39,10 @@ export type BoxMinAggregateOutputType = {
export type BoxMaxAggregateOutputType = {
id: string | null
houseId: string | null
icon: string | null
color: string | null
name: string | null
description: string | null
color: string | null
houseId: string | null
createrId: string | null
createdAt: Date | null
updatedAt: Date | null
@@ -54,12 +52,11 @@ export type BoxMaxAggregateOutputType = {
export type BoxCountAggregateOutputType = {
id: number
houseId: number
icon: number
color: number
name: number
description: number
tags: number
color: number
houseId: number
createrId: number
createdAt: number
updatedAt: number
@@ -71,11 +68,10 @@ export type BoxCountAggregateOutputType = {
export type BoxMinAggregateInputType = {
id?: true
houseId?: true
icon?: true
color?: true
name?: true
description?: true
color?: true
houseId?: true
createrId?: true
createdAt?: true
updatedAt?: true
@@ -85,11 +81,10 @@ export type BoxMinAggregateInputType = {
export type BoxMaxAggregateInputType = {
id?: true
houseId?: true
icon?: true
color?: true
name?: true
description?: true
color?: true
houseId?: true
createrId?: true
createdAt?: true
updatedAt?: true
@@ -99,12 +94,11 @@ export type BoxMaxAggregateInputType = {
export type BoxCountAggregateInputType = {
id?: true
houseId?: true
icon?: true
color?: true
name?: true
description?: true
tags?: true
color?: true
houseId?: true
createrId?: true
createdAt?: true
updatedAt?: true
@@ -187,12 +181,11 @@ export type BoxGroupByArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs
export type BoxGroupByOutputType = {
id: string
houseId: string | null
icon: string
color: string
name: string
description: string | null
tags: string[]
color: string | null
houseId: string | null
createrId: string
createdAt: Date
updatedAt: Date
@@ -223,12 +216,11 @@ export type BoxWhereInput = {
OR?: Prisma.BoxWhereInput[]
NOT?: Prisma.BoxWhereInput | Prisma.BoxWhereInput[]
id?: Prisma.StringFilter<"Box"> | string
houseId?: Prisma.StringNullableFilter<"Box"> | string | null
icon?: Prisma.StringFilter<"Box"> | string
color?: Prisma.StringFilter<"Box"> | string
name?: Prisma.StringFilter<"Box"> | string
description?: Prisma.StringNullableFilter<"Box"> | string | null
tags?: Prisma.StringNullableListFilter<"Box">
color?: Prisma.StringNullableFilter<"Box"> | string | null
houseId?: Prisma.StringNullableFilter<"Box"> | string | null
createrId?: Prisma.StringFilter<"Box"> | string
createdAt?: Prisma.DateTimeFilter<"Box"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Box"> | Date | string
@@ -241,12 +233,11 @@ export type BoxWhereInput = {
export type BoxOrderByWithRelationInput = {
id?: Prisma.SortOrder
houseId?: Prisma.SortOrderInput | Prisma.SortOrder
icon?: Prisma.SortOrder
color?: Prisma.SortOrder
name?: Prisma.SortOrder
description?: Prisma.SortOrderInput | Prisma.SortOrder
tags?: Prisma.SortOrder
color?: Prisma.SortOrderInput | Prisma.SortOrder
houseId?: Prisma.SortOrderInput | Prisma.SortOrder
createrId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -259,15 +250,14 @@ export type BoxOrderByWithRelationInput = {
export type BoxWhereUniqueInput = Prisma.AtLeast<{
id?: string
name?: string
AND?: Prisma.BoxWhereInput | Prisma.BoxWhereInput[]
OR?: Prisma.BoxWhereInput[]
NOT?: Prisma.BoxWhereInput | Prisma.BoxWhereInput[]
houseId?: Prisma.StringNullableFilter<"Box"> | string | null
icon?: Prisma.StringFilter<"Box"> | string
color?: Prisma.StringFilter<"Box"> | string
name?: Prisma.StringFilter<"Box"> | string
description?: Prisma.StringNullableFilter<"Box"> | string | null
tags?: Prisma.StringNullableListFilter<"Box">
color?: Prisma.StringNullableFilter<"Box"> | string | null
houseId?: Prisma.StringNullableFilter<"Box"> | string | null
createrId?: Prisma.StringFilter<"Box"> | string
createdAt?: Prisma.DateTimeFilter<"Box"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Box"> | Date | string
@@ -276,16 +266,15 @@ export type BoxWhereUniqueInput = Prisma.AtLeast<{
items?: Prisma.ItemListRelationFilter
house?: Prisma.XOR<Prisma.HouseNullableScalarRelationFilter, Prisma.HouseWhereInput> | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
}, "id" | "name">
}, "id">
export type BoxOrderByWithAggregationInput = {
id?: Prisma.SortOrder
houseId?: Prisma.SortOrderInput | Prisma.SortOrder
icon?: Prisma.SortOrder
color?: Prisma.SortOrder
name?: Prisma.SortOrder
description?: Prisma.SortOrderInput | Prisma.SortOrder
tags?: Prisma.SortOrder
color?: Prisma.SortOrderInput | Prisma.SortOrder
houseId?: Prisma.SortOrderInput | Prisma.SortOrder
createrId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -301,12 +290,11 @@ export type BoxScalarWhereWithAggregatesInput = {
OR?: Prisma.BoxScalarWhereWithAggregatesInput[]
NOT?: Prisma.BoxScalarWhereWithAggregatesInput | Prisma.BoxScalarWhereWithAggregatesInput[]
id?: Prisma.StringWithAggregatesFilter<"Box"> | string
houseId?: Prisma.StringNullableWithAggregatesFilter<"Box"> | string | null
icon?: Prisma.StringWithAggregatesFilter<"Box"> | string
color?: Prisma.StringWithAggregatesFilter<"Box"> | string
name?: Prisma.StringWithAggregatesFilter<"Box"> | string
description?: Prisma.StringNullableWithAggregatesFilter<"Box"> | string | null
tags?: Prisma.StringNullableListFilter<"Box">
color?: Prisma.StringNullableWithAggregatesFilter<"Box"> | string | null
houseId?: Prisma.StringNullableWithAggregatesFilter<"Box"> | string | null
createrId?: Prisma.StringWithAggregatesFilter<"Box"> | string
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Box"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Box"> | Date | string
@@ -316,11 +304,10 @@ export type BoxScalarWhereWithAggregatesInput = {
export type BoxCreateInput = {
id?: string
icon: string
color: string
name: string
description?: string | null
tags?: Prisma.BoxCreatetagsInput | string[]
color?: string | null
createdAt?: Date | string
updatedAt?: Date | string
deletedAt?: Date | string | null
@@ -332,12 +319,11 @@ export type BoxCreateInput = {
export type BoxUncheckedCreateInput = {
id?: string
houseId?: string | null
icon: string
color: string
name: string
description?: string | null
tags?: Prisma.BoxCreatetagsInput | string[]
color?: string | null
houseId?: string | null
createrId: string
createdAt?: Date | string
updatedAt?: Date | string
@@ -348,11 +334,10 @@ export type BoxUncheckedCreateInput = {
export type BoxUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
icon?: Prisma.StringFieldUpdateOperationsInput | string
color?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
tags?: Prisma.BoxUpdatetagsInput | string[]
color?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
@@ -364,12 +349,11 @@ export type BoxUpdateInput = {
export type BoxUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
houseId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
icon?: Prisma.StringFieldUpdateOperationsInput | string
color?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
tags?: Prisma.BoxUpdatetagsInput | string[]
color?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
houseId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createrId?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -380,12 +364,11 @@ export type BoxUncheckedUpdateInput = {
export type BoxCreateManyInput = {
id?: string
houseId?: string | null
icon: string
color: string
name: string
description?: string | null
tags?: Prisma.BoxCreatetagsInput | string[]
color?: string | null
houseId?: string | null
createrId: string
createdAt?: Date | string
updatedAt?: Date | string
@@ -395,11 +378,10 @@ export type BoxCreateManyInput = {
export type BoxUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
icon?: Prisma.StringFieldUpdateOperationsInput | string
color?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
tags?: Prisma.BoxUpdatetagsInput | string[]
color?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
@@ -408,12 +390,11 @@ export type BoxUpdateManyMutationInput = {
export type BoxUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
houseId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
icon?: Prisma.StringFieldUpdateOperationsInput | string
color?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
tags?: Prisma.BoxUpdatetagsInput | string[]
color?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
houseId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createrId?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -441,12 +422,11 @@ export type StringNullableListFilter<$PrismaModel = never> = {
export type BoxCountOrderByAggregateInput = {
id?: Prisma.SortOrder
houseId?: Prisma.SortOrder
icon?: Prisma.SortOrder
color?: Prisma.SortOrder
name?: Prisma.SortOrder
description?: Prisma.SortOrder
tags?: Prisma.SortOrder
color?: Prisma.SortOrder
houseId?: Prisma.SortOrder
createrId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -456,11 +436,10 @@ export type BoxCountOrderByAggregateInput = {
export type BoxMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
houseId?: Prisma.SortOrder
icon?: Prisma.SortOrder
color?: Prisma.SortOrder
name?: Prisma.SortOrder
description?: Prisma.SortOrder
color?: Prisma.SortOrder
houseId?: Prisma.SortOrder
createrId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -470,11 +449,10 @@ export type BoxMaxOrderByAggregateInput = {
export type BoxMinOrderByAggregateInput = {
id?: Prisma.SortOrder
houseId?: Prisma.SortOrder
icon?: Prisma.SortOrder
color?: Prisma.SortOrder
name?: Prisma.SortOrder
description?: Prisma.SortOrder
color?: Prisma.SortOrder
houseId?: Prisma.SortOrder
createrId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -598,11 +576,10 @@ export type BoxUpdateOneWithoutItemsNestedInput = {
export type BoxCreateWithoutUserInput = {
id?: string
icon: string
color: string
name: string
description?: string | null
tags?: Prisma.BoxCreatetagsInput | string[]
color?: string | null
createdAt?: Date | string
updatedAt?: Date | string
deletedAt?: Date | string | null
@@ -613,12 +590,11 @@ export type BoxCreateWithoutUserInput = {
export type BoxUncheckedCreateWithoutUserInput = {
id?: string
houseId?: string | null
icon: string
color: string
name: string
description?: string | null
tags?: Prisma.BoxCreatetagsInput | string[]
color?: string | null
houseId?: string | null
createdAt?: Date | string
updatedAt?: Date | string
deletedAt?: Date | string | null
@@ -657,12 +633,11 @@ export type BoxScalarWhereInput = {
OR?: Prisma.BoxScalarWhereInput[]
NOT?: Prisma.BoxScalarWhereInput | Prisma.BoxScalarWhereInput[]
id?: Prisma.StringFilter<"Box"> | string
houseId?: Prisma.StringNullableFilter<"Box"> | string | null
icon?: Prisma.StringFilter<"Box"> | string
color?: Prisma.StringFilter<"Box"> | string
name?: Prisma.StringFilter<"Box"> | string
description?: Prisma.StringNullableFilter<"Box"> | string | null
tags?: Prisma.StringNullableListFilter<"Box">
color?: Prisma.StringNullableFilter<"Box"> | string | null
houseId?: Prisma.StringNullableFilter<"Box"> | string | null
createrId?: Prisma.StringFilter<"Box"> | string
createdAt?: Prisma.DateTimeFilter<"Box"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Box"> | Date | string
@@ -672,11 +647,10 @@ export type BoxScalarWhereInput = {
export type BoxCreateWithoutHouseInput = {
id?: string
icon: string
color: string
name: string
description?: string | null
tags?: Prisma.BoxCreatetagsInput | string[]
color?: string | null
createdAt?: Date | string
updatedAt?: Date | string
deletedAt?: Date | string | null
@@ -687,11 +661,10 @@ export type BoxCreateWithoutHouseInput = {
export type BoxUncheckedCreateWithoutHouseInput = {
id?: string
icon: string
color: string
name: string
description?: string | null
tags?: Prisma.BoxCreatetagsInput | string[]
color?: string | null
createrId: string
createdAt?: Date | string
updatedAt?: Date | string
@@ -728,11 +701,10 @@ export type BoxUpdateManyWithWhereWithoutHouseInput = {
export type BoxCreateWithoutItemsInput = {
id?: string
icon: string
color: string
name: string
description?: string | null
tags?: Prisma.BoxCreatetagsInput | string[]
color?: string | null
createdAt?: Date | string
updatedAt?: Date | string
deletedAt?: Date | string | null
@@ -743,12 +715,11 @@ export type BoxCreateWithoutItemsInput = {
export type BoxUncheckedCreateWithoutItemsInput = {
id?: string
houseId?: string | null
icon: string
color: string
name: string
description?: string | null
tags?: Prisma.BoxCreatetagsInput | string[]
color?: string | null
houseId?: string | null
createrId: string
createdAt?: Date | string
updatedAt?: Date | string
@@ -774,11 +745,10 @@ export type BoxUpdateToOneWithWhereWithoutItemsInput = {
export type BoxUpdateWithoutItemsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
icon?: Prisma.StringFieldUpdateOperationsInput | string
color?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
tags?: Prisma.BoxUpdatetagsInput | string[]
color?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
@@ -789,12 +759,11 @@ export type BoxUpdateWithoutItemsInput = {
export type BoxUncheckedUpdateWithoutItemsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
houseId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
icon?: Prisma.StringFieldUpdateOperationsInput | string
color?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
tags?: Prisma.BoxUpdatetagsInput | string[]
color?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
houseId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createrId?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -804,12 +773,11 @@ export type BoxUncheckedUpdateWithoutItemsInput = {
export type BoxCreateManyUserInput = {
id?: string
houseId?: string | null
icon: string
color: string
name: string
description?: string | null
tags?: Prisma.BoxCreatetagsInput | string[]
color?: string | null
houseId?: string | null
createdAt?: Date | string
updatedAt?: Date | string
deletedAt?: Date | string | null
@@ -818,11 +786,10 @@ export type BoxCreateManyUserInput = {
export type BoxUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
icon?: Prisma.StringFieldUpdateOperationsInput | string
color?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
tags?: Prisma.BoxUpdatetagsInput | string[]
color?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
@@ -833,12 +800,11 @@ export type BoxUpdateWithoutUserInput = {
export type BoxUncheckedUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
houseId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
icon?: Prisma.StringFieldUpdateOperationsInput | string
color?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
tags?: Prisma.BoxUpdatetagsInput | string[]
color?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
houseId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
@@ -848,12 +814,11 @@ export type BoxUncheckedUpdateWithoutUserInput = {
export type BoxUncheckedUpdateManyWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
houseId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
icon?: Prisma.StringFieldUpdateOperationsInput | string
color?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
tags?: Prisma.BoxUpdatetagsInput | string[]
color?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
houseId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
@@ -862,11 +827,10 @@ export type BoxUncheckedUpdateManyWithoutUserInput = {
export type BoxCreateManyHouseInput = {
id?: string
icon: string
color: string
name: string
description?: string | null
tags?: Prisma.BoxCreatetagsInput | string[]
color?: string | null
createrId: string
createdAt?: Date | string
updatedAt?: Date | string
@@ -876,11 +840,10 @@ export type BoxCreateManyHouseInput = {
export type BoxUpdateWithoutHouseInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
icon?: Prisma.StringFieldUpdateOperationsInput | string
color?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
tags?: Prisma.BoxUpdatetagsInput | string[]
color?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
@@ -891,11 +854,10 @@ export type BoxUpdateWithoutHouseInput = {
export type BoxUncheckedUpdateWithoutHouseInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
icon?: Prisma.StringFieldUpdateOperationsInput | string
color?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
tags?: Prisma.BoxUpdatetagsInput | string[]
color?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createrId?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -906,11 +868,10 @@ export type BoxUncheckedUpdateWithoutHouseInput = {
export type BoxUncheckedUpdateManyWithoutHouseInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
icon?: Prisma.StringFieldUpdateOperationsInput | string
color?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
tags?: Prisma.BoxUpdatetagsInput | string[]
color?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createrId?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -951,12 +912,11 @@ export type BoxCountOutputTypeCountItemsArgs<ExtArgs extends runtime.Types.Exten
export type BoxSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
houseId?: boolean
icon?: boolean
color?: boolean
name?: boolean
description?: boolean
tags?: boolean
color?: boolean
houseId?: boolean
createrId?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -970,12 +930,11 @@ export type BoxSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = ru
export type BoxSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
houseId?: boolean
icon?: boolean
color?: boolean
name?: boolean
description?: boolean
tags?: boolean
color?: boolean
houseId?: boolean
createrId?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -987,12 +946,11 @@ export type BoxSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extension
export type BoxSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
houseId?: boolean
icon?: boolean
color?: boolean
name?: boolean
description?: boolean
tags?: boolean
color?: boolean
houseId?: boolean
createrId?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -1004,12 +962,11 @@ export type BoxSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extension
export type BoxSelectScalar = {
id?: boolean
houseId?: boolean
icon?: boolean
color?: boolean
name?: boolean
description?: boolean
tags?: boolean
color?: boolean
houseId?: boolean
createrId?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -1017,7 +974,7 @@ export type BoxSelectScalar = {
isPrivate?: boolean
}
export type BoxOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "houseId" | "icon" | "color" | "name" | "description" | "tags" | "createrId" | "createdAt" | "updatedAt" | "deletedAt" | "isPrivate", ExtArgs["result"]["box"]>
export type BoxOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "description" | "tags" | "color" | "houseId" | "createrId" | "createdAt" | "updatedAt" | "deletedAt" | "isPrivate", ExtArgs["result"]["box"]>
export type BoxInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
items?: boolean | Prisma.Box$itemsArgs<ExtArgs>
house?: boolean | Prisma.Box$houseArgs<ExtArgs>
@@ -1042,12 +999,11 @@ export type $BoxPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
houseId: string | null
icon: string
color: string
name: string
description: string | null
tags: string[]
color: string | null
houseId: string | null
createrId: string
createdAt: Date
updatedAt: Date
@@ -1480,12 +1436,11 @@ export interface Prisma__BoxClient<T, Null = never, ExtArgs extends runtime.Type
*/
export interface BoxFieldRefs {
readonly id: Prisma.FieldRef<"Box", 'String'>
readonly houseId: Prisma.FieldRef<"Box", 'String'>
readonly icon: Prisma.FieldRef<"Box", 'String'>
readonly color: Prisma.FieldRef<"Box", 'String'>
readonly name: Prisma.FieldRef<"Box", 'String'>
readonly description: Prisma.FieldRef<"Box", 'String'>
readonly tags: Prisma.FieldRef<"Box", 'String[]'>
readonly color: Prisma.FieldRef<"Box", 'String'>
readonly houseId: Prisma.FieldRef<"Box", 'String'>
readonly createrId: Prisma.FieldRef<"Box", 'String'>
readonly createdAt: Prisma.FieldRef<"Box", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"Box", 'DateTime'>

View File

@@ -2,9 +2,11 @@ import {
FileField,
HiddenField,
Select,
SelectHouse,
SelectNumber,
SelectUser,
SubscribeButton,
TagInput,
TextArea,
TextField,
} from '@/components/form/form-components';
@@ -22,6 +24,8 @@ export const { useAppForm } = createFormHook({
SelectNumber,
FileField,
SelectUser,
SelectHouse,
TagInput,
},
formComponents: {
SubscribeButton,

View File

@@ -27,6 +27,7 @@ import { Route as appauthManagementDashboardRouteImport } from './routes/(app)/(
import { Route as appauthKanriUsersRouteImport } from './routes/(app)/(auth)/kanri/users'
import { Route as appauthKanriSettingsRouteImport } from './routes/(app)/(auth)/kanri/settings'
import { Route as appauthKanriLogsRouteImport } from './routes/(app)/(auth)/kanri/logs'
import { Route as appauthKanriItemsRouteImport } from './routes/(app)/(auth)/kanri/items'
import { Route as appauthKanriHousesRouteImport } from './routes/(app)/(auth)/kanri/houses'
import { Route as appauthKanriBoxesRouteImport } from './routes/(app)/(auth)/kanri/boxes'
import { Route as appauthAccountSettingsRouteImport } from './routes/(app)/(auth)/account/settings'
@@ -123,6 +124,11 @@ const appauthKanriLogsRoute = appauthKanriLogsRouteImport.update({
path: '/logs',
getParentRoute: () => appauthKanriRouteRoute,
} as any)
const appauthKanriItemsRoute = appauthKanriItemsRouteImport.update({
id: '/items',
path: '/items',
getParentRoute: () => appauthKanriRouteRoute,
} as any)
const appauthKanriHousesRoute = appauthKanriHousesRouteImport.update({
id: '/houses',
path: '/houses',
@@ -163,6 +169,7 @@ export interface FileRoutesByFullPath {
'/account/settings': typeof appauthAccountSettingsRoute
'/kanri/boxes': typeof appauthKanriBoxesRoute
'/kanri/houses': typeof appauthKanriHousesRoute
'/kanri/items': typeof appauthKanriItemsRoute
'/kanri/logs': typeof appauthKanriLogsRoute
'/kanri/settings': typeof appauthKanriSettingsRoute
'/kanri/users': typeof appauthKanriUsersRoute
@@ -183,6 +190,7 @@ export interface FileRoutesByTo {
'/account/settings': typeof appauthAccountSettingsRoute
'/kanri/boxes': typeof appauthKanriBoxesRoute
'/kanri/houses': typeof appauthKanriHousesRoute
'/kanri/items': typeof appauthKanriItemsRoute
'/kanri/logs': typeof appauthKanriLogsRoute
'/kanri/settings': typeof appauthKanriSettingsRoute
'/kanri/users': typeof appauthKanriUsersRoute
@@ -209,6 +217,7 @@ export interface FileRoutesById {
'/(app)/(auth)/account/settings': typeof appauthAccountSettingsRoute
'/(app)/(auth)/kanri/boxes': typeof appauthKanriBoxesRoute
'/(app)/(auth)/kanri/houses': typeof appauthKanriHousesRoute
'/(app)/(auth)/kanri/items': typeof appauthKanriItemsRoute
'/(app)/(auth)/kanri/logs': typeof appauthKanriLogsRoute
'/(app)/(auth)/kanri/settings': typeof appauthKanriSettingsRoute
'/(app)/(auth)/kanri/users': typeof appauthKanriUsersRoute
@@ -234,6 +243,7 @@ export interface FileRouteTypes {
| '/account/settings'
| '/kanri/boxes'
| '/kanri/houses'
| '/kanri/items'
| '/kanri/logs'
| '/kanri/settings'
| '/kanri/users'
@@ -254,6 +264,7 @@ export interface FileRouteTypes {
| '/account/settings'
| '/kanri/boxes'
| '/kanri/houses'
| '/kanri/items'
| '/kanri/logs'
| '/kanri/settings'
| '/kanri/users'
@@ -279,6 +290,7 @@ export interface FileRouteTypes {
| '/(app)/(auth)/account/settings'
| '/(app)/(auth)/kanri/boxes'
| '/(app)/(auth)/kanri/houses'
| '/(app)/(auth)/kanri/items'
| '/(app)/(auth)/kanri/logs'
| '/(app)/(auth)/kanri/settings'
| '/(app)/(auth)/kanri/users'
@@ -425,6 +437,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof appauthKanriLogsRouteImport
parentRoute: typeof appauthKanriRouteRoute
}
'/(app)/(auth)/kanri/items': {
id: '/(app)/(auth)/kanri/items'
path: '/items'
fullPath: '/kanri/items'
preLoaderRoute: typeof appauthKanriItemsRouteImport
parentRoute: typeof appauthKanriRouteRoute
}
'/(app)/(auth)/kanri/houses': {
id: '/(app)/(auth)/kanri/houses'
path: '/houses'
@@ -483,6 +502,7 @@ const appauthAccountRouteRouteWithChildren =
interface appauthKanriRouteRouteChildren {
appauthKanriBoxesRoute: typeof appauthKanriBoxesRoute
appauthKanriHousesRoute: typeof appauthKanriHousesRoute
appauthKanriItemsRoute: typeof appauthKanriItemsRoute
appauthKanriLogsRoute: typeof appauthKanriLogsRoute
appauthKanriSettingsRoute: typeof appauthKanriSettingsRoute
appauthKanriUsersRoute: typeof appauthKanriUsersRoute
@@ -492,6 +512,7 @@ interface appauthKanriRouteRouteChildren {
const appauthKanriRouteRouteChildren: appauthKanriRouteRouteChildren = {
appauthKanriBoxesRoute: appauthKanriBoxesRoute,
appauthKanriHousesRoute: appauthKanriHousesRoute,
appauthKanriItemsRoute: appauthKanriItemsRoute,
appauthKanriLogsRoute: appauthKanriLogsRoute,
appauthKanriSettingsRoute: appauthKanriSettingsRoute,
appauthKanriUsersRoute: appauthKanriUsersRoute,

View File

@@ -1,7 +1,13 @@
import { boxColumns } from '@/components/boxes/box-columns';
import CreateBoxAction from '@/components/boxes/create-box-dialog';
import DataTable from '@/components/DataTable';
import SearchInput from '@/components/ui/search-input';
import { Skeleton } from '@/components/ui/skeleton';
import useDebounced from '@/hooks/use-debounced';
import { m } from '@/paraglide/messages';
import { boxQueries } from '@/service/queries';
import { PackageIcon } from '@phosphor-icons/react';
import { useQuery } from '@tanstack/react-query';
import { createFileRoute } from '@tanstack/react-router';
import { Card, CardContent, CardHeader, CardTitle } from '@ui/card';
import { useState } from 'react';
@@ -17,11 +23,27 @@ function RouteComponent() {
const [searchKeyword, setSearchKeyword] = useState('');
const debouncedSearch = useDebounced(searchKeyword, 500);
const { data, isLoading } = useQuery(
boxQueries.list({
page,
limit: pageLimit,
keyword: debouncedSearch,
}),
);
const onSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchKeyword(e.target.value);
setPage(1);
};
if (isLoading) {
return (
<div className="@container/main flex flex-1 flex-col gap-2 p-4">
<Skeleton className="h-130 w-full" />
</div>
);
}
return (
<div className="@container/main flex flex-1 flex-col gap-2 p-4">
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card *:data-[slot=card]:bg-linear-to-br *:data-[slot=card]:shadow-xs">
@@ -29,7 +51,7 @@ function RouteComponent() {
<CardHeader>
<CardTitle className="text-xl flex items-center gap-2">
<PackageIcon size={24} />
{m.boxes_pages_ui_title()}
{m.boxes_page_ui_title()}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
@@ -38,8 +60,21 @@ function RouteComponent() {
keywords={searchKeyword}
setKeyword={setSearchKeyword}
onChange={onSearchChange}
placeholder={m.common_search_placeholder_for_box()}
/>
<CreateBoxAction />
</div>
{data && (
<DataTable
data={data.result || []}
columns={boxColumns}
page={page}
setPage={setPage}
limit={pageLimit}
setLimit={setPageLimit}
pagination={data.pagination}
/>
)}
</CardContent>
</Card>
</div>

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/(app)/(auth)/kanri/items')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/(app)/(auth)/kanri/items"!</div>
}

View File

@@ -1,27 +1,12 @@
import { m } from '@paraglide/messages';
import { createFileRoute } from '@tanstack/react-router';
import { useState } from 'react';
export const Route = createFileRoute('/(app)/')({
component: App,
staticData: { breadcrumb: () => m.nav_home() },
});
const testselect = [
{
value: '1',
label: 'Sam',
email: 'luu.dat.tham@gmail.com',
},
{
value: '2',
label: 'Raysam',
email: 'raysam024@gmail.com',
},
];
function App() {
const [value, setValue] = useState<string>();
return (
<div className="@container/main flex flex-1 flex-col gap-2 p-4">
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card *:data-[slot=card]:bg-linear-to-br *:data-[slot=card]:shadow-xs grid grid-cols-1 @xl/main:grid-cols-2 @5xl/main:grid-cols-3 gap-4">

118
src/service/box.api.ts Normal file
View File

@@ -0,0 +1,118 @@
import { prisma } from '@/db';
import { BoxWhereInput } from '@/generated/prisma/models';
import { parseError } from '@/lib/errors';
import { DB_TABLE, LOG_ACTION } from '@/types/enum';
import { authMiddleware } from '@lib/middleware';
import { createServerFn } from '@tanstack/react-start';
import { boxListSchema, createBoxSchema } from './box.schema';
import { createAuditLog } from './repository';
export const getAllBox = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.inputValidator(boxListSchema)
.handler(async ({ data }) => {
try {
const { page, limit, keyword } = data;
const skip = (page - 1) * limit;
const where: BoxWhereInput = {
OR: [
{
name: {
contains: keyword,
mode: 'insensitive',
},
},
],
};
const [list, total]: [any[], number] = await prisma.$transaction([
prisma.box.findMany({
where,
orderBy: { createdAt: 'desc' },
include: {
_count: {
select: {
items: true,
},
},
house: {
select: {
id: true,
name: true,
color: true,
},
},
user: {
select: {
id: true,
name: true,
email: true,
image: true,
role: true,
},
},
},
omit: {
createrId: true,
houseId: true,
},
take: limit,
skip,
}),
prisma.box.count({ where }),
]);
const totalPage = Math.ceil(+total / limit);
return {
result: list,
pagination: {
currentPage: page,
totalPage,
totalItem: total,
},
};
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});
export const createBox = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
.inputValidator(createBoxSchema)
.handler(async ({ data, context: { user } }) => {
try {
const { name, description, color, tags, houseId } = data;
const result = await prisma.box.create({
data: {
name,
description,
color,
houseId,
tags,
createrId: user.id,
},
});
if (!result) throw Error('Failed to create box');
await createAuditLog({
action: LOG_ACTION.CREATE,
tableName: DB_TABLE.BOX,
recordId: result.id,
oldValue: '',
newValue: JSON.stringify(result),
userId: user.id,
});
return result;
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});

26
src/service/box.schema.ts Normal file
View File

@@ -0,0 +1,26 @@
import { m } from '@/paraglide/messages';
import z from 'zod';
export const baseBox = z.object({
id: z.string().nonempty(m.boxes_page_message_box_not_found()),
});
export const boxListSchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(10).max(100).default(10),
keyword: z.string().optional(),
});
export const createBoxSchema = z.object({
name: z
.string()
.nonempty(m.common_is_required({ field: m.boxes_page_form_name() })),
description: z
.string()
.nonempty(m.common_is_required({ field: m.boxes_page_form_description() })),
color: z
.string()
.nonempty(m.common_is_required({ field: m.boxes_page_form_color() })),
houseId: z.string().nonempty(m.houses_page_message_house_not_found()),
tags: z.array(z.string()),
});

View File

@@ -11,6 +11,7 @@ import {
baseHouse,
houseCreateBESchema,
houseEditBESchema,
houseForSelectSchema,
houseListSchema,
invitationCreateBESchema,
removeMemberSchema,
@@ -115,6 +116,39 @@ export const getCurrentUserHouses = createServerFn({ method: 'GET' })
}
});
export const getHouseForSelect = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.inputValidator(houseForSelectSchema)
.handler(async ({ data }) => {
try {
const result = await prisma.house.findMany({
where: {
OR: [
{
name: {
contains: data.keyword,
mode: 'insensitive',
},
},
],
},
select: {
id: true,
name: true,
color: true,
},
orderBy: { createdAt: 'desc' },
take: 5,
});
return result;
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});
export const createHouse = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
.inputValidator(houseCreateBESchema)

View File

@@ -11,6 +11,10 @@ export const houseListSchema = z.object({
keyword: z.string().optional(),
});
export const houseForSelectSchema = z.object({
keyword: z.string().optional(),
});
export const houseCreateSchema = z.object({
name: z
.string()

View File

@@ -1,7 +1,12 @@
import { getSession } from '@lib/auth/session';
import { queryOptions } from '@tanstack/react-query';
import { getAllAudit } from './audit.api';
import { getAllHouse, getCurrentUserHouses } from './house.api';
import { getAllBox } from './box.api';
import {
getAllHouse,
getCurrentUserHouses,
getHouseForSelect,
} from './house.api';
import { getAllNotifications, getTopFiveNotification } from './notify.api';
import {
getAdminSettings,
@@ -75,6 +80,11 @@ export const housesQueries = {
queryKey: [...housesQueries.all, 'currentUser'],
queryFn: () => getCurrentUserHouses(),
}),
select: (params: { keyword?: string }) =>
queryOptions({
queryKey: [...housesQueries.all, 'select', params],
queryFn: () => getHouseForSelect({ data: params }),
}),
};
export const notificationQueries = {
@@ -90,3 +100,12 @@ export const notificationQueries = {
queryFn: () => getTopFiveNotification(),
}),
};
export const boxQueries = {
all: ['boxes'],
list: (params: { page: number; limit: number; keyword?: string }) =>
queryOptions({
queryKey: [...boxQueries.all, 'list', params],
queryFn: () => getAllBox({ data: params }),
}),
};

View File

@@ -89,6 +89,8 @@ export const getUserForSelect = createServerFn({ method: 'GET' })
id: true,
name: true,
email: true,
image: true,
role: true,
},
orderBy: { createdAt: 'desc' },
take: 5,

30
src/types/db.d.ts vendored
View File

@@ -30,6 +30,36 @@ declare global {
};
}>;
type BoxWithCount = Prisma.BoxGetPayload<{
include: {
_count: {
select: {
items: true;
};
};
house: {
select: {
id: true;
name: true;
color: true;
};
};
user: {
select: {
id: true;
name: true;
email: true;
image: true;
role: true;
};
};
};
omit: {
createrId: true;
houseId: true;
};
}>;
type HouseWithMembersCount = HouseWithMembers & {
_count: {
members: number;