task-b complate

This commit is contained in:
phaichayon
2026-06-11 16:55:45 +07:00
parent 7a390cf0df
commit 67545d9295
135 changed files with 3295 additions and 6385 deletions

View File

@@ -0,0 +1,70 @@
'use client';
import PageContainer from '@/components/layout/page-container';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Icons } from '@/components/icons';
import { billingInfoContent } from '@/config/infoconfig';
import { useSession } from 'next-auth/react';
export default function BillingPage() {
const { data: session, status } = useSession();
const activeOrganization = session?.user?.organizations.find(
(organization) => organization.id === session?.user?.activeOrganizationId
);
return (
<PageContainer
isLoading={status === 'loading'}
access={!!activeOrganization}
accessFallback={
<div className='flex min-h-[400px] items-center justify-center'>
<div className='space-y-2 text-center'>
<h2 className='text-2xl font-semibold'>No Workspace Selected</h2>
<p className='text-muted-foreground'>
Please select or create a workspace to view billing placeholders.
</p>
</div>
</div>
}
infoContent={billingInfoContent}
pageTitle='Billing & Plans'
pageDescription={`Review the current plan state for ${activeOrganization?.name ?? 'your workspace'}`}
>
<div className='space-y-6'>
<Alert>
<Icons.info className='h-4 w-4' />
<AlertDescription>
Clerk Billing has been removed from this route. Plan enforcement now reads app-owned
workspace data, and payment provider integration can be added later.
</AlertDescription>
</Alert>
<Card>
<CardHeader>
<CardTitle>Current plan</CardTitle>
<CardDescription>
This page is intentionally lightweight while billing is no longer vendor-managed.
</CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
<div className='rounded-lg border p-4'>
<div className='text-muted-foreground text-sm'>Workspace</div>
<div className='text-lg font-semibold'>{activeOrganization?.name}</div>
</div>
<div className='rounded-lg border p-4'>
<div className='text-muted-foreground text-sm'>Plan</div>
<div className='text-lg font-semibold capitalize'>
{activeOrganization?.plan ?? 'free'}
</div>
</div>
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
Add subscriptions, invoices, and provider sync in a later pass. The `organizations`
table is already the source of truth for plan gates.
</div>
</CardContent>
</Card>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,9 @@
import ChatViewPage from '@/features/chat/components/chat-view-page';
export const metadata = {
title: 'Dashboard: Chat'
};
export default function Page() {
return <ChatViewPage />;
}

View File

@@ -1,21 +1,37 @@
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { auth } from '@/auth';
import PageContainer from '@/components/layout/page-container';
import { crmReferenceQueryOptions } from '@/features/crm/api/queries';
import { MasterOptionsPage } from '@/features/crm/components/settings-page';
import { getQueryClient } from '@/lib/query-client';
import MasterOptionsListingPage from '@/features/foundation/master-options/components/master-options-listing';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { searchParamsCache } from '@/lib/searchparams';
import type { SearchParams } from 'nuqs/server';
export default function MasterOptionsRoute() {
const queryClient = getQueryClient();
void queryClient.prefetchQuery(crmReferenceQueryOptions());
type PageProps = {
searchParams: Promise<SearchParams>;
};
export default async function MasterOptionsRoute(props: PageProps) {
const searchParams = await props.searchParams;
const session = await auth();
const canManageOptions =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes(PERMISSIONS.organizationManage)));
searchParamsCache.parse(searchParams);
return (
<PageContainer
pageTitle='Master Options'
pageDescription='Mock UI สำหรับ status options, product types, payment term, currency และ tax rate'
pageDescription='Organization-scoped CRM option registry for statuses, product types, payment terms, currency, and branch abstractions.'
access={canManageOptions}
accessFallback={
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
You do not have access to master option management.
</div>
}
>
<HydrationBoundary state={dehydrate(queryClient)}>
<MasterOptionsPage />
</HydrationBoundary>
<MasterOptionsListingPage />
</PageContainer>
);
}

View File

@@ -0,0 +1,9 @@
import IconsViewPage from '@/features/elements/components/icons-view-page';
export const metadata = {
title: 'Dashboard : Icons'
};
export default function page() {
return <IconsViewPage />;
}

View File

@@ -0,0 +1,61 @@
import PageContainer from '@/components/layout/page-container';
import { auth } from '@/auth';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Icons } from '@/components/icons';
import Link from 'next/link';
export default async function ExclusivePage() {
const session = await auth();
const activeOrganizationName = session?.user?.activeOrganizationName;
const activePlan = session?.user?.activeOrganizationPlan;
return (
<PageContainer>
{activePlan !== 'pro' ? (
<div className='flex h-full items-center justify-center'>
<Alert>
<Icons.lock className='h-5 w-5 text-yellow-600' />
<AlertDescription>
<div className='mb-1 text-lg font-semibold'>Pro Plan Required</div>
<div className='text-muted-foreground'>
This page is only available to workspaces on the{' '}
<span className='font-semibold'>Pro</span> plan.
<br />
Upgrade your workspace later through the app-owned{' '}
<Link className='underline' href='/dashboard/billing'>
Billing &amp; Plans
</Link>{' '}
flow.
</div>
</AlertDescription>
</Alert>
</div>
) : (
<div className='space-y-6'>
<div>
<h1 className='flex items-center gap-2 text-3xl font-bold tracking-tight'>
<Icons.badgeCheck className='h-7 w-7 text-green-600' />
Exclusive Area
</h1>
<p className='text-muted-foreground'>
Welcome, <span className='font-semibold'>{activeOrganizationName}</span>! This page
remains gated by the workspace plan stored in your own database.
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Exclusive content enabled</CardTitle>
<CardDescription>
Your active workspace currently satisfies the `pro` plan gate.
</CardDescription>
</CardHeader>
<CardContent>
<div className='text-lg'>Have a wonderful day!</div>
</CardContent>
</Card>
</div>
)}
</PageContainer>
);
}

