diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 9618a41..2ccdd8d 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -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'); diff --git a/app/Controllers/Api/v1/MedicalStoreApi.php b/app/Controllers/Api/v1/MedicalStoreApi.php index ac09a35..c541425 100644 --- a/app/Controllers/Api/v1/MedicalStoreApi.php +++ b/app/Controllers/Api/v1/MedicalStoreApi.php @@ -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']; diff --git a/app/Database/Migrations/2026-09-10-000001_LinkMedicalStoreToPharmacyProductMaster.php b/app/Database/Migrations/2026-09-10-000001_LinkMedicalStoreToPharmacyProductMaster.php new file mode 100644 index 0000000..36dc877 --- /dev/null +++ b/app/Database/Migrations/2026-09-10-000001_LinkMedicalStoreToPharmacyProductMaster.php @@ -0,0 +1,128 @@ +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'); + } + } + } +} diff --git a/public/App/MedicalStore/index.html b/public/App/MedicalStore/index.html index a6c671c..fd87a52 100644 --- a/public/App/MedicalStore/index.html +++ b/public/App/MedicalStore/index.html @@ -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 }) { -
+
- - - - + + + + - + - + {rows.map((r, i) => ( - @@ -4715,6 +5019,186 @@ function PurchaseView({ store, notify }) { )} + + {/* Medicine Master Add Modal */} + {showMasterModal && ( +
+
+
+
+
+
+ +
+
+
Add New Medicine to Pharmacy Master
+ Saved to shared master med_product_master (shared between MedicalStore & HMSPharmacy) +
+
+ +
+ +
+
+
+
+ Medicine Name & Formulation +
+
+
+ + setMasterForm({ ...masterForm, item_name: e.target.value })} + required + autoFocus + /> +
+
+ + setMasterForm({ ...masterForm, formulation: e.target.value })} + /> + + {(supportData.formulations || []).map((f, idx) => ( + +
+
+ + setMasterForm({ ...masterForm, generic_name: e.target.value })} + /> +
+
+
+ +
+
+ Packing, Tax & Regulatory Specifications +
+
+
+ + setMasterForm({ ...masterForm, units_per_pack: e.target.value, packing: e.target.value })} + required + /> +
+
+ + +
+
+ + setMasterForm({ ...masterForm, hsn_code: e.target.value })} + /> +
+
+ + +
+
+ + setMasterForm({ ...masterForm, company_name: e.target.value })} + /> + + {(supportData.companies || []).map((c, idx) => ( + +
+
+ + setMasterForm({ ...masterForm, min_reorder_level: e.target.value })} + /> +
+
+
+
+ +
+ + +
+ +
+
+
+ )} ); }
Medicine NameBatch NoExpiryPacks +
+ Medicine Name (Master Auto-Search) + +
+
Batch NoExpiryPacks FreeUnits/PackUnits/Pack PTR (₹) MRP (₹)GST%GST%
- updateRow(i, 'item_name', e.target.value)} /> + +
+ 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 && ( + + + + )} +
+ + {/* Subtext with Generic and Category */} + {r.generic_name ? ( +
+ {r.category || 'Tablet'} + {r.generic_name} +
+ ) : null} + + {/* Autocomplete Dropdown */} + {activeSearchRow === i && ( +
+ {/* Dropdown Header */} +
+ + {isSearchingMeds ? ( + Searching master... + ) : ( + Pharmacy Master ({suggestions.length} found) + )} + + +
+ + {/* Suggestions List */} + {suggestions.length > 0 ? ( + suggestions.map((item, sIdx) => ( +
{ + e.preventDefault(); + handleSelectMedicine(i, item); + }} + > +
+ {item.item_name} + + {item.category || 'Tablet'} + +
+ {item.generic_name && ( +
+ {item.generic_name} +
+ )} +
+ Pack: {item.units_per_pack || 10}s + GST: {item.gst_rate}% + HSN: {item.hsn_code || '3004'} + {item.drug_schedule && item.drug_schedule !== 'OTC' && ( + {item.drug_schedule} + )} +
+
+ )) + ) : ( + !isSearchingMeds && ( +
+ +
No medicine found matching "{r.item_name}"
+
Click below to add it directly into Pharmacy Master
+
+ ) + )} + + {/* Pinned Add to Master Option */} +
+ +
+
+ )}
- updateRow(i, 'batch_no', e.target.value)} /> + updateRow(i, 'batch_no', e.target.value)} + /> - updateRow(i, 'expiry_date', e.target.value)} /> + updateRow(i, 'expiry_date', e.target.value)} + /> updateRow(i, 'qty_packs', e.target.value)} /> @@ -4326,10 +4629,11 @@ function PurchaseView({ store, notify }) { + -