added Model Box and Item
added Box function for admin
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
@@ -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)
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
45
src/components/boxes/box-columns.tsx
Normal file
45
src/components/boxes/box-columns.tsx
Normal 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>
|
||||
),
|
||||
},
|
||||
];
|
||||
62
src/components/boxes/create-box-dialog.tsx
Normal file
62
src/components/boxes/create-box-dialog.tsx
Normal 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;
|
||||
177
src/components/boxes/view-detail-dialog.tsx
Normal file
177
src/components/boxes/view-detail-dialog.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
137
src/components/form/box/create-new-box-form.tsx
Normal file
137
src/components/form/box/create-new-box-form.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 có 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
123
src/components/ui/tag-input.tsx
Normal file
123
src/components/ui/tag-input.tsx
Normal 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 };
|
||||
37
src/components/ui/true-false.tsx
Normal file
37
src/components/ui/true-false.tsx
Normal 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
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
9
src/routes/(app)/(auth)/kanri/items.tsx
Normal file
9
src/routes/(app)/(auth)/kanri/items.tsx
Normal 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>
|
||||
}
|
||||
@@ -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
118
src/service/box.api.ts
Normal 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
26
src/service/box.schema.ts
Normal 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()),
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 }),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -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
30
src/types/db.d.ts
vendored
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user