View File

@@ -0,0 +1,17 @@
import PageContainer from '@/components/layout/page-container';
import AdvancedFormPatterns from '@/features/forms/components/advanced-form-patterns';
export const metadata = {
title: 'Dashboard: Advanced Form Patterns'
};
export default function Page() {
return (
<PageContainer
pageTitle='Advanced Form Patterns'
pageDescription='Linked fields, async validation, dynamic rows, nested objects, cross-field validation, and form-level errors.'
>
<AdvancedFormPatterns />
</PageContainer>
);
}

View File

@@ -0,0 +1,17 @@
import PageContainer from '@/components/layout/page-container';
import DemoForm from '@/components/forms/demo-form';
export const metadata = {
title: 'Dashboard: Basic Form'
};
export default function Page() {
return (
<PageContainer
pageTitle='Basic Form'
pageDescription='A comprehensive form demo with all field types.'
>
<DemoForm />
</PageContainer>
);
}

View File

@@ -0,0 +1,14 @@
import PageContainer from '@/components/layout/page-container';
import FormsShowcasePage from '@/features/forms/components/forms-showcase-page';
export const metadata = {
title: 'Dashboard: Multi-Step Form'
};
export default function Page() {
return (
<PageContainer pageTitle='Multi-Step Form' pageDescription='Multi-step wizard form pattern.'>
<FormsShowcasePage />
</PageContainer>
);
}

View File

@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';
export default function Page() {
redirect('/dashboard/forms/basic');
}

View File

@@ -0,0 +1,17 @@
import PageContainer from '@/components/layout/page-container';
import SheetFormDemo from '@/features/forms/components/sheet-form-demo';
export const metadata = {
title: 'Dashboard: Sheet Form'
};
export default function Page() {
return (
<PageContainer
pageTitle='Sheet & Dialog Forms'
pageDescription='Form patterns inside sheets and dialogs with external submit buttons.'
>
<SheetFormDemo />
</PageContainer>
);
}

View File

@@ -0,0 +1,9 @@
import KanbanViewPage from '@/features/kanban/components/kanban-view-page';
export const metadata = {
title: 'Dashboard : Kanban view'
};
export default function page() {
return <KanbanViewPage />;
}

