This commit is contained in:
fro1991
2025-02-01 19:53:11 +07:00
parent 13a2c4d42c
commit 7e67f5f828
61 changed files with 3 additions and 5609 deletions

View File

@ -4,7 +4,7 @@ import { DateRange } from 'react-day-picker';
import { formatDate } from 'date-fns'; import { formatDate } from 'date-fns';
import { apiConfig } from '@/config/api.config'; import { apiConfig } from '@/config/api.config';
const API_URL = apiConfig.service_credit; const API_URL = apiConfig.service_dashboard;
interface UseFetchCardDataResult { interface UseFetchCardDataResult {
cardData: any; cardData: any;

View File

@ -4,7 +4,7 @@ import { DateRange } from 'react-day-picker';
import { formatDate } from 'date-fns'; import { formatDate } from 'date-fns';
import { apiConfig } from '@/config/api.config'; import { apiConfig } from '@/config/api.config';
const API_URL = apiConfig.service_credit; const API_URL = apiConfig.service_dashboard;
interface UseFetchChartDataResult { interface UseFetchChartDataResult {
chartData: any[]; chartData: any[];

View File

@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
import { apiConfig } from '@/config/api.config'; import { apiConfig } from '@/config/api.config';
const API_URL = apiConfig.service_credit; const API_URL = apiConfig.service_dashboard;
interface YearData { interface YearData {
year: string; year: string;

View File

@ -1,20 +0,0 @@
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>
);
}

View File

@ -1,262 +0,0 @@
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 };

View File

@ -1,75 +0,0 @@
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 };

View File

@ -1,275 +0,0 @@
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 };

View File

@ -1,96 +0,0 @@
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 };

View File

@ -1,3 +0,0 @@
export * from './ListToolBar';
export * from './EditDialog';
export * from './AddDialog';

View File

@ -1,93 +0,0 @@
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 };

View File

@ -1,223 +0,0 @@
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 };

View File

@ -1,2 +0,0 @@
export * from './InstansiContext';
export * from './useInstansiContext';

View File

@ -1,12 +0,0 @@
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 };

View File

@ -1 +0,0 @@
export * from './InstansiPage';

View File

@ -1,189 +0,0 @@
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;

View File

@ -1,193 +0,0 @@
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 };

View File

@ -1,74 +0,0 @@
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 };

View File

@ -1,92 +0,0 @@
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 };

View File

@ -1,3 +0,0 @@
export * from './Chart';
export * from './YearPicker';
export * from './DateRangePicker';

View File

@ -1,17 +0,0 @@
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 };

View File

@ -1,3 +0,0 @@
export * from './useCreditChartContext';
export * from './useFetchCreditChartData';
export * from './CreditChartContext';

View File

@ -1,11 +0,0 @@
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 };

View File

@ -1,53 +0,0 @@
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 };

View File

@ -1 +0,0 @@
export * from './CreditChartPage';

View File

@ -1,12 +0,0 @@
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>
);
}

View File

@ -1,12 +0,0 @@
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>
);
}

View File

@ -1,14 +0,0 @@
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>
);
}

View File

@ -1,22 +0,0 @@
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 };

View File

@ -1,27 +0,0 @@
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 };

View File

@ -1,58 +0,0 @@
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 };

View File

@ -1,60 +0,0 @@
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 };

View File

@ -1,293 +0,0 @@
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 };

View File

@ -1,49 +0,0 @@
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 };

View File

@ -1,6 +0,0 @@
export * from './ListToolBar';
export * from './DateRangePicker';
export * from './DatePicker';
export * from './ChatMessageIn';
export * from './ChatMessageOut';
export * from './Notes'

View File

@ -1,215 +0,0 @@
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 };

View File

@ -1,532 +0,0 @@
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 };

View File

@ -1,379 +0,0 @@
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 };

View File

@ -1,329 +0,0 @@
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 };

View File

@ -1,7 +0,0 @@
export * from './CreditListContext';
export * from './CreditDetailContext';
export * from './CreditAddContext';
export * from './useCreditListContext';
export * from './useCreditDetailContext';
export * from './useCreditAddContext';
export * from './useCreditUpdateData';

