feat: add client management module
This commit is contained in:
36
src/app/(modules)/clients/hooks/useClientFilters.ts
Normal file
36
src/app/(modules)/clients/hooks/useClientFilters.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useDebounce } from '@/hooks/useDebounce';
|
||||
|
||||
export type ClientStatusFilter = 'all' | 'active' | 'inactive';
|
||||
|
||||
export function useClientFilters() {
|
||||
const [skip, setSkip] = useState(0);
|
||||
const [limit] = useState(10);
|
||||
const [searchTerm, setSearchTermValue] = useState('');
|
||||
const [statusFilter, setStatusFilterValue] = useState<ClientStatusFilter>('all');
|
||||
const debouncedSearchTerm = useDebounce(searchTerm.trim(), 400);
|
||||
|
||||
const setSearchTerm = useCallback((value: string) => {
|
||||
setSearchTermValue(value);
|
||||
setSkip(0);
|
||||
}, []);
|
||||
|
||||
const setStatusFilter = useCallback((value: ClientStatusFilter) => {
|
||||
setStatusFilterValue(value);
|
||||
setSkip(0);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
skip,
|
||||
setSkip,
|
||||
limit,
|
||||
searchTerm,
|
||||
debouncedSearchTerm,
|
||||
setSearchTerm,
|
||||
statusFilter,
|
||||
setStatusFilter,
|
||||
};
|
||||
}
|
||||
109
src/app/(modules)/clients/hooks/useClientForm.ts
Normal file
109
src/app/(modules)/clients/hooks/useClientForm.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { Client } from '@/types';
|
||||
import { useSaveClientMutation } from './useClientMutations';
|
||||
|
||||
export interface ClientFormValues {
|
||||
id?: number;
|
||||
name: string;
|
||||
email: string;
|
||||
landline_number: string;
|
||||
address: string;
|
||||
gst: string;
|
||||
pan: string;
|
||||
tan: string;
|
||||
contact_name: string;
|
||||
contact_phone_number: string;
|
||||
contact_email: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
const defaultValues: ClientFormValues = {
|
||||
name: '',
|
||||
email: '',
|
||||
landline_number: '',
|
||||
address: '',
|
||||
gst: '',
|
||||
pan: '',
|
||||
tan: '',
|
||||
contact_name: '',
|
||||
contact_phone_number: '',
|
||||
contact_email: '',
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
export function useClientForm({ onSaved }: { onSaved: () => void }) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit: submitForm,
|
||||
reset,
|
||||
control,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm<ClientFormValues>({
|
||||
defaultValues,
|
||||
});
|
||||
const saveMutation = useSaveClientMutation({
|
||||
onSaved: () => {
|
||||
reset(defaultValues);
|
||||
onSaved();
|
||||
},
|
||||
});
|
||||
|
||||
const clientId = useWatch({ control, name: 'id' });
|
||||
|
||||
const handleSubmit = submitForm(
|
||||
(values) => saveMutation.mutate(values),
|
||||
(formErrors) => {
|
||||
if (
|
||||
formErrors.name ||
|
||||
formErrors.email ||
|
||||
formErrors.landline_number ||
|
||||
formErrors.address ||
|
||||
formErrors.contact_name ||
|
||||
formErrors.contact_phone_number ||
|
||||
formErrors.contact_email
|
||||
) {
|
||||
toast.error('Complete all required client fields');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const openCreate = useCallback(() => {
|
||||
reset(defaultValues);
|
||||
}, [reset]);
|
||||
|
||||
const openEdit = useCallback(
|
||||
(client: Client) => {
|
||||
reset({
|
||||
id: client.id,
|
||||
name: client.name || '',
|
||||
email: client.email || '',
|
||||
landline_number: client.landline_number || '',
|
||||
address: client.address || '',
|
||||
gst: client.gst || '',
|
||||
pan: client.pan || '',
|
||||
tan: client.tan || '',
|
||||
contact_name: client.contact_name || '',
|
||||
contact_phone_number: client.contact_phone_number || '',
|
||||
contact_email: client.contact_email || '',
|
||||
is_active: client.is_active,
|
||||
});
|
||||
},
|
||||
[reset],
|
||||
);
|
||||
|
||||
return {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
openCreate,
|
||||
openEdit,
|
||||
clientId,
|
||||
errors,
|
||||
isSaving: isSubmitting || saveMutation.isPending,
|
||||
};
|
||||
}
|
||||
61
src/app/(modules)/clients/hooks/useClientMutations.ts
Normal file
61
src/app/(modules)/clients/hooks/useClientMutations.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { clientService } from '@/services/api';
|
||||
import type { Client } from '@/types';
|
||||
import { clientKeys } from '../queries/clientKeys';
|
||||
import type { ClientFormValues } from './useClientForm';
|
||||
|
||||
function optionalValue(value: string) {
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
export function useSaveClientMutation({ onSaved }: { onSaved: () => void }) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (values: ClientFormValues) =>
|
||||
clientService.saveClient({
|
||||
id: values.id,
|
||||
name: values.name.trim(),
|
||||
email: values.email.trim(),
|
||||
landline_number: values.landline_number.trim(),
|
||||
address: values.address.trim(),
|
||||
gst: optionalValue(values.gst),
|
||||
pan: optionalValue(values.pan),
|
||||
tan: optionalValue(values.tan),
|
||||
contact_name: values.contact_name.trim(),
|
||||
contact_phone_number: values.contact_phone_number.trim(),
|
||||
contact_email: values.contact_email.trim(),
|
||||
is_active: values.is_active,
|
||||
}),
|
||||
onSuccess: (_data, values) => {
|
||||
toast.success(values.id ? 'Client updated' : 'Client created');
|
||||
onSaved();
|
||||
queryClient.invalidateQueries({ queryKey: clientKeys.all });
|
||||
},
|
||||
onError: (_error, values) => {
|
||||
toast.error(values.id ? 'Failed to update client' : 'Failed to create client');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useClientStatusMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (client: Client) => clientService.updateClientStatus(client.id, !client.is_active),
|
||||
onSuccess: () => {
|
||||
toast.success('Client status updated');
|
||||
queryClient.invalidateQueries({ queryKey: clientKeys.all });
|
||||
},
|
||||
onError: () => toast.error('Failed to update client status'),
|
||||
});
|
||||
|
||||
return {
|
||||
...mutation,
|
||||
pendingClientId: mutation.isPending ? mutation.variables?.id : undefined,
|
||||
};
|
||||
}
|
||||
45
src/app/(modules)/clients/hooks/useClientQueries.ts
Normal file
45
src/app/(modules)/clients/hooks/useClientQueries.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { clientService } from '@/services/api';
|
||||
import type { ClientListParams } from '@/types';
|
||||
import { clientKeys } from '../queries/clientKeys';
|
||||
import type { ClientStatusFilter } from './useClientFilters';
|
||||
|
||||
interface UseClientsQueryParams {
|
||||
skip: number;
|
||||
limit: number;
|
||||
searchTerm: string;
|
||||
statusFilter: ClientStatusFilter;
|
||||
}
|
||||
|
||||
function buildClientListParams({
|
||||
skip,
|
||||
limit,
|
||||
searchTerm,
|
||||
statusFilter,
|
||||
}: UseClientsQueryParams): ClientListParams {
|
||||
return {
|
||||
skip,
|
||||
limit,
|
||||
search_term: searchTerm || undefined,
|
||||
is_active: statusFilter === 'all' ? undefined : statusFilter === 'active',
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc',
|
||||
};
|
||||
}
|
||||
|
||||
export function useClientsQuery(params: UseClientsQueryParams) {
|
||||
const { skip, limit, searchTerm, statusFilter } = params;
|
||||
const listParams = useMemo(
|
||||
() => buildClientListParams({ skip, limit, searchTerm, statusFilter }),
|
||||
[limit, searchTerm, skip, statusFilter],
|
||||
);
|
||||
|
||||
return useQuery({
|
||||
queryKey: clientKeys.list(listParams),
|
||||
queryFn: () => clientService.getClients(listParams),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user