0d7a081f5a
TaxBaik CI/CD / build-and-deploy (push) Successful in 52s
Phase 3 Completion:
- Step 3-2: TaxFilingSchedules.razor (신고 일정 추적, D-day 표시)
- Step 3-3: Contracts.razor (계약 관리, MRR 표시)
- Step 3-4: ConsultingActivities.razor (상담 활동 기록, 팔로업 추적)
- Step 3-5: RevenueTrackings.razor (수익/청구 추적, 납부 상태)
Entity Property Mapping:
- TaxFilingSchedule: Status='pending'|'completed', CompletedDate
- RevenueTracking: PaymentStatus='pending'|'paid', PaymentDate
- Contract: StartDate, EndDate (optional), MonthlyFee (nullable)
- ConsultingActivity: ActivityDate, NextFollowupDate (optional)
UI Patterns:
- All pages: MudDataGrid Dense (32px), Virtualize, 30 rows/page
- Deadline tracking: D-day chips with color status (Error/Warning/Success)
- Status display: Chips for pending/completed/active/inactive states
- Client links: Navigate to /admin/clients/{id} for detail view
- Modal dialogs: MudDialog for create/edit (no white-screen flashes)
- Confirmation dialogs: ConfirmDialog for delete operations
- Revenue tracking: 납부 처리 button for payment confirmation
SOLID Principles:
- Each page owns its own form class (TaxFilingScheduleForm, etc)
- Browser Client abstraction for API calls
- LocalDataGrid rendering for high-density data
- Async/await patterns for all API interactions
Build Status: 0 errors, 3 warnings (existing Dashboard unused fields)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
238 lines
8.5 KiB
Plaintext
238 lines
8.5 KiB
Plaintext
@page "/admin/revenue-trackings"
|
|
@using TaxBaik.Web.Services.AdminClients
|
|
@inject IRevenueTrackingBrowserClient RevenueClient
|
|
@inject IClientBrowserClient ClientClient
|
|
@inject ISnackbar Snackbar
|
|
@inject IDialogService DialogService
|
|
@attribute [Authorize]
|
|
|
|
<PageTitle>수익 추적 관리</PageTitle>
|
|
|
|
<div class="admin-container">
|
|
<div class="admin-header">
|
|
<MudText Typo="Typo.h5" Class="font-weight-bold">수익 추적 관리</MudText>
|
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add">
|
|
새 청구 추가
|
|
</MudButton>
|
|
</div>
|
|
|
|
@if (revenues == null)
|
|
{
|
|
<MudProgressCircular Indeterminate="true" Class="mt-4" />
|
|
}
|
|
else if (revenues.Count == 0)
|
|
{
|
|
<MudAlert Severity="Severity.Info" Class="mt-4">청구 기록이 없습니다.</MudAlert>
|
|
}
|
|
else
|
|
{
|
|
<MudDataGrid T="RevenueTracking"
|
|
Items="@revenues"
|
|
Dense="true"
|
|
Hover="true"
|
|
Striped="true"
|
|
Virtualize="true"
|
|
RowsPerPage="30"
|
|
Class="admin-grid mt-4">
|
|
<Columns>
|
|
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
|
|
<TemplateColumn Title="고객">
|
|
<CellTemplate>
|
|
@if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
|
|
{
|
|
<MudLink Href="@($"/taxbaik/admin/clients/{context.Item.ClientId}")" Color="Color.Primary">
|
|
@clientName
|
|
</MudLink>
|
|
}
|
|
</CellTemplate>
|
|
</TemplateColumn>
|
|
<PropertyColumn Property="x => x.InvoiceNumber" Title="청구번호" />
|
|
<PropertyColumn Property="x => x.InvoiceDate" Title="청구일" Format="yyyy-MM-dd" />
|
|
<PropertyColumn Property="x => x.Amount" Title="청구액" Format="C" />
|
|
<TemplateColumn Title="납부여부">
|
|
<CellTemplate>
|
|
@if (context.Item.PaymentStatus == "paid")
|
|
{
|
|
<MudChip Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">납부</MudChip>
|
|
}
|
|
else
|
|
{
|
|
<MudChip Size="Size.Small" Color="Color.Warning" Variant="Variant.Filled">미납</MudChip>
|
|
}
|
|
</CellTemplate>
|
|
</TemplateColumn>
|
|
<TemplateColumn Title="작업" Sortable="false">
|
|
<CellTemplate>
|
|
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
|
|
@if (context.Item.PaymentStatus != "paid")
|
|
{
|
|
<MudIconButton Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success"
|
|
OnClick="@(async () => await MarkPaid(context.Item.Id))" Title="납부 처리" />
|
|
}
|
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
|
|
OnClick="@(async () => await DeleteRevenue(context.Item.Id))" />
|
|
</MudButtonGroup>
|
|
</CellTemplate>
|
|
</TemplateColumn>
|
|
</Columns>
|
|
</MudDataGrid>
|
|
}
|
|
</div>
|
|
|
|
<!-- Create Dialog -->
|
|
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
|
|
<TitleContent>
|
|
<MudText Typo="Typo.h6">새 청구 추가</MudText>
|
|
</TitleContent>
|
|
<DialogContent>
|
|
<MudForm @ref="form">
|
|
<MudSelect T="int" @bind-Value="revenueForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
|
@foreach (var client in clients)
|
|
{
|
|
<MudSelectItem Value="@client.Id">@client.CompanyName</MudSelectItem>
|
|
}
|
|
</MudSelect>
|
|
<MudTextField T="string" @bind-Value="revenueForm.InvoiceNumber" Label="청구번호" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
|
<MudDatePicker @bind-Date="revenueForm.InvoiceDate" Label="청구일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
|
<MudNumericField T="decimal" @bind-Value="revenueForm.Amount" Label="청구액" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
|
<MudTextField T="string" @bind-Value="revenueForm.ServiceType" Label="서비스 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
|
<MudDatePicker @bind-Date="revenueForm.DueDate" Label="납부예정일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
|
</MudForm>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<MudButton OnClick="CloseDialog">취소</MudButton>
|
|
<MudButton Color="Color.Primary" OnClick="SaveRevenue">저장</MudButton>
|
|
</DialogActions>
|
|
</MudDialog>
|
|
|
|
@code {
|
|
private List<RevenueTracking>? revenues;
|
|
private List<Client> clients = [];
|
|
private Dictionary<int, string> clientMap = new();
|
|
private MudForm? form;
|
|
private bool isDialogOpen;
|
|
private RevenueForm revenueForm = new();
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadData();
|
|
}
|
|
|
|
private async Task LoadData()
|
|
{
|
|
try
|
|
{
|
|
revenues = await RevenueClient.GetAllAsync();
|
|
var (clientItems, _) = await ClientClient.GetPagedAsync();
|
|
clients = clientItems.ToList();
|
|
clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private void OpenCreateDialog()
|
|
{
|
|
revenueForm = new();
|
|
isDialogOpen = true;
|
|
}
|
|
|
|
private async Task SaveRevenue()
|
|
{
|
|
try
|
|
{
|
|
var newId = await RevenueClient.CreateAsync(
|
|
revenueForm.ClientId,
|
|
revenueForm.InvoiceNumber,
|
|
revenueForm.InvoiceDate ?? DateTime.Now,
|
|
revenueForm.Amount,
|
|
revenueForm.ServiceType,
|
|
revenueForm.DueDate);
|
|
|
|
if (newId > 0)
|
|
{
|
|
Snackbar.Add("청구가 추가되었습니다.", Severity.Success);
|
|
CloseDialog();
|
|
await LoadData();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task MarkPaid(int id)
|
|
{
|
|
try
|
|
{
|
|
await RevenueClient.MarkPaidAsync(id, DateTime.Now);
|
|
Snackbar.Add("납부가 처리되었습니다.", Severity.Success);
|
|
await LoadData();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"처리 실패: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task DeleteRevenue(int id)
|
|
{
|
|
var parameters = new DialogParameters();
|
|
parameters.Add("Title", "삭제 확인");
|
|
parameters.Add("Message", "이 청구를 삭제하시겠습니까?");
|
|
|
|
var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters);
|
|
var result = await dialog.Result;
|
|
|
|
if (result?.Canceled ?? true)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await RevenueClient.DeleteAsync(id);
|
|
Snackbar.Add("청구가 삭제되었습니다.", Severity.Success);
|
|
await LoadData();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private void CloseDialog()
|
|
{
|
|
isDialogOpen = false;
|
|
revenueForm = new();
|
|
}
|
|
|
|
private class RevenueForm
|
|
{
|
|
public int ClientId { get; set; }
|
|
public string InvoiceNumber { get; set; } = "";
|
|
public DateTime? InvoiceDate { get; set; }
|
|
public decimal Amount { get; set; }
|
|
public string? ServiceType { get; set; }
|
|
public DateTime? DueDate { get; set; }
|
|
}
|
|
}
|
|
|
|
<style>
|
|
.admin-container {
|
|
padding: 20px;
|
|
}
|
|
|
|
.admin-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.admin-grid {
|
|
font-size: 13px;
|
|
}
|
|
</style>
|