List out house

view house detail
This commit is contained in:
2026-02-01 11:12:33 +07:00
parent ed7e5baaea
commit afa26ab50d
25 changed files with 570 additions and 103 deletions

View File

@@ -56,7 +56,7 @@ const DataTable = <TData, TValue>({
return (
<>
<div className="overflow-hidden rounded-md border">
<Table>
<Table className="bg-white">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
@@ -66,7 +66,7 @@ const DataTable = <TData, TValue>({
key={header.id}
colSpan={header.colSpan}
className={cn(
'px-4',
'px-4 bg-primary text-white text-sm',
header.column.columnDef.meta?.thClass,
)}
>

View File

@@ -5,7 +5,7 @@ import { Badge } from '../ui/badge';
import { LOG_ACTION } from '@/types/enum';
import ActionBadge from './action-badge';
import ViewDetail from './view-detail-dialog';
import ViewDetailAudit from './view-detail-dialog';
export const logColumns: ColumnDef<AuditWithUser>[] = [
{
@@ -56,7 +56,7 @@ export const logColumns: ColumnDef<AuditWithUser>[] = [
},
cell: ({ row }) => (
<div className="flex justify-end">
<ViewDetail data={row.original} />
<ViewDetailAudit data={row.original} />
</div>
),
},

View File

@@ -23,7 +23,7 @@ type ViewDetailProps = {
data: AuditWithUser;
};
const ViewDetail = ({ data }: ViewDetailProps) => {
const ViewDetailAudit = ({ data }: ViewDetailProps) => {
const prevent = usePreventAutoFocus();
const { isCopied, copyToClipboard } = useCopyToClipboard();
@@ -134,4 +134,4 @@ const ViewDetail = ({ data }: ViewDetailProps) => {
);
};
export default ViewDetail;
export default ViewDetailAudit;

View File

@@ -74,6 +74,9 @@ const ProfileForm = () => {
);
} catch (error) {
console.error('update load file', error);
toast.error(JSON.stringify(error), {
richColors: true,
});
}
},
});

View File

@@ -0,0 +1,46 @@
import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
import { m } from '@/paraglide/messages';
import { PlusIcon } from '@phosphor-icons/react';
import { useState } from 'react';
import { Button } from '../ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '../ui/dialog';
const CreateNewHouse = () => {
const [_open, _setOpen] = useState(false);
const prevent = usePreventAutoFocus();
return (
<Dialog open={_open} onOpenChange={_setOpen}>
<DialogTrigger asChild>
<Button type="button" variant="default">
<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>
</DialogContent>
</Dialog>
);
};
export default CreateNewHouse;

View File

@@ -0,0 +1,47 @@
import { m } from '@/paraglide/messages';
import { formatters } from '@/utils/formatters';
import { ColumnDef } from '@tanstack/react-table';
import ViewDetailHouse from './view-detail-dialog';
export const houseColumns: ColumnDef<OrganizationWithMembers>[] = [
{
accessorKey: 'name',
header: m.houses_page_ui_table_header_name(),
meta: {
thClass: 'w-1/6',
},
},
{
accessorKey: 'members',
header: m.houses_page_ui_table_header_members(),
meta: {
thClass: 'w-1/6',
},
cell: ({ row }) => {
return row.original.members.length;
},
},
{
accessorKey: 'createdAt',
header: m.houses_page_ui_table_header_created_at(),
meta: {
thClass: 'w-1/6',
},
cell: ({ row }) => {
return formatters.dateTime(new Date(row.original.createdAt));
},
},
{
id: 'actions',
meta: {
thClass: 'w-1/6',
},
cell: ({ row }) => {
return (
<div className="flex justify-end">
<ViewDetailHouse data={row.original} />
</div>
);
},
},
];

View File

@@ -0,0 +1,120 @@
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 { 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';
type ViewDetailProps = {
data: OrganizationWithMembers;
};
const ViewDetailHouse = ({ data }: ViewDetailProps) => {
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_houses() })}
</DialogTitle>
<DialogDescription className="sr-only">
{m.ui_dialog_view_title({ type: m.nav_houses() })}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<span className="font-bold">
{m.houses_page_ui_table_header_name()}:
</span>
<Label>{data.name}</Label>
</div>
<div className="flex items-center gap-2">
<span className="font-bold">
{m.houses_page_ui_table_header_created_at()}:
</span>
<Label>{formatters.dateTime(new Date(data.createdAt))}</Label>
</div>
<div className="flex flex-col gap-2">
<span className="font-bold">
{m.houses_page_ui_table_header_members()}:
</span>
<div className="flex items-center gap-2">
<span className="font-bold">
{m.houses_page_ui_view_label_count()}:
</span>
<Label>{data.members.length}</Label>
</div>
<div className="overflow-hidden rounded-md border">
<Table className="bg-white">
<TableHeader>
<TableRow>
<TableHead className="px-2 h-7 bg-primary text-white text-xs w-1/2">
{m.houses_page_ui_view_table_header_email()}
</TableHead>
<TableHead className="px-2 h-7 bg-primary text-white text-xs w-1/2">
{m.houses_page_ui_view_table_header_role()}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.members.map((member) => (
<TableRow>
<TableCell>{member.user.email}</TableCell>
<TableCell>{member.role}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
};
export default ViewDetailHouse;

View File

@@ -5,6 +5,7 @@ import {
GearIcon,
HouseIcon,
UsersIcon,
WarehouseIcon,
} from '@phosphor-icons/react';
import { createLink } from '@tanstack/react-router';
import AdminShow from '../auth/AdminShow';
@@ -23,7 +24,7 @@ const SidebarMenuButtonLink = createLink(SidebarMenuButton);
const NAV_MAIN = [
{
id: '1',
title: 'Basic',
title: m.nav_label_basic(),
items: [
{
title: m.nav_home(),
@@ -43,8 +44,15 @@ const NAV_MAIN = [
},
{
id: '2',
title: 'Management',
title: m.nav_label_management(),
items: [
{
title: m.nav_houses(),
path: '/kanri/houses',
icon: WarehouseIcon,
isAuth: false,
admin: true,
},
{
title: m.nav_users(),
path: '/kanri/users',

View File

@@ -14,7 +14,7 @@ const SearchInput = ({ keywords, setKeyword, onChange }: SearchInputProps) => {
};
return (
<InputGroup className="w-70">
<InputGroup className="w-70 bg-white">
<InputGroupInput
id="keywords"
placeholder="Search...."

View File

@@ -31,7 +31,7 @@ const AddNewUserButton = () => {
onPointerDownOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-red-600">
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-primary">
<PlusIcon size={16} />
{m.nav_add_new()}
</DialogTitle>

View File

@@ -61,8 +61,8 @@ export const auth = betterAuth({
after: async (user) => {
await auth.api.createOrganization({
body: {
name: `${user.name || 'User'}'s Organization`,
slug: `${user.name?.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}`,
name: `${user.name || 'User'}'s House`,
slug: `${user.name?.toLowerCase().replace(/\s+/g, '-')}-house-${Date.now()}`,
userId: user.id,
color: '#000000',
},

View File

@@ -22,6 +22,7 @@ import { Route as appauthAccountIndexRouteImport } from './routes/(app)/(auth)/a
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 appauthKanriHousesRouteImport } from './routes/(app)/(auth)/kanri/houses'
import { Route as appauthAccountSettingsRouteImport } from './routes/(app)/(auth)/account/settings'
import { Route as appauthAccountProfileRouteImport } from './routes/(app)/(auth)/account/profile'
import { Route as appauthAccountChangePasswordRouteImport } from './routes/(app)/(auth)/account/change-password'
@@ -89,6 +90,11 @@ const appauthKanriLogsRoute = appauthKanriLogsRouteImport.update({
path: '/logs',
getParentRoute: () => appauthKanriRouteRoute,
} as any)
const appauthKanriHousesRoute = appauthKanriHousesRouteImport.update({
id: '/houses',
path: '/houses',
getParentRoute: () => appauthKanriRouteRoute,
} as any)
const appauthAccountSettingsRoute = appauthAccountSettingsRouteImport.update({
id: '/settings',
path: '/settings',
@@ -116,6 +122,7 @@ export interface FileRoutesByFullPath {
'/account/change-password': typeof appauthAccountChangePasswordRoute
'/account/profile': typeof appauthAccountProfileRoute
'/account/settings': typeof appauthAccountSettingsRoute
'/kanri/houses': typeof appauthKanriHousesRoute
'/kanri/logs': typeof appauthKanriLogsRoute
'/kanri/settings': typeof appauthKanriSettingsRoute
'/kanri/users': typeof appauthKanriUsersRoute
@@ -130,6 +137,7 @@ export interface FileRoutesByTo {
'/account/change-password': typeof appauthAccountChangePasswordRoute
'/account/profile': typeof appauthAccountProfileRoute
'/account/settings': typeof appauthAccountSettingsRoute
'/kanri/houses': typeof appauthKanriHousesRoute
'/kanri/logs': typeof appauthKanriLogsRoute
'/kanri/settings': typeof appauthKanriSettingsRoute
'/kanri/users': typeof appauthKanriUsersRoute
@@ -149,6 +157,7 @@ export interface FileRoutesById {
'/(app)/(auth)/account/change-password': typeof appauthAccountChangePasswordRoute
'/(app)/(auth)/account/profile': typeof appauthAccountProfileRoute
'/(app)/(auth)/account/settings': typeof appauthAccountSettingsRoute
'/(app)/(auth)/kanri/houses': typeof appauthKanriHousesRoute
'/(app)/(auth)/kanri/logs': typeof appauthKanriLogsRoute
'/(app)/(auth)/kanri/settings': typeof appauthKanriSettingsRoute
'/(app)/(auth)/kanri/users': typeof appauthKanriUsersRoute
@@ -167,6 +176,7 @@ export interface FileRouteTypes {
| '/account/change-password'
| '/account/profile'
| '/account/settings'
| '/kanri/houses'
| '/kanri/logs'
| '/kanri/settings'
| '/kanri/users'
@@ -181,6 +191,7 @@ export interface FileRouteTypes {
| '/account/change-password'
| '/account/profile'
| '/account/settings'
| '/kanri/houses'
| '/kanri/logs'
| '/kanri/settings'
| '/kanri/users'
@@ -199,6 +210,7 @@ export interface FileRouteTypes {
| '/(app)/(auth)/account/change-password'
| '/(app)/(auth)/account/profile'
| '/(app)/(auth)/account/settings'
| '/(app)/(auth)/kanri/houses'
| '/(app)/(auth)/kanri/logs'
| '/(app)/(auth)/kanri/settings'
| '/(app)/(auth)/kanri/users'
@@ -305,6 +317,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof appauthKanriLogsRouteImport
parentRoute: typeof appauthKanriRouteRoute
}
'/(app)/(auth)/kanri/houses': {
id: '/(app)/(auth)/kanri/houses'
path: '/houses'
fullPath: '/kanri/houses'
preLoaderRoute: typeof appauthKanriHousesRouteImport
parentRoute: typeof appauthKanriRouteRoute
}
'/(app)/(auth)/account/settings': {
id: '/(app)/(auth)/account/settings'
path: '/settings'
@@ -347,6 +366,7 @@ const appauthAccountRouteRouteWithChildren =
appauthAccountRouteRoute._addFileChildren(appauthAccountRouteRouteChildren)
interface appauthKanriRouteRouteChildren {
appauthKanriHousesRoute: typeof appauthKanriHousesRoute
appauthKanriLogsRoute: typeof appauthKanriLogsRoute
appauthKanriSettingsRoute: typeof appauthKanriSettingsRoute
appauthKanriUsersRoute: typeof appauthKanriUsersRoute
@@ -354,6 +374,7 @@ interface appauthKanriRouteRouteChildren {
}
const appauthKanriRouteRouteChildren: appauthKanriRouteRouteChildren = {
appauthKanriHousesRoute: appauthKanriHousesRoute,
appauthKanriLogsRoute: appauthKanriLogsRoute,
appauthKanriSettingsRoute: appauthKanriSettingsRoute,
appauthKanriUsersRoute: appauthKanriUsersRoute,

View File

@@ -0,0 +1,81 @@
import DataTable from '@/components/DataTable';
import CreateNewHouse from '@/components/house/create-house-dialog';
import { houseColumns } from '@/components/house/house-column';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
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 { housesQueries } from '@/service/queries';
import { WarehouseIcon } from '@phosphor-icons/react';
import { useQuery } from '@tanstack/react-query';
import { createFileRoute } from '@tanstack/react-router';
import { useState } from 'react';
export const Route = createFileRoute('/(app)/(auth)/kanri/houses')({
component: RouteComponent,
});
function RouteComponent() {
const [page, setPage] = useState(1);
const [pageLimit, setPageLimit] = useState(10);
const [searchKeyword, setSearchKeyword] = useState('');
const debouncedSearch = useDebounced(searchKeyword, 500);
const { data, isLoading } = useQuery(
housesQueries.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">
<Card>
<CardHeader>
<CardTitle className="text-xl flex items-center gap-2">
<WarehouseIcon size={24} />
{m.houses_page_ui_title()}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<SearchInput
keywords={searchKeyword}
setKeyword={setSearchKeyword}
onChange={onSearchChange}
/>
<CreateNewHouse />
</div>
{data && (
<DataTable
data={data.result || []}
columns={houseColumns}
page={page}
setPage={setPage}
limit={pageLimit}
setLimit={setPageLimit}
pagination={data.pagination}
/>
)}
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { logColumns } from '@/components/audit/audit-columns';
import DataTable from '@/components/DataTable';
import { Card, CardHeader, CardTitle } from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import SearchInput from '@/components/ui/search-input';
import { Skeleton } from '@/components/ui/skeleton';
import useDebounced from '@/hooks/use-debounced';
@@ -37,17 +37,14 @@ function RouteComponent() {
if (isLoading) {
return (
<div className="@container/main flex flex-1 flex-col gap-2 p-4">
<div className="flex flex-col gap-4">
<Skeleton className="h-20 w-full" />
<Skeleton className="h-130 w-full" />
</div>
<Skeleton className="h-150 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 flex flex-col gap-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">
<Card>
<CardHeader>
<CardTitle className="text-xl flex items-center gap-2">
@@ -55,25 +52,27 @@ function RouteComponent() {
{m.logs_page_ui_title()}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex items-center">
<SearchInput
keywords={searchKeyword}
setKeyword={setSearchKeyword}
onChange={onSearchChange}
/>
</div>
{data?.result && (
<DataTable
data={data.result || []}
columns={logColumns}
page={page}
setPage={setPage}
limit={pageLimit}
setLimit={setPageLimit}
pagination={data.pagination}
/>
)}
</CardContent>
</Card>
<div className="flex items-center">
<SearchInput
keywords={searchKeyword}
setKeyword={setSearchKeyword}
onChange={onSearchChange}
/>
</div>
{data?.result && (
<DataTable
data={data.result || []}
columns={logColumns}
page={page}
setPage={setPage}
limit={pageLimit}
setLimit={setPageLimit}
pagination={data.pagination}
/>
)}
</div>
</div>
);

View File

@@ -1,5 +1,5 @@
import DataTable from '@/components/DataTable';
import { Card, CardHeader, CardTitle } from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import SearchInput from '@/components/ui/search-input';
import { Skeleton } from '@/components/ui/skeleton';
import AddNewUserButton from '@/components/user/add-new-user-dialog';
@@ -39,44 +39,43 @@ function RouteComponent() {
if (isLoading) {
return (
<div className="@container/main flex flex-1 flex-col gap-2 p-4">
<div className="flex flex-col gap-4">
<Skeleton className="h-20 w-full" />
<Skeleton className="h-130 w-full" />
</div>
<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 flex flex-col gap-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">
<Card>
<CardHeader>
<CardHeader className="border-b">
<CardTitle className="text-xl flex items-center gap-2">
<UsersIcon size={24} />
{m.users_page_ui_title()}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<SearchInput
keywords={searchKeyword}
setKeyword={setSearchKeyword}
onChange={onSearchChange}
/>
<AddNewUserButton />
</div>
{data && (
<DataTable
data={data.result || []}
columns={userColumns}
page={page}
setPage={setPage}
limit={pageLimit}
setLimit={setPageLimit}
pagination={data.pagination}
/>
)}
</CardContent>
</Card>
<div className="flex items-center justify-between">
<SearchInput
keywords={searchKeyword}
setKeyword={setSearchKeyword}
onChange={onSearchChange}
/>
<AddNewUserButton />
</div>
{data && (
<DataTable
data={data.result || []}
columns={userColumns}
page={page}
setPage={setPage}
limit={pageLimit}
setLimit={setPageLimit}
pagination={data.pagination}
/>
)}
</div>
</div>
);

View File

@@ -1,6 +1,7 @@
import { prisma } from '@/db';
import { AuditWhereInput } from '@/generated/prisma/models';
import { authMiddleware } from '@/lib/middleware';
import { parseError } from '@/utils/helper';
import { createServerFn } from '@tanstack/react-start';
import { auditListSchema } from './audit.schema';
@@ -62,7 +63,8 @@ export const getAllAudit = createServerFn({ method: 'GET' })
},
};
} catch (error) {
console.log(error);
throw error;
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});

67
src/service/house.api.ts Normal file
View File

@@ -0,0 +1,67 @@
import { prisma } from '@/db';
import { OrganizationWhereInput } from '@/generated/prisma/models';
import { authMiddleware } from '@/lib/middleware';
import { parseError } from '@/utils/helper';
import { createServerFn } from '@tanstack/react-start';
import { houseListSchema } from './house.schema';
export const getAllHouse = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.inputValidator(houseListSchema)
.handler(async ({ data }) => {
try {
const { page, limit, keyword } = data;
const skip = (page - 1) * limit;
const where: OrganizationWhereInput = {
OR: [
{
name: {
contains: keyword,
mode: 'insensitive',
},
},
],
};
const [list, total]: [OrganizationWithMembers[], number] =
await Promise.all([
await prisma.organization.findMany({
where,
orderBy: { createdAt: 'desc' },
include: {
members: {
select: {
role: true,
user: {
select: {
name: true,
email: true,
image: true,
},
},
},
},
},
take: limit,
skip,
}),
await prisma.organization.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 };
}
});

View File

@@ -0,0 +1,12 @@
import { m } from '@/paraglide/messages';
import z from 'zod';
export const baseHouse = z.object({
id: z.string().nonempty(m.users_page_message_user_not_found()),
});
export const houseListSchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(10).max(100).default(10),
keyword: z.string().optional(),
});

View File

@@ -1,6 +1,7 @@
import { getSession } from '@/lib/auth/session';
import { queryOptions } from '@tanstack/react-query';
import { getAllAudit } from './audit.api';
import { getAllHouse } from './house.api';
import {
getAdminSettings,
getCurrentUserLanguage,
@@ -55,3 +56,12 @@ export const usersQueries = {
queryFn: () => getAllUser({ data: params }),
}),
};
export const housesQueries = {
all: ['houses'],
list: (params: { page: number; limit: number; keyword?: string }) =>
queryOptions({
queryKey: [...housesQueries.all, 'list', params],
queryFn: () => getAllHouse({ data: params }),
}),
};

View File

@@ -1,5 +1,6 @@
import { prisma } from '@/db';
import { Audit, Setting } from '@/generated/prisma/client';
import { parseError } from '@/utils/helper';
type AdminSettingValue = Pick<Setting, 'id' | 'key' | 'value'>;
@@ -48,7 +49,8 @@ export const createAuditLog = async (data: Omit<Audit, 'id' | 'createdAt'>) => {
},
});
} catch (error) {
console.log(error);
throw error;
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
};

View File

@@ -1,6 +1,6 @@
import { prisma } from '@/db';
import { authMiddleware } from '@/lib/middleware';
import { extractDiffObjects } from '@/utils/helper';
import { extractDiffObjects, parseError } from '@/utils/helper';
import { createServerFn } from '@tanstack/react-start';
import { createAuditLog, getAllAdminSettings } from './repository';
import { settingSchema, userSettingSchema } from './setting.schema';
@@ -25,8 +25,9 @@ export const getCurrentUserLanguage = createServerFn({ method: 'GET' })
return value.language;
} catch (error) {
console.log(error);
throw error;
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});
@@ -71,8 +72,9 @@ export const updateAdminSettings = createServerFn({ method: 'POST' })
return { success: true };
} catch (error) {
console.log(error);
throw error;
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});
@@ -102,8 +104,9 @@ export const getUserSettings = createServerFn({ method: 'GET' })
value: JSON.parse(settings.value) as UserSetting,
};
} catch (error) {
console.log(error);
throw error;
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});
@@ -147,7 +150,8 @@ export const updateUserSettings = createServerFn({ method: 'POST' })
return { success: true };
} catch (error) {
console.log(error);
throw error;
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});

View File

@@ -20,33 +20,39 @@ export const getAllUser = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.inputValidator(userListSchema)
.handler(async ({ data }) => {
const headers = getRequestHeaders();
const { page, limit, keyword } = data;
try {
const headers = getRequestHeaders();
const { page, limit, keyword } = data;
const list = await auth.api.listUsers({
query: {
searchValue: keyword,
searchField: 'name',
searchOperator: 'contains',
sortBy: 'createdAt',
sortDirection: 'asc',
limit,
offset: (page - 1) * limit,
},
headers,
});
const list = await auth.api.listUsers({
query: {
searchValue: keyword,
searchField: 'name',
searchOperator: 'contains',
sortBy: 'createdAt',
sortDirection: 'asc',
limit,
offset: (page - 1) * limit,
},
headers,
});
const totalItem = list.total;
const totalPage = Math.ceil(totalItem / limit);
const totalItem = list.total;
const totalPage = Math.ceil(totalItem / limit);
return {
result: list.users,
pagination: {
currentPage: page,
totalPage,
totalItem,
},
};
return {
result: list.users,
pagination: {
currentPage: page,
totalPage,
totalItem,
},
};
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
});
export const setUserPassword = createServerFn({ method: 'POST' })
@@ -74,6 +80,7 @@ export const setUserPassword = createServerFn({ method: 'POST' })
return result;
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
@@ -112,6 +119,7 @@ export const updateUserInformation = createServerFn({ method: 'POST' })
return result;
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
@@ -150,6 +158,7 @@ export const setUserRole = createServerFn({ method: 'POST' })
return result;
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
@@ -185,6 +194,7 @@ export const banUser = createServerFn({ method: 'POST' })
return result;
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}
@@ -215,6 +225,7 @@ export const unbanUser = createServerFn({ method: 'POST' })
return result;
} catch (error) {
console.error(error);
const { message, code } = parseError(error);
throw { message, code };
}

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

@@ -11,4 +11,21 @@ declare global {
};
};
}>;
type OrganizationWithMembers = Prisma.OrganizationGetPayload<{
include: {
members: {
select: {
role: true;
user: {
select: {
name: true;
email: true;
image: true;
};
};
};
};
};
}>;
}