This commit is contained in:
Vassshhh
2025-08-25 03:47:25 +07:00
parent ec2eb0d619
commit aa75247bd0
2 changed files with 286 additions and 228 deletions

View File

@@ -18,6 +18,13 @@ function parseJwt(token) {
} }
} }
function formatTimeLeft(ms) {
const totalSeconds = Math.max(Math.floor(ms / 1000), 0);
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
const seconds = String(totalSeconds % 60).padStart(2, '0');
return `${minutes}:${seconds}`;
}
const Checkout = ({ socketId, transactionSuccess }) => { const Checkout = ({ socketId, transactionSuccess }) => {
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
const [itemIds, setItemIds] = useState(null); const [itemIds, setItemIds] = useState(null);
@@ -30,38 +37,89 @@ const Checkout = ({ socketId, transactionSuccess }) => {
const [redirect_uri, setRedirect_Uri] = useState(''); const [redirect_uri, setRedirect_Uri] = useState('');
const [redirect_failed, setRedirect_Failed] = useState(''); const [redirect_failed, setRedirect_Failed] = useState('');
const [paymentMethod, setPaymentMethod] = useState('QRIS'); const [paymentMethod, setPaymentMethod] = useState('QRIS');
const [payTimeout, setPayTimeout] = useState(null);
const [timeLeft, setTimeLeft] = useState(null);
const [activeAccordion, setActiveAccordion] = useState('QRIS');
let grandTotal = 0;
let tax = 0;
useEffect(() => { useEffect(() => {
const urlParams = new URLSearchParams(window.location.search); if (!socketId) return;
const tokenParam = urlParams.get('token');
const itemsIdString = urlParams.get('itemsId'); let urlParams = new URLSearchParams(window.location.search);
let tokenParam = urlParams.get('token');
let itemsIdString = urlParams.get('itemsId');
const handlePay = async () => {
setLoadingPay(true);
try {
const newName = urlParams.get('new_name');
const setName = urlParams.get('set_name');
const params = new URLSearchParams();
itemsIdString.forEach((id) => params.append('itemsId', id));
params.append('socketId', socketId);
params.append('paymentMethod', paymentMethod);
if (newName) params.append('newName', newName);
if (setName) params.append('setName', setName);
const response = await fetch('https://bot.kediritechnopark.com/webhook/store-production/pay', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${tokenParam}`,
},
body: params.toString(),
});
const result = await response.json();
if (response.ok) {
setValue(result.total_price);
if (result.pay_timeout && result.time_now) {
const timeout = new Date(result.pay_timeout).getTime();
const now = new Date(result.time_now).getTime();
setPayTimeout(timeout);
setTimeLeft(timeout - now);
}
setQrisData(result.qris_dynamic || null);
setTransferData(result);
grandTotal = result.total_price;
tax = 0;
} else {
alert(`Request gagal: ${result?.error || 'Unknown error'}`);
}
} catch (error) {
console.error('Network error:', error);
alert('Terjadi kesalahan jaringan.');
} finally {
setLoadingPay(false);
}
};
itemsIdString = JSON.parse(urlParams.get('itemsId'));
setRedirect_Uri(urlParams.get('redirect_uri') || ''); setRedirect_Uri(urlParams.get('redirect_uri') || '');
setRedirect_Failed(urlParams.get('redirect_failed') || ''); setRedirect_Failed(urlParams.get('redirect_failed') || '');
setToken(tokenParam); setToken(tokenParam);
if (!itemsIdString) { if (!itemsIdString || !Array.isArray(itemsIdString) || itemsIdString.length === 0) {
window.location.href = urlParams.get('redirect_failed') || '/'; window.location.href = urlParams.get('redirect_failed') || '/';
return; return;
} }
try { setItemIds(itemsIdString);
const parsedIds = JSON.parse(itemsIdString); handlePay();
if (!Array.isArray(parsedIds) || parsedIds.length === 0) { }, [socketId]);
window.location.href = urlParams.get('redirect_failed') || '/';
return;
}
setItemIds(parsedIds);
} catch (e) {
console.error('Invalid itemsId format', e);
window.location.href = urlParams.get('redirect_failed') || '/';
}
}, []);
// Fetch products
useEffect(() => { useEffect(() => {
if (itemIds && Array.isArray(itemIds) && itemIds.length > 0) { if (itemIds?.length > 0) {
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', { fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -73,73 +131,6 @@ const Checkout = ({ socketId, transactionSuccess }) => {
} }
}, [itemIds]); }, [itemIds]);
const handleRemove = (id) => {
const updatedItemIds = itemIds.filter((itemId) => itemId !== id);
const updatedProducts = products.filter((product) => product.id !== id);
if (updatedItemIds.length === 0) {
window.location.href = redirect_failed;
return;
}
setItemIds(updatedItemIds);
setProducts(updatedProducts);
const url = new URL(window.location);
url.searchParams.set('itemsId', JSON.stringify(updatedItemIds));
window.history.replaceState(null, '', url.toString());
};
const handlePay = async () => {
if (!itemIds || !token) {
alert('Token atau itemsId tidak ditemukan.');
return;
}
setLoadingPay(true);
try {
const urlParams = new URLSearchParams(window.location.search);
const newName = urlParams.get('new_name');
const setName = urlParams.get('set_name');
const params = new URLSearchParams();
itemIds.forEach((id) => params.append('itemsId', id));
params.append('socketId', socketId);
params.append('paymentMethod', paymentMethod);
if (newName) params.append('newName', newName);
if (setName) params.append('setName', setName);
const response = await fetch('https://bot.kediritechnopark.com/webhook/store-production/pay', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${token}`,
},
body: params.toString(),
});
const result = await response.json();
if (response.ok) {
if (paymentMethod === 'QRIS' && result?.qris_dynamic) {
setQrisData(result.qris_dynamic);
setValue(result.total_price);
} else if (paymentMethod === 'Bank Transfer' && result?.bank_account) {
setTransferData(result);
setValue(result.total_price);
} else {
alert(`Gagal memproses pembayaran: ${result?.error || 'Unknown error'}`);
}
} else {
alert(`Request gagal: ${result?.error || 'Unknown error'}`);
}
} catch (error) {
console.error('Network error:', error);
alert('Terjadi kesalahan jaringan.');
} finally {
setLoadingPay(false);
}
};
useEffect(() => { useEffect(() => {
if (transactionSuccess) { if (transactionSuccess) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@@ -149,157 +140,139 @@ const Checkout = ({ socketId, transactionSuccess }) => {
} }
}, [transactionSuccess, redirect_uri]); }, [transactionSuccess, redirect_uri]);
const subtotal = products.reduce((acc, item) => acc + (item.price || 0), 0); useEffect(() => {
const shipping = 0; if (!payTimeout) return;
const tax = 0; const interval = setInterval(() => {
const grandTotal = subtotal + shipping + tax; const now = Date.now();
const remaining = payTimeout - now;
setTimeLeft(remaining);
if (remaining <= 0) {
clearInterval(interval);
alert('Waktu pembayaran habis.');
window.location.href = redirect_failed;
}
}, 1000);
return () => clearInterval(interval);
}, [payTimeout]);
return ( return (
<div className={styles.page}> <div className={styles.page}>
<div className={styles.checkoutCard}> <div className={styles.checkoutCard}>
<div className={styles.cartSection}> <div className={styles.cartSection}>
{!qrisData && !transferData ? ( <div className={styles.invHeader}>
<> <div>
<div className={styles.invHeader}> <h2 className={styles.brand}>
<div> KEDIRI<span className={styles.brandLight}>TECHNOPARK</span>
<h2 className={styles.brand}> </h2>
KEDIRI<span className={styles.brandLight}>TECHNOPARK</span> <p className={styles.greeting}>
</h2> Hello, {parseJwt(token)?.username || 'User'} <br />
<p className={styles.greeting}> Thank you for your order
Hello, {parseJwt(token)?.username || 'User'} <br /> </p>
Thank you for your order
</p>
</div>
<div className={styles.orderInfo}>
<div className={styles.invoiceLabel}>Invoice</div>
<div className={styles.orderMeta}>ORDER #{String(itemIds?.[0] || '').padStart(5, '0')}</div>
<div className={styles.orderMeta}>{new Date().toLocaleDateString()}</div>
</div>
</div>
<table className={styles.table}>
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th className={styles.textRight}>Subtotal</th>
<th></th>
</tr>
</thead>
<tbody>
{products.map((item) => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.qty ?? 1}</td>
<td className={styles.textRight}>
Rp{(item.price || 0).toLocaleString('id-ID')}
</td>
<td className={styles.textRight}>
<button
className={styles.removeBtn}
onClick={() => handleRemove(item.id)}
>
&times;
</button>
</td>
</tr>
))}
</tbody>
</table>
<div className={styles.summary}>
<div className={styles.summaryRow}>
<span>SUBTOTAL</span>
<span>Rp{subtotal.toLocaleString('id-ID')}</span>
</div>
<div className={styles.summaryRow}>
<span>SHIPPING</span>
<span>Rp{shipping.toLocaleString('id-ID')}</span>
</div>
<div className={styles.summaryRow}>
<span>TAX</span>
<span>Rp{tax.toLocaleString('id-ID')}</span>
</div>
<div className={styles.summaryTotal}>
<span>Total</span>
<span>Rp{grandTotal.toLocaleString('id-ID')}</span>
</div>
</div>
</>
) : paymentMethod === 'QRIS' ? (
<>
<p className={styles.qrTitle}>Silakan scan QRIS ini</p>
<div className={styles.qrBox}>
<QRCodeCanvas value={qrisData} size={256} />
{transactionSuccess && (
<div className={styles.CheckmarkOverlay}>
<svg className={styles.Checkmark} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<circle className={styles.CheckmarkCircle} cx="26" cy="26" r="25" />
<path className={styles.CheckmarkCheck} d="M14 27l7 7 16-16" />
</svg>
</div>
)}
{!transactionSuccess && <h2>Rp{value?.toLocaleString('id-ID')}</h2>}
</div>
</>
) : (
<div className={styles.transferBox}>
<h3 className={styles.transferTitle}>Bank Transfer Information</h3>
<div className={styles.transferRow}><span>Bank</span><span>{transferData?.bank_name}</span></div>
<div className={styles.transferRow}><span>Account No</span><span>{transferData?.bank_account}</span></div>
<div className={styles.transferRow}><span>Account Name</span><span>{transferData?.account_name}</span></div>
<div className={styles.transferRow}><span>Total</span><span>Rp{value?.toLocaleString('id-ID')}</span></div>
</div> </div>
)} <div className={styles.orderInfo}>
<div className={styles.invoiceLabel}>Invoice</div>
<div className={styles.orderMeta}>ORDER #{String(itemIds?.[0] || '').padStart(5, '0')}</div>
<div className={styles.orderMeta}>{new Date().toLocaleDateString()}</div>
</div>
</div>
<table className={styles.table}>
<thead>
<tr>
<th>Item</th>
<th className={styles.textRight}>{grandTotal}</th>
</tr>
</thead>
<tbody>
{products.map((item) => (
<tr key={item.id}>
<td>{item.name}</td>
<td className={styles.textRight}>
Rp{(item.price || 0).toLocaleString('id-ID')}
</td>
</tr>
))}
</tbody>
</table>
<div className={styles.summary}>
<div className={styles.summaryRow}>
<span>SUBTOTAL</span>
<span>Rp{grandTotal.toLocaleString('id-ID')}</span>
</div>
<div className={styles.summaryRow}>
<span>TAX</span>
<span>Rp{tax.toLocaleString('id-ID')}</span>
</div>
<div className={styles.summaryTotal}>
<span>Total</span>
<span>Rp{grandTotal.toLocaleString('id-ID')}</span>
</div>
</div>
</div> </div>
<div className={styles.checkoutSection}> <div className={styles.checkoutSection}>
<div> {(qrisData || transferData) && (
<h2 className={styles.checkoutTitle}> <div className="mt-4">
Rp{(qrisData || transferData ? value : grandTotal).toLocaleString('id-ID')} {qrisData && (
</h2> <div className={styles.accordion}>
<div
className={styles.accordionHeader}
onClick={() =>
setActiveAccordion(activeAccordion === 'QRIS' ? '' : 'QRIS')
}
>
QRIS Payment
</div>
{activeAccordion === 'QRIS' && (
<div className={styles.accordionBody}>
<QRCodeCanvas value={qrisData} size={200} />
{!transactionSuccess && (
<>
<h5 className="mt-3">Rp{value?.toLocaleString('id-ID')}</h5>
{timeLeft !== null && (
<p>Waktu tersisa: <strong>{formatTimeLeft(timeLeft)}</strong></p>
)}
</>
)}
{transactionSuccess && (
<div className={styles.CheckmarkOverlay}>
<svg className={styles.Checkmark} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<circle className={styles.CheckmarkCircle} cx="26" cy="26" r="25" />
<path className={styles.CheckmarkCheck} d="M14 27l7 7 16-16" />
</svg>
</div>
)}
</div>
)}
</div>
)}
<div className={styles.paymentInfo}> {transferData && (
<p className={styles.paymentHeading}>PAYMENT INFORMATION</p> <div className={styles.accordion}>
<label className={styles.radioLabel}> <div
<input className={styles.accordionHeader}
type="radio" onClick={() =>
name="payment" setActiveAccordion(activeAccordion === 'Bank' ? '' : 'Bank')
value="Bank Transfer" }
checked={paymentMethod === 'Bank Transfer'} >
onChange={(e) => setPaymentMethod(e.target.value)} Bank Transfer
/> </div>
Bank Transfer {activeAccordion === 'Bank' && (
</label> <div className={styles.accordionBody}>
<label className={styles.radioLabel}> <div><strong>Bank:</strong> {transferData?.bank_name}</div>
<input <div><strong>Account No:</strong> {transferData?.bank_account}</div>
type="radio" <div><strong>Account Name:</strong> {transferData?.account_name}</div>
name="payment" <div><strong>Total:</strong> Rp{value?.toLocaleString('id-ID')}</div>
value="QRIS" {timeLeft !== null && (
checked={paymentMethod === 'QRIS'} <div><strong>Waktu Tersisa:</strong> {formatTimeLeft(timeLeft)}</div>
onChange={(e) => setPaymentMethod(e.target.value)} )}
/> </div>
QRIS )}
</label> </div>
)}
</div> </div>
)}
<div className={styles.inputGroup}>
<input
type="text"
placeholder={parseJwt(token)?.username || 'User'}
className={styles.inputNote}
readOnly
/>
</div>
<button
className={styles.paymentBtn}
onClick={handlePay}
disabled={loadingPay || qrisData !== null || transferData !== null}
>
{loadingPay ? 'Processing...' : (qrisData || transferData) ? 'Payment Created' : 'PAY'}
</button>
</div>
<div className={styles.footerText}> <div className={styles.footerText}>
Powered by <span className={styles.footerHighlight}>KEDIRITECHNOPARK</span> Powered by <span className={styles.footerHighlight}>KEDIRITECHNOPARK</span>

View File

@@ -46,18 +46,20 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.brand { .brand {
margin-top: 0;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 800; font-weight: 800;
color: #2563eb; color: #2563eb;
} }
.brandLight { font-weight: 500; } .brandLight { font-weight: 500; }
.greeting { margin-top: .5rem; font-size: .875rem; color: #374151; } .greeting { text-align: left; margin-top: .5rem; font-size: .875rem; color: #374151; }
.orderInfo { text-align: right; font-size: .875rem; color: #6b7280; } .orderInfo { text-align: right; font-size: .875rem; color: #6b7280; }
.invoiceLabel { font-size: 1rem; color: #4b5563; } .invoiceLabel { font-size: 1rem; color: #4b5563; }
.orderMeta { margin-top: .125rem; } .orderMeta { margin-top: .125rem; }
/* Table */ /* Table */
.table { .table {
text-align: left;
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-top: .5rem; margin-top: .5rem;
@@ -67,7 +69,6 @@
.table th, .table td { padding: .75rem 0; } .table th, .table td { padding: .75rem 0; }
.table th { .table th {
border-bottom: 1px solid #d1d5db; border-bottom: 1px solid #d1d5db;
text-align: left;
color: #374151; color: #374151;
font-weight: 600; font-weight: 600;
} }
@@ -210,3 +211,87 @@
@media (max-width: 767px) { @media (max-width: 767px) {
.checkoutCard { flex-direction: column; } .checkoutCard { flex-direction: column; }
} }
.selectPayment {
width: 100%;
padding: 10px;
font-size: 16px;
border-radius: 6px;
border: 1px solid #ccc;
margin-top: 8px;
margin-bottom: 16px;
}
.selectPaymentModern {
width: 100%;
padding: 0.65rem 1rem;
font-size: 1rem;
border-radius: 0.5rem;
border: 1px solid #d1d5db;
background-color: #f9fafb;
appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg fill='gray' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/></svg>");
background-repeat: no-repeat;
background-position: right 1rem center;
background-size: 1rem;
cursor: pointer;
}
.selectPaymentModern:focus {
outline: none;
border-color: #3b82f6;
background-color: #fff;
}
.transferBox {
margin-top: 2rem;
padding: 1.5rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
background: #f9fafb;
text-align: left;
}
.transferBox span {
text-align: right;
}
.transferTitle {
font-size: 1.125rem;
font-weight: 700;
margin-bottom: 1rem;
color: #111827;
}
.transferRow {
display: flex;
justify-content: space-between;
padding: 0.5rem 0;
border-bottom: 1px dashed #d1d5db;
font-size: 0.95rem;
color: #374151;
}
.transferRow:last-child {
border-bottom: none;
}
.accordion {
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 1rem;
overflow: hidden;
}
.accordionHeader {
background-color: #f5f5f5;
padding: 12px 16px;
cursor: pointer;
font-weight: bold;
transition: background 0.2s ease;
}
.accordionHeader:hover {
background-color: #e0e0e0;
}
.accordionBody {
padding: 16px;
background-color: #fff;
border-top: 1px solid #ddd;
}