forked from ttc/micro-frontend
first
This commit is contained in:
@@ -0,0 +1,629 @@
|
||||
:host {
|
||||
display: block;
|
||||
padding: 2rem clamp(1.25rem, 4vw, 3rem) 3rem;
|
||||
background: radial-gradient(120% 120% at 0% 0%, #f6f8ff 0%, #eef5ff 55%, #ffffff 100%);
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dashboard__hero {
|
||||
background: #0f172a;
|
||||
color: #f8fafc;
|
||||
padding: 2rem;
|
||||
border-radius: 28px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.3);
|
||||
}
|
||||
|
||||
.hero__text h1 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: clamp(1.8rem, 3vw, 2.5rem);
|
||||
}
|
||||
|
||||
.hero__subtitle {
|
||||
margin: 0;
|
||||
color: rgba(248, 250, 252, 0.7);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
font-size: 0.8rem;
|
||||
color: rgba(248, 250, 252, 0.8);
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.hero__actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 0.65rem 1.5rem;
|
||||
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.35);
|
||||
}
|
||||
|
||||
.btn--ghost {
|
||||
background: rgba(248, 250, 252, 0.12);
|
||||
color: #f8fafc;
|
||||
border: 1px solid rgba(248, 250, 252, 0.2);
|
||||
}
|
||||
|
||||
.btn--compact {
|
||||
padding: 0.45rem 1.15rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn:focus-visible {
|
||||
outline: 3px solid rgba(14, 165, 233, 0.4);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dashboard__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dashboard__periods {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.period-card {
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
color: #f8fafc;
|
||||
border-radius: 22px;
|
||||
padding: 1.25rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
box-shadow: 0 18px 35px rgba(15, 23, 42, 0.4);
|
||||
}
|
||||
|
||||
.period-card__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
color: rgba(248, 250, 252, 0.75);
|
||||
}
|
||||
|
||||
.period-card__badge {
|
||||
padding: 0.2rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.period-card__badge--year { background: rgba(248, 250, 252, 0.14); }
|
||||
.period-card__badge--month { background: rgba(125, 211, 252, 0.25); }
|
||||
.period-card__badge--week { background: rgba(110, 231, 183, 0.2); }
|
||||
|
||||
.period-card__values {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
color: rgba(248, 250, 252, 0.65);
|
||||
}
|
||||
|
||||
.income,
|
||||
.expense,
|
||||
.net {
|
||||
margin: 0.1rem 0 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.income { color: #34d399; }
|
||||
.expense { color: #fbbf24; }
|
||||
.net { color: #38bdf8; }
|
||||
|
||||
.trend-chip {
|
||||
background: rgba(248, 250, 252, 0.12);
|
||||
padding: 0.35rem 0.9rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #ffffff;
|
||||
border-radius: 20px;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
box-shadow: 0 8px 30px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.stat-card__icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
flex-shrink: 0;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.accent-mint { background: linear-gradient(135deg, #a7f3d0, #34d399); }
|
||||
.accent-lavender { background: linear-gradient(135deg, #ddd6fe, #a78bfa); }
|
||||
.accent-amber { background: linear-gradient(135deg, #fde68a, #fbbf24); }
|
||||
.accent-teal { background: linear-gradient(135deg, #99f6e4, #14b8a6); }
|
||||
|
||||
.stat-card__label {
|
||||
margin: 0;
|
||||
color: #475569;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.stat-card__value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.stat-card__trend {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.dashboard__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #ffffff;
|
||||
border-radius: 24px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 12px 35px rgba(15, 23, 42, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.panel--main {
|
||||
grid-column: span 2;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.panel--side {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.panel--main {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
.panel__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.panel__header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.panel__header p {
|
||||
margin: 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.ledger-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.quick-log__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.quick-log__form label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.9rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.quick-log__form input,
|
||||
.quick-log__form textarea {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.quick-log__form input:focus,
|
||||
.quick-log__form textarea:focus {
|
||||
border-color: #0ea5e9;
|
||||
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.15);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.quick-log__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.quick-log__toggle {
|
||||
display: inline-flex;
|
||||
gap: 0.4rem;
|
||||
background: #f1f5f9;
|
||||
border-radius: 999px;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 999px;
|
||||
padding: 0.4rem 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-btn.is-active {
|
||||
background: #0ea5e9;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.ledger-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.pie-panel__content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) 1fr;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
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: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.pie-chart__center p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.pie-chart__center strong {
|
||||
font-size: 1.2rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.pie-legend {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.pie-legend__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.4rem 0;
|
||||
}
|
||||
|
||||
.swatch {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pie-legend__label {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.pie-legend__value {
|
||||
margin: 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.ledger-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 0.8fr 1.2fr;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0.4rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.ledger-head {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.08em;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.ledger-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.pill {
|
||||
padding: 0.2rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pill--income {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.pill--expense {
|
||||
background: rgba(248, 113, 113, 0.15);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.ledger-title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ledger-date {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.ledger-category {
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.ledger-amount {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ledger-note {
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.trend-chart {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-end;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.trend-chart__bar {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.trend-chart__value {
|
||||
width: 100%;
|
||||
border-radius: 16px 16px 6px 6px;
|
||||
background: linear-gradient(180deg, rgba(14, 165, 233, 0.8) 0%, rgba(56, 189, 248, 0.4) 100%);
|
||||
}
|
||||
|
||||
.trend-chart__label {
|
||||
font-size: 0.85rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.ratio-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.ratio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 18px;
|
||||
padding: 0.85rem 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ratio--positive {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.ratio--neutral {
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.ratio--warning {
|
||||
background: rgba(251, 191, 36, 0.15);
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.alerts-panel .alert {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f8fafc;
|
||||
border-radius: 18px;
|
||||
padding: 1rem 1.2rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.alert__title {
|
||||
margin: 0 0 0.3rem;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.alert__detail {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.alert__tag {
|
||||
padding: 0.4rem 0.9rem;
|
||||
border-radius: 999px;
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.task {
|
||||
background: #f8fafc;
|
||||
border-radius: 18px;
|
||||
padding: 1rem 1.2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.task__title {
|
||||
margin: 0 0 0.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task__due {
|
||||
margin: 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.task__badge {
|
||||
padding: 0.35rem 0.8rem;
|
||||
border-radius: 12px;
|
||||
background: #e2e8f0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.is-credit {
|
||||
color: #10b981;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.is-debit {
|
||||
color: #ef4444;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.dashboard__hero,
|
||||
.panel {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.quick-log__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pie-panel__content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.ledger-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.ledger-row span:nth-child(3),
|
||||
.ledger-row span:nth-child(4) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
<section class="dashboard">
|
||||
<header class="dashboard__hero">
|
||||
<div class="hero__text">
|
||||
<p class="eyebrow">ภาพรวมบัญชี</p>
|
||||
<h1>ยินดีต้อนรับกลับ, {{ ownerName }}</h1>
|
||||
<p class="hero__subtitle">
|
||||
จดบันทึกรายรับรายจ่าย และดูสรุปต่อปี เดือน สัปดาห์ ได้ในหน้าเดียว
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero__actions">
|
||||
<button class="btn btn--primary">สร้างรายงานด่วน</button>
|
||||
<!-- <button class="btn btn--ghost">อัปโหลดใบเสร็จ</button> -->
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="dashboard__periods">
|
||||
<article class="period-card" *ngFor="let summary of periodSummaries">
|
||||
<header class="period-card__header">
|
||||
<span class="period-card__badge" [ngClass]="'period-card__badge--' + summary.badge">
|
||||
{{ summary.label }}
|
||||
</span>
|
||||
<p>{{ summary.note }}</p>
|
||||
</header>
|
||||
<div class="period-card__values">
|
||||
<div>
|
||||
<p class="muted">รายรับ</p>
|
||||
<p class="income">{{ summary.income }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="muted">รายจ่าย</p>
|
||||
<p class="expense">{{ summary.expense }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="muted">คงเหลือสุทธิ</p>
|
||||
<p class="net">{{ summary.net }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<span class="trend-chip">แนวโน้ม {{ summary.trend }}</span>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="dashboard__stats">
|
||||
<article class="stat-card" *ngFor="let card of kpiCards">
|
||||
<div class="stat-card__icon" [ngClass]="'accent-' + card.accent"></div>
|
||||
<div class="stat-card__body">
|
||||
<p class="stat-card__label">{{ card.label }}</p>
|
||||
<div class="stat-card__value">{{ card.value }}</div>
|
||||
<p class="stat-card__trend">{{ card.trend }} · {{ card.context }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="ledger-grid">
|
||||
<article class="panel quick-log">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>บันทึกรายการแบบรวดเร็ว</h2>
|
||||
<p>จดรายรับรายจ่ายภายในไม่กี่คลิก</p>
|
||||
</div>
|
||||
</div>
|
||||
<form class="quick-log__form">
|
||||
<label>
|
||||
<span>ประเภท</span>
|
||||
<div class="quick-log__toggle">
|
||||
<button type="button" class="toggle-btn" [ngClass]="{ 'is-active': mode == 'i' }" (click)="mode = 'i'">รายรับ</button>
|
||||
<button type="button" class="toggle-btn" [ngClass]="{ 'is-active': mode == 'e' }" (click)="mode = 'e'">รายจ่าย</button>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<span>วันที่</span>
|
||||
<!-- <input type="text" disabled placeholder="10/04/2025 เวลา 12:00"/> -->
|
||||
|
||||
<input type="datetime-local"/>
|
||||
</label>
|
||||
<div class="quick-log__grid">
|
||||
<label>
|
||||
<span>หมวดหมู่</span>
|
||||
<input type="text" placeholder="เลือกหมวดหมู่" />
|
||||
</label>
|
||||
<label>
|
||||
<span>ยอดเงิน (฿)</span>
|
||||
<input type="number" placeholder="0.00" />
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<span>บันทึกเพิ่มเติม</span>
|
||||
<textarea rows="3" placeholder="รายละเอียดการรับ/จ่าย"></textarea>
|
||||
</label>
|
||||
<button type="button" class="btn btn--primary">บันทึกรายการ</button>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article class="panel ledger-panel">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>สมุดบันทึกล่าสุด</h2>
|
||||
<p>แยกสีระหว่างรายรับและรายจ่าย</p>
|
||||
</div>
|
||||
<button class="btn btn--ghost btn--compact">ดูทั้งหมด</button>
|
||||
</div>
|
||||
<div class="ledger-table">
|
||||
<div class="ledger-row ledger-head">
|
||||
<span>รายการ</span>
|
||||
<span>หมวดหมู่</span>
|
||||
<span>ยอดเงิน</span>
|
||||
<span>บันทึก</span>
|
||||
</div>
|
||||
<div class="ledger-row" *ngFor="let idx of ledgerEntries; let i = index;">
|
||||
<div class="ledger-main">
|
||||
<span class="pill" [ngClass]="idx.type == 'i' ? 'pill--income' : 'pill--expense'">
|
||||
{{ idx.type == 'i' ? 'รับ' : 'จ่าย' }}
|
||||
</span>
|
||||
<div>
|
||||
<p class="ledger-title">{{ idx.title }}</p>
|
||||
<p class="ledger-date">{{ idx.date }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ledger-category">{{ idx.category }}</span>
|
||||
<span class="ledger-amount" [ngClass]="idx.type == 'i' ? 'is-credit' : 'is-debit'">
|
||||
{{ idx.amount }}
|
||||
</span>
|
||||
<span class="ledger-note">{{ idx.note }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="dashboard__grid">
|
||||
<!-- <article class="panel panel--main">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>แนวโน้มรายรับ</h2>
|
||||
<p>สรุป 6 เดือนล่าสุด</p>
|
||||
</div>
|
||||
<button class="btn btn--ghost btn--compact">ดาวน์โหลดข้อมูล</button>
|
||||
</div>
|
||||
<div class="trend-chart">
|
||||
<div class="trend-chart__bar" *ngFor="let point of revenueTrend">
|
||||
<span class="trend-chart__value" [style.height.%]="point.value"></span>
|
||||
<span class="trend-chart__label">{{ point.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article> -->
|
||||
|
||||
<article class="panel panel--main pie-panel">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>สัดส่วนค่าใช้จ่าย</h2>
|
||||
<p>ดูหมวดไหนใช้เงินมากที่สุด</p>
|
||||
</div>
|
||||
<button class="btn btn--ghost btn--compact">จัดการหมวดหมู่</button>
|
||||
</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 class="pie-legend__item" *ngFor="let part of expenseBreakdown">
|
||||
<span class="swatch" [style.background]="part.color"></span>
|
||||
<div>
|
||||
<p class="pie-legend__label">{{ part.label }}</p>
|
||||
<p class="pie-legend__value">{{ part.value }}%</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
<!-- ตัวเลขซ้อนทับกัน -->
|
||||
<article class="panel panel--side">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>สรุปสภาพคล่อง</h2>
|
||||
<p>อัปเดตล่าสุด 5 นาทีที่แล้ว</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ratio-list">
|
||||
<div class="ratio" *ngFor="let ratio of quickRatios" [ngClass]="'ratio--' + ratio.status">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:0.5rem;">
|
||||
<p style="margin:0;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
{{ ratio.label }}
|
||||
</p>
|
||||
<span style="margin-left:0.5rem;flex:0 0 auto">{{ ratio.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel alerts-panel">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>การแจ้งเตือนสำคัญ</h2>
|
||||
<p>จัดลำดับงานค้างก่อนครบกำหนด</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert" *ngFor="let alert of alerts">
|
||||
<div>
|
||||
<p class="alert__title">{{ alert.title }}</p>
|
||||
<p class="alert__detail">{{ alert.detail }}</p>
|
||||
</div>
|
||||
<span class="alert__tag">{{ alert.tag }}</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel tasks-panel">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>รายการยอดค้างจ่าย</h2>
|
||||
<p>ช่วยเตือนความจำให้</p>
|
||||
</div>
|
||||
<button class="btn btn--primary btn--compact" (click)="isModalOpen = true">เพิ่มงาน</button>
|
||||
</div>
|
||||
<ul class="task-list">
|
||||
<li class="task" *ngFor="let task of tasks">
|
||||
<div>
|
||||
<p class="task__title">{{ task.title }}</p>
|
||||
<p class="task__due">{{ task.due }}</p>
|
||||
</div>
|
||||
<span class="task__badge">{{ task.priority }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
@if(isModalOpen == true){
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4 backdrop-blur-sm transition-all duration-300 ease-in-out" role="dialog" aria-modal="true" [formGroup]="arrearsForm">
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-auto overflow-hidden transform scale-100 transition-all duration-300 ease-out">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="flex items-center justify-between gap-4 px-6 py-5 border-b bg-linear-to-r from-rose-50 to-white">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-6 h-6 text-rose-600" viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M12 2v6M6 12h12M4 20h16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 m-0">เพิ่มยอดค้างชำระ</h2>
|
||||
<p class="text-sm text-gray-500 m-0">บันทึกยอดที่ยังค้างชำระเพื่อการติดตาม</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" (click)="isModalOpen = false" class="text-gray-400 hover:text-rose-600 p-2 rounded-md transition-colors duration-200" aria-label="ปิด">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M6 6l12 12M6 18L18 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Form -->
|
||||
<form class="px-6 py-6 bg-white" (ngSubmit)="onArrearsSubmit()" autocomplete="off" novalidate>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
|
||||
<!-- จำนวนเงิน -->
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-gray-700">จำนวนเงิน (฿)</span>
|
||||
<div class="mt-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
id="amount"
|
||||
formControlName="amount"
|
||||
placeholder="0.00"
|
||||
class="w-full px-4 py-2 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-rose-400 focus:border-rose-500 transition-all"
|
||||
/>
|
||||
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-gray-500">THB</span>
|
||||
</div>
|
||||
@if(arrearsForm.get('amount')?.touched && arrearsForm.get('amount')?.invalid) {
|
||||
<p class="mt-1 text-xs text-red-600">
|
||||
กรุณากรอกจำนวนเงินที่ถูกต้อง
|
||||
</p>
|
||||
}
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-gray-700">วันครบกำหนกจ่าย</span>
|
||||
<div class="mt-1 relative">
|
||||
<input type="datetime-local" formControlName="expdtm" class=" w-full px-4 py-2 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-rose-400 focus:border-rose-500 transition-all"/>
|
||||
</div>
|
||||
@if(arrearsForm.get('expdtm')?.touched && arrearsForm.get('expdtm')?.invalid) {
|
||||
<p class="mt-1 text-xs text-red-600">
|
||||
กรุณาระบุวันครบกำหนดชำระ
|
||||
</p>
|
||||
}
|
||||
</label>
|
||||
|
||||
<!-- เหตุผล -->
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-gray-700">เหตุผล</span>
|
||||
<input
|
||||
type="text"
|
||||
id="reason"
|
||||
formControlName="reason"
|
||||
placeholder="เช่น บิลค้างชำระจากผู้ขาย"
|
||||
class="mt-1 w-full px-4 py-2 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-rose-400 focus:border-rose-500 transition-all"
|
||||
/>
|
||||
@if(arrearsForm.get('reason')?.touched && arrearsForm.get('reason')?.invalid) {
|
||||
<p class="mt-1 text-xs text-red-600">
|
||||
กรุณากรอกเหตุผล
|
||||
</p>
|
||||
}
|
||||
</label>
|
||||
|
||||
<!-- บันทึกเพิ่มเติม -->
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-gray-700">บันทึกเพิ่มเติม (ไม่บังคับ)</span>
|
||||
<textarea
|
||||
rows="3"
|
||||
formControlName="note"
|
||||
placeholder="รายละเอียดเพิ่มเติม (เช่น เลขใบแจ้งหนี้ หรือ ผู้ติดต่อ)"
|
||||
class="mt-1 w-full px-4 py-2 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-rose-400 focus:border-rose-500 resize-none transition-all"
|
||||
></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="flex items-center justify-end gap-3 pt-4 border-t mt-4">
|
||||
<button type="button" (click)="isModalOpen = false" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-xl hover:bg-gray-300 transition-colors duration-200">
|
||||
ยกเลิก
|
||||
</button>
|
||||
<button type="submit" class="rounded-2xl">
|
||||
บันทึก
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||
import { GeneralService } from '../../services/generalservice';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main-dashboard',
|
||||
standalone: false,
|
||||
templateUrl: './main-dashboard.component.html',
|
||||
styleUrl: './main-dashboard.component.css'
|
||||
})
|
||||
export class MainDashboardComponent implements OnInit {
|
||||
|
||||
mode: string = 'i';
|
||||
isModalOpen: boolean = false;
|
||||
isSubmitting: boolean = false;
|
||||
arrearsForm!: FormGroup;
|
||||
saveFrm!: FormGroup;
|
||||
|
||||
|
||||
readonly ownerName = 'Nuttakit';
|
||||
|
||||
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 },
|
||||
{ label: 'มี.ค.', value: 73 },
|
||||
{ label: 'เม.ย.', value: 68 },
|
||||
{ label: 'พ.ค.', value: 82 },
|
||||
{ label: 'มิ.ย.', value: 77 }
|
||||
];
|
||||
|
||||
readonly quickRatios = [
|
||||
{ label: 'กระแสเงินสด', value: '+฿312K', status: 'positive' },
|
||||
{ label: 'วงเงินคงเหลือ', value: '฿890K', status: 'neutral' },
|
||||
{ label: 'ค่าใช้จ่ายเดือนนี้', value: '฿412K', status: 'warning' }
|
||||
];
|
||||
|
||||
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 = [
|
||||
{
|
||||
type: 'i',
|
||||
title: 'ค่าบริการที่ปรึกษา',
|
||||
category: 'บริการ',
|
||||
amount: '+฿85,000',
|
||||
date: 'วันนี้ · 10:15',
|
||||
note: 'โครงการ Warehouse Automation'
|
||||
},
|
||||
{
|
||||
type: 'e',
|
||||
title: 'ค่าเช่าออฟฟิศ',
|
||||
category: 'ค่าใช้จ่ายคงที่',
|
||||
amount: '-฿48,000',
|
||||
date: 'วันนี้ · 09:00',
|
||||
note: 'สำนักงานพระราม 9'
|
||||
},
|
||||
{
|
||||
type: 'i',
|
||||
title: 'รับเงินมัดจำ',
|
||||
category: 'สัญญาใหม่',
|
||||
amount: '+฿120,000',
|
||||
date: 'เมื่อวาน',
|
||||
note: 'ลูกค้า Urbane CoWorking'
|
||||
},
|
||||
{
|
||||
type: 'e',
|
||||
title: 'ค่าวัตถุดิบ',
|
||||
category: 'ต้นทุนโครงการ',
|
||||
amount: '-฿27,500',
|
||||
date: '12 มิ.ย.',
|
||||
note: 'สั่งผ่าน Blue Supply'
|
||||
}
|
||||
];
|
||||
|
||||
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' }
|
||||
];
|
||||
|
||||
readonly expenseGradient = this.buildExpenseGradient();
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setupFormControl();
|
||||
}
|
||||
|
||||
setupFormControl(){
|
||||
this.arrearsForm = new FormGroup({
|
||||
// email: new FormControl('',[Validators.required, Validators.email, Validators.maxLength(100)]),
|
||||
amount: new FormControl('',[Validators.required, Validators.maxLength(20)]),
|
||||
expdtm: new FormControl('',[Validators.required, Validators.maxLength(12)]),
|
||||
note: new FormControl('',[Validators.maxLength(200)]),
|
||||
reason: new FormControl('',[Validators.required, Validators.maxLength(200)])
|
||||
});
|
||||
|
||||
this.saveFrm = new FormGroup({
|
||||
actacpdtm: new FormControl('',[Validators.required, Validators.maxLength(12)]),
|
||||
actqty: new FormControl('',[Validators.required]),
|
||||
actcat: new FormControl('',[Validators.required, Validators.maxLength(1)]),
|
||||
actcmt: new FormControl('',[Validators.maxLength(200)])
|
||||
});
|
||||
}
|
||||
|
||||
onSaveSubmit(){
|
||||
|
||||
}
|
||||
|
||||
onArrearsSubmit(){
|
||||
|
||||
}
|
||||
|
||||
private buildExpenseGradient(): string {
|
||||
let current = 0;
|
||||
const segments = this.expenseBreakdown
|
||||
.map(part => {
|
||||
const start = current;
|
||||
const end = current + part.value;
|
||||
current = end;
|
||||
return `${part.color} ${start}% ${end}%`;
|
||||
})
|
||||
.join(', ');
|
||||
return `conic-gradient(${segments})`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user