devsofttech_hms_ci4/public/App/MedicalStore/index.html

9875 lines
626 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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;
}
/* Patient Pharmacy History & Records Sidebar */
.patient-history-sidebar {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 4px 18px rgba(15, 23, 42, 0.07);
overflow: hidden;
display: flex;
flex-direction: column;
}
.patient-history-header {
padding: 12px 16px;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #ffffff;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.patient-history-tabs {
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
padding: 6px 8px;
display: flex;
gap: 4px;
overflow-x: auto;
}
.patient-history-tab-btn {
font-size: 0.72rem;
font-weight: 700;
padding: 5px 8px;
border-radius: 6px;
border: 1px solid transparent;
background: transparent;
color: #64748b;
cursor: pointer;
white-space: nowrap;
transition: all 0.15s ease;
display: inline-flex;
align-items: center;
gap: 4px;
}
.patient-history-tab-btn.active {
background: #ffffff;
color: #0284c7;
border-color: #cbd5e1;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.patient-history-tab-btn:hover:not(.active) {
background: #f1f5f9;
color: #1e293b;
}
.patient-history-body {
padding: 12px;
overflow-y: auto;
flex: 1;
max-height: calc(100vh - 220px);
}
.patient-history-body::-webkit-scrollbar {
width: 5px;
}
.patient-history-body::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.history-med-card {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 8px;
transition: all 0.15s ease;
}
.history-med-card:hover {
border-color: #38bdf8;
box-shadow: 0 3px 10px rgba(56, 189, 248, 0.12);
}
.history-med-card.last-given {
background: linear-gradient(180deg, #f0fdf4 0%, #ffffff 100%);
border-color: #86efac;
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
100% { transform: rotate(360deg); }
}
.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 Landscape Invoice (210mm x 148mm) */
.print-a5-invoice {
width: 100%;
max-width: 820px;
min-height: auto;
font-family: 'Inter', sans-serif;
font-size: 10.5px;
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);
// Patient History & Old Records State (Right-Side Panel)
const [patientHistory, setPatientHistory] = useState(null);
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
const [showHistoryPanel, setShowHistoryPanel] = useState(true);
const [historyActiveTab, setHistoryActiveTab] = useState('last_given'); // 'last_given', 'all_bills', 'prescriptions', 'summary'
const [expandedBillId, setExpandedBillId] = useState(null);
// 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
}
// Fetch patient pharmacy history & old records on the right side
fetchPatientHistory(pt.patient_id, pt.uhid, pt.mobile);
setShowHistoryPanel(true);
};
const resetPatient = () => {
setSelectedPatient(null);
setPatientHistory(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));
};
const fetchPatientHistory = async (patientId, uhid, mobile) => {
if (!patientId && !uhid && !mobile) {
setPatientHistory(null);
return;
}
setIsLoadingHistory(true);
try {
const p = new URLSearchParams();
if (patientId) p.append('patient_id', patientId);
if (uhid) p.append('uhid', uhid);
if (mobile) p.append('mobile', mobile);
if (store?.store_id) p.append('store_id', store.store_id);
const res = await fetch(API_BASE + `patient/history?${p.toString()}`);
const data = await res.json();
if (data.status) {
setPatientHistory(data);
if (data.invoices && data.invoices.length > 0) {
setExpandedBillId(data.invoices[0].sale.sale_id);
}
} else {
setPatientHistory(null);
}
} catch (e) {
console.error('Error fetching patient history:', e);
setPatientHistory(null);
} finally {
setIsLoadingHistory(false);
}
};
const reDispenseMedicine = async (itemObj, defaultQty = 1) => {
try {
const searchTerm = itemObj.item_name || itemObj.med_name;
if (!searchTerm) return;
notify(`Finding current stock for "${searchTerm}"...`, 'info');
const res = await fetch(API_BASE + `items/search?store_id=${store.store_id}&q=${encodeURIComponent(searchTerm)}`);
const data = await res.json();
if (data.status && data.items && data.items.length > 0) {
let matched = null;
if (itemObj.item_id) {
matched = data.items.find(it => Number(it.item_id) === Number(itemObj.item_id));
}
if (!matched) matched = data.items[0];
if (matched && matched.fefo_batch) {
const qtyToAdd = defaultQty || (itemObj.qty ? Number(itemObj.qty) : 1);
addItemToCart(matched, matched.fefo_batch, qtyToAdd);
notify(`Added "${matched.item_name}" (Batch ${matched.fefo_batch.batch_no}) to cart!`, 'success');
} else if (matched) {
notify(`"${matched.item_name}" is currently OUT OF STOCK in this counter!`, 'error');
}
} else {
notify(`Medicine "${searchTerm}" not found in current inventory.`, 'error');
}
} catch (e) {
notify('Failed to add medicine: ' + e.message, 'error');
}
};
const initiateReturnFromHistory = (saleObj, itemObj) => {
if (!saleObj || !itemObj) return;
const inv = patientHistory?.invoices?.find(inv => Number(inv.sale.sale_id) === Number(saleObj.sale_id));
if (inv) {
selectReturnInvoice(inv);
const upp = Number(itemObj.units_per_pack) || 1;
const defUnit = (upp > 1 && itemObj.remaining_strips > 0) ? 'Strip' : 'Tab';
const defQty = defUnit === 'Strip' ? Math.max(1, itemObj.remaining_strips) : Math.max(1, itemObj.remaining_units);
setReturnDraftItems(prev => ({
...prev,
[itemObj.sale_item_id]: {
return_qty: defQty,
return_unit: defUnit,
condition: 'GOOD',
reason: 'EXCESS_UNUSED'
}
}));
openReturnModal();
notify(`Loaded bill ${saleObj.invoice_no} for return of ${itemObj.item_name}`, 'info');
} else {
setReturnSearchQuery(saleObj.invoice_no);
searchReturnInvoices(saleObj.invoice_no);
openReturnModal();
}
};
const viewPastBill = (saleId) => {
fetch(API_BASE + `sales/invoice/${saleId}`)
.then(r => r.json())
.then(data => {
if (data.status) {
setLastInvoice(data);
setIsPrintOpen(true);
} else {
notify('Unable to load invoice', 'error');
}
}).catch(err => notify('Error loading bill: ' + err.message, 'error'));
};
// 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">
{/* Workspace Column (col-lg-8 when patient history open, col-12 otherwise) */}
<div className={selectedPatient && showHistoryPanel ? "col-12 col-lg-8 col-xl-8" : "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-1.5 flex-wrap">
<button
type="button"
className={`btn btn-sm ${showHistoryPanel ? 'btn-primary text-white' : 'btn-outline-primary'} py-1 px-2.5 d-inline-flex align-items-center gap-1 shadow-sm`}
onClick={() => setShowHistoryPanel(!showHistoryPanel)}
title="Toggle Patient Old Records & Past Dispensed Medicines (Right-Side)"
style={{ fontSize: "0.75rem" }}
>
<i className="bi bi-clock-history"></i>
<span>Old Records</span>
{patientHistory && patientHistory.total_sales > 0 && (
<span className={`badge ${showHistoryPanel ? 'bg-white text-primary' : 'bg-primary text-white'} ms-1`} style={{ fontSize: "0.65rem" }}>
{patientHistory.total_sales}
</span>
)}
</button>
{(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>
{/* Right-Side Patient Pharmacy History & Records Panel */}
{selectedPatient && showHistoryPanel && (
<div className="col-12 col-lg-4 col-xl-4">
<div className="patient-history-sidebar position-sticky shadow-sm" style={{ top: '15px', maxHeight: 'calc(100vh - 100px)', display: 'flex', flexDirection: 'column' }}>
{/* Header */}
<div className="patient-history-header">
<div className="d-flex justify-content-between align-items-center mb-1.5">
<div className="d-flex align-items-center gap-2">
<i className="bi bi-clock-history text-info fs-5"></i>
<div>
<h6 className="mb-0 fw-bold text-white" style={{ fontSize: '0.9rem' }}>Patient Pharmacy History</h6>
<small className="text-secondary" style={{ fontSize: '0.72rem' }}>
UHID: <span className="text-warning fw-bold">{selectedPatient.uhid}</span>
</small>
</div>
</div>
<div className="d-flex align-items-center gap-1">
<button
type="button"
className="btn btn-xs btn-outline-light py-0.5 px-2"
onClick={() => fetchPatientHistory(selectedPatient.patient_id, selectedPatient.uhid, selectedPatient.mobile)}
disabled={isLoadingHistory}
title="Refresh Patient History"
style={{ fontSize: '0.7rem' }}
>
<i className={`bi bi-arrow-clockwise ${isLoadingHistory ? 'spin' : ''}`}></i>
</button>
<button
type="button"
className="btn btn-xs btn-outline-light py-0.5 px-2"
onClick={() => setShowHistoryPanel(false)}
title="Hide sidebar (Click 'Old Records' in patient bar to re-open)"
style={{ fontSize: '0.7rem' }}
>
<i className="bi bi-chevron-bar-right"></i>
</button>
</div>
</div>
{/* Patient Summary Sub-bar */}
<div className="d-flex flex-wrap align-items-center justify-content-between gap-1 pt-1.5 border-top border-secondary" style={{ fontSize: '0.72rem' }}>
<span className="text-light">
<strong>{selectedPatient.full_name}</strong> • {selectedPatient.gender}, {selectedPatient.age}
</span>
{patientHistory && (
<span className="badge bg-primary-subtle text-primary border border-primary px-1.5 py-0.5">
{patientHistory.total_sales || 0} Bills • ₹{patientHistory.total_spend || 0}
</span>
)}
</div>
</div>
{/* Navigation Tabs */}
<div className="patient-history-tabs">
<button
type="button"
className={`patient-history-tab-btn ${historyActiveTab === 'last_given' ? 'active' : ''}`}
onClick={() => setHistoryActiveTab('last_given')}
>
<i className="bi bi-capsule text-success"></i>
<span>Last Given</span>
{patientHistory?.last_dispensed_items?.length > 0 && (
<span className="badge bg-success text-white rounded-pill px-1" style={{ fontSize: '0.62rem' }}>
{patientHistory.last_dispensed_items.length}
</span>
)}
</button>
<button
type="button"
className={`patient-history-tab-btn ${historyActiveTab === 'all_bills' ? 'active' : ''}`}
onClick={() => setHistoryActiveTab('all_bills')}
>
<i className="bi bi-receipt text-primary"></i>
<span>Past Bills</span>
{patientHistory?.invoices?.length > 0 && (
<span className="badge bg-primary text-white rounded-pill px-1" style={{ fontSize: '0.62rem' }}>
{patientHistory.invoices.length}
</span>
)}
</button>
{patientHistory?.doctor_prescriptions?.length > 0 && (
<button
type="button"
className={`patient-history-tab-btn ${historyActiveTab === 'prescriptions' ? 'active' : ''}`}
onClick={() => setHistoryActiveTab('prescriptions')}
>
<i className="bi bi-stethoscope text-danger"></i>
<span>Doctor Rx</span>
<span className="badge bg-danger text-white rounded-pill px-1" style={{ fontSize: '0.62rem' }}>
{patientHistory.doctor_prescriptions.length}
</span>
</button>
)}
{patientHistory?.unique_medicines?.length > 0 && (
<button
type="button"
className={`patient-history-tab-btn ${historyActiveTab === 'summary' ? 'active' : ''}`}
onClick={() => setHistoryActiveTab('summary')}
>
<i className="bi bi-journal-medical text-warning"></i>
<span>All Meds</span>
<span className="badge bg-secondary text-white rounded-pill px-1" style={{ fontSize: '0.62rem' }}>
{patientHistory.unique_medicines.length}
</span>
</button>
)}
</div>
{/* Body */}
<div className="patient-history-body">
{isLoadingHistory ? (
<div className="text-center py-5 text-muted">
<div className="spinner-border spinner-border-sm text-primary me-2"></div>
Loading patient records...
</div>
) : !patientHistory || (patientHistory.total_sales === 0 && (!patientHistory.doctor_prescriptions || patientHistory.doctor_prescriptions.length === 0)) ? (
<div className="text-center py-4 px-2 text-muted">
<i className="bi bi-folder-x fs-1 text-secondary opacity-50 d-block mb-2"></i>
<h6 className="fw-semibold text-dark">No Previous Pharmacy Records</h6>
<small>This patient has no recorded medicine dispenses or bills in the pharmacy database yet.</small>
</div>
) : (
<>
{/* TAB 1: LAST GIVEN MEDICINES */}
{historyActiveTab === 'last_given' && (
<div>
{patientHistory.last_sale ? (
<>
<div className="p-2.5 mb-2 rounded-3 border" style={{ background: '#f0fdf4', borderColor: '#bbf7d0' }}>
<div className="d-flex justify-content-between align-items-center mb-1">
<span className="fw-bold text-success" style={{ fontSize: '0.78rem' }}>
<i className="bi bi-check-circle-fill me-1"></i> Last Dispensed Visit
</span>
<span className="badge bg-dark text-white" style={{ fontSize: '0.68rem' }}>
{patientHistory.last_sale.invoice_no}
</span>
</div>
<div className="text-muted" style={{ fontSize: '0.72rem' }}>
<div><i className="bi bi-calendar-event me-1"></i> {patientHistory.last_sale.sale_date ? patientHistory.last_sale.sale_date.substring(0, 16) : 'N/A'}</div>
{patientHistory.last_sale.doctor_name && (
<div><i className="bi bi-person-badge me-1"></i> Prescribed by: <strong>{patientHistory.last_sale.doctor_name}</strong></div>
)}
<div className="d-flex justify-content-between align-items-center mt-1.5 pt-1 border-top border-success-subtle">
<span>Paid: <strong>₹{patientHistory.last_sale.net_amount}</strong> ({patientHistory.last_sale.payment_mode})</span>
<button
type="button"
className="btn btn-xs btn-outline-success py-0 px-2 fw-bold"
onClick={() => viewPastBill(patientHistory.last_sale.sale_id)}
style={{ fontSize: '0.68rem' }}
>
<i className="bi bi-printer me-1"></i> View Bill
</button>
</div>
</div>
</div>
<div className="fw-bold text-dark mb-1.5 d-flex justify-content-between align-items-center" style={{ fontSize: '0.75rem' }}>
<span>Medicines Given ({patientHistory.last_dispensed_items.length}):</span>
<small className="text-muted">Click <strong>+</strong> to re-dispense</small>
</div>
{patientHistory.last_dispensed_items.map((it, mIdx) => (
<div key={`last-med-${mIdx}`} className="history-med-card last-given">
<div className="d-flex justify-content-between align-items-start mb-1">
<div>
<div className="fw-bold text-dark" style={{ fontSize: '0.82rem' }}>
{it.item_name}
</div>
<small className="text-muted d-block" style={{ fontSize: '0.68rem' }}>
{it.generic_name}
</small>
</div>
<div className="text-end">
<span className="fw-bold text-dark" style={{ fontSize: '0.82rem' }}>₹{it.total_amount}</span>
<small className="text-muted d-block" style={{ fontSize: '0.68rem' }}>₹{it.unit_mrp}/{it.sell_unit}</small>
</div>
</div>
<div className="d-flex flex-wrap align-items-center gap-1 mb-2" style={{ fontSize: '0.68rem' }}>
<span className="badge bg-light text-dark border">Batch: {it.batch_no}</span>
<span className="badge bg-light text-secondary border">Exp: {it.expiry_date}</span>
<span className="badge bg-primary text-white">Qty: {it.qty} {it.sell_unit}</span>
{it.remaining_units > 0 ? (
<span className="badge bg-success-subtle text-success border border-success">
Returnable: {it.remaining_strips > 0 ? `${it.remaining_strips} Str ` : ''}{it.remaining_loose > 0 ? `${it.remaining_loose} Tab` : ''}
</span>
) : (
<span className="badge bg-secondary-subtle text-muted border">Fully Returned</span>
)}
</div>
{/* Actions */}
<div className="d-flex gap-1.5 pt-1.5 border-top">
<button
type="button"
className="btn btn-xs btn-success py-1 px-2 fw-semibold d-inline-flex align-items-center gap-1 shadow-xs flex-fill justify-content-center"
onClick={() => reDispenseMedicine(it, it.qty)}
style={{ fontSize: '0.72rem', background: '#059669', borderColor: '#047857' }}
title="Add this medicine to active dispensing cart with current FEFO batch"
>
<i className="bi bi-plus-circle-fill"></i>
<span>Dispense Again</span>
</button>
{it.remaining_units > 0 && (
<button
type="button"
className="btn btn-xs btn-outline-danger py-1 px-2 fw-semibold d-inline-flex align-items-center gap-1 shadow-xs"
onClick={() => initiateReturnFromHistory(patientHistory.last_sale, it)}
style={{ fontSize: '0.72rem' }}
title="Return or exchange this dispensed item into current bill"
>
<i className="bi bi-arrow-return-left"></i>
<span>Return</span>
</button>
)}
</div>
</div>
))}
</>
) : (
<div className="text-center py-4 text-muted">
<i className="bi bi-basket text-secondary fs-3 d-block mb-1"></i>
No pharmacy sales recorded yet for this patient.
</div>
)}
</div>
)}
{/* TAB 2: ALL PAST BILLS */}
{historyActiveTab === 'all_bills' && (
<div>
{patientHistory.invoices?.map((inv, invIdx) => {
const s = inv.sale;
const isExp = expandedBillId === s.sale_id;
return (
<div key={`inv-${s.sale_id}`} className="card mb-2 border shadow-xs" style={{ borderRadius: '8px' }}>
<div
className="card-header py-2 px-2.5 bg-white d-flex justify-content-between align-items-center"
style={{ cursor: 'pointer', borderBottom: isExp ? '1px solid #e2e8f0' : 'none' }}
onClick={() => setExpandedBillId(isExp ? null : s.sale_id)}
>
<div>
<div className="fw-bold text-dark d-flex align-items-center gap-1.5" style={{ fontSize: '0.78rem' }}>
<i className={`bi bi-chevron-${isExp ? 'down' : 'right'} text-primary`} style={{ fontSize: '0.7rem' }}></i>
<span>{s.invoice_no}</span>
<span className="badge bg-light text-secondary border" style={{ fontSize: '0.65rem' }}>{inv.items.length} items</span>
</div>
<small className="text-muted" style={{ fontSize: '0.68rem' }}>
{s.sale_date ? s.sale_date.substring(0, 10) : ''} • Dr. {s.doctor_name || 'Consultant'}
</small>
</div>
<div className="text-end">
<span className="fw-bold text-success d-block" style={{ fontSize: '0.82rem' }}>₹{s.net_amount}</span>
<span className="badge bg-secondary-subtle text-secondary" style={{ fontSize: '0.62rem' }}>{s.payment_mode}</span>
</div>
</div>
{isExp && (
<div className="card-body p-2 bg-light">
<div className="d-flex justify-content-between align-items-center mb-1.5">
<span className="text-muted fw-semibold" style={{ fontSize: '0.7rem' }}>Items Dispensed:</span>
<div className="d-flex gap-1">
<button
type="button"
className="btn btn-xs btn-outline-dark py-0 px-1.5"
onClick={(e) => { e.stopPropagation(); viewPastBill(s.sale_id); }}
style={{ fontSize: '0.65rem' }}
>
<i className="bi bi-printer me-1"></i> Reprint
</button>
<button
type="button"
className="btn btn-xs btn-outline-danger py-0 px-1.5"
onClick={(e) => { e.stopPropagation(); selectReturnInvoice(inv); openReturnModal(); }}
style={{ fontSize: '0.65rem' }}
>
<i className="bi bi-arrow-return-left me-1"></i> Return All
</button>
</div>
</div>
{inv.items.map((it, itIdx) => (
<div key={`inv-item-${itIdx}`} className="p-1.5 mb-1 bg-white rounded border d-flex justify-content-between align-items-center" style={{ fontSize: '0.72rem' }}>
<div>
<div className="fw-bold text-dark">{it.item_name}</div>
<div className="text-muted" style={{ fontSize: '0.65rem' }}>
Batch: {it.batch_no} • {it.qty} {it.sell_unit} @ ₹{it.unit_mrp}
</div>
</div>
<div className="d-flex align-items-center gap-1">
<button
type="button"
className="btn btn-xs btn-outline-success py-0 px-1"
onClick={() => reDispenseMedicine(it, it.qty)}
title="Dispense again"
style={{ fontSize: '0.68rem' }}
>
<i className="bi bi-plus"></i>
</button>
{it.remaining_units > 0 && (
<button
type="button"
className="btn btn-xs btn-outline-danger py-0 px-1"
onClick={() => initiateReturnFromHistory(s, it)}
title="Return this item"
style={{ fontSize: '0.68rem' }}
>
<i className="bi bi-arrow-return-left"></i>
</button>
)}
</div>
</div>
))}
</div>
)}
</div>
);
})}
</div>
)}
{/* TAB 3: DOCTOR PRESCRIPTIONS */}
{historyActiveTab === 'prescriptions' && (
<div>
<div className="text-muted mb-2 fw-semibold" style={{ fontSize: '0.72rem' }}>
<i className="bi bi-stethoscope text-primary me-1"></i> Doctor Prescriptions from OPD Visits:
</div>
{patientHistory.doctor_prescriptions?.map((dp, dpIdx) => (
<div key={`doc-rx-${dpIdx}`} className="history-med-card mb-2">
<div className="d-flex justify-content-between align-items-start mb-1">
<div>
<div className="fw-bold text-dark" style={{ fontSize: '0.82rem' }}>{dp.med_name}</div>
<span className="badge bg-secondary-subtle text-secondary border" style={{ fontSize: '0.65rem' }}>{dp.med_type || 'Medicine'}</span>
</div>
<small className="text-muted" style={{ fontSize: '0.68rem' }}>{dp.date_opd_visit}</small>
</div>
<div className="text-muted mb-1.5" style={{ fontSize: '0.7rem' }}>
{dp.doc_name && <div>Doc: <strong>Dr. {dp.doc_name}</strong></div>}
<div>Dose: {dp.dosage_freq_str || dp.dosage || '1 Tab'} • Duration: {dp.no_of_days || '5 Days'}</div>
{dp.remark && <div>Note: {dp.remark}</div>}
</div>
<button
type="button"
className="btn btn-xs btn-primary w-100 py-1 fw-semibold d-flex align-items-center justify-content-center gap-1 shadow-xs"
onClick={() => reDispenseMedicine({ item_name: dp.med_name, qty: 10 })}
style={{ fontSize: '0.72rem' }}
>
<i className="bi bi-plus-circle"></i> Dispense Prescribed Medicine
</button>
</div>
))}
</div>
)}
{/* TAB 4: ALL MEDS SUMMARY */}
{historyActiveTab === 'summary' && (
<div>
<div className="text-muted mb-2 fw-semibold" style={{ fontSize: '0.72rem' }}>
Distinct Medicines Dispensed to Patient:
</div>
{patientHistory.unique_medicines?.map((um, uIdx) => (
<div key={`uniq-med-${uIdx}`} className="history-med-card mb-2">
<div className="d-flex justify-content-between align-items-start mb-1">
<div className="fw-bold text-dark" style={{ fontSize: '0.8rem' }}>{um.item_name}</div>
<span className="badge bg-primary-subtle text-primary border" style={{ fontSize: '0.65rem' }}>
{um.times_dispensed}x dispensed
</span>
</div>
<small className="text-muted d-block mb-1.5" style={{ fontSize: '0.68rem' }}>
Last on {um.last_dispensed ? um.last_dispensed.substring(0, 10) : ''} (Batch: {um.last_batch_no})
</small>
<button
type="button"
className="btn btn-xs btn-outline-success w-100 py-0.5 fw-semibold d-flex align-items-center justify-content-center gap-1"
onClick={() => reDispenseMedicine(um, um.last_qty || 1)}
style={{ fontSize: '0.7rem' }}
>
<i className="bi bi-plus-circle"></i> Dispense Again
</button>
</div>
))}
</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 Landscape (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 === 'a5' ? 'A5 LANDSCAPE' : printFormat.toUpperCase()})</span>
) : (
<span><i className="bi bi-printer me-2"></i> Complete Sale &amp; Print ({printFormat === 'a5' ? 'A5 LANDSCAPE' : 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 Landscape (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-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 === 'a5' ? 'A5 Landscape' : 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 === 'a5' ? 'A5 Landscape' : printFormat.toUpperCase()} PDF`}
style={{ fontSize: '0.75rem' }}
>
<i className="bi bi-printer me-1"></i> Print PDF ({printFormat === 'a5' ? 'A5 LANDSCAPE' : 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>
<button
type="button"
className="btn btn-xs btn-warning text-dark fw-bold py-0.5 px-2.5"
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 Landscape Invoice */}
{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 Landscape Tax Invoice (PDF Ready)</div>
<small className="text-muted" style={{ fontSize: '0.68rem' }}>Direct server vector PDF (Landscape: 210 × 148 mm) • 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 (A5 Landscape)
</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-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 === 'a5' ? 'A5 LANDSCAPE' : 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>