print and report
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 7m9s
Build Docker Image / Restart Docker Compose (push) Successful in 0s

This commit is contained in:
x2Skyz
2025-12-01 00:53:43 +07:00
parent 40e682e5d8
commit f332f8b6e2
5 changed files with 347 additions and 247 deletions

View File

@@ -45,37 +45,6 @@ export class MainDashboardComponent implements OnInit {
private dashboardStateService: DashboardStateService
){}
readonly kpiCards = [
{
label: 'รายรับรวม',
value: '฿1.28M',
trend: '+12.4%',
context: 'เทียบกับเดือนก่อน',
accent: 'mint'
},
{
label: 'รายจ่ายรวม',
value: '฿732K',
trend: '-4.1%',
context: 'จัดการได้ดีขึ้น',
accent: 'lavender'
},
{
label: 'ยอดค้างชำระ',
value: '฿184K',
trend: '-2 ใบแจ้งหนี้',
context: 'รอติดตาม',
accent: 'amber'
},
{
label: 'อัตรากำไร',
value: '37.8%',
trend: '+1.9 จุด',
context: 'ระยะ 30 วัน',
accent: 'teal'
}
];
// readonly revenueTrend = [
// { label: 'ม.ค.', value: 52 },
// { label: 'ก.พ.', value: 61 },
@@ -98,71 +67,8 @@ export class MainDashboardComponent implements OnInit {
isNumber(val: any): boolean {
return typeof val === 'number';
}
readonly periodSummaries = [
{
label: 'รายปี',
note: 'ปี 2567',
income: '฿9.6M',
expense: '฿5.1M',
net: '+฿4.5M',
trend: '+18%',
badge: 'year'
},
{
label: 'รายเดือน',
note: 'มิถุนายน 2567',
income: '฿1.28M',
expense: '฿732K',
net: '+฿548K',
trend: '+6%',
badge: 'month'
},
{
label: 'รายสัปดาห์',
note: 'สัปดาห์ที่ 24',
income: '฿312K',
expense: '฿188K',
net: '+฿124K',
trend: '+2%',
badge: 'week'
}
];
readonly alerts = [
{
title: 'ใบแจ้งหนี้ #INV-083 จะครบกำหนด',
detail: 'ลูกค้า Metro Engineering',
tag: 'ภายใน 3 วัน'
},
{
title: 'มีเอกสารที่ต้องอนุมัติ 2 รายการ',
detail: 'เบิกค่าใช้จ่ายฝ่ายการตลาด',
tag: 'รออนุมัติ'
},
{
title: 'พบรายการใช้จ่ายผิดปกติ',
detail: 'ค่าใช้จ่ายเดินทางสูงกว่าค่าเฉลี่ย 28%',
tag: 'ตรวจสอบ'
}
];
readonly tasks = [
{
title: 'กระทบยอดธนาคาร เดือน มิ.ย.',
due: 'วันนี้ 16:00',
priority: 'สูง'
},
{
title: 'เตรียมรายงาน VAT',
due: 'พรุ่งนี้ 10:30',
priority: 'กลาง'
},
{
title: 'ออกใบเสนอราคา โครงการใหม่',
due: 'ศุกร์ 14:00',
priority: 'ต่ำ'
}
];
// readonly ledgerEntries = [
// {
@@ -199,14 +105,6 @@ isNumber(val: any): boolean {
// }
// ];
readonly expenseBreakdown = [
{ label: 'ฝ่ายบริหาร', value: 32, color: '#0ea5e9' },
{ label: 'การตลาด', value: 18, color: '#f97316' },
{ label: 'ต้นทุนโครงการ', value: 27, color: '#10b981' },
{ label: 'บุคลากร', value: 15, color: '#a855f7' },
{ label: 'อื่นๆ', value: 8, color: '#e11d48' }
];
ngOnInit(): void {
this.setupFormControl();

View File

@@ -209,80 +209,127 @@
color: #dc2626;
}
/* --- UPDATED PIE CHART SECTION (Donut Style) --- */
.pie-panel__content {
display: flex;
gap: 1.5rem;
align-items: center;
justify-content: center;
gap: 3rem; /* ระยะห่างระหว่างกราฟกับ Legend */
padding: 1rem;
}
.chart-wrapper {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.pie-chart {
width: 180px;
height: 180px;
border-radius: 50%;
position: relative;
z-index: 2;
/* Flex เพื่อจัดรูตรงกลาง */
display: flex;
align-items: center;
justify-content: center;
}
.pie-chart {
width: 200px;
height: 200px;
border-radius: 50%;
position: relative;
box-shadow: inset 0 0 20px rgba(15, 23, 42, 0.08);
}
.pie-chart__center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 110px;
height: 110px;
border-radius: 50%;
width: 75%; /* ความหนาของขอบ */
height: 75%;
background: #fff;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.1);
box-shadow: inset 0 2px 10px rgba(0,0,0,0.05);
}
.pie-chart__center p {
.chart-shadow {
position: absolute;
width: 160px;
height: 160px;
border-radius: 50%;
background: rgba(0,0,0,0.03);
filter: blur(20px);
z-index: 1;
top: 15px;
}
.label-muted {
margin: 0;
color: #94a3b8;
font-size: 0.85rem;
font-size: 0.8rem;
color: #9ca3af;
margin-bottom: 0.25rem;
}
.pie-chart__center strong {
color: #0f172a;
.total-amount {
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
line-height: 1;
}
/* Legend Styles */
.pie-legend {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.8rem;
gap: 1.5rem; /* ระยะห่างแต่ละ item */
min-width: 160px;
}
.pie-legend li {
.pie-legend__item {
display: flex;
align-items: center;
gap: 0.6rem;
align-items: flex-start;
gap: 1rem;
}
.swatch {
width: 14px;
height: 14px;
width: 12px;
height: 12px;
border-radius: 4px;
margin-top: 6px; /* ดันลงมาให้ตรงกับ Text บรรทัดแรก */
flex-shrink: 0;
}
.legend-label {
.legend-text {
display: flex;
flex-direction: column;
line-height: 1.4;
}
.item-label {
font-size: 0.95rem;
font-weight: 500;
color: #374151;
margin: 0;
}
.item-percent {
font-size: 0.9rem;
font-weight: 600;
color: #1f2937;
margin: 0;
}
.legend-value {
margin: 0;
color: #94a3b8;
.item-value {
font-size: 0.85rem;
color: #6b7280;
margin: 0;
}
/* --- END UPDATED PIE CHART SECTION --- */
.preview-modal {
position: fixed;
inset: 0;
@@ -422,6 +469,7 @@
.pie-panel__content {
flex-direction: column;
gap: 2rem;
}
}

View File

@@ -1,33 +1,59 @@
<section class="report">
<header class="report__header">
<div class="report">
<div class="report__header">
<div>
<p class="eyebrow">สรุปรายงาน</p>
<h1>รายงานรายรับรายจ่าย</h1>
<p class="muted">ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}</p>
</div>
<div class="report__actions">
<button class="btn btn--ghost">ส่งออกเป็น Excel</button>
<!-- <button class="btn btn--ghost">ส่งออกเป็น Excel</button> -->
<button class="btn btn--primary" (click)="openPreview()">ปริ้นรายงาน</button>
</div>
</header>
</div>
<section class="summary-grid">
<article class="summary-card" *ngFor="let card of summaryCards">
<p class="summary-card__label">{{ card.label }}</p>
<h2>{{ card.value }}</h2>
<p class="summary-card__detail">{{ card.detail }}</p>
<span class="summary-card__tone" [ngClass]="'tone-' + card.tone"></span>
</article>
</section>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<section class="report__content">
<article class="panel">
<div class="p-6 rounded-2xl bg-green-50 border border-green-100 shadow-sm hover:shadow-md transition duration-200">
<p class="text-sm font-medium text-green-800 opacity-70">รายรับรวม</p>
<h2 class="text-3xl font-bold text-gray-800 mt-2">{{ myActSumData.summary.totalIncome | number:'1.2-2' }}</h2>
<div class="mt-2 flex items-center text-sm font-medium text-green-600">
<!-- <span>+12.4% MoM</span> -->
</div>
</div>
<div class="p-6 rounded-2xl bg-yellow-50 border border-yellow-100 shadow-sm hover:shadow-md transition duration-200">
<p class="text-sm font-medium text-yellow-800 opacity-70">รายจ่ายรวม</p>
<h2 class="text-3xl font-bold text-gray-800 mt-2">{{ myActSumData.summary.totalExpense | number:'1.2-2' }}</h2>
<div class="mt-2 flex items-center text-sm font-medium text-yellow-600">
<!-- <span>-4.1% MoM</span> -->
</div>
</div>
<div class="p-6 rounded-2xl bg-purple-50 border border-purple-100 shadow-sm hover:shadow-md transition duration-200">
<p class="text-sm font-medium text-purple-800 opacity-70">กำไรสุทธิ</p>
<h2 class="text-3xl font-bold text-gray-800 mt-2">{{ myActSumData.summary.netProfit | number:'1.2-2' }}</h2>
<div class="mt-2 flex items-center text-sm font-medium text-purple-600">
<span>Margin {{ myActSumData.summary.profitRate }}</span>
</div>
</div>
<div class="p-6 rounded-2xl bg-gray-50 border border-gray-100 shadow-sm hover:shadow-md transition duration-200">
<p class="text-sm font-medium text-gray-600 opacity-70">บันทึกรายการ</p>
<h2 class="text-3xl font-bold text-gray-800 mt-2">{{myActData.length}} รายการ</h2>
<div class="mt-2 flex items-center text-xs font-medium text-gray-500">
<!-- <span>32 รายรับ · 54 รายจ่าย</span> -->
</div>
</div>
</div>
<div class="report__content">
<div class="panel">
<div class="panel__header">
<div>
<h2>สมุดรายวัน</h2>
<p>บันทึกรายรับรายจ่ายทั้งหมดในช่วงเวลา</p>
</div>
<button class="btn btn--compact btn--ghost">กรองข้อมูล</button>
</div>
<div class="table">
<div class="table__head">
@@ -37,106 +63,123 @@
<span>หมวดหมู่</span>
<span class="amount-col">ยอดเงิน</span>
</div>
<div class="table__row" *ngFor="let record of formattedRecords">
<span>{{ record.date }}</span>
<span class="mono">{{ record.doc }}</span>
@for (idx of myActData; track idx.actseq) {
<div class="table__row">
<span>{{ idx.actacpdtm | date:'dd/MM/yyyy' }}</span>
<span class="mono">RCPT{{ idx.actseq }}</span>
<span>
<strong>{{ record.topic }}</strong>
<small class="muted">{{ record.type === 'income' ? 'รายรับ' : 'รายจ่าย' }}</small>
<strong>{{ idx.actcatnam }}</strong>
<small class="muted">{{ idx.acttyp === 'i' ? 'รับ' : 'จ่าย' }}</small>
</span>
<span>{{ record.category }}</span>
<span class="amount-col" [ngClass]="record.tone">{{ record.displayAmount }}</span>
<span>{{ idx.actcatnam }}</span>
<span class="amount-col"  [ngClass]="idx.acttyp === 'i' ? 'income' : 'expense' ">{{ idx.actqty | number:'1.2-2' }}</span>
</div>
} @empty {
<div class="p-4 text-center text-gray-400">ไม่มีข้อมูลรายการ</div>
}
</div>
</article>
</div>
<article class="panel pie-panel">
<div class="panel__header">
<div>
<h2>สัดส่วนค่าใช้จ่าย</h2>
<p>เปรียบเทียบหมวดหลักของรายจ่ายเดือนนี้</p>
</div>
</div>
<div class="pie-panel__content">
<div class="pie-chart" [style.background]="expenseGradient">
<div class="pie-chart__center">
<p>รวมรายจ่าย</p>
<strong>฿732K</strong>
</div>
</div>
<ul class="pie-legend">
<li *ngFor="let part of expenseBreakdown">
<span class="swatch" [style.background]="part.color"></span>
<div>
<p class="legend-label">{{ part.label }}</p>
<p class="legend-value">{{ part.value }}%</p>
</div>
</li>
</ul>
</div>
</article>
</section>
</section>
<section class="preview-modal" *ngIf="printPreviewOpen">
<div class="preview-modal__backdrop" (click)="closePreview()"></div>
<div class="preview-modal__content">
<header class="preview-modal__header">
<div>
<p class="eyebrow">Print Preview</p>
<h2>รายงานรายรับรายจ่าย</h2>
<p class="muted">ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}</p>
</div>
<div class="preview-modal__actions">
<button class="btn btn--ghost" (click)="closePreview()">ปิด</button>
<button class="btn btn--primary">พิมพ์</button>
</div>
</header>
<div class="preview-sheet">
<div class="preview-sheet__header">
<div>
<h3>Accounting Summary</h3>
<p>Prepared on {{ reportRange.end }}</p>
</div>
<div class="preview-totals">
<div *ngFor="let total of previewTotals">
<p>{{ total.label }}</p>
<strong>{{ total.value }}</strong>
</div>
</div>
</div>
<div class="preview-pie">
<div class="mini-pie" [style.background]="expenseGradient"></div>
<ul>
<li *ngFor="let part of expenseBreakdown">
<span class="swatch" [style.background]="part.color"></span>
<span>{{ part.label }} · {{ part.value }}%</span>
</li>
</ul>
</div>
<table class="preview-table">
<thead>
<tr>
<th>วันที่</th>
<th>เลขที่</th>
<th>หัวข้อ</th>
<th>หมวดหมู่</th>
<th>ยอดเงิน</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let record of formattedRecords">
<td>{{ record.date }}</td>
<td>{{ record.doc }}</td>
<td>{{ record.topic }}</td>
<td>{{ record.category }}</td>
<td [ngClass]="record.tone">{{ record.displayAmount }}</td>
</tr>
</tbody>
</table>
<div class="panel pie-panel">
<div class="panel__header">
<div>
<h2>สัดส่วนค่าใช้จ่าย</h2>
<p>เปรียบเทียบหมวดหลักของรายจ่ายเดือนนี้</p>
</div>
</div>
</section>
<div class="pie-panel__content">
<div class="chart-wrapper">
<div class="pie-chart" [style.background]="ActSumDataGradient">
<div class="pie-chart__center">
<p class="label-muted">รวมรายจ่าย</p>
<strong class="total-amount">{{ myActSumData.summary.totalExpense | number:'1.0-0' }}</strong>
</div>
</div>
<div class="chart-shadow"></div>
</div>
<ul class="pie-legend">
@for (part of myActSumData.pie.expense; track part.label) {
<li class="pie-legend__item">
<span class="swatch" [style.background]="part.color"></span>
<div class="legend-text">
<p class="item-label">{{ part.label }}</p>
<p class="item-percent">{{ part.percent }}%</p>
<p class="item-value">{{ part.value | number:'1.0-0' }} บาท</p>
</div>
</li>
}
</ul>
</div>
</div>
</div>
</div>
<!-- PRINT PREVIEW MODAL -->
@if(printPreviewOpen){
<div class="preview-modal">
<div class="preview-modal__backdrop" (click)="closePreview()"></div>
<div class="preview-modal__content">
<div class="preview-modal__header">
<div>
<p class="eyebrow">Print Preview</p>
<h2>รายงานรายรับรายจ่าย</h2>
</div>
<div class="preview-modal__actions">
<button class="btn btn--ghost" (click)="closePreview()">ปิด</button>
<!-- ACTION ADDED HERE -->
<button class="btn btn--primary" (click)="printReport()">พิมพ์</button>
</div>
</div>
<div class="preview-sheet">
<div class="preview-sheet__header">
<div>
<h3>Accounting Summary</h3>
<p>Prepared on {{ reportRange.end }}</p>
</div>
</div>
<div class="preview-pie">
<div class="mini-pie" [style.background]="ActSumDataGradient"></div>
<ul>
@for (part of myActSumData.pie.expense; track part.label) {
<li>
<span class="swatch" [style.background]="part.color"></span>
<span>{{ part.label }} · {{ part.percent }}%</span>
</li>
}
</ul>
</div>
<table class="preview-table">
<thead>
<tr>
<th>วันที่</th>
<th>เลขที่</th>
<th>หัวข้อ</th>
<th>หมวดหมู่</th>
<th>ยอดเงิน</th>
</tr>
</thead>
<tbody>
@for (idx of myActData; track idx.actseq) {
<tr>
<td>{{ idx.actacpdtm | date:'dd/MM/yyyy'}}</td>
<td>RCPT{{ idx.actseq }}</td>
<td>{{ idx.actcatnam }}</td>
<td>{{ idx.actcatnam }}</td>
<td [ngClass]="idx.acttyp === 'i' ? 'income' : 'expense' ">{{ idx.actqty | number:'1.2-2' }}</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
<!-- The rest of the dashboard HTML is excluded for brevity/focus on the core report logic -->

View File

@@ -1,4 +1,8 @@
import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { GeneralService } from './../../services/generalservice';
import { QuickRatio, IStateDrop, IActSumData } from './../../interfaces/dashboard.interface';
import { Component, OnInit } from '@angular/core';
import { IActData } from '../../interfaces/dashboard.interface';
@Component({
selector: 'app-main-report',
@@ -6,7 +10,36 @@ import { Component } from '@angular/core';
standalone: false,
styleUrls: ['./main-report.component.css']
})
export class MainReportComponent {
export class MainReportComponent implements OnInit{
myActData: IActData[] = [];
quickRatios: QuickRatio[] = [];
// myDropAct: IStateDrop[] = [];
myDropAct: IStateDrop = { income: [], expense: [] };
myActSumData: IActSumData = {
summary: {
totalIncome: '',
totalExpense: '',
netProfit: 0,
profitRate: '',
adjustedProfitRate: '',
period: ''
},
pie: {
income: [],
expense: []
}
};
ActSumDataGradient: any
constructor(
private generalService: GeneralService,
private route: ActivatedRoute,
private router: Router
){}
readonly reportRange = {
start: '1 มิถุนายน 2567',
end: '30 มิถุนายน 2567'
@@ -102,6 +135,60 @@ export class MainReportComponent {
printPreviewOpen = false;
ngOnInit(): void {
this.OnSearchSum({}, false);
this.OnSearchAct({});
}
OnSearchAct(value: any): void {
const uri = '/api/web/accountingSearch';
let request = {
token: value
}
this.generalService.postRequest(uri, request).subscribe({
next: (result: any) => {
if (result.code === '200') {
this.generalService.trowApi(result);
this.myActData = result.data;
}else{
this.generalService.trowApi(result);
}
},
error: (error: any) => {
this.generalService.trowApi(error);
},
complete: () => {
}
});
}
OnSearchSum(value: any, setupFirst: boolean): void {
const uri = '/api/web/accountingSum';
let request = {
token: value
}
this.generalService.postRequest(uri, request).subscribe({
next: (result: any) => {
if (result.code === '200') {
this.generalService.trowApi(result);
this.myActSumData = result.data
this.ActSumDataGradient = this.buildExpenseGradient();
}else{
this.generalService.trowApi(result);
}
},
error: (error: any) => {
this.generalService.trowApi(error);
},
complete: () => {
}
});
}
get expenseGradient(): string {
let current = 0;
const segments = this.expenseBreakdown
@@ -115,6 +202,24 @@ export class MainReportComponent {
return `conic-gradient(${segments})`;
}
private buildExpenseGradient(): string {
if (!this.myActSumData?.pie?.expense?.length) return '';
let current = 0;
const segments = this.myActSumData.pie.expense
.map(part => {
const start = current;
const percent = parseFloat(part.percent); // แปลงจาก string → number
const end = current + percent;
current = end;
return `${part.color} ${start}% ${end}%`;
})
.join(', ');
return `conic-gradient(${segments})`;
}
get formattedRecords() {
return this.ledgerRecords.map(record => ({
...record,
@@ -123,6 +228,10 @@ export class MainReportComponent {
}));
}
printReport(): void {
window.print();
}
openPreview(): void {
this.printPreviewOpen = true;
}

View File

@@ -59,6 +59,8 @@ export interface QuickRatio {
value: string | number;
colorClass: string; // ตัวเก็บชื่อ class สี
}
// export
// ข้อมูลสินค้าหลัก
// export interface IProduct {
// id: string;