191 lines
8.1 KiB
Plaintext
191 lines
8.1 KiB
Plaintext
@page "/admin/contracts"
|
|
@using TaxBaik.Web.Services.AdminClients
|
|
@inject IContractBrowserClient ContractClient
|
|
@inject IClientBrowserClient ClientClient
|
|
@inject IJSRuntime JS
|
|
@attribute [Authorize]
|
|
|
|
<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>
|
|
@if (mrr > 0)
|
|
{
|
|
<p class="admin-page-subtitle mt-2">월 정기수익: <strong>₩@mrr.ToString("N0")</strong></p>
|
|
}
|
|
</div>
|
|
<button type="button" class="site-button primary" @onclick="OpenCreateDialog">새 계약 추가</button>
|
|
</section>
|
|
|
|
<div class="admin-surface">
|
|
@if (contracts is null)
|
|
{
|
|
<Skeleton Count="5" CssClass="taxbaik-skeleton-grid" />
|
|
}
|
|
else if (contracts.Count == 0)
|
|
{
|
|
<div class="muted">계약이 없습니다.</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="admin-table-wrap">
|
|
<table class="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>고객</th>
|
|
<th>계약번호</th>
|
|
<th>서비스 유형</th>
|
|
<th>월 수수료</th>
|
|
<th>계약기간</th>
|
|
<th>상태</th>
|
|
<th>작업</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var item in contracts)
|
|
{
|
|
var isActive = !item.EndDate.HasValue || item.EndDate.Value >= DateTime.Today;
|
|
<tr>
|
|
<td>@item.Id</td>
|
|
<td>@(clientMap.TryGetValue(item.ClientId, out var clientName) ? clientName : "")</td>
|
|
<td>@item.ContractNumber</td>
|
|
<td>@item.ServiceType</td>
|
|
<td>@(item.MonthlyFee?.ToString("C") ?? "—")</td>
|
|
<td>@item.StartDate@if (item.EndDate.HasValue){<span>~ @item.EndDate.Value</span>}</td>
|
|
<td><span class="status-pill @(isActive ? "success" : "muted")">@(isActive ? "활성" : "만료")</span></td>
|
|
<td><button type="button" class="admin-icon-button danger" @onclick="@(async () => await DeleteContract(item.Id))">✕</button></td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<dialog class="admin-dialog" open="@isDialogOpen">
|
|
<form class="admin-dialog-card" @onsubmit="SaveContract" @onsubmit:preventDefault="true">
|
|
<h3>새 계약 추가</h3>
|
|
<label>고객
|
|
<select class="admin-input" @bind="ClientIdText">
|
|
<option value="">선택하세요</option>
|
|
@foreach (var client in clients)
|
|
{
|
|
<option value="@client.Id.ToString()">@GetClientDisplayName(client)</option>
|
|
}
|
|
</select>
|
|
</label>
|
|
<label>계약번호 <input class="admin-input" @bind="contractForm.ContractNumber" /></label>
|
|
<label>서비스 유형
|
|
<select class="admin-input" @bind="contractForm.ServiceType">
|
|
<option value="개인 기장대리">개인 기장대리</option>
|
|
<option value="법인 기장대리">법인 기장대리</option>
|
|
<option value="세무조정 대행">세무조정 대행</option>
|
|
<option value="양도세 신고대리">양도세 신고대리</option>
|
|
<option value="상속·증여 자문">상속·증여 자문</option>
|
|
<option value="세무조사 대응">세무조사 대응</option>
|
|
</select>
|
|
</label>
|
|
<label>계약 시작일 <input class="admin-input" type="text" placeholder="2026-06-29" @bind="StartDateText" /></label>
|
|
<label>월 수수료 <input class="admin-input" type="text" placeholder="100000" @bind="MonthlyFeeText" /></label>
|
|
<div class="admin-dialog-actions">
|
|
<button type="button" class="site-button secondary" @onclick="CloseDialog">취소</button>
|
|
<button type="submit" class="site-button primary">저장</button>
|
|
</div>
|
|
</form>
|
|
</dialog>
|
|
|
|
@code {
|
|
[CascadingParameter] private Task<AuthenticationState>? AuthStateTask { get; set; }
|
|
private List<Contract>? contracts;
|
|
private List<Client> clients = [];
|
|
private Dictionary<int, string> clientMap = new();
|
|
private decimal mrr = 0;
|
|
private bool isDialogOpen;
|
|
private ContractForm contractForm = new();
|
|
private string ClientIdText { get => contractForm.ClientId > 0 ? contractForm.ClientId.ToString() : ""; set => contractForm.ClientId = int.TryParse(value, out var id) ? id : 0; }
|
|
private string StartDateText { get => contractForm.StartDate?.ToString("yyyy-MM-dd") ?? ""; set => contractForm.StartDate = DateTime.TryParse(value, out var dt) ? dt : null; }
|
|
private string MonthlyFeeText { get => contractForm.MonthlyFee?.ToString() ?? ""; set => contractForm.MonthlyFee = decimal.TryParse(value, out var amount) ? amount : null; }
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender && AuthStateTask != null)
|
|
{
|
|
var authState = await AuthStateTask;
|
|
if (authState.User.Identity?.IsAuthenticated == true)
|
|
{
|
|
await LoadData();
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task LoadData()
|
|
{
|
|
try
|
|
{
|
|
contracts = await ContractClient.GetAllAsync();
|
|
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
|
|
clients = clientItems.ToList();
|
|
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
|
|
mrr = await ContractClient.GetMonthlyRecurringRevenueAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await JS.InvokeVoidAsync("alert", $"데이터 로드 실패: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void OpenCreateDialog()
|
|
{
|
|
contractForm = new ContractForm { ClientId = clients.FirstOrDefault()?.Id ?? 0, StartDate = DateTime.Today };
|
|
isDialogOpen = true;
|
|
}
|
|
|
|
private async Task SaveContract()
|
|
{
|
|
try
|
|
{
|
|
if (contractForm.ClientId <= 0)
|
|
{
|
|
await JS.InvokeVoidAsync("alert", "고객을 선택하세요.");
|
|
return;
|
|
}
|
|
|
|
var newId = await ContractClient.CreateAsync(contractForm.ClientId, contractForm.ContractNumber, contractForm.ServiceType, contractForm.StartDate ?? DateTime.Today, contractForm.MonthlyFee);
|
|
if (newId > 0)
|
|
{
|
|
await JS.InvokeVoidAsync("alert", "계약이 추가되었습니다.");
|
|
CloseDialog();
|
|
await LoadData();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await JS.InvokeVoidAsync("alert", $"저장 실패: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private async Task DeleteContract(int id)
|
|
{
|
|
if (!await JS.InvokeAsync<bool>("confirm", "이 계약을 삭제하시겠습니까?")) return;
|
|
try
|
|
{
|
|
await ContractClient.DeleteAsync(id);
|
|
await JS.InvokeVoidAsync("alert", "계약이 삭제되었습니다.");
|
|
await LoadData();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await JS.InvokeVoidAsync("alert", $"삭제 실패: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void CloseDialog() { isDialogOpen = false; contractForm = new(); }
|
|
private static string GetClientDisplayName(Client client) => !string.IsNullOrWhiteSpace(client.CompanyName) ? client.CompanyName : !string.IsNullOrWhiteSpace(client.Name) ? client.Name : $"Client #{client.Id}";
|
|
private sealed class ContractForm { public int ClientId { get; set; } public string ContractNumber { get; set; } = ""; public string ServiceType { get; set; } = ""; public DateTime? StartDate { get; set; } public decimal? MonthlyFee { get; set; } }
|
|
}
|