add refund logic and fix books inventory logic
This commit is contained in:
@@ -121,7 +121,6 @@ $selectedClasses = array_values(array_intersect(range(1,13), $selectedClasses));
|
||||
<label class="form-label">Unit</label>
|
||||
<input class="form-control" name="unit" placeholder="pcs, set" value="<?= esc($item['unit'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">SKU</label>
|
||||
<input class="form-control" name="sku" value="<?= esc($item['sku'] ?? '') ?>">
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 px-3">
|
||||
<h3 class="mb-0">Books</h3>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-outline-primary" href="<?= site_url('inventory/book-prices') ?>">Book Prices</a>
|
||||
<a class="btn btn-primary" href="<?= site_url('inventory/create/book') ?>">Add Book</a>
|
||||
<button class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#addCategoryModal">Add Category</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= view('partials/flash_messages') ?>
|
||||
<div class="px-3 mb-2"><?= $this->include('partials/academic_filter') ?></div>
|
||||
|
||||
<?php
|
||||
// Helper to render a compact grade label from a category row
|
||||
@@ -50,7 +50,7 @@
|
||||
<th>Grade Range</th> <!-- NEW -->
|
||||
<th>Qty</th><th>Unit</th>
|
||||
<th>Updated By</th><th>Updated Date</th>
|
||||
<th>SKU</th><th style="width:110px">Actions</th>
|
||||
<th>SKU</th><th style="width:150px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -72,7 +72,7 @@
|
||||
<td><?= esc($i['updated_at'] ? local_datetime($i['updated_at'], 'm-d-Y H:i') : '—') ?></td>
|
||||
<td><?= esc($i['sku']) ?></td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-outline-primary" href="<?= site_url('inventory/edit/'.$i['id']) ?>">Edit</a>
|
||||
<a class="btn btn-sm btn-outline-primary" href="<?= site_url('inventory/edit/'.$i['id']) ?>">Edit Book</a>
|
||||
<form action="<?= site_url('inventory/delete/'.$i['id']) ?>" method="post" class="d-inline">
|
||||
<?= csrf_field() ?>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this item?')">Del</button>
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?php
|
||||
$categoryById = [];
|
||||
foreach (($categories ?? []) as $category) {
|
||||
$categoryById[(int) ($category['id'] ?? 0)] = $category;
|
||||
}
|
||||
|
||||
$gradeLabel = static function (?array $category): string {
|
||||
if (! $category) {
|
||||
return '-';
|
||||
}
|
||||
$gmin = $category['grade_min'] ?? null;
|
||||
$gmax = $category['grade_max'] ?? null;
|
||||
if ($gmin === null && $gmax === null) {
|
||||
return '-';
|
||||
}
|
||||
if ($gmin !== null && $gmax !== null) {
|
||||
return 'G' . (int) $gmin . '-G' . (int) $gmax;
|
||||
}
|
||||
if ($gmin !== null) {
|
||||
return 'G' . (int) $gmin . '+';
|
||||
}
|
||||
return '<=G' . (int) $gmax;
|
||||
};
|
||||
?>
|
||||
|
||||
<div class="container-fluid px-0 mt-3">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3 px-3">
|
||||
<div>
|
||||
<h3 class="mb-0">Book Prices</h3>
|
||||
<div class="text-muted small">School year: <?= esc($schoolYear ?? '') ?></div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="<?= site_url('inventory/book') ?>">Back to Books</a>
|
||||
</div>
|
||||
|
||||
<?= view('partials/flash_messages') ?>
|
||||
|
||||
<form method="post" action="<?= site_url('inventory/book-prices') ?>">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<input class="form-control form-control-sm" id="bookPriceSearch" type="search" placeholder="Search title, ISBN, SKU" style="width: 260px;">
|
||||
<select class="form-select form-select-sm" id="bookPriceCategoryFilter" style="width: 240px;">
|
||||
<option value="">All Categories</option>
|
||||
<?php foreach (($categories ?? []) as $category): ?>
|
||||
<option value="<?= (int) ($category['id'] ?? 0) ?>"><?= esc($category['name'] ?? '') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm">Save Prices</button>
|
||||
</div>
|
||||
<div class="card-body table-responsive">
|
||||
<table class="table table-striped align-middle mb-0 no-mgmt-sticky no-dt-fixedheader" data-no-mgmt-sticky data-no-dt-fixedheader id="bookPriceTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Book</th>
|
||||
<th>ISBN / Edition / SKU</th>
|
||||
<th>Category</th>
|
||||
<th>Grade Range</th>
|
||||
<th class="text-end">Qty</th>
|
||||
<th style="width: 170px;">Price</th>
|
||||
<th style="width: 130px;">Confirmed</th>
|
||||
<th style="width: 120px;">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach (($books ?? []) as $book): ?>
|
||||
<?php
|
||||
$bookId = (int) ($book['id'] ?? 0);
|
||||
$category = $categoryById[(int) ($book['category_id'] ?? 0)] ?? null;
|
||||
$priceCents = (int) ($book['charge_price_cents'] ?? 0);
|
||||
$isConfirmed = $priceCents > 0 && (int) ($book['price_confirmed'] ?? 0) === 1;
|
||||
$search = strtolower(trim(implode(' ', [
|
||||
$book['name'] ?? '',
|
||||
$book['isbn'] ?? '',
|
||||
$book['edition'] ?? '',
|
||||
$book['sku'] ?? '',
|
||||
$category['name'] ?? '',
|
||||
])));
|
||||
?>
|
||||
<tr data-category="<?= esc($book['category_id'] ?? '') ?>" data-search="<?= esc($search) ?>">
|
||||
<td>
|
||||
<div class="fw-semibold"><?= esc($book['name'] ?? '') ?></div>
|
||||
<div class="small text-muted">ID #<?= $bookId ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div><?= esc(($book['isbn'] ?? '') ?: '-') ?></div>
|
||||
<div class="small text-muted">
|
||||
<?= esc(($book['edition'] ?? '') ?: '-') ?> / <?= esc(($book['sku'] ?? '') ?: '-') ?>
|
||||
</div>
|
||||
</td>
|
||||
<td><?= esc($category['name'] ?? 'Uncategorized') ?></td>
|
||||
<td><?= esc($gradeLabel($category)) ?></td>
|
||||
<td class="text-end"><?= (int) ($book['quantity'] ?? 0) ?></td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">$</span>
|
||||
<input class="form-control" name="prices[<?= $bookId ?>]" inputmode="decimal" pattern="^\d+(\.\d{1,2})?$" value="<?= esc(number_format($priceCents / 100, 2, '.', '')) ?>">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="confirmed[<?= $bookId ?>]" value="1" id="confirmed<?= $bookId ?>" <?= $isConfirmed ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="confirmed<?= $bookId ?>">Confirm</label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($isConfirmed): ?>
|
||||
<span class="badge bg-success">Ready</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-warning text-dark">Needs price</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<button class="btn btn-primary">Save Prices</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const search = document.getElementById('bookPriceSearch');
|
||||
const category = document.getElementById('bookPriceCategoryFilter');
|
||||
const rows = Array.from(document.querySelectorAll('#bookPriceTable tbody tr'));
|
||||
|
||||
function applyFilters() {
|
||||
const term = (search?.value || '').trim().toLowerCase();
|
||||
const categoryId = category?.value || '';
|
||||
rows.forEach(row => {
|
||||
const matchesText = !term || (row.getAttribute('data-search') || '').includes(term);
|
||||
const matchesCategory = !categoryId || row.getAttribute('data-category') === categoryId;
|
||||
row.style.display = matchesText && matchesCategory ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
search?.addEventListener('input', applyFilters);
|
||||
category?.addEventListener('change', applyFilters);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<div class="container py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3"><div><h2 class="mb-1">Book Issue History</h2><div class="text-muted"><?= esc(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')) ?> · <?= esc($student['school_id'] ?? '') ?></div></div><a class="btn btn-outline-secondary" href="<?= previous_url() ?>">Back</a></div>
|
||||
<div class="alert alert-info">Issued books are not returnable. A reversed row means an administrator corrected an issue that was entered in error; it is not a parent return.</div>
|
||||
<div class="table-responsive"><table class="table table-striped align-middle"><thead><tr><th>Year</th><th>Book</th><th>Issued</th><th>Qty</th><th>Status</th><th>Issued by</th><th>Correction audit</th></tr></thead><tbody>
|
||||
<?php foreach ($issues as $issue): ?><tr><td><?= esc($issue['school_year'] ?? '') ?></td><td><?= esc($issue['book_name'] ?? '') ?><div class="small text-muted"><?= esc($issue['isbn'] ?? '') ?> <?= esc($issue['edition'] ?? '') ?></div></td><td><?= esc($issue['issued_at'] ?? '') ?></td><td><?= (int) ($issue['quantity'] ?? 0) ?></td><td><span class="badge bg-<?= ($issue['status'] ?? '') === 'issued' ? 'success' : 'secondary' ?>"><?= esc($issue['status'] ?? '') ?></span></td><td><?= esc(trim(($issue['issued_by_firstname'] ?? '') . ' ' . ($issue['issued_by_lastname'] ?? ''))) ?></td><td><?php if (($issue['status'] ?? '') === 'reversed'): ?><?= esc($issue['reversal_reason'] ?? '') ?><div class="small text-muted"><?= esc($issue['reversed_at'] ?? '') ?> by <?= esc(trim(($issue['reversed_by_firstname'] ?? '') . ' ' . ($issue['reversed_by_lastname'] ?? ''))) ?></div><?php else: ?>—<?php endif; ?></td></tr><?php endforeach; ?>
|
||||
<?php if (empty($issues)): ?><tr><td colspan="7" class="text-muted">No book issues found.</td></tr><?php endif; ?>
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -47,8 +47,13 @@
|
||||
<strong>School Year:</strong> <?= esc($schoolYear) ?>,
|
||||
<strong>Semester:</strong> <?= esc($semester) ?>
|
||||
</div>
|
||||
<div><strong>On Hand:</strong> <?= (int)$onHand ?></div>
|
||||
<div>
|
||||
<strong>On Hand:</strong> <?= (int)$onHand ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ((int)($priceConfirmed ?? 0) !== 1 || (int)($unitChargeCents ?? 0) <= 0): ?>
|
||||
<div class="mt-2 text-danger">This book cannot be distributed until its active-year charge price is confirmed.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Submission form (POST) -->
|
||||
@@ -57,6 +62,7 @@
|
||||
<!-- Keep class section hidden -->
|
||||
<input type="hidden" name="class_section_id" value="<?= esc($class_section_id) ?>">
|
||||
<input type="hidden" name="item_id" value="<?= esc($item_id) ?>">
|
||||
<input type="hidden" name="idempotency_key" value="<?= esc(hash('sha256', (string) microtime(true) . ':' . (string) random_int(1, PHP_INT_MAX))) ?>">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
@@ -96,7 +102,7 @@
|
||||
class="form-check-input student-box"
|
||||
name="student_ids[]"
|
||||
value="<?= esc($sid) ?>"
|
||||
<?= $given ? 'checked' : '' ?><?= $disabledAttr ?>>
|
||||
<?= $given ? 'checked disabled' : $disabledAttr ?>>
|
||||
</td>
|
||||
<td><?= esc($student['school_id'] ?? '-') ?></td>
|
||||
<td><?= esc($student['firstname'] ?? '') ?></td>
|
||||
@@ -123,7 +129,7 @@
|
||||
<textarea class="form-control" rows="2" name="note" placeholder="e.g., Distributed in class today"<?= $disabledAttr ?>></textarea>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-success"<?= $disabledAttr ?>>Deduct & Save</button>
|
||||
<button type="submit" class="btn btn-success"<?= $disabledAttr ?><?= ((int)($priceConfirmed ?? 0) !== 1 || (int)($unitChargeCents ?? 0) <= 0) ? ' disabled' : '' ?>>Deduct & Save</button>
|
||||
<button type="button" class="btn btn-secondary" id="clearSelectionBtn"<?= $disabledAttr ?>>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,34 +154,18 @@
|
||||
|
||||
document.getElementById('checkAllBtn')?.addEventListener('click', () => {
|
||||
if (distributeReadonly) return;
|
||||
document.querySelectorAll('.student-box').forEach(cb => cb.checked = true);
|
||||
});
|
||||
document.getElementById('uncheckAllBtn')?.addEventListener('click', () => {
|
||||
if (distributeReadonly) return;
|
||||
document.querySelectorAll('.student-box').forEach(cb => cb.checked = false);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Auto-submit filter when book changes
|
||||
document.getElementById('bookSelect')?.addEventListener('change', () => {
|
||||
document.getElementById('filterForm').submit();
|
||||
});
|
||||
|
||||
document.getElementById('checkAllBtn')?.addEventListener('click', () => {
|
||||
if (distributeReadonly) return;
|
||||
document.querySelectorAll('.student-box').forEach(cb => cb.checked = true);
|
||||
document.querySelectorAll('.student-box:not(:disabled)').forEach(cb => cb.checked = true);
|
||||
});
|
||||
|
||||
document.getElementById('uncheckAllBtn')?.addEventListener('click', () => {
|
||||
if (distributeReadonly) return;
|
||||
document.querySelectorAll('.student-box').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('.student-box:not(:disabled)').forEach(cb => cb.checked = false);
|
||||
});
|
||||
|
||||
// Cancel button clears all checkboxes
|
||||
document.getElementById('clearSelectionBtn')?.addEventListener('click', () => {
|
||||
if (distributeReadonly) return;
|
||||
document.querySelectorAll('.student-box').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('.student-box:not(:disabled)').forEach(cb => cb.checked = false);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user