Giant Update regsiter
NPM Installation / build (16.x, ubuntu-latest) (push) Waiting to run
NPM Installation / build (16.x, windows-latest) (push) Waiting to run
NPM Installation / build (17.x, ubuntu-latest) (push) Waiting to run
NPM Installation / build (17.x, windows-latest) (push) Waiting to run
NPM Installation / build (18.x, ubuntu-latest) (push) Waiting to run
NPM Installation / build (18.x, windows-latest) (push) Waiting to run

This commit is contained in:
Wian Drs
2026-08-01 11:41:31 +07:00
parent 03fe4b1fd4
commit 484f89e2ee
28 changed files with 2439 additions and 320 deletions
+58 -151
View File
@@ -1,171 +1,78 @@
<script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue'
import BaseResponsiveDataView from '@/components/base/BaseResponsiveDataView.vue'
import { getMyWallets, getWalletLedger, transferWallet, withdrawWallet } from '@/services/walletService'
import { getMyWallets, getWalletLedger, withdrawWallet } from '@/services/walletService'
import { showError, showSuccess } from '@/utils/swal'
const loading = ref(true), wallets = ref([]), activeId = ref(null), ledger = ref([]), modal = ref(''), saving = ref(false)
const loading = ref(true), wallets = ref([]), activeId = ref(null), ledger = ref([]), modal = ref(false), saving = ref(false)
const pagination = ref({ current_page: 1, last_page: 1, per_page: 10, total: 0 }), query = ref({ page: 1, per_page: 10 })
const form = reactive({ amount: '', destination_wallet_id: '', bank_code: '', account_number: '', account_name: '' })
const active = computed(() => wallets.value.find((x) => x.id === activeId.value) || wallets.value[0])
const otherWallets = computed(() => wallets.value.filter((x) => x.id !== active.value?.id))
const form = reactive({ amount: '', bank_code: '', account_number: '', account_name: '' })
const active = computed(() => wallets.value.find((item) => item.id === activeId.value) || wallets.value[0])
const columns = [{ key: 'date', label: 'Tanggal' }, { key: 'number', label: 'Referensi' }, { key: 'type', label: 'Jenis' }, { key: 'debit', label: 'Debit' }, { key: 'credit', label: 'Kredit' }, { key: 'balance', label: 'Saldo' }]
const money = (value) => `Rp ${Number(value || 0).toLocaleString('id-ID')}`
async function loadWallets() { loading.value = true; try { wallets.value = (await getMyWallets())?.data || []; activeId.value ||= wallets.value[0]?.id; await loadLedger() } catch (e) { showError(e?.response?.data?.message || 'Gagal memuat dompet.') } finally { loading.value = false } }
async function loadBalances() {
loading.value = true
try {
wallets.value = (await getMyWallets())?.data || []
if (!wallets.value.some((item) => item.id === activeId.value)) activeId.value = wallets.value[0]?.id
await loadLedger()
} catch (error) { showError(error?.response?.data?.message || 'Gagal memuat saldo.') }
finally { loading.value = false }
}
async function loadLedger(params = query.value) {
if (!active.value) return
if (!active.value?.ledger_available) {
ledger.value = []; pagination.value = { current_page: 1, last_page: 1, per_page: 10, total: 0 }; return
}
query.value = { ...query.value, ...params }
const response = await getWalletLedger(active.value.id, query.value), rows = response?.data?.data || []
ledger.value = rows.map((x) => ({ ...x, date: new Date(x.created_at).toLocaleString('id-ID'), number: x.transaction?.transaction_number, type: x.transaction?.type, debit: x.entry_type === 'debit' ? money(x.amount) : '-', credit: x.entry_type === 'credit' ? money(x.amount) : '-', balance: money(x.balance_after) }))
ledger.value = rows.map((item) => ({ ...item, date: new Date(item.created_at).toLocaleString('id-ID'), number: item.transaction?.transaction_number, type: item.transaction?.type, debit: item.entry_type === 'debit' ? money(item.amount) : '-', credit: item.entry_type === 'credit' ? money(item.amount) : '-', balance: money(item.balance_after) }))
pagination.value = { current_page: response?.data?.current_page || 1, last_page: response?.data?.last_page || 1, per_page: response?.data?.per_page || 10, total: response?.data?.total || 0 }
}
function open(type) { Object.assign(form, { amount: '', destination_wallet_id: otherWallets.value[0]?.id || '', bank_code: '', account_number: '', account_name: '' }); modal.value = type }
async function submit() {
function openWithdrawal() { Object.assign(form, { amount: '', bank_code: '', account_number: '', account_name: '' }); modal.value = true }
async function submitWithdrawal() {
saving.value = true
try {
if (modal.value === 'transfer') await transferWallet(active.value.id, { destination_wallet_id: form.destination_wallet_id, amount: form.amount })
if (modal.value === 'withdraw') await withdrawWallet(active.value.id, { amount: form.amount, bank_code: form.bank_code, account_number: form.account_number, account_name: form.account_name })
modal.value = ''; showSuccess('Permintaan berhasil diproses.'); await loadWallets()
} catch (e) { showError(e?.response?.data?.message || 'Transaksi dompet gagal.') } finally { saving.value = false }
try { await withdrawWallet(active.value.id, form); modal.value = false; showSuccess('Permintaan penarikan settlement berhasil dibuat.'); await loadBalances() }
catch (error) { showError(error?.response?.data?.message || 'Penarikan saldo settlement gagal.') }
finally { saving.value = false }
}
watch(activeId, loadLedger)
onMounted(loadWallets)
onMounted(loadBalances)
</script>
<template><div class="brdvx-page-container"><div class="brdvx-page-header"><h5 class="mb-0 fw-semibold">Dompet</h5><small class="text-body-secondary">Saldo user dan tenant dengan pencatatan ledger</small></div><div v-if="loading" class="brdvx-loading-state"><CSpinner color="primary" /><p class="brdvx-loading-text">Memuat data...</p></div><template v-else>
<div class="wallet-grid"><button v-for="wallet in wallets" :key="wallet.id" class="wallet-card" :class="{ active: active?.id === wallet.id }" @click="activeId = wallet.id"><span>{{ wallet.wallet_type === 'tenant' ? 'DOMPET TENANT' : 'DOMPET USER' }}</span><strong>{{ money(wallet.available_balance) }}</strong><small>Pending {{ money(wallet.pending_balance) }} · IDR</small></button></div>
<div class="wallet-actions">
<button type="button" class="wallet-action" disabled>
<span class="wallet-action-icon topup"><CIcon icon="cil-data-transfer-down" size="xl" /></span>
<span><strong>Isi Saldo</strong><small>Tambahkan saldo melalui payment gateway</small></span>
</button>
<button type="button" class="wallet-action" :disabled="!active" @click="open('withdraw')">
<span class="wallet-action-icon withdraw"><CIcon icon="cil-data-transfer-up" size="xl" /></span>
<span><strong>Tarik Saldo</strong><small>Kirim saldo ke rekening bank</small></span>
</button>
<button type="button" class="wallet-action" :disabled="!otherWallets.length" @click="open('transfer')">
<span class="wallet-action-icon transfer"><CIcon icon="cil-transfer" size="xl" /></span>
<span><strong>Pindah Saldo</strong><small>Transfer antara dompet user dan tenant</small></span>
</button>
<template>
<div class="brdvx-page-container">
<div class="brdvx-page-header"><h5 class="mb-0 fw-semibold">Saldo Deposit</h5><small class="text-body-secondary">Saldo layanan user, settlement tenant, dan fasilitas kredit voucher pada tenant aktif</small></div>
<div v-if="loading" class="brdvx-loading-state"><CSpinner color="primary" /><p class="brdvx-loading-text">Memuat data...</p></div>
<template v-else>
<div class="wallet-grid">
<button v-for="balance in wallets" :key="balance.id" type="button" class="wallet-card" :class="{ active: active?.id === balance.id, credit: balance.balance_kind === 'credit' }" @click="activeId = balance.id">
<span>{{ (balance.label || 'Saldo Deposit').toUpperCase() }}</span><strong>{{ money(balance.available_balance) }}</strong>
<template v-if="balance.balance_kind === 'credit'"><small>Limit {{ money(balance.credit_limit) }} · Terpakai {{ money(balance.used_credit) }}</small><small class="usage-note">Khusus pembelian voucher hotspot</small></template>
<template v-else><small>Pending {{ money(balance.pending_balance) }} · IDR</small><small class="usage-note">{{ balance.usage_note }}</small></template>
</button>
</div>
<div class="wallet-actions">
<button v-if="active?.balance_kind === 'deposit'" type="button" class="wallet-action" disabled><span class="wallet-action-icon topup"><CIcon icon="cil-data-transfer-down" size="xl" /></span><span><strong>Isi Saldo Deposit</strong><small>Tambahkan saldo untuk transaksi layanan tenant</small></span></button>
<button v-if="active?.can_withdraw" type="button" class="wallet-action" @click="openWithdrawal"><span class="wallet-action-icon withdraw"><CIcon icon="cil-data-transfer-up" size="xl" /></span><span><strong>Tarik Settlement</strong><small>Kirim saldo settlement tenant ke rekening bank</small></span></button>
<div v-if="active?.balance_kind === 'credit'" class="credit-information"><CIcon icon="cil-info" size="xl" /><span><strong>Fasilitas Kredit Voucher</strong><small>Sisa kredit berkurang saat membeli voucher dan bertambah kembali setelah tagihan agen dibayar.</small></span></div>
</div>
<template v-if="active?.ledger_available"><h6 class="mt-4">Mutasi {{ active?.label }}</h6><BaseResponsiveDataView :columns="columns" :items="ledger" server-side :pagination="pagination" @query-change="loadLedger" /></template>
<CAlert v-else-if="active?.balance_kind === 'credit'" color="info" class="mt-4 mb-0">Riwayat pemakaian saldo kredit tersedia pada transaksi penjualan Voucher Hotspot.</CAlert>
</template>
</div>
<h6 class="mt-4">Mutasi {{ active?.wallet_type === 'tenant' ? 'Dompet Tenant' : 'Dompet User' }}</h6><BaseResponsiveDataView :columns="columns" :items="ledger" server-side :pagination="pagination" @query-change="loadLedger" />
</template></div>
<CModal :visible="Boolean(modal)" @close="modal = ''"><CModalHeader><CModalTitle>{{ modal === 'transfer' ? 'Pindah Dompet' : 'Tarik Saldo' }}</CModalTitle></CModalHeader><CModalBody><CFormLabel>Nominal</CFormLabel><CFormInput v-model.number="form.amount" type="number" min="1" class="mb-3" /><template v-if="modal === 'transfer'"><CFormLabel>Dompet Tujuan</CFormLabel><CFormSelect v-model="form.destination_wallet_id"><option v-for="item in otherWallets" :key="item.id" :value="item.id">{{ item.wallet_type === 'tenant' ? 'Dompet Tenant' : 'Dompet User' }}</option></CFormSelect></template><template v-else><CFormLabel>Bank</CFormLabel><CFormInput v-model="form.bank_code" class="mb-2" /><CFormLabel>Nomor Rekening</CFormLabel><CFormInput v-model="form.account_number" class="mb-2" /><CFormLabel>Nama Pemilik</CFormLabel><CFormInput v-model="form.account_name" /></template></CModalBody><CModalFooter><CButton color="secondary" @click="modal = ''">Batal</CButton><CButton color="primary" :disabled="saving" @click="submit">Proses</CButton></CModalFooter></CModal></template>
<CModal :visible="modal" @close="modal = false"><CModalHeader><CModalTitle>Tarik Saldo Settlement</CModalTitle></CModalHeader><CModalBody><CFormLabel>Nominal</CFormLabel><CFormInput v-model.number="form.amount" type="number" min="1" class="mb-3" /><CFormLabel>Bank</CFormLabel><CFormInput v-model="form.bank_code" class="mb-2" /><CFormLabel>Nomor Rekening</CFormLabel><CFormInput v-model="form.account_number" class="mb-2" /><CFormLabel>Nama Pemilik</CFormLabel><CFormInput v-model="form.account_name" /></CModalBody><CModalFooter><CButton color="secondary" @click="modal = false">Batal</CButton><CButton color="primary" :disabled="saving" @click="submitWithdrawal">Proses</CButton></CModalFooter></CModal>
</template>
<style scoped>
.wallet-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 400px));
gap: 1rem;
justify-content: center;
}
.wallet-card {
display: flex;
width: 100%;
max-width: 400px;
min-height: 130px;
flex-direction: column;
justify-content: center;
padding: 1.25rem;
color: white;
text-align: left;
background: linear-gradient(135deg, #f97316, #ff8c00 58%, #f9b115);
border: 3px solid transparent;
border-radius: 1rem;
box-shadow: 0 0.5rem 1rem rgba(249, 115, 22, 0.22);
}
.wallet-card.active {
border-color: rgba(255, 255, 255, 0.85);
}
.wallet-card span,
.wallet-card small {
opacity: 0.82;
}
.wallet-card strong {
margin: 0.4rem 0;
font-size: 1.5rem;
}
.wallet-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 250px));
justify-content: center;
gap: 0.75rem;
max-width: 800px;
margin: 1.25rem auto 0;
}
.wallet-action {
display: flex;
min-height: 78px;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
color: var(--cui-body-color);
text-align: left;
background: var(--cui-body-bg);
border: 1px solid var(--cui-border-color);
border-radius: 0.85rem;
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.06);
transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
.wallet-action:not(:disabled):hover {
border-color: #ff8c00;
box-shadow: 0 0.4rem 1rem rgba(249, 115, 22, 0.14);
transform: translateY(-2px);
}
.wallet-action:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.wallet-action > span:last-child {
display: flex;
min-width: 0;
flex-direction: column;
}
.wallet-action strong {
font-size: 0.9rem;
}
.wallet-action small {
margin-top: 0.15rem;
color: var(--cui-secondary-color);
font-size: 0.7rem;
line-height: 1.25;
}
.wallet-action-icon {
display: inline-flex;
width: 44px;
height: 44px;
flex: 0 0 44px;
align-items: center;
justify-content: center;
color: white;
border-radius: 0.75rem;
}
.wallet-action-icon.topup {
background: linear-gradient(135deg, #f97316, #f9b115);
}
.wallet-action-icon.withdraw {
background: linear-gradient(135deg, #2eb85c, #1f9d55);
}
.wallet-action-icon.transfer {
background: linear-gradient(135deg, #3399ff, #5856d6);
}
@media (max-width: 575.98px) {
.wallet-action {
width: 100%;
}
}
.wallet-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),400px)); justify-content:center; gap:1rem; }
.wallet-card { display:flex; width:100%; max-width:400px; min-height:150px; flex-direction:column; justify-content:center; padding:1.25rem; color:white; text-align:left; background:linear-gradient(135deg,#f97316,#ff8c00 58%,#f9b115); border:3px solid transparent; border-radius:1rem; box-shadow:0 .5rem 1rem rgba(249,115,22,.22); }
.wallet-card.active { border-color:rgba(255,255,255,.88); }.wallet-card.credit { background:linear-gradient(135deg,#5856d6,#3399ff); box-shadow:0 .5rem 1rem rgba(51,153,255,.2); }
.wallet-card span,.wallet-card small { opacity:.84; }.wallet-card strong { margin:.4rem 0; font-size:1.5rem; }.usage-note { margin-top:.4rem; font-size:.68rem; }
.wallet-actions { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,230px),300px)); justify-content:center; gap:.75rem; max-width:800px; margin:1.25rem auto 0; }
.wallet-action,.credit-information { display:flex; min-height:78px; align-items:center; gap:.75rem; padding:.75rem; text-align:left; border-radius:.85rem; }
.wallet-action { color:var(--cui-body-color); background:var(--cui-body-bg); border:1px solid var(--cui-border-color); box-shadow:0 .25rem .75rem rgba(0,0,0,.06); }.wallet-action:disabled { cursor:not-allowed; opacity:.55; }
.wallet-action>span:last-child,.credit-information span { display:flex; min-width:0; flex-direction:column; }.wallet-action small,.credit-information small { margin-top:.15rem; font-size:.7rem; line-height:1.25; }
.wallet-action-icon { display:inline-flex; width:44px; height:44px; flex:0 0 44px; align-items:center; justify-content:center; color:white; border-radius:.75rem; }.wallet-action-icon.topup { background:linear-gradient(135deg,#f97316,#f9b115); }.wallet-action-icon.withdraw { background:linear-gradient(135deg,#2eb85c,#1f9d55); }
.credit-information { color:var(--cui-info-text-emphasis); background:var(--cui-info-bg-subtle); border:1px solid var(--cui-info-border-subtle); }
</style>