feature/houses #11

Merged
sam merged 7 commits from feature/houses into develop 2026-02-12 15:45:48 +00:00
25 changed files with 570 additions and 103 deletions
Showing only changes of commit afa26ab50d - Show all commits

View File

@@ -56,6 +56,8 @@
"ui_view_all_notifications": "View All Notifications",
"ui_label_notifications": "Notifications",
"ui_change_password_btn": "Change password",
"nav_label_management": "Management",
"nav_label_basic": "Basic",
"nav_home": "Home",
"nav_dashboard": "Dashboard",
"nav_settings": "Settings",
@@ -63,8 +65,8 @@
"nav_edit": "Edit",
"nav_change_password": "Change password",
"nav_logs": "Logs",
"nav_users": "Người dùng",
"nav_roles": "Vai trò & quyền hạn",
"nav_users": "Users",
"nav_houses": "Houses",
"nav_box": "Box",
"nav_account": "Account",
"nav_profile": "Profile",
@@ -148,6 +150,13 @@
"users_page_ui_dialog_alert_ban_title": "",
"users_page_ui_dialog_alert_description": "Detail: \nName: {name}. \nEmail: {email}",
"users_page_ui_dialog_alert_description_2": "Reason: {reason}. \nExpiration: {exp}",
"houses_page_ui_title": "Houses",
"houses_page_ui_table_header_name": "Name",
"houses_page_ui_table_header_members": "Members",
"houses_page_ui_table_header_created_at": "Create date",
"houses_page_ui_view_label_count": "Quantity",
"houses_page_ui_view_table_header_email": "Email",
"houses_page_ui_view_table_header_role": "Quyền hạn",
"backend_message": [
{
"match": {

View File

@@ -52,10 +52,12 @@
"ui_update_password_btn": "Đặt lại mật khẩu",
"ui_change_role_btn": "Đặt lại quyền hạn",
"ui_edit_user_btn": "Chỉnh sửa người dùng",
"ui_dialog_view_title": "Xem chi tiết {type}",
"ui_dialog_view_title": "{type}: Xem chi tiết thông tin",
"ui_view_all_notifications": "Xem tất cả thông báo",
"ui_label_notifications": "Thông báo",
"ui_change_password_btn": "Đổi mật khẩu",
"nav_label_management": "Quản lý",
"nav_label_basic": "Cơ bản",
"nav_home": "Trang chủ",
"nav_dashboard": "Bảng điều khiển",
"nav_settings": "Cài đặt",
@@ -64,7 +66,7 @@
"nav_change_password": "Đổi mật khẩu",
"nav_logs": "Lịch sử",
"nav_users": "Người dùng",
"nav_roles": "Vai trò & quyền hạn",
"nav_houses": "Nhà",
"nav_box": "Hộp chứa",
"nav_account": "Tài khoản",
"nav_profile": "Hồ sơ",
@@ -148,6 +150,13 @@
"users_page_ui_dialog_alert_ban_title": "Bạn muốn khóa người dùng này?",
"users_page_ui_dialog_alert_description": "Chi tiết: \nTên: {name}. \nEmail: {email}",
"users_page_ui_dialog_alert_description_2": "\nLý do: {reason}. \nHiệu lực: {exp}",
"houses_page_ui_title": "Nhà",
"houses_page_ui_table_header_name": "Tên",
"houses_page_ui_table_header_members": "Thành viên",
"houses_page_ui_table_header_created_at": "Ngày tạo",
"houses_page_ui_view_label_count": "Số lượng",
"houses_page_ui_view_table_header_email": "Email",
"houses_page_ui_view_table_header_role": "Quyền hạn",
"backend_message": [
{
"match": {

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,7 +52,7 @@ function RouteComponent() {
{m.logs_page_ui_title()}
</CardTitle>
</CardHeader>
</Card>
<CardContent className="flex flex-col gap-4">
<div className="flex items-center">
<SearchInput
keywords={searchKeyword}
@@ -74,6 +71,8 @@ function RouteComponent() {
pagination={data.pagination}
/>
)}
</CardContent>
</Card>
</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,25 +39,22 @@ 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>
</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>
</Card>
<CardContent className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<SearchInput
keywords={searchKeyword}
@@ -77,6 +74,8 @@ function RouteComponent() {
pagination={data.pagination}
/>
)}
</CardContent>
</Card>
</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,6 +20,7 @@ export const getAllUser = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.inputValidator(userListSchema)
.handler(async ({ data }) => {
try {
const headers = getRequestHeaders();
const { page, limit, keyword } = data;
@@ -47,6 +48,11 @@ export const getAllUser = createServerFn({ method: 'GET' })
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;
};
};
};
};
};
}>;
}