feat: add core backend modules, inventory services, and patch scripts for AI Pharmacy API
This commit is contained in:
@@ -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();
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
$pdo = new PDO('mysql:host=127.0.0.1;dbname=telegaruda_v2;charset=utf8', 'root', '');
|
||||||
|
$stmt = $pdo->query("SELECT * FROM patient_health ORDER BY id DESC LIMIT 1");
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
print_r($row);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
$pdo = new PDO('mysql:host=127.0.0.1;dbname=telegaruda_v2;charset=utf8', 'root', '');
|
||||||
|
$stmt = $pdo->query('SHOW COLUMNS FROM patient_health');
|
||||||
|
print_r($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
$pdo = new PDO('mysql:host=127.0.0.1;dbname=telegaruda_v2;charset=utf8', 'root', '');
|
||||||
|
$stmt = $pdo->query("SELECT * FROM users WHERE id = 18");
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
print_r($row);
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
+2
-1
@@ -27,7 +27,8 @@
|
|||||||
"openai": "^7.1.0",
|
"openai": "^7.1.0",
|
||||||
"pdfkit": "^0.19.1",
|
"pdfkit": "^0.19.1",
|
||||||
"pg": "^8.22.0",
|
"pg": "^8.22.0",
|
||||||
"redis": "^6.1.0"
|
"redis": "^6.1.0",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
|
|||||||
@@ -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.");
|
||||||
@@ -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");
|
||||||
@@ -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')");
|
||||||
@@ -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");
|
||||||
@@ -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");
|
||||||
@@ -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);
|
||||||
@@ -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 <span className="text-slate-600 font-medium text-[14px]">{date.toLocaleDateString()}</span>;`;
|
||||||
|
const replacementStr = `const formattedDate = isNaN(date.getTime()) ? info.getValue() : date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });\n return <span className="text-slate-600 font-medium text-[14px]">{formattedDate}</span>;`;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
@@ -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 = `<datalist id="product-options">`;
|
||||||
|
if (content.includes(targetSnippet)) {
|
||||||
|
const startIdx = content.indexOf('<div className="col-span-2">');
|
||||||
|
const endIdx = content.indexOf(') : (', startIdx);
|
||||||
|
|
||||||
|
if (startIdx !== -1 && endIdx !== -1) {
|
||||||
|
const originalBlock = content.substring(startIdx, endIdx).trim();
|
||||||
|
// remove trailing </div> from original block if present
|
||||||
|
const lastDivIdx = originalBlock.lastIndexOf('</div>');
|
||||||
|
const targetBlockToReplace = originalBlock.substring(0, lastDivIdx + 6);
|
||||||
|
|
||||||
|
const replacementBlock = `<div className="col-span-2 relative">
|
||||||
|
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Product / Medicine (Search by Brand, Generic or Product ID)</label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-slate-400 group-focus-within:text-blue-500 transition-colors z-10">
|
||||||
|
<Search size={18} strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={productSearchQuery || formData.product_id}
|
||||||
|
onFocus={() => 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' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 right-0 pr-4 flex items-center cursor-pointer text-slate-400 hover:text-slate-600 z-10"
|
||||||
|
onClick={() => setIsProductDropdownOpen(!isProductDropdownOpen)}
|
||||||
|
>
|
||||||
|
<ChevronDown size={18} strokeWidth={2} className={\`transition-transform \${isProductDropdownOpen ? 'rotate-180' : ''}\`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Searchable Custom Dropdown Menu */}
|
||||||
|
{isProductDropdownOpen && (
|
||||||
|
<div
|
||||||
|
className="absolute left-0 right-0 top-full mt-2 bg-white border border-slate-200 rounded-xl shadow-2xl z-50 max-h-64 overflow-y-auto divide-y divide-slate-100"
|
||||||
|
style={{ boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)' }}
|
||||||
|
>
|
||||||
|
{filteredProductOptions.length > 0 ? (
|
||||||
|
filteredProductOptions.map((d: any) => (
|
||||||
|
<div
|
||||||
|
key={d.product_id}
|
||||||
|
onClick={() => {
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-bold text-slate-800 text-[14px] group-hover:text-blue-600">
|
||||||
|
{d.brand_name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-500">
|
||||||
|
{d.generic_name || 'Generic Medicine'} {d.manufacturer ? \`• \${d.manufacturer}\` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-mono font-bold bg-slate-100 text-slate-700 group-hover:bg-blue-100 group-hover:text-blue-700 px-2.5 py-1 rounded-md transition-colors shrink-0">
|
||||||
|
{d.product_id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="p-4 text-center text-slate-400 text-sm">
|
||||||
|
No matching medicines found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
@@ -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<any[]>([]);`;
|
||||||
|
const stateReplace = `const [supplierOptions, setSupplierOptions] = useState<any[]>([]);
|
||||||
|
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 = `<div className="col-span-2">
|
||||||
|
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Product ID</label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-slate-400 group-focus-within:text-blue-500 transition-colors z-10">
|
||||||
|
<Package size={18} strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
list="product-options"
|
||||||
|
value={formData.product_id}
|
||||||
|
onChange={(e) => 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' }}
|
||||||
|
/>
|
||||||
|
<datalist id="product-options">
|
||||||
|
{drugOptions.map(d => (
|
||||||
|
<option key={d.product_id} value={d.product_id}>
|
||||||
|
{d.brand_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const newBlock = `<div className="col-span-2 relative">
|
||||||
|
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500" style={{ marginBottom: '8px' }}>Product / Medicine (Search by Brand, Generic or Product ID)</label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-slate-400 group-focus-within:text-blue-500 transition-colors z-10">
|
||||||
|
<Search size={18} strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={productSearchQuery || formData.product_id}
|
||||||
|
onFocus={() => 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' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 right-0 pr-4 flex items-center cursor-pointer text-slate-400 hover:text-slate-600 z-10"
|
||||||
|
onClick={() => setIsProductDropdownOpen(!isProductDropdownOpen)}
|
||||||
|
>
|
||||||
|
<ChevronDown size={18} strokeWidth={2} className={\`transition-transform \${isProductDropdownOpen ? 'rotate-180' : ''}\`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Searchable Custom Dropdown Menu */}
|
||||||
|
{isProductDropdownOpen && (
|
||||||
|
<div
|
||||||
|
className="absolute left-0 right-0 top-full mt-2 bg-white border border-slate-200 rounded-xl shadow-2xl z-50 max-h-64 overflow-y-auto divide-y divide-slate-100"
|
||||||
|
style={{ boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)' }}
|
||||||
|
>
|
||||||
|
{filteredProductOptions.length > 0 ? (
|
||||||
|
filteredProductOptions.map((d: any) => (
|
||||||
|
<div
|
||||||
|
key={d.product_id}
|
||||||
|
onClick={() => {
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-bold text-slate-800 text-[14px] group-hover:text-blue-600">
|
||||||
|
{d.brand_name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-500">
|
||||||
|
{d.generic_name || 'Generic Medicine'} {d.manufacturer ? \`• \${d.manufacturer}\` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-mono font-bold bg-slate-100 text-slate-700 group-hover:bg-blue-100 group-hover:text-blue-700 px-2.5 py-1 rounded-md transition-colors shrink-0">
|
||||||
|
{d.product_id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="p-4 text-center text-slate-400 text-sm">
|
||||||
|
No matching medicines found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
content = content.replace(oldBlock, newBlock);
|
||||||
|
fs.writeFileSync(path, content);
|
||||||
|
console.log("Patched Inventory.tsx successfully!");
|
||||||
|
} else {
|
||||||
|
console.log("Already patched Inventory.tsx");
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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<string>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,7 @@ app.use('/v1/inventory', inventoryRouter);
|
|||||||
app.use('/v1/orders', dispensingRouter);
|
app.use('/v1/orders', dispensingRouter);
|
||||||
app.use('/v1/audit', auditRouter);
|
app.use('/v1/audit', auditRouter);
|
||||||
app.use('/v1/rx', prescriptionRouter);
|
app.use('/v1/rx', prescriptionRouter);
|
||||||
|
app.use('/v1/prescriptions', prescriptionRouter);
|
||||||
app.use('/v1/ai', aiRouter);
|
app.use('/v1/ai', aiRouter);
|
||||||
app.use('/v1/integration', integrationRouter);
|
app.use('/v1/integration', integrationRouter);
|
||||||
app.use('/v1/patients', integrationRouter);
|
app.use('/v1/patients', integrationRouter);
|
||||||
@@ -66,7 +67,10 @@ app.get('/health', (req, res) => {
|
|||||||
// Global Error Handler
|
// Global Error Handler
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
import { seedAllExistingTenants } from './db/seedTenant';
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Pharmacy Core API listening on port ${PORT}`);
|
console.log(`Pharmacy Core API listening on port ${PORT}`);
|
||||||
|
seedAllExistingTenants().catch(err => console.error('Error seeding existing tenants on startup:', err));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ declare global {
|
|||||||
interface Request {
|
interface Request {
|
||||||
actorId?: string;
|
actorId?: string;
|
||||||
tenantId?: 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 as any).user = payload;
|
||||||
req.actorId = payload.userId;
|
req.actorId = payload.userId;
|
||||||
req.tenantId = payload.tenant_id || payload.tenantId || 'act_admin';
|
req.tenantId = payload.tenant_id || payload.tenantId || 'act_admin';
|
||||||
|
req.hospitalId = payload.hospital_id || payload.hospitalId || req.tenantId;
|
||||||
} else {
|
} else {
|
||||||
// M2M Tenant JWT Token
|
// M2M Tenant JWT Token
|
||||||
if (!actorId) {
|
if (!actorId) {
|
||||||
@@ -77,6 +79,7 @@ export const requireAuth: RequestHandler = (req: Request, res: Response, next: N
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
req.tenantId = payload.tenantId;
|
req.tenantId = payload.tenantId;
|
||||||
|
req.hospitalId = payload.hospitalId || payload.tenantId;
|
||||||
req.actorId = actorId;
|
req.actorId = actorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,17 +133,17 @@ If the user asks to add a new patient, use add_patient.`;
|
|||||||
const args = call.arguments ? JSON.parse(call.arguments) : {};
|
const args = call.arguments ? JSON.parse(call.arguments) : {};
|
||||||
|
|
||||||
if (call.name === 'get_low_stock') {
|
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 };
|
toolData = { items_running_low: rows[0].count };
|
||||||
actionCommand = 'VIEW_LOW_STOCK';
|
actionCommand = 'VIEW_LOW_STOCK';
|
||||||
} else if (call.name === 'get_total_medicines') {
|
} else if (call.name === 'get_total_medicines') {
|
||||||
const { rows } = await pool.query('SELECT count(*) as count FROM products');
|
const { rows } = await pool.query('SELECT count(*) as count FROM products');
|
||||||
toolData = { total_unique_medicines: rows[0].count };
|
toolData = { total_unique_medicines: rows[0].count };
|
||||||
} else if (call.name === 'get_today_sales') {
|
} 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 };
|
toolData = { todays_revenue_rupees: rows[0].total || 0 };
|
||||||
} else if (call.name === 'get_expiring_medicines') {
|
} 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 };
|
toolData = { batches_expiring_in_30_days: rows[0].count };
|
||||||
actionCommand = 'VIEW_EXPIRING_STOCK';
|
actionCommand = 'VIEW_EXPIRING_STOCK';
|
||||||
} else if (call.name === 'get_total_patients') {
|
} else if (call.name === 'get_total_patients') {
|
||||||
|
|||||||
+43
-20
@@ -32,14 +32,15 @@ router.get('/ai-calls', requireAuth, async (req, res, next) => {
|
|||||||
const { rows } = await pool.query(query, values);
|
const { rows } = await pool.query(query, values);
|
||||||
|
|
||||||
// Also grab the stock movements for full audit visibility
|
// Also grab the stock movements for full audit visibility
|
||||||
|
const tenantId = req.tenantId || req.actorId;
|
||||||
const { rows: stockMovements } = await pool.query(`
|
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
|
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
|
FROM stock_movements sm
|
||||||
JOIN batches b ON sm.batch_no = b.batch_no
|
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
|
ORDER BY sm.created_at DESC
|
||||||
LIMIT $2
|
LIMIT $3
|
||||||
`, [req.actorId, limit ? parseInt(limit as string) : 50]);
|
`, [req.actorId, tenantId, limit ? parseInt(limit as string) : 50]);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
ai_calls: rows,
|
ai_calls: rows,
|
||||||
@@ -53,6 +54,7 @@ router.get('/ai-calls', requireAuth, async (req, res, next) => {
|
|||||||
// GET /v1/audit/overrides
|
// GET /v1/audit/overrides
|
||||||
router.get('/overrides', requireAuth, async (req, res, next) => {
|
router.get('/overrides', requireAuth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
const tenantId = req.tenantId || req.actorId;
|
||||||
const { rows } = await pool.query(`
|
const { rows } = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
dispensing_id,
|
dispensing_id,
|
||||||
@@ -61,10 +63,10 @@ router.get('/overrides', requireAuth, async (req, res, next) => {
|
|||||||
actor_user_id,
|
actor_user_id,
|
||||||
occurred_at
|
occurred_at
|
||||||
FROM overrides_log
|
FROM overrides_log
|
||||||
WHERE actor_user_id = $1
|
WHERE actor_user_id = $1 OR actor_user_id = $2
|
||||||
ORDER BY occurred_at DESC
|
ORDER BY occurred_at DESC
|
||||||
LIMIT 100
|
LIMIT 100
|
||||||
`, [req.actorId]);
|
`, [req.actorId, tenantId]);
|
||||||
|
|
||||||
// Grouping for summary
|
// Grouping for summary
|
||||||
const by_actor: Record<string, number> = {};
|
const by_actor: Record<string, number> = {};
|
||||||
@@ -90,14 +92,15 @@ router.get('/overrides', requireAuth, async (req, res, next) => {
|
|||||||
// GET /v1/audit/h1-register
|
// GET /v1/audit/h1-register
|
||||||
router.get('/h1-register', requireAuth, async (req, res, next) => {
|
router.get('/h1-register', requireAuth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
const tenantId = req.tenantId || req.actorId;
|
||||||
const { rows } = await pool.query(`
|
const { rows } = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
h.id, h.dispensing_id, h.patient_name, h.prescriber_name, h.drug_name, h.batch_no, h.qty, h.created_at
|
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
|
FROM h1_registers h
|
||||||
JOIN dispensing_records d ON h.dispensing_id = d.id
|
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
|
ORDER BY h.created_at DESC
|
||||||
`, [req.actorId]);
|
`, [req.actorId, tenantId]);
|
||||||
res.status(200).json({ register: rows });
|
res.status(200).json({ register: rows });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -107,6 +110,7 @@ router.get('/h1-register', requireAuth, async (req, res, next) => {
|
|||||||
// GET /v1/audit/sales-register
|
// GET /v1/audit/sales-register
|
||||||
router.get('/sales-register', requireAuth, async (req, res, next) => {
|
router.get('/sales-register', requireAuth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
const tenantId = req.tenantId || req.actorId;
|
||||||
const { rows } = await pool.query(`
|
const { rows } = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
d.id as invoice_no,
|
d.id as invoice_no,
|
||||||
@@ -116,9 +120,9 @@ router.get('/sales-register', requireAuth, async (req, res, next) => {
|
|||||||
d.total_amount,
|
d.total_amount,
|
||||||
d.payment_method
|
d.payment_method
|
||||||
FROM dispensing_records d
|
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
|
ORDER BY d.created_at DESC
|
||||||
`, [req.actorId]);
|
`, [req.actorId, tenantId]);
|
||||||
res.status(200).json({ register: rows });
|
res.status(200).json({ register: rows });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -128,26 +132,45 @@ router.get('/sales-register', requireAuth, async (req, res, next) => {
|
|||||||
// GET /v1/audit/dashboard-metrics
|
// GET /v1/audit/dashboard-metrics
|
||||||
router.get('/dashboard-metrics', requireAuth, async (req, res, next) => {
|
router.get('/dashboard-metrics', requireAuth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const actorId = req.actorId;
|
const actorId = req.actorId as string;
|
||||||
const { rows: patients } = await pool.query('SELECT COUNT(DISTINCT subject_ref) FROM dispensing_records WHERE tenant_id = $1', [actorId]);
|
const tenantId = (req.tenantId || req.actorId) as string;
|
||||||
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 { 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(`
|
const { rows: dispensedToday } = await pool.query(`
|
||||||
SELECT COUNT(*) FROM dispensing_records
|
SELECT COUNT(*) FROM dispensing_records
|
||||||
WHERE DATE(created_at) = CURRENT_DATE AND tenant_id = $1
|
WHERE DATE(created_at) = CURRENT_DATE
|
||||||
`, [actorId]);
|
AND (tenant_id = $1 OR tenant_id = $2)
|
||||||
|
`, [actorId, tenantId]);
|
||||||
|
|
||||||
const { rows: trendData } = await pool.query(`
|
const { rows: trendData } = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
TO_CHAR(DATE(created_at), 'Mon DD') as date,
|
TO_CHAR(DATE(sm.created_at), 'Mon DD') as date,
|
||||||
SUM(CASE WHEN movement_type = 'receipt' THEN qty_delta ELSE 0 END) as received,
|
SUM(CASE WHEN sm.movement_type = 'receipt' THEN sm.qty_delta ELSE 0 END) as received,
|
||||||
SUM(CASE WHEN movement_type = 'dispense' THEN ABS(qty_delta) ELSE 0 END) as dispensed
|
SUM(CASE WHEN sm.movement_type = 'dispense' THEN ABS(sm.qty_delta) ELSE 0 END) as dispensed
|
||||||
FROM stock_movements sm
|
FROM stock_movements sm
|
||||||
JOIN batches b ON sm.batch_no = b.batch_no
|
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)
|
GROUP BY DATE(sm.created_at)
|
||||||
ORDER BY DATE(sm.created_at) ASC
|
ORDER BY DATE(sm.created_at) ASC
|
||||||
`, [actorId]);
|
`, [actorId, tenantId]);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
totalPatients: parseInt(patients[0].count) || 0,
|
totalPatients: parseInt(patients[0].count) || 0,
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ const service = new CatalogueService();
|
|||||||
// GET /v1/drugs
|
// GET /v1/drugs
|
||||||
router.get('/drugs', requireAuth, async (req, res, next) => {
|
router.get('/drugs', requireAuth, async (req, res, next) => {
|
||||||
try {
|
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 actorId = req.actorId as string;
|
||||||
const drugs = await service.getAllDrugs(limit, actorId);
|
const tenantId = req.tenantId as string;
|
||||||
res.status(200).json({ drugs });
|
const drugs = await service.getAllDrugs(limit, actorId, tenantId);
|
||||||
|
res.status(200).json({ drugs, candidateDrugs: drugs, data: drugs });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -21,7 +22,8 @@ router.get('/drugs', requireAuth, async (req, res, next) => {
|
|||||||
router.post('/drugs', requireAuth, async (req, res, next) => {
|
router.post('/drugs', requireAuth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const actorId = req.actorId as string;
|
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);
|
res.status(201).json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -33,19 +35,19 @@ router.get('/drugs/search', requireAuth, async (req, res, next) => {
|
|||||||
try {
|
try {
|
||||||
const q = (req.query.q as string) || '';
|
const q = (req.query.q as string) || '';
|
||||||
const form = req.query.form ? (req.query.form as string) : undefined;
|
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 actorId = req.actorId as string;
|
||||||
|
const tenantId = req.tenantId as string;
|
||||||
|
|
||||||
if (!q || q.length < 2) {
|
const candidates = await service.searchDrugs(q, form, limit, actorId, tenantId);
|
||||||
res.status(400).json({
|
res.status(200).json({
|
||||||
error: 'BAD_REQUEST',
|
success: true,
|
||||||
message: 'Search query "q" is required and must be at least 2 characters.',
|
count: candidates.length,
|
||||||
});
|
candidates,
|
||||||
return;
|
drugs: candidates,
|
||||||
}
|
products: candidates,
|
||||||
|
data: candidates
|
||||||
const candidates = await service.searchDrugs(q, form, limit, actorId);
|
});
|
||||||
res.status(200).json({ candidates });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -56,7 +58,8 @@ router.get('/products/:product_id/batches', requireAuth, async (req, res, next)
|
|||||||
try {
|
try {
|
||||||
const productId = req.params.product_id as string;
|
const productId = req.params.product_id as string;
|
||||||
const actorId = req.actorId 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 });
|
res.status(200).json({ batches });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { pool } from '../../db/pool';
|
import { pool } from '../../db/pool';
|
||||||
|
|
||||||
export class CatalogueService {
|
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 = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
p.id as product_id,
|
p.id as product_id,
|
||||||
@@ -9,27 +11,29 @@ export class CatalogueService {
|
|||||||
d.generic_name,
|
d.generic_name,
|
||||||
p.strength,
|
p.strength,
|
||||||
p.form,
|
p.form,
|
||||||
p.manufacturer,
|
COALESCE(p.manufacturer, 'Cipla Ltd') as manufacturer,
|
||||||
d.schedule,
|
d.schedule,
|
||||||
d.is_nti,
|
d.is_nti,
|
||||||
d.pregnancy_category,
|
d.pregnancy_category,
|
||||||
p.mrp,
|
p.mrp,
|
||||||
p.pack_size,
|
p.pack_size,
|
||||||
p.dpco_ceiling,
|
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
|
FROM products p
|
||||||
JOIN drugs d ON p.drug_id = d.id
|
JOIN drugs d ON p.drug_id = d.id
|
||||||
WHERE (p.brand_name ILIKE $1 OR d.generic_name ILIKE $1)
|
WHERE (p.brand_name ILIKE $1 OR d.generic_name ILIKE $1 OR p.id ILIKE $1)
|
||||||
AND (p.tenant_id = $3 OR p.tenant_id IS NULL)
|
ORDER BY p.brand_name ASC
|
||||||
LIMIT $2
|
LIMIT $2
|
||||||
`;
|
`;
|
||||||
const values = [`%${q}%`, limit, actorId];
|
const values = [searchTerm, limit, actorId, tId];
|
||||||
|
|
||||||
const { rows } = await pool.query(query, values);
|
const { rows } = await pool.query(query, values);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllDrugs(limit: number = 50, actorId: string) {
|
async getAllDrugs(limit: number = 500, actorId: string, tenantId?: string) {
|
||||||
|
const tId = tenantId || actorId;
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
p.id as product_id,
|
p.id as product_id,
|
||||||
@@ -37,26 +41,27 @@ export class CatalogueService {
|
|||||||
d.generic_name,
|
d.generic_name,
|
||||||
p.strength,
|
p.strength,
|
||||||
p.form,
|
p.form,
|
||||||
p.manufacturer,
|
COALESCE(p.manufacturer, 'Cipla Ltd') as manufacturer,
|
||||||
d.schedule,
|
d.schedule,
|
||||||
d.is_nti,
|
d.is_nti,
|
||||||
d.pregnancy_category,
|
d.pregnancy_category,
|
||||||
p.mrp,
|
p.mrp,
|
||||||
p.pack_size,
|
p.pack_size,
|
||||||
p.dpco_ceiling,
|
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 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) as total_stock
|
(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
|
FROM products p
|
||||||
JOIN drugs d ON p.drug_id = d.id
|
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
|
ORDER BY p.brand_name ASC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
`;
|
`;
|
||||||
const { rows } = await pool.query(query, [limit, actorId]);
|
const { rows } = await pool.query(query, [limit, actorId, tId]);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProductBatches(productId: string, actorId: string) {
|
async getProductBatches(productId: string, actorId: string, tenantId?: string) {
|
||||||
|
const tId = tenantId || actorId;
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
batch_no,
|
batch_no,
|
||||||
@@ -69,27 +74,28 @@ export class CatalogueService {
|
|||||||
ELSE 'available'
|
ELSE 'available'
|
||||||
END as status
|
END as status
|
||||||
FROM batches
|
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
|
ORDER BY expiry ASC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const { rows } = await pool.query(query, [productId, actorId]);
|
const { rows } = await pool.query(query, [productId, actorId, tId]);
|
||||||
return rows;
|
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;
|
const { generic_name, schedule, brand_name, manufacturer, strength, form, mrp, pack_size } = payload;
|
||||||
|
|
||||||
// Check if generic exists
|
// Check if generic exists
|
||||||
let drugId;
|
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) {
|
if (drugs.length > 0) {
|
||||||
drugId = drugs[0].id;
|
drugId = drugs[0].id;
|
||||||
} else {
|
} else {
|
||||||
drugId = 'drg_' + Date.now();
|
drugId = 'drg_' + Date.now();
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`INSERT INTO drugs (id, generic_name, schedule, tenant_id) VALUES ($1, $2, $3, $4)`,
|
`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(`
|
await pool.query(`
|
||||||
INSERT INTO products (id, drug_id, brand_name, manufacturer, strength, form, mrp, pack_size, tenant_id)
|
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)
|
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 };
|
return { product_id: productId, drug_id: drugId };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ export class DispensingService {
|
|||||||
const { rows } = await pool.query(`
|
const { rows } = await pool.query(`
|
||||||
SELECT batch_no, expiry
|
SELECT batch_no, expiry
|
||||||
FROM batches
|
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
|
ORDER BY expiry ASC LIMIT 1
|
||||||
`, [item.product_id, tenantId]);
|
`, [item.product_id, tenantId, tenantId]);
|
||||||
|
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
const batch = rows[0];
|
const batch = rows[0];
|
||||||
@@ -152,10 +152,10 @@ export class DispensingService {
|
|||||||
const { rows: batches } = await client.query(`
|
const { rows: batches } = await client.query(`
|
||||||
SELECT batch_no, qty_on_hand, expiry
|
SELECT batch_no, qty_on_hand, expiry
|
||||||
FROM batches
|
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
|
ORDER BY expiry ASC
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
`, [item.product_id, tenantId]);
|
`, [item.product_id, actorId, tenantId]);
|
||||||
|
|
||||||
let allocatedTotal = 0;
|
let allocatedTotal = 0;
|
||||||
|
|
||||||
@@ -170,8 +170,8 @@ export class DispensingService {
|
|||||||
await client.query(`
|
await client.query(`
|
||||||
UPDATE batches
|
UPDATE batches
|
||||||
SET qty_on_hand = qty_on_hand - $1
|
SET qty_on_hand = qty_on_hand - $1
|
||||||
WHERE product_id = $2 AND batch_no = $3
|
WHERE product_id = $2 AND batch_no = $3 AND (tenant_id = $4 OR tenant_id = $5)
|
||||||
`, [takeQty, item.product_id, batch.batch_no]);
|
`, [takeQty, item.product_id, batch.batch_no, actorId, tenantId]);
|
||||||
|
|
||||||
// Write Stock Movement Audit (matches schema)
|
// Write Stock Movement Audit (matches schema)
|
||||||
pendingQueries.push({
|
pendingQueries.push({
|
||||||
@@ -236,6 +236,7 @@ export class DispensingService {
|
|||||||
// Dispatch webhook
|
// Dispatch webhook
|
||||||
await webhookService.dispatchEvent('dispensing.completed', `evt_${dispensingId}`, {
|
await webhookService.dispatchEvent('dispensing.completed', `evt_${dispensingId}`, {
|
||||||
dispensing_id: dispensingId,
|
dispensing_id: dispensingId,
|
||||||
|
prescription_id: prescriptionId,
|
||||||
subject_ref: subjectRef,
|
subject_ref: subjectRef,
|
||||||
occurred_at: new Date().toISOString()
|
occurred_at: new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,13 +55,27 @@ router.put('/me', requireAuth, upload.single('profileImage'), async (req, res) =
|
|||||||
// POST /v1/auth/register (Create a new pharmacist)
|
// POST /v1/auth/register (Create a new pharmacist)
|
||||||
router.post('/register', async (req, res) => {
|
router.post('/register', async (req, res) => {
|
||||||
try {
|
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() === '') {
|
if (!hospital_id || String(hospital_id).trim() === '') {
|
||||||
return res.status(400).json({ error: 'Hospital ID is required' });
|
return res.status(400).json({ error: 'Hospital ID is required' });
|
||||||
}
|
}
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
return res.status(400).json({ error: 'Username and password are required' });
|
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,}$/;
|
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
|
||||||
if (!passwordRegex.test(password)) {
|
if (!passwordRegex.test(password)) {
|
||||||
@@ -78,7 +92,9 @@ router.post('/register', async (req, res) => {
|
|||||||
dl_number,
|
dl_number,
|
||||||
gstin,
|
gstin,
|
||||||
pharmacist_reg_number,
|
pharmacist_reg_number,
|
||||||
address
|
address,
|
||||||
|
email: email.trim(),
|
||||||
|
phone_number: phone_number.trim()
|
||||||
});
|
});
|
||||||
res.status(201).json({ success: true, user });
|
res.status(201).json({ success: true, user });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { pool } from '../../db/pool';
|
import { pool } from '../../db/pool';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { seedDummyInventoryForTenant } from '../../db/seedTenant';
|
||||||
|
|
||||||
// JWT_SECRET evaluated at runtime
|
// JWT_SECRET evaluated at runtime
|
||||||
|
|
||||||
@@ -14,13 +15,25 @@ export class IdentityService {
|
|||||||
gstin?: string;
|
gstin?: string;
|
||||||
pharmacist_reg_number?: string;
|
pharmacist_reg_number?: string;
|
||||||
address?: 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
|
if (!email || !email.trim()) {
|
||||||
const existing = await pool.query('SELECT id FROM users WHERE username = $1', [username]);
|
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) {
|
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
|
// 2. Hash password
|
||||||
@@ -29,15 +42,28 @@ export class IdentityService {
|
|||||||
|
|
||||||
const id = "usr_" + Date.now();
|
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(
|
await pool.query(
|
||||||
`INSERT INTO users (id, username, password_hash, role, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address, tenant_id)
|
`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)`,
|
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]
|
[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) {
|
async login(username: string, passwordPlain: string) {
|
||||||
@@ -55,13 +81,16 @@ export class IdentityService {
|
|||||||
throw new Error('Invalid credentials');
|
throw new Error('Invalid credentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hospitalId = user.hospital_id || '1';
|
||||||
|
|
||||||
// 3. Issue JWT Token
|
// 3. Issue JWT Token
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{
|
{
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
role: user.role,
|
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',
|
process.env.JWT_SECRET || 'super_secret_pharmacy_key_2026',
|
||||||
{ expiresIn: '12h' } // Usually 8-12 hours for a shift
|
{ expiresIn: '12h' } // Usually 8-12 hours for a shift
|
||||||
@@ -73,14 +102,15 @@ export class IdentityService {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
tenant_id: user.tenant_id
|
tenant_id: user.tenant_id,
|
||||||
|
hospital_id: hospitalId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserProfile(userId: string) {
|
async getUserProfile(userId: string) {
|
||||||
const result = await pool.query(
|
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]
|
[userId]
|
||||||
);
|
);
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0) {
|
||||||
@@ -95,6 +125,8 @@ export class IdentityService {
|
|||||||
gstin?: string;
|
gstin?: string;
|
||||||
pharmacist_reg_number?: string;
|
pharmacist_reg_number?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
|
email?: string;
|
||||||
|
phone_number?: string;
|
||||||
profile_image_url?: string;
|
profile_image_url?: string;
|
||||||
}) {
|
}) {
|
||||||
// Only update allowed fields
|
// Only update allowed fields
|
||||||
@@ -105,9 +137,11 @@ export class IdentityService {
|
|||||||
gstin = COALESCE($3, gstin),
|
gstin = COALESCE($3, gstin),
|
||||||
pharmacist_reg_number = COALESCE($4, pharmacist_reg_number),
|
pharmacist_reg_number = COALESCE($4, pharmacist_reg_number),
|
||||||
address = COALESCE($5, address),
|
address = COALESCE($5, address),
|
||||||
profile_image_url = COALESCE($6, profile_image_url)
|
email = COALESCE($6, email),
|
||||||
WHERE id = $7`,
|
phone_number = COALESCE($7, phone_number),
|
||||||
[data.pharmacy_name, data.dl_number, data.gstin, data.pharmacist_reg_number, data.address, data.profile_image_url, userId]
|
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);
|
return this.getUserProfile(userId);
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ export class IntegrationController {
|
|||||||
// Endpoint to fetch all patients synced to Pharmacy DB
|
// Endpoint to fetch all patients synced to Pharmacy DB
|
||||||
static async getAllPatients(req: Request, res: Response) {
|
static async getAllPatients(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const tenantId = req.headers['x-tenant-id'] || 'act_admin';
|
const tenantId = req.headers['x-tenant-id'] || req.tenantId || 'act_admin';
|
||||||
const patients = await IntegrationService.getAllPatients(String(tenantId));
|
const hospitalId = req.headers['x-hospital-id'] || req.hospitalId || '1';
|
||||||
|
const patients = await IntegrationService.getAllPatients(String(tenantId), String(hospitalId));
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
message: 'Patients fetched successfully',
|
message: 'Patients fetched successfully',
|
||||||
data: patients
|
data: patients
|
||||||
@@ -158,7 +159,8 @@ export class IntegrationController {
|
|||||||
|
|
||||||
static async getAllPrescriptions(req: Request, res: Response) {
|
static async getAllPrescriptions(req: Request, res: Response) {
|
||||||
try {
|
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();
|
const client = await require('../../db/pool').pool.connect();
|
||||||
try {
|
try {
|
||||||
let query: string;
|
let query: string;
|
||||||
@@ -167,8 +169,8 @@ export class IntegrationController {
|
|||||||
query = `SELECT * FROM prescriptions ORDER BY created_at DESC;`;
|
query = `SELECT * FROM prescriptions ORDER BY created_at DESC;`;
|
||||||
values = [];
|
values = [];
|
||||||
} else {
|
} else {
|
||||||
query = `SELECT * FROM prescriptions WHERE tenant_id = $1 OR tenant_id = 'act_admin' OR tenant_id IS NULL ORDER BY created_at DESC;`;
|
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)];
|
values = [String(tenantId), String(hospitalId)];
|
||||||
}
|
}
|
||||||
const result = await client.query(query, values);
|
const result = await client.query(query, values);
|
||||||
res.status(200).json({ message: 'All prescriptions', data: result.rows });
|
res.status(200).json({ message: 'All prescriptions', data: result.rows });
|
||||||
|
|||||||
@@ -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();
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
let query: string;
|
let query: string;
|
||||||
@@ -59,8 +59,9 @@ export class IntegrationService {
|
|||||||
query = `SELECT * FROM patients ORDER BY created_at DESC;`;
|
query = `SELECT * FROM patients ORDER BY created_at DESC;`;
|
||||||
values = [];
|
values = [];
|
||||||
} else {
|
} else {
|
||||||
query = `SELECT * FROM patients WHERE tenant_id = $1 OR tenant_id = 'act_admin' OR tenant_id IS NULL ORDER BY created_at DESC;`;
|
const hospId = hospitalId || '1';
|
||||||
values = [tenantId];
|
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);
|
const result = await client.query(query, values);
|
||||||
return result.rows;
|
return result.rows;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import multer from 'multer';
|
||||||
import { requireAuth } from '../../middleware/auth';
|
import { requireAuth } from '../../middleware/auth';
|
||||||
import { requireHeaders } from '../../middleware/headers';
|
import { requireHeaders } from '../../middleware/headers';
|
||||||
import { requireIdempotency } from '../../middleware/idempotency';
|
import { requireIdempotency } from '../../middleware/idempotency';
|
||||||
@@ -6,15 +7,59 @@ import { InventoryService } from './service';
|
|||||||
import supplierRouter from './supplierRouter';
|
import supplierRouter from './supplierRouter';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
const service = new InventoryService();
|
const service = new InventoryService();
|
||||||
|
|
||||||
router.use(supplierRouter);
|
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
|
// POST /v1/inventory/grn
|
||||||
router.post('/grn', requireAuth, requireHeaders, requireIdempotency, async (req, res, next) => {
|
router.post('/grn', requireAuth, requireHeaders, requireIdempotency, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const actorId = req.actorId as string;
|
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);
|
res.status(201).json(result);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.status === 422) {
|
if (error.status === 422) {
|
||||||
@@ -27,9 +72,10 @@ router.post('/grn', requireAuth, requireHeaders, requireIdempotency, async (req,
|
|||||||
// GET /v1/inventory/expiring
|
// GET /v1/inventory/expiring
|
||||||
router.get('/expiring', requireAuth, async (req, res, next) => {
|
router.get('/expiring', requireAuth, async (req, res, next) => {
|
||||||
try {
|
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 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 });
|
res.status(200).json({ batches });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -39,9 +85,10 @@ router.get('/expiring', requireAuth, async (req, res, next) => {
|
|||||||
// GET /v1/inventory/low-stock
|
// GET /v1/inventory/low-stock
|
||||||
router.get('/low-stock', requireAuth, async (req, res, next) => {
|
router.get('/low-stock', requireAuth, async (req, res, next) => {
|
||||||
try {
|
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 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 });
|
res.status(200).json({ batches });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -51,7 +98,9 @@ router.get('/low-stock', requireAuth, async (req, res, next) => {
|
|||||||
// POST /v1/inventory/adjustments
|
// POST /v1/inventory/adjustments
|
||||||
router.post('/adjustments', requireAuth, requireHeaders, async (req, res, next) => {
|
router.post('/adjustments', requireAuth, requireHeaders, async (req, res, next) => {
|
||||||
try {
|
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);
|
res.status(201).json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -1,7 +1,104 @@
|
|||||||
import { pool } from '../../db/pool';
|
import { pool } from '../../db/pool';
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
|
||||||
export class InventoryService {
|
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 { batch_no, qty_delta, reason_code, notes } = payload;
|
||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
@@ -10,8 +107,8 @@ export class InventoryService {
|
|||||||
await client.query(`
|
await client.query(`
|
||||||
UPDATE batches
|
UPDATE batches
|
||||||
SET qty_on_hand = qty_on_hand + $1
|
SET qty_on_hand = qty_on_hand + $1
|
||||||
WHERE batch_no = $2
|
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]);
|
`, [qty_delta, batch_no, actorId, tId]);
|
||||||
|
|
||||||
await client.query(`
|
await client.query(`
|
||||||
INSERT INTO inventory_adjustments (batch_no, qty_delta, reason_code, notes, approved_by)
|
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 = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
b.batch_no,
|
b.batch_no,
|
||||||
@@ -67,15 +201,16 @@ export class InventoryService {
|
|||||||
JOIN products p ON b.product_id = p.id
|
JOIN products p ON b.product_id = p.id
|
||||||
WHERE b.qty_on_hand > 0
|
WHERE b.qty_on_hand > 0
|
||||||
AND b.expiry <= CURRENT_DATE + $1::int
|
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
|
ORDER BY b.expiry ASC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const { rows } = await pool.query(query, [daysThreshold, actorId]);
|
const { rows } = await pool.query(query, [daysThreshold, actorId, tId]);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLowStockBatches(qtyThreshold: number = 50, actorId: string) {
|
async getLowStockBatches(qtyThreshold: number = 5000, actorId: string, tenantId?: string) {
|
||||||
|
const tId = tenantId || actorId;
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
b.batch_no,
|
b.batch_no,
|
||||||
@@ -88,15 +223,16 @@ export class InventoryService {
|
|||||||
JOIN products p ON b.product_id = p.id
|
JOIN products p ON b.product_id = p.id
|
||||||
WHERE b.qty_on_hand > 0
|
WHERE b.qty_on_hand > 0
|
||||||
AND b.qty_on_hand <= $1::int
|
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
|
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;
|
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();
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -128,7 +264,7 @@ export class InventoryService {
|
|||||||
SET qty_on_hand = batches.qty_on_hand + EXCLUDED.qty_on_hand,
|
SET qty_on_hand = batches.qty_on_hand + EXCLUDED.qty_on_hand,
|
||||||
purchase_price = EXCLUDED.purchase_price,
|
purchase_price = EXCLUDED.purchase_price,
|
||||||
supplier_id = EXCLUDED.supplier_id
|
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)
|
// Log Receipt Movement (in loose units)
|
||||||
await client.query(`
|
await client.query(`
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ router.post('/', requireAuth, upload.single('image'), async (req, res, next) =>
|
|||||||
router.get('/', requireAuth, async (req, res, next) => {
|
router.get('/', requireAuth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const tenantId = req.tenantId || req.headers['x-tenant-id'] || 'act_admin';
|
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 });
|
res.status(200).json({ prescriptions });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -42,7 +43,8 @@ router.get('/', requireAuth, async (req, res, next) => {
|
|||||||
router.get('/:id', requireAuth, async (req, res, next) => {
|
router.get('/:id', requireAuth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const tenantId = req.tenantId || req.headers['x-tenant-id'] || 'act_admin';
|
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' });
|
if (!record) return res.status(404).json({ error: 'NOT_FOUND' });
|
||||||
|
|
||||||
res.status(200).json(record);
|
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) => {
|
router.post('/:id/confirm', requireAuth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const tenantId = req.tenantId || req.headers['x-tenant-id'] || 'act_admin';
|
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);
|
res.status(200).json(result);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -132,28 +132,34 @@ export class PrescriptionService {
|
|||||||
}, 100); // slight delay to allow response to return to client first
|
}, 100); // slight delay to allow response to return to client first
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPrescription(id: string, actorId: string) {
|
async getPrescription(id: string, actorId: string, hospitalId?: 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]);
|
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];
|
return rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllPrescriptions(actorId: string) {
|
async getAllPrescriptions(actorId: string, hospitalId?: string) {
|
||||||
let query: string;
|
let query: string;
|
||||||
let values: any[];
|
let values: any[];
|
||||||
if (!actorId || actorId === 'act_admin' || actorId === 'all' || actorId === 'undefined' || actorId === 'null') {
|
if (!actorId || actorId === 'act_admin' || actorId === 'all' || actorId === 'undefined' || actorId === 'null') {
|
||||||
query = `SELECT * FROM prescriptions ORDER BY created_at DESC`;
|
query = `SELECT * FROM prescriptions ORDER BY created_at DESC`;
|
||||||
values = [];
|
values = [];
|
||||||
} else {
|
} else {
|
||||||
query = `SELECT * FROM prescriptions WHERE tenant_id = $1 OR tenant_id = 'act_admin' OR tenant_id IS NULL ORDER BY created_at DESC`;
|
const hospId = hospitalId || '1';
|
||||||
values = [actorId];
|
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);
|
const { rows } = await pool.query(query, values);
|
||||||
return rows;
|
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
|
// 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];
|
const record = rows[0];
|
||||||
|
|
||||||
if (!record) throw { status: 404, error: 'NOT_FOUND' };
|
if (!record) throw { status: 404, error: 'NOT_FOUND' };
|
||||||
@@ -162,16 +168,18 @@ export class PrescriptionService {
|
|||||||
const originalData = record.extracted_data;
|
const originalData = record.extracted_data;
|
||||||
|
|
||||||
// Check if any low confidence items were not explicitly reviewed
|
// Check if any low confidence items were not explicitly reviewed
|
||||||
for (let i = 0; i < originalData.items.length; i++) {
|
if (originalData && Array.isArray(originalData.items)) {
|
||||||
const origItem = originalData.items[i];
|
for (let i = 0; i < originalData.items.length; i++) {
|
||||||
if (origItem.confidence < 0.85 || origItem.confusable_with.length > 0) {
|
const origItem = originalData.items[i];
|
||||||
// The pharmacist must supply a confirmed item for this index
|
if (origItem && (origItem.confidence < 0.85 || (origItem.confusable_with && origItem.confusable_with.length > 0))) {
|
||||||
if (!confirmedData.items || !confirmedData.items[i] || !confirmedData.items[i].explicitly_confirmed) {
|
// The pharmacist must supply a confirmed item for this index
|
||||||
throw {
|
if (!confirmedData.items || !confirmedData.items[i] || !confirmedData.items[i].explicitly_confirmed) {
|
||||||
status: 422,
|
throw {
|
||||||
error: 'MISSING_CONFIRMATION',
|
status: 422,
|
||||||
message: `Item at index ${i} requires explicit positive selection due to low confidence or confusable candidates.`
|
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(`
|
await pool.query(`
|
||||||
UPDATE prescriptions
|
UPDATE prescriptions
|
||||||
SET status = 'confirmed', confirmed_data = $1
|
SET status = 'confirmed', confirmed_data = $1
|
||||||
WHERE id = $2 AND (tenant_id = $3 OR tenant_id = 'act_admin' OR tenant_id IS NULL)
|
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]);
|
`, [confirmedData, id, actorId, hospId]);
|
||||||
|
|
||||||
return { id, status: 'confirmed' };
|
return { id, status: 'confirmed' };
|
||||||
}
|
}
|
||||||
|
|||||||
+21
@@ -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();
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
$pdo = new PDO('mysql:host=127.0.0.1;dbname=telegaruda_v2;charset=utf8', 'root', '');
|
||||||
|
$subjectRef = "vallika v";
|
||||||
|
$stmt = $pdo->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";
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user