-แก้ไขระบบ trow api และ ยิง
This commit is contained in:
@@ -22,6 +22,13 @@ const routes: Routes = [
|
|||||||
(m) => m.MainControlModule
|
(m) => m.MainControlModule
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'report',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./controls/report-control/report-control.module').then(
|
||||||
|
(m) => m.ReportControlModule
|
||||||
|
),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<section class="report">
|
||||||
|
<header 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--primary" (click)="openPreview()">ปริ้นรายงาน</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<section class="report__content">
|
||||||
|
<article 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">
|
||||||
|
<span>วันที่</span>
|
||||||
|
<span>เลขที่เอกสาร</span>
|
||||||
|
<span>หัวข้อ</span>
|
||||||
|
<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>
|
||||||
|
<span>
|
||||||
|
<strong>{{ record.topic }}</strong>
|
||||||
|
<small class="muted">{{ record.type === 'income' ? 'รายรับ' : 'รายจ่าย' }}</small>
|
||||||
|
</span>
|
||||||
|
<span>{{ record.category }}</span>
|
||||||
|
<span class="amount-col" [ngClass]="record.tone">{{ record.displayAmount }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ export class LoginContentComponent implements OnInit {
|
|||||||
if (this.loginPageComponent) {
|
if (this.loginPageComponent) {
|
||||||
this.loginPageComponent.message = errorMessage;
|
this.loginPageComponent.message = errorMessage;
|
||||||
}
|
}
|
||||||
this.generalService.trowApi(error.error || { message_th: 'เกิดข้อผิดพลาดไม่ทราบสาเหตุ' });
|
this.generalService.trowApi(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,10 +54,12 @@ export class MainDashboardContentComponent implements OnInit {
|
|||||||
this.generalService.trowApi(result);
|
this.generalService.trowApi(result);
|
||||||
this.myActData = result.data;
|
this.myActData = result.data;
|
||||||
this.dashboardStateService.setStateAccountResult(this.myActData);
|
this.dashboardStateService.setStateAccountResult(this.myActData);
|
||||||
|
}else{
|
||||||
|
this.generalService.trowApi(result);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error: any) => {
|
error: (error: any) => {
|
||||||
|
this.generalService.trowApi(error);
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
|
|
||||||
@@ -78,10 +80,12 @@ export class MainDashboardContentComponent implements OnInit {
|
|||||||
this.generalService.trowApi(result);
|
this.generalService.trowApi(result);
|
||||||
this.myDropAct = result.data
|
this.myDropAct = result.data
|
||||||
this.dashboardStateService.setStateResult(this.myDropAct)
|
this.dashboardStateService.setStateResult(this.myDropAct)
|
||||||
|
}else{
|
||||||
|
this.generalService.trowApi(result);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error: any) => {
|
error: (error: any) => {
|
||||||
|
this.generalService.trowApi(error);
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
|
|
||||||
@@ -100,10 +104,12 @@ export class MainDashboardContentComponent implements OnInit {
|
|||||||
this.generalService.trowApi(result);
|
this.generalService.trowApi(result);
|
||||||
this.myActSumData = result.data
|
this.myActSumData = result.data
|
||||||
this.dashboardStateService.setStateSumResult(this.myActSumData);
|
this.dashboardStateService.setStateSumResult(this.myActSumData);
|
||||||
|
}else{
|
||||||
|
this.generalService.trowApi(result);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error: any) => {
|
error: (error: any) => {
|
||||||
|
this.generalService.trowApi(error);
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
|
|
||||||
|
|||||||
@@ -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 { }
|
||||||
@@ -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 { }
|
||||||
@@ -54,8 +54,14 @@ export class GeneralService {
|
|||||||
return this.http.post(fullUrl, payload, this.getHttpOptions()).pipe(
|
return this.http.post(fullUrl, payload, this.getHttpOptions()).pipe(
|
||||||
map((res: any) => res),
|
map((res: any) => res),
|
||||||
catchError((error: any) => {
|
catchError((error: any) => {
|
||||||
|
const response = error?.error;
|
||||||
console.error('❌ [POST Request 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(
|
return this.http.get(fullUrl, this.getHttpOptions()).pipe(
|
||||||
map((res: any) => res),
|
map((res: any) => res),
|
||||||
catchError((error: any) => {
|
catchError((error: any) => {
|
||||||
|
const response = error?.error;
|
||||||
console.error('❌ [GET Request 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 ?? 'เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์'
|
||||||
|
})); })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user