View File

@ -1,12 +0,0 @@
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 };

View File

@ -1,12 +0,0 @@
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 };

View File

@ -1,12 +0,0 @@
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 };

View File

@ -1,61 +0,0 @@
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 };

View File

@ -1,3 +0,0 @@
export * from './CreditListPage';
export * from './CreditDetailPage';
export * from './CreditAddPage';

View File

@ -1,104 +0,0 @@
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 };

View File

@ -1,252 +0,0 @@
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 };

View File

@ -1,133 +0,0 @@
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 };

View File

@ -1,377 +0,0 @@
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 };

View File

@ -1,4 +0,0 @@
export * from './StepOne';
export * from './StepTwo';
export * from './StepThree';
export * from './StepFour';

View File

@ -1,224 +0,0 @@
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;

View File

@ -1,74 +0,0 @@
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 };

View File

@ -1,333 +0,0 @@
import { useEffect, useRef, useState, useMemo } from 'react';
import { fShortenNumber, fCurrency } from '@/utils/FormatNumber';
import { formatDate } from 'date-fns';
import { DataGrid, DataGridColumnHeader, DataGridProvider, KeenIcon } from '@/components';
import { ColumnDef } from '@tanstack/react-table';
import axios from 'axios';
import { apiConfig } from '@/config/api.config';
import moment from 'moment';
const API_URL = apiConfig.service_bank;
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
interface IModalProps {
open: boolean;
title: string;
desc: string;
props: { start_date: string; end_date: string; filter: { [key: string]: any } };
onOpenChange: () => void;
}
const defaultSorting = [{ id: 'id', desc: false }];
const DetailDialog = ({ open, title, desc, props, onOpenChange }: IModalProps) => {
const navBar = useRef<any | null>(null);
const parentRef = useRef<any | null>(null);
// console.log('open, title, desc, props, onOpenChange :', open, title, desc, props, onOpenChange);
const fecthData = async (page: number, limit: number, sorting: any, filter: any) => {
sorting = sorting.length == 0 ? [{ id: 'id', desc: false }] : sorting;
filter = filter.length == 0 ? [] : filter[0].value;
const startDate = props.start_date;
const endDate = props.end_date;
delete props.filter?.start_date;
delete props.filter?.end_date;
delete props.filter['ca.open_date'];
filter = {
...props.filter,
open_date_from: startDate,
open_date_to: endDate
};
const response = await axios.get(`${API_URL}/bank/dpk/list/`, {
params: {
filter: JSON.stringify(filter),
limit: limit,
page: page + 1,
with_deleted: false,
order_field: sorting[0].id,
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC'
}
});
return { data: response.data.data.list, totalCount: response.data.data.total_count };
};
const columns = useMemo<ColumnDef<any>[]>(
() => [
{
accessorFn: (row) => row.cif_number,
id: 'cif_number',
header: ({ column }) => <DataGridColumnHeader title="CIF Number" 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.account_number,
id: 'account_number',
header: ({ column }) => <DataGridColumnHeader title="No. Rekening" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.branch.code,
id: 'branch.code',
header: ({ column }) => (
<DataGridColumnHeader title="Kode Cabang Rekening" column={column} />
),
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => moment(row.open_date).format('YYYY-MM-DD'),
id: 'open_date',
header: ({ column }) => <DataGridColumnHeader title="Tanggal Buka" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.product.code,
id: 'product.code',
header: ({ column }) => <DataGridColumnHeader title="Product Type" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.product.name,
id: 'product.name',
header: ({ column }) => (
<DataGridColumnHeader title="Product Description" column={column} />
),
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => fCurrency(row.current_balance),
id: 'current_balance',
header: ({ column }) => <DataGridColumnHeader title="Current Balance" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.country_code,
id: 'country_code',
header: ({ column }) => <DataGridColumnHeader title="Negara" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.country_name,
id: 'country_name',
header: ({ column }) => <DataGridColumnHeader title="Warganegara" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.exposed_person_flag,
id: 'exposed_person_flag',
header: ({ column }) => (
<DataGridColumnHeader title="Exposed Person Flag" column={column} />
),
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.income_tier.name,
id: 'incomde_tier.code',
header: ({ column }) => (
<DataGridColumnHeader title="Penghasilan Per Bulan (tiering)" column={column} />
),
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.cif_type,
id: 'cif_type',
header: ({ column }) => <DataGridColumnHeader title="Tipe CIF" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.status,
id: 'status',
header: ({ column }) => <DataGridColumnHeader title="Status Rekening" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.gender,
id: 'gender',
header: ({ column }) => <DataGridColumnHeader title="Jenis Kelamin" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => (row.birth_date ? moment(row.birth_date).format('YYYY-MM-DD') : ''),
id: 'birth_date',
header: ({ column }) => <DataGridColumnHeader title="Tanggal Lahir" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.class_economi.name,
id: 'class.economi.id',
header: ({ column }) => (
<DataGridColumnHeader title="Sub Klasifikasi Ekonomi" column={column} />
),
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.district.name,
id: 'class.district.id',
header: ({ column }) => <DataGridColumnHeader title="District" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.munisipiu.name,
id: 'class.munisipiu.id',
header: ({ column }) => <DataGridColumnHeader title="Munisipiu" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.administrativu.name,
id: 'class.administrativu.id',
header: ({ column }) => <DataGridColumnHeader title="Administrativu" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.suco.name,
id: 'class.suco.id',
header: ({ column }) => <DataGridColumnHeader title="Suco" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
},
{
accessorFn: (row) => row.aldeia.name,
id: 'class.aldeia.id',
header: ({ column }) => <DataGridColumnHeader title="Aldeia" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[350px]'
}
}
],
[]
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="container-fixed max-w-[99%] flex flex-col p-10 overflow-hidden [&>button]:hidden">
<DialogHeader className="p-0 border-0">
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
<div className="flex items-center justify-between flex-wrap grow gap-5 pb-7.5">
<div className="flex flex-col justify-center gap-2">
<h1 className="text-xl font-semibold leading-none text-gray-900">{title}</h1>
<div className="flex items-center gap-2 text-sm font-normal text-gray-700">
{desc}
</div>
</div>
<button className="btn btn-sm btn-light" onClick={onOpenChange}>
Close
</button>
</div>
</DialogHeader>
<DialogBody className="scrollable-y py-0 mb-5 ps-0 pe-3 -me-7" ref={parentRef}>
<div className="grid gap-5 lg:gap-7.5">
<DataGrid
columns={columns}
rowSelection={true}
pagination={{ size: 10 }}
sorting={[{ id: 'id', desc: false }]}
serverSide={true}
layout={{ card: true }}
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
fecthData(pageIndex, pageSize, sorting, columnFilters)
}
/>
</div>
</DialogBody>
</DialogContent>
</Dialog>
);
};
export { DetailDialog };

