diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4648298 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +backend/.env +frontend/.env +backend/.local-storage/ +dist/ \ No newline at end of file diff --git a/README.md b/README.md index add14a5..bdf3f31 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Adherence Pack Study +# Extended-Dose Pack Study Web app for CREST Private Limited. Physicians sign a consultation agreement, admins review it, and approved physicians receive a survey link. @@ -69,3 +69,10 @@ npm run build ## Deploy See [docs/DEPLOY.md](docs/DEPLOY.md) for Ubuntu on EC2, Nginx, PM2, RDS, and S3. + + +## Import doctors from a CSV file +# File path: files/import/*.csv +# run below from root if you want to update/import the doctors +npm run import:doctors -- import-doctors-04-11-2026.csv + diff --git a/backend/.env.example b/backend/.env.example index 5da3022..a881c50 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -33,4 +33,7 @@ ADMIN_PASSWORD= DATA_ENCRYPTION_KEY= WEASYPRINT_BIN=weasyprint -TRUST_PROXY=false +# Stored consent and agreement PDFs: never = generate once (required in production), +# changed = rebuild when the template or data changed, always = rebuild on every view. +PDF_REGENERATE=never +TRUST_PROXY=false \ No newline at end of file diff --git a/backend/migrations/20261104120000_pppaimt_doctors.js b/backend/migrations/20261104120000_pppaimt_doctors.js new file mode 100644 index 0000000..a9fff4a --- /dev/null +++ b/backend/migrations/20261104120000_pppaimt_doctors.js @@ -0,0 +1,31 @@ +/** Create pppaimt_doctors, the doctor list imported from CSV and keyed by unique email. */ +export async function up(knex) { + await knex.schema.createTable('pppaimt_doctors', (table) => { + table.charset('utf8mb4'); + table.collate('utf8mb4_unicode_ci'); + table.increments('id').primary(); + table.string('taskforce', 255).nullable(); + table.string('zbm_name', 255).nullable(); + table.string('rbm_name', 255).nullable(); + table.string('abm_name', 255).nullable(); + table.string('hq', 255).nullable(); + table.string('tm_id', 64).nullable(); + table.string('tm_name', 255).nullable(); + table.string('hcp_name', 255).nullable(); + table.string('legacy_code', 64).nullable(); + table.string('pcode', 64).nullable(); + table.string('speciality', 255).nullable(); + table.string('class', 32).nullable(); + table.string('vendor_code', 64).nullable(); + table.string('vendor_name', 255).nullable(); + table.string('email', 255).notNullable().unique(); + table.string('contact_no', 64).nullable(); + table.string('source_file', 255).nullable(); + table.timestamps(true, true); + }); +} + +/** Drop pppaimt_doctors. */ +export async function down(knex) { + await knex.schema.dropTableIfExists('pppaimt_doctors'); +} diff --git a/backend/package.json b/backend/package.json index fe69e1a..68af7f0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,6 +12,7 @@ "migrate": "knex migrate:latest", "seed:admin": "node src/db/seedAdmin.js", "seed:survey": "node src/db/seedSurvey.js", + "import:doctors": "node src/db/importDoctors.js", "test": "vitest run", "test:coverage": "vitest run --coverage", "load": "node scripts/load.js", @@ -28,6 +29,7 @@ "argon2": "^0.45.1", "dotenv": "^17.2.2", "exceljs": "^4.4.0", + "fast-csv": "^4.3.6", "hapi-pino": "^12.1.0", "hapi-rate-limit": "^8.0.0", "jsonwebtoken": "^9.0.3", diff --git a/backend/src/config/env.js b/backend/src/config/env.js index 5a0415e..1296689 100644 --- a/backend/src/config/env.js +++ b/backend/src/config/env.js @@ -78,6 +78,10 @@ const envSchema = z.object({ ADMIN_PASSWORD: optionalText, DATA_ENCRYPTION_KEY: optionalText, WEASYPRINT_BIN: optionalText, + PDF_REGENERATE: z.preprocess( + (value) => (value === '' || value === undefined ? undefined : value), + z.enum(['never', 'changed', 'always']).default('never'), + ), TRUST_PROXY: z.preprocess((value) => { if (value === undefined || value === '') { return false; @@ -102,4 +106,11 @@ if (!parsed.success) { process.exit(1); } +if (parsed.data.NODE_ENV === 'production' && parsed.data.PDF_REGENERATE !== 'never') { + console.error( + `PDF_REGENERATE=${parsed.data.PDF_REGENERATE} is not allowed in production: stored consent and agreement PDFs must not change. Unset it or set PDF_REGENERATE=never.`, + ); + process.exit(1); +} + export const env = parsed.data; diff --git a/backend/src/db/importDoctors.js b/backend/src/db/importDoctors.js new file mode 100644 index 0000000..907b47a --- /dev/null +++ b/backend/src/db/importDoctors.js @@ -0,0 +1,16 @@ +import { db } from './knex.js'; +import { formatSummary, importDoctors } from '../services/doctorImport.js'; + +let exitCode = 0; + +try { + const summary = await importDoctors(process.argv[2]); + console.info(formatSummary(summary)); +} catch (error) { + console.error(`Doctor import failed: ${error instanceof Error ? error.message : error}`); + exitCode = 1; +} finally { + await db.destroy(); +} + +process.exit(exitCode); diff --git a/backend/src/middleware/requireUser.js b/backend/src/middleware/requireUser.js index 3c3a5e9..c0cedb5 100644 --- a/backend/src/middleware/requireUser.js +++ b/backend/src/middleware/requireUser.js @@ -27,7 +27,7 @@ export const requireUser = { * @param {unknown} value * @returns {string | null} */ -function jwtFromCookie(value) { +export function jwtFromCookie(value) { if (typeof value === 'string') { return value; } diff --git a/backend/src/pdf/agreementHtml.js b/backend/src/pdf/agreementHtml.js index d8f2eb6..e8b1411 100644 --- a/backend/src/pdf/agreementHtml.js +++ b/backend/src/pdf/agreementHtml.js @@ -50,6 +50,50 @@ function dateParts(iso) { return { day, month, label: `${day} ${month} ${match[1]}` }; } +const IST_DATE = new Intl.DateTimeFormat('en-GB', { + timeZone: 'Asia/Kolkata', + day: 'numeric', + month: 'long', + year: 'numeric', +}); + +const IST_TIME = new Intl.DateTimeFormat('en-US', { + timeZone: 'Asia/Kolkata', + hour: '2-digit', + minute: '2-digit', + hour12: true, +}); + +/** + * Database datetimes are UTC ("YYYY-MM-DD HH:mm:ss" without a zone). + * @param {unknown} value + * @returns {Date | null} + */ +function utcInstant(value) { + if (value instanceof Date) { + return Number.isNaN(value.getTime()) ? null : value; + } + + const text = String(value ?? '').trim(); + + if (!/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(text)) { + return null; + } + + const zoned = /(Z|[+-]\d{2}:?\d{2})$/.test(text) ? text : `${text}Z`; + const instant = new Date(zoned.replace(' ', 'T')); + return Number.isNaN(instant.getTime()) ? null : instant; +} + +/** + * The physician's signing moment in India time, e.g. "29 October 2026, 09:20 AM IST". + * @param {unknown} value a Date or a UTC database datetime + */ +export function signedAtLabel(value) { + const instant = utcInstant(value); + return instant ? `${IST_DATE.format(instant)}, ${IST_TIME.format(instant)} IST` : ''; +} + /** * @param {string} text * @param {RegExp} pattern @@ -66,7 +110,11 @@ function replaceOnce(text, pattern, replacement) { } /** - * @param {{ type?: string, imageDataUrl?: string, typedName?: string } | null | undefined} signature + * @typedef {{ type?: string, imageDataUrl?: string, typedName?: string, signedAt?: unknown }} PhysicianSignature + */ + +/** + * @param {PhysicianSignature | null | undefined} signature */ function physicianSignatureHtml(signature) { if (signature?.type === 'DRAWN' && signature.imageDataUrl) { @@ -200,7 +248,7 @@ function toParagraphs(text) { * @param {{ * application: Record, * signatory?: { name: string, designation: string, signatureDataUrl?: string } | null, - * physicianSignature?: { type?: string, imageDataUrl?: string, typedName?: string } | null, + * physicianSignature?: PhysicianSignature | null, * draft?: boolean, * crestDate?: string | null, * }} options @@ -268,7 +316,8 @@ export function agreementHtml(options) { /Signature: _+/, `Signature: ${physicianSignatureHtml(options.physicianSignature)}`, ); - tail = replaceOnce(tail, /Date: _+/, `Date: ${filled(date.label)}`); + const physicianDate = signedAtLabel(options.physicianSignature?.signedAt) || date.label; + tail = replaceOnce(tail, /Date: _+/, `Date: ${filled(physicianDate)}`); const watermark = options.draft ? '
DRAFT - NOT SIGNED
' : ''; diff --git a/backend/src/pdf/consentHtml.js b/backend/src/pdf/consentHtml.js index ec87f7b..1b65e40 100644 --- a/backend/src/pdf/consentHtml.js +++ b/backend/src/pdf/consentHtml.js @@ -1,18 +1,31 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { CONSENT_STATEMENTS } from '../schemas/consent.js'; import { escapeHtml } from './agreementHtml.js'; -const PIS_PATH = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../files/docs/pis.md', -); +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const PIS_PATH = path.resolve(HERE, '../../../files/docs/pis.md'); const pisSource = readFileSync(PIS_PATH, 'utf8'); -/** Needs a font with the ₹ sign; Noto Sans or DejaVu Sans on Linux servers. */ -const FONT_STACK = 'Calibri, Carlito, "Noto Sans", "DejaVu Sans", Arial, sans-serif'; +/** + * Carlito has Calibri's metrics (the original PIS/ICF font) and includes ₹. + * It is bundled so PDFs look the same on any machine; render.js runs + * WeasyPrint from a temp directory, so the font URLs must be absolute. + */ +const FONT_FACES = ['Regular', 'Bold'] + .map((style) => { + const url = pathToFileURL(path.join(HERE, 'fonts', `Carlito-${style}.ttf`)).href; + const weight = style === 'Bold' ? 700 : 400; + return `@font-face { font-family: "CREST Carlito"; src: url("${url}"); font-weight: ${weight}; }`; + }) + .join('\n '); + +const FONT_STACK = '"CREST Carlito", Carlito, Calibri, "Noto Sans", "DejaVu Sans", sans-serif'; + +/** Hyperlink blue used for the document titles in the original. */ +const TITLE_BLUE = '#0563C1'; /** The PIS section that starts page 2 in the original document. */ const PAGE_TWO_HEADING = 'What will you be expected to do?'; @@ -48,10 +61,37 @@ function paragraph(lines) { .join(''); } +/** + * A contact block is a bold role line ("**Principal Investigator**") followed + * by name, phone and email lines. + * @param {string[]} lines + */ +function isContactBlock(lines) { + return /^\*\*[^*]+\*\*\s*$/.test(lines[0].trim()) && lines.length > 1; +} + +/** + * @param {string[][]} contacts + */ +function contactsTable(contacts) { + const cells = contacts + .map((lines) => { + const role = inline(lines[0].trim()); + const details = lines + .slice(1) + .map((line) => inline(line.trim())) + .join('
'); + return `${role}
${details}`; + }) + .join(''); + return `${cells}
`; +} + /** * Render `files/docs/pis.md` the way the original PIS lays it out: the * document line becomes the page header, "Survey Title" runs inline with the - * title, and the "Background" label is not shown. + * title, the "Background" label is not shown, and the investigators' contact + * blocks sit side by side in a table. * @param {string} source */ export function pisBodyHtml(source = pisSource) { @@ -61,7 +101,17 @@ export function pisBodyHtml(source = pisSource) { .filter((lines) => lines.length > 0); /** @type {string[]} */ const html = []; + /** @type {string[][]} */ + let contacts = []; let surveyTitle = false; + let contactSection = false; + + const flushContacts = () => { + if (contacts.length) { + html.push(contactsTable(contacts)); + contacts = []; + } + }; for (const lines of blocks) { const first = lines[0].trim(); @@ -71,10 +121,14 @@ export function pisBodyHtml(source = pisSource) { } if (first.startsWith('## ')) { + flushContacts(); const heading = first.slice(3).trim(); + contactSection = heading === 'Contact Details'; if (heading === 'Survey Title') { surveyTitle = true; + } else if (contactSection) { + html.push(`

${inline(heading)}:

`); } else if (heading !== 'Background') { const pageBreak = heading === PAGE_TWO_HEADING ? ' class="page-break"' : ''; html.push(`${inline(heading)}`); @@ -83,6 +137,11 @@ export function pisBodyHtml(source = pisSource) { continue; } + if (contactSection && isContactBlock(lines)) { + contacts.push(lines); + continue; + } + if (lines.every((line) => line.trim().startsWith('- '))) { const items = lines.map((line) => `
  • ${inline(line.trim().slice(2))}
  • `).join(''); html.push(``); @@ -90,14 +149,15 @@ export function pisBodyHtml(source = pisSource) { } if (surveyTitle) { - html.push(`

    Survey Title: ${paragraph(lines)}

    `); + html.push(`

    Survey Title: ${paragraph(lines)}

    `); surveyTitle = false; continue; } - html.push(`

    ${paragraph(lines)}

    `); + html.push(`${paragraph(lines)}

    `); } + flushContacts(); return html.join('\n'); } @@ -123,7 +183,8 @@ function icfHtml(consent) { }).join('\n'); return `
    -

    ONLINE
    INFORMED CONSENT FORM (ICF)

    +

    ONLINE

    +

    INFORMED CONSENT FORM (ICF)

    I have read and understood the information provided above regarding this survey. I have had the opportunity to ask questions and have received satisfactory answers to questions.

    ${rows} @@ -156,21 +217,29 @@ export function consentHtml(consent) { ${escapeHtml(meta.version)} - Participant Information Sheet and Informed Consent Form -

    ONLINE
    PARTICIPANT INFORMATION SHEET (PIS)

    + +

    ONLINE

    +

    PARTICIPANT INFORMATION SHEET (PIS)

    ${pisBodyHtml()} ${icfHtml(consent)} diff --git a/backend/src/pdf/fonts/Carlito-Bold.ttf b/backend/src/pdf/fonts/Carlito-Bold.ttf new file mode 100644 index 0000000..67543b5 Binary files /dev/null and b/backend/src/pdf/fonts/Carlito-Bold.ttf differ diff --git a/backend/src/pdf/fonts/Carlito-OFL.txt b/backend/src/pdf/fonts/Carlito-OFL.txt new file mode 100644 index 0000000..8d6b170 --- /dev/null +++ b/backend/src/pdf/fonts/Carlito-OFL.txt @@ -0,0 +1,94 @@ +Copyright 2013 The Carlito Project Authors (https://github.com/googlefonts/carlito), with Reserved Font Name "Carlito" + + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/backend/src/pdf/fonts/Carlito-Regular.ttf b/backend/src/pdf/fonts/Carlito-Regular.ttf new file mode 100644 index 0000000..427b959 Binary files /dev/null and b/backend/src/pdf/fonts/Carlito-Regular.ttf differ diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 3d384f7..f1dfd3c 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -45,6 +45,9 @@ const exportQuerySchema = z.object({ const applicationsExportQuerySchema = z.object({ from: optionalDate, to: optionalDate, + full: z + .preprocess(blankToUndefined, z.enum(['true', 'false']).default('false')) + .transform((value) => value === 'true'), }); const reminderSchema = z.object({ @@ -288,7 +291,10 @@ export const adminRoutes = { return h .response(stream) .type('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') - .header('content-disposition', 'attachment; filename="applications-consent.xlsx"'); + .header( + 'content-disposition', + `attachment; filename="${parsed.data.full ? 'applications-consent-full.xlsx' : 'applications-consent.xlsx'}"`, + ); }, }); diff --git a/backend/src/routes/userAuth.js b/backend/src/routes/userAuth.js index 99d6cfe..ae5f299 100644 --- a/backend/src/routes/userAuth.js +++ b/backend/src/routes/userAuth.js @@ -1,9 +1,10 @@ import Boom from '@hapi/boom'; import { z } from 'zod'; import { auditFromRequest } from '../middleware/audit.js'; -import { requireUser } from '../middleware/requireUser.js'; +import { jwtFromCookie, requireUser } from '../middleware/requireUser.js'; import { getMe, + readUserId, requestLoginOtp, signUserToken, verifyLoginOtp, @@ -86,18 +87,20 @@ export const userAuthRoutes = { path: '/api/auth/logout', options: { auth: false, - pre: [requireUser], }, async handler(request, h) { - const user = request.pre.user; + const token = jwtFromCookie(request.state.user_token); + const userId = token ? readUserId(token) : null; - await auditFromRequest(request, { - actorType: 'PHYSICIAN', - actorId: user.id, - action: 'USER_LOGOUT', - entityType: 'users', - entityId: user.id, - }); + if (userId) { + await auditFromRequest(request, { + actorType: 'PHYSICIAN', + actorId: Number(userId), + action: 'USER_LOGOUT', + entityType: 'users', + entityId: Number(userId), + }); + } return h.response({ ok: true }).unstate('user_token'); }, diff --git a/backend/src/services/admin.js b/backend/src/services/admin.js index 713d142..028ee51 100644 --- a/backend/src/services/admin.js +++ b/backend/src/services/admin.js @@ -3,6 +3,7 @@ import argon2 from 'argon2'; import { z } from 'zod'; import { db } from '../db/knex.js'; import { auditFromRequest } from '../middleware/audit.js'; +import { refreshAgreementPdfs } from './agreementPdfRefresh.js'; import { recordAudit } from './audit.js'; import { finalizeSubmittedApplication } from './applicationApproval.js'; import { presentApplication } from './applications.js'; @@ -275,6 +276,7 @@ async function applicationById(id) { */ export async function getApplication(id) { const application = await applicationById(id); + await refreshAgreementPdfs(application); const signature = await db('signatures') .where({ application_id: id }) .whereNull('voided_at') diff --git a/backend/src/services/agreementDocuments.js b/backend/src/services/agreementDocuments.js index 4b4b01c..e646c0f 100644 --- a/backend/src/services/agreementDocuments.js +++ b/backend/src/services/agreementDocuments.js @@ -5,6 +5,7 @@ import { db } from '../db/knex.js'; import { auditFromRequest } from '../middleware/audit.js'; import { agreementHtml } from '../pdf/agreementHtml.js'; import { renderPdf } from '../pdf/render.js'; +import { refreshAgreementPdfs } from './agreementPdfRefresh.js'; import { readPngDataUrl } from './png.js'; import { recordAudit } from './audit.js'; import { finalizeSubmittedApplication } from './applicationApproval.js'; @@ -23,12 +24,10 @@ const signSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('DRAWN'), image: z.string().min(1), - consent: z.literal(true), }), z.object({ type: z.literal('TYPED'), typedName: z.string().trim().min(1).max(255), - consent: z.literal(true), }), ]); @@ -36,6 +35,7 @@ const signSchema = z.discriminatedUnion('type', [ * @param {Record} application */ export async function presentWithPdf(application) { + await refreshAgreementPdfs(application); const body = presentApplication(application); const finalKey = application.final_pdf_key ? String(application.final_pdf_key) : null; const signedKey = application.signed_pdf_key ? String(application.signed_pdf_key) : null; @@ -86,7 +86,7 @@ async function crestSignatory(application) { /** * @param {Record} application - * @param {{ draft: boolean, physicianSignature?: { type: string, imageDataUrl?: string, typedName?: string } | null }} options + * @param {{ draft: boolean, physicianSignature?: { type: string, imageDataUrl?: string, typedName?: string, signedAt?: string } | null }} options */ async function renderAgreement(application, options) { const html = agreementHtml({ @@ -98,6 +98,15 @@ async function renderAgreement(application, options) { return renderPdf(html); } +/** + * The database runs in UTC, but the driver writes JS Dates in the writing + * machine's time zone; an explicit UTC string keeps signing times comparable. + * @param {Date} date + */ +function utcDateTime(date) { + return date.toISOString().slice(0, 19).replace('T', ' '); +} + /** * @param {string} image */ @@ -145,15 +154,21 @@ export async function signApplication(request) { const parsed = signSchema.safeParse(request.payload); if (!parsed.success) { - throw Boom.badRequest('A signature and consent are required'); + throw Boom.badRequest('A signature is required'); } const signature = parsed.data; const png = signature.type === 'DRAWN' ? readPng(signature.image) : null; + const signedAt = new Date(); + const signedAtUtc = utcDateTime(signedAt); const physicianSignature = signature.type === 'DRAWN' - ? { type: 'DRAWN', imageDataUrl: `data:image/png;base64,${png.toString('base64')}` } - : { type: 'TYPED', typedName: signature.typedName }; + ? { + type: 'DRAWN', + imageDataUrl: `data:image/png;base64,${png.toString('base64')}`, + signedAt: signedAtUtc, + } + : { type: 'TYPED', typedName: signature.typedName, signedAt: signedAtUtc }; const pdf = await renderAgreement(application, { draft: false, physicianSignature }); const sha256 = createHash('sha256').update(pdf).digest('hex'); const pdfKey = `applications/${application.public_id}/v${application.version}/signed.pdf`; @@ -176,14 +191,14 @@ export async function signApplication(request) { throw Boom.conflict('This application can no longer be signed'); } - const signedAt = new Date(); await trx('signatures').insert({ application_id: application.id, version: application.version, type: signature.type, image_key: png ? imageKey : null, typed_text: signature.type === 'TYPED' ? signature.typedName : null, - signed_at: signedAt, + signed_at: signedAtUtc, + created_at: signedAtUtc, ip: request.info.remoteAddress, user_agent: request.headers['user-agent'] ?? null, }); diff --git a/backend/src/services/agreementPdfRefresh.js b/backend/src/services/agreementPdfRefresh.js new file mode 100644 index 0000000..81419bb --- /dev/null +++ b/backend/src/services/agreementPdfRefresh.js @@ -0,0 +1,84 @@ +import { agreementHtml } from '../pdf/agreementHtml.js'; +import { physicianSignatureForApproval } from './applicationApproval.js'; +import { presentApplication } from './applications.js'; +import { pdfRegenerationEnabled, refreshStoredPdf } from './pdfCache.js'; +import { getSignatory } from './settings.js'; +import { getObject } from './storage.js'; + +/** + * @param {Record} application + * @param {'signed' | 'final'} kind + */ +function currentKey(application, kind) { + return `applications/${application.public_id}/v${application.version}/${kind}.pdf`; +} + +/** + * The physician-signed agreement as it was rendered at signing: the CREST + * block still reads "Pending approval". + * @param {Record} application + */ +async function signedHtml(application) { + const presented = presentApplication(application); + presented.status = 'SUBMITTED'; + return agreementHtml({ + application: presented, + signatory: null, + physicianSignature: await physicianSignatureForApproval(application), + draft: false, + }); +} + +/** + * The CREST-signed agreement, dated on the approval day. + * @param {Record} application + */ +async function finalHtml(application) { + const signatory = await getSignatory(); + const bytes = await getObject(String(signatory.signatureKey)); + return agreementHtml({ + application: presentApplication(application), + signatory: { + name: signatory.name, + designation: signatory.designation, + signatureDataUrl: `data:image/png;base64,${Buffer.from(bytes).toString('base64')}`, + }, + physicianSignature: await physicianSignatureForApproval(application), + draft: false, + crestDate: String(application.decided_at ?? '').slice(0, 10) || null, + }); +} + +/** + * Rebuild the stored signed/final agreement PDFs when PDF_REGENERATE allows + * it. Only files for the application's current version are touched; a failure + * keeps the stored file so pages still load. + * @param {Record | null | undefined} application + */ +export async function refreshAgreementPdfs(application) { + if (!application || !pdfRegenerationEnabled()) { + return; + } + + /** @type {[string, () => Promise][]} */ + const jobs = []; + + if (application.signed_pdf_key === currentKey(application, 'signed')) { + jobs.push([String(application.signed_pdf_key), () => signedHtml(application)]); + } + + if ( + application.status === 'APPROVED' && + application.final_pdf_key === currentKey(application, 'final') + ) { + jobs.push([String(application.final_pdf_key), () => finalHtml(application)]); + } + + for (const [key, build] of jobs) { + try { + await refreshStoredPdf(key, build); + } catch (error) { + console.warn(`Could not regenerate ${key}:`, error instanceof Error ? error.message : error); + } + } +} diff --git a/backend/src/services/applicationApproval.js b/backend/src/services/applicationApproval.js index 6793f5e..0d8a316 100644 --- a/backend/src/services/applicationApproval.js +++ b/backend/src/services/applicationApproval.js @@ -21,6 +21,8 @@ function todayIso() { } /** + * `signedAt` uses the row's created_at, which the database (UTC) sets itself; + * signed_at on older rows was written in the signing machine's time zone. * @param {Record} application */ export async function physicianSignatureForApproval(application) { @@ -39,10 +41,11 @@ export async function physicianSignatureForApproval(application) { return { type: 'DRAWN', imageDataUrl: `data:image/png;base64,${Buffer.from(bytes).toString('base64')}`, + signedAt: signature.created_at, }; } - return { type: 'TYPED', typedName: signature.typed_text }; + return { type: 'TYPED', typedName: signature.typed_text, signedAt: signature.created_at }; } /** diff --git a/backend/src/services/applicationsExport.js b/backend/src/services/applicationsExport.js index a1ed659..9e778cd 100644 --- a/backend/src/services/applicationsExport.js +++ b/backend/src/services/applicationsExport.js @@ -18,16 +18,19 @@ const SURVEY_LABELS = { /** * `text: true` keeps Excel from turning account numbers and similar ids into - * numbers (scientific notation, dropped leading zeros). - * @type {{ key: string, header: string, width: number, text?: boolean }[]} + * numbers (scientific notation, dropped leading zeros). `basic: true` columns + * are the default report; the full report adds registration, bank and + * consent-statement details. + * @typedef {{ key: string, header: string, width: number, text?: boolean, basic?: boolean }} ExportColumn + * @type {ExportColumn[]} */ const BASE_COLUMNS = [ - { key: 'applicationId', header: 'Application id', width: 14 }, - { key: 'status', header: 'Status', width: 14 }, - { key: 'email', header: 'Email', width: 30 }, - { key: 'fullName', header: 'Full name', width: 26 }, - { key: 'age', header: 'Age', width: 8 }, - { key: 'address', header: 'Address', width: 40 }, + { key: 'applicationId', header: 'Application id', width: 14, basic: true }, + { key: 'status', header: 'Status', width: 14, basic: true }, + { key: 'email', header: 'Email', width: 30, basic: true }, + { key: 'fullName', header: 'Full name', width: 26, basic: true }, + { key: 'age', header: 'Age', width: 8, basic: true }, + { key: 'address', header: 'Address', width: 40, basic: true }, { key: 'registrationNo', header: 'Registration no', width: 18, text: true }, { key: 'medicalCouncil', header: 'Medical council', width: 26 }, { key: 'pan', header: 'PAN', width: 14, text: true }, @@ -36,32 +39,43 @@ const BASE_COLUMNS = [ { key: 'accountHolder', header: 'Account holder', width: 26 }, { key: 'accountNumber', header: 'Account number', width: 22, text: true }, { key: 'ifsc', header: 'IFSC', width: 14, text: true }, - { key: 'createdAt', header: 'Created at', width: 20 }, - { key: 'agreementDate', header: 'Agreement date', width: 14 }, + { key: 'createdAt', header: 'Created at', width: 20, basic: true }, + { key: 'agreementDate', header: 'Agreement date', width: 14, basic: true }, { key: 'submittedAt', header: 'Submitted at', width: 20 }, { key: 'decidedAt', header: 'Decided at', width: 20 }, { key: 'rejectionReason', header: 'Rejection reason', width: 30 }, { key: 'chequeUploaded', header: 'Cheque uploaded', width: 14 }, - { key: 'signed', header: 'Signed', width: 10 }, + { key: 'signed', header: 'Signed', width: 10, basic: true }, { key: 'finalPdf', header: 'Final PDF available', width: 16 }, { key: 'consentName', header: 'Consent participant name', width: 26 }, - { key: 'consentDate', header: 'Consent date', width: 14 }, + { key: 'consentDate', header: 'Consent date', width: 14, basic: true }, { key: 'consentSubmittedAt', header: 'Consent submitted at', width: 20 }, ]; +/** @type {ExportColumn[]} */ const CONSENT_COLUMNS = CONSENT_STATEMENTS.map((statement) => ({ key: `consent_${statement.id}`, header: statement.label, width: 36, })); +/** @type {ExportColumn[]} */ const SURVEY_COLUMNS = [ - { key: 'surveyStatus', header: 'Survey status', width: 14 }, - { key: 'surveyCompletedAt', header: 'Survey completed at', width: 20 }, + { key: 'surveyStatus', header: 'Survey status', width: 14, basic: true }, + { key: 'surveyCompletedAt', header: 'Survey completed at', width: 20, basic: true }, ]; export const APPLICATION_EXPORT_COLUMNS = [...BASE_COLUMNS, ...CONSENT_COLUMNS, ...SURVEY_COLUMNS]; +/** + * @param {boolean} full + */ +export function applicationExportColumns(full) { + return full + ? APPLICATION_EXPORT_COLUMNS + : APPLICATION_EXPORT_COLUMNS.filter((column) => column.basic); +} + /** * @param {unknown} value */ @@ -88,10 +102,13 @@ function yesNo(value) { } /** + * PAN and account numbers are only decrypted for the full report. * @param {Record} record * @param {Set} signedIds + * @param {{ full?: boolean }} [options] */ -export function applicationExportRow(record, signedIds) { +export function applicationExportRow(record, signedIds, options = {}) { + const full = options.full === true; const consentAnswers = record.consentId ? (parseJson(record.consentAnswers) ?? {}) : null; /** @type {Record} */ const row = { @@ -103,11 +120,11 @@ export function applicationExportRow(record, signedIds) { address: text(record.address), registrationNo: text(record.registration_no), medicalCouncil: text(record.medical_council), - pan: text(decryptField(record.pan)), + pan: full ? text(decryptField(record.pan)) : '', gstin: text(record.gstin), bankName: text(record.bank_name), accountHolder: text(record.account_holder), - accountNumber: text(decryptField(record.account_number)), + accountNumber: full ? text(decryptField(record.account_number)) : '', ifsc: text(record.ifsc), createdAt: text(record.created_at), agreementDate: text(record.agreement_date).slice(0, 10), @@ -136,9 +153,10 @@ export function applicationExportRow(record, signedIds) { } /** - * Decrypts PAN and account numbers before streaming starts, so a decryption - * failure surfaces as an error response instead of a truncated file. - * @param {{ from?: string, to?: string }} query + * Decrypts PAN and account numbers (full report only) before streaming starts, + * so a decryption failure surfaces as an error response instead of a + * truncated file. + * @param {{ from?: string, to?: string, full?: boolean }} query */ export async function loadApplicationsExport(query) { let request = db('applications') @@ -175,19 +193,22 @@ export async function loadApplicationsExport(query) { : []; const signedIds = new Set(signed.map((row) => row.application_id)); - return records.map((record) => applicationExportRow(record, signedIds)); + return records.map((record) => + applicationExportRow(record, signedIds, { full: query.full === true }), + ); } /** * @param {import('node:stream').Writable} stream * @param {Record[]} rows + * @param {ExportColumn[]} [columns] */ -export async function writeApplicationsWorkbook(stream, rows) { +export async function writeApplicationsWorkbook(stream, rows, columns = APPLICATION_EXPORT_COLUMNS) { const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({ stream, useStyles: true }); const sheet = workbook.addWorksheet('Applications', { views: [{ state: 'frozen', ySplit: 1 }], }); - sheet.columns = APPLICATION_EXPORT_COLUMNS.map((column) => ({ + sheet.columns = columns.map((column) => ({ header: column.header, key: column.key, width: column.width, @@ -206,12 +227,13 @@ export async function writeApplicationsWorkbook(stream, rows) { } /** - * @param {{ from?: string, to?: string }} query + * @param {{ from?: string, to?: string, full?: boolean }} query */ export async function exportApplicationsWorkbook(query) { const rows = await loadApplicationsExport(query); + const columns = applicationExportColumns(query.full === true); const stream = new PassThrough(); - const writing = writeApplicationsWorkbook(stream, rows).catch((error) => { + const writing = writeApplicationsWorkbook(stream, rows, columns).catch((error) => { stream.destroy(error); }); stream.on('error', () => { diff --git a/backend/src/services/consentPdf.js b/backend/src/services/consentPdf.js index 66e524e..6969370 100644 --- a/backend/src/services/consentPdf.js +++ b/backend/src/services/consentPdf.js @@ -2,6 +2,7 @@ import Boom from '@hapi/boom'; import { db } from '../db/knex.js'; import { consentHtml } from '../pdf/consentHtml.js'; import { renderPdf } from '../pdf/render.js'; +import { refreshStoredPdf, rememberPdfSource } from './pdfCache.js'; import { getObject, putObject } from './storage.js'; /** @@ -36,6 +37,17 @@ export function consentPdfFilename(application) { return `consent-${reference || 'form'}.pdf`; } +/** + * @param {Record} consent + */ +function consentDocumentHtml(consent) { + return consentHtml({ + participantName: String(consent.participant_name ?? ''), + consentDate: isoDate(consent.consent_date), + answers: parseJson(consent.answers) ?? {}, + }); +} + /** * Render the consent once and keep the stored file as the record, so later * edits to the PIS text do not change what a physician agreed to. @@ -44,19 +56,24 @@ export function consentPdfFilename(application) { * @returns {Promise} */ export async function storeConsentPdf(consent, application) { - const pdf = await renderPdf( - consentHtml({ - participantName: String(consent.participant_name ?? ''), - consentDate: isoDate(consent.consent_date), - answers: parseJson(consent.answers) ?? {}, - }), - ); + const html = consentDocumentHtml(consent); + const pdf = await renderPdf(html); const key = `applications/${application.public_id}/consent.pdf`; await putObject(key, pdf, 'application/pdf'); + await rememberPdfSource(key, html); await db('consents').where({ id: consent.id }).update({ consent_pdf_key: key }); return pdf; } +/** + * @param {string} key + * @param {Record} consent + */ +async function storedConsentPdf(key, consent) { + const fresh = await refreshStoredPdf(key, async () => consentDocumentHtml(consent)); + return fresh ?? getObject(key); +} + /** * Consents submitted before PDFs existed get theirs on first download. * @param {Record | undefined} consent @@ -74,7 +91,7 @@ export async function consentPdfFor(consent) { } const pdf = consent.consent_pdf_key - ? await getObject(String(consent.consent_pdf_key)) + ? await storedConsentPdf(String(consent.consent_pdf_key), consent) : await storeConsentPdf(consent, application); return { pdf, filename: consentPdfFilename(application) }; } diff --git a/backend/src/services/doctorImport.js b/backend/src/services/doctorImport.js new file mode 100644 index 0000000..19d6f65 --- /dev/null +++ b/backend/src/services/doctorImport.js @@ -0,0 +1,221 @@ +import { createReadStream, existsSync } from 'node:fs'; +import path from 'node:path'; +import { parseStream } from 'fast-csv'; +import { z } from 'zod'; +import { db } from '../db/knex.js'; +import { recordAudit } from './audit.js'; + +export const IMPORT_DIR = '/Users/amey/Developer/Stratawiz/PPPAIMT-Crest/files/import/'; + +/** CSV header to pppaimt_doctors column. */ +export const DOCTOR_COLUMNS = { + TASKFORCE: 'taskforce', + 'ZBM Name': 'zbm_name', + 'RBM Name': 'rbm_name', + 'ABM Name': 'abm_name', + HQ: 'hq', + 'TM ID': 'tm_id', + 'TM Name': 'tm_name', + 'HCP NAME': 'hcp_name', + LEGACY_CODE: 'legacy_code', + PCODE: 'pcode', + SPECIALITY: 'speciality', + CLASS: 'class', + VENDORCODE: 'vendor_code', + VENDORNAME: 'vendor_name', + 'Email ID': 'email', + 'Contact No': 'contact_no', +}; + +const IGNORED_HEADERS = ['Sr NO']; +const BATCH = 200; +const emailSchema = z.string().max(255).email(); + +/** Read a CSV stream into its headers and numbered rows; rows with the wrong column count have `data: null`. */ +export function parseDoctorCsv(stream) { + return new Promise((resolve, reject) => { + let headers = []; + const rows = []; + + parseStream(stream, { + headers: (raw) => { + headers = raw.map((header) => String(header ?? '').replace(/^\uFEFF/, '').trim()); + return headers; + }, + trim: true, + ignoreEmpty: true, + strictColumnHandling: true, + }) + .on('error', reject) + .on('data', (data) => rows.push({ row: rows.length + 1, data })) + .on('data-invalid', () => rows.push({ row: rows.length + 1, data: null })) + .on('end', () => resolve({ headers, rows })); + }); +} + +/** Check headers and every row: returns mapped records, invalid rows, duplicate email groups and header problems. */ +export function validateDoctors({ headers, rows }) { + const expected = Object.keys(DOCTOR_COLUMNS); + const missing = expected.filter((header) => !headers.includes(header)); + const unexpected = headers.filter( + (header) => !expected.includes(header) && !IGNORED_HEADERS.includes(header), + ); + const records = []; + const invalid = []; + const byEmail = new Map(); + + for (const { row, data } of rows) { + if (!data) { + invalid.push({ row, name: '', email: '', reason: 'wrong number of columns' }); + continue; + } + + const record = Object.fromEntries( + Object.entries(DOCTOR_COLUMNS).map(([header, column]) => [ + column, + String(data[header] ?? '').trim() || null, + ]), + ); + record.email = record.email ? record.email.toLowerCase() : null; + const label = { row, name: record.hcp_name ?? '' }; + + if (!record.email) { + invalid.push({ ...label, email: '', reason: 'email is missing' }); + } else if (!emailSchema.safeParse(record.email).success) { + invalid.push({ ...label, email: record.email, reason: `"${record.email}" is not a valid email` }); + } else { + byEmail.set(record.email, [...(byEmail.get(record.email) ?? []), label]); + records.push(record); + } + } + + const duplicates = [...byEmail.entries()] + .filter(([, labels]) => labels.length > 1) + .map(([email, labels]) => ({ email, rows: labels })); + + return { missing, unexpected, records, invalid, duplicates }; +} + +/** Import a CSV from files/import/: stops without writing on any problem, otherwise inserts new and updates existing doctors by email. */ +export async function importDoctors(fileName, { connection = db, directory = IMPORT_DIR } = {}) { + if (!fileName) { + throw new Error('Pass the CSV file name, e.g. npm run import:doctors -- doctors.csv'); + } + + if (path.basename(fileName) !== fileName || !fileName.toLowerCase().endsWith('.csv')) { + throw new Error(`Use a plain .csv file name from ${directory}, not a path: ${fileName}`); + } + + const filePath = path.join(directory, fileName); + + if (!existsSync(filePath)) { + throw new Error(`File not found: ${filePath}`); + } + + const parsed = await parseDoctorCsv(createReadStream(filePath)); + const { records, ...problems } = validateDoctors(parsed); + const headerProblem = problems.missing.length > 0 || problems.unexpected.length > 0; + const summary = { + file: fileName, + rowsRead: parsed.rows.length, + inserted: 0, + updated: 0, + rejected: headerProblem + ? parsed.rows.length + : problems.invalid.length + + problems.duplicates.reduce((total, group) => total + group.rows.length, 0), + stopped: false, + ...problems, + }; + summary.stopped = headerProblem || summary.rejected > 0 || summary.rowsRead === 0; + + if (summary.stopped) { + return summary; + } + + await connection.transaction(async (trx) => { + const emails = records.map((record) => record.email); + const existing = new Set(); + + for (let index = 0; index < emails.length; index += BATCH) { + const found = await trx('pppaimt_doctors') + .whereIn('email', emails.slice(index, index + BATCH)) + .pluck('email'); + found.forEach((email) => existing.add(String(email).toLowerCase())); + } + + const rows = records.map((record) => ({ ...record, source_file: fileName })); + const toInsert = rows.filter((record) => !existing.has(record.email)); + const toUpdate = rows.filter((record) => existing.has(record.email)); + + for (let index = 0; index < toInsert.length; index += BATCH) { + await trx('pppaimt_doctors').insert(toInsert.slice(index, index + BATCH)); + } + + for (const record of toUpdate) { + await trx('pppaimt_doctors') + .where({ email: record.email }) + .update({ ...record, updated_at: trx.fn.now() }); + } + + summary.inserted = toInsert.length; + summary.updated = toUpdate.length; + await recordAudit( + { + actorType: 'SYSTEM', + action: 'DOCTORS_IMPORTED', + entityType: 'pppaimt_doctors', + metadata: { + file: fileName, + rowsRead: summary.rowsRead, + inserted: summary.inserted, + updated: summary.updated, + rejected: summary.rejected, + }, + }, + trx, + ); + }); + + return summary; +} + +/** Format an import summary for the console. */ +export function formatSummary(summary) { + const describe = (row) => `row ${row.row} (${row.name || 'no name'})`; + const lines = [ + summary.stopped + ? `Import stopped: ${summary.file}. Nothing was written.` + : `Doctor import: ${summary.file}`, + ` Rows read: ${summary.rowsRead}`, + ` Inserted: ${summary.inserted}`, + ` Updated: ${summary.updated}`, + ` Rejected: ${summary.rejected}`, + ]; + + if (summary.missing.length) { + lines.push(`Missing columns: ${summary.missing.join(', ')}`); + } + + if (summary.unexpected.length) { + lines.push(`Unexpected columns: ${summary.unexpected.join(', ')}`); + } + + if (summary.stopped && summary.rowsRead === 0) { + lines.push('The file has no data rows.'); + } + + if (summary.duplicates.length) { + lines.push('Duplicate emails:'); + summary.duplicates.forEach((group) => + lines.push(` ${group.email} ${group.rows.map(describe).join(', ')}`), + ); + } + + if (summary.invalid.length) { + lines.push('Invalid rows:'); + summary.invalid.forEach((row) => lines.push(` ${describe(row)}: ${row.reason}`)); + } + + return lines.join('\n'); +} diff --git a/backend/src/services/pdfCache.js b/backend/src/services/pdfCache.js new file mode 100644 index 0000000..d5c3864 --- /dev/null +++ b/backend/src/services/pdfCache.js @@ -0,0 +1,94 @@ +import { createHash } from 'node:crypto'; +import { env } from '../config/env.js'; +import { renderPdf } from '../pdf/render.js'; +import { getObject, putObject } from './storage.js'; + +/** @type {Map>} */ +const inFlight = new Map(); + +/** + * Whether stored PDFs may be rebuilt (PDF_REGENERATE is `changed` or `always`). + */ +export function pdfRegenerationEnabled() { + return env.PDF_REGENERATE !== 'never'; +} + +/** + * @param {string} html + */ +function fingerprint(html) { + return createHash('sha256').update(html).digest('hex'); +} + +/** + * @param {string} key + */ +function fingerprintKey(key) { + return `${key}.sha256`; +} + +/** + * @param {string} key + * @returns {Promise} + */ +async function storedFingerprint(key) { + try { + return String(await getObject(fingerprintKey(key))).trim(); + } catch { + return null; + } +} + +/** + * Rebuild a stored PDF in place when PDF_REGENERATE allows it. The file is + * overwritten under the same key and the fingerprint lives beside it in + * storage, so database rows (shared with other environments) never change. + * @param {string} key + * @param {() => Promise} buildHtml + * @returns {Promise} the new PDF, or null when the stored file stays + */ +export async function refreshStoredPdf(key, buildHtml) { + if (!pdfRegenerationEnabled()) { + return null; + } + + const pending = inFlight.get(key); + + if (pending) { + return pending; + } + + const work = (async () => { + const html = await buildHtml(); + const next = fingerprint(html); + + if (env.PDF_REGENERATE === 'changed' && (await storedFingerprint(key)) === next) { + return null; + } + + const pdf = await renderPdf(html); + await putObject(key, pdf, 'application/pdf'); + await putObject(fingerprintKey(key), next, 'text/plain'); + return pdf; + })(); + + inFlight.set(key, work); + + try { + return await work; + } finally { + inFlight.delete(key); + } +} + +/** + * Record the fingerprint of a PDF that was just generated for the first time, + * so `changed` mode does not rebuild it straight away. + * @param {string} key + * @param {string} html + */ +export async function rememberPdfSource(key, html) { + if (pdfRegenerationEnabled()) { + await putObject(fingerprintKey(key), fingerprint(html), 'text/plain'); + } +} diff --git a/backend/src/services/userAuth.js b/backend/src/services/userAuth.js index 3c01668..70fbd13 100644 --- a/backend/src/services/userAuth.js +++ b/backend/src/services/userAuth.js @@ -3,6 +3,7 @@ import Boom from '@hapi/boom'; import jwt from 'jsonwebtoken'; import { db } from '../db/knex.js'; import { env } from '../config/env.js'; +import { refreshAgreementPdfs } from './agreementPdfRefresh.js'; import { presentApplication } from './applications.js'; import { recordAudit } from './audit.js'; import { sendLoginOtp } from './mail.js'; @@ -13,10 +14,7 @@ const OTP_TTL_MS = 10 * 60 * 1000; const MAX_ATTEMPTS = 5; const OTP_LENGTH = 6; -/** - * @param {number|string} userId - * @returns {string} - */ +/** Sign the physician session JWT (8 hours). */ export function signUserToken(userId) { if (!env.JWT_SECRET) { throw new Error('JWT_SECRET is required'); @@ -25,10 +23,7 @@ export function signUserToken(userId) { return jwt.sign({ sub: String(userId), role: 'user' }, env.JWT_SECRET, { expiresIn: '8h' }); } -/** - * @param {string} token - * @returns {string | null} - */ +/** Return the physician user id from a session JWT, or null if invalid. */ export function readUserId(token) { if (!env.JWT_SECRET) { return null; @@ -51,33 +46,42 @@ export function readUserId(token) { } } -/** - * @param {string} email - */ +/** Trim and lowercase an email for lookups. */ function normalizeEmail(email) { return email.trim().toLowerCase(); } -/** - * @returns {string} - */ +/** Return the listed doctor's HCP NAME without a leading "Dr"/"Dr."; unlisted emails get "Invalid User!". */ +async function doctorNameFor(email) { + const doctor = await db('pppaimt_doctors').where({ email }).first('hcp_name'); + + if (!doctor) { + throw Boom.forbidden('Invalid User!'); + } + + return String(doctor.hcp_name ?? '').trim().replace(/^dr(\.\s*|\s+)/i, '') || null; +} + +/** Generate a random 6-digit login code. */ function generateOtp() { const max = 10 ** OTP_LENGTH; return String(randomInt(0, max)).padStart(OTP_LENGTH, '0'); } -/** - * @param {string} email - */ -async function ensureUserAndApplication(email) { +/** Find or create the physician user and draft application, filling empty names from the doctor list. */ +async function ensureUserAndApplication(email, doctorName) { let user = await db('users').where({ email }).first(); if (!user) { const [id] = await db('users').insert({ email, + name: doctorName, is_active: true, }); user = await db('users').where({ id }).first(); + } else if (doctorName && !String(user.name ?? '').trim()) { + await db('users').where({ id: user.id }).update({ name: doctorName }); + user = await db('users').where({ id: user.id }).first(); } if (!user?.is_active) { @@ -102,20 +106,25 @@ async function ensureUserAndApplication(email) { email, status: 'DRAFT', user_id: user.id, + full_name: doctorName, }); application = await db('applications').where({ id }).first(); } } + if (doctorName && application.status === 'DRAFT' && !String(application.full_name ?? '').trim()) { + await db('applications').where({ id: application.id }).update({ full_name: doctorName }); + application = await db('applications').where({ id: application.id }).first(); + } + return { user, application }; } -/** - * @param {{ email: string, ip?: string | null }} input - */ +/** Email a login code to a doctor listed in pppaimt_doctors; others get "Invalid User!". */ export async function requestLoginOtp(input) { const email = normalizeEmail(input.email); - const { user, application } = await ensureUserAndApplication(email); + const doctorName = await doctorNameFor(email); + const { user, application } = await ensureUserAndApplication(email, doctorName); const code = generateOtp(); const now = new Date(); @@ -157,12 +166,11 @@ export async function requestLoginOtp(input) { }; } -/** - * @param {{ email: string, otp: string, ip?: string | null }} input - */ +/** Check a login code for a listed doctor and return the signed-in user. */ export async function verifyLoginOtp(input) { const email = normalizeEmail(input.email); const otp = String(input.otp ?? '').trim(); + const doctorName = await doctorNameFor(email); if (!/^\d{6}$/.test(otp)) { throw Boom.unauthorized('Invalid or expired code'); @@ -193,7 +201,7 @@ export async function verifyLoginOtp(input) { throw Boom.unauthorized('Invalid or expired code'); } - const { user, application } = await ensureUserAndApplication(email); + const { user, application } = await ensureUserAndApplication(email, doctorName); const now = new Date(); await db('login_otps').where({ id: row.id }).update({ used_at: now }); @@ -215,9 +223,7 @@ export async function verifyLoginOtp(input) { }; } -/** - * @param {Record} user - */ +/** Public fields of a physician user. */ export function presentUser(user) { return { id: user.id, @@ -226,9 +232,7 @@ export function presentUser(user) { }; } -/** - * @param {number} userId - */ +/** Dashboard state for a physician: user, application, survey, steps and next step. */ export async function getMe(userId) { const user = await db('users').where({ id: userId }).first(); @@ -263,6 +267,7 @@ export async function getMe(userId) { let applicationSummary = null; if (application) { + await refreshAgreementPdfs(application); const presented = presentApplication(application); applicationSummary = { id: application.id, diff --git a/backend/src/survey/definition.js b/backend/src/survey/definition.js index 307c4ed..6cb54aa 100644 --- a/backend/src/survey/definition.js +++ b/backend/src/survey/definition.js @@ -13,6 +13,10 @@ const packColumns = [ { id: 'copd', label: 'COPD' }, ]; +const packDefinitionsNote = + 'Regular Pack: Formoterol/Budesonide 200 & 400 mcg – Rotacaps (pack of 30) and MDI (120 doses).\n' + + 'Extended-Dose Pack: Formoterol/Budesonide 200 & 400 mcg – Rotacaps (pack of 60) and MDI (180 doses).'; + const indianStatesAndUnionTerritories = [ 'Andaman and Nicobar Islands', 'Andhra Pradesh', @@ -112,10 +116,12 @@ export const surveyDefinition = { type: 'multi_choice', label: 'Practice setting', required: true, + other: true, options: [ { id: 'private_clinic', label: 'Private Clinic' }, { id: 'private_hospital', label: 'Private Hospital' }, { id: 'govt_hospital', label: 'Govt. Hospital' }, + { id: 'other', label: 'Other (please specify)' }, ], }, { @@ -240,7 +246,13 @@ export const surveyDefinition = { id: 'pack_intro', type: 'section_header', label: - 'Among your patients receiving Foracort, approximately what proportion do you prescribe (each column must total 100%):', + 'Among your patients receiving Formoterol/Budesonide, approximately what proportion do you prescribe (each column must total 100%):', + }, + { + id: 'pack_definitions', + type: 'help', + style: 'note', + text: packDefinitionsNote, }, { id: 'pack_split', @@ -274,6 +286,12 @@ export const surveyDefinition = { type: 'help', text: 'A patient is considered "adherent" if the physician believes the patient takes at least 75% of the prescribed inhaled maintenance medication doses during a typical month of treatment.', }, + { + id: 'adherence_pack_definitions', + type: 'help', + style: 'note', + text: packDefinitionsNote, + }, { id: 'adherence_estimate', type: 'grid_number', @@ -406,36 +424,36 @@ export const surveyDefinition = { required: true, scale: likertScale, rows: [ - { id: 'convenient', label: 'Using an adherence pack is more convenient for patients' }, + { id: 'convenient', label: 'Using an Extended-Dose Pack is more convenient for patients' }, { id: 'refills', - label: 'Using an adherence pack reduces the frequency of medication refills', + label: 'Using an Extended-Dose Pack reduces the frequency of medication refills', }, { id: 'adherence', - label: 'Using an adherence pack improves medication adherence', + label: 'Using an Extended-Dose Pack improves medication adherence', }, { id: 'continuity', - label: 'Using an adherence pack helps in maintaining treatment continuity', + label: 'Using an Extended-Dose Pack helps in maintaining treatment continuity', }, { id: 'satisfaction', - label: 'Using an adherence pack improves patient satisfaction', + label: 'Using an Extended-Dose Pack improves patient satisfaction', }, { id: 'control', - label: 'Using an adherence pack helps achieve better disease control', + label: 'Using an Extended-Dose Pack helps achieve better disease control', }, { id: 'cost', label: - 'Using an adherence pack is more cost-beneficial for patients because it reduces the frequency of doctor visits', + 'Using an Extended-Dose Pack is more cost-beneficial for patients because it reduces the frequency of doctor visits', }, { id: 'delay', label: - 'Using an adherence pack may be detrimental because it can delay follow-up visits with the doctor', + 'Using an Extended-Dose Pack may be detrimental because it can delay follow-up visits with the doctor', }, { id: 'purchase', diff --git a/backend/test/agreementHtml.test.js b/backend/test/agreementHtml.test.js index 5705ab0..dafa5dc 100644 --- a/backend/test/agreementHtml.test.js +++ b/backend/test/agreementHtml.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { agreementHtml } from '../src/pdf/agreementHtml.js'; +import { agreementHtml, signedAtLabel } from '../src/pdf/agreementHtml.js'; const application = { status: 'DRAFT', @@ -75,4 +75,32 @@ describe('agreement HTML', () => { expect(html).not.toContain('Pending approval'); expect(html).not.toContain('DRAFT - NOT SIGNED'); }); + + it('dates the physician signature with the signing time in IST', () => { + const html = agreementHtml({ + application: { ...application, status: 'APPROVED' }, + signatory: { name: 'Dr Sudeep Salvi', designation: 'Medical Director' }, + physicianSignature: { type: 'TYPED', typedName: 'Asha', signedAt: '2026-10-29 03:50:00' }, + draft: false, + crestDate: '2026-10-30', + }); + + expect(html).toContain('Date: 29 October 2026, 09:20 AM IST'); + expect(html).toContain('Date: 30 October 2026'); + }); + + it('keeps the agreement date when there is no signing time yet', () => { + const html = agreementHtml({ application, physicianSignature: null, draft: true }); + + expect(html).toContain('Date: 30 September 2026'); + expect(html).not.toContain(' IST'); + }); + + it('formats UTC database times and Dates in 12-hour India time', () => { + expect(signedAtLabel('2026-10-28 20:00:00')).toBe('29 October 2026, 01:30 AM IST'); + expect(signedAtLabel('2026-10-29 09:00:00')).toBe('29 October 2026, 02:30 PM IST'); + expect(signedAtLabel(new Date('2026-10-29T06:30:00Z'))).toBe('29 October 2026, 12:00 PM IST'); + expect(signedAtLabel(null)).toBe(''); + expect(signedAtLabel('not a date')).toBe(''); + }); }); diff --git a/backend/test/applications.test.js b/backend/test/applications.test.js index 439b3ff..507e810 100644 --- a/backend/test/applications.test.js +++ b/backend/test/applications.test.js @@ -251,7 +251,7 @@ describe('applications', () => { method: 'POST', url: '/api/applications/me/sign', headers, - payload: { type: 'TYPED', typedName: 'Asha Rao', consent: true }, + payload: { type: 'TYPED', typedName: 'Asha Rao' }, }); expect(incomplete.statusCode).toBe(400); @@ -278,7 +278,7 @@ describe('applications', () => { method: 'POST', url: '/api/applications/me/sign', headers, - payload: { type: 'TYPED', typedName: 'Asha Rao', consent: true }, + payload: { type: 'TYPED', typedName: 'Asha Rao' }, }); expect(signed.statusCode).toBe(200); @@ -316,7 +316,7 @@ describe('applications', () => { method: 'POST', url: '/api/applications/me/sign', headers, - payload: { type: 'TYPED', typedName: 'Asha Rao', consent: true }, + payload: { type: 'TYPED', typedName: 'Asha Rao' }, }); expect(signed.statusCode).toBe(200); diff --git a/backend/test/applicationsExport.test.js b/backend/test/applicationsExport.test.js index 353c526..91ec860 100644 --- a/backend/test/applicationsExport.test.js +++ b/backend/test/applicationsExport.test.js @@ -39,7 +39,12 @@ describe('applications export', () => { await truncateTables(); }); - it('writes application, consent and survey columns for the created-date range', async () => { + /** + * One approved application inside the date range (with consent, signature + * and an in-progress survey) and one draft outside it. + * @returns {Promise} admin session cookie + */ + async function seedExport() { server = await createServer(); await db('admin_users').insert({ name: 'Administrator', @@ -102,20 +107,72 @@ describe('applications export', () => { started_at: new Date(), }); + return sessionCookie(login); + } + + /** + * @param {string} cookie + * @param {string} query + */ + async function exportSheet(cookie, query) { const response = await server.inject({ method: 'GET', - url: '/api/admin/applications/export?from=2026-10-01&to=2026-10-31', - headers: { cookie: sessionCookie(login) }, + url: `/api/admin/applications/export?${query}`, + headers: { cookie }, }); - expect(response.statusCode).toBe(200); - expect(response.headers['content-disposition']).toContain('applications-consent.xlsx'); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(response.rawPayload); const sheet = workbook.getWorksheet('Applications'); - const headers = sheet.getRow(1).values; - const cell = (/** @type {string} */ header) => sheet.getRow(2).getCell(headers.indexOf(header)).value; + const headers = sheet.getRow(1).values.slice(1); + const cell = (/** @type {string} */ header) => + sheet.getRow(2).getCell(headers.indexOf(header) + 1).value; + return { response, sheet, headers, cell }; + } + async function exportAudit() { + const audit = await db('audit_log').where({ action: 'APPLICATIONS_EXPORTED' }).first(); + return typeof audit.metadata === 'string' ? JSON.parse(audit.metadata) : audit.metadata; + } + + it('writes only basic columns by default', async () => { + const cookie = await seedExport(); + const { response, sheet, headers, cell } = await exportSheet( + cookie, + 'from=2026-10-01&to=2026-10-31', + ); + + expect(response.headers['content-disposition']).toContain('"applications-consent.xlsx"'); + expect(headers).toEqual([ + 'Application id', + 'Status', + 'Email', + 'Full name', + 'Age', + 'Address', + 'Created at', + 'Agreement date', + 'Signed', + 'Consent date', + 'Survey status', + 'Survey completed at', + ]); + expect(sheet.rowCount).toBe(2); + expect(cell('Status')).toBe('Approved'); + expect(cell('Consent date')).toBe('2026-10-10'); + expect(JSON.stringify(sheet.getSheetValues())).not.toContain('ABCDE1234F'); + expect(JSON.stringify(sheet.getSheetValues())).not.toContain('001234567890'); + expect(await exportAudit()).toMatchObject({ full: false }); + }); + + it('writes every column for the full report', async () => { + const cookie = await seedExport(); + const { response, sheet, cell } = await exportSheet( + cookie, + 'from=2026-10-01&to=2026-10-31&full=true', + ); + + expect(response.headers['content-disposition']).toContain('applications-consent-full.xlsx'); expect(sheet.rowCount).toBe(2); expect(sheet.getRow(1).getCell(1).font?.bold).toBe(true); expect(cell('Status')).toBe('Approved'); @@ -128,7 +185,7 @@ describe('applications export', () => { expect(cell(CONSENT_STATEMENTS[0].label)).toBe('Yes'); expect(cell('I agree and consent to participate in the survey.')).toBe('Yes'); expect(cell('Survey status')).toBe('In progress'); - expect(await db('audit_log').where({ action: 'APPLICATIONS_EXPORTED' }).first()).toBeTruthy(); + expect(await exportAudit()).toMatchObject({ full: true }); }); it('rejects malformed dates', async () => { diff --git a/backend/test/consent.test.js b/backend/test/consent.test.js index 0bb3b04..65dff4b 100644 --- a/backend/test/consent.test.js +++ b/backend/test/consent.test.js @@ -48,6 +48,7 @@ describe('consent', () => { }); async function start() { + await db('pppaimt_doctors').insert({ email, hcp_name: 'Dr Consent Test' }); server = await createServer(); return server; } diff --git a/backend/test/consentHtml.test.js b/backend/test/consentHtml.test.js index c40aa5c..684076a 100644 --- a/backend/test/consentHtml.test.js +++ b/backend/test/consentHtml.test.js @@ -30,19 +30,42 @@ describe('consent PDF html', () => { expect(html).not.toContain('class="box checked"'); }); - it('uses the PIS document line as the page header', () => { + it('uses the PIS document line as the page header and the original footer', () => { const html = consentHtml({ participantName: 'A', consentDate: '2026-10-27', answers: {} }); expect(html).toContain('@top-left { content: "PIS and ICF V01" "\\A" "Date: 14/10/2026"'); - expect(html).toContain('counter(page) " of " counter(pages)'); + expect(html).toContain('size: letter;'); + expect(html).toContain('Page of '); + expect(html).toContain('.footer .page::after { content: counter(page); font-weight: 700; }'); expect(html).not.toContain('**Document:**'); }); - it('starts page two at the same section as the original document', () => { + it('embeds the bundled Carlito font', () => { + const html = consentHtml({ participantName: 'A', consentDate: '2026-10-27', answers: {} }); + + expect(html).toMatch(/src: url\("file:\/\/[^"]+\/fonts\/Carlito-Regular\.ttf"\)/); + expect(html).toMatch(/src: url\("file:\/\/[^"]+\/fonts\/Carlito-Bold\.ttf"\)/); + }); + + it('lays out page one like the original', () => { + const html = consentHtml({ participantName: 'A', consentDate: '2026-10-27', answers: {} }); + const body = pisBodyHtml(); + + expect(html).toContain( + '

    ONLINE

    \n

    PARTICIPANT INFORMATION SHEET (PIS)

    ', + ); + expect(body).toContain('

    Survey Title: Physicians’ Perceptions'); + expect(body).toContain('maintenance therapy.
    Therefore, we are conducting'); + expect(body).not.toContain('Background'); + }); + + it('starts page two at the same section and puts the investigators side by side', () => { const body = pisBodyHtml(); expect(body).toContain('

    What will you be expected to do?

    '); - expect(body).toContain('Survey Title:'); - expect(body).not.toContain('Background'); + expect(body).toContain('

    Contact Details:

    '); + expect(body).toMatch( + /
    Principal Investigator<\/strong>
    Name: Dr\. Sundeep Salvi.*<\/td>
    Co-investigator<\/strong>/, + ); }); }); diff --git a/backend/test/doctorImport.test.js b/backend/test/doctorImport.test.js new file mode 100644 index 0000000..a51a3f0 --- /dev/null +++ b/backend/test/doctorImport.test.js @@ -0,0 +1,171 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { Readable } from 'node:stream'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { db } from '../src/db/knex.js'; +import { + formatSummary, + importDoctors, + parseDoctorCsv, + validateDoctors, +} from '../src/services/doctorImport.js'; +import { truncateTables } from './truncate.js'; + +const HEADER = + 'Sr NO,TASKFORCE,ZBM Name,RBM Name,ABM Name,HQ,TM ID,TM Name,HCP NAME,LEGACY_CODE,PCODE,SPECIALITY,CLASS,VENDORCODE,VENDORNAME,Email ID,Contact No'; + +/** One CSV data row in the field team's format. */ +function line(srNo, name, email, contact = '9769283034') { + return `${srNo},Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,014262,Salim Khan,${name},CIP833167,011667437,Chest Physician,A+,100064617,${name},${email},${contact}`; +} + +/** Parse and validate CSV text with the header row added. */ +async function check(lines, header = HEADER) { + return validateDoctors(await parseDoctorCsv(Readable.from([[header, ...lines].join('\n')]))); +} + +describe('doctor CSV validation', () => { + it('maps every column, ignores Sr NO and lowercases the email', async () => { + const result = await check([line(1, 'Sandeep S Tilve', ' Tilve@Gmail.com ')]); + + expect(result).toMatchObject({ missing: [], unexpected: [], invalid: [], duplicates: [] }); + expect(result.records[0]).toEqual({ + taskforce: 'Respiratory 2', + zbm_name: 'Shaikh Mohseen Ahmed', + rbm_name: 'Satish Dhonde', + abm_name: 'Adityakumar Tiwari', + hq: 'mumbai western', + tm_id: '014262', + tm_name: 'Salim Khan', + hcp_name: 'Sandeep S Tilve', + legacy_code: 'CIP833167', + pcode: '011667437', + speciality: 'Chest Physician', + class: 'A+', + vendor_code: '100064617', + vendor_name: 'Sandeep S Tilve', + email: 'tilve@gmail.com', + contact_no: '9769283034', + }); + }); + + it('reports missing and unexpected headers', async () => { + const result = await check(['1,A,a@x.com,y'], 'Sr NO,HCP NAME,Email,Extra'); + + expect(result.missing).toContain('Email ID'); + expect(result.unexpected).toEqual(['Email', 'Extra']); + }); + + it('rejects missing and badly formatted emails and short rows', async () => { + const result = await check([ + line(1, 'Dr A', 'a@x.com'), + line(2, 'Dr B', 'b@x'), + line(3, 'Dr C', ''), + '4,short', + ]); + + expect(result.records.map((record) => record.email)).toEqual(['a@x.com']); + expect(result.invalid).toEqual([ + { row: 2, name: 'Dr B', email: 'b@x', reason: '"b@x" is not a valid email' }, + { row: 3, name: 'Dr C', email: '', reason: 'email is missing' }, + { row: 4, name: '', email: '', reason: 'wrong number of columns' }, + ]); + }); + + it('groups duplicate emails regardless of case', async () => { + const result = await check([ + line(1, 'Dr A', 'a@x.com'), + line(2, 'Dr B', 'b@x.com'), + line(3, 'Dr A2', 'A@X.com'), + ]); + + expect(result.duplicates).toEqual([ + { + email: 'a@x.com', + rows: [ + { row: 1, name: 'Dr A' }, + { row: 3, name: 'Dr A2' }, + ], + }, + ]); + }); + + it('only accepts a plain .csv file name', async () => { + await expect(importDoctors(undefined)).rejects.toThrow(/Pass the CSV file name/); + await expect(importDoctors('../secrets.csv')).rejects.toThrow(/not a path/); + await expect(importDoctors('doctors.txt')).rejects.toThrow(/not a path/); + await expect(importDoctors('missing-file.csv')).rejects.toThrow(/File not found/); + }); +}); + +describe('doctor CSV import', () => { + let directory; + + beforeEach(async () => { + directory = await mkdtemp(path.join(tmpdir(), 'doctor-import-')); + }); + + afterEach(async () => { + await rm(directory, { recursive: true, force: true }); + await truncateTables(); + }); + + /** Write a CSV into the temp import folder and return its name. */ + async function write(name, lines) { + await writeFile(path.join(directory, name), [HEADER, ...lines].join('\n')); + return name; + } + + /** Rows currently in pppaimt_doctors. */ + async function doctorCount() { + return Number((await db('pppaimt_doctors').count({ total: '*' }).first())?.total); + } + + it('inserts new doctors, then updates them by email on re-import', async () => { + const first = await importDoctors( + await write('first.csv', [line(1, 'Dr A', 'a@x.com'), line(2, 'Dr B', 'b@x.com')]), + { directory }, + ); + + expect(first).toMatchObject({ stopped: false, rowsRead: 2, inserted: 2, updated: 0, rejected: 0 }); + expect(await doctorCount()).toBe(2); + + const second = await importDoctors( + await write('second.csv', [ + line(1, 'Dr A', 'A@X.com', '9000000000'), + line(3, 'Dr C', 'c@x.com'), + ]), + { directory }, + ); + + expect(second).toMatchObject({ stopped: false, inserted: 1, updated: 1, rejected: 0 }); + expect(await db('pppaimt_doctors').where({ email: 'a@x.com' }).first()).toMatchObject({ + contact_no: '9000000000', + source_file: 'second.csv', + }); + expect(await doctorCount()).toBe(3); + const audits = await db('audit_log').where({ action: 'DOCTORS_IMPORTED' }).count({ total: '*' }).first(); + expect(Number(audits?.total)).toBe(2); + }); + + it('writes nothing when the file has duplicate or invalid emails', async () => { + const summary = await importDoctors( + await write('bad.csv', [ + line(1, 'Dr A', 'a@x.com'), + line(2, 'Dr A again', 'a@x.com'), + line(3, 'Dr B', 'b@x'), + line(4, 'Dr C', 'c@x.com'), + ]), + { directory }, + ); + + expect(summary).toMatchObject({ stopped: true, inserted: 0, updated: 0, rejected: 3 }); + expect(await doctorCount()).toBe(0); + + const report = formatSummary(summary); + expect(report).toContain('Import stopped: bad.csv. Nothing was written.'); + expect(report).toContain('a@x.com row 1 (Dr A), row 2 (Dr A again)'); + expect(report).toContain('row 3 (Dr B): "b@x" is not a valid email'); + }); +}); diff --git a/backend/test/pdfCache.test.js b/backend/test/pdfCache.test.js new file mode 100644 index 0000000..36f4433 --- /dev/null +++ b/backend/test/pdfCache.test.js @@ -0,0 +1,117 @@ +import { spawnSync } from 'node:child_process'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + env: { PDF_REGENERATE: 'never' }, + files: new Map(), +})); + +vi.mock('../src/config/env.js', () => ({ env: mocks.env })); +vi.mock('../src/pdf/render.js', () => ({ + renderPdf: vi.fn(async (html) => Buffer.from(`pdf:${html}`)), +})); +vi.mock('../src/services/storage.js', () => ({ + getObject: vi.fn(async (key) => { + if (!mocks.files.has(key)) { + throw new Error('missing'); + } + return mocks.files.get(key); + }), + putObject: vi.fn(async (key, body) => { + mocks.files.set(key, Buffer.from(body)); + }), +})); + +const { refreshStoredPdf, rememberPdfSource } = await import('../src/services/pdfCache.js'); +const { renderPdf } = await import('../src/pdf/render.js'); + +const KEY = 'applications/abc/consent.pdf'; + +describe('PDF_REGENERATE', () => { + afterEach(() => { + mocks.files.clear(); + vi.mocked(renderPdf).mockClear(); + }); + + it('never rebuilds a stored PDF by default', async () => { + mocks.env.PDF_REGENERATE = 'never'; + const build = vi.fn(async () => '

    v2

    '); + + expect(await refreshStoredPdf(KEY, build)).toBeNull(); + expect(build).not.toHaveBeenCalled(); + expect(renderPdf).not.toHaveBeenCalled(); + }); + + it('rebuilds only when the html changed in changed mode', async () => { + mocks.env.PDF_REGENERATE = 'changed'; + await rememberPdfSource(KEY, '

    v1

    '); + + expect(await refreshStoredPdf(KEY, async () => '

    v1

    ')).toBeNull(); + expect(renderPdf).not.toHaveBeenCalled(); + + const rebuilt = await refreshStoredPdf(KEY, async () => '

    v2

    '); + expect(String(rebuilt)).toBe('pdf:

    v2

    '); + expect(String(mocks.files.get(KEY))).toBe('pdf:

    v2

    '); + + expect(await refreshStoredPdf(KEY, async () => '

    v2

    ')).toBeNull(); + expect(renderPdf).toHaveBeenCalledTimes(1); + }); + + it('rebuilds a PDF with no recorded source in changed mode', async () => { + mocks.env.PDF_REGENERATE = 'changed'; + mocks.files.set(KEY, Buffer.from('old pdf')); + + expect(String(await refreshStoredPdf(KEY, async () => '

    v1

    '))).toBe('pdf:

    v1

    '); + }); + + it('rebuilds on every call in always mode', async () => { + mocks.env.PDF_REGENERATE = 'always'; + await refreshStoredPdf(KEY, async () => '

    v1

    '); + await refreshStoredPdf(KEY, async () => '

    v1

    '); + + expect(renderPdf).toHaveBeenCalledTimes(2); + }); + + it('shares one render between simultaneous requests for the same file', async () => { + mocks.env.PDF_REGENERATE = 'always'; + const build = vi.fn(async () => '

    v1

    '); + + await Promise.all([refreshStoredPdf(KEY, build), refreshStoredPdf(KEY, build)]); + + expect(build).toHaveBeenCalledTimes(1); + expect(renderPdf).toHaveBeenCalledTimes(1); + }); +}); + +describe('PDF_REGENERATE in production', () => { + const envModule = pathToFileURL( + path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../src/config/env.js'), + ).href; + + /** + * Runs outside backend/ so a developer's backend/.env is not loaded. + * @param {Record} extra + */ + function loadEnv(extra) { + return spawnSync( + process.execPath, + ['--input-type=module', '-e', `await import(${JSON.stringify(envModule)})`], + { cwd: tmpdir(), env: { PATH: process.env.PATH, ...extra }, encoding: 'utf8' }, + ); + } + + it('refuses to start with regeneration enabled', () => { + const result = loadEnv({ NODE_ENV: 'production', PDF_REGENERATE: 'changed' }); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('PDF_REGENERATE=changed is not allowed in production'); + }); + + it('starts with regeneration off', () => { + expect(loadEnv({ NODE_ENV: 'production', PDF_REGENERATE: 'never' }).status).toBe(0); + expect(loadEnv({ NODE_ENV: 'production' }).status).toBe(0); + }); +}); diff --git a/backend/test/survey.test.js b/backend/test/survey.test.js index bda8741..279113a 100644 --- a/backend/test/survey.test.js +++ b/backend/test/survey.test.js @@ -204,7 +204,7 @@ describe('survey answers', () => { required: true, scale: [{ id: 'agree', label: 'Agree' }], rows: [ - { id: 'convenient', label: 'Using an adherence pack is more convenient for patients' }, + { id: 'convenient', label: 'Using an Extended-Dose Pack is more convenient for patients' }, ], }); expect(validateAnswers(likert, {})[0].message).toMatch(/required/); @@ -239,6 +239,18 @@ describe('survey answers', () => { expect(validateAnswers(setting, knownAnswers(setting, legacy))).toEqual([]); }); + it('requires the text when practice setting Other is ticked', () => { + const settingErrors = (/** @type {Record} */ practice) => + validateAnswers(surveyDefinition, { practice_setting: practice }).filter( + (error) => error.id === 'practice_setting', + ); + + expect(settingErrors({ choices: ['other'], other: '' })[0].message).toBe( + 'Practice setting requires the other text', + ); + expect(settingErrors({ choices: ['private_clinic', 'other'], other: 'Nursing home' })).toEqual([]); + }); + it('upgrades a typed state to the matching dropdown option', () => { const state = wrap({ id: 'state', diff --git a/backend/test/surveyExport.test.js b/backend/test/surveyExport.test.js index cd882ab..c539d4a 100644 --- a/backend/test/surveyExport.test.js +++ b/backend/test/surveyExport.test.js @@ -60,7 +60,7 @@ describe('survey export', () => { answers: JSON.stringify({ name: 'Dr Asha Rao', adherence_comparison: { choice: 'much_better' }, - practice_setting: { choices: ['private_clinic', 'govt_hospital'] }, + practice_setting: { choices: ['private_clinic', 'govt_hospital', 'other'], other: 'Nursing home' }, barriers: { choices: ['cost', 'other'], other: 'Lack of time' }, benefit: { choices: ['elderly'] }, pack_split: { @@ -117,15 +117,17 @@ describe('survey export', () => { const settingStart = headings.indexOf('Practice Setting') + 1; expect(settingStart).toBeGreaterThan(0); - expect(responses.getCell(1, settingStart + 2).isMerged).toBe(true); - expect(optionLabels.slice(settingStart, settingStart + 3)).toEqual([ + expect(responses.getCell(1, settingStart + 3).isMerged).toBe(true); + expect(optionLabels.slice(settingStart, settingStart + 4)).toEqual([ 'Private Clinic', 'Private Hospital', 'Govt. Hospital', + 'Other (please specify)', ]); expect(data.getCell(settingStart).value).toBe('Yes'); expect(data.getCell(settingStart + 1).value).toBeNull(); expect(data.getCell(settingStart + 2).value).toBe('Yes'); + expect(data.getCell(settingStart + 3).value).toBe('Nursing home'); const questions = workbook.getWorksheet('Questions'); const questionIds = questions.getColumn(1).values; expect(questionIds).toContain('pack_split'); diff --git a/backend/test/truncate.js b/backend/test/truncate.js index 26de4cc..97045b7 100644 --- a/backend/test/truncate.js +++ b/backend/test/truncate.js @@ -9,6 +9,7 @@ const TABLES = [ 'signatures', 'resume_tokens', 'consents', + 'pppaimt_doctors', 'login_otps', 'applications', 'users', diff --git a/backend/test/userAuth.test.js b/backend/test/userAuth.test.js index ba67139..d80f7ea 100644 --- a/backend/test/userAuth.test.js +++ b/backend/test/userAuth.test.js @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto'; import { afterEach, describe, expect, it } from 'vitest'; import { createServer } from '../src/app.js'; import { db } from '../src/db/knex.js'; @@ -28,6 +29,7 @@ describe('user OTP auth', () => { }); async function start() { + await db('pppaimt_doctors').insert({ email, hcp_name: 'Dr Test' }); server = await createServer(); return server; } @@ -98,6 +100,18 @@ describe('user OTP auth', () => { const missing = await api.inject({ method: 'GET', url: '/api/me' }); expect(missing.statusCode).toBe(401); + expect(await db('audit_log').where({ action: 'USER_LOGOUT' }).first()).toBeTruthy(); + }); + + it('clears the cookie even without a valid session', async () => { + const api = await start(); + + const logout = await api.inject({ method: 'POST', url: '/api/auth/logout' }); + + expect(logout.statusCode).toBe(200); + expect(logout.result).toEqual({ ok: true }); + expect(String(logout.headers['set-cookie'])).toMatch(/user_token=.*(Max-Age=0|Expires=)/i); + expect(await db('audit_log').where({ action: 'USER_LOGOUT' }).first()).toBeUndefined(); }); it('rejects an invalid OTP', async () => { @@ -117,4 +131,86 @@ describe('user OTP auth', () => { expect(verified.statusCode).toBe(401); }); + + it('refuses an email that is not in pppaimt_doctors without creating anything', async () => { + const api = await start(); + const stranger = 'not-a-doctor@example.com'; + + const requested = await api.inject({ + method: 'POST', + url: '/api/auth/otp/request', + payload: { email: stranger }, + }); + + expect(requested.statusCode).toBe(403); + expect(requested.result.error.message).toBe('Invalid User!'); + expect(await db('users').where({ email: stranger }).first()).toBeUndefined(); + expect(await db('applications').where({ email: stranger }).first()).toBeUndefined(); + expect(await db('login_otps').where({ email: stranger }).first()).toBeUndefined(); + expect(outbox).toHaveLength(0); + + const verified = await api.inject({ + method: 'POST', + url: '/api/auth/otp/verify', + payload: { email: stranger, otp: '123456' }, + }); + + expect(verified.statusCode).toBe(403); + }); + + it('fills the user and draft application name from HCP NAME without a leading Dr', async () => { + const api = await start(); + const doctorEmail = 'tilve@example.com'; + await db('pppaimt_doctors').insert({ email: doctorEmail, hcp_name: 'Dr. Sandeep S Tilve' }); + + await api.inject({ method: 'POST', url: '/api/auth/otp/request', payload: { email: doctorEmail } }); + + const user = await db('users').where({ email: doctorEmail }).first(); + expect(user.name).toBe('Sandeep S Tilve'); + const application = await db('applications').where({ user_id: user.id }).first(); + expect(application.full_name).toBe('Sandeep S Tilve'); + expect(application.account_holder).toBeNull(); + }); + + it('keeps names the doctor already entered', async () => { + const api = await start(); + const [userId] = await db('users').insert({ email, name: 'Typed Name', is_active: true }); + await db('applications').insert({ + public_id: randomUUID(), + email, + status: 'DRAFT', + user_id: userId, + full_name: 'Typed Full Name', + }); + + await api.inject({ method: 'POST', url: '/api/auth/otp/request', payload: { email } }); + + expect((await db('users').where({ id: userId }).first()).name).toBe('Typed Name'); + expect((await db('applications').where({ user_id: userId }).first()).full_name).toBe( + 'Typed Full Name', + ); + }); + + it('does not strip names that only start with the letters Dr', async () => { + const api = await start(); + const doctorEmail = 'drew@example.com'; + await db('pppaimt_doctors').insert({ email: doctorEmail, hcp_name: 'Drew Mathews' }); + + await api.inject({ method: 'POST', url: '/api/auth/otp/request', payload: { email: doctorEmail } }); + + expect((await db('users').where({ email: doctorEmail }).first()).name).toBe('Drew Mathews'); + }); + + it('matches the doctor email regardless of case', async () => { + const api = await start(); + + const requested = await api.inject({ + method: 'POST', + url: '/api/auth/otp/request', + payload: { email: 'Doctor@Example.COM' }, + }); + + expect(requested.statusCode).toBe(200); + expect(outbox).toHaveLength(1); + }); }); diff --git a/e2e/journey.spec.js b/e2e/journey.spec.js index 9280104..fd9394f 100644 --- a/e2e/journey.spec.js +++ b/e2e/journey.spec.js @@ -53,7 +53,6 @@ async function signAgreement(page) { await page.getByRole('button', { name: 'Proceed to sign' }).click(); await page.getByRole('tab', { name: 'Type instead' }).click(); await page.locator('#typed-name').fill('E2E Physician'); - await page.locator('#consent').check(); await page.getByRole('button', { name: 'Submit signature' }).click(); await expect(page.getByRole('heading', { name: 'Agreement submitted' })).toBeVisible(); } diff --git a/files/docs/pis.md b/files/docs/pis.md index 74eea29..1218edc 100644 --- a/files/docs/pis.md +++ b/files/docs/pis.md @@ -8,19 +8,16 @@ Physicians’ Perceptions of the Impact of an Extended-Dose Pack versus Regular ## Background -Asthma and chronic obstructive pulmonary disease (COPD) are chronic respiratory diseases that often require long-term treatment with inhaled maintenance medications. The effectiveness of treatment depends not only on the use of appropriate medication and correct inhaler technique but also on consistent adherence to the prescribed treatment. +Asthma and chronic obstructive pulmonary disease (COPD) are chronic respiratory diseases that often require long-term treatment with inhaled maintenance medications. The effectiveness of treatment depends not only on the use of appropriate medication and correct inhaler technique but also on consistent adherence to the prescribed treatment. For patient convenience, pharmaceutical companies in India also offer inhaled medications in extended-dose packs, such as dry powder inhaler (DPI) capsule packs containing a greater number of capsules and pressurized metered-dose inhalers (pMDIs) containing a greater number of inhaler puffs, while keeping the cost per unit the same. -For patient convenience, pharmaceutical companies in India also offer inhaled medications in extended-dose packs, such as dry powder inhaler (DPI) capsule packs containing a greater number of capsules and pressurized metered-dose inhalers (pMDIs) containing a greater number of inhaler puffs, while keeping the cost per unit the same. - -Routine clinical practice has not adequately studied the perceived impact of the Extended-Dose Pack on adherence to inhaled maintenance therapy. Therefore, we are conducting this survey to evaluate physicians’ perceptions regarding patient adherence to inhaled maintenance therapy, reasons for non-adherence, the potential benefits of the Extended-Dose Pack, and patient characteristics that may influence the choice of an Extended-Dose Pack compared with a Regular Pack. +Routine clinical practice has not adequately studied the perceived impact of the Extended-Dose Pack on adherence to inhaled maintenance therapy. +Therefore, we are conducting this survey to evaluate physicians’ perceptions regarding patient adherence to inhaled maintenance therapy, reasons for non-adherence, the potential benefits of the Extended-Dose Pack, and patient characteristics that may influence the choice of an Extended-Dose Pack compared with a Regular Pack. CREST would like to invite you to participate in this survey, which will be conducted among approximately 500 chest physicians and consultant physicians across India who have been prescribing both the Extended-Dose Pack and the Regular Pack for at least six months. You have been selected through purposive sampling from a predefined list of eligible physicians. ## About CREST -Chest Research and Training Pvt. Ltd. (CREST), Pune is an academic research and training organization founded by Dr. Sundeep Salvi. Dedicated to research, education and innovation in respiratory medicine. CREST focuses on generating high-quality evidence to improve respiratory healthcare in India, particularly in obstructive airways diseases. - -Equipped with state-of-the-art lung function testing facilities and a multidisciplinary team of clinicians, scientists, and allied professionals, CREST has extensive experience in epidemiological studies, clinical research and real-world generation. +Chest Research and Training Pvt. Ltd. (CREST), Pune is an academic research and training organization founded by Dr. Sundeep Salvi. Dedicated to research, education and innovation in respiratory medicine. CREST focuses on generating high-quality evidence to improve respiratory healthcare in India, particularly in obstructive airways diseases. Equipped with state-of-the-art lung function testing facilities and a multidisciplinary team of clinicians, scientists, and allied professionals, CREST has extensive experience in epidemiological studies, clinical research and real-world generation. ## What will you be expected to do? @@ -46,10 +43,12 @@ This survey is approved by the Central Independent Ethics Committee - CIEC, Pune ## Funding and Compensation -This survey is supported by Cipla Ltd. You will receive a professional honorarium of ₹15,000 as compensation for your time and participation, upon receipt of completed questionnaire and provision of your professional opinions and experience regarding prescribing practices. +You will receive a professional honorarium of ₹15,000 as compensation for your time and participation, upon receipt of completed questionnaire and provision of your professional opinions and experience regarding prescribing practices. As part of your participation, you will be required to sign a Memorandum of Understanding (MOU) with CREST outlining the terms and conditions of your participation in the survey. +This survey is supported by Cipla Ltd. + ## Contact Details Feel free to contact for any query/clarification. diff --git a/files/import/import-doctors-04-11-2026.csv b/files/import/import-doctors-04-11-2026.csv new file mode 100644 index 0000000..edc41e9 --- /dev/null +++ b/files/import/import-doctors-04-11-2026.csv @@ -0,0 +1,296 @@ +Sr NO,TASKFORCE,ZBM Name,RBM Name,ABM Name,HQ,TM ID,TM Name,HCP NAME,LEGACY_CODE,PCODE,SPECIALITY,CLASS,VENDORCODE,VENDORNAME,Email ID,Contact No +1,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,142621,Salim Khan,Sandeep S Tilve,CIP833167,11667437,Chest Physician,A+,100064617,Sandeep S Tilve,tilvesandeep@gmail.com,9769283034 +2,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,142624,Salim Khan,Rakesh Hansraj Rajpurohit,CIP481056952,13104066,Chest Physician,A+,100101375,Rakesh Hansraj Rajpurohit,dr.rajpurohit750@gmail.com,9545360750 +3,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,168186,Swati Pandey,Parthiv Atulkumar Shah,CIP30354,11834830,Chest Physician,A,100063029,Parthiv Atulkumar Shah,parth15dec@gmail.com,9029772904 +4,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,152148,Dev Pandey,Salil Subhash Bendre,CIP30347,11715346,Chest Physician,A+,100029108,Salil Subhash Bendre,salilsaumil@gmail.com,7738656663 +5,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,165235,Saurabh Pandey,Lancelot Pinto,CLX01430718,11410660,Chest Physician,A+,100027037,Lancelot M Pinto,lance.pinto@gmail.com,9820234567 +6,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,Thane western,146372,Shubham Tiwari,Sachin Ajgare,CIP4889023,13103699,Chest Physician,A,100089579,AJGARE SACHIN MADHUKAR,drsachinpulmao@yahoo.com,9975465142 +7,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,155785,Ujjwal pandey,Pankaj Bang ,CLX01367544,15020828,Chest Physician,A,100041992,PANKAJ KAMALKISHORE BANG,drpankaj.bang@gmail.com,8291122810 +8,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,Thane western,165197,Shiwakant Singh,Swami Pawar,CLX0803369,8079465,Chest Physician,A+,100039274,Swami D Pawar,swamipawar@yahoo.com,9820670931 +9,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,Thane western,165197,Shiwakant Singh,Kamlesh pandey,CLX1693907,8079464,Chest Physician,A,100063030,Kamleshkumar Vijayshankar Pandey,dr.kamleshp11@gmail.com,9833089690 +10,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,Thane western,146372,Shubham Tiwari,Shivprasad shinde,CIR85955,9179499,Chest Physician,A+,100065472,Shivprasad Narayan Shinde,shivprasadshinde07@gmail.com,9404734727 +11,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,168186,Swati Pandey,Bhavin Shah,CLX0187011,11834879,Chest Physician,A,100064570,BHAVIN M SHAH,drbhavins@gmail.com,9821664834 +12,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Amol Shinde,Kolhapur,121456,Sandip Kamble,AJINKYA GANGAN ,CIR57017,8192046,Chest Physician,A,100070661,Ajinkya Shripad Gangan,ajinkyagangan87@gmail.com,9833931287 +13,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Amol Shinde,Kolhapur,176737,Rushikesh Girgave,AJIT KULKARNI ,CLX0094077,11702674,Chest Physician,A+,100074052,AJIT VINAYAK KULKARNI,dr_ajit_kulkarni@yahoo.com,9371645808 +14,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Amol Shinde,Kolhapur,161827,Akash Jadhav,AKASH ARORA,CLX0682893,14173027,Chest Physician,A+,100086774,Akashdeep Singh Arora,drakasharora@hotmail.com,9823559033 +15,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Amol Shinde,Kolhapur,123424,Ganesh Deshmukh,ANANDKUMAR LUNIYA,CLX01683342,11718439,Chest Physician,A,100086771,Anandkumar Babulal Luniya,dranandluniya@gmail.com,9860727314 +16,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Amol Shinde,Kolhapur,121456,Sandip Kamble,GANGARAM T RANE,CIP14691,14068509,Consulting Physician,A,100075518,GANGARAM TATEBHAU RANE,drgirishrane@gmail.com,7083151666 +17,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Amol Shinde,Kolhapur,161827,Akash Jadhav,PRAVEEN N BHAT,CLX0006336,14192660,Chest Physician,A+,100062963,PRAVEEN BHAT,drpraveen.bhat@gmail.com,9822487130 +18,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Amol Shinde,Kolhapur,161827,Akash Jadhav,RAHUL PRABHUDESAI,CIP4839802,14182764,Chest Physician,A+,100086833,Rahul Rama Prabhudesai,respicaregoa@gmail.com,8208958129 +19,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Amol Shinde,Kolhapur,161827,Akash Jadhav,VIVEK PRABUDESAI,CLX0164484,14192674,Chest Physician,B,100033189,Vivek Govind Prabhudessai,govindprabhudesai@yahoo.com,9822104361 +20,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Amol Shinde,Kolhapur,123424,Ganesh Deshmukh,ASHUTOSH NARAYAN BARGE,CLX0745354,7781556,Consulting Physician,A,100077738,Ashutosh Narayan Barge,ashutoshbarge@gmail.com,9850242339 +21,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Amol Shinde,Kolhapur,161827,Akash Jadhav,HARADATT KARANDE,CLX0092462,14177997,Chest Physician,A,100067365,Haradatt R Karande,goapulmo@gmail.com,9822161328 +22,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,ANKIT MISHRA ,Mumbai Central ,117248,Navin vishwakarma ,Neel Sajjan Mehta,CIP287944,7853842,Chest Physician,A+,100091479,Neel Sajjan Mehta,neel742@gmail.com,9820141636 +23,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,ANKIT MISHRA ,THANE CENTRAL ,165088,BIPIN SHUKLA ,Khalid Ahmed Qadim Ahmad Ansari,CIP10281,14479358,Consulting Physician,A+,100049504,Khalid Ahmed Qadim Ahmad Ansari,drkhalidahmadansari1974@gmail.com,9975211798 +24,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,ANKIT MISHRA ,,,HITESH,Tushar G Shet,CIP4810433,11409187,Consulting Physician,A,100076069,Tushar G Shet,tushar.sheth@gmail.com,9665691664 +25,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,ANKIT MISHRA ,Mumbai Central ,117248,Navin vishwakarma ,Kavita Rohit Mody,CIP287796,10062613,Chest Physician,B,100056787,Kavita Rohit Mody,kavitamody@gmail.com,9820394451 +26,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,ANKIT MISHRA ,Mumbai Central ,117248,Navin vishwakarma ,Vikas Shantilal Oswal,CLX01268103,10891559,Chest Physician,B,100034628,Vikas Shantilal Oswal,fdr_vikas@rediffmail.com,9819954773 +27,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,ANKIT MISHRA ,RAIGADH ,150204,AKASH JAISWAL ,Vikram Bhagchand Jain,CLX0184081,11755408,Chest Physician,A+,100031972,Vikram Bhagchand Jain,dr4chest@gmail.com,9822965062 +28,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,ANKIT MISHRA ,RAIGADH ,150204,AKASH JAISWAL ,CHETAN KAILAS MAHAJAN,CIP796161,11771457,Chest Physician,A+,100065136,CHETAN KAILAS MAHAJAN,ckm287@gmail.com,9820996069 +29,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,ANKIT MISHRA ,THANE CENTRAL ,165088,BIPIN SHUKLA ,RAM ACHUTRAO SHINDE,CLX0491676,14459082,Chest Physician,A,100051118,RAM ACHUTRAO SHINDE,drramshinde@gmail.com,8693011888 +30,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,ANKIT MISHRA ,THANE CENTRAL ,165088,BIPIN SHUKLA ,PARAG UMESH TIKE,CIP4810435,14479373,Chest Physician,A+,100075247,PARAG UMESH TIKE,shwas.clinic18@gmail.com,9594798236 +31,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,ANKIT MISHRA ,Thane ,174684,Sachin chaurasiya ,JASPREET SINGH KHANDPUR,CIP4918990,14940250,Chest Physician,A,100119159,JASPREET SINGH KHANDPUR,thelemicdoctor@gmail.com,9930510753 +32,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,ANKIT MISHRA ,Mumbai Central ,172808,MOHD SOIL,Jay Bhanushali ,CIP5124756,15423983,Chest Physician,B,100145021,JAY DINESH BHANUSHALI,drjaybhanushalimd@gmail.com,9769044141 +33,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,ANKIT MISHRA ,Mumbai Central ,172808,MOHD SOIL,Sagar J Raiya,CIP10314,14801357,Chest Physician,A,100071109,Sagar J Raiya,info@healthylungs.in,9833100167 +34,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Pankaj Yadav ,Mumbai South ,157180,Vishwash Jaiswar ,NASEERUDDIN KHAN,CIP4945778,13841973,Consulting Physician,A,100064288,NASEERUDDIN KHAN,drnasi2104@gmail.com,9821844336 +35,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Pankaj Yadav ,Mumbai South ,174583,Ashwini hawaldar ,Sanket M Jain,CIP886999,14178917,Chest Physician,A,100061881,Sanket M Jain,mssanky1990@gmail.com ,9819923054 +36,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Pankaj Yadav ,Mumbai South ,157180,Vishwash Jaiswar ,PRACHI JIMISH SHAH,CIP5001103,14866489,Chest Physician,A,100061773,PRACHI JIMISH SHAH,vitality.heathcare01@gmail.com,9833834417 +37,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Pankaj Yadav ,Mumbai South ,161905,Shahnawaz Ansari ,Hardik Mineshkumar Jain,CIP817086,14213535,Chest Physician,A,100088606,Hardik Mineshkumar Jain,drhardikjain01@gmail.com ,9646074680 +38,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Pankaj Yadav ,Mumbai South ,174610,Rohan Yadav ,ANILKUMAR MITHALAL SINGAL,CIP5080587,15099584,Chest Physician,A+,100066555,ANILKUMAR MITHALAL SINGAL,anilsinghal52@gmail.com,9821010180 +39,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Pankaj Yadav ,Mumbai South ,161905,Shahnawaz Ansari ,Kinaz Ansari,CLX01463665,14194141,Consulting Physician,A,100049690,Kinaz Ansari,drkinaz86@gmail.com,9819134114 +40,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,171394,Dinesh Bamdale,PARJANE NILESH RAOSAHEB,CLX0058704,14793432,Chest Physician,A,100064702,PARJANE NILESH RAOSAHEB,dr_nilesh21@yahoo.co.in,9423791670 +41,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,166263,Sagar Dhole,Nitin Janardhan Jathar,CLX0083683,15012494,Chest Physician,A,100067572,Nitin Janardhan Jathar,drjatharn@gmail.com,9822322050 +42,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,166263,Sagar Dhole,AMEY ANIL DESHMUKH,CIP4824052,15012436,Chest Physician,A,100082823,AMEY ANIL DESHMUKH,amey17ster@gmail.com,9404296919 +43,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,171394,Dinesh Bamdale,RAJENDRA B WAGHADKAR,CIW894209,14793513,Consulting Physician,A,100076746,RAJENDRA B WAGHADKAR,drrajendra211@gmail.com,9594241494 +44,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,157684,Danish Shaikh,Swapnil Suresh Sakhala,CIW933435,10524183,Chest Physician,A+,100054233,Swapnil Suresh Sakhala,swapnil.sakla@gmail.com,9922718347 +45,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,156551,Shekhar Pawar,TAUSEEF NAEEM RASHID AKHTAR,CIP4846945,11968235,Chest Physician,A+,100082022,TAUSEEF NAEEM RASHID AKHTAR,tauseef2611@gmail.com,9967745622 +46,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,166263,Sagar Dhole,Mhase Ajinkya Changdeo,CLX01835247,15124377,Chest Physician,A,100065936,Mhase Ajinkya Changdeo,ajinkyamhase@gmail.com,9420801336 +47,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,166263,Sagar Dhole,Sachin K Pansare,CLX01478242,14524569,Chest Physician,A,100073348,Sachin K Pansare,sachin1386@gmail.com,7741052225 +48,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,157684,Danish Shaikh,VIJAY POPATLAL SURANA,CLX0051376,11068980,Chest Physician,A+,100040946,VIJAY POPATLAL SURANA,vijay.surana@gmail.com,9822079930 +49,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,157684,Danish Shaikh,JAYDEEP JANKIRAM BHAMBRE,CLX1300536,8043645,Consulting Physician,A,100069897,JAYDEEP JANKIRAM BHAMBRE,jaydeep.med@gmail.com,9850086022 +50,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,157684,Danish Shaikh,swapnil sanjay kakad,CIP4848693,13057685,Chest Physician,A,100096767,swapnil sanjay kakad,kakad.swapnil@gmail.com,9773165210 +51,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,157684,Danish Shaikh, ABHINANADAN MUTHA,CIP229837,7891930,Chest Physician,A+,100074844,ABHINANDAN BHIKCHAND MUTHA,abhimutha@gmail.com,8143210606 +52,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,156551,Shekhar Pawar,Yogesh Shivaji Patil,CLX0373705,11689293,Chest Physician,A,100031330,YOGESH SHIVAJI PATIL,dr.yogeshpatil.shivam1@gmail.co,9970947966 +53,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,166263,Sagar Dhole,Pranali Trimukhe,CIP4751477,14524558,Chest Physician,A,100069717,pranali rahul trimukhe,pranalidk@gmail.com,9420116100 +54,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,157389,Rahil Khan,Pradeep Jain,CLX0019858,14797445,Consulting Physician,A,100078755,Pradeep Ravindra Jain,drpradeepj77@gmail.com,9890555123 +55,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Kaiser Shaikh,Nashik,156551,Shekhar Pawar,Yash Kasliwal,CIP5116664,15011659,Consulting Physician,A,#N/A,#N/A,yashkasliwal65@gmail.com,8087991499 +56,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,Pune,174625,Namrata Pradip Gholap,Mahendra Mangilal Kawedia,CIP33545,15144457,Chest Physician,A+,100062260,Mahendra Mangilal Kawedia,mkawedia@hotmail.com,9822053410 +57,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,171470,Lokesh Rameshrao Date,AbhishekAnil Faye,CIP1014421,14802095,Chest Physician,A,100067222,ABHISHEK ANIL FAYE,abhifaye@gmail.com,8446359750 +58,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,171470,Lokesh Rameshrao Date,SyedTARIQUE S M Hussain,CLX01514683,14801750,Chest Physician,A+,100062906,SYED TARIQUE S M HUSSAIN,drtariq_chest@yahoo.com,9372282333 +59,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Sarfaraj Maniyar ,Solapur ,152846,Junaid Shaikh ,Sayyed Feroz,CIP254249,10424335,Chest Physician,A+,100043675,SAYYED FEROZ,sayyedferoz2007@gmail.com,9085694215 +60,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Sarfaraj Maniyar ,Solapur ,91168,Shrikant Jagtap ,Umesh Dnyandeo Salunkhe,CIP1748,7797675,Chest Physician,A+,100061939,Umesh Dnyandeo Salunkhe,mchlonand2021@gmail.com,9623626433 +61,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Sarfaraj Maniyar ,Solapur ,156782,sohail Momin ,Nikhil Magdum ,CIP4825016,13570935,Consulting Physician,A,100076123,NIKHIL APPASAHEB MAGDUM,nikhilmagdum@gmail.com,9860815936 +62,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Sarfaraj Maniyar ,Solapur ,91168,Shrikant Jagtap ,Harshavardhan vohra ,CIP5014228,7705775,Chest Physician,A+,100086964,Harshavardhan Suryakant Vhora,harshavardhan_vhora@yahoo.co.in,9860446543 +63,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Sarfaraj Maniyar ,Solapur ,155871,Ganesh Waghmare ,Omprakash kanade ,CLX0072577,11279112,Chest Physician,A,100145965,OMPRAKASH GURUPAD KANADE,omprakashkanade@yahoo.com,9422071126 +64,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Aurangabad,176376,Vinod Dokhale,Hafiz S Deshmuk,CIP4756767,10417290,Chest Physician,A+,100081961,HAFIZ MOHAMMAD DESHMUKH,dr.hafiz.deshmukh@gmail.com,9225020101 +65,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Aurangabad,176376,Vinod Dokhale,Aref Husain,CLX0006164,10417946,Consulting Physician,A+,100067540,Aref Husain,drarefhussain@gmail.com,8237282737 +66,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Aurangabad,176376,Vinod Dokhale,Nitin Chitte,CIP4811724,10417953,Chest Physician,A,100060974,CHITTE NITIN SHRIRAM,drnitinchitte@gmail.com,9765138787 +67,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Aurangabad,176376,Vinod Dokhale,Zaheer Abbas Bhikan Shah,CIP287349,10417979,Chest Physician,A+,100080799,ZAHEER ABBAS BHIKAN SHAH,dr.zaheerabbas14@gmail.com,9422563382 +68,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Aurangabad,176376,Vinod Dokhale,Iqbal Sidiqui,CIP4756768,10524884,Chest Physician,A,100049235,IQBAL MOHIUDDIN KHURSHEED MOHIUDDIN SIDD,drsiddiquiiqbal@rediffmail.com,9028989299 +69,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Aurangabad,176376,Vinod Dokhale,Suhas Bardapurkar,CLX0071077,14185164,Chest Physician,A+,100029881,Suhas Jagganathrao Bardapurkar,drsuhas.bardapurkar@gmail.com,9423150213 +70,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Aurangabad,176376,Vinod Dokhale,Shreehas S Bardapurkar,CLX0691727,14190154,Chest Physician,A+,100053723,Shreehas S Bardapurkar,sbardapurkar@gmail.com,9405912791 +71,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Aurangabad,176376,Vinod Dokhale,Abhilash Anil Golecha,CIP4751046,13390458,Consulting Physician,A,100076012,Abhilash Anil Golecha,drabhilekh.golechha17@gmail.com,9175108108 +72,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Aurangabad,167334,Aniket Waghrulkar,Ashish S Deshmukh,CIP796713,14794589,Chest Physician,A+,100055465,Ashish S Deshmukh,drdeshmukhas@gmail.com,8888412587 +73,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalna,172579,Shubham Nagare,Adinath Patil,CIP1154,7705242,Consulting Physician,A+,#N/A,#N/A,drpatilaadi@gmail.com,9834192385 +74,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalna,172579,Shubham Nagare,Pradeep Huse,CLX01047812,7705454,Consulting Physician,A,100140623,PRADEEP PRABHAKARRAO HUSHE,drpradeepphushe@gmail.com,9552519051 +75,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalna,172579,Shubham Nagare,Ramdaas Sangle,CIP4765173,10527471,Consulting Physician,A,100068640,RAMDAS BHAURAO SANGLE,sanglehospital@gmail.com,8805484148 +76,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalna,172579,Shubham Nagare,Krishna Madanrao Korde,CIP4819409,11651932,CARDIOLOGIST,A,100119905,KRISHNA MADANRAO KORDE,krishnamkorde@gmail.com,9689600968 +77,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalna,172579,Shubham Nagare,Sudarshan Mante,CIP5057689,14321246,Chest Physician,A+,100145103,SUDARSHAN RAMDAS MANTE,sudarshanmante@gmail.com,8657286289 +78,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalna,172579,Shubham Nagare,Akshay Chavan,CIP4878900,14680763,Chest Physician,A+,100144810,AKSHAY DNYANDEO CHAVAN,drakshay.chavan@gmail.com,8262851822 +79,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalna,172579,Shubham Nagare,Yuraj Mhske,CLX01618667,10039869,Chest Physician,A+,100082058,YUVRAJ VASANTRAO MHASKE,yuvrajmhaske65@gmail.comm,8087280488 +80,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalna,172579,Shubham Nagare,Pramod B Akat,CLX01109471,7959642,Consulting Physician,A+,100078062,AKAT PRAMOD BHAGWANRAO,dr_akat@rediffmail.com,9922662767 +81,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalgaon,136887,Shafeeque Shaikh,Rajesh Tukaram Manvatkar,CLX0079153,7533917,Consulting Physician,A+,100069465,RAJESH TUKARAM MANVATKAR,manwatkarrajesh@yahoo.com,9422780520 +82,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalgaon,136887,Shafeeque Shaikh, Mateen Patel,CIR84051,7703972,Consulting Physician,B,100074241,MATEEN AHMED B PATEL,matinpatel02@gmail.com,9856348625 +83,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalgaon,136887,Shafeeque Shaikh,Sunil V Chaudhary,CLX01070783,7703641,Consulting Physician,B,#N/A,#N/A,sunil.chaudhary@yahoo.com,9421536852 +84,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalgaon,136887,Shafeeque Shaikh,Sunil Suryavanshi,CIW980622,7703643,Consulting Physician,B,100094443,Sunil Suryawanshi,surya.sunil77@yahoo.com,9822961026 +85,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalgaon,136887,Shafeeque Shaikh,Parag Vasantrao Chaudhari,CIP1068023,7703669,Chest Physician,A+,100070635,PARAG VASANTRAO CHAUDHARI,parag.chaudhari@gmail.com,9823034235 +86,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Jalgaon,136887,Shafeeque Shaikh,Rahul M Mahajan,CIP1067906,7703670,Consulting Physician,A+,100088983,MAHAJAN RAHUL SURENDRA,sonu47@gmail.com,9822177957 +87,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Beed,164449,Subhash Ghodke,Anil B Maske,CIP1024220,13983809,Chest Physician,A+,100092130,Maske Anil Baburao,maskeanil@gmail.com,8087564717 +88,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Beed,164449,Subhash Ghodke,Rahul Dhakde,CIP1044929,13983973,Chest Physician,B,100146639,DHAKDE RAHUL DADARAO,dr.rahuld@gmail.com,9800352310 +89,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Praneet Thote,Beed,164449,Subhash Ghodke,Sunil Maruti Waghmare,CLX1654969,14068488,Chest Physician,A,100073042,WAGHMARE SUNIL MARUTI,iamsuneal@gmail.com,9920027363 +90,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,Pune,174625,Namrata Pradip Gholap,NitinYeshwant Abhyankar,CIP1017719,15091850,Chest Physician,A+,100037503,Nitin Yeshwant Abhyankar,nitimadh@hotmail.com,9822036360 +91,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,Pune,171392,Navnath Gade,GOYAL ASHSIH OMPRAKASH,CLX0513924,13797051,Chest Physician,A+,100037084,Goyal Ashish Omprakash,ashish_critical@yahoo.com,9952848554 +92,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,Pune,171392,Navnath Gade,Vishal Ramesh More,CIP228407,13489412,Chest Physician,A+,100063244,Vishal Ramesh More,dr.morevishal@gmail.com,7507606889 +93,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,Pune,174630,Altaf Pathan,PAWAR BALASAHEB YADAVRAO,CLX0295828,15162260,Chest Physician,A+,100062259,Balasaheb Yadavrao Pawar,bypawar_md@rediffmail.com,7875185800 +94,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,Pune,174630,Altaf Pathan,Magar Pankaj Kondiram,CLX0306589,15162263,Chest Physician,A+,100042205,MAGAR PANKAJ KONDIRAM,drpankaj_magar@rediffmail.com,7219105374 +95,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,Pune,174630,Altaf Pathan,POPHALE HIMANSU SHASHIKANT,CLX0689601,15162262,Chest Physician,A+,100040189,Himanshu Shashikant Pophale,himanshupophale@yahoo.com,9503939461 +96,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,Pune,175200,Rutuja Parve,NANA GOKUL KUNJIR,CIP1067056,14924889,Chest Physician,A+,100064594,NANA GOKUL KUNJIR,drnana.kunjir@gmail.com,9175209175 +97,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,Pune,175200,Rutuja Parve,Laxmikant B Kaotekwar,CLX1278819,14924895,Chest Physician,A+,100040769,Laxmikant B Kaotekwar,drklaxmikant2007@yahoo.com,8412812355 +98,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,Pune,175200,Rutuja Parve,Funde Pradeep Narayan,CIP4805390,14924886,Chest Physician,A+,100119850,FUNDE PRADEEP NARAYAN,pradipfunde@gmail.com,9972288479 +99,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,PCMC,152683,Angad Jambhale,DNYANDEEP BHOJRAJ SARPE,CIP4913066,14054977,Chest Physician,A+,100116341,DNYANDEEP BHOJRAJ SARPE,dnyandeep007@gmail.com,7888143059 +100,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,PCMC,152683,Angad Jambhale,Omkar Vilas Kajale,CLX0000191,12939729,Chest Physician,A+,100043100,Omkar Vilas Kajale,kajale.omkar.ok@gmail.com,9850444023 +101,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Imran Inamdar,Pune,174625,Namrata Pradip Gholap,PARAG GAJANAND KHATAVKAR,CIP891311,15091884,Chest Physician,A+,100070476,PARAG GAJANAN KHATAVKAR,drpgkhatavkar@gmail.com,9890141514 +102,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Chandrapur,170647,Nilesh Dehane,VaibhavKisanrao Aglawe,CIP4952590,14175601,Chest Physician,A,100105621,VAIBHAV KISANRAO AGLAWE,vaibhavgmc14@gmail.com,7304773857 +103,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Chandrapur,170647,Nilesh Dehane,AMITRAJIV DEWAIKAR,CIP1086,7557522,Chest Physician,A,100065969,AMIT RAJEEV DEWAIKAR,mdewaikar@gmail.com,9422838090 +104,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Gondia,150053,Vinay Kolhe,Yogesh K Girhepunje,CIP306253,7520671,Chest Physician,A,100072066,Yogesh k Girhepunje,yogesh.girhepunje@gmail.com,9899410745 +105,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Gondia,150053,Vinay Kolhe,Vivekkumar G S,CLX01813768,7520438,Chest Physician,A,100070800,Vivekkumar Ganesh Satdeve,vickysats@gmail.com,9029741151 +106,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Gondia,150053,Vinay Kolhe,Shirish Ratnap,CLX0081847,7749436,Chest Physician,A,100105029,SHIRISH PADMAKAR RATNAPARKHI,dr.sratnaparkhi@gmail.com,9823516600 +107,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Yavatmal,175737,Vinit Bhende,MohitMahesh Gupte,CIP4879205,12317450,Chest Physician,A,100088339,MOHIT MAHESH GUPTE,mohitgupte78@gmail.com,9420551378 +108,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,173848,Harshit Kore,VikramManmohan Rathi,CLX01478897,11306996,Chest Physician,A,100063456,Vikram Manmohan Rathi,drvmraathi@gmail.com,9823491913 +109,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,173848,Harshit Kore,VINITPRABHUDAS NIRANJANE,CLX405589,11306947,Chest Physician,A,100039919,Vinit Prabhudas Niranjane,respirachestcentre@gmail.com,9923701980 +110,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,173848,Harshit Kore,AvinashRambhau Gandhare,CIW981079,11306977,Chest Physician,A,100078752,AVINASH RAMBHAU GANDHARE,argandhare@gmail.com,8275556521 +111,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,173848,Harshit Kore,SushantDilip Muley,CLX01380859,11306976,Chest Physician,A,100086088,SUSHANT DILIP MULEY,muley.sushant@yahoo.in,9923203043 +112,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,159456,Ganesh Thakre,AshokPandurangji Arbat,CLX0071622,13955617,Chest Physician,A+,100084200,ASHOK PANDURANGJI ARBAT,drashok.arbat@krimshospitals.com,9822466640 +113,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,159456,Ganesh Thakre,SwapnilIshwar Bakamwar,CLX01550035,13955620,Chest Physician,A,100063362,Swapnil Ishwar Bakamwar,swapnilbakamwar@gmail.com,7588590379 +114,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,159456,Ganesh Thakre,RajeshNathuram Swarnakar,CLX0085933,14438175,Chest Physician,A+,100030609,Rajesh Nathuram Swarnakar,rajeshswarnakar@yahoo.com,9822225130 +115,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,159456,Ganesh Thakre,DITIV GANDHASIRI,CIP5103582,14864818,Chest Physician,B,100144723,DITI V GANDHASIRI,dr.dihi@icloud.com,7588231227 +116,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,171470,Lokesh Rameshrao Date,SuhasPANDURANG Tiple,CIR639232,14801397,Chest Physician,A,100097746,SUHAS PANDURANG TIPLE,drsuhastiple@gmail.com,8087925919 +117,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Saoner Midc,171470,Lokesh Rameshrao Date,ShivamRAJKUMAR Puniyani,CIP400842,14802684,Chest Physician,A,100067561,Shivam Rajkumar Puniyani,drshivampuniyani@gmail.com,8380098808 +118,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,171470,Lokesh Rameshrao Date,Shashank Wankhede,CIP5040141,14802085,Chest Physician,A,100074916,SHASHANK HARSHWARDHAN WANKHEDE,shashankwankhede7@gmail.com,9634675467 +119,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Shailendra Dodke,Nagpur,171470,Lokesh Rameshrao Date,JiteshSHRIRAM Atram,CLX01563422,14802475,Chest Physician,A,100091573,JITESH SHRIRAM ATRAM,dr_jitesh0302@rediffmail.com,9503109326 +120,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Nanded,Nanded,167039,Amol Kundre,BharatAnil Toshniwal,CIP936633,14617762,Chest Physician,A+,100069847,Bharat Anil Toshniwal,bharattosh@gmail.com,9767556611 +121,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Nanded,Nanded,167039,Amol Kundre,Pavan N Giri,CIP4839856,14617782,Chest Physician,A,#N/A,#N/A,dr.pavangiri@gmail.com,9834916821 +122,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Nanded,Nanded,158419,Syed Arif Sohel,SATISH RAMAKANT MAMIDWAR,CLX0022136,10537956,Chest Physician,A,100042734,Satish Ramakant Mamidwar,satishmamidwar@gmail.com,9823526651 +123,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Nanded,Nanded,158419,Syed Arif Sohel,Dattatray Sahebrao Totewad,CLX01118607,7886951,Chest Physician,A,100105933,Dattatray Sahebrao Totewad,dtotewad@gmail.com,7972523663 +124,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Nanded,Nanded,158419,Syed Arif Sohel,Sameer Gopalrao Kotalwar,CIP4818864,11461523,Chest Physician,A+,100075184,SAMEER GOPALRAO KOTALWAR,drsameergkotalwar@gmail.com,7385314443 +125,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Nanded,Nanded,158419,Syed Arif Sohel,Amol Shriram Shembatwad,CIR82536,12323645,Consulting Physician,A,100112467,AMOL SHRIRAM SHEMBATWAD,amol28989@gmail.com,8828357797 +126,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Nanded,Akola,153265,Gaurav Bangar,SanjayVijay Bharati,CIR58426,10904370,Chest Physician,A+,100062360,Sanjay Vijay Bharati,drsanjaybharati80@gmail.com,8446452920 +127,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Nanded,parbhani,139640,Shaikh Muneer,MurlidharAnantrao Sangle,CLX01406440,8472969,Consulting Physician,A+,100094940,Murlidhar Anantrao Sangle,murlidhar.sangle@gmail.com,9422176165 +128,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Nanded,parbhani,139640,Shaikh Muneer,RupeshNarendra Nagrale,CLX0078173,8472996,Consulting Physician,A+,100103675,RUPESH NARENDRA NAGRALE,drrupesh30@gmail.com,9970237653 +129,Respiratory 2,Shaikh Mohseen Ahmed,Shahbaz sayyed,Nanded,parbhani,139640,Shaikh Muneer,KAILASH RENGE .,CIP249343,9268513,Chest Physician,A,100128590,KAILAS BABURAO RENGE,rengekailas@gmail.com,9420432791 +130,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rankesh Prajapati,TM-JUNAGADH 22422,55509,Ramesh Vadukar,Vinayak C Bhanvadia,CLX01333447,7752259,Consulting Physician,A,100043188,VINAYAK C BHANVADIA,drbhan@gmail.com,8886676765 +131,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rankesh Prajapati,TM-JUNAGADH 22422,55509,Ramesh Vadukar,Rahulkumar A Pandya,CLX01579971,7721103,Chest Physician,A+,100081300,Rahulkumar A Pandya,drpandyarahul@gmail.com,9558613300 +132,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rankesh Prajapati,TM-Bhuj 11000024434,171541,Sama Daud,Rupali Shashikant Shah,CLX0018473,14409713,Chest Physician,A+,100048541,RUPALI SHASHIKANT SHAH,shrimahaviraynamah@gmail.com,9825371557 +133,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rankesh Prajapati,TM-JUNAGADH 22422,55509,Ramesh Vadukar,Hingrajia Nimish J,CIW927921,7721104,Consulting Physician,A+,100064704,Hingrajia Nimish J,venuhospital@gmail.com,7600740163 +134,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rankesh Prajapati,TM-JUNAGADH 22422,55509,Ramesh Vadukar,Sojitra Mehulkumar J,CLX0183754,7721111,Chest Physician,A,100122682,SOJITRA MEHULKUMAR J,drmehulsojitra@gmail.com,9979440804 +135,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rankesh Prajapati,TM-Bhuj 11000024434,171541,Sama Daud,Ranjit Govabhai Suva,CIR89745,14930586,Chest Physician,A,100097578,RANJIT GOVABHAI SUVA,rsuva777@gmail.com,9427229944 +136,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rankesh Prajapati,TM-Bhuj 11000024434,171541,Sama Daud,Oza Amit L,CIR68493,14930556,Chest Physician,A,100045448,Oza Amit L,ozaamit442@gmail.com,9099029832 +137,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rankesh Prajapati,TM-Mehsana 11000024435,171326,Dhavalkumar Patel,Patel Hareshkumar V,CLX689518,14802213,Chest Physician,A+,100041404,PATEL HARESHKUMAR V,heripatel1985@gmail.com,9909085282 +138,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rankesh Prajapati,TM-Mehsana 11000024435,171326,Dhavalkumar Patel,Kiranprahladbhai Thakkar,CLX689518,14802031,Chest Physician,A+,100038142,#N/A,drkiran317@yahoo.co.in,9825739943 +139,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rankesh Prajapati,TM-Mehsana 11000024435,171327,Dhavalkumar Patel,Patel Nihar Vishnubhai,CIR2757,14801690,Chest Physician,A,100051442,PATEL NIHAR VISHNUBHAI,nihar103@gmail.com,9033556038 +140,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Hardik Acharya,TM-Rajkot 10120,174560,Mitesh Kavathiya,Umeshchandra Savjibhai Gediya,CLX0018940,14939813,Consulting Physician,A,100050407,UMESHCHANDRA SAVJIBHAI GEDIYA,dr.umeshgediya1@gmail.com,9825545350 +141,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Hardik Acharya,TM-Rajkot 10120,174560,Mitesh Kavathiya,Tushar S Patel,CIR58267,14409720,Chest Physician,A,100072094,TUSHAR S PATEL,drtspatel@gmail.com,9727330941 +142,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Hardik Acharya,TM-Rajkot 10120,174560,Mitesh Kavathiya,Malasana Vipulkumar Ramjibhai,CLX01227012,14409732,Consulting Physician,A+,100067778,Malasana Vipulkumar Ramjibhai,drvipulmalasana@gmail.com,9879547425 +143,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Hardik Acharya,TM-Rajkot 10120,174560,Mitesh Kavathiya,Javia Abhay Narendrakumar,CLX0187218,14802415,Chest Physician,A,100052304,Javia Abhay Narendrakumar,shwas.rajkot@gmail.com,9033369070 +144,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rupesh Arora,TM-NADIAD 9289,165081,Tanvirasharaf Pathan,Vijay Thakore ,CLX0014090,12117232,Consulting Physician,A+,100109594,Vijay Chandulal Thakar,vijaythaker.medicare@yahoo.com,9825060213 +145,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Hardik Acharya,TM-Ahmedabad 11000024433,137799,Rakesh Roushan,Kalpesh Trikamlal Panchal,CLX01395088,14809359,Chest Physician,A+,100067555,KALPESH TRIKAMLAL PANCHAL,kalpesh2434@gmail.com,9712199914 +146,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Hardik Acharya,TM-Rajkot 10120,174560,Mitesh Kavathiya,Dhiren Ashokkumar Tanna,CLX0013732,14800387,Chest Physician,A+,100067556,DHIREN ASHOKKUMAR TANNA,drdtanna@yahoo.com,9825886756 +147,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Hardik Acharya,TM-AHMEDABAD F1229,171324,Atik Desai,Dhaivat Sunilkumar Desai,CIP4740673,15183453,Chest Physician,B,100093958,DHAIVAT SUNILKUMAR DESAI,arpitsaha21@gmail.com,7878772254 +148,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Hardik Acharya,TM-Rajkot 10120,137799,Rakesh Roushan,Vishal Thakkar,CIP304736,14809391,Chest Physician,A,100062302,THAKAR VISHAL KANAIYALAL,vishalmunshi@gmail.com,8757445778 +149,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Hardik Acharya,TM-Rajkot 11000024440,171447,Suryadipsinh Jadeja,Mahesh Rathod,CLX0022945,14802417,Chest Physician,B,100039710,MAHESH RATHOD,drmaheshrathod@yahoo.com,9824024246 +150,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Hardik Acharya,TM-Bhavnagar 11000024439,172399,Prashant Sarvaiya,Jignesh Mohanbhai Tank,CLX01449970,15016689,Chest Physician,A,100056731,Jignesh M Tank,jigneshtank612@gmail.com,9825027016 +151,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Hardik Acharya,TM-Bhavnagar 11000024439,172399,Prashant Sarvaiya,Brijen. Vadodariya,CLX01173884,14795906,Chest Physician,A,100032299,BRIJEN VADODARIYA,dr.vadodariya573@gmail.com,9879231393 +152,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rankesh Prajapati,TM-Mehsana 11000024435,171326,Dhavalkumar Patel,Anandkumar Bharatbhai Patel,CIP1048939,14801336,Chest Physician,A,100087534,Anandkumar Bharatbhai Patel,andpatel16@gmail.com,9429317995 +153,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Indazoddin shaikh,TM-SURAT 22557,177568,Rathod Vijaybhai,Hardip H Maniar,CIR29043,13207319,Consulting Physician,A+,100047994,Hardip H Maniar,drmaniarhardip@gmail.com,8758017182 +154,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rankesh Prajapati,TM-AHMEDABAD 3949,174770,Vaghela Vaibhav,Mitesh K Dave,CLX01695890,14809460,Chest Physician,A,100056634,Mitesh K Dave,dr.miteshdave@gmail.com,9766567494 +155,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Indazoddin shaikh,TM-NAVSARI 15647,161245,Suraj Mishra,Keyur Harishbhai Thakor,CLX01566898,8120678,Chest Physician,A+,100055816,keyur h thakor,drkeyur23@gmail.com,8401863187 +156,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Indazoddin shaikh,TM-SURAT 5245,155285,Gupta Ashish,Taresh C Patel,CLX53233,13207448,Consulting Physician,A+,100034667,Taresh C Patel,taresh.patel@gmail.com,9925909424 +157,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Indazoddin shaikh,TM-SURAT 22557,177568,Rathod Vijaybhai,Kshitij A Mandke,CIP832475,13207355,Chest Physician,A+,100061611,Kshitij A Mandke,kmandke399@yahoo.com,9662842685 +158,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Indazoddin shaikh,TM-Palanpur F11144,163685,Nikhilkumar Judal,Sachin Patel,CIP4837206,11608389,Chest Physician,A+,100087164,Sachin Laxmichandbhai Patel,sachinpatel961@gmail.com,9428847272 +159,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Indazoddin shaikh,TM-NAVSARI 15647,161245,Suraj Mishra,Vivek Rawal,CIP4746844,14804561,Chest Physician,A,100074679,Raval Vivek Kamleshkumar,vivek.raval11@gmail.com,9898150542 +160,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Indazoddin shaikh,TM-NAVSARI 15647,173422,Suraj Mishra,Mehul Patel,CLX01570795,11935225,Consulting Physician,A+,100105694,MEHULKUMAR CHANDUBHAI PATEL,mehulmd53@gmail.com,7203835009 +161,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Indazoddin shaikh,TM-SURAT 5245,155285,Gupta Ashish,Vitrag Shah,CIP4815022,13207456,Chest Physician,A+,100042737,VITRAG H SHAH,vitrag.shah@kiranhospital.com,9712909924 +162,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Indazoddin shaikh,TM-Palanpur F11144,163685,Nikhilkumar Judal,Surendra Gupta,CLX0018426,10433156,Consulting Physician,A,100036924,Surendrabhai Shreebhagvandas Gupta,ekant8@yahoo.com,9075641245 +163,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Indazoddin shaikh,TM-Palanpur F11144,163685,Nikhilkumar Judal,Divax Oza,CIP4829321,14445989,Chest Physician,A,100120575,OZA DIVAX PARESHKUMAR,divaxoza93@gmail.com,9408848448 +164,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Indazoddin shaikh,TM-SURAT 22557,177568,Rathod Vijaybhai,VADADORIYA SHIRISHBHAI RAMESHBHAI,CLX01915208,13207328,Consulting Physician,A+,100110836,Vadadoriya Shirishbhai Rameshbhai,drshirish.vadadoriya@gmail.com,9510981793 +165,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rupesh Arora,TM-NADIAD 9289,165081,Tanvirasharaf Pathan,Purvesh Vipinchandra Patel,CLX0065290,7744147,Chest Physician,A+,100094723,Purvesh vipinchandra patel,drpurvesh_patel@hotmail.com,9974704289 +166,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rupesh Arora,TM-NADIAD 9289,172426,Mubasshir Chanda,Juzar Limdiwala . ,CIR16876,8291681,Consulting Physician,A+,100066803,Limdiwala Juzar Mohamadbhai,drjlimdiwala@gmail.com,9824256874 +167,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Heruwala,Rupesh Arora,TM-NADIAD 9289,165081,Tanvirasharaf Pathan,Darpan Gandhi ,CIP887121,12117233,Chest Physician,A+,100103042,DARPAN GHANDHI,darpangandhi25@gmail.com,8238016151 +168,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Girdhari Lal,JHUNJHUNU,109287,Sidhant Sharma,Dr.SATISH CHANDRA JOHRI,CLX0305169,7717173,Chest Physician,A+,100103490,Satish Chandra Johri,drjohrisatish@gmail.com,9414247931 +169,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Girdhari Lal,JHUNJHUNU,109287,Sidhant Sharma,Dr.Rajiv Kumar Katara,CLX0266545,10533551,Chest Physician,A+,100062481,RAJEEV KATARA,drrajivkatara@gmail.com,9414076204 +170,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Girdhari Lal,JHUNJHUNU,109287,Sidhant Sharma,Dr.RAJNESH MATHUR,CLX0305175,7717351,Consulting Physician,A+,100112503,RAJNESH MATHUR,drrajneshmathur@gmail.com,9414080569 +171,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Girdhari Lal,JHUNJHUNU,109287,Sidhant Sharma,Dr.ARUN KUMAR,CLX0868450,7578965,Consulting Physician,A ,100123822,ARUN KUMAR,drsura007@gmail.com,9414264956 +172,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Girdhari Lal,JHUNJHUNU,109287,Sidhant Sharma,Dr.ABHILASH SONI,CIP5032644,15162388,Consulting Physician,A ,100144356,Abhilash . Soni,abhilashsoni90720@gmail.com,8107714045 +173,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Girdhari Lal,JHUNJHUNU,109287,Sidhant Sharma,Dr.SANJAY SINGH,CIP4933773,12988072,Consulting Physician,A ,100140040,Sanjay Singh,singhsanjay680@gmail.com,8587966088 +174,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Girdhari Lal,BHARATPUR,176685,Rohit Sharma,Dr.Ram Lakhan Goyal,CLX0350718,8999184,Chest Physician,A+,100062028,RAM LAKHAN GOYAL,gylrl11@gmail.com,9414701491 +175,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Girdhari Lal,BHARATPUR,176685,Rohit Sharma,Dr.HEMANT KUMAR SHARMA,CLX351032,8999221,Chest Physician,A+,100067296,HEMANT KUMAR SHARMA,drhs1776@gmail.com,9414307401 +176,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Girdhari Lal,BHARATPUR,176685,Rohit Sharma,Dr.Shiv charan singh gurjar,CLX0350614,8999158,Consulting Physician,A,100134322,S C GURJUR,dr.shivcharangurjar@gmail.com,9414278589 +177,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Girdhari Lal,BHARATPUR,176685,Rohit Sharma,Dr. Deepesh Agrawal,CLX01569176,12505132,Chest Physician,A+,100094344,Deepesh Agrawal,deepesh.garg8@gmail.com,9667409007 +178,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Girdhari Lal,SIKAR,174477,Keshav Sharma,Dr.BABU LAL SAINI,CIP4727984,10075566,Consulting Physician,A,100086811,Babu Lal Saini,dr_blsaini@yahoo.com,9414270510 +179,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Girdhari Lal,SIKAR,174477,Keshav Sharma,Dr. Manish Rohlania,CLX01708932,10192688,Consulting Physician,A,100113027,MANISH KUMAR ROHALANIYA,drmanish.rohalaniya@gmail.com,8875373051 +180,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Zeeshan Khan,Alwar,174698,Shubham Haldar,Dr.Saurabh Agarwal,CIW860339,10318100,Consulting Physician,A+,100067292,Saurabh . Agarwal,saurabhagarwal207@gmail.com,8727014296 +181,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Zeeshan Khan,Jaipur,162768,Mohit kumar Sharma,Dr.ADIL AZIZ,CLX0887293,14873631,Consulting Physician,A,100084276,ADIL AZIZ,adilaziz007@rediffmail.com,9828547703 +182,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Zeeshan Khan,Jaipur,162768,Mohit kumar Sharma,Dr.SURESH CHAND JAIN,CLX0944774,15024629,Consulting Physician,A,100135632,SURESH CHAND JAIN,drscjain0@gmail.com,9929985778 +183,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Zeeshan Khan,Jaipur,171418,Nirdesh Tyagi,Dr.MOHIT AGARWAL,CIP4903367,14925765,Chest Physician,A,100094959,MOHIT AGARWAL,mohitpharmacologist@gmail.com,9913908645 +184,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Zeeshan Khan,Jaipur,171418,Nirdesh Tyagi,Dr.SHUBHRANSHU,CIP4816534,12721996,Chest Physician,A+,100037023,SHUBHRANSHU,drshubh@yahoo.com,9829222626 +185,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Zeeshan Khan,Alwar,174698,Shubham Haldar,Dr. Kapil bhardwaj,CIP5003849,11517796,Chest Physician,A,100075848,Kapil bhardwaj,dr.kapilbhardwaj@gmail.com,7568097735 +186,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Zeeshan Khan,Jaipur,162768,Mohit kumar Sharma,Dr Sanjay Sogani,CLX0162426,12616057,Chest Physician,A+,100067412,Sanjay Kumar Sogani,sanjaysogani7@yahoo.com,9829331505 +187,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Zeeshan Khan,Jaipur,162768,Mohit kumar Sharma,Dr Rahul Ahluwalia,CIP4878875,12729421,Chest Physician,A+,100097091,Rahul Ahluwalia,dr.rahulahluwalia@gmail.com,9636442500 +188,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Vikram singh,Nagaur,113347,Dinesh jangir,Dr.ASHOK KUMAR JHARWAL,CIP4835214,11613492,Consulting Physician,A+,100094926,Ashok Kumar Jharwal,ashokjharwal007@gmail.com,9983231205 +189,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Vikram singh,Nagaur,113347,Dinesh jangir,Dr.Birma ram jangid,CLX1547629,7958074,Chest Physician,A+,100077122,Birma Ram Jangid,drbrjangid@gmail.com,7568281216 +190,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Vikram singh,Churu,168824,Bunty sharma,Dr.SANDEEP KUMAR CHAHAR,CIP4960227,14433746,Chest Physician,A+,100125440,Sandeep chahar,sandeepchahar@gmail.com,6376682876 +191,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Vikram singh,Churu,168824,Bunty sharma,Dr.MANOHAR LAL BHATIA,CIP4795355,14024351,Consulting Physician,A ,100125440,Manohar . Bhatia,dr_manu303@yahoo.com,9530202223 +192,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Vikram singh,Bikaner,153105,Ranveer singh,Dr.KAVITA,CIP5036599,14196368,Consulting Physician,A+,100124150,Kavita,drkavitapoonia@gmail.com,8209584954 +193,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Vikram singh,Bikaner,153105,Ranveer singh,Dr.Jaikishan khatri,CLX977755,11826627,Chest Physician,A+,100063199,Jaikishan khatri,dkkhatri07@gmail.com,9166521576 +194,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Vikram singh,Hanumangarh,171917,Sunil kumar,Dr.AISHWARY GUPTA,CIP288705,14783732,Consulting Physician,A+,100040227,Aishwary gupta,guptalifecarehospital@gmail.com,7852369856 +195,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Vikram singh,Hanumangarh,171917,Sunil kumar,Dr.KRISHAN KUMAR KUMAWAT,CIP5025488,14783737,Chest Physician,A+,100133920,Krishan kumar kumawat,krishanp@gmail.com,9414078564 +196,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Vikram singh,Hanumangarh,171917,Sunil kumar,Dr. Nikhil gilhotra,CIP4831710,14783817,Consulting Physician,A+,100117844,Nikhil kumar,kakmatgilhotra8@gmail.com,9879063374 +197,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,Vikram singh,Sriganganagar,176405,Vinod keswani,Dr. Rahul jain,CIP5012667,13248620,Chest Physician,A,100098906,Rahul jain,aolrahul28@gmail.com,9950761135 +198,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,SURAJ RAWAT,KOTA,173835,HARISH RATHORE,Dr.PREM PRAKASH BANSAL,14811577,CLX0983153,Chest Physician,A+,100088526,Dr.PREM PRAKASH BANSAL,ppbansalpulmologist@yahoo.in,9414183225 +199,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,SURAJ RAWAT,KOTA,173835,HARISH RATHORE,Dr.Har Lal Parihar,14811580,CLX0011693,Chest Physician,A+,100067429,Dr.Har Lal Parihar,hlparihar56@gmail.com,9414177088 +200,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,SURAJ RAWAT,KOTA,173835,HARISH RATHORE,Dr.ATAL BIHARI MEENA,CIP1045664,14811480,Chest Physician,A+,100107685,Dr.ATAL BIHARI MEENA,abbb6484@gmail.com,9602438620 +201,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,SURAJ RAWAT,KOTA,173835,HARISH RATHORE,DR PRIYANK JAIN,CIP5050647,14811533,Chest Physician,A,100075550,DR PRIYANK JAIN,priyankarzoo87@gmail.com,8440929320 +202,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,SURAJ RAWAT,KOTA,166311,SHUBHAM YADAV,Dr.Vidit Saxena,CIP1045587,14520349,Chest Physician,A,100087439,Dr.Vidit Saxena,drviditsaxena@gmail.com,8769794649 +203,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,SURAJ RAWAT,BHILWARA,172849,ARPIT JETLIYA,DR DINESH RAVI SHARMA,CLX0738001,10095124,Chest Physician,A+,100086459,DR DINESH RAVI SHARMA,sharmadinesh382@gmail.com,9460267170 +204,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,SURAJ RAWAT,AJMER,162115,HIMANSHU GARG,Dr.PRAMOD DADHICH,CLX0013432,11743902,Chest Physician,A+,100028825,Dr.PRAMOD DADHICH,dadhichrashmi@gmail.com,9829083879 +205,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,SURAJ RAWAT,AJMER,162115,HIMANSHU GARG,DR NEERAJ GUPTA,CIR21066,10280704,Chest Physician,A+,100116917,DR NEERAJ GUPTA,drneerajajmer@yahoo.com,9829101942 +206,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,SURAJ RAWAT,S MADHOPUR,168610,VIPUL SHARMA,Dr.Umesh Kumar Jatav,CLX0384426,10255366,Chest Physician,A+,100075453,Dr.Umesh Kumar Jatav,jahaji28@gmail.com,9602158647 +207,Respiratory 2,Shaikh Mohseen Ahmed,Prashant Gakkhar,SURAJ RAWAT,S MADHOPUR,168610,VIPUL SHARMA,Dr.Prem raj Singh,CIP610172,7659526,Chest Physician,A+,100037592,Dr.Prem raj Singh,drpremrajsingh@gmail.com,9929460655 &6350474968 +208,Respiratory 2,Amandeep sharma,Umesh garg,Abhinav Sharma,TM-Jalandhar 17089,150541,Ishant Setia,PUNEET SINGH PERHAR,CLX1239048,12632335,Chest Physician,A+,100041086,PUNEET SINGH PERHAR,puneetperhar@yahoo.com,9779149838 +209,Respiratory 2,Amandeep sharma,Umesh garg,Abhinav Sharma,TM-AMRITSAR 7465,171383,Arun Kumar,Mandeep Singh,CLX1650772,11311299,Chest Physician,A+,100056055,Mandeep Singh,mandeepsingh27@gmail.com,9811660365 +210,Respiratory 2,Amandeep sharma,Umesh garg,Abhinav Sharma,TM-AMRITSAR 23513,171358,Ankush Kapoor,naresh chawla,CIP1067150,15023224,Chest Physician,A,100087159,naresh chawla,dr.nareshchawla@gmail.com,9814104155 +211,Respiratory 2,Amandeep sharma,Umesh garg,Ripanjeet,TM-MOHALI 1819,178160,Aman Kaul,Zafar Ahmad Iqbal,CLX0721575,11663476,Consulting Physician,A+,100029192,Zafar Ahmad Iqbal,zafar.iqbal@fortishealthcare.com,7508328879 +212,Respiratory 2,Amandeep sharma,Umesh garg,Ripanjeet,TM-LUDHIANA 4190,88358,Jimmy Kapoor,SACHIN GOYAL,CLX01510777,7691245,Chest Physician,A+,100069997,SACHIN GOYAL,sachingoyal25_2001@yahoo.co.in,8284801004 +213,Respiratory 2,Amandeep sharma,Umesh garg,Ripanjeet,TM-LUDHIANA 4190,88358,Jimmy Kapoor,SAMIR KAPOOR,CIP4817009,14050155,CARDIOLOGIST,A+,100127643,SAMIR KAPOOR,drsamirkapoor82@gmail.com,8427007868 +214,Respiratory 2,Amandeep sharma,Umesh garg,Ripanjeet,TM-LUDHIANA 3272,173094,Arun Dharam,GURMEET SINGH,CLX0022204,14868786,Consulting Physician,B,100074503,GURMEET SINGH,purbugurmeet@gmail.com,8035465554 +215,Respiratory 2,Amandeep sharma,Umesh garg,Ripanjeet,TM-MOHALI 1819,178160,Aman Kaul,ONKAR GUPTA,CIW953206,7837233,Chest Physician,A,100110734,ONKAR GUPTA,dronkargupta23@gmail.com,8427005698 +216,Respiratory 2,Amandeep sharma,Umesh garg,Ripanjeet,TM-LUDHIANA F2476,163391,Abhishek Kumar,Sanjeev Chopra,CLX0940887,12754759,Consulting Physician,A+,100033019,Sanjeev Chopra,sanjeevchopra@gmail.com,9888287947 +217,Respiratory 2,Amandeep sharma,Umesh garg,Gourav Kumar,TM-HISSAR 3785,87757,Nikhil Verma,sunil kumar jalodia,CLX0033158,7625585,Consulting Physician,A+,100039616,sunil kumar jalodia,s_jalodia@yahoo.co.in,9812330416 +218,Respiratory 2,Amandeep sharma,Umesh garg,Gourav Kumar,TM-HISSAR 20868,171200,Devender Kumar,neha garg,CIP4885356,12444245,Chest Physician,A+,100090045,neha garg,nehagarg08034@gmail.com,7973431951 +219,Respiratory 2,Amandeep sharma,Umesh garg,Gourav Kumar,TM-HISSAR 3785,87757,Nikhil Verma,SUSHIL DHAMIJA,CIP819683,11831417,Chest Physician,A+,100069477,SUSHIL DHAMIJA,dhamijachesthospital@gmail.com,9812030447 +220,Respiratory 2,Amandeep sharma,Umesh garg,Gourav Kumar,TM-SIRSA 14188,169953,Sahil Kumar,SUBHASH CHANDER MEEL,CIP4794884,11169830,Consulting Physician,A+,100061534,SUBHASH CHANDER MEEL,drsubhashmeel@gmail.com,9996304008 +221,Respiratory 2,Amandeep sharma,Umesh garg,Puneet Khanna,TM-PANIPAT 13949,,VACANT,Sachin Bansal,CLX0895157,9334357,Consulting Physician,A,100037677,Sachin Bansal,sachinbansal97@gmail.com,9812303197 +222,Respiratory 2,Amandeep sharma,Umesh garg,Puneet Khanna,TM-Karnal 19701,175785,Yogesh .,RAJIV JAIN,CLX01481407,14265458,Consulting Physician,A,100103022,RAJIV JAIN,rajiv2882@gmail.com,8901474068 +223,Respiratory 2,Amandeep sharma,Umesh garg,Puneet Khanna,TM-Karnal 19701,175785,Yogesh .,Alok Gupta,CLX0030047,11487968,Consulting Physician,A+,100071464,Alok Gupta,alok@gbheartcentre.com,9812025082 +224,Respiratory 2,Amandeep sharma,Umesh garg,Puneet Khanna,TM-YAMUNANAGAR 15585,131893,Vishank Gupta,Alok Kapoor,CLX01013821,7663972,Chest Physician,A+,100076729,Alok Kapoor,kapoorhosp@yahoo.com,9812593793 +225,Respiratory 2,Amandeep sharma,Umesh garg,Puneet Khanna,TM-Kurukshetra 11000010046,164435,Amit Kaushal,Narinder kumar jhamb,CLX0009084,14380552,Chest Physician,A,100066230,Narinder kumar jhamb,narinderjhamb18@gmail.com,9466522936 +226,Respiratory 2,Amandeep sharma,Umesh garg,Puneet Khanna,TM-YAMUNANAGAR 15585,131893,Vishank Gupta,Vineet Jain,CLX0413789,7474949,Consulting Physician,A+,100047210,Vineet Jain,vineetjainynr@gmail.com,9896753380 +227,Respiratory 2,Amandeep sharma,Umesh garg,Puneet Khanna,TM-Ambala F11132,146045,Himanshu Sharma,ABHISHEK Mittal,CIP4756055,11864413,Consulting Physician,A,100097766,ABHISHEK Mittal,dr.abhishek1985@gmail.com,9599780152 +228,Respiratory 2,Amandeep sharma,Umesh garg,Puneet Khanna,TM-Kurukshetra 11000010046,164435,Amit Kaushal,Anshul mangla,CIP5003821,14380662,Chest Physician,A+,100085681,Anshul mangla,anshul03mangla@gmail.com,9687833895 +229,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Bathinda F2477,150539,Ankit Chalana,INDERJIT SINGH,CIP4946875,13521900,Consulting Physician,A+,100114592,INDERJIT SINGH,sunnybax@gmail.com,9654059495 +230,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-BATHINDA F5763,173711,Sahil Kumar,VIVEK CHHABRA,CLX01559053,7426173,Consulting Physician,A,100065411,VIVEK CHHABRA,doctr.vivek@gmail.com,8930663664 +231,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Bathinda F2477,150539,Ankit Chalana,robin Garg,CIP5130569,15029500,Consulting Physician,A,100138339,Robin Garg,gargrobin27@gmail.com,7888687115 +232,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Patiala 6716,168864,Rohit Kumar,SANDEEP GOYAL ,CIP4811365,12075991,CARDIOLOGIST,A+,100085032,SANDEEP GOEL,dr_sandeepgoel@yahoo.com,7087430708 +233,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Patiala 11000009971,161610,Bhavana Kumari,AMANDEEP KAUR ,CLX01439439,15039623,Consulting Physician,B,100114547,Amandeep Kaur,amandeepkauratwal0508@gmail.com,9875676453 +234,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Patiala 11000009971,161610,Bhavana Kumari,BINDU GOYAL,CIR4935,15039638,Chest Physician,B,100048410,BINDU GOYAL,bindu30thfeb@gmail.com,9814962641 +235,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Patiala 11000009971,161610,Bhavana Kumari,PARVINDER SINGH,CIP5132339,15039697,Chest Physician,B,100140785,Parvinder singh,aanchalhospitalrajpura@gmail.com,9896486575 +236,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Patiala 6716,168864,Rohit Kumar,RAVINDER BANSAL,CLX0130917,12076138,Consulting Physician,A,#N/A,#N/A,rabvindersanndhu@gmail.com,8686080100 +237,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Patiala 11000009971,161610,Bhavana Kumari,NAVADEEP SINGLA ,CIP280521,15039651,Chest Physician,B,100128007,Navdeep Singla,singla.navdeep@rediffmail.com,9815508154 +238,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Patiala 11000009971,161610,Bhavana Kumari,PRABHLEEN KAUR ,CIP877898,15039617,Chest Physician,B,100103769,Prabhleen Kaur,prabhleenkaur19@gmail.com,9815274978 +239,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Patiala 11000009971,161610,Bhavana Kumari,AMARJEET SINGH SAWHNEY ,CLX0438225,15039674,Consulting Physician,B,100034714,AMARJIT SINGH SAWHNEY,jsawhney98@gmail.com,9814220652 +240,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Patiala 11000009971,161610,Bhavana Kumari,SUKHJEET SINGH ,CLX0248182,15039666,Consulting Physician,B,100130750,SUKHJEET SINGH,drsukhjeetbhadson@gmail.com,9050830236 +241,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Patiala 11000009971,161610,Bhavana Kumari,AMIT GUPTA ,CIP5148011,15039664,Chest Physician,B,100031867,Amit Gupta,amiticu@gmail.com,8427477220 +242,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Patiala 11000009971,161610,Bhavana Kumari,CHARANPREET SINGH GROVER,CLX0091408,15039705,Chest Physician,B,100064773,Charanpreet Singh Grover,charanpreetgrover@gmail.com,9888278191 +243,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Bathinda F2477,150539,Ankit Chalana, kashyap goyal,CIP4919608,14437715,Chest Physician,A+,100132849,Kashyap goyal,kashyap.goyal13@gmail.com,9478116891 +244,Respiratory 2,Amandeep sharma,Umesh garg,Sukhregan,TM-Bathinda F2477,150539,Ankit Chalana,MANU GUPTA,CLX01399679,14202526,Chest Physician,A,100120046,MANU GUPTA,drmanugupta20@gmail.com,9876104880 +245,Respiratory 2,Kiran Kumar,Asphack,Shiraz,MANGALORE,158952,Ganesh,Dr.SINDHU KAMATH,CLX01618709,9359590,Chest Physician,A,,,drsindhukamath@gmail.com,7738337015 +246,Respiratory 2,Kiran Kumar,Asphack,Shiraz,MYSORE,146740,Venkatesh,Dr.Avinash R,CLX01184097,11973105,Chest Physician,A+,,,dravinashr@gmail.com,8884511163 +247,Respiratory 2,Kiran Kumar,Asphack,Bharath,BANGALORE,148629,Nishant Singh,Dr.VISHWA VIJETH K,CIP1007688,11662943,Chest Physician,A+,,,vishwa.vijieth29@gmail.com,9916344390 +248,Respiratory 2,Kiran Kumar,Asphack,Shiraz,MYSORE,171002,Kumar,Dr.S Rajesh Kumar Jain,CLX01483247,14800922,Chest Physician,A+,,,rajeshh2310@gmail.com,9742252271 +249,Respiratory 2,Kiran Kumar,Asphack,Swamy,CHIKKABALLAPUR,163440,Marenna Gari Anil Kumar,Dr.MANJUNATH H K,CIR22861,11835370,Chest Physician,A+,,,drmanjunathhk@gmail.com,8971974740 +250,Respiratory 2,Kiran Kumar,Asphack,Bharath,BANGALORE,148629,Nishant Singh,Dr.UMA DEVARAJ,CIP4760135,11849209,Chest Physician,A,,,druma.devraj@gmail.com,9845155957 +251,Respiratory 2,Kiran Kumar,Asphack,Shiraz,MYSORE,146740,Venkatesh,Dr.Suhail Azam Khan,CIP4904583,14199152,Chest Physician,A,,,drsushailazam16@gmail.com,7892533048 +252,Respiratory 2,Kiran Kumar,Asphack,Shiraz,MADIKERI,146740,Venkatesh,Dr.ALMANDA POOVAIAH PONNAPPA,CLX0123903,11973061,Consulting Physician,A+,,,drponnapa@gmail.com,8861550503 +253,Respiratory 2,Kiran Kumar,Asphack,Swamy,BANGALORE,163440,Marenna Gari Anil Kumar,Dr.SAJIN SUNNY MATHEW,CIP5032201,14299511,Chest Physician,A+,,,sajisun@gmail.com,9988369120 +254,Respiratory 2,Kiran Kumar,Asphack,Shiraz,UDUPI,158952,Ganesh,Dr.DHARMA U SHETTY,CIP5103313,14745616,Chest Physician,A,,,nadumane.dharma@gmail.com,9964230981 +255,Respiratory 2,Kiran Kumar,Asphack,Shiraz,CHANNARAYAPATNA,165204,Rupesh,Dr.CHIRAG K C,CIP5057004,14747077,Chest Physician,A,,,chiragkc23@gmail.com,9739343606 +256,Respiratory 2,Kiran Kumar,Asphack,Shiraz,HASSAN,165204,Rupesh,Dr.DEEPTI KRISHNAN,CLX01420896,14403354,Chest Physician,A,,,deeptihere@yahoo.com,9008216765 +257,Respiratory 2,Kiran Kumar,Asphack,Shiraz,UDUPI,157787,Vikram,Dr.Aswini Kumar Mohapatra,CLX0067092,14488522,Chest Physician,A+,,,aswinidr@gmail.com,9030259996 +258,Respiratory 2,Kiran Kumar,Asphack,Shiraz,SHIMOGA,144363,Irshad,Dr.PRUTHVI B C,CIW941526,14323961,Consulting Physician,A,,,drpruthvibc@gmail.com,9448064080 +259,Respiratory 2,Kiran Kumar,Asphack,Shiraz,MANGALORE,158952,Ganesh,Dr.KEERTHAN GANAPATHI,CIP4914785,14455447,Chest Physician,A,,,keerthan.ganapathi@gmail.com,9637813963 +260,Respiratory 2,Kiran Kumar,Asphack,Bharath,BANGALORE,156896,Neha Kumari,Dr.RAGHAVENDRA,CIP5131034,15028381,Chest Physician,A,,,drraghavendramk@gmail.com,9731580357 +261,Respiratory 2,Kiran Kumar,Asphack,Shiraz,MANDYA,165204,Rupesh,Dr.CHETHAN KUMAR KL,CIP4804818,14403289,Consulting Physician,A,,,drchethu@gmail.com,7022033181 +262,Respiratory 2,Kiran Kumar,Asphack,Swamy,BANGALORE,167591,Kavitha .,Dr.MD MAJEED PASHA,CIP4933377,14304583,Chest Physician,A,,,mdmajeedr@gmail.com,9591170784 +263,Respiratory 2,Kiran Kumar,Asphack,Swamy,CHIKKABALLAPUR,163440,Marenna Gari Anil Kumar,Dr.Jagmohan S V,CLX01565755,14866965,Chest Physician,B,,,drjagmohan99@gmail.com,9449203539 +264,Respiratory 2,Amandeep sharma,Vikas Shanker,Sunny Kumar,TM-Kanpur 11000024428,159539,Akhil Pandey,Manish Kumar Singh,CLX01248899,14797135,Consulting Physician,A+,100081957,Manish Kumar Singh,dr.manishkumarsingh@yahoo.com,7376070025 +265,Respiratory 2,Amandeep sharma,Vikas Shanker,Sushil Pandey,TM-Shahjahanpur 11000004865,158331,Harshit Awasthi,ASHISH MEHROTRA,CLX0673901,13730303,Consulting Physician,A+,100106839,ASHISH MEHROTRA,ashu114@gmail.com,9795824510 +266,Respiratory 2,Amandeep sharma,Vikas Shanker,Ravikant Mishra,TM-Gorakhpur 15728,173582,Manish Pandey,SANJAY KUMAR LATH,CIP4941058,15027092,Chest Physician,A,100088095,SANJAY KUMAR LATH,drlathsk@gmail.com,9415092138 +267,Respiratory 2,Amandeep sharma,Vikas Shanker,Ravikant Mishra,TM-BALLIA 6491,135838,Rajeev Kumar Singh,Bhanu Pratap Pandey,CIP26818,15430389,Consulting Physician,B,100099093,Bhanu Pratap Pandey,pandeybhanup@gmail.com,9456838048 +268,Respiratory 2,Amandeep sharma,Vikas Shanker,Sushil Pandey,TM-Lakhimpur 11000004904,158736,Durgesh Tiwari,NEERAJ KUMAR,CIP1097135,13146234,Consulting Physician,A+,100091593,NEERAJ KUMAR,drneeraj07@gmail.com,9554032646 +269,Respiratory 2,Amandeep sharma,Vikas Shanker,Ankit Mishra,TM-Allahabad 11000024429,171299,Vikash Dwivedi,SANJAY KUMAR MUSADDI,CIP978200,15429602,Consulting Physician,B,100062160,SANJAY KUMAR MUSADDI,sanjaymusaddi@yahoo.com,9415205535 +270,Respiratory 2,Amandeep sharma,Vikas Shanker,Sushil Pandey,TM-Lucknow 11000024430,174462,Mohit Shukla,Anil Kumar Singh,CIW816211,14800363,Chest Physician,A,100032862,Anil Kumar Singh,draksinghchestspecialist@gmail.com,9650170666 +271,Respiratory 2,Amandeep sharma,Vikas Shanker,Ankit Mishra,TM-Raibareilly 4667,171352,Amit Mishra,KAMLESH PRASAD VERMA,CIP996410,14916744,Chest Physician,A,100046551,KAMLESH PRASAD VERMA,drkpvermarbl@gmail.com,9936657267 +272,Respiratory 2,Amandeep sharma,Vikas Shanker,Sunny Kumar,TM-KANPUR 6072,141959,Ajay Gupta,BHARAT MEHROTRA,CIP5003841,15024696,Chest Physician,A,100039277,BHARAT MEHROTRA,bharatdoc14@gmail.com,9818201218 +273,Respiratory 2,Amandeep sharma,Vikas Shanker,Sunny Kumar,TM-KANPUR 6072,141959,Ajay Gupta,Ashok Kumar Singh,CLX280815,11186091,Chest Physician,A+,100040535,Ashok Kumar Singh,ashok_rhl@yahoo.com,9839096578 +274,Respiratory 2,Amandeep sharma,Vikas Shanker,Sunny Kumar,TM-KANPUR 6072,141959,Ajay Gupta,Chandra Shekhar,CLX280806,11186407,Chest Physician,A+,100045084,Chandra Shekhar,chandrachest@gmail.com,9415051205 +275,Respiratory 2,Amandeep sharma,Vikas Shanker,Sushil Pandey,TM-Shahjahanpur 11000004865,158331,Harshit Awasthi,Manoj Kumar Agrawal,CLX0387507,13730249,Consulting Physician,A,#N/A,#N/A,mohan.agarwal@gmail.com,7784165115 +276,Respiratory 2,Amandeep sharma,Vikas Shanker,Ankit Mishra,TM-Allahabad 11000010131,156678,Anuj Tiwari,ABID SAYEED,CLX0332259,13429819,Chest Physician,A+,100080659,ABID SAYEED,abidsyeed11@gmail.com,8299269490 +277,Respiratory 2,Amandeep sharma,Vikas Shanker,Ankit Mishra,TM-JHANSI 13997,175443,Satyendri Verma,Raj Kumar Jain,CLX0344878,8400815,Consulting Physician,B,100078503,Raj Kumar Jain,aayushman2006@yahoo.co.in,9415032200 +278,Respiratory 2,Amandeep sharma,Vikas Shanker,Akhalak Ahmad,TM-Jaunpur 11000006782,158039,Ashish Singh,ASHOK KUMAR YADAV,CIP4762879,13703378,Consulting Physician,B,100124941,ASHOK KUMAR YADAV,brajclinic76@gmail.com,9554438503 +279,Respiratory 2,Amandeep sharma,Vikas Shanker,Ravikant Mishra,TM-Deoria 19004,140857,Shashibhushan Sharma,RAJEEVE SINGH,CLX0351005,14214028,Consulting Physician,A+,100108518,RAJEEVE SINGH,rajeev.singh717@gmail.com,9118273179 +280,Respiratory 2,Amandeep sharma,Vikas Shanker,Sushil Pandey,TM-Lakhimpur 11000004904,158736,Durgesh Tiwari,TANMAI DHAWAN,CLX01535146,15435759,Consulting Physician,B,100067525,TANMAI DHAWAN,dr.tanmaidhawan@gmail.com,9415566882 +281,Respiratory 2,Amandeep sharma,Vikas Shanker,Sushil Pandey,TM-Shahjahanpur 11000004865,158331,Harshit Awasthi,Vimlesh Kumar,CLX01810418,15430196,Consulting Physician,B,100038918,Vimlesh Kumar,dr.vimleshpatel@katiyarnursinghome.com,8447817163 +282,Respiratory 2,Amandeep sharma,Vikas Shanker,Ravikant Mishra,TM-Deoria 19004,140857,Shashibhushan Sharma,PAWAN ARORA,CIP1117730,14214044,Consulting Physician,A,100062999,PAWAN ARORA,drpawan57@gmail.com,9775542356 +283,Respiratory 2,Amandeep sharma,Vikas Shanker,Ravikant Mishra,TM-Gorakhpur F5748,139795,Deepak Pandey,ASHWINI KUMAR MISHRA,CIW871998,14505520,Chest Physician,A+,100056693,ASHWINI KUMAR MISHRA,ashwini486@gmail.com,9358004090 +284,Respiratory 2,Amandeep sharma,Vikas Shanker,Ravikant Mishra,TM-Gorakhpur 15728,173582,Manish Pandey,THAKUR SHARDULAM SRIVASTAVA,CLX0350777,15027057,Chest Physician,A,100038961,THAKUR SHARDULAM SRIVASTAVA,drshardulam@gmail.com,8174971010 +285,Respiratory 2,Amandeep sharma,Vikas Shanker,Ankit Mishra,TM-Allahabad 11000024429,171299,Vikash Dwivedi,SHALABH MALVIYA,CLX0875575,15429607,Consulting Physician,B,100070060,SHALABH MALVIYA,drshalabh_elena@yahoo.com,9415355109 +286,Respiratory 2,Amandeep sharma,Vikas Shanker,Ankit Mishra,TM-Raibareilly 4667,171352,Amit Mishra,BEERBAL,CLX0313323,14916750,Chest Physician,A,100068477,BEERBAL,drbeerbal@gmail.com,9415954585 +287,Respiratory 2,Amandeep sharma,Vikas Shanker,Ankit Mishra,TM-Raibareilly 4667,171352,Amit Mishra,Dharm Raj Maurya,CLX1461668,14916794,Chest Physician,A,100043038,Dharm Raj Maurya,mauryadr72@gmail.com,7618812037 +288,Respiratory 2,Amandeep sharma,Vikas Shanker,Ravikant Mishra,TM-Gorakhpur 15728,173582,Manish Pandey,Nadeem Arshad,CLX0016766,15027043,Chest Physician,A,100036499,Nadeem Arshad,drnadeem1953@gmail.com,9335242324 +289,Respiratory 2,Test ZBM,Test RBM,Test ABM,Test HQ,555555,Test TM,Test Doctor 1,TST0000001,33333333,Chest Physician,A,444444444,Test Vendor 1,testdoc1@yopmail.com,1234567898 +290,Respiratory 2,Test ZBM,Test RBM,Test ABM,Test HQ2,555555,Test TM,Test Doctor 2,TST0000002,33333334,Chest Physician,A,444444445,Test Vendor 2,amey@stratawiz.com,1234567898 +291,Respiratory 2,Test ZBM,Test RBM,Test ABM,Test HQ2,555555,Test TM,Madhuragauri Shevade,TST0000002,33333334,Chest Physician,A,,,madhuragauri@crestindia.co.in,1234567898 +292,Respiratory 2,Test ZBM,Test RBM,Test ABM,Test HQ2,555555,Test TM,Monica Sumit Barne,TST0000002,33333334,Chest Physician,A,,,drmonica@crestindia.co.in,1234567898 +293,Respiratory 2,Test ZBM,Test RBM,Test ABM,Test HQ2,555555,Test TM,Abhishek Sabale,TST0000002,33333334,Chest Physician,A,,,stat@crestindia.co.in,1234567898 +294,Respiratory 2,Test ZBM,Test RBM,Test ABM,Test HQ2,555555,Test TM,SAPNA JITENDRA MADAS,TST0000002,33333334,Chest Physician,A,,,sapnamadas@purefoundation.in,1234567898 +295,Respiratory 2,Test ZBM,Test RBM,Test ABM,Test HQ2,555555,Test TM,SAPNA JITENDRA MADAS,TST0000002,33333334,Chest Physician,A,,,sapnamadas@crestindia.co.in,1234567898 diff --git a/files/import/import-doctors-test-04-11-2026.csv b/files/import/import-doctors-test-04-11-2026.csv new file mode 100644 index 0000000..70b50a3 --- /dev/null +++ b/files/import/import-doctors-test-04-11-2026.csv @@ -0,0 +1,6 @@ +Sr NO,TASKFORCE,ZBM Name,RBM Name,ABM Name,HQ,TM ID,TM Name,HCP NAME,LEGACY_CODE,PCODE,SPECIALITY,CLASS,VENDORCODE,VENDORNAME,Email ID,Contact No +1,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,142621,Salim Khan,Sandeep S Tilve,CIP833167,11667437,Chest Physician,A+,100064617,Sandeep S Tilve,tilvesandeep@gmail.com,9769283034 +2,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,142624,Salim Khan,Rakesh Hansraj Rajpurohit,CIP481056952,13104066,Chest Physician,A+,100101375,Rakesh Hansraj Rajpurohit,dr.rajpurohit750@gmail.com,9545360750 +3,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,168186,Swati Pandey,Parthiv Atulkumar Shah,CIP30354,11834830,Chest Physician,A,100063029,Parthiv Atulkumar Shah,parth15dec@gmail.com,9029772904 +4,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,152148,Dev Pandey,Salil Subhash Bendre,CIP30347,11715346,Chest Physician,A+,100029108,Salil Subhash Bendre,salilsaumil@gmail.com,7738656663 +5,Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,165235,Saurabh Pandey,Lancelot Pinto,CLX01430718,11410660,Chest Physician,A+,100027037,Lancelot M Pinto,lance.pinto@gmail.com,9820234567 diff --git a/frontend/src/admin/ReportsPage.jsx b/frontend/src/admin/ReportsPage.jsx index ec378cc..01fa2c8 100644 --- a/frontend/src/admin/ReportsPage.jsx +++ b/frontend/src/admin/ReportsPage.jsx @@ -8,13 +8,16 @@ import { useState } from 'react'; * endpoint: string, * filename: string, * baseParams?: Record, + * fullReport?: { description: string, filename: string, help: string }, * }} props */ -function ExportCard({ id, title, description, endpoint, filename, baseParams = {} }) { +function ExportCard({ id, title, description, endpoint, filename, baseParams = {}, fullReport }) { const [from, setFrom] = useState(''); const [to, setTo] = useState(''); + const [full, setFull] = useState(false); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); + const useFull = Boolean(fullReport && full); async function download(event) { event.preventDefault(); @@ -22,6 +25,10 @@ function ExportCard({ id, title, description, endpoint, filename, baseParams = { setBusy(true); const params = new URLSearchParams(baseParams); + if (useFull) { + params.set('full', 'true'); + } + if (from) { params.set('from', from); } @@ -49,7 +56,7 @@ function ExportCard({ id, title, description, endpoint, filename, baseParams = { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = filename; + link.download = useFull && fullReport ? fullReport.filename : filename; link.click(); URL.revokeObjectURL(url); } catch (downloadError) { @@ -69,8 +76,28 @@ function ExportCard({ id, title, description, endpoint, filename, baseParams = {

    {title}

    -

    {description}

    +

    + {useFull && fullReport ? fullReport.description : description} +

    + {fullReport ? ( +
    + setFull(event.target.checked)} + /> + +

    + {fullReport.help} +

    +
    + ) : null}