feat(stock): add modular hospital stock management with dashboard, masters, indents, purchase and reports

This commit is contained in:
DevSoftTech Server 2026-04-02 10:28:38 +05:30
parent 584c9ce59d
commit d8f75b3a29
17 changed files with 2571 additions and 0 deletions

View File

@ -53,6 +53,18 @@ class AuthGroups extends ShieldAuthGroups
'title' => 'Developer',
'description' => 'Site programmers.',
],
'stock_manager' => [
'title' => 'Stock Manager',
'description' => 'Manages stock masters, approvals, and procurement.',
],
'department_head' => [
'title' => 'Department Head',
'description' => 'Creates and tracks departmental stock indents.',
],
'storekeeper' => [
'title' => 'Storekeeper',
'description' => 'Handles item issuance and stock receiving operations.',
],
'user' => [
'title' => 'User',
'description' => 'General users of the site. Often customers.',
@ -129,6 +141,13 @@ class AuthGroups extends ShieldAuthGroups
'template.diagnosis_print' => 'Can access diagnosis print templates',
'template.document_print' => 'Can access document print templates',
'template.ipd_document' => 'Can access IPD document master templates',
'hospital_stock.access' => 'Can access hospital stock module',
'hospital_stock.master.manage' => 'Can manage hospital stock masters (category/item/supplier)',
'hospital_stock.indent.create' => 'Can create stock indents',
'hospital_stock.indent.approve' => 'Can approve stock indents',
'hospital_stock.issue' => 'Can issue stock against approved indents',
'hospital_stock.purchase.manage' => 'Can create/receive purchase orders',
'hospital_stock.report.view' => 'Can view stock reports and alerts',
];
/**
@ -150,6 +169,7 @@ class AuthGroups extends ShieldAuthGroups
'doctor_work.*',
'pharmacy.*',
'template.*',
'hospital_stock.*',
],
'admin' => [
'admin.access',
@ -163,6 +183,7 @@ class AuthGroups extends ShieldAuthGroups
'doctor_work.*',
'pharmacy.*',
'template.*',
'hospital_stock.*',
],
'developer' => [
'admin.access',
@ -176,6 +197,25 @@ class AuthGroups extends ShieldAuthGroups
'doctor_work.*',
'pharmacy.*',
'template.*',
'hospital_stock.*',
],
'stock_manager' => [
'hospital_stock.access',
'hospital_stock.master.manage',
'hospital_stock.indent.approve',
'hospital_stock.purchase.manage',
'hospital_stock.report.view',
],
'department_head' => [
'hospital_stock.access',
'hospital_stock.indent.create',
'hospital_stock.report.view',
],
'storekeeper' => [
'hospital_stock.access',
'hospital_stock.issue',
'hospital_stock.purchase.manage',
'hospital_stock.report.view',
],
'user' => [],
'beta' => [

View File

@ -961,6 +961,29 @@ $routes->group('setting', static function($routes) {
$routes->get('admin/bed-assignments', 'Setting\\BedManagement::assignmentHistory');
$routes->post('admin/bed-assignments/save', 'Setting\\BedManagement::saveAssignment');
$routes->post('admin/bed-assignments/delete', 'Setting\\BedManagement::deleteAssignment');
$routes->get('admin/hospital-stock', 'Setting\\HospitalStock::index');
$routes->get('admin/hospital-stock/dashboard', 'Setting\\HospitalStock::dashboard');
$routes->get('admin/hospital-stock/masters', 'Setting\\HospitalStock::masters');
$routes->get('admin/hospital-stock/indents', 'Setting\\HospitalStock::indents');
$routes->get('admin/hospital-stock/purchase', 'Setting\\HospitalStock::purchase');
$routes->get('admin/hospital-stock/reports', 'Setting\\HospitalStock::reports');
$routes->post('admin/hospital-stock/category/save', 'Setting\\HospitalStock::saveCategory');
$routes->post('admin/hospital-stock/category/update', 'Setting\\HospitalStock::updateCategory');
$routes->post('admin/hospital-stock/category/delete', 'Setting\\HospitalStock::deleteCategory');
$routes->post('admin/hospital-stock/item/save', 'Setting\\HospitalStock::saveItem');
$routes->post('admin/hospital-stock/item/update', 'Setting\\HospitalStock::updateItem');
$routes->post('admin/hospital-stock/item/delete', 'Setting\\HospitalStock::deleteItem');
$routes->post('admin/hospital-stock/supplier/save', 'Setting\\HospitalStock::saveSupplier');
$routes->post('admin/hospital-stock/supplier/update', 'Setting\\HospitalStock::updateSupplier');
$routes->post('admin/hospital-stock/supplier/delete', 'Setting\\HospitalStock::deleteSupplier');
$routes->post('admin/hospital-stock/indent/create', 'Setting\\HospitalStock::createIndent');
$routes->post('admin/hospital-stock/indent/approve', 'Setting\\HospitalStock::approveIndent');
$routes->post('admin/hospital-stock/indent/issue', 'Setting\\HospitalStock::issueIndent');
$routes->post('admin/hospital-stock/po/create', 'Setting\\HospitalStock::createPurchaseOrder');
$routes->post('admin/hospital-stock/po/receive', 'Setting\\HospitalStock::receivePurchaseOrder');
$routes->get('admin/hospital-stock/report/department-consumption', 'Setting\\HospitalStock::reportDepartmentConsumption');
$routes->get('admin/hospital-stock/report/monthly-issue', 'Setting\\HospitalStock::reportMonthlyIssue');
$routes->get('admin/hospital-stock/report/near-expiry', 'Setting\\HospitalStock::reportNearExpiry');
$routes->get('template', 'Setting\\Template::index');
$routes->get('charges', 'Setting\\Charges::index');
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,305 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateHospitalStockModule extends Migration
{
public function up()
{
$this->createCategoriesTable();
$this->createSuppliersTable();
$this->createItemsTable();
$this->createIndentsTable();
$this->createIndentItemsTable();
$this->createIssuesTable();
$this->createIssueItemsTable();
$this->createPurchaseOrdersTable();
$this->createPurchaseOrderItemsTable();
$this->createStockLedgerTable();
$this->createAuditLogTable();
}
public function down()
{
$this->forge->dropTable('hsm_audit_log', true);
$this->forge->dropTable('hsm_stock_ledger', true);
$this->forge->dropTable('hsm_purchase_order_items', true);
$this->forge->dropTable('hsm_purchase_orders', true);
$this->forge->dropTable('hsm_issue_items', true);
$this->forge->dropTable('hsm_issues', true);
$this->forge->dropTable('hsm_indent_items', true);
$this->forge->dropTable('hsm_indents', true);
$this->forge->dropTable('hsm_items', true);
$this->forge->dropTable('hsm_suppliers', true);
$this->forge->dropTable('hsm_categories', true);
}
private function createCategoriesTable(): void
{
if ($this->hasTable('hsm_categories')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'name' => ['type' => 'VARCHAR', 'constraint' => 120],
'description' => ['type' => 'TEXT', 'null' => true],
'status' => ['type' => 'ENUM', 'constraint' => ['active', 'inactive'], 'default' => 'active'],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey('name');
$this->forge->createTable('hsm_categories', true);
}
private function createSuppliersTable(): void
{
if ($this->hasTable('hsm_suppliers')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'name' => ['type' => 'VARCHAR', 'constraint' => 150],
'contact_person' => ['type' => 'VARCHAR', 'constraint' => 120, 'null' => true],
'phone' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => true],
'email' => ['type' => 'VARCHAR', 'constraint' => 120, 'null' => true],
'address' => ['type' => 'TEXT', 'null' => true],
'gst_no' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => true],
'status' => ['type' => 'ENUM', 'constraint' => ['active', 'inactive'], 'default' => 'active'],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('name');
$this->forge->createTable('hsm_suppliers', true);
}
private function createItemsTable(): void
{
if ($this->hasTable('hsm_items')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'item_code' => ['type' => 'VARCHAR', 'constraint' => 40],
'name' => ['type' => 'VARCHAR', 'constraint' => 150],
'category_id' => ['type' => 'INT', 'unsigned' => true],
'uom' => ['type' => 'VARCHAR', 'constraint' => 20, 'default' => 'Unit'],
'barcode' => ['type' => 'VARCHAR', 'constraint' => 80, 'null' => true],
'qr_code' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
'current_stock' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'min_stock_level' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'reorder_level' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'unit_cost' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'expiry_date' => ['type' => 'DATE', 'null' => true],
'status' => ['type' => 'ENUM', 'constraint' => ['active', 'inactive'], 'default' => 'active'],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey('item_code');
$this->forge->addKey('name');
$this->forge->addKey('category_id');
$this->forge->createTable('hsm_items', true);
}
private function createIndentsTable(): void
{
if ($this->hasTable('hsm_indents')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'indent_code' => ['type' => 'VARCHAR', 'constraint' => 40],
'department_name' => ['type' => 'VARCHAR', 'constraint' => 120],
'requested_by' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'approved_by' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'approved_at' => ['type' => 'DATETIME', 'null' => true],
'remarks' => ['type' => 'TEXT', 'null' => true],
'status' => ['type' => 'ENUM', 'constraint' => ['pending', 'approved', 'partial_issued', 'issued', 'rejected', 'cancelled'], 'default' => 'pending'],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey('indent_code');
$this->forge->addKey('status');
$this->forge->createTable('hsm_indents', true);
}
private function createIndentItemsTable(): void
{
if ($this->hasTable('hsm_indent_items')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'indent_id' => ['type' => 'INT', 'unsigned' => true],
'item_id' => ['type' => 'INT', 'unsigned' => true],
'requested_qty' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'approved_qty' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'issued_qty' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('indent_id');
$this->forge->addKey('item_id');
$this->forge->createTable('hsm_indent_items', true);
}
private function createIssuesTable(): void
{
if ($this->hasTable('hsm_issues')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'issue_code' => ['type' => 'VARCHAR', 'constraint' => 40],
'indent_id' => ['type' => 'INT', 'unsigned' => true],
'issued_by' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'department_name' => ['type' => 'VARCHAR', 'constraint' => 120],
'remarks' => ['type' => 'TEXT', 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey('issue_code');
$this->forge->addKey('indent_id');
$this->forge->createTable('hsm_issues', true);
}
private function createIssueItemsTable(): void
{
if ($this->hasTable('hsm_issue_items')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'issue_id' => ['type' => 'INT', 'unsigned' => true],
'item_id' => ['type' => 'INT', 'unsigned' => true],
'qty' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('issue_id');
$this->forge->addKey('item_id');
$this->forge->createTable('hsm_issue_items', true);
}
private function createPurchaseOrdersTable(): void
{
if ($this->hasTable('hsm_purchase_orders')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'po_code' => ['type' => 'VARCHAR', 'constraint' => 40],
'supplier_id' => ['type' => 'INT', 'unsigned' => true],
'order_date' => ['type' => 'DATE', 'null' => true],
'expected_date' => ['type' => 'DATE', 'null' => true],
'ordered_by' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'status' => ['type' => 'ENUM', 'constraint' => ['draft', 'ordered', 'partial_received', 'completed', 'cancelled'], 'default' => 'ordered'],
'remarks' => ['type' => 'TEXT', 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey('po_code');
$this->forge->addKey('supplier_id');
$this->forge->addKey('status');
$this->forge->createTable('hsm_purchase_orders', true);
}
private function createPurchaseOrderItemsTable(): void
{
if ($this->hasTable('hsm_purchase_order_items')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'purchase_order_id' => ['type' => 'INT', 'unsigned' => true],
'item_id' => ['type' => 'INT', 'unsigned' => true],
'ordered_qty' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'received_qty' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'unit_cost' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'expiry_date' => ['type' => 'DATE', 'null' => true],
'batch_no' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('purchase_order_id');
$this->forge->addKey('item_id');
$this->forge->createTable('hsm_purchase_order_items', true);
}
private function createStockLedgerTable(): void
{
if ($this->hasTable('hsm_stock_ledger')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'item_id' => ['type' => 'INT', 'unsigned' => true],
'txn_type' => ['type' => 'ENUM', 'constraint' => ['purchase', 'issue', 'adjustment_in', 'adjustment_out'], 'default' => 'adjustment_in'],
'ref_table' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
'ref_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'qty_in' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'qty_out' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'balance_after' => ['type' => 'DECIMAL', 'constraint' => '14,2', 'default' => 0],
'remarks' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
'created_by' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('item_id');
$this->forge->addKey('txn_type');
$this->forge->addKey('created_at');
$this->forge->createTable('hsm_stock_ledger', true);
}
private function createAuditLogTable(): void
{
if ($this->hasTable('hsm_audit_log')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'module' => ['type' => 'VARCHAR', 'constraint' => 60],
'action' => ['type' => 'VARCHAR', 'constraint' => 60],
'entity_table' => ['type' => 'VARCHAR', 'constraint' => 60],
'entity_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'description' => ['type' => 'TEXT', 'null' => true],
'meta_json' => ['type' => 'LONGTEXT', 'null' => true],
'created_by' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('module');
$this->forge->addKey('entity_table');
$this->forge->createTable('hsm_audit_log', true);
}
private function hasTable(string $table): bool
{
$query = $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table));
return $query->getNumRows() > 0;
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class EnhanceHospitalStockItems extends Migration
{
public function up()
{
if (! $this->db->tableExists('hsm_items')) {
return;
}
$columns = $this->getColumns('hsm_items');
$fields = [];
if (! in_array('item_type', $columns, true)) {
$fields['item_type'] = ['type' => 'VARCHAR', 'constraint' => 60, 'null' => true, 'after' => 'category_id'];
}
if (! in_array('purchase_uom', $columns, true)) {
$fields['purchase_uom'] = ['type' => 'VARCHAR', 'constraint' => 30, 'null' => true, 'after' => 'uom'];
}
if (! in_array('issue_uom', $columns, true)) {
$fields['issue_uom'] = ['type' => 'VARCHAR', 'constraint' => 30, 'null' => true, 'after' => 'purchase_uom'];
}
if (! in_array('issue_per_purchase', $columns, true)) {
$fields['issue_per_purchase'] = ['type' => 'DECIMAL', 'constraint' => '14,4', 'default' => 1, 'after' => 'issue_uom'];
}
if (! in_array('is_daily_use', $columns, true)) {
$fields['is_daily_use'] = ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0, 'after' => 'issue_per_purchase'];
}
if (! in_array('store_location', $columns, true)) {
$fields['store_location'] = ['type' => 'VARCHAR', 'constraint' => 120, 'null' => true, 'after' => 'is_daily_use'];
}
if ($fields !== []) {
$this->forge->addColumn('hsm_items', $fields);
}
// Backfill sensible defaults for unit conversion.
$this->db->query("UPDATE hsm_items SET purchase_uom = IFNULL(NULLIF(purchase_uom,''), IFNULL(NULLIF(uom,''), 'Unit'))");
$this->db->query("UPDATE hsm_items SET issue_uom = IFNULL(NULLIF(issue_uom,''), IFNULL(NULLIF(uom,''), 'Unit'))");
$this->db->query("UPDATE hsm_items SET issue_per_purchase = IFNULL(NULLIF(issue_per_purchase,0), 1)");
}
public function down()
{
if (! $this->db->tableExists('hsm_items')) {
return;
}
foreach (['store_location', 'is_daily_use', 'issue_per_purchase', 'issue_uom', 'purchase_uom', 'item_type'] as $column) {
if (in_array($column, $this->getColumns('hsm_items'), true)) {
$this->forge->dropColumn('hsm_items', $column);
}
}
}
/**
* @return string[]
*/
private function getColumns(string $table): array
{
$result = $this->db->query('SHOW COLUMNS FROM ' . $this->db->protectIdentifiers($table))->getResultArray();
return array_map(static fn (array $row): string => (string) ($row['Field'] ?? ''), $result);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class HospitalStockCategoryModel extends Model
{
protected $table = 'hsm_categories';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $allowedFields = ['name', 'description', 'status'];
protected $useTimestamps = true;
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class HospitalStockItemModel extends Model
{
protected $table = 'hsm_items';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $allowedFields = [
'item_code',
'name',
'category_id',
'item_type',
'uom',
'purchase_uom',
'issue_uom',
'issue_per_purchase',
'is_daily_use',
'store_location',
'barcode',
'qr_code',
'current_stock',
'min_stock_level',
'reorder_level',
'unit_cost',
'expiry_date',
'status',
];
protected $useTimestamps = true;
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class HospitalStockSupplierModel extends Model
{
protected $table = 'hsm_suppliers';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $allowedFields = [
'name',
'contact_person',
'phone',
'email',
'address',
'gst_no',
'status',
];
protected $useTimestamps = true;
}

View File

@ -104,6 +104,12 @@
<span>Bed Management</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/hospital-stock') ?>','maindiv','Hospital Stock Management');">
<i class="bi bi-box-seam"></i>
<span>Hospital Stock</span>
</a>
</div>
</div>
<hr/>
<div class="row mt-4">

View File

@ -0,0 +1,98 @@
<div class="card">
<div class="card-header d-flex align-items-center justify-content-between">
<h5 class="mb-0">Inventory Dashboard</h5>
<span class="badge bg-primary">Focus: Expiry, Short Item, New Request</span>
</div>
<div class="card-body">
<div class="row g-2 mb-3">
<div class="col-6 col-md-2"><div class="p-2 border rounded"><small>Items</small><h5 class="mb-0"><?= (int) ($stats['total_items'] ?? 0) ?></h5></div></div>
<div class="col-6 col-md-2"><div class="p-2 border rounded"><small>Low Stock</small><h5 class="mb-0 text-danger"><?= (int) ($stats['low_stock'] ?? 0) ?></h5></div></div>
<div class="col-6 col-md-2"><div class="p-2 border rounded"><small>Pending Indents</small><h5 class="mb-0 text-warning"><?= (int) ($stats['pending_indents'] ?? 0) ?></h5></div></div>
<div class="col-6 col-md-2"><div class="p-2 border rounded"><small>Open PO</small><h5 class="mb-0 text-info"><?= (int) ($stats['open_po'] ?? 0) ?></h5></div></div>
<div class="col-12 col-md-4"><div class="p-2 border rounded"><small>Stock Value</small><h5 class="mb-0 text-success">₹<?= number_format((float) ($stats['stock_value'] ?? 0), 2) ?></h5></div></div>
</div>
<div class="row g-3">
<div class="col-md-6">
<h6>New Request (Pending Indent)</h6>
<div class="table-responsive">
<table class="table table-sm table-striped" id="tblNewReq">
<thead><tr><th>Indent</th><th>Department</th><th>Date</th></tr></thead>
<tbody>
<?php foreach (($newRequests ?? []) as $r): ?>
<tr>
<td><?= esc((string) ($r['indent_code'] ?? '')) ?></td>
<td><?= esc((string) ($r['department_name'] ?? '')) ?></td>
<td><?= esc((string) ($r['created_at'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h6>Regularly Used Items Near Expiry</h6>
<div class="table-responsive">
<table class="table table-sm table-striped" id="tblNearExpiryUsed">
<thead><tr><th>Item</th><th>Used Qty</th><th>Stock</th><th>Expiry</th></tr></thead>
<tbody>
<?php foreach (($nearExpiryFrequent ?? []) as $r): ?>
<tr>
<td><?= esc((string) ($r['item_code'] ?? '')) ?> - <?= esc((string) ($r['name'] ?? '')) ?></td>
<td><?= esc((string) ($r['used_qty'] ?? '0')) ?></td>
<td><?= esc((string) ($r['current_stock'] ?? '0')) ?></td>
<td><?= esc((string) ($r['expiry_date'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h6>Short Item with No Replenishment Request</h6>
<div class="table-responsive">
<table class="table table-sm table-striped" id="tblShortNoReq">
<thead><tr><th>Item</th><th>Stock</th><th>Reorder</th></tr></thead>
<tbody>
<?php foreach (($lowStockNoRequest ?? []) as $r): ?>
<tr>
<td><?= esc((string) ($r['item_code'] ?? '')) ?> - <?= esc((string) ($r['name'] ?? '')) ?></td>
<td class="text-danger"><?= esc((string) ($r['current_stock'] ?? '0')) ?></td>
<td><?= esc((string) ($r['reorder_level'] ?? '0')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h6>Daily Use Item Status</h6>
<div class="table-responsive">
<table class="table table-sm table-striped" id="tblDailyStatus">
<thead><tr><th>Item</th><th>Today</th><th>7d Use</th><th>Stock</th></tr></thead>
<tbody>
<?php foreach (($dailyUseStatus ?? []) as $r): ?>
<tr>
<td><?= esc((string) ($r['item_code'] ?? '')) ?> - <?= esc((string) ($r['name'] ?? '')) ?></td>
<td><?= esc((string) ($r['issued_today'] ?? '0')) ?></td>
<td><?= esc((string) ($r['issued_7d'] ?? '0')) ?></td>
<td><?= esc((string) ($r['current_stock'] ?? '0')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
(function(){
if ($.fn && $.fn.DataTable) {
$('#tblNewReq').DataTable({pageLength:5, order:[[2,'desc']]});
$('#tblNearExpiryUsed').DataTable({pageLength:5, order:[[3,'asc']]});
$('#tblShortNoReq').DataTable({pageLength:5, order:[[1,'asc']]});
$('#tblDailyStatus').DataTable({pageLength:5, order:[[0,'asc']]});
}
})();
</script>

View File

@ -0,0 +1,138 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Department Indents & Issue</h5>
<button class="btn btn-primary btn-sm" data-bs-toggle="collapse" data-bs-target="#indentCreateBox">New Request</button>
</div>
<div class="card-body">
<div id="indentAlert" class="alert alert-danger d-none"></div>
<div id="indentCreateBox" class="collapse show mb-3">
<form id="indentForm" class="row g-2">
<?= csrf_field() ?>
<div class="col-md-3">
<select class="form-select" name="department_id" required>
<option value="">Department</option>
<?php foreach (($departments ?? []) as $d): ?>
<option value="<?= (int) ($d['iId'] ?? 0) ?>"><?= esc((string) ($d['vName'] ?? '')) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2"><input class="form-control" type="date" name="required_date"></div>
<div class="col-md-4"><input class="form-control" name="remarks" placeholder="Reason / remarks"></div>
<div class="col-md-3"><button class="btn btn-success btn-sm" type="submit">Submit Request</button></div>
</form>
<div class="row g-2 mt-1">
<div class="col-md-6">
<select id="indentItemSelect" class="form-select form-select-sm">
<option value="">Select item</option>
<?php foreach (($items ?? []) as $it): ?>
<option value="<?= (int) ($it['id'] ?? 0) ?>" data-uom="<?= esc((string) ($it['issue_uom'] ?? ($it['uom'] ?? 'Unit')), 'attr') ?>"><?= esc((string) (($it['item_code'] ?? '') . ' - ' . ($it['name'] ?? ''))) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2"><input type="number" step="0.01" min="0.01" id="indentQty" class="form-control form-control-sm" placeholder="Qty"></div>
<div class="col-md-2"><input id="indentUom" class="form-control form-control-sm" placeholder="UOM"></div>
<div class="col-md-2"><button id="addIndentItem" type="button" class="btn btn-outline-primary btn-sm w-100">Add</button></div>
</div>
<table class="table table-sm mt-2" id="indentItemsTable">
<thead><tr><th>Item</th><th>Qty</th><th>UOM</th><th></th></tr></thead>
<tbody></tbody>
</table>
</div>
<h6>Requests</h6>
<table class="table table-striped table-sm" id="indentsTable">
<thead><tr><th>Req No</th><th>Date</th><th>Department</th><th>Status</th><th>Requested By</th><th>Action</th></tr></thead>
<tbody>
<?php foreach (($indents ?? []) as $in): ?>
<tr>
<td><?= esc((string) ($in['indent_code'] ?? '')) ?></td>
<td><?= esc((string) ($in['created_at'] ?? '')) ?></td>
<td><?= esc((string) ($in['department_name'] ?? '')) ?></td>
<td><?= esc((string) ($in['status'] ?? 'pending')) ?></td>
<td><?= esc((string) ($in['requested_by'] ?? '')) ?></td>
<td class="d-flex gap-1">
<?php if (($in['status'] ?? '') === 'pending'): ?>
<button class="btn btn-outline-success btn-sm approve-indent" data-id="<?= (int) ($in['id'] ?? 0) ?>">Approve</button>
<?php endif; ?>
<?php if (($in['status'] ?? '') === 'approved'): ?>
<button class="btn btn-outline-primary btn-sm issue-indent" data-id="<?= (int) ($in['id'] ?? 0) ?>">Issue</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<script>
(function(){
var indentItems=[];
function showErr(msg){ var b=document.getElementById('indentAlert'); if(!b) return; b.textContent=msg; b.classList.remove('d-none'); }
function clearErr(){ var b=document.getElementById('indentAlert'); if(!b) return; b.textContent=''; b.classList.add('d-none'); }
function reloadIndents(){ load_form_div('<?= base_url('setting/admin/hospital-stock/indents') ?>','stockmaindiv','Stock Indents'); }
function renderIndentItems(){
var $tb = $('#indentItemsTable tbody'); $tb.empty();
indentItems.forEach(function(it,idx){
var tr = '<tr><td>'+it.item_name+'</td><td>'+it.quantity+'</td><td>'+it.uom+'</td><td><button type="button" class="btn btn-outline-danger btn-sm remove-indent-item" data-idx="'+idx+'">X</button></td></tr>';
$tb.append(tr);
});
}
$('#indentItemSelect').on('change', function(){
var uom = $('#indentItemSelect option:selected').data('uom') || '';
$('#indentUom').val(uom);
});
$('#addIndentItem').on('click', function(){
clearErr();
var id = parseInt($('#indentItemSelect').val() || '0', 10);
var qty = parseFloat($('#indentQty').val() || '0');
var uom = $('#indentUom').val() || '';
var itemName = $('#indentItemSelect option:selected').text();
if(id <= 0 || qty <= 0){ showErr('Select item and quantity.'); return; }
indentItems.push({ item_id:id, quantity:qty, uom:uom, item_name:itemName });
renderIndentItems();
$('#indentQty').val('');
});
$(document).on('click','.remove-indent-item', function(){
var idx = parseInt($(this).data('idx') || '-1', 10);
if(idx >= 0){ indentItems.splice(idx,1); renderIndentItems(); }
});
$('#indentForm').on('submit', function(e){
e.preventDefault(); clearErr();
if(indentItems.length === 0){ showErr('Add at least one item.'); return; }
var data = $(this).serializeArray();
data.push({name:'department_name', value: $('#indentForm [name="department_id"] option:selected').text()});
indentItems.forEach(function(it){
data.push({name:'item_id[]', value: it.item_id});
data.push({name:'qty[]', value: it.quantity});
});
$.post('<?= base_url('setting/admin/hospital-stock/indent/create') ?>', $.param(data))
.done(function(resp){ if(resp && resp.error_text){showErr(resp.error_text); return;} reloadIndents(); })
.fail(function(){ showErr('Save failed.'); });
});
$(document).on('click','.approve-indent', function(){
$.post('<?= base_url('setting/admin/hospital-stock/indent/approve') ?>',{indent_id:$(this).data('id'),'<?= csrf_token() ?>':'<?= csrf_hash() ?>'})
.done(function(resp){ if(resp && resp.error_text){showErr(resp.error_text); return;} reloadIndents(); })
.fail(function(){ showErr('Approve failed.'); });
});
$(document).on('click','.issue-indent', function(){
var id = $(this).data('id');
var note = prompt('Issue remarks (optional):', 'Issued by store');
if(note === null){ return; }
$.post('<?= base_url('setting/admin/hospital-stock/indent/issue') ?>',{indent_id:id, remarks:note,'<?= csrf_token() ?>':'<?= csrf_hash() ?>'})
.done(function(resp){ if(resp && resp.error_text){showErr(resp.error_text); return;} reloadIndents(); })
.fail(function(){ showErr('Issue failed.'); });
});
if($.fn && $.fn.DataTable){ $('#indentsTable').DataTable({pageLength:10, order:[[1,'desc']]}); }
})();
</script>

View File

@ -0,0 +1,84 @@
<section class="content">
<div class="pagetitle">
<h1>Hospital Stock Management</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item">Dashboard</li>
<li class="breadcrumb-item">Setting</li>
<li class="breadcrumb-item active">Hospital Stock</li>
</ol>
</nav>
</div>
<div class="row g-3">
<?php if ($canReportView ?? false): ?>
<div class="col-6 col-md-3">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/hospital-stock/dashboard') ?>','stockmaindiv','Stock Dashboard');">
<i class="bi bi-speedometer2"></i>
<span>Dashboard</span>
</a>
</div>
<?php endif; ?>
<?php if ($canMasterManage ?? false): ?>
<div class="col-6 col-md-3">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/hospital-stock/masters') ?>','stockmaindiv','Stock Masters');">
<i class="bi bi-boxes"></i>
<span>Item Masters</span>
</a>
</div>
<?php endif; ?>
<?php if (($canIndentCreate ?? false) || ($canIndentApprove ?? false) || ($canIssue ?? false)): ?>
<div class="col-6 col-md-3">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/hospital-stock/indents') ?>','stockmaindiv','Stock Indents');">
<i class="bi bi-journal-check"></i>
<span>Indent & Issue</span>
</a>
</div>
<?php endif; ?>
<?php if ($canPurchaseManage ?? false): ?>
<div class="col-6 col-md-3">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/hospital-stock/purchase') ?>','stockmaindiv','Purchase Management');">
<i class="bi bi-cart4"></i>
<span>Purchase</span>
</a>
</div>
<?php endif; ?>
<?php if ($canReportView ?? false): ?>
<div class="col-6 col-md-3">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/hospital-stock/reports') ?>','stockmaindiv','Stock Reports');">
<i class="bi bi-file-earmark-text"></i>
<span>Reports</span>
</a>
</div>
<?php endif; ?>
</div>
<hr/>
<div class="row mt-3">
<div class="col-12" id="stockmaindiv"></div>
</div>
</section>
<script>
(function() {
var defaultUrl = '<?= base_url('setting/admin/hospital-stock/dashboard') ?>';
var defaultTitle = 'Stock Dashboard';
<?php if (! ($canReportView ?? false) && ($canMasterManage ?? false)): ?>
defaultUrl = '<?= base_url('setting/admin/hospital-stock/masters') ?>';
defaultTitle = 'Stock Masters';
<?php elseif (! ($canReportView ?? false) && (($canIndentCreate ?? false) || ($canIndentApprove ?? false) || ($canIssue ?? false))): ?>
defaultUrl = '<?= base_url('setting/admin/hospital-stock/indents') ?>';
defaultTitle = 'Stock Indents';
<?php elseif (! ($canReportView ?? false) && ($canPurchaseManage ?? false)): ?>
defaultUrl = '<?= base_url('setting/admin/hospital-stock/purchase') ?>';
defaultTitle = 'Purchase Management';
<?php endif; ?>
load_form_div(defaultUrl, 'stockmaindiv', defaultTitle);
})();
</script>

View File

@ -0,0 +1,259 @@
<div class="card">
<div class="card-header"><h5 class="mb-0">Item-wise Masters</h5></div>
<div class="card-body">
<div id="masterAlert" class="alert alert-danger d-none"></div>
<div class="row g-3">
<div class="col-lg-4">
<h6>Category</h6>
<form id="categoryForm">
<?= csrf_field() ?>
<input type="hidden" name="id" id="category_id" value="0">
<input class="form-control mb-2" name="name" placeholder="Category name" required>
<textarea class="form-control mb-2" name="description" placeholder="Description"></textarea>
<select class="form-select mb-2" name="status"><option value="active">Active</option><option value="inactive">Inactive</option></select>
<div class="d-flex gap-2">
<button class="btn btn-primary btn-sm" type="submit" id="categorySubmitBtn">Save Category</button>
<button class="btn btn-light btn-sm" type="button" id="categoryResetBtn">Clear</button>
</div>
</form>
<hr/>
<table class="table table-sm table-striped" id="categoryTable">
<thead><tr><th>Category</th><th>Status</th><th>Action</th></tr></thead>
<tbody>
<?php foreach (($categories ?? []) as $cat): ?>
<tr>
<td><?= esc((string) ($cat['name'] ?? '')) ?></td>
<td><?= esc((string) ($cat['status'] ?? 'active')) ?></td>
<td>
<button class="btn btn-outline-secondary btn-sm edit-category"
data-id="<?= (int) ($cat['id'] ?? 0) ?>"
data-name="<?= esc((string) ($cat['name'] ?? ''), 'attr') ?>"
data-description="<?= esc((string) ($cat['description'] ?? ''), 'attr') ?>"
data-status="<?= esc((string) ($cat['status'] ?? 'active'), 'attr') ?>">Edit</button>
<button class="btn btn-outline-danger btn-sm delete-category" data-id="<?= (int) ($cat['id'] ?? 0) ?>">Delete</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="col-lg-8">
<h6>Item</h6>
<form id="itemForm">
<?= csrf_field() ?>
<input type="hidden" name="id" id="item_id_hidden" value="0">
<div class="row g-2">
<div class="col-md-2"><input class="form-control" name="item_code" placeholder="Item code" required></div>
<div class="col-md-4"><input class="form-control" name="name" placeholder="Item name" required></div>
<div class="col-md-2"><select class="form-select" name="category_id" required><option value="">Category</option><?php foreach (($categories ?? []) as $cat): ?><option value="<?= (int) ($cat['id'] ?? 0) ?>"><?= esc((string) ($cat['name'] ?? '')) ?></option><?php endforeach; ?></select></div>
<div class="col-md-2"><input class="form-control" name="item_type" placeholder="Type (paper/liquid/form)"></div>
<div class="col-md-2"><input class="form-control" name="store_location" placeholder="Store location"></div>
<div class="col-md-2"><input class="form-control" name="uom" value="Unit" placeholder="UOM"></div>
<div class="col-md-2"><input class="form-control" name="purchase_uom" value="Unit" placeholder="Purchase UOM"></div>
<div class="col-md-2"><input class="form-control" name="issue_uom" value="Unit" placeholder="Issue UOM"></div>
<div class="col-md-2"><input class="form-control" name="issue_per_purchase" type="number" step="0.0001" value="1" placeholder="Issue/Purchase"></div>
<div class="col-md-2"><input class="form-control" name="current_stock" type="number" step="0.01" placeholder="Stock"></div>
<div class="col-md-2"><input class="form-control" name="min_stock_level" type="number" step="0.01" placeholder="Min stock"></div>
<div class="col-md-2"><input class="form-control" name="reorder_level" type="number" step="0.01" placeholder="Reorder"></div>
<div class="col-md-2"><input class="form-control" name="unit_cost" type="number" step="0.01" placeholder="Unit cost"></div>
<div class="col-md-2"><input class="form-control" name="expiry_date" type="date"></div>
<div class="col-md-2"><input class="form-control" name="barcode" placeholder="Barcode"></div>
<div class="col-md-2"><input class="form-control" name="qr_code" placeholder="QR code"></div>
<div class="col-md-2">
<select class="form-select" name="status"><option value="active">Active</option><option value="inactive">Inactive</option></select>
</div>
<div class="col-md-2 d-flex align-items-center">
<div class="form-check"><input class="form-check-input" type="checkbox" name="is_daily_use" id="is_daily_use"><label class="form-check-label" for="is_daily_use">Daily use</label></div>
</div>
<div class="col-md-12 d-flex gap-2">
<button class="btn btn-primary btn-sm" type="submit" id="itemSubmitBtn">Save Item</button>
<button class="btn btn-light btn-sm" type="button" id="itemResetBtn">Clear</button>
</div>
</div>
</form>
<hr/>
<table class="table table-sm table-striped" id="itemMasterTable">
<thead><tr><th>Code</th><th>Name</th><th>Type</th><th>UOM</th><th>Stock</th><th>Action</th></tr></thead>
<tbody>
<?php foreach (($items ?? []) as $it): ?>
<tr>
<td><?= esc((string) ($it['item_code'] ?? '')) ?></td>
<td><?= esc((string) ($it['name'] ?? '')) ?></td>
<td><?= esc((string) ($it['item_type'] ?? '')) ?></td>
<td><?= esc((string) ($it['issue_uom'] ?? ($it['uom'] ?? ''))) ?></td>
<td><?= esc((string) ($it['current_stock'] ?? '0')) ?></td>
<td>
<button class="btn btn-outline-secondary btn-sm edit-item"
data-id="<?= (int) ($it['id'] ?? 0) ?>"
data-item_code="<?= esc((string) ($it['item_code'] ?? ''), 'attr') ?>"
data-name="<?= esc((string) ($it['name'] ?? ''), 'attr') ?>"
data-category_id="<?= (int) ($it['category_id'] ?? 0) ?>"
data-item_type="<?= esc((string) ($it['item_type'] ?? ''), 'attr') ?>"
data-store_location="<?= esc((string) ($it['store_location'] ?? ''), 'attr') ?>"
data-uom="<?= esc((string) ($it['uom'] ?? 'Unit'), 'attr') ?>"
data-purchase_uom="<?= esc((string) ($it['purchase_uom'] ?? 'Unit'), 'attr') ?>"
data-issue_uom="<?= esc((string) ($it['issue_uom'] ?? 'Unit'), 'attr') ?>"
data-issue_per_purchase="<?= esc((string) ($it['issue_per_purchase'] ?? '1'), 'attr') ?>"
data-current_stock="<?= esc((string) ($it['current_stock'] ?? '0'), 'attr') ?>"
data-min_stock_level="<?= esc((string) ($it['min_stock_level'] ?? '0'), 'attr') ?>"
data-reorder_level="<?= esc((string) ($it['reorder_level'] ?? '0'), 'attr') ?>"
data-unit_cost="<?= esc((string) ($it['unit_cost'] ?? '0'), 'attr') ?>"
data-expiry_date="<?= esc((string) ($it['expiry_date'] ?? ''), 'attr') ?>"
data-barcode="<?= esc((string) ($it['barcode'] ?? ''), 'attr') ?>"
data-qr_code="<?= esc((string) ($it['qr_code'] ?? ''), 'attr') ?>"
data-status="<?= esc((string) ($it['status'] ?? 'active'), 'attr') ?>"
data-is_daily_use="<?= (int) ($it['is_daily_use'] ?? 0) ?>">Edit</button>
<button class="btn btn-outline-danger btn-sm delete-item" data-id="<?= (int) ($it['id'] ?? 0) ?>">Delete</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="col-12">
<h6>Supplier</h6>
<form id="supplierForm" class="row g-2">
<?= csrf_field() ?>
<input type="hidden" name="id" id="supplier_id_hidden" value="0">
<div class="col-md-3"><input class="form-control" name="name" placeholder="Supplier name" required></div>
<div class="col-md-2"><input class="form-control" name="contact_person" placeholder="Contact person"></div>
<div class="col-md-2"><input class="form-control" name="phone" placeholder="Phone"></div>
<div class="col-md-2"><input class="form-control" name="email" placeholder="Email"></div>
<div class="col-md-2"><input class="form-control" name="gst_no" placeholder="GST"></div>
<div class="col-md-1"><select class="form-select" name="status"><option value="active">Active</option><option value="inactive">Inactive</option></select></div>
<div class="col-md-12"><textarea class="form-control" name="address" placeholder="Address"></textarea></div>
<div class="col-md-12 d-flex gap-2"><button class="btn btn-primary btn-sm" type="submit" id="supplierSubmitBtn">Save Supplier</button><button class="btn btn-light btn-sm" type="button" id="supplierResetBtn">Clear</button></div>
</form>
<hr/>
<table class="table table-sm table-striped" id="supplierTable">
<thead><tr><th>Name</th><th>Phone</th><th>Email</th><th>Action</th></tr></thead>
<tbody>
<?php foreach (($suppliers ?? []) as $sp): ?>
<tr>
<td><?= esc((string) ($sp['name'] ?? '')) ?></td>
<td><?= esc((string) ($sp['phone'] ?? '')) ?></td>
<td><?= esc((string) ($sp['email'] ?? '')) ?></td>
<td>
<button class="btn btn-outline-secondary btn-sm edit-supplier"
data-id="<?= (int) ($sp['id'] ?? 0) ?>"
data-name="<?= esc((string) ($sp['name'] ?? ''), 'attr') ?>"
data-contact_person="<?= esc((string) ($sp['contact_person'] ?? ''), 'attr') ?>"
data-phone="<?= esc((string) ($sp['phone'] ?? ''), 'attr') ?>"
data-email="<?= esc((string) ($sp['email'] ?? ''), 'attr') ?>"
data-gst_no="<?= esc((string) ($sp['gst_no'] ?? ''), 'attr') ?>"
data-address="<?= esc((string) ($sp['address'] ?? ''), 'attr') ?>"
data-status="<?= esc((string) ($sp['status'] ?? 'active'), 'attr') ?>">Edit</button>
<button class="btn btn-outline-danger btn-sm delete-supplier" data-id="<?= (int) ($sp['id'] ?? 0) ?>">Delete</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
(function(){
function showErr(msg){
var b=document.getElementById('masterAlert');
if(!b) return;
b.textContent=msg; b.classList.remove('d-none');
}
function clearErr(){
var b=document.getElementById('masterAlert');
if(!b) return;
b.textContent=''; b.classList.add('d-none');
}
function reloadPage(){ load_form_div('<?= base_url('setting/admin/hospital-stock/masters') ?>','stockmaindiv','Stock Masters'); }
function ajaxSubmit($form, saveUrl, updateUrl, idSelector){
$form.on('submit', function(e){
e.preventDefault(); clearErr();
var id = parseInt($(idSelector).val() || '0', 10);
var url = id > 0 ? updateUrl : saveUrl;
$.post(url, $form.serialize()).done(function(resp){
if(resp && resp.error_text){ showErr(resp.error_text); return; }
reloadPage();
}).fail(function(){ showErr('Request failed.'); });
});
}
ajaxSubmit($('#categoryForm'), '<?= base_url('setting/admin/hospital-stock/category/save') ?>', '<?= base_url('setting/admin/hospital-stock/category/update') ?>', '#category_id');
ajaxSubmit($('#itemForm'), '<?= base_url('setting/admin/hospital-stock/item/save') ?>', '<?= base_url('setting/admin/hospital-stock/item/update') ?>', '#item_id_hidden');
ajaxSubmit($('#supplierForm'), '<?= base_url('setting/admin/hospital-stock/supplier/save') ?>', '<?= base_url('setting/admin/hospital-stock/supplier/update') ?>', '#supplier_id_hidden');
$('#categoryResetBtn').on('click', function(){ $('#categoryForm')[0].reset(); $('#category_id').val(0); $('#categorySubmitBtn').text('Save Category'); });
$('#itemResetBtn').on('click', function(){ $('#itemForm')[0].reset(); $('#item_id_hidden').val(0); $('#itemSubmitBtn').text('Save Item'); $('#is_daily_use').prop('checked', false); });
$('#supplierResetBtn').on('click', function(){ $('#supplierForm')[0].reset(); $('#supplier_id_hidden').val(0); $('#supplierSubmitBtn').text('Save Supplier'); });
$(document).on('click','.edit-category', function(){
$('#category_id').val($(this).data('id'));
$('#categoryForm [name="name"]').val($(this).data('name'));
$('#categoryForm [name="description"]').val($(this).data('description'));
$('#categoryForm [name="status"]').val($(this).data('status'));
$('#categorySubmitBtn').text('Update Category');
});
$(document).on('click','.delete-category', function(){
if(!confirm('Delete this category?')) return;
$.post('<?= base_url('setting/admin/hospital-stock/category/delete') ?>',{id:$(this).data('id'),'<?= csrf_token() ?>':'<?= csrf_hash() ?>'}).done(function(resp){ if(resp && resp.error_text){showErr(resp.error_text);return;} reloadPage(); }).fail(function(){showErr('Request failed.');});
});
$(document).on('click','.edit-item', function(){
$('#item_id_hidden').val($(this).data('id'));
var f='#itemForm ';
$(f+'[name="item_code"]').val($(this).data('item_code'));
$(f+'[name="name"]').val($(this).data('name'));
$(f+'[name="category_id"]').val($(this).data('category_id'));
$(f+'[name="item_type"]').val($(this).data('item_type'));
$(f+'[name="store_location"]').val($(this).data('store_location'));
$(f+'[name="uom"]').val($(this).data('uom'));
$(f+'[name="purchase_uom"]').val($(this).data('purchase_uom'));
$(f+'[name="issue_uom"]').val($(this).data('issue_uom'));
$(f+'[name="issue_per_purchase"]').val($(this).data('issue_per_purchase'));
$(f+'[name="current_stock"]').val($(this).data('current_stock'));
$(f+'[name="min_stock_level"]').val($(this).data('min_stock_level'));
$(f+'[name="reorder_level"]').val($(this).data('reorder_level'));
$(f+'[name="unit_cost"]').val($(this).data('unit_cost'));
$(f+'[name="expiry_date"]').val($(this).data('expiry_date'));
$(f+'[name="barcode"]').val($(this).data('barcode'));
$(f+'[name="qr_code"]').val($(this).data('qr_code'));
$(f+'[name="status"]').val($(this).data('status'));
$('#is_daily_use').prop('checked', parseInt($(this).data('is_daily_use')||'0',10)===1);
$('#itemSubmitBtn').text('Update Item');
});
$(document).on('click','.delete-item', function(){
if(!confirm('Delete this item?')) return;
$.post('<?= base_url('setting/admin/hospital-stock/item/delete') ?>',{id:$(this).data('id'),'<?= csrf_token() ?>':'<?= csrf_hash() ?>'}).done(function(resp){ if(resp && resp.error_text){showErr(resp.error_text);return;} reloadPage(); }).fail(function(){showErr('Request failed.');});
});
$(document).on('click','.edit-supplier', function(){
$('#supplier_id_hidden').val($(this).data('id'));
var f='#supplierForm ';
$(f+'[name="name"]').val($(this).data('name'));
$(f+'[name="contact_person"]').val($(this).data('contact_person'));
$(f+'[name="phone"]').val($(this).data('phone'));
$(f+'[name="email"]').val($(this).data('email'));
$(f+'[name="gst_no"]').val($(this).data('gst_no'));
$(f+'[name="address"]').val($(this).data('address'));
$(f+'[name="status"]').val($(this).data('status'));
$('#supplierSubmitBtn').text('Update Supplier');
});
$(document).on('click','.delete-supplier', function(){
if(!confirm('Delete this supplier?')) return;
$.post('<?= base_url('setting/admin/hospital-stock/supplier/delete') ?>',{id:$(this).data('id'),'<?= csrf_token() ?>':'<?= csrf_hash() ?>'}).done(function(resp){ if(resp && resp.error_text){showErr(resp.error_text);return;} reloadPage(); }).fail(function(){showErr('Request failed.');});
});
if($.fn && $.fn.DataTable){
$('#categoryTable').DataTable({pageLength:5, order:[[0,'asc']]});
$('#itemMasterTable').DataTable({pageLength:8, order:[[0,'asc']]});
$('#supplierTable').DataTable({pageLength:5, order:[[0,'asc']]});
}
})();
</script>

View File

@ -0,0 +1,124 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Purchase Orders & Receiving</h5>
<button class="btn btn-primary btn-sm" data-bs-toggle="collapse" data-bs-target="#poCreateBox">New PO</button>
</div>
<div class="card-body">
<div id="purchaseAlert" class="alert alert-danger d-none"></div>
<div id="poCreateBox" class="collapse show mb-3">
<form id="poForm" class="row g-2">
<?= csrf_field() ?>
<div class="col-md-3">
<select class="form-select" name="supplier_id" required>
<option value="">Supplier</option>
<?php foreach (($suppliers ?? []) as $sp): ?>
<option value="<?= (int) ($sp['id'] ?? 0) ?>"><?= esc((string) ($sp['name'] ?? '')) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2"><input class="form-control" type="date" name="order_date"></div>
<div class="col-md-2"><input class="form-control" type="date" name="expected_date"></div>
<div class="col-md-3"><input class="form-control" name="remarks" placeholder="PO remarks"></div>
<div class="col-md-2"><button class="btn btn-success btn-sm" type="submit">Create PO</button></div>
</form>
<div class="row g-2 mt-1">
<div class="col-md-5">
<select id="poItemSelect" class="form-select form-select-sm">
<option value="">Select item</option>
<?php foreach (($items ?? []) as $it): ?>
<option value="<?= (int) ($it['id'] ?? 0) ?>"><?= esc((string) (($it['item_code'] ?? '') . ' - ' . ($it['name'] ?? ''))) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2"><input type="number" step="0.01" min="0.01" id="poQty" class="form-control form-control-sm" placeholder="Qty"></div>
<div class="col-md-2"><input type="number" step="0.01" min="0" id="poRate" class="form-control form-control-sm" placeholder="Rate"></div>
<div class="col-md-2"><input type="number" step="0.01" min="0" id="poTax" class="form-control form-control-sm" placeholder="Tax %"></div>
<div class="col-md-1"><button id="addPoItem" type="button" class="btn btn-outline-primary btn-sm w-100">Add</button></div>
</div>
<table class="table table-sm mt-2" id="poItemsTable">
<thead><tr><th>Item</th><th>Qty</th><th>Rate</th><th>Tax %</th><th></th></tr></thead>
<tbody></tbody>
</table>
</div>
<h6>Purchase Orders</h6>
<table class="table table-striped table-sm" id="poTable">
<thead><tr><th>PO No</th><th>Date</th><th>Supplier</th><th>Status</th><th>Total</th><th>Action</th></tr></thead>
<tbody>
<?php foreach (($purchaseOrders ?? []) as $po): ?>
<tr>
<td><?= esc((string) ($po['po_code'] ?? '')) ?></td>
<td><?= esc((string) ($po['order_date'] ?? '')) ?></td>
<td><?= esc((string) ($po['supplier_name'] ?? '')) ?></td>
<td><?= esc((string) ($po['status'] ?? 'ordered')) ?></td>
<td>-</td>
<td>
<?php if (! in_array((string) ($po['status'] ?? ''), ['completed', 'cancelled'], true)): ?>
<button class="btn btn-outline-success btn-sm receive-po" data-id="<?= (int) ($po['id'] ?? 0) ?>">Receive</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<script>
(function(){
var poItems=[];
function showErr(msg){ var b=document.getElementById('purchaseAlert'); if(!b) return; b.textContent=msg; b.classList.remove('d-none'); }
function clearErr(){ var b=document.getElementById('purchaseAlert'); if(!b) return; b.textContent=''; b.classList.add('d-none'); }
function reloadPurchase(){ load_form_div('<?= base_url('setting/admin/hospital-stock/purchase') ?>','stockmaindiv','Stock Purchase'); }
function renderPoItems(){
var $tb = $('#poItemsTable tbody'); $tb.empty();
poItems.forEach(function(it,idx){
$tb.append('<tr><td>'+it.item_name+'</td><td>'+it.quantity+'</td><td>'+it.unit_rate+'</td><td>'+it.tax_percent+'</td><td><button type="button" class="btn btn-outline-danger btn-sm remove-po-item" data-idx="'+idx+'">X</button></td></tr>');
});
}
$('#addPoItem').on('click', function(){
clearErr();
var id = parseInt($('#poItemSelect').val() || '0', 10);
var qty = parseFloat($('#poQty').val() || '0');
var rate = parseFloat($('#poRate').val() || '0');
var tax = parseFloat($('#poTax').val() || '0');
var itemName = $('#poItemSelect option:selected').text();
if(id <= 0 || qty <= 0){ showErr('Select item and quantity.'); return; }
poItems.push({ item_id:id, quantity:qty, unit_rate:rate, tax_percent:tax, item_name:itemName });
renderPoItems();
$('#poQty,#poRate,#poTax').val('');
});
$(document).on('click','.remove-po-item', function(){
var idx = parseInt($(this).data('idx') || '-1', 10);
if(idx >= 0){ poItems.splice(idx,1); renderPoItems(); }
});
$('#poForm').on('submit', function(e){
e.preventDefault(); clearErr();
if(poItems.length === 0){ showErr('Add at least one item.'); return; }
var data = $(this).serializeArray();
poItems.forEach(function(it){
data.push({name:'item_id[]', value: it.item_id});
data.push({name:'qty[]', value: it.quantity});
data.push({name:'unit_cost[]', value: it.unit_rate});
});
$.post('<?= base_url('setting/admin/hospital-stock/po/create') ?>', $.param(data))
.done(function(resp){ if(resp && resp.error_text){showErr(resp.error_text); return;} reloadPurchase(); })
.fail(function(){ showErr('Save failed.'); });
});
$(document).on('click','.receive-po', function(){
if(!confirm('Mark this PO as received and update stock ledger?')) return;
$.post('<?= base_url('setting/admin/hospital-stock/po/receive') ?>',{purchase_order_id:$(this).data('id'),'<?= csrf_token() ?>':'<?= csrf_hash() ?>'})
.done(function(resp){ if(resp && resp.error_text){showErr(resp.error_text); return;} reloadPurchase(); })
.fail(function(){ showErr('Receive failed.'); });
});
if($.fn && $.fn.DataTable){ $('#poTable').DataTable({pageLength:10, order:[[1,'desc']]}); }
})();
</script>

View File

@ -0,0 +1,56 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= esc((string) ($title ?? 'Report')) ?></title>
<style>
body { font-family: Arial, sans-serif; color: #1f2937; margin: 20px; }
h2 { margin: 0 0 4px 0; font-size: 22px; }
.subtitle { margin: 0 0 16px 0; color: #6b7280; font-size: 14px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #d1d5db; padding: 8px; font-size: 12px; }
th { background: #f3f4f6; text-align: left; }
.no-data { padding: 16px; color: #6b7280; border: 1px dashed #d1d5db; }
.print-note { margin-top: 10px; color: #6b7280; font-size: 12px; }
@media print {
.print-note { display: none; }
body { margin: 0; }
}
</style>
</head>
<body>
<h2><?= esc((string) ($title ?? 'Report')) ?></h2>
<p class="subtitle"><?= esc((string) ($subtitle ?? '')) ?></p>
<?php if (! empty($rows ?? [])): ?>
<table>
<thead>
<tr>
<?php foreach (($columns ?? []) as $col): ?>
<th><?= esc((string) $col) ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach (($rows ?? []) as $row): ?>
<tr>
<?php foreach ($row as $cell): ?>
<td><?= esc((string) $cell) ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div class="no-data">No records found for selected criteria.</div>
<?php endif; ?>
<p class="print-note">Generated at <?= esc(date('Y-m-d H:i:s')) ?> | Use browser print command.</p>
<script>
window.addEventListener('load', function () {
window.print();
});
</script>
</body>
</html>

View File

@ -0,0 +1,83 @@
<div class="card">
<div class="card-header"><h5 class="mb-0">Stock Reports & Alerts</h5></div>
<div class="card-body">
<div class="row g-2 align-items-end mb-3" id="reportFilterForm">
<div class="col-md-2">
<label class="form-label">From</label>
<input class="form-control" type="date" id="dateFrom" value="<?= date('Y-m-01') ?>">
</div>
<div class="col-md-2">
<label class="form-label">To</label>
<input class="form-control" type="date" id="dateTo" value="<?= date('Y-m-d') ?>">
</div>
<div class="col-md-2">
<label class="form-label">Near Expiry (Days)</label>
<input class="form-control" type="number" id="expiryDays" min="1" max="3650" value="60">
</div>
<div class="col-md-6 d-flex gap-2">
<button class="btn btn-outline-primary btn-sm" type="button" id="btnDeptConsumption">Department Consumption</button>
<button class="btn btn-outline-secondary btn-sm" type="button" id="btnMonthlyIssue">Monthly Issue</button>
<button class="btn btn-outline-danger btn-sm" type="button" id="btnNearExpiry">Near Expiry</button>
</div>
</div>
<div class="alert alert-info mb-3">
Reports open in print-ready format in a new tab.
</div>
<h6>Current Alert Items (Low / Reorder)</h6>
<table class="table table-sm table-striped" id="stockAlertsTable">
<thead>
<tr>
<th>Item Code</th>
<th>Item Name</th>
<th>Current Stock</th>
<th>Min Stock</th>
<th>Reorder Level</th>
<th>Expiry Date</th>
</tr>
</thead>
<tbody>
<?php foreach (($alerts ?? []) as $r): ?>
<tr>
<td><?= esc((string) ($r['item_code'] ?? '')) ?></td>
<td><?= esc((string) ($r['name'] ?? '')) ?></td>
<td><?= esc((string) ($r['current_stock'] ?? '0')) ?></td>
<td><?= esc((string) ($r['min_stock_level'] ?? '0')) ?></td>
<td><?= esc((string) ($r['reorder_level'] ?? '0')) ?></td>
<td><?= esc((string) ($r['expiry_date'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<script>
(function(){
function q(v){ return encodeURIComponent(v || ''); }
$('#btnDeptConsumption').on('click', function(){
var from = $('#dateFrom').val();
var to = $('#dateTo').val();
var url = '<?= base_url('setting/admin/hospital-stock/report/department-consumption') ?>' + '?from=' + q(from) + '&to=' + q(to);
window.open(url, '_blank');
});
$('#btnMonthlyIssue').on('click', function(){
var year = (new Date($('#dateFrom').val() || new Date())).getFullYear();
var url = '<?= base_url('setting/admin/hospital-stock/report/monthly-issue') ?>' + '?year=' + q(year);
window.open(url, '_blank');
});
$('#btnNearExpiry').on('click', function(){
var days = $('#expiryDays').val() || 60;
var url = '<?= base_url('setting/admin/hospital-stock/report/near-expiry') ?>' + '?days=' + q(days);
window.open(url, '_blank');
});
if($.fn && $.fn.DataTable){
$('#stockAlertsTable').DataTable({pageLength:15, order:[[2, 'asc']]});
}
})();
</script>

View File

@ -113,6 +113,12 @@ if (! $canDiagnosis && $user && method_exists($user, 'inGroup')) {
<span>Bed Status</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('setting/admin/hospital-stock') ?>','Hospital Stock Management');">
<i class="bi bi-box-seam"></i>
<span>Hospital Stock</span>
</a>
</li>
<?php if ($canBilling) { ?>
<li class="nav-heading">Billing</li>