Files
taxbaik/TaxBaik.Web/Components/Admin/Pages/Contracts.razor
T

305 lines
12 KiB
Plaintext

@page "/admin/contracts"
@using TaxBaik.Web.Services.AdminClients
@inject IContractBrowserClient ContractClient
@inject IClientBrowserClient ClientClient
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@attribute [Authorize]
<PageTitle>계약 관리</PageTitle>
<section class="admin-page-hero">
<div>
<MudText Typo="Typo.caption" Class="admin-eyebrow">CRM & 세무관리</MudText>
<MudText Typo="Typo.h4" Class="admin-page-title">계약 관리</MudText>
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객 계약과 월 정기수익을 함께 관리합니다.</MudText>
@if (mrr > 0)
{
<MudText Typo="Typo.body2" Class="mt-2">
월 정기수익:
<MudChip Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled">₩@mrr.ToString("N0")</MudChip>
</MudText>
}
</div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate" StartIcon="@Icons.Material.Filled.Add" id="btn-add-contract">
새 계약 추가
</MudButton>
</section>
@if (contracts is null)
{
<MudProgressLinear Indeterminate="true" />
}
else
{
<MudGrid Spacing="2" Class="mt-2">
<!-- Left: Dense Grid List -->
<MudItem XS="12" MD="8">
@if (contracts.Count == 0)
{
<MudAlert Severity="Severity.Info">
<MudIcon Icon="@Icons.Material.Filled.Description" Class="me-2" />
계약이 없습니다.
</MudAlert>
}
else
{
<MudDataGrid T="Contract"
Items="@contracts"
Dense="true"
Hover="true"
Striped="true"
Virtualize="true"
RowsPerPage="30"
SelectedItem="@selectedContract"
SelectedItemChanged="OnRowSelected"
Class="admin-grid">
<Columns>
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
<TemplateColumn Title="고객">
<CellTemplate>
@if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
{
@clientName
}
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.ContractNumber" Title="계약번호" />
<PropertyColumn Property="x => x.ServiceType" Title="서비스 유형" />
<PropertyColumn Property="x => x.MonthlyFee" Title="월 수수료" Format="C" />
<TemplateColumn Title="계약기간">
<CellTemplate>
@context.Item.StartDate.ToString("yyyy-MM-dd")
@if (context.Item.EndDate.HasValue)
{
<span>~@context.Item.EndDate.Value.ToString("yyyy-MM-dd")</span>
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="상태">
<CellTemplate>
@{
var isActive = !context.Item.EndDate.HasValue || context.Item.EndDate.Value >= DateTime.Today;
}
@if (isActive)
{
<MudChip Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">활성</MudChip>
}
else
{
<MudChip Size="Size.Small" Color="Color.Default" Variant="Variant.Outlined">만료</MudChip>
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="작업" Sortable="false">
<CellTemplate>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small"
OnClick="@(async () => await DeleteContract(context.Item.Id))" />
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
}
</MudItem>
<!-- Right: Detail Form Panel (Inline Editor) -->
<MudItem XS="12" MD="4">
<MudPaper Class="pa-4 admin-surface admin-editor-panel" Elevation="0" Style="border: 1px solid var(--border-color); min-height: 400px;">
<div class="d-flex align-center justify-space-between mb-4">
<MudText Typo="Typo.h6" Class="font-weight-bold">@(isEditMode ? "계약 상세 정보" : "새 계약 추가")</MudText>
@if (isEditMode)
{
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary" OnClick="PrepareCreate">
새로 작성
</MudButton>
}
</div>
<MudForm @ref="form">
<MudSelect T="int?" @bind-Value="contractForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" RequiredError="고객을 선택하세요." Disabled="@isEditMode">
@foreach (var client in clients)
{
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
}
</MudSelect>
<MudTextField T="string" @bind-Value="contractForm.ContractNumber" Label="계약번호" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true" />
<MudSelect T="string" @bind-Value="contractForm.ServiceType" Label="서비스 유형" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true">
<MudSelectItem Value="@("개인 기장대리")">개인 기장대리</MudSelectItem>
<MudSelectItem Value="@("법인 기장대리")">법인 기장대리</MudSelectItem>
<MudSelectItem Value="@("세무조정 대행")">세무조정 대행</MudSelectItem>
<MudSelectItem Value="@("양도세 신고대리")">양도세 신고대리</MudSelectItem>
<MudSelectItem Value="@("상속·증여 자문")">상속·증여 자문</MudSelectItem>
<MudSelectItem Value="@("세무조사 대응")">세무조사 대응</MudSelectItem>
</MudSelect>
<MudDatePicker @bind-Date="contractForm.StartDate" Label="계약 시작일" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true" />
<MudNumericField T="decimal?" @bind-Value="contractForm.MonthlyFee" Label="월 수수료" Variant="Variant.Outlined" FullWidth="@true" Class="mb-4" />
<div class="d-flex justify-end gap-2">
@if (isEditMode)
{
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="@(async () => await DeleteContract(selectedContract?.Id ?? 0))">삭제</MudButton>
}
else
{
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveContract" id="btn-save-contract">저장</MudButton>
}
</div>
</MudForm>
</MudPaper>
</MudItem>
</MudGrid>
}
@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 MudForm? form;
private bool isEditMode;
private Contract? selectedContract;
private ContractForm contractForm = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if (AuthStateTask != null)
{
var authState = await AuthStateTask;
if (authState.User.Identity?.IsAuthenticated == true)
{
await LoadData();
PrepareCreate();
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)
{
Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error);
}
}
private void PrepareCreate()
{
selectedContract = null;
isEditMode = false;
contractForm = new ContractForm
{
ClientId = clients.FirstOrDefault()?.Id,
StartDate = DateTime.Today
};
}
private void OnRowSelected(Contract contract)
{
if (contract == null) return;
selectedContract = contract;
isEditMode = true;
contractForm = new ContractForm
{
ClientId = contract.ClientId,
ContractNumber = contract.ContractNumber,
ServiceType = contract.ServiceType,
StartDate = contract.StartDate,
MonthlyFee = contract.MonthlyFee
};
}
private async Task SaveContract()
{
if (form != null)
{
await form.Validate();
if (!form.IsValid)
{
Snackbar.Add("필수 항목을 입력해주세요.", Severity.Warning);
return;
}
}
try
{
if (contractForm.ClientId == null) return;
var newId = await ContractClient.CreateAsync(
contractForm.ClientId.Value,
contractForm.ContractNumber,
contractForm.ServiceType,
contractForm.StartDate ?? DateTime.Now,
contractForm.MonthlyFee);
if (newId > 0)
{
Snackbar.Add("계약이 추가되었습니다.", Severity.Success);
PrepareCreate();
await LoadData();
}
}
catch (Exception ex)
{
Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
}
}
private async Task DeleteContract(int id)
{
var parameters = new DialogParameters
{
{ "Title", "삭제 확인" },
{ "Message", "이 계약을 삭제하시겠습니까?" }
};
var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters);
var result = await dialog.Result;
if (result?.Canceled ?? true)
return;
try
{
await ContractClient.DeleteAsync(id);
Snackbar.Add("계약이 삭제되었습니다.", Severity.Success);
if (selectedContract?.Id == id)
{
PrepareCreate();
}
await LoadData();
}
catch (Exception ex)
{
Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error);
}
}
private static string GetClientDisplayName(Client client)
=> !string.IsNullOrWhiteSpace(client.CompanyName)
? client.CompanyName
: !string.IsNullOrWhiteSpace(client.Name)
? client.Name
: $"Client #{client.Id}";
private 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; }
}
}