213 lines
8.1 KiB
Plaintext
213 lines
8.1 KiB
Plaintext
@page "/admin/dashboard"
|
|
@attribute [Authorize]
|
|
@using TaxBaik.Web.Services
|
|
@inject IAdminDashboardClient DashboardClient
|
|
@inject NavigationManager Nav
|
|
|
|
<PageTitle>대시보드</PageTitle>
|
|
|
|
<section class="admin-page-hero">
|
|
<div>
|
|
<div class="admin-eyebrow">Overview</div>
|
|
<h1 class="admin-page-title">대시보드</h1>
|
|
<p class="admin-page-subtitle">문의 흐름과 콘텐츠 상태를 한 화면에서 확인합니다.</p>
|
|
</div>
|
|
<button type="button" class="site-button primary" @onclick='() => Nav.NavigateTo("/taxbaik/admin/blog/create")'>새 포스트 작성</button>
|
|
</section>
|
|
|
|
@if (summary is null)
|
|
{
|
|
<div class="admin-metric-grid">
|
|
<Skeleton Count="4" CssClass="taxbaik-skeleton-grid" />
|
|
</div>
|
|
<div class="admin-surface mt-4">
|
|
<Skeleton Count="4" CssClass="taxbaik-skeleton-grid" />
|
|
</div>
|
|
<div class="admin-surface mt-4">
|
|
<Skeleton Count="4" CssClass="taxbaik-skeleton-grid" />
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="admin-metric-grid">
|
|
<div class="admin-metric-card accent-blue cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/inquiries"))'>
|
|
<div class="metric-card-inner">
|
|
<span class="metric-label">이번달 문의</span>
|
|
<div class="metric-value-row">
|
|
<span class="metric-value blue">@summary.ThisMonthInquiries</span>
|
|
<span class="metric-icon">💬</span>
|
|
</div>
|
|
<span class="metric-hint">월간 상담 유입</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-metric-card accent-amber cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/inquiries?status=new"))'>
|
|
<div class="metric-card-inner">
|
|
<span class="metric-label">신규 문의</span>
|
|
<div class="metric-value-row">
|
|
<span class="metric-value amber">@summary.NewInquiries</span>
|
|
<span class="metric-icon">⚠️</span>
|
|
</div>
|
|
<span class="metric-hint">처리 대기</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-metric-card accent-slate cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/blog"))'>
|
|
<div class="metric-card-inner">
|
|
<span class="metric-label">전체 포스트</span>
|
|
<div class="metric-value-row">
|
|
<span class="metric-value slate">@summary.TotalPosts</span>
|
|
<span class="metric-icon">📄</span>
|
|
</div>
|
|
<span class="metric-hint">콘텐츠 자산</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-metric-card accent-green cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/blog"))'>
|
|
<div class="metric-card-inner">
|
|
<span class="metric-label">발행된 포스트</span>
|
|
<div class="metric-value-row">
|
|
<span class="metric-value green">@summary.PublishedPosts</span>
|
|
<span class="metric-icon">🌐</span>
|
|
</div>
|
|
<span class="metric-hint">검색 노출 대상</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (upcomingFilings.Count == 0)
|
|
{
|
|
<div class="admin-surface mt-4">이번 달 마감 임박 신고가 없습니다.</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="admin-surface mt-4">
|
|
<div class="admin-section-header">
|
|
<div>
|
|
<h3 class="admin-section-title">이번 달 마감 임박 신고</h3>
|
|
<p class="muted">30일 이내 신고 예정 건</p>
|
|
</div>
|
|
<a class="site-button secondary" href="/taxbaik/admin/tax-filings">전체 일정 보기</a>
|
|
</div>
|
|
<div class="admin-table-wrap">
|
|
<table class="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>고객</th>
|
|
<th>신고 유형</th>
|
|
<th>기한</th>
|
|
<th>D-day</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var f in upcomingFilings)
|
|
{
|
|
var dday = (f.DueDate.Date - DateTime.Today).Days;
|
|
<tr>
|
|
<td><a href="@($"/taxbaik/admin/clients/{f.ClientId}")">@f.ClientName</a></td>
|
|
<td>@f.FilingType</td>
|
|
<td>@f.DueDate.ToString("yyyy-MM-dd")</td>
|
|
<td>
|
|
@if (dday < 0)
|
|
{
|
|
<span class="status-pill dark">기한 초과 (@(-dday)일)</span>
|
|
}
|
|
else if (dday <= 7)
|
|
{
|
|
<span class="status-pill danger">D-@dday</span>
|
|
}
|
|
else
|
|
{
|
|
<span>D-@dday</span>
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (summary is not null)
|
|
{
|
|
<div class="admin-surface mt-4">
|
|
<div class="admin-section-header">
|
|
<div>
|
|
<h3 class="admin-section-title">최근 문의</h3>
|
|
<p class="muted">최근 유입된 상담 요청을 빠르게 확인합니다.</p>
|
|
</div>
|
|
<a class="site-button secondary" href="/taxbaik/admin/inquiries">문의 전체 보기</a>
|
|
</div>
|
|
<div class="admin-table-wrap">
|
|
<table class="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>이름</th>
|
|
<th>전화</th>
|
|
<th>분야</th>
|
|
<th>상태</th>
|
|
<th>날짜</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var inquiry in summary.RecentInquiries)
|
|
{
|
|
<tr>
|
|
<td><a href="@($"/taxbaik/admin/inquiries/{inquiry.Id}")">@inquiry.Name</a></td>
|
|
<td>@inquiry.Phone</td>
|
|
<td>@inquiry.ServiceType</td>
|
|
<td><span class="status-pill @GetStatusClass(inquiry.Status)">@GetStatusLabel(inquiry.Status)</span></td>
|
|
<td>@inquiry.CreatedAt.ToString("yyyy-MM-dd")</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
[CascadingParameter]
|
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
|
|
|
private AdminDashboardSummary? summary;
|
|
private List<Domain.Entities.TaxFiling> upcomingFilings = [];
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender && AuthStateTask != null)
|
|
{
|
|
var authState = await AuthStateTask;
|
|
if (authState.User.Identity?.IsAuthenticated == true)
|
|
{
|
|
try
|
|
{
|
|
var summaryTask = DashboardClient.GetSummaryAsync();
|
|
var filingsTask = DashboardClient.GetUpcomingFilingsAsync(30);
|
|
await Task.WhenAll(summaryTask, filingsTask);
|
|
summary = await summaryTask;
|
|
upcomingFilings = (await filingsTask).ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.Error.WriteLine($"Dashboard error: {ex.Message}");
|
|
}
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string GetStatusLabel(string status) => InquiryStatusMapper.Labels.GetValueOrDefault(status, status);
|
|
private static string GetStatusClass(string status) => status switch
|
|
{
|
|
"new" => "warning",
|
|
"consulting" => "info",
|
|
"contracted" => "success",
|
|
"rejected" => "danger",
|
|
"closed" => "dark",
|
|
_ => "default"
|
|
};
|
|
}
|