[skip ci] add manage card on customer detail
This commit is contained in:
@ -10,7 +10,8 @@ interface apiConfigProps {
|
|||||||
service_disbursement: string;
|
service_disbursement: string;
|
||||||
service_notification: string;
|
service_notification: string;
|
||||||
service_feedback: string;
|
service_feedback: string;
|
||||||
api_dashboard:string
|
api_dashboard: string;
|
||||||
|
service_card: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_APP_API_URL;
|
const API_URL = import.meta.env.VITE_APP_API_URL;
|
||||||
@ -27,6 +28,7 @@ const apiConfig: apiConfigProps = {
|
|||||||
service_disbursement: `${API_URL}/s`,
|
service_disbursement: `${API_URL}/s`,
|
||||||
service_notification: `${API_URL}/n`,
|
service_notification: `${API_URL}/n`,
|
||||||
api_dashboard: `${API_URL}/r`,
|
api_dashboard: `${API_URL}/r`,
|
||||||
|
service_card: `${API_URL}/a`,
|
||||||
nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/`
|
nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { initialMember } from "../Columns";
|
import { initialMember } from "../Columns";
|
||||||
import AdmAccess from './AdmAccess';
|
import AdmAccess from './AdmAccess';
|
||||||
import CustomerWallet from './CustomerWallet';
|
import CustomerWallet from './CustomerWallet';
|
||||||
|
import ManageCard from './ManageCard';
|
||||||
const BASE_URL_MASTER_DATA = apiConfig.service_master_data;
|
const BASE_URL_MASTER_DATA = apiConfig.service_master_data;
|
||||||
const URL_NATIONALITY = apiConfig.nationality;
|
const URL_NATIONALITY = apiConfig.nationality;
|
||||||
const BASE_URL_CUSTOMER = apiConfig.service_customer;
|
const BASE_URL_CUSTOMER = apiConfig.service_customer;
|
||||||
@ -203,7 +204,7 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa
|
|||||||
handleReject(formData);
|
handleReject(formData);
|
||||||
// handleClose();
|
// handleClose();
|
||||||
}
|
}
|
||||||
|
console.log(formData.id);
|
||||||
return (
|
return (
|
||||||
<Dialog open={showAddDialog} onOpenChange={(open) => handleAddDialog(open)}>
|
<Dialog open={showAddDialog} onOpenChange={(open) => handleAddDialog(open)}>
|
||||||
<DialogContent className="container-fixed max-w-[85%] flex flex-col p-2">
|
<DialogContent className="container-fixed max-w-[85%] flex flex-col p-2">
|
||||||
@ -284,6 +285,9 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa
|
|||||||
{(formData.id && page!=='kyc') ? (
|
{(formData.id && page!=='kyc') ? (
|
||||||
<CustomerWallet customerid={formData.id}/>
|
<CustomerWallet customerid={formData.id}/>
|
||||||
) : ""}
|
) : ""}
|
||||||
|
{(formData.id && page!=='kyc') ? (
|
||||||
|
<ManageCard customerId={formData.id}/>
|
||||||
|
) : ""}
|
||||||
|
|
||||||
<div className="col-span-1 md:col-span-2 flex justify-end gap-5">
|
<div className="col-span-1 md:col-span-2 flex justify-end gap-5">
|
||||||
<Button type="button" disabled={isAdmin} onClick={(e:any) => btnPrevDef(e)} color="secondary">{ viewOnly ? (`Open Edit`) : (`Close Edit`) }</Button>
|
<Button type="button" disabled={isAdmin} onClick={(e:any) => btnPrevDef(e)} color="secondary">{ viewOnly ? (`Open Edit`) : (`Close Edit`) }</Button>
|
||||||
|
|||||||
272
src/pages/members/manage-members/blocks/ManageCard.tsx
Normal file
272
src/pages/members/manage-members/blocks/ManageCard.tsx
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
import { getAuth } from '@/auth';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogBody,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { apiConfig } from '@/config/api.config';
|
||||||
|
import { useCallApi } from '@/hooks';
|
||||||
|
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
id: string;
|
||||||
|
serial_number: string;
|
||||||
|
provisioning_date: string;
|
||||||
|
created_at: string;
|
||||||
|
created_by: string;
|
||||||
|
customer: {
|
||||||
|
msisdn: string;
|
||||||
|
fullname: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManageCardProps = {
|
||||||
|
customerId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const API_URL = apiConfig.service_card;
|
||||||
|
|
||||||
|
const ManageCard = ({ customerId }: ManageCardProps) => {
|
||||||
|
const { GetData, PostData } = useCallApi();
|
||||||
|
const parsedUser = getAuth()?.user;
|
||||||
|
const [cardList, setCardList] = useState<CardProps[]>([]);
|
||||||
|
const [selectedCardId, setSelectedCardId] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||||
|
|
||||||
|
const formatDateToTimorLeste = (isoDateString: string): string => {
|
||||||
|
const dateUTC = new Date(isoDateString);
|
||||||
|
|
||||||
|
// Tambahkan offset UTC+9 dalam milidetik
|
||||||
|
const offsetInMs = 9 * 60 * 60 * 1000;
|
||||||
|
const dateTL = new Date(dateUTC.getTime() + offsetInMs);
|
||||||
|
|
||||||
|
const day = dateTL.getDate().toString().padStart(2, '0');
|
||||||
|
const monthNames = [
|
||||||
|
'Januari',
|
||||||
|
'Februari',
|
||||||
|
'Maret',
|
||||||
|
'April',
|
||||||
|
'Mei',
|
||||||
|
'Juni',
|
||||||
|
'Juli',
|
||||||
|
'Agustus',
|
||||||
|
'September',
|
||||||
|
'Oktober',
|
||||||
|
'November',
|
||||||
|
'Desember'
|
||||||
|
];
|
||||||
|
const month = monthNames[dateTL.getMonth()];
|
||||||
|
const year = dateTL.getFullYear();
|
||||||
|
|
||||||
|
const hours = dateTL.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = dateTL.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
return `${day} ${month} ${year}, ${hours}:${minutes}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchCardList = async (page: number, limit: number, sorting: any, filter: any) => {
|
||||||
|
try {
|
||||||
|
sorting = sorting.length == 0 ? [{ id: 'provisioning_date', desc: true }] : sorting;
|
||||||
|
filter =
|
||||||
|
filter.length === 0 ? { 'customer.id': customerId } : { 'customer.id': filter[0].value };
|
||||||
|
// filter = filter.length == 0 ? {} : { 'customer.id': { like: `%${customerId}%` } };
|
||||||
|
|
||||||
|
const response = await GetData(`${API_URL}/card/list`, {
|
||||||
|
limit,
|
||||||
|
page: page + 1,
|
||||||
|
with_deleted: false,
|
||||||
|
order_field: sorting[0].id,
|
||||||
|
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC',
|
||||||
|
filter: JSON.stringify(filter)
|
||||||
|
});
|
||||||
|
|
||||||
|
setCardList(response?.data.list);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Error fetching Card List');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doRevokeCard = async (cardId: string) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await PostData(`${API_URL}/card/revoke`, {
|
||||||
|
id: cardId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.status) {
|
||||||
|
toast.success('Success revoking card');
|
||||||
|
fetchCardList(0, 10, [], []);
|
||||||
|
} else {
|
||||||
|
toast.error('Error revoking card');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Error revoking card');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevokeButton = (cardId: string) => {
|
||||||
|
setSelectedCardId(cardId);
|
||||||
|
setShowConfirmation(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelSubmit = () => {
|
||||||
|
setShowConfirmation(false);
|
||||||
|
setSelectedCardId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmRevoke = async () => {
|
||||||
|
if (!selectedCardId) return;
|
||||||
|
await doRevokeCard(selectedCardId);
|
||||||
|
setShowConfirmation(false);
|
||||||
|
setSelectedCardId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (customerId) {
|
||||||
|
fetchCardList(0, 10, [], []);
|
||||||
|
}
|
||||||
|
}, [customerId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-white p-6 rounded-md shadow-md space-y-4">
|
||||||
|
<h2 className="text-lg font-semibold">Card Member</h2>
|
||||||
|
<Card className="p-4">
|
||||||
|
{cardList ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full text-sm text-left">
|
||||||
|
<thead className="text-xs text-gray-500 border-b">
|
||||||
|
<tr>
|
||||||
|
<th className="p-2">No</th>
|
||||||
|
<th className="p-2">Name</th>
|
||||||
|
<th className="p-2">Msisdn</th>
|
||||||
|
<th className="p-2">Serial Number</th>
|
||||||
|
<th className="p-2">Provisioning Date</th>
|
||||||
|
<th className="p-2">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{cardList.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{cardList.map((item: CardProps, index) => (
|
||||||
|
<tr
|
||||||
|
// key={item.id}
|
||||||
|
key={item.id ?? `wallet-${index}`}
|
||||||
|
className={`${index % 2 === 0 ? 'bg-gray-50' : 'bg-white'} hover:bg-gray-100`}
|
||||||
|
>
|
||||||
|
<td className="p-2">{index + 1}</td>
|
||||||
|
<td className="p-2">{item.customer.fullname}</td>
|
||||||
|
<td className="p-2">{item.customer.msisdn}</td>
|
||||||
|
<td className="p-2">{item.serial_number}</td>
|
||||||
|
<td className="p-2">{formatDateToTimorLeste(item.provisioning_date)}</td>
|
||||||
|
<td className="p-2">
|
||||||
|
<Button
|
||||||
|
className="px-4 py-2 text-sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => handleRevokeButton(item.id)}
|
||||||
|
>
|
||||||
|
Revoke
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="pt-3 text-center text-slate-400">
|
||||||
|
Card is empty
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">No Card Available</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<Dialog open={showConfirmation} onOpenChange={(open) => setShowConfirmation(open)}>
|
||||||
|
<DialogContent className="container-fixed max-w-[600px] flex flex-col p-5 overflow-hidden">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-orange-500" />
|
||||||
|
Revoke Card
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription></DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogBody>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center p-8">
|
||||||
|
<div className="animate-pulse flex space-x-4 w-full">
|
||||||
|
<div className="flex-1 space-y-4 py-1">
|
||||||
|
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-4 bg-gray-200 rounded"></div>
|
||||||
|
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-gray-500">Loading Card Details...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="card-body grid gap-5">
|
||||||
|
<h1 className="font-bold text-gray-800">
|
||||||
|
Are you sure you want to revoke this card? This action cannot be undone.
|
||||||
|
</h1>
|
||||||
|
<p className="text-red-600 text-sm mt-1">
|
||||||
|
Revoking this card will deactivate it permanently. The customer will no longer
|
||||||
|
be able to use this card for any transactions or services.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex justify-end gap-3 pt-2.5">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={handleCancelSubmit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
onClick={handleConfirmRevoke}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="animate-spin h-4 w-4 mr-2" />
|
||||||
|
Revoking...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Revoke Card'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogBody>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManageCard;
|
||||||
Reference in New Issue
Block a user