diff --git a/accounting-ng-nuttakit/src/app/app-routing.module.ts b/accounting-ng-nuttakit/src/app/app-routing.module.ts index dd8c2a1..d42211b 100644 --- a/accounting-ng-nuttakit/src/app/app-routing.module.ts +++ b/accounting-ng-nuttakit/src/app/app-routing.module.ts @@ -22,6 +22,13 @@ const routes: Routes = [ (m) => m.MainControlModule ), }, + { + path: 'report', + loadChildren: () => + import('./controls/report-control/report-control.module').then( + (m) => m.ReportControlModule + ), + }, ], }, diff --git a/accounting-ng-nuttakit/src/app/component/main-report/main-report.component.css b/accounting-ng-nuttakit/src/app/component/main-report/main-report.component.css new file mode 100644 index 0000000..9a96ffa --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/main-report/main-report.component.css @@ -0,0 +1,441 @@ +:host { + display: block; + padding: 2rem clamp(1rem, 4vw, 3rem); + background: #f8fafc; + min-height: 100%; +} + +.report { + max-width: 1280px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.report__header { + display: flex; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + align-items: flex-end; +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.12em; + font-size: 0.8rem; + color: #94a3b8; + margin: 0 0 0.25rem; +} + +.report__header h1 { + margin: 0 0 0.25rem; + font-size: clamp(1.8rem, 4vw, 2.4rem); + color: #0f172a; +} + +.muted { + margin: 0; + color: #94a3b8; + font-size: 0.95rem; +} + +.report__actions { + display: inline-flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.btn { + border: none; + border-radius: 999px; + padding: 0.65rem 1.4rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.btn--primary { + background: linear-gradient(135deg, #22d3ee, #0ea5e9); + color: #0f172a; + box-shadow: 0 15px 30px rgba(14, 165, 233, 0.25); +} + +.btn--ghost { + background: #fff; + color: #0f172a; + border: 1px solid #cbd5f5; +} + +.btn--compact { + padding: 0.45rem 1.1rem; + font-size: 0.9rem; +} + +.btn:hover { + transform: translateY(-1px); +} + +.summary-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1rem; +} + +.summary-card { + position: relative; + background: #fff; + border-radius: 20px; + padding: 1.25rem; + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08); +} + +.summary-card__label { + margin: 0; + color: #64748b; + font-size: 0.9rem; +} + +.summary-card h2 { + margin: 0.4rem 0; + font-size: 1.6rem; + color: #0f172a; +} + +.summary-card__detail { + margin: 0; + color: #94a3b8; + font-size: 0.9rem; +} + +.summary-card__tone { + position: absolute; + inset: 0; + border-radius: 20px; + pointer-events: none; + opacity: 0.15; +} + +.tone-mint { background: linear-gradient(135deg, #a7f3d0, #34d399); } +.tone-amber { background: linear-gradient(135deg, #fde68a, #fbbf24); } +.tone-indigo { background: linear-gradient(135deg, #c4b5fd, #818cf8); } +.tone-slate { background: linear-gradient(135deg, #cbd5f5, #94a3b8); } + +.report__content { + display: grid; + grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr); + gap: 1.5rem; +} + +.panel { + background: #fff; + border-radius: 24px; + padding: 1.5rem; + box-shadow: 0 15px 45px rgba(15, 23, 42, 0.08); + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.panel__header { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; +} + +.panel__header h2 { + margin: 0; +} + +.panel__header p { + margin: 0; + color: #94a3b8; + font-size: 0.9rem; +} + +.table { + border: 1px solid #e2e8f0; + border-radius: 18px; + overflow: hidden; +} + +.table__head, +.table__row { + display: grid; + grid-template-columns: 1.2fr 1fr 1.6fr 1fr 0.8fr; + padding: 0.85rem 1rem; + gap: 1rem; + align-items: center; +} + +.table__head { + background: #f1f5f9; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #64748b; +} + +.table__row:nth-child(even) { + background: rgba(15, 23, 42, 0.015); +} + +.table__row strong { + display: block; +} + +.table__row small { + display: block; + font-size: 0.85rem; +} + +.mono { + font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; + font-size: 0.9rem; +} + +.amount-col { + text-align: right; + font-weight: 600; +} + +.income { + color: #16a34a; +} + +.expense { + color: #dc2626; +} + +.pie-panel__content { + display: flex; + gap: 1.5rem; + 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%; + background: #fff; + 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); +} + +.pie-chart__center p { + margin: 0; + color: #94a3b8; + font-size: 0.85rem; +} + +.pie-chart__center strong { + color: #0f172a; +} + +.pie-legend { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.pie-legend li { + display: flex; + align-items: center; + gap: 0.6rem; +} + +.swatch { + width: 14px; + height: 14px; + border-radius: 4px; +} + +.legend-label { + margin: 0; + font-weight: 600; +} + +.legend-value { + margin: 0; + color: #94a3b8; + font-size: 0.85rem; +} + +.preview-modal { + position: fixed; + inset: 0; + z-index: 120; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.preview-modal__backdrop { + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.55); + backdrop-filter: blur(4px); +} + +.preview-modal__content { + position: relative; + background: #fff; + border-radius: 24px; + padding: 1.5rem; + width: min(1100px, 100%); + max-height: 90vh; + overflow: auto; + box-shadow: 0 25px 60px rgba(15, 23, 42, 0.35); +} + +.preview-modal__header { + display: flex; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +.preview-modal__actions { + display: inline-flex; + gap: 0.5rem; +} + +.preview-sheet { + margin-top: 1.5rem; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 16px; + padding: 1.5rem; + font-size: 0.95rem; +} + +.preview-sheet__header { + display: flex; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +.preview-totals { + display: flex; + gap: 1.5rem; + flex-wrap: wrap; +} + +.preview-totals p { + margin: 0; + color: #94a3b8; + font-size: 0.85rem; +} + +.preview-totals strong { + display: block; + color: #0f172a; +} + +.preview-pie { + margin: 1.5rem 0; + display: flex; + gap: 1.5rem; + align-items: center; +} + +.mini-pie { + width: 140px; + height: 140px; + border-radius: 50%; + box-shadow: inset 0 0 20px rgba(15, 23, 42, 0.08); +} + +.preview-pie ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.preview-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +.preview-table th, +.preview-table td { + padding: 0.65rem 0.75rem; + border: 1px solid #e2e8f0; + text-align: left; +} + +.preview-table th { + background: #f1f5f9; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.08em; + color: #64748b; +} + +.preview-table td:last-child { + text-align: right; + font-weight: 600; +} + +@media (max-width: 900px) { + .report__content { + grid-template-columns: 1fr; + } + + .table__head, + .table__row { + grid-template-columns: repeat(2, minmax(0, 1fr)); + text-align: left; + } + + .amount-col { + text-align: left; + } + + .pie-panel__content { + flex-direction: column; + } +} + +@media (max-width: 640px) { + :host { + padding: 1.5rem 1rem 2rem; + } + + .report__actions { + width: 100%; + justify-content: flex-start; + } + + .preview-modal { + padding: 1rem; + } +} diff --git a/accounting-ng-nuttakit/src/app/component/main-report/main-report.component.html b/accounting-ng-nuttakit/src/app/component/main-report/main-report.component.html new file mode 100644 index 0000000..99bb809 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/main-report/main-report.component.html @@ -0,0 +1,142 @@ +
+
+
+

สรุปรายงาน

+

รายงานรายรับรายจ่าย

+

ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}

