diff --git a/src/app/(modules)/plans/components/PlanColumns.tsx b/src/app/(modules)/plans/components/PlanColumns.tsx new file mode 100644 index 0000000..086521e --- /dev/null +++ b/src/app/(modules)/plans/components/PlanColumns.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { useMemo } from 'react'; +import type { ColumnDef } from '@tanstack/react-table'; +import { Edit, RotateCcw } from 'lucide-react'; + +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { PERMISSIONS } from '@/constants/permissions'; +import { usePermissions } from '@/hooks/usePermissions'; +import type { Plan } from '@/types'; + +function formatDate(value?: string | null) { + if (!value) return '-'; + return new Intl.DateTimeFormat('en-IN', { + day: '2-digit', + month: 'short', + year: 'numeric', + }).format(new Date(value)); +} + +function formatPrice(value: string, cycle: string) { + const amount = Number(value); + const price = Number.isFinite(amount) ? amount.toFixed(2) : value; + return `${price} / ${cycle}`; +} + +interface UsePlanColumnsParams { + onEdit: (plan: Plan) => void; + onToggleStatus: (plan: Plan) => void; + pendingPlanId?: number; +} + +export function usePlanColumns({ + onEdit, + onToggleStatus, + pendingPlanId, +}: UsePlanColumnsParams): ColumnDef[] { + const { hasPermission } = usePermissions(); + const canEdit = hasPermission(PERMISSIONS.PLAN.UPDATE); + const canDelete = hasPermission(PERMISSIONS.PLAN.DELETE); + + return useMemo(() => { + const columns: ColumnDef[] = [ + { + accessorKey: 'name', + header: 'Plan', + cell: ({ row }) => ( +
+

{row.original.name}

+

{row.original.slug}

+
+ ), + }, + { + accessorKey: 'price', + header: 'Price', + cell: ({ row }) => formatPrice(row.original.price, row.original.billing_cycle), + }, + { + accessorKey: 'trial_days', + header: 'Trial', + cell: ({ row }) => `${row.original.trial_days} days`, + }, + { + id: 'limits', + header: 'Limits', + cell: ({ row }) => ( +
+

{row.original.max_projects} projects

+

{row.original.max_users} users

+
+ ), + }, + { + accessorKey: 'created_at', + header: 'Created', + cell: ({ row }) => formatDate(row.original.created_at), + }, + { + accessorKey: 'is_active', + header: 'Status', + cell: ({ row }) => ( +
+ + {row.original.is_active ? 'Active' : 'Inactive'} + + {row.original.is_custom ? Custom : null} +
+ ), + }, + ]; + + if (!canEdit && !canDelete) return columns; + + columns.push({ + id: 'actions', + header: () =>
Actions
, + cell: ({ row }) => { + const plan = row.original; + return ( +
+ {canEdit ? ( + + ) : null} + {canDelete ? ( + + ) : null} +
+ ); + }, + }); + + return columns; + }, [canDelete, canEdit, onEdit, onToggleStatus, pendingPlanId]); +} diff --git a/src/app/(modules)/plans/components/PlanFilters.tsx b/src/app/(modules)/plans/components/PlanFilters.tsx new file mode 100644 index 0000000..8b9071d --- /dev/null +++ b/src/app/(modules)/plans/components/PlanFilters.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { Search } from 'lucide-react'; + +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import type { PlanStatusFilter } from '../hooks/usePlanFilters'; + +interface PlanFiltersProps { + searchTerm: string; + statusFilter: PlanStatusFilter; + onSearchChange: (value: string) => void; + onStatusChange: (value: PlanStatusFilter) => void; +} + +export function PlanFilters({ + searchTerm, + statusFilter, + onSearchChange, + onStatusChange, +}: PlanFiltersProps) { + return ( +
+
+ + onSearchChange(event.target.value)} + placeholder="Search plans" + className="pl-9" + /> +
+ +
+ ); +} diff --git a/src/app/(modules)/plans/components/PlanSheet.tsx b/src/app/(modules)/plans/components/PlanSheet.tsx new file mode 100644 index 0000000..0799fd3 --- /dev/null +++ b/src/app/(modules)/plans/components/PlanSheet.tsx @@ -0,0 +1,237 @@ +'use client'; + +import type { ComponentProps } from 'react'; +import type { Control, UseFormRegister } from 'react-hook-form'; +import { Controller } from 'react-hook-form'; +import { Loader2 } from 'lucide-react'; + +import { PermissionTree } from '@/components/permission-tree'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import type { PermissionTreeItem } from '@/types'; + +import type { PlanFormValues } from '../hooks/usePlanForm'; + +interface PlanSheetProps { + open: boolean; + onOpenChange: (open: boolean) => void; + planId?: number; + register: UseFormRegister; + control: Control; + onSubmit: ComponentProps<'form'>['onSubmit']; + permissionTree: PermissionTreeItem[]; + permissionIds: number[]; + onPermissionIdsChange: (ids: number[]) => void; + isPermissionsLoading: boolean; + isSaving: boolean; +} + +export function PlanSheet({ + open, + onOpenChange, + planId, + register, + control, + onSubmit, + permissionTree, + permissionIds, + onPermissionIdsChange, + isPermissionsLoading, + isSaving, +}: PlanSheetProps) { + return ( + + + + {planId ? 'Edit Plan' : 'Create Plan'} + + Configure subscription limits, billing, and permission access. + + +
+
+
+ + +
+
+ + +
+
+ +
+ +