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">
......
...@@ -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,
...@@ -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';
} }
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,11 +283,12 @@ ...@@ -283,11 +283,12 @@
<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>
<div *ngIf="pdfPrintCheck == 0">
<button <button
*ngIf="last&&canEdit&&appraisalIdp.masfromEvaluationIdp.idpStatus=='1'" *ngIf="last&&canEdit&&appraisalIdp.masfromEvaluationIdp.idpStatus=='1'"
type="button" type="button"
...@@ -296,16 +297,20 @@ ...@@ -296,16 +297,20 @@
<i class="ri-add-line"></i> <i class="ri-add-line"></i>
Add 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">
<div *ngIf="pdfPrintCheck == 0">
<button type="button" <button type="button"
class="ti-btn ti-btn-soft-secondary h-45px m-0 shadow-md" class="ti-btn ti-btn-soft-secondary h-45px m-0 shadow-md"
(click)="openCompetencycourseDialog(i)"> (click)="openCompetencycourseDialog(i)">
<i class="ri-add-line"></i> <i class="ri-add-line"></i>
Add Add
</button> </button>
</div>
</ng-container> </ng-container>
</td> </td>
<td class="!p-0"></td> <td class="!p-0"></td>
...@@ -315,6 +320,8 @@ ...@@ -315,6 +320,8 @@
*ngIf="appraisalIdp.masfromEvaluationRound.apsPeriodStart &&appraisalIdp.masfromEvaluationRound.apsPeriodEnd"> *ngIf="appraisalIdp.masfromEvaluationRound.apsPeriodStart &&appraisalIdp.masfromEvaluationRound.apsPeriodEnd">
จาก&nbsp; จาก&nbsp;
</ng-container> </ng-container>
<ng-container *ngIf="pdfPrintCheck == 0">
<ng-container <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"
...@@ -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>
......
@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
}
} }
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