View File

@ -1,133 +0,0 @@
/* eslint-disable prettier/prettier */
import { DataGrid, DataGridColumnHeader } from '@/components';
import { fCurrency, fPercent } from '@/utils/FormatNumber';
import { ColumnDef } from '@tanstack/react-table';
import { ReactNode, useMemo, useState } from 'react';
import { format } from 'date-fns';
interface BgSummaryListInterface {
start_date: string;
end_date: string;
toolbar: ReactNode;
interval: string;
count: string;
openDetail: (title: string, props: {}) => void;
}
import { useFetchCreditSummaryData } from '../hooks';
import moment from 'moment';
import { snakeToCamelCase, snakeToTitleCase } from '@/utils';
const List = ({
start_date,
end_date,
interval,
count,
toolbar,
openDetail
}: BgSummaryListInterface) => {
const { data, isLoading, error } = useFetchCreditSummaryData(
start_date,
end_date,
interval,
count
);
const [columnVisibility, setColumnVisibility] = useState({
_filter: false
});
const columns = useMemo<ColumnDef<any>[]>(() => {
if (!data || data.length === 0) return [];
const dateKeys = Object.keys(data[0])
.filter((key) => key.match(/^\d{4}-\d{2}-\d{2}/))
.map((key) => key.split('_')[0])
.filter((value, index, self) => self.indexOf(value) === index);
const baseColumns = [
{
accessorFn: (row: any) => row['label'],
id: 'label',
header: ({ column }: any) => (
<DataGridColumnHeader
title="Pengajuan Kredit"
className="font-semibold text-gray-900"
column={column}
/>
),
enableSorting: false,
enableHiding: false,
cell: ({ row }: any) => {
const _className =
row.original.type === 'type'
? 'ms-0 font-semibold text-gray-700'
: row.original.type === 'type'
? 'ms-2 text-gray-900'
: 'ms-4 text-gray-700';
return <p className={_className}>{row.original['label']}</p>;
},
meta: {
headerClassName: 'w-auto'
}
}
];
// Create dynamic groups for multi-level headers
const dateColumns = dateKeys.map((date) => ({
id: date,
header: (() => {
if (interval === 'month') {
return format(new Date(date), 'MMM yyyy');
} else if (interval === 'week') {
const momentDate = moment(date);
const startOfMonth = momentDate.clone().startOf('month');
const weekNumber = momentDate.isoWeek() - startOfMonth.isoWeek() + 1;
return `Week ${weekNumber}, \n ${momentDate.format('MMM yyyy')}`;
} else if (interval === 'day') {
return format(new Date(date), 'MMM dd, yyyy');
}
})(),
columns: ['total'].map((metric) => ({
accessorFn: (row: any) => row[`${date}_${metric}`],
id: `${date}_${metric}`,
header: snakeToTitleCase(metric),
enableSorting: false,
enableHiding: false,
cell: ({ row }: { row: { original: Record<string, any> } }) => {
const __type = row.original.type;
const _className =
__type === 'type'
? 'ms-0 font-semibold text-gray-900'
: __type === 'type'
? 'ms-2 text-gray-900'
: 'ms-6 text-gray-700';
let value = row.original[`${date}_${metric}`] || '0';
if (count === 'sum') {
return <span className={_className}>{fCurrency(value)}</span>;
} else {
return <span className={_className}>{value}</span>;
}
},
meta: {
headerClassName: 'text-center text-gray-900',
cellClassName: 'text-end'
}
})),
meta: {
headerClassName: 'text-center'
}
}));
return [...baseColumns, ...dateColumns];
}, [data, openDetail, start_date, end_date]);
return (
<DataGrid
columns={columns}
data={data}
pagination={{ size: 100 }}
sorting={[{ id: 'label', desc: false }]}
layout={{ card: true }}
toolbar={toolbar}
/>
);
};
export { List };

