devsofttech_hms_ci4/public/App/MedicalStore/index.html

9293 lines
585 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>HMS Medical Store - Multi-Building Pharmacy System</title>
<link rel="manifest" href="/App/MedicalStore/manifest.json">
<meta name="theme-color" content="#0f172a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="icon" href="/assets/img/logo.png" type="image/png">
<!-- Google Fonts & Bootstrap 5 CSS & Icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<!-- React 18 & ReactDOM UMD & Babel -->
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js" crossorigin></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js"></script>
<style>
:root {
--slate-900: #0f172a;
--slate-800: #1e293b;
--slate-700: #334155;
--primary: #0891b2;
--primary-hover: #0e7490;
--accent-green: #10b981;
--accent-amber: #f59e0b;
--accent-rose: #f43f5e;
--bg-light: #f8fafc;
}
body {
background-color: var(--bg-light);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #1e293b;
margin: 0;
padding: 0;
overflow-x: hidden;
}
.navbar-store {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #ffffff;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
padding: 10px 16px;
}
.badge-license {
background: rgba(8, 145, 178, 0.2);
color: #38bdf8;
border: 1px solid rgba(56, 189, 248, 0.4);
font-size: 0.72rem;
padding: 4px 8px;
border-radius: 6px;
font-weight: 600;
}
.badge-gst {
background: rgba(16, 185, 129, 0.2);
color: #34d399;
border: 1px solid rgba(52, 211, 153, 0.4);
font-size: 0.72rem;
padding: 4px 8px;
border-radius: 6px;
font-weight: 600;
}
.nav-tabs-custom {
background: #ffffff;
border-bottom: 2px solid #e2e8f0;
padding: 0 16px;
}
.nav-tabs-custom .nav-link {
color: #64748b;
font-weight: 600;
border: none;
border-bottom: 3px solid transparent;
padding: 12px 18px;
transition: all 0.2s;
}
.nav-tabs-custom .nav-link.active {
color: var(--primary);
border-bottom-color: var(--primary);
background: transparent;
}
.counter-card {
background: #ffffff;
border-radius: 12px;
border: 1px solid #e2e8f0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 16px;
margin-bottom: 16px;
}
.table-pos thead th {
background-color: #f1f5f9;
color: #475569;
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 8px 10px;
border-bottom: 2px solid #e2e8f0;
}
.table-pos tbody td {
font-size: 0.85rem;
padding: 8px 10px;
vertical-align: middle;
border-bottom: 1px solid #f1f5f9;
}
.pos-summary-card {
background: linear-gradient(145deg, #1e293b 0%, #0f172a 100%);
color: #ffffff;
border-radius: 16px;
padding: 20px;
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.25);
}
/* Slide-in Checkout Desk Drawer */
.checkout-drawer-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(15, 23, 42, 0.65);
backdrop-filter: blur(4px);
z-index: 1045;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.checkout-drawer-overlay.open {
opacity: 1;
pointer-events: auto;
}
.checkout-drawer {
position: fixed;
top: 0;
right: 0;
width: 500px;
max-width: 96vw;
height: 100vh;
background: linear-gradient(165deg, #1e293b 0%, #0f172a 100%);
color: #ffffff;
z-index: 1050;
box-shadow: -12px 0 40px rgba(0, 0, 0, 0.55);
display: flex;
flex-direction: column;
transform: translateX(105%);
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.checkout-drawer.open {
transform: translateX(0);
}
.checkout-drawer-header {
padding: 16px 20px;
background: rgba(15, 23, 42, 0.85);
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.checkout-drawer-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.checkout-drawer-body::-webkit-scrollbar {
width: 6px;
}
.checkout-drawer-body::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
/* Slide-in Bill Print Drawer */
.print-drawer-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(15, 23, 42, 0.65);
backdrop-filter: blur(4px);
z-index: 1045;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.print-drawer-overlay.open {
opacity: 1;
pointer-events: auto;
}
.print-drawer {
position: fixed;
top: 0;
right: 0;
width: 720px;
max-width: 96vw;
height: 100vh;
background: #f1f5f9;
color: #1e293b;
z-index: 1050;
box-shadow: -12px 0 40px rgba(0, 0, 0, 0.55);
display: flex;
flex-direction: column;
transform: translateX(105%);
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.print-drawer.open {
transform: translateX(0);
}
.print-drawer-header {
padding: 14px 20px;
background: #0f172a;
color: #ffffff;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.print-drawer-body {
padding: 20px;
overflow-y: auto;
flex: 1;
background: #cbd5e1;
}
.print-drawer-body::-webkit-scrollbar {
width: 6px;
}
.print-drawer-body::-webkit-scrollbar-thumb {
background: rgba(100, 116, 139, 0.4);
border-radius: 4px;
}
.print-paper-card {
background: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
padding: 24px;
margin: 0 auto;
max-width: 100%;
}
.btn-primary-custom {
background: linear-gradient(135deg, #0891b2 0%, #0284c7 100%);
border: none;
color: #ffffff;
font-weight: 600;
border-radius: 8px;
padding: 8px 16px;
transition: all 0.2s;
}
.btn-primary-custom:hover {
background: linear-gradient(135deg, #0e7490 0%, #0369a1 100%);
color: #ffffff;
box-shadow: 0 4px 12px rgba(8, 145, 178, 0.3);
}
.btn-success-custom {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none;
color: #ffffff;
font-weight: 600;
border-radius: 8px;
padding: 8px 16px;
}
.badge-fefo {
background: #dcfce7;
color: #166534;
border: 1px solid #86efac;
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 4px;
font-weight: 700;
}
.badge-near-expiry {
background: #fef3c7;
color: #92400e;
border: 1px solid #fcd34d;
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
}
.badge-expired {
background: #fee2e2;
color: #991b1b;
border: 1px solid #fca5a5;
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 4px;
font-weight: 700;
}
.search-results-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #ffffff;
border: 1px solid #cbd5e1;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
max-height: 380px;
overflow-y: auto;
z-index: 1050;
}
.search-item-row {
padding: 10px 14px;
border-bottom: 1px solid #f1f5f9;
cursor: pointer;
transition: background 0.15s;
}
.search-item-row:hover {
background-color: #f8fafc;
}
/* -------------------------------------------------------------
PRINT FORMATS: 80mm Thermal, A5 Slip, and A4 Hospital Tax Invoice
------------------------------------------------------------- */
@media print {
@page {
size: auto;
margin: 6mm;
}
body * {
visibility: hidden;
}
#printable-container, #printable-container * {
visibility: visible;
}
#printable-container {
position: absolute;
left: 0;
top: 0;
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
}
.print-a4-invoice, .print-a5-invoice {
width: 100% !important;
max-width: 100% !important;
min-height: auto !important;
padding: 0 !important;
border: none !important;
box-shadow: none !important;
}
.print-thermal-80mm {
width: 76mm !important;
max-width: 100% !important;
border: none !important;
}
.no-print {
display: none !important;
}
}
/* 1. 80mm Thermal Slip */
.print-thermal-80mm {
width: 78mm;
max-width: 100%;
font-family: 'Courier New', Courier, monospace;
font-size: 11px;
line-height: 1.3;
color: #000;
padding: 8px;
margin: auto;
background: #fff;
border: 1px dashed #cbd5e1;
border-radius: 4px;
}
/* 2. A5 Half-Page Invoice */
.print-a5-invoice {
width: 100%;
max-width: 650px;
min-height: auto;
font-family: 'Inter', sans-serif;
font-size: 11px;
color: #1e293b;
padding: 16px;
margin: auto;
background: #fff;
border: 1px solid #cbd5e1;
border-radius: 6px;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
}
/* 3. A4 Full Comprehensive Tax Invoice */
.print-a4-invoice {
width: 100%;
max-width: 820px;
min-height: auto;
font-family: 'Inter', sans-serif;
font-size: 11.5px;
color: #0f172a;
padding: 20px;
margin: auto;
background: #fff;
border: 1px solid #cbd5e1;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
/* Machine Lock Screen Overlay */
.machine-lock-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.95);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
backdrop-filter: blur(8px);
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef, useMemo, useCallback } = React;
const API_BASE = '/api/v1/medical-store/';
// Detect store slug from URL (e.g. /MedicalStore/storeA or /app/medical-store/storeB)
function getInitialStoreSlug() {
if (window.INITIAL_STORE_SLUG) {
return window.INITIAL_STORE_SLUG;
}
const path = window.location.pathname;
const match = path.match(/(?:MedicalStore|app\/medical-store)\/([a-zA-Z0-9_\-]+)/i);
return match ? match[1] : null;
}
// =========================================================================
// ROOT ERROR BOUNDARY
// =========================================================================
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error("Medical Store UI Error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="d-flex flex-column align-items-center justify-content-center vh-100 bg-slate-900 text-white p-4 text-center" style={{ background: '#0f172a' }}>
<div className="bg-danger text-white rounded-circle p-3 mb-3 d-flex align-items-center justify-content-center" style={{ width: 64, height: 64 }}>
<i className="bi bi-exclamation-octagon fs-2"></i>
</div>
<h4 className="fw-bold">Counter Desk Interruption</h4>
<p className="text-secondary mb-3" style={{ maxWidth: 500 }}>
{this.state.error?.message || 'An unexpected error occurred while rendering the counter desk.'}
</p>
<button className="btn btn-outline-light btn-sm" onClick={() => window.location.reload()}>
<i className="bi bi-arrow-clockwise me-1"></i> Reload Counter Desk
</button>
</div>
);
}
return this.props.children;
}
}
function App() {
const [stores, setStores] = useState([]);
const [activeStoreId, setActiveStoreId] = useState(null);
const [activeTab, setActiveTab] = useState('pos'); // pos, inventory, purchase, transfer, ledgers
const [notification, setNotification] = useState(null);
const [isLoadingStores, setIsLoadingStores] = useState(true);
// Machine Authorization State
const [isDeviceAuthorized, setIsDeviceAuthorized] = useState(false);
const [deviceToken, setDeviceToken] = useState('');
const [machineName, setMachineName] = useState('Counter PC ' + (navigator.platform || 'Desktop'));
const [secKeyInput, setSecKeyInput] = useState('');
const [verifyMethod, setVerifyMethod] = useState('key'); // 'key' or 'otp'
const [isVerifying, setIsVerifying] = useState(false);
const storeSlugFromUrl = getInitialStoreSlug();
// Active Store Object
const activeStore = stores.find(s => s.store_id == activeStoreId) || stores[0] || null;
useEffect(() => {
fetchStores();
}, []);
useEffect(() => {
if (activeStore) {
checkMachineAuthorization(activeStore.store_id);
}
}, [activeStoreId, stores]);
const fetchStores = async () => {
setIsLoadingStores(true);
try {
const url = storeSlugFromUrl ? `${API_BASE}stores?slug=${encodeURIComponent(storeSlugFromUrl)}` : `${API_BASE}stores`;
const res = await fetch(url);
const data = await res.json();
if (data.status && data.stores && data.stores.length > 0) {
setStores(data.stores);
// If slug matched specific store, select it (case-insensitive)
if (storeSlugFromUrl) {
const matched = data.stores.find(s =>
(s.store_slug && s.store_slug.toLowerCase() === storeSlugFromUrl.toLowerCase()) ||
(s.store_code && s.store_code.toLowerCase() === storeSlugFromUrl.toLowerCase())
);
if (matched) {
setActiveStoreId(matched.store_id);
return;
}
}
if (!activeStoreId) {
setActiveStoreId(data.stores[0].store_id);
}
}
} catch (e) {
console.error('Failed to load stores:', e);
} finally {
setIsLoadingStores(false);
}
};
const checkMachineAuthorization = async (storeId) => {
const savedToken = localStorage.getItem('mst_device_token_' + storeId);
if (!savedToken) {
setIsDeviceAuthorized(false);
return;
}
try {
const res = await fetch(API_BASE + 'device/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ store_id: storeId, device_token: savedToken })
});
const data = await res.json();
if (data.status && data.authorized) {
setIsDeviceAuthorized(true);
setDeviceToken(savedToken);
if (data.machine_name) setMachineName(data.machine_name);
} else {
localStorage.removeItem('mst_device_token_' + storeId);
setIsDeviceAuthorized(false);
}
} catch (e) {
console.error('Device authorization check error:', e);
}
};
const submitDeviceVerification = async (e) => {
e.preventDefault();
if (!activeStore || !secKeyInput) return;
setIsVerifying(true);
try {
const payload = {
store_id: activeStore.store_id,
store_slug: activeStore.store_slug,
machine_name: machineName,
security_key: verifyMethod === 'key' ? secKeyInput : '',
otp: verifyMethod === 'otp' ? secKeyInput : ''
};
const res = await fetch(API_BASE + 'device/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.status && data.device_token) {
localStorage.setItem('mst_device_token_' + activeStore.store_id, data.device_token);
setDeviceToken(data.device_token);
setIsDeviceAuthorized(true);
notify(data.message, 'success');
} else {
alert(data.message || 'Verification failed. Please check with HMS Admin.');
}
} catch (e) {
alert('Verification request failed: ' + e.message);
} finally {
setIsVerifying(false);
}
};
const notify = (msg, type = 'success') => {
setNotification({ msg, type });
setTimeout(() => setNotification(null), 4000);
};
if (isLoadingStores && !activeStore) {
return (
<div className="d-flex flex-column align-items-center justify-content-center vh-100 bg-slate-900 text-white" style={{ background: '#0f172a' }}>
<div className="spinner-border text-info mb-3" style={{ width: '3rem', height: '3rem' }} role="status">
<span className="visually-hidden">Loading Store Counter...</span>
</div>
<h5 className="fw-bold tracking-wide">HMS Multi-Building Medical Store</h5>
<p className="text-secondary small">Synchronizing pharmacy counters, terminal licenses & ABDM...</p>
</div>
);
}
if (!activeStore) {
return (
<div className="d-flex flex-column align-items-center justify-content-center vh-100 bg-slate-900 text-white p-4 text-center" style={{ background: '#0f172a' }}>
<div className="bg-danger text-white rounded-circle p-3 mb-3 d-flex align-items-center justify-content-center" style={{ width: 64, height: 64 }}>
<i className="bi bi-exclamation-triangle fs-2"></i>
</div>
<h4 className="fw-bold">Medical Store Not Found</h4>
<p className="text-secondary mb-3" style={{ maxWidth: 480 }}>
Could not locate any active pharmacy store counter for <code>/{storeSlugFromUrl || 'default'}</code>.
Please verify store configuration in HMS Admin.
</p>
<div className="d-flex gap-2">
<a href="/MedicalStore/storeA" className="btn btn-outline-light btn-sm">
<i className="bi bi-shop me-1"></i> Try Main Store (storeA)
</a>
<a href="/app/admin/medical-stores" className="btn btn-primary-custom btn-sm">
<i className="bi bi-gear me-1"></i> Open HMS Store Settings
</a>
</div>
</div>
);
}
return (
<div>
{/* Machine Lock Screen Modal if PC is not verified */}
{!isDeviceAuthorized && activeStore && (
<div className="machine-lock-backdrop">
<div className="card shadow-lg border-0" style={{ maxWidth: 460, width: '100%', borderRadius: 16 }}>
<div className="card-header bg-dark text-white p-3 text-center border-0" style={{ borderTopLeftRadius: 16, borderTopRightRadius: 16 }}>
<i className="bi bi-shield-lock-fill text-warning fs-1 d-block mb-1"></i>
<h5 className="fw-bold mb-0">Terminal Machine Verification</h5>
<small className="text-slate-300" style={{ color: '#94a3b8' }}>Counter Authorization Required</small>
</div>
<div className="card-body p-4">
<div className="text-center mb-3">
<h6 className="fw-bold text-dark">{activeStore.store_name}</h6>
<small className="text-muted d-block">
<i className="bi bi-geo-alt me-1"></i>{activeStore.building_name || 'Main Hospital'} • {activeStore.floor_no || 'Ground Floor'}
</small>
<span className="badge bg-primary-subtle text-primary border border-primary-subtle px-2 py-1 mt-1">
Link: MedicalStore/{activeStore.store_slug}
</span>
</div>
<p className="small text-muted text-center mb-3">
This computer terminal is accessing the counter for the first time. Please enter the <strong>Security Key</strong> or <strong>6-digit OTP</strong> provided by HMS Admin.
</p>
<form onSubmit={submitDeviceVerification}>
<div className="mb-3">
<label className="form-label small fw-bold text-dark mb-1">Computer / Machine Name</label>
<input
type="text"
className="form-control form-control-sm"
value={machineName}
onChange={(e) => setMachineName(e.target.value)}
required
/>
</div>
<div className="mb-2">
<div className="btn-group btn-group-sm w-100 mb-2">
<button
type="button"
className={`btn ${verifyMethod === 'key' ? 'btn-primary' : 'btn-outline-secondary'}`}
onClick={() => setVerifyMethod('key')}
>
<i className="bi bi-key-fill me-1"></i> Admin Security Key
</button>
<button
type="button"
className={`btn ${verifyMethod === 'otp' ? 'btn-primary' : 'btn-outline-secondary'}`}
onClick={() => setVerifyMethod('otp')}
>
<i className="bi bi-123 me-1"></i> 6-Digit Counter OTP
</button>
</div>
<input
type={verifyMethod === 'otp' ? 'number' : 'text'}
className="form-control text-center fw-bold fs-5 letter-spacing-1"
placeholder={verifyMethod === 'key' ? 'e.g. HMS-XXXXXX-XXX' : 'Enter 6-digit OTP'}
value={secKeyInput}
onChange={(e) => setSecKeyInput(e.target.value)}
required
/>
</div>
<button
type="submit"
className="btn btn-success-custom w-100 py-2.5 mt-3 fw-bold"
disabled={isVerifying}
>
{isVerifying ? (
<span><i className="bi bi-hourglass-split me-1"></i> Verifying Terminal...</span>
) : (
<span><i className="bi bi-shield-check me-1"></i> Verify Machine &amp; Unlock Counter</span>
)}
</button>
</form>
</div>
</div>
</div>
)}
{/* Top Store Header */}
<header className="navbar-store">
<div className="container-fluid d-flex flex-wrap align-items-center justify-content-between gap-2">
<div className="d-flex align-items-center gap-3">
<div className="bg-primary text-white p-2 rounded-3 d-flex align-items-center justify-content-center" style={{ width: 40, height: 40 }}>
<i className="bi bi-capsule fs-5"></i>
</div>
<div>
<div className="d-flex align-items-center gap-2">
<h6 className="mb-0 fw-bold text-white fs-5">{activeStore ? activeStore.store_name : 'Medical Store Counter'}</h6>
{activeStore && (
<span className="badge bg-info-subtle text-info border border-info px-2 py-0.5 rounded-pill" style={{ fontSize: '0.65rem' }}>
/{activeStore.store_slug || activeStore.store_code}
</span>
)}
</div>
<small className="text-slate-400" style={{ color: '#94a3b8' }}>
<i className="bi bi-geo-alt me-1"></i>
{activeStore ? `${activeStore.building_name || 'Main Building'} • ${activeStore.floor_no || 'Ground Floor'}` : 'Counter'}
</small>
</div>
</div>
{/* Active Store Statutory License Badges */}
{activeStore && (
<div className="d-none d-md-flex align-items-center gap-2">
{activeStore.abdm_hfr_id && (
<span className="badge bg-success-subtle text-success border border-success px-2 py-1" title="ABDM Health Facility Registry">
<i className="bi bi-shield-fill-check me-1"></i> HFR: {activeStore.abdm_hfr_id}
</span>
)}
<span className="badge-license" title="Retail Allopathic Drug License">
DL 20B: {activeStore.drug_license_no_20b || 'N/A'}
</span>
<span className="badge-license" title="Schedule C/C1 Drug License">
DL 21B: {activeStore.drug_license_no_21b || 'N/A'}
</span>
<span className="badge-gst" title="GSTIN">
GSTIN: {activeStore.gstin || 'N/A'}
</span>
{isDeviceAuthorized && (
<span className="badge bg-success text-white rounded-pill px-2.5 py-1" style={{ fontSize: '0.7rem' }}>
<i className="bi bi-shield-check me-1"></i> Terminal Verified
</span>
)}
</div>
)}
{/* Store Selector & Exit */}
<div className="d-flex align-items-center gap-2">
<select
className="form-select form-select-sm bg-dark text-white border-secondary"
style={{ minWidth: 200 }}
value={activeStoreId || ''}
onChange={(e) => setActiveStoreId(Number(e.target.value))}
>
{stores.map(s => (
<option key={s.store_id} value={s.store_id}>
{s.store_name} ({s.store_slug})
</option>
))}
</select>
<a href="/app" className="btn btn-sm btn-outline-light" title="Return to App Hub">
<i className="bi bi-grid-fill"></i>
</a>
</div>
</div>
</header>
{/* Notification Alert Banner */}
{notification && (
<div className={`alert alert-${notification.type === 'error' ? 'danger' : 'success'} alert-dismissible fade show m-2 p-2 px-3`} role="alert">
<i className={`bi bi-${notification.type === 'error' ? 'exclamation-triangle' : 'check-circle'}-fill me-2`}></i>
{notification.msg}
<button type="button" className="btn-close p-2" onClick={() => setNotification(null)}></button>
</div>
)}
{/* Main Navigation Tabs */}
<nav className="nav-tabs-custom d-flex flex-wrap">
<button className={`nav-link ${activeTab === 'pos' ? 'active' : ''}`} onClick={() => setActiveTab('pos')}>
<i className="bi bi-cart3 me-1"></i> POS Counter Desk
</button>
<button className={`nav-link ${activeTab === 'inventory' ? 'active' : ''}`} onClick={() => setActiveTab('inventory')}>
<i className="bi bi-boxes me-1"></i> Live Stock &amp; Batches
</button>
<button className={`nav-link ${activeTab === 'purchase' ? 'active' : ''}`} onClick={() => setActiveTab('purchase')}>
<i className="bi bi-box-arrow-in-down me-1"></i> Purchases &amp; Stock Import
</button>
<button className={`nav-link ${activeTab === 'transfer' ? 'active' : ''}`} onClick={() => setActiveTab('transfer')}>
<i className="bi bi-arrow-left-right me-1"></i> Inter-Store Indents
</button>
<button className={`nav-link ${activeTab === 'ledgers' ? 'active' : ''}`} onClick={() => setActiveTab('ledgers')}>
<i className="bi bi-journal-text me-1"></i> Ledgers, Daybook &amp; GST
</button>
</nav>
{/* Main View Container */}
<main className="container-fluid py-3">
{activeTab === 'pos' && <PosCounter store={activeStore} notify={notify} />}
{activeTab === 'inventory' && <InventoryView store={activeStore} notify={notify} />}
{activeTab === 'purchase' && <PurchaseView store={activeStore} notify={notify} />}
{activeTab === 'transfer' && <TransferView store={activeStore} stores={stores} notify={notify} />}
{activeTab === 'ledgers' && <LedgersView store={activeStore} notify={notify} />}
</main>
</div>
);
}
// =========================================================================
// 1. POS BILLING & DISPENSING COUNTER (MULTI-PRINT: A4, A5, 80mm THERMAL)
// =========================================================================
function PosCounter({ store, notify }) {
if (!store) {
return (
<div className="text-center py-5 text-muted">
<div className="spinner-border spinner-border-sm text-primary me-2"></div>
Loading counter desk...
</div>
);
}
const [patientQuery, setPatientQuery] = useState('');
const [patientResults, setPatientResults] = useState([]);
const [selectedPatient, setSelectedPatient] = useState(null);
const [patientType, setPatientType] = useState('Walk-in'); // Walk-in, OPD, IPD
const [walkinAbhaId, setWalkinAbhaId] = useState('');
const [walkinAbhaAddress, setWalkinAbhaAddress] = useState('');
const [showFhirModal, setShowFhirModal] = useState(false);
const [fhirBundleData, setFhirBundleData] = useState(null);
const [isLoadingFhir, setIsLoadingFhir] = useState(false);
const [copiedFhir, setCopiedFhir] = useState(false);
const [doctorName, setDoctorName] = useState('');
const [doctorRegNo, setDoctorRegNo] = useState('');
const [itemQuery, setItemQuery] = useState('');
const [itemResults, setItemResults] = useState([]);
const [isSearchingItems, setIsSearchingItems] = useState(false);
const [cart, setCart] = useState([]);
const [wholeDiscountType, setWholeDiscountType] = useState('pct'); // 'pct' or 'val'
const [wholeDiscountVal, setWholeDiscountVal] = useState(0);
const [paymentMode, setPaymentMode] = useState('Cash'); // Cash, UPI, Card, Mixed, IPD_Credit
const [paymentRef, setPaymentRef] = useState('');
const [splitCash, setSplitCash] = useState('');
const [splitUpi, setSplitUpi] = useState('');
const [splitCard, setSplitCard] = useState('');
const [upiRefNo, setUpiRefNo] = useState('');
const [bankName, setBankName] = useState('');
const [cardRefNo, setCardRefNo] = useState('');
const [lastInvoice, setLastInvoice] = useState(null);
const [printFormat, setPrintFormat] = useState('thermal'); // 'thermal', 'a5', 'a4'
const [isProcessing, setIsProcessing] = useState(false);
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
const [isPrintOpen, setIsPrintOpen] = useState(false);
// Automatically preload latest completed sale for this store so "Last Bill" is immediately available
useEffect(() => {
if (store && store.store_id && !lastInvoice) {
fetch(API_BASE + `sales/recent?store_id=${store.store_id}&limit=1`)
.then(r => r.json())
.then(d => {
if (d.status && d.sales && d.sales.length > 0) {
const latestSaleId = d.sales[0].sale_id;
fetch(API_BASE + `sales/invoice/${latestSaleId}`)
.then(ir => ir.json())
.then(id => {
if (id.status) setLastInvoice(id);
}).catch(() => {});
}
}).catch(() => {});
}
}, [store?.store_id]);
// B2C Sales Return & Same-Invoice Exchange State
const [returnCart, setReturnCart] = useState([]);
const [showReturnModal, setShowReturnModal] = useState(false);
const [returnSearchQuery, setReturnSearchQuery] = useState('');
const [returnSearchResults, setReturnSearchResults] = useState([]);
const [isSearchingReturn, setIsSearchingReturn] = useState(false);
const [selectedReturnInvoice, setSelectedReturnInvoice] = useState(null);
const [returnDraftItems, setReturnDraftItems] = useState({});
const [refundMode, setRefundMode] = useState('Cash'); // 'Cash', 'UPI'
const [refundRefNo, setRefundRefNo] = useState('');
const returnSearchInputRef = useRef(null);
const openReturnModal = () => {
setShowReturnModal(true);
setTimeout(() => {
if (returnSearchInputRef.current) {
returnSearchInputRef.current.focus();
returnSearchInputRef.current.select();
}
}, 120);
};
const searchReturnInvoices = async (query = returnSearchQuery) => {
if (!query || query.trim().length < 2) {
notify('Please enter at least 2 characters to search bills.', 'error');
return;
}
setIsSearchingReturn(true);
setSelectedReturnInvoice(null);
try {
const res = await fetch(API_BASE + `returns/lookup-invoice?store_id=${store.store_id}&q=${encodeURIComponent(query.trim())}`);
const data = await res.json();
if (data.status) {
setReturnSearchResults(data.results || []);
if (data.results && data.results.length === 1) {
selectReturnInvoice(data.results[0]);
} else if (!data.results || data.results.length === 0) {
notify('No invoices found matching "' + query.trim() + '"', 'error');
}
} else {
notify(data.message || 'Error looking up invoices', 'error');
}
} catch (e) {
notify('Invoice search error: ' + e.message, 'error');
} finally {
setIsSearchingReturn(false);
}
};
const selectReturnInvoice = (resObj) => {
setSelectedReturnInvoice(resObj);
const drafts = {};
if (resObj && resObj.items) {
resObj.items.forEach(it => {
if (it.remaining_units > 0) {
drafts[it.sale_item_id] = {
return_qty: 0,
return_unit: (it.units_per_pack > 1 && it.remaining_strips > 0) ? 'Strip' : 'Tab',
condition: 'GOOD',
reason: 'EXCESS_UNUSED'
};
}
});
}
setReturnDraftItems(drafts);
};
const updateReturnDraft = (saleItemId, field, val) => {
setReturnDraftItems(prev => ({
...prev,
[saleItemId]: {
...prev[saleItemId],
[field]: val
}
}));
};
const addSelectedReturnsToBill = () => {
if (!selectedReturnInvoice) return;
const newReturns = [];
let hasAnyQty = false;
selectedReturnInvoice.items.forEach(it => {
const draft = returnDraftItems[it.sale_item_id];
if (draft && Number(draft.return_qty) > 0) {
hasAnyQty = true;
const qty = Number(draft.return_qty);
const upp = Number(it.units_per_pack) || 1;
const totalUnits = draft.return_unit === 'Strip' ? (qty * upp) : qty;
if (totalUnits > it.remaining_units) {
notify(`Cannot return more than available (${it.remaining_units} tabs) for ${it.item_name}!`, 'error');
return;
}
const lineTotal = Number((totalUnits * it.effective_unit_price).toFixed(2));
const itemData = {
ref_sale_item_id: it.sale_item_id,
ref_sale_id: selectedReturnInvoice.sale.sale_id,
ref_invoice_no: selectedReturnInvoice.sale.invoice_no,
item_id: it.item_id,
item_name: it.item_name,
generic_name: it.generic_name,
batch_id: it.batch_id,
batch_no: it.batch_no,
expiry_date: it.expiry_date,
units_per_pack: upp,
return_qty: qty,
return_unit: draft.return_unit,
return_total_units: totalUnits,
unit_price: it.effective_unit_price,
unit_mrp: it.unit_mrp,
gst_rate: it.gst_rate,
condition: draft.condition || 'GOOD',
reason: draft.reason || 'EXCESS_UNUSED',
line_total: lineTotal
};
const existingIdx = returnCart.findIndex(rc => rc.ref_sale_item_id === it.sale_item_id);
if (existingIdx >= 0) {
returnCart[existingIdx] = itemData;
} else {
newReturns.push(itemData);
}
}
});
if (!hasAnyQty) {
notify('Please enter quantity > 0 for at least one item to return.', 'error');
return;
}
setReturnCart([...returnCart, ...newReturns]);
// If patient context is currently blank, copy patient from original invoice
if (!selectedPatient && selectedReturnInvoice.sale) {
const s = selectedReturnInvoice.sale;
if (s.patient_name || s.uhid) {
setPatientQuery(s.patient_name + (s.uhid ? ` (${s.uhid})` : ''));
if (s.doctor_name) setDoctorName(s.doctor_name);
}
}
setShowReturnModal(false);
notify('Added return medicines to current invoice! Adjustments reflected in totals.', 'success');
};
// Keyboard Navigation & Focus Refs
const medicineSearchRef = useRef(null);
const patientSearchRef = useRef(null);
const wholeDiscountRef = useRef(null);
const [selectedBatchIndex, setSelectedBatchIndex] = useState(0);
const [selectedPatientIndex, setSelectedPatientIndex] = useState(0);
// Flatten non-expired available batches for keyboard arrow navigation
const selectableBatches = [];
itemResults.forEach(it => {
if (it.batches && it.batches.length > 0) {
it.batches.forEach(b => {
if (!b.is_expired && Number(b.current_qty) > 0) {
selectableBatches.push({ item: it, batch: b });
}
});
}
});
useEffect(() => {
setSelectedBatchIndex(0);
}, [itemResults]);
useEffect(() => {
setSelectedPatientIndex(0);
}, [patientResults]);
const focusMedicineSearch = () => {
if (medicineSearchRef.current) {
medicineSearchRef.current.focus();
medicineSearchRef.current.select();
}
};
// Auto-focus on initial mount
useEffect(() => {
const timer = setTimeout(() => {
focusMedicineSearch();
}, 150);
return () => clearTimeout(timer);
}, []);
// Search patient
const searchPatients = async (val) => {
setPatientQuery(val);
if (val.length < 2) {
setPatientResults([]);
return;
}
try {
const res = await fetch(API_BASE + `patient/search?q=${encodeURIComponent(val)}`);
const data = await res.json();
if (data.status) {
setPatientResults(data.patients || []);
}
} catch (e) {
console.error(e);
}
};
const handlePatientSearchKeyDown = (e) => {
if (patientResults.length > 0) {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedPatientIndex(prev => (prev + 1) % patientResults.length);
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedPatientIndex(prev => (prev - 1 + patientResults.length) % patientResults.length);
return;
}
if (e.key === 'Enter') {
e.preventDefault();
const chosen = patientResults[selectedPatientIndex >= 0 && selectedPatientIndex < patientResults.length ? selectedPatientIndex : 0];
if (chosen) {
selectPatient(chosen);
setTimeout(() => focusMedicineSearch(), 50);
}
return;
}
if (e.key === 'Escape') {
e.preventDefault();
setPatientResults([]);
return;
}
} else if (e.key === 'Enter') {
e.preventDefault();
focusMedicineSearch();
}
};
const handleMedicineSearchKeyDown = (e) => {
if (itemResults.length > 0 && selectableBatches.length > 0) {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedBatchIndex(prev => (prev + 1) % selectableBatches.length);
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedBatchIndex(prev => (prev - 1 + selectableBatches.length) % selectableBatches.length);
return;
}
if (e.key === 'Enter') {
e.preventDefault();
const chosen = selectableBatches[selectedBatchIndex >= 0 && selectedBatchIndex < selectableBatches.length ? selectedBatchIndex : 0];
if (chosen) {
addItemToCart(chosen.item, chosen.batch, 1);
}
return;
}
if (e.key === 'Escape') {
e.preventDefault();
setItemResults([]);
setItemQuery('');
return;
}
} else if (e.key === 'Enter') {
e.preventDefault();
if (itemResults.length === 1 && itemResults[0].fefo_batch) {
addItemToCart(itemResults[0], itemResults[0].fefo_batch, 1);
}
}
};
const selectPatient = (pt) => {
setSelectedPatient(pt);
setPatientResults([]);
setPatientQuery(pt.full_name + ' (' + pt.uhid + ')');
setWalkinAbhaId(pt.abha_id || '');
setWalkinAbhaAddress(pt.abha_address || '');
if (pt.has_active_ipd && pt.active_ipd) {
setPatientType('IPD');
setDoctorName(pt.active_ipd.doctor_name || '');
setPrintFormat('a4'); // Default IPD bills to A4
} else if (pt.has_active_opd && pt.active_opd) {
setPatientType('OPD');
setDoctorName(pt.active_opd.doctor_name || '');
setPrintFormat('a5'); // Default OPD bills to A5
} else {
setPatientType('Walk-in');
setPrintFormat('thermal'); // Default walkin to Thermal
}
};
const resetPatient = () => {
setSelectedPatient(null);
setPatientQuery('');
setPatientType('Walk-in');
setDoctorName('');
setDoctorRegNo('');
setWalkinAbhaId('');
setWalkinAbhaAddress('');
setPrintFormat('thermal');
};
const viewAbdmBundle = async (saleId) => {
setIsLoadingFhir(true);
setShowFhirModal(true);
setCopiedFhir(false);
try {
const res = await fetch(API_BASE + `abdm/bundle/${saleId}`);
const data = await res.json();
if (data.status) {
setFhirBundleData(data);
} else {
notify(data.message || 'Unable to retrieve ABDM FHIR bundle', 'error');
setShowFhirModal(false);
}
} catch (e) {
notify('FHIR Fetch error: ' + e.message, 'error');
setShowFhirModal(false);
} finally {
setIsLoadingFhir(false);
}
};
const copyFhirToClipboard = () => {
if (!fhirBundleData?.bundle) return;
navigator.clipboard.writeText(JSON.stringify(fhirBundleData.bundle, null, 2));
setCopiedFhir(true);
setTimeout(() => setCopiedFhir(false), 2500);
};
// 1-Click Load Doctor Rx
const loadDoctorPrescription = async () => {
if (!selectedPatient) return;
const encType = selectedPatient.has_active_ipd ? 'ipd' : (selectedPatient.has_active_opd ? 'opd' : null);
const encId = selectedPatient.has_active_ipd ? selectedPatient.active_ipd.ipd_id : (selectedPatient.has_active_opd ? selectedPatient.active_opd.opd_id : null);
if (!encType || !encId) {
notify('No active OPD or IPD prescription found for this patient.', 'error');
return;
}
try {
const res = await fetch(API_BASE + `patient/prescription/${encType}/${encId}`);
const data = await res.json();
if (data.status && data.medicines && data.medicines.length > 0) {
let addedCount = 0;
for (const med of data.medicines) {
const sRes = await fetch(API_BASE + `items/search?store_id=${store.store_id}&q=${encodeURIComponent(med.medicine_name)}`);
const sData = await sRes.json();
if (sData.status && sData.items && sData.items.length > 0) {
const matchedItem = sData.items[0];
if (matchedItem.fefo_batch) {
addItemToCart(matchedItem, matchedItem.fefo_batch, med.prescribed_qty || 10);
addedCount++;
}
}
}
notify(`Imported ${addedCount} prescribed medicines into cart!`, 'success');
} else {
notify('No electronic medicines found for this visit.', 'error');
}
} catch (e) {
notify('Failed to load prescription: ' + e.message, 'error');
}
};
// Search Items in Store
const searchItems = async (val) => {
setItemQuery(val);
if (val.length < 2) {
setItemResults([]);
return;
}
setIsSearchingItems(true);
try {
const res = await fetch(API_BASE + `items/search?store_id=${store.store_id}&q=${encodeURIComponent(val)}`);
const data = await res.json();
if (data.status) {
setItemResults(data.items || []);
}
} catch (e) {
console.error(e);
} finally {
setIsSearchingItems(false);
}
};
const getItemLineInfo = (it) => {
const upp = Number(it.units_per_pack) > 0 ? Number(it.units_per_pack) : 1;
const stripMrp = Number(it.unit_mrp) || 0;
const perUnitMrp = upp > 0 ? (stripMrp / upp) : stripMrp;
const sellUnit = it.sell_unit || (upp > 1 ? 'Strip' : 'Unit');
let totalUnits = 0;
let lineGross = 0;
let unitLabel = '';
if (upp <= 1) {
totalUnits = Math.max(1, Number(it.qty) || 1);
lineGross = totalUnits * stripMrp;
unitLabel = it.unit_pack || 'Unit';
} else if (sellUnit === 'Tablet' || sellUnit === 'Unit') {
totalUnits = Math.max(1, Number(it.loose_qty ?? it.qty) || 1);
lineGross = totalUnits * perUnitMrp;
unitLabel = 'Tablet';
} else if (sellUnit === 'Combo') {
const sq = Math.max(0, Number(it.strip_qty) || 0);
const lq = Math.max(0, Number(it.loose_qty) || 0);
totalUnits = (sq * upp) + lq;
if (totalUnits <= 0) totalUnits = 1;
lineGross = (sq * stripMrp) + (lq * perUnitMrp);
unitLabel = 'Combo';
} else { // 'Strip'
const sq = Math.max(1, Number(it.strip_qty ?? it.qty) || 1);
totalUnits = sq * upp;
lineGross = sq * stripMrp;
unitLabel = 'Strip';
}
lineGross = Number(lineGross.toFixed(2));
const effectiveUnitPrice = (sellUnit === 'Tablet' || sellUnit === 'Unit') ? Number(perUnitMrp.toFixed(2)) : stripMrp;
return {
upp,
stripMrp,
perUnitMrp: Number(perUnitMrp.toFixed(2)),
sellUnit,
totalUnits,
lineGross,
unitLabel,
effectiveUnitPrice
};
};
const addItemToCart = (item, batch, qty = 1) => {
if (!batch) {
notify('No active batch available for ' + item.item_name, 'error');
return;
}
if (batch.is_expired) {
notify(`STATUTORY BLOCK: Batch ${batch.batch_no} is expired! Cannot be sold.`, 'error');
return;
}
const upp = Number(item.units_per_pack) > 0 ? Number(item.units_per_pack) : 1;
const existingIdx = cart.findIndex(c => c.item_id === item.item_id && c.batch_id === batch.batch_id);
const targetIdx = existingIdx >= 0 ? existingIdx : cart.length;
if (existingIdx >= 0) {
const updated = [...cart];
if (updated[existingIdx].sell_unit === 'Tablet') {
updated[existingIdx].loose_qty = (Number(updated[existingIdx].loose_qty) || 0) + qty;
updated[existingIdx].qty = updated[existingIdx].loose_qty;
} else {
updated[existingIdx].strip_qty = (Number(updated[existingIdx].strip_qty) || 0) + qty;
updated[existingIdx].qty = updated[existingIdx].strip_qty;
}
setCart(updated);
} else {
setCart([...cart, {
item_id: item.item_id,
item_name: item.item_name,
generic_name: item.generic_name,
unit_pack: item.unit_pack,
units_per_pack: upp,
drug_schedule: item.drug_schedule,
hsn_code: item.hsn_code || '3004',
gst_rate: batch.gst_rate || 12.0,
batch_id: batch.batch_id,
batch_no: batch.batch_no,
expiry_display: batch.expiry_display,
expiry_date: batch.expiry_date,
current_qty: batch.current_qty,
unit_mrp: batch.mrp,
sell_unit: upp > 1 ? 'Tablet' : 'Unit',
strip_qty: 0,
loose_qty: qty,
discount_type: 'pct',
discount_val: 0,
qty: qty
}]);
}
setItemResults([]);
setItemQuery('');
setSelectedBatchIndex(0);
// Auto-suggest format based on item count
if (cart.length + 1 > 5 && printFormat === 'thermal') {
setPrintFormat('a5');
}
// Auto-focus quantity input of the newly added/updated cart line (default to loose)
setTimeout(() => {
const row = existingIdx >= 0 ? cart[existingIdx] : null;
const sellUnit = row ? row.sell_unit : (upp > 1 ? 'Tablet' : 'Unit');
let el = null;
if (sellUnit === 'Strip') {
el = document.getElementById(`cart-qty-strip-${targetIdx}`);
} else {
el = document.getElementById(`cart-qty-loose-${targetIdx}`) || document.getElementById(`cart-qty-strip-${targetIdx}`);
}
if (el) {
el.focus();
el.select();
}
}, 50);
};
const getItemDiscount = (it) => {
const info = getItemLineInfo(it);
const lineGross = info.lineGross;
const val = Number(it.discount_val) || 0;
if (val <= 0) return 0;
if (it.discount_type === 'val') {
return Math.min(lineGross, val);
} else {
const pct = Math.min(100, Math.max(0, val));
return Number(((lineGross * pct) / 100).toFixed(2));
}
};
const toggleItemDiscType = (idx) => {
const updated = [...cart];
const it = updated[idx];
const info = getItemLineInfo(it);
const lineGross = info.lineGross;
const currentDisc = getItemDiscount(it);
if (it.discount_type === 'val') {
it.discount_type = 'pct';
it.discount_val = lineGross > 0 ? Number(((currentDisc / lineGross) * 100).toFixed(1)) : 0;
} else {
it.discount_type = 'val';
it.discount_val = Math.round(currentDisc);
}
setCart(updated);
};
const switchSellUnit = (idx, newUnit) => {
const updated = [...cart];
const it = updated[idx];
const upp = it.units_per_pack || 1;
it.sell_unit = newUnit;
if (newUnit === 'Tablet') {
if (!it.loose_qty || Number(it.loose_qty) <= 0) {
it.loose_qty = (Number(it.strip_qty) > 0 ? (Number(it.strip_qty) * upp) : (it.qty || 1));
}
it.qty = it.loose_qty;
setCart(updated);
setTimeout(() => {
const el = document.getElementById(`cart-qty-loose-${idx}`);
if (el) { el.focus(); el.select(); }
}, 30);
} else if (newUnit === 'Strip') {
if (!it.strip_qty || Number(it.strip_qty) <= 0) {
it.strip_qty = (Number(it.loose_qty) > 0 ? Math.max(1, Math.round(Number(it.loose_qty) / upp)) : 1);
}
it.qty = it.strip_qty;
setCart(updated);
setTimeout(() => {
const el = document.getElementById(`cart-qty-strip-${idx}`);
if (el) { el.focus(); el.select(); }
}, 30);
} else if (newUnit === 'Combo') {
if (it.strip_qty === undefined || Number(it.strip_qty) <= 0) it.strip_qty = 1;
if (it.loose_qty === undefined) it.loose_qty = 0;
setCart(updated);
setTimeout(() => {
const el = document.getElementById(`cart-qty-strip-${idx}`);
if (el) { el.focus(); el.select(); }
}, 30);
}
};
const updateCartItem = (idx, field, val) => {
const updated = [...cart];
updated[idx][field] = val;
setCart(updated);
};
const removeCartItem = (idx) => {
setCart(cart.filter((_, i) => i !== idx));
};
// Calculate totals with whole bill discount and return items
const grossTotal = cart.reduce((acc, it) => acc + getItemLineInfo(it).lineGross, 0);
const itemDiscountTotal = cart.reduce((acc, it) => acc + getItemDiscount(it), 0);
const balanceAfterItemDisc = Math.max(0, grossTotal - itemDiscountTotal);
let wholeDiscountAmt = 0;
const numWholeDisc = Number(wholeDiscountVal) || 0;
if (numWholeDisc > 0) {
if (wholeDiscountType === 'val') {
wholeDiscountAmt = Math.min(balanceAfterItemDisc, numWholeDisc);
} else {
const pct = Math.min(100, Math.max(0, numWholeDisc));
wholeDiscountAmt = (balanceAfterItemDisc * pct) / 100;
}
}
const discountTotal = itemDiscountTotal + wholeDiscountAmt;
const netSalesBeforeReturn = Math.max(0, grossTotal - discountTotal);
const returnTotal = returnCart.reduce((acc, it) => acc + (Number(it.line_total) || 0), 0);
const netDifference = netSalesBeforeReturn - returnTotal;
let netPayable = 0;
let refundPayable = 0;
let roundOff = '0.00';
if (netDifference >= 0) {
netPayable = Math.round(netDifference);
roundOff = (netPayable - netDifference).toFixed(2);
refundPayable = 0;
} else {
refundPayable = Math.round(Math.abs(netDifference));
roundOff = (refundPayable - Math.abs(netDifference)).toFixed(2);
netPayable = 0;
}
const splitCashNum = Number(splitCash) || 0;
const splitUpiNum = Number(splitUpi) || 0;
const splitCardNum = Number(splitCard) || 0;
const totalSplitAllocated = splitCashNum + splitUpiNum + splitCardNum;
const splitRemaining = Number((netPayable - totalSplitAllocated).toFixed(2));
// Save and print sale or return/exchange bill
const processSale = async () => {
if (cart.length === 0 && returnCart.length === 0) {
notify('Cart has no items to bill or return!', 'error');
return;
}
if (paymentMode === 'IPD_Credit' && (!selectedPatient || !selectedPatient.has_active_ipd)) {
notify('IPD Credit mode is only allowed for admitted IPD patients.', 'error');
return;
}
let finalCash = 0;
let finalUpi = 0;
let finalCard = 0;
let finalCredit = 0;
if (refundPayable > 0) {
// Patient receives a refund; no positive payment collected
} else {
if (paymentMode === 'Cash') {
finalCash = netPayable;
} else if (paymentMode === 'UPI') {
finalUpi = netPayable;
} else if (paymentMode === 'Card') {
finalCard = netPayable;
} else if (paymentMode === 'IPD_Credit') {
finalCredit = netPayable;
} else if (paymentMode === 'Mixed') {
finalCash = splitCashNum;
finalUpi = splitUpiNum;
finalCard = splitCardNum;
if (Math.abs(splitRemaining) > 0.01) {
notify(`Split payment total (₹${totalSplitAllocated.toFixed(2)}) does not match Net Payable (₹${netPayable})! Difference: ₹${splitRemaining.toFixed(2)}`, 'error');
return;
}
}
}
setIsProcessing(true);
const payload = {
store_id: store.store_id,
patient_type: patientType,
uhid: selectedPatient ? selectedPatient.uhid : null,
patient_id: selectedPatient ? selectedPatient.patient_id : null,
opd_id: selectedPatient && selectedPatient.active_opd ? selectedPatient.active_opd.opd_id : null,
ipd_id: selectedPatient && selectedPatient.active_ipd ? selectedPatient.active_ipd.ipd_id : null,
patient_name: selectedPatient ? selectedPatient.full_name : (patientQuery || 'Walk-in Customer'),
patient_mobile: selectedPatient ? selectedPatient.mobile : '',
patient_address: selectedPatient ? selectedPatient.address : '',
abha_id: selectedPatient?.abha_id || walkinAbhaId || null,
abha_address: selectedPatient?.abha_address || walkinAbhaAddress || null,
age: selectedPatient ? selectedPatient.age : '',
gender: selectedPatient ? selectedPatient.gender : '',
doctor_name: doctorName || (selectedPatient?.active_opd?.doctor_name || selectedPatient?.active_ipd?.doctor_name || 'Dr. Consultant'),
doctor_reg_no: doctorRegNo,
payment_mode: refundPayable > 0 ? (refundMode === 'Cash' ? 'Cash' : 'UPI') : paymentMode,
cash_paid: finalCash,
upi_paid: finalUpi,
card_paid: finalCard,
credit_amount: finalCredit,
refund_amount: refundPayable,
refund_mode: refundPayable > 0 ? refundMode : '',
refund_ref_no: refundPayable > 0 ? refundRefNo : '',
payment_reference: paymentRef || upiRefNo || cardRefNo,
bank_name: bankName,
upi_ref_no: upiRefNo,
card_ref_no: cardRefNo,
whole_discount_type: wholeDiscountType,
whole_discount_val: Number(wholeDiscountVal) || 0,
items: cart.map(it => {
const info = getItemLineInfo(it);
const lineGross = info.lineGross;
const lineDisc = getItemDiscount(it);
const discPct = lineGross > 0 ? Number(((lineDisc / lineGross) * 100).toFixed(2)) : 0;
return {
item_id: it.item_id,
batch_id: it.batch_id,
qty: Number(it.qty) || 1,
sell_unit: info.sellUnit,
units_per_pack: info.upp,
strip_qty: Number(it.strip_qty) || 0,
loose_qty: Number(it.loose_qty) || 0,
total_units: info.totalUnits,
effective_rate: info.effectiveUnitPrice,
line_gross: lineGross,
discount_type: it.discount_type || 'pct',
discount_val: Number(it.discount_val) || 0,
discount_pct: discPct,
discount_amount: lineDisc
};
}),
return_items: returnCart.map(rc => ({
ref_sale_item_id: rc.ref_sale_item_id,
ref_sale_id: rc.ref_sale_id,
ref_invoice_no: rc.ref_invoice_no,
item_id: rc.item_id,
batch_id: rc.batch_id,
return_qty: rc.return_qty,
return_unit: rc.return_unit,
units_per_pack: rc.units_per_pack,
return_total_units: rc.return_total_units,
unit_price: rc.unit_price,
return_amount: rc.line_total,
condition: rc.condition || 'GOOD',
reason: rc.reason || 'EXCESS_UNUSED'
}))
};
try {
const res = await fetch(API_BASE + 'sales/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.status) {
const msg = data.refund_amount > 0
? `Return Bill ${data.invoice_no} processed! Refund: ₹${data.refund_amount} (${data.refund_mode})`
: `Invoice ${data.invoice_no} generated successfully! Net Amount: ₹${data.net_amount}`;
notify(msg, 'success');
fetchInvoiceDetails(data.sale_id);
setIsCheckoutOpen(false);
setCart([]);
setReturnCart([]);
setWholeDiscountVal(0);
setSplitCash('');
setSplitUpi('');
setSplitCard('');
setUpiRefNo('');
setBankName('');
setCardRefNo('');
setPaymentRef('');
setRefundRefNo('');
resetPatient();
} else {
notify(data.message || 'Failed to complete transaction.', 'error');
}
} catch (e) {
notify('Sale error: ' + e.message, 'error');
} finally {
setIsProcessing(false);
}
};
const fetchInvoiceDetails = async (saleId) => {
try {
const res = await fetch(API_BASE + `sales/invoice/${saleId}`);
const data = await res.json();
if (data.status) {
setLastInvoice(data);
setIsPrintOpen(true);
}
} catch (e) {
console.error(e);
}
};
const generateThermalReceiptText = (inv) => {
if (!inv || !inv.sale) return '';
const sale = inv.sale;
const items = inv.items || [];
const returnItems = inv.return_items || [];
const LINE_WIDTH = 48; // Standard 80mm thermal receipt line length (Font A: 48 chars)
const center = (str) => {
str = (str || '').trim();
if (str.length >= LINE_WIDTH) return str.substring(0, LINE_WIDTH);
const pad = Math.floor((LINE_WIDTH - str.length) / 2);
return ' '.repeat(pad) + str;
};
const padLine = (left, right) => {
left = left || '';
right = right || '';
const space = LINE_WIDTH - left.length - right.length;
if (space <= 0) return left + ' ' + right;
return left + ' '.repeat(space) + right;
};
const divider = '='.repeat(LINE_WIDTH);
const dashDivider = '-'.repeat(LINE_WIDTH);
let lines = [];
lines.push(divider);
lines.push(center(sale.store_name || 'HOSPITAL PHARMACY'));
if (sale.building_name || sale.floor_no) {
lines.push(center(`${sale.building_name || ''}, ${sale.floor_no || ''}`));
}
if (sale.store_address) {
lines.push(center(sale.store_address));
}
if (sale.contact_phone) {
lines.push(center(`Phone: ${sale.contact_phone}`));
}
if (sale.drug_license_no_20b || sale.drug_license_no_21b) {
lines.push(center(`DL 20B: ${sale.drug_license_no_20b || 'N/A'} | 21B: ${sale.drug_license_no_21b || 'N/A'}`));
}
if (sale.gstin) {
lines.push(center(`GSTIN: ${sale.gstin}${sale.state_code ? ` (State: ${sale.state_code})` : ''}`));
}
if (sale.fssai_no) {
lines.push(center(`FSSAI: ${sale.fssai_no}`));
}
if (sale.abdm_hfr_id) {
lines.push(center(`HFR ID: ${sale.abdm_hfr_id}`));
}
lines.push(divider);
lines.push(center('RETAIL TAX INVOICE'));
lines.push(padLine(`Bill No: ${sale.invoice_no}`, `Date: ${sale.sale_date ? sale.sale_date.substring(0, 16) : ''}`));
lines.push(padLine(`Patient: ${sale.patient_name || 'Walk-in'}`, `UHID: ${sale.uhid || 'Walk-in'}`));
if (sale.patient_mobile) {
lines.push(padLine(`Mobile : ${sale.patient_mobile}`, `Age/Sex: ${sale.age || 'N/A'}/${sale.gender || 'N/A'}`));
}
if (sale.doctor_name) {
lines.push(`Doctor : ${sale.doctor_name}`);
}
if (sale.abha_id || sale.abha_address) {
lines.push(`ABHA : ${sale.abha_id || ''} ${sale.abha_address ? `(${sale.abha_address})` : ''}`);
}
if (sale.abdm_care_context_ref) {
lines.push(`ABDM Ref: ${sale.abdm_care_context_ref}`);
}
if (sale.ipd_id) {
lines.push(`IPD # : ${sale.ipd_id} (Ward: ${sale.ward_name || ''} Bed: ${sale.bed_no || ''})`);
}
lines.push(dashDivider);
// Header: Item (22), Batch (9), Qty (7), Total (10)
lines.push(
'Item'.padEnd(22) +
'Batch'.padEnd(9) +
'Qty'.padStart(7) +
'Total'.padStart(10)
);
lines.push(dashDivider);
if (items.length === 0) {
lines.push(center('- Return / Refund Only -'));
} else {
items.forEach((it) => {
const name = (it.item_name || '').substring(0, 21).padEnd(22);
const batch = (it.batch_no || '').substring(0, 8).padEnd(9);
const qtyStr = (it.sell_unit === 'Tablet'
? `${it.total_units}T`
: it.sell_unit === 'Combo'
? `${it.qty}S+${it.loose_qty}T`
: `${it.qty}`).substring(0, 6).padStart(7);
const amtStr = Number(it.total_amount || 0).toFixed(2).padStart(10);
lines.push(name + batch + qtyStr + amtStr);
let sub = ` Exp:${(it.expiry_date || '').substring(0, 7)} HSN:${it.hsn_code || '3004'} GST:${it.gst_rate}%`;
if (it.sell_unit === 'Tablet') {
sub += ` @Rs.${it.unit_price}/tab`;
} else {
sub += ` @Rs.${it.unit_mrp}`;
}
lines.push(sub.substring(0, LINE_WIDTH));
});
}
if (returnItems.length > 0) {
lines.push(dashDivider);
lines.push(center('-- RETURNED / EXCHANGED --'));
if (inv.credit_note) {
lines.push(center(`Credit Note: ${inv.credit_note.credit_note_no}`));
}
returnItems.forEach((it) => {
const name = ('*' + (it.item_name || '')).substring(0, 21).padEnd(22);
const batch = (it.batch_no || '').substring(0, 8).padEnd(9);
const qtyStr = ('-' + (it.sell_unit === 'Tablet' ? `${it.total_units}T` : `${it.qty}`)).substring(0, 6).padStart(7);
const amtStr = ('-' + Math.abs(Number(it.total_amount || 0)).toFixed(2)).padStart(10);
lines.push(name + batch + qtyStr + amtStr);
});
}
lines.push(dashDivider);
lines.push(padLine('Gross Total:', `Rs. ${Number(sale.gross_amount || 0).toFixed(2)}`));
if (Number(sale.discount_amount) > 0) {
lines.push(padLine('Discounts Allowed:', `-Rs. ${Number(sale.discount_amount).toFixed(2)}`));
}
if (Number(sale.return_amount) > 0) {
lines.push(padLine('Less Return Credit:', `-Rs. ${Number(sale.return_amount).toFixed(2)}`));
}
lines.push(padLine('Taxable Amount:', `Rs. ${Number(sale.taxable_amount || 0).toFixed(2)}`));
lines.push(padLine('CGST Amount:', `Rs. ${Number(sale.cgst_amount || 0).toFixed(2)}`));
lines.push(padLine('SGST Amount:', `Rs. ${Number(sale.sgst_amount || 0).toFixed(2)}`));
if (Number(sale.round_off) !== 0) {
lines.push(padLine('Round Off:', `Rs. ${Number(sale.round_off || 0).toFixed(2)}`));
}
lines.push(divider);
lines.push(padLine('NET PAYABLE:', `Rs. ${Number(sale.net_amount || 0).toFixed(2)}`));
lines.push(divider);
if (Number(sale.refund_amount) > 0) {
lines.push(padLine('REFUND PAID:', `Rs. ${Number(sale.refund_amount).toFixed(2)}`));
lines.push(padLine('Refund Mode:', `${sale.refund_mode} ${sale.refund_ref_no || ''}`));
lines.push(dashDivider);
}
lines.push(`Settlement: ${sale.payment_mode}`);
if (Number(sale.cash_paid) > 0) lines.push(` Cash Paid : Rs. ${Number(sale.cash_paid).toFixed(2)}`);
if (Number(sale.upi_paid) > 0) lines.push(` UPI/Bank : Rs. ${Number(sale.upi_paid).toFixed(2)} ${sale.upi_ref_no ? `[UTR:${sale.upi_ref_no}]` : ''}`);
if (Number(sale.card_paid) > 0) lines.push(` Card Paid : Rs. ${Number(sale.card_paid).toFixed(2)} ${sale.card_ref_no ? `[Ref:${sale.card_ref_no}]` : ''}`);
if (Number(sale.credit_amount) > 0) lines.push(` IPD Credit: Rs. ${Number(sale.credit_amount).toFixed(2)}`);
if (sale.is_bank_reconciled == 1) lines.push(' Status : Bank Statement Audited');
else lines.push(' Status : Bank Statement Pending Audit');
lines.push(dashDivider);
if (sale.registered_pharmacist_name) {
lines.push(`R.Ph: ${sale.registered_pharmacist_name} (${sale.pharmacist_reg_no || 'N/A'})`);
}
lines.push(center(sale.terms_conditions || 'Thank you! Get well soon.'));
lines.push(divider);
return lines.join('\n');
};
const copyThermalReceiptText = () => {
if (!lastInvoice) return;
const txt = generateThermalReceiptText(lastInvoice);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(txt).then(() => {
notify('Thermal receipt text copied to clipboard!', 'success');
}).catch(() => {
fallbackCopyText(txt);
});
} else {
fallbackCopyText(txt);
}
};
const fallbackCopyText = (txt) => {
const ta = document.createElement('textarea');
ta.value = txt;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
notify('Thermal receipt text copied to clipboard!', 'success');
} catch (err) {
notify('Unable to copy text.', 'error');
}
document.body.removeChild(ta);
};
const printThermalReceipt = () => {
if (!lastInvoice) return;
const txt = generateThermalReceiptText(lastInvoice);
const win = window.open('', '_blank', 'width=420,height=650');
if (!win) {
notify('Pop-up blocked. Please allow pop-ups to print thermal receipt.', 'warning');
return;
}
try {
win.document.title = 'Thermal Receipt - ' + (lastInvoice.sale ? lastInvoice.sale.invoice_no : '');
const style = win.document.createElement('style');
style.textContent = '@page { size: 80mm auto; margin: 0; } html, body { margin: 0; padding: 0; background: #fff; } body { padding: 4mm 2mm; font-family: Courier, monospace; font-size: 11px; line-height: 1.25; color: #000; } pre { margin: 0; font-family: inherit; font-size: inherit; white-space: pre-wrap; word-break: break-all; }';
win.document.head.appendChild(style);
const pre = win.document.createElement('pre');
pre.textContent = txt;
win.document.body.appendChild(pre);
setTimeout(() => {
try {
win.focus();
win.print();
setTimeout(() => { try { win.close(); } catch (err) {} }, 1000);
} catch (err) {}
}, 250);
} catch (e) {
console.error('Print thermal error:', e);
}
};
const printPdf = (format) => {
if (!lastInvoice || !lastInvoice.sale) return;
const saleId = lastInvoice.sale.sale_id;
const url = `/api/v1/medical-store/sales/invoice-pdf/${saleId}?format=${format || 'a4'}`;
let iframe = document.getElementById('pdf-print-iframe');
if (!iframe) {
iframe = document.createElement('iframe');
iframe.id = 'pdf-print-iframe';
iframe.style.position = 'fixed';
iframe.style.right = '0';
iframe.style.bottom = '0';
iframe.style.width = '0';
iframe.style.height = '0';
iframe.style.border = '0';
document.body.appendChild(iframe);
}
iframe.src = url;
iframe.onload = function() {
try {
iframe.contentWindow.focus();
iframe.contentWindow.print();
} catch (e) {
window.open(url, '_blank');
}
};
};
const downloadPdf = (format) => {
if (!lastInvoice || !lastInvoice.sale) return;
const saleId = lastInvoice.sale.sale_id;
window.open(`/api/v1/medical-store/sales/invoice-pdf/${saleId}?format=${format || 'a4'}&download=1`, '_blank');
};
// Global Keyboard Shortcuts (Desktop Pharmacy Standard)
useEffect(() => {
const handleGlobalKeyDown = (e) => {
// Dismiss Modals or Drawers on Escape
if (showFhirModal || showReturnModal || isCheckoutOpen || isPrintOpen) {
if (e.key === 'Escape') {
e.preventDefault();
if (showFhirModal) setShowFhirModal(false);
else if (showReturnModal) setShowReturnModal(false);
else if (isCheckoutOpen) setIsCheckoutOpen(false);
else if (isPrintOpen) setIsPrintOpen(false);
}
return;
}
// F2: Jump to Medicine Search / Barcode scanner
if (e.key === 'F2') {
e.preventDefault();
focusMedicineSearch();
return;
}
// F4: Jump to Patient Search
if (e.key === 'F4') {
e.preventDefault();
if (patientSearchRef.current) {
patientSearchRef.current.focus();
patientSearchRef.current.select();
}
return;
}
// F6 or Alt+R: Jump to Sales Return & Same-Invoice Exchange Desk
if (e.key === 'F6' || (e.altKey && (e.key === 'r' || e.key === 'R'))) {
e.preventDefault();
openReturnModal();
return;
}
// F7: Jump to Whole Bill Discount (opens Checkout Drawer if closed)
if (e.key === 'F7') {
e.preventDefault();
setIsCheckoutOpen(true);
setTimeout(() => {
if (wholeDiscountRef.current) {
wholeDiscountRef.current.focus();
wholeDiscountRef.current.select();
}
}, 150);
return;
}
// F8: Open Checkout Drawer or Cycle Payment Mode
if (e.key === 'F8') {
e.preventDefault();
if (!isCheckoutOpen) {
setIsCheckoutOpen(true);
} else {
const modes = ['Cash', 'UPI', 'Card', 'Mixed'];
if (selectedPatient?.has_active_ipd) modes.push('IPD_Credit');
const curIdx = modes.indexOf(paymentMode);
const nextMode = modes[(curIdx + 1) % modes.length];
setPaymentMode(nextMode);
}
return;
}
// F9 or Ctrl+Enter or Alt+S: Open Checkout or Complete Sale & Print Bill
if (e.key === 'F9' || (e.ctrlKey && e.key === 'Enter') || (e.altKey && (e.key === 's' || e.key === 'S'))) {
e.preventDefault();
if (!isCheckoutOpen) {
setIsCheckoutOpen(true);
} else {
if (!isProcessing && (cart.length > 0 || returnCart.length > 0)) {
processSale();
}
}
return;
}
};
window.addEventListener('keydown', handleGlobalKeyDown);
return () => window.removeEventListener('keydown', handleGlobalKeyDown);
}, [showFhirModal, showReturnModal, isCheckoutOpen, isPrintOpen, paymentMode, selectedPatient, isProcessing, cart, returnCart, splitRemaining, splitCashNum, splitUpiNum, splitCardNum, refundPayable, refundMode]);
return (
<div className="pos-counter-wrapper position-relative">
<div className="row g-3">
{/* Full-Width Workspace: Patient Selector, Medicine Lookup & Cart Grid */}
<div className="col-12">
{/* Patient Information Panel */}
<div className="counter-card position-relative">
<div className="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2">
<div className="d-flex align-items-center gap-2">
<span className="fw-bold text-dark fs-6"><i className="bi bi-person-fill text-primary me-1"></i> Patient &amp; Prescription</span>
<span className={`badge ${patientType === 'IPD' ? 'bg-danger' : (patientType === 'OPD' ? 'bg-primary' : 'bg-secondary')}`}>
{patientType}
</span>
</div>
{lastInvoice && (
<button
type="button"
className="btn btn-sm btn-outline-success py-0.5 px-2.5 fw-semibold d-inline-flex align-items-center gap-1 shadow-sm"
onClick={() => setIsPrintOpen(true)}
title="View & Print Last Bill"
style={{ fontSize: "0.75rem" }}
>
<i className="bi bi-printer"></i> Last Bill: <strong>{lastInvoice.sale.invoice_no}</strong>
</button>
)}
{selectedPatient && (
<div className="d-flex gap-2">
{(selectedPatient.has_active_opd || selectedPatient.has_active_ipd) && (
<button className="btn btn-sm btn-outline-success py-1 px-2.5" onClick={loadDoctorPrescription}>
<i className="bi bi-file-earmark-medical me-1"></i> 1-Click Load Doctor Rx
</button>
)}
<button className="btn btn-sm btn-outline-secondary py-1 px-2" onClick={resetPatient}>
<i className="bi bi-x-circle me-1"></i> Clear
</button>
</div>
)}
</div>
<div className="row g-2">
<div className="col-md-7 position-relative">
<div className="input-group input-group-sm">
<span className="input-group-text bg-light text-primary fw-bold" style={{ fontSize: '0.72rem' }}>
<kbd className="me-1" style={{ background: '#0f172a', color: '#38bdf8', fontWeight: 800, padding: '1px 5px', borderRadius: '3px' }}>F4</kbd>
<i className="bi bi-search"></i>
</span>
<input
type="text"
id="patient-search-input"
ref={patientSearchRef}
className="form-control"
placeholder="Search by UHID, Mobile, Name, OPD No, IPD No... (F4)"
value={patientQuery}
onChange={(e) => searchPatients(e.target.value)}
onKeyDown={handlePatientSearchKeyDown}
autoComplete="off"
/>
</div>
{/* Patient Search Results Dropdown */}
{patientResults.length > 0 && (
<div className="search-results-dropdown">
{patientResults.map((p, pIdx) => {
const isHighlighted = pIdx === selectedPatientIndex;
return (
<div
key={p.patient_id}
className={`search-item-row d-flex justify-content-between align-items-center ${isHighlighted ? 'bg-primary-subtle border-primary' : ''}`}
onClick={() => {
selectPatient(p);
setTimeout(() => focusMedicineSearch(), 50);
}}
style={{ cursor: 'pointer' }}
>
<div>
<div className="fw-bold text-dark">
{p.full_name} <small className="text-primary">({p.uhid})</small>
{isHighlighted && <span className="badge bg-primary text-white ms-2" style={{ fontSize: '0.65rem' }}>↵ Enter</span>}
{(p.abha_id || p.abha_address) && (
<span className="badge bg-success-subtle text-success border border-success ms-2" style={{ fontSize: '0.65rem' }}>
<i className="bi bi-shield-fill-check me-1"></i>ABHA: {p.abha_id || p.abha_address}
</span>
)}
</div>
<small className="text-muted"><i className="bi bi-telephone me-1"></i>{p.mobile || 'No Mobile'} • {p.gender} • {p.age}</small>
</div>
<div className="text-end">
{p.has_active_ipd && <span className="badge bg-danger d-block mb-1">IPD Admitted: {p.active_ipd.bed_info?.bed_no || 'Bed'}</span>}
{p.has_active_opd && <span className="badge bg-info d-block">OPD Visit Today</span>}
</div>
</div>
);
})}
</div>
)}
</div>
<div className="col-md-5">
<input
type="text"
className="form-control form-control-sm"
placeholder="Consultant Doctor Name"
value={doctorName}
onChange={(e) => setDoctorName(e.target.value)}
/>
</div>
</div>
{/* ABDM ABHA ID & Address Quick-Link Row */}
<div className="row g-2 mt-1">
<div className="col-md-6">
<div className="input-group input-group-sm">
<span className="input-group-text bg-light text-success fw-bold" style={{ fontSize: '0.72rem' }}>
<i className="bi bi-shield-check me-1"></i> ABHA No
</span>
<input
type="text"
className="form-control form-control-sm"
placeholder="14-Digit ABHA (e.g. 12-3456-7890-1234)"
value={walkinAbhaId}
onChange={(e) => setWalkinAbhaId(e.target.value)}
/>
</div>
</div>
<div className="col-md-6">
<div className="input-group input-group-sm">
<span className="input-group-text bg-light text-success fw-bold" style={{ fontSize: '0.72rem' }}>
<i className="bi bi-at me-1"></i> ABHA Address
</span>
<input
type="text"
className="form-control form-control-sm"
placeholder="username@abdm"
value={walkinAbhaAddress}
onChange={(e) => setWalkinAbhaAddress(e.target.value)}
/>
</div>
</div>
</div>
{/* Patient Context Tags */}
{selectedPatient && (
<div className="mt-2 p-2 bg-light rounded-2 d-flex flex-wrap gap-3 align-items-center" style={{ fontSize: '0.8rem' }}>
<span><strong>Name:</strong> {selectedPatient.full_name}</span>
<span><strong>UHID:</strong> <code className="text-primary">{selectedPatient.uhid}</code></span>
<span><strong>Gender/Age:</strong> {selectedPatient.gender}, {selectedPatient.age}</span>
{(selectedPatient.abha_id || selectedPatient.abha_address) && (
<span className="badge bg-success text-white py-1 px-2" style={{ fontSize: '0.75rem' }}>
<i className="bi bi-shield-fill-check me-1"></i> ABDM Linked: {selectedPatient.abha_id || ''} {selectedPatient.abha_address ? `(${selectedPatient.abha_address})` : ''}
</span>
)}
{selectedPatient.has_active_ipd && (
<span className="text-danger fw-bold">
<i className="bi bi-hospital me-1"></i>
{selectedPatient.active_ipd.bed_info?.ward_name || 'Ward'} / {selectedPatient.active_ipd.bed_info?.bed_no || 'Bed'} (Dr. {selectedPatient.active_ipd.doctor_name})
</span>
)}
{selectedPatient.has_active_opd && !selectedPatient.has_active_ipd && (
<span className="text-primary fw-bold">
<i className="bi bi-stethoscope me-1"></i>
OPD Dr. {selectedPatient.active_opd.doctor_name}
</span>
)}
</div>
)}
</div>
{/* Medicine Search & Barcode Scanner Line */}
<div className="counter-card position-relative">
{/* Keyboard Shortcuts Helper Ribbon */}
<div className="d-flex flex-wrap align-items-center gap-1.5 mb-2.5 py-1.5 px-2.5 rounded-3 shadow-sm" style={{ background: '#f8fafc', border: '1px solid #cbd5e1' }}>
<span className="fw-bold d-flex align-items-center me-1" style={{ color: '#0f172a', fontSize: '0.74rem' }}>
<i className="bi bi-keyboard-fill text-primary me-1 fs-6"></i>
<span>Keys:</span>
</span>
<span className="d-inline-flex align-items-center gap-1 px-2 py-0.5 bg-white rounded border shadow-xs" style={{ fontSize: '0.72rem' }}>
<kbd style={{ background: '#0f172a', color: '#38bdf8', fontWeight: 800, padding: '1px 5px', borderRadius: '3px', fontSize: '0.68rem' }}>F2</kbd>
<span className="fw-bold" style={{ color: '#0f172a' }}>Search</span>
</span>
<span className="d-inline-flex align-items-center gap-1 px-2 py-0.5 bg-white rounded border shadow-xs" style={{ fontSize: '0.72rem' }}>
<kbd style={{ background: '#0f172a', color: '#38bdf8', fontWeight: 800, padding: '1px 5px', borderRadius: '3px', fontSize: '0.68rem' }}>↑ / ↓</kbd>
<span className="fw-bold" style={{ color: '#0f172a' }}>Select</span>
</span>
<span className="d-inline-flex align-items-center gap-1 px-2 py-0.5 bg-white rounded border shadow-xs" style={{ fontSize: '0.72rem' }}>
<kbd style={{ background: '#0f172a', color: '#38bdf8', fontWeight: 800, padding: '1px 5px', borderRadius: '3px', fontSize: '0.68rem' }}>↵ Enter</kbd>
<span className="fw-bold" style={{ color: '#0f172a' }}>Batch</span>
</span>
<span className="d-inline-flex align-items-center gap-1 px-2 py-0.5 bg-white rounded border border-primary shadow-xs" style={{ fontSize: '0.72rem' }}>
<kbd style={{ background: '#0891b2', color: '#ffffff', fontWeight: 800, padding: '1px 5px', borderRadius: '3px', fontSize: '0.68rem' }}>↵ on Qty</kbd>
<span className="fw-bold text-primary">Next Item</span>
</span>
<span className="d-inline-flex align-items-center gap-1 px-2 py-0.5 bg-white rounded border shadow-xs" style={{ fontSize: '0.72rem' }}>
<kbd style={{ background: '#0f172a', color: '#10b981', fontWeight: 800, padding: '1px 5px', borderRadius: '3px', fontSize: '0.68rem' }}>Alt+T</kbd>
<span className="fw-bold" style={{ color: '#0f172a' }}>Loose</span>
</span>
<span className="d-inline-flex align-items-center gap-1 px-2 py-0.5 bg-white rounded border shadow-xs" style={{ fontSize: '0.72rem' }}>
<kbd style={{ background: '#0f172a', color: '#38bdf8', fontWeight: 800, padding: '1px 5px', borderRadius: '3px', fontSize: '0.68rem' }}>Alt+S</kbd>
<span className="fw-bold" style={{ color: '#0f172a' }}>Strip</span>
</span>
<span className="d-inline-flex align-items-center gap-1 px-2 py-0.5 bg-white rounded border shadow-xs" style={{ fontSize: '0.72rem' }}>
<kbd style={{ background: '#0f172a', color: '#f59e0b', fontWeight: 800, padding: '1px 5px', borderRadius: '3px', fontSize: '0.68rem' }}>F4</kbd>
<span className="fw-bold" style={{ color: '#0f172a' }}>Patient</span>
</span>
<span className="d-inline-flex align-items-center gap-1 px-2 py-0.5 bg-white rounded border shadow-xs" style={{ fontSize: '0.72rem' }}>
<kbd style={{ background: '#0f172a', color: '#a855f7', fontWeight: 800, padding: '1px 5px', borderRadius: '3px', fontSize: '0.68rem' }}>F8</kbd>
<span className="fw-bold" style={{ color: '#0f172a' }}>Payment</span>
</span>
<button
type="button"
className="btn btn-xs py-0.5 px-2 bg-white rounded border border-danger shadow-xs d-inline-flex align-items-center gap-1"
onClick={openReturnModal}
style={{ fontSize: '0.72rem' }}
title="Accept patient returns or exchange on current bill (Alt+R / F6)"
>
<kbd style={{ background: '#e11d48', color: '#ffffff', fontWeight: 800, padding: '1px 5px', borderRadius: '3px', fontSize: '0.68rem' }}>Alt+R / F6</kbd>
<span className="fw-bold text-danger">Return / Exch</span>
</button>
<span className="d-inline-flex align-items-center gap-1 px-2.5 py-0.5 rounded text-white shadow-sm" style={{ background: '#059669', fontSize: '0.72rem', border: '1px solid #047857' }}>
<kbd style={{ background: '#ffffff', color: '#065f46', fontWeight: 800, padding: '1px 5px', borderRadius: '3px', fontSize: '0.68rem' }}>F9 / Ctrl+↵</kbd>
<span className="fw-bold">Bill &amp; Print</span>
</span>
</div>
<div className="d-flex align-items-center gap-2 mb-2">
<i className="bi bi-upc-scan text-primary fs-5"></i>
<span className="fw-bold fs-6">Medicine Lookup &amp; Barcode Dispenser</span>
</div>
<div className="input-group">
<span className="input-group-text bg-light text-primary fw-bold" style={{ fontSize: '0.75rem' }}>
<kbd className="me-1" style={{ background: '#0f172a', color: '#38bdf8', fontWeight: 800, padding: '1px 5px', borderRadius: '3px' }}>F2</kbd>
<i className="bi bi-capsule"></i>
</span>
<input
type="text"
id="item-search-input"
ref={medicineSearchRef}
className="form-control fw-semibold"
placeholder="Type Medicine Name, Generic Molecule, or Scan Box Barcode... (Press F2 anytime)"
value={itemQuery}
onChange={(e) => searchItems(e.target.value)}
onKeyDown={handleMedicineSearchKeyDown}
autoComplete="off"
/>
{isSearchingItems && <span className="input-group-text bg-light"><i className="bi bi-hourglass-split"></i></span>}
</div>
{/* Item & FEFO Batch Dropdown */}
{itemResults.length > 0 && (
<div className="search-results-dropdown">
{itemResults.map(it => (
<div key={it.item_id} className="search-item-row">
<div className="d-flex justify-content-between align-items-center mb-1">
<div>
<span className="fw-bold text-dark fs-6">{it.item_name}</span>
<span className="badge bg-secondary ms-2" style={{ fontSize: '0.65rem' }}>{it.unit_pack}</span>
{it.drug_schedule && it.drug_schedule !== 'OTC' && (
<span className="badge bg-warning-subtle text-warning border border-warning ms-1" style={{ fontSize: '0.65rem' }}>
{it.drug_schedule}
</span>
)}
</div>
<div>
<span className="text-muted me-2" style={{ fontSize: '0.75rem' }}>Total Stock: {it.total_stock}</span>
</div>
</div>
<small className="text-muted d-block mb-1"><i className="bi bi-diagram-3 me-1"></i>{it.generic_name} • HSN: {it.hsn_code}</small>
{/* Batches Table */}
{it.batches && it.batches.length > 0 ? (
<div className="d-flex flex-wrap gap-2 mt-1">
{it.batches.map(b => {
const isHighlighted = selectableBatches[selectedBatchIndex] &&
selectableBatches[selectedBatchIndex].batch.batch_id === b.batch_id &&
selectableBatches[selectedBatchIndex].item.item_id === it.item_id;
return (
<button
key={b.batch_id}
className={`btn btn-sm ${isHighlighted ? 'btn-warning text-dark border-2 shadow-sm fw-bold' : (b.is_expired ? 'btn-outline-danger disabled' : 'btn-outline-dark')} d-flex align-items-center gap-1 py-0.5 px-2`}
style={{ fontSize: '0.75rem', transform: isHighlighted ? 'scale(1.04)' : 'scale(1)', transition: 'transform 0.1s' }}
onClick={() => addItemToCart(it, b, 1)}
disabled={b.is_expired}
title={isHighlighted ? 'Press Enter to select this batch' : `Select batch ${b.batch_no}`}
>
<strong>{b.batch_no}</strong>
<span>(Exp: {b.expiry_display})</span>
<span className="text-success fw-bold">₹{b.mrp}</span>
<span className="badge bg-primary text-white">{b.current_qty} left</span>
{it.fefo_batch && it.fefo_batch.batch_id === b.batch_id && (
<span className="badge-fefo ms-1">FEFO</span>
)}
{b.is_near_expiry && (
<span className="badge-near-expiry ms-1">Near Exp</span>
)}
{b.is_expired && (
<span className="badge-expired ms-1">EXPIRED</span>
)}
{isHighlighted && (
<span className="badge bg-dark text-warning ms-1" style={{ fontSize: '0.62rem' }}>↵ Enter</span>
)}
</button>
);
})}
</div>
) : (
<span className="text-danger fw-bold" style={{ fontSize: '0.75rem' }}>Out of Stock in this Counter</span>
)}
</div>
))}
</div>
)}
</div>
{/* Billing Cart Table */}
<div className="counter-card p-0 overflow-hidden">
<div className="p-3 pb-2 d-flex flex-wrap justify-content-between align-items-center gap-2 border-bottom">
<div className="d-flex align-items-center gap-2">
<h6 className="mb-0 fw-bold"><i className="bi bi-cart-check-fill text-success me-1"></i> Dispensing Cart ({cart.length} new)</h6>
{returnCart.length > 0 && (
<span className="badge bg-danger-subtle text-danger border border-danger">
<i className="bi bi-arrow-return-left me-1"></i>{returnCart.length} return/exchange
</span>
)}
</div>
<div className="d-flex gap-2 align-items-center">
{(cart.length > 0 || returnCart.length > 0) && (
<button
type="button"
className="btn btn-sm btn-success py-0.5 px-2.5 fw-bold d-inline-flex align-items-center gap-1.5 shadow-sm"
onClick={() => setIsCheckoutOpen(true)}
style={{ fontSize: '0.75rem', background: '#10b981', borderColor: '#059669' }}
title="Open Checkout Desk to finalize bill (F8 / F9)"
>
<i className="bi bi-wallet2"></i> Checkout {refundPayable > 0 ? `(Refund ₹${refundPayable})` : `(₹${netPayable})`} <i className="bi bi-arrow-right"></i>
</button>
)}
<button
type="button"
className="btn btn-sm btn-outline-danger py-0.5 px-2.5 fw-semibold d-inline-flex align-items-center gap-1"
onClick={openReturnModal}
style={{ fontSize: '0.75rem' }}
title="Add patient return or item exchange (Alt+R / F6)"
>
<i className="bi bi-arrow-return-left"></i> Add Return / Exch
</button>
{(cart.length > 0 || returnCart.length > 0) && (
<button className="btn btn-sm btn-link text-muted text-decoration-none p-0" onClick={() => { setCart([]); setReturnCart([]); }}>
Clear All
</button>
)}
</div>
</div>
<div className="table-responsive" style={{ maxHeight: 380 }}>
<table className="table table-pos mb-0">
<thead>
<tr>
<th>#</th>
<th>Medicine Name</th>
<th>Batch / Exp</th>
<th style={{ width: 155 }}>Qty &amp; Unit</th>
<th>Rate (₹)</th>
<th style={{ width: 125 }}>Disc (% / ₹)</th>
<th>GST%</th>
<th className="text-end">Total (₹)</th>
<th style={{ width: 40 }}></th>
</tr>
</thead>
<tbody>
{cart.length === 0 && returnCart.length === 0 ? (
<tr>
<td colSpan="9" className="text-center py-4 text-muted">
<i className="bi bi-basket3 fs-3 d-block mb-1"></i>
Scan barcode or search medicine to add items, or click <strong className="text-danger">Add Return / Exch</strong> (Alt+R / F6).
</td>
</tr>
) : (
cart.map((it, idx) => {
const info = getItemLineInfo(it);
const lineGross = info.lineGross;
const lineDisc = getItemDiscount(it);
const lineNet = Math.max(0, lineGross - lineDisc).toFixed(2);
const upp = info.upp;
return (
<tr key={`sale-${idx}`}>
<td>{idx + 1}</td>
<td>
<div className="fw-bold">{it.item_name}</div>
<small className="text-muted">{it.generic_name} • HSN: {it.hsn_code}</small>
{upp > 1 && (
<span className="badge bg-secondary-subtle text-secondary border ms-1" style={{ fontSize: '0.62rem' }}>
Pack: {it.unit_pack || `${upp} Tabs`}
</span>
)}
</td>
<td>
<span className="badge bg-light text-dark border">{it.batch_no}</span>
<div className="text-muted" style={{ fontSize: '0.7rem' }}>Exp: {it.expiry_display}</div>
</td>
<td>
{upp > 1 ? (
<div style={{ maxWidth: 155 }}>
{it.sell_unit === 'Combo' ? (
<div className="input-group input-group-sm">
<select
className="form-select form-select-sm py-0 px-1 fw-bold bg-warning-subtle text-dark border-warning"
style={{ maxWidth: 64, fontSize: '0.72rem' }}
value={it.sell_unit}
onChange={(e) => switchSellUnit(idx, e.target.value)}
title="Unit Type: Combo (Strips + Loose Tablets)"
>
<option value="Tablet">Tab</option>
<option value="Strip">Strip</option>
<option value="Combo">Str+Tab</option>
</select>
<input
type="number"
id={`cart-qty-strip-${idx}`}
className="form-control form-control-sm text-center fw-bold px-1"
min="0"
placeholder="Str"
title="Whole Strips"
value={it.strip_qty ?? 1}
onChange={(e) => updateCartItem(idx, 'strip_qty', Number(e.target.value))}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
focusMedicineSearch();
} else if (e.key === 'Delete' && e.shiftKey) {
e.preventDefault();
removeCartItem(idx);
focusMedicineSearch();
}
}}
/>
<input
type="number"
id={`cart-qty-loose-${idx}`}
className="form-control form-control-sm text-center fw-bold text-success border-success px-1"
min="0"
placeholder="Tab"
title="Loose Tablets"
value={it.loose_qty ?? 0}
onChange={(e) => updateCartItem(idx, 'loose_qty', Number(e.target.value))}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
focusMedicineSearch();
} else if (e.key === 'Delete' && e.shiftKey) {
e.preventDefault();
removeCartItem(idx);
focusMedicineSearch();
}
}}
/>
</div>
) : (
<div className="input-group input-group-sm">
<select
className={`form-select form-select-sm py-0 px-1 fw-bold ${it.sell_unit === 'Strip' ? 'bg-primary-subtle text-primary border-primary' : 'bg-success-subtle text-success border-success'}`}
style={{ maxWidth: 65, fontSize: '0.72rem' }}
value={it.sell_unit || 'Tablet'}
onChange={(e) => switchSellUnit(idx, e.target.value)}
title="Select Unit Type before quantity"
>
<option value="Tablet">Tab</option>
<option value="Strip">Strip</option>
<option value="Combo">Str+Tab</option>
</select>
<input
type="number"
id={it.sell_unit === 'Strip' ? `cart-qty-strip-${idx}` : `cart-qty-loose-${idx}`}
className={`form-control form-control-sm text-center fw-bold ${it.sell_unit === 'Strip' ? 'text-primary' : 'text-success border-success'}`}
min="1"
max={it.current_qty}
value={it.sell_unit === 'Strip' ? (it.strip_qty ?? it.qty ?? 1) : (it.loose_qty ?? it.qty ?? 1)}
onChange={(e) => {
const val = Number(e.target.value);
const updated = [...cart];
if (it.sell_unit === 'Strip') {
updated[idx].strip_qty = val;
} else {
updated[idx].loose_qty = val;
}
updated[idx].qty = val;
setCart(updated);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
focusMedicineSearch();
} else if (e.altKey && (e.key === 's' || e.key === 'S')) {
e.preventDefault();
switchSellUnit(idx, 'Strip');
} else if (e.altKey && (e.key === 't' || e.key === 'T')) {
e.preventDefault();
switchSellUnit(idx, 'Tablet');
} else if (e.altKey && (e.key === 'c' || e.key === 'C')) {
e.preventDefault();
switchSellUnit(idx, 'Combo');
} else if (e.key === 'Delete' && e.shiftKey) {
e.preventDefault();
removeCartItem(idx);
focusMedicineSearch();
}
}}
/>
</div>
)}
<div className="text-muted text-center mt-0.5" style={{ fontSize: '0.64rem' }}>
{it.sell_unit === 'Tablet' ? `= ${info.totalUnits} Tab (₹${info.perUnitMrp}/tab)` : (it.sell_unit === 'Combo' ? `= ${info.totalUnits} Tab total` : `= ${info.totalUnits} Tab (${upp}/str)`)}
</div>
</div>
) : (
<div style={{ maxWidth: 120 }}>
<div className="input-group input-group-sm">
<span className="input-group-text bg-light text-muted px-1.5 py-0 fw-semibold" style={{ fontSize: '0.68rem', maxWidth: 50 }}>
{it.unit_pack || 'Unit'}
</span>
<input
type="number"
id={`cart-qty-strip-${idx}`}
className="form-control form-control-sm text-center fw-bold"
min="1"
max={it.current_qty}
value={it.qty}
onChange={(e) => updateCartItem(idx, 'qty', Number(e.target.value))}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
focusMedicineSearch();
} else if (e.key === 'Delete' && e.shiftKey) {
e.preventDefault();
removeCartItem(idx);
focusMedicineSearch();
}
}}
/>
</div>
<div className="text-muted text-center mt-0.5" style={{ fontSize: '0.64rem' }}>
₹{it.unit_mrp} each
</div>
</div>
)}
</td>
<td>
<div className="fw-semibold">
{it.sell_unit === 'Tablet' ? (
<span>₹{info.perUnitMrp} <small className="text-muted">/tab</small></span>
) : it.sell_unit === 'Combo' ? (
<span>₹{it.unit_mrp} <small className="text-muted">/str</small></span>
) : (
<span>₹{it.unit_mrp} {upp > 1 ? <small className="text-muted">/strip</small> : ''}</span>
)}
</div>
{upp > 1 && (
<small className="text-muted d-block" style={{ fontSize: '0.68rem' }}>
{it.sell_unit === 'Tablet' ? `Strip: ₹${it.unit_mrp}` : `Tab: ₹${info.perUnitMrp}`}
</small>
)}
</td>
<td>
<div className="input-group input-group-sm" style={{ width: 115 }}>
<input
type="number"
id={`cart-disc-${idx}`}
className="form-control form-control-sm text-center px-1 fw-semibold"
min="0"
max={it.discount_type === 'pct' ? 100 : lineGross}
step={it.discount_type === 'pct' ? '0.5' : '1'}
value={it.discount_val ?? ''}
placeholder="0"
onChange={(e) => updateCartItem(idx, 'discount_val', e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
focusMedicineSearch();
}
}}
/>
<button
type="button"
className={`btn btn-sm ${it.discount_type === 'pct' ? 'btn-primary' : 'btn-warning text-dark'} px-2 py-0 fw-bold`}
title={`Click to switch between % and ₹ (Currently: ${it.discount_type === 'pct' ? 'Percentage' : 'Value in Rupees'})`}
onClick={() => toggleItemDiscType(idx)}
>
{it.discount_type === 'pct' ? '%' : '₹'}
</button>
</div>
{lineDisc > 0 && (
<div className="text-muted text-center mt-0.5" style={{ fontSize: '0.68rem' }}>
{it.discount_type === 'pct'
? `-₹${lineDisc.toFixed(2)}`
: `${(lineGross > 0 ? ((lineDisc / lineGross) * 100).toFixed(1) : 0)}% off`}
</div>
)}
</td>
<td>{it.gst_rate}%</td>
<td className="text-end fw-bold text-dark">₹{lineNet}</td>
<td>
<button className="btn btn-sm btn-outline-danger py-0 px-1" onClick={() => removeCartItem(idx)}>
<i className="bi bi-trash"></i>
</button>
</td>
</tr>
);
}).concat(
returnCart.map((rc, rIdx) => (
<tr key={`ret-${rIdx}`} style={{ backgroundColor: 'rgba(244, 63, 94, 0.05)' }}>
<td>
<span className="badge bg-danger text-white">RET</span>
</td>
<td>
<div className="fw-bold text-danger">
<i className="bi bi-arrow-return-left me-1"></i>
{rc.item_name}
</div>
<small className="text-muted">
Bill: <code>{rc.ref_invoice_no}</code> • {rc.reason ? rc.reason.replace(/_/g, ' ') : 'Return'}
</small>
</td>
<td>
<span className="badge bg-light text-dark border">{rc.batch_no}</span>
<div className="text-muted" style={{ fontSize: '0.7rem' }}>Exp: {rc.expiry_date ? rc.expiry_date.substring(0,7) : ''}</div>
</td>
<td>
<span className="badge bg-danger-subtle text-danger border border-danger fw-bold">
- {rc.return_qty} {rc.return_unit}
</span>
<div className="text-muted mt-0.5" style={{ fontSize: '0.64rem' }}>
({rc.return_total_units} Tabs • {rc.condition === 'GOOD' ? 'Restocking' : 'Discard'})
</div>
</td>
<td>
<div className="fw-semibold text-danger">
₹{rc.unit_price} <small className="text-muted">/tab</small>
</div>
{rc.units_per_pack > 1 && (
<small className="text-muted d-block" style={{ fontSize: '0.68rem' }}>
MRP: ₹{rc.unit_mrp}
</small>
)}
</td>
<td>
<span className="badge bg-secondary-subtle text-secondary" style={{ fontSize: '0.7rem' }}>
{rc.condition === 'GOOD' ? 'Restock' : 'Scrap'}
</span>
</td>
<td>{rc.gst_rate || 12}%</td>
<td className="text-end fw-bold text-danger">
-₹{Number(rc.line_total).toFixed(2)}
</td>
<td>
<button
className="btn btn-sm btn-outline-danger py-0 px-1"
onClick={() => setReturnCart(returnCart.filter((_, i) => i !== rIdx))}
title="Remove from returns"
>
<i className="bi bi-trash"></i>
</button>
</td>
</tr>
))
)
)}
</tbody>
</table>
</div>
{/* Cart Footer Bar: Live Totals & Checkout Trigger */}
<div className="p-3 bg-light border-top d-flex flex-wrap justify-content-between align-items-center gap-3">
<div className="d-flex flex-wrap align-items-center gap-3">
<div className="d-flex align-items-center gap-2">
<span className="badge bg-secondary-subtle text-secondary border px-2.5 py-1.5 fw-semibold" style={{ fontSize: '0.8rem' }}>
<i className="bi bi-cart-check me-1"></i> {cart.length + returnCart.length} Line Items
</span>
<span className="text-muted small">
Gross: <strong className="text-dark">₹{grossTotal.toFixed(2)}</strong>
</span>
{itemDiscountTotal > 0 && (
<span className="text-muted small">
Item Disc: <strong className="text-danger">-₹{itemDiscountTotal.toFixed(2)}</strong>
</span>
)}
{wholeDiscountAmt > 0 && (
<span className="text-muted small">
Bill Disc: <strong className="text-danger">-₹{wholeDiscountAmt.toFixed(2)}</strong>
</span>
)}
{returnTotal > 0 && (
<span className="text-muted small">
Return Credit: <strong className="text-danger">-₹{returnTotal.toFixed(2)}</strong>
</span>
)}
</div>
</div>
<div className="d-flex align-items-center gap-3">
<div className="text-end">
<div className="text-muted small text-uppercase fw-semibold" style={{ fontSize: '0.68rem', letterSpacing: '0.5px' }}>
{refundPayable > 0 ? 'Customer Refund' : 'Net Amount'}
</div>
<div className={`fs-4 fw-bolder ${refundPayable > 0 ? 'text-danger' : 'text-success'}`} style={{ lineHeight: 1.1 }}>
₹{refundPayable > 0 ? refundPayable.toFixed(2) : netPayable.toFixed(2)}
</div>
</div>
<button
type="button"
id="btn-pos-checkout-trigger"
className="btn btn-success btn-lg px-4 py-2.5 fw-bold shadow d-flex align-items-center gap-2"
disabled={cart.length === 0 && returnCart.length === 0}
onClick={() => setIsCheckoutOpen(true)}
style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
border: 'none',
borderRadius: '10px',
fontSize: '1rem',
letterSpacing: '0.3px',
transition: 'transform 0.15s ease, box-shadow 0.15s ease'
}}
title="Open Checkout Desk to review totals, select payment method and complete sale (F8 / F9)"
>
<i className="bi bi-wallet2 fs-5"></i>
<span>Checkout</span>
<span className="badge bg-white text-success ms-1 px-2 py-0.5" style={{ fontSize: '0.72rem', fontWeight: 800 }}>F8 / F9</span>
<i className="bi bi-arrow-right fs-5"></i>
</button>
</div>
</div>
</div>
</div>
</div>
{/* Slide-in Checkout Desk Drawer & Backdrop */}
<div
className={`checkout-drawer-overlay ${isCheckoutOpen ? 'open' : ''}`}
onClick={() => setIsCheckoutOpen(false)}
/>
<div className={`checkout-drawer ${isCheckoutOpen ? 'open' : ''}`} id="pos-checkout-drawer">
<div className="checkout-drawer-header">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-cash-coin text-warning fs-5"></i>
<div>
<h6 className="mb-0 fw-bold text-white">Checkout Desk</h6>
<small className="text-secondary" style={{ fontSize: '0.72rem' }}>
Counter: <span className="text-info fw-bold">{store?.store_code || ''}</span> • {store?.store_name}
</small>
</div>
</div>
<button
type="button"
className="btn btn-sm btn-outline-light rounded-pill px-3 py-1 d-flex align-items-center gap-1.5"
onClick={() => setIsCheckoutOpen(false)}
title="Close Checkout Drawer (Esc)"
>
<i className="bi bi-x-lg"></i>
<span>Close</span>
<kbd className="ms-1" style={{ background: '#334155', color: '#f8fafc', fontSize: '0.65rem', padding: '1px 4px', borderRadius: '3px' }}>Esc</kbd>
</button>
</div>
<div className="checkout-drawer-body">
<h5 className="fw-bold text-white mb-3 d-flex justify-content-between align-items-center">
<span>Checkout Desk</span>
<span className="fs-6 text-primary">{store?.store_code || ''}</span>
</h5>
{/* Whole Bill Discount Controller */}
<div className="p-2.5 rounded-3 mb-3" style={{ background: 'rgba(255, 255, 255, 0.08)', border: '1px solid rgba(255, 255, 255, 0.15)' }}>
<div className="d-flex justify-content-between align-items-center mb-1.5">
<label className="text-white small fw-bold mb-0 d-flex align-items-center gap-1">
<i className="bi bi-tag-fill text-warning"></i> Whole Bill Discount
</label>
<div className="btn-group btn-group-sm">
<button
type="button"
className={`btn btn-xs py-0.5 px-2 ${wholeDiscountType === 'pct' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light text-white'}`}
style={{ fontSize: '0.72rem' }}
onClick={() => setWholeDiscountType('pct')}
>
% Percent
</button>
<button
type="button"
className={`btn btn-xs py-0.5 px-2 ${wholeDiscountType === 'val' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light text-white'}`}
style={{ fontSize: '0.72rem' }}
onClick={() => setWholeDiscountType('val')}
>
₹ Fixed Value
</button>
</div>
</div>
<div className="input-group input-group-sm mb-2">
<span className="input-group-text bg-dark border-secondary text-warning fw-bold">
{wholeDiscountType === 'pct' ? '%' : '₹'}
</span>
<input
type="number"
id="whole-discount-input"
ref={wholeDiscountRef}
className="form-control form-control-sm bg-dark text-white border-secondary fw-bold"
min="0"
max={wholeDiscountType === 'pct' ? 100 : balanceAfterItemDisc}
step={wholeDiscountType === 'pct' ? '0.5' : '1'}
placeholder={wholeDiscountType === 'pct' ? 'Enter % (F7)' : 'Enter ₹ (F7)'}
value={wholeDiscountVal || ''}
onChange={(e) => setWholeDiscountVal(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (!isProcessing && cart.length > 0) {
processSale();
}
}
}}
/>
{Number(wholeDiscountVal) > 0 && (
<button
type="button"
className="btn btn-sm btn-outline-danger"
title="Reset Whole Bill Discount"
onClick={() => setWholeDiscountVal(0)}
>
<i className="bi bi-x-lg"></i>
</button>
)}
</div>
{/* Quick Preset Buttons */}
<div className="d-flex flex-wrap gap-1 align-items-center">
<small className="text-slate-400 me-1" style={{ fontSize: '0.7rem', color: '#94a3b8' }}>Presets:</small>
{wholeDiscountType === 'pct' ? (
[5, 10, 15, 20].map(p => (
<button
key={p}
type="button"
className={`badge border ${Number(wholeDiscountVal) === p ? 'bg-warning text-dark border-warning' : 'bg-transparent text-white border-secondary'}`}
style={{ cursor: 'pointer', padding: '4px 8px', fontSize: '0.7rem' }}
onClick={() => setWholeDiscountVal(p)}
>
{p}%
</button>
))
) : (
[20, 50, 100, 200].map(v => (
<button
key={v}
type="button"
className={`badge border ${Number(wholeDiscountVal) === v ? 'bg-warning text-dark border-warning' : 'bg-transparent text-white border-secondary'}`}
style={{ cursor: 'pointer', padding: '4px 8px', fontSize: '0.7rem' }}
onClick={() => setWholeDiscountVal(v)}
>
₹{v}
</button>
))
)}
{Number(wholeDiscountVal) > 0 && (
<button
type="button"
className="badge bg-danger-subtle text-danger border border-danger ms-auto"
style={{ cursor: 'pointer', padding: '4px 6px', fontSize: '0.7rem' }}
onClick={() => setWholeDiscountVal(0)}
>
Clear
</button>
)}
</div>
</div>
{/* Breakdown */}
<div className="d-flex justify-content-between mb-1.5 text-slate-300" style={{ color: '#cbd5e1' }}>
<span>Gross Items Total:</span>
<span className="fw-bold">₹{grossTotal.toFixed(2)}</span>
</div>
{itemDiscountTotal > 0 && (
<div className="d-flex justify-content-between mb-1 text-slate-300" style={{ fontSize: '0.84rem', color: '#94a3b8' }}>
<span>Item-Level Discounts:</span>
<span className="text-danger">-₹{itemDiscountTotal.toFixed(2)}</span>
</div>
)}
{wholeDiscountAmt > 0 && (
<div className="d-flex justify-content-between mb-1 text-slate-300" style={{ fontSize: '0.84rem', color: '#94a3b8' }}>
<span>Whole Bill Discount ({wholeDiscountType === 'pct' ? `${wholeDiscountVal}%` : `₹${wholeDiscountVal}`}):</span>
<span className="text-danger">-₹{wholeDiscountAmt.toFixed(2)}</span>
</div>
)}
{discountTotal > 0 && (
<div className="d-flex justify-content-between mb-2 text-slate-300" style={{ color: '#cbd5e1' }}>
<span>Total Discounts:</span>
<span className="text-danger fw-bold">-₹{discountTotal.toFixed(2)}</span>
</div>
)}
{returnTotal > 0 && (
<div className="d-flex justify-content-between mb-2 text-danger fw-bold py-1 px-2 rounded" style={{ background: 'rgba(244, 63, 94, 0.15)', border: '1px solid rgba(244, 63, 94, 0.3)' }}>
<span><i className="bi bi-arrow-return-left me-1"></i> Less Return Credit:</span>
<span>-₹{returnTotal.toFixed(2)}</span>
</div>
)}
<div className="d-flex justify-content-between mb-2 text-slate-300" style={{ color: '#cbd5e1' }}>
<span>Round Off:</span>
<span>₹{roundOff}</span>
</div>
<hr style={{ borderColor: 'rgba(255,255,255,0.15)' }} />
{refundPayable > 0 ? (
<div className="p-3 rounded-3 mb-3 border border-warning" style={{ background: 'rgba(245, 158, 11, 0.15)' }}>
<div className="d-flex justify-content-between align-items-center">
<span className="fs-6 fw-bold text-white"><i className="bi bi-wallet2 text-warning me-1"></i> REFUND TO PATIENT:</span>
<span className="fs-3 fw-bolder text-warning">₹{refundPayable.toLocaleString('en-IN')}</span>
</div>
<small className="text-white-50 d-block mt-1" style={{ fontSize: '0.72rem' }}>
Return credit exceeds new items. Refund this net difference from counter cash drawer or UPI.
</small>
<div className="mt-2.5">
<label className="form-label text-white small fw-bold mb-1">Refund Outflow Method</label>
<div className="btn-group btn-group-sm w-100 mb-2">
<button
type="button"
className={`btn ${refundMode === 'Cash' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light'}`}
onClick={() => setRefundMode('Cash')}
>
<i className="bi bi-cash-stack me-1"></i> Cash Refund
</button>
<button
type="button"
className={`btn ${refundMode === 'UPI' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light'}`}
onClick={() => setRefundMode('UPI')}
>
<i className="bi bi-qr-code me-1"></i> UPI / Bank Refund
</button>
</div>
{refundMode === 'UPI' && (
<input
type="text"
className="form-control form-control-sm bg-dark text-white border-warning"
placeholder="Patient UPI VPA or Bank Reference #"
value={refundRefNo}
onChange={(e) => setRefundRefNo(e.target.value)}
/>
)}
</div>
</div>
) : (
<div className="d-flex justify-content-between align-items-center mb-4">
<span className="fs-5 fw-bold text-white">Net Payable:</span>
<span className="fs-3 fw-bolder text-warning">₹{netPayable.toLocaleString('en-IN')}</span>
</div>
)}
{/* Payment Mode Selection (Only when patient owes money) */}
{refundPayable <= 0 && (
<div>
<div className="d-flex justify-content-between align-items-center mb-2">
<label className="form-label text-white fw-bold mb-0">Payment Method</label>
{paymentMode === 'Mixed' && (
<span className="badge bg-warning text-dark"><i className="bi bi-pie-chart me-1"></i> Partial / Split Mode</span>
)}
</div>
<div className="row g-2 mb-3">
<div className="col-4">
<button
className={`btn btn-sm w-100 ${paymentMode === 'Cash' ? 'btn-primary-custom' : 'btn-outline-light'}`}
onClick={() => setPaymentMode('Cash')}
>
<i className="bi bi-cash-stack me-1"></i> Cash
</button>
</div>
<div className="col-4">
<button
className={`btn btn-sm w-100 ${paymentMode === 'UPI' ? 'btn-primary-custom' : 'btn-outline-light'}`}
onClick={() => setPaymentMode('UPI')}
>
<i className="bi bi-qr-code-scan me-1"></i> UPI / QR
</button>
</div>
<div className="col-4">
<button
className={`btn btn-sm w-100 ${paymentMode === 'Mixed' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light'}`}
onClick={() => {
setPaymentMode('Mixed');
if (!splitCash && !splitUpi) {
setSplitCash('');
setSplitUpi('');
}
}}
title="Partial payment by Cash and UPI/Bank"
>
<i className="bi bi-pie-chart-fill me-1"></i> Split / Mixed
</button>
</div>
<div className="col-6">
<button
className={`btn btn-sm w-100 ${paymentMode === 'Card' ? 'btn-primary-custom' : 'btn-outline-light'}`}
onClick={() => setPaymentMode('Card')}
>
<i className="bi bi-credit-card me-1"></i> Card Swipe
</button>
</div>
<div className="col-6">
<button
className={`btn btn-sm w-100 ${paymentMode === 'IPD_Credit' ? 'btn-danger' : 'btn-outline-light'}`}
onClick={() => setPaymentMode('IPD_Credit')}
title="Charge to IPD Patient Bill"
>
<i className="bi bi-hospital me-1"></i> IPD Bill Post
</button>
</div>
</div>
{paymentMode === 'IPD_Credit' && (
<div className="alert alert-warning py-2 mb-3" style={{ fontSize: '0.78rem' }}>
<i className="bi bi-info-circle me-1"></i>
Medicines will be charged directly to the patient's IPD Hospital Account and settled at discharge.
</div>
)}
{/* Mixed / Split Payment Breakdown Panel */}
{paymentMode === 'Mixed' && (
<div className="card bg-dark border-warning mb-3 p-2.5 rounded-3 text-white" style={{ fontSize: '0.82rem' }}>
<div className="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom border-secondary">
<span className="fw-bold text-warning"><i className="bi bi-receipt-cutoff me-1"></i> Split Payment Allocation</span>
<span className="fw-bold">Total: ₹{netPayable}</span>
</div>
<div className="row g-2 mb-2">
<div className="col-6">
<label className="form-label text-white-50 mb-1" style={{ fontSize: '0.75rem' }}>Cash Amount (₹)</label>
<div className="input-group input-group-sm">
<span className="input-group-text bg-secondary border-0 text-white">₹</span>
<input
type="number"
className="form-control form-control-sm bg-dark text-white border-secondary"
placeholder="e.g. 35"
value={splitCash}
onChange={(e) => setSplitCash(e.target.value)}
step="any"
/>
</div>
</div>
<div className="col-6">
<label className="form-label text-white-50 mb-1" style={{ fontSize: '0.75rem' }}>UPI / Bank (₹)</label>
<div className="input-group input-group-sm">
<span className="input-group-text bg-secondary border-0 text-white">₹</span>
<input
type="number"
className="form-control form-control-sm bg-dark text-white border-secondary"
placeholder="e.g. 100"
value={splitUpi}
onChange={(e) => setSplitUpi(e.target.value)}
step="any"
/>
</div>
</div>
</div>
{/* UTR & Bank Details for the UPI portion */}
{Number(splitUpi) > 0 && (
<div className="p-2 rounded bg-black bg-opacity-25 border border-secondary mb-2">
<div className="row g-2">
<div className="col-7">
<label className="form-label text-warning mb-0" style={{ fontSize: '0.72rem' }}>
UPI UTR / Trans. Ref # <span className="text-danger">*</span>
</label>
<input
type="text"
className="form-control form-control-sm bg-dark text-white border-warning"
placeholder="e.g. 425109876543 (12-digit)"
value={upiRefNo}
onChange={(e) => setUpiRefNo(e.target.value)}
/>
<span style={{ fontSize: '0.65rem' }} className="text-white-50">Used by Accountant for statement audit</span>
</div>
<div className="col-5">
<label className="form-label text-white-50 mb-0" style={{ fontSize: '0.72rem' }}>Bank / QR</label>
<input
type="text"
className="form-control form-control-sm bg-dark text-white border-secondary"
placeholder="e.g. SBI, HDFC"
list="commonBankList"
value={bankName}
onChange={(e) => setBankName(e.target.value)}
/>
<datalist id="commonBankList">
<option value="SBI" />
<option value="HDFC Bank" />
<option value="ICICI Bank" />
<option value="Axis Bank" />
<option value="Punjab National Bank" />
<option value="Bank of Baroda" />
<option value="Google Pay / PhonePe" />
<option value="Paytm" />
</datalist>
</div>
</div>
</div>
)}
{/* Optional Card portion */}
<div className="mb-2">
<div className="d-flex justify-content-between align-items-center">
<span style={{ fontSize: '0.75rem' }} className="text-white-50">Card Swiped (if any):</span>
<div className="input-group input-group-sm" style={{ width: 140 }}>
<span className="input-group-text bg-secondary border-0 text-white" style={{ fontSize: '0.7rem' }}>₹</span>
<input
type="number"
className="form-control form-control-sm bg-dark text-white border-secondary"
placeholder="0"
value={splitCard}
onChange={(e) => setSplitCard(e.target.value)}
step="any"
/>
</div>
</div>
{Number(splitCard) > 0 && (
<input
type="text"
className="form-control form-control-sm bg-dark text-white border-secondary mt-1"
placeholder="Card Slip Auth / Ref No"
value={cardRefNo}
onChange={(e) => setCardRefNo(e.target.value)}
/>
)}
</div>
{/* Live Balance Summary & Autofill helpers */}
<div className="p-2 rounded bg-black bg-opacity-40 border border-secondary">
<div className="d-flex justify-content-between mb-1" style={{ fontSize: '0.75rem' }}>
<span>Allocated: <strong>₹{totalSplitAllocated.toFixed(2)}</strong></span>
<span>Bill Net: <strong>₹{netPayable}</strong></span>
</div>
{Math.abs(splitRemaining) <= 0.01 ? (
<div className="text-success fw-bold d-flex align-items-center gap-1" style={{ fontSize: '0.78rem' }}>
<i className="bi bi-check-circle-fill"></i> Balanced! Ready to print bill.
</div>
) : splitRemaining > 0 ? (
<div>
<div className="text-warning fw-bold mb-1.5" style={{ fontSize: '0.78rem' }}>
<i className="bi bi-exclamation-circle me-1"></i> Balance Remaining: ₹{splitRemaining.toFixed(2)}
</div>
<div className="d-flex gap-1">
<button
type="button"
className="btn btn-xs btn-outline-warning py-0"
style={{ fontSize: '0.7rem' }}
onClick={() => setSplitUpi(String(((Number(splitUpi) || 0) + splitRemaining).toFixed(2)))}
>
+ ₹{splitRemaining.toFixed(2)} to UPI
</button>
<button
type="button"
className="btn btn-xs btn-outline-info py-0"
style={{ fontSize: '0.7rem' }}
onClick={() => setSplitCash(String(((Number(splitCash) || 0) + splitRemaining).toFixed(2)))}
>
+ ₹{splitRemaining.toFixed(2)} to Cash
</button>
</div>
</div>
) : (
<div className="text-danger fw-bold" style={{ fontSize: '0.78rem' }}>
<i className="bi bi-x-circle-fill me-1"></i> Overallocated by ₹{Math.abs(splitRemaining).toFixed(2)}
</div>
)}
</div>
</div>
)}
{/* Single UPI Mode Details */}
{paymentMode === 'UPI' && (
<div className="bg-dark p-2.5 rounded-3 mb-3 border border-secondary">
{store?.upi_id && (
<div className="text-center mb-2">
<small className="text-muted d-block" style={{ fontSize: '0.75rem' }}>Store UPI VPA: <span className="text-warning fw-bold">{store?.upi_id}</span></small>
</div>
)}
<div className="row g-2">
<div className="col-7">
<label className="form-label text-white-50 mb-1" style={{ fontSize: '0.72rem' }}>UPI UTR / Ref No <span className="text-danger">*</span></label>
<input
type="text"
className="form-control form-control-sm bg-dark text-white border-warning"
placeholder="12-digit UTR No"
value={upiRefNo}
onChange={(e) => setUpiRefNo(e.target.value)}
/>
</div>
<div className="col-5">
<label className="form-label text-white-50 mb-1" style={{ fontSize: '0.72rem' }}>Bank Name</label>
<input
type="text"
className="form-control form-control-sm bg-dark text-white border-secondary"
placeholder="e.g. SBI, HDFC"
value={bankName}
onChange={(e) => setBankName(e.target.value)}
/>
</div>
</div>
<small className="text-white-50 d-block mt-1" style={{ fontSize: '0.68rem' }}>
<i className="bi bi-shield-check me-1 text-success"></i> Accountant will match this UTR against the hospital bank statement.
</small>
</div>
)}
{/* Single Card Mode Details */}
{paymentMode === 'Card' && (
<div className="bg-dark p-2.5 rounded-3 mb-3 border border-secondary">
<div className="row g-2">
<div className="col-7">
<label className="form-label text-white-50 mb-1" style={{ fontSize: '0.72rem' }}>Card Slip / Auth Ref No</label>
<input
type="text"
className="form-control form-control-sm bg-dark text-white border-secondary"
placeholder="POS Machine Auth Code"
value={cardRefNo}
onChange={(e) => setCardRefNo(e.target.value)}
/>
</div>
<div className="col-5">
<label className="form-label text-white-50 mb-1" style={{ fontSize: '0.72rem' }}>Bank / Terminal</label>
<input
type="text"
className="form-control form-control-sm bg-dark text-white border-secondary"
placeholder="e.g. HDFC EDC"
value={bankName}
onChange={(e) => setBankName(e.target.value)}
/>
</div>
</div>
</div>
)}
</div>
)}
{/* Print Format Selector */}
<div className="mb-3">
<label className="form-label text-white small fw-bold mb-1">Invoice Print Format</label>
<div className="btn-group btn-group-sm w-100">
<button
type="button"
className={`btn ${printFormat === 'thermal' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light'}`}
onClick={() => setPrintFormat('thermal')}
>
<i className="bi bi-receipt me-1"></i> Thermal (Text)
</button>
<button
type="button"
className={`btn ${printFormat === 'a5' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light'}`}
onClick={() => setPrintFormat('a5')}
>
<i className="bi bi-file-earmark-pdf me-1"></i> A5 (PDF)
</button>
<button
type="button"
className={`btn ${printFormat === 'a4' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light'}`}
onClick={() => setPrintFormat('a4')}
>
<i className="bi bi-file-earmark-pdf me-1"></i> A4 (PDF)
</button>
</div>
</div>
<button
className={`btn ${refundPayable > 0 ? 'btn-warning text-dark fw-bold' : 'btn-success-custom'} w-100 py-2.5 fs-6`}
onClick={processSale}
disabled={isProcessing || (cart.length === 0 && returnCart.length === 0) || (refundPayable === 0 && paymentMode === 'Mixed' && Math.abs(splitRemaining) > 0.01)}
>
{isProcessing ? (
<span><i className="bi bi-hourglass-split me-2"></i> Generating Bill &amp; Credit Note...</span>
) : (refundPayable === 0 && paymentMode === 'Mixed' && Math.abs(splitRemaining) > 0.01) ? (
<span><i className="bi bi-exclamation-triangle me-2"></i> Allocate Balance (₹{splitRemaining > 0 ? splitRemaining.toFixed(2) + ' left' : Math.abs(splitRemaining).toFixed(2) + ' over'})</span>
) : refundPayable > 0 ? (
<span><i className="bi bi-arrow-return-left me-2"></i> Process Return &amp; Refund ₹{refundPayable}</span>
) : returnCart.length > 0 ? (
<span><i className="bi bi-arrow-left-right me-2"></i> Complete Exchange Bill &amp; Print ({printFormat.toUpperCase()})</span>
) : (
<span><i className="bi bi-printer me-2"></i> Complete Sale &amp; Print ({printFormat.toUpperCase()})</span>
)}
</button>
</div>
</div>
{/* Slide-in Bill Print Drawer & Backdrop */}
<div
className={`print-drawer-overlay ${isPrintOpen ? 'open' : ''}`}
onClick={() => setIsPrintOpen(false)}
/>
<div className={`print-drawer ${isPrintOpen ? 'open' : ''}`} id="pos-print-drawer">
{lastInvoice && (
<>
<div className="print-drawer-header">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-receipt text-warning fs-5"></i>
<div>
<h6 className="mb-0 fw-bold text-white">Invoice &amp; Print Desk</h6>
<small className="text-secondary" style={{ fontSize: '0.72rem' }}>
Bill: <span className="text-info fw-bold">{lastInvoice.sale.invoice_no}</span>
{lastInvoice.sale.abdm_care_context_ref && (
<span className="ms-2 text-success">
<i className="bi bi-shield-fill-check me-0.5"></i> ABDM Linked
</span>
)}
</small>
</div>
</div>
<div className="d-flex align-items-center gap-1.5 flex-wrap">
<button
className="btn btn-xs btn-outline-success text-white py-1 px-2"
onClick={() => viewAbdmBundle(lastInvoice.sale.sale_id)}
title="View official ABDM FHIR R4 MedicationDispense JSON Document Bundle"
style={{ fontSize: '0.72rem' }}
>
<i className="bi bi-filetype-json me-1"></i> FHIR R4
</button>
<div className="btn-group btn-group-sm">
<button className={`btn btn-xs py-1 px-2 ${printFormat === 'thermal' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light text-white'}`} style={{ fontSize: '0.72rem' }} onClick={() => setPrintFormat('thermal')}>
<i className="bi bi-receipt me-1"></i> Thermal (Text)
</button>
<button className={`btn btn-xs py-1 px-2 ${printFormat === 'a5' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light text-white'}`} style={{ fontSize: '0.72rem' }} onClick={() => setPrintFormat('a5')}>
<i className="bi bi-file-earmark-pdf me-1"></i> A5 (PDF)
</button>
<button className={`btn btn-xs py-1 px-2 ${printFormat === 'a4' ? 'btn-warning text-dark fw-bold' : 'btn-outline-light text-white'}`} style={{ fontSize: '0.72rem' }} onClick={() => setPrintFormat('a4')}>
<i className="bi bi-file-earmark-pdf me-1"></i> A4 (PDF)
</button>
</div>
{printFormat === 'thermal' ? (
<>
<button
className="btn btn-sm btn-outline-light py-1 px-2 text-white"
onClick={copyThermalReceiptText}
title="Copy plain-text receipt to clipboard"
style={{ fontSize: '0.75rem' }}
>
<i className="bi bi-clipboard me-1 text-warning"></i> Copy Text
</button>
<button
className="btn btn-sm btn-warning text-dark fw-bold py-1 px-2.5 ms-0.5"
onClick={printThermalReceipt}
title="Print on 80mm Thermal Receipt Printer"
style={{ fontSize: '0.75rem' }}
>
<i className="bi bi-printer-fill me-1"></i> Print Thermal
</button>
</>
) : (
<>
<button
className="btn btn-sm btn-outline-light py-1 px-2 text-white"
onClick={() => downloadPdf(printFormat)}
title={`Download ${printFormat.toUpperCase()} PDF Document`}
style={{ fontSize: '0.75rem' }}
>
<i className="bi bi-download me-1 text-info"></i> PDF
</button>
<button
className="btn btn-sm btn-primary py-1 px-2.5 ms-0.5 fw-bold"
onClick={() => printPdf(printFormat)}
title={`Print Vector-Clean ${printFormat.toUpperCase()} PDF`}
style={{ fontSize: '0.75rem' }}
>
<i className="bi bi-printer me-1"></i> Print PDF ({printFormat.toUpperCase()})
</button>
</>
)}
<button
type="button"
className="btn btn-sm btn-outline-light rounded-pill px-2.5 py-1 ms-1 d-flex align-items-center gap-1"
onClick={() => setIsPrintOpen(false)}
title="Close Print Drawer (Esc)"
>
<i className="bi bi-x-lg"></i>
<kbd style={{ background: '#334155', color: '#f8fafc', fontSize: '0.62rem', padding: '1px 3px', borderRadius: '3px' }}>Esc</kbd>
</button>
</div>
</div>
<div className="print-drawer-body">
<div className="print-paper-card">
<div id="printable-container">
{/* Format 1: 80mm Thermal Slip in Pure Text Layout */}
{printFormat === 'thermal' && (
<div className="p-2 text-center">
<div className="d-flex justify-content-between align-items-center mb-2 mx-auto" style={{ maxWidth: '360px' }}>
<span className="badge bg-dark text-warning border border-secondary" style={{ fontSize: '0.72rem' }}>
<i className="bi bi-receipt me-1"></i> 80mm Thermal Receipt (Plain Text)
</span>
<div className="d-flex gap-1">
<button
type="button"
className="btn btn-xs btn-outline-secondary text-dark bg-light py-0.5 px-2"
onClick={copyThermalReceiptText}
title="Copy receipt text to clipboard"
style={{ fontSize: '0.72rem' }}
>
<i className="bi bi-clipboard me-1 text-primary"></i> Copy Text
</button>
<button
type="button"
className="btn btn-xs btn-warning text-dark fw-bold py-0.5 px-2"
onClick={printThermalReceipt}
title="Print Thermal 80mm Receipt"
style={{ fontSize: '0.72rem' }}
>
<i className="bi bi-printer-fill me-1"></i> Print Thermal
</button>
</div>
</div>
<pre
id="thermal-receipt-preview"
style={{
fontFamily: "'Courier New', Courier, monospace",
fontSize: '11px',
lineHeight: '1.28',
backgroundColor: '#ffffff',
color: '#000000',
padding: '16px',
border: '1px solid #cbd5e1',
borderRadius: '6px',
boxShadow: '0 4px 15px rgba(0,0,0,0.06)',
width: '360px',
maxWidth: '100%',
margin: '0 auto',
textAlign: 'left',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all'
}}
>
{generateThermalReceiptText(lastInvoice)}
</pre>
</div>
)}
{/* Format 2: A5 Half-Page Invoice (Normal OPD/Walk-in) */}
{printFormat === 'a5' && (
<div className="print-a5-invoice">
<div className="d-flex justify-content-between align-items-center bg-primary-subtle border border-primary-subtle rounded px-2.5 py-1.5 mb-2.5 no-print">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-file-earmark-pdf-fill text-danger fs-5"></i>
<div>
<div className="fw-bold text-dark" style={{ fontSize: '0.78rem' }}>A5 Retail Tax Invoice (PDF Ready)</div>
<small className="text-muted" style={{ fontSize: '0.68rem' }}>Direct server vector PDF • Zero clipped columns</small>
</div>
</div>
<div className="d-flex gap-1.5">
<button className="btn btn-xs btn-outline-primary py-1 px-2" onClick={() => downloadPdf('a5')} style={{ fontSize: '0.72rem' }}>
<i className="bi bi-download me-1"></i> Download PDF
</button>
<button className="btn btn-xs btn-primary py-1 px-2.5 fw-bold" onClick={() => printPdf('a5')} style={{ fontSize: '0.72rem' }}>
<i className="bi bi-printer me-1"></i> Print PDF
</button>
</div>
</div>
<div className="d-flex justify-content-between align-items-start border-bottom pb-2 mb-2">
<div>
<h6 className="fw-bold mb-0 text-primary">{lastInvoice.sale.store_name}</h6>
<div style={{ fontSize: '10px' }}>{lastInvoice.sale.building_name}, {lastInvoice.sale.floor_no}, {lastInvoice.sale.store_address}</div>
<div style={{ fontSize: '10px' }}>
<strong>DL 20B:</strong> {lastInvoice.sale.drug_license_no_20b} | <strong>DL 21B:</strong> {lastInvoice.sale.drug_license_no_21b}
{lastInvoice.sale.abdm_hfr_id && <span> | <strong>HFR ID:</strong> {lastInvoice.sale.abdm_hfr_id}</span>}
</div>
<div style={{ fontSize: '10px' }}><strong>GSTIN:</strong> {lastInvoice.sale.gstin} | <strong>Phone:</strong> {lastInvoice.sale.contact_phone}</div>
</div>
<div className="text-end">
<span className="badge bg-dark text-white mb-1">RETAIL TAX INVOICE</span>
<div className="fw-bold" style={{ fontSize: '12px' }}>{lastInvoice.sale.invoice_no}</div>
<div className="text-muted" style={{ fontSize: '10px' }}>{lastInvoice.sale.sale_date}</div>
</div>
</div>
<div className="row g-1 mb-2 p-1.5 bg-light rounded" style={{ fontSize: '10px' }}>
<div className="col-6"><strong>Patient:</strong> {lastInvoice.sale.patient_name} {lastInvoice.sale.uhid ? `(${lastInvoice.sale.uhid})` : ''}</div>
<div className="col-6"><strong>Prescribed By:</strong> {lastInvoice.sale.doctor_name}</div>
<div className="col-6"><strong>Age/Gender:</strong> {lastInvoice.sale.age || 'N/A'} / {lastInvoice.sale.gender || 'N/A'}</div>
<div className="col-6"><strong>Payment Mode:</strong> {lastInvoice.sale.payment_mode}</div>
{(lastInvoice.sale.abha_id || lastInvoice.sale.abha_address) && (
<div className="col-12 text-success fw-bold">
<strong>ABHA:</strong> {lastInvoice.sale.abha_id || 'N/A'} ({lastInvoice.sale.abha_address || 'N/A'})
{lastInvoice.sale.abdm_care_context_ref && <span> | <strong>Care Context:</strong> {lastInvoice.sale.abdm_care_context_ref}</span>}
</div>
)}
</div>
<table className="table table-bordered table-sm mb-2" style={{ fontSize: '10px' }}>
<thead className="table-light">
<tr>
<th>#</th>
<th>Medicine Name</th>
<th>HSN</th>
<th>Batch</th>
<th>Exp</th>
<th>Qty &amp; Unit</th>
<th>Unit MRP (₹)</th>
<th className="text-end">Amount (₹)</th>
</tr>
</thead>
<tbody>
{lastInvoice.items.length === 0 ? (
<tr>
<td colSpan={8} className="text-center text-muted py-2">
No medicines dispensed on this bill (Return / Refund Only)
</td>
</tr>
) : (
lastInvoice.items.map((it, i) => (
<tr key={i}>
<td>{i + 1}</td>
<td className="fw-bold">
{it.item_name}
{it.snomed_ct_code && <div className="text-muted" style={{ fontSize: '8px' }}>SNOMED: {it.snomed_ct_code}</div>}
{Number(it.units_per_pack) > 1 && (
<div className="text-muted fw-normal" style={{ fontSize: '8px' }}>Pack: 1 Strip = {it.units_per_pack} Tabs</div>
)}
</td>
<td>{it.hsn_code}</td>
<td>{it.batch_no}</td>
<td>{it.expiry_date.substring(0,7)}</td>
<td>
<span className="fw-bold">
{it.sell_unit === 'Tablet'
? `${it.total_units} Tab`
: it.sell_unit === 'Combo'
? `${it.qty} Str + ${it.loose_qty} Tab`
: (Number(it.units_per_pack) > 1 ? `${it.qty} Strip` : `${it.qty} ${it.unit_pack || 'Unit'}`)}
</span>
{Number(it.units_per_pack) > 1 && it.sell_unit !== 'Tablet' && (
<div className="text-muted" style={{ fontSize: '8px' }}>({it.total_units} Tabs)</div>
)}
</td>
<td>
{it.sell_unit === 'Tablet' ? (
<span>₹{it.unit_price} <small className="text-muted">/tab</small></span>
) : (
<span>₹{it.unit_mrp} {Number(it.units_per_pack) > 1 ? <small className="text-muted">/strip</small> : ''}</span>
)}
</td>
<td className="text-end fw-bold">₹{it.total_amount}</td>
</tr>
))
)}
</tbody>
</table>
{lastInvoice.return_items && lastInvoice.return_items.length > 0 && (
<div className="mb-2">
<div className="d-flex justify-content-between align-items-center bg-danger-subtle px-2 py-1 rounded-top border border-bottom-0 border-danger-subtle text-danger" style={{ fontSize: '10px' }}>
<span className="fw-bold"><i className="bi bi-arrow-return-left me-1"></i> RETURNED / EXCHANGED MEDICINES</span>
{lastInvoice.credit_note && (
<span className="badge bg-danger">Credit Note: {lastInvoice.credit_note.credit_note_no}</span>
)}
</div>
<table className="table table-bordered table-sm mb-0 text-danger border-danger-subtle" style={{ fontSize: '9px' }}>
<thead className="table-danger">
<tr>
<th>#</th>
<th>Medicine Name</th>
<th>Batch</th>
<th>Original Bill</th>
<th>Condition</th>
<th>Returned Qty</th>
<th>Credit Rate (₹)</th>
<th className="text-end">Credit Total (₹)</th>
</tr>
</thead>
<tbody>
{lastInvoice.return_items.map((it, i) => (
<tr key={`a5-ret-${i}`}>
<td>{i + 1}</td>
<td className="fw-bold text-decoration-line-through">{it.item_name}</td>
<td>{it.batch_no}</td>
<td><code>{it.ref_invoice_no || 'N/A'}</code></td>
<td><span className="badge bg-secondary">{it.return_condition}</span></td>
<td className="fw-bold">
-{it.sell_unit === 'Tablet' ? `${it.total_units} Tab` : `${it.qty} ${it.unit_pack || 'Unit'}`}
</td>
<td>₹{it.unit_price}</td>
<td className="text-end fw-bold">-₹{Math.abs(Number(it.total_amount)).toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<div className="d-flex justify-content-between align-items-center border-top pt-2">
<div style={{ fontSize: '9px' }}>
R.Ph: {lastInvoice.sale.registered_pharmacist_name || 'Pharmacist'} ({lastInvoice.sale.pharmacist_reg_no})
{lastInvoice.sale.pharmacist_hpr_id && <span> | HPR ID: {lastInvoice.sale.pharmacist_hpr_id}</span>}
</div>
<div className="text-end">
<div style={{ fontSize: '10px' }} className="text-muted">
Gross: ₹{lastInvoice.sale.gross_amount}
{Number(lastInvoice.sale.discount_amount) > 0 && (
<span className="text-danger ms-2">Disc: -₹{lastInvoice.sale.discount_amount}</span>
)}
{Number(lastInvoice.sale.return_amount) > 0 && (
<span className="text-danger fw-bold ms-2">Return Credit: -₹{lastInvoice.sale.return_amount}</span>
)}
</div>
<span className="fs-6 fw-bold text-primary">
{Number(lastInvoice.sale.refund_amount) > 0 ? `NET REFUND: ₹${lastInvoice.sale.refund_amount}` : `TOTAL: ₹${lastInvoice.sale.net_amount}`}
</span>
{Number(lastInvoice.sale.refund_amount) > 0 && (
<div className="text-danger fw-bold small">
Refund Paid via {lastInvoice.sale.refund_mode} {lastInvoice.sale.refund_ref_no ? `[Ref: ${lastInvoice.sale.refund_ref_no}]` : ''}
</div>
)}
<div style={{ fontSize: '9px' }} className="text-muted mt-0.5">
<span>Settlement: <strong>{lastInvoice.sale.payment_mode}</strong></span>
{Number(lastInvoice.sale.cash_paid) > 0 && <span className="ms-1.5">Cash: ₹{lastInvoice.sale.cash_paid}</span>}
{Number(lastInvoice.sale.upi_paid) > 0 && (
<span className="ms-1.5">UPI: ₹{lastInvoice.sale.upi_paid} {lastInvoice.sale.upi_ref_no ? `(UTR: ${lastInvoice.sale.upi_ref_no})` : ''}</span>
)}
{Number(lastInvoice.sale.card_paid) > 0 && <span className="ms-1.5">Card: ₹{lastInvoice.sale.card_paid}</span>}
{lastInvoice.sale.is_bank_reconciled == 1 && <span className="ms-1.5 text-success fw-bold">✓ Audited</span>}
</div>
</div>
</div>
</div>
)}
{/* Format 3: A4 Comprehensive Hospital Tax Invoice (IPD & Large Bills) */}
{printFormat === 'a4' && (
<div className="print-a4-invoice">
<div className="d-flex justify-content-between align-items-center bg-primary-subtle border border-primary-subtle rounded px-3 py-1.5 mb-3 no-print">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-file-earmark-pdf-fill text-danger fs-5"></i>
<div>
<div className="fw-bold text-dark" style={{ fontSize: '0.8rem' }}>A4 Hospital GST Tax Invoice (PDF Ready)</div>
<small className="text-muted" style={{ fontSize: '0.68rem' }}>Server-rendered vector PDF • Zero clipped columns</small>
</div>
</div>
<div className="d-flex gap-1.5">
<button className="btn btn-xs btn-outline-primary py-1 px-2.5" onClick={() => downloadPdf('a4')} style={{ fontSize: '0.72rem' }}>
<i className="bi bi-download me-1"></i> Download PDF
</button>
<button className="btn btn-xs btn-primary py-1 px-2.5 fw-bold" onClick={() => printPdf('a4')} style={{ fontSize: '0.72rem' }}>
<i className="bi bi-printer me-1"></i> Print PDF
</button>
</div>
</div>
<div className="d-flex justify-content-between align-items-center border-bottom pb-3 mb-3">
<div>
<h4 className="fw-bold text-primary mb-1">{lastInvoice.sale.store_name}</h4>
<div className="text-muted">{lastInvoice.sale.building_name}, {lastInvoice.sale.floor_no}, {lastInvoice.sale.store_address}</div>
<div><strong>Drug License 20-B:</strong> {lastInvoice.sale.drug_license_no_20b} | <strong>21-B:</strong> {lastInvoice.sale.drug_license_no_21b} | <strong>FSSAI:</strong> {lastInvoice.sale.fssai_no || 'N/A'}</div>
<div>
<strong>GSTIN:</strong> {lastInvoice.sale.gstin} | <strong>State:</strong> {lastInvoice.sale.state_name} (Code: {lastInvoice.sale.state_code})
{lastInvoice.sale.abdm_hfr_id && <span> | <strong>ABDM HFR Facility ID:</strong> {lastInvoice.sale.abdm_hfr_id}</span>}
</div>
</div>
<div className="text-end">
<span className="badge bg-primary fs-6 px-3 py-1.5 mb-1">HOSPITAL GST TAX INVOICE</span>
<h5 className="fw-bold mb-0 text-dark">{lastInvoice.sale.invoice_no}</h5>
<div className="text-muted small">Date &amp; Time: {lastInvoice.sale.sale_date}</div>
</div>
</div>
{/* Patient & Hospital Admission Info */}
<div className="row g-2 mb-3 p-2 bg-light rounded border">
<div className="col-4"><strong>Patient Name:</strong> {lastInvoice.sale.patient_name}</div>
<div className="col-4"><strong>UHID No:</strong> <code className="text-primary fw-bold">{lastInvoice.sale.uhid || 'Walk-in'}</code></div>
<div className="col-4"><strong>Phone:</strong> {lastInvoice.sale.patient_mobile || 'N/A'}</div>
<div className="col-4"><strong>Age / Gender:</strong> {lastInvoice.sale.age || 'N/A'} / {lastInvoice.sale.gender || 'N/A'}</div>
<div className="col-4"><strong>Consultant Doctor:</strong> {lastInvoice.sale.doctor_name}</div>
<div className="col-4"><strong>Doctor Reg No:</strong> {lastInvoice.sale.doctor_reg_no || 'N/A'}</div>
{(lastInvoice.sale.abha_id || lastInvoice.sale.abha_address) && (
<div className="col-12 bg-success-subtle p-1.5 rounded text-success fw-bold border border-success mt-1">
<i className="bi bi-shield-fill-check me-1"></i> ABDM Linked: ABHA Number: {lastInvoice.sale.abha_id || 'N/A'} | ABHA Address: {lastInvoice.sale.abha_address || 'N/A'} | Care Context: {lastInvoice.sale.abdm_care_context_ref || 'N/A'}
</div>
)}
{lastInvoice.sale.ipd_id && (
<div className="col-12 text-danger fw-bold border-top pt-1 mt-1">
<i className="bi bi-hospital me-1"></i> IPD Admission #{lastInvoice.sale.ipd_id} • Ward: {lastInvoice.sale.ward_name || 'General'} • Bed: {lastInvoice.sale.bed_no || 'Bed'} (Charged to IPD Running Bill)
</div>
)}
</div>
{/* Items Table */}
<table className="table table-bordered mb-3">
<thead className="table-light">
<tr>
<th style={{ width: 40 }}>#</th>
<th>Medicine &amp; Formulation</th>
<th>HSN</th>
<th>Batch No</th>
<th>Exp Date</th>
<th style={{ width: 105 }}>Qty &amp; Unit</th>
<th>Rate (₹)</th>
<th>Taxable (₹)</th>
<th>CGST (₹)</th>
<th>SGST (₹)</th>
<th className="text-end">Total (₹)</th>
</tr>
</thead>
<tbody>
{lastInvoice.items.length === 0 ? (
<tr>
<td colSpan={11} className="text-center text-muted py-3">
No medicines dispensed on this bill (Pure Sales Return &amp; Customer Refund)
</td>
</tr>
) : (
lastInvoice.items.map((it, i) => (
<tr key={i}>
<td>{i + 1}</td>
<td className="fw-bold">
{it.item_name}
{it.snomed_ct_code && (
<div className="text-muted fw-normal" style={{ fontSize: '9px' }}>
SNOMED-CT: <code>{it.snomed_ct_code}</code> ({it.snomed_display || ''})
</div>
)}
{Number(it.units_per_pack) > 1 && (
<div className="text-muted fw-normal" style={{ fontSize: '9px' }}>
Packing: 1 Strip of {it.units_per_pack} Tablets
</div>
)}
</td>
<td>{it.hsn_code}</td>
<td>{it.batch_no}</td>
<td>{it.expiry_date}</td>
<td>
<span className="fw-bold">
{it.sell_unit === 'Tablet'
? `${it.total_units} Tab`
: it.sell_unit === 'Combo'
? `${it.qty} Str + ${it.loose_qty} Tab`
: (Number(it.units_per_pack) > 1 ? `${it.qty} Strip` : `${it.qty} ${it.unit_pack || 'Unit'}`)}
</span>
{Number(it.units_per_pack) > 1 && it.sell_unit !== 'Tablet' && (
<div className="text-muted" style={{ fontSize: '9px' }}>
({it.total_units} Tablets)
</div>
)}
</td>
<td>
{it.sell_unit === 'Tablet' ? (
<span>₹{it.unit_price} <small className="text-muted">/tab</small></span>
) : (
<span>₹{it.unit_mrp} {Number(it.units_per_pack) > 1 ? <small className="text-muted">/strip</small> : ''}</span>
)}
</td>
<td>₹{it.taxable_value}</td>
<td>₹{it.cgst_amount}</td>
<td>₹{it.sgst_amount}</td>
<td className="text-end fw-bold">₹{it.total_amount}</td>
</tr>
))
)}
</tbody>
</table>
{/* Credit Note / Returned Medicines Table */}
{lastInvoice.return_items && lastInvoice.return_items.length > 0 && (
<div className="mb-3">
<div className="d-flex justify-content-between align-items-center bg-danger-subtle p-2 rounded-top border border-bottom-0 border-danger text-danger">
<span className="fw-bold">
<i className="bi bi-arrow-return-left me-1"></i> GST CREDIT NOTE / RETURNED &amp; EXCHANGED MEDICINES
</span>
{lastInvoice.credit_note && (
<span className="badge bg-danger fs-7">
Credit Note No: {lastInvoice.credit_note.credit_note_no} ({lastInvoice.credit_note.return_date})
</span>
)}
</div>
<table className="table table-bordered mb-0 text-danger border-danger">
<thead className="table-danger">
<tr>
<th style={{ width: 40 }}>#</th>
<th>Returned Medicine &amp; Formulation</th>
<th>HSN</th>
<th>Batch No</th>
<th>Original Invoice</th>
<th>Condition / Reason</th>
<th style={{ width: 105 }}>Qty Returned</th>
<th>Credit Rate (₹)</th>
<th>Taxable (₹)</th>
<th>CGST (₹)</th>
<th>SGST (₹)</th>
<th className="text-end">Credit Total (₹)</th>
</tr>
</thead>
<tbody>
{lastInvoice.return_items.map((it, i) => (
<tr key={`a4-ret-${i}`}>
<td>{i + 1}</td>
<td className="fw-bold text-decoration-line-through">{it.item_name}</td>
<td>{it.hsn_code}</td>
<td>{it.batch_no}</td>
<td><code>{it.ref_invoice_no || 'N/A'}</code></td>
<td>
<span className="badge bg-secondary me-1">{it.return_condition}</span>
<small className="text-muted">{it.return_reason}</small>
</td>
<td>
<span className="fw-bold">
-{it.sell_unit === 'Tablet' ? `${it.total_units} Tab` : `${it.qty} ${it.unit_pack || 'Unit'}`}
</span>
</td>
<td>₹{it.unit_price}</td>
<td>-₹{Math.abs(Number(it.taxable_value)).toFixed(2)}</td>
<td>-₹{Math.abs(Number(it.cgst_amount)).toFixed(2)}</td>
<td>-₹{Math.abs(Number(it.sgst_amount)).toFixed(2)}</td>
<td className="text-end fw-bold">-₹{Math.abs(Number(it.total_amount)).toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* HSN Summary Table */}
{lastInvoice.hsn_summary && lastInvoice.hsn_summary.length > 0 && (
<div className="mb-3">
<label className="form-label small fw-bold text-muted mb-1">GST Tax Breakdown</label>
<table className="table table-sm table-bordered text-center" style={{ fontSize: '10px' }}>
<thead className="table-light">
<tr>
<th>HSN Code</th>
<th>GST Rate</th>
<th>Taxable Value (₹)</th>
<th>CGST (₹)</th>
<th>SGST (₹)</th>
<th>Total Tax (₹)</th>
</tr>
</thead>
<tbody>
{lastInvoice.hsn_summary.map((h, idx) => (
<tr key={idx}>
<td>{h.hsn_code}</td>
<td>{h.gst_rate}%</td>
<td>₹{h.taxable_value.toFixed(2)}</td>
<td>₹{h.cgst_amount.toFixed(2)}</td>
<td>₹{h.sgst_amount.toFixed(2)}</td>
<td className="fw-bold">₹{h.total_tax.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Summary Totals & Signatures */}
<div className="row g-3 border-top pt-3">
<div className="col-7">
<small className="text-muted d-block">
<strong>Registered Pharmacist:</strong> {lastInvoice.sale.registered_pharmacist_name || 'Pharmacist'} (Reg No: {lastInvoice.sale.pharmacist_reg_no || 'N/A'})
{lastInvoice.sale.pharmacist_hpr_id && <span> | <strong>HPR ID:</strong> {lastInvoice.sale.pharmacist_hpr_id}</span>}
</small>
<small className="text-muted d-block mt-1">
{lastInvoice.sale.terms_conditions || '1. Goods once sold are returnable only as per Drug Rules.\n2. Store medicines below 25°C.'}
</small>
<div className="mt-2 p-2 bg-light rounded border" style={{ fontSize: '11px' }}>
<div className="fw-bold text-dark mb-1"><i className="bi bi-wallet2 me-1"></i> Payment Settlement:</div>
<div className="d-flex flex-wrap gap-2 align-items-center mb-1">
<span className="badge bg-secondary">{lastInvoice.sale.payment_mode}</span>
{Number(lastInvoice.sale.cash_paid) > 0 && <span><strong>Cash:</strong> ₹{lastInvoice.sale.cash_paid}</span>}
{Number(lastInvoice.sale.upi_paid) > 0 && (
<span>
<strong>UPI / Bank:</strong> ₹{lastInvoice.sale.upi_paid}
{lastInvoice.sale.upi_ref_no && <code className="text-primary ms-1">[UTR: {lastInvoice.sale.upi_ref_no}]</code>}
{lastInvoice.sale.bank_name && <span className="text-muted ms-1">({lastInvoice.sale.bank_name})</span>}
</span>
)}
{Number(lastInvoice.sale.card_paid) > 0 && (
<span><strong>Card:</strong> ₹{lastInvoice.sale.card_paid} {lastInvoice.sale.card_ref_no ? `[Ref: ${lastInvoice.sale.card_ref_no}]` : ''}</span>
)}
{Number(lastInvoice.sale.credit_amount) > 0 && (
<span><strong>Hospital Credit:</strong> ₹{lastInvoice.sale.credit_amount}</span>
)}
</div>
{Number(lastInvoice.sale.refund_amount) > 0 && (
<div className="mb-1">
<span className="badge bg-danger-subtle text-danger border border-danger">
<i className="bi bi-cash-stack me-1"></i> Refund Issued: ₹{lastInvoice.sale.refund_amount} via {lastInvoice.sale.refund_mode} {lastInvoice.sale.refund_ref_no ? `[Ref: ${lastInvoice.sale.refund_ref_no}]` : ''}
</span>
</div>
)}
<div>
{lastInvoice.sale.is_bank_reconciled == 1 ? (
<span className="badge bg-success-subtle text-success border border-success"><i className="bi bi-patch-check-fill me-1"></i> Bank Statement Reconciled &amp; Audited</span>
) : (
<span className="badge bg-warning-subtle text-dark border border-warning"><i className="bi bi-clock-history me-1"></i> Bank Statement: Pending Audit</span>
)}
</div>
</div>
</div>
<div className="col-5">
<div className="d-flex justify-content-between mb-1">
<span>Gross Items Total:</span>
<span>₹{lastInvoice.sale.gross_amount}</span>
</div>
{Number(lastInvoice.sale.discount_amount) > 0 && (
<div className="d-flex justify-content-between mb-1 text-danger">
<span>Total Discounts Allowed:</span>
<span>-₹{lastInvoice.sale.discount_amount}</span>
</div>
)}
{Number(lastInvoice.sale.return_amount) > 0 && (
<div className="d-flex justify-content-between mb-1 text-danger fw-bold">
<span>Less Return / Exchange Credit:</span>
<span>-₹{lastInvoice.sale.return_amount}</span>
</div>
)}
<div className="d-flex justify-content-between mb-1">
<span>Taxable Amount:</span>
<span>₹{lastInvoice.sale.taxable_amount}</span>
</div>
<div className="d-flex justify-content-between mb-1">
<span>CGST Amount:</span>
<span>₹{lastInvoice.sale.cgst_amount}</span>
</div>
<div className="d-flex justify-content-between mb-1">
<span>SGST Amount:</span>
<span>₹{lastInvoice.sale.sgst_amount}</span>
</div>
<div className="d-flex justify-content-between mb-1">
<span>Round Off:</span>
<span>₹{lastInvoice.sale.round_off}</span>
</div>
<div className="d-flex justify-content-between fs-5 fw-bolder border-top pt-2 text-primary">
<span>GRAND TOTAL / NET PAYABLE:</span>
<span>₹{lastInvoice.sale.net_amount}</span>
</div>
{Number(lastInvoice.sale.refund_amount) > 0 && (
<div className="d-flex justify-content-between fs-6 fw-bold text-danger border-top border-bottom py-1 mt-2">
<span>REFUND PAID TO CUSTOMER:</span>
<span>₹{lastInvoice.sale.refund_amount} ({lastInvoice.sale.refund_mode}{lastInvoice.sale.refund_ref_no ? ` - ${lastInvoice.sale.refund_ref_no}` : ''})</span>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
</div>
<div className="p-2.5 bg-white border-top d-flex justify-content-between align-items-center flex-shrink-0 px-3">
<span className="text-muted small">
Format: <strong className="text-dark text-uppercase">{printFormat}</strong> • Bill No: <strong>{lastInvoice.sale.invoice_no}</strong>
</span>
<div className="d-flex gap-2 flex-wrap align-items-center">
<button className="btn btn-sm btn-outline-secondary px-3" onClick={() => setIsPrintOpen(false)}>
Done / Close (Esc)
</button>
{printFormat === 'thermal' ? (
<>
<button className="btn btn-sm btn-outline-dark px-3" onClick={copyThermalReceiptText}>
<i className="bi bi-clipboard me-1 text-primary"></i> Copy Text
</button>
<button className="btn btn-sm btn-warning text-dark px-3 fw-bold" onClick={printThermalReceipt}>
<i className="bi bi-printer-fill me-1"></i> Print Thermal Receipt
</button>
</>
) : (
<>
<button className="btn btn-sm btn-outline-primary px-3" onClick={() => downloadPdf(printFormat)}>
<i className="bi bi-download me-1"></i> Download PDF
</button>
<button className="btn btn-sm btn-primary px-3 fw-bold" onClick={() => printPdf(printFormat)}>
<i className="bi bi-printer me-1"></i> Print PDF ({printFormat.toUpperCase()})
</button>
</>
)}
</div>
</div>
</>
)}
</div>
{/* ABDM FHIR R4 Bundle Modal */}
{showFhirModal && (
<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-scrollable">
<div className="modal-content border-0 shadow-lg">
<div className="modal-header bg-dark text-white py-2 px-3">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-shield-fill-check text-success fs-5"></i>
<div>
<h6 className="modal-title mb-0 fw-bold">ABDM NRCES FHIR R4 MedicationDispense Document</h6>
<small className="text-secondary" style={{ fontSize: '0.75rem' }}>
Profile: MedicationDispenseDocument | LOINC: 60590-7
</small>
</div>
</div>
<button type="button" className="btn-close btn-close-white" onClick={() => setShowFhirModal(false)}></button>
</div>
<div className="modal-body p-3 bg-light">
{isLoadingFhir ? (
<div className="text-center py-5">
<div className="spinner-border text-primary mb-2" role="status"></div>
<div className="text-muted">Retrieving &amp; Validating ABDM FHIR R4 Bundle...</div>
</div>
) : fhirBundleData ? (
<div>
<div className="row g-2 mb-3">
<div className="col-md-4">
<div className="bg-white p-2 rounded border">
<small className="text-muted d-block">Care Context Reference</small>
<code className="text-primary fw-bold">{fhirBundleData.care_context_ref}</code>
</div>
</div>
<div className="col-md-4">
<div className="bg-white p-2 rounded border">
<small className="text-muted d-block">ABHA Number / Address</small>
<strong className="text-success">{fhirBundleData.abha_id || fhirBundleData.abha_address || 'Not ABHA Linked'}</strong>
</div>
</div>
<div className="col-md-4">
<div className="bg-white p-2 rounded border">
<small className="text-muted d-block">Gateway Sync Status</small>
<span className={`badge ${fhirBundleData.sync_status === 'SYNCED' ? 'bg-success' : 'bg-warning text-dark'}`}>
{fhirBundleData.sync_status || 'QUEUED_FOR_BRIDGE'}
</span>
</div>
</div>
</div>
<div className="d-flex justify-content-between align-items-center mb-1">
<span className="small fw-bold text-dark">NRCES FHIR R4 Document Bundle (JSON):</span>
<button className="btn btn-sm btn-outline-secondary py-0 px-2" onClick={copyFhirToClipboard}>
<i className={`bi ${copiedFhir ? 'bi-check-lg text-success' : 'bi-clipboard'} me-1`}></i>
{copiedFhir ? 'Copied!' : 'Copy JSON'}
</button>
</div>
<pre className="bg-dark text-light p-3 rounded-2" style={{ maxHeight: 360, fontSize: '0.75rem', overflow: 'auto' }}>
{JSON.stringify(fhirBundleData.bundle, null, 2)}
</pre>
</div>
) : (
<div className="alert alert-warning mb-0">No FHIR bundle data found.</div>
)}
</div>
<div className="modal-footer py-1 px-3 bg-white">
<small className="text-muted me-auto" style={{ fontSize: '0.75rem' }}>
Includes SNOMED-CT clinical codes, HFR Facility ID, and Practitioner HPR ID.
</small>
<button type="button" className="btn btn-sm btn-secondary" onClick={() => setShowFhirModal(false)}>Close</button>
</div>
</div>
</div>
</div>
)}
{/* Sales Return & Same-Invoice Exchange Modal (B2C Retail Standard) */}
{showReturnModal && (
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(15, 23, 42, 0.75)' }} tabIndex="-1">
<div className="modal-dialog modal-xl modal-dialog-scrollable">
<div className="modal-content border-0 shadow-lg">
<div className="modal-header bg-dark text-white py-2 px-3">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-arrow-left-right text-warning fs-5"></i>
<div>
<h6 className="modal-title mb-0 fw-bold">Sales Return &amp; Same-Invoice Exchange Desk</h6>
<small className="text-secondary" style={{ fontSize: '0.75rem' }}>
B2C Retail Returns • Stock Replenishment • GST Credit Note • Fast POS Standard (F6 / Alt+R)
</small>
</div>
</div>
<button type="button" className="btn-close btn-close-white" onClick={() => setShowReturnModal(false)}></button>
</div>
<div className="modal-body p-3 bg-light">
{/* Search Bar for Past Invoices */}
<div className="card shadow-sm border-0 mb-3">
<div className="card-body p-3">
<label className="form-label small fw-bold text-muted mb-1">
Lookup Original Tax Invoice / Patient UHID / Phone:
</label>
<div className="input-group">
<span className="input-group-text bg-white"><i className="bi bi-search"></i></span>
<input
type="text"
ref={returnSearchInputRef}
className="form-control"
placeholder="Enter Invoice No (e.g. MAIN/26-27/08176), UHID, Patient Name, or Mobile..."
value={returnSearchQuery}
onChange={(e) => setReturnSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
searchReturnInvoices();
}
}}
/>
<button
className="btn btn-primary px-3"
type="button"
disabled={isSearchingReturn}
onClick={() => searchReturnInvoices()}
>
{isSearchingReturn ? (
<>
<span className="spinner-border spinner-border-sm me-1"></span>
Searching...
</>
) : (
<>
<i className="bi bi-arrow-repeat me-1"></i> Search Bill
</>
)}
</button>
</div>
</div>
</div>
{/* Search Results List (if multiple bills match) */}
{!selectedReturnInvoice && returnSearchResults.length > 1 && (
<div className="card shadow-sm border-0 mb-3">
<div className="card-header bg-white py-2 fw-bold text-dark small">
Matching Invoices ({returnSearchResults.length} found) - Select an invoice to process returns:
</div>
<div className="table-responsive" style={{ maxHeight: '220px' }}>
<table className="table table-hover table-sm align-middle mb-0" style={{ fontSize: '0.85rem' }}>
<thead className="table-light">
<tr>
<th>Bill No</th>
<th>Date</th>
<th>Patient</th>
<th>Doctor</th>
<th>Original Net</th>
<th>Returnable Items</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{returnSearchResults.map((resObj) => {
const returnableCount = (resObj.items || []).filter(it => it.remaining_units > 0).length;
return (
<tr key={resObj.sale.sale_id}>
<td className="fw-bold text-primary">{resObj.sale.invoice_no}</td>
<td className="text-muted">{resObj.sale.sale_date}</td>
<td>
<strong>{resObj.sale.patient_name}</strong>
{resObj.sale.uhid && <span className="text-muted ms-1">({resObj.sale.uhid})</span>}
</td>
<td>{resObj.sale.doctor_name || 'N/A'}</td>
<td className="fw-bold">₹{resObj.sale.net_amount}</td>
<td>
<span className={`badge ${returnableCount > 0 ? 'bg-success-subtle text-success' : 'bg-secondary'}`}>
{returnableCount} items eligible
</span>
</td>
<td>
<button
type="button"
className="btn btn-sm btn-outline-primary py-0 px-2"
onClick={() => selectReturnInvoice(resObj)}
>
Select Bill <i className="bi bi-chevron-right ms-1"></i>
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
{/* Selected Invoice Header Details */}
{selectedReturnInvoice && (
<div className="card shadow-sm border-0 mb-3">
<div className="card-header bg-white py-2 d-flex justify-content-between align-items-center">
<div className="d-flex align-items-center gap-2">
<span className="badge bg-primary fs-7">{selectedReturnInvoice.sale.invoice_no}</span>
<span className="text-muted small">Date: {selectedReturnInvoice.sale.sale_date}</span>
{selectedReturnInvoice.sale.uhid && (
<span className="badge bg-light text-dark border">UHID: {selectedReturnInvoice.sale.uhid}</span>
)}
</div>
{returnSearchResults.length > 1 && (
<button
type="button"
className="btn btn-sm btn-outline-secondary py-0"
onClick={() => setSelectedReturnInvoice(null)}
>
<i className="bi bi-arrow-left me-1"></i> Change Bill
</button>
)}
</div>
<div className="card-body p-2 bg-light-subtle">
<div className="row g-2 small">
<div className="col-md-3"><strong>Patient:</strong> {selectedReturnInvoice.sale.patient_name}</div>
<div className="col-md-3"><strong>Phone:</strong> {selectedReturnInvoice.sale.patient_mobile || 'N/A'}</div>
<div className="col-md-3"><strong>Doctor:</strong> {selectedReturnInvoice.sale.doctor_name || 'N/A'}</div>
<div className="col-md-3"><strong>Original Net Paid:</strong> ₹{selectedReturnInvoice.sale.net_amount}</div>
</div>
</div>
{/* Returnable Items Grid */}
<div className="table-responsive">
<table className="table table-bordered table-sm align-middle mb-0" style={{ fontSize: '0.85rem' }}>
<thead className="table-light">
<tr>
<th>#</th>
<th>Medicine Name</th>
<th>Batch / Exp</th>
<th>Sold / Returned</th>
<th>Eligible Units</th>
<th style={{ width: 150 }}>Return Qty &amp; Unit</th>
<th style={{ width: 140 }}>Condition</th>
<th style={{ width: 150 }}>Reason</th>
<th>Credit Rate</th>
<th className="text-end" style={{ width: 110 }}>Line Credit</th>
</tr>
</thead>
<tbody>
{selectedReturnInvoice.items.map((it, idx) => {
const draft = returnDraftItems[it.sale_item_id] || {
return_qty: 0,
return_unit: (it.units_per_pack > 1 && it.remaining_strips > 0) ? 'Strip' : 'Tab',
condition: 'GOOD',
reason: 'EXCESS_UNUSED'
};
const isEligible = it.remaining_units > 0;
const qty = Number(draft.return_qty) || 0;
const upp = Number(it.units_per_pack) || 1;
const totalReturnUnits = draft.return_unit === 'Strip' ? (qty * upp) : qty;
const lineCredit = Number((totalReturnUnits * it.effective_unit_price).toFixed(2));
const isOverLimit = totalReturnUnits > it.remaining_units;
return (
<tr key={it.sale_item_id} className={!isEligible ? 'opacity-50 table-light' : (qty > 0 ? 'table-warning' : '')}>
<td>{idx + 1}</td>
<td>
<strong className="text-dark">{it.item_name}</strong>
{it.generic_name && <div className="text-muted" style={{ fontSize: '0.75rem' }}>{it.generic_name}</div>}
{upp > 1 && <div className="text-muted" style={{ fontSize: '0.72rem' }}>1 Strip = {upp} Tabs</div>}
</td>
<td>
<span className="badge bg-secondary-subtle text-dark border">{it.batch_no}</span>
<div className="text-muted" style={{ fontSize: '0.72rem' }}>Exp: {it.expiry_date ? it.expiry_date.substring(0, 7) : 'N/A'}</div>
</td>
<td>
<div>Sold: {it.total_units} tab</div>
{Number(it.already_returned_units) > 0 && (
<div className="text-danger" style={{ fontSize: '0.75rem' }}>
Returned: {it.already_returned_units} tab
</div>
)}
</td>
<td>
{isEligible ? (
<span className="badge bg-success-subtle text-success border border-success">
{it.remaining_units} Tabs {upp > 1 ? `(${it.remaining_strips} Str + ${it.remaining_loose} Tab)` : ''}
</span>
) : (
<span className="badge bg-secondary">Fully Returned</span>
)}
</td>
<td>
{isEligible ? (
<div className="input-group input-group-sm">
<input
type="number"
min="0"
max={draft.return_unit === 'Strip' ? it.remaining_strips : it.remaining_units}
className={`form-control ${isOverLimit ? 'is-invalid' : ''}`}
value={draft.return_qty}
onChange={(e) => updateReturnDraft(it.sale_item_id, 'return_qty', e.target.value)}
/>
{upp > 1 && (
<select
className="form-select"
style={{ maxWidth: 80 }}
value={draft.return_unit}
onChange={(e) => updateReturnDraft(it.sale_item_id, 'return_unit', e.target.value)}
>
<option value="Tab">Tab</option>
<option value="Strip">Strip</option>
</select>
)}
</div>
) : (
<span className="text-muted small">-</span>
)}
{isOverLimit && (
<small className="text-danger d-block">Max {it.remaining_units} tabs</small>
)}
</td>
<td>
{isEligible && qty > 0 ? (
<select
className="form-select form-select-sm"
value={draft.condition}
onChange={(e) => updateReturnDraft(it.sale_item_id, 'condition', e.target.value)}
>
<option value="GOOD">Good (Restock)</option>
<option value="DAMAGED">Damaged / Broken</option>
<option value="EXPIRED">Expired / Defective</option>
</select>
) : (
<span className="text-muted small">-</span>
)}
</td>
<td>
{isEligible && qty > 0 ? (
<select
className="form-select form-select-sm"
value={draft.reason}
onChange={(e) => updateReturnDraft(it.sale_item_id, 'reason', e.target.value)}
>
<option value="EXCESS_UNUSED">Excess / Unused</option>
<option value="DOCTOR_CHANGED_RX">Doctor Changed Rx</option>
<option value="PATIENT_DISCHARGED">Patient Discharged</option>
<option value="WRONG_MEDICINE">Wrong Dispense</option>
</select>
) : (
<span className="text-muted small">-</span>
)}
</td>
<td>
₹{it.effective_unit_price} <small className="text-muted">/tab</small>
{upp > 1 && <div className="text-muted" style={{ fontSize: '0.72rem' }}>₹{(it.effective_unit_price * upp).toFixed(2)}/str</div>}
</td>
<td className="text-end fw-bold">
{qty > 0 ? (
<span className="text-danger">-₹{lineCredit.toFixed(2)}</span>
) : (
<span className="text-muted">₹0.00</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
{!selectedReturnInvoice && returnSearchResults.length === 0 && (
<div className="text-center py-5 text-muted bg-white rounded border">
<i className="bi bi-receipt-cutoff fs-1 text-secondary mb-2 d-block"></i>
<div className="fw-bold">No Invoice Selected</div>
<div className="small">Search by Bill Number, Patient UHID, Name, or Mobile to load returnable items.</div>
</div>
)}
</div>
<div className="modal-footer py-2 px-3 bg-white d-flex justify-content-between align-items-center">
<div>
{selectedReturnInvoice && (
<div className="d-flex align-items-center gap-3">
{(() => {
let totalCredit = 0;
let totalItemsCount = 0;
selectedReturnInvoice.items.forEach(it => {
const draft = returnDraftItems[it.sale_item_id];
if (draft && Number(draft.return_qty) > 0) {
const qty = Number(draft.return_qty);
const upp = Number(it.units_per_pack) || 1;
const tu = draft.return_unit === 'Strip' ? (qty * upp) : qty;
totalCredit += (tu * it.effective_unit_price);
totalItemsCount++;
}
});
return (
<div>
<span className="text-muted small me-2">Selected Items: <strong>{totalItemsCount}</strong></span>
<span className="text-danger fw-bold fs-6">
Total Return Credit: -₹{totalCredit.toFixed(2)}
</span>
</div>
);
})()}
</div>
)}
</div>
<div className="d-flex gap-2">
<button type="button" className="btn btn-secondary btn-sm" onClick={() => setShowReturnModal(false)}>
Cancel
</button>
<button
type="button"
className="btn btn-danger btn-sm px-3 fw-bold"
disabled={!selectedReturnInvoice}
onClick={addSelectedReturnsToBill}
>
<i className="bi bi-cart-plus me-1"></i> Add to Current Bill (Exchange / Net-Off)
</button>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
}
// =========================================================================
// 2. LIVE INVENTORY & BATCH MATRIX VIEW
// =========================================================================
function InventoryView({ store, notify }) {
if (!store) return null;
const [stock, setStock] = useState([]);
const [summary, setSummary] = useState({});
const [filter, setFilter] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => {
if (store?.store_id) {
loadStock();
}
}, [store?.store_id, filter]);
const loadStock = async () => {
setLoading(true);
try {
const res = await fetch(API_BASE + `stock/list?store_id=${store.store_id}&filter=${filter}`);
const data = await res.json();
if (data.status) {
setStock(data.stock || []);
setSummary(data.summary || {});
}
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
const filteredStock = stock.filter(s =>
s.item_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(s.generic_name && s.generic_name.toLowerCase().includes(searchTerm.toLowerCase())) ||
s.batch_no.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div>
{/* KPI Summary Cards */}
<div className="row g-3 mb-3">
<div className="col-md-3 col-sm-6">
<div className="counter-card p-3 border-start border-4 border-primary">
<small className="text-muted fw-bold text-uppercase">Total Batches in Store</small>
<h4 className="fw-bolder text-dark mt-1 mb-0">{summary.total_rows || 0}</h4>
</div>
</div>
<div className="col-md-3 col-sm-6">
<div className="counter-card p-3 border-start border-4 border-success">
<small className="text-muted fw-bold text-uppercase">Stock Cost Valuation (PTR)</small>
<h4 className="fw-bolder text-success mt-1 mb-0">₹{(summary.total_cost_valuation || 0).toLocaleString('en-IN')}</h4>
</div>
</div>
<div className="col-md-3 col-sm-6">
<div className="counter-card p-3 border-start border-4 border-warning">
<small className="text-muted fw-bold text-uppercase">Stock Retail Valuation (MRP)</small>
<h4 className="fw-bolder text-dark mt-1 mb-0">₹{(summary.total_mrp_valuation || 0).toLocaleString('en-IN')}</h4>
</div>
</div>
<div className="col-md-3 col-sm-6">
<div className="counter-card p-3 border-start border-4 border-info">
<small className="text-muted fw-bold text-uppercase">Counter / URL</small>
<h6 className="fw-bolder text-primary mt-1 mb-0 text-truncate">MedicalStore/{store.store_slug}</h6>
</div>
</div>
</div>
{/* Filter Tabs & Search Bar */}
<div className="counter-card p-3 mb-3">
<div className="d-flex flex-wrap justify-content-between align-items-center gap-2">
<div className="btn-group btn-group-sm">
<button className={`btn ${filter === 'all' ? 'btn-primary' : 'btn-outline-secondary'}`} onClick={() => setFilter('all')}>
All Stock
</button>
<button className={`btn ${filter === 'low' ? 'btn-primary' : 'btn-outline-secondary'}`} onClick={() => setFilter('low')}>
<i className="bi bi-arrow-down-circle me-1"></i> Low Stock
</button>
<button className={`btn ${filter === 'near_expiry' ? 'btn-warning' : 'btn-outline-secondary'}`} onClick={() => setFilter('near_expiry')}>
<i className="bi bi-clock-history me-1"></i> Near Expiry (&lt;90 Days)
</button>
<button className={`btn ${filter === 'expired' ? 'btn-danger' : 'btn-outline-secondary'}`} onClick={() => setFilter('expired')}>
<i className="bi bi-shield-x me-1"></i> Expired Stock
</button>
</div>
<div style={{ maxWidth: 300 }} className="w-100">
<input
type="text"
className="form-control form-control-sm"
placeholder="Search item, molecule, batch..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
</div>
{/* Stock Table */}
<div className="counter-card p-0 overflow-hidden">
<div className="table-responsive">
<table className="table table-pos table-hover mb-0">
<thead>
<tr>
<th>#</th>
<th>Item &amp; Generic Molecule</th>
<th>Category</th>
<th>Batch No</th>
<th>Expiry</th>
<th>Current Qty</th>
<th>PTR (₹)</th>
<th>MRP (₹)</th>
<th>Cost Value (₹)</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan="10" className="text-center py-4 text-muted">
<i className="bi bi-hourglass-split fs-4 d-block mb-1"></i> Loading inventory...
</td>
</tr>
) : filteredStock.length === 0 ? (
<tr>
<td colSpan="10" className="text-center py-4 text-muted">No stock records found.</td>
</tr>
) : (
filteredStock.map((s, idx) => (
<tr key={s.stock_id}>
<td>{idx + 1}</td>
<td>
<div className="fw-bold">{s.item_name}</div>
<small className="text-muted">{s.generic_name} • HSN: {s.hsn_code}</small>
</td>
<td><span className="badge bg-secondary-subtle text-secondary">{s.category}</span></td>
<td><span className="badge bg-light text-dark border fw-bold">{s.batch_no}</span></td>
<td>{s.expiry_display}</td>
<td>
<span className={`fw-bold fs-6 ${s.current_qty <= s.min_reorder_level ? 'text-danger' : 'text-success'}`}>
{s.current_qty}
</span>
</td>
<td>₹{s.ptr}</td>
<td className="fw-semibold">₹{s.mrp}</td>
<td className="fw-bold">₹{s.cost_valuation}</td>
<td>
{s.is_expired ? (
<span className="badge-expired">EXPIRED</span>
) : s.is_near_expiry ? (
<span className="badge-near-expiry">Near Expiry</span>
) : (
<span className="badge-fefo">Good Stock</span>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
}
// =========================================================================
// 3. INWARD PURCHASES, SUPPLIERS & EXCEL / CSV IMPORT
// =========================================================================
function PurchaseView({ store, notify }) {
if (!store) return null;
const [subTab, setSubTab] = useState('inward'); // inward, excel, suppliers
const [suppliers, setSuppliers] = useState([]);
const [selectedSupplierId, setSelectedSupplierId] = useState('');
const [invoiceNo, setInvoiceNo] = useState('');
const [invoiceDate, setInvoiceDate] = useState(new Date().toISOString().split('T')[0]);
// Supplier Add/Edit Modal State
const [showSupplierModal, setShowSupplierModal] = useState(false);
const [supplierSearch, setSupplierSearch] = useState('');
const [isSavingSupplier, setIsSavingSupplier] = useState(false);
const [supplierForm, setSupplierForm] = useState({
supplier_id: 0,
supplier_name: '',
contact_person: '',
phone: '',
email: '',
gstin: '',
pan_no: '',
dl_no_20b: '',
dl_no_21b: '',
state_code: '07',
credit_days: 30,
address: '',
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, 18, 40, 12, 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: 5,
drug_schedule: 'Schedule H',
company_name: '',
min_reorder_level: 10
});
const searchTimerRef = useRef(null);
const [rows, setRows] = useState([]);
// --- Delivery Challans & Product History States ---
const [isChallan, setIsChallan] = useState(false);
const [supplierChallans, setSupplierChallans] = useState([]);
const [isLoadingChallans, setIsLoadingChallans] = useState(false);
const [convertedChallanId, setConvertedChallanId] = useState(null);
const [productHistory, setProductHistory] = useState([]);
const [isLoadingProductHistory, setIsLoadingProductHistory] = useState(false);
const [historyTargetItemName, setHistoryTargetItemName] = useState('');
// --- Active Item Form State (Pharma Purchase Input - Image 2) ---
const initialItemForm = {
item_id: 0,
product_master_id: 0,
item_name: '',
generic_name: '',
category: 'Tablet',
units_per_pack: 10,
batch_no: '',
exp_month: String(new Date().getMonth() + 1).padStart(2, '0'),
exp_year: String(new Date().getFullYear() + 2).slice(-2),
mrp: '',
qty_packs: 1,
free_qty_packs: 0,
ptr: '',
amount: 0,
discount_pct: 0,
sch_disc_amount: 0,
sch_disc_pct: 0,
taxable_amount: 0,
cgst_pct: 6.0,
sgst_pct: 6.0,
net_amount: 0,
selling_price: '',
hsn_code: '3004',
storage_type: 'Normal',
shelf_no: '',
rack_no: '',
challan_item_ref_id: 0
};
const [itemForm, setItemForm] = useState(initialItemForm);
const [editingItemIndex, setEditingItemIndex] = useState(null);
const recomputeItemForm = (updated) => {
let ptr = parseFloat(updated.ptr) || 0;
const qty = parseFloat(updated.qty_packs) || 0;
const disc = parseFloat(updated.discount_pct) || 0;
const schAmt = parseFloat(updated.sch_disc_amount) || 0;
const schDisc = parseFloat(updated.sch_disc_pct) || 0;
if (schDisc > 0) {
ptr = ptr - (ptr * schDisc / 100);
}
let amount = ptr * qty;
if (schAmt > 0) {
amount = Math.max(0, amount - schAmt);
}
const taxable = amount - (amount * disc / 100);
const cgst = parseFloat(updated.cgst_pct) || 0;
const sgst = parseFloat(updated.sgst_pct) || 0;
const cgstAmt = taxable * (cgst / 100);
const sgstAmt = taxable * (sgst / 100);
const net = taxable + cgstAmt + sgstAmt;
let sellingPrice = updated.selling_price;
if (!sellingPrice && updated.mrp) {
sellingPrice = updated.mrp;
}
return {
...updated,
amount: Number(amount.toFixed(2)),
taxable_amount: Number(taxable.toFixed(2)),
net_amount: Number(net.toFixed(2)),
selling_price: sellingPrice
};
};
const handleItemFormChange = (field, val) => {
setItemForm(prev => {
const next = { ...prev, [field]: val };
if (field === 'mrp' && (!prev.selling_price || prev.selling_price == prev.mrp)) {
next.selling_price = val;
}
return recomputeItemForm(next);
});
};
const resetItemForm = () => {
setItemForm(initialItemForm);
setEditingItemIndex(null);
};
const billTotals = useMemo(() => {
let totalTaxable = 0;
let totalCgst = 0;
let totalSgst = 0;
let totalNet = 0;
rows.forEach(r => {
const taxable = Number(r.taxable_amount) || 0;
const cgst = Number(r.cgst_amount) || (taxable * ((Number(r.cgst_pct) || ((Number(r.gst_rate) || 12) / 2)) / 100));
const sgst = Number(r.sgst_amount) || (taxable * ((Number(r.sgst_pct) || ((Number(r.gst_rate) || 12) / 2)) / 100));
const net = Number(r.net_amount) || (taxable + cgst + sgst);
totalTaxable += taxable;
totalCgst += cgst;
totalSgst += sgst;
totalNet += net;
});
return {
taxable: totalTaxable.toFixed(2),
cgst: totalCgst.toFixed(2),
sgst: totalSgst.toFixed(2),
net: totalNet.toFixed(2)
};
}, [rows]);
const fetchSupplierChallans = async (supplierId) => {
if (!supplierId) {
setSupplierChallans([]);
return;
}
setIsLoadingChallans(true);
try {
const res = await fetch(API_BASE + 'supplier-challans/' + supplierId);
const data = await res.json();
if (data.status && data.challans) {
setSupplierChallans(data.challans);
} else {
setSupplierChallans([]);
}
} catch (e) {
console.error('fetchSupplierChallans error:', e);
setSupplierChallans([]);
} finally {
setIsLoadingChallans(false);
}
};
const fetchProductPurchaseHistory = async (itemId, itemName = '') => {
if (!itemId) {
setProductHistory([]);
setHistoryTargetItemName('');
return;
}
setIsLoadingProductHistory(true);
setHistoryTargetItemName(itemName || 'Selected Product');
try {
const res = await fetch(API_BASE + 'product-purchase-history/' + itemId);
const data = await res.json();
if (data.status && data.history) {
setProductHistory(data.history);
} else {
setProductHistory([]);
}
if (data.status && data.latest_info) {
const info = data.latest_info;
setItemForm(prev => recomputeItemForm({
...prev,
batch_no: info.batch_no || prev.batch_no,
exp_month: info.exp_month || prev.exp_month,
exp_year: info.exp_year || prev.exp_year,
expiry_date: info.expiry_date || prev.expiry_date,
mrp: (info.mrp !== undefined && info.mrp !== '') ? Number(info.mrp) : prev.mrp,
ptr: (info.ptr !== undefined && info.ptr !== '') ? Number(info.ptr) : prev.ptr,
discount_pct: info.discount_pct !== undefined ? Number(info.discount_pct) : prev.discount_pct,
gst_rate: info.gst_rate !== undefined ? Number(info.gst_rate) : prev.gst_rate,
cgst_pct: info.cgst_pct !== undefined ? Number(info.cgst_pct) : prev.cgst_pct,
sgst_pct: info.sgst_pct !== undefined ? Number(info.sgst_pct) : prev.sgst_pct,
hsn_code: info.hsn_code || prev.hsn_code || '3004',
shelf_no: info.shelf_no || prev.shelf_no,
rack_no: info.rack_no || prev.rack_no,
storage_type: info.storage_type || prev.storage_type || 'Normal',
units_per_pack: info.units_per_pack ? Number(info.units_per_pack) : (prev.units_per_pack || 10),
selling_price: info.selling_price ? Number(info.selling_price) : (info.mrp ? Number(info.mrp) : prev.selling_price)
}));
}
} catch (e) {
console.error('fetchProductPurchaseHistory error:', e);
setProductHistory([]);
} finally {
setIsLoadingProductHistory(false);
}
};
const handleAddOrUpdateItem = () => {
if (!itemForm.item_name.trim()) {
notify('Please select or enter a medicine name.', 'error');
return;
}
if (!itemForm.batch_no.trim()) {
notify('Please enter a Batch Number.', 'error');
return;
}
if (Number(itemForm.qty_packs) <= 0) {
notify('Quantity in packs must be greater than 0.', 'error');
return;
}
const yy = String(itemForm.exp_year).length === 2 ? ('20' + itemForm.exp_year) : String(itemForm.exp_year);
const mm = String(itemForm.exp_month).padStart(2, '0');
const expiryDate = `${yy}-${mm}-01`;
const rowItem = recomputeItemForm({
...itemForm,
expiry_date: expiryDate,
exp_month: mm,
exp_year: itemForm.exp_year,
gst_rate: (Number(itemForm.cgst_pct) || 0) + (Number(itemForm.sgst_pct) || 0)
});
if (editingItemIndex !== null && editingItemIndex >= 0 && editingItemIndex < rows.length) {
const nextRows = [...rows];
nextRows[editingItemIndex] = rowItem;
setRows(nextRows);
setEditingItemIndex(null);
notify(`Updated "${rowItem.item_name}" in invoice items list.`, 'success');
} else {
setRows(prev => [...prev, rowItem]);
notify(`Added "${rowItem.item_name}" to invoice items list.`, 'success');
}
resetItemForm();
};
const handleEditRow = (index) => {
const r = rows[index];
if (!r) return;
setEditingItemIndex(index);
let expMonth = r.exp_month;
let expYear = r.exp_year;
if (!expMonth && r.expiry_date) {
const parts = r.expiry_date.split('-');
if (parts.length >= 2) {
expYear = parts[0].slice(-2);
expMonth = parts[1];
}
}
setItemForm(recomputeItemForm({
...initialItemForm,
...r,
exp_month: expMonth || '12',
exp_year: expYear || '28',
cgst_pct: r.cgst_pct !== undefined ? r.cgst_pct : ((r.gst_rate || 12) / 2),
sgst_pct: r.sgst_pct !== undefined ? r.sgst_pct : ((r.gst_rate || 12) / 2)
}));
if (r.item_id > 0) {
fetchProductPurchaseHistory(r.item_id, r.item_name);
}
};
const includeChallanItem = (challan, cItem) => {
let expMonth = '12';
let expYear = '28';
if (cItem.expiry_date) {
const parts = cItem.expiry_date.split('-');
if (parts.length >= 2) {
expYear = parts[0].slice(-2);
expMonth = parts[1];
}
}
const cgst = (cItem.gst_rate || 12) / 2;
const sgst = (cItem.gst_rate || 12) / 2;
const rowItem = recomputeItemForm({
item_id: cItem.item_id || 0,
product_master_id: 0,
item_name: cItem.item_name,
generic_name: cItem.generic_name || '',
category: cItem.category || 'Tablet',
units_per_pack: Number(cItem.units_per_pack) || 10,
batch_no: cItem.batch_no || 'CHL-BATCH',
exp_month: expMonth,
exp_year: expYear,
expiry_date: cItem.expiry_date || `${'20' + expYear}-${expMonth}-01`,
mrp: Number(cItem.mrp) || 0,
qty_packs: Number(cItem.qty_packs) || 1,
free_qty_packs: Number(cItem.free_qty_packs) || 0,
ptr: Number(cItem.ptr) || 0,
discount_pct: Number(cItem.discount_pct) || 0,
sch_disc_amount: Number(cItem.sch_disc_amount) || 0,
sch_disc_pct: Number(cItem.sch_disc_pct) || 0,
cgst_pct: cgst,
sgst_pct: sgst,
gst_rate: cItem.gst_rate || 12,
selling_price: Number(cItem.selling_price) || Number(cItem.mrp) || 0,
hsn_code: cItem.hsn_code || '3004',
storage_type: cItem.storage_type || 'Normal',
shelf_no: cItem.shelf_no || '',
rack_no: cItem.rack_no || '',
challan_item_ref_id: cItem.challan_ref_id || challan.id
});
setConvertedChallanId(challan.id);
setRows(prev => [...prev, rowItem]);
notify(`Challan item "${cItem.item_name}" included into invoice!`, 'success');
};
const includeAllChallanItems = (challan) => {
if (!challan.items || challan.items.length === 0) return;
challan.items.forEach(cItem => includeChallanItem(challan, cItem));
notify(`All ${challan.items.length} items from Challan #${challan.challan_no} included into invoice!`, 'success');
};
// --- Supplier Ledger & Payments States ---
const [ledgerSummary, setLedgerSummary] = useState([]);
const [ledgerOverall, setLedgerOverall] = useState({ total_purchases: 0, total_paid: 0, outstanding_balance: 0, total_pending_bills: 0, total_suppliers: 0 });
const [activeLedgerSupplierId, setActiveLedgerSupplierId] = useState('');
const [activeLedgerDetail, setActiveLedgerDetail] = useState(null);
const [isLoadingLedger, setIsLoadingLedger] = useState(false);
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [isRecordingPayment, setIsRecordingPayment] = useState(false);
const [paymentForm, setPaymentForm] = useState({
supplier_id: '',
purchase_id: '',
amount: '',
payment_mode: 'Bank',
reference_no: '',
bank_name: '',
payment_date: new Date().toISOString().split('T')[0],
remarks: ''
});
// --- Short Items / Deficiency Reorder States ---
const [shortItems, setShortItems] = useState([]);
const [selectedShortItemIds, setSelectedShortItemIds] = useState([]);
const [shortFilter, setShortFilter] = useState('all'); // all, out_of_stock, low_stock
const [shortSearch, setShortSearch] = useState('');
const [isLoadingShortItems, setIsLoadingShortItems] = useState(false);
const [showQuickReorderModal, setShowQuickReorderModal] = useState(false);
const [quickReorderForm, setQuickReorderForm] = useState({ item_id: '', item_name: '', min_reorder_level: 10 });
const [isSavingShortItem, setIsSavingShortItem] = useState(false);
// --- Purchase Orders States ---
const [purchaseOrders, setPurchaseOrders] = useState([]);
const [poStatusFilter, setPoStatusFilter] = useState('all');
const [isLoadingPOs, setIsLoadingPOs] = useState(false);
const [showPoCreateModal, setShowPoCreateModal] = useState(false);
const [poCreateForm, setPoCreateForm] = useState({
supplier_id: '',
expected_delivery_date: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
remarks: '',
items: []
});
const [isSavingPO, setIsSavingPO] = useState(false);
const [showPoViewModal, setShowPoViewModal] = useState(false);
const [activeViewPo, setActiveViewPo] = useState(null);
const [loadedPoId, setLoadedPoId] = useState(null);
const [loadedPoNumber, setLoadedPoNumber] = useState('');
// --- Purchase Invoice History & Edit States ---
const [editingPurchaseId, setEditingPurchaseId] = useState(null);
const [recentPurchases, setRecentPurchases] = useState([]);
const [isLoadingPurchases, setIsLoadingPurchases] = useState(false);
const [showPurchaseHistoryModal, setShowPurchaseHistoryModal] = useState(false);
const [purchaseHistorySearch, setPurchaseHistorySearch] = useState('');
// --- Data Fetching & Action Handlers ---
const fetchLedgerSummary = async () => {
setIsLoadingLedger(true);
try {
const res = await fetch(API_BASE + 'suppliers/ledger-summary');
const data = await res.json();
if (data.status) {
setLedgerSummary(data.suppliers || []);
if (data.summary) setLedgerOverall(data.summary);
}
} catch (e) {
console.error('fetchLedgerSummary error:', e);
} finally {
setIsLoadingLedger(false);
}
};
const fetchRecentPurchases = async () => {
setIsLoadingPurchases(true);
try {
const res = await fetch(API_BASE + 'purchases?store_id=' + store.store_id);
const data = await res.json();
if (data.status && data.purchases) {
setRecentPurchases(data.purchases);
}
} catch (e) {
console.error('fetchRecentPurchases error:', e);
} finally {
setIsLoadingPurchases(false);
}
};
const loadPurchaseForEdit = async (purchaseId) => {
if (!purchaseId) return;
try {
const res = await fetch(API_BASE + 'purchase/' + purchaseId);
const data = await res.json();
if (data.status && data.purchase) {
setEditingPurchaseId(Number(data.purchase.purchase_id));
setSelectedSupplierId(String(data.purchase.supplier_id));
setIsChallan(Number(data.purchase.is_challan || 0) === 1);
setInvoiceNo(data.purchase.supplier_invoice_no || '');
setInvoiceDate(data.purchase.invoice_date || new Date().toISOString().split('T')[0]);
setLoadedPoId(null);
setLoadedPoNumber('');
if (data.items && data.items.length > 0) {
setRows(data.items.map(it => {
let expMonth = '12';
let expYear = '28';
if (it.expiry_date) {
const parts = it.expiry_date.split('-');
if (parts.length >= 2) {
expYear = parts[0].slice(-2);
expMonth = parts[1];
}
}
const gst = Number(it.gst_rate) || 12;
return recomputeItemForm({
item_id: Number(it.item_id || 0),
product_master_id: 0,
item_name: it.item_name || '',
generic_name: it.generic_name || '',
category: it.category || 'Tablet',
units_per_pack: Number(it.units_per_pack || 10),
batch_no: it.batch_no || '',
exp_month: expMonth,
exp_year: expYear,
expiry_date: it.expiry_date || '',
mrp: Number(it.mrp || 0),
qty_packs: Number(it.qty_packs || 1),
free_qty_packs: Number(it.free_qty_packs || 0),
ptr: Number(it.ptr || 0),
discount_pct: Number(it.discount_pct || 0),
sch_disc_amount: Number(it.sch_disc_amount || 0),
sch_disc_pct: Number(it.sch_disc_pct || 0),
cgst_pct: gst / 2,
sgst_pct: gst / 2,
gst_rate: gst,
selling_price: Number(it.selling_price || it.mrp || 0),
hsn_code: it.hsn_code || '3004',
storage_type: it.storage_type || 'Normal',
shelf_no: it.shelf_no || '',
rack_no: it.rack_no || '',
challan_item_ref_id: Number(it.challan_item_ref_id || 0)
});
}));
}
setSubTab('inward');
setShowPurchaseHistoryModal(false);
fetchSupplierChallans(data.purchase.supplier_id);
notify('Purchase Invoice #' + data.purchase.supplier_invoice_no + ' loaded for editing.', 'info');
} else {
notify(data.message || 'Could not load purchase details.', 'error');
}
} catch (e) {
notify('Error loading purchase invoice: ' + e.message, 'error');
}
};
const cancelEditPurchase = () => {
setEditingPurchaseId(null);
setIsChallan(false);
setInvoiceNo('');
setLoadedPoId(null);
setLoadedPoNumber('');
setConvertedChallanId(null);
setRows([]);
resetItemForm();
notify('Exited purchase edit mode. Ready for new inward entry.', 'info');
};
const fetchSupplierLedger = async (supplierId) => {
if (!supplierId) {
setActiveLedgerDetail(null);
return;
}
setIsLoadingLedger(true);
try {
const res = await fetch(API_BASE + 'suppliers/ledger/' + supplierId);
const data = await res.json();
if (data.status) {
setActiveLedgerDetail(data);
} else {
notify(data.message || 'Failed to fetch supplier ledger.', 'error');
}
} catch (e) {
notify('Error fetching ledger: ' + e.message, 'error');
} finally {
setIsLoadingLedger(false);
}
};
const openPaymentModal = (supplierId, purchaseId = 0, defaultAmount = 0) => {
const sId = supplierId ? String(supplierId) : (activeLedgerSupplierId || '');
setPaymentForm({
supplier_id: sId,
purchase_id: purchaseId ? String(purchaseId) : '',
amount: defaultAmount > 0 ? defaultAmount : '',
payment_mode: 'Bank',
reference_no: '',
bank_name: '',
payment_date: new Date().toISOString().split('T')[0],
remarks: purchaseId ? ('Payment for invoice #' + purchaseId) : 'On-account settlement'
});
setShowPaymentModal(true);
};
const handleRecordPayment = async (e) => {
if (e) e.preventDefault();
const supId = Number(paymentForm.supplier_id);
const amt = parseFloat(paymentForm.amount);
if (!supId || isNaN(amt) || amt <= 0) {
notify('Please specify a supplier and valid payment amount.', 'error');
return;
}
setIsRecordingPayment(true);
try {
const res = await fetch(API_BASE + 'suppliers/record-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
store_id: store.store_id,
supplier_id: supId,
purchase_id: Number(paymentForm.purchase_id) || 0,
amount: amt,
payment_mode: paymentForm.payment_mode,
reference_no: paymentForm.reference_no,
bank_name: paymentForm.bank_name,
payment_date: paymentForm.payment_date,
remarks: paymentForm.remarks
})
});
const data = await res.json();
if (data.status) {
notify('Payment recorded! Voucher #' + data.voucher_no, 'success');
setShowPaymentModal(false);
fetchLedgerSummary();
if (activeLedgerSupplierId && String(activeLedgerSupplierId) === String(supId)) {
fetchSupplierLedger(supId);
}
} else {
notify(data.message || 'Failed to record payment.', 'error');
}
} catch (err) {
notify('Payment error: ' + err.message, 'error');
} finally {
setIsRecordingPayment(false);
}
};
const fetchShortItems = async () => {
setIsLoadingShortItems(true);
try {
const res = await fetch(API_BASE + 'stock/short-items?store_id=' + store.store_id);
const data = await res.json();
if (data.status) {
setShortItems(data.short_items || []);
}
} catch (e) {
console.error('fetchShortItems error:', e);
} finally {
setIsLoadingShortItems(false);
}
};
const handleSaveShortItemFlag = async (e) => {
if (e) e.preventDefault();
if (!quickReorderForm.item_id) {
notify('Please select an item to track in Short Book.', 'error');
return;
}
setIsSavingShortItem(true);
try {
const res = await fetch(API_BASE + 'stock/flag-short-item', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
item_id: Number(quickReorderForm.item_id),
min_reorder_level: Number(quickReorderForm.min_reorder_level) || 10
})
});
const data = await res.json();
if (data.status) {
notify(data.message || 'Item updated in Short Book!', 'success');
setShowQuickReorderModal(false);
fetchShortItems();
} else {
notify(data.message || 'Failed to update reorder level.', 'error');
}
} catch (err) {
notify('Error: ' + err.message, 'error');
} finally {
setIsSavingShortItem(false);
}
};
const fetchPurchaseOrders = async () => {
setIsLoadingPOs(true);
try {
const res = await fetch(API_BASE + 'purchase-orders?store_id=' + store.store_id + '&status=' + poStatusFilter);
const data = await res.json();
if (data.status) {
setPurchaseOrders(data.purchase_orders || []);
}
} catch (e) {
console.error('fetchPurchaseOrders error:', e);
} finally {
setIsLoadingPOs(false);
}
};
const openCreatePoFromShortItems = () => {
const selected = shortItems.filter(it => selectedShortItemIds.includes(it.item_id));
if (selected.length === 0) {
notify('Please select at least one item from the short list.', 'warning');
return;
}
const defaultSupplier = selected.find(s => s.last_supplier_id > 0)?.last_supplier_id || (suppliers[0]?.supplier_id || '');
setPoCreateForm({
supplier_id: defaultSupplier ? String(defaultSupplier) : '',
expected_delivery_date: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
remarks: 'Replenishment purchase order from hospital deficiency book',
items: selected.map(it => ({
item_id: it.item_id,
item_name: it.item_name,
generic_name: it.generic_name || '',
category: it.category || 'Tablet',
order_packs: it.suggested_packs || 1,
units_per_pack: it.units_per_pack || 10,
estimated_ptr: it.last_ptr || 0,
gst_rate: it.gst_rate || 12
}))
});
setShowPoCreateModal(true);
};
const handleSavePurchaseOrder = async (e) => {
if (e) e.preventDefault();
const supId = Number(poCreateForm.supplier_id);
if (!supId || poCreateForm.items.length === 0) {
notify('Supplier and at least one line item are required for PO.', 'error');
return;
}
setIsSavingPO(true);
try {
const res = await fetch(API_BASE + 'purchase-orders/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
store_id: store.store_id,
supplier_id: supId,
expected_delivery_date: poCreateForm.expected_delivery_date,
remarks: poCreateForm.remarks,
items: poCreateForm.items
})
});
const data = await res.json();
if (data.status) {
notify('Purchase Order ' + data.po_number + ' created successfully!', 'success');
setShowPoCreateModal(false);
setSelectedShortItemIds([]);
fetchPurchaseOrders();
setSubTab('purchase_orders');
} else {
notify(data.message || 'Failed to create PO.', 'error');
}
} catch (err) {
notify('PO error: ' + err.message, 'error');
} finally {
setIsSavingPO(false);
}
};
const handleViewPo = async (poId) => {
try {
const res = await fetch(API_BASE + 'purchase-orders/' + poId);
const data = await res.json();
if (data.status) {
setActiveViewPo(data);
setShowPoViewModal(true);
} else {
notify(data.message || 'PO not found', 'error');
}
} catch (err) {
notify('Error loading PO: ' + err.message, 'error');
}
};
const handleCancelPo = async (poId) => {
if (!confirm('Are you sure you want to cancel this Purchase Order?')) return;
try {
const res = await fetch(API_BASE + 'purchase-orders/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ po_id: poId })
});
const data = await res.json();
if (data.status) {
notify(data.message || 'PO cancelled', 'success');
fetchPurchaseOrders();
} else {
notify(data.message || 'Failed to cancel PO', 'error');
}
} catch (err) {
notify('Error: ' + err.message, 'error');
}
};
const loadPoIntoInwardBill = async (poId) => {
try {
const res = await fetch(API_BASE + 'purchase-orders/' + poId);
const data = await res.json();
if (data.status && data.purchase_order) {
const po = data.purchase_order;
const poItems = data.items || [];
setSelectedSupplierId(String(po.supplier_id));
setLoadedPoId(po.po_id);
setLoadedPoNumber(po.po_number);
setInvoiceNo('INV-' + po.po_number);
if (poItems.length > 0) {
setRows(poItems.map(it => recomputeItemForm({
...initialItemForm,
item_id: it.item_id || 0,
item_name: it.item_name,
generic_name: it.generic_name || '',
category: it.category || 'Tablet',
batch_no: '',
expiry_date: '',
qty_packs: it.order_packs || 1,
free_qty_packs: 0,
units_per_pack: it.units_per_pack || 10,
mrp: it.estimated_ptr ? Math.round(Number(it.estimated_ptr) * 1.35) : 0,
ptr: Number(it.estimated_ptr) || 0,
discount_pct: 0,
gst_rate: Number(it.gst_rate) || 12,
cgst_pct: (Number(it.gst_rate) || 12) / 2,
sgst_pct: (Number(it.gst_rate) || 12) / 2,
hsn_code: '3004'
})));
}
setSubTab('inward');
setShowPoViewModal(false);
notify('Loaded ' + poItems.length + ' items from ' + po.po_number + '. Verify batch & expiry to record inward bill.', 'info');
} else {
notify('Could not load PO details.', 'error');
}
} catch (err) {
notify('Error loading PO: ' + err.message, 'error');
}
};
useEffect(() => {
fetchSuppliers();
fetchMasterSupportData();
fetchRecentPurchases();
}, []);
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');
const data = await res.json();
if (data.status) setSuppliers(data.suppliers || []);
} catch (e) {
console.error(e);
}
};
const openAddSupplier = () => {
setSupplierForm({
supplier_id: 0,
supplier_name: '',
contact_person: '',
phone: '',
email: '',
gstin: '',
pan_no: '',
dl_no_20b: '',
dl_no_21b: '',
state_code: '07',
credit_days: 30,
address: '',
bank_details: ''
});
setShowSupplierModal(true);
};
const openEditSupplier = (s) => {
setSupplierForm({
supplier_id: s.supplier_id,
supplier_name: s.supplier_name || '',
contact_person: s.contact_person || '',
phone: s.phone || '',
email: s.email || '',
gstin: s.gstin || '',
pan_no: s.pan_no || '',
dl_no_20b: s.dl_no_20b || '',
dl_no_21b: s.dl_no_21b || '',
state_code: s.state_code || '07',
credit_days: s.credit_days || 30,
address: s.address || '',
bank_details: s.bank_details || ''
});
setShowSupplierModal(true);
};
const handleSaveSupplier = async (e) => {
if (e) e.preventDefault();
if (!supplierForm.supplier_name.trim()) {
notify('Supplier / Distributor name is required.', 'error');
return;
}
setIsSavingSupplier(true);
try {
const res = await fetch(API_BASE + 'suppliers/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(supplierForm)
});
const data = await res.json();
if (data.status) {
notify(supplierForm.supplier_id ? 'Supplier updated successfully!' : 'New supplier added successfully!', 'success');
setShowSupplierModal(false);
await fetchSuppliers();
if (data.supplier_id) {
setSelectedSupplierId(String(data.supplier_id));
}
} else {
notify(data.message || 'Failed to save supplier.', 'error');
}
} catch (err) {
notify('Network error saving supplier: ' + err.message, 'error');
} finally {
setIsSavingSupplier(false);
}
};
const handleDeleteSupplier = async (supplierId, supplierName) => {
if (!confirm(`Are you sure you want to remove / deactivate supplier "${supplierName}"?`)) return;
try {
const res = await fetch(API_BASE + 'suppliers/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ supplier_id: supplierId })
});
const data = await res.json();
if (data.status) {
notify(data.message || 'Supplier removed successfully.', 'success');
await fetchSuppliers();
} else {
notify(data.message || 'Failed to remove supplier.', 'error');
}
} catch (err) {
notify('Error: ' + err.message, 'error');
}
};
const filteredSuppliers = suppliers.filter(s => {
if (!supplierSearch.trim()) return true;
const q = supplierSearch.toLowerCase();
return (s.supplier_name && s.supplier_name.toLowerCase().includes(q)) ||
(s.gstin && s.gstin.toLowerCase().includes(q)) ||
(s.dl_no_20b && s.dl_no_20b.toLowerCase().includes(q)) ||
(s.dl_no_21b && s.dl_no_21b.toLowerCase().includes(q)) ||
(s.phone && s.phone.toLowerCase().includes(q)) ||
(s.contact_person && s.contact_person.toLowerCase().includes(q));
});
const addRow = () => {
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) => {
const updated = [...rows];
updated[idx][field] = val;
setRows(updated);
};
const removeRow = (idx) => {
setRows(prev => prev.filter((_, i) => i !== idx));
if (editingItemIndex === idx) {
resetItemForm();
}
};
// Autocomplete searching for Medicine Name in Product Search
const handleProductSearchChange = (value) => {
setItemForm(prev => ({ ...prev, item_name: value, item_id: 0, product_master_id: 0 }));
setActiveSearchRow(true);
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 = (item) => {
const gstRate = Number(item.gst_rate) || 12;
const uPack = Number(item.units_per_pack) || 10;
const cgst = Number((gstRate / 2).toFixed(2));
const sgst = Number((gstRate / 2).toFixed(2));
setItemForm(prev => recomputeItemForm({
...prev,
item_id: item.item_id || 0,
product_master_id: item.product_master_id || item.item_id || 0,
item_name: item.item_name,
generic_name: item.generic_name || '',
category: item.category || 'Tablet',
units_per_pack: uPack,
hsn_code: item.hsn_code || '3004',
cgst_pct: cgst,
sgst_pct: sgst,
gst_rate: gstRate,
mrp: item.mrp || prev.mrp,
ptr: item.ptr || prev.ptr
}));
setActiveSearchRow(false);
setSuggestions([]);
if (item.item_id) {
fetchProductPurchaseHistory(item.item_id, item.item_name);
}
};
const openAddMasterModal = (initialName = '') => {
const prefillName = initialName || itemForm.item_name || '';
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(false);
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);
handleSelectMedicine(data.item);
} 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.trim()) {
notify('Please select supplier and enter invoice / challan number.', 'error');
return;
}
if (rows.length === 0) {
notify('Please add at least one medicine item to this inward bill using the form.', 'error');
return;
}
try {
const itemsPayload = rows.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),
sch_disc_amount: Number(r.sch_disc_amount || 0),
sch_disc_pct: Number(r.sch_disc_pct || 0),
gst_rate: Number(r.gst_rate || 12),
hsn_code: r.hsn_code || '3004',
selling_price: Number(r.selling_price || r.mrp || 0),
storage_type: r.storage_type || 'Normal',
shelf_no: r.shelf_no || '',
rack_no: r.rack_no || '',
challan_item_ref_id: Number(r.challan_item_ref_id || 0)
}));
const endpoint = editingPurchaseId ? (API_BASE + 'purchase/update') : (API_BASE + 'purchase/save');
const payload = {
store_id: store.store_id,
supplier_id: Number(selectedSupplierId),
supplier_invoice_no: invoiceNo.trim(),
is_challan: isChallan ? 1 : 0,
challan_no: isChallan ? invoiceNo.trim() : null,
converted_from_challan_id: convertedChallanId,
invoice_date: invoiceDate,
items: itemsPayload
};
if (editingPurchaseId) {
payload.purchase_id = editingPurchaseId;
} else if (loadedPoId) {
payload.po_id = loadedPoId;
}
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.status) {
if (editingPurchaseId) {
notify('Purchase invoice #' + invoiceNo + ' updated successfully! Stock & supplier ledger adjusted.', 'success');
} else if (isChallan) {
notify('Delivery Challan #' + invoiceNo + ' recorded! Stock replenished.', 'success');
} else {
notify('Purchase inward recorded! Stock & Supplier Ledger updated.' + (loadedPoNumber ? (' (PO ' + loadedPoNumber + ' marked as received)') : ''), 'success');
}
setEditingPurchaseId(null);
setIsChallan(false);
setInvoiceNo('');
setLoadedPoId(null);
setLoadedPoNumber('');
setConvertedChallanId(null);
setRows([]);
resetItemForm();
fetchRecentPurchases();
if (selectedSupplierId) {
fetchSupplierChallans(selectedSupplierId);
}
if (activeLedgerSupplierId) fetchSupplierLedger(activeLedgerSupplierId);
fetchLedgerSummary();
} else {
notify(data.message || 'Failed to save purchase invoice.', 'error');
}
} catch (e) {
notify('Purchase invoice error: ' + e.message, 'error');
}
};
return (
<div>
<div className="d-flex flex-wrap gap-2 mb-3 align-items-center">
<button className={`btn btn-sm ${subTab === 'inward' ? 'btn-primary shadow-sm' : 'btn-outline-secondary'}`} onClick={() => setSubTab('inward')}>
<i className="bi bi-box-arrow-in-down me-1"></i> Inward Purchase Bill
</button>
<button className={`btn btn-sm ${subTab === 'ledger' ? 'btn-primary shadow-sm' : 'btn-outline-secondary'}`} onClick={() => { setSubTab('ledger'); fetchLedgerSummary(); }}>
<i className="bi bi-journal-bookmark me-1"></i> Supplier Ledger &amp; Payments
</button>
<button className={`btn btn-sm ${subTab === 'short_items' ? 'btn-warning text-dark fw-bold shadow-sm' : 'btn-outline-secondary'}`} onClick={() => { setSubTab('short_items'); fetchShortItems(); }}>
<i className="bi bi-exclamation-triangle me-1"></i> Short Items &amp; PO Generator
{shortItems.length > 0 && <span className="badge bg-danger ms-1">{shortItems.length}</span>}
</button>
<button className={`btn btn-sm ${subTab === 'purchase_orders' ? 'btn-primary shadow-sm' : 'btn-outline-secondary'}`} onClick={() => { setSubTab('purchase_orders'); fetchPurchaseOrders(); }}>
<i className="bi bi-file-earmark-text me-1"></i> Purchase Orders
</button>
<button className={`btn btn-sm ${subTab === 'suppliers' ? 'btn-primary shadow-sm' : 'btn-outline-secondary'}`} onClick={() => setSubTab('suppliers')}>
<i className="bi bi-truck me-1"></i> Supplier Directory ({suppliers.length})
</button>
<button className={`btn btn-sm ${subTab === 'excel' ? 'btn-success shadow-sm' : 'btn-outline-secondary'}`} onClick={() => setSubTab('excel')}>
<i className="bi bi-file-earmark-spreadsheet me-1"></i> Excel / CSV Import
</button>
</div>
{subTab === 'inward' && (
<div className="counter-card">
{/* Header Row */}
<div className="d-flex flex-wrap justify-content-between align-items-center mb-3 gap-2">
<div>
<h5 className="fw-bold mb-0">
<i className={`bi ${isChallan ? 'bi-truck text-warning' : 'bi-receipt text-primary'} me-1.5`}></i>
{editingPurchaseId
? `Edit Purchase Invoice #${invoiceNo}`
: (isChallan ? 'Distributor Delivery Challan (Stock Entry)' : 'Distributor Purchase Inward (Tax Invoice)')
}
</h5>
<small className="text-muted">
{editingPurchaseId
? `Modifying existing Bill #${invoiceNo} (Ref #${editingPurchaseId}) - adjustments reflect in stock & ledger`
: (isChallan
? 'Record inward stock under vendor delivery challan (can be converted to tax invoice later)'
: 'Enter vendor tax invoice to replenish store inventory and post supplier ledger payable'
)
}
</small>
</div>
<div className="d-flex align-items-center gap-2">
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={() => {
fetchRecentPurchases();
setShowPurchaseHistoryModal(true);
}}
title="View recent inward bills and select one to edit"
>
<i className="bi bi-clock-history me-1"></i> Inward Bills History / Edit ({recentPurchases.length})
</button>
{loadedPoNumber ? (
<div className="badge bg-info-subtle text-info border border-info px-2 py-1.5 d-flex align-items-center gap-1.5" style={{ fontSize: '12px' }}>
<i className="bi bi-link-45deg fs-6"></i>
<span>Linked PO: <strong>{loadedPoNumber}</strong></span>
<button type="button" className="btn-close btn-close-dark ms-1" style={{ fontSize: '9px' }} onClick={() => { setLoadedPoId(null); setLoadedPoNumber(''); }} title="Unlink PO"></button>
</div>
) : (
<button
type="button"
className="btn btn-sm btn-outline-primary"
onClick={() => {
fetchPurchaseOrders();
setSubTab('purchase_orders');
}}
>
<i className="bi bi-box-arrow-in-down-right me-1"></i> Load from Purchase Order
</button>
)}
</div>
</div>
{/* Edit Mode Alert Banner */}
{editingPurchaseId && (
<div className="alert alert-warning border border-warning-subtle shadow-sm py-2 px-3 mb-3 d-flex justify-content-between align-items-center">
<div className="d-flex align-items-center gap-2">
<span className="badge bg-warning text-dark fs-6 px-2.5 py-1.5"><i className="bi bi-pencil-square me-1"></i>EDIT MODE</span>
<div>
<strong className="text-dark d-block">Editing Purchase Invoice #{invoiceNo} (Purchase ID: #{editingPurchaseId})</strong>
<small className="text-muted">Updating this invoice will recalculate batch stock in this store and adjust supplier ledger balances.</small>
</div>
</div>
<button
type="button"
className="btn btn-outline-dark btn-sm fw-semibold"
onClick={cancelEditPurchase}
>
<i className="bi bi-x-circle me-1"></i> Cancel Edit &amp; New Inward
</button>
</div>
)}
{/* Step 1: Top Controls (Supplier, Entry Type, Invoice/Challan No, Date) */}
<div className="card border-0 shadow-sm p-3 mb-3" style={{ background: '#f8fafc', borderRadius: '10px' }}>
<div className="row g-2 align-items-center">
{/* Supplier Select */}
<div className="col-md-4">
<div className="d-flex justify-content-between align-items-center mb-1">
<label className="form-label small fw-bold mb-0">Supplier / Distributor <span className="text-danger">*</span></label>
<button type="button" className="btn btn-link btn-sm p-0 text-decoration-none fw-semibold" style={{ fontSize: '11px' }} onClick={openAddSupplier}>
<i className="bi bi-plus-circle me-1"></i>+ Add New
</button>
</div>
<select
className="form-select form-select-sm"
value={selectedSupplierId}
onChange={(e) => {
const sId = e.target.value;
setSelectedSupplierId(sId);
fetchSupplierChallans(sId);
}}
>
<option value="">-- Select Supplier --</option>
{suppliers.map(s => (
<option key={s.supplier_id} value={s.supplier_id}>
{s.supplier_name} (GST: {s.gstin || 'N/A'})
</option>
))}
</select>
</div>
{/* Entry Type Toggle: Tax Invoice vs Delivery Challan */}
<div className="col-md-3">
<label className="form-label small fw-bold mb-1">Entry Type</label>
<div className="btn-group btn-group-sm w-100" role="group">
<button
type="button"
className={`btn ${!isChallan ? 'btn-primary fw-bold' : 'btn-outline-secondary'}`}
onClick={() => setIsChallan(false)}
>
<i className="bi bi-receipt me-1"></i> Tax Invoice
</button>
<button
type="button"
className={`btn ${isChallan ? 'btn-warning text-dark fw-bold' : 'btn-outline-secondary'}`}
onClick={() => setIsChallan(true)}
>
<i className="bi bi-truck me-1"></i> Challan
</button>
</div>
</div>
{/* Bill / Invoice / Challan No */}
<div className="col-md-3">
<label className="form-label small fw-bold mb-1">
{isChallan ? 'Challan Number' : 'Bill / Invoice No.'} <span className="text-danger">*</span>
</label>
<input
type="text"
className="form-control form-control-sm"
placeholder={isChallan ? "e.g. CHL-2026-001" : "e.g. APX-9941"}
value={invoiceNo}
onChange={(e) => setInvoiceNo(e.target.value)}
/>
</div>
{/* Date */}
<div className="col-md-2">
<label className="form-label small fw-bold mb-1">
{isChallan ? 'Challan Date' : 'Invoice Date'}
</label>
<input
type="date"
className="form-control form-control-sm"
value={invoiceDate}
onChange={(e) => setInvoiceDate(e.target.value)}
/>
</div>
{/* Delivery Challan Note */}
{isChallan && (
<div className="col-12 mt-1">
<div className="badge bg-warning-subtle text-dark border border-warning px-2.5 py-1 d-inline-flex align-items-center gap-1">
<i className="bi bi-info-circle-fill text-warning"></i>
<span><strong>Challan Inward:</strong> Adds physical stock immediately without ledger debt. This challan can later be imported directly into a final Tax Invoice.</span>
</div>
</div>
)}
</div>
</div>
{/* Progressive Disclosure Check: Must select supplier and enter invoice/challan no */}
{(!selectedSupplierId || !invoiceNo.trim()) && !editingPurchaseId ? (
<div className="card border border-2 border-dashed bg-light text-center p-4 my-4" style={{ borderRadius: '12px' }}>
<div className="mx-auto mb-2 text-primary" style={{ width: '48px', height: '48px', borderRadius: '50%', background: '#e0f2fe', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<i className="bi bi-arrow-up-circle fs-3"></i>
</div>
<h6 className="fw-bold text-dark mb-1">Step 1: Select Supplier &amp; Enter {isChallan ? 'Challan' : 'Invoice'} Details Above</h6>
<p className="text-muted small mb-0" style={{ maxWidth: '560px', margin: '0 auto' }}>
Please choose the <strong>Supplier / Distributor</strong> and provide the <strong>{isChallan ? 'Delivery Challan No.' : 'Supplier Bill / Invoice No.'}</strong> above to unlock the medicine entry form, historical rate lookup, and pending challan imports.
</p>
</div>
) : (
/* Step 2: Main Pharma Entry Workstation (2-Column Grid) */
<div className="row g-3">
{/* Left Column (8 cols): Items Table + Totals + Pharma Entry Form + Primary Save */}
<div className="col-xl-8 col-lg-7">
{/* Top Items Table (Matching Image 2) */}
<div className="card border shadow-sm mb-3">
<div className="card-header bg-white py-2 px-3 d-flex justify-content-between align-items-center">
<span className="fw-bold text-dark small">
<i className="bi bi-list-check me-1 text-primary"></i>
Invoice Line Items ({rows.length})
</span>
{rows.length > 0 && (
<span className="badge bg-primary-subtle text-primary border">
Total Items: {rows.length}
</span>
)}
</div>
<div className="table-responsive" style={{ maxHeight: '280px', overflowY: 'auto' }}>
<table className="table table-sm table-hover table-bordered mb-0 align-middle" style={{ fontSize: '11.5px' }}>
<thead className="table-light sticky-top">
<tr>
<th style={{ width: '30px' }} className="text-center">#</th>
<th style={{ width: '50px' }} className="text-center">RecNo</th>
<th style={{ minWidth: '130px', maxWidth: '160px' }}>Item Name</th>
<th style={{ width: '80px' }}>Batch No</th>
<th style={{ width: '65px' }} className="text-center">Exp.</th>
<th style={{ width: '65px' }} className="text-end">MRP</th>
<th style={{ width: '55px' }} className="text-center">Qty.</th>
<th style={{ width: '65px' }} className="text-end">Rate</th>
<th style={{ width: '75px' }} className="text-end">Amount</th>
<th style={{ width: '50px' }} className="text-end">Disc.</th>
<th style={{ width: '70px' }} className="text-end">Tax Amount</th>
<th style={{ width: '60px' }} className="text-end">CGST</th>
<th style={{ width: '60px' }} className="text-end">SGST</th>
<th style={{ width: '80px' }} className="text-end">Net Amount</th>
<th style={{ width: '60px' }} className="text-center">Action</th>
</tr>
</thead>
<tbody>
{rows.length === 0 ? (
<tr>
<td colSpan="15" className="text-center text-muted py-4">
<i className="bi bi-inbox fs-4 d-block mb-1 text-secondary"></i>
<span>No medicines added yet. Use the form below or include pending challans from the right.</span>
</td>
</tr>
) : (
rows.map((r, i) => {
const expDisplay = r.exp_month && r.exp_year ? `${r.exp_month}/${r.exp_year}` : (r.expiry_date ? r.expiry_date.slice(2, 7) : '-');
const taxAmt = ((Number(r.cgst_amount) || 0) + (Number(r.sgst_amount) || 0)).toFixed(2);
return (
<tr key={i} className={editingItemIndex === i ? 'table-warning' : ''}>
<td className="text-center text-secondary">{i + 1}</td>
<td className="text-center text-secondary">{r.item_id || r.product_master_id || (i + 1)}</td>
<td style={{ maxWidth: '160px' }}>
<div className="fw-bold text-truncate text-dark" title={r.item_name}>{r.item_name}</div>
{r.generic_name && (
<div className="text-muted text-truncate" style={{ fontSize: '10px' }} title={r.generic_name}>{r.generic_name}</div>
)}
</td>
<td><span className="badge bg-light text-dark border font-monospace">{r.batch_no || '-'}</span></td>
<td className="text-center">{expDisplay}</td>
<td className="text-end">₹{Number(r.mrp || 0).toFixed(2)}</td>
<td className="text-center">
<span className="fw-bold">{r.qty_packs}</span>
{Number(r.free_qty_packs) > 0 && <span className="text-success" style={{ fontSize: '10px' }}>+{r.free_qty_packs}F</span>}
</td>
<td className="text-end">₹{Number(r.ptr || 0).toFixed(2)}</td>
<td className="text-end">₹{Number(r.amount || 0).toFixed(2)}</td>
<td className="text-end">{Number(r.discount_pct || 0)}%</td>
<td className="text-end">₹{taxAmt}</td>
<td className="text-end">₹{Number(r.cgst_amount || (Number(r.taxable_amount || 0) * (Number(r.cgst_pct || 6) / 100))).toFixed(2)}</td>
<td className="text-end">₹{Number(r.sgst_amount || (Number(r.taxable_amount || 0) * (Number(r.sgst_pct || 6) / 100))).toFixed(2)}</td>
<td className="text-end fw-bold text-dark">₹{Number(r.net_amount || 0).toFixed(2)}</td>
<td className="text-center">
<div className="btn-group btn-group-sm">
<button
type="button"
className="btn btn-outline-primary btn-sm py-0 px-1"
onClick={() => handleEditRow(i)}
title="Edit this row in form below"
>
<i className="bi bi-pencil"></i>
</button>
<button
type="button"
className="btn btn-outline-danger btn-sm py-0 px-1"
onClick={() => removeRow(i)}
title="Delete this row"
>
<i className="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
{/* Footer Totals (Matching Image 2: Total 5708.00 | 143.00 | 143.00 | 5994.00) */}
<div className="card-footer bg-light py-2 px-3 d-flex flex-wrap justify-content-end align-items-center gap-4 text-secondary small fw-semibold">
<div>
<span className="text-muted fw-normal">Total Taxable: </span>
<strong className="text-dark fs-6">₹{billTotals.taxable}</strong>
</div>
<div>
<span className="text-muted fw-normal">CGST: </span>
<strong className="text-dark">₹{billTotals.cgst}</strong>
</div>
<div>
<span className="text-muted fw-normal">SGST: </span>
<strong className="text-dark">₹{billTotals.sgst}</strong>
</div>
<div>
<span className="text-muted fw-normal">Net Amount: </span>
<strong className="text-primary fs-5">₹{billTotals.net}</strong>
</div>
</div>
</div>
{/* Pharma Stock Entry Form (Matching Image 2) */}
<div className="card border shadow-sm p-3 mb-3 bg-white" style={{ borderRadius: '10px' }}>
<div className="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
<h6 className="fw-bold mb-0 text-dark">
<i className="bi bi-plus-circle-dotted text-primary me-1"></i>
{editingItemIndex !== null ? `Modify Row #${editingItemIndex + 1}` : 'Add Medicine to Inward Bill'}
</h6>
{editingItemIndex !== null && (
<button type="button" className="btn btn-sm btn-link text-danger p-0 text-decoration-none" onClick={resetItemForm}>
<i className="bi bi-x-circle me-1"></i> Cancel Row Edit
</button>
)}
</div>
{/* Row 1: Product Search & Product Code */}
<div className="row g-2 mb-2">
<div className="col-md-9 position-relative">
<label className="form-label small fw-bold mb-1 d-flex justify-content-between align-items-center">
<span>Product Search (Master Auto-Search) <span className="text-danger">*</span></span>
<button
type="button"
className="btn btn-link btn-sm p-0 text-decoration-none fw-semibold"
style={{ fontSize: '11px' }}
onClick={() => openAddMasterModal(itemForm.item_name)}
>
<i className="bi bi-plus-circle me-1"></i>+ Add to Master
</button>
</label>
<div className="input-group input-group-sm">
<input
type="text"
id="pharma_product_search_input"
className={`form-control form-control-sm fw-semibold ${itemForm.item_id ? 'border-success' : ''}`}
placeholder="Type medicine name (e.g. ACEFE-P, Augmentin)..."
value={itemForm.item_name}
autoComplete="off"
onChange={(e) => handleProductSearchChange(e.target.value)}
onFocus={() => {
if (itemForm.item_name.trim().length >= 1) {
handleProductSearchChange(itemForm.item_name);
}
}}
/>
{itemForm.item_id > 0 && (
<span className="input-group-text bg-success text-white py-0 px-2" title="Matched in Master Catalog">
<i className="bi bi-check2"></i>
</span>
)}
</div>
{/* Generic Subtext */}
{itemForm.generic_name ? (
<div className="text-muted text-truncate mt-1 d-flex align-items-center gap-1" style={{ fontSize: '11px' }}>
<span className="badge bg-light text-primary border" style={{ fontSize: '9.5px' }}>{itemForm.category || 'Tablet'}</span>
<span>{itemForm.generic_name}</span>
</div>
) : null}
{/* Autocomplete Dropdown */}
{activeSearchRow && (
<div
className="shadow-lg border rounded-2 bg-white"
style={{
position: 'absolute',
top: '100%',
left: 0,
width: '100%',
maxHeight: '280px',
overflowY: 'auto',
zIndex: 1060,
marginTop: '2px'
}}
>
<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(false)}
></button>
</div>
{suggestions.length > 0 ? (
suggestions.map((sug, 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(sug);
}}
>
<div className="d-flex justify-content-between align-items-center">
<span className="fw-bold text-dark" style={{ fontSize: '12.5px' }}>{sug.item_name}</span>
<span className="badge bg-primary-subtle text-primary border" style={{ fontSize: '10px' }}>
{sug.category || 'Tablet'}
</span>
</div>
{sug.generic_name && (
<div className="text-muted text-truncate small" style={{ fontSize: '11px' }}>{sug.generic_name}</div>
)}
<div className="d-flex gap-2 mt-1 align-items-center" style={{ fontSize: '10.5px' }}>
<span className="text-secondary">Pack: <strong>{sug.units_per_pack || 10}s</strong></span>
<span className="text-secondary">GST: <strong>{sug.gst_rate}%</strong></span>
<span className="text-secondary">HSN: <strong>{sug.hsn_code || '3004'}</strong></span>
{sug.mrp ? <span className="text-secondary">MRP: <strong>₹{sug.mrp}</strong></span> : null}
</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 "{itemForm.item_name}"</div>
<div className="text-muted" style={{ fontSize: '11px' }}>Click below to add it directly into Pharmacy Master</div>
</div>
)
)}
<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(itemForm.item_name);
}}
>
<i className="bi bi-plus-circle-fill me-1"></i>+ Add "{itemForm.item_name || 'New Medicine'}" in Master
</button>
</div>
</div>
)}
</div>
{/* Product Code */}
<div className="col-md-3">
<label className="form-label small fw-bold mb-1">Product Code</label>
<input
type="text"
className="form-control form-control-sm bg-light text-secondary font-monospace"
value={itemForm.product_master_id || itemForm.item_id || 'AUTO'}
readOnly
/>
</div>
</div>
{/* Conditional display: Only show details below Product Search when a product is selected or editing */}
{(itemForm.item_id > 0 || editingItemIndex !== null) ? (
<>
{/* Auto-filled Notification Banner */}
<div className="d-flex align-items-center justify-content-between bg-primary-subtle border border-primary-subtle px-2.5 py-1 rounded mb-2">
<span className="small text-primary fw-semibold">
<i className="bi bi-check-circle-fill me-1 text-success"></i>
{editingItemIndex !== null
? `Editing: ${itemForm.item_name}`
: `Selected: ${itemForm.item_name} (Auto-filled with last batch, rate & location)`
}
</span>
<button type="button" className="btn btn-link btn-sm p-0 text-danger text-decoration-none fw-semibold" style={{ fontSize: '11px' }} onClick={resetItemForm}>
<i className="bi bi-x-circle me-1"></i> Change Product
</button>
</div>
{/* Row 2: Packaging, Batch No, Exp Month, Exp Year, MRP, Qty */}
<div className="row g-2 mb-2">
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Packaging</label>
<input
type="number"
className="form-control form-control-sm"
placeholder="10"
value={itemForm.units_per_pack}
onChange={(e) => handleItemFormChange('units_per_pack', e.target.value)}
/>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Batch No. <span className="text-danger">*</span></label>
<input
type="text"
className="form-control form-control-sm text-uppercase"
placeholder="e.g. B-902"
value={itemForm.batch_no}
onChange={(e) => handleItemFormChange('batch_no', e.target.value)}
/>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Exp. Month</label>
<select
className="form-select form-select-sm"
value={itemForm.exp_month}
onChange={(e) => handleItemFormChange('exp_month', e.target.value)}
>
{['01','02','03','04','05','06','07','08','09','10','11','12'].map(m => (
<option key={m} value={m}>{m} ({['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][parseInt(m)-1]})</option>
))}
</select>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Exp. Year</label>
<select
className="form-select form-select-sm"
value={itemForm.exp_year}
onChange={(e) => handleItemFormChange('exp_year', e.target.value)}
>
{['24','25','26','27','28','29','30','31','32','33','34','35','36'].map(y => (
<option key={y} value={y}>20{y}</option>
))}
</select>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">MRP (₹)</label>
<input
type="number"
step="0.01"
className="form-control form-control-sm"
placeholder="0.00"
value={itemForm.mrp}
onChange={(e) => handleItemFormChange('mrp', e.target.value)}
/>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Qty (Packs) <span className="text-danger">*</span></label>
<input
type="number"
min="1"
className="form-control form-control-sm fw-bold"
value={itemForm.qty_packs}
onChange={(e) => handleItemFormChange('qty_packs', e.target.value)}
/>
</div>
</div>
{/* Row 3: Free Qty, Purchase Rate, Amount, Discount %, SCH Amount, SCH Discount % */}
<div className="row g-2 mb-2">
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Free Qty</label>
<input
type="number"
min="0"
className="form-control form-control-sm"
value={itemForm.free_qty_packs}
onChange={(e) => handleItemFormChange('free_qty_packs', e.target.value)}
/>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Purchase Rate (PTR)</label>
<input
type="number"
step="0.01"
className="form-control form-control-sm"
placeholder="0.00"
value={itemForm.ptr}
onChange={(e) => handleItemFormChange('ptr', e.target.value)}
/>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Amount</label>
<input
type="text"
className="form-control form-control-sm bg-light text-secondary fw-semibold"
value={'₹' + Number(itemForm.amount || 0).toFixed(2)}
readOnly
/>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Discount %</label>
<input
type="number"
step="0.1"
min="0"
max="100"
className="form-control form-control-sm"
value={itemForm.discount_pct}
onChange={(e) => handleItemFormChange('discount_pct', e.target.value)}
/>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">SCH Amount</label>
<input
type="number"
step="0.01"
min="0"
className="form-control form-control-sm"
value={itemForm.sch_disc_amount}
onChange={(e) => handleItemFormChange('sch_disc_amount', e.target.value)}
/>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">SCH Discount %</label>
<input
type="number"
step="0.1"
min="0"
max="100"
className="form-control form-control-sm"
value={itemForm.sch_disc_pct}
onChange={(e) => handleItemFormChange('sch_disc_pct', e.target.value)}
/>
</div>
</div>
{/* Row 4: Taxable Amount, CGST %, SGST / IGST %, Net Amount, Selling Price, HSNCODE */}
<div className="row g-2 mb-2">
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Taxable Amount</label>
<input
type="text"
className="form-control form-control-sm bg-light text-secondary fw-semibold"
value={'₹' + Number(itemForm.taxable_amount || 0).toFixed(2)}
readOnly
/>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">CGST %</label>
<select
className="form-select form-select-sm"
value={itemForm.cgst_pct}
onChange={(e) => {
const val = parseFloat(e.target.value) || 0;
handleItemFormChange('cgst_pct', val);
handleItemFormChange('sgst_pct', val);
}}
>
<option value="0">0% (Nil / Exempt)</option>
<option value="2.5">2.5% (5% GST)</option>
<option value="9">9.0% (18% GST)</option>
<option value="20">20.0% (40% GST)</option>
<option value="6">6.0% (12% Legacy)</option>
<option value="14">14.0% (28% Legacy)</option>
</select>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">SGST / IGST %</label>
<select
className="form-select form-select-sm"
value={itemForm.sgst_pct}
onChange={(e) => handleItemFormChange('sgst_pct', parseFloat(e.target.value) || 0)}
>
<option value="0">0% (Nil / Exempt)</option>
<option value="2.5">2.5% (5% GST)</option>
<option value="9">9.0% (18% GST)</option>
<option value="20">20.0% (40% GST)</option>
<option value="6">6.0% (12% Legacy)</option>
<option value="14">14.0% (28% Legacy)</option>
</select>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Net Amount</label>
<input
type="text"
className="form-control form-control-sm bg-primary-subtle text-primary fw-bold"
value={'₹' + Number(itemForm.net_amount || 0).toFixed(2)}
readOnly
/>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Selling Price</label>
<input
type="number"
step="0.01"
className="form-control form-control-sm"
placeholder="MRP"
value={itemForm.selling_price}
onChange={(e) => handleItemFormChange('selling_price', e.target.value)}
/>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">HSNCODE</label>
<input
type="text"
className="form-control form-control-sm"
value={itemForm.hsn_code}
onChange={(e) => handleItemFormChange('hsn_code', e.target.value)}
/>
</div>
</div>
{/* Row 5: Storage, Shelf No., Rack No., Buttons */}
<div className="row g-2 align-items-end">
<div className="col-md-3 col-6">
<label className="form-label small fw-bold mb-1">Storage</label>
<select
className="form-select form-select-sm"
value={itemForm.storage_type}
onChange={(e) => handleItemFormChange('storage_type', e.target.value)}
>
<option value="Normal">Normal</option>
<option value="Cold Storage">Cold Storage (2-8°C)</option>
<option value="Cool">Cool (8-15°C)</option>
<option value="Deep Freeze">Deep Freeze (-20°C)</option>
</select>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Shelf No.</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. S-1"
value={itemForm.shelf_no}
onChange={(e) => handleItemFormChange('shelf_no', e.target.value)}
/>
</div>
<div className="col-md-2 col-6">
<label className="form-label small fw-bold mb-1">Rack No.</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. R-10"
value={itemForm.rack_no}
onChange={(e) => handleItemFormChange('rack_no', e.target.value)}
/>
</div>
<div className="col-md-5 col-12 d-flex gap-2 justify-content-end mt-2 mt-md-0">
<button
type="button"
className="btn btn-outline-secondary btn-sm px-3"
onClick={resetItemForm}
>
<i className="bi bi-arrow-counterclockwise me-1"></i> Clear
</button>
<button
type="button"
className={editingItemIndex !== null ? "btn btn-warning text-dark fw-bold btn-sm px-3 shadow-sm" : "btn btn-primary fw-bold btn-sm px-4 shadow-sm"}
onClick={handleAddOrUpdateItem}
>
<i className={editingItemIndex !== null ? "bi bi-check2-circle me-1" : "bi bi-plus-circle me-1"}></i>
{editingItemIndex !== null ? 'Update Stock Item' : 'Add Stock'}
</button>
</div>
</div>
</>
) : (
<div className="text-center py-4 text-muted small bg-light rounded-2 border border-dashed mt-2">
<i className="bi bi-search text-primary me-1.5 fs-5 d-block mb-1"></i>
<span>Search and select a medicine in <strong>Product Search</strong> above to enter stock details and auto-fill last batch, expiry, MRP, rate &amp; rack location.</span>
</div>
)}
</div>
{/* Primary Save Button Bar */}
<div className="d-flex flex-wrap justify-content-between align-items-center p-3 bg-light rounded-3 border">
<div>
<span className="text-muted small">Ready to commit to inventory:</span>
<div className="fw-bold text-dark">
{rows.length} {rows.length === 1 ? 'medicine' : 'medicines'} | Net Payable: <span className="text-primary fs-6">₹{billTotals.net}</span>
</div>
</div>
<div className="d-flex gap-2">
{editingPurchaseId && (
<button type="button" className="btn btn-outline-secondary btn-sm px-3" onClick={cancelEditPurchase}>
<i className="bi bi-x-circle me-1"></i> Cancel Edit
</button>
)}
<button
className={editingPurchaseId ? "btn btn-warning shadow-sm fw-bold px-3 py-2 text-dark" : (isChallan ? "btn btn-warning fw-bold text-dark shadow-sm px-4 py-2" : "btn btn-success-custom shadow-sm px-4 py-2")}
onClick={savePurchaseBill}
>
<i className={editingPurchaseId ? "bi bi-check2-all me-1" : (isChallan ? "bi bi-truck me-1" : "bi bi-check-circle me-1")}></i>
{editingPurchaseId
? `Update Purchase Invoice #${invoiceNo}`
: (isChallan ? 'Record Delivery Challan & Update Stock' : 'Record Inward Bill & Update Inventory')
}
</button>
</div>
</div>
</div>
{/* Right Column (4 cols): Pending Delivery Challans + Previous Purchase History */}
<div className="col-xl-4 col-lg-5">
{/* Panel 1: Pending Challans for Selected Supplier */}
<div className="card border shadow-sm mb-3">
<div className="card-header bg-white py-2 px-3 d-flex justify-content-between align-items-center">
<span className="fw-bold text-dark small">
<i className="bi bi-truck text-warning me-1"></i>
Pending Delivery Challans
</span>
{selectedSupplierId && (
<button
type="button"
className="btn btn-sm btn-link p-0 text-decoration-none"
onClick={() => fetchSupplierChallans(selectedSupplierId)}
title="Refresh supplier challans"
>
<i className={`bi bi-arrow-repeat ${isLoadingChallans ? 'spinner-border spinner-border-sm' : ''}`}></i>
</button>
)}
</div>
<div className="card-body p-2" style={{ maxHeight: '280px', overflowY: 'auto' }}>
{!selectedSupplierId ? (
<div className="text-muted small p-3 text-center bg-light rounded">
Select a supplier above to view pending challans.
</div>
) : isLoadingChallans ? (
<div className="text-center py-3 text-muted small">
<div className="spinner-border spinner-border-sm text-primary me-2"></div>
Loading supplier challans...
</div>
) : supplierChallans.length === 0 ? (
<div className="text-muted small p-3 text-center bg-light rounded">
No challan items available for this supplier.
</div>
) : (
supplierChallans.map((ch, chIdx) => (
<div key={chIdx} className="border rounded-2 p-2 mb-2 bg-light">
<div className="d-flex justify-content-between align-items-center mb-1.5 pb-1 border-bottom">
<div>
<strong className="text-dark" style={{ fontSize: '12px' }}>
Challan #{ch.challan_no}
</strong>
<div className="text-muted" style={{ fontSize: '10.5px' }}>
Date: {ch.date || 'N/A'} • {ch.items ? ch.items.length : 0} items
</div>
</div>
<button
type="button"
className="btn btn-xs btn-outline-primary py-0.5 px-2"
style={{ fontSize: '11px' }}
onClick={() => includeAllChallanItems(ch)}
title="Import all items from this challan into invoice"
>
<i className="bi bi-box-arrow-in-right me-1"></i> Include All
</button>
</div>
{ch.items && ch.items.length > 0 ? (
<div className="d-flex flex-column gap-1">
{ch.items.map((ci, cItemIdx) => (
<div key={cItemIdx} className="d-flex justify-content-between align-items-center bg-white p-1.5 rounded border" style={{ fontSize: '11px' }}>
<div className="text-truncate me-2" style={{ maxWidth: '170px' }}>
<div className="fw-semibold text-dark text-truncate">{ci.item_name}</div>
<span className="text-muted font-monospace" style={{ fontSize: '10px' }}>Batch: {ci.batch_no || '-'} | Qty: {ci.qty_packs}</span>
</div>
<button
type="button"
className="btn btn-xs btn-primary py-0 px-1.5 text-nowrap"
style={{ fontSize: '10.5px' }}
onClick={() => includeChallanItem(ch, ci)}
title="Import this item into invoice"
>
+ Include
</button>
</div>
))}
</div>
) : (
<div className="text-muted small" style={{ fontSize: '11px' }}>No items found in this challan.</div>
)}
</div>
))
)}
</div>
</div>
{/* Panel 2: Previous Purchase History for Selected Product */}
<div className="card border shadow-sm">
<div className="card-header bg-white py-2 px-3 d-flex justify-content-between align-items-center">
<span className="fw-bold text-dark small">
<i className="bi bi-clock-history text-info me-1"></i>
Previous Purchase History
</span>
{historyTargetItemName && (
<span className="badge bg-info-subtle text-info border text-truncate" style={{ maxWidth: '140px', fontSize: '10.5px' }}>
{historyTargetItemName}
</span>
)}
</div>
<div className="card-body p-2" style={{ maxHeight: '310px', overflowY: 'auto' }}>
{isLoadingProductHistory ? (
<div className="text-center py-3 text-muted small">
<div className="spinner-border spinner-border-sm text-info me-2"></div>
Fetching purchase rate history...
</div>
) : !historyTargetItemName && productHistory.length === 0 ? (
<div className="text-muted small p-3 text-center bg-light rounded">
<i className="bi bi-search d-block mb-1 fs-5 text-secondary"></i>
Type or select a product in <strong>Product Search</strong> to check previous purchase prices &amp; vendors.
</div>
) : productHistory.length === 0 ? (
<div className="text-muted small p-3 text-center bg-light rounded">
No purchase history found for selected product.
</div>
) : (
<div className="d-flex flex-column gap-2">
{productHistory.map((ph, phIdx) => (
<div key={phIdx} className="p-2 border rounded-2 bg-light" style={{ fontSize: '11px' }}>
<div className="d-flex justify-content-between align-items-center mb-1">
<strong className="text-primary text-truncate" style={{ maxWidth: '160px' }}>
{ph.supplier_name || 'Distributor'}
</strong>
<span className="text-muted" style={{ fontSize: '10px' }}>
{ph.invoice_date || ph.created_at ? (ph.invoice_date || ph.created_at).slice(0, 10) : ''}
</span>
</div>
<div className="d-flex justify-content-between text-secondary mb-1" style={{ fontSize: '10.5px' }}>
<span>Inv: <strong>{ph.supplier_invoice_no || '-'}</strong></span>
<span>Batch: <span className="font-monospace">{ph.batch_no || '-'}</span></span>
</div>
<div className="d-flex justify-content-between align-items-center pt-1 border-top mt-1">
<div>
<span className="text-muted">PTR: </span>
<strong className="text-dark">₹{Number(ph.ptr || 0).toFixed(2)}</strong>
{ph.mrp ? <span className="text-muted ms-2">MRP: ₹{Number(ph.mrp).toFixed(2)}</span> : null}
{Number(ph.discount_pct) > 0 ? <span className="text-success ms-1">(-{ph.discount_pct}%)</span> : null}
</div>
<button
type="button"
className="btn btn-xs btn-outline-info py-0 px-1.5"
style={{ fontSize: '10.5px' }}
onClick={() => {
setItemForm(prev => recomputeItemForm({
...prev,
ptr: ph.ptr || prev.ptr,
mrp: ph.mrp || prev.mrp,
discount_pct: ph.discount_pct !== undefined ? ph.discount_pct : prev.discount_pct,
gst_rate: ph.gst_rate !== undefined ? ph.gst_rate : prev.gst_rate,
cgst_pct: (Number(ph.gst_rate) || 12) / 2,
sgst_pct: (Number(ph.gst_rate) || 12) / 2
}));
notify(`Applied historical PTR ₹${ph.ptr} to entry form.`, 'info');
}}
title="Apply these purchase rates to current item form"
>
Use Rate
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
)}
</div>
)}
{subTab === 'excel' && (
<div className="counter-card">
<h5 className="fw-bold text-success mb-2"><i className="bi bi-file-earmark-spreadsheet me-1"></i> Excel / CSV Inventory Importer</h5>
<p className="text-muted small mb-3">
Upload your stock export from Microsoft Excel or CSV format to seamlessly import medicines, batch numbers, MRP, PTR, HSN codes, and opening stock into <strong>{store.store_name}</strong>.
</p>
<div className="p-4 bg-light rounded-3 border text-center mb-3">
<i className="bi bi-cloud-arrow-up fs-1 text-success d-block mb-2"></i>
<h6 className="fw-bold">Upload CSV / Excel File</h6>
<p className="small text-muted mb-3">Ensure file is formatted as standard CSV or Excel-exported CSV.</p>
<input type="file" id="excel_file_input" accept=".csv, .txt" className="form-control form-control-sm w-50 mx-auto mb-3" />
<button
className="btn btn-success-custom"
onClick={async () => {
const fileInput = document.getElementById('excel_file_input');
if (!fileInput.files || fileInput.files.length === 0) {
alert('Please select a CSV file first.');
return;
}
const formData = new FormData();
formData.append('store_id', store.store_id);
formData.append('import_file', fileInput.files[0]);
try {
const res = await fetch(API_BASE + 'stock/import-excel', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.ok) {
alert(data.message);
fileInput.value = '';
} else {
alert(data.error || 'Import failed.');
}
} catch (e) {
alert('Import error: ' + e.message);
}
}}
>
<i className="bi bi-upload me-1"></i> Import Inventory into {store.store_name}
</button>
</div>
</div>
)}
{subTab === 'suppliers' && (
<div className="counter-card">
<div className="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<h5 className="fw-bold mb-0 text-dark">
<i className="bi bi-truck text-primary me-1"></i> Approved Drug Distributors &amp; Suppliers
</h5>
<small className="text-muted">Manage licensed pharmaceutical distributors, DL Form 20B/21B, GSTIN, and credit terms</small>
</div>
<div className="d-flex gap-2">
<div className="input-group input-group-sm" style={{ width: 250 }}>
<span className="input-group-text bg-white border-end-0"><i className="bi bi-search text-muted"></i></span>
<input
type="text"
className="form-control border-start-0"
placeholder="Search distributor, GST..."
value={supplierSearch}
onChange={(e) => setSupplierSearch(e.target.value)}
/>
{supplierSearch && (
<button className="btn btn-outline-secondary" type="button" onClick={() => setSupplierSearch('')}>
<i className="bi bi-x"></i>
</button>
)}
</div>
<button className="btn btn-primary btn-sm px-3" onClick={openAddSupplier}>
<i className="bi bi-plus-circle me-1"></i> Add New Supplier
</button>
</div>
</div>
<div className="table-responsive">
<table className="table table-pos align-middle mb-0">
<thead>
<tr>
<th style={{ width: 45 }}>#</th>
<th>Distributor Name</th>
<th>DL Form 20B / 21B</th>
<th>GSTIN &amp; PAN</th>
<th>Phone / Contact</th>
<th>Credit Terms</th>
<th>Address / Bank</th>
<th style={{ width: 90 }} className="text-end pe-3">Actions</th>
</tr>
</thead>
<tbody>
{filteredSuppliers.length === 0 ? (
<tr>
<td colSpan="8" className="text-center py-4 text-muted">
<i className="bi bi-truck fs-2 d-block mb-1 text-secondary"></i>
{suppliers.length === 0
? 'No distributors registered yet. Click "Add New Supplier" to create one.'
: 'No suppliers match your search query.'}
</td>
</tr>
) : (
filteredSuppliers.map((s, i) => (
<tr key={s.supplier_id}>
<td className="fw-semibold text-secondary">{i + 1}</td>
<td>
<div className="fw-bold text-dark fs-6">{s.supplier_name}</div>
{s.email && <small className="text-muted"><i className="bi bi-envelope me-1"></i>{s.email}</small>}
</td>
<td>
<div className="small"><strong>20B:</strong> {s.dl_no_20b || <span className="text-muted">N/A</span>}</div>
<div className="small"><strong>21B:</strong> {s.dl_no_21b || <span className="text-muted">N/A</span>}</div>
</td>
<td>
<div><code className="text-primary fw-bold">{s.gstin || 'N/A'}</code></div>
{s.pan_no && <small className="text-muted d-block">PAN: {s.pan_no}</small>}
</td>
<td>
<div className="fw-semibold text-dark">{s.phone || 'N/A'}</div>
{s.contact_person && <small className="text-muted"><i className="bi bi-person me-1"></i>{s.contact_person}</small>}
</td>
<td>
<span className="badge bg-light text-dark border px-2 py-1">
<i className="bi bi-calendar-check me-1 text-primary"></i>{s.credit_days || 30} Days
</span>
</td>
<td style={{ maxWidth: 220 }}>
{s.address && <div className="small text-truncate" title={s.address}><i className="bi bi-geo-alt me-1 text-muted"></i>{s.address}</div>}
{s.bank_details && <div className="small text-muted text-truncate" title={s.bank_details}><i className="bi bi-bank me-1"></i>{s.bank_details}</div>}
{!s.address && !s.bank_details && <span className="text-muted small">-</span>}
</td>
<td className="text-end pe-3">
<div className="btn-group btn-group-sm">
<button
className="btn btn-outline-primary py-0.5 px-2"
title="Edit Supplier"
onClick={() => openEditSupplier(s)}
>
<i className="bi bi-pencil-square"></i>
</button>
<button
className="btn btn-outline-danger py-0.5 px-2"
title="Remove Supplier"
onClick={() => handleDeleteSupplier(s.supplier_id, s.supplier_name)}
>
<i className="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
{/* ================================================================= */}
{/* SUB-TAB: SUPPLIER LEDGER & PAYMENTS */}
{/* ================================================================= */}
{subTab === 'ledger' && (
<div className="counter-card">
{/* KPI Cards Row */}
<div className="row g-3 mb-4">
<div className="col-md-3">
<div className="card border-0 shadow-sm p-3 bg-white h-100 border-start border-4 border-primary">
<div className="d-flex justify-content-between align-items-center">
<div>
<small className="text-muted fw-semibold text-uppercase" style={{ fontSize: '11px' }}>Total Inward Purchases</small>
<h4 className="fw-bold text-dark mb-0 mt-1">₹{Number(ledgerOverall.total_purchases || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</h4>
</div>
<div className="bg-primary-subtle text-primary p-2.5 rounded-circle">
<i className="bi bi-box-arrow-in-down fs-4"></i>
</div>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card border-0 shadow-sm p-3 bg-white h-100 border-start border-4 border-success">
<div className="d-flex justify-content-between align-items-center">
<div>
<small className="text-muted fw-semibold text-uppercase" style={{ fontSize: '11px' }}>Total Payments Settled</small>
<h4 className="fw-bold text-success mb-0 mt-1">₹{Number(ledgerOverall.total_paid || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</h4>
</div>
<div className="bg-success-subtle text-success p-2.5 rounded-circle">
<i className="bi bi-check2-circle fs-4"></i>
</div>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card border-0 shadow-sm p-3 bg-white h-100 border-start border-4 border-danger">
<div className="d-flex justify-content-between align-items-center">
<div>
<small className="text-muted fw-semibold text-uppercase" style={{ fontSize: '11px' }}>Balance Outstanding</small>
<h4 className="fw-bold text-danger mb-0 mt-1">₹{Number(ledgerOverall.outstanding_balance || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</h4>
</div>
<div className="bg-danger-subtle text-danger p-2.5 rounded-circle">
<i className="bi bi-exclamation-octagon fs-4"></i>
</div>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card border-0 shadow-sm p-3 bg-white h-100 border-start border-4 border-warning">
<div className="d-flex justify-content-between align-items-center">
<div>
<small className="text-muted fw-semibold text-uppercase" style={{ fontSize: '11px' }}>Pending Invoices</small>
<h4 className="fw-bold text-warning-emphasis mb-0 mt-1">{ledgerOverall.total_pending_bills || 0} Bills</h4>
</div>
<div className="bg-warning-subtle text-warning-emphasis p-2.5 rounded-circle">
<i className="bi bi-receipt fs-4"></i>
</div>
</div>
</div>
</div>
</div>
{/* Filter & Action Toolbar */}
<div className="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3 bg-light p-2.5 rounded-3 border">
<div className="d-flex align-items-center gap-2 flex-grow-1">
<label className="form-label small fw-bold mb-0 text-nowrap"><i className="bi bi-funnel me-1"></i>Select Supplier:</label>
<select
className="form-select form-select-sm"
style={{ maxWidth: 320 }}
value={activeLedgerSupplierId}
onChange={(e) => {
const val = e.target.value;
setActiveLedgerSupplierId(val);
if (val) fetchSupplierLedger(val);
else setActiveLedgerDetail(null);
}}
>
<option value="">-- All Suppliers Summary --</option>
{suppliers.map(s => (
<option key={s.supplier_id} value={s.supplier_id}>
{s.supplier_name}
</option>
))}
</select>
{activeLedgerSupplierId && (
<button className="btn btn-outline-secondary btn-sm" onClick={() => { setActiveLedgerSupplierId(''); setActiveLedgerDetail(null); }}>
<i className="bi bi-x-circle me-1"></i> View All
</button>
)}
</div>
<div className="d-flex gap-2">
<button className="btn btn-success btn-sm shadow-sm fw-semibold" onClick={() => openPaymentModal(activeLedgerSupplierId)}>
<i className="bi bi-cash-stack me-1"></i> Record Supplier Payment
</button>
<button className="btn btn-outline-primary btn-sm" onClick={() => { fetchLedgerSummary(); if (activeLedgerSupplierId) fetchSupplierLedger(activeLedgerSupplierId); }} title="Refresh Ledger Data">
<i className="bi bi-arrow-clockwise"></i>
</button>
</div>
</div>
{/* VIEW A: Summary Table of All Suppliers */}
{!activeLedgerSupplierId && (
<div className="table-responsive shadow-sm rounded border">
<table className="table table-hover align-middle mb-0">
<thead className="table-light">
<tr>
<th style={{ width: 45 }}>#</th>
<th>Distributor / Agency</th>
<th>GSTIN &amp; Phone</th>
<th className="text-end">Total Purchases</th>
<th className="text-end">Total Paid</th>
<th className="text-end">Outstanding Due</th>
<th className="text-center">Pending Bills</th>
<th className="text-end pe-3">Actions</th>
</tr>
</thead>
<tbody>
{isLoadingLedger ? (
<tr><td colSpan="8" className="text-center py-4"><span className="spinner-border spinner-border-sm me-2"></span>Loading ledger summary...</td></tr>
) : ledgerSummary.length === 0 ? (
<tr><td colSpan="8" className="text-center py-4 text-muted">No suppliers found.</td></tr>
) : (
ledgerSummary.map((sup, idx) => (
<tr key={sup.supplier_id}>
<td className="text-muted fw-semibold">{idx + 1}</td>
<td>
<div className="fw-bold text-dark">{sup.supplier_name}</div>
{sup.contact_person && <small className="text-muted"><i className="bi bi-person me-1"></i>{sup.contact_person}</small>}
</td>
<td>
<div><code className="text-primary fw-semibold">{sup.gstin || 'No GSTIN'}</code></div>
{sup.phone && <small className="text-muted"><i className="bi bi-telephone me-1"></i>{sup.phone}</small>}
</td>
<td className="text-end fw-semibold">₹{Number(sup.total_purchases || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}</td>
<td className="text-end text-success fw-semibold">₹{Number(sup.total_paid || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}</td>
<td className="text-end">
<span className={`badge ${sup.outstanding_balance > 0 ? 'bg-danger-subtle text-danger border border-danger-subtle fs-6' : 'bg-success-subtle text-success border border-success-subtle'} px-2.5 py-1.5`}>
₹{Number(sup.outstanding_balance || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}
</span>
</td>
<td className="text-center">
{sup.pending_invoices > 0 ? (
<span className="badge bg-warning text-dark border border-warning px-2 py-1">{sup.pending_invoices} Unpaid</span>
) : (
<span className="badge bg-light text-muted border px-2 py-1">All Settled</span>
)}
</td>
<td className="text-end pe-3">
<div className="btn-group btn-group-sm">
<button
className="btn btn-outline-primary"
onClick={() => {
setActiveLedgerSupplierId(String(sup.supplier_id));
fetchSupplierLedger(sup.supplier_id);
}}
title="View detailed ledger statement"
>
<i className="bi bi-journal-text me-1"></i> Statement
</button>
<button
className="btn btn-outline-success"
onClick={() => openPaymentModal(sup.supplier_id)}
title="Record payment against this supplier"
>
<i className="bi bi-credit-card me-1"></i> Pay
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
{/* VIEW B: Detailed Ledger Statement for Selected Supplier */}
{activeLedgerSupplierId && activeLedgerDetail && (
<div>
{/* Supplier Profile Banner */}
<div className="card border-0 shadow-sm p-3 mb-3 bg-white border-start border-4 border-primary">
<div className="d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<h5 className="fw-bold text-dark mb-1 d-flex align-items-center gap-2">
<i className="bi bi-building text-primary"></i>
{activeLedgerDetail.supplier.supplier_name}
</h5>
<div className="d-flex flex-wrap gap-3 text-muted small">
<span><strong>GSTIN:</strong> {activeLedgerDetail.supplier.gstin || 'N/A'}</span>
<span><strong>DL No:</strong> {activeLedgerDetail.supplier.dl_no_20b || activeLedgerDetail.supplier.dl_no_21b || 'N/A'}</span>
<span><strong>Phone:</strong> {activeLedgerDetail.supplier.phone || 'N/A'}</span>
<span><strong>Credit Days:</strong> {activeLedgerDetail.supplier.credit_days || 30} Days</span>
</div>
</div>
<div className="text-end">
<small className="text-muted text-uppercase d-block" style={{ fontSize: '11px' }}>Net Outstanding Balance</small>
<span className={`badge ${activeLedgerDetail.summary.outstanding_balance > 0 ? 'bg-danger text-white fs-5' : 'bg-success text-white fs-5'} px-3 py-2`}>
₹{Number(activeLedgerDetail.summary.outstanding_balance || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}
</span>
</div>
</div>
</div>
{/* Section 1: Pending Unpaid / Partially Paid Invoices */}
<div className="card border-0 shadow-sm mb-3">
<div className="card-header bg-white py-2.5 px-3 d-flex justify-content-between align-items-center">
<h6 className="fw-bold text-dark mb-0">
<i className="bi bi-hourglass-split text-warning me-1"></i> Pending Invoices ({activeLedgerDetail.pending_invoices?.length || 0})
</h6>
<small className="text-muted">Invoices awaiting full or partial payment</small>
</div>
<div className="table-responsive">
<table className="table table-hover align-middle mb-0" style={{ fontSize: '13px' }}>
<thead className="table-light">
<tr>
<th>Bill / Invoice No</th>
<th>Invoice Date</th>
<th className="text-end">Bill Amount</th>
<th className="text-end">Paid Amount</th>
<th className="text-end">Pending Amount</th>
<th className="text-center">Status</th>
<th className="text-end pe-3">Action</th>
</tr>
</thead>
<tbody>
{(!activeLedgerDetail.pending_invoices || activeLedgerDetail.pending_invoices.length === 0) ? (
<tr><td colSpan="7" className="text-center py-3 text-muted"><i className="bi bi-check2-all text-success me-1"></i>All purchase bills for this supplier are fully paid.</td></tr>
) : (
activeLedgerDetail.pending_invoices.map((inv) => (
<tr key={inv.purchase_id}>
<td className="fw-bold text-primary">{inv.supplier_invoice_no}</td>
<td>{inv.invoice_date}</td>
<td className="text-end fw-semibold">₹{Number(inv.net_amount).toFixed(2)}</td>
<td className="text-end text-success">₹{Number(inv.paid_amount).toFixed(2)}</td>
<td className="text-end fw-bold text-danger">₹{Number(inv.pending_amount).toFixed(2)}</td>
<td className="text-center">
<span className={`badge ${inv.payment_status === 'partially_paid' ? 'bg-warning text-dark' : 'bg-danger'} px-2 py-1`}>
{inv.payment_status === 'partially_paid' ? 'Partially Paid' : 'Unpaid'}
</span>
</td>
<td className="text-end pe-3">
<div className="btn-group btn-group-sm">
<button
className="btn btn-outline-primary py-1 px-2 shadow-sm"
onClick={() => loadPurchaseForEdit(inv.purchase_id)}
title="Edit this Purchase Invoice"
>
<i className="bi bi-pencil-square me-1"></i> Edit
</button>
<button
className="btn btn-success py-1 px-2.5 shadow-sm fw-semibold"
onClick={() => openPaymentModal(activeLedgerDetail.supplier.supplier_id, inv.purchase_id, inv.pending_amount)}
>
<i className="bi bi-cash me-1"></i> Pay Bill
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
{/* Section 2: Chronological Running Ledger Statement */}
<div className="card border-0 shadow-sm">
<div className="card-header bg-white py-2.5 px-3 d-flex justify-content-between align-items-center">
<h6 className="fw-bold text-dark mb-0">
<i className="bi bi-journal-check text-primary me-1"></i> Complete Ledger Statement &amp; Running Balance
</h6>
<span className="badge bg-light text-dark border px-2 py-1">
Total Transactions: {activeLedgerDetail.statement?.length || 0}
</span>
</div>
<div className="table-responsive">
<table className="table table-bordered table-hover align-middle mb-0" style={{ fontSize: '12.5px' }}>
<thead className="table-light">
<tr>
<th style={{ width: 100 }}>Date</th>
<th style={{ width: 130 }}>Voucher / Ref</th>
<th style={{ width: 100 }}>Type</th>
<th>Particulars &amp; Details</th>
<th className="text-end" style={{ width: 120 }}>Debit (₹ Payments)</th>
<th className="text-end" style={{ width: 120 }}>Credit (₹ Inward)</th>
<th className="text-end" style={{ width: 140 }}>Balance (₹ Payable)</th>
</tr>
</thead>
<tbody>
{(!activeLedgerDetail.statement || activeLedgerDetail.statement.length === 0) ? (
<tr><td colSpan="7" className="text-center py-4 text-muted">No transactions recorded for this supplier.</td></tr>
) : (
activeLedgerDetail.statement.map((row, idx) => (
<tr key={idx} className={row.type === 'PAYMENT' ? 'table-success-subtle' : ''}>
<td className="text-nowrap">{row.date}</td>
<td className="fw-semibold text-primary">
{row.type === 'INVOICE' && row.invoice_id ? (
<button
type="button"
className="btn btn-link p-0 text-decoration-none fw-bold text-primary"
onClick={() => loadPurchaseForEdit(row.invoice_id)}
title="Click to edit purchase invoice"
>
{row.voucher_no} <i className="bi bi-pencil-square text-muted ms-1" style={{ fontSize: '11px' }}></i>
</button>
) : (
row.voucher_no
)}
</td>
<td>
<span className={`badge ${row.type === 'INVOICE' ? 'bg-primary-subtle text-primary border border-primary-subtle' : 'bg-success text-white'}`}>
{row.type === 'INVOICE' ? 'Purchase Bill' : 'Payment'}
</span>
</td>
<td>{row.particulars}</td>
<td className="text-end text-success fw-bold">
{row.debit > 0 ? ('₹' + Number(row.debit).toFixed(2)) : '-'}
</td>
<td className="text-end text-dark fw-bold">
{row.credit > 0 ? ('₹' + Number(row.credit).toFixed(2)) : '-'}
</td>
<td className="text-end fw-bold">
<span className={row.balance > 0 ? 'text-danger' : 'text-success'}>
₹{Number(row.balance).toFixed(2)}
</span>
</td>
</tr>
))
)}
</tbody>
{activeLedgerDetail.statement?.length > 0 && (
<tfoot className="table-light fw-bold">
<tr>
<td colSpan="4" className="text-end">Total Summary:</td>
<td className="text-end text-success">₹{Number(activeLedgerDetail.summary.total_paid || 0).toFixed(2)}</td>
<td className="text-end text-dark">₹{Number(activeLedgerDetail.summary.total_purchases || 0).toFixed(2)}</td>
<td className="text-end text-danger fs-6">₹{Number(activeLedgerDetail.summary.outstanding_balance || 0).toFixed(2)}</td>
</tr>
</tfoot>
)}
</table>
</div>
</div>
</div>
)}
</div>
)}
{/* ================================================================= */}
{/* SUB-TAB: SHORT ITEMS / DEFICIENCY BOOK & PO GENERATOR */}
{/* ================================================================= */}
{subTab === 'short_items' && (
<div className="counter-card">
<div className="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<h5 className="fw-bold mb-0 text-dark">
<i className="bi bi-exclamation-triangle text-warning me-1"></i> Short Items &amp; Deficiency Reorder Book
</h5>
<small className="text-muted">Real-time inventory shortage detection. Select items to generate consolidated Purchase Orders in 1-click.</small>
</div>
<div className="d-flex gap-2">
<button
className="btn btn-primary btn-sm px-3 shadow-sm fw-bold"
disabled={selectedShortItemIds.length === 0}
onClick={openCreatePoFromShortItems}
>
<i className="bi bi-cart-plus me-1"></i> Create Purchase Order ({selectedShortItemIds.length} Selected)
</button>
<button className="btn btn-outline-primary btn-sm" onClick={fetchShortItems} title="Refresh Short List">
<i className="bi bi-arrow-clockwise"></i>
</button>
</div>
</div>
{/* Short Items Filter Bar */}
<div className="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3 bg-light p-2.5 rounded-3 border">
<div className="btn-group btn-group-sm">
<button className={`btn ${shortFilter === 'all' ? 'btn-primary' : 'btn-outline-secondary'}`} onClick={() => setShortFilter('all')}>
All Shortages ({shortItems.length})
</button>
<button className={`btn ${shortFilter === 'out_of_stock' ? 'btn-danger' : 'btn-outline-secondary'}`} onClick={() => setShortFilter('out_of_stock')}>
Out of Stock ({shortItems.filter(s => s.status === 'OUT_OF_STOCK').length})
</button>
<button className={`btn ${shortFilter === 'low_stock' ? 'btn-warning text-dark' : 'btn-outline-secondary'}`} onClick={() => setShortFilter('low_stock')}>
Low Stock ({shortItems.filter(s => s.status === 'LOW_STOCK').length})
</button>
</div>
<div className="input-group input-group-sm" style={{ width: 280 }}>
<span className="input-group-text bg-white border-end-0"><i className="bi bi-search text-muted"></i></span>
<input
type="text"
className="form-control border-start-0"
placeholder="Filter short medicine..."
value={shortSearch}
onChange={(e) => setShortSearch(e.target.value)}
/>
{shortSearch && (
<button className="btn btn-outline-secondary" type="button" onClick={() => setShortSearch('')}>
<i className="bi bi-x"></i>
</button>
)}
</div>
</div>
{/* Short Items Table */}
{(() => {
const filtered = shortItems.filter(it => {
if (shortFilter === 'out_of_stock' && it.status !== 'OUT_OF_STOCK') return false;
if (shortFilter === 'low_stock' && it.status !== 'LOW_STOCK') return false;
if (shortSearch.trim()) {
const q = shortSearch.toLowerCase();
return (it.item_name && it.item_name.toLowerCase().includes(q)) ||
(it.category && it.category.toLowerCase().includes(q)) ||
(it.last_supplier_name && it.last_supplier_name.toLowerCase().includes(q));
}
return true;
});
const allSelected = filtered.length > 0 && filtered.every(it => selectedShortItemIds.includes(it.item_id));
return (
<div className="table-responsive shadow-sm rounded border">
<table className="table table-hover align-middle mb-0">
<thead className="table-light">
<tr>
<th style={{ width: 40 }} className="text-center">
<input
type="checkbox"
className="form-check-input"
checked={allSelected}
onChange={(e) => {
if (e.target.checked) {
setSelectedShortItemIds(filtered.map(it => it.item_id));
} else {
setSelectedShortItemIds([]);
}
}}
/>
</th>
<th>Medicine / Formulation</th>
<th>Stock Status</th>
<th className="text-center">Current Stock</th>
<th className="text-center">Min Reorder</th>
<th style={{ width: 110 }}>Suggested Order</th>
<th className="text-end">Estimated PTR</th>
<th>Last Supplier</th>
<th className="text-end pe-3">Action</th>
</tr>
</thead>
<tbody>
{isLoadingShortItems ? (
<tr><td colSpan="9" className="text-center py-4"><span className="spinner-border spinner-border-sm me-2"></span>Checking stock levels...</td></tr>
) : filtered.length === 0 ? (
<tr><td colSpan="9" className="text-center py-4 text-muted"><i className="bi bi-check-circle text-success fs-3 d-block mb-1"></i>No medicines currently below reorder levels. Inventory is well-stocked!</td></tr>
) : (
filtered.map((it) => {
const isSelected = selectedShortItemIds.includes(it.item_id);
return (
<tr key={it.item_id} className={isSelected ? 'table-primary-subtle' : ''}>
<td className="text-center">
<input
type="checkbox"
className="form-check-input"
checked={isSelected}
onChange={(e) => {
if (e.target.checked) {
setSelectedShortItemIds([...selectedShortItemIds, it.item_id]);
} else {
setSelectedShortItemIds(selectedShortItemIds.filter(id => id !== it.item_id));
}
}}
/>
</td>
<td>
<div className="fw-bold text-dark fs-6">{it.item_name}</div>
<div className="small text-muted d-flex gap-2 align-items-center">
<span className="badge bg-light text-dark border">{it.category || 'Tablet'}</span>
<span>{it.unit_pack || (it.units_per_pack + ' Units/Pack')}</span>
{it.drug_schedule && <span className="badge bg-danger-subtle text-danger border">{it.drug_schedule}</span>}
</div>
</td>
<td>
{it.status === 'OUT_OF_STOCK' ? (
<span className="badge bg-danger-subtle text-danger border border-danger-subtle px-2 py-1">
<i className="bi bi-x-circle me-1"></i>Out of Stock
</span>
) : (
<span className="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle px-2 py-1">
<i className="bi bi-exclamation-triangle me-1"></i>Low Stock
</span>
)}
</td>
<td className="text-center">
<span className="fw-bold fs-6 text-danger">{it.current_qty}</span>
<small className="text-muted d-block" style={{ fontSize: '11px' }}>({it.current_packs} packs)</small>
</td>
<td className="text-center text-muted fw-semibold">
{it.min_reorder_level} units
</td>
<td>
<div className="input-group input-group-sm">
<input
type="number"
className="form-control form-control-sm fw-bold text-center"
min="1"
value={it.suggested_packs}
onChange={(e) => {
const val = Math.max(1, parseInt(e.target.value) || 1);
setShortItems(shortItems.map(si => si.item_id === it.item_id ? { ...si, suggested_packs: val } : si));
}}
/>
<span className="input-group-text py-0 px-1 text-muted" style={{ fontSize: '11px' }}>Pks</span>
</div>
</td>
<td className="text-end fw-semibold">
{it.last_ptr > 0 ? ('₹' + Number(it.last_ptr).toFixed(2)) : <span className="text-muted">-</span>}
</td>
<td>
<div className="fw-semibold text-truncate" style={{ maxWidth: 180 }} title={it.last_supplier_name}>
{it.last_supplier_name}
</div>
{it.last_purchase_date && <small className="text-muted" style={{ fontSize: '11px' }}>Last: {it.last_purchase_date}</small>}
</td>
<td className="text-end pe-3">
<button
className="btn btn-sm btn-outline-primary py-1 px-2 text-nowrap"
onClick={() => {
setSelectedShortItemIds([it.item_id]);
openCreatePoFromShortItems();
}}
>
<i className="bi bi-cart-plus me-1"></i> Order
</button>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
);
})()}
</div>
)}
{/* ================================================================= */}
{/* SUB-TAB: PURCHASE ORDERS DIRECTORY */}
{/* ================================================================= */}
{subTab === 'purchase_orders' && (
<div className="counter-card">
<div className="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<h5 className="fw-bold mb-0 text-dark">
<i className="bi bi-file-earmark-text text-primary me-1"></i> Hospital Purchase Orders Directory
</h5>
<small className="text-muted">Track order status, print official supplier order formats, and load directly into inward bills.</small>
</div>
<div className="d-flex gap-2">
<button
className="btn btn-primary btn-sm px-3 shadow-sm fw-semibold"
onClick={() => {
setPoCreateForm({
supplier_id: suppliers[0]?.supplier_id ? String(suppliers[0].supplier_id) : '',
expected_delivery_date: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
remarks: '',
items: [
{ item_id: null, item_name: '', generic_name: '', category: 'Tablet', order_packs: 5, units_per_pack: 10, estimated_ptr: 0, gst_rate: 12 }
]
});
setShowPoCreateModal(true);
}}
>
<i className="bi bi-plus-circle me-1"></i> New Purchase Order
</button>
<button className="btn btn-outline-primary btn-sm" onClick={fetchPurchaseOrders} title="Refresh PO List">
<i className="bi bi-arrow-clockwise"></i>
</button>
</div>
</div>
{/* PO Status Filter Bar */}
<div className="btn-group btn-group-sm mb-3">
<button className={`btn ${poStatusFilter === 'all' ? 'btn-primary' : 'btn-outline-secondary'}`} onClick={() => { setPoStatusFilter('all'); fetchPurchaseOrders(); }}>
All Orders
</button>
<button className={`btn ${poStatusFilter === 'ordered' ? 'btn-primary' : 'btn-outline-secondary'}`} onClick={() => { setPoStatusFilter('ordered'); fetchPurchaseOrders(); }}>
Ordered / In-Transit
</button>
<button className={`btn ${poStatusFilter === 'received' ? 'btn-success' : 'btn-outline-secondary'}`} onClick={() => { setPoStatusFilter('received'); fetchPurchaseOrders(); }}>
Received / Inwarded
</button>
<button className={`btn ${poStatusFilter === 'cancelled' ? 'btn-danger' : 'btn-outline-secondary'}`} onClick={() => { setPoStatusFilter('cancelled'); fetchPurchaseOrders(); }}>
Cancelled
</button>
</div>
{/* PO Table */}
<div className="table-responsive shadow-sm rounded border">
<table className="table table-hover align-middle mb-0">
<thead className="table-light">
<tr>
<th>PO Number</th>
<th>PO Date</th>
<th>Expected Delivery</th>
<th>Supplier / Distributor</th>
<th className="text-center">Items</th>
<th className="text-end">Est. Total Amount</th>
<th className="text-center">Status</th>
<th className="text-end pe-3">Actions</th>
</tr>
</thead>
<tbody>
{isLoadingPOs ? (
<tr><td colSpan="8" className="text-center py-4"><span className="spinner-border spinner-border-sm me-2"></span>Loading purchase orders...</td></tr>
) : purchaseOrders.length === 0 ? (
<tr><td colSpan="8" className="text-center py-4 text-muted">No purchase orders found. Click "New Purchase Order" or generate from Short Items.</td></tr>
) : (
purchaseOrders.map((po) => (
<tr key={po.po_id}>
<td className="fw-bold text-primary">{po.po_number}</td>
<td>{po.po_date}</td>
<td>{po.expected_delivery_date || <span className="text-muted">-</span>}</td>
<td>
<div className="fw-semibold text-dark">{po.supplier_name}</div>
{po.phone && <small className="text-muted"><i className="bi bi-telephone me-1"></i>{po.phone}</small>}
</td>
<td className="text-center fw-bold">{po.total_items}</td>
<td className="text-end fw-bold">₹{Number(po.total_estimated_amount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}</td>
<td className="text-center">
{po.status === 'ordered' && (
<span className="badge bg-primary-subtle text-primary border border-primary-subtle px-2 py-1">
<i className="bi bi-clock me-1"></i>Ordered
</span>
)}
{po.status === 'received' && (
<span className="badge bg-success-subtle text-success border border-success-subtle px-2 py-1">
<i className="bi bi-check2-circle me-1"></i>Received
</span>
)}
{po.status === 'cancelled' && (
<span className="badge bg-secondary-subtle text-secondary border px-2 py-1">Cancelled</span>
)}
</td>
<td className="text-end pe-3">
<div className="btn-group btn-group-sm">
<button
className="btn btn-outline-primary"
onClick={() => handleViewPo(po.po_id)}
title="View & Print PO"
>
<i className="bi bi-printer me-1"></i> Print
</button>
{po.status === 'ordered' && (
<>
<button
className="btn btn-success"
onClick={() => loadPoIntoInwardBill(po.po_id)}
title="Load this PO into Inward Purchase Bill"
>
<i className="bi bi-box-arrow-in-down me-1"></i> Inward
</button>
<button
className="btn btn-outline-danger"
onClick={() => handleCancelPo(po.po_id)}
title="Cancel this PO"
>
<i className="bi bi-x"></i>
</button>
</>
)}
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
{/* Supplier Add / Edit Modal */}
{showSupplierModal && (
<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-primary text-white rounded p-1.5 d-flex align-items-center justify-content-center" style={{ width: 32, height: 32 }}>
<i className="bi bi-truck fs-5"></i>
</div>
<div>
<h6 className="modal-title mb-0 fw-bold">
{supplierForm.supplier_id ? 'Edit Supplier / Distributor Details' : 'Add New Drug Distributor & Supplier'}
</h6>
<small className="text-light opacity-75">Statutory Drug Licenses, GSTIN, contact &amp; credit terms</small>
</div>
</div>
<button type="button" className="btn-close btn-close-white" onClick={() => setShowSupplierModal(false)}></button>
</div>
<form onSubmit={handleSaveSupplier}>
<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-building me-1"></i> Basic Distributor Information
</h6>
<div className="row g-2">
<div className="col-md-8">
<label className="form-label small fw-bold mb-1">Distributor / Agency Name <span className="text-danger">*</span></label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. Apex Healthcare Distributors Pvt Ltd"
value={supplierForm.supplier_name}
onChange={(e) => setSupplierForm({ ...supplierForm, supplier_name: e.target.value })}
required
autoFocus
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">Contact Person</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. Ramesh Gupta"
value={supplierForm.contact_person}
onChange={(e) => setSupplierForm({ ...supplierForm, contact_person: e.target.value })}
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">Phone / Mobile No.</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="+91 9811002233"
value={supplierForm.phone}
onChange={(e) => setSupplierForm({ ...supplierForm, phone: e.target.value })}
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">Email Address</label>
<input
type="email"
className="form-control form-control-sm"
placeholder="sales@distributor.com"
value={supplierForm.email}
onChange={(e) => setSupplierForm({ ...supplierForm, email: e.target.value })}
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">Credit Terms</label>
<div className="input-group input-group-sm">
<input
type="number"
className="form-control form-control-sm"
min="0"
placeholder="30"
value={supplierForm.credit_days}
onChange={(e) => setSupplierForm({ ...supplierForm, credit_days: e.target.value })}
/>
<span className="input-group-text bg-light">Days</span>
</div>
</div>
</div>
</div>
<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-shield-check me-1"></i> Indian Statutory Drug Licenses &amp; GSTIN
</h6>
<div className="row g-2">
<div className="col-md-6">
<label className="form-label small fw-bold mb-1">Drug License Form 20-B (Allopathic)</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. DL-20B-DEL-98441"
value={supplierForm.dl_no_20b}
onChange={(e) => setSupplierForm({ ...supplierForm, dl_no_20b: e.target.value })}
/>
</div>
<div className="col-md-6">
<label className="form-label small fw-bold mb-1">Drug License Form 21-B (Schedule C &amp; C1)</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. DL-21B-DEL-98442"
value={supplierForm.dl_no_21b}
onChange={(e) => setSupplierForm({ ...supplierForm, dl_no_21b: e.target.value })}
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">GSTIN (15-Digit)</label>
<input
type="text"
className="form-control form-control-sm text-uppercase"
maxLength="15"
placeholder="07AACCA1234F1Z9"
value={supplierForm.gstin}
onChange={(e) => setSupplierForm({ ...supplierForm, gstin: e.target.value.toUpperCase() })}
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">PAN No. (10-Digit)</label>
<input
type="text"
className="form-control form-control-sm text-uppercase"
maxLength="10"
placeholder="AACCA1234F"
value={supplierForm.pan_no}
onChange={(e) => setSupplierForm({ ...supplierForm, pan_no: e.target.value.toUpperCase() })}
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">State Code (GST)</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="07 (Delhi)"
value={supplierForm.state_code}
onChange={(e) => setSupplierForm({ ...supplierForm, state_code: 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-geo-alt me-1"></i> Address &amp; Banking Details
</h6>
<div className="row g-2">
<div className="col-md-6">
<label className="form-label small fw-bold mb-1">Full Postal Address</label>
<textarea
className="form-control form-control-sm"
rows="2"
placeholder="Shop / Office No, Market, City, Pincode"
value={supplierForm.address}
onChange={(e) => setSupplierForm({ ...supplierForm, address: e.target.value })}
></textarea>
</div>
<div className="col-md-6">
<label className="form-label small fw-bold mb-1">Bank Account &amp; Settlement Details</label>
<textarea
className="form-control form-control-sm"
rows="2"
placeholder="Bank Name, A/c Number, IFSC Code, UPI ID"
value={supplierForm.bank_details}
onChange={(e) => setSupplierForm({ ...supplierForm, bank_details: e.target.value })}
></textarea>
</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={() => setShowSupplierModal(false)}
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary btn-sm px-4"
disabled={isSavingSupplier}
>
{isSavingSupplier ? (
<>
<span className="spinner-border spinner-border-sm me-1" role="status"></span>
Saving...
</>
) : (
<>
<i className="bi bi-check2-circle me-1"></i>
{supplierForm.supplier_id ? 'Update Supplier' : 'Save Supplier'}
</>
)}
</button>
</div>
</form>
</div>
</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 &amp; 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 &amp; 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 &amp; 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) })}
>
<option value="0">0% (Nil / Exempt)</option>
<option value="5">5% (Common / Basic Services)</option>
<option value="18">18% (Standard - Most Products)</option>
<option value="40">40% (Demerit / Sin Tax)</option>
<option value="12">12% (Legacy Medicine Slab)</option>
<option value="28">28% (Legacy High Slab)</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 &amp; Select into Bill
</>
)}
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* ================================================================= */}
{/* MODAL: RECORD SUPPLIER PAYMENT */}
{/* ================================================================= */}
{showPaymentModal && (
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(15, 23, 42, 0.75)' }} tabIndex="-1">
<div className="modal-dialog modal-md modal-dialog-centered">
<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-cash-coin fs-5"></i>
</div>
<div>
<h6 className="modal-title mb-0 fw-bold">Record Supplier Payment Voucher</h6>
<small className="text-light opacity-75">Outflow accounting against invoice or on-account</small>
</div>
</div>
<button type="button" className="btn-close btn-close-white" onClick={() => setShowPaymentModal(false)}></button>
</div>
<form onSubmit={handleRecordPayment}>
<div className="modal-body p-3 bg-light">
<div className="card border-0 shadow-sm p-3 mb-3 bg-white">
<div className="row g-2">
<div className="col-12">
<label className="form-label small fw-bold mb-1">Supplier / Distributor <span className="text-danger">*</span></label>
<select
className="form-select form-select-sm fw-semibold"
value={paymentForm.supplier_id}
onChange={(e) => setPaymentForm({ ...paymentForm, supplier_id: e.target.value })}
required
>
<option value="">-- Choose Supplier --</option>
{suppliers.map(s => (
<option key={s.supplier_id} value={s.supplier_id}>
{s.supplier_name} (GST: {s.gstin || 'N/A'})
</option>
))}
</select>
</div>
<div className="col-md-6">
<label className="form-label small fw-bold mb-1">Payment Amount (₹) <span className="text-danger">*</span></label>
<input
type="number"
step="0.01"
min="0.01"
className="form-control form-control-sm fw-bold text-success fs-6"
placeholder="0.00"
value={paymentForm.amount}
onChange={(e) => setPaymentForm({ ...paymentForm, amount: e.target.value })}
required
autoFocus
/>
</div>
<div className="col-md-6">
<label className="form-label small fw-bold mb-1">Payment Mode</label>
<select
className="form-select form-select-sm"
value={paymentForm.payment_mode}
onChange={(e) => setPaymentForm({ ...paymentForm, payment_mode: e.target.value })}
>
<option value="Bank">Bank Transfer (NEFT/RTGS)</option>
<option value="UPI">UPI / QR Code</option>
<option value="Cash">Cash in Hand</option>
<option value="Cheque">Bank Cheque</option>
</select>
</div>
<div className="col-md-6">
<label className="form-label small fw-bold mb-1">Payment Date</label>
<input
type="date"
className="form-control form-control-sm"
value={paymentForm.payment_date}
onChange={(e) => setPaymentForm({ ...paymentForm, payment_date: e.target.value })}
required
/>
</div>
<div className="col-md-6">
<label className="form-label small fw-bold mb-1">UTR / Cheque / Ref No</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. UTR-9988112"
value={paymentForm.reference_no}
onChange={(e) => setPaymentForm({ ...paymentForm, reference_no: e.target.value })}
/>
</div>
<div className="col-12">
<label className="form-label small fw-bold mb-1">Bank Name / Account Details</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. HDFC Bank - Main Current A/c"
value={paymentForm.bank_name}
onChange={(e) => setPaymentForm({ ...paymentForm, bank_name: e.target.value })}
/>
</div>
<div className="col-12">
<label className="form-label small fw-bold mb-1">Remarks / Settlement Note</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. Cleared bill #APX-9941"
value={paymentForm.remarks}
onChange={(e) => setPaymentForm({ ...paymentForm, remarks: 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={() => setShowPaymentModal(false)}>
Cancel
</button>
<button type="submit" className="btn btn-success btn-sm fw-bold px-4 shadow-sm" disabled={isRecordingPayment}>
{isRecordingPayment ? (
<>
<span className="spinner-border spinner-border-sm me-1"></span>
Recording Voucher...
</>
) : (
<>
<i className="bi bi-check2-circle me-1"></i>
Record Payment Voucher
</>
)}
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* ================================================================= */}
{/* MODAL: CREATE PURCHASE ORDER */}
{/* ================================================================= */}
{showPoCreateModal && (
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(15, 23, 42, 0.75)' }} tabIndex="-1">
<div className="modal-dialog modal-xl 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-primary text-white rounded p-1.5 d-flex align-items-center justify-content-center" style={{ width: 32, height: 32 }}>
<i className="bi bi-cart-check fs-5"></i>
</div>
<div>
<h6 className="modal-title mb-0 fw-bold">Generate New Purchase Order (PO)</h6>
<small className="text-light opacity-75">Order requisition for pharmaceutical distributor</small>
</div>
</div>
<button type="button" className="btn-close btn-close-white" onClick={() => setShowPoCreateModal(false)}></button>
</div>
<form onSubmit={handleSavePurchaseOrder}>
<div className="modal-body p-3 bg-light">
{/* Header Info */}
<div className="card border-0 shadow-sm p-3 mb-3 bg-white">
<div className="row g-2">
<div className="col-md-5">
<label className="form-label small fw-bold mb-1">Target Supplier / Agency <span className="text-danger">*</span></label>
<select
className="form-select form-select-sm fw-semibold"
value={poCreateForm.supplier_id}
onChange={(e) => setPoCreateForm({ ...poCreateForm, supplier_id: e.target.value })}
required
>
<option value="">-- Choose Supplier --</option>
{suppliers.map(s => (
<option key={s.supplier_id} value={s.supplier_id}>
{s.supplier_name} (GST: {s.gstin || 'N/A'})
</option>
))}
</select>
</div>
<div className="col-md-3">
<label className="form-label small fw-bold mb-1">Expected Delivery Date</label>
<input
type="date"
className="form-control form-control-sm"
value={poCreateForm.expected_delivery_date}
onChange={(e) => setPoCreateForm({ ...poCreateForm, expected_delivery_date: e.target.value })}
required
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">Remarks / Order Instructions</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. Urgent stock replenishment"
value={poCreateForm.remarks}
onChange={(e) => setPoCreateForm({ ...poCreateForm, remarks: e.target.value })}
/>
</div>
</div>
</div>
{/* Order Items Table */}
<div className="card border-0 shadow-sm p-3 bg-white">
<div className="d-flex justify-content-between align-items-center mb-2">
<h6 className="fw-bold text-primary mb-0" style={{ fontSize: '13px' }}>
<i className="bi bi-box-seam me-1"></i> Ordered Items ({poCreateForm.items.length})
</h6>
<button
type="button"
className="btn btn-outline-primary btn-sm py-0.5 px-2"
onClick={() => {
setPoCreateForm({
...poCreateForm,
items: [
...poCreateForm.items,
{ item_id: null, item_name: '', generic_name: '', category: 'Tablet', order_packs: 1, units_per_pack: 10, estimated_ptr: 0, gst_rate: 12 }
]
});
}}
>
<i className="bi bi-plus me-1"></i> Add Another Medicine
</button>
</div>
<div className="table-responsive">
<table className="table table-bordered align-middle mb-0" style={{ fontSize: '12.5px' }}>
<thead className="table-light">
<tr>
<th style={{ minWidth: 240 }}>Medicine Name</th>
<th style={{ width: 100 }}>Category</th>
<th style={{ width: 90 }}>Packs</th>
<th style={{ width: 90 }}>Units/Pack</th>
<th style={{ width: 100 }}>Est. PTR (₹)</th>
<th style={{ width: 85 }}>GST%</th>
<th style={{ width: 120 }} className="text-end">Est. Total (₹)</th>
<th style={{ width: 40 }}></th>
</tr>
</thead>
<tbody>
{poCreateForm.items.map((it, idx) => {
const lineTotal = Number(it.order_packs || 0) * Number(it.estimated_ptr || 0) * (1 + (Number(it.gst_rate || 0) / 100));
return (
<tr key={idx}>
<td>
<input
type="text"
className="form-control form-control-sm fw-semibold"
placeholder="Medicine name..."
value={it.item_name}
onChange={(e) => {
const items = [...poCreateForm.items];
items[idx].item_name = e.target.value;
setPoCreateForm({ ...poCreateForm, items });
}}
required
/>
</td>
<td>
<input
type="text"
className="form-control form-control-sm"
value={it.category}
onChange={(e) => {
const items = [...poCreateForm.items];
items[idx].category = e.target.value;
setPoCreateForm({ ...poCreateForm, items });
}}
/>
</td>
<td>
<input
type="number"
min="1"
className="form-control form-control-sm text-center fw-bold"
value={it.order_packs}
onChange={(e) => {
const items = [...poCreateForm.items];
items[idx].order_packs = Math.max(1, parseInt(e.target.value) || 1);
setPoCreateForm({ ...poCreateForm, items });
}}
required
/>
</td>
<td>
<input
type="number"
min="1"
className="form-control form-control-sm text-center"
value={it.units_per_pack}
onChange={(e) => {
const items = [...poCreateForm.items];
items[idx].units_per_pack = Math.max(1, parseInt(e.target.value) || 1);
setPoCreateForm({ ...poCreateForm, items });
}}
/>
</td>
<td>
<input
type="number"
step="0.01"
min="0"
className="form-control form-control-sm text-end"
value={it.estimated_ptr}
onChange={(e) => {
const items = [...poCreateForm.items];
items[idx].estimated_ptr = parseFloat(e.target.value) || 0;
setPoCreateForm({ ...poCreateForm, items });
}}
/>
</td>
<td>
<select
className="form-select form-select-sm"
value={it.gst_rate}
onChange={(e) => {
const items = [...poCreateForm.items];
items[idx].gst_rate = parseFloat(e.target.value) || 0;
setPoCreateForm({ ...poCreateForm, items });
}}
>
<option value="0">0% (Exempt)</option>
<option value="5">5%</option>
<option value="18">18%</option>
<option value="40">40%</option>
<option value="12">12% (Legacy)</option>
<option value="28">28% (Legacy)</option>
</select>
</td>
<td className="text-end fw-bold">
₹{lineTotal.toFixed(2)}
</td>
<td className="text-center">
{poCreateForm.items.length > 1 && (
<button
type="button"
className="btn btn-sm btn-outline-danger py-0 px-1"
onClick={() => {
setPoCreateForm({
...poCreateForm,
items: poCreateForm.items.filter((_, i) => i !== idx)
});
}}
>
<i className="bi bi-x"></i>
</button>
)}
</td>
</tr>
);
})}
</tbody>
<tfoot className="table-light fw-bold">
<tr>
<td colSpan="6" className="text-end">Estimated Total Amount:</td>
<td className="text-end text-primary fs-6">
₹{poCreateForm.items.reduce((sum, it) => sum + (Number(it.order_packs || 0) * Number(it.estimated_ptr || 0) * (1 + (Number(it.gst_rate || 0) / 100))), 0).toFixed(2)}
</td>
<td></td>
</tr>
</tfoot>
</table>
</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={() => setShowPoCreateModal(false)}>
Cancel
</button>
<button type="submit" className="btn btn-primary btn-sm fw-bold px-4 shadow-sm" disabled={isSavingPO}>
{isSavingPO ? (
<>
<span className="spinner-border spinner-border-sm me-1"></span>
Generating PO...
</>
) : (
<>
<i className="bi bi-check2-circle me-1"></i>
Generate Purchase Order
</>
)}
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* ================================================================= */}
{/* MODAL: VIEW & PRINT PURCHASE ORDER */}
{/* ================================================================= */}
{showPoViewModal && activeViewPo && (
<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-primary text-white rounded p-1.5 d-flex align-items-center justify-content-center" style={{ width: 32, height: 32 }}>
<i className="bi bi-printer fs-5"></i>
</div>
<div>
<h6 className="modal-title mb-0 fw-bold">Purchase Order Document: {activeViewPo.purchase_order.po_number}</h6>
<small className="text-light opacity-75">Status: <span className="text-uppercase fw-bold text-info">{activeViewPo.purchase_order.status}</span></small>
</div>
</div>
<button type="button" className="btn-close btn-close-white" onClick={() => setShowPoViewModal(false)}></button>
</div>
<div className="modal-body p-4 bg-white" id="printable-po-document">
{/* Print Header */}
<div className="d-flex justify-content-between align-items-start border-bottom pb-3 mb-3">
<div>
<h4 className="fw-bold text-primary mb-1">{store.store_name}</h4>
<div className="text-muted small">{store.building_name} | {store.room_no}</div>
<div className="text-muted small">Hospital Pharmacy &amp; Medical Supplies Dept.</div>
</div>
<div className="text-end">
<h5 className="fw-bold mb-0 text-dark">PURCHASE ORDER</h5>
<div className="badge bg-primary fs-6 mt-1">{activeViewPo.purchase_order.po_number}</div>
<div className="small text-muted mt-1">Date: <strong>{activeViewPo.purchase_order.po_date}</strong></div>
<div className="small text-muted">Delivery: <strong>{activeViewPo.purchase_order.expected_delivery_date || 'Prompt'}</strong></div>
</div>
</div>
{/* Vendor Card */}
<div className="row g-3 mb-3">
<div className="col-md-6">
<div className="p-2.5 bg-light rounded border">
<small className="text-muted text-uppercase fw-bold" style={{ fontSize: '10px' }}>Vendor / Supplier:</small>
<h6 className="fw-bold mb-1 text-dark">{activeViewPo.purchase_order.supplier_name}</h6>
{activeViewPo.purchase_order.address && <div className="small text-muted">{activeViewPo.purchase_order.address}</div>}
<div className="small text-muted">GSTIN: <strong>{activeViewPo.purchase_order.gstin || 'N/A'}</strong></div>
<div className="small text-muted">Phone: {activeViewPo.purchase_order.phone || 'N/A'}</div>
</div>
</div>
<div className="col-md-6">
<div className="p-2.5 bg-light rounded border">
<small className="text-muted text-uppercase fw-bold" style={{ fontSize: '10px' }}>Ship To / Delivery Destination:</small>
<h6 className="fw-bold mb-1 text-dark">{store.store_name}</h6>
<div className="small text-muted">{store.building_name}, {store.floor_no}</div>
<div className="small text-muted">Remarks: <em>{activeViewPo.purchase_order.remarks || 'Supply per hospital contract terms'}</em></div>
</div>
</div>
</div>
{/* Items Table */}
<div className="table-responsive mb-3">
<table className="table table-bordered table-sm align-middle mb-0" style={{ fontSize: '12px' }}>
<thead className="table-light">
<tr>
<th style={{ width: 35 }}>#</th>
<th>Item Name &amp; Description</th>
<th>Category</th>
<th className="text-center" style={{ width: 85 }}>Ordered Packs</th>
<th className="text-center" style={{ width: 85 }}>Units/Pack</th>
<th className="text-end" style={{ width: 95 }}>Est. PTR</th>
<th className="text-center" style={{ width: 65 }}>GST%</th>
<th className="text-end" style={{ width: 110 }}>Est. Amount</th>
</tr>
</thead>
<tbody>
{activeViewPo.items?.map((it, idx) => (
<tr key={idx}>
<td className="text-muted text-center">{idx + 1}</td>
<td>
<div className="fw-bold text-dark">{it.item_name}</div>
{it.generic_name && <small className="text-muted">{it.generic_name}</small>}
</td>
<td>{it.category || 'Tablet'}</td>
<td className="text-center fw-bold">{it.order_packs}</td>
<td className="text-center">{it.units_per_pack}</td>
<td className="text-end">₹{Number(it.estimated_ptr).toFixed(2)}</td>
<td className="text-center">{it.gst_rate}%</td>
<td className="text-end fw-bold">₹{Number(it.estimated_amount).toFixed(2)}</td>
</tr>
))}
</tbody>
<tfoot className="table-light fw-bold">
<tr>
<td colSpan="7" className="text-end">Total Estimated Value:</td>
<td className="text-end text-primary fs-6">₹{Number(activeViewPo.purchase_order.total_estimated_amount).toFixed(2)}</td>
</tr>
</tfoot>
</table>
</div>
{/* Signatures */}
<div className="d-flex justify-content-between align-items-end pt-4 mt-4 border-top">
<div className="text-center" style={{ width: 180 }}>
<div className="border-bottom pb-4 mb-1"></div>
<small className="text-muted fw-bold">Prepared By / Pharmacist</small>
</div>
<div className="text-center" style={{ width: 180 }}>
<div className="border-bottom pb-4 mb-1"></div>
<small className="text-muted fw-bold">Authorized Signatory</small>
</div>
</div>
</div>
<div className="modal-footer py-2 px-3 bg-light d-flex justify-content-between">
<button type="button" className="btn btn-outline-secondary btn-sm" onClick={() => setShowPoViewModal(false)}>
Close
</button>
<div className="d-flex gap-2">
{activeViewPo.purchase_order.status === 'ordered' && (
<button
type="button"
className="btn btn-success btn-sm fw-bold px-3 shadow-sm"
onClick={() => loadPoIntoInwardBill(activeViewPo.purchase_order.po_id)}
>
<i className="bi bi-box-arrow-in-down me-1"></i> Inward into Purchase Bill
</button>
)}
<button
type="button"
className="btn btn-primary btn-sm fw-bold px-3 shadow-sm"
onClick={() => window.print()}
>
<i className="bi bi-printer me-1"></i> Print PO
</button>
</div>
</div>
</div>
</div>
</div>
)}
{/* ================================================================= */}
{/* MODAL: INWARD PURCHASE BILLS HISTORY & EDIT SELECTOR */}
{/* ================================================================= */}
{showPurchaseHistoryModal && (
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(15, 23, 42, 0.75)' }} tabIndex="-1">
<div className="modal-dialog modal-xl 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-primary text-white rounded p-1.5 d-flex align-items-center justify-content-center" style={{ width: 32, height: 32 }}>
<i className="bi bi-clock-history fs-5"></i>
</div>
<div>
<h6 className="modal-title mb-0 fw-bold">Inward Purchase Bills History &amp; Edit</h6>
<small className="text-light opacity-75">Browse past invoices and click Edit to modify lines, rates, or batches</small>
</div>
</div>
<button type="button" className="btn-close btn-close-white" onClick={() => setShowPurchaseHistoryModal(false)}></button>
</div>
<div className="modal-body p-3 bg-light">
<div className="d-flex justify-content-between align-items-center gap-2 mb-3">
<div className="input-group input-group-sm" style={{ maxWidth: 350 }}>
<span className="input-group-text bg-white"><i className="bi bi-search text-muted"></i></span>
<input
type="text"
className="form-control"
placeholder="Search by Bill No or Supplier Name..."
value={purchaseHistorySearch}
onChange={(e) => setPurchaseHistorySearch(e.target.value)}
/>
{purchaseHistorySearch && (
<button className="btn btn-outline-secondary" onClick={() => setPurchaseHistorySearch('')}>
<i className="bi bi-x"></i>
</button>
)}
</div>
<button className="btn btn-outline-primary btn-sm" onClick={fetchRecentPurchases} title="Refresh Invoices">
<i className="bi bi-arrow-clockwise me-1"></i> Refresh
</button>
</div>
<div className="table-responsive bg-white rounded border shadow-sm">
<table className="table table-hover align-middle mb-0" style={{ fontSize: '13px' }}>
<thead className="table-light">
<tr>
<th style={{ width: 110 }}>Date</th>
<th>Bill / Invoice No</th>
<th>Supplier / Distributor</th>
<th className="text-center">Items</th>
<th className="text-end">Net Bill Amount</th>
<th className="text-end">Paid Amount</th>
<th className="text-center">Payment Status</th>
<th className="text-end pe-3">Action</th>
</tr>
</thead>
<tbody>
{isLoadingPurchases ? (
<tr><td colSpan="8" className="text-center py-4"><span className="spinner-border spinner-border-sm me-2"></span>Loading invoices...</td></tr>
) : recentPurchases.length === 0 ? (
<tr><td colSpan="8" className="text-center py-4 text-muted">No inward purchase bills recorded yet.</td></tr>
) : (
recentPurchases
.filter(p => {
if (!purchaseHistorySearch) return true;
const q = purchaseHistorySearch.toLowerCase();
return (p.supplier_invoice_no || '').toLowerCase().includes(q) || (p.supplier_name || '').toLowerCase().includes(q);
})
.map(p => (
<tr key={p.purchase_id}>
<td>{p.invoice_date}</td>
<td className="fw-bold text-primary">{p.supplier_invoice_no}</td>
<td>
<div className="fw-semibold text-dark">{p.supplier_name || 'Unknown Supplier'}</div>
{p.gstin && <small className="text-muted">GST: {p.gstin}</small>}
</td>
<td className="text-center">
<span className="badge bg-light text-dark border px-2 py-1">{p.items_count || 1} lines</span>
</td>
<td className="text-end fw-bold text-dark">₹{Number(p.net_amount || 0).toFixed(2)}</td>
<td className="text-end text-success">₹{Number(p.paid_amount || 0).toFixed(2)}</td>
<td className="text-center">
<span className={`badge ${p.payment_status === 'paid' ? 'bg-success' : p.payment_status === 'partially_paid' ? 'bg-warning text-dark' : 'bg-danger'} px-2 py-1`}>
{p.payment_status === 'paid' ? 'Paid' : p.payment_status === 'partially_paid' ? 'Partially Paid' : 'Unpaid'}
</span>
</td>
<td className="text-end pe-3">
<button
className="btn btn-sm btn-outline-primary py-1 px-2.5 shadow-sm fw-semibold"
onClick={() => loadPurchaseForEdit(p.purchase_id)}
title="Load this bill into Inward form to edit items and rates"
>
<i className="bi bi-pencil-square me-1"></i> Edit Invoice
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
<div className="modal-footer py-2 bg-light">
<button type="button" className="btn btn-secondary btn-sm" onClick={() => setShowPurchaseHistoryModal(false)}>Close</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}
// =========================================================================
// 4. INTER-STORE INDENTS & TRANSFERS
// =========================================================================
function TransferView({ store, stores, notify }) {
if (!store) return null;
const [toStoreId, setToStoreId] = useState('');
const [remarks, setRemarks] = useState('');
const [transferItems, setTransferItems] = useState([
{ item_name: '', qty: 10 }
]);
const addTransferRow = () => {
setTransferItems([...transferItems, { item_name: '', qty: 10 }]);
};
const submitIndent = async () => {
if (!toStoreId) {
notify('Please select destination store counter.', 'error');
return;
}
try {
const res = await fetch(API_BASE + 'transfer/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
from_store_id: store.store_id,
to_store_id: Number(toStoreId),
remarks: remarks,
items: transferItems.map(it => ({ item_id: 1, qty: Number(it.qty) }))
})
});
const data = await res.json();
if (data.status) {
notify(`Store Indent ${data.transfer_no} submitted to Central Pharmacy!`, 'success');
setRemarks('');
} else {
notify(data.message || 'Failed to submit indent.', 'error');
}
} catch (e) {
notify('Indent error: ' + e.message, 'error');
}
};
return (
<div className="counter-card">
<h5 className="fw-bold mb-3"><i className="bi bi-arrow-left-right text-primary me-1"></i> Multi-Building Store Requisition (Indent)</h5>
<p className="text-muted small">
Building counters can requisition medicines from the Main Hospital Central Store. Once dispatched and physically verified, stock transfers automatically.
</p>
<div className="row g-2 mb-3">
<div className="col-md-6">
<label className="form-label small fw-bold">Source Store (From)</label>
<input type="text" className="form-control form-control-sm" value={store.store_name} disabled />
</div>
<div className="col-md-6">
<label className="form-label small fw-bold">Target Counter / Building (To)</label>
<select className="form-select form-select-sm" value={toStoreId} onChange={(e) => setToStoreId(e.target.value)}>
<option value="">-- Select Destination Store --</option>
{stores.filter(s => s.store_id !== store.store_id).map(s => (
<option key={s.store_id} value={s.store_id}>
{s.store_name} ({s.store_slug})
</option>
))}
</select>
</div>
</div>
<div className="mb-3">
<label className="form-label small fw-bold">Requested Medicines</label>
{transferItems.map((ti, i) => (
<div key={i} className="row g-2 mb-2">
<div className="col-8">
<input
type="text"
className="form-control form-control-sm"
placeholder="Medicine Name"
value={ti.item_name}
onChange={(e) => {
const updated = [...transferItems];
updated[i].item_name = e.target.value;
setTransferItems(updated);
}}
/>
</div>
<div className="col-4">
<input
type="number"
className="form-control form-control-sm"
placeholder="Quantity Required"
min="1"
value={ti.qty}
onChange={(e) => {
const updated = [...transferItems];
updated[i].qty = e.target.value;
setTransferItems(updated);
}}
/>
</div>
</div>
))}
<button className="btn btn-sm btn-outline-primary mt-1" onClick={addTransferRow}>
<i className="bi bi-plus-circle me-1"></i> Add Another Medicine
</button>
</div>
<button className="btn btn-primary-custom" onClick={submitIndent}>
<i className="bi bi-send me-1"></i> Submit Store Indent Requisition
</button>
</div>
);
}
// =========================================================================
// 5. ACCOUNTING, LEDGERS, DAYBOOK & GST REPORTS
// =========================================================================
function LedgersView({ store, notify }) {
if (!store) return null;
const [accTab, setAccTab] = useState('daybook');
const [daybookData, setDaybookData] = useState(null);
const [bankAuditData, setBankAuditData] = useState(null);
const [auditFilterStatus, setAuditFilterStatus] = useState('all'); // 'all', 'pending', 'reconciled'
const [auditSearch, setAuditSearch] = useState('');
const [reconcilingSale, setReconcilingSale] = useState(null);
const [auditNotes, setAuditNotes] = useState('');
const [auditorName, setAuditorName] = useState('Accountant');
const [isSubmittingAudit, setIsSubmittingAudit] = useState(false);
const [gstData, setGstData] = useState(null);
const [h1Records, setH1Records] = useState([]);
useEffect(() => {
if (!store?.store_id) return;
if (accTab === 'daybook') loadDaybook();
if (accTab === 'bank_audit') loadBankAudit(auditFilterStatus, auditSearch);
if (accTab === 'gst') loadGst();
if (accTab === 'schedule_h1') loadScheduleH1();
}, [accTab, store?.store_id, auditFilterStatus]);
const loadDaybook = async () => {
try {
const res = await fetch(API_BASE + `accounting/daybook?store_id=${store.store_id}`);
const data = await res.json();
if (data.status) setDaybookData(data);
} catch (e) {
console.error(e);
}
};
const loadBankAudit = async (status = auditFilterStatus, search = auditSearch) => {
try {
const params = new URLSearchParams({
store_id: store.store_id,
status: status,
search: search
});
const res = await fetch(API_BASE + `accounting/bank-reconciliation?${params.toString()}`);
const data = await res.json();
if (data.status) setBankAuditData(data);
} catch (e) {
console.error(e);
}
};
const handleReconcileSubmit = async (saleId, isReconciled) => {
setIsSubmittingAudit(true);
try {
const res = await fetch(API_BASE + 'accounting/reconcile-bank', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sale_id: saleId,
is_reconciled: isReconciled ? 1 : 0,
reconciled_notes: auditNotes,
reconciled_by: auditorName
})
});
const data = await res.json();
if (data.status) {
notify(data.message, 'success');
setReconcilingSale(null);
setAuditNotes('');
loadBankAudit(auditFilterStatus, auditSearch);
} else {
notify(data.message || 'Failed to update reconciliation', 'error');
}
} catch (e) {
notify('Network error updating audit status: ' + e.message, 'error');
} finally {
setIsSubmittingAudit(false);
}
};
const loadGst = async () => {
try {
const res = await fetch(API_BASE + `accounting/gst-report?store_id=${store.store_id}`);
const data = await res.json();
if (data.status) setGstData(data);
} catch (e) {
console.error(e);
}
};
const loadScheduleH1 = async () => {
try {
const res = await fetch(API_BASE + `compliance/schedule-h1-register?store_id=${store.store_id}`);
const data = await res.json();
if (data.status) setH1Records(data.records || []);
} catch (e) {
console.error(e);
}
};
return (
<div>
<div className="d-flex flex-wrap gap-2 mb-3">
<button className={`btn btn-sm ${accTab === 'daybook' ? 'btn-primary' : 'btn-outline-secondary'}`} onClick={() => setAccTab('daybook')}>
<i className="bi bi-journal-check me-1"></i> Daily Cash Book / Day Book
</button>
<button className={`btn btn-sm ${accTab === 'bank_audit' ? 'btn-primary' : 'btn-outline-secondary'}`} onClick={() => setAccTab('bank_audit')}>
<i className="bi bi-bank2 me-1"></i> Bank &amp; UPI Statement Audit
</button>
<button className={`btn btn-sm ${accTab === 'gst' ? 'btn-primary' : 'btn-outline-secondary'}`} onClick={() => setAccTab('gst')}>
<i className="bi bi-file-earmark-spreadsheet me-1"></i> GST Returns (GSTR-1 &amp; 2)
</button>
<button className={`btn btn-sm ${accTab === 'schedule_h1' ? 'btn-primary' : 'btn-outline-secondary'}`} onClick={() => setAccTab('schedule_h1')}>
<i className="bi bi-shield-check me-1"></i> Statutory Schedule H1 Register
</button>
</div>
{accTab === 'daybook' && daybookData && (
<div>
<div className="row g-3 mb-3">
<div className="col-md-3 col-sm-6">
<div className="counter-card p-3 border-start border-4 border-success">
<small className="text-muted fw-bold">TOTAL TODAY SALES</small>
<h4 className="fw-bolder text-dark mt-1">₹{daybookData.summary.total_sales.toLocaleString('en-IN')}</h4>
<small className="text-muted">{daybookData.summary.total_invoices} Bills</small>
</div>
</div>
<div className="col-md-3 col-sm-6">
<div className="counter-card p-3 border-start border-4 border-primary">
<small className="text-muted fw-bold">CASH IN DRAWER</small>
<h4 className="fw-bolder text-primary mt-1">₹{daybookData.summary.cash_in_drawer.toLocaleString('en-IN')}</h4>
</div>
</div>
<div className="col-md-3 col-sm-6">
<div className="counter-card p-3 border-start border-4 border-warning">
<small className="text-muted fw-bold">UPI / QR COLLECTIONS</small>
<h4 className="fw-bolder text-warning mt-1">₹{daybookData.summary.upi_collection.toLocaleString('en-IN')}</h4>
</div>
</div>
<div className="col-md-3 col-sm-6">
<div className="counter-card p-3 border-start border-4 border-danger">
<small className="text-muted fw-bold">IPD CREDIT BILLED</small>
<h4 className="fw-bolder text-danger mt-1">₹{daybookData.summary.ipd_credit.toLocaleString('en-IN')}</h4>
</div>
</div>
</div>
<div className="counter-card p-0 overflow-hidden">
<div className="p-3 border-bottom">
<h6 className="mb-0 fw-bold">Today's Transactions Journal</h6>
</div>
<div className="table-responsive">
<table className="table table-pos mb-0">
<thead>
<tr>
<th>Invoice No</th>
<th>Time</th>
<th>Patient (UHID)</th>
<th>Doctor</th>
<th>Mode</th>
<th>CGST (₹)</th>
<th>SGST (₹)</th>
<th className="text-end">Net Amount</th>
</tr>
</thead>
<tbody>
{daybookData.transactions.map((t) => (
<tr key={t.sale_id}>
<td className="fw-bold">{t.invoice_no}</td>
<td>{t.sale_date.split(' ')[1]}</td>
<td>{t.patient_name} {t.uhid ? `(${t.uhid})` : ''}</td>
<td>{t.doctor_name}</td>
<td><span className="badge bg-secondary">{t.payment_mode}</span></td>
<td>₹{t.cgst_amount}</td>
<td>₹{t.sgst_amount}</td>
<td className="text-end fw-bold text-success">₹{t.net_amount}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{accTab === 'bank_audit' && (
<div>
{/* Summary KPI Cards */}
<div className="row g-3 mb-3">
<div className="col-md-4 col-sm-6">
<div className="counter-card p-3 border-start border-4 border-primary">
<small className="text-muted fw-bold">TOTAL ONLINE / UPI SALES</small>
<h4 className="fw-bolder text-dark mt-1">₹{Number(bankAuditData?.summary?.total_amount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}</h4>
<small className="text-muted">{bankAuditData?.summary?.total_count || 0} Transactions Recorded</small>
</div>
</div>
<div className="col-md-4 col-sm-6">
<div className="counter-card p-3 border-start border-4 border-success">
<small className="text-success fw-bold">STATEMENT RECONCILED (VERIFIED)</small>
<h4 className="fw-bolder text-success mt-1">₹{Number(bankAuditData?.summary?.reconciled_amount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}</h4>
<small className="text-success">{bankAuditData?.summary?.reconciled_count || 0} Bills Matched with Bank</small>
</div>
</div>
<div className="col-md-4 col-sm-6">
<div className="counter-card p-3 border-start border-4 border-warning">
<small className="text-warning fw-bold">PENDING AUDIT VERIFICATION</small>
<h4 className="fw-bolder text-warning mt-1">₹{Number(bankAuditData?.summary?.pending_amount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}</h4>
<small className="text-warning">{bankAuditData?.summary?.pending_count || 0} Bills Unverified</small>
</div>
</div>
</div>
{/* Filter and Search Toolbar */}
<div className="counter-card p-3 mb-3">
<div className="row g-2 align-items-center">
<div className="col-md-6">
<div className="btn-group btn-group-sm">
<button
className={`btn ${auditFilterStatus === 'all' ? 'btn-primary' : 'btn-outline-secondary'}`}
onClick={() => setAuditFilterStatus('all')}
>
All ({bankAuditData?.summary?.total_count || 0})
</button>
<button
className={`btn ${auditFilterStatus === 'pending' ? 'btn-warning text-dark fw-bold' : 'btn-outline-secondary'}`}
onClick={() => setAuditFilterStatus('pending')}
>
<i className="bi bi-hourglass-split me-1"></i> Pending Audit ({bankAuditData?.summary?.pending_count || 0})
</button>
<button
className={`btn ${auditFilterStatus === 'reconciled' ? 'btn-success' : 'btn-outline-secondary'}`}
onClick={() => setAuditFilterStatus('reconciled')}
>
<i className="bi bi-check-circle me-1"></i> Reconciled ({bankAuditData?.summary?.reconciled_count || 0})
</button>
</div>
</div>
<div className="col-md-6">
<div className="input-group input-group-sm">
<input
type="text"
className="form-control"
placeholder="Search UTR No, Bill No, Patient Name, Bank..."
value={auditSearch}
onChange={(e) => setAuditSearch(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') loadBankAudit(auditFilterStatus, auditSearch); }}
/>
<button className="btn btn-outline-secondary" onClick={() => loadBankAudit(auditFilterStatus, auditSearch)}>
<i className="bi bi-search"></i>
</button>
<button className="btn btn-outline-secondary" onClick={() => { setAuditSearch(''); loadBankAudit(auditFilterStatus, ''); }} title="Reset">
<i className="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
</div>
{/* Bank Reconciliation Table */}
<div className="counter-card p-0 overflow-hidden mb-3">
<div className="p-3 border-bottom d-flex justify-content-between align-items-center">
<h6 className="mb-0 fw-bold"><i className="bi bi-journal-text text-primary me-1"></i> Bank &amp; UPI Transaction Audit Register</h6>
<button className="btn btn-xs btn-outline-primary" onClick={() => loadBankAudit(auditFilterStatus, auditSearch)}>
<i className="bi bi-arrow-clockwise me-1"></i> Refresh
</button>
</div>
<div className="table-responsive">
<table className="table table-pos mb-0 align-middle">
<thead>
<tr>
<th>Date &amp; Time</th>
<th>Bill No</th>
<th>Patient (UHID)</th>
<th>Settlement Mode</th>
<th>Payment Breakdown</th>
<th>Bank / UPI Reference (UTR)</th>
<th className="text-end">Bank/UPI (₹)</th>
<th className="text-center">Audit Status</th>
<th className="text-end">Action</th>
</tr>
</thead>
<tbody>
{(!bankAuditData?.records || bankAuditData.records.length === 0) ? (
<tr>
<td colSpan="9" className="text-center py-4 text-muted">
No UPI / Bank transactions match this filter.
</td>
</tr>
) : (
bankAuditData.records.map((r) => {
const bankAmount = (Number(r.upi_paid) || 0) + (Number(r.card_paid) || 0);
const utrText = r.upi_ref_no || r.card_ref_no || r.payment_reference || 'N/A';
return (
<tr key={r.sale_id}>
<td style={{ fontSize: '12px' }}>{r.sale_date}</td>
<td className="fw-bold">{r.invoice_no}</td>
<td>
<div>{r.patient_name}</div>
{r.uhid && <small className="text-muted">{r.uhid}</small>}
</td>
<td>
<span className={`badge ${r.payment_mode === 'Mixed' ? 'bg-warning text-dark' : r.payment_mode === 'UPI' ? 'bg-info text-dark' : 'bg-secondary'}`}>
{r.payment_mode}
</span>
</td>
<td style={{ fontSize: '11px' }}>
{Number(r.cash_paid) > 0 && <span className="d-block text-muted">Cash: ₹{r.cash_paid}</span>}
{Number(r.upi_paid) > 0 && <span className="d-block text-primary fw-bold">UPI: ₹{r.upi_paid}</span>}
{Number(r.card_paid) > 0 && <span className="d-block text-dark">Card: ₹{r.card_paid}</span>}
</td>
<td>
<div>
<code className="text-primary fw-bold fs-7">{utrText}</code>
{utrText !== 'N/A' && (
<button
className="btn btn-xs btn-link text-decoration-none p-0 ms-1 text-muted"
title="Copy UTR"
onClick={() => {
navigator.clipboard.writeText(utrText);
notify('Copied UTR: ' + utrText, 'info');
}}
>
<i className="bi bi-clipboard"></i>
</button>
)}
</div>
{r.bank_name && <small className="text-muted d-block">{r.bank_name}</small>}
</td>
<td className="text-end fw-bolder text-primary fs-6">
₹{bankAmount.toLocaleString('en-IN', { minimumFractionDigits: 2 })}
</td>
<td className="text-center">
{r.is_bank_reconciled == 1 ? (
<div>
<span className="badge bg-success"><i className="bi bi-check-circle-fill me-1"></i> Reconciled</span>
<div style={{ fontSize: '10px' }} className="text-muted mt-0.5">
{r.reconciled_by || 'Accountant'} • {r.reconciled_at ? r.reconciled_at.split(' ')[0] : ''}
</div>
{r.reconciled_notes && (
<div style={{ fontSize: '9px' }} className="text-secondary fst-italic">
"{r.reconciled_notes}"
</div>
)}
</div>
) : (
<span className="badge bg-warning-subtle text-dark border border-warning">
<i className="bi bi-clock-history me-1"></i> Pending Audit
</span>
)}
</td>
<td className="text-end">
{r.is_bank_reconciled == 1 ? (
<button
className="btn btn-xs btn-outline-danger"
onClick={() => handleReconcileSubmit(r.sale_id, 0)}
title="Undo Reconciliation Verification"
disabled={isSubmittingAudit}
>
<i className="bi bi-arrow-counterclockwise me-1"></i> Undo
</button>
) : (
<button
className="btn btn-sm btn-success fw-bold py-1 px-2"
onClick={() => {
setReconcilingSale(r);
setAuditNotes(`Matched ₹${bankAmount} in bank statement credit on ` + new Date().toISOString().split('T')[0]);
}}
title="Verify with bank statement"
>
<i className="bi bi-check2-circle me-1"></i> Verify Entry
</button>
)}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
{/* Audit Verification Modal */}
{reconcilingSale && (
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(15, 23, 42, 0.75)' }} tabIndex="-1">
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content border-0 shadow-lg">
<div className="modal-header bg-primary text-white py-2.5 px-3">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-bank2 fs-5"></i>
<h6 className="modal-title mb-0 fw-bold">Audit &amp; Reconcile Bank Statement</h6>
</div>
<button type="button" className="btn-close btn-close-white" onClick={() => setReconcilingSale(null)}></button>
</div>
<div className="modal-body p-3">
<div className="alert alert-primary py-2 mb-3" style={{ fontSize: '0.85rem' }}>
<div className="d-flex justify-content-between">
<span>Bill No: <strong>{reconcilingSale.invoice_no}</strong></span>
<span>Date: <strong>{reconcilingSale.sale_date}</strong></span>
</div>
<div className="d-flex justify-content-between mt-1">
<span>Patient: <strong>{reconcilingSale.patient_name}</strong></span>
<span>Total Bill: <strong>₹{reconcilingSale.net_amount}</strong></span>
</div>
</div>
<div className="p-2.5 rounded bg-light border mb-3">
<label className="text-muted small fw-bold d-block mb-1">BANK STATEMENT MATCH CRITERIA</label>
<div className="d-flex justify-content-between align-items-center mb-1">
<span>Credit Amount in Bank:</span>
<span className="fs-5 fw-bold text-success">
₹{((Number(reconcilingSale.upi_paid) || 0) + (Number(reconcilingSale.card_paid) || 0)).toFixed(2)}
</span>
</div>
<div className="d-flex justify-content-between align-items-center mb-1">
<span>UPI UTR / Trans Ref:</span>
<span className="fw-bold text-primary fs-6">
<code>{reconcilingSale.upi_ref_no || reconcilingSale.card_ref_no || reconcilingSale.payment_reference || 'Not recorded'}</code>
</span>
</div>
<div className="d-flex justify-content-between align-items-center">
<span>Target Bank / A/c:</span>
<span>{reconcilingSale.bank_name || store.bank_name || 'Hospital Current A/c'}</span>
</div>
</div>
<div className="mb-2">
<label className="form-label small fw-bold mb-1">Audited By (Accountant / User)</label>
<input
type="text"
className="form-control form-control-sm"
value={auditorName}
onChange={(e) => setAuditorName(e.target.value)}
/>
</div>
<div className="mb-3">
<label className="form-label small fw-bold mb-1">Accountant Statement Audit Note</label>
<textarea
className="form-control form-control-sm"
rows="2"
value={auditNotes}
onChange={(e) => setAuditNotes(e.target.value)}
placeholder="e.g. Matched in SBI A/c statement entry on 08-Sep-2026, Credit Ref verified"
></textarea>
</div>
</div>
<div className="modal-footer py-2 px-3 bg-light">
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={() => setReconcilingSale(null)}>
Cancel
</button>
<button
type="button"
className="btn btn-sm btn-success fw-bold"
onClick={() => handleReconcileSubmit(reconcilingSale.sale_id, 1)}
disabled={isSubmittingAudit}
>
{isSubmittingAudit ? (
<span><i className="bi bi-hourglass-split me-1"></i> Saving...</span>
) : (
<span><i className="bi bi-check-circle me-1"></i> Mark Verified with Statement</span>
)}
</button>
</div>
</div>
</div>
</div>
)}
</div>
)}
{accTab === 'gst' && gstData && (
<div className="counter-card mb-3">
<h6 className="fw-bold mb-3"><i className="bi bi-file-earmark-spreadsheet text-primary me-1"></i> GSTR-1 Outward Sales Summary (HSN-wise)</h6>
<div className="table-responsive">
<table className="table table-pos mb-0">
<thead>
<tr>
<th>HSN Code</th>
<th>GST Slab</th>
<th>Taxable Value (₹)</th>
<th>CGST (₹)</th>
<th>SGST (₹)</th>
<th>IGST (₹)</th>
<th className="text-end">Total Invoice Value (₹)</th>
</tr>
</thead>
<tbody>
{gstData.gstr1_sales.map((g, i) => (
<tr key={i}>
<td><code>{g.hsn_code}</code></td>
<td><span className="badge bg-primary">{g.gst_rate}%</span></td>
<td>₹{Number(g.taxable_val).toFixed(2)}</td>
<td>₹{Number(g.cgst_val).toFixed(2)}</td>
<td>₹{Number(g.sgst_val).toFixed(2)}</td>
<td>₹{Number(g.igst_val).toFixed(2)}</td>
<td className="text-end fw-bold">₹{Number(g.total_val).toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{accTab === 'schedule_h1' && (
<div className="counter-card">
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
<h6 className="fw-bold mb-0"><i className="bi bi-shield-check text-success me-1"></i> Statutory Schedule H1 Drug Register</h6>
<small className="text-muted">Mandatory under Indian Drugs &amp; Cosmetics Rules (4th Amendment 2013). Retain records for 3 years.</small>
</div>
<button className="btn btn-sm btn-outline-dark" onClick={() => window.print()}>
<i className="bi bi-printer me-1"></i> Export / Print Register
</button>
</div>
<div className="table-responsive">
<table className="table table-pos table-bordered mb-0">
<thead>
<tr>
<th>Date</th>
<th>Bill No</th>
<th>Patient Name &amp; Address</th>
<th>Prescribing Doctor &amp; Reg No</th>
<th>Medicine Name</th>
<th>Batch No</th>
<th>Qty Supplied</th>
</tr>
</thead>
<tbody>
{h1Records.length === 0 ? (
<tr>
<td colSpan="7" className="text-center py-4 text-muted">No Schedule H1 medicines dispensed in this period.</td>
</tr>
) : (
h1Records.map((h, i) => (
<tr key={i}>
<td>{h.sale_date.split(' ')[0]}</td>
<td className="fw-bold">{h.invoice_no}</td>
<td>{h.patient_name} {h.patient_address ? `(${h.patient_address})` : ''}</td>
<td>{h.doctor_name} {h.doctor_reg_no ? `(Reg: ${h.doctor_reg_no})` : ''}</td>
<td className="fw-bold text-danger">{h.item_name}</td>
<td><code>{h.batch_no}</code></td>
<td className="fw-bold">{h.qty}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}
// Note: Store & Statutory Licensing Setup is managed centrally in HMS -> Admin -> Medical Store.
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
<App />
</ErrorBoundary>
);
</script>
</body>
</html>