Commit 530c28df by pantakan konthang

pms ปริ้น pdf

parent a27b33aa
...@@ -59,6 +59,7 @@ ...@@ -59,6 +59,7 @@
"glob-watcher": "^6.0.0", "glob-watcher": "^6.0.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"helmet": "^7.0.0", "helmet": "^7.0.0",
"html2pdf.js": "^0.10.3",
"keen-slider": "^6.8.5", "keen-slider": "^6.8.5",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lightgallery": "^2.1.2", "lightgallery": "^2.1.2",
...@@ -102,6 +103,7 @@ ...@@ -102,6 +103,7 @@
"@types/d3-shape": "^3.1.1", "@types/d3-shape": "^3.1.1",
"@types/hammerjs": "^2.0.41", "@types/hammerjs": "^2.0.41",
"@types/jasmine": "~4.3.4", "@types/jasmine": "~4.3.4",
"@types/jspdf": "^1.3.3",
"@types/leaflet": "^1.9.3", "@types/leaflet": "^1.9.3",
"@types/mousetrap": "^1.6.11", "@types/mousetrap": "^1.6.11",
"@types/wnumb": "^1.2.1", "@types/wnumb": "^1.2.1",
......
...@@ -617,7 +617,7 @@ ...@@ -617,7 +617,7 @@
<app-pms-idp [currentTap]="currentTap" [canSave]="canSave" [appraisalIdp]="compentency.data.idp" <app-pms-idp [currentTap]="currentTap" [canSave]="canSave" [appraisalIdp]="compentency.data.idp"
[evaluaterId]="evaluaterId" [evaluateeId]="evaluateeId" [evaluaterId]="evaluaterId" [evaluateeId]="evaluateeId"
[canEdit]="evaluationForm=='sup'?canEdit:false" [currentStep]="compentency.data.currentStep" [canEdit]="evaluationForm=='sup'?canEdit:false" [currentStep]="compentency.data.currentStep"
[dateIso]="dateIso" (idpForm)="compentency.data.idp=$event" [complete]="complete"></app-pms-idp> [dateIso]="dateIso" (idpForm)="compentency.data.idp=$event" [complete]="complete" [pdfPrint]="pdfPrint" (pdfPrinted)="onPdfPrinted()"></app-pms-idp>
</div> </div>
<ng-container *ngIf="compentency.data&&canSave"> <ng-container *ngIf="compentency.data&&canSave">
<div class="box-footer text-end space-x-3 rtl:space-x-reverse" <div class="box-footer text-end space-x-3 rtl:space-x-reverse"
...@@ -626,6 +626,11 @@ ...@@ -626,6 +626,11 @@
<textarea type="text" class="ti-form-input" rows="2" placeholder="ใส่ Comment ที่นี่" <textarea type="text" class="ti-form-input" rows="2" placeholder="ใส่ Comment ที่นี่"
[(ngModel)]="comment"></textarea> [(ngModel)]="comment"></textarea>
</div> </div>
<button (click)="toggleStatusPdfPrint()" class="ti-btn m-0 ti-btn-soft-warning"
*ngIf="currentTap == 'แผนพัฒนาบุคลากร'">
<i class="ri-draft-fill"></i>
PDF
</button>
<button (click)="save('approve')" class="ti-btn m-0 ti-btn-soft-secondary" <button (click)="save('approve')" class="ti-btn m-0 ti-btn-soft-secondary"
[disabled]="compentencyFormRemain||kpiFormRemain" [disabled]="compentencyFormRemain||kpiFormRemain"
[class.ti-btn-disabled]="compentencyFormRemain||kpiFormRemain"> [class.ti-btn-disabled]="compentencyFormRemain||kpiFormRemain">
...@@ -659,4 +664,4 @@ ...@@ -659,4 +664,4 @@
</div> </div>
</div> </div>
</div> </div>
</ng-template> </ng-template>
\ No newline at end of file
...@@ -8,3 +8,18 @@ ...@@ -8,3 +8,18 @@
opacity: 1; opacity: 1;
} }
} }
@media print {
body * {
visibility: hidden; /* ซ่อนทุก element */
}
#printArea, #printArea * {
visibility: visible; /* แสดงเฉพาะ #printArea */
}
#printArea {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
}
...@@ -74,7 +74,7 @@ export class PmsFormEmployeeComponent { ...@@ -74,7 +74,7 @@ export class PmsFormEmployeeComponent {
complete = false complete = false
@ViewChild('scrollContainer') scrollContainer!: ElementRef; @ViewChild('scrollContainer') scrollContainer!: ElementRef;
pdfPrint = false;
constructor( constructor(
private router: Router, private router: Router,
...@@ -429,7 +429,7 @@ export class PmsFormEmployeeComponent { ...@@ -429,7 +429,7 @@ export class PmsFormEmployeeComponent {
<g id="alert cart"> <g id="alert cart">
<g id="mdi:file-export"> <g id="mdi:file-export">
<circle cx="22.5" cy="19.5" r="33.5" fill="#E8F8EE"/> <circle cx="22.5" cy="19.5" r="33.5" fill="#E8F8EE"/>
<path d="M9.75 3.25C8.88805 3.25 8.0614 3.59241 7.4519 4.2019C6.84241 4.8114 6.5 5.63805 6.5 6.5V32.5C6.5 33.362 6.84241 34.1886 7.4519 34.7981C8.0614 35.4076 8.88805 35.75 9.75 35.75H29.25C30.112 35.75 30.9386 35.4076 31.5481 34.7981C32.1576 34.1886 32.5 33.362 32.5 32.5V13L22.75 3.25M21.125 5.6875L30.0625 14.625H21.125M14.5113 19.8575H26V31.3463L22.555 27.9013L17.9563 32.5L13.3575 27.9013L17.9563 23.3188" <path d="M9.75 3.25C8.88805 3.25 8.0614 3.59241 7.4519 4.2019C6.84241 4.8114 6.5 5.63805 6.5 6.5V32.5C6.5 33.362 6.84241 34.1886 7.4519 34.7981C8.0614 35.4076 8.88805 35.75 9.75 35.75H29.25C30.112 35.75 30.9386 35.4076 31.5481 34.7981C32.1576 34.1886 32.5 33.362 32.5 32.5V13L22.75 3.25M21.125 5.6875L30.0625 14.625H21.125M14.5113 19.8575H26V31.3463L22.555 27.9013L17.9563 32.5L13.3575 27.9013L17.9563 23.3188"
fill="#1DBE5A"/> fill="#1DBE5A"/>
</g> </g>
</g> </g>
...@@ -759,4 +759,14 @@ export class PmsFormEmployeeComponent { ...@@ -759,4 +759,14 @@ export class PmsFormEmployeeComponent {
const imgElement = event.target as HTMLImageElement; const imgElement = event.target as HTMLImageElement;
imgElement.src = './assets/img/users/defaultperson.jpg'; imgElement.src = './assets/img/users/defaultperson.jpg';
} }
}
\ No newline at end of file toggleStatusPdfPrint() {
this.pdfPrint = !this.pdfPrint; // สลับ true/false เพื่อให้ Child จับการเปลี่ยน
}
onPdfPrinted() {
// เมื่อ Child แจ้งว่าพิมพ์เสร็จ ให้รีเซ็ตกลับเป็น false
this.pdfPrint = false;
}
}
<ng-container *ngTemplateOutlet="idpEvaluation"></ng-container> <ng-container *ngTemplateOutlet="idpEvaluation"></ng-container>
<ng-template #idpEvaluation> <ng-template #idpEvaluation>
<ng-container *ngIf="appraisalIdp"> <ng-container *ngIf="appraisalIdp">
<div style="overflow-y: auto;" style="padding-top: 1rem"> <div style="overflow-y: auto; padding-top: 1rem" #pdfArea class="pdf-container">
<div class="pb-2"> <div class="pb-2">
<div class="font-size-18px font-weight-700 text-gray-500"> <div class="font-size-18px font-weight-700 text-gray-500">
ส่วนที่ 1: ข้อมูลทั่วไป ส่วนที่ 1: ข้อมูลทั่วไป
...@@ -283,29 +283,34 @@ ...@@ -283,29 +283,34 @@
<div class="flex !items-center" style="min-height: 100px;"> <div class="flex !items-center" style="min-height: 100px;">
{{competencyCourse.tdesc}} {{competencyCourse.tdesc}}
<span class="ciricon border cursor-pointer" <span class="ciricon border cursor-pointer"
*ngIf="canEdit&&appraisalIdp.masfromEvaluationIdp.idpStatus=='1'" *ngIf="canEdit&&appraisalIdp.masfromEvaluationIdp.idpStatus=='1' && pdfPrintCheck == 0"
(click)="deleteCompetencyCourse(i,l)"> (click)="deleteCompetencyCourse(i,l)">
<i class="ri-close-line text-red-500"></i> <i class="ri-close-line text-red-500"></i>
</span> </span>
</div> </div>
<button <div *ngIf="pdfPrintCheck == 0">
*ngIf="last&&canEdit&&appraisalIdp.masfromEvaluationIdp.idpStatus=='1'" <button
type="button" *ngIf="last&&canEdit&&appraisalIdp.masfromEvaluationIdp.idpStatus=='1'"
class="ti-btn ti-btn-soft-secondary h-45px m-0 shadow-md" type="button"
(click)="openCompetencycourseDialog(i) "> class="ti-btn ti-btn-soft-secondary h-45px m-0 shadow-md"
<i class="ri-add-line"></i> (click)="openCompetencycourseDialog(i) ">
Add <i class="ri-add-line"></i>
Add
</button> </button>
</div>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container <ng-container
*ngIf="canEdit&&!data.competencyCourse?.length&&appraisalIdp.masfromEvaluationIdp.idpStatus=='1'&&data.idpDevelopmentPlan?.training"> *ngIf="canEdit&&!data.competencyCourse?.length&&appraisalIdp.masfromEvaluationIdp.idpStatus=='1'&&data.idpDevelopmentPlan?.training">
<button type="button" <div *ngIf="pdfPrintCheck == 0">
class="ti-btn ti-btn-soft-secondary h-45px m-0 shadow-md"
(click)="openCompetencycourseDialog(i)"> <button type="button"
<i class="ri-add-line"></i> class="ti-btn ti-btn-soft-secondary h-45px m-0 shadow-md"
Add (click)="openCompetencycourseDialog(i)">
<i class="ri-add-line"></i>
Add
</button> </button>
</div>
</ng-container> </ng-container>
</td> </td>
<td class="!p-0"></td> <td class="!p-0"></td>
...@@ -315,7 +320,9 @@ ...@@ -315,7 +320,9 @@
*ngIf="appraisalIdp.masfromEvaluationRound.apsPeriodStart &&appraisalIdp.masfromEvaluationRound.apsPeriodEnd"> *ngIf="appraisalIdp.masfromEvaluationRound.apsPeriodStart &&appraisalIdp.masfromEvaluationRound.apsPeriodEnd">
จาก&nbsp; จาก&nbsp;
</ng-container> </ng-container>
<ng-container
<ng-container *ngIf="pdfPrintCheck == 0">
<ng-container
*ngIf="data.idpDevelopmentPlan&&data.idpDevelopmentPlan?.training && appraisalIdp.apsapprove1.employeeId == evaluaterId"> *ngIf="data.idpDevelopmentPlan&&data.idpDevelopmentPlan?.training && appraisalIdp.apsapprove1.employeeId == evaluaterId">
<input type="date" id="input-label" class="ti-form-input" <input type="date" id="input-label" class="ti-form-input"
[(ngModel)]="appraisalIdp.masfromEvaluationRound.apsPeriodStart"> [(ngModel)]="appraisalIdp.masfromEvaluationRound.apsPeriodStart">
...@@ -335,6 +342,18 @@ ...@@ -335,6 +342,18 @@
</ng-container> </ng-container>
{{convertDateFormat(appraisalIdp.masfromEvaluationRound.apsPeriodEnd)}} {{convertDateFormat(appraisalIdp.masfromEvaluationRound.apsPeriodEnd)}}
</ng-container> </ng-container>
</ng-container>
<ng-container *ngIf="pdfPrintCheck == 1">
{{convertDateFormat(appraisalIdp.masfromEvaluationRound.apsPeriodStart)}}
<ng-container
*ngIf="appraisalIdp.masfromEvaluationRound.apsPeriodStart &&appraisalIdp.masfromEvaluationRound.apsPeriodEnd">
<p></p>ถึง&nbsp;
</ng-container>
{{convertDateFormat(appraisalIdp.masfromEvaluationRound.apsPeriodEnd)}}
</ng-container>
</td> </td>
</tr> </tr>
</ng-container> </ng-container>
...@@ -414,4 +433,4 @@ ...@@ -414,4 +433,4 @@
ย้อนกลับ ย้อนกลับ
</button> </button>
</mat-dialog-actions> </mat-dialog-actions>
</ng-template> </ng-template>
\ No newline at end of file
@media print {
body * {
visibility: hidden; /* ซ่อนทุก element */
}
#printArea, #printArea * {
visibility: visible; /* แสดงเฉพาะ #printArea */
}
#printArea {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
}
.pdf-container {
/* เอาตามที่คุณมีได้เลย */
background: #fff;
padding: 10px;
transform: translateZ(0);
}
.pdf-container table {
// table-layout: fixed;
width: 100%;
// word-break: break-word;
white-space: normal;
}
.page-break {
break-after: page; /* modern */
page-break-after: always; /* legacy */
}
.no-break {
break-inside: avoid;
page-break-inside: avoid;
}
.pdf-header {
display: flex;
gap: 12px;
align-items: center;
img {
width: 72px;
height: 72px;
object-fit: cover;
border-radius: 50%;
}
}
/* ตารางใน PDF ให้ไม่แตกกลางหน้า */
table, .card, .section-block {
break-inside: avoid;
page-break-inside: avoid;
}
.pdf-container > :last-child {
margin-bottom: 0 !important;
padding-bottom: 0 !important;
}
// .pdf-container .page-break:last-child {
// page-break-after: auto !important;
// break-after: auto !important;
// }
import { ChangeDetectorRef, Component, EventEmitter, Input, Output, SimpleChanges, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Output, SimpleChanges, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Idp } from 'src/app/shared/model/competency.model'; import { Idp } from 'src/app/shared/model/competency.model';
import { CompetencycourseMiniModel, MyCompetencycourseMiniModel } from 'src/app/shared/model/competencycourse-mini.model'; import { CompetencycourseMiniModel, MyCompetencycourseMiniModel } from 'src/app/shared/model/competencycourse-mini.model';
...@@ -35,6 +35,10 @@ export class PmsIdpComponent { ...@@ -35,6 +35,10 @@ export class PmsIdpComponent {
@Input() currentStep = "" @Input() currentStep = ""
@Input() currentTap = "" @Input() currentTap = ""
@Output() idpForm: EventEmitter<any> = new EventEmitter<any>(); @Output() idpForm: EventEmitter<any> = new EventEmitter<any>();
@Input() pdfPrint = false;
@Output() pdfPrinted = new EventEmitter<void>();
pdfPrintCheck = 0
@ViewChild('pdfArea') pdfArea!: ElementRef<HTMLElement>;
competencycourse: { loading: boolean, data: CompetencycourseMiniModel[] } = { loading: false, data: [] } competencycourse: { loading: boolean, data: CompetencycourseMiniModel[] } = { loading: false, data: [] }
competencycourseTable: table = { competencycourseTable: table = {
...@@ -58,6 +62,17 @@ export class PmsIdpComponent { ...@@ -58,6 +62,17 @@ export class PmsIdpComponent {
this.getCompetencycourseMiniList() this.getCompetencycourseMiniList()
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes['pdfPrint']) {
const { previousValue, currentValue } = changes['pdfPrint'];
console.log('pdfPrint changed:', previousValue, '→', currentValue);
// สั่งพิมพ์เฉพาะตอนเป็น true
if (currentValue === true) {
this.pdfPrintCheck = 1
this.exportPdf();
this.pdfPrinted.emit(); // แจ้ง Parent ให้รีเซ็ต flag
}
}
if (changes['currentTap']?.currentValue || changes['appraisalIdp']?.currentValue) { if (changes['currentTap']?.currentValue || changes['appraisalIdp']?.currentValue) {
this.getFormIdp() this.getFormIdp()
} }
...@@ -173,4 +188,164 @@ export class PmsIdpComponent { ...@@ -173,4 +188,164 @@ export class PmsIdpComponent {
this.idpForm.emit(this.appraisalIdp) this.idpForm.emit(this.appraisalIdp)
} }
print() {
window.print();
}
/** ล็อคความกว้างเทียบ A4 (≈794px @96dpi) ให้ layout เสถียรตอนแคปเจอร์ */
private freezeWidthForA4(el: HTMLElement) {
const prev = {
width: el.style.width,
maxWidth: el.style.maxWidth,
transform: el.style.transform,
transformOrigin: el.style.transformOrigin,
};
el.style.width = '794px';
el.style.maxWidth = '794px';
el.style.transformOrigin = 'top left';
el.style.transform = 'scale(1)';
return () => {
el.style.width = prev.width;
el.style.maxWidth = prev.maxWidth;
el.style.transform = prev.transform;
el.style.transformOrigin = prev.transformOrigin;
};
}
/** ขยายเป็นความกว้างจริง (scrollWidth) แล้วสเกลลงให้พอดี A4 */
private expandToFullWidthThenScale(el: HTMLElement) {
const A4_PX = 794; // ≈ 210mm @ 96dpi
const full = el.scrollWidth; // ความกว้างจริงทั้งหมด (รวมพื้นที่ต้องเลื่อนขวา)
const scale = Math.min(1, A4_PX / full); // สเกลลงให้พอดี A4 (ถ้ากว้างกว่า)
// เก็บค่าเดิมไว้คืนทีหลัง
const prev = {
width: el.style.width,
maxWidth: el.style.maxWidth,
overflowX: el.style.overflowX,
transform: el.style.transform,
transformOrigin: el.style.transformOrigin,
};
// คลี่ความกว้างออกทั้งหมด แล้วสเกลให้พอดี A4
el.style.width = `${full}px`;
el.style.maxWidth = 'none';
el.style.overflowX = 'visible';
el.style.transformOrigin = 'top left';
el.style.transform = `scale(${scale})`;
// คืนค่า
return () => {
el.style.width = prev.width;
el.style.maxWidth = prev.maxWidth;
el.style.overflowX = prev.overflowX;
el.style.transform = prev.transform;
el.style.transformOrigin = prev.transformOrigin;
};
}
/** ทำให้ทุกกล่องใน subtree ไม่ตัดขอบขวา: overflowX => visible, ยกเลิก position:sticky ชั่วคราว */
private unclipOverflows(el: HTMLElement) {
const patched: Array<{node: HTMLElement, prev: Partial<CSSStyleDeclaration>}> = [];
const walker = document.createTreeWalker(el, NodeFilter.SHOW_ELEMENT);
const patchNode = (node: HTMLElement) => {
const style = window.getComputedStyle(node);
const needOverflowPatch = style.overflowX !== 'visible' || style.overflow !== 'visible';
const isSticky = style.position === 'sticky';
if (needOverflowPatch || isSticky) {
const prev = {
overflow: node.style.overflow,
overflowX: node.style.overflowX,
overflowY: node.style.overflowY,
position: node.style.position,
clipPath: (node.style as any).clipPath,
};
patched.push({ node, prev });
node.style.overflow = 'visible';
node.style.overflowX = 'visible';
node.style.overflowY = 'visible';
if (isSticky) node.style.position = 'static';
(node.style as any).clipPath = 'none';
}
};
// รวม el เองด้วย
patchNode(el as HTMLElement);
while (walker.nextNode()) {
patchNode(walker.currentNode as HTMLElement);
}
// ฟังก์ชันคืนค่าเดิม
return () => {
for (const { node, prev } of patched) {
node.style.overflow = prev.overflow || '';
node.style.overflowX = prev.overflowX || '';
node.style.overflowY = prev.overflowY || '';
node.style.position = prev.position || '';
(node.style as any).clipPath = prev.clipPath || '';
}
};
}
// @ViewChild('pdfArea') pdfArea!: ElementRef<HTMLElement>;
async exportPdf() {
const el = this.pdfArea?.nativeElement;
if (!el) return;
const mod: any = await import('html2pdf.js');
const html2pdf = mod.default ?? mod;
// 1) คลี่กว้างจริงแล้วสเกลลงพอดี A4
const cleanupScale = this.expandToFullWidthThenScale(el);
// 2) ปลดคลิป overflow/sticky ของลูก ๆ ทั้งหมด
const cleanupUnclip = this.unclipOverflows(el);
const prevBodyOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
// ขนาดจริงของ element (หลังถูกขยายแล้ว)
const fullW = el.scrollWidth;
const fullH = el.scrollHeight;
const epsilon = 2; // กันเผื่อ
const opt = {
margin: [10,10,10,10],
filename: `pms-${this.evaluateeId || 'employee'}-${new Date().toISOString().slice(0,10)}.pdf`,
image: { type: 'jpeg', quality: 0.96 },
html2canvas: {
scale: 2,
useCORS: true,
logging: false,
scrollX: 0,
scrollY: -window.scrollY,
// 👇 บอกขนาดที่จะเรนเดอร์ให้เท่าของจริงทั้งหมด
width: 800,
// height: fullH,
height: Math.max(0, fullH - epsilon),
windowWidth: 800,
// windowHeight: fullH,
windowHeight: Math.max(0, fullH - epsilon),
// foreignObjectRendering: true, // ลองเปิดถ้า layout ยังเพี้ยน (มีข้อจำกัดกับบางเคส)
},
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
pagebreak: { mode: ['css','legacy'], avoid: ['.no-break','table','img'] }
};
try {
await html2pdf().from(el).set(opt).save();
} finally {
cleanupUnclip();
cleanupScale();
document.body.style.overflow = prevBodyOverflow;
}
this.pdfPrintCheck = 0
}
} }
...@@ -127,4 +127,4 @@ export class SelfEvaluationComponent implements OnInit { ...@@ -127,4 +127,4 @@ export class SelfEvaluationComponent implements OnInit {
} }
return date?.toLocaleDateString('th-TH', { day: 'numeric', month: 'long', year: 'numeric' }) || '' return date?.toLocaleDateString('th-TH', { day: 'numeric', month: 'long', year: 'numeric' }) || ''
} }
} }
\ No newline at end of file
declare module 'html2pdf.js' {
const html2pdf: any;
export default html2pdf;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment