diff --git a/TaxBaik.Web/Components/Admin/Pages/Contracts.razor b/TaxBaik.Web/Components/Admin/Pages/Contracts.razor index 9d480be..6fb8398 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Contracts.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Contracts.razor @@ -21,116 +21,133 @@ } - + 새 계약 추가 - - @if (contracts is null) - { - - } - else if (contracts.Count == 0) - { - - - 계약이 없습니다. - - } - else - { - - - - - - @if (clientMap.TryGetValue(context.Item.ClientId, out var clientName)) +@if (contracts is null) +{ + +} +else +{ + + + + @if (contracts.Count == 0) + { + + + 계약이 없습니다. + + } + else + { + + + + + + @if (clientMap.TryGetValue(context.Item.ClientId, out var clientName)) + { + @clientName + } + + + + + + + + @context.Item.StartDate.ToString("yyyy-MM-dd") + @if (context.Item.EndDate.HasValue) + { + ~@context.Item.EndDate.Value.ToString("yyyy-MM-dd") + } + + + + + @{ + var isActive = !context.Item.EndDate.HasValue || context.Item.EndDate.Value >= DateTime.Today; + } + @if (isActive) + { + 활성 + } + else + { + 만료 + } + + + + + + + + + + } + + + + + +
+ @(isEditMode ? "계약 상세 정보" : "새 계약 추가") + @if (isEditMode) + { + + 새로 작성 + + } +
+ + + @foreach (var client in clients) { - - @clientName - + @GetClientDisplayName(client) } -
-
- - - - - - @context.Item.StartDate.ToString("yyyy-MM-dd") - @if (context.Item.EndDate.HasValue) + + + + 개인 기장대리 + 법인 기장대리 + 세무조정 대행 + 양도세 신고대리 + 상속·증여 자문 + 세무조사 대응 + + + + +
+ @if (isEditMode) { - ~@context.Item.EndDate.Value.ToString("yyyy-MM-dd") - } - - - - - @{ - var isActive = !context.Item.EndDate.HasValue || context.Item.EndDate.Value >= DateTime.Today; - } - @if (isActive) - { - 활성 + 삭제 } else { - 만료 + 저장 } - - - - - - - - - - - - } - - - - - - 새 계약 추가 - - - - - @foreach (var client in clients) - { - @GetClientDisplayName(client) - } - - - - 개인 기장대리 - 법인 기장대리 - 세무조정 대행 - 양도세 신고대리 - 상속·증여 자문 - 세무조사 대응 - - - - - - - 취소 - 저장 - - +
+ +
+ + +} @code { [CascadingParameter] @@ -141,7 +158,8 @@ private Dictionary clientMap = new(); private decimal mrr = 0; private MudForm? form; - private bool isDialogOpen; + private bool isEditMode; + private Contract? selectedContract; private ContractForm contractForm = new(); protected override async Task OnAfterRenderAsync(bool firstRender) @@ -154,6 +172,7 @@ if (authState.User.Identity?.IsAuthenticated == true) { await LoadData(); + PrepareCreate(); StateHasChanged(); } } @@ -176,14 +195,30 @@ } } - private void OpenCreateDialog() + private void PrepareCreate() { + selectedContract = null; + isEditMode = false; contractForm = new ContractForm { ClientId = clients.FirstOrDefault()?.Id, StartDate = DateTime.Today }; - isDialogOpen = true; + } + + 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() @@ -211,7 +246,7 @@ if (newId > 0) { Snackbar.Add("계약이 추가되었습니다.", Severity.Success); - CloseDialog(); + PrepareCreate(); await LoadData(); } } @@ -239,6 +274,10 @@ { await ContractClient.DeleteAsync(id); Snackbar.Add("계약이 삭제되었습니다.", Severity.Success); + if (selectedContract?.Id == id) + { + PrepareCreate(); + } await LoadData(); } catch (Exception ex) @@ -247,18 +286,13 @@ } } - 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 class ContractForm { public int? ClientId { get; set; } diff --git a/TaxBaik.Web/Components/Admin/Pages/TaxFilingSchedules.razor b/TaxBaik.Web/Components/Admin/Pages/TaxFilingSchedules.razor index 4ef829e..098a58a 100644 --- a/TaxBaik.Web/Components/Admin/Pages/TaxFilingSchedules.razor +++ b/TaxBaik.Web/Components/Admin/Pages/TaxFilingSchedules.razor @@ -14,141 +14,163 @@ 신고 일정 고객별 마감일과 처리 상태를 한 화면에서 관리합니다. - + 새 일정 추가 - - @if (schedules is null) - { - - } - else if (schedules.Count == 0) - { - - - 신고 일정이 없습니다. - - } - else - { - - - - - - @if (clientMap.TryGetValue(context.Item.ClientId, out var clientName)) +@if (schedules is null) +{ + +} +else +{ + + + + @if (schedules.Count == 0) + { + + + 신고 일정이 없습니다. + + } + else + { + + + + + + @if (clientMap.TryGetValue(context.Item.ClientId, out var clientName)) + { + @clientName + } + + + + + + @{ + var daysLeft = (context.Item.DueDate.Date - DateTime.Today).Days; + var statusColor = daysLeft < 0 ? Color.Error : daysLeft <= 7 ? Color.Warning : Color.Success; + } + + @context.Item.DueDate.ToString("yyyy-MM-dd") + @if (daysLeft >= 0) + { + (D-@daysLeft) + } + else + { + (마감 @Math.Abs(daysLeft)일 경과) + } + + + + + + + @if (context.Item.Status == "completed") + { + 완료 + } + else + { + 대기 + } + + + + + + @if (context.Item.Status != "completed") + { + + } + + + + + + + } + + + + + +
+ @(isEditMode ? "신고 일정 상세" : "새 신고 일정 추가") + @if (isEditMode) + { + + 새로 작성 + + } +
+ + + @foreach (var client in clients) { - - @clientName - + @GetClientDisplayName(client) } -
-
- - - - @{ - var daysLeft = (context.Item.DueDate.Date - DateTime.Today).Days; - var statusColor = daysLeft < 0 ? Color.Error : daysLeft <= 7 ? Color.Warning : Color.Success; - } - - @context.Item.DueDate.ToString("yyyy-MM-dd") - @if (daysLeft >= 0) - { - (D-@daysLeft) - } - else - { - (마감 @Math.Abs(daysLeft)일 경과) - } - - - - - - - @if (context.Item.Status == "completed") + + + 종합소득세 + 부가가치세 + 법인세 + 원천세 + 종합부동산세 + 양도소득세 + 상속·증여세 + 세무조정 + + + + +
+ @if (isEditMode && selectedSchedule?.Status != "completed") { - 완료 + 완료 처리 + } + @if (isEditMode) + { + 삭제 } else { - 대기 + 저장 } - - - - - - @if (context.Item.Status != "completed") - { - - } - - - - - - - } - - - - - 새 신고 일정 추가 - - - - - @foreach (var client in clients) - { - @GetClientDisplayName(client) - } - - - 종합소득세 - 부가가치세 - 법인세 - 원천세 - 종합부동산세 - 양도소득세 - 상속·증여세 - 세무조정 - - - - - - - 취소 - 저장 - - +
+ +
+ + +} @code { [CascadingParameter] @@ -158,7 +180,8 @@ private List clients = []; private Dictionary clientMap = new(); private MudForm? form; - private bool isDialogOpen; + private bool isEditMode; + private TaxFilingSchedule? selectedSchedule; private TaxFilingScheduleForm scheduleForm = new(); protected override async Task OnAfterRenderAsync(bool firstRender) @@ -171,6 +194,7 @@ if (authState.User.Identity?.IsAuthenticated == true) { await LoadData(); + PrepareCreate(); StateHasChanged(); } } @@ -192,15 +216,30 @@ } } - private void OpenCreateDialog() + private void PrepareCreate() { + selectedSchedule = null; + isEditMode = false; scheduleForm = new TaxFilingScheduleForm { FilingYear = DateTime.Now.Year, DueDate = DateTime.Today, ClientId = clients.FirstOrDefault()?.Id }; - isDialogOpen = true; + } + + private void OnRowSelected(TaxFilingSchedule schedule) + { + if (schedule == null) return; + selectedSchedule = schedule; + isEditMode = true; + scheduleForm = new TaxFilingScheduleForm + { + ClientId = schedule.ClientId, + FilingType = schedule.FilingType, + DueDate = schedule.DueDate, + FilingYear = schedule.FilingYear + }; } private async Task SaveSchedule() @@ -227,7 +266,7 @@ if (newId > 0) { Snackbar.Add("신고 일정이 추가되었습니다.", Severity.Success); - CloseDialog(); + PrepareCreate(); await LoadData(); } else @@ -247,6 +286,10 @@ { await TaxFilingClient.MarkCompletedAsync(id); Snackbar.Add("신고 일정이 완료 처리되었습니다.", Severity.Success); + if (selectedSchedule?.Id == id) + { + PrepareCreate(); + } await LoadData(); } catch (Exception ex) @@ -272,6 +315,10 @@ { await TaxFilingClient.DeleteAsync(id); Snackbar.Add("신고 일정이 삭제되었습니다.", Severity.Success); + if (selectedSchedule?.Id == id) + { + PrepareCreate(); + } await LoadData(); } catch (Exception ex) @@ -280,18 +327,13 @@ } } - private void CloseDialog() - { - isDialogOpen = false; - scheduleForm = new(); - } - private static string GetClientDisplayName(Client client) => !string.IsNullOrWhiteSpace(client.CompanyName) ? client.CompanyName : !string.IsNullOrWhiteSpace(client.Name) ? client.Name : $"Client #{client.Id}"; + private class TaxFilingScheduleForm { public int? ClientId { get; set; } diff --git a/TaxBaik.Web/Components/Admin/Pages/TaxProfiles.razor b/TaxBaik.Web/Components/Admin/Pages/TaxProfiles.razor index e456d1d..d1a7602 100644 --- a/TaxBaik.Web/Components/Admin/Pages/TaxProfiles.razor +++ b/TaxBaik.Web/Components/Admin/Pages/TaxProfiles.razor @@ -15,7 +15,7 @@ 세무 프로필 고객별 세무 프로필, 신고 일정, 위험도 추적 - + 새 프로필 추가 @@ -24,94 +24,109 @@ { } -else if (profiles.Count == 0) -{ - 세무 프로필이 없습니다. -} else { - - - - - - @if (clientMap.TryGetValue(context.Item.ClientId, out var clientName)) - { - - @clientName - - } - - - - - - - @context.Item.TaxRiskLevel - - - - - - @if (context.Item.NextFilingDueDate.HasValue) - { - @context.Item.NextFilingDueDate.Value.ToString("yyyy-MM-dd") - } - - - - - - - - - - - - -} + + + + @if (profiles.Count == 0) + { + 세무 프로필이 없습니다. + } + else + { + + + + + + @if (clientMap.TryGetValue(context.Item.ClientId, out var clientName)) + { + @clientName + } + + + + + + + @context.Item.TaxRiskLevel + + + + + + @if (context.Item.NextFilingDueDate.HasValue) + { + @context.Item.NextFilingDueDate.Value.ToString("yyyy-MM-dd") + } + + + + + + + + + + } + - - - - @(isEditMode ? "세무 프로필 수정" : "새 세무 프로필 추가") - - - - - @foreach (var client in clients) - { - @GetClientDisplayName(client) - } - - - @foreach (var type in businessTypes) - { - @type.CodeName - } - - - @foreach (var level in riskLevels) - { - @level.CodeName - } - - - - - - - 취소 - 저장 - - + + + +
+ @(isEditMode ? "세무 프로필 수정" : "새 세무 프로필 추가") + @if (isEditMode) + { + + 새로 작성 + + } +
+ + + @foreach (var client in clients) + { + @GetClientDisplayName(client) + } + + + @foreach (var type in businessTypes) + { + @type.CodeName + } + + + @foreach (var level in riskLevels) + { + @level.CodeName + } + + + + +
+ @if (isEditMode) + { + 삭제 + } + 저장 +
+
+
+
+
+} @code { [CascadingParameter] @@ -123,9 +138,8 @@ else private List businessTypes = []; private List riskLevels = []; private MudForm? form; - private bool isDialogOpen; private bool isEditMode; - private TaxProfile? editingProfile; + private TaxProfile? selectedProfile; private TaxProfileForm profileForm = new(); protected override async Task OnAfterRenderAsync(bool firstRender) @@ -138,6 +152,7 @@ else if (authState.User.Identity?.IsAuthenticated == true) { await LoadData(); + PrepareCreate(); StateHasChanged(); } } @@ -185,23 +200,23 @@ else } } - private void OpenCreateDialog() + private void PrepareCreate() { + selectedProfile = null; isEditMode = false; - editingProfile = null; profileForm = new TaxProfileForm { ClientId = clients.FirstOrDefault()?.Id, TaxRiskLevel = "normal", NextFilingDueDate = DateTime.Today.AddMonths(1) }; - isDialogOpen = true; } - private async Task OpenEditDialog(TaxProfile profile) + private void OnRowSelected(TaxProfile profile) { + if (profile == null) return; + selectedProfile = profile; isEditMode = true; - editingProfile = profile; profileForm = new TaxProfileForm { ClientId = profile.ClientId, @@ -210,7 +225,6 @@ else NextFilingDueDate = profile.NextFilingDueDate, SpecialNotes = profile.SpecialNotes }; - isDialogOpen = true; } private async Task SaveProfile() @@ -220,16 +234,16 @@ else await form.Validate(); if (!form.IsValid) { - Snackbar.Add("고객을 선택하세요.", Severity.Warning); + Snackbar.Add("고객을 선택하세요.", Severity.Warning); return; } } try { - if (isEditMode && editingProfile != null) + if (isEditMode && selectedProfile != null) { - await TaxProfileClient.UpdateAsync(editingProfile.Id, profileForm.BusinessType, + await TaxProfileClient.UpdateAsync(selectedProfile.Id, profileForm.BusinessType, null, profileForm.NextFilingDueDate, profileForm.TaxRiskLevel); Snackbar.Add("세무 프로필이 수정되었습니다.", Severity.Success); } @@ -245,7 +259,6 @@ else profileForm.BusinessType); if (newId > 0) { - // 생성 후 상태 업데이트 처리 await TaxProfileClient.UpdateAsync( newId, profileForm.BusinessType, @@ -255,7 +268,7 @@ else Snackbar.Add("세무 프로필이 추가되었습니다.", Severity.Success); } } - CloseDialog(); + PrepareCreate(); await LoadData(); } catch (Exception ex) @@ -280,6 +293,10 @@ else { await TaxProfileClient.DeleteAsync(id); Snackbar.Add("세무 프로필이 삭제되었습니다.", Severity.Success); + if (selectedProfile?.Id == id) + { + PrepareCreate(); + } await LoadData(); } catch (Exception ex) @@ -288,14 +305,6 @@ else } } - private void CloseDialog() - { - isDialogOpen = false; - isEditMode = false; - editingProfile = null; - profileForm = new(); - } - private Color GetRiskColor(string riskLevel) => riskLevel switch { "high" => Color.Error, @@ -310,6 +319,7 @@ else : !string.IsNullOrWhiteSpace(client.Name) ? client.Name : $"Client #{client.Id}"; + private class TaxProfileForm { public int? ClientId { get; set; } diff --git a/tests/e2e/admin-crm-pages.spec.ts b/tests/e2e/admin-crm-pages.spec.ts index 928dc90..4a6d2fc 100644 --- a/tests/e2e/admin-crm-pages.spec.ts +++ b/tests/e2e/admin-crm-pages.spec.ts @@ -90,14 +90,13 @@ test.describe('admin CRM pages', () => { } }); - test('TaxProfiles modal dialog opens on add button click', async ({ page }) => { + test('TaxProfiles editor panel is visible on add button click', async ({ page }) => { await navigateInBlazor(page, `${baseUrl}/admin/tax-profiles`); const addButton = page.getByRole('button', { name: /새 프로필 추가/ }); await expect(addButton).toBeVisible(); await addButton.click(); - await expect(page).toHaveURL(/\/taxbaik\/admin\/tax-profiles$/); - await expect(addButton).toBeVisible(); + await expect(page.locator('.admin-editor-panel')).toBeVisible({ timeout: 5000 }); }); test('No console errors on CRM page navigation', async ({ page }) => { @@ -131,11 +130,11 @@ test.describe('admin CRM pages', () => { const addButton = page.getByRole('button', { name: /새 프로필 추가/ }); await addButton.click(); - // 대화상자(MudDialog) 자체의 노출 대기 - await expect(page.locator('.mud-dialog')).toBeVisible({ timeout: 5000 }); + // 분할 편집기(admin-editor-panel) 노출 대기 + await expect(page.locator('.admin-editor-panel')).toBeVisible({ timeout: 5000 }); // mud-select 내의 input 클릭 (이벤트 핸들러 격발 유도) - const select = page.locator('.mud-dialog .mud-select').filter({ hasText: '사업 유형' }).first(); + const select = page.locator('.admin-editor-panel .mud-select').filter({ hasText: '사업 유형' }).first(); await page.waitForTimeout(500); await select.locator('input').click(); @@ -153,9 +152,9 @@ test.describe('admin CRM pages', () => { const addButton = page.getByRole('button', { name: /새 일정 추가/ }); await addButton.click(); - await expect(page.locator('.mud-dialog')).toBeVisible({ timeout: 5000 }); + await expect(page.locator('.admin-editor-panel')).toBeVisible({ timeout: 5000 }); - const select = page.locator('.mud-dialog .mud-select').filter({ hasText: '신고 유형' }).first(); + const select = page.locator('.admin-editor-panel .mud-select').filter({ hasText: '신고 유형' }).first(); await page.waitForTimeout(500); await select.locator('input').click(); @@ -171,9 +170,9 @@ test.describe('admin CRM pages', () => { const addButton = page.getByRole('button', { name: /새 계약 추가/ }); await addButton.click(); - await expect(page.locator('.mud-dialog')).toBeVisible({ timeout: 5000 }); + await expect(page.locator('.admin-editor-panel')).toBeVisible({ timeout: 5000 }); - const select = page.locator('.mud-dialog .mud-select').filter({ hasText: '서비스 유형' }).first(); + const select = page.locator('.admin-editor-panel .mud-select').filter({ hasText: '서비스 유형' }).first(); await page.waitForTimeout(500); await select.locator('input').click(); @@ -181,4 +180,5 @@ test.describe('admin CRM pages', () => { await expect(popover.getByText('개인 기장대리')).toBeVisible({ timeout: 5000 }); await expect(popover.getByText('법인 기장대리')).toBeVisible({ timeout: 5000 }); }); -}); +} +);