style: do inputs design modification

This commit is contained in:
2026-06-17 16:14:50 +05:30
parent 15f54372f4
commit b54242cf7e
49 changed files with 665 additions and 783 deletions

View File

@@ -2,12 +2,8 @@
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 { Client } from '@/types';
function formatDate(value?: string | null) {
@@ -19,23 +15,9 @@ function formatDate(value?: string | null) {
}).format(new Date(value));
}
interface UseClientColumnsParams {
onEdit: (client: Client) => void;
onToggleStatus: (client: Client) => void;
pendingClientId?: number;
}
export function useClientColumns({
onEdit,
onToggleStatus,
pendingClientId,
}: UseClientColumnsParams): ColumnDef<Client>[] {
const { hasPermission } = usePermissions();
const canEdit = hasPermission(PERMISSIONS.CLIENT.UPDATE);
const canDelete = hasPermission(PERMISSIONS.CLIENT.DELETE);
export function useClientColumns(): ColumnDef<Client>[] {
return useMemo(() => {
const columns: ColumnDef<Client>[] = [
return [
{
accessorKey: 'name',
header: 'Client',
@@ -75,38 +57,5 @@ export function useClientColumns({
),
},
];
if (!canEdit && !canDelete) return columns;
columns.push({
id: 'actions',
header: () => <div className="text-right">Actions</div>,
enableSorting: false,
cell: ({ row }) => {
const client = row.original;
return (
<div className="flex justify-end gap-2">
{canEdit ? (
<Button variant="outline" size="sm" onClick={() => onEdit(client)}>
<Edit className="size-4" />
</Button>
) : null}
{canDelete ? (
<Button
variant="outline"
size="sm"
disabled={pendingClientId === client.id}
onClick={() => onToggleStatus(client)}
>
<RotateCcw className="mr-2 size-4" />
{client.is_active ? 'Deactivate' : 'Activate'}
</Button>
) : null}
</div>
);
},
});
return columns;
}, [canDelete, canEdit, onEdit, onToggleStatus, pendingClientId]);
}, []);
}

View File

@@ -2,8 +2,10 @@
import type { ReactNode } from 'react';
import type { ColumnDef, SortingState } from '@tanstack/react-table';
import { Edit, RotateCcw } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import { PERMISSIONS } from '@/constants/permissions';
import type { Client } from '@/types';
interface ClientTableProps {
@@ -18,6 +20,9 @@ interface ClientTableProps {
onPageChange: (skip: number) => void;
onLimitChange: (limit: number) => void;
onSortingChange: (sorting: SortingState) => void;
onEdit: (client: Client) => void;
onToggleStatus: (client: Client) => void;
pendingClientId?: number;
}
export function ClientTable({
@@ -32,6 +37,9 @@ export function ClientTable({
onPageChange,
onLimitChange,
onSortingChange,
onEdit,
onToggleStatus,
pendingClientId,
}: ClientTableProps) {
return (
<DataTable
@@ -41,6 +49,21 @@ export function ClientTable({
isLoading={isLoading}
toolbar={toolbar}
emptyTitle="No clients found."
actions={[
{
label: 'Edit',
icon: <Edit className="size-4" />,
permission: PERMISSIONS.CLIENT.UPDATE,
onClick: onEdit,
},
{
label: (client) => (client.is_active ? 'Deactivate' : 'Activate'),
icon: <RotateCcw className="size-4" />,
permission: PERMISSIONS.CLIENT.DELETE,
disabled: (client) => pendingClientId === client.id,
onClick: onToggleStatus,
},
]}
pagination={{
skip,
limit,

View File

@@ -69,11 +69,7 @@ export default function ClientsPage() {
[updateClientStatus],
);
const columns = useClientColumns({
onEdit: openEdit,
onToggleStatus: toggleStatus,
pendingClientId,
});
const columns = useClientColumns();
const total = clientsQuery.data?.total ?? 0;
const clients = clientsQuery.data?.items ?? [];
@@ -118,6 +114,9 @@ export default function ClientsPage() {
onPageChange={setSkip}
onLimitChange={setLimit}
onSortingChange={setSorting}
onEdit={openEdit}
onToggleStatus={toggleStatus}
pendingClientId={pendingClientId}
/>
</main>

View File

@@ -1,7 +1,7 @@
'use client';
import type { ComponentProps } from 'react';
import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form';
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
import { Globe, Loader2, Package as PackageIcon } from 'lucide-react';
import { FormField } from '@/components/form';
@@ -36,7 +36,7 @@ interface PackageDialogProps {
onProjectChange: (projectId: string) => void;
register: UseFormRegister<PackageFormValues>;
errors: FieldErrors<PackageFormValues>;
touchedFields: UseFormReturn<PackageFormValues>['formState']['touchedFields'];
isSubmitted: boolean;
onSubmit: ComponentProps<'form'>['onSubmit'];
canSubmit: boolean;
isSaving: boolean;
@@ -52,19 +52,15 @@ export function PackageDialog({
onProjectChange,
register,
errors,
touchedFields,
isSubmitted,
onSubmit,
canSubmit,
isSaving,
}: PackageDialogProps) {
const projectErrorMessage = touchedFields.project_id ? errors.project_id?.message : undefined;
const nameErrorMessage = touchedFields.name ? errors.name?.message : undefined;
const startErrorMessage = touchedFields.chainage_start_km
? errors.chainage_start_km?.message
: undefined;
const endErrorMessage = touchedFields.chainage_end_km
? errors.chainage_end_km?.message
: undefined;
const projectErrorMessage = isSubmitted ? errors.project_id?.message : undefined;
const nameErrorMessage = isSubmitted ? errors.name?.message : undefined;
const startErrorMessage = isSubmitted ? errors.chainage_start_km?.message : undefined;
const endErrorMessage = isSubmitted ? errors.chainage_end_km?.message : undefined;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -155,7 +151,12 @@ export function PackageDialog({
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isSaving}>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSaving}
>
Cancel
</Button>
<Button type="submit" disabled={isSaving || !canSubmit}>

View File

@@ -1,6 +1,7 @@
'use client';
import type { ColumnDef } from '@tanstack/react-table';
import { Edit3, Trash2 } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import type { Package } from '@/types';
@@ -35,8 +36,19 @@ export function PackageTable({
title="Packages"
data={packages}
columns={columns}
onEdit={onEdit}
onDelete={onDelete}
actions={[
{
label: 'Edit',
icon: <Edit3 className="size-4" />,
onClick: onEdit,
},
{
label: 'Delete',
icon: <Trash2 className="size-4" />,
className: 'text-destructive',
onClick: onDelete,
},
]}
isLoading={isLoading}
emptyTitle="No packages found."
pagination={{

View File

@@ -67,10 +67,10 @@ export function usePackageForm({ onSaved }: { onSaved: () => void }) {
reset,
setValue,
control,
formState: { errors, isSubmitting, touchedFields },
formState: { errors, isSubmitting, isSubmitted },
} = useForm<PackageFormValues>({
defaultValues,
mode: 'onTouched',
mode: 'onSubmit',
reValidateMode: 'onChange',
resolver: zodResolver(packageFormSchema),
});
@@ -144,7 +144,7 @@ export function usePackageForm({ onSaved }: { onSaved: () => void }) {
projectId,
setProjectId,
errors,
touchedFields,
isSubmitted,
canSubmit,
isSaving: isSubmitting || saveMutation.isPending,
};

View File

@@ -37,7 +37,7 @@ export default function PackagePage() {
projectId,
setProjectId,
errors,
touchedFields,
isSubmitted,
canSubmit,
isSaving,
} = packageForm;
@@ -119,7 +119,7 @@ export default function PackagePage() {
onProjectChange={setProjectId}
register={register}
errors={errors}
touchedFields={touchedFields}
isSubmitted={isSubmitted}
onSubmit={onSubmit}
canSubmit={canSubmit}
isSaving={isSaving}

View File

@@ -2,12 +2,8 @@
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) {
@@ -25,23 +21,9 @@ function formatPrice(value: string, cycle: string) {
return `${price} / ${cycle}`;
}
interface UsePlanColumnsParams {
onEdit: (plan: Plan) => void;
onToggleStatus: (plan: Plan) => void;
pendingPlanId?: number;
}
export function usePlanColumns({
onEdit,
onToggleStatus,
pendingPlanId,
}: UsePlanColumnsParams): ColumnDef<Plan>[] {
const { hasPermission } = usePermissions();
const canEdit = hasPermission(PERMISSIONS.PLAN.UPDATE);
const canDelete = hasPermission(PERMISSIONS.PLAN.DELETE);
export function usePlanColumns(): ColumnDef<Plan>[] {
return useMemo(() => {
const columns: ColumnDef<Plan>[] = [
return [
{
accessorKey: 'name',
header: 'Plan',
@@ -91,38 +73,5 @@ export function usePlanColumns({
),
},
];
if (!canEdit && !canDelete) return columns;
columns.push({
id: 'actions',
header: () => <div className="text-right">Actions</div>,
enableSorting: false,
cell: ({ row }) => {
const plan = row.original;
return (
<div className="flex justify-end gap-2">
{canEdit ? (
<Button variant="outline" size="sm" onClick={() => onEdit(plan)}>
<Edit className="size-4" />
</Button>
) : null}
{canDelete ? (
<Button
variant="outline"
size="sm"
disabled={pendingPlanId === plan.id}
onClick={() => onToggleStatus(plan)}
>
<RotateCcw className="mr-2 size-4" />
{plan.is_active ? 'Deactivate' : 'Activate'}
</Button>
) : null}
</div>
);
},
});
return columns;
}, [canDelete, canEdit, onEdit, onToggleStatus, pendingPlanId]);
}, []);
}

View File

@@ -93,7 +93,7 @@ export function PlanSheet({
placeholder="Basic plan for small teams"
{...register('description', { required: true })}
required
className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 outline-none focus-visible:border-ring"
/>
</div>

View File

@@ -2,8 +2,10 @@
import type { ReactNode } from 'react';
import type { ColumnDef, SortingState } from '@tanstack/react-table';
import { Edit, RotateCcw } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import { PERMISSIONS } from '@/constants/permissions';
import type { Plan } from '@/types';
interface PlanTableProps {
@@ -18,6 +20,9 @@ interface PlanTableProps {
onPageChange: (skip: number) => void;
onLimitChange: (limit: number) => void;
onSortingChange: (sorting: SortingState) => void;
onEdit: (plan: Plan) => void;
onToggleStatus: (plan: Plan) => void;
pendingPlanId?: number;
}
export function PlanTable({
@@ -32,6 +37,9 @@ export function PlanTable({
onPageChange,
onLimitChange,
onSortingChange,
onEdit,
onToggleStatus,
pendingPlanId,
}: PlanTableProps) {
return (
<DataTable
@@ -41,6 +49,21 @@ export function PlanTable({
isLoading={isLoading}
toolbar={toolbar}
emptyTitle="No plans found."
actions={[
{
label: 'Edit',
icon: <Edit className="size-4" />,
permission: PERMISSIONS.PLAN.UPDATE,
onClick: onEdit,
},
{
label: (plan) => (plan.is_active ? 'Deactivate' : 'Activate'),
icon: <RotateCcw className="size-4" />,
permission: PERMISSIONS.PLAN.DELETE,
disabled: (plan) => pendingPlanId === plan.id,
onClick: onToggleStatus,
},
]}
pagination={{
skip,
limit,

View File

@@ -57,15 +57,8 @@ export default function PlansPage() {
onSaved: () => handleSheetOpenChange(false),
});
const { openCreate: prepareCreatePlan, openEdit: prepareEditPlan } = planForm;
const {
register,
control,
onSubmit,
planId,
permissionIds,
setPermissionIds,
isSaving,
} = planForm;
const { register, control, onSubmit, planId, permissionIds, setPermissionIds, isSaving } =
planForm;
const statusMutation = usePlanStatusMutation();
const openCreate = useCallback(() => {
@@ -123,11 +116,7 @@ export default function PlansPage() {
[statusMutation],
);
const columns = usePlanColumns({
onEdit: openEdit,
onToggleStatus: toggleStatus,
pendingPlanId: statusMutation.variables?.id,
});
const columns = usePlanColumns();
const plans = plansQuery.data?.items ?? [];
const total = plansQuery.data?.total ?? plansQuery.data?.totalItems ?? 0;
@@ -172,6 +161,9 @@ export default function PlansPage() {
onPageChange={setSkip}
onLimitChange={setLimit}
onSortingChange={setSorting}
onEdit={openEdit}
onToggleStatus={toggleStatus}
pendingPlanId={statusMutation.variables?.id}
/>
</main>

View File

@@ -1,7 +1,7 @@
'use client';
import type { ComponentProps } from 'react';
import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form';
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
import { Layers, Loader2, MapPin, Route } from 'lucide-react';
import { FormField } from '@/components/form';
@@ -24,7 +24,7 @@ interface ProjectDialogProps {
projectId?: string;
register: UseFormRegister<ProjectFormValues>;
errors: FieldErrors<ProjectFormValues>;
touchedFields: UseFormReturn<ProjectFormValues>['formState']['touchedFields'];
isSubmitted: boolean;
onSubmit: ComponentProps<'form'>['onSubmit'];
canSubmit: boolean;
isSaving: boolean;
@@ -36,16 +36,16 @@ export function ProjectDialog({
projectId,
register,
errors,
touchedFields,
isSubmitted,
onSubmit,
canSubmit,
isSaving,
}: ProjectDialogProps) {
const nameErrorMessage = touchedFields.name ? errors.name?.message : undefined;
const startLatErrorMessage = touchedFields.start_lat ? errors.start_lat?.message : undefined;
const startLngErrorMessage = touchedFields.start_lng ? errors.start_lng?.message : undefined;
const endLatErrorMessage = touchedFields.end_lat ? errors.end_lat?.message : undefined;
const endLngErrorMessage = touchedFields.end_lng ? errors.end_lng?.message : undefined;
const nameErrorMessage = isSubmitted ? errors.name?.message : undefined;
const startLatErrorMessage = isSubmitted ? errors.start_lat?.message : undefined;
const startLngErrorMessage = isSubmitted ? errors.start_lng?.message : undefined;
const endLatErrorMessage = isSubmitted ? errors.end_lat?.message : undefined;
const endLngErrorMessage = isSubmitted ? errors.end_lng?.message : undefined;
return (
<Dialog open={open} onOpenChange={onOpenChange}>

View File

@@ -1,6 +1,7 @@
'use client';
import type { ColumnDef } from '@tanstack/react-table';
import { Edit3, Trash2 } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import type { Project } from '@/types';
@@ -35,8 +36,19 @@ export function ProjectTable({
title="Projects"
data={projects}
columns={columns}
onEdit={onEdit}
onDelete={onDelete}
actions={[
{
label: 'Edit',
icon: <Edit3 className="size-4" />,
onClick: onEdit,
},
{
label: 'Delete',
icon: <Trash2 className="size-4" />,
className: 'text-destructive',
onClick: onDelete,
},
]}
isLoading={isLoading}
emptyTitle="No projects found."
pagination={{

View File

@@ -72,10 +72,10 @@ export function useProjectForm({ onSaved }: { onSaved: () => void }) {
handleSubmit,
reset,
control,
formState: { errors, isSubmitting, touchedFields },
formState: { errors, isSubmitting, isSubmitted },
} = useForm<ProjectFormValues>({
defaultValues,
mode: 'onTouched',
mode: 'onSubmit',
reValidateMode: 'onChange',
resolver: zodResolver(projectFormSchema),
});
@@ -139,7 +139,7 @@ export function useProjectForm({ onSaved }: { onSaved: () => void }) {
resetForm: () => reset(defaultValues),
projectId,
errors,
touchedFields,
isSubmitted,
canSubmit,
isSaving: isSubmitting || saveMutation.isPending,
};

View File

@@ -30,7 +30,7 @@ export default function ProjectPage() {
resetForm,
projectId,
errors,
touchedFields,
isSubmitted,
canSubmit,
isSaving,
} = projectForm;
@@ -107,7 +107,7 @@ export default function ProjectPage() {
projectId={projectId}
register={register}
errors={errors}
touchedFields={touchedFields}
isSubmitted={isSubmitted}
onSubmit={onSubmit}
canSubmit={canSubmit}
isSaving={isSaving}

View File

@@ -2,31 +2,13 @@
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 { Role } from '@/types';
interface UseRoleColumnsParams {
onEdit: (role: Role) => void;
onToggleStatus: (role: Role) => void;
isStatusPending: boolean;
}
export function useRoleColumns({
onEdit,
onToggleStatus,
isStatusPending,
}: UseRoleColumnsParams): ColumnDef<Role>[] {
const { hasPermission } = usePermissions();
const canEdit = hasPermission(PERMISSIONS.ROLE.UPDATE);
const canDelete = hasPermission(PERMISSIONS.ROLE.DELETE);
export function useRoleColumns(): ColumnDef<Role>[] {
return useMemo(() => {
const columns: ColumnDef<Role>[] = [
return [
{
accessorKey: 'name',
header: 'Name',
@@ -60,38 +42,5 @@ export function useRoleColumns({
),
},
];
if (!canEdit && !canDelete) return columns;
columns.push({
id: 'actions',
header: () => <div className="text-right">Actions</div>,
enableSorting: false,
cell: ({ row }) => {
const role = row.original;
return (
<div className="flex justify-end gap-2">
{canEdit ? (
<Button variant="outline" size="sm" onClick={() => onEdit(role)}>
<Edit className="size-4" />
</Button>
) : null}
{canDelete ? (
<Button
variant="outline"
size="sm"
disabled={isStatusPending}
onClick={() => onToggleStatus(role)}
>
<RotateCcw className="mr-2 size-4" />
{role.effective_status ? 'Deactivate' : 'Activate'}
</Button>
) : null}
</div>
);
},
});
return columns;
}, [canDelete, canEdit, isStatusPending, onEdit, onToggleStatus]);
}, []);
}

View File

@@ -1,7 +1,7 @@
'use client';
import type { ComponentProps } from 'react';
import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form';
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
import { Loader2 } from 'lucide-react';
import { FormField } from '@/components/form';
@@ -28,7 +28,7 @@ interface RoleSheetProps {
roleId?: number;
register: UseFormRegister<RoleFormValues>;
errors: FieldErrors<RoleFormValues>;
touchedFields: UseFormReturn<RoleFormValues>['formState']['touchedFields'];
isSubmitted: boolean;
onSubmit: ComponentProps<'form'>['onSubmit'];
permissionTree: PermissionTreeItem[];
permissionIds: number[];
@@ -44,7 +44,7 @@ export function RoleSheet({
roleId,
register,
errors,
touchedFields,
isSubmitted,
onSubmit,
permissionTree,
permissionIds,
@@ -53,10 +53,8 @@ export function RoleSheet({
canSubmit,
isSaving,
}: RoleSheetProps) {
const nameErrorMessage = touchedFields.name ? errors.name?.message : undefined;
const displayNameErrorMessage = touchedFields.display_name
? errors.display_name?.message
: undefined;
const nameErrorMessage = isSubmitted ? errors.name?.message : undefined;
const displayNameErrorMessage = isSubmitted ? errors.display_name?.message : undefined;
return (
<Sheet open={open} onOpenChange={onOpenChange}>
@@ -74,12 +72,7 @@ export function RoleSheet({
<form onSubmit={onSubmit} className="flex min-h-0 flex-1 flex-col">
<div className="flex-1 space-y-5 overflow-y-auto px-6 py-4">
<div className="grid gap-4 md:grid-cols-2">
<FormField
id="role-name"
label="Role Name"
required
error={nameErrorMessage}
>
<FormField id="role-name" label="Role Name" required error={nameErrorMessage}>
<Input
id="role-name"
placeholder="Enter role name"
@@ -114,7 +107,7 @@ export function RoleSheet({
<FormField
label="Permissions"
required
error={errors.permission_ids?.message}
error={isSubmitted ? errors.permission_ids?.message : undefined}
className="space-y-3"
labelEnd={
<span className="text-muted-foreground">{permissionIds.length} selected</span>

View File

@@ -2,8 +2,10 @@
import type { ColumnDef, SortingState } from '@tanstack/react-table';
import type { ReactNode } from 'react';
import { Edit, RotateCcw } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import { PERMISSIONS } from '@/constants/permissions';
import type { Role } from '@/types';
interface RoleTableProps {
@@ -18,6 +20,9 @@ interface RoleTableProps {
onPageChange: (skip: number) => void;
onLimitChange: (limit: number) => void;
onSortingChange: (sorting: SortingState) => void;
onEdit: (role: Role) => void;
onToggleStatus: (role: Role) => void;
isStatusPending: boolean;
}
export function RoleTable({
@@ -32,6 +37,9 @@ export function RoleTable({
onPageChange,
onLimitChange,
onSortingChange,
onEdit,
onToggleStatus,
isStatusPending,
}: RoleTableProps) {
return (
<DataTable
@@ -41,6 +49,21 @@ export function RoleTable({
isLoading={isLoading}
toolbar={toolbar}
emptyTitle="No roles found."
actions={[
{
label: 'Edit',
icon: <Edit className="size-4" />,
permission: PERMISSIONS.ROLE.UPDATE,
onClick: onEdit,
},
{
label: (role) => (role.effective_status ? 'Deactivate' : 'Activate'),
icon: <RotateCcw className="size-4" />,
permission: PERMISSIONS.ROLE.DELETE,
disabled: () => isStatusPending,
onClick: onToggleStatus,
},
]}
pagination={{
skip,
limit,

View File

@@ -48,10 +48,10 @@ export function useRoleForm({
handleSubmit,
reset,
setValue,
formState: { errors, isSubmitting, touchedFields },
formState: { errors, isSubmitting, isSubmitted },
} = useForm<RoleFormValues>({
defaultValues,
mode: 'onTouched',
mode: 'onSubmit',
reValidateMode: 'onChange',
resolver: zodResolver(roleFormSchema),
});
@@ -136,7 +136,7 @@ export function useRoleForm({
permissionIds,
roleId,
errors,
touchedFields,
isSubmitted,
canSubmit,
isSaving: isSubmitting || saveMutation.isPending,
};

View File

@@ -61,7 +61,7 @@ export default function RolesPage() {
permissionIds,
setPermissionIds,
errors,
touchedFields,
isSubmitted,
canSubmit,
isSaving,
} = roleForm;
@@ -88,11 +88,7 @@ export default function RolesPage() {
[updateRoleStatus],
);
const columns = useRoleColumns({
onEdit: openEdit,
onToggleStatus: toggleStatus,
isStatusPending,
});
const columns = useRoleColumns();
const total = rolesQuery.data?.total ?? 0;
const roles = rolesQuery.data?.items ?? [];
@@ -134,6 +130,9 @@ export default function RolesPage() {
onPageChange={setSkip}
onLimitChange={setLimit}
onSortingChange={setSorting}
onEdit={openEdit}
onToggleStatus={toggleStatus}
isStatusPending={isStatusPending}
/>
</main>
@@ -143,7 +142,7 @@ export default function RolesPage() {
roleId={roleId}
register={register}
errors={errors}
touchedFields={touchedFields}
isSubmitted={isSubmitted}
onSubmit={onSubmit}
permissionTree={permissionsQuery.permissionTree}
permissionIds={permissionIds}

View File

@@ -1,7 +1,7 @@
'use client';
import type { ComponentProps } from 'react';
import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form';
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
import { Loader2, MapPin, Milestone } from 'lucide-react';
import { FormField } from '@/components/form';
@@ -42,7 +42,7 @@ interface SegmentDialogProps {
onDirectionChange: (direction: 'UP' | 'DOWN') => void;
register: UseFormRegister<SegmentFormValues>;
errors: FieldErrors<SegmentFormValues>;
touchedFields: UseFormReturn<SegmentFormValues>['formState']['touchedFields'];
isSubmitted: boolean;
onSubmit: ComponentProps<'form'>['onSubmit'];
canSubmit: boolean;
isSaving: boolean;
@@ -64,13 +64,13 @@ export function SegmentDialog({
onDirectionChange,
register,
errors,
touchedFields,
isSubmitted,
onSubmit,
canSubmit,
isSaving,
}: SegmentDialogProps) {
const getError = (field: keyof SegmentFormValues) =>
touchedFields[field] ? errors[field]?.message : undefined;
isSubmitted ? errors[field]?.message : undefined;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -131,7 +131,9 @@ export function SegmentDialog({
Loading...
</span>
) : (
<SelectValue placeholder={projectId ? 'Choose a package' : 'Select project first'} />
<SelectValue
placeholder={projectId ? 'Choose a package' : 'Select project first'}
/>
)}
</SelectTrigger>
<SelectContent>
@@ -278,7 +280,12 @@ export function SegmentDialog({
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isSaving}>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSaving}
>
Cancel
</Button>
<Button type="submit" disabled={isSaving || !canSubmit}>

View File

@@ -1,6 +1,7 @@
'use client';
import type { ColumnDef } from '@tanstack/react-table';
import { Edit3, Trash2 } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import type { Chainage } from '@/types';
@@ -35,8 +36,19 @@ export function SegmentTable({
title="Segments"
data={segments}
columns={columns}
onEdit={onEdit}
onDelete={onDelete}
actions={[
{
label: 'Edit',
icon: <Edit3 className="size-4" />,
onClick: onEdit,
},
{
label: 'Delete',
icon: <Trash2 className="size-4" />,
className: 'text-destructive',
onClick: onDelete,
},
]}
isLoading={isLoading}
emptyTitle="No segments found."
pagination={{

View File

@@ -86,10 +86,10 @@ export function useSegmentForm({ onSaved }: { onSaved: () => void }) {
reset,
setValue,
control,
formState: { errors, isSubmitting, touchedFields },
formState: { errors, isSubmitting, isSubmitted },
} = useForm<SegmentFormValues>({
defaultValues,
mode: 'onTouched',
mode: 'onSubmit',
reValidateMode: 'onChange',
resolver: zodResolver(segmentFormSchema),
});
@@ -200,7 +200,7 @@ export function useSegmentForm({ onSaved }: { onSaved: () => void }) {
direction,
setDirection,
errors,
touchedFields,
isSubmitted,
canSubmit,
isSaving: isSubmitting || saveMutation.isPending,
};

View File

@@ -44,7 +44,7 @@ export default function SegmentPage() {
direction,
setDirection,
errors,
touchedFields,
isSubmitted,
canSubmit,
isSaving,
} = segmentForm;
@@ -136,7 +136,7 @@ export default function SegmentPage() {
onDirectionChange={setDirection}
register={register}
errors={errors}
touchedFields={touchedFields}
isSubmitted={isSubmitted}
onSubmit={onSubmit}
canSubmit={canSubmit}
isSaving={isSaving}

View File

@@ -2,12 +2,8 @@
import { useMemo } from 'react';
import type { ColumnDef } from '@tanstack/react-table';
import { Edit, Trash2 } 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 { Tenant } from '@/types';
function formatDate(value?: string | null) {
@@ -19,23 +15,9 @@ function formatDate(value?: string | null) {
}).format(new Date(value));
}
interface UseTenantColumnsParams {
onEdit: (tenant: Tenant) => void;
onDelete: (tenant: Tenant) => void;
pendingDeleteId?: number;
}
export function useTenantColumns({
onEdit,
onDelete,
pendingDeleteId,
}: UseTenantColumnsParams): ColumnDef<Tenant>[] {
const { hasPermission } = usePermissions();
const canEdit = hasPermission(PERMISSIONS.TENANT.UPDATE);
const canDelete = hasPermission(PERMISSIONS.TENANT.DELETE);
export function useTenantColumns(): ColumnDef<Tenant>[] {
return useMemo(() => {
const columns: ColumnDef<Tenant>[] = [
return [
{
accessorKey: 'name',
header: 'Tenant',
@@ -76,37 +58,5 @@ export function useTenantColumns({
),
},
];
if (!canEdit && !canDelete) return columns;
columns.push({
id: 'actions',
header: () => <div className="text-right">Actions</div>,
enableSorting: false,
cell: ({ row }) => {
const tenant = row.original;
return (
<div className="flex justify-end gap-2">
{canEdit ? (
<Button variant="outline" size="sm" onClick={() => onEdit(tenant)}>
<Edit className="size-4" />
</Button>
) : null}
{canDelete ? (
<Button
variant="outline"
size="sm"
disabled={pendingDeleteId === tenant.id}
onClick={() => onDelete(tenant)}
>
<Trash2 className="size-4" />
</Button>
) : null}
</div>
);
},
});
return columns;
}, [canDelete, canEdit, onDelete, onEdit, pendingDeleteId]);
}, []);
}

View File

@@ -108,7 +108,9 @@ export function TenantSheet({
<Label>Client</Label>
<Select value={clientId} onValueChange={onClientChange} disabled={isLookupsLoading}>
<SelectTrigger>
<SelectValue placeholder={isLookupsLoading ? 'Loading clients...' : 'Select client'} />
<SelectValue
placeholder={isLookupsLoading ? 'Loading clients...' : 'Select client'}
/>
</SelectTrigger>
<SelectContent>
{clients.map((client) => (
@@ -123,7 +125,9 @@ export function TenantSheet({
<Label>Subscription Plan</Label>
<Select value={planId} onValueChange={onPlanChange} disabled={isLookupsLoading}>
<SelectTrigger>
<SelectValue placeholder={isLookupsLoading ? 'Loading plans...' : 'Select plan'} />
<SelectValue
placeholder={isLookupsLoading ? 'Loading plans...' : 'Select plan'}
/>
</SelectTrigger>
<SelectContent>
{plans.map((plan) => (
@@ -147,7 +151,7 @@ export function TenantSheet({
id="tenant-description"
placeholder="North zone operations"
{...register('description')}
className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 outline-none focus-visible:border-ring"
/>
</div>
</div>

View File

@@ -2,8 +2,10 @@
import type { ReactNode } from 'react';
import type { ColumnDef, SortingState } from '@tanstack/react-table';
import { Edit, Trash2 } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import { PERMISSIONS } from '@/constants/permissions';
import type { Tenant } from '@/types';
interface TenantTableProps {
@@ -18,6 +20,9 @@ interface TenantTableProps {
onPageChange: (skip: number) => void;
onLimitChange: (limit: number) => void;
onSortingChange: (sorting: SortingState) => void;
onEdit: (tenant: Tenant) => void;
onDelete: (tenant: Tenant) => void;
pendingDeleteId?: number;
}
export function TenantTable({
@@ -32,6 +37,9 @@ export function TenantTable({
onPageChange,
onLimitChange,
onSortingChange,
onEdit,
onDelete,
pendingDeleteId,
}: TenantTableProps) {
return (
<DataTable
@@ -41,6 +49,22 @@ export function TenantTable({
isLoading={isLoading}
toolbar={toolbar}
emptyTitle="No tenants found."
actions={[
{
label: 'Edit',
icon: <Edit className="size-4" />,
permission: PERMISSIONS.TENANT.UPDATE,
onClick: onEdit,
},
{
label: 'Delete',
icon: <Trash2 className="size-4" />,
permission: PERMISSIONS.TENANT.DELETE,
className: 'text-destructive',
disabled: (tenant) => pendingDeleteId === tenant.id,
onClick: onDelete,
},
]}
pagination={{
skip,
limit,

View File

@@ -116,11 +116,7 @@ export default function TenantsPage() {
[deleteMutation],
);
const columns = useTenantColumns({
onEdit: openEdit,
onDelete: handleDelete,
pendingDeleteId: deleteMutation.isPending ? deleteMutation.variables : undefined,
});
const columns = useTenantColumns();
const tenants = tenantsQuery.data?.items ?? [];
const total = tenantsQuery.data?.total ?? 0;
@@ -169,6 +165,9 @@ export default function TenantsPage() {
onPageChange={setSkip}
onLimitChange={setLimit}
onSortingChange={setSorting}
onEdit={openEdit}
onDelete={handleDelete}
pendingDeleteId={deleteMutation.isPending ? deleteMutation.variables : undefined}
/>
</main>

View File

@@ -2,12 +2,8 @@
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 { AdministrationUser } from '@/types';
function formatDate(value?: string | null) {
@@ -19,23 +15,9 @@ function formatDate(value?: string | null) {
}).format(new Date(value));
}
interface UseUserColumnsParams {
onEdit: (user: AdministrationUser) => void;
onToggleStatus: (user: AdministrationUser) => void;
pendingUserId?: number;
}
export function useUserColumns({
onEdit,
onToggleStatus,
pendingUserId,
}: UseUserColumnsParams): ColumnDef<AdministrationUser>[] {
const { hasPermission } = usePermissions();
const canEdit = hasPermission(PERMISSIONS.USER.UPDATE);
const canDelete = hasPermission(PERMISSIONS.USER.DELETE);
export function useUserColumns(): ColumnDef<AdministrationUser>[] {
return useMemo(() => {
const columns: ColumnDef<AdministrationUser>[] = [
return [
{
accessorKey: 'first_name',
header: 'Name',
@@ -88,38 +70,5 @@ export function useUserColumns({
),
},
];
if (!canEdit && !canDelete) return columns;
columns.push({
id: 'actions',
header: () => <div className="text-right">Actions</div>,
enableSorting: false,
cell: ({ row }) => {
const user = row.original;
return (
<div className="flex justify-end gap-2">
{canEdit ? (
<Button variant="outline" size="sm" onClick={() => onEdit(user)}>
<Edit className="size-4" />
</Button>
) : null}
{canDelete ? (
<Button
variant="outline"
size="sm"
disabled={pendingUserId === user.id || user.effective_status === 'pending'}
onClick={() => onToggleStatus(user)}
>
<RotateCcw className="mr-2 size-4" />
{user.effective_status === 'active' ? 'Deactivate' : 'Activate'}
</Button>
) : null}
</div>
);
},
});
return columns;
}, [canDelete, canEdit, onEdit, onToggleStatus, pendingUserId]);
}, []);
}

View File

@@ -1,7 +1,7 @@
'use client';
import { useRef, type ComponentProps } from 'react';
import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form';
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
import { Loader2 } from 'lucide-react';
import { FormField } from '@/components/form';
@@ -24,7 +24,7 @@ interface UserSheetProps {
userId?: number;
register: UseFormRegister<UserFormValues>;
errors: FieldErrors<UserFormValues>;
touchedFields: UseFormReturn<UserFormValues>['formState']['touchedFields'];
isSubmitted: boolean;
onSubmit: ComponentProps<'form'>['onSubmit'];
roleId: string;
onRoleChange: (roleId: string) => void;
@@ -38,7 +38,7 @@ export function UserSheet({
userId,
register,
errors,
touchedFields,
isSubmitted,
onSubmit,
roleId,
onRoleChange,
@@ -46,11 +46,11 @@ export function UserSheet({
isSaving,
}: UserSheetProps) {
const roleComboboxPortalRef = useRef<HTMLDivElement | null>(null);
const firstNameErrorMessage = touchedFields.first_name ? errors.first_name?.message : undefined;
const lastNameErrorMessage = touchedFields.last_name ? errors.last_name?.message : undefined;
const emailErrorMessage = touchedFields.email ? errors.email?.message : undefined;
const phoneErrorMessage = touchedFields.phone_number ? errors.phone_number?.message : undefined;
const roleErrorMessage = touchedFields.role_id ? errors.role_id?.message : undefined;
const firstNameErrorMessage = isSubmitted ? errors.first_name?.message : undefined;
const lastNameErrorMessage = isSubmitted ? errors.last_name?.message : undefined;
const emailErrorMessage = isSubmitted ? errors.email?.message : undefined;
const phoneErrorMessage = isSubmitted ? errors.phone_number?.message : undefined;
const roleErrorMessage = isSubmitted ? errors.role_id?.message : undefined;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -63,12 +63,7 @@ export function UserSheet({
<form onSubmit={onSubmit} className="flex flex-1 flex-col gap-5 px-4">
<div className="grid gap-4 md:grid-cols-2">
<FormField
id="first-name"
label="First Name"
required
error={firstNameErrorMessage}
>
<FormField id="first-name" label="First Name" required error={firstNameErrorMessage}>
<Input
id="first-name"
placeholder="Enter first name"
@@ -77,12 +72,7 @@ export function UserSheet({
/>
</FormField>
<FormField
id="last-name"
label="Last Name"
required
error={lastNameErrorMessage}
>
<FormField id="last-name" label="Last Name" required error={lastNameErrorMessage}>
<Input
id="last-name"
placeholder="Enter last name"
@@ -92,12 +82,7 @@ export function UserSheet({
</FormField>
</div>
<FormField
id="email"
label="Email"
required
error={emailErrorMessage}
>
<FormField id="email" label="Email" required error={emailErrorMessage}>
<Input
id="email"
placeholder="name@example.com"
@@ -106,11 +91,7 @@ export function UserSheet({
/>
</FormField>
<FormField
id="phone-number"
label="Phone Number"
error={phoneErrorMessage}
>
<FormField id="phone-number" label="Phone Number" error={phoneErrorMessage}>
<Input
id="phone-number"
placeholder="+919876543210"
@@ -130,12 +111,7 @@ export function UserSheet({
<div ref={roleComboboxPortalRef} />
<input
type="hidden"
{...register('role_id')}
value={roleId}
readOnly
/>
<input type="hidden" {...register('role_id')} value={roleId} readOnly />
</FormField>
<DialogFooter className="px-0">

View File

@@ -2,8 +2,10 @@
import type { ReactNode } from 'react';
import type { ColumnDef, SortingState } from '@tanstack/react-table';
import { Edit, RotateCcw } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import { PERMISSIONS } from '@/constants/permissions';
import type { AdministrationUser } from '@/types';
interface UserTableProps {
@@ -18,6 +20,9 @@ interface UserTableProps {
onPageChange: (skip: number) => void;
onLimitChange: (limit: number) => void;
onSortingChange: (sorting: SortingState) => void;
onEdit: (user: AdministrationUser) => void;
onToggleStatus: (user: AdministrationUser) => void;
pendingUserId?: number;
}
export function UserTable({
@@ -32,6 +37,9 @@ export function UserTable({
onPageChange,
onLimitChange,
onSortingChange,
onEdit,
onToggleStatus,
pendingUserId,
}: UserTableProps) {
return (
<DataTable
@@ -41,6 +49,21 @@ export function UserTable({
isLoading={isLoading}
toolbar={toolbar}
emptyTitle="No users found."
actions={[
{
label: 'Edit',
icon: <Edit className="size-4" />,
permission: PERMISSIONS.USER.UPDATE,
onClick: onEdit,
},
{
label: (user) => (user.effective_status === 'active' ? 'Deactivate' : 'Activate'),
icon: <RotateCcw className="size-4" />,
permission: PERMISSIONS.USER.DELETE,
disabled: (user) => pendingUserId === user.id || user.effective_status === 'pending',
onClick: onToggleStatus,
},
]}
pagination={{
skip,
limit,

View File

@@ -42,10 +42,10 @@ export function useUserForm({ onSaved }: { onSaved: () => void }) {
handleSubmit: submitForm,
reset,
setValue,
formState: { errors, isSubmitting, touchedFields },
formState: { errors, isSubmitting, isSubmitted },
} = useForm<UserFormValues>({
defaultValues,
mode: 'onTouched',
mode: 'onSubmit',
reValidateMode: 'onChange',
resolver: zodResolver(userFormSchema),
});
@@ -62,7 +62,8 @@ export function useUserForm({ onSaved }: { onSaved: () => void }) {
const lastName = useWatch({ control, name: 'last_name' }) || '';
const email = useWatch({ control, name: 'email' }) || '';
const phoneNumber = useWatch({ control, name: 'phone_number' }) || '';
const isPhoneValid = phoneNumber.trim() === '' || /^\+(?:[0-9] ?){6,14}[0-9]$/.test(phoneNumber.trim());
const isPhoneValid =
phoneNumber.trim() === '' || /^\+(?:[0-9] ?){6,14}[0-9]$/.test(phoneNumber.trim());
const canSubmit =
firstName.trim().length > 0 &&
lastName.trim().length > 0 &&
@@ -131,7 +132,7 @@ export function useUserForm({ onSaved }: { onSaved: () => void }) {
roleId,
userId,
errors,
touchedFields,
isSubmitted,
canSubmit,
isSaving: isSubmitting || saveMutation.isPending,
};

View File

@@ -54,7 +54,7 @@ export default function UsersPage() {
roleId,
setRoleId,
errors,
touchedFields,
isSubmitted,
canSubmit,
isSaving,
} = userForm;
@@ -81,11 +81,7 @@ export default function UsersPage() {
[updateUserStatus],
);
const columns = useUserColumns({
onEdit: openEdit,
onToggleStatus: toggleStatus,
pendingUserId,
});
const columns = useUserColumns();
const total = usersQuery.data?.total ?? 0;
const users = usersQuery.data?.items ?? [];
@@ -137,6 +133,9 @@ export default function UsersPage() {
onPageChange={setSkip}
onLimitChange={setLimit}
onSortingChange={setSorting}
onEdit={openEdit}
onToggleStatus={toggleStatus}
pendingUserId={pendingUserId}
/>
</main>
@@ -146,7 +145,7 @@ export default function UsersPage() {
userId={userId}
register={register}
errors={errors}
touchedFields={touchedFields}
isSubmitted={isSubmitted}
onSubmit={handleSubmit}
roleId={roleId}
onRoleChange={setRoleId}