View File

@@ -1,26 +1,37 @@
import type { ReactNode } from 'react';
import KBar from '@/components/kbar';
import AppSidebar from '@/components/layout/app-sidebar';
import Header from '@/components/layout/header';
import { InfoSidebar } from '@/components/layout/info-sidebar';
import KBar from '@/components/kbar';
import { InfobarProvider } from '@/components/ui/infobar';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import type { Metadata } from 'next';
import { cookies } from 'next/headers';
export default function DashboardLayout({ children }: { children: ReactNode }) {
export const metadata: Metadata = {
title: 'Next Shadcn Dashboard Starter',
description: 'Basic dashboard with Next.js and Shadcn',
robots: {
index: false,
follow: false
}
};
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
// Persisting the sidebar state in the cookie.
const cookieStore = await cookies();
const defaultOpen = cookieStore.get('sidebar_state')?.value === 'true';
return (
<KBar>
<InfobarProvider defaultOpen={false}>
<SidebarProvider defaultOpen>
<AppSidebar />
<SidebarInset>
<Header />
<div className='flex flex-1 overflow-hidden'>
<div className='flex min-w-0 flex-1 flex-col'>{children}</div>
<InfoSidebar side='right' />
</div>
</SidebarInset>
</SidebarProvider>
</InfobarProvider>
<SidebarProvider defaultOpen={defaultOpen}>
<AppSidebar />
<SidebarInset>
<Header />
<InfobarProvider defaultOpen={false}>
{children}
<InfoSidebar side='right' />
</InfobarProvider>
</SidebarInset>
</SidebarProvider>
</KBar>
);
}

View File

@@ -0,0 +1,9 @@
import NotificationsPage from '@/features/notifications/components/notifications-page';
export const metadata = {
title: 'Dashboard: Notifications'
};
export default function Page() {
return <NotificationsPage />;
}

View File

@@ -0,0 +1,3 @@
export default function Default() {
return null;
}

View File

@@ -0,0 +1,14 @@
'use client';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Icons } from '@/components/icons';
export default function AreaStatsError({ error }: { error: Error }) {
return (
<Alert variant='destructive'>
<Icons.alertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Failed to load area statistics: {error.message}</AlertDescription>
</Alert>
);
}

View File

@@ -0,0 +1,5 @@
import { AreaGraphSkeleton } from '@/features/overview/components/area-graph-skeleton';
export default function Loading() {
return <AreaGraphSkeleton />;
}

View File

@@ -0,0 +1,7 @@
import { delay } from '@/constants/mock-api';
import { AreaGraph } from '@/features/overview/components/area-graph';
export default async function AreaStats() {
await delay(2000);
return <AreaGraph />;
}

View File

@@ -0,0 +1,3 @@
export default function Default() {
return null;
}

View File

@@ -0,0 +1,60 @@
'use client';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Icons } from '@/components/icons';
import { useRouter } from 'next/navigation';
import { useEffect, useTransition } from 'react';
import * as Sentry from '@sentry/nextjs';
interface StatsErrorProps {
error: Error;
reset: () => void; // Add reset function from error boundary
}
export default function StatsError({ error, reset }: StatsErrorProps) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
useEffect(() => {
Sentry.captureException(error);
}, [error]);
// the reload fn ensures the refresh is deffered until the next render phase allowing react to handle any pending states before processing
const reload = () => {
startTransition(() => {
router.refresh();
reset();
});
};
return (
<Card className='border-red-500'>
<CardHeader className='flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row'>
<div className='flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6'>
<Alert variant='destructive' className='border-none'>
<Icons.alertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription className='mt-2'>
Failed to load statistics: {error.message}
</AlertDescription>
</Alert>
</div>
</CardHeader>
<CardContent className='flex h-[316px] items-center justify-center p-6'>
<div className='text-center'>
<p className='text-muted-foreground mb-4 text-sm'>
Unable to display statistics at this time
</p>
<Button
onClick={() => reload()}
variant='outline'
className='min-w-[120px]'
disabled={isPending}
>
Try again
</Button>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,5 @@
import { BarGraphSkeleton } from '@/features/overview/components/bar-graph-skeleton';
export default function Loading() {
return <BarGraphSkeleton />;
}

View File

@@ -0,0 +1,8 @@
import { delay } from '@/constants/mock-api';
import { BarGraph } from '@/features/overview/components/bar-graph';
export default async function BarStats() {
await delay(1000);
return <BarGraph />;
}

View File

@@ -0,0 +1,3 @@
export default function Default() {
return null;
}

View File

@@ -0,0 +1,14 @@
'use client';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Icons } from '@/components/icons';
export default function PieStatsError({ error }: { error: Error }) {
return (
<Alert variant='destructive'>
<Icons.alertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Failed to load pie statistics: {error.message}</AlertDescription>
</Alert>
);
}