View File

@ -1,15 +0,0 @@
import { type ReactNode } from 'react';
export interface DpkSummaryListToolBarInterface {
children?: ReactNode;
}
const ListToolBar = ({ children }: DpkSummaryListToolBarInterface) => {
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">{children}</div>
</div>
</div>
);
};
export { ListToolBar };

View File

@ -1,4 +0,0 @@
export * from './DateRangePicker';
export * from './List';
export * from './ListToolBar';
export * from './DetailDialog';

View File

@ -1,60 +0,0 @@
import React, { createContext } from 'react';
import { apiConfig } from '@/config/api.config';
import { useCallApi } from '@/hooks';
import { format } from 'date-fns';
import { doSaveLogActivity } from '@/actions/GlobalActions';
import { getAuth } from '@/auth';
interface ContextProps {
doExportData: (startDate: Date, endDate: Date, interval: string, count: string) => Promise<void>;
}
const initialProps: ContextProps = {
doExportData: async () => {}
};
const CreditSummaryContext = createContext<ContextProps>(initialProps);
const CreditSummaryContextProvider = ({ children }: { children: React.ReactNode }) => {
/* state */
const API_URL = apiConfig.service_credit;
const { GetExportData } = useCallApi();
/* action */
const doExportData = async (startDate: Date, endDate: Date, interval: string, count: string) => {
try {
let param = {
interval: interval,
aggregate: count,
start_date: format(startDate, 'yyyy-MM-dd'),
end_date: format(endDate, 'yyyy-MM-dd'),
token: await getAuth()?.access_token
};
let url = `${API_URL}/application/summary/export`;
GetExportData(url, param, 'bank_garansi_summary_export');
const user = localStorage.getItem('user');
const parsedUser = user ? JSON.parse(user) : null;
const createActivity = {
module: 'Summary Pengajuan Kredit',
description: `Export Summary Pengajuan Kredit => ${parsedUser ? parsedUser.name : 'Unknown User'}`,
action: 'E'
};
doSaveLogActivity(createActivity);
} catch (error) {
console.error('Export failed:', error);
throw error;
}
};
return (
<CreditSummaryContext.Provider value={{ doExportData }}>
{children}
</CreditSummaryContext.Provider>
);
};
export { CreditSummaryContextProvider, CreditSummaryContext };