+
+
+ + +
+
+ +
+
+

{{ card.label }}

+

{{ card.value }}

+

{{ card.detail }}

+ +
+
+ +
+
+
+
+

สมุดรายวัน

+

บันทึกรายรับรายจ่ายทั้งหมดในช่วงเวลา

+
+ +
+
+
+ วันที่ + เลขที่เอกสาร + หัวข้อ + หมวดหมู่ + ยอดเงิน +
+
+ {{ record.date }} + {{ record.doc }} + + {{ record.topic }} + {{ record.type === 'income' ? 'รายรับ' : 'รายจ่าย' }} + + {{ record.category }} + {{ record.displayAmount }} +
+
+
+ +
+
+
+

สัดส่วนค่าใช้จ่าย

+

เปรียบเทียบหมวดหลักของรายจ่ายเดือนนี้

+
+
+
+
+
+

รวมรายจ่าย

+ ฿732K +
+
+
    +
  • + +
    +

    {{ part.label }}

    +

    {{ part.value }}%

    +
    +
  • +
+
+
+
+
+ +
+
+
+
+
+

Print Preview

+

รายงานรายรับรายจ่าย

+

ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}

+
+
+ + +
+
+ +
+
+
+

Accounting Summary

+

Prepared on {{ reportRange.end }}

+
+
+
+

{{ total.label }}

+ {{ total.value }} +
+
+
+ +
+
+
    +
  • + + {{ part.label }} · {{ part.value }}% +
  • +