View File

@@ -0,0 +1,5 @@
import { PieGraphSkeleton } from '@/features/overview/components/pie-graph-skeleton';
export default function Loading() {
return <PieGraphSkeleton />;
}

View File

@@ -0,0 +1,7 @@
import { delay } from '@/constants/mock-api';
import { PieGraph } from '@/features/overview/components/pie-graph';
export default async function Stats() {
await delay(1000);
return <PieGraph />;
}

View File

@@ -0,0 +1,3 @@
export default function Default() {
return null;
}

View File

@@ -0,0 +1,14 @@
'use client';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Icons } from '@/components/icons';
export default function SalesError({ error }: { error: Error }) {
return (
<Alert variant='destructive'>
<Icons.alertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Failed to load sales data: {error.message}</AlertDescription>
</Alert>
);
}

View File

@@ -0,0 +1,6 @@
import { RecentSalesSkeleton } from '@/features/overview/components/recent-sales-skeleton';
import React from 'react';
export default function Loading() {
return <RecentSalesSkeleton />;
}

View File

@@ -0,0 +1,7 @@
import { delay } from '@/constants/mock-api';
import { RecentSales } from '@/features/overview/components/recent-sales';
export default async function Sales() {
await delay(3000);
return <RecentSales />;
}

View File

@@ -0,0 +1,14 @@
'use client';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Icons } from '@/components/icons';
export default function OverviewError({ error }: { error: Error }) {
return (
<Alert variant='destructive'>
<Icons.alertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Failed to load statistics: {error.message}</AlertDescription>
</Alert>
);
}

View File

