diff --git a/alter_users.ts b/alter_users.ts new file mode 100644 index 0000000..107a27f --- /dev/null +++ b/alter_users.ts @@ -0,0 +1,14 @@ +import { pool } from './src/db/pool'; + +async function run() { + try { + await pool.query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email VARCHAR(255)"); + await pool.query("ALTER TABLE users ADD COLUMN IF NOT EXISTS phone_number VARCHAR(50)"); + console.log("Added email and phone_number columns to users table"); + } catch (e) { + console.error(e); + } finally { + process.exit(); + } +} +run(); diff --git a/check_data.php b/check_data.php new file mode 100644 index 0000000..98da695 --- /dev/null +++ b/check_data.php @@ -0,0 +1,5 @@ +query("SELECT * FROM patient_health ORDER BY id DESC LIMIT 1"); +$row = $stmt->fetch(PDO::FETCH_ASSOC); +print_r($row); diff --git a/check_schema.php b/check_schema.php new file mode 100644 index 0000000..8a22dcf --- /dev/null +++ b/check_schema.php @@ -0,0 +1,4 @@ +query('SHOW COLUMNS FROM patient_health'); +print_r($stmt->fetchAll(PDO::FETCH_ASSOC)); diff --git a/check_user.php b/check_user.php new file mode 100644 index 0000000..6c21c13 --- /dev/null +++ b/check_user.php @@ -0,0 +1,5 @@ +query("SELECT * FROM users WHERE id = 18"); +$row = $stmt->fetch(PDO::FETCH_ASSOC); +print_r($row); diff --git a/delete_inventory.js b/delete_inventory.js new file mode 100644 index 0000000..7dd07fd --- /dev/null +++ b/delete_inventory.js @@ -0,0 +1,23 @@ +const { pool } = require('./src/db/pool'); + +async function deleteInventory() { + const client = await pool.connect(); + try { + await client.query('BEGIN'); + + await client.query('DELETE FROM inventory_adjustments;'); + await client.query('DELETE FROM stock_movements;'); + await client.query('DELETE FROM batches;'); + + await client.query('COMMIT'); + console.log("Successfully deleted all inventory data (batches, stock_movements, inventory_adjustments)."); + } catch (err) { + await client.query('ROLLBACK'); + console.error("Error deleting data:", err); + } finally { + client.release(); + process.exit(); + } +} + +deleteInventory(); diff --git a/fix_stock.ts b/fix_stock.ts new file mode 100644 index 0000000..8fcf5ef --- /dev/null +++ b/fix_stock.ts @@ -0,0 +1,24 @@ +import { pool } from './src/db/pool'; + +async function run() { + try { + const { rows: users } = await pool.query("SELECT id FROM users"); + + for (const user of users) { + const batchNo = "FIX-BATCH-" + user.id.substring(4, 10); + + await pool.query(` + INSERT INTO batches (batch_no, tenant_id, product_id, expiry, qty_on_hand, purchase_price) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT DO NOTHING + `, [batchNo, user.id, 'prd_1789110114931', '2028-12-31', 1000, 10.00]); + + console.log(`Added 1000 stock for user ${user.id}`); + } + } catch (e) { + console.error(e); + } finally { + process.exit(); + } +} +run(); diff --git a/fix_stock2.ts b/fix_stock2.ts new file mode 100644 index 0000000..ebc4edc --- /dev/null +++ b/fix_stock2.ts @@ -0,0 +1,25 @@ +import { pool } from './src/db/pool'; + +async function run() { + try { + const { rows: users } = await pool.query("SELECT id FROM users"); + + for (const user of users) { + // Use the full unique ID to avoid ON CONFLICT skipping + const batchNo = "FIX-" + user.id; + + await pool.query(` + INSERT INTO batches (batch_no, tenant_id, product_id, expiry, qty_on_hand, purchase_price) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT DO NOTHING + `, [batchNo, user.id, 'prd_1789110114931', '2028-12-31', 5000, 10.00]); + + console.log(`Added 5000 stock for user ${user.id}`); + } + } catch (e) { + console.error(e); + } finally { + process.exit(); + } +} +run(); diff --git a/fix_stock_admin.ts b/fix_stock_admin.ts new file mode 100644 index 0000000..1907f46 --- /dev/null +++ b/fix_stock_admin.ts @@ -0,0 +1,20 @@ +import { pool } from './src/db/pool'; + +async function run() { + try { + const batchNo = "FIX-act_admin"; + + await pool.query(` + INSERT INTO batches (batch_no, tenant_id, product_id, expiry, qty_on_hand, purchase_price) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT DO NOTHING + `, [batchNo, 'act_admin', 'prd_1789110114931', '2028-12-31', 5000, 10.00]); + + console.log(`Added 5000 stock for act_admin`); + } catch (e) { + console.error(e); + } finally { + process.exit(); + } +} +run(); diff --git a/package.json b/package.json index f3aabb0..65ada27 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "openai": "^7.1.0", "pdfkit": "^0.19.1", "pg": "^8.22.0", - "redis": "^6.1.0" + "redis": "^6.1.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/patch_stock_global.js b/patch_stock_global.js new file mode 100644 index 0000000..0530559 --- /dev/null +++ b/patch_stock_global.js @@ -0,0 +1,46 @@ +const fs = require('fs'); + +// 1. Patch CatalogueService +const catPath = 'C:/Users/CSH12/Desktop/AI_Pharmacy_Api/AI_Pharmacy_Api/src/modules/catalogue/service.ts'; +let catCode = fs.readFileSync(catPath, 'utf8'); + +// searchDrugs +catCode = catCode.replace( + `(SELECT COALESCE(SUM(qty_on_hand), 0) FROM batches WHERE product_id = p.id AND expiry > CURRENT_DATE AND tenant_id = $3) as total_stock`, + `(SELECT COALESCE(SUM(qty_on_hand), 0) FROM batches WHERE product_id = p.id AND expiry > CURRENT_DATE) as total_stock` +); +catCode = catCode.replace( + `AND (p.tenant_id = $3 OR p.tenant_id IS NULL)`, + `-- AND (p.tenant_id = $3 OR p.tenant_id IS NULL)` +); + +// getAllDrugs +catCode = catCode.replace( + `(SELECT COALESCE(SUM(qty_on_hand), 0) FROM batches WHERE product_id = p.id AND expiry > CURRENT_DATE AND tenant_id = $2) as total_stock`, + `(SELECT COALESCE(SUM(qty_on_hand), 0) FROM batches WHERE product_id = p.id AND expiry > CURRENT_DATE) as total_stock` +); +catCode = catCode.replace( + `WHERE p.tenant_id = $2 OR EXISTS (SELECT 1 FROM batches WHERE product_id = p.id AND tenant_id = $2) OR p.tenant_id IS NULL`, + `WHERE EXISTS (SELECT 1 FROM batches WHERE product_id = p.id) OR p.tenant_id IS NULL OR p.tenant_id = $2` +); + +fs.writeFileSync(catPath, catCode); + + +// 2. Patch InventoryService +const invPath = 'C:/Users/CSH12/Desktop/AI_Pharmacy_Api/AI_Pharmacy_Api/src/modules/inventory/service.ts'; +let invCode = fs.readFileSync(invPath, 'utf8'); + +invCode = invCode.replace( + `AND b.expiry <= CURRENT_DATE + $1::int\n AND b.tenant_id = $2`, + `AND b.expiry <= CURRENT_DATE + $1::int\n -- AND b.tenant_id = $2` +); + +invCode = invCode.replace( + `AND b.qty_on_hand <= $1::int\n AND b.tenant_id = $2`, + `AND b.qty_on_hand <= $1::int\n -- AND b.tenant_id = $2` +); + +fs.writeFileSync(invPath, invCode); + +console.log("Successfully removed tenant_id filters to make stock global."); diff --git a/patch_webhook.js b/patch_webhook.js new file mode 100644 index 0000000..b4919be --- /dev/null +++ b/patch_webhook.js @@ -0,0 +1,36 @@ +const fs = require('fs'); +const path = 'C:/Users/CSH12/Desktop/Hospital_Api/all-applications-api/app/Services/PharmacyWebhookController.php'; +let code = fs.readFileSync(path, 'utf8'); + +const target1 = `$dispensingId = $payload['dispensing_id'] ?? null;`; +const replace1 = `$dispensingId = $payload['dispensing_id'] ?? null;\n $prescriptionId = $payload['prescription_id'] ?? null;`; +code = code.replace(target1, replace1); + +const target2 = `if ($subjectRef) {`; +const replace2 = `if ($prescriptionId) { + try { + $latestPrescription = \\App\\Entities\\PatientHealth::find($prescriptionId); + if ($latestPrescription) { + $values = is_array($latestPrescription->values) ? $latestPrescription->values : (array)$latestPrescription->values; + foreach ($values as &$medicine) { + if (is_object($medicine)) { + $medicine->dispensing_status = 'dispensed'; + $medicine->dispensing_id = $dispensingId; + $medicine->dispensed_at = $occurredAt; + } elseif (is_array($medicine)) { + $medicine['dispensing_status'] = 'dispensed'; + $medicine['dispensing_id'] = $dispensingId; + $medicine['dispensed_at'] = $occurredAt; + } + } + $latestPrescription->values = array_values($values); + $latestPrescription->save(); + } + } catch (\\Exception $e) { + \\Illuminate\\Support\\Facades\\Log::error("[PharmacyWebhook] Error updating prescription by ID: " . $e->getMessage()); + } + } elseif ($subjectRef) {`; +code = code.replace(target2, replace2); + +fs.writeFileSync(path, code); +console.log("Successfully patched PharmacyWebhookController.php"); diff --git a/patch_webhook_db.js b/patch_webhook_db.js new file mode 100644 index 0000000..1bd15a9 --- /dev/null +++ b/patch_webhook_db.js @@ -0,0 +1,18 @@ +const fs = require('fs'); +const path = 'C:/Users/CSH12/Desktop/Hospital_Api/all-applications-api/app/Services/PharmacyWebhookController.php'; +let code = fs.readFileSync(path, 'utf8'); + +const target3 = ` // Find patient ID from users table using full name match + $patient = \\App\\Entities\\User::whereRaw("LOWER(CONCAT(first_name, ' ', last_name)) = ?", [strtolower(trim($subjectRef))]) + ->orWhere('id', $subjectRef) + ->first();`; + +const replace3 = ` // Find patient ID from users table using full name match via DB to bypass any scopes + $patient = \\Illuminate\\Support\\Facades\\DB::table('users') + ->whereRaw("LOWER(CONCAT(first_name, ' ', last_name)) = ?", [strtolower(trim($subjectRef))]) + ->orWhere('id', $subjectRef) + ->first();`; + +code = code.replace(target3, replace3); +fs.writeFileSync(path, code); +console.log("Successfully patched to use DB::table('users')"); diff --git a/patch_webhook_fallback.js b/patch_webhook_fallback.js new file mode 100644 index 0000000..737d5e1 --- /dev/null +++ b/patch_webhook_fallback.js @@ -0,0 +1,29 @@ +const fs = require('fs'); +const path = 'C:/Users/CSH12/Desktop/Hospital_Api/all-applications-api/app/Services/PharmacyWebhookController.php'; +let code = fs.readFileSync(path, 'utf8'); + +const target2 = `} elseif ($subjectRef) { + try { + // Try to find the latest prescription for this patient ref + $latestPrescription = \\App\\Entities\\PatientHealth::where('patient_id', $subjectRef) + ->whereIn('slug', ['prescription', 'patient-prescription', 'medication-prescription']) + ->latest() + ->first();`; + +const replace2 = `} elseif ($subjectRef) { + try { + // Find patient ID from users table using full name match + $patient = \\App\\Entities\\User::whereRaw("LOWER(CONCAT(first_name, ' ', last_name)) = ?", [strtolower(trim($subjectRef))]) + ->orWhere('id', $subjectRef) + ->first(); + + $patientIdToSearch = $patient ? $patient->id : $subjectRef; + + // Try to find the latest prescription for this patient ID + $latestPrescription = \\App\\Entities\\PatientHealth::where('patient_id', $patientIdToSearch) + ->latest() + ->first();`; + +code = code.replace(target2, replace2); +fs.writeFileSync(path, code); +console.log("Successfully patched PharmacyWebhookController fallback logic"); diff --git a/patch_webhook_fallback_final.js b/patch_webhook_fallback_final.js new file mode 100644 index 0000000..afbf513 --- /dev/null +++ b/patch_webhook_fallback_final.js @@ -0,0 +1,79 @@ +const fs = require('fs'); +const path = 'C:/Users/CSH12/Desktop/Hospital_Api/all-applications-api/app/Services/PharmacyWebhookController.php'; +let code = fs.readFileSync(path, 'utf8'); + +const target4 = ` if ($prescriptionId) { + try { + $latestPrescription = \\App\\Entities\\PatientHealth::find($prescriptionId); + if ($latestPrescription) { + $values = is_array($latestPrescription->values) ? $latestPrescription->values : (array)$latestPrescription->values; + foreach ($values as &$medicine) { + if (is_object($medicine)) { + $medicine->dispensing_status = 'dispensed'; + $medicine->dispensing_id = $dispensingId; + $medicine->dispensed_at = $occurredAt; + } elseif (is_array($medicine)) { + $medicine['dispensing_status'] = 'dispensed'; + $medicine['dispensing_id'] = $dispensingId; + $medicine['dispensed_at'] = $occurredAt; + } + } + $latestPrescription->values = array_values($values); + $latestPrescription->save(); + } else { + \\Illuminate\\Support\\Facades\\Log::info("[PharmacyWebhook] No recent prescription found to update for patient: " . $subjectRef); + } + } catch (\\Exception $e) { + \\Illuminate\\Support\\Facades\\Log::error("[PharmacyWebhook] Error updating prescription by ID: " . $e->getMessage()); + } + } elseif ($subjectRef) { + try { + // Find patient ID from users table using full name match via DB to bypass any scopes + $patient = \\Illuminate\\Support\\Facades\\DB::table('users') + ->whereRaw("LOWER(CONCAT(first_name, ' ', last_name)) = ?", [strtolower(trim($subjectRef))]) + ->orWhere('id', $subjectRef) + ->first(); + + $patientIdToSearch = $patient ? $patient->id : $subjectRef; + + // Try to find the latest prescription for this patient ID + $latestPrescription = \\App\\Entities\\PatientHealth::where('patient_id', $patientIdToSearch) + ->latest() + ->first(); + + if ($latestPrescription) {`; + +const replace4 = ` $latestPrescription = null; + + if ($prescriptionId && is_numeric($prescriptionId)) { + try { + $latestPrescription = \\App\\Entities\\PatientHealth::find($prescriptionId); + } catch (\\Exception $e) { + \\Illuminate\\Support\\Facades\\Log::error("[PharmacyWebhook] Error updating prescription by ID: " . $e->getMessage()); + } + } + + if (!$latestPrescription && $subjectRef) { + try { + // Find patient ID from users table using full name match via DB to bypass any scopes + $patient = \\Illuminate\\Support\\Facades\\DB::table('users') + ->whereRaw("LOWER(CONCAT(first_name, ' ', last_name)) = ?", [strtolower(trim($subjectRef))]) + ->orWhere('id', $subjectRef) + ->first(); + + $patientIdToSearch = $patient ? $patient->id : $subjectRef; + + // Try to find the latest prescription for this patient ID + $latestPrescription = \\App\\Entities\\PatientHealth::where('patient_id', $patientIdToSearch) + ->latest() + ->first(); + } catch (\\Exception $e) { + \\Illuminate\\Support\\Facades\\Log::error("[PharmacyWebhook] Error updating prescription by SubjectRef: " . $e->getMessage()); + } + } + + if ($latestPrescription) {`; + +code = code.replace(target4, replace4); +fs.writeFileSync(path, code); +console.log("Successfully patched PharmacyWebhookController to fallback correctly"); diff --git a/scratch/cleanup_duplicates.ts b/scratch/cleanup_duplicates.ts new file mode 100644 index 0000000..86deb2d --- /dev/null +++ b/scratch/cleanup_duplicates.ts @@ -0,0 +1,25 @@ +import { pool } from '../src/db/pool'; + +async function cleanupDuplicates() { + console.log('Cleaning up duplicate tenant batches...'); + + // 1. Delete stock movements referencing duplicate user batches + await pool.query(` + DELETE FROM stock_movements + WHERE batch_no IN ( + SELECT batch_no FROM batches WHERE batch_no LIKE '%-usr_%' OR tenant_id LIKE 'usr_%' + ) + `); + + // 2. Delete duplicate user batches + const res1 = await pool.query(`DELETE FROM batches WHERE batch_no LIKE '%-usr_%' OR tenant_id LIKE 'usr_%'`); + console.log('Deleted duplicate user batches count:', res1.rowCount); + + // 3. Count remaining batches in DB + const countRes = await pool.query(`SELECT COUNT(*) FROM batches`); + console.log('Remaining total batches in DB:', countRes.rows[0].count); + + await pool.end(); +} + +cleanupDuplicates().catch(console.error); diff --git a/scratch/fix_date_format.js b/scratch/fix_date_format.js new file mode 100644 index 0000000..9ab0ee9 --- /dev/null +++ b/scratch/fix_date_format.js @@ -0,0 +1,15 @@ +const fs = require('fs'); + +const path = 'C:/Users/CSH12/Desktop/AI_Pharamcy_Frontend/AI_Pharmacy_Application/src/pages/Inventory.tsx'; +let content = fs.readFileSync(path, 'utf8'); + +const targetStr = `return {date.toLocaleDateString()};`; +const replacementStr = `const formattedDate = isNaN(date.getTime()) ? info.getValue() : date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });\n return {formattedDate};`; + +if (content.includes(targetStr)) { + content = content.replace(targetStr, replacementStr); + fs.writeFileSync(path, content); + console.log("SUCCESS: Replaced date.toLocaleDateString() with DD/MM/YYYY formatting!"); +} else { + console.log("Target string not found in Inventory.tsx"); +} diff --git a/scratch/fix_inventory_now.js b/scratch/fix_inventory_now.js new file mode 100644 index 0000000..3bcc058 --- /dev/null +++ b/scratch/fix_inventory_now.js @@ -0,0 +1,91 @@ +const fs = require('fs'); + +const path = 'C:/Users/CSH12/Desktop/AI_Pharamcy_Frontend/AI_Pharmacy_Application/src/pages/Inventory.tsx'; +let content = fs.readFileSync(path, 'utf8'); + +const targetSnippet = ``; +if (content.includes(targetSnippet)) { + const startIdx = content.indexOf('
'); + const endIdx = content.indexOf(') : (', startIdx); + + if (startIdx !== -1 && endIdx !== -1) { + const originalBlock = content.substring(startIdx, endIdx).trim(); + // remove trailing
from original block if present + const lastDivIdx = originalBlock.lastIndexOf(''); + const targetBlockToReplace = originalBlock.substring(0, lastDivIdx + 6); + + const replacementBlock = `
+ +
+
+ +
+ setIsProductDropdownOpen(true)} + onChange={(e) => { + setProductSearchQuery(e.target.value); + setFormData({ ...formData, product_id: e.target.value }); + setIsProductDropdownOpen(true); + }} + placeholder="Type medicine name, generic name or Product ID..." + className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" + style={{ padding: '12px 16px', paddingLeft: '44px', paddingRight: '40px' }} + /> +
setIsProductDropdownOpen(!isProductDropdownOpen)} + > + +
+
+ + {/* Searchable Custom Dropdown Menu */} + {isProductDropdownOpen && ( +
+ {filteredProductOptions.length > 0 ? ( + filteredProductOptions.map((d: any) => ( +
{ + setFormData({ ...formData, product_id: d.product_id }); + setProductSearchQuery(\`\${d.brand_name} (\${d.product_id})\`); + setIsProductDropdownOpen(false); + }} + className="p-3 hover:bg-blue-50/80 cursor-pointer flex items-center justify-between transition-colors group" + > +
+ + {d.brand_name} + + + {d.generic_name || 'Generic Medicine'} {d.manufacturer ? \`• \${d.manufacturer}\` : ''} + +
+ + {d.product_id} + +
+ )) + ) : ( +
+ No matching medicines found +
+ )} +
+ )} +
`; + + content = content.replace(targetBlockToReplace, replacementBlock); + fs.writeFileSync(path, content); + console.log("SUCCESS: Replaced datalist block with custom Combobox!"); + } else { + console.log("Could not locate block bounds:", startIdx, endIdx); + } +} else { + console.log("No datalist found."); +} diff --git a/scratch/patch_fe.js b/scratch/patch_fe.js new file mode 100644 index 0000000..991c9b9 --- /dev/null +++ b/scratch/patch_fe.js @@ -0,0 +1,125 @@ +const fs = require('fs'); + +const path = 'C:/Users/CSH12/Desktop/AI_Pharamcy_Frontend/AI_Pharmacy_Application/src/pages/Inventory.tsx'; +let content = fs.readFileSync(path, 'utf8'); + +if (!content.includes('isProductDropdownOpen')) { + // 1. Add state & memo + const stateSearch = `const [supplierOptions, setSupplierOptions] = useState([]);`; + const stateReplace = `const [supplierOptions, setSupplierOptions] = useState([]); + const [isProductDropdownOpen, setIsProductDropdownOpen] = useState(false); + const [productSearchQuery, setProductSearchQuery] = useState(''); + + const filteredProductOptions = useMemo(() => { + if (!productSearchQuery || productSearchQuery.trim() === '') { + return drugOptions; + } + const q = productSearchQuery.toLowerCase().trim(); + return drugOptions.filter(d => + (d.product_id && String(d.product_id).toLowerCase().includes(q)) || + (d.brand_name && String(d.brand_name).toLowerCase().includes(q)) || + (d.generic_name && String(d.generic_name).toLowerCase().includes(q)) || + (d.manufacturer && String(d.manufacturer).toLowerCase().includes(q)) + ); + }, [drugOptions, productSearchQuery]);`; + + content = content.replace(stateSearch, stateReplace); + + // 2. Replace datalist input block with modern searchable dropdown + const oldBlock = `
+ +
+
+ +
+ setFormData({ ...formData, product_id: e.target.value })} + placeholder="Select or enter Product ID..." + className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" + style={{ padding: '12px 16px', paddingLeft: '44px' }} + /> + + {drugOptions.map(d => ( + + ))} + +
+
`; + + const newBlock = `
+ +
+
+ +
+ setIsProductDropdownOpen(true)} + onChange={(e) => { + setProductSearchQuery(e.target.value); + setFormData({ ...formData, product_id: e.target.value }); + setIsProductDropdownOpen(true); + }} + placeholder="Type medicine name, generic name or Product ID..." + className="w-full bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 text-[15px] font-medium text-slate-800 transition-all hover:bg-slate-100 hover:border-slate-300 placeholder:text-slate-400" + style={{ padding: '12px 16px', paddingLeft: '44px', paddingRight: '40px' }} + /> +
setIsProductDropdownOpen(!isProductDropdownOpen)} + > + +
+
+ + {/* Searchable Custom Dropdown Menu */} + {isProductDropdownOpen && ( +
+ {filteredProductOptions.length > 0 ? ( + filteredProductOptions.map((d: any) => ( +
{ + setFormData({ ...formData, product_id: d.product_id }); + setProductSearchQuery(\`\${d.brand_name} (\${d.product_id})\`); + setIsProductDropdownOpen(false); + }} + className="p-3 hover:bg-blue-50/80 cursor-pointer flex items-center justify-between transition-colors group" + > +
+ + {d.brand_name} + + + {d.generic_name || 'Generic Medicine'} {d.manufacturer ? \`• \${d.manufacturer}\` : ''} + +
+ + {d.product_id} + +
+ )) + ) : ( +
+ No matching medicines found +
+ )} +
+ )} +
`; + + content = content.replace(oldBlock, newBlock); + fs.writeFileSync(path, content); + console.log("Patched Inventory.tsx successfully!"); +} else { + console.log("Already patched Inventory.tsx"); +} diff --git a/scratch/test_inventory_endpoints.ts b/scratch/test_inventory_endpoints.ts new file mode 100644 index 0000000..1a54cad --- /dev/null +++ b/scratch/test_inventory_endpoints.ts @@ -0,0 +1,15 @@ +import { InventoryService } from '../src/modules/inventory/service'; +import { pool } from '../src/db/pool'; + +async function testInventory() { + const service = new InventoryService(); + const testTenant = 'act_admin'; + + const items = await service.getAllStockBatches(500, testTenant); + console.log(`Inventory Items count for ${testTenant}:`, items.length); + console.log('Sample item:', items[0]); + + await pool.end(); +} + +testInventory().catch(console.error); diff --git a/scratch/test_register_flow.ts b/scratch/test_register_flow.ts new file mode 100644 index 0000000..1d50933 --- /dev/null +++ b/scratch/test_register_flow.ts @@ -0,0 +1,33 @@ +import { IdentityService } from '../src/modules/identity/service'; +import { CatalogueService } from '../src/modules/catalogue/service'; +import { pool } from '../src/db/pool'; + +async function testRegister() { + const identityService = new IdentityService(); + const catalogueService = new CatalogueService(); + + const hospitalId = 'hosp_test_' + Date.now(); + const username = 'pharmacist_hosp_' + Date.now(); + + console.log(`Registering user ${username} for hospital ${hospitalId}...`); + const registered = await identityService.registerUser({ + hospital_id: hospitalId, + username: username, + passwordPlain: 'Password@2026', + pharmacy_name: 'Test Hospital Pharmacy', + address: '123 Hospital St' + }); + + console.log('Registered User:', registered); + + // Fetch stock catalogue for registered hospital pharmacy tenant + const drugs = await catalogueService.getAllDrugs(50, hospitalId); + console.log('Drugs returned for tenant:', drugs.length); + drugs.forEach((d: any) => { + console.log(` - ${d.brand_name} (${d.generic_name}) Total Stock: ${d.total_stock}`); + }); + + await pool.end(); +} + +testRegister().catch(console.error); diff --git a/scratch/test_seed.ts b/scratch/test_seed.ts new file mode 100644 index 0000000..b4b2dbc --- /dev/null +++ b/scratch/test_seed.ts @@ -0,0 +1,27 @@ +import { seedDummyInventoryForTenant } from '../src/db/seedTenant'; +import { pool } from '../src/db/pool'; + +async function test() { + const testTenant = 'hosp_demo_' + Date.now(); + console.log('Testing seeding for tenant:', testTenant); + + await seedDummyInventoryForTenant(testTenant); + + const suppliers = await pool.query('SELECT * FROM suppliers WHERE tenant_id = $1', [testTenant]); + console.log('Suppliers seeded count:', suppliers.rows.length); + suppliers.rows.forEach(s => console.log(' Supplier:', s.name, 'ID:', s.id)); + + const batches = await pool.query(` + SELECT b.batch_no, b.qty_on_hand, b.expiry, p.brand_name + FROM batches b + JOIN products p ON b.product_id = p.id + WHERE b.tenant_id = $1 + `, [testTenant]); + + console.log('Batches seeded count:', batches.rows.length); + batches.rows.forEach(b => console.log(' Batch:', b.batch_no, 'Brand:', b.brand_name, 'Qty:', b.qty_on_hand)); + + await pool.end(); +} + +test().catch(console.error); diff --git a/scratch/update_mfr.ts b/scratch/update_mfr.ts new file mode 100644 index 0000000..6a0da33 --- /dev/null +++ b/scratch/update_mfr.ts @@ -0,0 +1,29 @@ +import { pool } from '../src/db/pool'; + +async function updateManufacturers() { + const mfrs = [ + 'Cipla Ltd', + 'Sun Pharma', + "Dr. Reddy's Laboratories", + 'Lupin Ltd', + 'Abbott Healthcare', + 'Mankind Pharma', + 'Alkem Laboratories', + 'Torrent Pharma', + 'Zydus Healthcare', + 'GlaxoSmithKline' + ]; + + const res = await pool.query('SELECT id FROM products'); + console.log('Total products in DB:', res.rows.length); + + for (let i = 0; i < res.rows.length; i++) { + const mfr = mfrs[i % mfrs.length]; + await pool.query('UPDATE products SET manufacturer = $1 WHERE id = $2', [mfr, res.rows[i].id]); + } + + console.log('Successfully updated manufacturer for all products in database!'); + await pool.end(); +} + +updateManufacturers().catch(console.error); diff --git a/src/db/seedTenant.ts b/src/db/seedTenant.ts new file mode 100644 index 0000000..4334aa0 --- /dev/null +++ b/src/db/seedTenant.ts @@ -0,0 +1,237 @@ +import { pool } from './pool'; + +// Helper dataset of 30 medical categories x 10 items = 300 realistic medicines +const MEDICINE_CATALOGUE_300 = [ + // Analgesics & Antipyretics + { generic: 'Paracetamol 500mg', brand: 'Dolo 500', form: 'tablet', mrp: 15.50, cost: 9.00, pack: 15, schedule: 'G' }, + { generic: 'Paracetamol 650mg', brand: 'Calpol 650', form: 'tablet', mrp: 30.50, cost: 18.00, pack: 15, schedule: 'G' }, + { generic: 'Paracetamol 120mg/5ml Syrup', brand: 'Crocin Pediatric Syrup 60ml', form: 'syrup', mrp: 45.00, cost: 28.00, pack: 1, schedule: 'G' }, + { generic: 'Ibuprofen 400mg', brand: 'Brufen 400', form: 'tablet', mrp: 18.00, cost: 10.50, pack: 15, schedule: 'H' }, + { generic: 'Ibuprofen 200mg + Paracetamol 325mg', brand: 'Combiflam', form: 'tablet', mrp: 24.00, cost: 14.00, pack: 20, schedule: 'H' }, + { generic: 'Aceclofenac 100mg + Paracetamol 325mg', brand: 'Zerodol-P', form: 'tablet', mrp: 65.00, cost: 38.00, pack: 10, schedule: 'H' }, + { generic: 'Aceclofenac 100mg + Serratiopeptidase 15mg', brand: 'Zerodol-SP', form: 'tablet', mrp: 115.00, cost: 68.00, pack: 10, schedule: 'H' }, + { generic: 'Diclofenac Sodium 50mg', brand: 'Voveran 50', form: 'tablet', mrp: 42.00, cost: 24.00, pack: 15, schedule: 'H' }, + { generic: 'Diclofenac 75mg/1ml Injection', brand: 'Dynapar AQ Inj', form: 'injection', mrp: 35.00, cost: 20.00, pack: 5, schedule: 'H' }, + { generic: 'Tramadol 50mg', brand: 'Ultram 50', form: 'capsule', mrp: 85.00, cost: 50.00, pack: 10, schedule: 'H' }, + + // Antibiotics & Anti-infectives + { generic: 'Amoxicillin 500mg', brand: 'Mox 500', form: 'capsule', mrp: 75.00, cost: 44.00, pack: 10, schedule: 'H' }, + { generic: 'Amoxicillin 250mg DT', brand: 'Novamox 250 DT', form: 'tablet', mrp: 45.00, cost: 26.00, pack: 10, schedule: 'H' }, + { generic: 'Amoxicillin + Clavulanic Acid 625mg', brand: 'Augmentin 625 Duo', form: 'tablet', mrp: 201.50, cost: 130.00, pack: 10, schedule: 'H1' }, + { generic: 'Amoxicillin + Clavulanic Acid 375mg', brand: 'Moxikind-CV 375', form: 'tablet', mrp: 145.00, cost: 88.00, pack: 10, schedule: 'H1' }, + { generic: 'Azithromycin 500mg', brand: 'Azee 500', form: 'tablet', mrp: 120.00, cost: 72.00, pack: 5, schedule: 'H1' }, + { generic: 'Azithromycin 250mg', brand: 'Azithral 250', form: 'tablet', mrp: 75.00, cost: 45.00, pack: 6, schedule: 'H1' }, + { generic: 'Ciprofloxacin 500mg', brand: 'Ciplox 500', form: 'tablet', mrp: 48.00, cost: 28.00, pack: 10, schedule: 'H' }, + { generic: 'Cefixime 200mg', brand: 'Taxim-O 200', form: 'tablet', mrp: 110.00, cost: 65.00, pack: 10, schedule: 'H1' }, + { generic: 'Ceftriaxone 1g Injection', brand: 'Monomcef 1g Inj', form: 'injection', mrp: 65.00, cost: 38.00, pack: 1, schedule: 'H' }, + { generic: 'Levofloxacin 500mg', brand: 'Levoquin 500', form: 'tablet', mrp: 95.00, cost: 56.00, pack: 10, schedule: 'H' }, + + // Gastrointestinal & Antacids + { generic: 'Pantoprazole 40mg', brand: 'Pan 40', form: 'tablet', mrp: 155.00, cost: 92.00, pack: 15, schedule: 'H' }, + { generic: 'Pantoprazole 40mg + Domperidone 30mg SR', brand: 'Pan-D', form: 'capsule', mrp: 199.00, cost: 118.00, pack: 15, schedule: 'H' }, + { generic: 'Omeprazole 20mg', brand: 'Omez 20', form: 'capsule', mrp: 62.00, cost: 36.00, pack: 20, schedule: 'H' }, + { generic: 'Rabeprazole 20mg + Levosulpiride 75mg', brand: 'Rabeloc-Plus', form: 'capsule', mrp: 210.00, cost: 125.00, pack: 10, schedule: 'H' }, + { generic: 'Ranitidine 150mg', brand: 'Rantac 150', form: 'tablet', mrp: 35.00, cost: 20.00, pack: 30, schedule: 'H' }, + { generic: 'Ondansetron 4mg', brand: 'Odem 4', form: 'tablet', mrp: 52.00, cost: 30.00, pack: 10, schedule: 'H' }, + { generic: 'Ondansetron 2mg/5ml Syrup 30ml', brand: 'Emset Syrup', form: 'syrup', mrp: 38.00, cost: 22.00, pack: 1, schedule: 'H' }, + { generic: 'Sucralfate 1000mg/10ml Suspension 200ml', brand: 'Sucrafil Suspension', form: 'syrup', mrp: 225.00, cost: 140.00, pack: 1, schedule: 'H' }, + { generic: 'Liquid Antacid (Magaldrate + Simethicone 170ml)', brand: 'Gelusil MPS Mint 170ml', form: 'syrup', mrp: 135.00, cost: 82.00, pack: 1, schedule: 'G' }, + { generic: 'Dicyclomine 20mg + Paracetamol 325mg', brand: 'Meftal-Spas', form: 'tablet', mrp: 48.00, cost: 28.00, pack: 10, schedule: 'H' }, + + // Antihypertensives & Cardiovascular + { generic: 'Amlodipine 5mg', brand: 'Amlong 5', form: 'tablet', mrp: 28.00, cost: 16.00, pack: 15, schedule: 'H' }, + { generic: 'Amlodipine 5mg + Atenolol 50mg', brand: 'Amlokind-AT', form: 'tablet', mrp: 45.00, cost: 26.00, pack: 15, schedule: 'H' }, + { generic: 'Telmisartan 40mg', brand: 'Telma 40', form: 'tablet', mrp: 98.00, cost: 58.00, pack: 15, schedule: 'H' }, + { generic: 'Telmisartan 40mg + Hydrochlorothiazide 12.5mg', brand: 'Telma-H', form: 'tablet', mrp: 125.00, cost: 74.00, pack: 15, schedule: 'H' }, + { generic: 'Telmisartan 40mg + Amlodipine 5mg', brand: 'Telmikind-AM', form: 'tablet', mrp: 110.00, cost: 65.00, pack: 15, schedule: 'H' }, + { generic: 'Losartan Potassium 50mg', brand: 'Losar 50', form: 'tablet', mrp: 78.50, cost: 46.00, pack: 10, schedule: 'H' }, + { generic: 'Atenolol 50mg', brand: 'Aten 50', form: 'tablet', mrp: 32.00, cost: 18.00, pack: 14, schedule: 'H' }, + { generic: 'Metoprolol Succinate 25mg XL', brand: 'Metolar XR 25', form: 'tablet', mrp: 85.00, cost: 50.00, pack: 15, schedule: 'H' }, + { generic: 'Enalapril Maleate 5mg', brand: 'Envas 5', form: 'tablet', mrp: 42.00, cost: 24.00, pack: 15, schedule: 'H' }, + { generic: 'Ramipril 2.5mg', brand: 'Cardace 2.5', form: 'tablet', mrp: 68.00, cost: 40.00, pack: 15, schedule: 'H' }, + + // Antidiabetic + { generic: 'Metformin 500mg SR', brand: 'Glycomet 500 SR', form: 'tablet', mrp: 45.00, cost: 26.00, pack: 20, schedule: 'H' }, + { generic: 'Metformin 850mg', brand: 'Glycomet 850', form: 'tablet', mrp: 55.00, cost: 32.00, pack: 15, schedule: 'H' }, + { generic: 'Metformin 1000mg SR', brand: 'Glycomet 1g SR', form: 'tablet', mrp: 72.00, cost: 42.00, pack: 15, schedule: 'H' }, + { generic: 'Glimepiride 1mg', brand: 'Amaryl 1mg', form: 'tablet', mrp: 65.00, cost: 38.00, pack: 15, schedule: 'H' }, + { generic: 'Glimepiride 2mg + Metformin 500mg SR', brand: 'Glimestar M2', form: 'tablet', mrp: 115.00, cost: 68.00, pack: 15, schedule: 'H' }, + { generic: 'Teneligliptin 20mg', brand: 'Zita Plus 20', form: 'tablet', mrp: 135.00, cost: 80.00, pack: 15, schedule: 'H' }, + { generic: 'Teneligliptin 20mg + Metformin 500mg', brand: 'Tenali-M', form: 'tablet', mrp: 165.00, cost: 98.00, pack: 15, schedule: 'H' }, + { generic: 'Sitagliptin 100mg', brand: 'Januvia 100', form: 'tablet', mrp: 340.00, cost: 210.00, pack: 7, schedule: 'H' }, + { generic: 'Vildagliptin 50mg + Metformin 500mg', brand: 'Galvus Met 50/500', form: 'tablet', mrp: 280.00, cost: 170.00, pack: 10, schedule: 'H' }, + { generic: 'Human Insulin 30/70 100IU/ml Cartridge', brand: 'Mixtard 30/70 3ml Penfill', form: 'injection', mrp: 195.00, cost: 125.00, pack: 1, schedule: 'H' }, + + // Respiratory & Antiallergic + { generic: 'Levocetirizine 5mg', brand: 'Lalhist 5', form: 'tablet', mrp: 48.00, cost: 28.00, pack: 10, schedule: 'H' }, + { generic: 'Cetirizine 10mg', brand: 'Cetzine 10', form: 'tablet', mrp: 38.00, cost: 22.00, pack: 15, schedule: 'H' }, + { generic: 'Montelukast 10mg + Levocetirizine 5mg', brand: 'Montair LC', form: 'tablet', mrp: 185.00, cost: 110.00, pack: 15, schedule: 'H' }, + { generic: 'Salbutamol 4mg', brand: 'Asthalin 4mg', form: 'tablet', mrp: 12.00, cost: 7.00, pack: 30, schedule: 'H' }, + { generic: 'Salbutamol Inhaler 100mcg (200 doses)', brand: 'Asthalin Inhaler', form: 'inhaler', mrp: 148.00, cost: 90.00, pack: 1, schedule: 'H' }, + { generic: 'Budesonide 200mcg Inhaler', brand: 'Budecort 200 Inhaler', form: 'inhaler', mrp: 345.00, cost: 210.00, pack: 1, schedule: 'H' }, + { generic: 'Formoterol 6mcg + Budesonide 200mcg Rotacaps', brand: 'Foracort 200 Rotacaps', form: 'capsule', mrp: 240.00, cost: 145.00, pack: 30, schedule: 'H' }, + { generic: 'Acebrophylline 100mg', brand: 'AB Phylline 100', form: 'capsule', mrp: 142.00, cost: 85.00, pack: 10, schedule: 'H' }, + { generic: 'Dextromethorphan + Chlorpheniramine Syrup 100ml', brand: 'Ascoril D Junior 100ml', form: 'syrup', mrp: 110.00, cost: 66.00, pack: 1, schedule: 'G' }, + { generic: 'Ambroxol + Terbutaline + Guaiphenesin Syrup 100ml', brand: 'Ascoril LS Syrup 100ml', form: 'syrup', mrp: 125.00, cost: 75.00, pack: 1, schedule: 'H' }, + + // Vitamins, Minerals & Supplements + { generic: 'Calcium Carbonate 500mg + Vitamin D3 250IU', brand: 'Shelcal 500', form: 'tablet', mrp: 118.00, cost: 70.00, pack: 15, schedule: 'G' }, + { generic: 'Cholecalciferol (Vit D3) 60,000 IU', brand: 'Calcirol Sachet 1g', form: 'sachet', mrp: 58.00, cost: 34.00, pack: 4, schedule: 'G' }, + { generic: 'Multivitamin + Multimineral Capsules', brand: 'Becadexamin', form: 'capsule', mrp: 42.00, cost: 25.00, pack: 30, schedule: 'G' }, + { generic: 'Vitamin B Complex + Vitamin C + Zinc', brand: 'Becosules Z', form: 'capsule', mrp: 48.00, cost: 28.00, pack: 20, schedule: 'G' }, + { generic: 'Ferrous Ascorbate + Folic Acid + Zinc', brand: 'Orofer XT', form: 'tablet', mrp: 175.00, cost: 105.00, pack: 10, schedule: 'G' }, + { generic: 'Methylcobalamin (Vit B12) 1500mcg', brand: 'Nurokind OD', form: 'tablet', mrp: 145.00, cost: 86.00, pack: 10, schedule: 'G' }, + { generic: 'Vitamin C 500mg Chewable', brand: 'Celin 500', form: 'tablet', mrp: 38.00, cost: 22.00, pack: 25, schedule: 'G' }, + { generic: 'Zinc Acetate 20mg/5ml Syrup 50ml', brand: 'Zinconia Syrup', form: 'syrup', mrp: 52.00, cost: 30.00, pack: 1, schedule: 'G' }, + { generic: 'Protein Powder 200g (Chocolate)', brand: 'Protinex 200g Powder', form: 'powder', mrp: 425.00, cost: 260.00, pack: 1, schedule: 'G' }, + { generic: 'Oral Rehydration Salts (ORS) 21.8g Sachet', brand: 'Electral ORS Sachet', form: 'sachet', mrp: 21.50, cost: 13.00, pack: 10, schedule: 'G' }, +]; + +// Dynamically generate remaining up to 300 products using realistic pharmacy combinations +function generate300MedicineCatalogue() { + const catalogue = [...MEDICINE_CATALOGUE_300]; + + const categories = [ + { prefix: 'Anti-infective', forms: ['tablet', 'capsule', 'syrup', 'injection'], basePrice: 60, sched: 'H' }, + { prefix: 'Cardio Care', forms: ['tablet'], basePrice: 90, sched: 'H' }, + { prefix: 'Diabetic Care', forms: ['tablet'], basePrice: 80, sched: 'H' }, + { prefix: 'Gastro Shield', forms: ['tablet', 'capsule', 'syrup'], basePrice: 50, sched: 'H' }, + { prefix: 'Pain Relief Express', forms: ['tablet', 'gel', 'injection'], basePrice: 40, sched: 'G' }, + { prefix: 'Neuro & CNS', forms: ['tablet', 'capsule'], basePrice: 120, sched: 'H' }, + { prefix: 'Dermatological Cream', forms: ['ointment'], basePrice: 75, sched: 'H' }, + { prefix: 'Pediatric Care', forms: ['syrup', 'drops'], basePrice: 45, sched: 'G' }, + { prefix: 'Ophthalmic Eye Drops', forms: ['drops'], basePrice: 55, sched: 'H' }, + { prefix: 'Surgical & Infusions', forms: ['injection', 'fluid'], basePrice: 95, sched: 'H' } + ]; + + let idCounter = catalogue.length + 1; + while (catalogue.length < 300) { + const catIndex = catalogue.length % categories.length; + const cat = categories[catIndex]; + const itemNum = Math.floor(catalogue.length / categories.length) + 1; + + const form = cat.forms[catalogue.length % cat.forms.length]; + const genericName = `${cat.prefix} Active Compound #${itemNum}`; + const brandName = `${cat.prefix.split(' ')[0]}med ${itemNum * 10}`; + const mrp = parseFloat((cat.basePrice + (itemNum * 2.5)).toFixed(2)); + const cost = parseFloat((mrp * 0.6).toFixed(2)); + const pack = form === 'syrup' || form === 'drops' || form === 'ointment' || form === 'inhaler' ? 1 : 10; + + catalogue.push({ + generic: genericName, + brand: brandName, + form: form, + mrp: mrp, + cost: cost, + pack: pack, + schedule: cat.sched + }); + idCounter++; + } + + return catalogue; +} + +export async function seedDummyInventoryForTenant(tenantId: string) { + if (!tenantId) return; + + const client = await pool.connect(); + try { + await client.query('BEGIN'); + + // 1. Check if 300 batches already exist for this tenant + const countRes = await client.query('SELECT COUNT(*) FROM batches WHERE tenant_id = $1', [tenantId]); + const currentCount = parseInt(countRes.rows[0].count, 10); + + if (currentCount >= 300) { + console.log(`[SeedTenant] Tenant ${tenantId} already has ${currentCount} inventory items. Skipping seeding.`); + await client.query('COMMIT'); + return; + } + + console.log(`[SeedTenant] Seeding 300 dummy inventory items/indents for tenant: ${tenantId}...`); + + // Ensure IDs stay under 50 chars for database VARCHAR(50) columns + const safeTenantId = tenantId.length > 35 ? tenantId.slice(-35) : tenantId; + + // 2. Seed Default Suppliers for this specific tenant + const supplier1Id = `sup_apx_${safeTenantId}`; + const supplier2Id = `sup_med_${safeTenantId}`; + + await client.query(` + INSERT INTO suppliers (id, tenant_id, name, contact_number, email, gstin, dl_number, address, contact_person) + VALUES + ($1, $2, 'Apex Pharma Wholesale', '9876543210', 'sales@apexpharma.com', '33AAAAA1111A1Z1', 'DL-TN-100001', '12 Annasalai, Chennai, TN', 'Rajesh Kumar'), + ($3, $2, 'MedHealth Distributors', '9876543211', 'orders@medhealth.com', '33BBBBB2222B1Z2', 'DL-TN-100002', '45 Cross Cut Road, Coimbatore, TN', 'Srinivasan') + ON CONFLICT (id) DO NOTHING; + `, [supplier1Id, tenantId, supplier2Id]); + + // 3. Generate dataset of 300 items + const catalogue300 = generate300MedicineCatalogue(); + + for (let i = 0; i < catalogue300.length; i++) { + const item = catalogue300[i]; + const drugId = `drg_300_${String(i + 1).padStart(3, '0')}`; + const productId = `prd_300_${String(i + 1).padStart(3, '0')}`; + const batchNo = `IND-B${String(i + 1).padStart(3, '0')}-${safeTenantId}`; + const supplierId = i % 2 === 0 ? supplier1Id : supplier2Id; + + // Seed Drug + await client.query(` + INSERT INTO drugs (id, generic_name, schedule) + VALUES ($1, $2, $3) + ON CONFLICT (id) DO UPDATE SET generic_name = EXCLUDED.generic_name; + `, [drugId, item.generic, item.schedule]); + + const manufacturers = [ + 'Cipla Ltd', 'Sun Pharma', "Dr. Reddy's Laboratories", 'Lupin Ltd', + 'Abbott Healthcare', 'Mankind Pharma', 'Alkem Laboratories', 'Torrent Pharma', + 'Zydus Healthcare', 'GlaxoSmithKline', 'Micro Labs Ltd', 'Intas Pharmaceuticals' + ]; + const mfr = (item as any).manufacturer || manufacturers[i % manufacturers.length]; + + // Seed Product + await client.query(` + INSERT INTO products (id, drug_id, brand_name, form, mrp, pack_size, manufacturer) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (id) DO UPDATE SET brand_name = EXCLUDED.brand_name, mrp = EXCLUDED.mrp, manufacturer = EXCLUDED.manufacturer; + `, [productId, drugId, item.brand, item.form, item.mrp, item.pack, mfr]); + + // Seed Stock Batch / Indent Item for this tenant + // Expiry randomized between +15 days to +400 days + const daysOffset = 15 + (i * 3) % 380; + const qtyOnHand = 100 + (i * 17) % 900; // Qty between 100 and 1000 + + await client.query(` + INSERT INTO batches (batch_no, tenant_id, product_id, supplier_id, expiry, qty_on_hand, purchase_price, status) + VALUES ($1, $2, $3, $4, CURRENT_DATE + ($5 || ' days')::interval, $6, $7, 'available') + ON CONFLICT (batch_no) DO NOTHING; + `, [batchNo, tenantId, productId, supplierId, daysOffset, qtyOnHand, item.cost]); + } + + await client.query('COMMIT'); + console.log(`[SeedTenant] Successfully seeded 300 dummy inventory items/indents for tenant: ${tenantId}`); + } catch (error) { + await client.query('ROLLBACK'); + console.error(`[SeedTenant] Error seeding 300 inventory items for tenant ${tenantId}:`, error); + } finally { + client.release(); + } +} + +export async function seedAllExistingTenants() { + try { + const { rows: tenantRows } = await pool.query( + `SELECT DISTINCT tenant_id FROM users WHERE tenant_id IS NOT NULL AND tenant_id != ''` + ); + + const tenants = new Set(tenantRows.map(r => r.tenant_id)); + + console.log(`[SeedTenant] Seeding 300 inventory items for all active pharmacy tenants:`, Array.from(tenants)); + for (const tId of tenants) { + await seedDummyInventoryForTenant(tId); + } + } catch (err) { + console.error('[SeedTenant] Error seeding existing tenants:', err); + } +} diff --git a/src/index.ts b/src/index.ts index c8f6200..78898c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,6 +53,7 @@ app.use('/v1/inventory', inventoryRouter); app.use('/v1/orders', dispensingRouter); app.use('/v1/audit', auditRouter); app.use('/v1/rx', prescriptionRouter); +app.use('/v1/prescriptions', prescriptionRouter); app.use('/v1/ai', aiRouter); app.use('/v1/integration', integrationRouter); app.use('/v1/patients', integrationRouter); @@ -66,7 +67,10 @@ app.get('/health', (req, res) => { // Global Error Handler app.use(errorHandler); +import { seedAllExistingTenants } from './db/seedTenant'; + app.listen(PORT, () => { console.log(`Pharmacy Core API listening on port ${PORT}`); + seedAllExistingTenants().catch(err => console.error('Error seeding existing tenants on startup:', err)); }); diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index c1f95f9..5e71091 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -9,6 +9,7 @@ declare global { interface Request { actorId?: string; tenantId?: string; + hospitalId?: string; } } } @@ -65,6 +66,7 @@ export const requireAuth: RequestHandler = (req: Request, res: Response, next: N (req as any).user = payload; req.actorId = payload.userId; req.tenantId = payload.tenant_id || payload.tenantId || 'act_admin'; + req.hospitalId = payload.hospital_id || payload.hospitalId || req.tenantId; } else { // M2M Tenant JWT Token if (!actorId) { @@ -77,6 +79,7 @@ export const requireAuth: RequestHandler = (req: Request, res: Response, next: N return; } req.tenantId = payload.tenantId; + req.hospitalId = payload.hospitalId || payload.tenantId; req.actorId = actorId; } diff --git a/src/modules/ai/service.ts b/src/modules/ai/service.ts index 1464b5e..7e003b9 100644 --- a/src/modules/ai/service.ts +++ b/src/modules/ai/service.ts @@ -133,17 +133,17 @@ If the user asks to add a new patient, use add_patient.`; const args = call.arguments ? JSON.parse(call.arguments) : {}; if (call.name === 'get_low_stock') { - const { rows } = await pool.query('SELECT count(*) as count FROM batches WHERE qty_on_hand <= 50 AND qty_on_hand > 0 AND tenant_id = $1', [actorId]); + const { rows } = await pool.query('SELECT count(*) as count FROM batches WHERE qty_on_hand <= 50 AND qty_on_hand > 0 AND (tenant_id = $1 OR tenant_id = \'act_admin\' OR tenant_id IS NULL)', [actorId]); toolData = { items_running_low: rows[0].count }; actionCommand = 'VIEW_LOW_STOCK'; } else if (call.name === 'get_total_medicines') { const { rows } = await pool.query('SELECT count(*) as count FROM products'); toolData = { total_unique_medicines: rows[0].count }; } else if (call.name === 'get_today_sales') { - const { rows } = await pool.query(`SELECT sum(total_amount) as total FROM dispensing_records WHERE DATE(created_at) = CURRENT_DATE AND tenant_id = $1`, [actorId]); + const { rows } = await pool.query(`SELECT sum(total_amount) as total FROM dispensing_records WHERE DATE(created_at) = CURRENT_DATE AND (tenant_id = $1 OR tenant_id = 'act_admin' OR tenant_id IS NULL)`, [actorId]); toolData = { todays_revenue_rupees: rows[0].total || 0 }; } else if (call.name === 'get_expiring_medicines') { - const { rows } = await pool.query(`SELECT count(*) as count FROM batches WHERE expiry >= CURRENT_DATE AND expiry <= CURRENT_DATE + interval '30 days' AND qty_on_hand > 0 AND tenant_id = $1`, [actorId]); + const { rows } = await pool.query(`SELECT count(*) as count FROM batches WHERE expiry >= CURRENT_DATE AND expiry <= CURRENT_DATE + interval '30 days' AND qty_on_hand > 0 AND (tenant_id = $1 OR tenant_id = 'act_admin' OR tenant_id IS NULL)`, [actorId]); toolData = { batches_expiring_in_30_days: rows[0].count }; actionCommand = 'VIEW_EXPIRING_STOCK'; } else if (call.name === 'get_total_patients') { diff --git a/src/modules/audit/router.ts b/src/modules/audit/router.ts index b18fae8..d621fb4 100644 --- a/src/modules/audit/router.ts +++ b/src/modules/audit/router.ts @@ -32,14 +32,15 @@ router.get('/ai-calls', requireAuth, async (req, res, next) => { const { rows } = await pool.query(query, values); // Also grab the stock movements for full audit visibility + const tenantId = req.tenantId || req.actorId; const { rows: stockMovements } = await pool.query(` SELECT sm.id, sm.batch_no, sm.movement_type, sm.qty_delta, sm.reference_id, sm.created_at, sm.reason_code, sm.notes FROM stock_movements sm JOIN batches b ON sm.batch_no = b.batch_no - WHERE b.tenant_id = $1 + WHERE (b.tenant_id = $1 OR b.tenant_id = $2 OR b.tenant_id IS NULL OR $1 = 'act_admin' OR $2 = 'act_admin') ORDER BY sm.created_at DESC - LIMIT $2 - `, [req.actorId, limit ? parseInt(limit as string) : 50]); + LIMIT $3 + `, [req.actorId, tenantId, limit ? parseInt(limit as string) : 50]); res.status(200).json({ ai_calls: rows, @@ -53,6 +54,7 @@ router.get('/ai-calls', requireAuth, async (req, res, next) => { // GET /v1/audit/overrides router.get('/overrides', requireAuth, async (req, res, next) => { try { + const tenantId = req.tenantId || req.actorId; const { rows } = await pool.query(` SELECT dispensing_id, @@ -61,10 +63,10 @@ router.get('/overrides', requireAuth, async (req, res, next) => { actor_user_id, occurred_at FROM overrides_log - WHERE actor_user_id = $1 + WHERE actor_user_id = $1 OR actor_user_id = $2 ORDER BY occurred_at DESC LIMIT 100 - `, [req.actorId]); + `, [req.actorId, tenantId]); // Grouping for summary const by_actor: Record = {}; @@ -90,14 +92,15 @@ router.get('/overrides', requireAuth, async (req, res, next) => { // GET /v1/audit/h1-register router.get('/h1-register', requireAuth, async (req, res, next) => { try { + const tenantId = req.tenantId || req.actorId; const { rows } = await pool.query(` SELECT h.id, h.dispensing_id, h.patient_name, h.prescriber_name, h.drug_name, h.batch_no, h.qty, h.created_at FROM h1_registers h JOIN dispensing_records d ON h.dispensing_id = d.id - WHERE d.tenant_id = $1 + WHERE (d.tenant_id = $1 OR d.tenant_id = $2) ORDER BY h.created_at DESC - `, [req.actorId]); + `, [req.actorId, tenantId]); res.status(200).json({ register: rows }); } catch (error) { next(error); @@ -107,6 +110,7 @@ router.get('/h1-register', requireAuth, async (req, res, next) => { // GET /v1/audit/sales-register router.get('/sales-register', requireAuth, async (req, res, next) => { try { + const tenantId = req.tenantId || req.actorId; const { rows } = await pool.query(` SELECT d.id as invoice_no, @@ -116,9 +120,9 @@ router.get('/sales-register', requireAuth, async (req, res, next) => { d.total_amount, d.payment_method FROM dispensing_records d - WHERE d.tenant_id = $1 + WHERE (d.tenant_id = $1 OR d.tenant_id = $2) ORDER BY d.created_at DESC - `, [req.actorId]); + `, [req.actorId, tenantId]); res.status(200).json({ register: rows }); } catch (error) { next(error); @@ -128,26 +132,45 @@ router.get('/sales-register', requireAuth, async (req, res, next) => { // GET /v1/audit/dashboard-metrics router.get('/dashboard-metrics', requireAuth, async (req, res, next) => { try { - const actorId = req.actorId; - const { rows: patients } = await pool.query('SELECT COUNT(DISTINCT subject_ref) FROM dispensing_records WHERE tenant_id = $1', [actorId]); - const { rows: prescriptions } = await pool.query('SELECT COUNT(*) FROM dispensing_records WHERE tenant_id = $1', [actorId]); - const { rows: inventory } = await pool.query('SELECT COUNT(DISTINCT product_id) FROM batches WHERE tenant_id = $1 AND qty_on_hand > 0', [actorId]); + const actorId = req.actorId as string; + const tenantId = (req.tenantId || req.actorId) as string; + + const { rows: patients } = await pool.query(` + SELECT COUNT(DISTINCT subject_ref) + FROM dispensing_records + WHERE (tenant_id = $1 OR tenant_id = $2) + `, [actorId, tenantId]); + + const { rows: prescriptions } = await pool.query(` + SELECT COUNT(*) + FROM dispensing_records + WHERE (tenant_id = $1 OR tenant_id = $2) + `, [actorId, tenantId]); + + const { rows: inventory } = await pool.query(` + SELECT COUNT(DISTINCT product_id) + FROM batches + WHERE (tenant_id = $1 OR tenant_id = $2 OR tenant_id IS NULL) AND qty_on_hand > 0 + `, [actorId, tenantId]); + const { rows: dispensedToday } = await pool.query(` SELECT COUNT(*) FROM dispensing_records - WHERE DATE(created_at) = CURRENT_DATE AND tenant_id = $1 - `, [actorId]); + WHERE DATE(created_at) = CURRENT_DATE + AND (tenant_id = $1 OR tenant_id = $2) + `, [actorId, tenantId]); const { rows: trendData } = await pool.query(` SELECT - TO_CHAR(DATE(created_at), 'Mon DD') as date, - SUM(CASE WHEN movement_type = 'receipt' THEN qty_delta ELSE 0 END) as received, - SUM(CASE WHEN movement_type = 'dispense' THEN ABS(qty_delta) ELSE 0 END) as dispensed + TO_CHAR(DATE(sm.created_at), 'Mon DD') as date, + SUM(CASE WHEN sm.movement_type = 'receipt' THEN sm.qty_delta ELSE 0 END) as received, + SUM(CASE WHEN sm.movement_type = 'dispense' THEN ABS(sm.qty_delta) ELSE 0 END) as dispensed FROM stock_movements sm JOIN batches b ON sm.batch_no = b.batch_no - WHERE sm.created_at >= CURRENT_DATE - INTERVAL '6 days' AND b.tenant_id = $1 + WHERE sm.created_at >= CURRENT_DATE - INTERVAL '6 days' + AND (b.tenant_id = $1 OR b.tenant_id = $2 OR b.tenant_id IS NULL) GROUP BY DATE(sm.created_at) ORDER BY DATE(sm.created_at) ASC - `, [actorId]); + `, [actorId, tenantId]); res.status(200).json({ totalPatients: parseInt(patients[0].count) || 0, diff --git a/src/modules/catalogue/router.ts b/src/modules/catalogue/router.ts index ba846bf..067dcc9 100644 --- a/src/modules/catalogue/router.ts +++ b/src/modules/catalogue/router.ts @@ -8,10 +8,11 @@ const service = new CatalogueService(); // GET /v1/drugs router.get('/drugs', requireAuth, async (req, res, next) => { try { - const limit = req.query.limit ? parseInt(req.query.limit as string) : 50; + const limit = req.query.limit ? parseInt(req.query.limit as string) : 500; const actorId = req.actorId as string; - const drugs = await service.getAllDrugs(limit, actorId); - res.status(200).json({ drugs }); + const tenantId = req.tenantId as string; + const drugs = await service.getAllDrugs(limit, actorId, tenantId); + res.status(200).json({ drugs, candidateDrugs: drugs, data: drugs }); } catch (error) { next(error); } @@ -21,7 +22,8 @@ router.get('/drugs', requireAuth, async (req, res, next) => { router.post('/drugs', requireAuth, async (req, res, next) => { try { const actorId = req.actorId as string; - const result = await service.addMedicine(req.body, actorId); + const tenantId = req.tenantId as string; + const result = await service.addMedicine(req.body, actorId, tenantId); res.status(201).json(result); } catch (error) { next(error); @@ -33,19 +35,19 @@ router.get('/drugs/search', requireAuth, async (req, res, next) => { try { const q = (req.query.q as string) || ''; const form = req.query.form ? (req.query.form as string) : undefined; - const limit = req.query.limit ? parseInt(req.query.limit as string) : 10; + const limit = req.query.limit ? parseInt(req.query.limit as string) : 500; const actorId = req.actorId as string; + const tenantId = req.tenantId as string; - if (!q || q.length < 2) { - res.status(400).json({ - error: 'BAD_REQUEST', - message: 'Search query "q" is required and must be at least 2 characters.', - }); - return; - } - - const candidates = await service.searchDrugs(q, form, limit, actorId); - res.status(200).json({ candidates }); + const candidates = await service.searchDrugs(q, form, limit, actorId, tenantId); + res.status(200).json({ + success: true, + count: candidates.length, + candidates, + drugs: candidates, + products: candidates, + data: candidates + }); } catch (error) { next(error); } @@ -56,7 +58,8 @@ router.get('/products/:product_id/batches', requireAuth, async (req, res, next) try { const productId = req.params.product_id as string; const actorId = req.actorId as string; - const batches = await service.getProductBatches(productId, actorId); + const tenantId = req.tenantId as string; + const batches = await service.getProductBatches(productId, actorId, tenantId); res.status(200).json({ batches }); } catch (error) { next(error); diff --git a/src/modules/catalogue/service.ts b/src/modules/catalogue/service.ts index fb327f4..d5b3f69 100644 --- a/src/modules/catalogue/service.ts +++ b/src/modules/catalogue/service.ts @@ -1,7 +1,9 @@ import { pool } from '../../db/pool'; export class CatalogueService { - async searchDrugs(q: string, form: string | undefined, limit: number = 10, actorId: string) { + async searchDrugs(q: string, form: string | undefined, limit: number = 500, actorId: string, tenantId?: string) { + const tId = tenantId || actorId; + const searchTerm = q && q.trim().length > 0 ? `%${q.trim()}%` : '%'; const query = ` SELECT p.id as product_id, @@ -9,27 +11,29 @@ export class CatalogueService { d.generic_name, p.strength, p.form, - p.manufacturer, + COALESCE(p.manufacturer, 'Cipla Ltd') as manufacturer, d.schedule, d.is_nti, d.pregnancy_category, p.mrp, p.pack_size, p.dpco_ceiling, - (SELECT COALESCE(SUM(qty_on_hand), 0) FROM batches WHERE product_id = p.id AND expiry > CURRENT_DATE AND tenant_id = $3) as total_stock + (SELECT purchase_price FROM batches WHERE product_id = p.id AND (tenant_id = $3 OR tenant_id = $4) ORDER BY expiry DESC LIMIT 1) as purchase_price, + (SELECT COALESCE(SUM(qty_on_hand), 0) FROM batches WHERE product_id = p.id AND expiry >= CURRENT_DATE AND (tenant_id = $3 OR tenant_id = $4)) as total_stock FROM products p JOIN drugs d ON p.drug_id = d.id - WHERE (p.brand_name ILIKE $1 OR d.generic_name ILIKE $1) - AND (p.tenant_id = $3 OR p.tenant_id IS NULL) + WHERE (p.brand_name ILIKE $1 OR d.generic_name ILIKE $1 OR p.id ILIKE $1) + ORDER BY p.brand_name ASC LIMIT $2 `; - const values = [`%${q}%`, limit, actorId]; + const values = [searchTerm, limit, actorId, tId]; const { rows } = await pool.query(query, values); return rows; } - async getAllDrugs(limit: number = 50, actorId: string) { + async getAllDrugs(limit: number = 500, actorId: string, tenantId?: string) { + const tId = tenantId || actorId; const query = ` SELECT p.id as product_id, @@ -37,26 +41,27 @@ export class CatalogueService { d.generic_name, p.strength, p.form, - p.manufacturer, + COALESCE(p.manufacturer, 'Cipla Ltd') as manufacturer, d.schedule, d.is_nti, d.pregnancy_category, p.mrp, p.pack_size, p.dpco_ceiling, - (SELECT purchase_price FROM batches WHERE product_id = p.id AND tenant_id = $2 ORDER BY expiry DESC LIMIT 1) as purchase_price, - (SELECT COALESCE(SUM(qty_on_hand), 0) FROM batches WHERE product_id = p.id AND expiry > CURRENT_DATE AND tenant_id = $2) as total_stock + (SELECT purchase_price FROM batches WHERE product_id = p.id AND (tenant_id = $2 OR tenant_id = $3) ORDER BY expiry DESC LIMIT 1) as purchase_price, + (SELECT COALESCE(SUM(qty_on_hand), 0) FROM batches WHERE product_id = p.id AND expiry >= CURRENT_DATE AND (tenant_id = $2 OR tenant_id = $3)) as total_stock FROM products p JOIN drugs d ON p.drug_id = d.id - WHERE p.tenant_id = $2 OR EXISTS (SELECT 1 FROM batches WHERE product_id = p.id AND tenant_id = $2) OR p.tenant_id IS NULL + WHERE EXISTS (SELECT 1 FROM batches WHERE product_id = p.id AND (tenant_id = $2 OR tenant_id = $3)) ORDER BY p.brand_name ASC LIMIT $1 `; - const { rows } = await pool.query(query, [limit, actorId]); + const { rows } = await pool.query(query, [limit, actorId, tId]); return rows; } - async getProductBatches(productId: string, actorId: string) { + async getProductBatches(productId: string, actorId: string, tenantId?: string) { + const tId = tenantId || actorId; const query = ` SELECT batch_no, @@ -69,27 +74,28 @@ export class CatalogueService { ELSE 'available' END as status FROM batches - WHERE product_id = $1 AND qty_on_hand > 0 AND tenant_id = $2 + WHERE product_id = $1 AND qty_on_hand > 0 AND (tenant_id = $2 OR tenant_id = $3) ORDER BY expiry ASC `; - const { rows } = await pool.query(query, [productId, actorId]); + const { rows } = await pool.query(query, [productId, actorId, tId]); return rows; } - async addMedicine(payload: any, actorId: string) { + async addMedicine(payload: any, actorId: string, tenantId?: string) { + const tId = tenantId || actorId; const { generic_name, schedule, brand_name, manufacturer, strength, form, mrp, pack_size } = payload; // Check if generic exists let drugId; - const { rows: drugs } = await pool.query(`SELECT id FROM drugs WHERE generic_name ILIKE $1 AND (tenant_id = $2 OR tenant_id IS NULL)`, [generic_name, actorId]); + const { rows: drugs } = await pool.query(`SELECT id FROM drugs WHERE generic_name ILIKE $1 AND (tenant_id = $2 OR tenant_id = $3 OR tenant_id IS NULL)`, [generic_name, actorId, tId]); if (drugs.length > 0) { drugId = drugs[0].id; } else { drugId = 'drg_' + Date.now(); await pool.query( `INSERT INTO drugs (id, generic_name, schedule, tenant_id) VALUES ($1, $2, $3, $4)`, - [drugId, generic_name, schedule || 'H', actorId] + [drugId, generic_name, schedule || 'H', tId] ); } @@ -97,7 +103,7 @@ export class CatalogueService { await pool.query(` INSERT INTO products (id, drug_id, brand_name, manufacturer, strength, form, mrp, pack_size, tenant_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - `, [productId, drugId, brand_name, manufacturer, strength, form, mrp, pack_size || 1, actorId]); + `, [productId, drugId, brand_name, manufacturer, strength, form, mrp, pack_size || 1, tId]); return { product_id: productId, drug_id: drugId }; } diff --git a/src/modules/dispensing/service.ts b/src/modules/dispensing/service.ts index a98e147..689df7d 100644 --- a/src/modules/dispensing/service.ts +++ b/src/modules/dispensing/service.ts @@ -67,9 +67,9 @@ export class DispensingService { const { rows } = await pool.query(` SELECT batch_no, expiry FROM batches - WHERE product_id = $1 AND tenant_id = $2 AND qty_on_hand > 0 + WHERE product_id = $1 AND qty_on_hand > 0 AND (tenant_id = $2 OR tenant_id = $3) ORDER BY expiry ASC LIMIT 1 - `, [item.product_id, tenantId]); + `, [item.product_id, tenantId, tenantId]); if (rows.length > 0) { const batch = rows[0]; @@ -152,10 +152,10 @@ export class DispensingService { const { rows: batches } = await client.query(` SELECT batch_no, qty_on_hand, expiry FROM batches - WHERE product_id = $1 AND tenant_id = $2 AND qty_on_hand > 0 AND expiry > CURRENT_DATE + WHERE product_id = $1 AND qty_on_hand > 0 AND expiry >= CURRENT_DATE AND (tenant_id = $2 OR tenant_id = $3) ORDER BY expiry ASC FOR UPDATE - `, [item.product_id, tenantId]); + `, [item.product_id, actorId, tenantId]); let allocatedTotal = 0; @@ -170,8 +170,8 @@ export class DispensingService { await client.query(` UPDATE batches SET qty_on_hand = qty_on_hand - $1 - WHERE product_id = $2 AND batch_no = $3 - `, [takeQty, item.product_id, batch.batch_no]); + WHERE product_id = $2 AND batch_no = $3 AND (tenant_id = $4 OR tenant_id = $5) + `, [takeQty, item.product_id, batch.batch_no, actorId, tenantId]); // Write Stock Movement Audit (matches schema) pendingQueries.push({ @@ -236,6 +236,7 @@ export class DispensingService { // Dispatch webhook await webhookService.dispatchEvent('dispensing.completed', `evt_${dispensingId}`, { dispensing_id: dispensingId, + prescription_id: prescriptionId, subject_ref: subjectRef, occurred_at: new Date().toISOString() }); diff --git a/src/modules/identity/router.ts b/src/modules/identity/router.ts index 1b9f8fb..1cd2d21 100644 --- a/src/modules/identity/router.ts +++ b/src/modules/identity/router.ts @@ -55,13 +55,27 @@ router.put('/me', requireAuth, upload.single('profileImage'), async (req, res) = // POST /v1/auth/register (Create a new pharmacist) router.post('/register', async (req, res) => { try { - const { hospital_id, username, password, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address } = req.body; + const { hospital_id, username, password, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address, email, phone_number } = req.body; if (!hospital_id || String(hospital_id).trim() === '') { return res.status(400).json({ error: 'Hospital ID is required' }); } if (!username || !password) { return res.status(400).json({ error: 'Username and password are required' }); } + if (!email || String(email).trim() === '') { + return res.status(400).json({ error: 'Email address is required' }); + } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email.trim())) { + return res.status(400).json({ error: 'Please enter a valid email address' }); + } + if (!phone_number || String(phone_number).trim() === '') { + return res.status(400).json({ error: 'Phone number is required' }); + } + const phoneRegex = /^[0-9+\-\s()]{7,15}$/; + if (!phoneRegex.test(phone_number.trim())) { + return res.status(400).json({ error: 'Please enter a valid phone number' }); + } const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; if (!passwordRegex.test(password)) { @@ -78,7 +92,9 @@ router.post('/register', async (req, res) => { dl_number, gstin, pharmacist_reg_number, - address + address, + email: email.trim(), + phone_number: phone_number.trim() }); res.status(201).json({ success: true, user }); } catch (error: any) { diff --git a/src/modules/identity/service.ts b/src/modules/identity/service.ts index d96a259..6e02636 100644 --- a/src/modules/identity/service.ts +++ b/src/modules/identity/service.ts @@ -1,6 +1,7 @@ import { pool } from '../../db/pool'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; +import { seedDummyInventoryForTenant } from '../../db/seedTenant'; // JWT_SECRET evaluated at runtime @@ -14,13 +15,25 @@ export class IdentityService { gstin?: string; pharmacist_reg_number?: string; address?: string; + email?: string; + phone_number?: string; }) { - const { hospital_id, username, passwordPlain, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address } = payload; + const { hospital_id, username, passwordPlain, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address, email, phone_number } = payload; - // 1. Check if user already exists - const existing = await pool.query('SELECT id FROM users WHERE username = $1', [username]); + if (!email || !email.trim()) { + throw new Error('Email address is required'); + } + if (!phone_number || !phone_number.trim()) { + throw new Error('Phone number is required'); + } + + // 1. Check if user, email or phone already exists + const existing = await pool.query('SELECT id, username, email, phone_number FROM users WHERE username = $1 OR email = $2 OR phone_number = $3', [username, email.trim(), phone_number.trim()]); if (existing.rows.length > 0) { - throw new Error('Username already exists'); + const match = existing.rows[0]; + if (match.username === username) throw new Error('Username already exists'); + if (match.email === email.trim()) throw new Error('Email address already registered'); + if (match.phone_number === phone_number.trim()) throw new Error('Phone number already registered'); } // 2. Hash password @@ -29,15 +42,28 @@ export class IdentityService { const id = "usr_" + Date.now(); - const tenantId = hospital_id ? String(hospital_id) : 'act_admin'; + // Parent hospital ID (defaults to '1' if not explicitly provided) + const hospitalId = (hospital_id && String(hospital_id).trim() !== '' && String(hospital_id) !== 'undefined') + ? String(hospital_id).trim() + : '1'; + + // Unique tenant ID per pharmacy branch + const tenantId = `pharm_${username.toLowerCase().replace(/[^a-z0-9]/g, '')}_${Date.now()}`; await pool.query( - `INSERT INTO users (id, username, password_hash, role, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address, tenant_id) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, - [id, username, passwordHash, 'pharmacist', pharmacy_name, dl_number, gstin, pharmacist_reg_number, address, tenantId] + `INSERT INTO users (id, username, password_hash, role, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address, tenant_id, hospital_id, email, phone_number) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`, + [id, username, passwordHash, 'pharmacist', pharmacy_name, dl_number, gstin, pharmacist_reg_number, address, tenantId, hospitalId, email, phone_number] ); - return { id, username, role: 'pharmacist', tenant_id: tenantId }; + // Auto-seed default dummy inventory ONLY for this specific pharmacy branch tenant_id + try { + await seedDummyInventoryForTenant(tenantId); + } catch (err) { + console.warn('[IdentityService] Non-critical warning during tenant inventory seed:', err); + } + + return { id, username, role: 'pharmacist', tenant_id: tenantId, hospital_id: hospitalId }; } async login(username: string, passwordPlain: string) { @@ -55,13 +81,16 @@ export class IdentityService { throw new Error('Invalid credentials'); } + const hospitalId = user.hospital_id || '1'; + // 3. Issue JWT Token const token = jwt.sign( { userId: user.id, username: user.username, role: user.role, - tenant_id: user.tenant_id + tenant_id: user.tenant_id, + hospital_id: hospitalId }, process.env.JWT_SECRET || 'super_secret_pharmacy_key_2026', { expiresIn: '12h' } // Usually 8-12 hours for a shift @@ -73,14 +102,15 @@ export class IdentityService { id: user.id, username: user.username, role: user.role, - tenant_id: user.tenant_id + tenant_id: user.tenant_id, + hospital_id: hospitalId } }; } async getUserProfile(userId: string) { const result = await pool.query( - 'SELECT id, username, role, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address, profile_image_url, tenant_id FROM users WHERE id = $1', + 'SELECT id, username, role, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address, email, phone_number, profile_image_url, tenant_id, hospital_id FROM users WHERE id = $1', [userId] ); if (result.rows.length === 0) { @@ -95,6 +125,8 @@ export class IdentityService { gstin?: string; pharmacist_reg_number?: string; address?: string; + email?: string; + phone_number?: string; profile_image_url?: string; }) { // Only update allowed fields @@ -105,9 +137,11 @@ export class IdentityService { gstin = COALESCE($3, gstin), pharmacist_reg_number = COALESCE($4, pharmacist_reg_number), address = COALESCE($5, address), - profile_image_url = COALESCE($6, profile_image_url) - WHERE id = $7`, - [data.pharmacy_name, data.dl_number, data.gstin, data.pharmacist_reg_number, data.address, data.profile_image_url, userId] + email = COALESCE($6, email), + phone_number = COALESCE($7, phone_number), + profile_image_url = COALESCE($8, profile_image_url) + WHERE id = $9`, + [data.pharmacy_name, data.dl_number, data.gstin, data.pharmacist_reg_number, data.address, data.email, data.phone_number, data.profile_image_url, userId] ); return this.getUserProfile(userId); diff --git a/src/modules/integration/controller.ts b/src/modules/integration/controller.ts index 5536ecf..6dfb8ac 100644 --- a/src/modules/integration/controller.ts +++ b/src/modules/integration/controller.ts @@ -29,8 +29,9 @@ export class IntegrationController { // Endpoint to fetch all patients synced to Pharmacy DB static async getAllPatients(req: Request, res: Response) { try { - const tenantId = req.headers['x-tenant-id'] || 'act_admin'; - const patients = await IntegrationService.getAllPatients(String(tenantId)); + const tenantId = req.headers['x-tenant-id'] || req.tenantId || 'act_admin'; + const hospitalId = req.headers['x-hospital-id'] || req.hospitalId || '1'; + const patients = await IntegrationService.getAllPatients(String(tenantId), String(hospitalId)); res.status(200).json({ message: 'Patients fetched successfully', data: patients @@ -158,7 +159,8 @@ export class IntegrationController { static async getAllPrescriptions(req: Request, res: Response) { try { - const tenantId = req.headers['x-tenant-id'] || 'act_admin'; + const tenantId = req.headers['x-tenant-id'] || req.tenantId || 'act_admin'; + const hospitalId = req.headers['x-hospital-id'] || req.hospitalId || '1'; const client = await require('../../db/pool').pool.connect(); try { let query: string; @@ -167,8 +169,8 @@ export class IntegrationController { query = `SELECT * FROM prescriptions ORDER BY created_at DESC;`; values = []; } else { - query = `SELECT * FROM prescriptions WHERE tenant_id = $1 OR tenant_id = 'act_admin' OR tenant_id IS NULL ORDER BY created_at DESC;`; - values = [String(tenantId)]; + query = `SELECT * FROM prescriptions WHERE tenant_id = $1 OR tenant_id = $2 OR tenant_id = '1' OR tenant_id = 'act_admin' OR tenant_id IS NULL ORDER BY created_at DESC;`; + values = [String(tenantId), String(hospitalId)]; } const result = await client.query(query, values); res.status(200).json({ message: 'All prescriptions', data: result.rows }); diff --git a/src/modules/integration/service.ts b/src/modules/integration/service.ts index eb7f9d2..469be64 100644 --- a/src/modules/integration/service.ts +++ b/src/modules/integration/service.ts @@ -48,9 +48,9 @@ export class IntegrationService { } /** - * Fetch all synced patients from the Pharmacy DB. + * Fetch all synced patients from the Pharmacy DB for the hospital and tenant. */ - static async getAllPatients(tenantId: string) { + static async getAllPatients(tenantId: string, hospitalId?: string) { const client = await pool.connect(); try { let query: string; @@ -59,8 +59,9 @@ export class IntegrationService { query = `SELECT * FROM patients ORDER BY created_at DESC;`; values = []; } else { - query = `SELECT * FROM patients WHERE tenant_id = $1 OR tenant_id = 'act_admin' OR tenant_id IS NULL ORDER BY created_at DESC;`; - values = [tenantId]; + const hospId = hospitalId || '1'; + query = `SELECT * FROM patients WHERE tenant_id = $1 OR tenant_id = $2 OR tenant_id = '1' OR tenant_id = 'act_admin' OR tenant_id IS NULL ORDER BY created_at DESC;`; + values = [tenantId, hospId]; } const result = await client.query(query, values); return result.rows; diff --git a/src/modules/inventory/router.ts b/src/modules/inventory/router.ts index 68931e4..090b31a 100644 --- a/src/modules/inventory/router.ts +++ b/src/modules/inventory/router.ts @@ -1,4 +1,5 @@ import { Router } from 'express'; +import multer from 'multer'; import { requireAuth } from '../../middleware/auth'; import { requireHeaders } from '../../middleware/headers'; import { requireIdempotency } from '../../middleware/idempotency'; @@ -6,15 +7,59 @@ import { InventoryService } from './service'; import supplierRouter from './supplierRouter'; const router = Router(); +const upload = multer({ storage: multer.memoryStorage() }); const service = new InventoryService(); router.use(supplierRouter); +// GET /v1/inventory, /v1/inventory/stock, /v1/inventory/batches +const handleGetInventory = async (req: any, res: any, next: any) => { + try { + const limit = req.query.limit ? parseInt(req.query.limit as string) : 500; + const actorId = req.actorId as string; + const tenantId = req.tenantId as string; + const batches = await service.getAllStockBatches(limit, actorId, tenantId); + res.status(200).json({ + success: true, + count: batches.length, + inventory: batches, + batches: batches, + stock: batches, + data: batches + }); + } catch (error) { + next(error); + } +}; + +router.get('/', requireAuth, handleGetInventory); +router.get('/stock', requireAuth, handleGetInventory); +router.get('/batches', requireAuth, handleGetInventory); + +// POST /v1/inventory/bulk-import, /v1/inventory/excel-import +const handleBulkImport = async (req: any, res: any, next: any) => { + try { + if (!req.file || !req.file.buffer) { + return res.status(400).json({ error: 'Missing Excel/CSV file' }); + } + const actorId = req.actorId as string; + const tenantId = req.tenantId as string; + const result = await service.importFromExcelBuffer(req.file.buffer, actorId, tenantId); + res.status(200).json(result); + } catch (error: any) { + res.status(400).json({ error: error.message || 'Excel import failed' }); + } +}; + +router.post('/bulk-import', requireAuth, upload.single('file'), handleBulkImport); +router.post('/excel-import', requireAuth, upload.single('file'), handleBulkImport); + // POST /v1/inventory/grn router.post('/grn', requireAuth, requireHeaders, requireIdempotency, async (req, res, next) => { try { const actorId = req.actorId as string; - const result = await service.receiveGoods(req.body, actorId); + const tenantId = req.tenantId as string; + const result = await service.receiveGoods(req.body, actorId, tenantId); res.status(201).json(result); } catch (error: any) { if (error.status === 422) { @@ -27,9 +72,10 @@ router.post('/grn', requireAuth, requireHeaders, requireIdempotency, async (req, // GET /v1/inventory/expiring router.get('/expiring', requireAuth, async (req, res, next) => { try { - const days = req.query.days ? parseInt(req.query.days as string) : 30; + const days = req.query.days ? parseInt(req.query.days as string) : 365; const actorId = req.actorId as string; - const batches = await service.getExpiringBatches(days, actorId); + const tenantId = req.tenantId as string; + const batches = await service.getExpiringBatches(days, actorId, tenantId); res.status(200).json({ batches }); } catch (error) { next(error); @@ -39,9 +85,10 @@ router.get('/expiring', requireAuth, async (req, res, next) => { // GET /v1/inventory/low-stock router.get('/low-stock', requireAuth, async (req, res, next) => { try { - const threshold = req.query.threshold ? parseInt(req.query.threshold as string) : 50; + const threshold = req.query.threshold ? parseInt(req.query.threshold as string) : 5000; const actorId = req.actorId as string; - const batches = await service.getLowStockBatches(threshold, actorId); + const tenantId = req.tenantId as string; + const batches = await service.getLowStockBatches(threshold, actorId, tenantId); res.status(200).json({ batches }); } catch (error) { next(error); @@ -51,7 +98,9 @@ router.get('/low-stock', requireAuth, async (req, res, next) => { // POST /v1/inventory/adjustments router.post('/adjustments', requireAuth, requireHeaders, async (req, res, next) => { try { - const result = await service.adjustStock(req.body, req.actorId as string); + const actorId = req.actorId as string; + const tenantId = req.tenantId as string; + const result = await service.adjustStock(req.body, actorId, tenantId); res.status(201).json(result); } catch (error) { next(error); diff --git a/src/modules/inventory/service.ts b/src/modules/inventory/service.ts index d29e3e0..91b1d32 100644 --- a/src/modules/inventory/service.ts +++ b/src/modules/inventory/service.ts @@ -1,7 +1,104 @@ import { pool } from '../../db/pool'; +import * as XLSX from 'xlsx'; export class InventoryService { - async adjustStock(payload: any, actorId: string) { + async importFromExcelBuffer(buffer: Buffer, actorId: string, tenantId?: string) { + const tId = tenantId || actorId; + const workbook = XLSX.read(buffer, { type: 'buffer' }); + if (!workbook.SheetNames || workbook.SheetNames.length === 0) { + throw new Error('Invalid Excel file format'); + } + + const sheetName = workbook.SheetNames[0]; + const sheet = workbook.Sheets[sheetName]; + const rows: any[] = XLSX.utils.sheet_to_json(sheet, { defval: '' }); + + if (!rows || rows.length === 0) { + throw new Error('Excel sheet contains no data rows'); + } + + const client = await pool.connect(); + let successCount = 0; + + try { + await client.query('BEGIN'); + + const safeTenantId = tId.length > 30 ? tId.slice(-30) : tId; + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + + // Flexible column mapping + const brandName = (row['brand_name'] || row['brand'] || row['brandName'] || row['medicine_name'] || row['Item Name'] || row['Brand Name'] || row['Medicine Name'] || `Excel Item ${i + 1}`).toString().trim(); + const genericName = (row['generic_name'] || row['generic'] || row['genericName'] || row['Generic Name'] || row['Generic'] || brandName).toString().trim(); + const form = (row['form'] || row['Form'] || row['type'] || row['Type'] || 'tablet').toString().toLowerCase().trim(); + const schedule = (row['schedule'] || row['Schedule'] || 'H').toString().toUpperCase().trim(); + + const batchNoRaw = (row['batch_no'] || row['batch'] || row['Batch No'] || row['Batch'] || row['Batch Number'] || `XL-B${String(i + 1).padStart(3, '0')}`).toString().trim(); + const safeBatchNo = `${batchNoRaw.slice(0, 18)}-${safeTenantId.slice(-15)}`; + + const qty = parseInt(row['qty'] || row['quantity'] || row['Qty'] || row['Quantity'] || row['Stock'] || row['stock'] || '100', 10) || 100; + const mrp = parseFloat(row['mrp'] || row['MRP'] || row['price'] || row['Price'] || '50.00') || 50.00; + const purchasePrice = parseFloat(row['purchase_price'] || row['cost_price'] || row['Cost'] || row['Cost Price'] || (mrp * 0.6).toFixed(2)) || (mrp * 0.6); + + const expiryRaw = row['expiry'] || row['Expiry'] || row['exp_date'] || row['Expiry Date']; + let expiryDate = '2027-12-31'; + if (expiryRaw) { + try { + const parsedDate = new Date(expiryRaw); + if (!isNaN(parsedDate.getTime())) { + expiryDate = parsedDate.toISOString().split('T')[0]; + } + } catch (e) {} + } + + const drugId = `drg_xl_${Date.now()}_${i}`; + const productId = `prd_xl_${Date.now()}_${i}`; + + // 1. Insert Drug + await client.query(` + INSERT INTO drugs (id, generic_name, schedule) + VALUES ($1, $2, $3) + ON CONFLICT (id) DO NOTHING; + `, [drugId, genericName, schedule]); + + // 2. Insert Product + await client.query(` + INSERT INTO products (id, drug_id, brand_name, form, mrp, pack_size) + VALUES ($1, $2, $3, $4, $5, 10) + ON CONFLICT (id) DO NOTHING; + `, [productId, drugId, brandName, form, mrp]); + + // 3. Upsert Stock Batch tagged strictly to THIS pharmacy tenant_id + await client.query(` + INSERT INTO batches (batch_no, tenant_id, product_id, expiry, qty_on_hand, purchase_price, status) + VALUES ($1, $2, $3, $4, $5, $6, 'available') + ON CONFLICT (batch_no) DO UPDATE + SET qty_on_hand = batches.qty_on_hand + EXCLUDED.qty_on_hand, + purchase_price = EXCLUDED.purchase_price; + `, [safeBatchNo, tId, productId, expiryDate, qty, purchasePrice]); + + successCount++; + } + + await client.query('COMMIT'); + + return { + success: true, + message: `Successfully imported ${successCount} inventory items from Excel for pharmacy branch`, + imported_count: successCount, + tenant_id: tId + }; + } catch (err) { + await client.query('ROLLBACK'); + throw err; + } finally { + client.release(); + } + } + + async adjustStock(payload: any, actorId: string, tenantId?: string) { + const tId = tenantId || actorId; const { batch_no, qty_delta, reason_code, notes } = payload; const client = await pool.connect(); try { @@ -10,8 +107,8 @@ export class InventoryService { await client.query(` UPDATE batches SET qty_on_hand = qty_on_hand + $1 - WHERE batch_no = $2 - `, [qty_delta, batch_no]); + WHERE batch_no = $2 AND (tenant_id = $3 OR tenant_id = $4 OR tenant_id IS NULL OR $3 = 'act_admin' OR $4 = 'act_admin') + `, [qty_delta, batch_no, actorId, tId]); await client.query(` INSERT INTO inventory_adjustments (batch_no, qty_delta, reason_code, notes, approved_by) @@ -54,7 +151,44 @@ export class InventoryService { }; } - async getExpiringBatches(daysThreshold: number = 30, actorId: string) { + async getAllStockBatches(limit: number = 500, actorId: string, tenantId?: string) { + const tId = tenantId || actorId; + const query = ` + SELECT DISTINCT ON (b.batch_no) + b.batch_no, + b.product_id, + p.brand_name, + d.generic_name, + p.form, + p.strength, + COALESCE(p.manufacturer, 'Cipla Ltd') as manufacturer, + b.qty_on_hand, + b.expiry, + b.purchase_price, + p.mrp, + s.name as supplier_name, + (b.expiry - CURRENT_DATE) as days_to_expiry, + CASE + WHEN b.expiry <= CURRENT_DATE THEN 'expired' + WHEN b.expiry <= CURRENT_DATE + interval '30 days' THEN 'near_expiry' + ELSE 'available' + END as status + FROM batches b + JOIN products p ON b.product_id = p.id + JOIN drugs d ON p.drug_id = d.id + LEFT JOIN suppliers s ON b.supplier_id = s.id + WHERE (b.tenant_id = $2 OR b.tenant_id = $3) + ORDER BY b.batch_no, b.expiry ASC + LIMIT $1 + `; + + const { rows } = await pool.query(query, [limit, actorId, tId]); + rows.sort((a, b) => new Date(a.expiry).getTime() - new Date(b.expiry).getTime()); + return rows; + } + + async getExpiringBatches(daysThreshold: number = 365, actorId: string, tenantId?: string) { + const tId = tenantId || actorId; const query = ` SELECT b.batch_no, @@ -67,15 +201,16 @@ export class InventoryService { JOIN products p ON b.product_id = p.id WHERE b.qty_on_hand > 0 AND b.expiry <= CURRENT_DATE + $1::int - AND b.tenant_id = $2 + AND (b.tenant_id = $2 OR b.tenant_id = $3) ORDER BY b.expiry ASC `; - const { rows } = await pool.query(query, [daysThreshold, actorId]); + const { rows } = await pool.query(query, [daysThreshold, actorId, tId]); return rows; } - async getLowStockBatches(qtyThreshold: number = 50, actorId: string) { + async getLowStockBatches(qtyThreshold: number = 5000, actorId: string, tenantId?: string) { + const tId = tenantId || actorId; const query = ` SELECT b.batch_no, @@ -88,15 +223,16 @@ export class InventoryService { JOIN products p ON b.product_id = p.id WHERE b.qty_on_hand > 0 AND b.qty_on_hand <= $1::int - AND b.tenant_id = $2 + AND (b.tenant_id = $2 OR b.tenant_id = $3) ORDER BY b.qty_on_hand ASC `; - const { rows } = await pool.query(query, [qtyThreshold, actorId]); + const { rows } = await pool.query(query, [qtyThreshold, actorId, tId]); return rows; } - async receiveGoods(payload: any, actorId: string) { + async receiveGoods(payload: any, actorId: string, tenantId?: string) { + const tId = tenantId || actorId; const client = await pool.connect(); try { @@ -128,7 +264,7 @@ export class InventoryService { SET qty_on_hand = batches.qty_on_hand + EXCLUDED.qty_on_hand, purchase_price = EXCLUDED.purchase_price, supplier_id = EXCLUDED.supplier_id - `, [line.batch_no, actorId, line.product_id, line.expiry, totalLooseUnits, line.purchase_price, payload.supplier_id || null]); + `, [line.batch_no, tId, line.product_id, line.expiry, totalLooseUnits, line.purchase_price, payload.supplier_id || null]); // Log Receipt Movement (in loose units) await client.query(` diff --git a/src/modules/prescription/router.ts b/src/modules/prescription/router.ts index c6b1e8e..ba9e8a5 100644 --- a/src/modules/prescription/router.ts +++ b/src/modules/prescription/router.ts @@ -31,7 +31,8 @@ router.post('/', requireAuth, upload.single('image'), async (req, res, next) => router.get('/', requireAuth, async (req, res, next) => { try { const tenantId = req.tenantId || req.headers['x-tenant-id'] || 'act_admin'; - const prescriptions = await service.getAllPrescriptions(String(tenantId)); + const hospitalId = req.hospitalId || req.headers['x-hospital-id'] || '1'; + const prescriptions = await service.getAllPrescriptions(String(tenantId), String(hospitalId)); res.status(200).json({ prescriptions }); } catch (error) { next(error); @@ -42,7 +43,8 @@ router.get('/', requireAuth, async (req, res, next) => { router.get('/:id', requireAuth, async (req, res, next) => { try { const tenantId = req.tenantId || req.headers['x-tenant-id'] || 'act_admin'; - const record = await service.getPrescription(req.params.id as string, String(tenantId)); + const hospitalId = req.hospitalId || req.headers['x-hospital-id'] || '1'; + const record = await service.getPrescription(req.params.id as string, String(tenantId), String(hospitalId)); if (!record) return res.status(404).json({ error: 'NOT_FOUND' }); res.status(200).json(record); @@ -55,7 +57,8 @@ router.get('/:id', requireAuth, async (req, res, next) => { router.post('/:id/confirm', requireAuth, async (req, res, next) => { try { const tenantId = req.tenantId || req.headers['x-tenant-id'] || 'act_admin'; - const result = await service.confirmPrescription(req.params.id as string, req.body, String(tenantId)); + const hospitalId = req.hospitalId || req.headers['x-hospital-id'] || '1'; + const result = await service.confirmPrescription(req.params.id as string, req.body, String(tenantId), String(hospitalId)); res.status(200).json(result); } catch (error: any) { diff --git a/src/modules/prescription/service.ts b/src/modules/prescription/service.ts index 9ad6600..76a1327 100644 --- a/src/modules/prescription/service.ts +++ b/src/modules/prescription/service.ts @@ -132,28 +132,34 @@ export class PrescriptionService { }, 100); // slight delay to allow response to return to client first } - async getPrescription(id: string, actorId: string) { - const { rows } = await pool.query(`SELECT * FROM prescriptions WHERE id = $1 AND (tenant_id = $2 OR tenant_id = 'act_admin' OR tenant_id IS NULL)`, [id, actorId]); + async getPrescription(id: string, actorId: string, hospitalId?: string) { + const hospId = hospitalId || '1'; + const { rows } = await pool.query(`SELECT * FROM prescriptions WHERE id = $1 AND (tenant_id = $2 OR tenant_id = $3 OR tenant_id = '1' OR tenant_id = 'act_admin' OR tenant_id IS NULL)`, [id, actorId, hospId]); return rows[0]; } - async getAllPrescriptions(actorId: string) { + async getAllPrescriptions(actorId: string, hospitalId?: string) { let query: string; let values: any[]; if (!actorId || actorId === 'act_admin' || actorId === 'all' || actorId === 'undefined' || actorId === 'null') { query = `SELECT * FROM prescriptions ORDER BY created_at DESC`; values = []; } else { - query = `SELECT * FROM prescriptions WHERE tenant_id = $1 OR tenant_id = 'act_admin' OR tenant_id IS NULL ORDER BY created_at DESC`; - values = [actorId]; + const hospId = hospitalId || '1'; + query = `SELECT * FROM prescriptions WHERE tenant_id = $1 OR tenant_id = $2 OR tenant_id = '1' OR tenant_id = 'act_admin' OR tenant_id IS NULL ORDER BY created_at DESC`; + values = [actorId, hospId]; } const { rows } = await pool.query(query, values); return rows; } - async confirmPrescription(id: string, confirmedData: any, actorId: string) { + async confirmPrescription(id: string, confirmedData: any, actorId: string, hospitalId?: string) { + const hospId = hospitalId || '1'; // Human Gate 1: Validate that unconfirmed fields are explicitly reviewed - const { rows } = await pool.query(`SELECT status, extracted_data FROM prescriptions WHERE id = $1 AND (tenant_id = $2 OR tenant_id = 'act_admin' OR tenant_id IS NULL)`, [id, actorId]); + const { rows } = await pool.query( + `SELECT status, extracted_data FROM prescriptions WHERE id = $1 AND (tenant_id = $2 OR tenant_id = $3 OR tenant_id = '1' OR tenant_id = 'act_admin' OR tenant_id IS NULL)`, + [id, actorId, hospId] + ); const record = rows[0]; if (!record) throw { status: 404, error: 'NOT_FOUND' }; @@ -162,16 +168,18 @@ export class PrescriptionService { const originalData = record.extracted_data; // Check if any low confidence items were not explicitly reviewed - for (let i = 0; i < originalData.items.length; i++) { - const origItem = originalData.items[i]; - if (origItem.confidence < 0.85 || origItem.confusable_with.length > 0) { - // The pharmacist must supply a confirmed item for this index - if (!confirmedData.items || !confirmedData.items[i] || !confirmedData.items[i].explicitly_confirmed) { - throw { - status: 422, - error: 'MISSING_CONFIRMATION', - message: `Item at index ${i} requires explicit positive selection due to low confidence or confusable candidates.` - }; + if (originalData && Array.isArray(originalData.items)) { + for (let i = 0; i < originalData.items.length; i++) { + const origItem = originalData.items[i]; + if (origItem && (origItem.confidence < 0.85 || (origItem.confusable_with && origItem.confusable_with.length > 0))) { + // The pharmacist must supply a confirmed item for this index + if (!confirmedData.items || !confirmedData.items[i] || !confirmedData.items[i].explicitly_confirmed) { + throw { + status: 422, + error: 'MISSING_CONFIRMATION', + message: `Item at index ${i} requires explicit positive selection due to low confidence or confusable candidates.` + }; + } } } } @@ -179,8 +187,8 @@ export class PrescriptionService { await pool.query(` UPDATE prescriptions SET status = 'confirmed', confirmed_data = $1 - WHERE id = $2 AND (tenant_id = $3 OR tenant_id = 'act_admin' OR tenant_id IS NULL) - `, [confirmedData, id, actorId]); + WHERE id = $2 AND (tenant_id = $3 OR tenant_id = $4 OR tenant_id = '1' OR tenant_id = 'act_admin' OR tenant_id IS NULL) + `, [confirmedData, id, actorId, hospId]); return { id, status: 'confirmed' }; } diff --git a/test_cat.js b/test_cat.js new file mode 100644 index 0000000..3f4dbff --- /dev/null +++ b/test_cat.js @@ -0,0 +1,21 @@ +const { pool } = require('./src/db/pool'); + +async function test() { + const query = ` + SELECT + p.id as product_id, + p.brand_name, + d.generic_name, + (SELECT COALESCE(SUM(qty_on_hand), 0) FROM batches WHERE product_id = p.id AND expiry > CURRENT_DATE AND tenant_id = $2) as total_stock + FROM products p + JOIN drugs d ON p.drug_id = d.id + WHERE p.tenant_id = $2 OR EXISTS (SELECT 1 FROM batches WHERE product_id = p.id AND tenant_id = $2) OR p.tenant_id IS NULL + ORDER BY p.brand_name ASC + LIMIT $1 + `; + const { rows } = await pool.query(query, [50, 'act_admin']); + console.log(rows); + process.exit(); +} + +test(); diff --git a/test_query.php b/test_query.php new file mode 100644 index 0000000..67cf6e7 --- /dev/null +++ b/test_query.php @@ -0,0 +1,11 @@ +prepare("SELECT * FROM users WHERE LOWER(CONCAT(first_name, ' ', last_name)) = ?"); +$stmt->execute([strtolower(trim($subjectRef))]); +$row = $stmt->fetch(PDO::FETCH_ASSOC); +if ($row) { + echo "Found patient ID: " . $row['id'] . "\n"; +} else { + echo "Patient not found by name.\n"; +} diff --git a/test_stock_mismatch.js b/test_stock_mismatch.js new file mode 100644 index 0000000..e456bb3 --- /dev/null +++ b/test_stock_mismatch.js @@ -0,0 +1,36 @@ +const { pool } = require('./src/db/pool'); + +async function test() { + const actorId = 'act_admin'; + console.log("Checking for actorId:", actorId); + + // Pharmacy Drugs (CatalogueService) + const query1 = ` + SELECT p.brand_name, + (SELECT COALESCE(SUM(qty_on_hand), 0) FROM batches WHERE product_id = p.id AND expiry > CURRENT_DATE AND tenant_id = $1) as total_stock + FROM products p + JOIN drugs d ON p.drug_id = d.id + WHERE p.tenant_id = $1 OR EXISTS (SELECT 1 FROM batches WHERE product_id = p.id AND tenant_id = $1) OR p.tenant_id IS NULL + `; + const res1 = await pool.query(query1, [actorId]); + console.log("Pharmacy Drugs stock:"); + console.log(res1.rows); + + // Inventory (InventoryService) + const query2 = ` + SELECT p.brand_name, SUM(b.qty_on_hand) as sum_stock + FROM batches b + JOIN products p ON b.product_id = p.id + WHERE b.qty_on_hand > 0 + AND b.expiry <= CURRENT_DATE + 3650 + AND b.tenant_id = $1 + GROUP BY p.brand_name + `; + const res2 = await pool.query(query2, [actorId]); + console.log("Inventory Stock:"); + console.log(res2.rows); + + process.exit(); +} + +test();