style: do inputs design modification
This commit is contained in:
@@ -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]);
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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]);
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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]);
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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]);
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user