156 lines
6.2 KiB
Plaintext
156 lines
6.2 KiB
Plaintext
@page "/admin/clients"
|
|
@attribute [Authorize]
|
|
@using TaxBaik.Web.Services
|
|
@using TaxBaik.Domain.Entities
|
|
@inject IClientBrowserClient ClientClient
|
|
@inject NavigationManager Navigation
|
|
@inject IJSRuntime JS
|
|
|
|
<PageTitle>고객 관리</PageTitle>
|
|
<section class="admin-page-hero">
|
|
<div>
|
|
<div class="admin-eyebrow">CRM</div>
|
|
<h1 class="admin-page-title">고객 관리</h1>
|
|
<p class="admin-page-subtitle">고객 카드를 등록하고 상담 이력을 관리합니다.</p>
|
|
</div>
|
|
<button type="button" class="site-button primary" @onclick='() => Navigation.NavigateTo("/taxbaik/admin/clients/create")'>고객 등록</button>
|
|
</section>
|
|
|
|
<div class="admin-surface mb-3 pa-3">
|
|
<div class="admin-filter-grid">
|
|
<input class="admin-input" placeholder="검색 (이름·연락처·회사명)" @bind="searchText" @onkeyup="OnSearchKeyUp" />
|
|
<select class="admin-input" @bind="statusFilter">
|
|
<option value="">전체</option>
|
|
<option value="active">활성</option>
|
|
<option value="inactive">비활성</option>
|
|
</select>
|
|
<button type="button" class="site-button secondary" @onclick="SearchAsync">검색</button>
|
|
<button type="button" class="site-button secondary" @onclick="ResetAsync">초기화</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-surface">
|
|
@if (clients is null)
|
|
{
|
|
<Skeleton Count="6" CssClass="taxbaik-skeleton-grid" />
|
|
}
|
|
else if (!clients.Any())
|
|
{
|
|
<div class="muted mt-4">등록된 고객이 없습니다.</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="admin-table-wrap">
|
|
<table class="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>이름</th>
|
|
<th>회사명</th>
|
|
<th>연락처</th>
|
|
<th>서비스</th>
|
|
<th>세금 유형</th>
|
|
<th>상태</th>
|
|
<th>유입 경로</th>
|
|
<th>등록일</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var c in clients)
|
|
{
|
|
<tr>
|
|
<td><strong>@c.Name</strong></td>
|
|
<td>@(c.CompanyName ?? "—")</td>
|
|
<td>@(c.Phone ?? "—")</td>
|
|
<td>@(c.ServiceType ?? "—")</td>
|
|
<td>@(c.TaxType ?? "—")</td>
|
|
<td>@(c.Status == "active" ? "활성" : "비활성")</td>
|
|
<td>@(c.Source ?? "—")</td>
|
|
<td class="small">@c.CreatedAt.ToLocalTime().ToString("yy.MM.dd")</td>
|
|
<td>
|
|
<div class="admin-row-actions">
|
|
<button type="button" class="admin-icon-button" @onclick="@(() => Navigation.NavigateTo($"/taxbaik/admin/clients/{c.Id}/edit"))">✎</button>
|
|
<button type="button" class="admin-icon-button danger" @onclick="@(() => DeleteAsync(c))">✕</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@if (totalPages > 1)
|
|
{
|
|
<div class="admin-pagination">
|
|
<button type="button" class="site-button secondary" disabled="@(currentPage <= 1)" @onclick="PreviousPage">이전</button>
|
|
<span>@currentPage / @totalPages</span>
|
|
<button type="button" class="site-button secondary" disabled="@(currentPage >= totalPages)" @onclick="NextPage">다음</button>
|
|
</div>
|
|
}
|
|
<div class="admin-table-footer">총 @(totalCount)명</div>
|
|
}
|
|
</div>
|
|
|
|
@code {
|
|
[CascadingParameter] private Task<AuthenticationState>? AuthStateTask { get; set; }
|
|
private List<Client>? clients;
|
|
private string searchText = "";
|
|
private string statusFilter = "";
|
|
private int currentPage = 1;
|
|
private int totalCount;
|
|
private int totalPages;
|
|
private const int PageSize = 20;
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender && AuthStateTask != null)
|
|
{
|
|
var authState = await AuthStateTask;
|
|
if (authState.User.Identity?.IsAuthenticated == true)
|
|
{
|
|
await LoadAsync();
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task LoadAsync()
|
|
{
|
|
try
|
|
{
|
|
var (items, total) = await ClientClient.GetPagedAsync(currentPage, PageSize, string.IsNullOrEmpty(statusFilter) ? null : statusFilter, string.IsNullOrEmpty(searchText) ? null : searchText);
|
|
clients = items.ToList();
|
|
totalCount = total;
|
|
totalPages = (int)Math.Ceiling((double)total / PageSize);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}");
|
|
clients = [];
|
|
}
|
|
}
|
|
|
|
private async Task SearchAsync() { currentPage = 1; await LoadAsync(); }
|
|
private async Task ResetAsync() { searchText = ""; statusFilter = ""; currentPage = 1; await LoadAsync(); }
|
|
private async Task PreviousPage() { if (currentPage > 1) { currentPage--; await LoadAsync(); } }
|
|
private async Task NextPage() { if (currentPage < totalPages) { currentPage++; await LoadAsync(); } }
|
|
private async Task OnSearchKeyUp(KeyboardEventArgs e) { if (e.Key == "Enter") await SearchAsync(); }
|
|
private async Task DeleteAsync(Client client)
|
|
{
|
|
var confirmed = await JS.InvokeAsync<bool>("confirm", $"'{client.Name}' 고객을 삭제하시겠습니까? 관련 데이터도 함께 삭제됩니다.");
|
|
if (!confirmed) return;
|
|
try
|
|
{
|
|
var success = await ClientClient.DeleteAsync(client.Id);
|
|
if (success)
|
|
{
|
|
await JS.InvokeVoidAsync("alert", $"{client.Name} 고객이 삭제되었습니다.");
|
|
await LoadAsync();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}");
|
|
}
|
|
}
|
|
}
|