task-b complate
This commit is contained in:
70
src/app/dashboard/billing/page.tsx
Normal file
70
src/app/dashboard/billing/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
9
src/app/dashboard/chat/page.tsx
Normal file
9
src/app/dashboard/chat/page.tsx
Normal 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 />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
9
src/app/dashboard/elements/icons/page.tsx
Normal file
9
src/app/dashboard/elements/icons/page.tsx
Normal 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 />;
|
||||
}
|
||||
61
src/app/dashboard/exclusive/page.tsx
Normal file
61
src/app/dashboard/exclusive/page.tsx
Normal 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 & 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>
|
||||
);
|
||||
}
|
||||
17
src/app/dashboard/forms/advanced/page.tsx
Normal file
17
src/app/dashboard/forms/advanced/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
17
src/app/dashboard/forms/basic/page.tsx
Normal file
17
src/app/dashboard/forms/basic/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
14
src/app/dashboard/forms/multi-step/page.tsx
Normal file
14
src/app/dashboard/forms/multi-step/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
src/app/dashboard/forms/page.tsx
Normal file
5
src/app/dashboard/forms/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function Page() {
|
||||
redirect('/dashboard/forms/basic');
|
||||
}
|
||||
17
src/app/dashboard/forms/sheet-form/page.tsx
Normal file
17
src/app/dashboard/forms/sheet-form/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
9
src/app/dashboard/kanban/page.tsx
Normal file
9
src/app/dashboard/kanban/page.tsx
Normal 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 />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
9
src/app/dashboard/notifications/page.tsx
Normal file
9
src/app/dashboard/notifications/page.tsx
Normal 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 />;
|
||||
}
|
||||
3
src/app/dashboard/overview/@area_stats/default.tsx
Normal file
3
src/app/dashboard/overview/@area_stats/default.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Default() {
|
||||
return null;
|
||||
}
|
||||
14
src/app/dashboard/overview/@area_stats/error.tsx
Normal file
14
src/app/dashboard/overview/@area_stats/error.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
src/app/dashboard/overview/@area_stats/loading.tsx
Normal file
5
src/app/dashboard/overview/@area_stats/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { AreaGraphSkeleton } from '@/features/overview/components/area-graph-skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <AreaGraphSkeleton />;
|
||||
}
|
||||
7
src/app/dashboard/overview/@area_stats/page.tsx
Normal file
7
src/app/dashboard/overview/@area_stats/page.tsx
Normal 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 />;
|
||||
}
|
||||
3
src/app/dashboard/overview/@bar_stats/default.tsx
Normal file
3
src/app/dashboard/overview/@bar_stats/default.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Default() {
|
||||
return null;
|
||||
}
|
||||
60
src/app/dashboard/overview/@bar_stats/error.tsx
Normal file
60
src/app/dashboard/overview/@bar_stats/error.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
src/app/dashboard/overview/@bar_stats/loading.tsx
Normal file
5
src/app/dashboard/overview/@bar_stats/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { BarGraphSkeleton } from '@/features/overview/components/bar-graph-skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <BarGraphSkeleton />;
|
||||
}
|
||||
8
src/app/dashboard/overview/@bar_stats/page.tsx
Normal file
8
src/app/dashboard/overview/@bar_stats/page.tsx
Normal 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 />;
|
||||
}
|
||||
3
src/app/dashboard/overview/@pie_stats/default.tsx
Normal file
3
src/app/dashboard/overview/@pie_stats/default.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Default() {
|
||||
return null;
|
||||
}
|
||||
14
src/app/dashboard/overview/@pie_stats/error.tsx
Normal file
14
src/app/dashboard/overview/@pie_stats/error.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
src/app/dashboard/overview/@pie_stats/loading.tsx
Normal file
5
src/app/dashboard/overview/@pie_stats/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { PieGraphSkeleton } from '@/features/overview/components/pie-graph-skeleton';
|
||||
|
||||
export default function Loading() {
|
||||
return <PieGraphSkeleton />;
|
||||
}
|
||||
7
src/app/dashboard/overview/@pie_stats/page.tsx
Normal file
7
src/app/dashboard/overview/@pie_stats/page.tsx
Normal 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 />;
|
||||
}
|
||||
3
src/app/dashboard/overview/@sales/default.tsx
Normal file
3
src/app/dashboard/overview/@sales/default.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Default() {
|
||||
return null;
|
||||
}
|
||||
14
src/app/dashboard/overview/@sales/error.tsx
Normal file
14
src/app/dashboard/overview/@sales/error.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
6
src/app/dashboard/overview/@sales/loading.tsx
Normal file
6
src/app/dashboard/overview/@sales/loading.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { RecentSalesSkeleton } from '@/features/overview/components/recent-sales-skeleton';
|
||||
import React from 'react';
|
||||
|
||||
export default function Loading() {
|
||||
return <RecentSalesSkeleton />;
|
||||
}
|
||||
7
src/app/dashboard/overview/@sales/page.tsx
Normal file
7
src/app/dashboard/overview/@sales/page.tsx
Normal 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 />;
|
||||
}
|
||||
14
src/app/dashboard/overview/error.tsx
Normal file
14
src/app/dashboard/overview/error.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
126
src/app/dashboard/overview/layout.tsx
Normal file
126
src/app/dashboard/overview/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
30
src/app/dashboard/product/[productId]/page.tsx
Normal file
30
src/app/dashboard/product/[productId]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
51
src/app/dashboard/product/page.tsx
Normal file
51
src/app/dashboard/product/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
9
src/app/dashboard/profile/[[...profile]]/page.tsx
Normal file
9
src/app/dashboard/profile/[[...profile]]/page.tsx
Normal 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 />;
|
||||
}
|
||||
33
src/app/dashboard/react-query/page.tsx
Normal file
33
src/app/dashboard/react-query/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user