+
+ + + + + + + + + + + + + + + + + + + + +
วันที่เลขที่หัวข้อหมวดหมู่ยอดเงิน
{{ record.date }}{{ record.doc }}{{ record.topic }}{{ record.category }}{{ record.displayAmount }}
+
+
+
diff --git a/accounting-ng-nuttakit/src/app/component/main-report/main-report.component.ts b/accounting-ng-nuttakit/src/app/component/main-report/main-report.component.ts new file mode 100644 index 0000000..1ca16fc --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/main-report/main-report.component.ts @@ -0,0 +1,138 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-main-report', + templateUrl: './main-report.component.html', + standalone: false, + styleUrls: ['./main-report.component.css'] +}) +export class MainReportComponent { + readonly reportRange = { + start: '1 มิถุนายน 2567', + end: '30 มิถุนายน 2567' + }; + + readonly summaryCards = [ + { label: 'รายรับรวม', value: '฿1,284,500', detail: '+12.4% MoM', tone: 'mint' }, + { label: 'รายจ่ายรวม', value: '฿732,800', detail: '-4.1% MoM', tone: 'amber' }, + { label: 'กำไรสุทธิ', value: '฿551,700', detail: 'Margin 42.9%', tone: 'indigo' }, + { label: 'บันทึกรายการ', value: '86 รายการ', detail: '32 รายรับ · 54 รายจ่าย', tone: 'slate' } + ]; + + readonly ledgerRecords = [ + { + date: '01 มิ.ย. 2567', + doc: 'RCPT-9101', + type: 'income', + topic: 'ค่าบริการที่ปรึกษา', + category: 'บริการ', + amount: 145000 + }, + { + date: '02 มิ.ย. 2567', + doc: 'EXP-4407', + type: 'expense', + topic: 'ค่าวัสดุโครงการ A', + category: 'ต้นทุนโครงการ', + amount: -38900 + }, + { + date: '06 มิ.ย. 2567', + doc: 'RCPT-9110', + type: 'income', + topic: 'รับเงินมัดจำโครงการ', + category: 'สัญญาใหม่', + amount: 220000 + }, + { + date: '09 มิ.ย. 2567', + doc: 'EXP-4412', + type: 'expense', + topic: 'เงินเดือนพนักงาน', + category: 'บุคลากร', + amount: -180000 + }, + { + date: '12 มิ.ย. 2567', + doc: 'EXP-4416', + type: 'expense', + topic: 'ค่าเช่าออฟฟิศ', + category: 'ค่าใช้จ่ายคงที่', + amount: -48000 + }, + { + date: '19 มิ.ย. 2567', + doc: 'RCPT-9122', + type: 'income', + topic: 'ค่าสัญญาบริการรายปี', + category: 'บริการ', + amount: 325000 + }, + { + date: '23 มิ.ย. 2567', + doc: 'EXP-4425', + type: 'expense', + topic: 'ค่าโฆษณาออนไลน์', + category: 'การตลาด', + amount: -72000 + }, + { + date: '28 มิ.ย. 2567', + doc: 'RCPT-9133', + type: 'income', + topic: 'รายรับจากคู่ค้าใหม่', + category: 'พันธมิตร', + amount: 210500 + } + ]; + + readonly expenseBreakdown = [ + { label: 'ต้นทุนโครงการ', value: 34, color: '#10b981' }, + { label: 'บุคลากร', value: 26, color: '#6366f1' }, + { label: 'การตลาด', value: 18, color: '#f97316' }, + { label: 'ค่าใช้จ่ายคงที่', value: 14, color: '#0ea5e9' }, + { label: 'อื่นๆ', value: 8, color: '#e11d48' } + ]; + + readonly previewTotals = [ + { label: 'รายรับรวม', value: '฿1,284,500' }, + { label: 'รายจ่ายรวม', value: '฿732,800' }, + { label: 'กำไรสุทธิ', value: '฿551,700' } + ]; + + printPreviewOpen = false; + + get expenseGradient(): string { + let current = 0; + const segments = this.expenseBreakdown + .map(slice => { + const start = current; + const end = current + slice.value; + current = end; + return `${slice.color} ${start}% ${end}%`; + }) + .join(', '); + return `conic-gradient(${segments})`; + } + + get formattedRecords() { + return this.ledgerRecords.map(record => ({ + ...record, + displayAmount: this.formatCurrency(record.amount), + tone: record.type === 'income' ? 'income' : 'expense' + })); + } + + openPreview(): void { + this.printPreviewOpen = true; + } + + closePreview(): void { + this.printPreviewOpen = false; + } + + private formatCurrency(amount: number): string { + const formatter = new Intl.NumberFormat('th-TH', { style: 'currency', currency: 'THB', maximumFractionDigits: 0 }); + return formatter.format(amount); + } +} diff --git a/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.ts b/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.ts index 5918822..9bc734e 100644 --- a/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.ts +++ b/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.ts @@ -71,7 +71,7 @@ export class LoginContentComponent implements OnInit { if (this.loginPageComponent) { this.loginPageComponent.message = errorMessage; } - this.generalService.trowApi(error.error || { message_th: 'เกิดข้อผิดพลาดไม่ทราบสาเหตุ' }); + this.generalService.trowApi(error); } }); } diff --git a/accounting-ng-nuttakit/src/app/content/main-dashboard-content/main-dashboard-content.component.ts b/accounting-ng-nuttakit/src/app/content/main-dashboard-content/main-dashboard-content.component.ts index 3a049ed..7ac22b0 100644 --- a/accounting-ng-nuttakit/src/app/content/main-dashboard-content/main-dashboard-content.component.ts +++ b/accounting-ng-nuttakit/src/app/content/main-dashboard-content/main-dashboard-content.component.ts @@ -54,10 +54,12 @@ export class MainDashboardContentComponent implements OnInit { this.generalService.trowApi(result); this.myActData = result.data; this.dashboardStateService.setStateAccountResult(this.myActData); + }else{ + this.generalService.trowApi(result); } }, error: (error: any) => { - + this.generalService.trowApi(error); }, complete: () => { @@ -78,10 +80,12 @@ export class MainDashboardContentComponent implements OnInit { this.generalService.trowApi(result); this.myDropAct = result.data this.dashboardStateService.setStateResult(this.myDropAct) + }else{ + this.generalService.trowApi(result); } }, error: (error: any) => { - + this.generalService.trowApi(error); }, complete: () => { @@ -100,10 +104,12 @@ export class MainDashboardContentComponent implements OnInit { this.generalService.trowApi(result); this.myActSumData = result.data this.dashboardStateService.setStateSumResult(this.myActSumData); + }else{ + this.generalService.trowApi(result); } }, error: (error: any) => { - + this.generalService.trowApi(error); }, complete: () => { diff --git a/accounting-ng-nuttakit/src/app/controls/report-control/report-control-routing.module.ts b/accounting-ng-nuttakit/src/app/controls/report-control/report-control-routing.module.ts new file mode 100644 index 0000000..20967f4 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/controls/report-control/report-control-routing.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { MainReportComponent } from '../../component/main-report/main-report.component'; + +const routes: Routes = [ + { + path: '', + component: MainReportComponent + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ReportControlRoutingModule { } diff --git a/accounting-ng-nuttakit/src/app/controls/report-control/report-control.module.ts b/accounting-ng-nuttakit/src/app/controls/report-control/report-control.module.ts new file mode 100644 index 0000000..89c6491 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/controls/report-control/report-control.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MainReportComponent } from '../../component/main-report/main-report.component'; +import { ReportControlRoutingModule } from './report-control-routing.module'; +import { ReactiveFormsModule } from '@angular/forms'; + + +@NgModule({ + declarations: [ + MainReportComponent + ], + imports: [ + CommonModule, + ReportControlRoutingModule, + ReactiveFormsModule + ] +}) +export class ReportControlModule { } diff --git a/accounting-ng-nuttakit/src/app/services/generalservice.ts b/accounting-ng-nuttakit/src/app/services/generalservice.ts index 0d5376c..3be2af2 100644 --- a/accounting-ng-nuttakit/src/app/services/generalservice.ts +++ b/accounting-ng-nuttakit/src/app/services/generalservice.ts @@ -54,8 +54,14 @@ export class GeneralService { return this.http.post(fullUrl, payload, this.getHttpOptions()).pipe( map((res: any) => res), catchError((error: any) => { + const response = error?.error; console.error('❌ [POST Request Error]:', error); - return throwError(() => error); + return throwError(() => ({ + status: error.status, + code: response?.code ?? '500', + message: response?.message ?? 'Internal Server Error', + message_th: response?.message_th ?? 'เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์' + })); }) ); } @@ -66,9 +72,14 @@ export class GeneralService { return this.http.get(fullUrl, this.getHttpOptions()).pipe( map((res: any) => res), catchError((error: any) => { + const response = error?.error; console.error('❌ [GET Request Error]:', error); - return throwError(() => error); - }) + return throwError(() => ({ + status: error.status, + code: response?.code ?? '500', + message: response?.message ?? 'Internal Server Error', + message_th: response?.message_th ?? 'เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์' + })); }) ); }