Files
revenue-fe/src/pages/settings/user/manage-user/blocks/AddDialog.tsx
2025-05-09 15:12:44 +07:00

548 lines
19 KiB
TypeScript

import { MouseEvent, useCallback, useEffect, useRef, useState } from 'react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from '@/components/ui/command';
import { ChevronDown } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { useUserContext } from '../hooks';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { apiConfig } from '@/config/api.config';
import { Alert, KeenIcon, useDataGrid } from '@/components';
import { toast } from 'sonner';
import { useCallApi } from '@/hooks';
import { doSaveLogActivity } from '@/actions/GlobalActions';
import clsx from 'clsx';
import {
CustomerProps,
initialStateCreateUser,
RoleListProps,
validateFormCreateUser
} from './Types';
const API_URL_CUSTOMER = apiConfig.service_customer;
const API_URL = apiConfig.service_dashboard;
type PasswordType = 'password' | 'retype_password';
const AddDialog = () => {
const parentRef = useRef<any | null>(null);
const { showAddDialog, handleAddDialog } = useUserContext();
const { reload } = useDataGrid();
const { PostData, GetData } = useCallApi();
const [roles, setRoles] = useState<RoleListProps[]>([]);
const [errors, setErrors] = useState<Record<string, string>>({});
const [formField, setFormField] = useState(initialStateCreateUser);
const [showPassword, setShowPassword] = useState({
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 [open, setOpen] = useState(false);
const [customers, setCustomers] = useState<CustomerProps[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [passwordErrors, setPasswordErrors] = useState<string[]>([]);
const [notMatch, setNotMatch] = useState<string[]>([]);
const validatePassword = (password: string, confirmPassword: string) => {
const errors: string[] = [];
const notMatch: string[] = [];
if (password) {
if (password.length < 8) {
errors.push('Password must be at least 8 characters long');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain at least one capital letter');
}
if (!/[0-9]/.test(password)) {
errors.push('Password must contain at least one number');
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.push('Password must contain at least one special character');
}
if (password !== confirmPassword) {
notMatch.push('Passwords do not match');
}
}
return {
isValid: errors.length === 0,
errors,
notMatch
};
};
const resetForm = () => {
setFormField(initialStateCreateUser);
setSelectedCustomerName('');
setSearchTerm('');
setErrors({});
};
/* actions */
const doCreateUser = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsSubmitting(true);
try {
const response = await PostData(`${API_URL}/user/create`, formField);
if (response?.status) {
handleAddDialog(false);
resetForm();
reload();
const createActivity = {
module: 'Manage User',
description: `Create New User => ${formField.username}`,
action: 'C'
};
doSaveLogActivity(createActivity);
toast.success('Success Create User');
} else {
toast.error(response?.message);
}
} catch (err) {
toast.error('Something went wrong');
console.log(err);
} finally {
setIsSubmitting(false);
}
},
[formField]
);
const fetchRoles = useCallback(async () => {
const params = {
limit: 100,
page: 1,
with_deleted: false,
order_field: 'name',
order_direction: 'ASC',
filter: JSON.stringify({
status: 'Y'
})
};
const response = await GetData(`${API_URL}/user_role/list`, params);
if (response?.status) {
const roleList = response.data?.list || [];
setRoles(roleList);
} else {
setRoles(() => []);
}
}, []);
const getCustomerList = async (sorting: any, filterValue: string) => {
const filter: any =
filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } };
sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting;
const query: any = {
limit: 100,
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) {
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(() => {
fetchRoles();
}, [fetchRoles]);
useEffect(() => {
const validation = validatePassword(formField.password, formField.retype_password);
setMessagePassword(validation.isValid);
setPasswordErrors(validation.errors);
setNotMatch(validation.notMatch);
}, [formField.password, formField.retype_password]);
useEffect(() => {
if (showAddDialog === false) {
resetForm();
setDropdownOpen(false);
}
}, [showAddDialog]);
useEffect(() => {
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) => {
event.preventDefault();
setShowPassword((prev) => ({ ...prev, [key]: !prev[key as PasswordType] }));
}, []);
return (
<Dialog open={showAddDialog} onOpenChange={(open) => handleAddDialog(open)}>
<DialogContent className="container-fixed max-w-[768px] flex flex-col p-10 overflow-hidden [&>button]:hidden">
<DialogHeader className="p-0 border-0">
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
<div className="flex items-center justify-between flex-wrap grow">
<div className="flex flex-col justify-center">
<h1 className="text-xl font-semibold leading-none text-gray-900">User - Create</h1>
</div>
<div
className="cursor-pointer hover:opacity-100 opacity-50"
onClick={() => {
handleAddDialog(false);
resetForm();
}}
>
<KeenIcon icon="cross" className="text-1.5xl" />
</div>
</div>
</DialogHeader>
<DialogBody className="scrollable-y p-5 pb-0" ref={parentRef}>
<form onSubmit={handleSubmit} className="grid gap-5">
{/* Name */}
<div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Name</label>
<div className="grow flex flex-col">
<Input
className={`input ${errors.name ? 'border-red-500' : ''}`}
type="text"
autoComplete="off"
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>
{/* 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>
</DialogContent>
</Dialog>
);
};
export { AddDialog };