Files
2026-05-16 13:44:12 -04:00

276 lines
11 KiB
PHP
Executable File

<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center my-3">
<h2 class="mb-0">Inventory Movements</h2>
<div class="d-flex gap-2">
<!-- Top bulk delete submits the master form by id -->
<button id="bulkDeleteBtnTop" type="submit" form="bulkDeleteForm" class="btn btn-danger" disabled>
<i class="bi bi-trash"></i> Delete Selected
</button>
<a href="<?= site_url('inventory/movements/create') ?>" class="btn btn-success">
<i class="bi bi-plus-circle"></i> New Movement
</a>
</div>
</div>
<div class="mb-2"><?= $this->include('partials/academic_filter') ?></div>
<?php if (session()->getFlashdata('status')): ?>
<div class="alert alert-<?= session('status') === 'success' ? 'success' : 'danger' ?>">
<?= esc(session('message')) ?>
</div>
<?php endif; ?>
<div class="table-responsive">
<!-- Single master form holds the selected ids[] for bulk delete -->
<form id="bulkDeleteForm" action="<?= site_url('inventory/movements/bulk-delete') ?>" method="post">
<?= csrf_field() ?>
<table id="movementsTable" class="table table-striped table-hover align-middle" data-no-mgmt-sticky="1">
<thead class="table-dark">
<tr>
<th style="width:36px">
<input type="checkbox" id="checkAll" title="Select all on this page">
</th>
<th>ID</th>
<th>Item</th>
<th>Qty Change</th>
<th>Movement Type</th>
<th>Reason</th>
<th>Semester</th>
<th>School Year</th>
<th>Teacher</th>
<th>Student</th>
<th>Class Section</th>
<th>Performed By</th>
<th>Created At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (!empty($movements)): ?>
<?php foreach ($movements as $m): ?>
<tr>
<td>
<input type="checkbox" class="row-check" name="ids[]" value="<?= esc($m['id']) ?>">
</td>
<td><?= esc($m['id']) ?></td>
<td><?= esc($m['item_name'] ?? $m['item_id']) ?></td>
<td><?= esc($m['qty_change']) ?></td>
<td><span class="badge bg-info"><?= esc($m['movement_type']) ?></span></td>
<td><?= esc($m['reason']) ?></td>
<td><?= esc($m['semester']) ?></td>
<td><?= esc($m['school_year']) ?></td>
<td><?= esc($m['teacher_name'] ?? $m['teacher_id']) ?></td>
<td><?= esc($m['student_name'] ?? $m['student_id']) ?></td>
<td><?= esc($m['class_section_name'] ?? $m['class_section_id']) ?></td>
<td><?= esc($m['performed_by_name'] ?? $m['performed_by']) ?></td>
<td><?= esc(!empty($m['created_at']) ? local_datetime($m['created_at'], 'm-d-Y H:i') : '') ?></td>
<td class="text-nowrap">
<a href="<?= site_url('inventory/movements/edit/'.$m['id']) ?>" class="btn btn-sm btn-primary">
<i class="bi bi-pencil"></i> Edit
</a>
<!-- Single delete button (no nested form). JS will create & submit a POST form with CSRF -->
<button type="button"
class="btn btn-sm btn-danger btn-delete-one"
data-action="<?= site_url('inventory/movements/delete/'.$m['id']) ?>">
<i class="bi bi-trash"></i> Delete
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<div class="mt-2">
<button id="bulkDeleteBtnBottom" type="submit" class="btn btn-danger" disabled>
<i class="bi bi-trash"></i> Delete Selected
</button>
</div>
</form>
</div>
</div>
<script>
$(function(){
const table = $('#movementsTable').DataTable({
pageLength: 100,
order: [[1, 'desc']], // 0=checkbox, 1=ID
columnDefs: [
{ orderable: false, targets: [0, 13] } // checkbox + actions
]
});
const $checkAll = $('#checkAll');
const $bulkTop = $('#bulkDeleteBtnTop');
const $bulkBot = $('#bulkDeleteBtnBottom');
function selectionCount() { return $('#movementsTable tbody .row-check:checked').length; }
function syncBulkButtons() {
const n = selectionCount();
$bulkTop.prop('disabled', n === 0);
$bulkBot.prop('disabled', n === 0);
}
// Individual row checks
$(document).on('change', '.row-check', syncBulkButtons);
// Header "select all" (only visible rows on current page)
$checkAll.on('change', function() {
const checked = this.checked;
$('#movementsTable tbody input.row-check').prop('checked', checked);
syncBulkButtons();
});
// Keep header checkbox in sync after redraws
table.on('draw', function(){
const $visible = $('#movementsTable tbody input.row-check');
const allChecked = $visible.length && $visible.filter(':checked').length === $visible.length;
$checkAll.prop('checked', allChecked && $visible.length > 0);
syncBulkButtons();
});
// Confirm for bulk delete (both top and bottom submit the same #bulkDeleteForm)
$('#bulkDeleteForm').on('submit', function(e){
const n = selectionCount();
if (n === 0) { e.preventDefault(); return false; }
if (!confirm(`Delete ${n} selected movement(s)?`)) { e.preventDefault(); return false; }
});
// ---- Single delete (avoid nested forms) ----
const csrfName = '<?= esc(csrf_token()) ?>';
const CSRF_COOKIE_NAME = <?= json_encode(config('Security')->csrfCookieName ?? (config('Security')->cookieName ?? 'csrf_cookie_name')) ?>;
function getCookie(name){
return document.cookie.split(';').map(v=>v.trim()).find(v=>v.startsWith(name+'='))?.split('=')[1] || '';
}
$(document).on('click', '.btn-delete-one', async function(e){
e.preventDefault();
const url = $(this).data('action');
if (!url) return;
if (!confirm('Delete this movement?')) return;
const token = decodeURIComponent(getCookie(CSRF_COOKIE_NAME) || '<?= esc(csrf_hash()) ?>');
// Try fast path: POST via fetch, then reload regardless of redirect
try {
const body = new URLSearchParams();
body.append(csrfName, token);
const resp = await fetch(url, {
method: 'POST',
body,
credentials: 'same-origin',
headers: { 'Accept': 'text/html,application/json', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' }
});
// If the server responded (200/302/etc), just reload to reflect changes
if (resp.ok || resp.status === 302 || resp.status === 303 || resp.status === 301) {
window.location.reload();
return;
}
} catch (_) {
// fall through to form submit
}
// Fallback: create a one-off form and submit (POST with latest CSRF)
const form = document.createElement('form');
form.method = 'post';
form.action = url;
const input = document.createElement('input');
input.type = 'hidden';
input.name = csrfName;
input.value = token;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
});
// Keep bulk delete form's CSRF fresh just before submit
$('#bulkDeleteForm').on('submit', function(){
try {
const fresh = decodeURIComponent(getCookie(CSRF_COOKIE_NAME) || '');
if (fresh) {
const $hidden = $(this).find('input[name="' + csrfName + '"]');
if ($hidden.length) $hidden.val(fresh);
}
} catch(_) {}
});
});
</script>
<script>
// jQuery-free fallback: enables bulk delete buttons and single delete
(function(){
if (window.jQuery) return; // jQuery path already binds everything
document.addEventListener('DOMContentLoaded', function(){
function qsa(sel, root){ return Array.prototype.slice.call((root||document).querySelectorAll(sel)); }
function selCount(){ return qsa('#movementsTable tbody .row-check:checked').length; }
function sync(){
var n = selCount();
var top = document.getElementById('bulkDeleteBtnTop');
var bot = document.getElementById('bulkDeleteBtnBottom');
if (top) top.disabled = (n === 0);
if (bot) bot.disabled = (n === 0);
}
// Individual row checks
document.addEventListener('change', function(e){
if (e.target && e.target.classList && e.target.classList.contains('row-check')) sync();
});
// Header select all
var checkAll = document.getElementById('checkAll');
if (checkAll) {
checkAll.addEventListener('change', function(){
var on = !!this.checked;
qsa('#movementsTable tbody input.row-check').forEach(function(el){ el.checked = on; });
sync();
});
}
// Confirm bulk delete
var bulkForm = document.getElementById('bulkDeleteForm');
if (bulkForm) {
bulkForm.addEventListener('submit', function(e){
var n = selCount();
if (n === 0) { e.preventDefault(); return; }
if (!confirm('Delete ' + n + ' selected movement(s)?')) { e.preventDefault(); }
});
}
// Single delete via fetch fallback
(function(){
var TOKEN_NAME = '<?= esc(csrf_token()) ?>';
var COOKIE_NAME = <?= json_encode(config('Security')->csrfCookieName ?? (config('Security')->cookieName ?? 'csrf_cookie_name')) ?>;
function getCookie(n){
try { return document.cookie.split(';').map(function(v){return v.trim();}).find(function(v){return v.indexOf(n+'=')===0;})?.split('=')[1] || ''; } catch(_) { return ''; }
}
document.addEventListener('click', function(e){
var btn = e.target && e.target.closest ? e.target.closest('.btn-delete-one') : null;
if (!btn) return;
e.preventDefault();
var url = btn.getAttribute('data-action');
if (!url) return;
if (!confirm('Delete this movement?')) return;
var token = decodeURIComponent(getCookie(COOKIE_NAME) || '<?= esc(csrf_hash()) ?>');
var body = new URLSearchParams();
body.append(TOKEN_NAME, token);
fetch(url, { method:'POST', body, credentials:'same-origin', headers:{ 'Accept':'text/html,application/json', 'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With':'XMLHttpRequest' } })
.then(function(){ location.reload(); })
.catch(function(){
var form = document.createElement('form');
form.method = 'post'; form.action = url;
var inp = document.createElement('input');
inp.type = 'hidden'; inp.name = TOKEN_NAME; inp.value = token;
form.appendChild(inp); document.body.appendChild(form); form.submit();
});
});
})();
// Initial state
sync();
});
})();
</script>
<?= $this->endSection() ?>