@@ -0,0 +1,126 @@
import PageContainer from '@/components/layout/page-container';
import { Badge } from '@/components/ui/badge';
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardAction,
CardFooter
} from '@/components/ui/card';
import { Icons } from '@/components/icons';
import React from 'react';
export default function OverViewLayout({
sales,
pie_stats,
bar_stats,
area_stats
}: {
sales: React.ReactNode;
pie_stats: React.ReactNode;
bar_stats: React.ReactNode;
area_stats: React.ReactNode;
}) {
return (
<PageContainer>
<div className='flex flex-1 flex-col space-y-2'>
<div className='flex items-center justify-between'>
<h2 className='text-2xl font-bold tracking-tight'>Hi, Welcome back 👋</h2>
</div>
<div className='*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs md:grid-cols-2 lg:grid-cols-4'>
<Card className='@container/card'>
<CardHeader>
<CardDescription>Total Revenue</CardDescription>
<CardTitle className='text-2xl font-semibold tabular-nums @[250px]/card:text-3xl'>
$1,250.00
</CardTitle>
<CardAction>
<Badge variant='outline'>
<Icons.trendingUp />
+12.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className='flex-col items-start gap-1.5 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Trending up this month <Icons.trendingUp className='size-4' />
</div>
<div className='text-muted-foreground'>Visitors for the last 6 months</div>
</CardFooter>
</Card>
<Card className='@container/card'>
<CardHeader>
<CardDescription>New Customers</CardDescription>
<CardTitle className='text-2xl font-semibold tabular-nums @[250px]/card:text-3xl'>
1,234
</CardTitle>
<CardAction>
<Badge variant='outline'>
<Icons.trendingDown />
-20%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className='flex-col items-start gap-1.5 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Down 20% this period <Icons.trendingDown className='size-4' />
</div>
<div className='text-muted-foreground'>Acquisition needs attention</div>
</CardFooter>
</Card>
<Card className='@container/card'>
<CardHeader>
<CardDescription>Active Accounts</CardDescription>
<CardTitle className='text-2xl font-semibold tabular-nums @[250px]/card:text-3xl'>
45,678
</CardTitle>
<CardAction>
<Badge variant='outline'>
<Icons.trendingUp />
+12.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className='flex-col items-start gap-1.5 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Strong user retention <Icons.trendingUp className='size-4' />
</div>
<div className='text-muted-foreground'>Engagement exceed targets</div>
</CardFooter>
</Card>
<Card className='@container/card'>
<CardHeader>
<CardDescription>Growth Rate</CardDescription>
<CardTitle className='text-2xl font-semibold tabular-nums @[250px]/card:text-3xl'>
4.5%
</CardTitle>
<CardAction>
<Badge variant='outline'>
<Icons.trendingUp />
+4.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className='flex-col items-start gap-1.5 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Steady performance increase <Icons.trendingUp className='size-4' />
</div>
<div className='text-muted-foreground'>Meets growth projections</div>
</CardFooter>
</Card>
</div>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>
<div className='col-span-4'>{bar_stats}</div>
<div className='col-span-4 md:col-span-3'>
{/* sales arallel routes */}
{sales}
</div>
<div className='col-span-4'>{area_stats}</div>
<div className='col-span-4 min-h-0 md:col-span-3'>{pie_stats}</div>
</div>
</div>
</PageContainer>
);
}

View File

