revamp template
This commit is contained in:
1
src/pages/account/home/index.ts
Normal file
1
src/pages/account/home/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './user-profile/AccountUserProfilePage';
|
||||
@ -0,0 +1,16 @@
|
||||
import { BasicSettings, Password } from './blocks';
|
||||
import { AccountUserProfileContextProvider } from './hooks';
|
||||
|
||||
const AccountUserProfileContent = () => {
|
||||
return (
|
||||
<div className="grid gap-5 lg:gap-7.5 xl:w-[38.75rem] mx-auto">
|
||||
<AccountUserProfileContextProvider>
|
||||
<BasicSettings />
|
||||
<Password />
|
||||
{/* <DeleteAccount /> */}
|
||||
</AccountUserProfileContextProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { AccountUserProfileContent };
|
||||
@ -0,0 +1,15 @@
|
||||
import { Fragment } from 'react';
|
||||
import { Container } from '@/components/container';
|
||||
import { AccountUserProfileContent } from '.';
|
||||
|
||||
const AccountUserProfilePage = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<Container>
|
||||
<AccountUserProfileContent />
|
||||
</Container>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export { AccountUserProfilePage };
|
||||
109
src/pages/account/home/user-profile/blocks/BasicSettings.tsx
Normal file
109
src/pages/account/home/user-profile/blocks/BasicSettings.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import { useState, useContext } from 'react';
|
||||
import { AccountUserProfileContext } from '../hooks';
|
||||
|
||||
// import { KeenIcon } from '@/components';
|
||||
// import { toAbsoluteUrl } from '@/utils/Assets';
|
||||
import { getAuth, useAuthContext } from '@/auth';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface IBasicSettingsProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const BasicSettings = () => {
|
||||
// const user = localStorage.getItem('user');
|
||||
// const parsedUser = user ? JSON.parse(user) : null;
|
||||
const parsedUser = getAuth()?.user;
|
||||
const [newUsername, setNewUsername] = useState<string>(parsedUser?.username || '');
|
||||
const [newEmail, setNewEmail] = useState<string>(parsedUser?.email || '');
|
||||
const [newName, setNewName] = useState<string>(parsedUser?.name || '');
|
||||
|
||||
const { setProfile } = useContext(AccountUserProfileContext);
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const isChanged =
|
||||
newUsername !== parsedUser?.username ||
|
||||
newEmail !== parsedUser?.email ||
|
||||
newName !== parsedUser?.name;
|
||||
|
||||
const handleChangeProfile = async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await setProfile({ name: newName, username: newUsername, email: newEmail });
|
||||
|
||||
const newCache = {
|
||||
name: newName,
|
||||
email: newEmail,
|
||||
username: newUsername
|
||||
};
|
||||
|
||||
localStorage.setItem('user', JSON.stringify(newCache));
|
||||
} catch (error: any) {
|
||||
const errorMessage =
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
'An error occurred while resetting the password.';
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card pb-2.5">
|
||||
<div className="card-header" id="general_settings">
|
||||
<h3 className="card-title">Account</h3>
|
||||
</div>
|
||||
|
||||
<div className="card-body grid gap-5">
|
||||
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
|
||||
<label className="form-label max-w-56">Name</label>
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
|
||||
<label className="form-label max-w-56">Username</label>
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
|
||||
<label className="form-label max-w-56">Email</label>
|
||||
<input
|
||||
className="input"
|
||||
type="email"
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
{/* <div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
|
||||
<label className="form-label max-w-56">Name</label>
|
||||
<input className="input" type="text" value={parsedUser?.name} />
|
||||
</div> */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleChangeProfile}
|
||||
disabled={isSubmitting || !isChanged}
|
||||
>
|
||||
{isSubmitting ? 'loading...' : 'Save Change'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { BasicSettings, type IBasicSettingsProps };
|
||||
200
src/pages/account/home/user-profile/blocks/Password.tsx
Normal file
200
src/pages/account/home/user-profile/blocks/Password.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
import { useState, useContext, useEffect, useCallback, MouseEvent } from 'react';
|
||||
import { AccountUserProfileContext } from '../hooks';
|
||||
import { toast } from 'sonner';
|
||||
import { useAuthContext } from '@/auth';
|
||||
import { KeenIcon } from '@/components';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type PasswordType = 'password' | 'retype_password' | 'current_password';
|
||||
const Password = () => {
|
||||
const { setPassword } = useContext(AccountUserProfileContext);
|
||||
const { getUser } = useAuthContext();
|
||||
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [messagePassword, setMessagePassword] = useState(true);
|
||||
const [showPassword, setShowPassword] = useState({
|
||||
current_password: false,
|
||||
password: false,
|
||||
retype_password: false
|
||||
});
|
||||
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([]);
|
||||
|
||||
const validatePassword = (password: string) => {
|
||||
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
|
||||
const hasCapitalLetter = /[A-Z]/.test(password);
|
||||
const hasNumber = /[0-9]/.test(password);
|
||||
return hasSpecialChar && hasCapitalLetter && hasNumber;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const passwordsMatch = newPassword === confirmPassword;
|
||||
const isPasswordValid = validatePassword(newPassword);
|
||||
setMessagePassword(passwordsMatch && isPasswordValid);
|
||||
|
||||
const errors: string[] = [];
|
||||
if (newPassword) {
|
||||
if (!/[A-Z]/.test(newPassword)) {
|
||||
errors.push('Password must contain at least one capital letter');
|
||||
}
|
||||
if (!/[0-9]/.test(newPassword)) {
|
||||
errors.push('Password must contain at least one number');
|
||||
}
|
||||
if (!/[!@#$%^&*(),.?":{}|<>]/.test(newPassword)) {
|
||||
errors.push('Password must contain at least one special character');
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
errors.push('Passwords do not match');
|
||||
}
|
||||
}
|
||||
setPasswordErrors(errors);
|
||||
}, [newPassword, confirmPassword]);
|
||||
|
||||
const handleResetPassword = useCallback(async () => {
|
||||
const user: any = await getUser();
|
||||
|
||||
if (!messagePassword) {
|
||||
toast.error('Passwords do not match!');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
await setPassword({
|
||||
current_password: currentPassword,
|
||||
password: newPassword,
|
||||
retype_password: confirmPassword,
|
||||
username: user.data.username ?? ''
|
||||
});
|
||||
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
setCurrentPassword('');
|
||||
} catch (error: any) {
|
||||
const errorMessage =
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
'An error occurred while resetting the password.';
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [currentPassword, newPassword, newPassword, messagePassword, getUser]);
|
||||
|
||||
const isButtonDisabled = isSubmitting || !newPassword || !confirmPassword || !messagePassword;
|
||||
|
||||
const togglePassword = useCallback((event: MouseEvent<HTMLButtonElement>, key: string) => {
|
||||
event.preventDefault();
|
||||
setShowPassword((prev) => ({ ...prev, [key]: !prev[key as PasswordType] }));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="card pb-2.5">
|
||||
<div className="card-header" id="password_settings">
|
||||
<h3 className="card-title">Password</h3>
|
||||
</div>
|
||||
<div className="card-body grid gap-5">
|
||||
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
|
||||
<label className="form-label max-w-56">Current Password</label>
|
||||
<div className="input">
|
||||
<input
|
||||
type={showPassword.current_password ? 'text' : 'password'}
|
||||
className="form-control"
|
||||
placeholder="Current password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<button className="btn btn-icon" onClick={(e) => togglePassword(e, 'current_password')}>
|
||||
<KeenIcon
|
||||
icon="eye"
|
||||
className={clsx('text-gray-500', { hidden: showPassword.current_password })}
|
||||
/>
|
||||
<KeenIcon
|
||||
icon="eye-slash"
|
||||
className={clsx('text-gray-500', { hidden: !showPassword.current_password })}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
|
||||
<label className="form-label max-w-56">New Password</label>
|
||||
<div className="input">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="New password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
type={showPassword.password ? 'text' : '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>
|
||||
</div>
|
||||
<div className="mb-2.5">
|
||||
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
|
||||
<label className="form-label max-w-56">Confirm New Password</label>
|
||||
<div className="input">
|
||||
<input
|
||||
type={showPassword.retype_password ? 'text' : 'password'}
|
||||
className="form-control"
|
||||
placeholder="Confirm new password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<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>
|
||||
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
|
||||
<label className="form-label max-w-56 text-white">Confirm New Password</label>
|
||||
{passwordErrors.length > 0 && (
|
||||
<div className="text-xs text-red-500 mt-1 ms-3">
|
||||
{passwordErrors.map((error, index) => (
|
||||
<p key={index}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleResetPassword}
|
||||
disabled={isButtonDisabled}
|
||||
>
|
||||
{isSubmitting ? 'Updating...' : 'Update Password'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Password };
|
||||
2
src/pages/account/home/user-profile/blocks/index.ts
Normal file
2
src/pages/account/home/user-profile/blocks/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './BasicSettings';
|
||||
export * from './Password';
|
||||
@ -0,0 +1,119 @@
|
||||
import React, { createContext, useState } from 'react';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { toast } from 'sonner';
|
||||
import axios from 'axios';
|
||||
import { useAuthContext } from '@/auth';
|
||||
|
||||
interface Password {
|
||||
password: string;
|
||||
retype_password: string;
|
||||
username: string;
|
||||
current_password: string;
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
name: string;
|
||||
email: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
interface ContextProps {
|
||||
password: Password | null;
|
||||
profile: Profile | null;
|
||||
setPassword: (password: Password) => Promise<void>;
|
||||
setProfile: (profile: Profile) => Promise<void>;
|
||||
}
|
||||
|
||||
const initialProps: ContextProps = {
|
||||
profile: null,
|
||||
password: null,
|
||||
setPassword: async () => {},
|
||||
setProfile: async () => {}
|
||||
};
|
||||
|
||||
const AccountUserProfileContext = createContext<ContextProps>(initialProps);
|
||||
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
const AccountUserProfileContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
/* state */
|
||||
const { login } = useAuthContext();
|
||||
const [profile, setProfile] = useState<Profile | null>(null);
|
||||
const [password, setPassword] = useState<Password | null>(null);
|
||||
const [alert, setAlert] = useState({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
|
||||
const { PutData } = useCallApi();
|
||||
const { PostData } = useCallApi();
|
||||
const handleSetPassword = async (newPassword: Password) => {
|
||||
setPassword(newPassword);
|
||||
|
||||
const handleError = (error: any, defaultMessage: string) => {
|
||||
const errorMessage = error?.response?.data?.message || error?.message || defaultMessage;
|
||||
setAlert({ show: true, message: errorMessage });
|
||||
toast.error(errorMessage);
|
||||
};
|
||||
|
||||
try {
|
||||
const validate = await PostData(`${API_URL}/login`, {
|
||||
password: newPassword.current_password,
|
||||
username: newPassword.username,
|
||||
application: 'credit'
|
||||
});
|
||||
|
||||
if (!validate || !validate.status) {
|
||||
toast.error('Current password validation failed.');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await PutData(`${API_URL}/user/update_password`, {
|
||||
password: newPassword.password,
|
||||
retype_password: newPassword.retype_password
|
||||
});
|
||||
|
||||
if (response && response.status) {
|
||||
toast.success('Password updated successfully!');
|
||||
} else {
|
||||
toast.error(response?.message || 'Failed to update password.');
|
||||
}
|
||||
} catch (error: any) {
|
||||
handleError(error, 'Failed to update password. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetProfile = async (newProfile: Profile) => {
|
||||
try {
|
||||
setProfile(newProfile);
|
||||
|
||||
const response = await PutData(`${API_URL}/user/update_profile/`, {
|
||||
name: newProfile.name,
|
||||
email: newProfile.email,
|
||||
username: newProfile.username
|
||||
});
|
||||
|
||||
toast.success('Profile updated successfully.');
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || 'Failed to update Profile. Please try again.';
|
||||
setAlert({ show: true, message: errorMessage });
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AccountUserProfileContext.Provider
|
||||
value={{
|
||||
password,
|
||||
profile,
|
||||
setPassword: handleSetPassword,
|
||||
setProfile: handleSetProfile
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AccountUserProfileContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { AccountUserProfileContextProvider, AccountUserProfileContext };
|
||||
2
src/pages/account/home/user-profile/hooks/index.ts
Normal file
2
src/pages/account/home/user-profile/hooks/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './AccountUserProfileContext';
|
||||
export * from './useAccountUserProfileContext';
|
||||
@ -0,0 +1,12 @@
|
||||
import { useContext } from 'react';
|
||||
import { AccountUserProfileContext } from './AccountUserProfileContext';
|
||||
|
||||
const useAccountUserProfileContext = () => {
|
||||
const context = useContext(AccountUserProfileContext);
|
||||
|
||||
if (!context) throw new Error('useAccountUserProfileContext must be used within AuthProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useAccountUserProfileContext };
|
||||
3
src/pages/account/home/user-profile/index.ts
Normal file
3
src/pages/account/home/user-profile/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './AccountUserProfileContent';
|
||||
export * from './AccountUserProfilePage';
|
||||
export * from './blocks';
|
||||
1
src/pages/account/index.ts
Normal file
1
src/pages/account/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './home';
|
||||
@ -0,0 +1,14 @@
|
||||
import { Container, DataGridInner } from '@/components';
|
||||
import { LogActivityContextProvider } from './hooks';
|
||||
|
||||
export default function LogActivityPage() {
|
||||
return (
|
||||
<LogActivityContextProvider>
|
||||
<Container>
|
||||
<div className="grid gap-5 lg:gap-7.5">
|
||||
<DataGridInner />
|
||||
</div>
|
||||
</Container>
|
||||
</LogActivityContextProvider>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
import { useState } from 'react';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { format } from 'date-fns';
|
||||
import { KeenIcon } from '@/components/keenicons';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface DateRangePickerProps {
|
||||
date: DateRange | undefined;
|
||||
setDate: (date: DateRange | undefined) => void;
|
||||
}
|
||||
|
||||
const DatePicker = ({ date, setDate }: any) => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
id="date"
|
||||
className={cn(
|
||||
'btn btn-sm btn-light data-[state=open]:bg-light-active',
|
||||
!date && 'text-gray-400'
|
||||
)}
|
||||
>
|
||||
<KeenIcon icon="calendar" className="me-0.5" />
|
||||
{date?.to ? <>{format(date.to, 'LLL dd, y')}</> : <span>Pick a date</span>}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="end">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="single"
|
||||
defaultMonth={date?.to}
|
||||
selected={date?.to}
|
||||
onSelect={(value) => setDate({ from: value, to: value })}
|
||||
numberOfMonths={1}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export { DatePicker };
|
||||
@ -0,0 +1,53 @@
|
||||
import { useState } from 'react';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { format } from 'date-fns';
|
||||
import { KeenIcon } from '@/components/keenicons';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface DateRangePickerProps {
|
||||
date: DateRange | undefined;
|
||||
setDate: (date: DateRange | undefined) => void;
|
||||
}
|
||||
|
||||
const DateRangePicker = ({ date, setDate }: DateRangePickerProps) => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
id="date"
|
||||
className={cn(
|
||||
'btn btn-sm btn-light data-[state=open]:bg-light-active',
|
||||
!date && 'text-gray-400'
|
||||
)}
|
||||
>
|
||||
<KeenIcon icon="calendar" className="me-0.5" />
|
||||
{date?.from ? (
|
||||
date.to ? (
|
||||
<>
|
||||
{format(date.from, 'LLL dd, y')} - {format(date.to, 'LLL dd, y')}
|
||||
</>
|
||||
) : (
|
||||
format(date.from, 'LLL dd, y')
|
||||
)
|
||||
) : (
|
||||
<span>Pick a date range</span>
|
||||
)}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="end">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={date?.from}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export { DateRangePicker };
|
||||
146
src/pages/configuration/user/log-activity/blocks/ListToolBar.tsx
Normal file
146
src/pages/configuration/user/log-activity/blocks/ListToolBar.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import { ContentLoader, DefaultTooltip, KeenIcon, useDataGrid } from '@/components';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useLogActivityContext } from '../hooks';
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
import { DateRangePicker } from './DateRangePicker';
|
||||
import { toast } from 'sonner';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
|
||||
type LoadingButton = 'filter' | 'reset' | 'export' | 'refresh' | null;
|
||||
|
||||
const ListToolBar = () => {
|
||||
const { table, reload } = useDataGrid();
|
||||
const { date, setDate, doExportData } = useLogActivityContext();
|
||||
const [searchUsername, setSearchUsername] = useState('');
|
||||
// const [filteredDate, setFilteredDate] = useState<DateRange | undefined>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loadingButton, setLoadingButton] = useState<LoadingButton>(null);
|
||||
|
||||
const [filter, setFilter] = useState<{
|
||||
from: Date | undefined;
|
||||
to: Date | undefined;
|
||||
type?: string;
|
||||
}>({
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 31)),
|
||||
to: new Date(),
|
||||
type: ''
|
||||
});
|
||||
|
||||
const handleExport = () => {
|
||||
const sorting = table.getState().sorting;
|
||||
doExportData(sorting, table.getState().columnFilters);
|
||||
};
|
||||
|
||||
const handleFilterData = useCallback(() => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setLoadingButton('filter');
|
||||
const filters = [];
|
||||
|
||||
if (searchUsername) {
|
||||
filters.push({
|
||||
id: 'username',
|
||||
value: searchUsername
|
||||
});
|
||||
}
|
||||
|
||||
if (date?.from && date?.to) {
|
||||
filters.push({
|
||||
id: 'user_activity.created_at',
|
||||
value: {
|
||||
from: `${date.from.toISOString().split('T')[0]} 00:00:00`,
|
||||
to: `${date.to.toISOString().split('T')[0]} 23:59:59`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
table.setColumnFilters(filters);
|
||||
} catch (error) {
|
||||
toast.error('Error filtering data');
|
||||
} finally {
|
||||
setLoadingButton(null);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [date, searchUsername, table]);
|
||||
|
||||
const handleResetData = () => {
|
||||
const today = new Date();
|
||||
const oneMonthAgo = new Date(today);
|
||||
oneMonthAgo.setDate(today.getDate() - 31);
|
||||
|
||||
setDate({ from: oneMonthAgo, to: today });
|
||||
setFilter({
|
||||
from: oneMonthAgo,
|
||||
to: today,
|
||||
type: ''
|
||||
});
|
||||
setSearchUsername('');
|
||||
table.setColumnFilters([]);
|
||||
reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card-header flex-wrap gap-2 border-b-0 px-5">
|
||||
<div className="flex flex-wrap gap-2 lg:gap-5 w-full">
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="w-auto min-w-[120px]">
|
||||
<label className="input input-sm">
|
||||
<KeenIcon icon="magnifier" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search username"
|
||||
value={searchUsername}
|
||||
onChange={(event) => setSearchUsername(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-auto min-w-[220px]">
|
||||
<DateRangePicker date={date} setDate={setDate} />
|
||||
</div>
|
||||
<DefaultTooltip title={'Filter'} placement={'top'}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-7.5 disabled:bg-gray-400"
|
||||
disabled={isLoading}
|
||||
onClick={handleFilterData}
|
||||
>
|
||||
{loadingButton === 'filter' ? <ContentLoader /> : <KeenIcon icon="filter" />}
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
<DefaultTooltip title={'Reset Filter'} placement={'top'}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-7.5 disabled:bg-gray-400"
|
||||
onClick={handleResetData}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<KeenIcon icon="arrow-circle-left" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<DefaultTooltip title={'Refresh'} placement={'top'}>
|
||||
<Button variant={'outline'} className="h-7.5" onClick={() => reload()}>
|
||||
<KeenIcon icon="arrows-circle" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
<DefaultTooltip title={'Export Data'} placement={'top'}>
|
||||
<Button onClick={handleExport} className="h-7.5" variant={'outline'}>
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/file-types/xls.svg')}
|
||||
className="dark:hidden h-5"
|
||||
alt="Export to Excel"
|
||||
/>
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ListToolBar };
|
||||
@ -0,0 +1 @@
|
||||
export * from './ListToolBar';
|
||||
@ -0,0 +1,248 @@
|
||||
import React, { createContext, useCallback, useMemo, useState } from 'react';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { DataGridColumnHeader, DataGridProvider } from '@/components';
|
||||
import { ListToolBar } from '../blocks';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import moment from 'moment';
|
||||
import { getAuth } from '@/auth';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { format } from 'date-fns';
|
||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
|
||||
interface ContextProps {
|
||||
doExportData: (sorting: any, filter: any) => Promise<any>;
|
||||
date: DateRange | undefined;
|
||||
setDate: (date: DateRange | undefined) => void;
|
||||
}
|
||||
|
||||
const initialProps: ContextProps = {
|
||||
doExportData: async () => ({ data: [], totalCount: 0 }),
|
||||
date: undefined,
|
||||
setDate: () => {}
|
||||
};
|
||||
|
||||
const LogActivityContext = createContext<ContextProps>(initialProps);
|
||||
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
const LogActivityContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
/* state */
|
||||
const { GetData } = useCallApi();
|
||||
const { GetExportData } = useCallApi();
|
||||
|
||||
/* action */
|
||||
const [date, setDate] = useState<DateRange | undefined>({
|
||||
from: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
to: new Date()
|
||||
});
|
||||
|
||||
/* Data Grid Options */
|
||||
const columns = useMemo<ColumnDef<any>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorFn: (row) => row.created_at,
|
||||
id: 'created_at',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Timestamp" column={column} />,
|
||||
enableSorting: true,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => moment(row.original.created_at).format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.user.username,
|
||||
id: 'username',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Username" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-[350px]'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.module,
|
||||
id: 'module',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Module" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-[350px]'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.description,
|
||||
id: 'description',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Description" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-[350px]'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.action,
|
||||
id: 'action',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Action" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const { action } = row.original;
|
||||
const statusMap: any = {
|
||||
C: ['Create', 'success'],
|
||||
U: ['Update', 'default'],
|
||||
D: ['Delete', 'destructive'],
|
||||
l: ['Login', 'outline'],
|
||||
O: ['Logout', 'outline'],
|
||||
E: ['Export', 'outline']
|
||||
};
|
||||
|
||||
return <Badge variant={statusMap[action][1]}>{statusMap[action][0]}</Badge>;
|
||||
},
|
||||
meta: {
|
||||
headerClassName: 'w-[100px]',
|
||||
cellClassName: 'text-center'
|
||||
}
|
||||
}
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const doGetListData = useCallback(
|
||||
async (page: number, limit: number, sorting: any, filter: any) => {
|
||||
const auth = getAuth();
|
||||
let all_user = false;
|
||||
if (auth) {
|
||||
all_user = auth.role_name === 'Super Admin';
|
||||
}
|
||||
|
||||
const user = localStorage.getItem('brillian-bri-tl-auth-v1=9.1.1');
|
||||
const parsedUser = user ? JSON.parse(user) : null;
|
||||
const desc = sorting.length > 0 ? sorting[0].desc : false;
|
||||
|
||||
sorting = [{ id: 'user_activity.created_at', desc }];
|
||||
|
||||
let dataFilter: any = {
|
||||
application: 'credit'
|
||||
};
|
||||
|
||||
if (filter && filter.length > 0) {
|
||||
filter.forEach((f: any) => {
|
||||
if (f.id === 'username') {
|
||||
dataFilter['username'] = { like: f.value };
|
||||
} else if (f.id === 'user_activity.created_at') {
|
||||
dataFilter['user_activity.created_at'] = f.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!dataFilter['user_activity.created_at']) {
|
||||
const defaultFrom = date?.from || new Date(new Date().setMonth(new Date().getMonth() - 1));
|
||||
const defaultTo = date?.to || new Date();
|
||||
|
||||
dataFilter['user_activity.created_at'] = {
|
||||
from: `${format(defaultFrom, 'yyyy-MM-dd')} 00:00:00`,
|
||||
to: `${format(defaultTo, 'yyyy-MM-dd')} 23:59:59`
|
||||
};
|
||||
}
|
||||
|
||||
const response = await GetData(`${API_URL}/user_activity/list`, {
|
||||
limit: limit,
|
||||
page: page + 1,
|
||||
with_deleted: false,
|
||||
order_field: sorting[0].id,
|
||||
order_direction: sorting[0].desc === false ? 'ASC' : 'DESC',
|
||||
filter: JSON.stringify(dataFilter),
|
||||
token: parsedUser?.access_token,
|
||||
all_user: all_user
|
||||
});
|
||||
|
||||
return { data: response?.data.list, totalCount: response?.data.total_count };
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const doExportData = async (sorting: any, filter: any) => {
|
||||
const user = localStorage.getItem('brillian-bri-tl-auth-v1=9.1.1');
|
||||
const parsedUser = user ? JSON.parse(user) : null;
|
||||
sorting = sorting.length === 0 ? [{ id: 'user_activity.created_at', desc: false }] : sorting;
|
||||
filter = filter && filter.length > 0 ? filter : [];
|
||||
const auth = getAuth();
|
||||
let all_user = false;
|
||||
if (auth) {
|
||||
all_user = auth.role_name === 'Super Admin';
|
||||
}
|
||||
let dataFilter: any = {
|
||||
application: 'credit'
|
||||
};
|
||||
|
||||
if (filter && filter.length > 0) {
|
||||
filter.forEach((f: any) => {
|
||||
if (f.id === 'username') {
|
||||
dataFilter['username'] = { like: f.value };
|
||||
} else if (f.id === 'user_activity.created_at') {
|
||||
dataFilter['user_activity.created_at'] = f.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!dataFilter['user_activity.created_at']) {
|
||||
const defaultFrom = new Date(new Date().setMonth(new Date().getMonth() - 1));
|
||||
const defaultTo = new Date();
|
||||
|
||||
dataFilter['user_activity.created_at'] = {
|
||||
from: `${format(defaultFrom, 'yyyy-MM-dd')} 00:00:00`,
|
||||
to: `${format(defaultTo, 'yyyy-MM-dd')} 23:59:59`
|
||||
};
|
||||
}
|
||||
let order = 'user_activity.created_at';
|
||||
let param = {
|
||||
all_user: false,
|
||||
// order_field: sorting[0].id,
|
||||
order_field: order,
|
||||
order_direction: sorting[0].desc === false ? 'ASC' : 'DESC',
|
||||
filter: JSON.stringify(dataFilter),
|
||||
token: parsedUser?.access_token
|
||||
};
|
||||
console.log('param:', param);
|
||||
|
||||
let url = `${API_URL}/user_activity/export`;
|
||||
|
||||
GetExportData(url, param, 'user_activity_export_');
|
||||
|
||||
const createActivity = {
|
||||
module: 'Update Pengajuan Kredit',
|
||||
description: `Export Pengajuan Kredit => Log Activity`,
|
||||
action: 'E'
|
||||
};
|
||||
doSaveLogActivity(createActivity);
|
||||
};
|
||||
|
||||
return (
|
||||
<LogActivityContext.Provider
|
||||
value={{
|
||||
date,
|
||||
setDate,
|
||||
doExportData
|
||||
}}
|
||||
>
|
||||
<Toaster expand visibleToasts={9} duration={3000} />
|
||||
|
||||
<DataGridProvider
|
||||
columns={columns}
|
||||
pagination={{ size: 10 }}
|
||||
toolbar={<ListToolBar />}
|
||||
layout={{ card: true }}
|
||||
sorting={[{ id: 'UserActivity.created_at', desc: true }]}
|
||||
serverSide={true}
|
||||
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
|
||||
doGetListData(pageIndex, pageSize, sorting, columnFilters)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</DataGridProvider>
|
||||
</LogActivityContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { LogActivityContextProvider, LogActivityContext };
|
||||
2
src/pages/configuration/user/log-activity/hooks/index.ts
Normal file
2
src/pages/configuration/user/log-activity/hooks/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './LogActivityContext';
|
||||
export * from './useLogActivityContext';
|
||||
@ -0,0 +1,12 @@
|
||||
import { useContext } from 'react';
|
||||
import { LogActivityContext } from './LogActivityContext';
|
||||
|
||||
const useLogActivityContext = () => {
|
||||
const context = useContext(LogActivityContext);
|
||||
|
||||
if (!context) throw new Error('useLogActivityContext must be used within AuthProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useLogActivityContext };
|
||||
1
src/pages/configuration/user/log-activity/index.ts
Normal file
1
src/pages/configuration/user/log-activity/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './LogActivityPage';
|
||||
@ -0,0 +1,20 @@
|
||||
import { Container, DataGridInner } from '@/components';
|
||||
import { EditDialog } from './blocks';
|
||||
import { ManagePositionContextProvider } from './hooks';
|
||||
import { AddDialog } from './blocks/AddDialog';
|
||||
import { DeleteDialog } from './blocks/DeleteDialog';
|
||||
|
||||
export default function ManagePositionPage() {
|
||||
return (
|
||||
<ManagePositionContextProvider>
|
||||
<Container>
|
||||
<div className="grid gap-5 lg:gap-7.5">
|
||||
<DataGridInner />
|
||||
</div>
|
||||
<EditDialog />
|
||||
<AddDialog />
|
||||
<DeleteDialog />
|
||||
</Container>
|
||||
</ManagePositionContextProvider>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,186 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
import { useManagePositionContext } 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 { Checkbox } from '@/components/ui/checkbox';
|
||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
name: string;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
const MenuItemComponent: React.FC<{
|
||||
menu: MenuItem;
|
||||
selectMenus: string[];
|
||||
handleCheckboxChange: (id: string) => void;
|
||||
}> = ({ menu, selectMenus, handleCheckboxChange }) => {
|
||||
const { id, name, children = [] } = menu;
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<div className="text-sm flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={selectMenus.includes(id)}
|
||||
id={`label-${id}`}
|
||||
onCheckedChange={() => handleCheckboxChange(id)}
|
||||
/>
|
||||
<label htmlFor={`label-${id}`}>{name}</label>
|
||||
</div>
|
||||
{children.length > 0 && (
|
||||
<div className="pl-5">
|
||||
{children.map((child) => (
|
||||
<MenuItemComponent
|
||||
key={child.id}
|
||||
menu={child}
|
||||
selectMenus={selectMenus}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AddDialog = () => {
|
||||
const parentRef = useRef<any | null>(null);
|
||||
const { showAddDialog, handleAddDialog, menus } = useManagePositionContext();
|
||||
const { reload } = useDataGrid();
|
||||
const { PostData } = useCallApi();
|
||||
const [alert, setAlert] = useState({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
const [selectMenus, setSelectMenus] = useState<string[]>([]);
|
||||
const [formField, setFormField] = useState({
|
||||
name: ''
|
||||
});
|
||||
|
||||
/* actions */
|
||||
const handleCheckboxChange = useCallback((key: string) => {
|
||||
setSelectMenus((prev) =>
|
||||
prev.includes(key) ? prev.filter((item) => item !== key) : [...prev, key]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const doCreatePosition = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const response = await PostData(`${API_URL}/user_role/create`, {
|
||||
name: formField.name,
|
||||
roles: selectMenus,
|
||||
status: 'Y',
|
||||
application: 'credit'
|
||||
});
|
||||
|
||||
if (response?.status) {
|
||||
setAlert((prev) => ({ ...prev, show: false, message: '' }));
|
||||
handleAddDialog(false);
|
||||
toast.success('Success Create Position');
|
||||
reload();
|
||||
const createActivity = {
|
||||
module: 'Manage Position',
|
||||
description: `Create New User => ${formField.name}`,
|
||||
action: 'C'
|
||||
};
|
||||
|
||||
doSaveLogActivity(createActivity);
|
||||
} else {
|
||||
setAlert((prev) => ({ ...prev, show: true, message: response?.message }));
|
||||
}
|
||||
},
|
||||
[formField, selectMenus]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={showAddDialog} onOpenChange={(open) => handleAddDialog(open)}>
|
||||
<DialogContent className="container-fixed max-w-[1024px] flex flex-col p-5 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">
|
||||
Positions - Create
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer hover:opacity-100 opacity-50"
|
||||
onClick={() => {
|
||||
handleAddDialog(false);
|
||||
}}
|
||||
>
|
||||
<KeenIcon icon="cross" className="text-1.5xl" />
|
||||
</div>
|
||||
</div>
|
||||
</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={doCreatePosition}>
|
||||
<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"
|
||||
value={formField.name}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, name: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 w-full gap-5">
|
||||
{menus.map((menu) => (
|
||||
<div className="card " key={menu.id}>
|
||||
<div className="card-body p-5 pt-2">
|
||||
<MenuItemComponent
|
||||
menu={menu}
|
||||
selectMenus={selectMenus}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button className="btn btn-primary" type="submit">
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export { AddDialog };
|
||||
@ -0,0 +1,97 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog';
|
||||
import { useManagePositionContext } from '../hooks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, useDataGrid } from '@/components';
|
||||
import { ChangeEvent, useCallback, useState } from 'react';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { toast } from 'sonner';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
import { EnforceSwitch } from '@/components/switch';
|
||||
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
const DeleteDialog = () => {
|
||||
const { showDeleteDialog, handleDeleteDialog, selectedPosition } = useManagePositionContext();
|
||||
const { reload } = useDataGrid();
|
||||
const [enforce, setEnforce] = useState(false);
|
||||
|
||||
const { DeleteData } = useCallApi();
|
||||
const [alert, setAlert] = useState({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
|
||||
/* actions */
|
||||
const doDeleteData = useCallback(async () => {
|
||||
if (!selectedPosition) {
|
||||
toast.success('Please Select Position');
|
||||
return;
|
||||
}
|
||||
const response = await DeleteData(
|
||||
`${API_URL}/user_role/delete/${selectedPosition.id}/${enforce}`,
|
||||
{
|
||||
id: selectedPosition.id
|
||||
}
|
||||
);
|
||||
if (response?.status) {
|
||||
setAlert((prev) => ({ ...prev, show: false, message: '' }));
|
||||
handleDeleteDialog(false, null);
|
||||
toast.success('Success Delete Position');
|
||||
reload();
|
||||
const createActivity = {
|
||||
module: 'Manage Position',
|
||||
description: `Delete Position => ${selectedPosition.name}`,
|
||||
action: 'D'
|
||||
};
|
||||
|
||||
doSaveLogActivity(createActivity);
|
||||
} else {
|
||||
setAlert((prev) => ({ ...prev, show: true, message: response?.message }));
|
||||
}
|
||||
}, [selectedPosition, enforce]);
|
||||
|
||||
return (
|
||||
<Dialog open={showDeleteDialog} onOpenChange={(open) => handleDeleteDialog(open, null)}>
|
||||
<DialogContent className="container-fixed max-w-md flex flex-col p-5 overflow-hidden [&>button]:hidden">
|
||||
<DialogTitle></DialogTitle>
|
||||
<DialogHeader className="p-0 border-0 block">
|
||||
<Alert variant="warning">
|
||||
<h3 className="text-lg">Are you sure?</h3>
|
||||
<span className="text-sm">you will delete this data!</span>
|
||||
<div className="mt-2 flex items-center gap-x-2">
|
||||
<label className="form-label max-w-56">Hard Delete</label>
|
||||
<EnforceSwitch
|
||||
enforce={enforce}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setEnforce(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Alert>
|
||||
{alert.show && (
|
||||
<Alert variant="danger">
|
||||
<h3>{alert.message}</h3>
|
||||
</Alert>
|
||||
)}
|
||||
</DialogHeader>
|
||||
<DialogFooter className="flex justify-end items-center gap-4 mt-3">
|
||||
<Button variant={'outline'} onClick={() => handleDeleteDialog(false, null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant={'destructive'} onClick={() => doDeleteData()}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export { DeleteDialog };
|
||||
@ -0,0 +1,227 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog';
|
||||
import { useManagePositionContext } 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 { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
name: string;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
const MenuItemComponent: React.FC<{
|
||||
menu: MenuItem;
|
||||
selectMenus: string[];
|
||||
handleCheckboxChange: (id: string) => void;
|
||||
}> = ({ menu, selectMenus, handleCheckboxChange }) => {
|
||||
const { id, name, children = [] } = menu;
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<div className="text-sm flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={selectMenus.includes(id)}
|
||||
id={`label-${id}`}
|
||||
onCheckedChange={() => handleCheckboxChange(id)}
|
||||
/>
|
||||
<label htmlFor={`label-${id}`}>{name}</label>
|
||||
</div>
|
||||
{children.length > 0 && (
|
||||
<div className="pl-5">
|
||||
{children.map((child) => (
|
||||
<MenuItemComponent
|
||||
key={child.id}
|
||||
menu={child}
|
||||
selectMenus={selectMenus}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EditDialog = () => {
|
||||
const parentRef = useRef<any | null>(null);
|
||||
const { showEditDialog, handleEditDialog, menus, selectedPosition } = useManagePositionContext();
|
||||
const { reload } = useDataGrid();
|
||||
const { PutData } = useCallApi();
|
||||
const [alert, setAlert] = useState({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
const [selectMenus, setSelectMenus] = useState<string[]>([]);
|
||||
const [formField, setFormField] = useState({
|
||||
name: '',
|
||||
status: ''
|
||||
});
|
||||
|
||||
/* actions */
|
||||
const handleCheckboxChange = useCallback((key: string) => {
|
||||
setSelectMenus((prev) =>
|
||||
prev.includes(key) ? prev.filter((item) => item !== key) : [...prev, key]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const doEditPosition = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!selectedPosition) {
|
||||
toast.success('Please Select Position');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await PutData(`${API_URL}/user_role/update/${selectedPosition.id}`, {
|
||||
name: formField.name,
|
||||
roles: selectMenus,
|
||||
status: formField.status,
|
||||
application: 'credit'
|
||||
});
|
||||
|
||||
if (response?.status) {
|
||||
setAlert((prev) => ({ ...prev, show: false, message: '' }));
|
||||
handleEditDialog(false, null);
|
||||
toast.success('Success Update Position');
|
||||
reload();
|
||||
const createActivity = {
|
||||
module: 'Manage Position',
|
||||
description: `Edit Position => ${selectedPosition.name}`,
|
||||
action: 'U'
|
||||
};
|
||||
|
||||
doSaveLogActivity(createActivity);
|
||||
} else {
|
||||
setAlert((prev) => ({ ...prev, show: true, message: response?.message }));
|
||||
}
|
||||
},
|
||||
[formField, selectMenus, selectedPosition]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPosition) {
|
||||
setFormField((prev) => ({
|
||||
...prev,
|
||||
name: selectedPosition.name,
|
||||
status: selectedPosition.status
|
||||
}));
|
||||
|
||||
setSelectMenus(selectedPosition.roles);
|
||||
}
|
||||
}, [selectedPosition]);
|
||||
|
||||
return (
|
||||
<Dialog open={showEditDialog} onOpenChange={(open) => handleEditDialog(open, null)}>
|
||||
<DialogContent className="container-fixed max-w-[1024px] flex flex-col p-5 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">Positions - Edit</h1>
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer hover:opacity-100 opacity-50"
|
||||
onClick={() => handleEditDialog(false, null)}
|
||||
>
|
||||
<KeenIcon icon="cross" className="text-1.5xl" />
|
||||
</div>
|
||||
</div>
|
||||
</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={doEditPosition}>
|
||||
<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"
|
||||
value={formField.name}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, name: 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">Status</label>
|
||||
|
||||
<div className="grow">
|
||||
<Select
|
||||
value={formField.status}
|
||||
onValueChange={(status) => setFormField((prev) => ({ ...prev, status }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Y">Active</SelectItem>
|
||||
<SelectItem value="N">Non Active</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 w-full gap-5">
|
||||
{menus.map((menu) => (
|
||||
<div className="card" key={menu.id}>
|
||||
<div className="card-body p-5 pt-2">
|
||||
<MenuItemComponent
|
||||
menu={menu}
|
||||
selectMenus={selectMenus}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<hr />
|
||||
<div className="flex justify-end">
|
||||
<Button className="btn btn-primary" type="submit">
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export { EditDialog };
|
||||
@ -0,0 +1,43 @@
|
||||
import { DefaultTooltip, KeenIcon, useDataGrid } from '@/components';
|
||||
import { useManagePositionContext } from '../hooks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const ListToolBar = () => {
|
||||
const { table, reload } = useDataGrid();
|
||||
const { handleAddDialog } = useManagePositionContext();
|
||||
|
||||
return (
|
||||
<div className="card-header flex-wrap gap-2 border-b-0 px-5">
|
||||
<div className="flex flex-wrap gap-2 lg:gap-5 w-full">
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<label className="input input-sm w-1/6">
|
||||
<KeenIcon icon="magnifier" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search roles"
|
||||
value={(table.getColumn('name')?.getFilterValue() as string) ?? ''}
|
||||
onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="flex gap-3 items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-7.5 text-[0.8rem]"
|
||||
onClick={() => handleAddDialog(true)}
|
||||
>
|
||||
Add Data
|
||||
</Button>
|
||||
<DefaultTooltip title={'Refresh'} placement={'top'}>
|
||||
<Button variant="outline" className="h-7.5" onClick={() => reload()}>
|
||||
<KeenIcon icon="arrows-circle" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ListToolBar };
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './ListToolBar';
|
||||
export * from './EditDialog'
|
||||
@ -0,0 +1,204 @@
|
||||
import React, { createContext, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { DataGridColumnHeader, DataGridProvider, KeenIcon } from '@/components';
|
||||
import { EnforceSwitch } from '@/components/switch';
|
||||
import { ListToolBar } from '../blocks';
|
||||
import { useCallApi } from '@/hooks';
|
||||
|
||||
interface ContextProps {
|
||||
showEditDialog: boolean;
|
||||
handleEditDialog: (show: boolean, selected_user: selectedPosition | null) => void;
|
||||
showAddDialog: boolean;
|
||||
handleAddDialog: (show: boolean) => void;
|
||||
showDeleteDialog: boolean;
|
||||
handleDeleteDialog: (show: boolean, selected_user: selectedPosition | null) => void;
|
||||
selectedPosition: selectedPosition | null;
|
||||
menus: any[];
|
||||
}
|
||||
|
||||
interface selectedPosition {
|
||||
id: string;
|
||||
name: string;
|
||||
roles: string[];
|
||||
status: string;
|
||||
}
|
||||
|
||||
const initialProps: ContextProps = {
|
||||
showEditDialog: false,
|
||||
handleEditDialog: () => {},
|
||||
showAddDialog: false,
|
||||
handleAddDialog: () => {},
|
||||
showDeleteDialog: false,
|
||||
handleDeleteDialog: () => {},
|
||||
selectedPosition: null,
|
||||
menus: []
|
||||
};
|
||||
|
||||
const ManagePositionContext = createContext<ContextProps>(initialProps);
|
||||
|
||||
const API_URL = apiConfig.service_user;
|
||||
const API_URL_MD = apiConfig.service_master_data;
|
||||
|
||||
const ManagePositionContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
/* state */
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [selectedPosition, setSelectedPosition] = useState<selectedPosition | null>(null);
|
||||
const [menus, setMenus] = useState<any[]>([]);
|
||||
const { GetData } = useCallApi();
|
||||
|
||||
/* action */
|
||||
const handleEditDialog = useCallback((show: boolean, selected_user: selectedPosition | null) => {
|
||||
setSelectedPosition(show ? selected_user : null);
|
||||
setShowEditDialog(show);
|
||||
}, []);
|
||||
|
||||
const handleAddDialog = useCallback((show: boolean) => {
|
||||
setShowAddDialog(show);
|
||||
}, []);
|
||||
|
||||
const handleDeleteDialog = useCallback(
|
||||
(show: boolean, selected_user: selectedPosition | null) => {
|
||||
setSelectedPosition(show ? selected_user : null);
|
||||
setShowDeleteDialog(show);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/* Data Grid Options */
|
||||
const columns = useMemo<ColumnDef<any>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorFn: (row) => row.name,
|
||||
id: 'name',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Name" column={column} />,
|
||||
enableSorting: true,
|
||||
enableHiding: false
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.status,
|
||||
id: 'status',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Status" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<EnforceSwitch
|
||||
enforce={row.original.status == 'Y' ? true : false}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
headerClassName: 'w-[100px]',
|
||||
cellClassName: 'text-center'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
header: ({ column }) => <DataGridColumnHeader title="Action" column={column} />,
|
||||
cell: (data: any) => {
|
||||
const row = data.row.original;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm btn-icon btn-clear btn-light"
|
||||
onClick={() => handleEditDialog(true, row)}
|
||||
>
|
||||
<KeenIcon icon="notepad-edit" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-icon btn-clear btn-light"
|
||||
onClick={() => handleDeleteDialog(true, row)}
|
||||
>
|
||||
<KeenIcon icon="trash" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
headerClassName: 'w-[100px]',
|
||||
cellClassName: 'text-center'
|
||||
}
|
||||
}
|
||||
],
|
||||
[handleEditDialog, handleDeleteDialog]
|
||||
);
|
||||
|
||||
const doGetListData = async (page: number, limit: number, sorting: any, filter: any) => {
|
||||
sorting = sorting.length == 0 ? [{ id: 'name', desc: false }] : sorting;
|
||||
filter = filter.length == 0 ? {} : { any: filter[0].value.toLowerCase() };
|
||||
|
||||
const response = await GetData(`${API_URL}/user_role/list`, {
|
||||
limit: limit,
|
||||
page: page + 1,
|
||||
with_deleted: false,
|
||||
order_field: sorting[0].id,
|
||||
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC',
|
||||
filter: JSON.stringify({ ...filter, application: 'credit' })
|
||||
});
|
||||
|
||||
return { data: response?.data.list, totalCount: response?.data.total_count };
|
||||
};
|
||||
|
||||
const fetchMenus = useCallback(async () => {
|
||||
const params = {
|
||||
limit: 100,
|
||||
page: 1,
|
||||
with_deleted: false,
|
||||
order_field: 'order_number',
|
||||
order_direction: 'ASC',
|
||||
filter: JSON.stringify({ application: 'credit' })
|
||||
};
|
||||
const response = await GetData(`${API_URL_MD}/menu/list`, params);
|
||||
if (response?.status) {
|
||||
setMenus(() => [...response.data.list]);
|
||||
} else {
|
||||
setMenus(() => []);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMenus();
|
||||
}, [fetchMenus]);
|
||||
|
||||
return (
|
||||
<ManagePositionContext.Provider
|
||||
value={{
|
||||
showEditDialog,
|
||||
handleEditDialog,
|
||||
selectedPosition,
|
||||
showAddDialog,
|
||||
handleAddDialog,
|
||||
menus,
|
||||
showDeleteDialog,
|
||||
handleDeleteDialog
|
||||
}}
|
||||
>
|
||||
<Toaster expand visibleToasts={9} duration={3000} />
|
||||
|
||||
<DataGridProvider
|
||||
columns={columns}
|
||||
pagination={{ size: 10 }}
|
||||
toolbar={<ListToolBar />}
|
||||
layout={{ card: true }}
|
||||
sorting={[{ id: 'name', desc: false }]}
|
||||
serverSide={true}
|
||||
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
|
||||
doGetListData(pageIndex, pageSize, sorting, columnFilters)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</DataGridProvider>
|
||||
</ManagePositionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { ManagePositionContextProvider, ManagePositionContext };
|
||||
export type { selectedPosition };
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './ManagePositionContext';
|
||||
export * from './useManagePositionContext';
|
||||
@ -0,0 +1,12 @@
|
||||
import { useContext } from 'react';
|
||||
import { ManagePositionContext } from './ManagePositionContext';
|
||||
|
||||
const useManagePositionContext = () => {
|
||||
const context = useContext(ManagePositionContext);
|
||||
|
||||
if (!context) throw new Error('useManagePositionContext must be used within ManagePositionContextProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useManagePositionContext };
|
||||
1
src/pages/configuration/user/manage-position/index.ts
Normal file
1
src/pages/configuration/user/manage-position/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './ManagePositionPage';
|
||||
20
src/pages/configuration/user/user/ConfigUserPage.tsx
Normal file
20
src/pages/configuration/user/user/ConfigUserPage.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { Container, DataGridInner } from '@/components';
|
||||
import { EditDialog } from './blocks';
|
||||
import { ConfigUserContextProvider } from './hooks';
|
||||
import { AddDialog } from './blocks/AddDialog';
|
||||
import { DeleteDialog } from './blocks/DeleteDialog';
|
||||
|
||||
export default function ConfigUserPage() {
|
||||
return (
|
||||
<ConfigUserContextProvider>
|
||||
<Container>
|
||||
<div className="grid gap-5 lg:gap-7.5">
|
||||
<DataGridInner />
|
||||
</div>
|
||||
<EditDialog />
|
||||
<AddDialog />
|
||||
<DeleteDialog />
|
||||
</Container>
|
||||
</ConfigUserContextProvider>
|
||||
);
|
||||
}
|
||||
354
src/pages/configuration/user/user/blocks/AddDialog.tsx
Normal file
354
src/pages/configuration/user/user/blocks/AddDialog.tsx
Normal file
@ -0,0 +1,354 @@
|
||||
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 { 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';
|
||||
|
||||
interface CreateUserParams {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
retype_password: string;
|
||||
name: string;
|
||||
id_role: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const API_URL = apiConfig.service_user;
|
||||
type PasswordType = 'password' | 'retype_password';
|
||||
|
||||
const AddDialog = () => {
|
||||
const parentRef = useRef<any | null>(null);
|
||||
const { showAddDialog, handleAddDialog, roles } = useUserContext();
|
||||
const { reload } = useDataGrid();
|
||||
const { PostData, PutData } = useCallApi();
|
||||
const [alert, setAlert] = useState({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
const initialState = {
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
retype_password: '',
|
||||
name: '',
|
||||
id_role: '',
|
||||
status: ''
|
||||
};
|
||||
const [formField, setFormField] = useState(initialState);
|
||||
const resetForm = () => {
|
||||
setFormField(initialState);
|
||||
};
|
||||
const [showPassword, setShowPassword] = useState({
|
||||
password: false,
|
||||
retype_password: false
|
||||
});
|
||||
|
||||
const [messagePassword, setMessagePassword] = useState(true);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([]);
|
||||
|
||||
const validatePassword = (password: string, confirmPassword: string) => {
|
||||
const errors: 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) {
|
||||
errors.push('Passwords do not match');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const validation = validatePassword(formField.password, formField.retype_password);
|
||||
setMessagePassword(validation.isValid);
|
||||
setPasswordErrors(validation.errors);
|
||||
}, [formField.password, formField.retype_password]);
|
||||
|
||||
const isButtonDisabled = !messagePassword || isSubmitting || passwordErrors.length > 0;
|
||||
|
||||
/* actions */
|
||||
const doCreateUser = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const response = await PostData(`${API_URL}/user/create`, {
|
||||
...formField,
|
||||
id_role: undefined
|
||||
});
|
||||
if (response?.status) {
|
||||
const responseUserAddRole = await PutData(
|
||||
`${API_URL}/user/add_role/${response?.message?.id}/${formField.id_role}`,
|
||||
{}
|
||||
);
|
||||
setAlert((prev) => ({ ...prev, show: false, message: '' }));
|
||||
handleAddDialog(false);
|
||||
toast.success('Success Create User');
|
||||
reload();
|
||||
const createActivity = {
|
||||
module: 'Manage User',
|
||||
description: `Create New User => ${formField.username}`,
|
||||
action: 'C'
|
||||
};
|
||||
|
||||
doSaveLogActivity(createActivity);
|
||||
} else {
|
||||
setAlert((prev) => ({ ...prev, show: true, message: response?.message }));
|
||||
}
|
||||
},
|
||||
[formField]
|
||||
);
|
||||
|
||||
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-5 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 className="flex items-center gap-2 text-sm font-normal text-gray-700"></div>
|
||||
</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 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={doCreateUser}>
|
||||
<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">
|
||||
<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">Username</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={formField.username}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, username: target.value }))
|
||||
}
|
||||
/>
|
||||
</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) => setFormField((prev) => ({ ...prev, id_role }))}
|
||||
>
|
||||
<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) => setFormField((prev) => ({ ...prev, status }))}
|
||||
>
|
||||
<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}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export { AddDialog };
|
||||
83
src/pages/configuration/user/user/blocks/DeleteDialog.tsx
Normal file
83
src/pages/configuration/user/user/blocks/DeleteDialog.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader } from '@/components/ui/dialog';
|
||||
import { useUserContext } from '../hooks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, useDataGrid } from '@/components';
|
||||
import { ChangeEvent, useCallback, useState } from 'react';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { toast } from 'sonner';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
import { EnforceSwitch } from '@/components/switch';
|
||||
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
const DeleteDialog = () => {
|
||||
const { showDeleteDialog, handleDeleteDialog, selectedUser } = useUserContext();
|
||||
const { reload } = useDataGrid();
|
||||
const [enforce, setEnforce] = useState(false);
|
||||
|
||||
const { DeleteData } = useCallApi();
|
||||
const [alert, setAlert] = useState({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
|
||||
/* actions */
|
||||
const doDeleteData = useCallback(async () => {
|
||||
const response = await DeleteData(`${API_URL}/user/delete/${selectedUser}/${enforce}`, {
|
||||
id: selectedUser
|
||||
});
|
||||
if (response?.status) {
|
||||
setAlert((prev) => ({ ...prev, show: false, message: '' }));
|
||||
handleDeleteDialog(false, null);
|
||||
toast.success('Success Delete User');
|
||||
reload();
|
||||
const createActivity = {
|
||||
module: 'Manage User',
|
||||
description: `Delete User => ${selectedUser}`,
|
||||
action: 'D'
|
||||
};
|
||||
|
||||
doSaveLogActivity(createActivity);
|
||||
} else {
|
||||
setAlert((prev) => ({ ...prev, show: true, message: response?.message }));
|
||||
}
|
||||
}, [selectedUser, enforce]);
|
||||
|
||||
return (
|
||||
<Dialog open={showDeleteDialog} onOpenChange={(open) => handleDeleteDialog(open, null)}>
|
||||
<DialogContent className="container-fixed max-w-md flex flex-col p-5 overflow-hidden [&>button]:hidden">
|
||||
<DialogHeader className="p-0 border-0 block">
|
||||
<Alert variant="warning">
|
||||
<h3 className="text-lg">Are you sure?</h3>
|
||||
<span className="text-sm">you will delete this data!</span>
|
||||
<div className="mt-2 flex items-center gap-x-2">
|
||||
<label className="form-label max-w-56">Hard Delete</label>
|
||||
<EnforceSwitch
|
||||
enforce={enforce}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setEnforce(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Alert>
|
||||
{alert.show && (
|
||||
<Alert variant="danger">
|
||||
<h3>{alert.message}</h3>
|
||||
</Alert>
|
||||
)}
|
||||
</DialogHeader>
|
||||
<DialogFooter className="flex justify-end items-center gap-4 mt-3">
|
||||
<Button variant={'outline'} onClick={() => handleDeleteDialog(false, null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant={'destructive'} onClick={() => doDeleteData()}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export { DeleteDialog };
|
||||
244
src/pages/configuration/user/user/blocks/EditDialog.tsx
Normal file
244
src/pages/configuration/user/user/blocks/EditDialog.tsx
Normal file
@ -0,0 +1,244 @@
|
||||
import { 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 { 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';
|
||||
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
const EditDialog = () => {
|
||||
const parentRef = useRef<any | null>(null);
|
||||
const { showEditDialog, selectedUser, handleEditDialog, roles } = useUserContext();
|
||||
const { reload } = useDataGrid();
|
||||
const { GetData, PutData } = useCallApi();
|
||||
const [alert, setAlert] = useState({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
const [formField, setFormField] = useState({
|
||||
name: '',
|
||||
username: '',
|
||||
email: '',
|
||||
id_role: '',
|
||||
id_role_old: '',
|
||||
status: ''
|
||||
});
|
||||
|
||||
/* actions */
|
||||
const doUpdateUser = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const response = await PutData(`${API_URL}/user/update/${selectedUser}`, {
|
||||
...formField,
|
||||
id_role: undefined,
|
||||
id_role_old: undefined
|
||||
});
|
||||
if (response?.status) {
|
||||
if (formField.id_role_old != '') {
|
||||
const responseUserDeleteRole = await PutData(
|
||||
`${API_URL}/user/delete_role/${selectedUser}/${formField.id_role_old}`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
const responseUserAddRole = await PutData(
|
||||
`${API_URL}/user/add_role/${selectedUser}/${formField.id_role}`,
|
||||
{}
|
||||
);
|
||||
setAlert((prev) => ({ ...prev, show: false, message: '' }));
|
||||
handleEditDialog(false, null);
|
||||
toast.success('Success Update User');
|
||||
reload();
|
||||
const createActivity = {
|
||||
module: 'Manage User',
|
||||
description: `Edit User => ${formField.username}`,
|
||||
action: 'U'
|
||||
};
|
||||
|
||||
doSaveLogActivity(createActivity);
|
||||
} else {
|
||||
setAlert((prev) => ({ ...prev, show: true, message: response?.message }));
|
||||
}
|
||||
},
|
||||
[selectedUser, formField]
|
||||
);
|
||||
|
||||
const doFetchUserData = useCallback(async (id: string) => {
|
||||
const response = await GetData(`${API_URL}/user/detail/${id}`, { id });
|
||||
if (response?.status) {
|
||||
let id_role = response.data.roles.filter((item: any) => item.application.id == 'credit');
|
||||
|
||||
setFormField((prev) => ({
|
||||
...prev,
|
||||
name: response.data.name,
|
||||
username: response.data.username,
|
||||
email: response.data.email,
|
||||
id_role: id_role.length != 0 ? id_role[0].id : '',
|
||||
id_role_old: id_role.length != 0 ? id_role[0].id : '',
|
||||
status: response.data.status
|
||||
}));
|
||||
} else {
|
||||
setFormField((prev) => ({
|
||||
...prev,
|
||||
name: '',
|
||||
username: '',
|
||||
email: '',
|
||||
id_role: '0',
|
||||
id_role_old: '',
|
||||
status: ''
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedUser) {
|
||||
doFetchUserData(selectedUser);
|
||||
}
|
||||
}, [selectedUser]);
|
||||
|
||||
return (
|
||||
<Dialog open={showEditDialog} onOpenChange={(open) => handleEditDialog(open, null)}>
|
||||
<DialogContent className="container-fixed max-w-[768px] flex flex-col p-5 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 - Update</h1>
|
||||
<div className="flex items-center gap-2 text-sm font-normal text-gray-700"></div>
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer hover:opacity-100 opacity-50"
|
||||
onClick={() => handleEditDialog(false, null)}
|
||||
>
|
||||
<KeenIcon icon="cross" className="text-1.5xl" />
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<DialogBody className="scrollable-y px-0 pb-0" ref={parentRef}>
|
||||
<div className="flex flex-col px-0">
|
||||
{alert.show && (
|
||||
<Alert variant="danger">
|
||||
<h3>{alert.message}</h3>
|
||||
</Alert>
|
||||
)}
|
||||
<form onSubmit={doUpdateUser}>
|
||||
<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"
|
||||
value={formField.name}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, name: target.value }))
|
||||
}
|
||||
/>
|
||||
</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">Username</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formField.username}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, username: target.value }))
|
||||
}
|
||||
/>
|
||||
</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"
|
||||
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) => setFormField((prev) => ({ ...prev, id_role }))}
|
||||
>
|
||||
<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) => setFormField((prev) => ({ ...prev, status }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Y">Active</SelectItem>
|
||||
<SelectItem value="N">Non Active</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button className="btn btn-primary">Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export { EditDialog };
|
||||
57
src/pages/configuration/user/user/blocks/ListToolBar.tsx
Normal file
57
src/pages/configuration/user/user/blocks/ListToolBar.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { DefaultTooltip, KeenIcon, useDataGrid } from '@/components';
|
||||
import { useUserContext } from '../hooks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const ListToolBar = () => {
|
||||
const { table, reload } = useDataGrid();
|
||||
const { handleAddDialog } = useUserContext();
|
||||
|
||||
return (
|
||||
<div className="card-header flex-wrap gap-2 border-b-0 px-5">
|
||||
<div className="flex flex-wrap gap-2 lg:gap-5 w-full">
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div className="flex w-[50%] gap-3 items-center">
|
||||
<label className="input input-sm w-1/3">
|
||||
<KeenIcon icon="magnifier" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search users"
|
||||
value={(table.getColumn('username')?.getFilterValue() as string) ?? ''}
|
||||
onChange={(event) =>
|
||||
table.getColumn('username')?.setFilterValue(event.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<DefaultTooltip title={'Filter'} placement={'top'}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-7.5 disabled:bg-gray-400"
|
||||
// disabled={isLoading}
|
||||
// onClick={handleFilterData}
|
||||
>
|
||||
{/* {loadingButton === 'filter' ? <ContentLoader /> : <KeenIcon icon="filter" />} */}
|
||||
<KeenIcon icon="filter" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-7.5 text-[0.8rem]"
|
||||
onClick={() => handleAddDialog(true)}
|
||||
>
|
||||
Add Data
|
||||
</Button>
|
||||
<DefaultTooltip title={'Refresh'} placement={'top'}>
|
||||
<Button variant="outline" className="h-7.5" onClick={() => reload()}>
|
||||
<KeenIcon icon="arrows-circle" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ListToolBar };
|
||||
2
src/pages/configuration/user/user/blocks/index.ts
Normal file
2
src/pages/configuration/user/user/blocks/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './ListToolBar';
|
||||
export * from './EditDialog';
|
||||
252
src/pages/configuration/user/user/hooks/ConfigUserContext.tsx
Normal file
252
src/pages/configuration/user/user/hooks/ConfigUserContext.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
import React, { createContext, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { DataGridColumnHeader, DataGridProvider, KeenIcon } from '@/components';
|
||||
import { EnforceSwitch } from '@/components/switch';
|
||||
import { ListToolBar } from '../blocks';
|
||||
import { useCallApi } from '@/hooks';
|
||||
|
||||
interface ContextProps {
|
||||
showEditDialog: boolean;
|
||||
handleEditDialog: (show: boolean, selected_user: string | null) => void;
|
||||
showAddDialog: boolean;
|
||||
handleAddDialog: (show: boolean) => void;
|
||||
showDeleteDialog: boolean;
|
||||
handleDeleteDialog: (show: boolean, selected_user: string | null) => void;
|
||||
selectedUser: string | null;
|
||||
roles: RoleListProps[];
|
||||
}
|
||||
|
||||
interface SelectedUser {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
username: string;
|
||||
role: string;
|
||||
new_password: string;
|
||||
check_new_password: string;
|
||||
}
|
||||
|
||||
interface RoleListProps {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const initialProps: ContextProps = {
|
||||
showEditDialog: false,
|
||||
handleEditDialog: () => {},
|
||||
showAddDialog: false,
|
||||
handleAddDialog: () => {},
|
||||
showDeleteDialog: false,
|
||||
handleDeleteDialog: () => {},
|
||||
selectedUser: null,
|
||||
roles: []
|
||||
};
|
||||
|
||||
const ConfigUserContext = createContext<ContextProps>(initialProps);
|
||||
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
const ConfigUserContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
/* state */
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
||||
const [roles, setRoles] = useState<RoleListProps[]>([]);
|
||||
const { GetData } = useCallApi();
|
||||
|
||||
/* action */
|
||||
const handleEditDialog = useCallback((show: boolean, selected_user: string | null) => {
|
||||
setSelectedUser(show ? selected_user : null);
|
||||
setShowEditDialog(show);
|
||||
}, []);
|
||||
|
||||
const handleAddDialog = useCallback((show: boolean) => {
|
||||
setShowAddDialog(show);
|
||||
}, []);
|
||||
|
||||
const handleDeleteDialog = useCallback((show: boolean, selected_user: string | null) => {
|
||||
setSelectedUser(show ? selected_user : null);
|
||||
setShowDeleteDialog(show);
|
||||
}, []);
|
||||
|
||||
/* Data Grid Options */
|
||||
const columns = useMemo<ColumnDef<any>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorFn: (row) => row.username,
|
||||
id: 'username',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Username" column={column} />,
|
||||
enableSorting: true,
|
||||
enableHiding: false
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.email,
|
||||
id: 'email',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Email" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-[350px]'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.name,
|
||||
id: 'name',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Name" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-[350px]'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.role_name,
|
||||
id: 'role_name',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Role Name" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: (data: any) => {
|
||||
const { roles } = data.row.original;
|
||||
|
||||
let html = <p className="text-danger italic">Unassigned</p>;
|
||||
for (let _role of roles) {
|
||||
if (_role.application.id == 'credit') {
|
||||
html = _role.name;
|
||||
}
|
||||
}
|
||||
return html;
|
||||
},
|
||||
meta: {
|
||||
headerClassName: 'w-[350px]'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.status,
|
||||
id: 'status',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Status" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<EnforceSwitch
|
||||
enforce={row.original.status == 'Y' ? true : false}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
headerClassName: 'w-[100px]',
|
||||
cellClassName: 'text-center'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
header: ({ column }) => <DataGridColumnHeader title="Action" column={column} />,
|
||||
cell: (data: any) => {
|
||||
const row = data.row.original;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm btn-icon btn-clear btn-light"
|
||||
onClick={() => handleEditDialog(true, row.id)}
|
||||
>
|
||||
<KeenIcon icon="notepad-edit" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-icon btn-clear btn-light"
|
||||
onClick={() => handleDeleteDialog(true, row.id)}
|
||||
>
|
||||
<KeenIcon icon="trash" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
headerClassName: 'w-[100px]',
|
||||
cellClassName: 'text-center'
|
||||
}
|
||||
}
|
||||
],
|
||||
[handleEditDialog, handleDeleteDialog]
|
||||
);
|
||||
|
||||
const doGetListData = async (page: number, limit: number, sorting: any, filter: any) => {
|
||||
sorting = sorting.length == 0 ? [{ id: 'username', desc: false }] : sorting;
|
||||
filter = filter.length == 0 ? {} : { any: filter[0].value.toLowerCase() };
|
||||
const response = await GetData(`${API_URL}/user/list`, {
|
||||
limit: limit,
|
||||
page: page + 1,
|
||||
with_deleted: true,
|
||||
order_field: sorting[0].id,
|
||||
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC',
|
||||
filter: JSON.stringify(filter)
|
||||
});
|
||||
|
||||
return { data: response?.data.list, totalCount: response?.data.total_count };
|
||||
};
|
||||
|
||||
const fetchRoles = useCallback(async () => {
|
||||
const params = {
|
||||
limit: 100,
|
||||
page: 1,
|
||||
with_deleted: false,
|
||||
order_field: 'name',
|
||||
order_direction: 'ASC',
|
||||
filter: JSON.stringify({
|
||||
status: 'Y',
|
||||
application: 'credit'
|
||||
})
|
||||
};
|
||||
const response = await GetData(`${API_URL}/user_role/list`, params);
|
||||
if (response?.status) {
|
||||
setRoles(() => [...response.data.list]);
|
||||
} else {
|
||||
setRoles(() => []);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoles();
|
||||
}, [fetchRoles]);
|
||||
|
||||
return (
|
||||
<ConfigUserContext.Provider
|
||||
value={{
|
||||
showEditDialog,
|
||||
handleEditDialog,
|
||||
selectedUser,
|
||||
showAddDialog,
|
||||
handleAddDialog,
|
||||
roles,
|
||||
showDeleteDialog,
|
||||
handleDeleteDialog
|
||||
}}
|
||||
>
|
||||
<Toaster expand visibleToasts={9} duration={3000} />
|
||||
|
||||
<DataGridProvider
|
||||
columns={columns}
|
||||
pagination={{ size: 10 }}
|
||||
toolbar={<ListToolBar />}
|
||||
layout={{ card: true }}
|
||||
sorting={[{ id: 'username', desc: false }]}
|
||||
serverSide={true}
|
||||
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
|
||||
doGetListData(pageIndex, pageSize, sorting, columnFilters)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</DataGridProvider>
|
||||
</ConfigUserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { ConfigUserContextProvider, ConfigUserContext };
|
||||
export type { SelectedUser };
|
||||
2
src/pages/configuration/user/user/hooks/index.ts
Normal file
2
src/pages/configuration/user/user/hooks/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './ConfigUserContext';
|
||||
export * from './useUserContext';
|
||||
12
src/pages/configuration/user/user/hooks/useUserContext.tsx
Normal file
12
src/pages/configuration/user/user/hooks/useUserContext.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { useContext } from 'react';
|
||||
import { ConfigUserContext } from './ConfigUserContext';
|
||||
|
||||
const useUserContext = () => {
|
||||
const context = useContext(ConfigUserContext);
|
||||
|
||||
if (!context) throw new Error('useUserContext must be used within AuthProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useUserContext };
|
||||
1
src/pages/configuration/user/user/index.ts
Normal file
1
src/pages/configuration/user/user/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './ConfigUserPage';
|
||||
200
src/pages/dashboards/home/DashboardHomePage.tsx
Normal file
200
src/pages/dashboards/home/DashboardHomePage.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
import { Container, KeenIcon, DefaultTooltip } from '@/components';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Chart, YearPicker } from './blocks';
|
||||
import moment from 'moment';
|
||||
import { useFetchCardData, useFetchChartData } from './hooks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useFetchYear } from './hooks/useFetchYear';
|
||||
import { get5LastYear } from '@/utils/Date';
|
||||
// sum -> nominal, count-> total
|
||||
type CountType = 'sum' | 'count';
|
||||
type ChartType = 'line' | 'bar';
|
||||
type ChartLegend = 'true' | 'false';
|
||||
|
||||
const DashboardHomePage = () => {
|
||||
const selectYear = get5LastYear();
|
||||
const [initialYear, setInitialYear] = useState<string>('');
|
||||
const [selectedYear, setSelectedYear] = useState<string>('');
|
||||
const [count, setCount] = useState<CountType>('sum');
|
||||
const [chartType, setChartType] = useState<ChartType>('line');
|
||||
const [chartLegend, setChartLegend] = useState<ChartLegend>('true');
|
||||
const [dateRange, setDateRange] = useState<{ from: Date; to: Date }>({
|
||||
from: new Date(),
|
||||
to: new Date()
|
||||
});
|
||||
|
||||
// Menyusun tanggal awal dan akhir berdasarkan selectedYear
|
||||
useEffect(() => {
|
||||
if (selectYear.length > 0 && !selectedYear) {
|
||||
let latestYear = Math.max(...selectYear.map((item) => parseInt(item, 10))).toString();
|
||||
console.log('latestYear :', latestYear);
|
||||
setSelectedYear(latestYear);
|
||||
setInitialYear(latestYear);
|
||||
}
|
||||
}, [selectYear, selectedYear]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedYear) {
|
||||
setDateRange({
|
||||
from: moment(`${selectedYear}-01-01`, 'YYYY-MM-DD').toDate(),
|
||||
to: moment(`${selectedYear}-12-31`, 'YYYY-MM-DD').toDate()
|
||||
});
|
||||
}
|
||||
}, [selectedYear]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialYear) {
|
||||
setDateRange({
|
||||
from: moment(`${initialYear}-01-01`, 'YYYY-MM-DD').toDate(),
|
||||
to: moment(`${initialYear}-12-31`, 'YYYY-MM-DD').toDate()
|
||||
});
|
||||
}
|
||||
}, [initialYear]);
|
||||
|
||||
const handleYearChange = (year: string) => {
|
||||
setSelectedYear(year);
|
||||
};
|
||||
|
||||
const handleCountType = (value: CountType) => {
|
||||
setCount(value);
|
||||
};
|
||||
|
||||
const handleChartType = (value: ChartType) => {
|
||||
setChartType(value);
|
||||
};
|
||||
|
||||
const handleChartLegend = (value: ChartLegend) => {
|
||||
setChartLegend(value);
|
||||
};
|
||||
|
||||
// const handleFilter = useCallback(
|
||||
// (date: DateRange | undefined) => {
|
||||
// setDateRange({
|
||||
// from: date?.from ?? moment(`${selectedYear}-01-01`, 'YYYY-MM-DD').toDate(),
|
||||
// to: date?.to ?? moment(`${selectedYear}-12-31`, 'YYYY-MM-DD').toDate()
|
||||
// });
|
||||
// },
|
||||
// [selectedYear]
|
||||
// );
|
||||
|
||||
const resetFilter = useCallback(() => {
|
||||
setDateRange({
|
||||
from: moment(`${initialYear}-01-01`, 'YYYY-MM-DD').toDate(),
|
||||
to: moment(`${initialYear}-12-31`, 'YYYY-MM-DD').toDate()
|
||||
});
|
||||
setSelectedYear(initialYear);
|
||||
setCount('sum');
|
||||
setChartType('line');
|
||||
setChartLegend('true');
|
||||
}, [initialYear]);
|
||||
|
||||
const { cardData } = useFetchCardData(
|
||||
moment(dateRange.from).format('YYYY-MM-DD'),
|
||||
moment(dateRange.to).format('YYYY-MM-DD'),
|
||||
count
|
||||
);
|
||||
|
||||
const { chartData } = useFetchChartData(
|
||||
moment(dateRange.from).format('YYYY-MM-DD'),
|
||||
moment(dateRange.to).format('YYYY-MM-DD'),
|
||||
count
|
||||
);
|
||||
|
||||
const toolbar = (
|
||||
<div className="flex gap-3 items-center w-1/2">
|
||||
<div className="w-auto min-w-[120px]">
|
||||
<Select value={chartType} onValueChange={handleChartType}>
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue placeholder="Select Chart Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-32">
|
||||
<SelectItem value="line">Line</SelectItem>
|
||||
<SelectItem value="bar">Bar</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-auto min-w-[120px]">
|
||||
<Select value={chartLegend} onValueChange={handleChartLegend}>
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue placeholder="Select Legend Visibility" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-full">
|
||||
<SelectItem value="true">Show Legend</SelectItem>
|
||||
<SelectItem value="false">Hide Legend</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="flex gap-3 items-center mb-6">
|
||||
<YearPicker selectedYear={selectedYear} setSelectedYear={handleYearChange} />
|
||||
<div className="w-auto min-w-[120px]">
|
||||
<Select value={count} onValueChange={handleCountType}>
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue placeholder="Select Count Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-32">
|
||||
<SelectItem value="sum">Nominal</SelectItem>
|
||||
<SelectItem value="count">Status</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* <DefaultTooltip title="Filter" placement="top">
|
||||
<Button variant="outline" className="h-7.5" onClick={() => handleFilter(dateRange)}>
|
||||
<KeenIcon icon="filter" />
|
||||
</Button>
|
||||
</DefaultTooltip> */}
|
||||
<DefaultTooltip title="Reset Filter" placement="top">
|
||||
<Button variant="outline" className="h-7.5" onClick={resetFilter}>
|
||||
<KeenIcon icon="arrow-circle-left" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="grid gap-5 lg:gap-7.5 mb-5">
|
||||
<div className="grid lg:grid-cols-4 gap-y-5 lg:gap-5 items-stretch">
|
||||
{cardData.map((data: any) => (
|
||||
<div className="lg:col-span-1" key={data.type}>
|
||||
<Card
|
||||
title={data.type.toUpperCase()}
|
||||
total={data.total}
|
||||
type={data.type}
|
||||
count={count}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<div className="grid gap-5 lg:gap-7.5">
|
||||
<div className="grid lg:grid-cols-1 gap-y-5 lg:gap-5 items-stretch">
|
||||
<div className="lg:col-span-1">
|
||||
<Chart
|
||||
title="Overview"
|
||||
count={count}
|
||||
toolbar={toolbar}
|
||||
chartData={chartData}
|
||||
chartType={chartType}
|
||||
chartLegend={chartLegend}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardHomePage;
|
||||
52
src/pages/dashboards/home/blocks/Card.tsx
Normal file
52
src/pages/dashboards/home/blocks/Card.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { useLanguage } from '@/i18n';
|
||||
import { fCurrency } from '@/utils/FormatNumber';
|
||||
import { toAbsoluteUrl } from '@/utils/Assets';
|
||||
|
||||
interface CardDataProduct {
|
||||
title: string;
|
||||
total: string;
|
||||
type: Array<{ label: string; value: string; total: string }>;
|
||||
count: 'sum' | 'count';
|
||||
}
|
||||
|
||||
const Card = ({ title, total, type, count }: CardDataProduct) => {
|
||||
const { isRTL } = useLanguage();
|
||||
|
||||
return (
|
||||
<div className="card h-full bg-[length:85%] bg-[length:85%] [background-position:9rem_-4rem] rtl:[background-position:-4rem_-4rem] bg-no-repeat channel-stats-bg">
|
||||
<div className="card-body flex flex-col gap-4 p-5 lg:p-b-7.5 lg:pt-4">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-col w-8/12" style={{ background: '' }}>
|
||||
<span className="text-sm font-normal text-gray-600 mb-5">{title}</span>
|
||||
<span className="text-3xl font-semibold text-gray-900 mb-0">
|
||||
{count === 'sum' ? fCurrency(total) : total}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col w-4/12 justify-between" style={{ background: '' }}>
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/file-types/chart.svg')}
|
||||
className="dark:hidden h-5 h-8"
|
||||
alt="Chart Icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <hr className="" />
|
||||
<div className="">
|
||||
{type.map((data: any, index: number) => (
|
||||
<div key={data.label}>
|
||||
<div className="flex justify-between mb-1">
|
||||
<div className="w-8/12 text-sm">{data.label}</div>
|
||||
<div className="w-4/12 text-sm text-end">
|
||||
{count === 'sum' ? fCurrency(data.total) : data.total}{' '}
|
||||
</div>
|
||||
</div>
|
||||
{index !== type.length - 1 && <hr className="border-dashed my-2" />}
|
||||
</div>
|
||||
))}
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Card };
|
||||
166
src/pages/dashboards/home/blocks/Chart.tsx
Normal file
166
src/pages/dashboards/home/blocks/Chart.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import ApexChart from 'react-apexcharts';
|
||||
import { ApexOptions } from 'apexcharts';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { fCurrency } from '@/utils/FormatNumber';
|
||||
|
||||
interface Series {
|
||||
name: string;
|
||||
data: any[];
|
||||
}
|
||||
|
||||
const Chart = ({
|
||||
title,
|
||||
toolbar,
|
||||
chartType,
|
||||
chartLegend,
|
||||
subtitle,
|
||||
number,
|
||||
count,
|
||||
chartData = []
|
||||
}: any) => {
|
||||
const [series, setSeries] = useState<Series[]>([]);
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
|
||||
let legendOpt = null;
|
||||
if (chartLegend == 'true') {
|
||||
legendOpt = true;
|
||||
} else {
|
||||
legendOpt = false;
|
||||
}
|
||||
const options: ApexOptions = {
|
||||
chart: {
|
||||
type: 'area',
|
||||
toolbar: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: '50%'
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
offsetY: -10,
|
||||
offsetX: chartType === 'bar' ? 1.5 : 0,
|
||||
formatter: (value: any) => {
|
||||
if (count == 'sum') {
|
||||
return fCurrency(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
markers: {
|
||||
size: 0,
|
||||
shape: 'circle'
|
||||
},
|
||||
xaxis: {
|
||||
categories: categories,
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'var(--tw-gray-500)',
|
||||
fontSize: '12px'
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'var(--tw-gray-500)',
|
||||
fontSize: '12px'
|
||||
},
|
||||
formatter: (value: any) => {
|
||||
if (count == 'sum') {
|
||||
return fCurrency(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
borderColor: 'var(--tw-gray-200)',
|
||||
strokeDashArray: 5,
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 20,
|
||||
left: 0
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
shared: true,
|
||||
intersect: false,
|
||||
y: {
|
||||
formatter: (value: any) => {
|
||||
if (count == 'sum') {
|
||||
return fCurrency(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
curve: 'smooth',
|
||||
lineCap: 'butt',
|
||||
colors: undefined,
|
||||
width: 3,
|
||||
dashArray: 0
|
||||
},
|
||||
legend: {
|
||||
show: legendOpt,
|
||||
position: 'right',
|
||||
floating: false
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (chartData && chartData.length !== 0) {
|
||||
const categories = chartData.map((item: any) => item.month);
|
||||
const statusNames = Object.keys(chartData[0]).filter((key) => key !== 'month');
|
||||
const series = statusNames.map((status) => {
|
||||
const cleanName = status
|
||||
.replace('_count', '')
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/(^|\s)\S/g, (match) => match.toUpperCase());
|
||||
return {
|
||||
name: cleanName,
|
||||
data: chartData.map((item: any) => item[status])
|
||||
};
|
||||
});
|
||||
setCategories(categories);
|
||||
setSeries(series);
|
||||
}
|
||||
}, [chartData]);
|
||||
|
||||
return (
|
||||
<div className="card h-full">
|
||||
<div className="card-header border-0 ps-5 pb-0">
|
||||
<h3 className="card-title">{title}</h3>
|
||||
</div>
|
||||
<div className="card-body flex flex-col gap-4 p-2 lg:p-b7.5 lg:pt-4">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="ps-3">{toolbar && <div>{toolbar}</div>}</div>
|
||||
<span className="text-sm font-normal text-gray-700 mb-1">{subtitle}</span>
|
||||
<span className="text-3xl font-semibold text-gray-900 mb-0">{number}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ApexChart
|
||||
id="earnings_chart" //
|
||||
options={options}
|
||||
series={series}
|
||||
type={chartType}
|
||||
legend={chartLegend}
|
||||
height={350}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Chart };
|
||||
53
src/pages/dashboards/home/blocks/DateRangePicker.tsx
Normal file
53
src/pages/dashboards/home/blocks/DateRangePicker.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { useState } from 'react';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { format } from 'date-fns';
|
||||
import { KeenIcon } from '@/components/keenicons';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface DateRangePickerProps {
|
||||
date: DateRange | undefined;
|
||||
setDate: (date: DateRange | undefined) => void;
|
||||
}
|
||||
|
||||
const DateRangePicker = ({ date, setDate }: DateRangePickerProps) => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
id="date"
|
||||
className={cn(
|
||||
'btn btn-sm btn-light data-[state=open]:bg-light-active',
|
||||
!date && 'text-gray-400'
|
||||
)}
|
||||
>
|
||||
<KeenIcon icon="calendar" className="me-0.5" />
|
||||
{date?.from ? (
|
||||
date.to ? (
|
||||
<>
|
||||
{format(date.from, 'LLL dd, y')} - {format(date.to, 'LLL dd, y')}
|
||||
</>
|
||||
) : (
|
||||
format(date.from, 'LLL dd, y')
|
||||
)
|
||||
) : (
|
||||
<span>Pick a date range</span>
|
||||
)}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="end">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={date?.from}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export { DateRangePicker };
|
||||
48
src/pages/dashboards/home/blocks/YearPicker.tsx
Normal file
48
src/pages/dashboards/home/blocks/YearPicker.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { get5LastYear } from '@/utils/Date';
|
||||
|
||||
interface YearPickerProps {
|
||||
selectedYear: string;
|
||||
setSelectedYear: (year: string) => void;
|
||||
}
|
||||
|
||||
// Komponen YearPicker
|
||||
const YearPicker = ({ selectedYear, setSelectedYear }: YearPickerProps) => {
|
||||
const selectYear = get5LastYear();
|
||||
|
||||
const handleYearChange = (year: string) => {
|
||||
setSelectedYear(year);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<Select value={selectedYear} onValueChange={handleYearChange}>
|
||||
<SelectTrigger size="sm" className="w-28">
|
||||
<SelectValue placeholder="Pilih Tahun" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{selectYear.length > 0 ? (
|
||||
selectYear.map((year, index) => (
|
||||
<SelectItem key={index} value={year}>
|
||||
{year}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="no-data" disabled>
|
||||
No data available
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { YearPicker };
|
||||
4
src/pages/dashboards/home/blocks/index.ts
Normal file
4
src/pages/dashboards/home/blocks/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './Card';
|
||||
export * from './DateRangePicker';
|
||||
export * from './Chart';
|
||||
export * from './YearPicker';
|
||||
17
src/pages/dashboards/home/hooks/DashboardHomeContext.tsx
Normal file
17
src/pages/dashboards/home/hooks/DashboardHomeContext.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React, { createContext, useCallback, useState } from 'react';
|
||||
|
||||
interface ContextProps {}
|
||||
|
||||
const initialProps: ContextProps = {};
|
||||
|
||||
const DashboardHomeContext = createContext<ContextProps>(initialProps);
|
||||
|
||||
const DashboardHomeContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
/* state */
|
||||
|
||||
/* action */
|
||||
|
||||
return <DashboardHomeContext.Provider value={{}}>{children}</DashboardHomeContext.Provider>;
|
||||
};
|
||||
|
||||
export { DashboardHomeContextProvider, DashboardHomeContext };
|
||||
5
src/pages/dashboards/home/hooks/index.ts
Normal file
5
src/pages/dashboards/home/hooks/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './DashboardHomeContext';
|
||||
export * from './useDashboardHomeContext';
|
||||
export * from './useFetchCardData';
|
||||
export * from './useFetchChartData';
|
||||
// export * from './useFetchYear';
|
||||
12
src/pages/dashboards/home/hooks/useDashboardHomeContext.tsx
Normal file
12
src/pages/dashboards/home/hooks/useDashboardHomeContext.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { useContext } from 'react';
|
||||
import { DashboardHomeContext } from './DashboardHomeContext';
|
||||
|
||||
const useDashboardHomeContext = () => {
|
||||
const context = useContext(DashboardHomeContext);
|
||||
|
||||
if (!context) throw new Error('useDashboardHomeContext must be used within AuthProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useDashboardHomeContext };
|
||||
49
src/pages/dashboards/home/hooks/useFetchCardData.tsx
Normal file
49
src/pages/dashboards/home/hooks/useFetchCardData.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { formatDate } from 'date-fns';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
interface UseFetchCardDataResult {
|
||||
cardData: any;
|
||||
isCardLoading: boolean;
|
||||
cardError: any;
|
||||
}
|
||||
|
||||
const useFetchCardData = (
|
||||
start_date: string,
|
||||
end_date: string,
|
||||
count: string
|
||||
): UseFetchCardDataResult => {
|
||||
const [cardData, setCardData] = useState<[]>([]);
|
||||
const [isCardLoading, setIsCardLoading] = useState<boolean>(false);
|
||||
const [cardError, setCardError] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsCardLoading(true);
|
||||
const response = await axios.get(`${API_URL}/dashboard/card`, {
|
||||
params: {
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
aggregate: count
|
||||
}
|
||||
});
|
||||
setCardData(response.data.data);
|
||||
} catch (err) {
|
||||
setCardError(err);
|
||||
} finally {
|
||||
setIsCardLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [start_date, end_date, count]); // Dependensi pada from dan to
|
||||
|
||||
return { cardData, isCardLoading, cardError };
|
||||
};
|
||||
|
||||
export { useFetchCardData };
|
||||
49
src/pages/dashboards/home/hooks/useFetchChartData.tsx
Normal file
49
src/pages/dashboards/home/hooks/useFetchChartData.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { formatDate } from 'date-fns';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
interface UseFetchChartDataResult {
|
||||
chartData: any[];
|
||||
isChartLoading: boolean;
|
||||
chartError: any;
|
||||
}
|
||||
|
||||
const useFetchChartData = (
|
||||
start_date: string,
|
||||
end_date: string,
|
||||
count: string
|
||||
): UseFetchChartDataResult => {
|
||||
const [chartData, setChartData] = useState<any[]>([]);
|
||||
const [isChartLoading, setIsChartLoading] = useState<boolean>(false);
|
||||
const [chartError, setChartError] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsChartLoading(true);
|
||||
const response = await axios.get(`${API_URL}/dashboard/chart`, {
|
||||
params: {
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
aggregate: count
|
||||
}
|
||||
});
|
||||
setChartData(response.data.data);
|
||||
} catch (err) {
|
||||
setChartError(err);
|
||||
} finally {
|
||||
setIsChartLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [start_date, end_date, count]); // Dependensi pada from dan to
|
||||
|
||||
return { chartData, isChartLoading, chartError };
|
||||
};
|
||||
|
||||
export { useFetchChartData };
|
||||
45
src/pages/dashboards/home/hooks/useFetchYear.tsx
Normal file
45
src/pages/dashboards/home/hooks/useFetchYear.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
interface YearData {
|
||||
year: string;
|
||||
}
|
||||
interface UseFetchselectYearResult {
|
||||
selectYear: YearData[];
|
||||
isselectYearLoading: boolean;
|
||||
selectYearError: any;
|
||||
}
|
||||
|
||||
const useFetchYear = (
|
||||
path: string = '',
|
||||
start_date: string = '',
|
||||
end_date: string = '',
|
||||
year: string = ''
|
||||
): UseFetchselectYearResult => {
|
||||
const [selectYear, setSelectYear] = useState<YearData[]>([]);
|
||||
const [isselectYearLoading, setIsselectYearLoading] = useState<boolean>(false);
|
||||
const [selectYearError, setSelectYearError] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsselectYearLoading(true);
|
||||
const response = await axios.get(`${API_URL}/dashboard/year`);
|
||||
setSelectYear(response.data.data.map((item: { year: string }) => ({ year: item.year })));
|
||||
} catch (err) {
|
||||
setSelectYearError(err);
|
||||
} finally {
|
||||
setIsselectYearLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [path, start_date, end_date, year]);
|
||||
|
||||
return { selectYear, isselectYearLoading, selectYearError };
|
||||
};
|
||||
|
||||
export { useFetchYear };
|
||||
1
src/pages/dashboards/home/index.ts
Normal file
1
src/pages/dashboards/home/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './DashboardHomePage';
|
||||
20
src/pages/instansi/InstansiPage.tsx
Normal file
20
src/pages/instansi/InstansiPage.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { Container, DataGridInner } from '@/components';
|
||||
import { EditDialog } from './blocks';
|
||||
import { InstansiContextProvider } from './hooks';
|
||||
import { AddDialog } from './blocks/AddDialog';
|
||||
import { DeleteDialog } from './blocks/DeleteDialog';
|
||||
|
||||
export default function InstansiPage() {
|
||||
return (
|
||||
<InstansiContextProvider>
|
||||
<Container>
|
||||
<div className="grid gap-5 lg:gap-7.5">
|
||||
<DataGridInner />
|
||||
</div>
|
||||
<EditDialog />
|
||||
<AddDialog />
|
||||
<DeleteDialog />
|
||||
</Container>
|
||||
</InstansiContextProvider>
|
||||
);
|
||||
}
|
||||
262
src/pages/instansi/blocks/AddDialog.tsx
Normal file
262
src/pages/instansi/blocks/AddDialog.tsx
Normal file
@ -0,0 +1,262 @@
|
||||
import { 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 { useInstansiContext } 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 { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
const AddDialog = () => {
|
||||
const parentRef = useRef<any | null>(null);
|
||||
const { showAddDialog, handleAddDialog } = useInstansiContext();
|
||||
const { reload } = useDataGrid();
|
||||
const { PostData } = useCallApi();
|
||||
const [alert, setAlert] = useState({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
type: '',
|
||||
pic_name: '',
|
||||
pic_email: '',
|
||||
pic_phone: '',
|
||||
status: ''
|
||||
};
|
||||
|
||||
const [formField, setFormField] = useState(initialState);
|
||||
|
||||
const resetForm = () => {
|
||||
setFormField(initialState);
|
||||
};
|
||||
/* actions */
|
||||
const doCreate = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const response = await PostData(`${API_URL}/company/create`, formField);
|
||||
if (response?.status) {
|
||||
setAlert((prev) => ({ ...prev, show: false, message: '' }));
|
||||
handleAddDialog(false);
|
||||
toast.success('Success Create Insansi');
|
||||
reload();
|
||||
const createActivity = {
|
||||
module: 'Instansi',
|
||||
description: `Add Insansi => ${formField.name}`,
|
||||
action: 'C'
|
||||
};
|
||||
|
||||
doSaveLogActivity(createActivity);
|
||||
|
||||
resetForm();
|
||||
} else {
|
||||
setAlert((prev) => ({ ...prev, show: true, message: response?.message }));
|
||||
}
|
||||
},
|
||||
[formField]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={showAddDialog} onOpenChange={(open) => handleAddDialog(open)}>
|
||||
<DialogContent className="container-fixed max-w-[720px] flex flex-col p-5 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 gap-2">
|
||||
<h1 className="text-xl font-semibold leading-none text-gray-900">Tambah Instansi</h1>
|
||||
<div className="flex items-center gap-2 text-sm font-normal text-gray-700"></div>
|
||||
</div>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
color="#ddd"
|
||||
size={'sm'}
|
||||
onClick={() => handleAddDialog(false)}
|
||||
className="btn btn-sm btn-clear btn-light px-0 py-0"
|
||||
>
|
||||
<KeenIcon icon="cross" className="text-3sm" />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<DialogBody className="scrollable-y px-0 pb-0" ref={parentRef}>
|
||||
<div className="flex flex-col px-0">
|
||||
{alert.show && (
|
||||
<Alert variant="danger">
|
||||
<h3>{alert.message}</h3>
|
||||
</Alert>
|
||||
)}
|
||||
<form action="" onSubmit={doCreate}>
|
||||
<div className="card-body p-0">
|
||||
<h2 className="font-semibold mb-2">Instansi Information</h2>
|
||||
<div className="grid gap-5 mb-5">
|
||||
<div className="flex gap-5">
|
||||
<div className="w-8/12">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">
|
||||
Nama Instansi
|
||||
</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formField.name}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, name: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-4/12">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">
|
||||
Kode Instansi
|
||||
</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formField.code}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, code: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-3/3">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">
|
||||
Tipe Instansi
|
||||
</label>
|
||||
<div className="grow">
|
||||
<Select
|
||||
value={formField.type}
|
||||
onValueChange={(type) => setFormField((prev) => ({ ...prev, type }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="sipil">Sipil</SelectItem>
|
||||
<SelectItem value="veteran">Veteran</SelectItem>
|
||||
<SelectItem value="bctl">BCTL</SelectItem>
|
||||
<SelectItem value="pntl">PNTL</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-3/3">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">Deskrpsi</label>
|
||||
<Textarea
|
||||
className="input focus-visible:ring-offset-0 focus-visible:ring-0"
|
||||
value={formField.description}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, description: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="font-semibold mb-2">Instansi PIC</h2>
|
||||
<div className="grid gap-5 mb-5">
|
||||
<div className="flex gap-5">
|
||||
<div className="w-5/12">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">PIC Name</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formField.pic_name}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, pic_name: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-4/12">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">
|
||||
Alamat Email
|
||||
</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="email"
|
||||
value={formField.pic_email}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, pic_email: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-3/12">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">
|
||||
No. Telepon
|
||||
</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formField.pic_phone}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, pic_phone: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="items-baseline lg:flex-nowrap gap-5 mb-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">Status</label>
|
||||
<div className="grow">
|
||||
<Select
|
||||
value={formField.status}
|
||||
onValueChange={(status) => setFormField((prev) => ({ ...prev, status }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Y">Active</SelectItem>
|
||||
<SelectItem value="N">Non Active</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end pt-2.5">
|
||||
<Button className="btn btn-primary" type="submit">
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export { AddDialog };
|
||||
75
src/pages/instansi/blocks/DeleteDialog.tsx
Normal file
75
src/pages/instansi/blocks/DeleteDialog.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader } from '@/components/ui/dialog';
|
||||
import { useInstansiContext } from '../hooks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, useDataGrid } from '@/components';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { toast } from 'sonner';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
const DeleteDialog = () => {
|
||||
const { showDeleteDialog, handleDeleteDialog, selectedInstansi } = useInstansiContext();
|
||||
const { reload } = useDataGrid();
|
||||
|
||||
const { DeleteData } = useCallApi();
|
||||
const [alert, setAlert] = useState({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
|
||||
/* actions */
|
||||
const doDeleteData = useCallback(async () => {
|
||||
if (!selectedInstansi) {
|
||||
toast.error('Selected branch is not defined');
|
||||
return;
|
||||
}
|
||||
const response = await DeleteData(`${API_URL}/company/delete/${selectedInstansi.id}/true`, {
|
||||
id: selectedInstansi.id
|
||||
});
|
||||
if (response?.status) {
|
||||
setAlert((prev) => ({ ...prev, show: false, message: '' }));
|
||||
handleDeleteDialog(false, null);
|
||||
toast.success('Success Delete Instansi');
|
||||
reload();
|
||||
const createActivity = {
|
||||
module: 'Instansi',
|
||||
description: `Delete Instansi => ${selectedInstansi.name}`,
|
||||
action: 'D'
|
||||
};
|
||||
doSaveLogActivity(createActivity);
|
||||
} else {
|
||||
setAlert((prev) => ({ ...prev, show: true, message: response?.message }));
|
||||
}
|
||||
}, [selectedInstansi]);
|
||||
|
||||
return (
|
||||
<Dialog open={showDeleteDialog} onOpenChange={(open) => handleDeleteDialog(open, null)}>
|
||||
<DialogContent className="container-fixed max-w-md flex flex-col p-5 overflow-hidden [&>button]:hidden">
|
||||
<DialogHeader className="p-0 border-0 block">
|
||||
<Alert variant="warning">
|
||||
<h3 className="text-lg">Are you sure?</h3>
|
||||
<span className="text-sm">you will delete this data!</span>
|
||||
</Alert>
|
||||
{alert.show && (
|
||||
<Alert variant="danger">
|
||||
<h3>{alert.message}</h3>
|
||||
</Alert>
|
||||
)}
|
||||
</DialogHeader>
|
||||
<DialogFooter className="flex justify-end items-center gap-4 mt-3">
|
||||
<Button variant={'outline'} onClick={() => handleDeleteDialog(false, null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant={'destructive'} onClick={() => doDeleteData()}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export { DeleteDialog };
|
||||
275
src/pages/instansi/blocks/EditDialog.tsx
Normal file
275
src/pages/instansi/blocks/EditDialog.tsx
Normal file
@ -0,0 +1,275 @@
|
||||
import { 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 { useInstansiContext } from '../hooks';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
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';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
const EditDialog = () => {
|
||||
const parentRef = useRef<any | null>(null);
|
||||
const { showEditDialog, selectedInstansi, handleEditDialog } = useInstansiContext();
|
||||
const { reload } = useDataGrid();
|
||||
const { PutData } = useCallApi();
|
||||
const [alert, setAlert] = useState({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
|
||||
const [formField, setFormField] = useState({
|
||||
name: selectedInstansi?.name,
|
||||
code: selectedInstansi?.code,
|
||||
description: selectedInstansi?.description,
|
||||
type: selectedInstansi?.type,
|
||||
pic_name: selectedInstansi?.pic_name,
|
||||
pic_email: selectedInstansi?.pic_email,
|
||||
pic_phone: selectedInstansi?.pic_phone,
|
||||
status: selectedInstansi?.status
|
||||
});
|
||||
|
||||
/* actions */
|
||||
const doUpdate = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!selectedInstansi) {
|
||||
toast.error('Selected instansi is not defined');
|
||||
return;
|
||||
}
|
||||
const response = await PutData(`${API_URL}/company/update/${selectedInstansi.id}`, formField);
|
||||
if (response?.status) {
|
||||
setAlert((prev) => ({ ...prev, show: false, message: '' }));
|
||||
handleEditDialog(false, null);
|
||||
toast.success('Success Update Instansi');
|
||||
reload();
|
||||
const createActivity = {
|
||||
module: 'Instansi',
|
||||
description: `Edit Insansi => ${selectedInstansi.name}`,
|
||||
action: 'U'
|
||||
};
|
||||
|
||||
doSaveLogActivity(createActivity);
|
||||
} else {
|
||||
setAlert((prev) => ({ ...prev, show: true, message: response?.message }));
|
||||
}
|
||||
},
|
||||
[selectedInstansi, formField]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedInstansi) {
|
||||
setFormField((prev) => ({
|
||||
...prev,
|
||||
name: selectedInstansi?.name,
|
||||
code: selectedInstansi?.code,
|
||||
description: selectedInstansi?.description,
|
||||
type: selectedInstansi?.type,
|
||||
pic_name: selectedInstansi?.pic_name,
|
||||
pic_email: selectedInstansi?.pic_email,
|
||||
pic_phone: selectedInstansi?.pic_phone,
|
||||
status: selectedInstansi?.status
|
||||
}));
|
||||
}
|
||||
}, [selectedInstansi]);
|
||||
|
||||
return (
|
||||
<Dialog open={showEditDialog} onOpenChange={(open) => handleEditDialog(open, null)}>
|
||||
<DialogContent className="container-fixed max-w-[720px] flex flex-col p-5 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 gap-2">
|
||||
<h1 className="text-xl font-semibold leading-none text-gray-900">
|
||||
Instansi - Update
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 text-sm font-normal text-gray-700"></div>
|
||||
</div>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
color="#ddd"
|
||||
size={'sm'}
|
||||
onClick={() => handleEditDialog(false, null)}
|
||||
className="btn btn-sm btn-clear btn-light px-0 py-0"
|
||||
>
|
||||
<KeenIcon icon="cross" className="text-3sm" />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<DialogBody className="scrollable-y px-0 pb-0" ref={parentRef}>
|
||||
<div className="flex flex-col px-0">
|
||||
{alert.show && (
|
||||
<Alert variant="danger">
|
||||
<h3>{alert.message}</h3>
|
||||
</Alert>
|
||||
)}
|
||||
<form onSubmit={doUpdate}>
|
||||
<div className="card-body p-0">
|
||||
<h2 className="font-semibold mb-2">Instansi Information</h2>
|
||||
<div className="grid gap-5 mb-5">
|
||||
<div className="flex gap-5">
|
||||
<div className="w-8/12">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">
|
||||
Nama Instansi
|
||||
</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formField.name}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, name: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-4/12">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">
|
||||
Kode Instansi
|
||||
</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formField.code}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, code: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-3/3">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">
|
||||
Tipe Instansi
|
||||
</label>
|
||||
<div className="grow">
|
||||
<Select
|
||||
value={formField.type}
|
||||
onValueChange={(type) => setFormField((prev) => ({ ...prev, type }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="sipil">Sipil</SelectItem>
|
||||
<SelectItem value="veteran">Veteran</SelectItem>
|
||||
<SelectItem value="bctl">BCTL</SelectItem>
|
||||
<SelectItem value="pntl">PNTL</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-3/3">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">Deskrpsi</label>
|
||||
<Textarea
|
||||
className="input focus-visible:ring-offset-0 focus-visible:ring-0"
|
||||
value={formField.description}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, description: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="font-semibold mb-2">Instansi PIC</h2>
|
||||
<div className="grid gap-5 mb-5">
|
||||
<div className="flex gap-5">
|
||||
<div className="w-5/12">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">PIC Name</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formField.pic_name}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, pic_name: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-4/12">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">
|
||||
Alamat Email
|
||||
</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="email"
|
||||
value={formField.pic_email}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, pic_email: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-3/12">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">
|
||||
No. Telepon
|
||||
</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formField.pic_phone}
|
||||
onChange={({ target }) =>
|
||||
setFormField((prev) => ({ ...prev, pic_phone: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="items-baseline lg:flex-nowrap gap-5 mb-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2">Status</label>
|
||||
<div className="grow">
|
||||
<Select
|
||||
value={formField.status}
|
||||
onValueChange={(status) => setFormField((prev) => ({ ...prev, status }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Y">Active</SelectItem>
|
||||
<SelectItem value="N">Non Active</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end pt-2.5">
|
||||
<Button className="btn btn-primary">Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export { EditDialog };
|
||||
96
src/pages/instansi/blocks/ListToolBar.tsx
Normal file
96
src/pages/instansi/blocks/ListToolBar.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { DefaultTooltip, KeenIcon, useDataGrid } from '@/components';
|
||||
import { useInstansiContext } from '../hooks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { useState } from 'react';
|
||||
|
||||
const ListToolBar = ({ setFilter }: any) => {
|
||||
const { table, reload } = useDataGrid();
|
||||
const { handleAddDialog } = useInstansiContext();
|
||||
const [code, setCode] = useState('');
|
||||
const [company, setCompany] = useState('');
|
||||
|
||||
const handleReload = () => {
|
||||
reload();
|
||||
};
|
||||
|
||||
const handleFilterData = () => {
|
||||
try {
|
||||
const filters = [];
|
||||
if (code != '') filters.push({ id: 'code', value: `%${code}%` });
|
||||
if (company != '') filters.push({ id: 'name', value: `%${company}%` });
|
||||
|
||||
table.setColumnFilters(filters);
|
||||
} catch (error) {
|
||||
toast.error('Error filter data');
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetData = () => {
|
||||
setCode('');
|
||||
setCompany('');
|
||||
table.setColumnFilters([]);
|
||||
reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card-header flex-wrap gap-2 border-b-0 px-5">
|
||||
<div className="flex flex-wrap gap-2 lg:gap-5 w-full">
|
||||
<div className="flex justify-end w-full items-center">
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div className="flex gap-3">
|
||||
<label className="input input-sm w-3/6">
|
||||
<KeenIcon icon="filter" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Code"
|
||||
value={code}
|
||||
onChange={(event) => setCode(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="input input-sm w-3/6">
|
||||
<KeenIcon icon="filter" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Company Name"
|
||||
value={company}
|
||||
onChange={(event) => setCompany(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<div className="flex item-center gap-3 ms-2 me-10">
|
||||
<DefaultTooltip title={'Filter'} placement={'top'}>
|
||||
<Button variant="outline" className="h-7.5" onClick={handleFilterData}>
|
||||
<KeenIcon icon="filter" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
<DefaultTooltip title={'Reset Filter'} placement={'top'}>
|
||||
<Button variant="outline" className="h-7.5" onClick={handleResetData}>
|
||||
<KeenIcon icon="arrow-circle-left" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-7.5 text-[0.8rem]"
|
||||
onClick={() => handleAddDialog(true)}
|
||||
>
|
||||
Add Data
|
||||
</Button>
|
||||
<DefaultTooltip title={'Refresh'} placement={'top'}>
|
||||
<Button variant="outline" className="h-7.5" onClick={() => reload()}>
|
||||
<KeenIcon icon="arrows-circle" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ListToolBar };
|
||||
3
src/pages/instansi/blocks/index.ts
Normal file
3
src/pages/instansi/blocks/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './ListToolBar';
|
||||
export * from './EditDialog';
|
||||
export * from './AddDialog';
|
||||
93
src/pages/instansi/components/AlertCrud.tsx
Normal file
93
src/pages/instansi/components/AlertCrud.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface ResponseAlertCrudProps {
|
||||
status: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const AlertCrud: React.FC<{ ResponseAlertCrudProps: ResponseAlertCrudProps }> = ({
|
||||
ResponseAlertCrudProps
|
||||
}) => {
|
||||
const { status, message } = ResponseAlertCrudProps;
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isFullyVisible, setIsFullyVisible] = useState(false);
|
||||
|
||||
const handleDismiss = () => {
|
||||
setIsFullyVisible(false);
|
||||
setTimeout(() => setIsVisible(false), 300);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const delayTimeout = setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
setTimeout(() => setIsFullyVisible(true), 10);
|
||||
}, 500);
|
||||
|
||||
const autoDismissTimeout = setTimeout(() => handleDismiss(), 5000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(delayTimeout);
|
||||
clearTimeout(autoDismissTimeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
const icon = status ? (
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
className="h-5 w-5"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const bgColor = status ? 'bg-green-500' : 'bg-red-500';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed z-30 bottom-0 lg:bottom-5 transition-opacity duration-300 ${
|
||||
isFullyVisible ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`gap-x-2 mt-3 relative flex items-center w-full p-3 text-sm text-white font-medium rounded-md ${bgColor}`}
|
||||
>
|
||||
{icon}
|
||||
<span>{message}</span>
|
||||
<button onClick={handleDismiss}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
className="h-4 w-4"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { AlertCrud };
|
||||
223
src/pages/instansi/hooks/InstansiContext.tsx
Normal file
223
src/pages/instansi/hooks/InstansiContext.tsx
Normal file
@ -0,0 +1,223 @@
|
||||
import React, { createContext, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { DataGridColumnHeader, DataGridProvider, KeenIcon } from '@/components';
|
||||
import { EnforceSwitch } from '@/components/switch';
|
||||
import { ListToolBar } from '../blocks/ListToolBar';
|
||||
import { useCallApi } from '@/hooks';
|
||||
|
||||
interface ContextProps {
|
||||
showEditDialog: boolean;
|
||||
handleEditDialog: (show: boolean, selectedInstansi: SelectedInstansi | null) => void;
|
||||
showAddDialog: boolean;
|
||||
handleAddDialog: (show: boolean) => void;
|
||||
showDeleteDialog: boolean;
|
||||
handleDeleteDialog: (show: boolean, selectedInstansi: SelectedInstansi | null) => void;
|
||||
selectedInstansi: SelectedInstansi | null;
|
||||
}
|
||||
|
||||
interface SelectedInstansi {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
description: string;
|
||||
type: string;
|
||||
pic_name: string;
|
||||
pic_email: string;
|
||||
pic_phone: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
const initialProps: ContextProps = {
|
||||
showEditDialog: false,
|
||||
handleEditDialog: () => {},
|
||||
showAddDialog: false,
|
||||
handleAddDialog: () => {},
|
||||
showDeleteDialog: false,
|
||||
handleDeleteDialog: () => {},
|
||||
selectedInstansi: null
|
||||
};
|
||||
|
||||
const InstansiContext = createContext<ContextProps>(initialProps);
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
const InstansiContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
/* state */
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [selectedInstansi, setSelectedInstansi] = useState<SelectedInstansi | null>(null);
|
||||
const { GetData } = useCallApi();
|
||||
|
||||
/* action */
|
||||
const handleEditDialog = useCallback(
|
||||
(show: boolean, selected_branch: SelectedInstansi | null) => {
|
||||
setSelectedInstansi(show ? selected_branch : null);
|
||||
setShowEditDialog(show);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleAddDialog = useCallback((show: boolean) => {
|
||||
setShowAddDialog(show);
|
||||
}, []);
|
||||
|
||||
const handleDeleteDialog = useCallback(
|
||||
(show: boolean, selected_branch: SelectedInstansi | null) => {
|
||||
setSelectedInstansi(show ? selected_branch : null);
|
||||
setShowDeleteDialog(show);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/* Data Grid Options */
|
||||
const columns = useMemo<ColumnDef<any>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorFn: (row) => row.code,
|
||||
id: 'code',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Code" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-1/12'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.name,
|
||||
id: 'name',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Name" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-3/12'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.description,
|
||||
id: 'description',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Description" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-6/12'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.status,
|
||||
id: 'status',
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader title="Status" className="text-center" column={column} />
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<EnforceSwitch
|
||||
enforce={row.original.status == 'Y' ? true : false}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
headerClassName: 'w-1/12',
|
||||
cellClassName: 'text-center'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader title="Action" className="text-center" column={column} />
|
||||
),
|
||||
cell: (data: any) => {
|
||||
const row = data.row.original;
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm btn-icon btn-clear btn-light"
|
||||
onClick={() => handleEditDialog(true, row)}
|
||||
>
|
||||
<KeenIcon icon="notepad-edit" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-icon btn-clear btn-light"
|
||||
onClick={() => handleDeleteDialog(true, row)}
|
||||
>
|
||||
<KeenIcon icon="trash" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
headerClassName: 'w-1/12',
|
||||
cellClassName: 'text-center'
|
||||
}
|
||||
}
|
||||
],
|
||||
[handleEditDialog, handleDeleteDialog]
|
||||
);
|
||||
|
||||
const doGetListData = async (page: number, limit: number, sorting: any, filter: any) => {
|
||||
sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting;
|
||||
|
||||
filter = filter?.length === 0 ? {} : filter;
|
||||
let filterObject: Record<string, string | string[]> = {};
|
||||
if (Object.keys(filter).length !== 0) {
|
||||
for (let _filter of filter) {
|
||||
filterObject[_filter.id] = _filter.value;
|
||||
}
|
||||
}
|
||||
|
||||
filter = filterObject;
|
||||
|
||||
const response = await GetData(`${API_URL}/company/list`, {
|
||||
limit: limit,
|
||||
page: page + 1,
|
||||
with_deleted: false,
|
||||
order_field: sorting[0].id,
|
||||
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC',
|
||||
filter: JSON.stringify(filter)
|
||||
});
|
||||
|
||||
return { data: response?.data.list, totalCount: response?.data.total_count };
|
||||
};
|
||||
|
||||
return (
|
||||
<InstansiContext.Provider
|
||||
value={{
|
||||
showEditDialog,
|
||||
handleEditDialog,
|
||||
selectedInstansi,
|
||||
showAddDialog,
|
||||
handleAddDialog,
|
||||
showDeleteDialog,
|
||||
handleDeleteDialog
|
||||
}}
|
||||
>
|
||||
<Toaster expand visibleToasts={9} duration={3000} />
|
||||
|
||||
<DataGridProvider
|
||||
columns={columns}
|
||||
pagination={{ size: 10 }}
|
||||
toolbar={<ListToolBar />}
|
||||
layout={{ card: true }}
|
||||
sorting={[{ id: 'created_at', desc: false }]}
|
||||
serverSide={true}
|
||||
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
|
||||
doGetListData(pageIndex, pageSize, sorting, columnFilters)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</DataGridProvider>
|
||||
</InstansiContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { InstansiContextProvider, InstansiContext };
|
||||
export type { SelectedInstansi };
|
||||
2
src/pages/instansi/hooks/index.ts
Normal file
2
src/pages/instansi/hooks/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './InstansiContext';
|
||||
export * from './useInstansiContext';
|
||||
12
src/pages/instansi/hooks/useInstansiContext.tsx
Normal file
12
src/pages/instansi/hooks/useInstansiContext.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { useContext } from 'react';
|
||||
import { InstansiContext } from './InstansiContext';
|
||||
|
||||
const useInstansiContext = () => {
|
||||
const context = useContext(InstansiContext);
|
||||
|
||||
if (!context) throw new Error('useInstansiContext must be used within AuthProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useInstansiContext };
|
||||
1
src/pages/instansi/index.ts
Normal file
1
src/pages/instansi/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './InstansiPage';
|
||||
189
src/pages/pengajuan_kredit/chart/CreditChartPage.tsx
Normal file
189
src/pages/pengajuan_kredit/chart/CreditChartPage.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Container, KeenIcon, DefaultTooltip } from '@/components';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { CreditChartContextProvider, useFetchCreditChartData } from './hooks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Divider } from '@mui/material';
|
||||
import { formatDate } from 'date-fns';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { Chart, DateRangePicker } from './blocks';
|
||||
import moment from 'moment';
|
||||
|
||||
type IntervalType = 'day' | 'week' | 'month';
|
||||
type CountType = 'sum' | 'count';
|
||||
type ChartLegend = 'true | false';
|
||||
|
||||
const CreditChartPage = () => {
|
||||
const [date, setDate] = useState<DateRange | undefined>({
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 30)),
|
||||
to: new Date()
|
||||
});
|
||||
const [chartType, setChartType] = useState('line');
|
||||
const [chartLegend, setChartLegend] = useState('true');
|
||||
const [interval, setInterval] = useState<IntervalType>('day');
|
||||
const [count, setCount] = useState<CountType>('sum');
|
||||
const [selectedType, setSelectedType] = useState<CountType>('sum');
|
||||
const [filter, setFilter] = useState({
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 30)),
|
||||
to: new Date(),
|
||||
interval: 'day' as IntervalType,
|
||||
count: 'sum' as CountType,
|
||||
chartType: 'line',
|
||||
chartLegend: 'false'
|
||||
});
|
||||
|
||||
const { chartData, isChartLoading, chartError } = useFetchCreditChartData(
|
||||
formatDate(filter.from ?? new Date(), 'yyyy-MM-dd'),
|
||||
formatDate(filter.to ?? new Date(), 'yyyy-MM-dd'),
|
||||
filter.interval,
|
||||
filter.count
|
||||
);
|
||||
|
||||
const [type, setType] = useState<CountType>('sum');
|
||||
|
||||
useEffect(() => {
|
||||
if (chartData && chartData.length > 0) {
|
||||
setType(count);
|
||||
}
|
||||
}, [chartData]);
|
||||
|
||||
const handleFilter = useCallback(
|
||||
(date: DateRange | undefined) => {
|
||||
setFilter((prev) => ({
|
||||
...prev,
|
||||
from: date?.from ?? new Date(new Date().setDate(new Date().getDate() - 30)),
|
||||
to: date?.to ?? new Date(),
|
||||
interval: interval,
|
||||
count: selectedType,
|
||||
setChartType: chartType,
|
||||
setChartLegend: chartLegend
|
||||
}));
|
||||
},
|
||||
[interval, selectedType, chartType, chartLegend]
|
||||
);
|
||||
|
||||
const handleInterval = (value: IntervalType) => {
|
||||
setInterval(value);
|
||||
if (value === 'day') {
|
||||
setDate({
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 30)),
|
||||
to: new Date()
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleCount = (value: CountType) => {
|
||||
setCount(value);
|
||||
setSelectedType(value);
|
||||
};
|
||||
|
||||
const handleChartType = (value: CountType) => {
|
||||
setChartType(value);
|
||||
};
|
||||
const handleChartLegend = (value: ChartLegend) => {
|
||||
setChartLegend(value);
|
||||
};
|
||||
|
||||
const resetFilter = useCallback(() => {
|
||||
setFilter((prev) => ({
|
||||
...prev,
|
||||
interval: 'day',
|
||||
count: 'sum',
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 30)),
|
||||
to: new Date()
|
||||
}));
|
||||
setInterval('day');
|
||||
setCount('sum');
|
||||
setDate({
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 30)),
|
||||
to: new Date()
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CreditChartContextProvider>
|
||||
<Container>
|
||||
<div className="flex gap-3 items-center w-1/2 mb-4">
|
||||
<div className="w-auto min-w-[120px]">
|
||||
<Select value={chartType} onValueChange={handleChartType}>
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue placeholder="Select Chart Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-32">
|
||||
<SelectItem value="line">Line</SelectItem>
|
||||
<SelectItem value="bar">Bar</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-auto min-w-[120px] me-2">
|
||||
<Select value={chartLegend} onValueChange={handleChartLegend}>
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue placeholder="Select Chart Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-full">
|
||||
<SelectItem value="true">Show Legend</SelectItem>
|
||||
<SelectItem value="false">Hide Legend</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-auto min-w-[120px]">
|
||||
<Select value={interval} onValueChange={handleInterval}>
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-32">
|
||||
<SelectItem value="day">Daily</SelectItem>
|
||||
<SelectItem value="week">Weekly</SelectItem>
|
||||
<SelectItem value="month">Monthly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-auto min-w-[120px]">
|
||||
<Select value={count} onValueChange={handleCount}>
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-32">
|
||||
<SelectItem value="sum">Sum</SelectItem>
|
||||
<SelectItem value="count">Count</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-auto min-w-[220px]">
|
||||
<DateRangePicker date={date} setDate={setDate} interval={interval} />
|
||||
</div>
|
||||
<DefaultTooltip title={'Filter'} placement={'top'}>
|
||||
<Button variant="outline" className="h-7.5" onClick={() => handleFilter(date)}>
|
||||
<KeenIcon icon="filter" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
<DefaultTooltip title={'Reset Filter'} placement={'top'}>
|
||||
<Button variant="outline" className="h-7.5" onClick={() => resetFilter()}>
|
||||
<KeenIcon icon="arrow-circle-left" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
<div className="grid gap-5 lg:gap-7.5">
|
||||
<div className="grid lg:grid-cols-2 gap-5 items-stretch">
|
||||
<div className="lg:col-span-2">
|
||||
<Chart
|
||||
title="Overview"
|
||||
chartData={chartData}
|
||||
type={type}
|
||||
chartType={chartType}
|
||||
chartLegend={chartLegend}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</CreditChartContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreditChartPage;
|
||||
193
src/pages/pengajuan_kredit/chart/blocks/Chart.tsx
Normal file
193
src/pages/pengajuan_kredit/chart/blocks/Chart.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
import ApexChart from 'react-apexcharts';
|
||||
import { ApexOptions } from 'apexcharts';
|
||||
import { useEffect, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
import { fCurrency } from '@/utils/FormatNumber';
|
||||
|
||||
interface series {
|
||||
name: string;
|
||||
data: any[];
|
||||
}
|
||||
|
||||
const Chart = ({ title, subtitle, number, type, chartType, chartLegend, chartData = [] }: any) => {
|
||||
const [series, setSeries] = useState<series[]>([]);
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
const [yoyData, setYoyData] = useState<string[]>([]);
|
||||
|
||||
let legendOpt = null;
|
||||
if (chartLegend == 'true') {
|
||||
legendOpt = true;
|
||||
} else {
|
||||
legendOpt = false;
|
||||
}
|
||||
const options: ApexOptions = {
|
||||
annotations: {
|
||||
xaxis: categories.map((cat, index) => ({
|
||||
x: cat,
|
||||
x2: cat,
|
||||
borderColor: '#00000000',
|
||||
label: {
|
||||
text: yoyData[index] || '',
|
||||
orientation: 'horizontal',
|
||||
position: 'bottom',
|
||||
style: {
|
||||
background: yoyData[index]?.includes('↓') ? '#fee2e2' : '#dcfce7',
|
||||
color: yoyData[index]?.includes('↓') ? '#991b1b' : '#166534',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
padding: {
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 2,
|
||||
bottom: 2
|
||||
},
|
||||
borderRadius: 4
|
||||
}
|
||||
}
|
||||
}))
|
||||
},
|
||||
chart: {
|
||||
type: 'area',
|
||||
toolbar: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: '50%'
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
offsetY: -10,
|
||||
offsetX: chartType === 'bar' ? 1.5 : 0,
|
||||
formatter: (value: any) => {
|
||||
if (type == 'sum') {
|
||||
return fCurrency(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
markers: {
|
||||
size: 0,
|
||||
shape: 'circle'
|
||||
},
|
||||
xaxis: {
|
||||
categories: categories,
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'var(--tw-gray-500)',
|
||||
fontSize: '12px'
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'var(--tw-gray-500)',
|
||||
fontSize: '12px'
|
||||
},
|
||||
formatter: (value: any) => {
|
||||
if (type == 'sum') {
|
||||
return fCurrency(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
borderColor: 'var(--tw-gray-200)',
|
||||
strokeDashArray: 5,
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 20,
|
||||
left: 0
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
shared: true,
|
||||
intersect: false,
|
||||
y: {
|
||||
formatter: (value: any) => {
|
||||
if (type == 'sum') {
|
||||
return fCurrency(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
curve: 'smooth',
|
||||
lineCap: 'butt',
|
||||
colors: undefined,
|
||||
width: 3,
|
||||
dashArray: 0
|
||||
},
|
||||
legend: {
|
||||
show: legendOpt,
|
||||
position: 'right',
|
||||
floating: false
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (chartData && chartData.length != 0) {
|
||||
const categories = chartData[0].data.map((item: any) => item.x);
|
||||
const currentData = chartData[0].data.map((item: any) => item.y);
|
||||
const yoyData = chartData[1].data.map((item: any) => item.y);
|
||||
|
||||
const percentageChange = currentData.map((current: number, index: number) => {
|
||||
const previous = yoyData[index];
|
||||
if (!previous || previous === 0) return 0;
|
||||
return (((current - previous) / previous) * 100).toFixed(2);
|
||||
});
|
||||
|
||||
const yoyLabels = percentageChange.map((value: number) => {
|
||||
if (value === 0) return '-';
|
||||
const arrow = value >= 0 ? '↑' : '↓';
|
||||
return `${arrow}${Math.abs(value)}% YoY`;
|
||||
});
|
||||
|
||||
const series: series[] = [
|
||||
{
|
||||
name: chartData[0].name,
|
||||
data: currentData
|
||||
},
|
||||
{
|
||||
name: chartData[1].name,
|
||||
data: yoyData
|
||||
}
|
||||
];
|
||||
|
||||
setSeries(series);
|
||||
setCategories(categories);
|
||||
setYoyData(yoyLabels);
|
||||
}
|
||||
}, [chartData]);
|
||||
|
||||
return (
|
||||
<div className="card h-full">
|
||||
<div className="card-header border-0 ps-5 pb-0">
|
||||
<h3 className="card-title">{title}</h3>
|
||||
</div>
|
||||
<div className="card-body flex flex-col gap-4 p-2">
|
||||
<ApexChart
|
||||
id="earnings_chart" //
|
||||
options={options}
|
||||
series={series}
|
||||
type={chartType}
|
||||
legend={chartLegend}
|
||||
height={350}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Chart };
|
||||
74
src/pages/pengajuan_kredit/chart/blocks/DateRangePicker.tsx
Normal file
74
src/pages/pengajuan_kredit/chart/blocks/DateRangePicker.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { format } from 'date-fns';
|
||||
import { KeenIcon } from '@/components/keenicons';
|
||||
import { cn } from '@/lib/utils';
|
||||
import moment from 'moment';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface DateRangePickerProps {
|
||||
date: DateRange | undefined;
|
||||
setDate: (date: DateRange | undefined) => void;
|
||||
interval: 'day' | 'week' | 'month';
|
||||
}
|
||||
|
||||
function getDateRangeLength(startDate: Date, endDate: Date) {
|
||||
const start = moment(startDate);
|
||||
const end = moment(endDate);
|
||||
|
||||
return end.diff(start, 'days') + 1;
|
||||
}
|
||||
|
||||
const DateRangePicker = ({ date, setDate, interval }: DateRangePickerProps) => {
|
||||
const handleSelectDate = useCallback(
|
||||
(date: DateRange | undefined) => {
|
||||
if (date && date.from && date.to) {
|
||||
const dateRange = getDateRangeLength(date.from, date.to);
|
||||
setDate(date);
|
||||
} else {
|
||||
setDate(date);
|
||||
}
|
||||
},
|
||||
[interval, setDate]
|
||||
);
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
id="date"
|
||||
className={cn(
|
||||
'btn btn-sm btn-light data-[state=open]:bg-light-active',
|
||||
!date && 'text-gray-400'
|
||||
)}
|
||||
>
|
||||
<KeenIcon icon="calendar" className="me-0.5" />
|
||||
{date?.from ? (
|
||||
date.to ? (
|
||||
<>
|
||||
{format(date.from, 'LLL dd, y')} - {format(date.to, 'LLL dd, y')}
|
||||
</>
|
||||
) : (
|
||||
format(date.from, 'LLL dd, y')
|
||||
)
|
||||
) : (
|
||||
<span>Pick a date range</span>
|
||||
)}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="end">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={date?.from}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export { DateRangePicker };
|
||||
92
src/pages/pengajuan_kredit/chart/blocks/YearPicker.tsx
Normal file
92
src/pages/pengajuan_kredit/chart/blocks/YearPicker.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import axios from 'axios';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { KeenIcon } from '@/components';
|
||||
|
||||
interface YearData {
|
||||
year: string;
|
||||
}
|
||||
|
||||
interface YearPickerProps {
|
||||
selectedYear: string;
|
||||
setSelectedYear: (year: string) => void;
|
||||
}
|
||||
|
||||
const API_URL = apiConfig.service_bg;
|
||||
|
||||
// Hook untuk mengambil data tahun dari API
|
||||
const useFetchYear = (): { selectYear: YearData[]; isYearLoading: boolean; yearError: any } => {
|
||||
const [selectYear, setSelectYear] = useState<YearData[]>([]);
|
||||
const [isYearLoading, setIsYearLoading] = useState<boolean>(false);
|
||||
const [yearError, setYearError] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchYearData = async () => {
|
||||
try {
|
||||
setIsYearLoading(true);
|
||||
const response = await axios.get(`${API_URL}/dashboard/year`);
|
||||
const data = response.data?.data || [];
|
||||
setSelectYear(data.map((item: { year: string }) => ({ year: item.year })));
|
||||
} catch (error) {
|
||||
setYearError(error);
|
||||
} finally {
|
||||
setIsYearLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchYearData();
|
||||
}, []);
|
||||
|
||||
return { selectYear, isYearLoading, yearError };
|
||||
};
|
||||
|
||||
// Komponen YearPicker
|
||||
const YearPicker = ({ selectedYear, setSelectedYear }: YearPickerProps) => {
|
||||
const { selectYear, isYearLoading, yearError } = useFetchYear();
|
||||
|
||||
// Menangani kondisi loading dan error
|
||||
if (isYearLoading) {
|
||||
return <div>Loading year data...</div>;
|
||||
}
|
||||
|
||||
if (yearError) {
|
||||
return <div>Error fetching year data: {yearError.message}</div>;
|
||||
}
|
||||
|
||||
const handleYearChange = (year: string) => {
|
||||
setSelectedYear(year); // Memperbarui tahun yang dipilih
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<Select value={selectedYear} onValueChange={handleYearChange}>
|
||||
<SelectTrigger size="sm" className="w-28">
|
||||
<KeenIcon icon="calendar" className="" />
|
||||
<SelectValue placeholder="Pilih Tahun" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{selectYear.length > 0 ? (
|
||||
selectYear.map((year, index) => (
|
||||
<SelectItem key={index} value={year.year}>
|
||||
{year.year}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="no-data" disabled>
|
||||
No data available
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { YearPicker };
|
||||
3
src/pages/pengajuan_kredit/chart/blocks/index.ts
Normal file
3
src/pages/pengajuan_kredit/chart/blocks/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './Chart';
|
||||
export * from './YearPicker';
|
||||
export * from './DateRangePicker';
|
||||
@ -0,0 +1,17 @@
|
||||
import React, { createContext, useCallback, useState } from 'react';
|
||||
|
||||
interface ContextProps {}
|
||||
|
||||
const initialProps: ContextProps = {};
|
||||
|
||||
const CreditChartContext = createContext<ContextProps>(initialProps);
|
||||
|
||||
const CreditChartContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
/* state */
|
||||
|
||||
/* action */
|
||||
|
||||
return <CreditChartContext.Provider value={{}}>{children}</CreditChartContext.Provider>;
|
||||
};
|
||||
|
||||
export { CreditChartContextProvider, CreditChartContext };
|
||||
3
src/pages/pengajuan_kredit/chart/hooks/index.ts
Normal file
3
src/pages/pengajuan_kredit/chart/hooks/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './useCreditChartContext';
|
||||
export * from './useFetchCreditChartData';
|
||||
export * from './CreditChartContext';
|
||||
@ -0,0 +1,11 @@
|
||||
import { useContext } from 'react';
|
||||
import { CreditChartContext } from './CreditChartContext';
|
||||
const useCreditChartContext = () => {
|
||||
const context = useContext(CreditChartContext);
|
||||
|
||||
if (!context) throw new Error('useCreditChartContext must be used within AuthProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useCreditChartContext };
|
||||
@ -0,0 +1,53 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { formatDate } from 'date-fns';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
interface UseFetchCreditChartDataResult {
|
||||
chartData: any[];
|
||||
isChartLoading: boolean;
|
||||
chartError: any;
|
||||
}
|
||||
|
||||
const useFetchCreditChartData = (
|
||||
start_date: string,
|
||||
end_date: string,
|
||||
interval: string,
|
||||
count: string
|
||||
): UseFetchCreditChartDataResult => {
|
||||
const [chartData, setChartData] = useState<any[]>([]);
|
||||
const [isChartLoading, setIsChartLoading] = useState<boolean>(false);
|
||||
const [chartError, setChartError] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const filterInterval = interval;
|
||||
try {
|
||||
setIsChartLoading(true);
|
||||
const response = await axios.get(`${API_URL}/application/chart`, {
|
||||
params: {
|
||||
interval: filterInterval,
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
aggregate: count
|
||||
}
|
||||
});
|
||||
setChartData(response.data.data);
|
||||
} catch (err) {
|
||||
setChartError(err);
|
||||
} finally {
|
||||
setIsChartLoading(false);
|
||||
}
|
||||
};
|
||||
if (start_date && end_date) {
|
||||
fetchData();
|
||||
}
|
||||
}, [start_date, end_date, interval, count]); // Dependensi pada from dan to
|
||||
|
||||
return { chartData, isChartLoading, chartError };
|
||||
};
|
||||
|
||||
export { useFetchCreditChartData };
|
||||
1
src/pages/pengajuan_kredit/chart/index.ts
Normal file
1
src/pages/pengajuan_kredit/chart/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './CreditChartPage';
|
||||
12
src/pages/pengajuan_kredit/list/CreditAddPage.tsx
Normal file
12
src/pages/pengajuan_kredit/list/CreditAddPage.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Container } from '@/components';
|
||||
import { CreditAddContextProvider } from './hooks';
|
||||
|
||||
export default function CreditAddPage() {
|
||||
return (
|
||||
<CreditAddContextProvider>
|
||||
<Container>
|
||||
<div className="grid gap-5 lg:gap-7.5"></div>
|
||||
</Container>
|
||||
</CreditAddContextProvider>
|
||||
);
|
||||
}
|
||||
12
src/pages/pengajuan_kredit/list/CreditDetailPage.tsx
Normal file
12
src/pages/pengajuan_kredit/list/CreditDetailPage.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Container } from '@/components';
|
||||
import { CreditDetailContextProvider } from './hooks';
|
||||
|
||||
export default function CreditDetailPage() {
|
||||
return (
|
||||
<CreditDetailContextProvider>
|
||||
<Container>
|
||||
<div className="grid gap-5 lg:gap-7.5"></div>
|
||||
</Container>
|
||||
</CreditDetailContextProvider>
|
||||
);
|
||||
}
|
||||
14
src/pages/pengajuan_kredit/list/CreditListPage.tsx
Normal file
14
src/pages/pengajuan_kredit/list/CreditListPage.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { Container, DataGridInner } from '@/components';
|
||||
import { CreditListContextProvider } from './hooks';
|
||||
|
||||
export default function CreditListPage() {
|
||||
return (
|
||||
<CreditListContextProvider>
|
||||
<Container>
|
||||
<div className="grid gap-5 lg:gap-7.5">
|
||||
<DataGridInner />
|
||||
</div>
|
||||
</Container>
|
||||
</CreditListContextProvider>
|
||||
);
|
||||
}
|
||||
22
src/pages/pengajuan_kredit/list/blocks/ChatMessageIn.tsx
Normal file
22
src/pages/pengajuan_kredit/list/blocks/ChatMessageIn.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
|
||||
interface IChatMessageInProps {
|
||||
text: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
const ChatMessageIn = ({ text, time }: IChatMessageInProps) => {
|
||||
return (
|
||||
<div className="flex items-end gap-3.5 px-5">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div
|
||||
className="card shadow-none flex flex-col bg-gray-100 gap-2.5 p-3 rounded-bl-none text-2sm font-medium text-gray-700"
|
||||
dangerouslySetInnerHTML={{ __html: text }}
|
||||
/>
|
||||
<span className="text-2xs font-medium text-gray-500">{time}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ChatMessageIn, type IChatMessageInProps };
|
||||
27
src/pages/pengajuan_kredit/list/blocks/ChatMessageOut.tsx
Normal file
27
src/pages/pengajuan_kredit/list/blocks/ChatMessageOut.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
import { KeenIcon } from '@/components';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface IChatMessageOutProps {
|
||||
text: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
const ChatMessageOut = ({ text, time }: IChatMessageOutProps) => {
|
||||
return (
|
||||
<div className="flex items-end justify-end gap-3.5 px-5">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div
|
||||
className="card shadow-none flex bg-primary text-primary-inverse text-2sm font-medium flex-col gap-2.5 p-3 rounded-be-none"
|
||||
dangerouslySetInnerHTML={{ __html: text }}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end relative">
|
||||
<span className="text-2xs font-medium text-gray-600 me-6">{time}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ChatMessageOut, type IChatMessageOutProps };
|
||||
58
src/pages/pengajuan_kredit/list/blocks/DatePicker.tsx
Normal file
58
src/pages/pengajuan_kredit/list/blocks/DatePicker.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { format } from 'date-fns';
|
||||
import { KeenIcon } from '@/components/keenicons';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface DatePickerProps {
|
||||
date?: Date;
|
||||
setDate: (date: Date) => void;
|
||||
value?: string;
|
||||
onChange?: (e: { target: { value: string } }) => void;
|
||||
className?: string; // Tambahkan className sebagai props
|
||||
}
|
||||
|
||||
const DatePicker = ({
|
||||
date = new Date(),
|
||||
setDate,
|
||||
value,
|
||||
onChange,
|
||||
className
|
||||
}: DatePickerProps) => {
|
||||
const handleDateSelect = (selectedDate: Date | undefined) => {
|
||||
if (selectedDate) {
|
||||
setDate(selectedDate);
|
||||
if (onChange) {
|
||||
onChange({ target: { value: format(selectedDate, 'yyyy-MM-dd') } });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
'btn btn-sm btn-light data-[state=open]:bg-light-active w-full',
|
||||
!date && 'text-gray-400',
|
||||
className // Terapkan className di sini
|
||||
)}
|
||||
>
|
||||
<KeenIcon icon="calendar" className="me-0.5 mb-0.5 text-info" />
|
||||
{value || (date ? format(date, 'yyyy-MM-dd') : 'Pilih Tanggal')}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="end">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={handleDateSelect}
|
||||
defaultMonth={date}
|
||||
numberOfMonths={1}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export { DatePicker };
|
||||
60
src/pages/pengajuan_kredit/list/blocks/DateRangePicker.tsx
Normal file
60
src/pages/pengajuan_kredit/list/blocks/DateRangePicker.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { format, subDays } from 'date-fns';
|
||||
import { KeenIcon } from '@/components/keenicons';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface DateRangePickerProps {
|
||||
date: DateRange | undefined;
|
||||
setDate: (date: DateRange | undefined) => void;
|
||||
}
|
||||
|
||||
const DateRangePicker = ({ date, setDate }: DateRangePickerProps) => {
|
||||
useEffect(() => {
|
||||
if (!date) {
|
||||
const today = new Date();
|
||||
const last30Days = subDays(today, 30);
|
||||
setDate({ from: last30Days, to: today });
|
||||
}
|
||||
}, [date, setDate]);
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
id="date"
|
||||
className={cn(
|
||||
'btn btn-sm btn-light data-[state=open]:bg-light-active w-full',
|
||||
!date && 'text-gray-400'
|
||||
)}
|
||||
>
|
||||
<KeenIcon icon="calendar" className="me-0.5" />
|
||||
{date?.from ? (
|
||||
date.to ? (
|
||||
<>
|
||||
{format(date.from, 'LLL dd, y')} - {format(date.to, 'LLL dd, y')}
|
||||
</>
|
||||
) : (
|
||||
format(date.from, 'LLL dd, y')
|
||||
)
|
||||
) : (
|
||||
<span>Pick a date range</span>
|
||||
)}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="end">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={date?.from}
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export { DateRangePicker };
|
||||
293
src/pages/pengajuan_kredit/list/blocks/ListToolBar.tsx
Normal file
293
src/pages/pengajuan_kredit/list/blocks/ListToolBar.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
import { DefaultTooltip, KeenIcon, useDataGrid } from '@/components';
|
||||
import { useCreditListContext } from '../hooks';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DateRangePicker } from './DateRangePicker';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import XlsIcon from '@/public_media/file-types/xls.svg';
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
const ListToolBar = ({ setFilter }: any) => {
|
||||
const navigate = useNavigate();
|
||||
const { table, reload } = useDataGrid();
|
||||
const { date, setDate, doExportData } = useCreditListContext();
|
||||
const [filteredDate, setFilteredDate] = useState<DateRange | null>(null);
|
||||
const [selectedStatus, setSelectedStatus] = useState<string[]>([]);
|
||||
const [isStatusDropdownOpen, setIsStatusDropdownOpen] = useState(false);
|
||||
|
||||
const [debitorName, setDebitorName] = useState('');
|
||||
const [company, setCompany] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
const [employeeId, setEmployeeId] = useState('');
|
||||
|
||||
const handleReload = () => {
|
||||
reload();
|
||||
};
|
||||
|
||||
const handleFilterData = () => {
|
||||
try {
|
||||
const filters = [];
|
||||
if (selectedStatus.length > 0) {
|
||||
filters.push({
|
||||
id: 'a.status',
|
||||
value: selectedStatus
|
||||
});
|
||||
}
|
||||
|
||||
if (debitorName != '') filters.push({ id: 'debtor.name', value: `%${debitorName}%` });
|
||||
if (company != '') filters.push({ id: 'company.name', value: `%${company}%` });
|
||||
if (employeeId != '') filters.push({ id: 'debtor.employee_id', value: `%${employeeId}%` });
|
||||
|
||||
if (code != '') {
|
||||
filters.push({ id: 'a.code', value: code });
|
||||
}
|
||||
|
||||
if (date?.from && date?.to) {
|
||||
setFilteredDate({
|
||||
from: date.from,
|
||||
to: date.to
|
||||
});
|
||||
|
||||
filters.push({
|
||||
id: 'application_date_from',
|
||||
value: date.from.toISOString().split('T')[0]
|
||||
});
|
||||
filters.push({
|
||||
id: 'application_date_to',
|
||||
value: date.to.toISOString().split('T')[0]
|
||||
});
|
||||
|
||||
table.setColumnFilters(filters);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Error filter data');
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetData = () => {
|
||||
setDate({
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 31)),
|
||||
to: new Date()
|
||||
});
|
||||
setFilteredDate(null);
|
||||
setSelectedStatus([]);
|
||||
setCode('');
|
||||
table.setColumnFilters([]);
|
||||
reload();
|
||||
};
|
||||
|
||||
const handleCreateNew = () => {
|
||||
navigate('/pengajuan_kredit/list/add');
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const sorting = table.getState().sorting;
|
||||
doExportData(sorting, table.getState().columnFilters);
|
||||
};
|
||||
|
||||
const handleStatusChange = (statusId: string) => {
|
||||
console.log('statusId :', statusId);
|
||||
setSelectedStatus((prev) =>
|
||||
prev.includes(statusId) ? prev.filter((id) => id != statusId) : [...prev, statusId]
|
||||
);
|
||||
};
|
||||
|
||||
const handleStatusDropdownOpen = () => {
|
||||
setIsStatusDropdownOpen(true);
|
||||
};
|
||||
|
||||
const handleStatusDropdownClose = () => {
|
||||
setIsStatusDropdownOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
handleStatusDropdownClose();
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [handleStatusDropdownClose]);
|
||||
|
||||
return (
|
||||
<div className="card-header flex-wrap gap-2 border-b-0 px-5">
|
||||
<div className="flex flex-wrap gap-2 lg:gap-5 w-full">
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="w-auto min-w-[220px]">
|
||||
<DateRangePicker date={date} setDate={setDate} />
|
||||
</div>
|
||||
<div className="w-auto min-w-[150px]">
|
||||
<DropdownMenu open={isStatusDropdownOpen} onOpenChange={handleStatusDropdownOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="h-7.5 text-[0.8rem] w-full justify-between">
|
||||
{selectedStatus.length > 0
|
||||
? `Select Status: ${selectedStatus.length}`
|
||||
: 'Select Status'}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuCheckboxItem
|
||||
key="open"
|
||||
checked={selectedStatus.includes('open')}
|
||||
onCheckedChange={() => handleStatusChange('open')}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="justify-between h-6 text-[0.8rem]"
|
||||
>
|
||||
OPEN
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
key="under_review"
|
||||
checked={selectedStatus.includes('under_review')}
|
||||
onCheckedChange={() => handleStatusChange('under_review')}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="justify-between h-6 text-[0.8rem]"
|
||||
>
|
||||
UNDER REVIEW
|
||||
</DropdownMenuCheckboxItem>
|
||||
{/* <DropdownMenuCheckboxItem
|
||||
key="revision"
|
||||
checked={selectedStatus.includes('revision')}
|
||||
onCheckedChange={() => handleStatusChange('revision')}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="justify-between h-6 text-[0.8rem]"
|
||||
>
|
||||
REVISON
|
||||
</DropdownMenuCheckboxItem> */}
|
||||
<DropdownMenuCheckboxItem
|
||||
key="approved"
|
||||
checked={selectedStatus.includes('approved')}
|
||||
onCheckedChange={() => handleStatusChange('approved')}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="justify-between h-6 text-[0.8rem]"
|
||||
>
|
||||
APPROVE
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
key="rejected"
|
||||
checked={selectedStatus.includes('rejected')}
|
||||
onCheckedChange={() => handleStatusChange('rejected')}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="justify-between h-6 text-[0.8rem]"
|
||||
>
|
||||
REJECT
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="w-auto min-w-[100px]">
|
||||
<label className="input input-sm">
|
||||
<KeenIcon icon="filter" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Code"
|
||||
value={code}
|
||||
onChange={(event) => setCode(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-auto min-w-[100px]">
|
||||
<label className="input input-sm">
|
||||
<KeenIcon icon="filter" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Company"
|
||||
value={company}
|
||||
onChange={(event) => setCompany(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-auto min-w-[100px]">
|
||||
<label className="input input-sm">
|
||||
<KeenIcon icon="filter" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Debitor Name"
|
||||
value={debitorName}
|
||||
onChange={(event) => setDebitorName(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-auto min-w-[100px]">
|
||||
<label className="input input-sm">
|
||||
<KeenIcon icon="filter" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Employee ID"
|
||||
value={employeeId}
|
||||
onChange={(event) => setEmployeeId(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex item-center gap-3 ms-2 me-10">
|
||||
<DefaultTooltip title={'Filter'} placement={'top'}>
|
||||
<Button variant="outline" className="h-7.5" onClick={handleFilterData}>
|
||||
<KeenIcon icon="filter" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
<DefaultTooltip title={'Reset Filter'} placement={'top'}>
|
||||
<Button variant="outline" className="h-7.5" onClick={handleResetData}>
|
||||
<KeenIcon icon="arrow-circle-left" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center">
|
||||
{/* <Button
|
||||
variant="outline"
|
||||
className="h-7.5 text-[0.8rem]"
|
||||
onClick={() => handleCreateNew()}
|
||||
>
|
||||
Add Data
|
||||
</Button> */}
|
||||
<DefaultTooltip title={'Export Data'} placement={'top'}>
|
||||
<Button variant={'outline'} className="h-7.5 min-w-[58px]" onClick={handleExport}>
|
||||
<img src={toAbsoluteUrl('/media/file-types/xls.svg')} className="" alt="" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
<DefaultTooltip title={'Refresh'} placement={'top'}>
|
||||
<Button variant="outline" className="h-7.5" onClick={handleReload}>
|
||||
<KeenIcon icon="arrows-circle" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ListToolBar };
|
||||
49
src/pages/pengajuan_kredit/list/blocks/Notes.tsx
Normal file
49
src/pages/pengajuan_kredit/list/blocks/Notes.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface IDropdownNotificationsItemProps {
|
||||
userName: string;
|
||||
avatar: string;
|
||||
description: string;
|
||||
time: string;
|
||||
text: string;
|
||||
company: string;
|
||||
}
|
||||
|
||||
const Notes = ({
|
||||
userName,
|
||||
avatar,
|
||||
description,
|
||||
time,
|
||||
text,
|
||||
company
|
||||
}: IDropdownNotificationsItemProps) => {
|
||||
const [emailInput, setEmailInput] = useState('');
|
||||
return (
|
||||
<div className="flex grow gap-2.5">
|
||||
<div className="relative shrink-0 mt-0.5">
|
||||
<img className="h-[20px] max-w-none" src={toAbsoluteUrl(avatar)} alt="logo" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-2sm font-medium flex justify-between">
|
||||
<p className="text-gray-900 font-semibold">{userName}</p>
|
||||
<span className="text-gray-700"> {description} </span>
|
||||
<span className="flex items-center text-2xs font-medium text-gray-500">{time}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="card shadow-none flex flex-col gap-2.5 p-3.5 rounded-lg bg-light-active"
|
||||
style={{ borderColor: company == 'BRI' ? '#02529c' : '' }}
|
||||
>
|
||||
<div className="text-2sm font-semibold text-gray-600 mb-px">
|
||||
<span className="text-gray-700 font-medium"> {text} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Notes };
|
||||
6
src/pages/pengajuan_kredit/list/blocks/index.ts
Normal file
6
src/pages/pengajuan_kredit/list/blocks/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './ListToolBar';
|
||||
export * from './DateRangePicker';
|
||||
export * from './DatePicker';
|
||||
export * from './ChatMessageIn';
|
||||
export * from './ChatMessageOut';
|
||||
export * from './Notes'
|
||||
215
src/pages/pengajuan_kredit/list/hooks/CreditAddContext.tsx
Normal file
215
src/pages/pengajuan_kredit/list/hooks/CreditAddContext.tsx
Normal file
@ -0,0 +1,215 @@
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLanguage } from '@/i18n';
|
||||
import { DefaultTooltip, KeenIcon } from '@/components';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { Stepper, Step, StepLabel } from '@mui/material';
|
||||
import { StepOne, StepTwo, StepThree, StepFour } from '../steps';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { toast } from 'sonner';
|
||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
const CreditAddContext = createContext<any | null>(null);
|
||||
|
||||
const steps = ['Step 1', 'Step 2', 'Step 3', 'Step 4'];
|
||||
|
||||
const initialState = {
|
||||
name: '',
|
||||
employee_id: '',
|
||||
phone: '',
|
||||
application_date: '',
|
||||
identity_type: '',
|
||||
identity_file: '',
|
||||
companyId: '', // => wajib
|
||||
type: '',
|
||||
marriage_type: '',
|
||||
marriage_file: '', //Surat Keterangan Menikah
|
||||
family_file: '', // Kartu keluarga
|
||||
form: [],
|
||||
spouse_type: '',
|
||||
spouse_file_type: '',
|
||||
spouse_file: '',
|
||||
photo: '',
|
||||
spouse_photo: '',
|
||||
account_number: '',
|
||||
amount: '',
|
||||
period_amount: '',
|
||||
period_type: '',
|
||||
status: '',
|
||||
description: ''
|
||||
};
|
||||
|
||||
// console.log('initialState:', initialState);
|
||||
|
||||
const CreditAddContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [formData, setFormData] = useState(initialState);
|
||||
const { PostData } = useCallApi();
|
||||
const [alert, setAlert] = useState({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
|
||||
type State = typeof initialState; // Gunakan tipe otomatis dari initialState
|
||||
|
||||
const validateState = (state: State): Record<string, string> => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!state.companyId || state.companyId.trim() === '') {
|
||||
errors.companyId = 'Instansi pengaju harus diisi.';
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
const handleBackClick = () => navigate('/pengajuan_kredit/list');
|
||||
|
||||
const handleNext = () => {
|
||||
if (activeStep < steps.length - 1) {
|
||||
if (activeStep === 0) {
|
||||
const errors = validateState(formData);
|
||||
if (Object.keys(errors).length > 0) {
|
||||
toast.error(errors.companyId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setActiveStep((prevStep) => prevStep + 1);
|
||||
} else {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => setActiveStep((prev) => prev - 1);
|
||||
|
||||
const handleSaveToDraft = () => {
|
||||
setFormData((prev: any) => ({ ...prev, status: 'draft' }));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setFormData((prev: any) => ({ ...prev, status: 'open' }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (formData.status !== '') {
|
||||
doCreateCredit(formData);
|
||||
}
|
||||
}, [formData]);
|
||||
|
||||
const renderStepContent = (step: number) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return <StepOne setFormData={setFormData} formData={formData} />;
|
||||
case 1:
|
||||
return <StepTwo setFormData={setFormData} formData={formData} />;
|
||||
case 2:
|
||||
return <StepThree setFormData={setFormData} formData={formData} />;
|
||||
case 3:
|
||||
return <StepFour setFormData={setFormData} formData={formData} />;
|
||||
default:
|
||||
return <div>Langkah tidak dikenal</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData(initialState);
|
||||
};
|
||||
|
||||
const doCreateCredit = useCallback(async (updatedFormData: typeof formData) => {
|
||||
console.log('formData: ', updatedFormData);
|
||||
|
||||
const response = await PostData(`${API_URL}/application/create`, updatedFormData);
|
||||
if (response?.status) {
|
||||
setAlert((prev) => ({ ...prev, show: false, message: '' }));
|
||||
toast.success('Success Create New Credit');
|
||||
resetForm();
|
||||
const createActivity = {
|
||||
module: 'Create Pengajuan Credit',
|
||||
description: `Add new data Credit for => ${updatedFormData?.name}`,
|
||||
action: 'C'
|
||||
};
|
||||
doSaveLogActivity(createActivity);
|
||||
handleBackClick();
|
||||
} else {
|
||||
setAlert((prev) => ({ ...prev, show: true, message: response?.message }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div className="px-5 lg:w-9/12 w-full" style={{ marginTop: '-1.25rem' }}>
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<div className="flex items-center">
|
||||
<DefaultTooltip title="Back to list" placement="top">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-7.5 border-0 px-0 me-[14px]"
|
||||
style={{ marginLeft: -5 }}
|
||||
onClick={handleBackClick}
|
||||
>
|
||||
<KeenIcon icon="arrow-left" className="text-[20px] px-1 card-title" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
<p className="font-semibold text-[16px] card-title">Pengajuan Kredit</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stepper */}
|
||||
<Stepper activeStep={activeStep}>
|
||||
{steps.map((label, index) => (
|
||||
<Step key={index} style={{ marginRight: '-16px', marginLeft: '-8px' }}>
|
||||
<StepLabel></StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
|
||||
{/* Step Content */}
|
||||
{activeStep === steps.length ? (
|
||||
<div className="">
|
||||
<p style={{ marginTop: 2, marginBottom: 1 }}>
|
||||
Semua langkah selesai - Anda telah menyelesaikan proses
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="mt-3 mb-5 text-[12px] text-gray-500">Langkah {activeStep + 1}/4</p>
|
||||
{renderStepContent(activeStep)}
|
||||
<div style={{ display: 'flex', flexDirection: 'row', paddingTop: '16px' }}>
|
||||
{activeStep !== 0 && (
|
||||
<Button
|
||||
className="me-5 text-[14px] w-48 btn btn-outline-secondary bg-gray-200 text-gray-800"
|
||||
style={{ borderRadius: 50 }}
|
||||
disabled={activeStep === 0}
|
||||
onClick={handleBack}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
)}
|
||||
{activeStep === steps.length - 1 && (
|
||||
<Button
|
||||
className="me-5 text-[14px] w-48 btn btn-outline-secondary bg-gray-200 text-gray-800"
|
||||
style={{ borderRadius: 50 }}
|
||||
onClick={handleSaveToDraft}
|
||||
>
|
||||
Simpan ke Draft
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className="text-[14px] w-full btn btn-primary bg-primary"
|
||||
style={{ borderRadius: 50, backgroundColor: '#00519D' }}
|
||||
onClick={handleNext}
|
||||
>
|
||||
{activeStep === steps.length - 1 ? 'Kirim Pengajuan' : 'Selanjutnya'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { CreditAddContext, CreditAddContextProvider };
|
||||
532
src/pages/pengajuan_kredit/list/hooks/CreditDetailContext.tsx
Normal file
532
src/pages/pengajuan_kredit/list/hooks/CreditDetailContext.tsx
Normal file
@ -0,0 +1,532 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLanguage } from '@/i18n';
|
||||
import axios from 'axios';
|
||||
import { fCurrency } from '@/utils/FormatNumber';
|
||||
import { DefaultTooltip, KeenIcon } from '@/components';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import moment from 'moment';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
statusCreditList,
|
||||
toCamelCase,
|
||||
toAbsoluteUrl,
|
||||
ApplicationFileDownloadUrl,
|
||||
getFileExtension,
|
||||
snakeToTitleCase,
|
||||
excludeKeys,
|
||||
updateKeyValueInArray
|
||||
} from '@/utils';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { useAuthContext } from '@/auth';
|
||||
import { ChatMessageIn, ChatMessageOut, Notes } from '../blocks';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { toast } from 'sonner';
|
||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
const CreditDetailContext = createContext<any | null>(null);
|
||||
|
||||
const CreditDetailContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const navigate = useNavigate();
|
||||
const [data, setData] = useState<any>({});
|
||||
const [newChat, setNewChat] = useState<any>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { isRTL } = useLanguage();
|
||||
const { state } = useLocation();
|
||||
const { id, code, status } = state || {};
|
||||
const { auth } = useAuthContext();
|
||||
const { PutData } = useCallApi();
|
||||
const [alert, setAlert] = useState({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
|
||||
const handleBackClick = () => {
|
||||
navigate('/pengajuan_kredit/list');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!id) return;
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/application/detail/${id}`);
|
||||
setData(response.data.data || {});
|
||||
} catch (err) {
|
||||
setError('Failed to fetch data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [id]);
|
||||
|
||||
const handleSelectChange = (
|
||||
newStatus: string,
|
||||
fileKey: string,
|
||||
isStatic: boolean,
|
||||
index?: number
|
||||
) => {
|
||||
if (isStatic) {
|
||||
let debtor = data.debtor;
|
||||
debtor[fileKey] = newStatus;
|
||||
setData({
|
||||
...data,
|
||||
debtor: debtor
|
||||
});
|
||||
} else {
|
||||
if (index !== undefined) {
|
||||
const updatedForm = [...data.form];
|
||||
updatedForm[index].status = newStatus;
|
||||
setData({ ...data, form: updatedForm });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
doUpdate({ ...data, newChat, isReject: false, statusClick: 'Approve' });
|
||||
};
|
||||
|
||||
const handleReject = () => {
|
||||
doUpdate({ ...data, newChat, isReject: true, statusClick: 'Rejected' });
|
||||
};
|
||||
const doUpdate = useCallback(async (data: any) => {
|
||||
let formField = data;
|
||||
|
||||
let application = excludeKeys(data, [
|
||||
'id',
|
||||
'debtor',
|
||||
'company',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
'form',
|
||||
'code',
|
||||
'chat',
|
||||
'newChat',
|
||||
'isReject',
|
||||
'statusClick',
|
||||
'finalize_by'
|
||||
]);
|
||||
|
||||
let debtor = excludeKeys(data.debtor, [
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
'status'
|
||||
]);
|
||||
let form = data.form;
|
||||
|
||||
formField = {
|
||||
...application,
|
||||
...debtor,
|
||||
form: updateKeyValueInArray(form, 'id', 'form_id'),
|
||||
companyId: data.company.id,
|
||||
chat: data.newChat,
|
||||
status: data.isReject ? 'rejected' : 'approved'
|
||||
};
|
||||
|
||||
console.log('formField:', formField);
|
||||
|
||||
const response = await PutData(`${API_URL}/application/update/${data.id}`, formField);
|
||||
if (response?.status) {
|
||||
setAlert((prev) => ({ ...prev, show: false, message: '' }));
|
||||
toast.success('Success Update Instansi');
|
||||
const createActivity = {
|
||||
module: 'Pengajuan Kredit',
|
||||
description: `${data.statusClick} Pengajuan Kredit => ${data.code}`,
|
||||
action: 'U'
|
||||
};
|
||||
doSaveLogActivity(createActivity);
|
||||
navigate('/pengajuan_kredit/list');
|
||||
} else {
|
||||
setAlert((prev) => ({ ...prev, show: true, message: response?.message }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const DivImageAction = (
|
||||
fileName: string,
|
||||
fileStatus: string,
|
||||
onChange: (value: string) => void
|
||||
) => (
|
||||
<>
|
||||
<a href={ApplicationFileDownloadUrl(API_URL, auth?.access_token, fileName)} target="_blank">
|
||||
<img
|
||||
src={toAbsoluteUrl(
|
||||
`/media/file-types/${getFileExtension(fileName) === 'pdf' ? 'pdf.svg' : 'image.svg'}`
|
||||
)}
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
|
||||
<Select value={fileStatus} onValueChange={onChange} disabled={data?.status != 'under_review'}>
|
||||
<SelectTrigger size="sm" className="w-28">
|
||||
<SelectValue placeholder="Action" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="approved">Approved</SelectItem>
|
||||
<SelectItem value="rejected">Rejected</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-center">Fetching data...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>{error}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-5" style={{ marginTop: '-1.25rem' }}>
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<div className="flex items-center">
|
||||
<DefaultTooltip title={'Back to list'} placement={'top'}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-7.5 border-0 px-0 me-[14px]"
|
||||
onClick={handleBackClick}
|
||||
>
|
||||
<KeenIcon icon="arrow-left" className="text-[20px] px-1 card-title" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
<p className="font-semibold text-[22px] card-title">{code}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:flex md:flex-row sm:flex-row gap-5 justify-between">
|
||||
<div className="flex-row gap-5 w-full">
|
||||
<div className=" card shadow-none border-0 w-full ">
|
||||
<div className="card-body p-3 pl-10 pr-10">
|
||||
<p className="card-title text-[14px]">Credit Request</p>
|
||||
<hr className="my-2 border-dashed border-gray-300" />
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">BRI Account Number</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{data?.account_number || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Amount</p>
|
||||
{data?.status === 'under_review' ? (
|
||||
<Input
|
||||
className="input w-9/12"
|
||||
type="number"
|
||||
value={data?.amount}
|
||||
onChange={({ target }) =>
|
||||
setData((prev: any) => ({ ...prev, amount: target.value }))
|
||||
}
|
||||
placeholder="Nominal Pinjaman"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-[14px] text-end" style={{ color: '#212121' }}>
|
||||
{fCurrency(data?.amount) || 'Loading...'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Period</p>
|
||||
{data?.status === 'under_review' ? (
|
||||
<div className="flex gap-3 justify-between">
|
||||
<div className="w-full lg:w-6/12">
|
||||
<Input
|
||||
className="input"
|
||||
type="number"
|
||||
value={data?.period_amount}
|
||||
onChange={({ target }) =>
|
||||
setData((prev: any) => ({ ...prev, period_amount: target.value }))
|
||||
}
|
||||
placeholder="Jangka Waktu"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full lg:w-6/12">
|
||||
<Select
|
||||
value={data?.period_type}
|
||||
onValueChange={(period_type) =>
|
||||
setData((prev: any) => ({ ...prev, period_type }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Pilih jangka waktu" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem className="" value={'month'}>
|
||||
Bulan
|
||||
</SelectItem>
|
||||
<SelectItem className="" value={'year'}>
|
||||
Tahun
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[14px] text-end" style={{ color: '#212121' }}>
|
||||
{data?.period_amount} {toCamelCase(data?.period_type) || 'Loading...'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Status</p>
|
||||
<p className="text-[14px] text-end" style={{ color: '#212121' }}>
|
||||
{snakeToTitleCase(data?.status) || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Finalized by</p>
|
||||
<p className="text-[14px] text-end" style={{ color: '#212121' }}>
|
||||
{data.finalize_by &&
|
||||
`${snakeToTitleCase(data?.finalize_by)} at ${moment(data?.updated_at).format('DD-MM-YYYY HH:mm:ss')}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card shadow-none border-0 w-full ">
|
||||
<div className="card-body p-3 pl-10 pr-10">
|
||||
<p className="card-title text-[14px]">Debitur Info's</p>
|
||||
<hr className="my-2 border-dashed border-gray-300" />
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Application Date</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{moment(data?.application_date).format('dddd, MMMM DD, YYYY') || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Company</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{data?.company?.name || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Employee Id</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{data?.debtor?.employee_id || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Name</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{data?.debtor?.name || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Phone Number</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{data?.debtor?.phone || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Type</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{toCamelCase(data?.debtor?.type) || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center">
|
||||
<p className="text-[14px] w-3/12">Mariage</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{data?.debtor?.marriage_type || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className=" card shadow-none border-0 w-full ">
|
||||
<div className="card-body p-3 pl-10 pr-10">
|
||||
<p className="card-title text-[14px]">Note's</p>
|
||||
<hr className="my-2 border-dashed border-gray-300" />
|
||||
<div className="flex flex-col gap-5 py-5">
|
||||
{data?.chat
|
||||
?.sort(
|
||||
(a: any, b: any) =>
|
||||
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
)
|
||||
.map(
|
||||
(
|
||||
message: {
|
||||
name: string;
|
||||
company: string;
|
||||
message: string;
|
||||
created_at: string;
|
||||
},
|
||||
index: number
|
||||
) => (
|
||||
<Notes
|
||||
key={'CHATS++' + index}
|
||||
userName={message.name}
|
||||
avatar={
|
||||
message.company == 'BRI'
|
||||
? '/media/app/mini-logo.svg'
|
||||
: '/media/avatars/blank.png'
|
||||
}
|
||||
description={''}
|
||||
time={moment(message.created_at).format('ddd DD MMM, hh.mm A')}
|
||||
text={message.message}
|
||||
company={message.company}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{data?.chat && data.chat.length === 0 && (
|
||||
<p className="text-[14px]">No note's available</p>
|
||||
)}
|
||||
{data?.status === 'under_review' && (
|
||||
<>
|
||||
<hr />
|
||||
<Textarea
|
||||
className="input text-[14px] focus-visible:ring-offset-0 focus-visible:ring-0"
|
||||
value={newChat}
|
||||
onChange={({ target }) => setNewChat(target.value)}
|
||||
placeholder="Type your message here..."
|
||||
></Textarea>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-row gap-5 w-full">
|
||||
<div className=" card shadow-none border-0 w-full ">
|
||||
<div className="card-body p-3 pl-10 pr-10">
|
||||
<p className="card-title text-[14px]">Debitur Document's</p>
|
||||
<hr className="my-2 border-dashed border-gray-300" />
|
||||
<div className="flex gap-3 justify-between align-center mb-4">
|
||||
<p className="text-[14px]">
|
||||
{toCamelCase(data?.debtor?.identity_type) || 'Loading...'}
|
||||
</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{data?.debtor?.identity_file &&
|
||||
DivImageAction(
|
||||
data?.debtor?.identity_file,
|
||||
data.debtor?.identity_file_status,
|
||||
(newStatus) => handleSelectChange(newStatus, 'identity_file_status', true)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-between align-center mb-4">
|
||||
<p className="text-[14px]">Kartu Keluarga (Vica Familia)</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{data?.debtor?.family_file &&
|
||||
DivImageAction(
|
||||
data?.debtor?.family_file,
|
||||
data.debtor.family_file_status,
|
||||
(newStatus) => handleSelectChange(newStatus, 'family_file_status', true)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-between align-center mb-4">
|
||||
<p className="text-[14px]">{data?.debtor?.marriage_type || 'Loading...'}</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{data?.debtor?.marriage_file &&
|
||||
DivImageAction(
|
||||
data?.debtor?.marriage_file,
|
||||
data.debtor.marriage_file_status,
|
||||
(newStatus) => handleSelectChange(newStatus, 'marriage_file_status', true)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{data?.debtor?.photo && (
|
||||
<div className="flex gap-3 justify-between align-center mb-4">
|
||||
<p className="text-[14px]">Photo</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{data?.debtor?.photo &&
|
||||
DivImageAction(
|
||||
data?.debtor?.photo, //
|
||||
data?.debtor?.photo_status,
|
||||
(newStatus) => handleSelectChange(newStatus, 'photo_status', true)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.debtor?.spouse_photo && (
|
||||
<div className="flex gap-3 justify-between align-center mb-4">
|
||||
<p className="text-[14px]">Spouse ({data?.debtor?.spouse_type}) Photo</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{data?.debtor?.spouse_photo &&
|
||||
DivImageAction(
|
||||
data?.debtor?.spouse_photo, //
|
||||
data?.debtor?.spouse_photo_status,
|
||||
(newStatus) => handleSelectChange(newStatus, 'spouse_photo_status', true)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.debtor?.spouse_type && (
|
||||
<div className="flex gap-3 justify-between align-center">
|
||||
<p className="text-[14px]">
|
||||
Spouse ({data?.debtor?.spouse_type}) {data?.debtor?.spouse_file_type}
|
||||
</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{data?.debtor?.spouse_file &&
|
||||
DivImageAction(
|
||||
data?.debtor?.spouse_file, //
|
||||
data?.debtor?.spouse_file_status,
|
||||
(newStatus) => handleSelectChange(newStatus, 'spouse_file_status', true)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className=" card shadow-none border-0 w-full ">
|
||||
<div className="card-body p-3 pl-10 pr-10">
|
||||
<p className="card-title text-[14px]">Supporting Document's</p>
|
||||
<hr className="my-2 border-dashed border-gray-300" />
|
||||
{data?.form?.map(
|
||||
(item: { label: string; value: string; status: string }, index: any) => (
|
||||
<div
|
||||
key={`appDetailSupportDocument${index}`}
|
||||
className="flex gap-3 justify-between align-start mb-4"
|
||||
>
|
||||
<p className="text-[14px] w-8/12">{item.label}</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{item.value &&
|
||||
DivImageAction(
|
||||
item.value,
|
||||
item.status,
|
||||
(newStatus) => handleSelectChange(newStatus, 'status', false, index) // Menambahkan index
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{data?.status === 'under_review' && (
|
||||
<div className="flex justify-center gap-5">
|
||||
<div className="">
|
||||
<Button
|
||||
className="px-5 text-[14px] w-48 btn btn-danger bg-danger"
|
||||
onClick={handleReject}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-center gap-5">
|
||||
<div className="">
|
||||
<Button className="px-5 text-[14px] w-48 btn" onClick={handleSubmit}>
|
||||
Terima
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { CreditDetailContext, CreditDetailContextProvider };
|
||||
@ -0,0 +1,379 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLanguage } from '@/i18n';
|
||||
import axios from 'axios';
|
||||
import { fCurrency } from '@/utils/FormatNumber';
|
||||
import { DefaultTooltip, KeenIcon } from '@/components';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { createContext, useEffect, useState } from 'react';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
statusCreditList,
|
||||
toCamelCase,
|
||||
toAbsoluteUrl,
|
||||
ApplicationFileDownloadUrl,
|
||||
getFileExtension,
|
||||
snakeToTitleCase
|
||||
} from '@/utils';
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { useAuthContext } from '@/auth';
|
||||
import { ChatMessageIn, ChatMessageOut, Notes } from '../blocks';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
const CreditDetailContext = createContext<any | null>(null);
|
||||
|
||||
const CreditDetailContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const navigate = useNavigate();
|
||||
const [data, setData] = useState<any>({});
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { isRTL } = useLanguage();
|
||||
const { state } = useLocation();
|
||||
const { id, code, status } = state || {};
|
||||
const { auth } = useAuthContext();
|
||||
|
||||
const handleBackClick = () => {
|
||||
navigate('/pengajuan_kredit/list');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!id) return;
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/application/detail/${id}`);
|
||||
setData(response.data.data || {});
|
||||
console.log('data:', data.form);
|
||||
} catch (err) {
|
||||
setError('Failed to fetch data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [id]);
|
||||
|
||||
const DivImageAction = (
|
||||
fileName: string,
|
||||
fileStatus: string,
|
||||
onChange: (value: string) => void
|
||||
) => (
|
||||
<>
|
||||
<a href={ApplicationFileDownloadUrl(API_URL, auth?.access_token, fileName)} target="_blank">
|
||||
<img
|
||||
src={toAbsoluteUrl(
|
||||
`/media/file-types/${getFileExtension(fileName) == 'dpf' ? 'pdf.svg' : 'image.svg'}`
|
||||
)}
|
||||
className=""
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
|
||||
<Select
|
||||
value={fileStatus}
|
||||
onValueChange={onChange}
|
||||
disabled={data.status == 'revision' ? true : false}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-28">
|
||||
<SelectValue placeholder="Action" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={'approved'}>Approved</SelectItem>
|
||||
<SelectItem value={'rejected'}>Rejected</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-center">Fetching data...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>{error}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-5" style={{ marginTop: '-1.25rem' }}>
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<div className="flex items-center">
|
||||
<DefaultTooltip title={'Back to list'} placement={'top'}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-7.5 border-0 px-0 me-[14px]"
|
||||
onClick={handleBackClick}
|
||||
>
|
||||
<KeenIcon icon="arrow-left" className="text-[20px] px-1 card-title" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
<p className="font-semibold text-[22px] card-title">{code}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:flex md:flex-row sm:flex-row gap-5 justify-between">
|
||||
<div className="flex-row gap-5 w-full">
|
||||
<div className=" card shadow-none border-0 w-full ">
|
||||
<div className="card-body p-3 pl-10 pr-10">
|
||||
<p className="card-title text-[14px]">Credit Request</p>
|
||||
<hr className="my-2 border-dashed border-gray-300" />
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">BRI Account Number</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{data?.account_number || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Amount</p>
|
||||
<p className="text-[14px] text-end" style={{ color: '#212121' }}>
|
||||
{fCurrency(data?.amount) || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Period</p>
|
||||
<p className="text-[14px] text-end" style={{ color: '#212121' }}>
|
||||
{data?.period_amount} {toCamelCase(data?.period_type) || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Status</p>
|
||||
<p className="text-[14px] text-end" style={{ color: '#212121' }}>
|
||||
{snakeToTitleCase(data?.status) || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card shadow-none border-0 w-full ">
|
||||
<div className="card-body p-3 pl-10 pr-10">
|
||||
<p className="card-title text-[14px]">Debitur Info's</p>
|
||||
<hr className="my-2 border-dashed border-gray-300" />
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Application Date</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{moment(data?.application_date).format('dddd, MMMM DD, YYYY') || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Company</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{data?.company?.name || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Employee Id</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{data?.debtor?.employee_id || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Name</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{data?.debtor?.name || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Phone Number</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{data?.debtor?.phone || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center mb-4">
|
||||
<p className="text-[14px] w-3/12">Type</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{toCamelCase(data?.debtor?.type) || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-center">
|
||||
<p className="text-[14px] w-3/12">Mariage</p>
|
||||
<p className="text-[14px]" style={{ color: '#212121' }}>
|
||||
{data?.debtor?.marriage_type || 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className=" card shadow-none border-0 w-full ">
|
||||
<div className="card-body p-3 pl-10 pr-10">
|
||||
<p className="card-title text-[14px]">Note's</p>
|
||||
<hr className="my-2 border-dashed border-gray-300" />
|
||||
<div className="flex flex-col gap-5 py-5">
|
||||
{data?.chat
|
||||
?.sort(
|
||||
(a: any, b: any) =>
|
||||
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
)
|
||||
.map(
|
||||
(
|
||||
message: {
|
||||
name: string;
|
||||
company: string;
|
||||
message: string;
|
||||
created_at: string;
|
||||
},
|
||||
index: number
|
||||
) => (
|
||||
<Notes
|
||||
key={'CHATS++' + index}
|
||||
userName={message.name}
|
||||
avatar={
|
||||
message.company == 'BRI'
|
||||
? '/media/app/mini-logo.svg'
|
||||
: '/media/avatars/blank.png'
|
||||
}
|
||||
description={''}
|
||||
time={moment(message.created_at).format('ddd DD MMM, hh.mm A')}
|
||||
text={message.message}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
className="input focus-visible:ring-offset-0 focus-visible:ring-0"
|
||||
// value={formField.description}
|
||||
// onChange={({ target }) =>
|
||||
// setFormField((prev) => ({ ...prev, description: target.value }))
|
||||
// }
|
||||
></Textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-row gap-5 w-full">
|
||||
<div className=" card shadow-none border-0 w-full ">
|
||||
<div className="card-body p-3 pl-10 pr-10">
|
||||
<p className="card-title text-[14px]">Debitur Document's</p>
|
||||
<hr className="my-2 border-dashed border-gray-300" />
|
||||
<div className="flex gap-3 justify-between align-center mb-4">
|
||||
<p className="text-[14px]">
|
||||
{toCamelCase(data?.debtor?.identity_type) || 'Loading...'}
|
||||
</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{data?.debtor?.identity_file &&
|
||||
DivImageAction(
|
||||
data?.debtor?.identity_file,
|
||||
data.debtor.identity_file_status,
|
||||
() => {}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-between align-center mb-4">
|
||||
<p className="text-[14px]">Kartu Keluarga (Vica Familia)</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{data?.debtor?.family_file &&
|
||||
DivImageAction(
|
||||
data?.debtor?.family_file,
|
||||
data?.debtor?.family_file_status,
|
||||
() => {}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-between align-center mb-4">
|
||||
<p className="text-[14px]">{data?.debtor?.marriage_type || 'Loading...'}</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{data?.debtor?.marriage_file &&
|
||||
DivImageAction(
|
||||
data?.debtor?.marriage_file,
|
||||
data?.debtor?.marriage_file_status,
|
||||
() => {}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{data?.debtor?.photo && (
|
||||
<div className="flex gap-3 justify-between align-center mb-4">
|
||||
<p className="text-[14px]">Photo</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{data?.debtor?.photo &&
|
||||
DivImageAction(
|
||||
data?.debtor?.photo, //
|
||||
data?.debtor?.photo_status,
|
||||
() => {}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.debtor?.spouse_photo && (
|
||||
<div className="flex gap-3 justify-between align-center mb-4">
|
||||
<p className="text-[14px]">Spouse ({data?.debtor?.spouse_type}) Photo</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{data?.debtor?.spouse_photo &&
|
||||
DivImageAction(
|
||||
data?.debtor?.spouse_photo, //
|
||||
data?.debtor?.spouse_photo_status,
|
||||
() => {}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.debtor?.spouse_type && (
|
||||
<div className="flex gap-3 justify-between align-center">
|
||||
<p className="text-[14px]">
|
||||
Spouse ({data?.debtor?.spouse_type}) {data?.debtor?.spouse_file_type}
|
||||
</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{data?.debtor?.spouse_file &&
|
||||
DivImageAction(
|
||||
data?.debtor?.spouse_file, //
|
||||
data?.debtor?.spouse_file_status,
|
||||
() => {}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className=" card shadow-none border-0 w-full ">
|
||||
<div className="card-body p-3 pl-10 pr-10">
|
||||
<p className="card-title text-[14px]">Supporting Document's</p>
|
||||
<hr className="my-2 border-dashed border-gray-300" />
|
||||
{data?.form?.map(
|
||||
(item: { label: string; value: string; status: string }, index: any) => (
|
||||
<div
|
||||
key={`appDetailSupportDocument${index}`}
|
||||
className="flex gap-3 justify-between align-start mb-4"
|
||||
>
|
||||
<p className="text-[14px] w-8/12">{item.label}</p>
|
||||
<div className="flex justify-between gap-3 items-center">
|
||||
{item.value && DivImageAction(item.value, item.status, () => {})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center gap-5">
|
||||
<div className="">
|
||||
<Button
|
||||
className="px-5 text-[14px] w-48 btn btn-danger bg-danger"
|
||||
style={{ borderRadius: 50 }}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
</div>
|
||||
<div className="">
|
||||
<Button
|
||||
className="px-5 text-[14px] w-48 btn btn-success bg-success"
|
||||
style={{ borderRadius: 50 }}
|
||||
>
|
||||
Terima
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { CreditDetailContext, CreditDetailContextProvider };
|
||||
329
src/pages/pengajuan_kredit/list/hooks/CreditListContext.tsx
Normal file
329
src/pages/pengajuan_kredit/list/hooks/CreditListContext.tsx
Normal file
@ -0,0 +1,329 @@
|
||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
import { DataGridColumnHeader, DataGridProvider, KeenIcon } from '@/components';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { fCurrency } from '@/utils/FormatNumber';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { addDays, format } from 'date-fns';
|
||||
import moment from 'moment';
|
||||
import React, { createContext, useMemo, useState } from 'react';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { ListToolBar } from '../blocks/ListToolBar';
|
||||
// import { CardList } from '../blocks';
|
||||
|
||||
import { getAuth } from '@/auth';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { snakeToTitleCase, toCamelCase } from '@/utils';
|
||||
|
||||
interface ContextProps {
|
||||
date: DateRange | undefined;
|
||||
setDate: (date: DateRange | undefined) => void;
|
||||
doExportData: (sorting: any, filter: any) => Promise<any>;
|
||||
}
|
||||
|
||||
const initialProps: ContextProps = {
|
||||
date: undefined,
|
||||
setDate: () => {},
|
||||
doExportData: async () => ({ data: [], totalCount: 0 })
|
||||
};
|
||||
|
||||
const CreditListContext = createContext<ContextProps>(initialProps);
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
const CreditListContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
/* state */
|
||||
const { GetData } = useCallApi();
|
||||
const { GetExportData } = useCallApi();
|
||||
const [date, setDate] = useState<DateRange | undefined>({
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 31)),
|
||||
to: new Date()
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
/* Data Grid Options */
|
||||
const columns = useMemo<ColumnDef<any>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorFn: (row) => row.application_date,
|
||||
id: 'application_date',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Application At" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-1/12 text-center',
|
||||
cellClassName: 'text-center'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.code,
|
||||
id: 'code',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Code" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-1/12'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.company.name,
|
||||
id: 'company.name',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Company" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-2/12'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.debtor.name,
|
||||
id: 'debtor.name',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Debitor" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-3/12'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.debtor.employee_id,
|
||||
id: 'debtor.employee_id',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Employee Id" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-1/12'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.amount,
|
||||
id: 'amount',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Amount" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-1/12 text-end',
|
||||
cellClassName: 'text-end'
|
||||
},
|
||||
cell: (data: any) => fCurrency(data.row.original.amount)
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.period,
|
||||
id: 'period',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Period" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-1/12 text-center',
|
||||
cellClassName: 'text-center'
|
||||
},
|
||||
cell: (data: any) => {
|
||||
const row = data.row.original;
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{data.row.original.period_amount} {toCamelCase(data.row.original.period_type)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.status,
|
||||
id: 'status',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Status" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-1/12 text-center',
|
||||
cellClassName: 'text-center'
|
||||
},
|
||||
cell: (data: any) => String(snakeToTitleCase(data.row.original.status)).toUpperCase()
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.finalize_by,
|
||||
id: 'finalize_by',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Finalized by" column={column} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-1/12 text-center',
|
||||
cellClassName: 'text-center'
|
||||
},
|
||||
cell: (data: any) => {
|
||||
const row = data.row.original;
|
||||
return (
|
||||
<>
|
||||
<p>{String(snakeToTitleCase(data.row.original.finalize_by || '')).toUpperCase()}</p>
|
||||
<p className="text-[12px] whitespace-nowrap">
|
||||
<em>
|
||||
{data.row.original.finalize_by &&
|
||||
moment(data.row.original.update_at).format('DD-MM-YYYY HH:mm:ss')}
|
||||
</em>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
},
|
||||
// {
|
||||
// accessorFn: (row) => row.finalize_by,
|
||||
// id: 'finalize_by',
|
||||
// header: ({ column }) => <DataGridColumnHeader title="Finalized At" column={column} />,
|
||||
// enableSorting: false,
|
||||
// enableHiding: false,
|
||||
// meta: {
|
||||
// headerClassName: 'w-2/12 text-center',
|
||||
// cellClassName: 'text-center'
|
||||
// },
|
||||
// cell: (data: any) =>
|
||||
// data.row.original.finalize_by &&
|
||||
// moment(data.row.original.update_at).format('DD-MM-YYYY HH:mm:ss')
|
||||
// },
|
||||
{
|
||||
id: 'actions',
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader title="Action" className="text-center" column={column} />
|
||||
),
|
||||
cell: (data: any) => {
|
||||
const row = data.row.original;
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm btn-icon btn-clear btn-light"
|
||||
onClick={() =>
|
||||
navigate('/pengajuan_kredit/list/details', {
|
||||
state: { id: row.id, code: row.code, status: row.status }
|
||||
})
|
||||
}
|
||||
>
|
||||
<KeenIcon icon="notepad-edit" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
cellClassName: 'text-center'
|
||||
}
|
||||
}
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const doGetListData = async (page: number, limit: number, sorting: any, filter: any) => {
|
||||
sorting = sorting.length === 0 ? [{ id: 'created_at', desc: false }] : sorting;
|
||||
|
||||
filter = filter?.length === 0 ? {} : filter;
|
||||
let filterObject: Record<string, string | string[]> = {};
|
||||
if (Object.keys(filter).length !== 0) {
|
||||
for (let _filter of filter) {
|
||||
filterObject[_filter.id] = _filter.value;
|
||||
}
|
||||
}
|
||||
|
||||
filter = filterObject;
|
||||
|
||||
const startDate = date?.from
|
||||
? format(date.from, 'yyyy-MM-dd')
|
||||
: format(new Date(new Date().setDate(new Date().getDate() - 30)), 'yyyy-MM-dd');
|
||||
// const endDate = date?.to ? format(date.to, 'yyyy-MM-dd') : format(new Date(), 'yyyy-MM-dd');
|
||||
const endDate = date?.to
|
||||
? format(addDays(date.to, 1), 'yyyy-MM-dd')
|
||||
: format(addDays(new Date(), 1), 'yyyy-MM-dd');
|
||||
|
||||
const response = await GetData(`${API_URL}/application/list`, {
|
||||
limit: limit,
|
||||
page: page + 1,
|
||||
with_deleted: false,
|
||||
order_field: sorting[0].id,
|
||||
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC',
|
||||
filter: JSON.stringify({
|
||||
...filter,
|
||||
application_date_from: startDate,
|
||||
application_date_to: endDate
|
||||
})
|
||||
});
|
||||
|
||||
return { data: response?.data.list, totalCount: response?.data.total_count };
|
||||
};
|
||||
|
||||
const doExportData = async (sorting: any, filter: any) => {
|
||||
sorting = sorting.length === 0 ? [{ id: 'created_at', desc: false }] : sorting;
|
||||
|
||||
const startDate = date?.from
|
||||
? format(date.from, 'yyyy-MM-dd')
|
||||
: format(new Date(2024, 5, 1), 'yyyy-MM-dd');
|
||||
|
||||
const endDate = date?.to ? format(date.to, 'yyyy-MM-dd') : format(new Date(), 'yyyy-MM-dd');
|
||||
|
||||
const filterObject: Record<string, string> = {};
|
||||
if (filter && Array.isArray(filter)) {
|
||||
filter.forEach((f: { id: string; value: string }) => {
|
||||
if (f?.id && f?.value) {
|
||||
filterObject[f.id] = f.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const filtes = {
|
||||
...filterObject,
|
||||
application_date_from: startDate,
|
||||
application_date_to: endDate
|
||||
};
|
||||
|
||||
let param = {
|
||||
with_deleted: false,
|
||||
order_field: sorting[0].id,
|
||||
order_direction: sorting[0].desc === false ? 'ASC' : 'DESC',
|
||||
filter: JSON.stringify(filtes),
|
||||
token: await getAuth()?.access_token
|
||||
};
|
||||
let url = `${API_URL}/application/list/export`;
|
||||
const blob = await GetExportData(url, param, 'pengajuan_kredit_export_');
|
||||
|
||||
if (!(blob instanceof Blob)) {
|
||||
throw new Error('Failed to export data. Invalid response format.');
|
||||
}
|
||||
|
||||
const user = localStorage.getItem('user');
|
||||
const parsedUser = user ? JSON.parse(user) : null;
|
||||
|
||||
const createActivity = {
|
||||
module: 'List Pengajuan Kredit',
|
||||
description: `Export List Pengajuan Kredit => ${parsedUser ? parsedUser.name : 'Unknown User'}`,
|
||||
action: 'E'
|
||||
};
|
||||
|
||||
doSaveLogActivity(createActivity);
|
||||
};
|
||||
|
||||
return (
|
||||
<CreditListContext.Provider
|
||||
value={{
|
||||
date,
|
||||
setDate,
|
||||
doExportData
|
||||
}}
|
||||
>
|
||||
<Toaster expand visibleToasts={9} duration={3000} />
|
||||
<DataGridProvider
|
||||
columns={columns}
|
||||
pagination={{ size: 10 }}
|
||||
toolbar={<ListToolBar />}
|
||||
layout={{ card: true }}
|
||||
sorting={[{ id: 'created_at', desc: false }]}
|
||||
serverSide={true}
|
||||
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
|
||||
doGetListData(pageIndex, pageSize, sorting, columnFilters)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</DataGridProvider>
|
||||
</CreditListContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { CreditListContext, CreditListContextProvider };
|
||||
7
src/pages/pengajuan_kredit/list/hooks/index.ts
Normal file
7
src/pages/pengajuan_kredit/list/hooks/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './CreditListContext';
|
||||
export * from './CreditDetailContext';
|
||||
export * from './CreditAddContext';
|
||||
export * from './useCreditListContext';
|
||||
export * from './useCreditDetailContext';
|
||||
export * from './useCreditAddContext';
|
||||
export * from './useCreditUpdateData';
|
||||
@ -0,0 +1,12 @@
|
||||
import { useContext } from 'react';
|
||||
import { CreditAddContext } from './CreditAddContext';
|
||||
|
||||
const useCreditAddContext = () => {
|
||||
const context = useContext(CreditAddContext);
|
||||
|
||||
if (!context) throw new Error('useCreditAddContext must be used within AuthProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useCreditAddContext };
|
||||
@ -0,0 +1,12 @@
|
||||
import { useContext } from 'react';
|
||||
import { CreditDetailContext } from './CreditDetailContext';
|
||||
|
||||
const useCreditDetailContext = () => {
|
||||
const context = useContext(CreditDetailContext);
|
||||
|
||||
if (!context) throw new Error('useCreditDetailContext must be used within AuthProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useCreditDetailContext };
|
||||
@ -0,0 +1,12 @@
|
||||
import { useContext } from 'react';
|
||||
import { CreditListContext } from './CreditListContext';
|
||||
|
||||
const useCreditListContext = () => {
|
||||
const context = useContext(CreditListContext);
|
||||
|
||||
if (!context) throw new Error('useCreditListContext must be used within AuthProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useCreditListContext };
|
||||
@ -0,0 +1,61 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { formatDate } from 'date-fns';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
interface useCreditUpdateDataResult {
|
||||
cardData: any;
|
||||
isCardLoading: boolean;
|
||||
cardError: any;
|
||||
}
|
||||
|
||||
const useCreditUpdateData = (data: any, isOpenUnder: boolean): any => {
|
||||
const [cardData, setCardData] = useState<[]>([]);
|
||||
const [responseData, setResponseData] = useState<any>(null);
|
||||
const updateData = useCallback(async () => {
|
||||
let dtUpdate = {
|
||||
name: data.deptor.name,
|
||||
employee_id: data.deptor.employe_id,
|
||||
phone: data.deptop.phone,
|
||||
identity_type: data.deptor.identity_type,
|
||||
identity_file: data.deptor.identity_file,
|
||||
identity_file_status: data.deptor.identity_file_status,
|
||||
type: data.deptor.type,
|
||||
marriage_type: data.deptor.marriage_type,
|
||||
marriage_file: data.deptor.marriage_file,
|
||||
marriage_file_status: data.deptor.marriage_file_status,
|
||||
family_file: data.deptor.family_file,
|
||||
family_file_status: data.deptor.family_file_status,
|
||||
spouse_type: data.deptor.spouse_type,
|
||||
spouse_file_type: data.deptor.spouse_file_type,
|
||||
spouse_file: data.deptor.spouse_file,
|
||||
spouse_file_status: data.deptor.spouse_file_status,
|
||||
photo: data.deptor.photo,
|
||||
photo_status: data.deptor.photo_status,
|
||||
spouse_photo: data.deptor.spouse_photo,
|
||||
spouse_photo_status: data.deptor.spouse_photo_status,
|
||||
description: data.deptor.description,
|
||||
form: data.form,
|
||||
account_number: data.account_number,
|
||||
amount: data.amount,
|
||||
period_type: data.period_type,
|
||||
period_amount: data.period_amount,
|
||||
application_date: data.application_date,
|
||||
companyId: data.companyId,
|
||||
chat: isOpenUnder ? '' : data.chat,
|
||||
status: data.status
|
||||
};
|
||||
const response = await axios.put(`${API_URL}/application/update/${data.id}`, { dtUpdate });
|
||||
setResponseData(response.data.data);
|
||||
}, [data, isOpenUnder]);
|
||||
useEffect(() => {
|
||||
updateData();
|
||||
}, [updateData]);
|
||||
|
||||
return { doUpdate: updateData };
|
||||
};
|
||||
|
||||
export { useCreditUpdateData };
|
||||
3
src/pages/pengajuan_kredit/list/index.ts
Normal file
3
src/pages/pengajuan_kredit/list/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './CreditListPage';
|
||||
export * from './CreditDetailPage';
|
||||
export * from './CreditAddPage';
|
||||
104
src/pages/pengajuan_kredit/list/steps/StepFour.tsx
Normal file
104
src/pages/pengajuan_kredit/list/steps/StepFour.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { useState } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { IImageInputFile } from '@/components/image-input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
|
||||
const StepFour = ({ setFormData, formData }: any) => {
|
||||
const [tanggalBuka, setTanggalBuka] = useState<Date>(new Date());
|
||||
const [imageFiles, setImageFiles] = useState<IImageInputFile[]>([]);
|
||||
|
||||
const handleChange = (newFiles: IImageInputFile[], updatedIndexes?: number[]) => {
|
||||
setImageFiles(newFiles);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="" style={{ minHeight: '53vh' }}>
|
||||
<div className="">
|
||||
<p className="text-[14px] form-label">Tipe Debitur</p>
|
||||
</div>
|
||||
<hr className="border-dashed my-3 border-gray-300" />
|
||||
<div className="lg:flex items-baseline gap-5 mb-5">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5 w-full">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">
|
||||
Nomor Rekening
|
||||
</label>
|
||||
<div className="w-full">
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formData.account_number}
|
||||
onChange={({ target }) =>
|
||||
setFormData((prev: any) => ({ ...prev, account_number: target.value }))
|
||||
}
|
||||
placeholder="Nomor Rekening"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:flex items-baseline gap-5 mb-5">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5 w-full">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">Nominal</label>
|
||||
<div className="w-full">
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formData.amount}
|
||||
onChange={({ target }) =>
|
||||
setFormData((prev: any) => ({ ...prev, amount: target.value }))
|
||||
}
|
||||
placeholder="Nominal Pinjaman"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:flex items-baseline gap-5">
|
||||
<div className="w-full">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">
|
||||
Jangka Waktu
|
||||
</label>
|
||||
<div className="items-baseline flex gap-5">
|
||||
<div className="w-full lg:w-6/12 mb-5">
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formData.period_amount}
|
||||
onChange={({ target }) =>
|
||||
setFormData((prev: any) => ({ ...prev, period_amount: target.value }))
|
||||
}
|
||||
placeholder="Jangka Waktu"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full lg:w-6/12 mb-5">
|
||||
<Select
|
||||
value={formData.period_type}
|
||||
onValueChange={(period_type) =>
|
||||
setFormData((prev: any) => ({ ...prev, period_type }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Pilih jangka waktu" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem className="" value={'month'}>
|
||||
Bulan
|
||||
</SelectItem>
|
||||
<SelectItem className="" value={'year'}>
|
||||
Tahun
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { StepFour };
|
||||
252
src/pages/pengajuan_kredit/list/steps/StepOne.tsx
Normal file
252
src/pages/pengajuan_kredit/list/steps/StepOne.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { DatePicker } from '../blocks';
|
||||
import { KeenIcon } from '@/components';
|
||||
import { ImageInput, IImageInputFile } from '@/components/image-input';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
const StepOne = ({ setFormData, formData }: any) => {
|
||||
const [applicationDate, setApplicationDate] = useState<Date>(new Date());
|
||||
const [imageFiles, setImageFiles] = useState<{ [key: string]: IImageInputFile[] }>({});
|
||||
const [selectCompanyList, setSelectCompanyList] = useState<any[]>([]);
|
||||
const { PostData, GetData } = useCallApi();
|
||||
|
||||
const uploadFile = async (file: File, name: any) => {
|
||||
console.log(file);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await PostData(`${API_URL}/application/file/upload`, formData);
|
||||
console.log(response?.message.name);
|
||||
if (response?.status) {
|
||||
toast.success('Success upload document');
|
||||
let obj = [];
|
||||
obj[name] = response.message.name;
|
||||
setFormData((prevState: any) => ({
|
||||
...prevState,
|
||||
...obj
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageChange = (name: string) => (value: IImageInputFile[]) => {
|
||||
setImageFiles((prev) => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
value.forEach((item) => {
|
||||
if (item.file) {
|
||||
uploadFile(item.file, name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const doGetListCompanyList = async (page: number, limit: number, sorting: any, filter: any) => {
|
||||
sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting;
|
||||
filter = filter.length == 0 ? [] : filter[0].value;
|
||||
|
||||
const response = await GetData(`${API_URL}/company/list`, {
|
||||
limit: 1000,
|
||||
page: 1,
|
||||
with_deleted: false,
|
||||
order_field: 'id',
|
||||
order_direction: 'ASC',
|
||||
filter: JSON.stringify({ status: 'Y' })
|
||||
});
|
||||
|
||||
return { data: response?.data.list, totalCount: response?.data.total_count };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCompanyList = async () => {
|
||||
try {
|
||||
const { data } = await doGetListCompanyList(1, 1000, [], []);
|
||||
setSelectCompanyList(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch instansi', error);
|
||||
}
|
||||
};
|
||||
fetchCompanyList();
|
||||
}, []);
|
||||
return (
|
||||
<div className="" style={{ minHeight: '53vh' }}>
|
||||
<div className="">
|
||||
<p className="text-[14px] form-label">Data Calon Debitur</p>
|
||||
</div>
|
||||
<hr className="border-dashed my-3 border-gray-300" />
|
||||
<div className="lg:flex items-baseline gap-5">
|
||||
<div className="w-full lg:w-6/12 mb-5">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">
|
||||
Nama Lengkap
|
||||
</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={({ target }) =>
|
||||
setFormData((prev: any) => ({ ...prev, name: target.value }))
|
||||
}
|
||||
placeholder="Nama Lengkap Debitur"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-6/12 mb-5">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">ID</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formData.employee_id}
|
||||
onChange={({ target }) =>
|
||||
setFormData((prev: any) => ({ ...prev, employee_id: target.value }))
|
||||
}
|
||||
placeholder="Employee ID"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:flex items-baseline gap-5">
|
||||
<div className="w-full lg:w-6/12 mb-5">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">
|
||||
Nomor Telpon
|
||||
</label>
|
||||
<Input
|
||||
className="input"
|
||||
type="text"
|
||||
value={formData.phone}
|
||||
onChange={({ target }) =>
|
||||
setFormData((prev: any) => ({ ...prev, phone: target.value }))
|
||||
}
|
||||
placeholder="Nomor Telpon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-6/12 mb-5">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">
|
||||
Tanggal Pengajuan
|
||||
</label>
|
||||
<DatePicker
|
||||
date={applicationDate}
|
||||
setDate={setApplicationDate}
|
||||
value={formData.application_date}
|
||||
className="h-10 text-2sm"
|
||||
onChange={({ target }) =>
|
||||
setFormData((prev: any) => ({ ...prev, application_date: target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:flex items-baseline gap-5">
|
||||
<div className="lg:flex items-baseline gap-5 w-full lg:w-6/12 mb-5">
|
||||
<div className="w-full">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">
|
||||
Tipe Dokumen
|
||||
</label>
|
||||
<div className="items-baseline flex gap-5 w-full mb-5">
|
||||
<Select
|
||||
value={formData.identity_type}
|
||||
onValueChange={(identity_type) =>
|
||||
setFormData((prev: any) => ({ ...prev, identity_type }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Pilih tipe dokumen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem className="" value={'electoral'}>
|
||||
Electoral
|
||||
</SelectItem>
|
||||
<SelectItem className="" value={'passport'}>
|
||||
Passport
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<ImageInput
|
||||
value={imageFiles.identity_file}
|
||||
onChange={handleImageChange('identity_file')}
|
||||
multiple={false}
|
||||
>
|
||||
{({ fileList, onImageUpload, onImageRemove, dragProps, isDragging }) => (
|
||||
<button onClick={onImageUpload} className="text-[13px] text-gray-500 w-full">
|
||||
<div
|
||||
{...dragProps}
|
||||
style={{
|
||||
border: isDragging ? '1px dashed #4CAF50' : '1px dashed #006599',
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
borderRadius: '0.375rem',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<KeenIcon icon="file-up" className="text-[20px] px-1 card-title" />
|
||||
{fileList.length > 0 ? (
|
||||
fileList.map((file, index) => (
|
||||
<div key={index}>
|
||||
<p>{file.file?.name}</p>
|
||||
<button onClick={() => onImageRemove(index)} className="text-danger">
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="">Click here to import file</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</ImageInput>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-6/12 mb-5">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">
|
||||
Instansi Pengaju
|
||||
</label>
|
||||
<Select
|
||||
value={formData.companyId}
|
||||
onValueChange={(companyId) => setFormData((prev: any) => ({ ...prev, companyId }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Pilih instansi" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{selectCompanyList.map((type, index) => (
|
||||
<SelectItem className="" key={index} value={type.id}>
|
||||
{type.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { StepOne };
|
||||
133
src/pages/pengajuan_kredit/list/steps/StepThree.tsx
Normal file
133
src/pages/pengajuan_kredit/list/steps/StepThree.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { KeenIcon } from '@/components';
|
||||
import { ImageInput, IImageInputFile } from '@/components/image-input';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
const StepThree = ({ setFormData, formData }: any) => {
|
||||
const [imageFiles, setImageFiles] = useState<{ [key: string]: IImageInputFile[] }>({});
|
||||
const [formList, setFormList] = useState<any[]>([]);
|
||||
const { PostData, GetData } = useCallApi();
|
||||
|
||||
const uploadFile = async (file: File, name: string, id: string) => {
|
||||
// console.log(file);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await PostData(`${API_URL}/application/file/upload`, formData);
|
||||
|
||||
if (response?.status) {
|
||||
toast.success('Success upload document');
|
||||
// let obj = { [name]: response.message.name };
|
||||
let form = {
|
||||
id: id,
|
||||
name: name,
|
||||
type: 'file',
|
||||
value: response?.message.name
|
||||
};
|
||||
console.log('form:', form);
|
||||
setFormData((prevState: any) => ({
|
||||
...prevState,
|
||||
form: [...prevState.form, form] // Tambahkan form ke array form
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error);
|
||||
}
|
||||
};
|
||||
const handleImageChange = (name: string, id: string) => (value: IImageInputFile[]) => {
|
||||
setImageFiles((prev) => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
value.forEach((item) => {
|
||||
if (item.file) {
|
||||
uploadFile(item.file, name, id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const doGetFormList = async (code: string) => {
|
||||
const response = await GetData(`${API_URL}/application/form/list`, {
|
||||
code: formData?.type
|
||||
});
|
||||
return { data: response?.data };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFormList = async () => {
|
||||
try {
|
||||
const { data } = await doGetFormList(formData?.type);
|
||||
setFormList(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch form type', error);
|
||||
}
|
||||
};
|
||||
fetchFormList();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mb-5" style={{ minHeight: '53vh' }}>
|
||||
<div className="">
|
||||
<p className="text-[14px] form-label">Persyaratan Lain</p>
|
||||
</div>
|
||||
<hr className="border-dashed my-3 border-gray-300" />
|
||||
<div className="grid lg:grid-cols-2 gap-y-5 lg:gap-5 items-stretch">
|
||||
{formList.map((type, index) => (
|
||||
<div key={index} className="flex flex-col justify-between">
|
||||
<div className="mb-2">
|
||||
<label className="form-label flex items-center gap-1 text-[13px]">{type.label}</label>
|
||||
<p className="text-danger text-[13px]">{type.label_red}</p>
|
||||
</div>
|
||||
<ImageInput
|
||||
value={imageFiles[type.name]}
|
||||
onChange={handleImageChange(type.name, type.id)}
|
||||
multiple={false}
|
||||
>
|
||||
{({ fileList, onImageUpload, onImageRemove, dragProps, isDragging }) => (
|
||||
<button onClick={onImageUpload} className="text-[13px] text-gray-500 w-full">
|
||||
<div
|
||||
{...dragProps}
|
||||
style={{
|
||||
border: isDragging ? '1px dashed #4CAF50' : '1px dashed #006599',
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
borderRadius: '0.375rem',
|
||||
minHeight: '152px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<KeenIcon icon="file-up" className="text-[20px] px-1 card-title" />
|
||||
{fileList.length > 0 ? (
|
||||
fileList.map((file, index) => (
|
||||
<div key={index}>
|
||||
<p>{file.file?.name}</p>
|
||||
<button onClick={() => onImageRemove(index)} className="text-danger">
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="">Click here to import file</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</ImageInput>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { StepThree };
|
||||
377
src/pages/pengajuan_kredit/list/steps/StepTwo.tsx
Normal file
377
src/pages/pengajuan_kredit/list/steps/StepTwo.tsx
Normal file
@ -0,0 +1,377 @@
|
||||
import { useState } from 'react';
|
||||
import { KeenIcon } from '@/components';
|
||||
import { ImageInput, IImageInputFile } from '@/components/image-input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const API_URL = apiConfig.service_credit;
|
||||
|
||||
const StepTwo = ({ setFormData, formData }: any) => {
|
||||
const [imageFiles, setImageFiles] = useState<{ [key: string]: IImageInputFile[] }>({});
|
||||
const { PostData } = useCallApi();
|
||||
|
||||
const uploadFile = async (file: File, name: string) => {
|
||||
// console.log(file);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await PostData(`${API_URL}/application/file/upload`, formData);
|
||||
if (response?.status) {
|
||||
toast.success('Success upload document');
|
||||
let obj = { [name]: response.message.name };
|
||||
setFormData((prevState: any) => ({
|
||||
...prevState,
|
||||
...obj
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageChange = (name: string) => (value: IImageInputFile[]) => {
|
||||
setImageFiles((prev) => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
value.forEach((item) => {
|
||||
if (item.file) {
|
||||
uploadFile(item.file, name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '53vh' }}>
|
||||
<div>
|
||||
<p className="text-[14px] form-label">Tipe Debitur</p>
|
||||
</div>
|
||||
<hr className="border-dashed my-3 border-gray-300" />
|
||||
<div className="lg:flex items-baseline gap-5 mb-5">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5 w-full">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">
|
||||
Tipe Debitur
|
||||
</label>
|
||||
<div className="w-full">
|
||||
<Select
|
||||
value={formData.type}
|
||||
onValueChange={(type) => setFormData((prev: any) => ({ ...prev, type }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Pilih tipe debitur" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={'sipil'}>Sipil</SelectItem>
|
||||
<SelectItem value={'veteran'}>Veteran</SelectItem>
|
||||
<SelectItem value={'bctl'}>BCTL</SelectItem>
|
||||
<SelectItem value={'pntl'}>PNTL</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:flex items-baseline gap-5">
|
||||
<div className="lg:flex items-baseline gap-5 w-full lg:w-6/12 mb-5">
|
||||
<div className="w-full">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">
|
||||
Surat Keterangan Menikah/ Belum Menikah
|
||||
</label>
|
||||
<div className="items-baseline flex gap-5 w-full">
|
||||
<Select
|
||||
value={formData.marriage_type}
|
||||
onValueChange={(marriage_type) =>
|
||||
setFormData((prev: any) => ({ ...prev, marriage_type }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Pilih tipe dokumen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={'Deklarasaun Solteiro'}>Deklarasaun Solteiro</SelectItem>
|
||||
<SelectItem value={'Certidão Casamento'}>Certidão Casamento</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<ImageInput
|
||||
value={imageFiles.marriage_file}
|
||||
onChange={handleImageChange('marriage_file')}
|
||||
multiple={false}
|
||||
>
|
||||
{({ fileList, onImageUpload, onImageRemove, dragProps, isDragging }) => (
|
||||
<button onClick={onImageUpload} className="text-[13px] text-gray-500 w-full">
|
||||
<div
|
||||
{...dragProps}
|
||||
style={{
|
||||
border: isDragging ? '1px dashed #4CAF50' : '1px dashed #006599',
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
borderRadius: '0.375rem'
|
||||
}}
|
||||
>
|
||||
<KeenIcon icon="file-up" className="text-[20px] px-1 card-title block" />
|
||||
<div>
|
||||
{fileList.length > 0 ? (
|
||||
fileList.map((file, index) => (
|
||||
<div key={index}>
|
||||
<p>{file.file?.name}</p>
|
||||
<button onClick={() => onImageRemove(index)} className="text-danger">
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="">Click here to import file</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</ImageInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-6/12 mb-5">
|
||||
<div className="items-baseline lg:flex-nowrap gap-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">
|
||||
Kartu Keluarga (Vica Familia)
|
||||
</label>
|
||||
<ImageInput
|
||||
value={imageFiles.family_file}
|
||||
onChange={handleImageChange('family_file')}
|
||||
multiple={false}
|
||||
>
|
||||
{({ fileList, onImageUpload, onImageRemove, dragProps, isDragging }) => (
|
||||
<button onClick={onImageUpload} className="text-[13px] text-gray-500 w-full">
|
||||
<div
|
||||
{...dragProps}
|
||||
style={{
|
||||
border: isDragging ? '1px dashed #4CAF50' : '1px dashed #006599',
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
borderRadius: '0.375rem',
|
||||
minHeight: '152px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<KeenIcon icon="file-up" className="text-[20px] px-1 card-title" />
|
||||
{fileList.length > 0 ? (
|
||||
fileList.map((file, index) => (
|
||||
<div key={index}>
|
||||
<p>{file.file?.name}</p>
|
||||
<button onClick={() => onImageRemove(index)} className="text-danger">
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="">Click here to import file</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</ImageInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{formData.marriage_type === 'Certidão Casamento' && (
|
||||
<>
|
||||
<hr className="mb-3 border-dashed" />
|
||||
<div className="lg:flex items-baseline gap-5 w-full">
|
||||
<div className="w-full mb-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">
|
||||
Tipe Dokumen
|
||||
</label>
|
||||
<div className="items-baseline flex gap-5 w-full mb-5">
|
||||
<Select
|
||||
value={formData.spouse_type}
|
||||
onValueChange={(spouse_type) =>
|
||||
setFormData((prev: any) => ({ ...prev, spouse_type }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Pilih pasangan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem className="" value={'husband'}>
|
||||
Suami
|
||||
</SelectItem>
|
||||
<SelectItem className="" value={'wife'}>
|
||||
Istri
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={formData.spouse_file_type}
|
||||
onValueChange={(spouse_file_type) =>
|
||||
setFormData((prev: any) => ({ ...prev, spouse_file_type }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Pilih tipe dokumen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem className="" value={'electoral'}>
|
||||
Electoral
|
||||
</SelectItem>
|
||||
<SelectItem className="" value={'passport'}>
|
||||
Passport
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<ImageInput
|
||||
value={imageFiles.spouse_file}
|
||||
onChange={handleImageChange('spouse_file')}
|
||||
multiple={false}
|
||||
>
|
||||
{({ fileList, onImageUpload, onImageRemove, dragProps, isDragging }) => (
|
||||
<button onClick={onImageUpload} className="text-[13px] text-gray-500 w-full">
|
||||
<div
|
||||
{...dragProps}
|
||||
style={{
|
||||
border: isDragging ? '1px dashed #4CAF50' : '1px dashed #006599',
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
borderRadius: '0.375rem',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex',
|
||||
minHeight: '148px'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<KeenIcon icon="file-up" className="text-[20px] px-1 card-title" />
|
||||
{fileList.length > 0 ? (
|
||||
fileList.map((file, index) => (
|
||||
<div key={index}>
|
||||
<p>{file.file?.name}</p>
|
||||
<button onClick={() => onImageRemove(index)} className="text-danger">
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="">Click here to import file</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</ImageInput>
|
||||
</div>
|
||||
|
||||
<div className="w-full mb-5">
|
||||
<label className="form-label flex items-center gap-1 mb-2 text-[13px]">
|
||||
Pas Foto Debitur & Pasangan (3x4)
|
||||
</label>
|
||||
<div className="mb-5">
|
||||
<ImageInput
|
||||
value={imageFiles.photo}
|
||||
onChange={handleImageChange('photo')}
|
||||
multiple={false}
|
||||
>
|
||||
{({ fileList, onImageUpload, onImageRemove, dragProps, isDragging }) => (
|
||||
<button onClick={onImageUpload} className="text-[13px] text-gray-500 w-full">
|
||||
<div
|
||||
{...dragProps}
|
||||
style={{
|
||||
border: isDragging ? '1px dashed #4CAF50' : '1px dashed #006599',
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
borderRadius: '0.375rem',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<KeenIcon icon="file-up" className="text-[20px] px-1 card-title" />
|
||||
{fileList.length > 0 ? (
|
||||
fileList.map((file, index) => (
|
||||
<div key={index}>
|
||||
<p>{file.file?.name}</p>
|
||||
<button
|
||||
onClick={() => onImageRemove(index)}
|
||||
className="text-danger"
|
||||
>
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="">Click here to import file</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</ImageInput>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<ImageInput
|
||||
value={imageFiles.spouse_photo}
|
||||
onChange={handleImageChange('spouse_photo')}
|
||||
multiple={false}
|
||||
>
|
||||
{({ fileList, onImageUpload, onImageRemove, dragProps, isDragging }) => (
|
||||
<button onClick={onImageUpload} className="text-[13px] text-gray-500 w-full">
|
||||
<div
|
||||
{...dragProps}
|
||||
style={{
|
||||
border: isDragging ? '1px dashed #4CAF50' : '1px dashed #006599',
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
borderRadius: '0.375rem',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<KeenIcon icon="file-up" className="text-[20px] px-1 card-title" />
|
||||
{fileList.length > 0 ? (
|
||||
fileList.map((file, index) => (
|
||||
<div key={index}>
|
||||
<p>{file.file?.name}</p>
|
||||
<button
|
||||
onClick={() => onImageRemove(index)}
|
||||
className="text-danger"
|
||||
>
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="">Click here to import file</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</ImageInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { StepTwo };
|
||||
4
src/pages/pengajuan_kredit/list/steps/index.ts
Normal file
4
src/pages/pengajuan_kredit/list/steps/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './StepOne';
|
||||
export * from './StepTwo';
|
||||
export * from './StepThree';
|
||||
export * from './StepFour';
|
||||
224
src/pages/pengajuan_kredit/summary/CreditSummaryPage.tsx
Normal file
224
src/pages/pengajuan_kredit/summary/CreditSummaryPage.tsx
Normal file
@ -0,0 +1,224 @@
|
||||
import { Container, KeenIcon, ContentLoader, DefaultTooltip } from '@/components';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { DateRangePicker, List, ListToolBar, DetailDialog } from './blocks';
|
||||
|
||||
import { CreditSummaryContextProvider, useCreditSummaryContext } from './hooks';
|
||||
|
||||
import { formatDate } from 'date-fns';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type IntervalType = 'day' | 'week' | 'month';
|
||||
type CountType = 'sum' | 'count';
|
||||
|
||||
interface CreditSummaryExportProps {
|
||||
date: DateRange | undefined;
|
||||
interval: 'day' | 'week' | 'month';
|
||||
count: 'sum' | 'count';
|
||||
loadingButton: LoadingButton | boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
type LoadingButton = 'filter' | 'reset' | 'export' | 'refresh' | null;
|
||||
|
||||
const CreditSummaryPage = () => {
|
||||
const [date, setDate] = useState<DateRange | undefined>({
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 14)),
|
||||
to: new Date()
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [loadingButton, setLoadingButton] = useState<LoadingButton>(null);
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [titleDialog, setTitleDialog] = useState('');
|
||||
const [propsDialog, setPropsDialog] = useState<any>();
|
||||
const [interval, setInterval] = useState<IntervalType>('day');
|
||||
const [count, setCount] = useState<CountType>('sum');
|
||||
const handleDialogClick = useCallback((title: string, props: {}) => {
|
||||
setOpenDialog((openDialog) => !openDialog);
|
||||
setTitleDialog(title);
|
||||
setPropsDialog(props);
|
||||
}, []);
|
||||
|
||||
const [filter, setFilter] = useState({
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 14)),
|
||||
to: new Date(),
|
||||
interval: 'day' as IntervalType,
|
||||
count: 'sum' as CountType
|
||||
});
|
||||
|
||||
const handleFilter = useCallback(
|
||||
(date: DateRange | undefined) => {
|
||||
setFilter((prev) => ({
|
||||
...prev,
|
||||
from: date?.from ?? new Date(new Date().setDate(new Date().getDate() - 14)),
|
||||
to: date?.to ?? new Date(),
|
||||
interval: interval,
|
||||
count: count
|
||||
}));
|
||||
},
|
||||
[interval, count]
|
||||
);
|
||||
|
||||
const handleSelect = (value: IntervalType) => {
|
||||
setInterval(value);
|
||||
if (value === 'day') {
|
||||
setDate({
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 14)),
|
||||
to: new Date()
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleCount = (value: CountType) => {
|
||||
setCount(value);
|
||||
};
|
||||
|
||||
const resetFilter = useCallback(() => {
|
||||
setFilter((prev) => ({
|
||||
...prev,
|
||||
interval: 'day',
|
||||
count: 'sum',
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 14)),
|
||||
to: new Date()
|
||||
}));
|
||||
setInterval('day');
|
||||
setCount('sum');
|
||||
setDate({
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 14)),
|
||||
to: new Date()
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CreditSummaryContextProvider>
|
||||
<Container>
|
||||
<div className="grid gap-5 lg:gap-7.5">
|
||||
<List
|
||||
start_date={formatDate(filter?.from ?? new Date(), 'yyyy-MM-dd')}
|
||||
end_date={formatDate(filter?.to ?? new Date(), 'yyyy-MM-dd')}
|
||||
interval={filter.interval}
|
||||
count={filter.count}
|
||||
toolbar={
|
||||
<ListToolBar>
|
||||
<div className="flex gap-3 items-center w-1/2">
|
||||
<div className="w-auto min-w-[120px]">
|
||||
<Select value={interval} onValueChange={handleSelect}>
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-32">
|
||||
<SelectItem value="day">Daily</SelectItem>
|
||||
<SelectItem value="week">Weekly</SelectItem>
|
||||
<SelectItem value="month">Monthly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-auto min-w-[120px]">
|
||||
<Select value={count} onValueChange={handleCount}>
|
||||
<SelectTrigger size="sm">
|
||||
<SelectValue placeholder="Select Count" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-32">
|
||||
<SelectItem value="sum">Sum</SelectItem>
|
||||
<SelectItem value="count">Count</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-auto min-w-[220px]">
|
||||
<DateRangePicker date={date} setDate={setDate} interval={interval} />
|
||||
</div>
|
||||
<DefaultTooltip title={'Filter'} placement={'top'}>
|
||||
<Button variant="outline" className="h-7.5" onClick={() => handleFilter(date)}>
|
||||
<KeenIcon icon="filter" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
<DefaultTooltip title={'Reset Filter'} placement={'top'}>
|
||||
<Button variant="outline" className="h-7.5" onClick={() => resetFilter()}>
|
||||
<KeenIcon icon="arrow-circle-left" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center">
|
||||
<BgSummaryExport
|
||||
date={date}
|
||||
interval={interval}
|
||||
loadingButton={loadingButton}
|
||||
isLoading={isLoading}
|
||||
count={count}
|
||||
/>
|
||||
</div>
|
||||
</ListToolBar>
|
||||
}
|
||||
openDetail={handleDialogClick}
|
||||
/>
|
||||
</div>
|
||||
<DetailDialog
|
||||
open={openDialog}
|
||||
title={titleDialog}
|
||||
desc=""
|
||||
props={propsDialog}
|
||||
onOpenChange={() => handleDialogClick('', {})}
|
||||
/>
|
||||
</Container>
|
||||
</CreditSummaryContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const BgSummaryExport = ({
|
||||
date,
|
||||
interval,
|
||||
count,
|
||||
loadingButton,
|
||||
isLoading
|
||||
}: CreditSummaryExportProps) => {
|
||||
const { doExportData } = useCreditSummaryContext();
|
||||
|
||||
const handleExport = useCallback(
|
||||
async (date: DateRange | undefined) => {
|
||||
try {
|
||||
const startDate = date?.from ?? new Date(new Date().setDate(new Date().getDate() - 31));
|
||||
const endDate = date?.to ?? new Date();
|
||||
|
||||
await doExportData(startDate, endDate, interval, count);
|
||||
toast.success('Success export summary pengajuan kredit.');
|
||||
} catch (error) {
|
||||
toast.error('Failed export data. Please try again.');
|
||||
}
|
||||
},
|
||||
[doExportData, interval]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DefaultTooltip title={'Export Data'} placement={'top'}>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className="btn h-7.5"
|
||||
disabled={isLoading || !!loadingButton}
|
||||
onClick={() => handleExport(date)}
|
||||
>
|
||||
{loadingButton === 'export' ? (
|
||||
<ContentLoader />
|
||||
) : (
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/file-types/xls.svg')}
|
||||
className="dark:hidden h-5"
|
||||
alt="Export to Excel"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreditSummaryPage;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user