feat(stock): add modular hospital stock management with dashboard, masters, indents, purchase and reports
This commit is contained in:
parent
584c9ce59d
commit
d8f75b3a29
|
|
@ -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' => [
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
1211
app/Controllers/Setting/HospitalStock.php
Normal file
1211
app/Controllers/Setting/HospitalStock.php
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
14
app/Models/HospitalStockCategoryModel.php
Normal file
14
app/Models/HospitalStockCategoryModel.php
Normal 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;
|
||||
}
|
||||
33
app/Models/HospitalStockItemModel.php
Normal file
33
app/Models/HospitalStockItemModel.php
Normal 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;
|
||||
}
|
||||
22
app/Models/HospitalStockSupplierModel.php
Normal file
22
app/Models/HospitalStockSupplierModel.php
Normal 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;
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
98
app/Views/Setting/Stock/dashboard.php
Normal file
98
app/Views/Setting/Stock/dashboard.php
Normal 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>
|
||||
138
app/Views/Setting/Stock/indents.php
Normal file
138
app/Views/Setting/Stock/indents.php
Normal 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>
|
||||
84
app/Views/Setting/Stock/index.php
Normal file
84
app/Views/Setting/Stock/index.php
Normal 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>
|
||||
259
app/Views/Setting/Stock/masters.php
Normal file
259
app/Views/Setting/Stock/masters.php
Normal 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>
|
||||
124
app/Views/Setting/Stock/purchase.php
Normal file
124
app/Views/Setting/Stock/purchase.php
Normal 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>
|
||||
56
app/Views/Setting/Stock/report_print.php
Normal file
56
app/Views/Setting/Stock/report_print.php
Normal 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>
|
||||
83
app/Views/Setting/Stock/reports.php
Normal file
83
app/Views/Setting/Stock/reports.php
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user