View File

@ -1,3 +0,0 @@
export * from './CreditSummaryContext';
export * from './useCreditSummaryContext';
export * from './useFetchCreditSummaryData';

View File

@ -1,12 +0,0 @@
import { useContext } from 'react';
import { CreditSummaryContext } from './CreditSummaryContext';
const useCreditSummaryContext = () => {
const context = useContext(CreditSummaryContext);
if (!context) throw new Error('useCreditSummaryContext must be used within AuthProvider');
return context;
};
export { useCreditSummaryContext };

View File

@ -1,56 +0,0 @@
import { useState, useEffect } from 'react';
import axios from 'axios';
import { formatDate } from 'date-fns';
import { DateRange } from 'react-day-picker';
import { apiConfig } from '@/config/api.config';
import moment from 'moment';
const API_URL = apiConfig.service_credit;
interface useFetchCreditSummaryDataResult {
data: any;
isLoading: boolean;
error: any;
}
const useFetchCreditSummaryData = (
start_date: string,
end_date: string,
interval: string,
count: string
): useFetchCreditSummaryDataResult => {
const [data, setData] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<any>(null);
useEffect(() => {
const fetchData = async () => {
const filterInterval = interval;
try {
setIsLoading(true);
const response = await axios.get(`${API_URL}/application/summary`, {
params: {
interval: filterInterval,
start_date: start_date,
end_date: end_date,
aggregate: count
}
});
setData(response.data.data);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
};
if (start_date && end_date) {
fetchData();
}
}, [start_date, end_date, interval, count]);
return { data, isLoading, error };
};
export { useFetchCreditSummaryData };

View File

@ -1 +0,0 @@
export * from './CreditSummaryPage';

View File

@ -9,17 +9,9 @@ import { ErrorsRouting } from '@/errors';
import DashboardHomePage from '@/pages/dashboards/home/DashboardHomePage'; import DashboardHomePage from '@/pages/dashboards/home/DashboardHomePage';
import ConfigUserPage from '@/pages/settings/user/user/ConfigUserPage'; import ConfigUserPage from '@/pages/settings/user/user/ConfigUserPage';
import CreditChartPage from '@/pages/pengajuan_kredit/chart/CreditChartPage';
import CreditSummaryPage from '@/pages/pengajuan_kredit/summary/CreditSummaryPage';
import CreditListPage from '@/pages/pengajuan_kredit/list/CreditListPage';
import CreditDetailPage from '@/pages/pengajuan_kredit/list/CreditDetailPage';
import CreditAddPage from '@/pages/pengajuan_kredit/list/CreditAddPage';
import LogActivityPage from '@/pages/settings/user/log-activity/LogActivityPage'; import LogActivityPage from '@/pages/settings/user/log-activity/LogActivityPage';
import ManagePositionPage from '@/pages/settings/user/manage-position/ManagePositionPage'; import ManagePositionPage from '@/pages/settings/user/manage-position/ManagePositionPage';
import InstansiPage from '@/pages/instansi/InstansiPage';
const AppRoutingSetup = (): ReactElement => { const AppRoutingSetup = (): ReactElement => {
return ( return (
<Routes> <Routes>