Files
wms-app/app/asset/manage_asset.php
T
Thanakorn d199fa7f67 Fix transfer quantity, unify date format, align demo seeds
Stock Transfer list showed 0.00 (read in instead of out); stock-out/transfer forms show the location quantity; dates display as YYYY-MM-DD HH:mm:ss. Demo seeds map product accounts, use product names and supplier batches, and add demo_seed_assets.php.
2026-09-15 12:43:47 +07:00

596 lines
28 KiB
PHP

<?php
session_start();
require '../config.php';
require '../include_header.php';
$asset_id = (int)($_GET['id'] ?? 0);
$invoice_id = (int)($_GET['invoice_id'] ?? 0);
$item_id = (int)($_GET['item_id'] ?? 0);
$pi_link_ok = in_array($_SESSION['login_app_access'] ?? 'wms', ['all', 'accounting'], true);
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_asset.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center gap-3">
<div>
<h1 class="fs-3 mb-1" id="page_title">New Asset</h1>
<p class="mb-0 text-muted" id="page_subtitle">Straight-line depreciation with your own salvage value</p>
</div>
<a href="<?php echo $server_url?>asset/index.php" class="btn btn-light">
<i class="ti ti-arrow-left me-1"></i>Back
</a>
</div>
</div>
</div>
<div class="row g-5">
<!-- Left: form + schedule -->
<div class="col-lg-8">
<div class="alert alert-info small d-none" id="source_alert"></div>
<div class="card mb-5" id="assetForm">
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Asset Details</h2>
<input type="hidden" id="id" value="<?php echo $asset_id ?: ''; ?>">
<div class="row g-4">
<div class="col-md-8">
<label class="form-label">Asset Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="asset_name" required>
</div>
<div class="col-md-4">
<label class="form-label">Department</label>
<select class="form-select" id="department_id"></select>
</div>
<div class="col-md-6">
<label class="form-label">Category <span class="text-danger">*</span></label>
<select class="form-select" id="category_id" required></select>
<div class="form-text" id="class_hint"></div>
</div>
<div class="col-md-6">
<label class="form-label">Clearing Account</label>
<select class="form-select" id="clearing_account_code"></select>
<div class="form-text">Credited when the asset is capitalized — the account the purchase was posted to. Leave empty if the asset is already on the books.</div>
</div>
<div class="col-md-3">
<label class="form-label">Acquisition Date <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="acquisition_date" placeholder="DD/MM/YYYY" required>
</div>
<div class="col-md-3">
<label class="form-label">In-Service Date <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="in_service_date" placeholder="DD/MM/YYYY" required>
</div>
<div class="col-md-6"></div>
<div class="col-md-4">
<label class="form-label">Cost <span class="text-danger">*</span></label>
<input type="number" class="form-control text-end" id="cost" min="0" step="0.01" required>
</div>
<div class="col-md-4">
<label class="form-label">Salvage Value</label>
<input type="number" class="form-control text-end" id="salvage_value" min="0" step="0.01" value="0">
</div>
<div class="col-md-4">
<label class="form-label">Useful Life (months) <span class="text-danger">*</span></label>
<input type="number" class="form-control text-end" id="useful_life_months" min="1" max="1200" step="1" required>
</div>
<div class="col-12">
<div class="bg-light rounded-2 px-4 py-3 d-flex flex-wrap gap-4 small">
<div><span class="text-muted">Amount to <span class="label-charge">depreciate</span>:</span> <strong id="calc_base">—</strong></div>
<div><span class="text-muted">Per month:</span> <strong id="calc_monthly">—</strong></div>
<div><span class="text-muted">Last month:</span> <strong id="calc_last">—</strong></div>
</div>
</div>
<div class="col-12">
<label class="form-label">Description</label>
<textarea class="form-control" id="description" rows="2" placeholder="Serial number, location, notes"></textarea>
</div>
</div>
<div class="form-text mt-3 d-none" id="locked_hint">
<i class="ti ti-lock me-1"></i>Cost, salvage value, life, dates and category are locked because months have already been charged.
</div>
</div>
</div>
<div class="card d-none" id="schedule_card">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fs-5 mb-0"><span class="label-schedule">Depreciation</span> Schedule</h2>
<span class="small text-muted" id="schedule_summary"></span>
</div>
<div class="table-responsive" style="max-height:480px;">
<table class="table table-sm mb-0 table-centered">
<thead class="table-light" style="position:sticky; top:0;">
<tr>
<th>#</th>
<th>Month</th>
<th class="text-end">Amount</th>
<th class="text-end">Accumulated</th>
<th class="text-end">Book Value</th>
<th>State</th>
</tr>
</thead>
<tbody id="schedule_tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Right: status + actions -->
<div class="col-lg-4">
<div class="card mb-5 d-none" id="status_card">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">Status</h2>
<div class="mb-3 d-flex gap-2 flex-wrap" id="status_badges"></div>
<div class="small mb-2"><span class="text-muted">Accumulated:</span> <span class="fw-semibold ms-1" id="display_accumulated">—</span></div>
<div class="small mb-2"><span class="text-muted">Book value:</span> <span class="fw-semibold ms-1" id="display_book_value">—</span></div>
<div class="small mb-2"><span class="text-muted">Last month charged:</span> <span class="ms-1" id="display_last_period">—</span></div>
<div class="small mb-2 d-none" id="source_row"><span class="text-muted">Source:</span> <span class="ms-1" id="display_source"></span></div>
<hr>
<h3 class="fs-6 mb-2">GL Entries <span class="badge bg-success text-white ms-1">[Asset Mgmt]</span></h3>
<div class="small mb-2"><div class="text-muted">Capitalization</div><div id="gl_capitalization"></div></div>
<div class="small mb-2 d-none" id="gl_disposal_row"><div class="text-muted">Disposal</div><div id="gl_disposal"></div></div>
<div class="d-none" id="disposal_info">
<hr>
<h3 class="fs-6 mb-2">Disposal</h3>
<div class="small mb-1"><span class="text-muted">Date:</span> <span class="ms-1" id="display_disposal_date"></span></div>
<div class="small mb-1"><span class="text-muted">Sale price:</span> <span class="ms-1" id="display_sale_price"></span></div>
<div class="small mb-1"><span class="text-muted">Gain / (loss):</span> <span class="ms-1 fw-semibold" id="display_gain_loss"></span></div>
<div class="small mb-1 text-muted" id="display_disposal_notes"></div>
</div>
</div>
</div>
<div class="card">
<div class="card-body p-4 d-flex flex-column gap-2">
<button class="btn btn-primary w-100" id="btn_save" onclick="save_asset()">
<i class="ti ti-device-floppy me-1"></i><span>Save as Draft</span>
</button>
<button class="btn btn-success w-100 d-none" id="btn_activate" onclick="asset_action('activate')">
<i class="ti ti-circle-check me-1"></i>Activate
</button>
<button class="btn btn-outline-dark w-100 d-none" id="btn_dispose" onclick="open_disposal()">
<i class="ti ti-receipt-refund me-1"></i>Sell / Dispose
</button>
<button class="btn btn-outline-warning w-100 d-none" id="btn_undo_disposal" onclick="asset_action('undo_disposal')">
<i class="ti ti-arrow-back-up me-1"></i>Undo Disposal
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_void" onclick="asset_action('void')">
<i class="ti ti-ban me-1"></i>Void
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="asset_action('delete')">
<i class="ti ti-trash me-1"></i>Delete Draft
</button>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Disposal modal -->
<div class="modal fade" id="disposal_modal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Sell / Dispose of Asset</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Disposal Date</label>
<input type="text" class="form-control" id="disposal_date" placeholder="DD/MM/YYYY">
</div>
<div class="col-md-6">
<label class="form-label">Sale Price</label>
<input type="number" class="form-control text-end" id="sale_price" min="0" step="0.01" value="0">
</div>
<div class="col-12">
<label class="form-label">Proceeds Account</label>
<select class="form-select" id="proceeds_account_code"></select>
<div class="form-text">Cash, bank or receivable that received the money. Not needed for a write-off (price 0).</div>
</div>
<div class="col-12">
<label class="form-label">Notes</label>
<textarea class="form-control" id="disposal_notes" rows="2" placeholder="Buyer, reason"></textarea>
</div>
<div class="col-12">
<div class="bg-light rounded-2 px-3 py-2 small">
<div class="d-flex justify-content-between"><span class="text-muted">Cost</span><span id="dp_cost"></span></div>
<div class="d-flex justify-content-between"><span class="text-muted">Accumulated (months charged so far)</span><span id="dp_accumulated"></span></div>
<div class="d-flex justify-content-between"><span class="text-muted">Book value</span><span id="dp_book"></span></div>
<div class="d-flex justify-content-between fw-semibold"><span id="dp_result_label">Gain / (loss)</span><span id="dp_result"></span></div>
</div>
<div class="form-text">Every month before the disposal month must already be charged. The disposal month itself is not charged.</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Back</button>
<button class="btn btn-dark" onclick="submit_disposal()"><i class="ti ti-receipt-refund me-1"></i>Record Disposal</button>
</div>
</div>
</div>
</div>
<?php require '../include_ending.php'; ?>
<script src="<?php echo $server_url?>assets/js/asset.js"></script>
<script>
var asset_id = <?php echo $asset_id; ?>;
var invoice_id = <?php echo $invoice_id; ?>;
var item_id = <?php echo $item_id; ?>;
var pi_link_ok = <?php echo $pi_link_ok ? 'true' : 'false'; ?>;
var can_admin = ['owner', 'admin'].includes(user_role);
var can_edit = ['owner', 'admin', 'staff'].includes(user_role);
var categories = [];
var accounts = [];
var detail = null;
var invoice_line = null;
var life_touched = false;
var FINANCIAL_FIELDS = '#category_id, #acquisition_date, #in_service_date, #cost, #salvage_value, #useful_life_months, #clearing_account_code';
// ── lookups ────────────────────────────────────────────────────────────────
function load_categories() {
return ajax_request({
url: server_url + 'asset/api/engine/category.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
noLoading: true,
onSuccess: function(res) { categories = res.output || []; }
});
}
function category_by_id(id) {
return categories.find(function(c) { return String(c.id) === String(id); }) || null;
}
function fill_category_select(selected) {
var html = '<option value="">— Select category —</option>';
categories.forEach(function(c) {
if (String(c.status) !== '1' && String(c.id) !== String(selected)) return;
html += '<option value="' + c.id + '">' + escape_html(c.category_code + ' — ' + c.category_name)
+ (c.asset_class === 'intangible' ? ' (intangible)' : '') + '</option>';
});
$('#category_id').html(html).val(selected ? String(selected) : '');
}
function update_class_hint() {
var c = category_by_id($('#category_id').val());
var intangible = c && c.asset_class === 'intangible';
$('#class_hint').html(c ? asset_class_badge(c.asset_class) : '');
$('.label-charge').text(intangible ? 'amortize' : 'depreciate');
$('.label-schedule').text(intangible ? 'Amortization' : 'Depreciation');
}
// ── live calculation ───────────────────────────────────────────────────────
function update_calc() {
var cost = parseFloat($('#cost').val()) || 0;
var salvage = parseFloat($('#salvage_value').val()) || 0;
var life = parseInt($('#useful_life_months').val(), 10) || 0;
var base = round_dp(cost - salvage, 2);
if (base <= 0 || life <= 0) {
$('#calc_base, #calc_monthly, #calc_last').text('—');
return;
}
var monthly = round_dp(base / life, 2);
var last = round_dp(base - monthly * (life - 1), 2);
$('#calc_base').text(format_number(base, 2));
$('#calc_monthly').text(format_number(monthly, 2));
$('#calc_last').text(life > 1 ? format_number(Math.max(last, 0), 2) : format_number(base, 2));
}
// ── load ───────────────────────────────────────────────────────────────────
function retrieve_asset() {
return ajax_request({
url: server_url + 'asset/api/engine/retrieve_asset.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { id: asset_id },
onSuccess: function(res) {
detail = res.output;
render_detail();
}
});
}
function load_invoice_line() {
return ajax_request({
url: server_url + 'asset/api/engine/invoice_lines.php',
autoPrepare: true,
checkRequired: 0,
action: 'line',
data: { invoice_id: invoice_id, item_id: item_id },
onSuccess: function(res) {
invoice_line = res.output;
var l = invoice_line;
$('#asset_name').val(l.product_name || l.product_sku);
$('#acquisition_date, #in_service_date').val(format_date_input(l.issued_date));
$('#cost').val(Math.max(parseFloat(l.remaining) || 0, 0).toFixed(2));
$('#clearing_account_code').html(account_options(accounts, [], l.clearing_account_code, '— None —'));
load_departments('department_id', l.department_id);
$('#source_alert').removeClass('d-none').html(
'<i class="ti ti-file-invoice me-1"></i>From purchase invoice <strong>' + escape_html(l.invoice_number) + '</strong>'
+ ' (' + escape_html(l.contact_name || '—') + '), line <strong>' + escape_html(l.product_sku) + '</strong>: '
+ 'amount ' + format_number(l.line_amount, 2) + ', already capitalized ' + format_number(l.capitalized, 2)
+ ', remaining <strong>' + format_number(l.remaining, 2) + '</strong>.'
+ (l.clearing_account_code ? '' : ' <span class="text-danger">Select the account the invoice line was posted to as the clearing account.</span>')
);
$('#page_subtitle').text('Capitalizing a purchase invoice line');
update_calc();
}
});
}
function render_detail() {
var a = detail.asset;
var status = parseInt(a.status, 10);
var recorded = detail.schedule.some(function(r) { return r.state === 'recorded'; });
var closed = status === 3 || status === 4;
$('#page_title').text(a.asset_number + ' · ' + a.asset_name);
$('#page_subtitle').text(a.category_code + ' ' + a.category_name);
$('#id').val(a.id);
$('#asset_name').val(a.asset_name);
fill_category_select(a.category_id);
update_class_hint();
$('#clearing_account_code').html(account_options(accounts, [], a.clearing_account_code, '— None —'));
$('#acquisition_date').val(format_date_input(a.acquisition_date));
$('#in_service_date').val(format_date_input(a.in_service_date));
$('#cost').val(parseFloat(a.cost).toFixed(2));
$('#salvage_value').val(parseFloat(a.salvage_value).toFixed(2));
$('#useful_life_months').val(a.useful_life_months);
$('#description').val(a.description || '');
load_departments('department_id', a.department_id);
life_touched = true;
update_calc();
// Status card
$('#status_card').removeClass('d-none');
$('#status_badges').html(asset_status_badge(a.status, a.asset_class) + asset_class_badge(a.asset_class));
$('#display_accumulated').text(format_number(a.accumulated, 2));
$('#display_book_value').text(format_number(a.book_value, 2));
$('#display_last_period').text(a.last_period || '—');
if (parseInt(a.invoice_id, 10) > 0) {
var ref = escape_html(a.invoice_number || ('PI #' + a.invoice_id));
$('#display_source').html(pi_link_ok
? '<a href="' + server_url + 'expense/manage_purchase_invoice.php?id=' + a.invoice_id + '">' + ref + '</a>'
: ref);
$('#source_row').removeClass('d-none');
}
// GL entries
var cap_html;
if (!a.clearing_account_code) {
cap_html = '<span class="text-muted">No entry — no clearing account.</span>';
} else if (status === 0) {
cap_html = '<span class="text-muted">Activate the asset, then Accounting posts it.</span>';
} else {
cap_html = gl_entry_html(detail.gl.capitalization);
}
$('#gl_capitalization').html(cap_html);
$('#gl_disposal_row').toggleClass('d-none', status !== 3);
$('#gl_disposal').html(gl_entry_html(detail.gl.disposal));
// Disposal
if (status === 3) {
var gain = round_dp(parseFloat(a.sale_price) - (parseFloat(a.cost) - parseFloat(a.accumulated)), 2);
$('#display_disposal_date').text(format_date(a.disposal_date));
$('#display_sale_price').text(format_number(a.sale_price, 2));
$('#display_gain_loss').text(gain < 0 ? '(' + format_number(-gain, 2) + ')' : format_number(gain, 2))
.toggleClass('text-danger', gain < 0).toggleClass('text-success', gain > 0);
$('#display_disposal_notes').text(a.disposal_notes || '');
$('#disposal_info').removeClass('d-none');
} else {
$('#disposal_info').addClass('d-none');
}
render_schedule();
// Editing rules
$(FINANCIAL_FIELDS).prop('disabled', closed || recorded || !can_edit);
$('#asset_name, #description, #department_id').prop('disabled', closed || !can_edit);
$('#locked_hint').toggleClass('d-none', !(recorded && !closed));
$('#btn_save').toggleClass('d-none', closed || !can_edit).find('span').text(status === 0 ? 'Save Draft' : 'Save');
$('#btn_activate').toggleClass('d-none', !(status === 0 && can_admin));
$('#btn_dispose').toggleClass('d-none', !((status === 1 || status === 2) && can_admin));
$('#btn_undo_disposal').toggleClass('d-none', !(status === 3 && can_admin));
$('#btn_void').toggleClass('d-none', !((status === 1 || status === 2) && can_admin && !recorded));
$('#btn_delete').toggleClass('d-none', !(status === 0 && can_admin));
}
function gl_entry_html(entry) {
if (entry) {
return '<span class="badge bg-success-subtle text-success">Posted</span>'
+ ' <span class="text-muted ms-1">' + escape_html(entry.reference) + ' · ' + format_date(entry.journal_date) + '</span>';
}
return '<span class="badge bg-danger-subtle text-danger">Not posted</span>'
+ ' <span class="text-muted ms-1">Accounting → Batch GL Entries</span>';
}
function render_schedule() {
var rows = detail.schedule || [];
if (!rows.length) { $('#schedule_card').addClass('d-none'); return; }
var state_badge = {
recorded: function(r) { return '<span class="badge bg-success-subtle text-success">Charged</span> <span class="small text-muted">' + escape_html(r.run_number || '') + '</span>'; },
projected: function() { return '<span class="badge bg-light text-dark">Projected</span>'; },
stopped: function() { return '<span class="badge bg-secondary-subtle text-secondary">Not charged</span>'; }
};
var html = '';
var charged = 0;
rows.forEach(function(r, i) {
if (r.state === 'recorded') charged++;
html += '<tr class="' + (r.state === 'stopped' ? 'text-muted' : '') + '">'
+ '<td>' + (i + 1) + '</td>'
+ '<td>' + escape_html(r.period) + '</td>'
+ '<td class="text-end">' + format_number(r.amount, 2) + '</td>'
+ '<td class="text-end">' + format_number(r.accumulated, 2) + '</td>'
+ '<td class="text-end">' + format_number(r.book_value, 2) + '</td>'
+ '<td>' + state_badge[r.state](r) + '</td>'
+ '</tr>';
});
$('#schedule_tbody').html(html);
$('#schedule_summary').text(charged + ' of ' + rows.length + ' months charged · ' + format_number(detail.monthly_amount, 2) + ' per month');
$('#schedule_card').removeClass('d-none');
}
// ── actions ────────────────────────────────────────────────────────────────
function save_asset() {
return ajax_request({
url: server_url + 'asset/api/engine/manage_asset.php',
autoPrepare: true,
checkRequired: 1,
action: 'manage',
data: { id: asset_id || '', invoice_id: invoice_id, invoice_item_id: item_id },
onSuccess: function(res) {
var saved_id = res.output && res.output.id;
if (!asset_id && saved_id) {
window.location.href = server_url + 'asset/manage_asset.php?id=' + saved_id;
return;
}
toastr.success(res.message || 'Saved.');
retrieve_asset();
}
});
}
var ACTION_CONFIRM = {
activate: ['Activate this asset? It will start appearing in depreciation runs from its in-service month.', 'Activate', 'btn-success'],
undo_disposal: ['Undo the disposal? If its GL entry was posted, a reversal entry is created.', 'Undo Disposal', 'btn-warning'],
void: ['<strong>Void this asset?</strong><br>If its capitalization was posted, a reversal entry is created. This cannot be undone.', 'Void', 'btn-danger'],
delete: ['Delete this draft asset?', 'Delete', 'btn-danger']
};
function asset_action(action) {
var c = ACTION_CONFIRM[action];
bootbox.confirm({
message: c[0],
buttons: { confirm: { label: c[1], className: c[2] }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(ok) {
if (!ok) return;
ajax_request({
url: server_url + 'asset/api/engine/manage_asset.php',
autoPrepare: false,
action: action,
data: { json: JSON.stringify({
otp: document.getElementById('session-context').dataset.otp,
company_id: company_id,
action: action,
id: asset_id
}) },
onSuccess: function(res) {
if (action === 'delete') { window.location.href = server_url + 'asset/index.php'; return; }
toastr.success(res.message || 'Done.');
retrieve_asset();
}
});
}
});
}
function open_disposal() {
var a = detail.asset;
$('#proceeds_account_code').html(account_options(accounts, ['asset'], '', '— None (write-off) —'));
$('#sale_price').val('0');
$('#disposal_notes').val('');
$('#dp_cost').text(format_number(a.cost, 2));
$('#dp_accumulated').text(format_number(a.accumulated, 2));
$('#dp_book').text(format_number(a.book_value, 2));
update_disposal_result();
$('#disposal_modal').modal('show');
}
function update_disposal_result() {
if (!detail) return;
var gain = round_dp((parseFloat($('#sale_price').val()) || 0) - parseFloat(detail.asset.book_value), 2);
$('#dp_result_label').text(gain < 0 ? 'Loss' : 'Gain');
$('#dp_result').text(format_number(Math.abs(gain), 2)).toggleClass('text-danger', gain < 0).toggleClass('text-success', gain > 0);
}
function submit_disposal() {
ajax_request({
url: server_url + 'asset/api/engine/manage_asset.php',
autoPrepare: false,
action: 'dispose',
data: { json: JSON.stringify({
otp: document.getElementById('session-context').dataset.otp,
company_id: company_id,
action: 'dispose',
id: asset_id,
disposal_date: $('#disposal_date').val(),
sale_price: $('#sale_price').val(),
proceeds_account_code: $('#proceeds_account_code').val(),
disposal_notes: $('#disposal_notes').val()
}) },
onSuccess: function(res) {
$('#disposal_modal').modal('hide');
toastr.success(res.message || 'Disposal recorded.');
retrieve_asset();
}
});
}
// ── events ─────────────────────────────────────────────────────────────────
$('#cost, #salvage_value, #useful_life_months').on('input', update_calc);
$('#useful_life_months').on('input', function() { life_touched = true; });
$('#category_id').on('change', function() {
var c = category_by_id($(this).val());
if (c && !life_touched) {
$('#useful_life_months').val(c.useful_life_months);
update_calc();
}
update_class_hint();
});
$('#sale_price').on('input', update_disposal_result);
$(async function() {
flatpickr('#acquisition_date, #in_service_date, #disposal_date', { dateFormat: 'd/m/Y', allowInput: true });
try {
await load_categories();
await load_posting_accounts(function(rows) { accounts = rows; });
fill_category_select('');
$('#clearing_account_code').html(account_options(accounts, [], '', '— None —'));
if (asset_id) {
await retrieve_asset();
} else {
load_departments('department_id', 0);
if (invoice_id && item_id) await load_invoice_line();
if (!can_edit) $('#btn_save').addClass('d-none');
}
} catch (e) { console.log(e); }
});
</script>
</body>
</html>