Allowance check

This commit is contained in:
ISMAIL MASSERAN
2026-04-14 02:34:00 +00:00
parent 0c4d200839
commit 3775c4a876
100 changed files with 81493 additions and 3226 deletions
@@ -0,0 +1,688 @@
<template>
<div class="admin-data-table">
<div v-if="exportable" class="clearfix admin-data-table__toolbar">
<div class="btn-group pull-right">
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="fa fa-download"></i> Eksport <span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="#" @click.prevent="exportToCSV">Eksport CSV</a></li>
<li><a href="#" @click.prevent="exportToExcel">Eksport Excel</a></li>
<li><a href="#" @click.prevent="exportToPDF">Eksport PDF</a></li>
<li><a href="#" @click.prevent="exportToJSON">Eksport JSON</a></li>
</ul>
</div>
</div>
<div class="admin-data-table__shell">
<div class="admin-data-table__scroll">
<table class="admin-data-table__table table table-hover">
<thead>
<tr>
<th v-for="h in tableHeaders" :key="h.key" :class="{
'is-sortable': h.sortable,
'admin-data-table__col-index': h.key === '__adt_index'
}" @click="h.sortable && toggleSort(h.key)">
<div class="admin-data-table__th-inner">
<span class="admin-data-table__th-title">{{ h.title }}</span>
<span v-if="h.sortable" class="admin-data-table__sort" aria-hidden="true">
<template v-if="sortKey === h.key">
<svg v-if="sortDir === 'asc'" class="admin-data-table__sort-icon"
xmlns="http://www.w3.org/2000/svg" width="14" height="14"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round">
<path d="M18 15l-6-6-6 6" />
</svg>
<svg v-else class="admin-data-table__sort-icon"
xmlns="http://www.w3.org/2000/svg" width="14" height="14"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round">
<path d="M6 9l6 6 6-6" />
</svg>
</template>
<span v-else class="admin-data-table__sort-hint">
<svg class="admin-data-table__sort-icon admin-data-table__sort-icon--faint"
xmlns="http://www.w3.org/2000/svg" width="10" height="10"
viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.5">
<path d="M18 15l-6-6-6 6" />
</svg>
<svg class="admin-data-table__sort-icon admin-data-table__sort-icon--faint"
xmlns="http://www.w3.org/2000/svg" width="10" height="10"
viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.5">
<path d="M6 9l6 6 6-6" />
</svg>
</span>
</span>
</div>
</th>
</tr>
</thead>
<tbody>
<tr v-if="loading" class="admin-data-table__state-row">
<td :colspan="tableHeaders.length">
<div class="admin-data-table__state">
<i class="fa fa-refresh fa-spin admin-data-table__state-icon"></i>
<div class="admin-data-table__state-title">Memuatkan</div>
</div>
</td>
</tr>
<tr v-else-if="!displayRows.length" class="admin-data-table__state-row">
<td :colspan="tableHeaders.length">
<div class="admin-data-table__state">
<i class="fa fa-inbox admin-data-table__state-icon"></i>
<div class="admin-data-table__state-title">{{ emptyText }}</div>
<div class="admin-data-table__state-hint">Tiada rekod untuk dipaparkan</div>
</div>
</td>
</tr>
<tr v-else v-for="(item, rowIdx) in displayRows" :key="String(item[itemKey])">
<td v-for="h in tableHeaders" :key="h.key" :class="cellTdClass(h)">
<template v-if="h.key === '__adt_index'">
{{ rowGlobalIndex(rowIdx) }}
</template>
<template v-else>
<slot :name="'item-' + h.key" :item="item" :value="cellValue(item, h.key)">
{{ formatCell(item, h.key) }}
</slot>
</template>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="showPagination && !loading && totalItems > 0" class="admin-data-table__footer">
<div class="admin-data-table__footer-left">
<label class="admin-data-table__footer-label">Baris setiap halaman</label>
<select v-model.number="localPerPage" class="form-control input-sm admin-data-table__page-size"
@change="onPerPageChange">
<option v-for="n in perPageOptions" :key="n" :value="n">{{ n }}</option>
</select>
</div>
<div class="admin-data-table__footer-mid">
{{ rangeFrom }}{{ rangeTo }} daripada {{ totalItems }}
</div>
<nav class="admin-data-table__footer-right" aria-label="Pagination">
<ul class="pagination pagination-sm admin-data-table__pagination">
<li :class="{ disabled: page <= 1 }">
<a href="#" @click.prevent="goPage(page - 1)">Sebelum</a>
</li>
<li :class="{ disabled: page >= totalPages }">
<a href="#" @click.prevent="goPage(page + 1)">Seterusnya</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</template>
<script>
import { jsPDF } from 'jspdf';
import autoTable from 'jspdf-autotable';
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
export default {
name: 'AdminDataTable',
props: {
headers: {
type: Array,
default: function () {
return [];
}
},
items: {
type: Array,
default: function () {
return [];
}
},
itemsPerPage: {
type: Number,
default: 10
},
showPagination: {
type: Boolean,
default: true
},
loading: {
type: Boolean,
default: false
},
itemKey: {
type: String,
default: 'id'
},
emptyText: {
type: String,
default: 'Tiada data'
},
exportable: {
type: Boolean,
default: false
},
exportFileName: {
type: String,
default: 'table-data'
},
showIndex: {
type: Boolean,
default: true
},
indexTitle: {
type: String,
default: 'Bil'
}
},
data: function () {
return {
page: 1,
localPerPage: this.itemsPerPage,
sortKey: null,
sortDir: 'asc',
perPageOptions: [10, 25, 50, 100]
};
},
computed: {
totalItems: function () {
return this.sortedItems.length;
},
totalPages: function () {
var n = Math.ceil(this.totalItems / this.localPerPage) || 1;
return n;
},
sortedItems: function () {
var list = this.items.slice();
if (!this.sortKey) {
return list;
}
var key = this.sortKey;
var dir = this.sortDir === 'desc' ? -1 : 1;
list.sort(function (a, b) {
var va = a[key];
var vb = b[key];
if (va == null && vb == null) return 0;
if (va == null) return 1;
if (vb == null) return -1;
if (typeof va === 'number' && typeof vb === 'number') {
return (va - vb) * dir;
}
return String(va).localeCompare(String(vb), undefined, { numeric: true }) * dir;
});
return list;
},
displayRows: function () {
if (!this.showPagination) {
return this.sortedItems;
}
var start = (this.page - 1) * this.localPerPage;
return this.sortedItems.slice(start, start + this.localPerPage);
},
rangeFrom: function () {
if (!this.totalItems) return 0;
return (this.page - 1) * this.localPerPage + 1;
},
rangeTo: function () {
return Math.min(this.page * this.localPerPage, this.totalItems);
},
tableHeaders: function () {
if (!this.showIndex) {
return this.headers;
}
return [
{ title: this.indexTitle, key: '__adt_index', sortable: false }
].concat(this.headers);
}
},
watch: {
items: function () {
if (this.page > this.totalPages) {
this.page = Math.max(1, this.totalPages);
}
},
itemsPerPage: function (v) {
this.localPerPage = v;
}
},
methods: {
rowGlobalIndex: function (rowIdx) {
if (!this.showPagination) {
return rowIdx + 1;
}
return (this.page - 1) * this.localPerPage + rowIdx + 1;
},
cellTdClass: function (h) {
var o = {};
if (h.key === '__adt_index') o['admin-data-table__col-index'] = true;
return o;
},
cellValue: function (item, key) {
return item[key];
},
formatCell: function (item, key) {
var v = item[key];
return v == null ? '' : v;
},
toggleSort: function (key) {
if (this.sortKey === key) {
this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
} else {
this.sortKey = key;
this.sortDir = 'asc';
}
},
goPage: function (p) {
if (p < 1 || p > this.totalPages) return;
this.page = p;
},
onPerPageChange: function () {
this.page = 1;
},
isActionColumnKey: function (key) {
if (!key) return false;
var k = String(key).toLowerCase();
return k === 'actions' || k === 'action' || k === 'opsi' || k === 'tindakan';
},
resolveFieldValue: function (item, headerKey) {
if (headerKey === '#') return '';
var keys = String(headerKey).split('.');
var value = item;
for (var i = 0; i < keys.length; i++) {
if (value && typeof value === 'object' && keys[i] in value) {
value = value[keys[i]];
} else {
return null;
}
}
if (Array.isArray(value)) {
return value
.map(function (v) {
return v && typeof v === 'object' && v.name != null ? v.name : String(v);
})
.join(', ');
}
if (value && typeof value === 'object' && 'name' in value) {
return value.name;
}
return value;
},
cellStringForExport: function (header, item, rowIndex) {
if (typeof header.exportValue === 'function') {
return header.exportValue(item, rowIndex) || '';
}
if (header.key === '__adt_index' || header.key === '#') {
return String(rowIndex + 1);
}
if (this.isActionColumnKey(header.key)) {
return '';
}
var raw = this.resolveFieldValue(item, header.key);
if (raw == null) return '';
return String(raw);
},
prepareExportMatrix: function () {
var rows = [];
var headerTitles = this.tableHeaders.map(function (h) {
return h.title || h.key;
});
var vm = this;
this.sortedItems.forEach(function (item, index) {
var line = vm.tableHeaders.map(function (h) {
return vm.cellStringForExport(h, item, index);
});
rows.push(line);
});
return { headerTitles: headerTitles, rows: rows };
},
exportEmitError: function (err) {
this.$emit('export-error', err instanceof Error ? err : new Error(String(err)));
},
exportToCSV: function () {
try {
var m = this.prepareExportMatrix();
var csvContent = [m.headerTitles]
.concat(m.rows)
.map(function (row) {
return row
.map(function (cell) {
return '"' + String(cell).replace(/"/g, '""') + '"';
})
.join(',');
})
.join('\n');
var blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
saveAs(blob, this.exportFileName + '.csv');
} catch (e) {
this.exportEmitError(e);
}
},
exportToExcel: function () {
try {
var m = this.prepareExportMatrix();
var aoa = [m.headerTitles].concat(m.rows);
var worksheet = XLSX.utils.aoa_to_sheet(aoa);
var workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
var buf = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
var blob = new Blob([buf], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
saveAs(blob, this.exportFileName + '.xlsx');
} catch (e) {
this.exportEmitError(e);
}
},
exportToPDF: function () {
try {
var m = this.prepareExportMatrix();
var colCount = m.headerTitles.length;
var useLandscape = colCount > 6;
var doc = new jsPDF(useLandscape ? 'l' : 'p', 'pt', 'a4');
var fontSize = 8;
if (colCount > 12) fontSize = 6;
else if (colCount > 8) fontSize = 7;
var cellPadding = colCount > 8 ? 4 : 8;
var tableOptions = {
head: [m.headerTitles],
body: m.rows,
styles: { fontSize: fontSize, cellPadding: cellPadding },
headStyles: { fillColor: [25, 118, 210] },
margin: { top: 20 }
};
if (colCount > 8) {
tableOptions.tableWidth = 'wrap';
tableOptions.horizontalPageBreak = true;
tableOptions.styles = Object.assign({}, tableOptions.styles, {
minCellWidth: 36,
overflow: 'linebreak'
});
}
autoTable(doc, tableOptions);
doc.save(this.exportFileName + '.pdf');
} catch (e) {
this.exportEmitError(e);
}
},
exportToJSON: function () {
try {
var vm = this;
var jsonData = this.sortedItems.map(function (item, index) {
var obj = {};
vm.tableHeaders.forEach(function (h) {
var title = h.title || h.key;
if (vm.isActionColumnKey(h.key)) return;
if (h.key === '__adt_index') {
obj[title] = index + 1;
return;
}
if (typeof h.exportValue === 'function') {
obj[title] = h.exportValue(item, index);
return;
}
var keys = String(h.key).split('.');
var value = item;
for (var i = 0; i < keys.length; i++) {
if (value && typeof value === 'object' && keys[i] in value) {
value = value[keys[i]];
} else {
value = null;
break;
}
}
obj[title] = value;
});
return obj;
});
var jsonContent = JSON.stringify(jsonData, null, 2);
var blob = new Blob([jsonContent], { type: 'application/json' });
saveAs(blob, this.exportFileName + '.json');
} catch (e) {
this.exportEmitError(e);
}
}
}
};
</script>
<style scoped>
.admin-data-table__toolbar {
margin-bottom: 12px;
}
.admin-data-table {
--adt-header-bg: #1976d2;
--adt-header-color: #fff;
--adt-radius: 12px;
--adt-border: 1px solid #e0e0e0;
--adt-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
--adt-row-hover: #e3f2fd;
--adt-footer-bg: #fafafa;
--adt-text-muted: #757575;
--adt-font-size: 16px;
--adt-header-font-size: 16px;
}
.admin-data-table__shell {
border-radius: var(--adt-radius);
border: var(--adt-border);
box-shadow: var(--adt-shadow);
overflow: hidden;
background: #fff;
}
.admin-data-table__scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.admin-data-table__table {
width: 100%;
margin-bottom: 0;
border-collapse: separate;
border-spacing: 0;
}
.admin-data-table__table thead tr:first-child th:first-child {
border-top-left-radius: var(--adt-radius);
}
.admin-data-table__table thead tr:first-child th:last-child {
border-top-right-radius: var(--adt-radius);
}
.admin-data-table__table thead th {
background-color: var(--adt-header-bg);
color: var(--adt-header-color);
font-weight: 600;
font-size: var(--adt-header-font-size);
text-transform: none;
letter-spacing: 0.02em;
border: none !important;
padding: 14px 16px;
vertical-align: middle;
}
.admin-data-table__table thead th.is-sortable {
cursor: pointer;
user-select: none;
}
.admin-data-table__table thead th.is-sortable:hover {
filter: brightness(1.06);
}
.admin-data-table__table thead th.admin-data-table__col-index,
.admin-data-table__table tbody td.admin-data-table__col-index {
width: 3.25rem;
max-width: 4rem;
text-align: center;
font-variant-numeric: tabular-nums;
vertical-align: middle !important;
}
.admin-data-table__th-inner {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
min-height: 20px;
}
.admin-data-table__th-title {
flex: 1;
min-width: 0;
}
.admin-data-table__sort {
display: inline-flex;
align-items: center;
flex-shrink: 0;
opacity: 0.95;
}
.admin-data-table__sort-hint {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1;
opacity: 0.45;
margin-top: 1px;
}
.admin-data-table__sort-icon {
display: block;
flex-shrink: 0;
}
.admin-data-table__sort-icon--faint {
margin-top: -3px;
}
.admin-data-table__sort-hint .admin-data-table__sort-icon--faint:first-child {
margin-top: 0;
}
.admin-data-table__table tbody td {
padding: 12px 16px;
vertical-align: middle;
border-top: 1px solid #eee;
font-size: var(--adt-font-size);
}
.admin-data-table__table tbody tr:first-child:not(.admin-data-table__state-row) td {
border-top: 1px solid #e0e0e0;
}
.admin-data-table__table tbody tr.admin-data-table__state-row td {
border-top: none;
padding: 0;
}
.admin-data-table__table tbody tr:not(.admin-data-table__state-row):hover td {
background-color: var(--adt-row-hover);
}
.admin-data-table__state {
text-align: center;
padding: 40px 24px;
color: var(--adt-text-muted);
}
.admin-data-table__state-icon {
font-size: 40px;
opacity: 0.45;
margin-bottom: 12px;
display: block;
margin-left: auto;
margin-right: auto;
}
.admin-data-table__state-title {
font-size: 16px;
font-weight: 600;
color: #424242;
margin-bottom: 4px;
}
.admin-data-table__state-hint {
font-size: 13px;
color: var(--adt-text-muted);
}
.admin-data-table__footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px 16px;
padding: 12px 16px;
border-top: var(--adt-border);
background: var(--adt-footer-bg);
}
.admin-data-table__footer-left {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.admin-data-table__footer-label {
margin: 0;
font-size: 12px;
font-weight: normal;
color: var(--adt-text-muted);
}
.admin-data-table__page-size {
width: auto;
min-width: 64px;
display: inline-block;
height: 30px;
padding: 4px 8px;
font-size: 12px;
}
.admin-data-table__footer-mid {
font-size: 13px;
font-weight: 500;
color: #616161;
}
.admin-data-table__footer-right {
flex-shrink: 0;
}
.admin-data-table__pagination {
margin: 0;
}
.admin-data-table__pagination>li>a {
padding: 5px 12px;
font-size: 12px;
color: var(--adt-header-bg);
}
.admin-data-table__pagination>li.disabled>a {
color: #bbb;
pointer-events: none;
cursor: default;
}
</style>