List out house
view house detail
This commit is contained in:
@@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -74,6 +74,9 @@ const ProfileForm = () => {
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('update load file', error);
|
||||
toast.error(JSON.stringify(error), {
|
||||
richColors: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
46
src/components/house/create-house-dialog.tsx
Normal file
46
src/components/house/create-house-dialog.tsx
Normal 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;
|
||||
47
src/components/house/house-column.tsx
Normal file
47
src/components/house/house-column.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
120
src/components/house/view-detail-dialog.tsx
Normal file
120
src/components/house/view-detail-dialog.tsx
Normal 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;
|
||||
@@ -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',
|
||||
|
||||
@@ -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...."
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
81
src/routes/(app)/(auth)/kanri/houses.tsx
Normal file
81
src/routes/(app)/(auth)/kanri/houses.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
67
src/service/house.api.ts
Normal 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 };
|
||||
}
|
||||
});
|
||||
12
src/service/house.schema.ts
Normal file
12
src/service/house.schema.ts
Normal 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(),
|
||||
});
|
||||
@@ -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 }),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
17
src/types/db.d.ts
vendored
@@ -11,4 +11,21 @@ declare global {
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
type OrganizationWithMembers = Prisma.OrganizationGetPayload<{
|
||||
include: {
|
||||
members: {
|
||||
select: {
|
||||
role: true;
|
||||
user: {
|
||||
select: {
|
||||
name: true;
|
||||
email: true;
|
||||
image: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user