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 [products, setProducts] = useState([]);
const [itemIds, setItemIds] = useState(null);
@@ -30,80 +37,32 @@ const Checkout = ({ socketId, transactionSuccess }) => {
const [redirect_uri, setRedirect_Uri] = useState('');
const [redirect_failed, setRedirect_Failed] = useState('');
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(() => {
const urlParams = new URLSearchParams(window.location.search);
const tokenParam = urlParams.get('token');
const itemsIdString = urlParams.get('itemsId');
setRedirect_Uri(urlParams.get('redirect_uri') || '');
setRedirect_Failed(urlParams.get('redirect_failed') || '');
setToken(tokenParam);
if (!socketId) return;
if (!itemsIdString) {
window.location.href = urlParams.get('redirect_failed') || '/';
return;
}
try {
const parsedIds = JSON.parse(itemsIdString);
if (!Array.isArray(parsedIds) || parsedIds.length === 0) {
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(() => {
if (itemIds && Array.isArray(itemIds) && itemIds.length > 0) {
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemsId: itemIds, noParents: true }),
})
.then((res) => res.json())
.then((data) => setProducts(data))
.catch((err) => console.error('Error fetching products:', err));
}
}, [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());
};
let urlParams = new URLSearchParams(window.location.search);
let tokenParam = urlParams.get('token');
let itemsIdString = urlParams.get('itemsId');
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));
itemsIdString.forEach((id) => params.append('itemsId', id));
params.append('socketId', socketId);
params.append('paymentMethod', paymentMethod);
if (newName) params.append('newName', newName);
@@ -113,22 +72,27 @@ const Checkout = ({ socketId, transactionSuccess }) => {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${token}`,
Authorization: `Bearer ${tokenParam}`,
},
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'}`);
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'}`);
}
@@ -140,6 +104,33 @@ const Checkout = ({ socketId, transactionSuccess }) => {
}
};
itemsIdString = JSON.parse(urlParams.get('itemsId'));
setRedirect_Uri(urlParams.get('redirect_uri') || '');
setRedirect_Failed(urlParams.get('redirect_failed') || '');
setToken(tokenParam);
if (!itemsIdString || !Array.isArray(itemsIdString) || itemsIdString.length === 0) {
window.location.href = urlParams.get('redirect_failed') || '/';
return;
}
setItemIds(itemsIdString);
handlePay();
}, [socketId]);
useEffect(() => {
if (itemIds?.length > 0) {
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemsId: itemIds, noParents: true }),
})
.then((res) => res.json())
.then((data) => setProducts(data))
.catch((err) => console.error('Error fetching products:', err));
}
}, [itemIds]);
useEffect(() => {
if (transactionSuccess) {
const timer = setTimeout(() => {
@@ -149,17 +140,25 @@ const Checkout = ({ socketId, transactionSuccess }) => {
}
}, [transactionSuccess, redirect_uri]);
const subtotal = products.reduce((acc, item) => acc + (item.price || 0), 0);
const shipping = 0;
const tax = 0;
const grandTotal = subtotal + shipping + tax;
useEffect(() => {
if (!payTimeout) return;
const interval = setInterval(() => {
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 (
<div className={styles.page}>
<div className={styles.checkoutCard}>
<div className={styles.cartSection}>
{!qrisData && !transferData ? (
<>
<div className={styles.invHeader}>
<div>
<h2 className={styles.brand}>
@@ -181,27 +180,16 @@ const Checkout = ({ socketId, transactionSuccess }) => {
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th className={styles.textRight}>Subtotal</th>
<th></th>
<th className={styles.textRight}>{grandTotal}</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>
@@ -210,11 +198,7 @@ const Checkout = ({ socketId, transactionSuccess }) => {
<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>
<span>Rp{grandTotal.toLocaleString('id-ID')}</span>
</div>
<div className={styles.summaryRow}>
<span>TAX</span>
@@ -225,12 +209,32 @@ const Checkout = ({ socketId, transactionSuccess }) => {
<span>Rp{grandTotal.toLocaleString('id-ID')}</span>
</div>
</div>
</>
) : paymentMethod === 'QRIS' ? (
</div>
<div className={styles.checkoutSection}>
{(qrisData || transferData) && (
<div className="mt-4">
{qrisData && (
<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 && (
<>
<p className={styles.qrTitle}>Silakan scan QRIS ini</p>
<div className={styles.qrBox}>
<QRCodeCanvas value={qrisData} size={256} />
<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">
@@ -239,67 +243,36 @@ const Checkout = ({ socketId, transactionSuccess }) => {
</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.checkoutSection}>
<div>
<h2 className={styles.checkoutTitle}>
Rp{(qrisData || transferData ? value : grandTotal).toLocaleString('id-ID')}
</h2>
<div className={styles.paymentInfo}>
<p className={styles.paymentHeading}>PAYMENT INFORMATION</p>
<label className={styles.radioLabel}>
<input
type="radio"
name="payment"
value="Bank Transfer"
checked={paymentMethod === 'Bank Transfer'}
onChange={(e) => setPaymentMethod(e.target.value)}
/>
Bank Transfer
</label>
<label className={styles.radioLabel}>
<input
type="radio"
name="payment"
value="QRIS"
checked={paymentMethod === 'QRIS'}
onChange={(e) => setPaymentMethod(e.target.value)}
/>
QRIS
</label>
</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}
{transferData && (
<div className={styles.accordion}>
<div
className={styles.accordionHeader}
onClick={() =>
setActiveAccordion(activeAccordion === 'Bank' ? '' : 'Bank')
}
>
{loadingPay ? 'Processing...' : (qrisData || transferData) ? 'Payment Created' : 'PAY'}
</button>
Bank Transfer
</div>
{activeAccordion === 'Bank' && (
<div className={styles.accordionBody}>
<div><strong>Bank:</strong> {transferData?.bank_name}</div>
<div><strong>Account No:</strong> {transferData?.bank_account}</div>
<div><strong>Account Name:</strong> {transferData?.account_name}</div>
<div><strong>Total:</strong> Rp{value?.toLocaleString('id-ID')}</div>
{timeLeft !== null && (
<div><strong>Waktu Tersisa:</strong> {formatTimeLeft(timeLeft)}</div>
)}
</div>
)}
</div>
)}
</div>
)}
<div className={styles.footerText}>
Powered by <span className={styles.footerHighlight}>KEDIRITECHNOPARK</span>

View File

@@ -46,18 +46,20 @@
margin-bottom: 1rem;
}
.brand {
margin-top: 0;
font-size: 1.25rem;
font-weight: 800;
color: #2563eb;
}
.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; }
.invoiceLabel { font-size: 1rem; color: #4b5563; }
.orderMeta { margin-top: .125rem; }
/* Table */
.table {
text-align: left;
width: 100%;
border-collapse: collapse;
margin-top: .5rem;
@@ -67,7 +69,6 @@
.table th, .table td { padding: .75rem 0; }
.table th {
border-bottom: 1px solid #d1d5db;
text-align: left;
color: #374151;
font-weight: 600;
}
@@ -210,3 +211,87 @@
@media (max-width: 767px) {
.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;
}