From 6ce81eedd8d4e3a37be6818fede54cb3421adb88 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Fri, 18 Sep 2026 05:49:20 +0530 Subject: [PATCH] feat(medical-store): auto-convert draft bills to invoice on checkout and reduce pending draft list --- app/Config/Routes.php | 1 + app/Controllers/Api/v1/MedicalStoreApi.php | 60 +++++++- ...eateCreditAccountsAndMobileDraftSchema.php | 2 +- public/App/MedicalStore/index.html | 128 ++++++++++++++---- 4 files changed, 161 insertions(+), 30 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 03908e1..588e3eb 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1747,6 +1747,7 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct $routes->post('medical-store/drafts/save', 'MedicalStoreApi::saveDraftBill'); $routes->get('medical-store/drafts/list', 'MedicalStoreApi::getDraftBills'); $routes->post('medical-store/drafts/cancel', 'MedicalStoreApi::cancelDraftBill'); + $routes->post('medical-store/drafts/convert', 'MedicalStoreApi::convertDraftBill'); // Mobile Dashboard & Analytics $routes->get('medical-store/mobile/dashboard', 'MedicalStoreApi::getMobileDashboardData'); diff --git a/app/Controllers/Api/v1/MedicalStoreApi.php b/app/Controllers/Api/v1/MedicalStoreApi.php index 7f2d8cf..4c50ed5 100644 --- a/app/Controllers/Api/v1/MedicalStoreApi.php +++ b/app/Controllers/Api/v1/MedicalStoreApi.php @@ -1821,6 +1821,31 @@ class MedicalStoreApi extends BaseController $this->db->table('mst_sales')->insert($saleData); $saleId = $this->db->insertID(); + // If this sale originated from a Mobile Draft Bill, mark the draft as CONVERTED + $draftId = !empty($json['draft_id']) ? (int)$json['draft_id'] : 0; + $draftCode = trim((string)($json['draft_code'] ?? '')); + if ($this->db->tableExists('mst_draft_bills')) { + if ($draftId > 0) { + $this->db->table('mst_draft_bills') + ->where('draft_id', $draftId) + ->where('store_id', $storeId) + ->update([ + 'status' => 'CONVERTED', + 'converted_sale_id' => $saleId, + 'updated_at' => date('Y-m-d H:i:s') + ]); + } elseif (!empty($draftCode)) { + $this->db->table('mst_draft_bills') + ->where('draft_code', $draftCode) + ->where('store_id', $storeId) + ->update([ + 'status' => 'CONVERTED', + 'converted_sale_id' => $saleId, + 'updated_at' => date('Y-m-d H:i:s') + ]); + } + } + // Queue in central ABDM Gateway Sync Outbox if applicable if ($fhirBundleJson && (!empty($abhaAddress) || !empty($patientId)) && $this->db->tableExists('abdm_sync_record')) { $this->db->table('abdm_sync_record')->insert([ @@ -6009,14 +6034,20 @@ class MedicalStoreApi extends BaseController $storeId = (int)($this->request->getGet('store_id') ?? 1); $status = trim((string)($this->request->getGet('status') ?? 'DRAFT')); - $builder = $this->db->table('mst_draft_bills') - ->where('store_id', $storeId); + $builder = $this->db->table('mst_draft_bills d') + ->select('d.*, s.invoice_no as converted_invoice_no') + ->join('mst_sales s', 's.sale_id = d.converted_sale_id', 'left') + ->where('d.store_id', $storeId); if ($status !== 'all') { - $builder->where('status', $status); + if ($status === 'CONVERTED') { + $builder->whereIn('d.status', ['CONVERTED', 'CONVERTED_TO_SALE']); + } else { + $builder->where('d.status', $status); + } } - $drafts = $builder->orderBy('draft_id', 'DESC') + $drafts = $builder->orderBy('d.draft_id', 'DESC') ->limit(30) ->get() ->getResultArray(); @@ -6124,6 +6155,27 @@ class MedicalStoreApi extends BaseController return $this->response->setJSON(['status' => 1, 'message' => 'Draft bill cancelled.']); } + public function convertDraftBill() + { + $json = $this->request->getJSON(true) ?: $this->request->getPost(); + $draftId = (int)($json['draft_id'] ?? 0); + $saleId = !empty($json['sale_id']) ? (int)$json['sale_id'] : null; + + if ($draftId <= 0) { + return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Draft ID is required.']); + } + + $this->db->table('mst_draft_bills') + ->where('draft_id', $draftId) + ->update([ + 'status' => 'CONVERTED', + 'converted_sale_id' => $saleId, + 'updated_at' => date('Y-m-d H:i:s') + ]); + + return $this->response->setJSON(['status' => 1, 'message' => 'Draft bill marked as converted.']); + } + // ========================================================================= // 29. MOBILE ADMIN DASHBOARD & ADVANCED METRICS // ========================================================================= diff --git a/app/Database/Migrations/2026-09-18-000001_CreateCreditAccountsAndMobileDraftSchema.php b/app/Database/Migrations/2026-09-18-000001_CreateCreditAccountsAndMobileDraftSchema.php index e0386b2..4f02534 100644 --- a/app/Database/Migrations/2026-09-18-000001_CreateCreditAccountsAndMobileDraftSchema.php +++ b/app/Database/Migrations/2026-09-18-000001_CreateCreditAccountsAndMobileDraftSchema.php @@ -68,7 +68,7 @@ class CreateCreditAccountsAndMobileDraftSchema extends Migration `total_items` INT NOT NULL DEFAULT 0, `estimated_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `items_json` LONGTEXT NOT NULL, - `status` ENUM('DRAFT', 'CONVERTED_TO_SALE', 'CANCELLED') NOT NULL DEFAULT 'DRAFT', + `status` ENUM('DRAFT', 'CONVERTED', 'CONVERTED_TO_SALE', 'CANCELLED') NOT NULL DEFAULT 'DRAFT', `converted_sale_id` INT DEFAULT NULL, `collected_by_name` VARCHAR(100) DEFAULT 'Pharmacist', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, diff --git a/public/App/MedicalStore/index.html b/public/App/MedicalStore/index.html index 85852ae..6b25e5c 100644 --- a/public/App/MedicalStore/index.html +++ b/public/App/MedicalStore/index.html @@ -1505,13 +1505,16 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act const [paymentMode, setPaymentMode] = useState('Cash'); // Cash, UPI, Card, Mixed, IPD_Credit, Staff_Credit const [draftBillsList, setDraftBillsList] = useState([]); const [showDraftModal, setShowDraftModal] = useState(false); + const [draftModalTab, setDraftModalTab] = useState('DRAFT'); // 'DRAFT' or 'CONVERTED' + const [loadedDraft, setLoadedDraft] = useState(null); const [creditAccounts, setCreditAccounts] = useState([]); const [selectedCreditAccount, setSelectedCreditAccount] = useState(null); - const loadDraftBills = async () => { + const loadDraftBills = async (statusOverride) => { if (!store?.store_id) return; try { - const res = await fetch(`${API_BASE}drafts/list?store_id=${store.store_id}&status=DRAFT`); + const st = statusOverride !== undefined ? statusOverride : 'DRAFT'; + const res = await fetch(`${API_BASE}drafts/list?store_id=${store.store_id}&status=${st}`); const data = await res.json(); if (data.status) { setDraftBillsList(data.drafts || []); @@ -1597,6 +1600,7 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act }); setCart(formattedCart); + setLoadedDraft(draft); if (draft.patient_name) setPatientQuery(draft.patient_name); if (draft.uhid) setWalkinUhid(draft.uhid); if (draft.patient_mobile) setWalkinMobile(draft.patient_mobile); @@ -1825,6 +1829,7 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act setCart([]); setReturnCart([]); resetPatient(); + setLoadedDraft(null); setDoctorName(''); setDoctorRegNo(''); setWholeDiscountVal(0); @@ -2642,6 +2647,8 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act setIsProcessing(true); const payload = { store_id: store.store_id, + draft_id: loadedDraft ? loadedDraft.draft_id : null, + draft_code: loadedDraft ? loadedDraft.draft_code : null, patient_type: patientType, uhid: selectedPatient ? selectedPatient.uhid : (walkinUhid || null), patient_id: selectedPatient ? selectedPatient.patient_id : null, @@ -2739,6 +2746,8 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act setCardRefNo(''); setPaymentRef(''); resetPatient(); + setLoadedDraft(null); + loadDraftBills('DRAFT'); try { localStorage.removeItem(draftKey); } catch (e) {} @@ -3368,6 +3377,34 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act {walkinUhid && UHID: {walkinUhid}} )} + {loadedDraft && ( +
+
+ + Draft: {loadedDraft.draft_code} + + + Collector: {loadedDraft.collected_by_name || 'Staff'} + {loadedDraft.ward_name ? ` • Ward: ${loadedDraft.ward_name} (${loadedDraft.bed_no || ''})` : ''} + + + Auto-converts to invoice on checkout + +
+ +
+ )} {/* Medicine Search & Barcode Scanner Line */} @@ -6045,11 +6082,42 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act
+ {/* Tabs: Pending Queue vs Billed / Converted */} +
+
+ + +
+ +
+ {draftBillsList.length === 0 ? (
-
No Pending Draft Bills
-

Medicines added by rack collectors from the mobile app will automatically show up here.

+
{draftModalTab === 'CONVERTED' ? 'No Converted Draft Bills' : 'No Pending Draft Bills'}
+

+ {draftModalTab === 'CONVERTED' + ? 'Draft bills will appear here once converted into pharmacy sales invoices.' + : 'Medicines added by rack collectors from the mobile app will automatically show up here.'} +

) : (
@@ -6069,7 +6137,9 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act {draftBillsList.map((d) => ( - {d.draft_code} + + {d.draft_code} +
{d.patient_name || 'Walk-in'}
@@ -6090,21 +6160,29 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act {d.created_at?.split(' ')[1] || d.created_at} - - + {d.status === 'CONVERTED' ? ( + + Billed: {d.converted_invoice_no || 'INV'} + + ) : ( + <> + + + + )} ))} @@ -6114,14 +6192,14 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act )}
+ + {draftModalTab === 'DRAFT' ? `${draftBillsList.length} draft bill(s) pending` : `${draftBillsList.length} converted draft bill(s)`} + -