@@ -37,7 +37,8 @@ const LAYOUT = {
3737 leftColumnX : 50 ,
3838 leftColumnWidth : 180 ,
3939 rightColumnX : 240 ,
40- rightColumnWidth : 315
40+ rightColumnWidth : 315 ,
41+ pageHeight : 792
4142} ;
4243
4344async function fetchImageBuffer ( url ) {
@@ -50,6 +51,14 @@ async function fetchImageBuffer(url) {
5051 }
5152}
5253
54+ function checkPageBreak ( doc , currentY , requiredSpace ) {
55+ if ( currentY + requiredSpace > LAYOUT . pageHeight - MARGINS . bottom ) {
56+ doc . addPage ( ) ;
57+ return MARGINS . top ;
58+ }
59+ return currentY ;
60+ }
61+
5362function addHeader ( doc , data ) {
5463 const { fullName, email, phone, city, address, nationality, gender } = data . basicDetails ;
5564
@@ -100,6 +109,8 @@ function addHeader(doc, data) {
100109}
101110
102111function addLeftColumnSection ( doc , title , y ) {
112+ y = checkPageBreak ( doc , y , 30 ) ;
113+
103114 doc . fontSize ( SIZES . sectionHeading ) . fillColor ( COLORS . primary ) . font ( FONTS . bold )
104115 . text ( title . toUpperCase ( ) , LAYOUT . leftColumnX , y ) ;
105116
@@ -170,6 +181,8 @@ function addEducation(doc, data, startY) {
170181 educationEntries . forEach ( ( entry , index ) => {
171182 if ( index > 0 ) y += 12 ;
172183
184+ y = checkPageBreak ( doc , y , 60 ) ;
185+
173186 doc . fontSize ( SIZES . body ) . fillColor ( COLORS . primary ) . font ( FONTS . bold )
174187 . text ( entry . degree , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth } ) ;
175188 y += SIZES . lineHeight ;
@@ -214,6 +227,8 @@ function addLanguages(doc, data, startY) {
214227 doc . fontSize ( SIZES . small ) . fillColor ( COLORS . text ) . font ( FONTS . regular ) ;
215228
216229 data . basicDetails . languages . forEach ( lang => {
230+ y = checkPageBreak ( doc , y , 25 ) ;
231+
217232 const fluencyLevel = lang . fluency . charAt ( 0 ) . toUpperCase ( ) + lang . fluency . slice ( 1 ) ;
218233 doc . text ( `• ${ lang . language } ` , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth } ) ;
219234 y += 10 ;
@@ -238,19 +253,23 @@ function addSkillsSection(doc, data, startY) {
238253 const skills = data . skills . skillsList . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( Boolean ) ;
239254
240255 skills . forEach ( skill => {
256+ y = checkPageBreak ( doc , y , 15 ) ;
241257 doc . text ( `• ${ skill } ` , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth } ) ;
242258 y += 11 ;
243259 } ) ;
244260 }
245261
246262 if ( data . skills ?. supportingDocuments && data . skills . supportingDocuments . length > 0 ) {
247263 y += 5 ;
264+ y = checkPageBreak ( doc , y , 30 ) ;
265+
248266 doc . fontSize ( SIZES . small ) . fillColor ( COLORS . darkGray ) . font ( FONTS . bold )
249267 . text ( 'Documents:' , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth } ) ;
250268 y += 11 ;
251269
252270 data . skills . supportingDocuments . forEach ( docItem => {
253271 if ( docItem . url ) {
272+ y = checkPageBreak ( doc , y , 15 ) ;
254273 doc . fontSize ( SIZES . small ) . fillColor ( COLORS . accent ) . font ( FONTS . regular )
255274 . text ( 'View Document' , LAYOUT . leftColumnX , y , {
256275 width : LAYOUT . leftColumnWidth ,
@@ -281,6 +300,7 @@ function addUSMLEScores(doc, data, startY) {
281300 doc . fontSize ( SIZES . small ) . fillColor ( COLORS . text ) . font ( FONTS . regular ) ;
282301
283302 if ( data . usmleScores . step1Status && data . usmleScores . step1Status !== 'not-taken' ) {
303+ y = checkPageBreak ( doc , y , 25 ) ;
284304 doc . font ( FONTS . bold ) . text ( 'Step 1:' , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth , continued : true } )
285305 . font ( FONTS . regular ) . text ( ` ${ data . usmleScores . step1Status . toUpperCase ( ) } ` ) ;
286306 y += 11 ;
@@ -297,6 +317,7 @@ function addUSMLEScores(doc, data, startY) {
297317 }
298318
299319 if ( data . usmleScores . step2ckScore ) {
320+ y = checkPageBreak ( doc , y , 25 ) ;
300321 doc . fillColor ( COLORS . text ) . font ( FONTS . bold ) . text ( 'Step 2 CK:' , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth , continued : true } )
301322 . font ( FONTS . regular ) . text ( ` ${ data . usmleScores . step2ckScore } ` ) ;
302323 y += 11 ;
@@ -313,12 +334,14 @@ function addUSMLEScores(doc, data, startY) {
313334 }
314335
315336 if ( data . usmleScores . step2csStatus && data . usmleScores . step2csStatus !== 'not-taken' ) {
337+ y = checkPageBreak ( doc , y , 15 ) ;
316338 doc . fillColor ( COLORS . text ) . font ( FONTS . bold ) . text ( 'Step 2 CS:' , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth , continued : true } )
317339 . font ( FONTS . regular ) . text ( ` ${ data . usmleScores . step2csStatus . toUpperCase ( ) } ` ) ;
318340 y += 11 ;
319341 }
320342
321343 if ( data . usmleScores . oetScore ) {
344+ y = checkPageBreak ( doc , y , 25 ) ;
322345 doc . fillColor ( COLORS . text ) . font ( FONTS . bold ) . text ( 'OET Score:' , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth , continued : true } )
323346 . font ( FONTS . regular ) . text ( ` ${ data . usmleScores . oetScore } ` ) ;
324347 y += 11 ;
@@ -335,6 +358,7 @@ function addUSMLEScores(doc, data, startY) {
335358 }
336359
337360 if ( data . usmleScores . ecfmgCertified ) {
361+ y = checkPageBreak ( doc , y , 15 ) ;
338362 doc . font ( FONTS . bold ) . fillColor ( COLORS . accent )
339363 . text ( '✓ ECFMG Certified' , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth } ) ;
340364 y += 11 ;
@@ -370,6 +394,8 @@ function addCertifications(doc, data, startY) {
370394 doc . fontSize ( SIZES . small ) . fillColor ( COLORS . text ) ;
371395
372396 certs . forEach ( cert => {
397+ y = checkPageBreak ( doc , y , 40 ) ;
398+
373399 doc . font ( FONTS . bold ) . text ( cert . name , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth } ) ;
374400 y += 11 ;
375401
@@ -406,17 +432,22 @@ function addEMRTraining(doc, data, startY) {
406432 doc . fontSize ( SIZES . small ) . fillColor ( COLORS . text ) . font ( FONTS . regular ) ;
407433
408434 if ( data . emrRcmTraining . emrSystems && data . emrRcmTraining . emrSystems . length > 0 ) {
435+ y = checkPageBreak ( doc , y , 30 ) ;
436+
409437 doc . font ( FONTS . bold ) . text ( 'EMR Systems:' , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth } ) ;
410438 y += 11 ;
411439
412440 data . emrRcmTraining . emrSystems . forEach ( system => {
441+ y = checkPageBreak ( doc , y , 15 ) ;
413442 doc . font ( FONTS . regular ) . text ( `• ${ system } ` , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth } ) ;
414443 y += 10 ;
415444 } ) ;
416445 y += 5 ;
417446 }
418447
419448 if ( data . emrRcmTraining . rcmTraining ) {
449+ y = checkPageBreak ( doc , y , 25 ) ;
450+
420451 doc . font ( FONTS . bold ) . text ( 'RCM Training' , LAYOUT . leftColumnX , y , { width : LAYOUT . leftColumnWidth } ) ;
421452 y += 11 ;
422453
@@ -431,6 +462,8 @@ function addEMRTraining(doc, data, startY) {
431462}
432463
433464function addRightColumnSection ( doc , title , y ) {
465+ y = checkPageBreak ( doc , y , 30 ) ;
466+
434467 doc . fontSize ( SIZES . sectionHeading ) . fillColor ( COLORS . primary ) . font ( FONTS . bold )
435468 . text ( title . toUpperCase ( ) , LAYOUT . rightColumnX , y ) ;
436469
@@ -452,6 +485,8 @@ function addExperienceSection(doc, experiences, title, startY) {
452485 experiences . forEach ( ( exp , index ) => {
453486 if ( index > 0 ) y += 15 ;
454487
488+ y = checkPageBreak ( doc , y , 80 ) ;
489+
455490 const titleText = exp . title || exp . position || exp . role || '' ;
456491 const orgText = exp . hospital || exp . organization || '' ;
457492
@@ -496,11 +531,14 @@ function addExperienceSection(doc, experiences, title, startY) {
496531 doc . fontSize ( SIZES . small ) . fillColor ( COLORS . text ) . font ( FONTS . regular ) ;
497532 const lines = exp . description . split ( '\n' ) . filter ( l => l . trim ( ) ) ;
498533 lines . forEach ( line => {
534+ const lineHeight = doc . heightOfString ( line , { width : LAYOUT . rightColumnWidth } ) ;
535+ y = checkPageBreak ( doc , y , lineHeight + 5 ) ;
536+
499537 doc . text ( `• ${ line . trim ( ) } ` , LAYOUT . rightColumnX , y , {
500538 width : LAYOUT . rightColumnWidth ,
501539 align : 'left'
502540 } ) ;
503- y += doc . heightOfString ( line , { width : LAYOUT . rightColumnWidth } ) + 2 ;
541+ y += lineHeight + 2 ;
504542 } ) ;
505543 }
506544 } ) ;
@@ -520,16 +558,21 @@ function addAchievements(doc, data, startY) {
520558 doc . fontSize ( SIZES . small ) . fillColor ( COLORS . text ) . font ( FONTS . regular ) ;
521559 const lines = data . significantAchievements . split ( '\n' ) . filter ( l => l . trim ( ) ) ;
522560 lines . forEach ( line => {
561+ const lineHeight = doc . heightOfString ( line , { width : LAYOUT . rightColumnWidth } ) ;
562+ y = checkPageBreak ( doc , y , lineHeight + 5 ) ;
563+
523564 doc . text ( `• ${ line . trim ( ) } ` , LAYOUT . rightColumnX , y , {
524565 width : LAYOUT . rightColumnWidth
525566 } ) ;
526- y += doc . heightOfString ( line , { width : LAYOUT . rightColumnWidth } ) + 3 ;
567+ y += lineHeight + 3 ;
527568 } ) ;
528569 y += 8 ;
529570 }
530571
531572 if ( hasAchievements ) {
532573 data . achievements . forEach ( ( achievement , index ) => {
574+ y = checkPageBreak ( doc , y , 60 ) ;
575+
533576 doc . fontSize ( SIZES . body ) . fillColor ( COLORS . primary ) . font ( FONTS . bold )
534577 . text ( achievement . title , LAYOUT . rightColumnX , y , { width : LAYOUT . rightColumnWidth } ) ;
535578 y += SIZES . lineHeight ;
@@ -541,9 +584,10 @@ function addAchievements(doc, data, startY) {
541584 }
542585
543586 if ( achievement . description ) {
587+ const descHeight = doc . heightOfString ( achievement . description , { width : LAYOUT . rightColumnWidth } ) ;
544588 doc . fontSize ( SIZES . small ) . fillColor ( COLORS . text ) . font ( FONTS . regular )
545589 . text ( achievement . description , LAYOUT . rightColumnX , y , { width : LAYOUT . rightColumnWidth } ) ;
546- y += doc . heightOfString ( achievement . description , { width : LAYOUT . rightColumnWidth } ) + 5 ;
590+ y += descHeight + 5 ;
547591 }
548592
549593 if ( achievement . url && achievement . url . trim ( ) ) {
@@ -571,11 +615,14 @@ function addPublications(doc, data, startY) {
571615 data . publications . forEach ( ( pub , index ) => {
572616 if ( index > 0 ) y += 10 ;
573617
618+ y = checkPageBreak ( doc , y , 50 ) ;
619+
574620 doc . fontSize ( SIZES . small ) . fillColor ( COLORS . text ) . font ( FONTS . regular ) ;
621+ const titleHeight = doc . heightOfString ( pub . title , { width : LAYOUT . rightColumnWidth } ) ;
575622 doc . text ( `${ index + 1 } . ${ pub . title } ` , LAYOUT . rightColumnX , y , {
576623 width : LAYOUT . rightColumnWidth
577624 } ) ;
578- y += doc . heightOfString ( pub . title , { width : LAYOUT . rightColumnWidth } ) + 2 ;
625+ y += titleHeight + 2 ;
579626
580627 doc . font ( FONTS . italic ) . fillColor ( COLORS . darkGray ) ;
581628 const pubDetails = [ pub . journal , pub . year ] ;
@@ -605,6 +652,8 @@ function addConferences(doc, data, startY) {
605652 data . conferences . forEach ( ( conf , index ) => {
606653 if ( index > 0 ) y += 12 ;
607654
655+ y = checkPageBreak ( doc , y , 80 ) ;
656+
608657 doc . fontSize ( SIZES . body ) . fillColor ( COLORS . primary ) . font ( FONTS . bold )
609658 . text ( conf . name , LAYOUT . rightColumnX , y , { width : LAYOUT . rightColumnWidth } ) ;
610659 y += SIZES . lineHeight ;
@@ -627,9 +676,10 @@ function addConferences(doc, data, startY) {
627676 }
628677
629678 if ( conf . description ) {
679+ const descHeight = doc . heightOfString ( conf . description , { width : LAYOUT . rightColumnWidth } ) ;
630680 doc . fontSize ( SIZES . small ) . fillColor ( COLORS . text ) . font ( FONTS . regular )
631681 . text ( conf . description , LAYOUT . rightColumnX , y , { width : LAYOUT . rightColumnWidth } ) ;
632- y += doc . heightOfString ( conf . description , { width : LAYOUT . rightColumnWidth } ) + 5 ;
682+ y += descHeight + 5 ;
633683 }
634684
635685 if ( conf . certificateAwarded ) {
@@ -660,6 +710,8 @@ function addWorkshops(doc, data, startY) {
660710 data . workshops . forEach ( ( workshop , index ) => {
661711 if ( index > 0 ) y += 12 ;
662712
713+ y = checkPageBreak ( doc , y , 60 ) ;
714+
663715 doc . fontSize ( SIZES . body ) . fillColor ( COLORS . primary ) . font ( FONTS . bold )
664716 . text ( workshop . name , LAYOUT . rightColumnX , y , { width : LAYOUT . rightColumnWidth } ) ;
665717 y += SIZES . lineHeight ;
@@ -675,9 +727,10 @@ function addWorkshops(doc, data, startY) {
675727 }
676728
677729 if ( workshop . description ) {
730+ const descHeight = doc . heightOfString ( workshop . description , { width : LAYOUT . rightColumnWidth } ) ;
678731 doc . fontSize ( SIZES . small ) . fillColor ( COLORS . text ) . font ( FONTS . regular )
679732 . text ( workshop . description , LAYOUT . rightColumnX , y , { width : LAYOUT . rightColumnWidth } ) ;
680- y += doc . heightOfString ( workshop . description , { width : LAYOUT . rightColumnWidth } ) + 5 ;
733+ y += descHeight + 5 ;
681734 }
682735
683736 if ( workshop . awards ) {
0 commit comments