@@ -1,5 +1,5 @@
import { redirect } from 'next/navigation';
import OverViewPage from '@/features/overview/components/overview';
export default function DashboardIndexPage() {
redirect('/dashboard/crm');
export default function DashboardPage() {
return <OverViewPage />;
}

View File

@@ -0,0 +1,30 @@
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { getQueryClient } from '@/lib/query-client';
import { productByIdOptions } from '@/features/products/api/queries';
import PageContainer from '@/components/layout/page-container';
import ProductViewPage from '@/features/products/components/product-view-page';
export const metadata = {
title: 'Dashboard : Product View'
};
type PageProps = { params: Promise<{ productId: string }> };
export default async function Page(props: PageProps) {
const params = await props.params;
const queryClient = getQueryClient();
if (params.productId !== 'new') {
void queryClient.prefetchQuery(productByIdOptions(Number(params.productId)));
}
return (
<PageContainer>
<div className='flex-1 space-y-4'>
<HydrationBoundary state={dehydrate(queryClient)}>
<ProductViewPage productId={params.productId} />
</HydrationBoundary>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,51 @@
import { auth } from '@/auth';
import PageContainer from '@/components/layout/page-container';
import { buttonVariants } from '@/components/ui/button';
import ProductListingPage from '@/features/products/components/product-listing';
import { searchParamsCache } from '@/lib/searchparams';
import { cn } from '@/lib/utils';
import { Icons } from '@/components/icons';
import Link from 'next/link';
import { SearchParams } from 'nuqs/server';
import { productInfoContent } from '@/config/infoconfig';
export const metadata = {
title: 'Dashboard: Products'
};
type pageProps = {
searchParams: Promise<SearchParams>;
};
export default async function Page(props: pageProps) {
const searchParams = await props.searchParams;
const session = await auth();
const hasActiveOrganization = !!session?.user?.activeOrganizationId;
searchParamsCache.parse(searchParams);
return (
<PageContainer
pageTitle='Products'
pageDescription='Manage products (React Query + nuqs table pattern.)'
infoContent={productInfoContent}
access={hasActiveOrganization}
accessFallback={
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
Select an active workspace before managing products.
</div>
}
pageHeaderAction={
hasActiveOrganization ? (
<Link
href='/dashboard/product/new'
className={cn(buttonVariants(), 'text-xs md:text-sm')}
>
<Icons.add className='mr-2 h-4 w-4' /> Add New
</Link>
) : null
}
>
<ProductListingPage canPrefetch={hasActiveOrganization} />
</PageContainer>
);
}

View File

@@ -0,0 +1,9 @@
import ProfileViewPage from '@/features/profile/components/profile-view-page';
export const metadata = {
title: 'Dashboard : Profile'
};
export default async function Page() {
return <ProfileViewPage />;
}

View File

@@ -0,0 +1,33 @@
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { getQueryClient } from '@/lib/query-client';
import { pokemonOptions } from '@/features/react-query-demo/api/queries';
import { PokemonInfo } from '@/features/react-query-demo/components/pokemon-info';
import PageContainer from '@/components/layout/page-container';
import { Suspense } from 'react';
import { PokemonSkeleton } from '@/features/react-query-demo/components/pokemon-skeleton';
import { reactQueryInfoContent } from '@/features/react-query-demo/info-content';
export const metadata = {
title: 'Dashboard: React Query'
};
export default function ReactQueryPage() {
const queryClient = getQueryClient();
// Prefetch on the server — data is ready before client JS loads
void queryClient.prefetchQuery(pokemonOptions(25));
return (
<PageContainer
pageTitle='React Query'
pageDescription='Server prefetch + client hydration + suspense query pattern.'
infoContent={reactQueryInfoContent}
>
<HydrationBoundary state={dehydrate(queryClient)}>
<Suspense fallback={<PokemonSkeleton />}>
<PokemonInfo />
</Suspense>
</HydrationBoundary>
</PageContainer>
);
}

View File

@@ -1,47 +1,44 @@
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { auth } from '@/auth';
import PageContainer from '@/components/layout/page-container';
import { getQueryClient } from '@/lib/query-client';
import UserListingPage from '@/features/users/components/user-listing';
import { searchParamsCache } from '@/lib/searchparams';
import type { SearchParams } from 'nuqs/server';
import { exampleUsersQueryOptions } from '@/features/example-dashboard/api';
import { ExampleUserTable } from '@/features/example-dashboard/components/example-user-table';
import { usersInfoContent } from '@/features/users/info-content';
import { UserFormSheetTrigger } from '@/features/users/components/user-form-sheet';
export const metadata = {
title: 'Dashboard: Users'
};
type PageProps = {
searchParams: Promise<SearchParams>;
};
export const metadata = {
title: 'Example Dashboard: Users'
};
export default async function ExampleUsersPage(props: PageProps) {
export default async function UsersPage(props: PageProps) {
const searchParams = await props.searchParams;
const session = await auth();
const canManageUsers =
session?.user?.systemRole === 'super_admin' ||
(!!session?.user?.activeOrganizationId &&
(session.user.activeMembershipRole === 'admin' ||
session.user.activePermissions.includes('users:manage')));
searchParamsCache.parse(searchParams);
const page = searchParamsCache.get('page');
const search = searchParamsCache.get('name');
const pageLimit = searchParamsCache.get('perPage');
const roles = searchParamsCache.get('role');
const sort = searchParamsCache.get('sort');
const filters = {
page,
limit: pageLimit,
...(search && { search }),
...(roles && { roles }),
...(sort && { sort })
};
const queryClient = getQueryClient();
void queryClient.prefetchQuery(exampleUsersQueryOptions(filters));
return (
<PageContainer
pageTitle='Users'
pageDescription='Public demo of the organization-aware user listing pattern without requiring auth.'
pageDescription='Manage users (React Query + nuqs table pattern.)'
infoContent={usersInfoContent}
access={canManageUsers}
accessFallback={
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
You do not have access to user management.
</div>
}
pageHeaderAction={canManageUsers ? <UserFormSheetTrigger /> : null}
>
<HydrationBoundary state={dehydrate(queryClient)}>
<ExampleUserTable />
</HydrationBoundary>
<UserListingPage />
</PageContainer>
);
}