fix alert empty validation on manage user

This commit is contained in:
Wikzyy
2025-05-09 15:09:36 +07:00
parent 94c6b7aea1
commit c4fbb1c196
4 changed files with 681 additions and 522 deletions

View File

@ -34,31 +34,12 @@ import { toast } from 'sonner';
import { useCallApi } from '@/hooks'; import { useCallApi } from '@/hooks';
import { doSaveLogActivity } from '@/actions/GlobalActions'; import { doSaveLogActivity } from '@/actions/GlobalActions';
import clsx from 'clsx'; import clsx from 'clsx';
import {
export interface CustomerProps { CustomerProps,
id: string; initialStateCreateUser,
msisdn: string; RoleListProps,
email: string; validateFormCreateUser
fullname: string; } from './Types';
username: string;
}
interface RoleListProps {
id: string;
name: string;
roles: string;
status: string;
}
interface CreateUserParams {
email: string;
username: string;
password: string;
retype_password: string;
name: string;
id_role: string;
status: string;
}
const API_URL_CUSTOMER = apiConfig.service_customer; const API_URL_CUSTOMER = apiConfig.service_customer;
const API_URL = apiConfig.service_dashboard; const API_URL = apiConfig.service_dashboard;
@ -71,34 +52,30 @@ const AddDialog = () => {
const { reload } = useDataGrid(); const { reload } = useDataGrid();
const { PostData, GetData } = useCallApi(); const { PostData, GetData } = useCallApi();
const [roles, setRoles] = useState<RoleListProps[]>([]); const [roles, setRoles] = useState<RoleListProps[]>([]);
const [alert, setAlert] = useState({ const [errors, setErrors] = useState<Record<string, string>>({});
show: false,
message: '' const [formField, setFormField] = useState(initialStateCreateUser);
});
const initialState = {
email: '',
username: '',
password: '',
retype_password: '',
name: '',
id_role: '',
status: '',
customerid: ''
};
const [formField, setFormField] = useState(initialState);
const [showPassword, setShowPassword] = useState({ const [showPassword, setShowPassword] = useState({
password: false, password: false,
retype_password: false retype_password: false
}); });
const [searchTerm, setSearchTerm] = useState('');
const [selectedCustomerName, setSelectedCustomerName] = useState('');
const [dropdownOpen, setDropdownOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const [messagePassword, setMessagePassword] = useState(true); const [messagePassword, setMessagePassword] = useState(true);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [customers, setCustomers] = useState<CustomerProps[]>([]); const [customers, setCustomers] = useState<CustomerProps[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [passwordErrors, setPasswordErrors] = useState<string[]>([]); const [passwordErrors, setPasswordErrors] = useState<string[]>([]);
const [notMatch, setNotMatch] = useState<string[]>([]);
const validatePassword = (password: string, confirmPassword: string) => { const validatePassword = (password: string, confirmPassword: string) => {
const errors: string[] = []; const errors: string[] = [];
const notMatch: string[] = [];
if (password) { if (password) {
if (password.length < 8) { if (password.length < 8) {
@ -114,25 +91,29 @@ const AddDialog = () => {
errors.push('Password must contain at least one special character'); errors.push('Password must contain at least one special character');
} }
if (password !== confirmPassword) { if (password !== confirmPassword) {
errors.push('Passwords do not match'); notMatch.push('Passwords do not match');
} }
} }
return { return {
isValid: errors.length === 0, isValid: errors.length === 0,
errors errors,
notMatch
}; };
}; };
const resetForm = () => { const resetForm = () => {
setFormField(initialState); setFormField(initialStateCreateUser);
setAlert({ show: false, message: '' }); setSelectedCustomerName('');
setSearchTerm('');
setErrors({});
}; };
/* actions */ /* actions */
const doCreateUser = useCallback( const doCreateUser = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => { async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true);
try { try {
const response = await PostData(`${API_URL}/user/create`, formField); const response = await PostData(`${API_URL}/user/create`, formField);
@ -150,8 +131,7 @@ const AddDialog = () => {
doSaveLogActivity(createActivity); doSaveLogActivity(createActivity);
toast.success('Success Create User'); toast.success('Success Create User');
} else { } else {
toast.error('Failed to create user'); toast.error(response?.message);
setAlert({ show: true, message: 'Failed to create user. Please try again.' });
} }
} catch (err) { } catch (err) {
toast.error('Something went wrong'); toast.error('Something went wrong');
@ -175,80 +155,112 @@ const AddDialog = () => {
}) })
}; };
const response = await GetData(`${API_URL}/user_role/list`, params); const response = await GetData(`${API_URL}/user_role/list`, params);
// console.log('ini data:', response);
if (response?.status) { if (response?.status) {
const roleList = response.data?.list || []; const roleList = response.data?.list || [];
setRoles(roleList); setRoles(roleList);
} else { } else {
setRoles(() => []); setRoles(() => []);
} }
// console.log('ini data user_role:', response?.data);
}, []); }, []);
const getCustomerList = async (sorting: any) => { const getCustomerList = async (sorting: any, filterValue: string) => {
try { const filter: any =
sorting = sorting.length == 0 ? [{ id: 'name', desc: false }] : sorting; filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } };
const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, {
limit: 100,
page: 1,
with_deleted: false,
order_field: sorting[0].id,
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC'
});
setCustomers(response?.data.list); sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting;
const query: any = {
limit: 5,
page: 1,
with_deleted: false,
order_field: sorting[0].id,
order_direction: sorting[0].desc ? 'DESC' : 'ASC'
};
if (filter && Object.keys(filter).length > 0) {
query.filter = JSON.stringify(filter);
}
try {
const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query);
if (response?.status) {
setCustomers(response?.data.list);
} else {
toast.error(response?.message);
}
} catch (error) { } catch (error) {
console.error('Error fetching customer', error); toast.error('Failed to fetch customer list');
} finally {
setIsLoading(false);
} }
}; };
const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsLoading(true);
setSearchTerm(e.target.value);
setSelectedCustomerName(e.target.value);
setDropdownOpen(true);
const timer = setTimeout(() => {
getCustomerList([{ id: 'fullname', desc: false }], e.target.value);
}, 500);
return () => clearTimeout(timer);
};
const handleCustomerSelect = (customer: CustomerProps) => {
setFormField({ ...formField, customerid: customer.id });
setSelectedCustomerName(customer.fullname);
setDropdownOpen(false);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (isSubmitting) return;
if (!validateFormCreateUser(formField, setErrors, 'create')) {
return;
}
doCreateUser(e);
};
const isButtonDisabled = !messagePassword || isSubmitting || passwordErrors.length > 0;
const filteredCustomer = customers
.filter((item) => item.fullname.toLowerCase().includes(searchTerm.toLowerCase()))
.slice(0, 10);
useEffect(() => { useEffect(() => {
fetchRoles(); fetchRoles();
}, [fetchRoles]); }, [fetchRoles]);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// console.log('Form data before submit:', formField);
if (isSubmitting) return;
if (
formField.email.trim() === '' ||
formField.username.trim() === '' ||
formField.password.trim() === '' ||
formField.retype_password.trim() === '' ||
formField.name.trim() === '' ||
formField.id_role.trim() === '' ||
formField.status.trim() === '' ||
formField.customerid.trim() === ''
) {
setAlert({ show: true, message: 'Please fill name field.' });
return;
}
setIsSubmitting(true);
doCreateUser(e);
// console.log(formField);
setAlert({ show: false, message: '' });
};
useEffect(() => { useEffect(() => {
const validation = validatePassword(formField.password, formField.retype_password); const validation = validatePassword(formField.password, formField.retype_password);
setMessagePassword(validation.isValid); setMessagePassword(validation.isValid);
setPasswordErrors(validation.errors); setPasswordErrors(validation.errors);
setNotMatch(validation.notMatch);
}, [formField.password, formField.retype_password]); }, [formField.password, formField.retype_password]);
const isButtonDisabled = !messagePassword || isSubmitting || passwordErrors.length > 0;
useEffect(() => { useEffect(() => {
if (showAddDialog === false) { if (showAddDialog === false) {
resetForm(); resetForm();
setDropdownOpen(false);
} }
}, [showAddDialog]); }, [showAddDialog]);
useEffect(() => { useEffect(() => {
getCustomerList([{ id: 'id', desc: false }]); getCustomerList([{ id: 'created_at', desc: false }], '');
const handleClickOutside = (event: any) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []); }, []);
const togglePassword = useCallback((event: MouseEvent<HTMLButtonElement>, key: string) => { const togglePassword = useCallback((event: MouseEvent<HTMLButtonElement>, key: string) => {
@ -258,14 +270,13 @@ const AddDialog = () => {
return ( return (
<Dialog open={showAddDialog} onOpenChange={(open) => handleAddDialog(open)}> <Dialog open={showAddDialog} onOpenChange={(open) => handleAddDialog(open)}>
<DialogContent className="container-fixed max-w-[768px] flex flex-col p-5 overflow-hidden [&>button]:hidden"> <DialogContent className="container-fixed max-w-[768px] flex flex-col p-10 overflow-hidden [&>button]:hidden">
<DialogHeader className="p-0 border-0"> <DialogHeader className="p-0 border-0">
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
<DialogDescription></DialogDescription> <DialogDescription></DialogDescription>
<div className="flex items-center justify-between flex-wrap grow"> <div className="flex items-center justify-between flex-wrap grow">
<div className="flex flex-col justify-center"> <div className="flex flex-col justify-center">
<h1 className="text-xl font-semibold leading-none text-gray-900">User - Create</h1> <h1 className="text-xl font-semibold leading-none text-gray-900">User - Create</h1>
<div className="flex items-center gap-2 text-sm font-normal text-gray-700"></div>
</div> </div>
<div <div
className="cursor-pointer hover:opacity-100 opacity-50" className="cursor-pointer hover:opacity-100 opacity-50"
@ -278,249 +289,255 @@ const AddDialog = () => {
</div> </div>
</div> </div>
</DialogHeader> </DialogHeader>
<DialogBody className="scrollable-y px-0 pb-0" ref={parentRef}>
<div className="flex flex-col px-0">
{alert.show && (
<Alert variant="danger" className="mb-3">
<h3>{alert.message}</h3>
</Alert>
)}
<form action="" onSubmit={handleSubmit}>
<div className="card-body grid gap-5 p-0">
<div className="w-full">
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Name</label>
<Input
className="input"
type="text"
autoComplete="off"
value={formField.name}
onChange={({ target }) =>
setFormField((prev) => ({ ...prev, name: target.value }))
}
/>
</div>
</div>
<div className="w-full"> <DialogBody className="scrollable-y p-5 pb-0" ref={parentRef}>
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5"> <form onSubmit={handleSubmit} className="grid gap-5">
<label className="form-label flex items-center gap-1 max-w-56">Username</label> {/* Name */}
<Input <div className="flex gap-2.5">
className="input" <label className="form-label flex items-center gap-1 max-w-56">Name</label>
type="text" <div className="grow flex flex-col">
autoComplete="off" <Input
value={formField.username} className={`input ${errors.name ? 'border-red-500' : ''}`}
onChange={({ target }) => type="text"
setFormField((prev) => ({ ...prev, username: target.value })) autoComplete="off"
} value={formField.name}
/> onChange={({ target }) => {
</div> setFormField((prev) => ({ ...prev, name: target.value }));
</div> setErrors((prev) => ({ ...prev, name: '' }));
}}
<div className="w-full"> />
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5"> {errors.name && <span className="text-red-500 text-xs mt-1">{errors.name}</span>}
<label className="form-label flex items-center gap-1 max-w-56">Customer</label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
className="input col-span-5 text-left flex justify-between"
style={{ color: 'inherit' }}
>
<span>
{customers.find((customer) => customer.id === formField.customerid)
?.username || 'Select Customer'}
</span>
<ChevronDown className="w-4 h-4 opacity-70" />
</button>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0">
<Command>
<CommandInput placeholder="Search Customer..." />
<CommandList
className="max-h-[300px] overflow-y-auto"
style={{ touchAction: 'pan-y' }}
onWheel={(e) => {
e.currentTarget.scrollTop += e.deltaY;
}}
>
<CommandEmpty>No Customer found.</CommandEmpty>
<CommandGroup>
{customers.map((customer) => (
<CommandItem
key={customer.id}
value={customer.username}
onSelect={() => {
setFormField({
...formField,
customerid: customer.id
});
setOpen(false);
}}
>
{customer.username}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
<div className="w-full">
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Email</label>
<Input
className="input"
type="email"
autoComplete="off"
value={formField.email}
onChange={({ target }) =>
setFormField((prev) => ({ ...prev, email: target.value }))
}
/>
</div>
</div>
<div className="w-full">
<div className="flex items-center flex-wrap gap-2.5">
<label className="form-label max-w-56">Role</label>
<div className="grow">
<Select
value={formField.id_role}
onValueChange={(id_role) => {
setTimeout(() => {
setFormField((prev) => ({ ...prev, id_role }));
}, 0);
// console.log('Role selected:', value);`
}}
>
<SelectTrigger>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
{roles.map((role, idx) => (
<SelectItem value={role.id} key={role.id}>
{role.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="flex items-center flex-wrap gap-2.5">
<label className="form-label max-w-56">Status</label>
<div className="grow">
<Select
value={formField.status}
onValueChange={(status) => {
setTimeout(() => {
setFormField((prev) => ({ ...prev, status }));
}, 0);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Y">Active</SelectItem>
<SelectItem value="N">Non Active</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="w-full">
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Password</label>
<div className="input">
<input
className="form-control"
type={showPassword.password ? 'text' : 'password'}
value={formField.password}
onChange={({ target }) =>
setFormField((prev) => ({ ...prev, password: target.value }))
}
/>
<button
className="btn btn-icon"
onClick={(e) => togglePassword(e, 'password')}
>
<KeenIcon
icon="eye"
className={clsx('text-gray-500', { hidden: showPassword.password })}
/>
<KeenIcon
icon="eye-slash"
className={clsx('text-gray-500', {
hidden: !showPassword.password
})}
/>
</button>
</div>
</div>
</div>
<div className="w-full">
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">
Confirm Password
</label>
<div className="w-full">
<div className="input block">
<input
className="form-control"
autoComplete="off"
type={showPassword.retype_password ? 'text' : 'password'}
value={formField.retype_password}
onChange={({ target }) =>
setFormField((prev) => ({ ...prev, retype_password: target.value }))
}
/>
<button
className="btn btn-icon"
onClick={(e) => togglePassword(e, 'retype_password')}
>
<KeenIcon
icon="eye"
className={clsx('text-gray-500', {
hidden: showPassword.retype_password
})}
/>
<KeenIcon
icon="eye-slash"
className={clsx('text-gray-500', {
hidden: !showPassword.retype_password
})}
/>
</button>
</div>
<div className="w-full">
{passwordErrors.length > 0 && (
<div className="text-xs text-red-500 mt-2">
{passwordErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
</div>
</div>
<div className="flex justify-end pt-2.5">
<Button className="btn btn-primary" type="submit" disabled={isButtonDisabled}>
{isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div> </div>
</form> </div>
</div>
{/* Username */}
<div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Username</label>
<div className="grow flex flex-col">
<Input
className={`input ${errors.username ? 'border-red-500' : ''}`}
type="text"
autoComplete="off"
value={formField.username}
onChange={({ target }) => {
setFormField((prev) => ({ ...prev, username: target.value }));
setErrors((prev) => ({ ...prev, username: '' }));
}}
/>
{errors.username && (
<span className="text-red-500 text-xs mt-1">{errors.username}</span>
)}
</div>
</div>
{/* Customer */}
<div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Customer</label>
<div className="grow flex flex-col" ref={dropdownRef}>
<Input
id="customer"
type="text"
value={selectedCustomerName || searchTerm}
onChange={handleCustomerSearch}
placeholder="Search Customer"
onClick={() => setDropdownOpen(true)}
className={`input ${errors.customerid ? 'border-red-500' : ''}`}
/>
{dropdownOpen && (
<div className="absolute z-10 w-[60%] mt-11 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-y-auto">
{filteredCustomer.length > 0 ? (
filteredCustomer.map((item) => (
<div
key={item.id}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
onClick={() => handleCustomerSelect(item)}
>
{item.fullname}
</div>
))
) : (
<div className="px-4 py-2 text-gray-500">
{isLoading ? 'Loading...' : 'No results found'}
</div>
)}
</div>
)}
{errors.customerid && (
<span className="text-red-500 text-xs mt-1">{errors.customerid}</span>
)}
</div>
</div>
{/* Email */}
<div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Email</label>
<div className="grow flex flex-col">
<Input
className={`input ${errors.email ? 'border-red-500' : ''}`}
type="email"
autoComplete="off"
value={formField.email}
onChange={({ target }) => {
setFormField((prev) => ({ ...prev, email: target.value }));
setErrors((prev) => ({ ...prev, email: '' }));
}}
/>
{errors.email && <span className="text-red-500 text-xs mt-1">{errors.email}</span>}
</div>
</div>
{/* Role */}
<div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Role</label>
<div className="grow flex flex-col">
<Select
value={formField.id_role}
onValueChange={(id_role) => {
setTimeout(() => {
setFormField((prev) => ({ ...prev, id_role }));
setErrors((prev) => ({ ...prev, id_role: '' }));
}, 0);
}}
>
<SelectTrigger className={errors.id_role ? 'border-red-500' : ''}>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
{roles.map((role) => (
<SelectItem value={role.id} key={role.id}>
{role.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.id_role && (
<span className="text-red-500 text-xs mt-1">{errors.id_role}</span>
)}
</div>
</div>
{/* Status */}
<div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Status</label>
<div className="grow flex flex-col">
<Select
value={formField.status}
onValueChange={(status) => {
setTimeout(() => {
setFormField((prev) => ({ ...prev, status }));
setErrors((prev) => ({ ...prev, status: '' }));
}, 0);
}}
>
<SelectTrigger className={errors.status ? 'border-red-500' : ''}>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Y">Active</SelectItem>
<SelectItem value="N">Non Active</SelectItem>
</SelectContent>
</Select>
{errors.status && (
<span className="text-red-500 text-xs mt-1">{errors.status}</span>
)}
</div>
</div>
{/* Password */}
<div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Password</label>
<div className="grow flex flex-col">
<div className="input">
<input
className={`w-full ${errors.password ? 'border-red-500' : ''}`}
type={showPassword.password ? 'text' : 'password'}
value={formField.password}
onChange={({ target }) => {
setFormField((prev) => ({ ...prev, password: target.value }));
setErrors((prev) => ({ ...prev, password: '' }));
}}
/>
<button className="btn btn-icon" onClick={(e) => togglePassword(e, 'password')}>
<KeenIcon
icon="eye"
className={clsx('text-gray-500', { hidden: showPassword.password })}
/>
<KeenIcon
icon="eye-slash"
className={clsx('text-gray-500', {
hidden: !showPassword.password
})}
/>
</button>
</div>
{passwordErrors.length > 0 && (
<div className="text-xs text-red-500 mt-2">
{passwordErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
{errors.password && (
<span className="text-red-500 text-xs mt-1">{errors.password}</span>
)}
</div>
</div>
{/* Confirm Password */}
<div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">
Confirm Password
</label>
<div className="grow flex flex-col">
<div className="input">
<input
className={`form-control w-full ${passwordErrors.length > 0 ? 'border-red-500' : ''}`}
type={showPassword.retype_password ? 'text' : 'password'}
autoComplete="off"
value={formField.retype_password}
onChange={({ target }) => {
setFormField((prev) => ({ ...prev, retype_password: target.value }));
setErrors((prev) => ({ ...prev, retype_password: '' }));
}}
/>
<button
className="btn btn-icon"
onClick={(e) => togglePassword(e, 'retype_password')}
>
<KeenIcon
icon="eye"
className={clsx('text-gray-500', {
hidden: showPassword.retype_password
})}
/>
<KeenIcon
icon="eye-slash"
className={clsx('text-gray-500', {
hidden: !showPassword.retype_password
})}
/>
</button>
</div>
{notMatch.length > 0 && (
<div className="text-xs text-red-500 mt-2">
{notMatch.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
{errors.retype_password && (
<span className="text-red-500 text-xs mt-1">{errors.retype_password}</span>
)}
</div>
</div>
{/* Submit Button */}
<div className="flex justify-end pt-2.5">
<Button className="btn btn-primary" type="submit" disabled={isButtonDisabled}>
{isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</form>
</DialogBody> </DialogBody>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -23,7 +23,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle DialogTitle
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { CustomerProps } from './AddDialog';
import { useUserContext } from '../hooks'; import { useUserContext } from '../hooks';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@ -33,25 +32,16 @@ import { Alert, KeenIcon, useDataGrid } from '@/components';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useCallApi } from '@/hooks'; import { useCallApi } from '@/hooks';
import { doSaveLogActivity } from '@/actions/GlobalActions'; import { doSaveLogActivity } from '@/actions/GlobalActions';
import {
interface RoleListProps { CustomerProps,
id: string; initialStateCreateUser,
name: string; RoleListProps,
status: string; validateFormCreateUser
} } from './Types';
const API_URL_CUSTOMER = apiConfig.service_customer; const API_URL_CUSTOMER = apiConfig.service_customer;
const API_URL = apiConfig.service_dashboard; const API_URL = apiConfig.service_dashboard;
const initialState = {
name: '',
username: '',
email: '',
id_role: '',
status: '',
customerid: ''
};
const EditDialog = () => { const EditDialog = () => {
const parentRef = useRef<any | null>(null); const parentRef = useRef<any | null>(null);
const { showEditDialog, selectedUser, handleEditDialog } = useUserContext(); const { showEditDialog, selectedUser, handleEditDialog } = useUserContext();
@ -61,16 +51,22 @@ const EditDialog = () => {
const [roles, setRoles] = useState<RoleListProps[]>([]); const [roles, setRoles] = useState<RoleListProps[]>([]);
const [customers, setCustomers] = useState<CustomerProps[]>([]); const [customers, setCustomers] = useState<CustomerProps[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [alert, setAlert] = useState({ const [isFetching, setIsFetching] = useState(false);
show: false,
message: ''
});
const [formField, setFormField] = useState(initialState); const [errors, setErrors] = useState<Record<string, string>>({});
const [searchTerm, setSearchTerm] = useState('');
const [selectedCustomerName, setSelectedCustomerName] = useState('');
const [dropdownOpen, setDropdownOpen] = useState(false);
const [isLoadingCustomer, setIsLoadingCustomer] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const [formField, setFormField] = useState(initialStateCreateUser);
const resetForm = () => { const resetForm = () => {
setFormField(initialState); setFormField(initialStateCreateUser);
setAlert({ show: false, message: '' }); setSelectedCustomerName('');
setSearchTerm('');
setErrors({});
}; };
/* actions */ /* actions */
@ -79,15 +75,8 @@ const EditDialog = () => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
if ( if (!validateFormCreateUser(formField, setErrors, 'update')) {
!formField.name.trim() || setIsSubmitting(false);
!formField.username.trim() ||
!formField.email.trim() ||
!formField.id_role ||
!formField.status ||
!formField.customerid
) {
setAlert({ show: true, message: 'Please fill all required field' });
return; return;
} }
@ -109,7 +98,7 @@ const EditDialog = () => {
doSaveLogActivity(createActivity); doSaveLogActivity(createActivity);
} else { } else {
setAlert((prev) => ({ ...prev, show: true, message: response?.message })); toast.error(response?.message);
} }
} catch (error) { } catch (error) {
toast.error('Something went wrong, please try again.'); toast.error('Something went wrong, please try again.');
@ -131,35 +120,48 @@ const EditDialog = () => {
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC' order_direction: sorting[0].desc == false ? 'ASC' : 'DESC'
}); });
// console.log('User ID_ROLE:', response?.data.id_role);
// console.log('Role: ', response?.data.list);
setRoles(response?.data.list); setRoles(response?.data.list);
} catch (error) { } catch (error) {
console.error('Error fetching role', error); console.error('Error fetching role', error);
} }
}, []); }, []);
const getCustomerList = async (sorting: any) => { const getCustomerList = async (sorting: any, filterValue: string) => {
try { const filter: any =
sorting = sorting.length == 0 ? [{ id: 'name', desc: false }] : sorting; filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } };
const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, {
limit: 100,
page: 1,
with_deleted: false,
order_field: sorting[0].id,
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC'
});
// console.log('CUSTOMER: ', response?.data); sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting;
setCustomers(response?.data.list);
const query: any = {
limit: 5,
page: 1,
with_deleted: false,
order_field: sorting[0].id,
order_direction: sorting[0].desc ? 'DESC' : 'ASC'
};
if (filter && Object.keys(filter).length > 0) {
query.filter = JSON.stringify(filter);
}
try {
const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query);
if (response?.status) {
setCustomers(response?.data.list);
} else {
toast.error(response?.message);
}
} catch (error) { } catch (error) {
console.error('Error fetching customer', error); toast.error('Failed to fetch customer list');
} finally {
setIsLoadingCustomer(false);
} }
}; };
const doFetchUserData = useCallback(async (id: string) => { const doFetchUserData = useCallback(async (id: string) => {
setIsFetching(true);
const response = await GetData(`${API_URL}/user/detail/${id}`, { id }); const response = await GetData(`${API_URL}/user/detail/${id}`, { id });
// console.log('User detail response:', response?.data);
if (response?.status) { if (response?.status) {
setFormField((prev) => ({ setFormField((prev) => ({
@ -171,44 +173,71 @@ const EditDialog = () => {
status: response.data.status, status: response.data.status,
customerid: response.data.customer?.id || '' customerid: response.data.customer?.id || ''
})); }));
// console.log('Customer ID from API:', response?.data.customerid);
} else { } else {
setFormField((prev) => ({ setFormField((prev) => ({
...prev, ...prev,
name: '', name: '',
username: '', username: '',
email: '', email: '',
id_role: '0', id_role: '',
status: '', status: '',
customerid: '' customerid: ''
})); }));
} }
// console.log('Fetched ID Role:', response?.data.id_role); setIsFetching(false);
}, []); }, []);
const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsLoadingCustomer(true);
setSearchTerm(e.target.value);
setSelectedCustomerName(e.target.value);
setDropdownOpen(true);
const timer = setTimeout(() => {
getCustomerList([{ id: 'fullname', desc: false }], e.target.value);
}, 500);
return () => clearTimeout(timer);
};
const handleCustomerSelect = (customer: CustomerProps) => {
setFormField({ ...formField, customerid: customer.id });
setSelectedCustomerName(customer.fullname);
setDropdownOpen(false);
};
const filteredCustomer = customers
.filter((item) => item.fullname.toLowerCase().includes(searchTerm.toLowerCase()))
.slice(0, 10);
useEffect(() => { useEffect(() => {
if (showEditDialog === false) { if (showEditDialog === false) {
resetForm(); resetForm();
setDropdownOpen(false);
} }
}, [showEditDialog]); }, [showEditDialog]);
useEffect(() => { useEffect(() => {
const fetchAllData = async () => { getCustomerList([{ id: 'created_at', desc: false }], '');
await getCustomerList([{ id: 'id', desc: false }]); doFetchUserRole([{ id: 'name', desc: false }]);
await doFetchUserRole([{ id: 'name', desc: false }]); }, []);
if (selectedUser) {
await doFetchUserData(selectedUser); useEffect(() => {
if (selectedUser) {
doFetchUserData(selectedUser);
}
}, [selectedUser]);
useEffect(() => {
const handleClickOutside = (event: any) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setDropdownOpen(false);
} }
}; };
fetchAllData(); document.addEventListener('mousedown', handleClickOutside);
}, [selectedUser]); return () => {
document.removeEventListener('mousedown', handleClickOutside);
// console.log('ini role: ', roles); };
// useEffect(() => { }, []);
// console.log('Selected User ID Role:', formField.id_role);
// // console.log('Available Roles:', roles);
// }, [formField.id_role, roles]);
return ( return (
<Dialog open={showEditDialog} onOpenChange={(open) => handleEditDialog(open, null)}> <Dialog open={showEditDialog} onOpenChange={(open) => handleEditDialog(open, null)}>
@ -231,160 +260,184 @@ const EditDialog = () => {
</DialogHeader> </DialogHeader>
<DialogBody className="scrollable-y px-0 pb-0" ref={parentRef}> <DialogBody className="scrollable-y px-0 pb-0" ref={parentRef}>
<div className="flex flex-col px-0"> <div className="flex flex-col px-0">
{alert.show && ( {isFetching ? (
<Alert variant="danger"> <div className="flex flex-col items-center justify-center p-8">
<h3>{alert.message}</h3> <div className="animate-pulse flex space-x-4 w-full">
</Alert> <div className="flex-1 space-y-4 py-1">
)} <div className="h-4 bg-gray-200 rounded w-3/4"></div>
<form onSubmit={doUpdateUser}> <div className="space-y-2">
<div className="card-body grid gap-5 p-0"> <div className="h-4 bg-gray-200 rounded"></div>
<div className="w-full"> <div className="h-4 bg-gray-200 rounded w-5/6"></div>
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5"> </div>
</div>
</div>
<p className="mt-4 text-gray-500">Loading User Details...</p>
</div>
) : (
<form onSubmit={doUpdateUser}>
<div className="card-body grid gap-5 p-0">
<div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Name</label> <label className="form-label flex items-center gap-1 max-w-56">Name</label>
<Input <div className="grow flex flex-col">
className="input" <Input
type="text" className={`input ${errors.name ? 'border-red-500' : ''}`}
value={formField.name} type="text"
onChange={({ target }) => autoComplete="off"
setFormField((prev) => ({ ...prev, name: target.value })) value={formField.name}
} onChange={({ target }) => {
/> setFormField((prev) => ({ ...prev, name: target.value }));
setErrors((prev) => ({ ...prev, name: '' }));
}}
/>
{errors.name && (
<span className="text-red-500 text-xs mt-1">{errors.name}</span>
)}
</div>
</div> </div>
</div>
<div className="w-full"> {/* Username */}
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5"> <div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Username</label> <label className="form-label flex items-center gap-1 max-w-56">Username</label>
<Input <div className="grow flex flex-col">
className="input" <Input
type="text" className={`input ${errors.username ? 'border-red-500' : ''}`}
value={formField.username} type="text"
onChange={({ target }) => autoComplete="off"
setFormField((prev) => ({ ...prev, username: target.value })) value={formField.username}
} onChange={({ target }) => {
/> setFormField((prev) => ({ ...prev, username: target.value }));
setErrors((prev) => ({ ...prev, username: '' }));
}}
/>
{errors.username && (
<span className="text-red-500 text-xs mt-1">{errors.username}</span>
)}
</div>
</div> </div>
</div>
<div className="w-full"> {/* Customer */}
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5"> <div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Customer</label> <label className="form-label flex items-center gap-1 max-w-56">Customer</label>
<Popover open={open} onOpenChange={setOpen}> <div className="grow flex flex-col" ref={dropdownRef}>
<PopoverTrigger asChild> <Input
<button id="customer"
type="button" type="text"
className="input col-span-5 text-left flex justify-between" value={selectedCustomerName || searchTerm}
style={{ color: 'inherit' }} onChange={handleCustomerSearch}
> placeholder="Search Customer"
<span> onClick={() => setDropdownOpen(true)}
{customers.find((customer) => customer.id === formField.customerid) className={`input ${errors.customerid ? 'border-red-500' : ''}`}
?.username || 'Select Customer'} />
</span> {dropdownOpen && (
<ChevronDown className="w-4 h-4 opacity-70" /> <div className="absolute z-10 w-[60%] mt-11 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-y-auto">
</button> {filteredCustomer.length > 0 ? (
</PopoverTrigger> filteredCustomer.map((item) => (
<PopoverContent className="w-[400px] p-0"> <div
<Command> key={item.id}
<CommandInput placeholder="Search Customer..." /> className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
<CommandList onClick={() => handleCustomerSelect(item)}
className="max-h-[300px] overflow-y-auto" >
style={{ touchAction: 'pan-y' }} {item.fullname}
onWheel={(e) => { </div>
e.currentTarget.scrollTop += e.deltaY; ))
}} ) : (
> <div className="px-4 py-2 text-gray-500">
<CommandEmpty>No Customer found.</CommandEmpty> {isLoadingCustomer ? 'Loading...' : 'No results found'}
<CommandGroup> </div>
{customers.map((customer) => ( )}
<CommandItem </div>
key={customer.id} )}
value={customer.username} {errors.customerid && (
onSelect={() => { <span className="text-red-500 text-xs mt-1">{errors.customerid}</span>
setFormField({ )}
...formField, </div>
customerid: customer.id
});
setOpen(false);
}}
>
{customer.username}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div> </div>
</div>
<div className="w-full"> {/* Email */}
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5"> <div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Email</label> <label className="form-label flex items-center gap-1 max-w-56">Email</label>
<Input <div className="grow flex flex-col">
className="input" <Input
type="email" className={`input ${errors.email ? 'border-red-500' : ''}`}
value={formField.email} type="email"
onChange={({ target }) => autoComplete="off"
setFormField((prev) => ({ ...prev, email: target.value })) value={formField.email}
} onChange={({ target }) => {
/> setFormField((prev) => ({ ...prev, email: target.value }));
setErrors((prev) => ({ ...prev, email: '' }));
}}
/>
{errors.email && (
<span className="text-red-500 text-xs mt-1">{errors.email}</span>
)}
</div>
</div> </div>
</div>
<div className="w-full"> {/* Role */}
<div className="flex items-center flex-wrap gap-2.5"> <div className="flex gap-2.5">
<label className="form-label max-w-56">Role</label> <label className="form-label flex items-center gap-1 max-w-56">Role</label>
<div className="grow flex flex-col">
<div className="grow">
<Select <Select
value={formField.id_role} value={formField.id_role}
onValueChange={(id_role) => { onValueChange={(id_role) => {
// console.log('Role changed to:', id_role); setTimeout(() => {
setFormField((prev) => ({ ...prev, id_role })); setFormField((prev) => ({ ...prev, id_role }));
setErrors((prev) => ({ ...prev, id_role: '' }));
}, 0);
}} }}
> >
<SelectTrigger> <SelectTrigger className={errors.id_role ? 'border-red-500' : ''}>
<SelectValue placeholder="Select" /> <SelectValue placeholder="Select" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{roles.map((role, idx) => ( {roles.map((role) => (
<SelectItem value={role.id} key={role.id}> <SelectItem value={role.id} key={role.id}>
{role.name} {role.name}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
{errors.id_role && (
<span className="text-red-500 text-xs mt-1">{errors.id_role}</span>
)}
</div> </div>
</div> </div>
</div>
<div className="flex items-center flex-wrap gap-2.5"> {/* Status */}
<label className="form-label max-w-56">Status</label> <div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Status</label>
<div className="grow flex flex-col">
<Select
value={formField.status}
onValueChange={(status) => {
setTimeout(() => {
setFormField((prev) => ({ ...prev, status }));
setErrors((prev) => ({ ...prev, status: '' }));
}, 0);
}}
>
<SelectTrigger className={errors.status ? 'border-red-500' : ''}>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Y">Active</SelectItem>
<SelectItem value="N">Non Active</SelectItem>
</SelectContent>
</Select>
{errors.status && (
<span className="text-red-500 text-xs mt-1">{errors.status}</span>
)}
</div>
</div>
<div className="grow"> <div className="flex justify-end pt-2.5">
<Select <Button className="btn btn-primary" type="submit" disabled={isSubmitting}>
value={formField.status} {isSubmitting ? 'Saving...' : 'Save Changes'}
onValueChange={(status) => setFormField((prev) => ({ ...prev, status }))} </Button>
>
<SelectTrigger>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Y">Active</SelectItem>
<SelectItem value="N">Non Active</SelectItem>
</SelectContent>
</Select>
</div> </div>
</div> </div>
</form>
<div className="flex justify-end pt-2.5"> )}
<Button className="btn btn-primary" type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
</form>
</div> </div>
</DialogBody> </DialogBody>
</DialogContent> </DialogContent>

View File

@ -0,0 +1,91 @@
import { toast } from 'sonner';
export interface CustomerProps {
id: string;
msisdn: string;
email: string;
fullname: string;
username: string;
}
export interface RoleListProps {
id: string;
name: string;
roles: string;
status: string;
}
export interface CreateUserParams {
email: string;
username: string;
password: string;
retype_password: string;
name: string;
id_role: string;
status: string;
}
export const initialStateCreateUser: {
email: string;
username: string;
password: string;
retype_password: string;
name: string;
id_role: string;
status: string;
customerid: string;
} = {
email: '',
username: '',
password: '',
retype_password: '',
name: '',
id_role: '',
status: '',
customerid: ''
};
export const validateFormCreateUser = (
formField: typeof initialStateCreateUser,
setErrors: React.Dispatch<React.SetStateAction<Record<string, string>>>,
mode: 'create' | 'update' = 'create'
) => {
const requiredFields =
mode === 'create'
? [
{ key: 'name', label: 'Name' },
{ key: 'username', label: 'Username' },
{ key: 'email', label: 'Email' },
{ key: 'customerid', label: 'Customer ID' },
{ key: 'id_role', label: 'Role' },
{ key: 'status', label: 'Status' },
{ key: 'password', label: 'Password' },
{ key: 'retype_password', label: 'Retype Password' }
]
: [
{ key: 'name', label: 'Name' },
{ key: 'username', label: 'Username' },
{ key: 'customerid', label: 'Customer ID' },
{ key: 'email', label: 'Email' },
{ key: 'id_role', label: 'Role' },
{ key: 'status', label: 'Status' }
];
const newErrors: Record<string, string> = {};
let isValid = true;
requiredFields.forEach(({ key, label }) => {
if (
formField[key as keyof typeof formField] === '' ||
formField[key as keyof typeof formField] === null ||
formField[key as keyof typeof formField] === undefined
) {
newErrors[key] = `${label} is required`;
toast.error(`${label} is required`);
isValid = false;
}
});
setErrors(newErrors);
return isValid;
};

View File

@ -232,8 +232,6 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
handleDeleteDialog handleDeleteDialog
}} }}
> >
<Toaster expand visibleToasts={9} duration={3000} />
<DataGridProvider <DataGridProvider
columns={columns} columns={columns}
pagination={{ size: 10 }} pagination={{ size: 10 }}