feat: add core backend modules, inventory services, and patch scripts for AI Pharmacy API
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user