feat(medical-store): auto-convert draft bills to invoice on checkout and reduce pending draft list
This commit is contained in:
parent
98652ce646
commit
6ce81eedd8
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 && <span><strong>UHID:</strong> <code className="text-primary">{walkinUhid}</code></span>}
|
||||
</div>
|
||||
)}
|
||||
{loadedDraft && (
|
||||
<div className="mt-2 p-2 rounded-2 d-flex flex-wrap justify-content-between align-items-center gap-2" style={{ background: '#fef3c7', border: '1px solid #f59e0b', fontSize: '0.8rem' }}>
|
||||
<div className="d-flex flex-wrap align-items-center gap-2">
|
||||
<span className="badge bg-warning text-dark fw-bold">
|
||||
<i className="bi bi-phone me-1"></i> Draft: {loadedDraft.draft_code}
|
||||
</span>
|
||||
<span className="text-dark">
|
||||
Collector: <strong>{loadedDraft.collected_by_name || 'Staff'}</strong>
|
||||
{loadedDraft.ward_name ? ` • Ward: ${loadedDraft.ward_name} (${loadedDraft.bed_no || ''})` : ''}
|
||||
</span>
|
||||
<span className="badge bg-light text-secondary border" style={{ fontSize: '0.7rem' }}>
|
||||
<i className="bi bi-arrow-right-circle me-1 text-success"></i>Auto-converts to invoice on checkout
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline-dark py-0 px-2"
|
||||
onClick={() => {
|
||||
setLoadedDraft(null);
|
||||
notify(`Draft [${loadedDraft.draft_code}] unlinked. Cart items kept.`, 'info');
|
||||
}}
|
||||
title="Unlink this draft without clearing the cart"
|
||||
style={{ fontSize: '0.72rem' }}
|
||||
>
|
||||
<i className="bi bi-link-45deg me-1"></i> Unlink Draft
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Medicine Search & Barcode Scanner Line */}
|
||||
|
|
@ -6045,11 +6082,42 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act
|
|||
<button type="button" className="btn-close btn-close-white" onClick={() => setShowDraftModal(false)}></button>
|
||||
</div>
|
||||
<div className="modal-body p-3">
|
||||
{/* Tabs: Pending Queue vs Billed / Converted */}
|
||||
<div className="d-flex align-items-center justify-content-between mb-3 pb-2 border-bottom">
|
||||
<div className="btn-group btn-group-sm">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${draftModalTab === 'DRAFT' ? 'btn-warning text-dark fw-bold' : 'btn-outline-secondary'}`}
|
||||
onClick={() => { setDraftModalTab('DRAFT'); loadDraftBills('DRAFT'); }}
|
||||
>
|
||||
<i className="bi bi-hourglass-split me-1"></i> Pending Queue
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${draftModalTab === 'CONVERTED' ? 'btn-success text-white fw-bold' : 'btn-outline-secondary'}`}
|
||||
onClick={() => { setDraftModalTab('CONVERTED'); loadDraftBills('CONVERTED'); }}
|
||||
>
|
||||
<i className="bi bi-check2-all me-1"></i> Billed / Converted
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline-primary py-0.5 px-2"
|
||||
onClick={() => loadDraftBills(draftModalTab)}
|
||||
>
|
||||
<i className="bi bi-arrow-clockwise me-1"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{draftBillsList.length === 0 ? (
|
||||
<div className="text-center py-5 text-muted">
|
||||
<i className="bi bi-basket3 fs-1 d-block mb-2 text-secondary"></i>
|
||||
<h5>No Pending Draft Bills</h5>
|
||||
<p className="small mb-0">Medicines added by rack collectors from the mobile app will automatically show up here.</p>
|
||||
<h5>{draftModalTab === 'CONVERTED' ? 'No Converted Draft Bills' : 'No Pending Draft Bills'}</h5>
|
||||
<p className="small mb-0">
|
||||
{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.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
|
|
@ -6069,7 +6137,9 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act
|
|||
{draftBillsList.map((d) => (
|
||||
<tr key={d.draft_id}>
|
||||
<td>
|
||||
<span className="badge bg-warning text-dark fw-bold">{d.draft_code}</span>
|
||||
<span className={`badge ${d.status === 'CONVERTED' ? 'bg-secondary' : 'bg-warning text-dark'} fw-bold`}>
|
||||
{d.draft_code}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="fw-bold">{d.patient_name || 'Walk-in'}</div>
|
||||
|
|
@ -6090,21 +6160,29 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act
|
|||
<small className="text-muted">{d.created_at?.split(' ')[1] || d.created_at}</small>
|
||||
</td>
|
||||
<td className="text-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-success fw-bold px-2.5 py-1 me-1 shadow-sm"
|
||||
onClick={() => handleLoadDraftBillIntoCart(d)}
|
||||
>
|
||||
<i className="bi bi-cart-plus me-1"></i> Load Bill
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline-danger py-1 px-1.5"
|
||||
onClick={() => handleCancelDraft(d.draft_id)}
|
||||
title="Discard draft"
|
||||
>
|
||||
<i className="bi bi-trash"></i>
|
||||
</button>
|
||||
{d.status === 'CONVERTED' ? (
|
||||
<span className="badge bg-success-subtle text-success border border-success fw-bold px-2 py-1" style={{ fontSize: '0.75rem' }}>
|
||||
<i className="bi bi-check-circle-fill me-1"></i> Billed: {d.converted_invoice_no || 'INV'}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-success fw-bold px-2.5 py-1 me-1 shadow-sm"
|
||||
onClick={() => handleLoadDraftBillIntoCart(d)}
|
||||
>
|
||||
<i className="bi bi-cart-plus me-1"></i> Load Bill
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline-danger py-1 px-1.5"
|
||||
onClick={() => handleCancelDraft(d.draft_id)}
|
||||
title="Discard draft"
|
||||
>
|
||||
<i className="bi bi-trash"></i>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -6114,14 +6192,14 @@ function PosCounter({ store, notify, initialEditInvoice, onClearEditInvoice, act
|
|||
)}
|
||||
</div>
|
||||
<div className="modal-footer py-2 px-3 bg-light d-flex justify-content-between">
|
||||
<small className="text-muted">
|
||||
{draftModalTab === 'DRAFT' ? `${draftBillsList.length} draft bill(s) pending` : `${draftBillsList.length} converted draft bill(s)`}
|
||||
</small>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={loadDraftBills}
|
||||
className="btn btn-sm btn-secondary"
|
||||
onClick={() => setShowDraftModal(false)}
|
||||
>
|
||||
<i className="bi bi-arrow-clockwise me-1"></i> Refresh Queue
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm btn-secondary" onClick={() => setShowDraftModal(false)}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user