Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0dc05c58c | |||
| 6a5740ec68 |
+16
-16
@@ -425,9 +425,9 @@ Todo:
|
||||
- 텔레그램 전송 실패 시 로그만 남기고 앱 정상 운영 유지
|
||||
|
||||
Todo:
|
||||
- [x] BackgroundService 또는 Hangfire 기반 스케줄러 추가
|
||||
- [x] 일간/주간 리포트 메시지 템플릿
|
||||
- [x] TelegramNotificationService에 리포트 메서드 추가
|
||||
- [ ] BackgroundService 또는 Hangfire 기반 스케줄러 추가
|
||||
- [ ] 일간/주간 리포트 메시지 템플릿
|
||||
- [ ] TelegramNotificationService에 리포트 메서드 추가
|
||||
|
||||
## WBS-CRM-07 고객 포털 (읽기 전용) — Phase 3
|
||||
|
||||
@@ -439,9 +439,9 @@ Todo:
|
||||
- 개인정보 열람 범위는 세무사가 허용한 항목만
|
||||
|
||||
Todo:
|
||||
- [x] 고객 포털 설계 (인증 방식 결정 — WBS-CRM-08 선행)
|
||||
- [x] 고객 전용 Razor Pages 추가
|
||||
- [x] 세무사 허용 권한 설정 UI
|
||||
- [ ] 고객 포털 설계 (인증 방식 결정 — WBS-CRM-08 선행)
|
||||
- [ ] 고객 전용 Razor Pages 추가
|
||||
- [ ] 세무사 허용 권한 설정 UI
|
||||
|
||||
## WBS-CRM-08 고객 회원가입 · 소셜 로그인 — Phase 3
|
||||
|
||||
@@ -485,16 +485,16 @@ DB 스키마:
|
||||
- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`
|
||||
|
||||
Todo:
|
||||
- [x] WBS-CRM-07 고객 포털 기본 구조 완성 (선행)
|
||||
- [x] OAuth 앱 등록 (네이버·카카오·구글 개발자 콘솔)
|
||||
- [x] V011__CreatePortalUsers.sql 마이그레이션 (실제 V016__CreatePortalUsers.sql로 대체됨)
|
||||
- [x] PortalUser 엔티티 / IPortalUserRepository / PortalUserRepository
|
||||
- [x] 네이버 OAuth Handler 구현
|
||||
- [x] 카카오·구글 패키지 추가 및 설정
|
||||
- [x] 기본 계정 회원가입 폼 (`/taxbaik/portal/register`)
|
||||
- [x] 소셜 로그인 콜백 처리 → portal_users 자동 생성
|
||||
- [x] 신규 가입 시 clients 테이블 연결 또는 신규 생성
|
||||
- [x] 포털 로그인 페이지 (`/taxbaik/portal/login`) — 소셜 버튼 + 이메일 폼
|
||||
- [ ] WBS-CRM-07 고객 포털 기본 구조 완성 (선행)
|
||||
- [ ] OAuth 앱 등록 (네이버·카카오·구글 개발자 콘솔)
|
||||
- [ ] V011__CreatePortalUsers.sql 마이그레이션
|
||||
- [ ] PortalUser 엔티티 / IPortalUserRepository / PortalUserRepository
|
||||
- [ ] 네이버 OAuth Handler 구현
|
||||
- [ ] 카카오·구글 패키지 추가 및 설정
|
||||
- [ ] 기본 계정 회원가입 폼 (`/taxbaik/portal/register`)
|
||||
- [ ] 소셜 로그인 콜백 처리 → portal_users 자동 생성
|
||||
- [ ] 신규 가입 시 clients 테이블 연결 또는 신규 생성
|
||||
- [ ] 포털 로그인 페이지 (`/taxbaik/portal/login`) — 소셜 버튼 + 이메일 폼
|
||||
- [ ] Gitea Secrets에 OAuth 키 추가
|
||||
- [ ] 배포 후 소셜 로그인 3종 E2E 테스트
|
||||
|
||||
|
||||
@@ -1,34 +1,171 @@
|
||||
@page "/portal"
|
||||
@model TaxBaik.Web.Pages.Portal.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "고객 포털";
|
||||
ViewData["Description"] = "고객이 신고 일정, 상담 요약, 중요 알림을 확인하는 전용 포털입니다.";
|
||||
ViewData["CanonicalUrl"] = $"{Request.Scheme}://{Request.Host}/taxbaik/portal";
|
||||
ViewData["Title"] = "마이 포털 - 세무사 백원숙";
|
||||
ViewData["Description"] = "고객님의 세무 신고 일정과 상담 이력을 실시간으로 확인하실 수 있는 마이페이지입니다.";
|
||||
}
|
||||
|
||||
<section class="container py-5">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-7">
|
||||
<p class="text-uppercase text-muted small mb-2">Portal</p>
|
||||
<h1 class="display-6 fw-bold mb-3">고객 포털</h1>
|
||||
<p class="lead text-muted mb-4">
|
||||
신고 일정, 상담 요약, 승인된 알림을 확인할 수 있는 전용 공간입니다.
|
||||
</p>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<a class="btn btn-dark" href="/taxbaik/portal/login">로그인</a>
|
||||
<a class="btn btn-outline-dark" href="/taxbaik/portal/register">회원가입</a>
|
||||
<div class="bg-light py-5">
|
||||
<div class="container">
|
||||
<!-- 상단 헤더 & 환영 문구 -->
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center mb-5 pb-4 border-bottom">
|
||||
<div>
|
||||
<p class="text-primary fw-bold mb-1">TaxBaik My Portal</p>
|
||||
<h1 class="display-6 fw-bold text-dark">안녕하세요, @(User.Identity?.Name)님!</h1>
|
||||
@if (Model.ClientInfo != null)
|
||||
{
|
||||
<p class="text-muted mb-0">
|
||||
<i class="bi bi-building"></i> @(string.IsNullOrEmpty(Model.ClientInfo.CompanyName) ? "개인 고객" : Model.ClientInfo.CompanyName)
|
||||
| <i class="bi bi-telephone"></i> @Model.ClientInfo.Phone
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<div class="mt-3 mt-sm-0">
|
||||
<form method="post" action="/taxbaik/portal/logout" class="d-inline">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||
<i class="bi bi-box-arrow-right"></i> 로그아웃
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="p-4 bg-light border rounded-3">
|
||||
<h2 class="h5 fw-bold mb-3">제공 예정 기능</h2>
|
||||
<ul class="mb-0 text-muted">
|
||||
<li>본인 신고 일정 확인</li>
|
||||
<li>상담 요약 열람</li>
|
||||
<li>중요 알림 수신</li>
|
||||
<li>관리자 승인 범위 내 정보 제공</li>
|
||||
</ul>
|
||||
|
||||
@if (Model.ClientInfo == null)
|
||||
{
|
||||
<!-- 연동 대기 경고 -->
|
||||
<div class="card border-warning shadow-sm mb-5">
|
||||
<div class="card-body p-5 text-center">
|
||||
<div class="mb-4">
|
||||
<span class="display-1 text-warning"><i class="bi bi-exclamation-triangle-fill"></i></span>
|
||||
</div>
|
||||
<h3 class="fw-bold text-dark mb-3">고객 정보 연동 대기 중</h3>
|
||||
<p class="text-muted max-width-md mx-auto mb-4">
|
||||
가입하신 계정 정보(이메일/연락처)와 일치하는 세무 대리 고객 레코드를 찾지 못했습니다.<br />
|
||||
세무사 측에서 고객 등록을 완료하거나 관리자 백오피스에서 이메일/전화번호가 일치하도록 지정하면 자동으로 포털 데이터가 활성화됩니다.
|
||||
</p>
|
||||
<a href="/taxbaik/contact" class="btn btn-primary px-4 py-2">
|
||||
<i class="bi bi-chat-dots"></i> 세무사에게 문의하기
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row g-4">
|
||||
<!-- 왼쪽: 세무 신고 현황 (Tax Filings) -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0 shadow-sm rounded-3 mb-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="h5 fw-bold text-dark mb-0">
|
||||
<i class="bi bi-calendar-check text-primary me-2"></i> 나의 세무 신고 현황
|
||||
</h3>
|
||||
<span class="badge bg-secondary">총 @(Model.Filings.Count)건</span>
|
||||
</div>
|
||||
|
||||
@if (!Model.Filings.Any())
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-folder-x display-4 d-block mb-3 text-secondary"></i>
|
||||
등록된 세무 신고 일정이 없습니다.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col">신고 종류</th>
|
||||
<th scope="col">신고 기한</th>
|
||||
<th scope="col">진행 상태</th>
|
||||
<th scope="col">메모</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var filing in Model.Filings)
|
||||
{
|
||||
var dDay = (filing.DueDate - DateTime.Today).Days;
|
||||
var statusClass = filing.Status switch
|
||||
{
|
||||
"filed" => "bg-success-subtle text-success",
|
||||
"overdue" => "bg-danger-subtle text-danger",
|
||||
_ => "bg-warning-subtle text-warning-emphasis"
|
||||
};
|
||||
var statusLabel = filing.Status switch
|
||||
{
|
||||
"filed" => "신고 완료",
|
||||
"overdue" => "기한 초과",
|
||||
_ => $"D-{dDay}"
|
||||
};
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<span class="fw-bold text-dark">@filing.FilingType</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>@filing.DueDate.ToString("yyyy-MM-dd")</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge @statusClass px-2.5 py-1.5 fs-7">@statusLabel</span>
|
||||
</td>
|
||||
<td class="text-muted small">
|
||||
@(string.IsNullOrEmpty(filing.Memo) ? "-" : filing.Memo)
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 오른쪽: 상담 이력 요약 (Consulting Activities) -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 shadow-sm rounded-3">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="h5 fw-bold text-dark mb-4">
|
||||
<i class="bi bi-chat-text text-primary me-2"></i> 최근 상담 및 지원 이력
|
||||
</h3>
|
||||
|
||||
@if (!Model.Consultations.Any())
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-chat-square-dots display-4 d-block mb-3 text-secondary"></i>
|
||||
최근 상담 이력이 없습니다.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="timeline">
|
||||
@foreach (var activity in Model.Consultations)
|
||||
{
|
||||
<div class="border-start border-2 border-primary-subtle ps-3 pb-4 position-relative">
|
||||
<!-- 타임라인 아이콘 -->
|
||||
<div class="position-absolute start-0 translate-middle-x bg-primary rounded-circle"
|
||||
style="width: 10px; height: 10px; margin-left: -1px; top: 6px;"></div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="badge bg-primary-subtle text-primary small">@activity.ActivityType</span>
|
||||
<small class="text-muted">@activity.ActivityDate.ToString("yyyy-MM-dd")</small>
|
||||
</div>
|
||||
<p class="text-dark small mb-1 fw-semibold">@activity.Description</p>
|
||||
@if (!string.IsNullOrEmpty(activity.Outcome))
|
||||
{
|
||||
<div class="bg-light p-2 rounded small text-muted mt-1">
|
||||
<strong>결과:</strong> @activity.Outcome
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using TaxBaik.Application.Services;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Web.Services;
|
||||
|
||||
namespace TaxBaik.Web.Pages.Portal;
|
||||
@@ -7,7 +11,39 @@ namespace TaxBaik.Web.Pages.Portal;
|
||||
[Authorize(AuthenticationSchemes = PortalAuthDefaults.Scheme)]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
private readonly TaxFilingService _taxFilingService;
|
||||
private readonly ConsultingActivityService _consultingActivityService;
|
||||
private readonly ClientService _clientService;
|
||||
|
||||
public IndexModel(
|
||||
TaxFilingService taxFilingService,
|
||||
ConsultingActivityService consultingActivityService,
|
||||
ClientService clientService)
|
||||
{
|
||||
_taxFilingService = taxFilingService;
|
||||
_consultingActivityService = consultingActivityService;
|
||||
_clientService = clientService;
|
||||
}
|
||||
|
||||
public Client? ClientInfo { get; private set; }
|
||||
public List<TaxFiling> Filings { get; private set; } = new();
|
||||
public List<ConsultingActivity> Consultations { get; private set; } = new();
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var clientIdClaim = User.FindFirst("client_id");
|
||||
if (clientIdClaim != null && int.TryParse(clientIdClaim.Value, out var clientId))
|
||||
{
|
||||
ClientInfo = await _clientService.GetByIdAsync(clientId);
|
||||
if (ClientInfo != null)
|
||||
{
|
||||
var filingsData = await _taxFilingService.GetByClientIdAsync(clientId);
|
||||
Filings = filingsData.OrderBy(f => f.DueDate).ToList();
|
||||
|
||||
var consultationsData = await _consultingActivityService.GetByClientIdAsync(clientId);
|
||||
Consultations = consultationsData.OrderByDescending(c => c.ActivityDate).ToList();
|
||||
}
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,5 +52,5 @@ public class LoginModel : PageModel
|
||||
public IActionResult OnPostKakao() => Challenge(BuildProps("kakao"), PortalOAuthDefaults.KakaoScheme);
|
||||
|
||||
private static AuthenticationProperties BuildProps(string provider) =>
|
||||
new() { RedirectUri = $"/taxbaik/portal/external-callback?provider={provider}" };
|
||||
new() { RedirectUri = $"/portal/external-callback?provider={provider}" };
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ var authenticationBuilder = builder.Services.AddAuthentication(opts =>
|
||||
opts.Cookie.HttpOnly = true;
|
||||
opts.Cookie.SameSite = SameSiteMode.Lax;
|
||||
opts.Cookie.SecurePolicy = isProduction ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
|
||||
opts.LoginPath = "/taxbaik/portal/login";
|
||||
opts.AccessDeniedPath = "/taxbaik/portal/login";
|
||||
opts.LoginPath = "/portal/login";
|
||||
opts.AccessDeniedPath = "/portal/login";
|
||||
opts.SlidingExpiration = true;
|
||||
opts.ExpireTimeSpan = TimeSpan.FromDays(7);
|
||||
})
|
||||
@@ -111,7 +111,7 @@ if (!string.IsNullOrWhiteSpace(googleClientId) && !string.IsNullOrWhiteSpace(goo
|
||||
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
||||
opts.ClientId = googleClientId;
|
||||
opts.ClientSecret = googleClientSecret;
|
||||
opts.CallbackPath = "/taxbaik/portal/signin-google";
|
||||
opts.CallbackPath = "/portal/signin-google";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ if (!string.IsNullOrWhiteSpace(naverClientId) && !string.IsNullOrWhiteSpace(nave
|
||||
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
||||
opts.ClientId = naverClientId;
|
||||
opts.ClientSecret = naverClientSecret;
|
||||
opts.CallbackPath = "/taxbaik/portal/signin-naver";
|
||||
opts.CallbackPath = "/portal/signin-naver";
|
||||
opts.AuthorizationEndpoint = "https://nid.naver.com/oauth2.0/authorize";
|
||||
opts.TokenEndpoint = "https://nid.naver.com/oauth2.0/token";
|
||||
opts.UserInformationEndpoint = "https://openapi.naver.com/v1/nid/me";
|
||||
@@ -156,7 +156,7 @@ if (!string.IsNullOrWhiteSpace(kakaoClientId) && !string.IsNullOrWhiteSpace(kaka
|
||||
opts.SignInScheme = PortalOAuthDefaults.ExternalScheme;
|
||||
opts.ClientId = kakaoClientId;
|
||||
opts.ClientSecret = kakaoClientSecret;
|
||||
opts.CallbackPath = "/taxbaik/portal/signin-kakao";
|
||||
opts.CallbackPath = "/portal/signin-kakao";
|
||||
opts.AuthorizationEndpoint = "https://kauth.kakao.com/oauth/authorize";
|
||||
opts.TokenEndpoint = "https://kauth.kakao.com/oauth/token";
|
||||
opts.UserInformationEndpoint = "https://kapi.kakao.com/v2/user/me";
|
||||
|
||||
@@ -411,11 +411,41 @@ textarea:focus-visible {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.admin-shell .mud-typography--h4 {
|
||||
font-size: 1.35rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.admin-shell .mud-typography--h6 {
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.admin-shell .mud-typography--subtitle1 {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.admin-shell .mud-typography--body1 {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.admin-shell .mud-typography--body2 {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.admin-shell .mud-typography--caption {
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.admin-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
padding: 8px 20px;
|
||||
gap: 12px;
|
||||
padding: 6px 16px;
|
||||
background-color: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
z-index: var(--z-dropdown);
|
||||
@@ -429,7 +459,7 @@ textarea:focus-visible {
|
||||
.admin-topbar-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.admin-topbar-title span {
|
||||
@@ -437,20 +467,25 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.admin-topbar-title .mud-typography--h6 {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.2;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.15;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.admin-topbar-action {
|
||||
white-space: nowrap;
|
||||
min-height: 36px;
|
||||
padding: 6px 12px;
|
||||
font-size: 0.875rem;
|
||||
min-height: 32px;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.admin-shell .mud-button-root {
|
||||
min-height: 32px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.admin-drawer {
|
||||
width: 224px;
|
||||
width: 208px;
|
||||
background-color: var(--bg-primary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
@@ -460,8 +495,8 @@ textarea:focus-visible {
|
||||
.admin-drawer-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: 12px;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border-color-light);
|
||||
}
|
||||
|
||||
@@ -469,37 +504,37 @@ textarea:focus-visible {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
|
||||
color: var(--primary-contrast);
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: 1.125rem;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-nav {
|
||||
padding: 6px 0;
|
||||
padding: 4px 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.admin-nav .mud-nav-link,
|
||||
.admin-nav .mud-nav-group-header {
|
||||
margin: 1px 8px !important;
|
||||
margin: 1px 6px !important;
|
||||
border-radius: 6px !important;
|
||||
transition: all var(--transition-base) !important;
|
||||
}
|
||||
|
||||
.admin-nav .mud-nav-link {
|
||||
min-height: 36px;
|
||||
font-size: 0.85rem;
|
||||
min-height: 32px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.admin-nav .mud-nav-group-header {
|
||||
min-height: 36px;
|
||||
font-size: 0.85rem;
|
||||
min-height: 32px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.admin-nav .mud-nav-link:hover {
|
||||
@@ -543,7 +578,7 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
@@ -557,9 +592,9 @@ textarea:focus-visible {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
@@ -572,8 +607,8 @@ textarea:focus-visible {
|
||||
color: var(--primary-color);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
text-transform: uppercase;
|
||||
font-size: var(--font-size-xs);
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0;
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
@@ -581,16 +616,16 @@ textarea:focus-visible {
|
||||
display: block;
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin-bottom: 4px;
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 2px;
|
||||
font-size: 1.45rem;
|
||||
line-height: var(--line-height-tight);
|
||||
}
|
||||
|
||||
.admin-page-subtitle {
|
||||
display: block;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
line-height: var(--line-height-normal);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* Metrics Grid */
|
||||
@@ -604,7 +639,7 @@ textarea:focus-visible {
|
||||
|
||||
/* Metric Card - Enterprise Grade */
|
||||
.admin-metric-card {
|
||||
padding: 12px;
|
||||
padding: 10px;
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -613,12 +648,52 @@ textarea:focus-visible {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 128px;
|
||||
min-height: 116px;
|
||||
box-shadow: var(--shadow-xs);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-metric-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.admin-metric-card-label {
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.admin-metric-card-value-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-metric-card-value {
|
||||
font-size: 1.45rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.admin-metric-card-icon {
|
||||
font-size: 1.9rem;
|
||||
opacity: 0.14;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.admin-metric-card-caption {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.admin-metric-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -692,11 +767,11 @@ textarea:focus-visible {
|
||||
|
||||
/* Surfaces & Containers */
|
||||
.admin-surface {
|
||||
padding: 12px !important;
|
||||
padding: 10px !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
background-color: var(--bg-primary) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
margin-bottom: 12px !important;
|
||||
margin-bottom: 10px !important;
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
|
||||
@@ -704,9 +779,9 @@ textarea:focus-visible {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border-color-light);
|
||||
}
|
||||
|
||||
@@ -715,14 +790,14 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.admin-section-header h6 {
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.admin-section-header p {
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
@@ -731,7 +806,7 @@ textarea:focus-visible {
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.admin-table thead {
|
||||
@@ -740,13 +815,13 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.admin-table thead th {
|
||||
padding: 6px 10px;
|
||||
padding: 5px 8px;
|
||||
text-align: left;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.admin-table tbody tr {
|
||||
@@ -763,11 +838,16 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.admin-table tbody td {
|
||||
padding: 6px 10px;
|
||||
padding: 5px 8px;
|
||||
color: var(--text-primary);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.admin-table .mud-chip {
|
||||
font-size: 0.68rem;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.admin-table tbody a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
|
||||
Reference in New Issue
Block a user