Merge remote-tracking branch 'production/radja-dev'
This commit is contained in:
@ -50,10 +50,13 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.4",
|
||||
"date-fns": "^3.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"formik": "^2.4.6",
|
||||
"helmet": "^8.1.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"https": "^1.0.0",
|
||||
"jspdf": "^3.0.1",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.456.0",
|
||||
"metronic-tailwind-react": "file:",
|
||||
@ -86,10 +89,12 @@
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vite-plugin-windicss": "^1.9.3",
|
||||
"xlsx": "^0.18.5",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/leaflet": "^1.9.14",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/react": "^18.3.12",
|
||||
|
||||
BIN
public/media/avatars/KopDetailWalletStatement.png
Normal file
BIN
public/media/avatars/KopDetailWalletStatement.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 MiB |
@ -17,12 +17,16 @@ const loginSchema = Yup.object().shape({
|
||||
.min(3, 'Minimum 3 symbols')
|
||||
.max(50, 'Maximum 50 symbols')
|
||||
.required('Password is required'),
|
||||
token: Yup.string(),
|
||||
// .max(3, 'Maximum 6 code'),
|
||||
// .required('Token is required'),
|
||||
remember: Yup.boolean()
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
username: '',
|
||||
password: '',
|
||||
token: '',
|
||||
remember: false
|
||||
};
|
||||
|
||||
@ -43,7 +47,7 @@ const Login = () => {
|
||||
|
||||
try {
|
||||
if (!login) throw new Error('JWTProvider is required for this form.');
|
||||
await login(values.username, values.password);
|
||||
await login(values.username, values.password, values.token);
|
||||
|
||||
if (values.remember) {
|
||||
localStorage.setItem('username', values.username);
|
||||
@ -141,6 +145,33 @@ const Login = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="form-label text-gray-900 ps-2.5">Code Verify</label>
|
||||
<label className="input">
|
||||
<input
|
||||
placeholder="Enter code verify"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="off"
|
||||
maxLength={6}
|
||||
{...formik.getFieldProps('token')}
|
||||
className={clsx('form-control', {
|
||||
'is-invalid': formik.touched.token && formik.errors.token
|
||||
})}
|
||||
onKeyPress={(e) => {
|
||||
if (!/[0-9]/.test(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
{formik.touched.token && formik.errors.token && (
|
||||
<span role="alert" className="text-danger text-xs mt-1">
|
||||
{formik.errors.token}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<label className="checkbox-group">
|
||||
<input
|
||||
|
||||
@ -28,7 +28,7 @@ interface AuthContextProps {
|
||||
saveAuth: (auth: AuthModel | undefined) => void;
|
||||
currentUser: UserModel | undefined;
|
||||
setCurrentUser: Dispatch<SetStateAction<UserModel | undefined>>;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
login: (email: string, password: string, token?: string) => Promise<void>;
|
||||
requestPasswordResetLink: (email: string) => Promise<void>;
|
||||
changePassword: (token: string, password: string, password_confirmation: string) => Promise<void>;
|
||||
getUser: () => Promise<AxiosResponse<any> | {}>;
|
||||
@ -52,10 +52,10 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
const login = async (username: string, password: string, token?: string) => {
|
||||
try {
|
||||
const { data: auth } = await axios
|
||||
.post(LOGIN_URL, { username, password })
|
||||
.post(LOGIN_URL, { username, password, token })
|
||||
.then((response) => response.data);
|
||||
|
||||
const enhancedAuth: AuthModel = {
|
||||
@ -63,7 +63,7 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
||||
id: auth.user.id,
|
||||
role_name: auth.role_name,
|
||||
user: auth.user,
|
||||
statusbalance: auth.role?.status_balance ?? null // SAFE ACCESS
|
||||
statusbalance: auth.role?.status_balance ?? null // SAFE ACCESS
|
||||
};
|
||||
|
||||
saveAuth(enhancedAuth);
|
||||
|
||||
@ -68,6 +68,7 @@ const GenerateQr = () => {
|
||||
toast.error('Something went wrong, please try again');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setQrCodeUrl(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
import { CustomerProps } from '@/pages/master/provider/blocks/AddDialog';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
import { initialStateNotification, selectedNotification, validateFormNotification } from './Types';
|
||||
import { useDebounce } from './useDebounce';
|
||||
|
||||
const API_URL_CUSTOMER = apiConfig.service_customer;
|
||||
const API_URL_NOTIFICATION = apiConfig.service_notification;
|
||||
@ -39,6 +40,8 @@ const AddDialog = () => {
|
||||
const [alert, setAlert] = useState({ show: false, message: '' });
|
||||
const isSubmittingRef = useRef(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const debouncedSearchQuery = useDebounce(searchQuery, 500);
|
||||
|
||||
const [formField, setFormField] = useState<selectedNotification>(initialStateNotification);
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -95,7 +98,6 @@ const AddDialog = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error('Something went wrong');
|
||||
console.log(err);
|
||||
} finally {
|
||||
isSubmittingRef.current = false;
|
||||
setIsSubmitting(false);
|
||||
@ -104,22 +106,35 @@ const AddDialog = () => {
|
||||
[PostData, formField, handleAddDialog, reload]
|
||||
);
|
||||
|
||||
const getCustomerList = async (sorting: any) => {
|
||||
try {
|
||||
sorting = sorting.length == 0 ? [{ id: 'name', desc: false }] : sorting;
|
||||
const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, {
|
||||
limit: 100,
|
||||
page: 1,
|
||||
with_deleted: false,
|
||||
order_field: sorting[0].id,
|
||||
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC'
|
||||
});
|
||||
useEffect(() => {
|
||||
const fetchCustomers = async () => {
|
||||
try {
|
||||
const filters = [];
|
||||
|
||||
setCustomers(response?.data.list);
|
||||
} catch (error) {
|
||||
console.error('Error fetching customer', error);
|
||||
}
|
||||
};
|
||||
if (debouncedSearchQuery) {
|
||||
filters.push({
|
||||
id: 'fullname',
|
||||
value: debouncedSearchQuery
|
||||
});
|
||||
}
|
||||
|
||||
const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, {
|
||||
limit: 1000,
|
||||
page: 1,
|
||||
with_deleted: false,
|
||||
order_field: 'fullname',
|
||||
order_direction: 'ASC',
|
||||
filter: JSON.stringify(filters),
|
||||
specialFilter: filters.length > 0
|
||||
});
|
||||
setCustomers(response?.data.list);
|
||||
} catch (error) {
|
||||
console.error('Error fetching customers', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCustomers();
|
||||
}, [debouncedSearchQuery]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@ -127,10 +142,6 @@ const AddDialog = () => {
|
||||
doCreateNotification(e);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getCustomerList([{ id: 'id', desc: false }]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showAddDialog === false) {
|
||||
resetForm();
|
||||
@ -217,7 +228,11 @@ const AddDialog = () => {
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search Customer..." />
|
||||
<CommandInput
|
||||
placeholder="Search Customer..."
|
||||
value={searchQuery}
|
||||
onValueChange={setSearchQuery}
|
||||
/>
|
||||
<CommandList
|
||||
className="max-h-[300px] overflow-y-auto"
|
||||
style={{ touchAction: 'pan-y' }}
|
||||
@ -232,24 +247,20 @@ const AddDialog = () => {
|
||||
return (
|
||||
<CommandItem
|
||||
key={customer.id}
|
||||
value={customer.username}
|
||||
value={customer.fullname}
|
||||
onSelect={() => {
|
||||
const customerId = customer.id;
|
||||
setFormField((prev) => {
|
||||
const isSelected = prev.customers.includes(customerId);
|
||||
return {
|
||||
...prev,
|
||||
customers: isSelected
|
||||
? prev.customers.filter((id) => id !== customerId)
|
||||
: [...prev.customers, customerId]
|
||||
};
|
||||
});
|
||||
setFormField((prev) => ({
|
||||
...prev,
|
||||
customers: isSelected
|
||||
? prev.customers.filter((id) => id !== customer.id)
|
||||
: [...prev.customers, customer.id]
|
||||
}));
|
||||
}}
|
||||
className={isSelected ? 'bg-accent font-semibold' : ''}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{isSelected && <Check className="w-4 h-4 text-green-600" />}
|
||||
{customer.username}
|
||||
{customer.fullname} ({customer.msisdn})
|
||||
</span>
|
||||
</CommandItem>
|
||||
);
|
||||
|
||||
17
src/pages/notification/blocks/useDebounce.tsx
Normal file
17
src/pages/notification/blocks/useDebounce.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
@ -158,7 +158,7 @@ const ManageNotifContextProvider = ({ children }: { children: React.ReactNode })
|
||||
const getNotificationList = async (page: number, limit: number, sorting: any, filter: any) => {
|
||||
try {
|
||||
const sortField = sorting.length > 0 ? sorting[0].id : 'created_at';
|
||||
const sortDirection = sorting.length > 0 ? (sorting[0].desc ? 'DESC' : 'ASC') : 'DESC';
|
||||
const sortDirection = sorting.length > 0 ? (sorting[0].desc ? 'ASC' : 'DESC') : 'ASC';
|
||||
|
||||
// Initialize filter object
|
||||
let filterParams: any = {};
|
||||
@ -219,7 +219,7 @@ const ManageNotifContextProvider = ({ children }: { children: React.ReactNode })
|
||||
pagination={{ size: 10 }}
|
||||
toolbar={<ListToolBar />}
|
||||
layout={{ card: true }}
|
||||
sorting={[{ id: 'created_at', desc: true }]}
|
||||
sorting={[{ id: 'created_at', desc: false }]}
|
||||
serverSide={true}
|
||||
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
|
||||
getNotificationList(pageIndex, pageSize, sorting, columnFilters)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import React, { useCallback, useState, useEffect, useContext } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
@ -11,6 +11,7 @@ import { DefaultTooltip, KeenIcon } from '@/components';
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
import { useCallApi } from '@/hooks';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { showDetailWalletContext } from './ShowDetailDialog';
|
||||
|
||||
interface TransferType {
|
||||
id: string;
|
||||
@ -40,24 +41,44 @@ const FilterSection: React.FC<FilterSectionProps> = ({
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [category, setCategory] = useState<string[]>([]);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const { doExportExcel, doExportPDF } = useContext(showDetailWalletContext);
|
||||
|
||||
const handleExportExcel = async () => {
|
||||
try {
|
||||
await doExportExcel();
|
||||
// console.log('Excel export initiated successfully');
|
||||
} catch (error) {
|
||||
console.error('Error during Excel export:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportPDF = async () => {
|
||||
try {
|
||||
await doExportPDF();
|
||||
// console.log('PDF export initiated successfully');
|
||||
} catch (error) {
|
||||
console.error('Error during PDF export:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTransferType = useCallback(async () => {
|
||||
try {
|
||||
const response = await GetData(`${apiConfig.service_transaction}/transactiontype/list`, {
|
||||
limit: 100,
|
||||
page: 1,
|
||||
with_deleted: false,
|
||||
order_field: 'created_at',
|
||||
order_direction: 'ASC'
|
||||
});
|
||||
try {
|
||||
const response = await GetData(`${apiConfig.service_transaction}/transactiontype/list`, {
|
||||
limit: 100,
|
||||
page: 1,
|
||||
with_deleted: false,
|
||||
order_field: 'created_at',
|
||||
order_direction: 'ASC'
|
||||
});
|
||||
|
||||
const sortedData = (response?.data?.list || [])
|
||||
.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
setTransferType(sortedData);
|
||||
} catch (error) {
|
||||
console.error('Error fetching transfer types', error);
|
||||
}
|
||||
}, [GetData]);
|
||||
const sortedData = (response?.data?.list || []).sort((a: any, b: any) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
setTransferType(sortedData);
|
||||
} catch (error) {
|
||||
console.error('Error fetching transfer types', error);
|
||||
}
|
||||
}, [GetData]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTransferType();
|
||||
@ -70,8 +91,8 @@ const FilterSection: React.FC<FilterSectionProps> = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCategory(categories);
|
||||
}, [transaction, categoryMap]);
|
||||
setCategory(categories);
|
||||
}, [transaction, categoryMap]);
|
||||
|
||||
const reverseCategoryMap = Object.fromEntries(
|
||||
Object.entries(categoryMap).map(([key, value]) => [value, key])
|
||||
@ -108,7 +129,7 @@ const FilterSection: React.FC<FilterSectionProps> = ({
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedTransferType) {
|
||||
if (selectedTransferType) {
|
||||
appliedFilters.id_transaction_type = selectedTransferType;
|
||||
}
|
||||
|
||||
@ -200,17 +221,19 @@ const FilterSection: React.FC<FilterSectionProps> = ({
|
||||
Filter Data
|
||||
</Button>
|
||||
|
||||
<DefaultTooltip title={'Export Data'} placement={'top'}>
|
||||
<Button variant={'outline'} className="h-7.5 min-w-[58px]">
|
||||
<img src={toAbsoluteUrl('/media/file-types/xls.svg')} className="" alt="" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
<div className="flex gap-2">
|
||||
<DefaultTooltip title={'Export to Excel'} placement={'top'}>
|
||||
<Button variant={'outline'} className="h-7.5 min-w-[58px]" onClick={handleExportExcel}>
|
||||
<img src={toAbsoluteUrl('/media/file-types/xls.svg')} className="" alt="Excel" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
|
||||
<DefaultTooltip title={'Export Data'} placement={'top'}>
|
||||
<Button variant={'outline'} className="h-7.5 min-w-[58px]">
|
||||
<img src={toAbsoluteUrl('/media/file-types/pdf.svg')} className="" alt="" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
<DefaultTooltip title={'Export to PDF'} placement={'top'}>
|
||||
<Button variant={'outline'} className="h-7.5 min-w-[58px]" onClick={handleExportPDF}>
|
||||
<img src={toAbsoluteUrl('/media/file-types/pdf.svg')} className="" alt="PDF" />
|
||||
</Button>
|
||||
</DefaultTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -11,16 +11,29 @@ import { useCallApi } from '@/hooks';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { DataGridColumnHeader, DataGridProvider } from '@/components';
|
||||
import { useManageStatementContext } from '../hooks/useManageWalletStatementContext';
|
||||
import { Toaster } from 'sonner';
|
||||
import { toast, Toaster } from 'sonner';
|
||||
import { format } from 'date-fns';
|
||||
import FilterSection from './FilterSection';
|
||||
import * as XLSX from 'xlsx';
|
||||
import jsPDF from 'jspdf';
|
||||
import 'jspdf-autotable';
|
||||
|
||||
interface AutoTableData {
|
||||
pageNumber: number;
|
||||
table: any;
|
||||
doc: jsPDF;
|
||||
}
|
||||
|
||||
interface ContextProps {
|
||||
doExportData: (sorting: any, filter: any) => Promise<any>;
|
||||
doExportExcel: () => Promise<void>;
|
||||
doExportPDF: () => Promise<void>;
|
||||
}
|
||||
|
||||
const initialProps: ContextProps = {
|
||||
doExportData: async () => ({ data: [], totalCount: 0 })
|
||||
doExportData: async () => ({ data: [], totalCount: 0 }),
|
||||
doExportExcel: async () => {},
|
||||
doExportPDF: async () => {}
|
||||
};
|
||||
|
||||
export const showDetailWalletContext = createContext<ContextProps>(initialProps);
|
||||
@ -174,6 +187,336 @@ const ShowDetailWalletDialog = () => {
|
||||
[GetData, filters, selectedWallet]
|
||||
);
|
||||
|
||||
const getAllTransactionData = async () => {
|
||||
try {
|
||||
const response = await GetData(
|
||||
`${apiConfig.service_wallet}/dashboard/balance/list-balance-detail/${selectedWallet?.ID}`,
|
||||
{
|
||||
limit: 10000,
|
||||
page: 1,
|
||||
with_deleted: false,
|
||||
order_field: 'amount',
|
||||
order_direction: 'DESC',
|
||||
filter: JSON.stringify(filters)
|
||||
}
|
||||
);
|
||||
|
||||
return response?.data?.list || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching all transaction data', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const formatTransactionData = (data: any[]) => {
|
||||
return data.map((item) => ({
|
||||
'Transaction Code': item.transaction_code || '-',
|
||||
'MSISDN Reffer': item.msisdn_reff || '-',
|
||||
'Transaction Type': item.transaction_type?.name || '-',
|
||||
Type: item.type || '-',
|
||||
'Pre Amount': item.pre_amount?.toFixed(2) || '0.00',
|
||||
Amount: item.amount?.toFixed(2) || '0.00',
|
||||
'Post Amount': item.post_amount?.toFixed(2) || '0.00',
|
||||
Category: categoryMap[item.category] || '-',
|
||||
Date: item.date
|
||||
? new Date(item.date).toLocaleString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
: '-',
|
||||
Purpose: item.purpose || '-',
|
||||
Notes: item.notes || '-'
|
||||
}));
|
||||
};
|
||||
|
||||
const doExportExcel = async () => {
|
||||
try {
|
||||
const allData = await getAllTransactionData();
|
||||
const formattedData = formatTransactionData(allData);
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet([]);
|
||||
|
||||
const companyHeader = [
|
||||
['T•PAY'],
|
||||
['Telin Digital Solution'],
|
||||
['Av. Pres. Nicolau Lobato, Dili'],
|
||||
['Tel: (+670) 74147147 / 147'],
|
||||
['www.t-pay.tl'],
|
||||
[''],
|
||||
['Wallet Statement Details'],
|
||||
[`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`],
|
||||
[`Wallet ID: ${selectedWallet?.ID || '-'}`],
|
||||
['']
|
||||
];
|
||||
|
||||
const columnHeaders = [
|
||||
[
|
||||
'Transaction Code',
|
||||
'MSISDN Reffer',
|
||||
'Transaction Type',
|
||||
'Type',
|
||||
'Pre Amount',
|
||||
'Amount',
|
||||
'Post Amount',
|
||||
'Category',
|
||||
'Date',
|
||||
'Purpose',
|
||||
'Notes'
|
||||
]
|
||||
];
|
||||
|
||||
const excelData = [
|
||||
...companyHeader,
|
||||
...columnHeaders,
|
||||
...formattedData.map((item) => Object.values(item))
|
||||
];
|
||||
|
||||
XLSX.utils.sheet_add_aoa(ws, excelData);
|
||||
|
||||
const colWidths = [
|
||||
{ wch: 20 },
|
||||
{ wch: 15 },
|
||||
{ wch: 25 },
|
||||
{ wch: 10 },
|
||||
{ wch: 12 },
|
||||
{ wch: 12 },
|
||||
{ wch: 12 },
|
||||
{ wch: 15 },
|
||||
{ wch: 20 },
|
||||
{ wch: 25 },
|
||||
{ wch: 25 }
|
||||
];
|
||||
ws['!cols'] = colWidths;
|
||||
|
||||
const tpayCell = XLSX.utils.encode_cell({ r: 0, c: 0 });
|
||||
if (ws[tpayCell]) {
|
||||
ws[tpayCell].s = {
|
||||
font: { bold: true, sz: 18, color: { rgb: '000000' } },
|
||||
alignment: { horizontal: 'center' },
|
||||
fill: { fgColor: { rgb: 'F4C2C2' } }
|
||||
};
|
||||
}
|
||||
|
||||
for (let i = 1; i < 5; i++) {
|
||||
const cellRef = XLSX.utils.encode_cell({ r: i, c: 0 });
|
||||
if (ws[cellRef]) {
|
||||
ws[cellRef].s = {
|
||||
font: { sz: 12, color: { rgb: '000000' } },
|
||||
alignment: { horizontal: 'center' },
|
||||
fill: { fgColor: { rgb: 'F4C2C2' } }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const titleCell = XLSX.utils.encode_cell({ r: 6, c: 0 });
|
||||
if (ws[titleCell]) {
|
||||
ws[titleCell].s = {
|
||||
font: { bold: true, sz: 14 },
|
||||
alignment: { horizontal: 'center' }
|
||||
};
|
||||
}
|
||||
|
||||
for (let i = 7; i < 9; i++) {
|
||||
const cellRef = XLSX.utils.encode_cell({ r: i, c: 0 });
|
||||
if (ws[cellRef]) {
|
||||
ws[cellRef].s = {
|
||||
font: { sz: 11 },
|
||||
alignment: { horizontal: 'left' }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const range = XLSX.utils.encode_range({ s: { r: i, c: 0 }, e: { r: i, c: 10 } });
|
||||
if (!ws['!merges']) ws['!merges'] = [];
|
||||
ws['!merges'].push(XLSX.utils.decode_range(range));
|
||||
}
|
||||
|
||||
for (let col = 0; col < columnHeaders[0].length; col++) {
|
||||
const cellRef = XLSX.utils.encode_cell({ r: 10, c: col });
|
||||
if (ws[cellRef]) {
|
||||
ws[cellRef].s = {
|
||||
font: { bold: true, color: { rgb: 'FFFFFF' } },
|
||||
fill: { fgColor: { rgb: '4472C4' } },
|
||||
alignment: { horizontal: 'center' },
|
||||
border: {
|
||||
top: { style: 'thin', color: { rgb: '000000' } },
|
||||
bottom: { style: 'thin', color: { rgb: '000000' } },
|
||||
left: { style: 'thin', color: { rgb: '000000' } },
|
||||
right: { style: 'thin', color: { rgb: '000000' } }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (let row = 11; row < 11 + formattedData.length; row++) {
|
||||
for (let col = 0; col < columnHeaders[0].length; col++) {
|
||||
const cellRef = XLSX.utils.encode_cell({ r: row, c: col });
|
||||
if (ws[cellRef]) {
|
||||
ws[cellRef].s = {
|
||||
border: {
|
||||
top: { style: 'thin', color: { rgb: 'D9D9D9' } },
|
||||
bottom: { style: 'thin', color: { rgb: 'D9D9D9' } },
|
||||
left: { style: 'thin', color: { rgb: 'D9D9D9' } },
|
||||
right: { style: 'thin', color: { rgb: 'D9D9D9' } }
|
||||
}
|
||||
};
|
||||
|
||||
if ([4, 5, 6].includes(col)) {
|
||||
ws[cellRef].s.alignment = { horizontal: 'right' };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Wallet Statement');
|
||||
|
||||
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`;
|
||||
XLSX.writeFile(wb, fileName);
|
||||
|
||||
// console.log('Excel export completed successfully');
|
||||
} catch (error) {
|
||||
console.error('Error exporting to Excel:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Perbaikan untuk fungsi doExportPDF
|
||||
const doExportPDF = async () => {
|
||||
try {
|
||||
// Import jsPDF dengan cara yang benar
|
||||
const jsPDF = (await import('jspdf')).default;
|
||||
|
||||
// Import autoTable plugin
|
||||
const autoTable = (await import('jspdf-autotable')).default;
|
||||
|
||||
const allData = await getAllTransactionData();
|
||||
const formattedData = formatTransactionData(allData);
|
||||
|
||||
// Buat instance jsPDF
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'landscape', // Ubah ke landscape untuk tabel yang lebar
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
});
|
||||
|
||||
// Header perusahaan
|
||||
pdf.setFontSize(18);
|
||||
pdf.setFont('helvetica', 'bold');
|
||||
pdf.text('T•PAY', pdf.internal.pageSize.getWidth() / 2, 20, { align: 'center' });
|
||||
|
||||
pdf.setFontSize(12);
|
||||
pdf.setFont('helvetica', 'normal');
|
||||
pdf.text('Telin Digital Solution', pdf.internal.pageSize.getWidth() / 2, 28, { align: 'center' });
|
||||
pdf.text('Av. Pres. Nicolau Lobato, Dili', pdf.internal.pageSize.getWidth() / 2, 34, { align: 'center' });
|
||||
pdf.text('Tel: (+670) 74147147 / 147', pdf.internal.pageSize.getWidth() / 2, 40, { align: 'center' });
|
||||
pdf.text('www.t-pay.tl', pdf.internal.pageSize.getWidth() / 2, 46, { align: 'center' });
|
||||
|
||||
// Garis pemisah
|
||||
pdf.setDrawColor(0, 0, 0);
|
||||
pdf.setLineWidth(0.5);
|
||||
pdf.line(20, 52, pdf.internal.pageSize.getWidth() - 20, 52);
|
||||
|
||||
// Judul laporan
|
||||
pdf.setFontSize(16);
|
||||
pdf.setFont('helvetica', 'bold');
|
||||
pdf.text('Wallet Statement Details', pdf.internal.pageSize.getWidth() / 2, 62, { align: 'center' });
|
||||
|
||||
// Informasi tambahan
|
||||
pdf.setFontSize(10);
|
||||
pdf.setFont('helvetica', 'normal');
|
||||
pdf.text(`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`, 20, 72);
|
||||
pdf.text(`Wallet ID: ${selectedWallet?.ID || '-'}`, 20, 78);
|
||||
|
||||
// Siapkan data tabel
|
||||
const tableHeaders = [
|
||||
'Transaction Code',
|
||||
'MSISDN Reffer',
|
||||
'Transaction Type',
|
||||
'Type',
|
||||
'Pre Amount',
|
||||
'Amount',
|
||||
'Post Amount',
|
||||
'Category',
|
||||
'Date',
|
||||
'Purpose',
|
||||
'Notes'
|
||||
];
|
||||
|
||||
const tableData = formattedData.map((item) => [
|
||||
item['Transaction Code'],
|
||||
item['MSISDN Reffer'],
|
||||
item['Transaction Type'],
|
||||
item['Type'],
|
||||
item['Pre Amount'],
|
||||
item['Amount'],
|
||||
item['Post Amount'],
|
||||
item['Category'],
|
||||
item['Date'],
|
||||
item['Purpose'],
|
||||
item['Notes']
|
||||
]);
|
||||
|
||||
// Gunakan autoTable dengan cara yang benar
|
||||
autoTable(pdf, {
|
||||
head: [tableHeaders],
|
||||
body: tableData,
|
||||
startY: 85,
|
||||
styles: {
|
||||
fontSize: 7,
|
||||
cellPadding: 2,
|
||||
overflow: 'linebreak',
|
||||
halign: 'center'
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [68, 114, 196],
|
||||
textColor: [255, 255, 255],
|
||||
fontStyle: 'bold',
|
||||
halign: 'center'
|
||||
},
|
||||
columnStyles: {
|
||||
0: { cellWidth: 25, halign: 'left' }, // Transaction Code
|
||||
1: { cellWidth: 20, halign: 'left' }, // MSISDN Reffer
|
||||
2: { cellWidth: 25, halign: 'left' }, // Transaction Type
|
||||
3: { cellWidth: 15, halign: 'center' }, // Type
|
||||
4: { cellWidth: 18, halign: 'right' }, // Pre Amount
|
||||
5: { cellWidth: 18, halign: 'right' }, // Amount
|
||||
6: { cellWidth: 18, halign: 'right' }, // Post Amount
|
||||
7: { cellWidth: 20, halign: 'left' }, // Category
|
||||
8: { cellWidth: 30, halign: 'left' }, // Date
|
||||
9: { cellWidth: 25, halign: 'left' }, // Purpose
|
||||
10: { cellWidth: 25, halign: 'left' } // Notes
|
||||
},
|
||||
margin: { top: 85, left: 10, right: 10 },
|
||||
tableWidth: 'auto',
|
||||
// Callback untuk styling baris
|
||||
didDrawCell: (data: any) => {
|
||||
// Tambahkan border pada setiap cell
|
||||
if (data.section === 'body') {
|
||||
pdf.setDrawColor(200, 200, 200);
|
||||
pdf.setLineWidth(0.1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Generate nama file
|
||||
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.pdf`;
|
||||
|
||||
// Save file
|
||||
pdf.save(fileName);
|
||||
|
||||
toast.success('PDF exported successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error exporting to PDF:', error);
|
||||
toast.error('Failed to export PDF');
|
||||
}
|
||||
};
|
||||
|
||||
const doExportData = async (sorting: any, filter: any) => {
|
||||
const user = localStorage.getItem(
|
||||
`${import.meta.env.VITE_APP_NAME}-auth-v${import.meta.env.VITE_APP_VERSION}`
|
||||
@ -219,7 +562,7 @@ const ShowDetailWalletDialog = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<showDetailWalletContext.Provider value={{ doExportData }}>
|
||||
<showDetailWalletContext.Provider value={{ doExportData, doExportExcel, doExportPDF }}>
|
||||
<Toaster expand visibleToasts={9} duration={3000} />
|
||||
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
|
||||
<DialogContent className="w-screen max-w-screen max-h-screen p-4 overflow-hidden">
|
||||
|
||||
Reference in New Issue
Block a user