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 && (
+
Medicines added by rack collectors from the mobile app will automatically show up here.
++ {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.'} +