feat(medical-store): inward purchase medicine auto-search and shared pharmacy master integration
This commit is contained in:
parent
101e0631a8
commit
62397c5a98
|
|
@ -1663,6 +1663,9 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct
|
|||
$routes->get('medical-store/patient/search', 'MedicalStoreApi::searchPatient');
|
||||
$routes->get('medical-store/patient/prescription/(:segment)/(:num)', 'MedicalStoreApi::getPrescription/$1/$2');
|
||||
$routes->get('medical-store/items/search', 'MedicalStoreApi::searchItems');
|
||||
$routes->get('medical-store/items/search-master', 'MedicalStoreApi::searchMasterItems');
|
||||
$routes->get('medical-store/items/master-support-data', 'MedicalStoreApi::getMasterSupportData');
|
||||
$routes->post('medical-store/items/save-master', 'MedicalStoreApi::saveProductMaster');
|
||||
$routes->post('medical-store/stock/opening', 'MedicalStoreApi::saveOpeningStock');
|
||||
$routes->get('medical-store/stock/list', 'MedicalStoreApi::stockList');
|
||||
$routes->get('medical-store/stock/alerts', 'MedicalStoreApi::stockAlerts');
|
||||
|
|
|
|||
|
|
@ -350,6 +350,393 @@ class MedicalStoreApi extends BaseController
|
|||
return $this->response->setJSON(['status' => 1, 'items' => $enriched]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches both med_product_master and mst_items for purchase inward autocomplete.
|
||||
*/
|
||||
public function searchMasterItems()
|
||||
{
|
||||
$q = trim($this->request->getGet('q') ?? '');
|
||||
|
||||
$results = [];
|
||||
$seenNames = [];
|
||||
|
||||
// 1. Query mst_items
|
||||
$builder = $this->db->table('mst_items i');
|
||||
$builder->select('i.item_id, i.product_master_id, i.item_name, i.generic_name, i.category, i.hsn_code, i.gst_rate, i.unit_pack, i.units_per_pack, i.drug_schedule, i.manufacturer_name');
|
||||
$builder->where('i.is_active', 1);
|
||||
|
||||
if (!empty($q)) {
|
||||
$builder->groupStart()
|
||||
->like('i.item_name', $q)
|
||||
->orLike('i.generic_name', $q)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$mstItems = $builder->limit(40)->get()->getResultArray();
|
||||
|
||||
foreach ($mstItems as $it) {
|
||||
$nameKey = strtolower(trim($it['item_name']));
|
||||
$seenNames[$nameKey] = true;
|
||||
$results[] = [
|
||||
'item_id' => (int)$it['item_id'],
|
||||
'product_master_id' => !empty($it['product_master_id']) ? (int)$it['product_master_id'] : null,
|
||||
'item_name' => $it['item_name'],
|
||||
'generic_name' => $it['generic_name'] ?? '',
|
||||
'category' => $it['category'] ?? 'Tablet',
|
||||
'hsn_code' => $it['hsn_code'] ?? '3004',
|
||||
'gst_rate' => (float)($it['gst_rate'] ?? 12.00),
|
||||
'unit_pack' => $it['unit_pack'] ?? '',
|
||||
'units_per_pack' => (int)($it['units_per_pack'] ?? 10),
|
||||
'drug_schedule' => $it['drug_schedule'] ?? 'OTC',
|
||||
'manufacturer_name' => $it['manufacturer_name'] ?? '',
|
||||
'source' => 'mst_items'
|
||||
];
|
||||
}
|
||||
|
||||
// 2. Query legacy/shared med_product_master
|
||||
if ($this->db->tableExists('med_product_master')) {
|
||||
$pmBuilder = $this->db->table('med_product_master p');
|
||||
$pmBuilder->select('p.id, p.item_name, p.formulation, p.genericname, p.packing, p.HSNCODE, p.CGST_per, p.SGST_per, p.company_name, p.mfgname, p.schedule_h, p.schedule_h1, p.schedule_x, p.narcotic');
|
||||
$pmBuilder->where('p.is_continue', 1);
|
||||
|
||||
if (!empty($q)) {
|
||||
$pmBuilder->groupStart()
|
||||
->like('p.item_name', $q)
|
||||
->orLike('p.genericname', $q)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$pmItems = $pmBuilder->limit(40)->get()->getResultArray();
|
||||
|
||||
foreach ($pmItems as $pm) {
|
||||
$nameKey = strtolower(trim($pm['item_name']));
|
||||
if (isset($seenNames[$nameKey])) {
|
||||
continue;
|
||||
}
|
||||
$seenNames[$nameKey] = true;
|
||||
|
||||
$packNum = (int)preg_replace('/[^0-9]/', '', (string)($pm['packing'] ?? ''));
|
||||
if ($packNum <= 0) $packNum = 10;
|
||||
|
||||
$gst = (float)($pm['CGST_per'] ?? 0) + (float)($pm['SGST_per'] ?? 0);
|
||||
if ($gst <= 0) $gst = 12.00;
|
||||
|
||||
$schedule = 'OTC';
|
||||
if (!empty($pm['schedule_h1'])) $schedule = 'Schedule H1';
|
||||
else if (!empty($pm['schedule_h'])) $schedule = 'Schedule H';
|
||||
else if (!empty($pm['schedule_x'])) $schedule = 'Schedule X';
|
||||
else if (!empty($pm['narcotic'])) $schedule = 'Narcotic';
|
||||
|
||||
$company = !empty($pm['company_name']) && $pm['company_name'] !== '0' ? $pm['company_name'] : (!empty($pm['mfgname']) && $pm['mfgname'] !== '0' ? $pm['mfgname'] : '');
|
||||
$formulation = !empty($pm['formulation']) && $pm['formulation'] !== '0' ? trim($pm['formulation']) : 'Tablet';
|
||||
|
||||
// Ensure it exists in mst_items with linked product_master_id
|
||||
$existingMst = $this->db->table('mst_items')
|
||||
->where('product_master_id', (int)$pm['id'])
|
||||
->orWhere('LOWER(TRIM(item_name))', $nameKey)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($existingMst) {
|
||||
$itemId = (int)$existingMst['item_id'];
|
||||
if (empty($existingMst['product_master_id'])) {
|
||||
$this->db->table('mst_items')->where('item_id', $itemId)->update(['product_master_id' => (int)$pm['id']]);
|
||||
}
|
||||
} else {
|
||||
$newMstData = [
|
||||
'product_master_id' => (int)$pm['id'],
|
||||
'item_name' => trim($pm['item_name']),
|
||||
'generic_name' => trim($pm['genericname'] ?? ''),
|
||||
'category' => $formulation,
|
||||
'hsn_code' => !empty($pm['HSNCODE']) && $pm['HSNCODE'] !== '0' ? trim($pm['HSNCODE']) : '3004',
|
||||
'gst_rate' => $gst,
|
||||
'unit_pack' => $packNum . ' ' . $formulation,
|
||||
'units_per_pack' => $packNum,
|
||||
'drug_schedule' => $schedule,
|
||||
'manufacturer_name' => $company,
|
||||
'is_active' => 1,
|
||||
'min_reorder_level' => 10
|
||||
];
|
||||
$this->db->table('mst_items')->insert($newMstData);
|
||||
$itemId = (int)$this->db->insertID();
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'item_id' => $itemId,
|
||||
'product_master_id' => (int)$pm['id'],
|
||||
'item_name' => trim($pm['item_name']),
|
||||
'generic_name' => trim($pm['genericname'] ?? ''),
|
||||
'category' => $formulation,
|
||||
'hsn_code' => !empty($pm['HSNCODE']) && $pm['HSNCODE'] !== '0' ? trim($pm['HSNCODE']) : '3004',
|
||||
'gst_rate' => $gst,
|
||||
'unit_pack' => $packNum . ' ' . $formulation,
|
||||
'units_per_pack' => $packNum,
|
||||
'drug_schedule' => $schedule,
|
||||
'manufacturer_name' => $company,
|
||||
'source' => 'med_product_master'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'items' => $results
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns support table data (formulations, companies, GST rates) for adding medicines to master.
|
||||
*/
|
||||
public function getMasterSupportData()
|
||||
{
|
||||
$formulations = [];
|
||||
if ($this->db->tableExists('med_formulation')) {
|
||||
$rows = $this->db->table('med_formulation')
|
||||
->select('formulation, formulation_length')
|
||||
->where('formulation !=', '')
|
||||
->where('formulation !=', '0')
|
||||
->orderBy('formulation', 'ASC')
|
||||
->limit(100)
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($rows as $r) {
|
||||
$f = trim($r['formulation']);
|
||||
if ($f !== '' && !in_array($f, $formulations, true)) {
|
||||
$formulations[] = $f;
|
||||
}
|
||||
}
|
||||
}
|
||||
$defaults = ['Tablet', 'Capsule', 'Syrup', 'Injection', 'Ointment', 'Cream', 'Gel', 'Drops', 'Suspension', 'Inhaler', 'IV Fluid', 'Powder', 'Lotion', 'Mouthwash', 'Spray', 'Soap'];
|
||||
foreach ($defaults as $d) {
|
||||
if (!in_array($d, $formulations, true)) {
|
||||
array_unshift($formulations, $d);
|
||||
}
|
||||
}
|
||||
|
||||
$companies = [];
|
||||
if ($this->db->tableExists('med_company')) {
|
||||
$cRows = $this->db->table('med_company')
|
||||
->select('id, company_name')
|
||||
->where('company_name !=', '')
|
||||
->where('company_name !=', '0')
|
||||
->orderBy('company_name', 'ASC')
|
||||
->limit(200)
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($cRows as $cr) {
|
||||
$cn = trim($cr['company_name']);
|
||||
if ($cn !== '' && !in_array($cn, $companies, true)) {
|
||||
$companies[] = $cn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$gstRates = [0, 5, 12, 18, 28];
|
||||
if ($this->db->tableExists('med_gst_per')) {
|
||||
$gRows = $this->db->table('med_gst_per')->select('gst_per')->orderBy('gst_per', 'ASC')->get()->getResultArray();
|
||||
if (!empty($gRows)) {
|
||||
$customRates = [];
|
||||
foreach ($gRows as $gr) {
|
||||
$r = (float)$gr['gst_per'];
|
||||
if (!in_array($r, $customRates, true)) {
|
||||
$customRates[] = $r;
|
||||
}
|
||||
}
|
||||
if (!empty($customRates)) {
|
||||
$gstRates = $customRates;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$schedules = ['OTC', 'Schedule H', 'Schedule H1', 'Schedule X', 'Schedule G', 'Narcotic'];
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'formulations' => array_values($formulations),
|
||||
'companies' => array_values($companies),
|
||||
'gst_rates' => $gstRates,
|
||||
'schedules' => $schedules
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new or updated medicine into the shared med_product_master and mirrors into mst_items.
|
||||
*/
|
||||
public function saveProductMaster()
|
||||
{
|
||||
$json = $this->request->getJSON(true) ?: $this->request->getPost();
|
||||
$itemName = trim($json['item_name'] ?? '');
|
||||
|
||||
if ($itemName === '') {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
'status' => 0,
|
||||
'message' => 'Medicine Name is required.'
|
||||
]);
|
||||
}
|
||||
|
||||
$genericName = trim($json['generic_name'] ?? '');
|
||||
$formulation = trim($json['formulation'] ?? $json['category'] ?? 'Tablet');
|
||||
if ($formulation === '') $formulation = 'Tablet';
|
||||
$unitsPerPack = max(1, (int)($json['units_per_pack'] ?? 10));
|
||||
$packing = trim($json['packing'] ?? (string)$unitsPerPack);
|
||||
$hsnCode = trim($json['hsn_code'] ?? '3004');
|
||||
$gstRate = (float)($json['gst_rate'] ?? 12.00);
|
||||
$drugSchedule = trim($json['drug_schedule'] ?? 'OTC');
|
||||
$companyName = trim($json['company_name'] ?? $json['manufacturer_name'] ?? '');
|
||||
$minReorder = max(1, (int)($json['min_reorder_level'] ?? 10));
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
// 1. Sync support table med_formulation
|
||||
$formulationId = 0;
|
||||
if ($this->db->tableExists('med_formulation') && $formulation !== '') {
|
||||
$existingForm = $this->db->table('med_formulation')
|
||||
->where('LOWER(TRIM(formulation))', strtolower($formulation))
|
||||
->get()
|
||||
->getRowArray();
|
||||
if ($existingForm) {
|
||||
$formulationId = (int)$existingForm['id'];
|
||||
} else {
|
||||
$this->db->table('med_formulation')->insert([
|
||||
'formulation' => $formulation,
|
||||
'formulation_length' => $formulation
|
||||
]);
|
||||
$formulationId = (int)$this->db->insertID();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Sync support table med_company
|
||||
$companyId = 0;
|
||||
if ($this->db->tableExists('med_company') && $companyName !== '') {
|
||||
$existingComp = $this->db->table('med_company')
|
||||
->where('LOWER(TRIM(company_name))', strtolower($companyName))
|
||||
->get()
|
||||
->getRowArray();
|
||||
if ($existingComp) {
|
||||
$companyId = (int)$existingComp['id'];
|
||||
} else {
|
||||
$this->db->table('med_company')->insert([
|
||||
'company_name' => $companyName
|
||||
]);
|
||||
$companyId = (int)$this->db->insertID();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Save into med_product_master
|
||||
$productMasterId = 0;
|
||||
$halfGst = round($gstRate / 2, 2);
|
||||
if ($this->db->tableExists('med_product_master')) {
|
||||
$pFields = $this->db->getFieldNames('med_product_master') ?? [];
|
||||
|
||||
$pmRow = $this->db->table('med_product_master')
|
||||
->where('LOWER(TRIM(item_name))', strtolower($itemName))
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
$pmData = [
|
||||
'item_name' => $itemName,
|
||||
'formulation' => $formulation,
|
||||
'formulation_id' => $formulationId,
|
||||
'genericname' => $genericName,
|
||||
'packing' => $packing,
|
||||
'unit_1' => '0',
|
||||
'unit_2' => '0',
|
||||
'HSNCODE' => $hsnCode,
|
||||
'CGST_per' => $halfGst,
|
||||
'SGST_per' => $halfGst,
|
||||
'company_name' => $companyName,
|
||||
'company_id' => $companyId,
|
||||
'mfgname' => $companyName,
|
||||
're_order_qty' => $minReorder,
|
||||
'is_continue' => 1,
|
||||
'batch_applicable' => 1,
|
||||
'exp_date_applicable' => 1,
|
||||
'schedule_h' => (stripos($drugSchedule, 'Schedule H1') === false && stripos($drugSchedule, 'Schedule H') !== false) ? 1 : 0,
|
||||
'schedule_h1' => stripos($drugSchedule, 'Schedule H1') !== false ? 1 : 0,
|
||||
'schedule_x' => stripos($drugSchedule, 'Schedule X') !== false ? 1 : 0,
|
||||
'schedule_g' => stripos($drugSchedule, 'Schedule G') !== false ? 1 : 0,
|
||||
'narcotic' => stripos($drugSchedule, 'Narcotic') !== false ? 1 : 0,
|
||||
'update_by' => 'MedicalStore'
|
||||
];
|
||||
|
||||
$filteredPm = [];
|
||||
foreach ($pmData as $k => $v) {
|
||||
if (in_array($k, $pFields, true)) {
|
||||
$filteredPm[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
if ($pmRow) {
|
||||
$productMasterId = (int)$pmRow['id'];
|
||||
$this->db->table('med_product_master')->where('id', $productMasterId)->update($filteredPm);
|
||||
} else {
|
||||
if (in_array('insert_by', $pFields, true)) {
|
||||
$filteredPm['insert_by'] = 'MedicalStore';
|
||||
}
|
||||
$this->db->table('med_product_master')->insert($filteredPm);
|
||||
$productMasterId = (int)$this->db->insertID();
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Save/mirror into mst_items
|
||||
$mstRow = $this->db->table('mst_items')
|
||||
->where('LOWER(TRIM(item_name))', strtolower($itemName))
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
$mstData = [
|
||||
'product_master_id' => $productMasterId > 0 ? $productMasterId : null,
|
||||
'item_name' => $itemName,
|
||||
'generic_name' => $genericName,
|
||||
'category' => $formulation,
|
||||
'hsn_code' => $hsnCode,
|
||||
'gst_rate' => $gstRate,
|
||||
'unit_pack' => $unitsPerPack . ' ' . $formulation,
|
||||
'units_per_pack' => $unitsPerPack,
|
||||
'drug_schedule' => $drugSchedule,
|
||||
'manufacturer_name' => $companyName,
|
||||
'min_reorder_level' => $minReorder,
|
||||
'is_active' => 1,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
if ($mstRow) {
|
||||
$itemId = (int)$mstRow['item_id'];
|
||||
$this->db->table('mst_items')->where('item_id', $itemId)->update($mstData);
|
||||
} else {
|
||||
$mstData['created_at'] = date('Y-m-d H:i:s');
|
||||
$this->db->table('mst_items')->insert($mstData);
|
||||
$itemId = (int)$this->db->insertID();
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'status' => 0,
|
||||
'message' => 'Database transaction failed while saving medicine to master.'
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'message' => 'Medicine "' . $itemName . '" added to Master successfully!',
|
||||
'item' => [
|
||||
'item_id' => $itemId,
|
||||
'product_master_id' => $productMasterId,
|
||||
'item_name' => $itemName,
|
||||
'generic_name' => $genericName,
|
||||
'category' => $formulation,
|
||||
'units_per_pack' => $unitsPerPack,
|
||||
'packing' => $packing,
|
||||
'hsn_code' => $hsnCode,
|
||||
'gst_rate' => $gstRate,
|
||||
'drug_schedule' => $drugSchedule,
|
||||
'manufacturer_name' => $companyName
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveOpeningStock()
|
||||
{
|
||||
$json = $this->request->getJSON(true) ?: $this->request->getPost();
|
||||
|
|
@ -1682,7 +2069,61 @@ class MedicalStoreApi extends BaseController
|
|||
$purchaseItems = [];
|
||||
|
||||
foreach ($items as $it) {
|
||||
$itemId = (int)$it['item_id'];
|
||||
$itemId = (int)($it['item_id'] ?? 0);
|
||||
$itemName = trim($it['item_name'] ?? '');
|
||||
|
||||
if ($itemId <= 0 && $itemName !== '') {
|
||||
$mst = $this->db->table('mst_items')
|
||||
->where('LOWER(TRIM(item_name))', strtolower($itemName))
|
||||
->get()
|
||||
->getRowArray();
|
||||
if ($mst) {
|
||||
$itemId = (int)$mst['item_id'];
|
||||
} else {
|
||||
$uPack = (int)($it['units_per_pack'] ?? 10);
|
||||
$gRate = (float)($it['gst_rate'] ?? 12.00);
|
||||
$hCode = trim($it['hsn_code'] ?? '3004');
|
||||
|
||||
$pmId = 0;
|
||||
if ($this->db->tableExists('med_product_master')) {
|
||||
$pmRow = $this->db->table('med_product_master')
|
||||
->where('LOWER(TRIM(item_name))', strtolower($itemName))
|
||||
->get()
|
||||
->getRowArray();
|
||||
if ($pmRow) {
|
||||
$pmId = (int)$pmRow['id'];
|
||||
} else {
|
||||
$this->db->table('med_product_master')->insert([
|
||||
'item_name' => $itemName,
|
||||
'formulation' => 'Tablet',
|
||||
'genericname' => '',
|
||||
'packing' => (string)$uPack,
|
||||
'HSNCODE' => $hCode,
|
||||
'CGST_per' => round($gRate / 2, 2),
|
||||
'SGST_per' => round($gRate / 2, 2),
|
||||
'is_continue' => 1,
|
||||
'insert_by' => 'Purchase Inward Auto'
|
||||
]);
|
||||
$pmId = (int)$this->db->insertID();
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->table('mst_items')->insert([
|
||||
'product_master_id' => $pmId > 0 ? $pmId : null,
|
||||
'item_name' => $itemName,
|
||||
'category' => 'Tablet',
|
||||
'hsn_code' => $hCode,
|
||||
'gst_rate' => $gRate,
|
||||
'units_per_pack' => $uPack,
|
||||
'unit_pack' => $uPack . ' Units',
|
||||
'drug_schedule' => 'OTC',
|
||||
'is_active' => 1,
|
||||
'min_reorder_level' => 10
|
||||
]);
|
||||
$itemId = (int)$this->db->insertID();
|
||||
}
|
||||
}
|
||||
|
||||
$batchNo = trim($it['batch_no']);
|
||||
$expiryDate = trim($it['expiry_date']);
|
||||
$qtyPacks = (int)$it['qty_packs'];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class LinkMedicalStoreToPharmacyProductMaster extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
// 1. Add product_master_id to mst_items if it doesn't exist
|
||||
if ($db->tableExists('mst_items')) {
|
||||
$fields = $db->getFieldNames('mst_items') ?? [];
|
||||
if (!in_array('product_master_id', $fields, true)) {
|
||||
$this->forge->addColumn('mst_items', [
|
||||
'product_master_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'after' => 'item_id'
|
||||
]
|
||||
]);
|
||||
|
||||
// Add index
|
||||
$db->query("ALTER TABLE `mst_items` ADD INDEX `idx_mst_items_product_master_id` (`product_master_id`)");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Link existing mst_items to med_product_master where item_name matches
|
||||
if ($db->tableExists('mst_items') && $db->tableExists('med_product_master')) {
|
||||
$db->query("
|
||||
UPDATE `mst_items` i
|
||||
JOIN `med_product_master` p ON LOWER(TRIM(i.item_name)) = LOWER(TRIM(p.item_name))
|
||||
SET i.product_master_id = p.id
|
||||
WHERE i.product_master_id IS NULL OR i.product_master_id = 0
|
||||
");
|
||||
|
||||
// 3. For any mst_items not yet in med_product_master, sync them into med_product_master
|
||||
// so HMSPharmacy immediately has all MedicalStore medicines!
|
||||
$unlinkedItems = $db->table('mst_items')
|
||||
->where('product_master_id IS NULL', null, false)
|
||||
->orWhere('product_master_id', 0)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$pFields = $db->getFieldNames('med_product_master') ?? [];
|
||||
|
||||
foreach ($unlinkedItems as $item) {
|
||||
$itemName = trim($item['item_name']);
|
||||
if ($itemName === '') continue;
|
||||
|
||||
// Check if already in med_product_master
|
||||
$existing = $db->table('med_product_master')
|
||||
->where('LOWER(TRIM(item_name))', strtolower($itemName))
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($existing) {
|
||||
$db->table('mst_items')
|
||||
->where('item_id', $item['item_id'])
|
||||
->update(['product_master_id' => $existing['id']]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$gst = (float)($item['gst_rate'] ?? 12.00);
|
||||
$halfGst = round($gst / 2, 2);
|
||||
|
||||
$insertData = [
|
||||
'item_name' => $itemName,
|
||||
'formulation' => !empty($item['category']) ? trim($item['category']) : 'Tablet',
|
||||
'formulation_id' => 0,
|
||||
'genericname' => !empty($item['generic_name']) ? trim($item['generic_name']) : '',
|
||||
'packing' => !empty($item['units_per_pack']) ? (string)$item['units_per_pack'] : '10',
|
||||
'unit_1' => '0',
|
||||
'unit_2' => '0',
|
||||
'HSNCODE' => !empty($item['hsn_code']) ? trim($item['hsn_code']) : '3004',
|
||||
'CGST_per' => $halfGst,
|
||||
'SGST_per' => $halfGst,
|
||||
'company_name' => !empty($item['manufacturer_name']) ? trim($item['manufacturer_name']) : '',
|
||||
'company_id' => 0,
|
||||
'mfgname' => !empty($item['manufacturer_name']) ? trim($item['manufacturer_name']) : '',
|
||||
're_order_qty' => !empty($item['min_reorder_level']) ? (int)$item['min_reorder_level'] : 10,
|
||||
'is_continue' => 1,
|
||||
'batch_applicable' => 1,
|
||||
'exp_date_applicable' => 1,
|
||||
'schedule_h' => (stripos($item['drug_schedule'] ?? '', 'Schedule H1') === false && stripos($item['drug_schedule'] ?? '', 'Schedule H') !== false) ? 1 : 0,
|
||||
'schedule_h1' => stripos($item['drug_schedule'] ?? '', 'Schedule H1') !== false ? 1 : 0,
|
||||
'schedule_x' => stripos($item['drug_schedule'] ?? '', 'Schedule X') !== false ? 1 : 0,
|
||||
'insert_by' => 'MedicalStore Migration'
|
||||
];
|
||||
|
||||
// Filter only valid columns present in med_product_master
|
||||
$filtered = [];
|
||||
foreach ($insertData as $k => $v) {
|
||||
if (in_array($k, $pFields, true)) {
|
||||
$filtered[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$db->table('med_product_master')->insert($filtered);
|
||||
$newMasterId = (int)$db->insertID();
|
||||
if ($newMasterId > 0) {
|
||||
$db->table('mst_items')
|
||||
->where('item_id', $item['item_id'])
|
||||
->update(['product_master_id' => $newMasterId]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Migration med_product_master insert error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
if ($db->tableExists('mst_items')) {
|
||||
$fields = $db->getFieldNames('mst_items') ?? [];
|
||||
if (in_array('product_master_id', $fields, true)) {
|
||||
$this->forge->dropColumn('mst_items', 'product_master_id');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4041,14 +4041,49 @@ function PurchaseView({ store, notify }) {
|
|||
bank_details: ''
|
||||
});
|
||||
|
||||
// Master Medicine Autocomplete & Add Modal State
|
||||
const [activeSearchRow, setActiveSearchRow] = useState(null);
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
const [isSearchingMeds, setIsSearchingMeds] = useState(false);
|
||||
const [showMasterModal, setShowMasterModal] = useState(false);
|
||||
const [masterTargetRow, setMasterTargetRow] = useState(null);
|
||||
const [supportData, setSupportData] = useState({ formulations: [], companies: [], gst_rates: [0, 5, 12, 18, 28], schedules: [] });
|
||||
const [isSavingMaster, setIsSavingMaster] = useState(false);
|
||||
const [masterForm, setMasterForm] = useState({
|
||||
item_name: '',
|
||||
generic_name: '',
|
||||
formulation: 'Tablet',
|
||||
units_per_pack: 10,
|
||||
packing: '10',
|
||||
hsn_code: '3004',
|
||||
gst_rate: 12,
|
||||
drug_schedule: 'Schedule H',
|
||||
company_name: '',
|
||||
min_reorder_level: 10
|
||||
});
|
||||
const searchTimerRef = useRef(null);
|
||||
|
||||
const [rows, setRows] = useState([
|
||||
{ item_name: '', generic_name: '', batch_no: '', expiry_date: '', qty_packs: 1, free_qty_packs: 0, units_per_pack: 10, mrp: 0, ptr: 0, discount_pct: 0, gst_rate: 12 }
|
||||
{ item_id: 0, item_name: '', generic_name: '', category: 'Tablet', batch_no: '', expiry_date: '', qty_packs: 1, free_qty_packs: 0, units_per_pack: 10, mrp: 0, ptr: 0, discount_pct: 0, gst_rate: 12, hsn_code: '3004' }
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSuppliers();
|
||||
fetchMasterSupportData();
|
||||
}, []);
|
||||
|
||||
const fetchMasterSupportData = async () => {
|
||||
try {
|
||||
const res = await fetch(API_BASE + 'items/master-support-data');
|
||||
const data = await res.json();
|
||||
if (data.status) {
|
||||
setSupportData(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching master support data:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSuppliers = async () => {
|
||||
try {
|
||||
const res = await fetch(API_BASE + 'suppliers');
|
||||
|
|
@ -4161,7 +4196,7 @@ function PurchaseView({ store, notify }) {
|
|||
});
|
||||
|
||||
const addRow = () => {
|
||||
setRows([...rows, { item_name: '', generic_name: '', batch_no: '', expiry_date: '', qty_packs: 1, free_qty_packs: 0, units_per_pack: 10, mrp: 0, ptr: 0, discount_pct: 0, gst_rate: 12 }]);
|
||||
setRows([...rows, { item_id: 0, item_name: '', generic_name: '', category: 'Tablet', batch_no: '', expiry_date: '', qty_packs: 1, free_qty_packs: 0, units_per_pack: 10, mrp: 0, ptr: 0, discount_pct: 0, gst_rate: 12, hsn_code: '3004' }]);
|
||||
};
|
||||
|
||||
const updateRow = (idx, field, val) => {
|
||||
|
|
@ -4171,9 +4206,137 @@ function PurchaseView({ store, notify }) {
|
|||
};
|
||||
|
||||
const removeRow = (idx) => {
|
||||
if (rows.length <= 1) {
|
||||
setRows([{ item_id: 0, item_name: '', generic_name: '', category: 'Tablet', batch_no: '', expiry_date: '', qty_packs: 1, free_qty_packs: 0, units_per_pack: 10, mrp: 0, ptr: 0, discount_pct: 0, gst_rate: 12, hsn_code: '3004' }]);
|
||||
return;
|
||||
}
|
||||
setRows(rows.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
// Autocomplete searching for Medicine Name
|
||||
const handleMedicineInputChange = (rowIndex, value) => {
|
||||
updateRow(rowIndex, 'item_name', value);
|
||||
setActiveSearchRow(rowIndex);
|
||||
|
||||
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
||||
|
||||
if (!value.trim() || value.trim().length < 1) {
|
||||
setSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimerRef.current = setTimeout(async () => {
|
||||
setIsSearchingMeds(true);
|
||||
try {
|
||||
const res = await fetch(API_BASE + 'items/search-master?q=' + encodeURIComponent(value.trim()));
|
||||
const data = await res.json();
|
||||
if (data.status) {
|
||||
setSuggestions(data.items || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Master search error:', err);
|
||||
} finally {
|
||||
setIsSearchingMeds(false);
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const handleSelectMedicine = (rowIndex, item) => {
|
||||
const updated = [...rows];
|
||||
updated[rowIndex] = {
|
||||
...updated[rowIndex],
|
||||
item_id: item.item_id || 0,
|
||||
item_name: item.item_name,
|
||||
generic_name: item.generic_name || '',
|
||||
category: item.category || 'Tablet',
|
||||
units_per_pack: Number(item.units_per_pack) > 0 ? Number(item.units_per_pack) : 10,
|
||||
gst_rate: item.gst_rate !== undefined ? Number(item.gst_rate) : 12,
|
||||
hsn_code: item.hsn_code || '3004'
|
||||
};
|
||||
setRows(updated);
|
||||
setActiveSearchRow(null);
|
||||
setSuggestions([]);
|
||||
|
||||
// Focus Batch No field for this row
|
||||
setTimeout(() => {
|
||||
const batchInput = document.getElementById(`batch-input-${rowIndex}`);
|
||||
if (batchInput) {
|
||||
batchInput.focus();
|
||||
batchInput.select();
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const openAddMasterModal = (rowIndex = null, initialName = '') => {
|
||||
const prefillName = initialName || (rowIndex !== null && rows[rowIndex] ? rows[rowIndex].item_name : '');
|
||||
setMasterTargetRow(rowIndex);
|
||||
setMasterForm({
|
||||
item_name: prefillName,
|
||||
generic_name: '',
|
||||
formulation: 'Tablet',
|
||||
units_per_pack: 10,
|
||||
packing: '10',
|
||||
hsn_code: '3004',
|
||||
gst_rate: 12,
|
||||
drug_schedule: 'Schedule H',
|
||||
company_name: '',
|
||||
min_reorder_level: 10
|
||||
});
|
||||
setActiveSearchRow(null);
|
||||
setSuggestions([]);
|
||||
setShowMasterModal(true);
|
||||
};
|
||||
|
||||
const handleSaveMasterMedicine = async (e) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!masterForm.item_name.trim()) {
|
||||
notify('Medicine name is required.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSavingMaster(true);
|
||||
try {
|
||||
const res = await fetch(API_BASE + 'items/save-master', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(masterForm)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status && data.item) {
|
||||
notify(`"${data.item.item_name}" saved to Pharmacy Master & selected!`, 'success');
|
||||
setShowMasterModal(false);
|
||||
|
||||
// Auto-select into the target row or create a new row
|
||||
if (masterTargetRow !== null && masterTargetRow < rows.length) {
|
||||
handleSelectMedicine(masterTargetRow, data.item);
|
||||
} else {
|
||||
setRows([...rows, {
|
||||
item_id: data.item.item_id || 0,
|
||||
item_name: data.item.item_name,
|
||||
generic_name: data.item.generic_name || '',
|
||||
category: data.item.category || 'Tablet',
|
||||
batch_no: '',
|
||||
expiry_date: '',
|
||||
qty_packs: 1,
|
||||
free_qty_packs: 0,
|
||||
units_per_pack: Number(data.item.units_per_pack) || 10,
|
||||
mrp: 0,
|
||||
ptr: 0,
|
||||
discount_pct: 0,
|
||||
gst_rate: Number(data.item.gst_rate) || 12,
|
||||
hsn_code: data.item.hsn_code || '3004'
|
||||
}]);
|
||||
}
|
||||
} else {
|
||||
notify(data.message || 'Failed to save medicine to master.', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
notify('Error saving to master: ' + err.message, 'error');
|
||||
} finally {
|
||||
setIsSavingMaster(false);
|
||||
}
|
||||
};
|
||||
|
||||
const savePurchaseBill = async () => {
|
||||
if (!selectedSupplierId || !invoiceNo) {
|
||||
notify('Please select supplier and enter invoice number.', 'error');
|
||||
|
|
@ -4187,27 +4350,22 @@ function PurchaseView({ store, notify }) {
|
|||
}
|
||||
|
||||
try {
|
||||
const itemsPayload = [];
|
||||
for (const r of validItems) {
|
||||
const searchRes = await fetch(API_BASE + `items/search?store_id=${store.store_id}&q=${encodeURIComponent(r.item_name)}`);
|
||||
const sData = await searchRes.json();
|
||||
let itemId = 1;
|
||||
if (sData.status && sData.items && sData.items.length > 0) {
|
||||
itemId = sData.items[0].item_id;
|
||||
}
|
||||
itemsPayload.push({
|
||||
item_id: itemId,
|
||||
batch_no: r.batch_no,
|
||||
expiry_date: r.expiry_date,
|
||||
qty_packs: Number(r.qty_packs),
|
||||
free_qty_packs: Number(r.free_qty_packs || 0),
|
||||
units_per_pack: Number(r.units_per_pack || 10),
|
||||
mrp: Number(r.mrp),
|
||||
ptr: Number(r.ptr),
|
||||
discount_pct: Number(r.discount_pct || 0),
|
||||
gst_rate: Number(r.gst_rate || 12)
|
||||
});
|
||||
}
|
||||
const itemsPayload = validItems.map(r => ({
|
||||
item_id: Number(r.item_id || 0),
|
||||
item_name: r.item_name,
|
||||
generic_name: r.generic_name || '',
|
||||
category: r.category || 'Tablet',
|
||||
batch_no: r.batch_no,
|
||||
expiry_date: r.expiry_date,
|
||||
qty_packs: Number(r.qty_packs),
|
||||
free_qty_packs: Number(r.free_qty_packs || 0),
|
||||
units_per_pack: Number(r.units_per_pack || 10),
|
||||
mrp: Number(r.mrp),
|
||||
ptr: Number(r.ptr),
|
||||
discount_pct: Number(r.discount_pct || 0),
|
||||
gst_rate: Number(r.gst_rate || 12),
|
||||
hsn_code: r.hsn_code || '3004'
|
||||
}));
|
||||
|
||||
const res = await fetch(API_BASE + 'purchase/save', {
|
||||
method: 'POST',
|
||||
|
|
@ -4224,7 +4382,7 @@ function PurchaseView({ store, notify }) {
|
|||
if (data.status) {
|
||||
notify('Purchase inward recorded! Supplier Ledger & Stock updated.', 'success');
|
||||
setInvoiceNo('');
|
||||
setRows([{ item_name: '', generic_name: '', batch_no: '', expiry_date: '', qty_packs: 1, free_qty_packs: 0, units_per_pack: 10, mrp: 0, ptr: 0, discount_pct: 0, gst_rate: 12 }]);
|
||||
setRows([{ item_id: 0, item_name: '', generic_name: '', category: 'Tablet', batch_no: '', expiry_date: '', qty_packs: 1, free_qty_packs: 0, units_per_pack: 10, mrp: 0, ptr: 0, discount_pct: 0, gst_rate: 12, hsn_code: '3004' }]);
|
||||
} else {
|
||||
notify(data.message || 'Failed to save purchase bill.', 'error');
|
||||
}
|
||||
|
|
@ -4277,33 +4435,178 @@ function PurchaseView({ store, notify }) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-responsive">
|
||||
<div className="table-responsive" style={{ overflow: 'visible' }}>
|
||||
<table className="table table-pos table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Medicine Name</th>
|
||||
<th>Batch No</th>
|
||||
<th>Expiry</th>
|
||||
<th style={{ width: 70 }}>Packs</th>
|
||||
<th style={{ minWidth: 280 }}>
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<span>Medicine Name (Master Auto-Search)</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link btn-sm p-0 text-decoration-none fw-semibold"
|
||||
style={{ fontSize: '11px' }}
|
||||
onClick={() => openAddMasterModal(rows.length - 1, '')}
|
||||
>
|
||||
<i className="bi bi-plus-circle me-1"></i>+ Add to Master
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<th style={{ width: 130 }}>Batch No</th>
|
||||
<th style={{ width: 140 }}>Expiry</th>
|
||||
<th style={{ width: 75 }}>Packs</th>
|
||||
<th style={{ width: 65 }}>Free</th>
|
||||
<th style={{ width: 80 }}>Units/Pack</th>
|
||||
<th style={{ width: 85 }}>Units/Pack</th>
|
||||
<th style={{ width: 85 }}>PTR (₹)</th>
|
||||
<th style={{ width: 85 }}>MRP (₹)</th>
|
||||
<th style={{ width: 65 }}>GST%</th>
|
||||
<th style={{ width: 75 }}>GST%</th>
|
||||
<th style={{ width: 40 }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r, i) => (
|
||||
<tr key={i}>
|
||||
<td>
|
||||
<input type="text" className="form-control form-control-sm" placeholder="e.g. Augmentin 625" value={r.item_name} onChange={(e) => updateRow(i, 'item_name', e.target.value)} />
|
||||
<td style={{ position: 'relative' }}>
|
||||
<div className="input-group input-group-sm">
|
||||
<input
|
||||
type="text"
|
||||
id={`med-search-input-${i}`}
|
||||
className={`form-control form-control-sm fw-semibold ${r.item_id ? 'border-success' : ''}`}
|
||||
placeholder="Type medicine name (e.g. Augmentin)..."
|
||||
value={r.item_name}
|
||||
autoComplete="off"
|
||||
onChange={(e) => handleMedicineInputChange(i, e.target.value)}
|
||||
onFocus={(e) => {
|
||||
setActiveSearchRow(i);
|
||||
if (e.target.value.trim().length >= 1) {
|
||||
handleMedicineInputChange(i, e.target.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{r.item_id > 0 && (
|
||||
<span className="input-group-text bg-success text-white py-0 px-1.5" title="Item matched in Master Catalog">
|
||||
<i className="bi bi-check2"></i>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Subtext with Generic and Category */}
|
||||
{r.generic_name ? (
|
||||
<div className="text-muted text-truncate mt-1 d-flex align-items-center gap-1" style={{ fontSize: '11px', maxWidth: '270px' }}>
|
||||
<span className="badge bg-light text-primary border" style={{ fontSize: '9.5px' }}>{r.category || 'Tablet'}</span>
|
||||
<span>{r.generic_name}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Autocomplete Dropdown */}
|
||||
{activeSearchRow === i && (
|
||||
<div
|
||||
className="shadow-lg border rounded-2 bg-white"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
width: '380px',
|
||||
maxHeight: '280px',
|
||||
overflowY: 'auto',
|
||||
zIndex: 1060,
|
||||
marginTop: '2px'
|
||||
}}
|
||||
>
|
||||
{/* Dropdown Header */}
|
||||
<div className="bg-light px-2.5 py-1.5 border-bottom d-flex justify-content-between align-items-center">
|
||||
<span className="small fw-bold text-secondary" style={{ fontSize: '11px' }}>
|
||||
{isSearchingMeds ? (
|
||||
<span><i className="bi bi-hourglass-split spinner-border spinner-border-sm me-1"></i>Searching master...</span>
|
||||
) : (
|
||||
<span><i className="bi bi-database-check me-1 text-primary"></i>Pharmacy Master ({suggestions.length} found)</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
style={{ fontSize: '9px' }}
|
||||
onClick={() => setActiveSearchRow(null)}
|
||||
></button>
|
||||
</div>
|
||||
|
||||
{/* Suggestions List */}
|
||||
{suggestions.length > 0 ? (
|
||||
suggestions.map((item, sIdx) => (
|
||||
<div
|
||||
key={sIdx}
|
||||
className="p-2 border-bottom hover-bg-light"
|
||||
style={{ cursor: 'pointer', transition: 'background-color 0.15s' }}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
handleSelectMedicine(i, item);
|
||||
}}
|
||||
>
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<span className="fw-bold text-dark" style={{ fontSize: '12.5px' }}>{item.item_name}</span>
|
||||
<span className="badge bg-primary-subtle text-primary border" style={{ fontSize: '10px' }}>
|
||||
{item.category || 'Tablet'}
|
||||
</span>
|
||||
</div>
|
||||
{item.generic_name && (
|
||||
<div className="text-muted text-truncate small" style={{ fontSize: '11px' }}>
|
||||
{item.generic_name}
|
||||
</div>
|
||||
)}
|
||||
<div className="d-flex gap-2 mt-1 align-items-center" style={{ fontSize: '10.5px' }}>
|
||||
<span className="text-secondary">Pack: <strong>{item.units_per_pack || 10}s</strong></span>
|
||||
<span className="text-secondary">GST: <strong>{item.gst_rate}%</strong></span>
|
||||
<span className="text-secondary">HSN: <strong>{item.hsn_code || '3004'}</strong></span>
|
||||
{item.drug_schedule && item.drug_schedule !== 'OTC' && (
|
||||
<span className="badge bg-danger-subtle text-danger" style={{ fontSize: '9px' }}>{item.drug_schedule}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
!isSearchingMeds && (
|
||||
<div className="p-3 text-center text-muted">
|
||||
<i className="bi bi-search fs-4 d-block mb-1 text-secondary"></i>
|
||||
<div className="small fw-semibold">No medicine found matching "{r.item_name}"</div>
|
||||
<div className="text-muted" style={{ fontSize: '11px' }}>Click below to add it directly into Pharmacy Master</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Pinned Add to Master Option */}
|
||||
<div className="p-2 bg-light border-top text-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline-primary w-100 fw-bold py-1"
|
||||
style={{ fontSize: '12px' }}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
openAddMasterModal(i, r.item_name);
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-plus-circle-fill me-1"></i>+ Add "{r.item_name || 'New Medicine'}" in Master
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" className="form-control form-control-sm" placeholder="Batch" value={r.batch_no} onChange={(e) => updateRow(i, 'batch_no', e.target.value)} />
|
||||
<input
|
||||
type="text"
|
||||
id={`batch-input-${i}`}
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Batch No"
|
||||
value={r.batch_no}
|
||||
onChange={(e) => updateRow(i, 'batch_no', e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="date" className="form-control form-control-sm" value={r.expiry_date} onChange={(e) => updateRow(i, 'expiry_date', e.target.value)} />
|
||||
<input
|
||||
type="date"
|
||||
className="form-control form-control-sm"
|
||||
value={r.expiry_date}
|
||||
onChange={(e) => updateRow(i, 'expiry_date', e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" className="form-control form-control-sm" min="1" value={r.qty_packs} onChange={(e) => updateRow(i, 'qty_packs', e.target.value)} />
|
||||
|
|
@ -4326,10 +4629,11 @@ function PurchaseView({ store, notify }) {
|
|||
<option value="5">5%</option>
|
||||
<option value="12">12%</option>
|
||||
<option value="18">18%</option>
|
||||
<option value="28">28%</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button className="btn btn-sm btn-outline-danger py-0 px-1" onClick={() => removeRow(i)}>
|
||||
<button className="btn btn-sm btn-outline-danger py-0 px-1" onClick={() => removeRow(i)} title="Remove row">
|
||||
<i className="bi bi-x"></i>
|
||||
</button>
|
||||
</td>
|
||||
|
|
@ -4715,6 +5019,186 @@ function PurchaseView({ store, notify }) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Medicine Master Add Modal */}
|
||||
{showMasterModal && (
|
||||
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(15, 23, 42, 0.75)' }} tabIndex="-1">
|
||||
<div className="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
||||
<div className="modal-content border-0 shadow-lg">
|
||||
<div className="modal-header bg-dark text-white py-2.5 px-3 d-flex justify-content-between align-items-center">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<div className="bg-success text-white rounded p-1.5 d-flex align-items-center justify-content-center" style={{ width: 32, height: 32 }}>
|
||||
<i className="bi bi-capsule fs-5"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="modal-title mb-0 fw-bold">Add New Medicine to Pharmacy Master</h6>
|
||||
<small className="text-light opacity-75">Saved to shared master <code>med_product_master</code> (shared between MedicalStore & HMSPharmacy)</small>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="btn-close btn-close-white" onClick={() => setShowMasterModal(false)}></button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSaveMasterMedicine}>
|
||||
<div className="modal-body p-3 bg-light">
|
||||
<div className="card border-0 shadow-sm p-3 mb-3 bg-white">
|
||||
<h6 className="fw-bold text-primary mb-2" style={{ fontSize: '13px' }}>
|
||||
<i className="bi bi-prescription2 me-1"></i> Medicine Name & Formulation
|
||||
</h6>
|
||||
<div className="row g-2">
|
||||
<div className="col-md-7">
|
||||
<label className="form-label small fw-bold mb-1">Brand / Medicine Name <span className="text-danger">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm fw-semibold"
|
||||
placeholder="e.g. Augmentin 625 Duo Tablet"
|
||||
value={masterForm.item_name}
|
||||
onChange={(e) => setMasterForm({ ...masterForm, item_name: e.target.value })}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-5">
|
||||
<label className="form-label small fw-bold mb-1">Formulation / Category</label>
|
||||
<input
|
||||
type="text"
|
||||
list="ms-formulation-list"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="e.g. Tablet, Capsule, Syrup..."
|
||||
value={masterForm.formulation}
|
||||
onChange={(e) => setMasterForm({ ...masterForm, formulation: e.target.value })}
|
||||
/>
|
||||
<datalist id="ms-formulation-list">
|
||||
{(supportData.formulations || []).map((f, idx) => (
|
||||
<option key={idx} value={f} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form-label small fw-bold mb-1">Generic Molecule Composition</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="e.g. Amoxycillin (500mg) + Clavulanic Acid (125mg)"
|
||||
value={masterForm.generic_name}
|
||||
onChange={(e) => setMasterForm({ ...masterForm, generic_name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card border-0 shadow-sm p-3 bg-white">
|
||||
<h6 className="fw-bold text-primary mb-2" style={{ fontSize: '13px' }}>
|
||||
<i className="bi bi-box-seam me-1"></i> Packing, Tax & Regulatory Specifications
|
||||
</h6>
|
||||
<div className="row g-2">
|
||||
<div className="col-md-3">
|
||||
<label className="form-label small fw-bold mb-1">Units per Pack (Strip/Bottle) <span className="text-danger">*</span></label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
className="form-control form-control-sm fw-bold"
|
||||
placeholder="10"
|
||||
value={masterForm.units_per_pack}
|
||||
onChange={(e) => setMasterForm({ ...masterForm, units_per_pack: e.target.value, packing: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label className="form-label small fw-bold mb-1">GST Rate (%)</label>
|
||||
<select
|
||||
className="form-select form-select-sm fw-bold"
|
||||
value={masterForm.gst_rate}
|
||||
onChange={(e) => setMasterForm({ ...masterForm, gst_rate: Number(e.target.value) })}
|
||||
>
|
||||
{(supportData.gst_rates || [0, 5, 12, 18, 28]).map((g, idx) => (
|
||||
<option key={idx} value={g}>{g}% GST</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label className="form-label small fw-bold mb-1">HSN Code</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="3004"
|
||||
value={masterForm.hsn_code}
|
||||
onChange={(e) => setMasterForm({ ...masterForm, hsn_code: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label className="form-label small fw-bold mb-1">Drug Schedule</label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={masterForm.drug_schedule}
|
||||
onChange={(e) => setMasterForm({ ...masterForm, drug_schedule: e.target.value })}
|
||||
>
|
||||
{(supportData.schedules || ['OTC', 'Schedule H', 'Schedule H1', 'Schedule X', 'Narcotic']).map((sc, idx) => (
|
||||
<option key={idx} value={sc}>{sc}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-8">
|
||||
<label className="form-label small fw-bold mb-1">Manufacturer / Pharma Company</label>
|
||||
<input
|
||||
type="text"
|
||||
list="ms-company-list"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="e.g. Cipla Ltd, Sun Pharma, Alkem..."
|
||||
value={masterForm.company_name}
|
||||
onChange={(e) => setMasterForm({ ...masterForm, company_name: e.target.value })}
|
||||
/>
|
||||
<datalist id="ms-company-list">
|
||||
{(supportData.companies || []).map((c, idx) => (
|
||||
<option key={idx} value={c} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<label className="form-label small fw-bold mb-1">Min Reorder Level</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="10"
|
||||
value={masterForm.min_reorder_level}
|
||||
onChange={(e) => setMasterForm({ ...masterForm, min_reorder_level: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer py-2 px-3 bg-white d-flex justify-content-between">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
onClick={() => setShowMasterModal(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-sm fw-bold px-4"
|
||||
disabled={isSavingMaster}
|
||||
>
|
||||
{isSavingMaster ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-1" role="status"></span>
|
||||
Saving to Master...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="bi bi-check-circle me-1"></i>
|
||||
Save to Master & Select into Bill
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user