refactor(fe): ApprovalQueue - EditFormPage pattern
- Restructure detail panel as approval form (EditFormPage pattern) - Separate concerns: Header (read-only) + Content (form) + Footer (actions) - Use form/fieldset semantics for read-only vs editable sections - Request Details and Metrics as disabled fieldsets (read-only) - Review Comment as textarea form input - Footer buttons (Approve/Reject) with validation - CSS refactor: approval-form, form-header/content/footer structure - Improved accessibility and semantic HTML Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -200,33 +200,114 @@ const handleRetry = () => {
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<!-- Detail Panel -->
|
||||
<!-- Detail Panel (EditForm Pattern) -->
|
||||
<template #detail>
|
||||
<div class="detail-panel" aria-label="선택 요청 상세 및 검토">
|
||||
<!-- Header with Actions (fixed at top) -->
|
||||
<div v-if="selectedRequest" class="detail-header">
|
||||
<form v-if="selectedRequest" class="approval-form" @submit.prevent="handleApprove">
|
||||
<!-- Header: Read-only Title -->
|
||||
<div class="form-header">
|
||||
<h2>{{ selectedRequest.modelName }}</h2>
|
||||
<div v-if="canApprove || canReject" class="header-actions">
|
||||
<KsButton
|
||||
label="Approve"
|
||||
variant="primary"
|
||||
:disabled="!canApprove"
|
||||
@click="handleApprove"
|
||||
size="sm"
|
||||
/>
|
||||
<KsButton
|
||||
label="Reject"
|
||||
variant="secondary"
|
||||
:disabled="!canReject"
|
||||
@click="handleReject"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<small class="form-meta">{{ getActionLabel(selectedRequest.action) }}</small>
|
||||
</div>
|
||||
<div v-else class="empty-detail">Select a request from list</div>
|
||||
|
||||
<!-- Scrollable Content -->
|
||||
<div v-if="selectedRequest" class="request-detail">
|
||||
<!-- Content: Read-only Info + Form Input -->
|
||||
<div class="form-content">
|
||||
<!-- Request Details (read-only) -->
|
||||
<fieldset class="form-section" disabled>
|
||||
<legend>Request Details</legend>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="label">Request ID</span>
|
||||
<span class="value ks-financial-number">{{ selectedRequest.requestId }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Action</span>
|
||||
<span class="value">{{ getActionLabel(selectedRequest.action) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Requested By</span>
|
||||
<span class="value">{{ selectedRequest.requesterName }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Requested At</span>
|
||||
<span class="value ks-financial-number">{{ formatDate(selectedRequest.requestedAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Validation Metrics (read-only) -->
|
||||
<fieldset v-if="selectedRequest.metadata.pbo" class="form-section" disabled>
|
||||
<legend>Validation Metrics</legend>
|
||||
<div class="metric-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">PBO</div>
|
||||
<div class="metric-value ks-financial-number">{{ selectedRequest.metadata.pbo?.toFixed(2) }}%</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">DSR</div>
|
||||
<div class="metric-value ks-financial-number">{{ selectedRequest.metadata.dsr?.toFixed(2) }}%</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">OOS</div>
|
||||
<div class="metric-value ks-financial-number">{{ selectedRequest.metadata.oos?.toFixed(2) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Review Comment (form input) -->
|
||||
<fieldset v-if="canApprove || canReject" class="form-section">
|
||||
<legend>Review & Approval Comment</legend>
|
||||
<textarea
|
||||
v-model="reviewComment"
|
||||
placeholder="Enter your review comment for audit trail..."
|
||||
class="form-textarea"
|
||||
aria-label="승인 심사 의견"
|
||||
required
|
||||
></textarea>
|
||||
</fieldset>
|
||||
|
||||
<!-- Review History (read-only) -->
|
||||
<fieldset v-if="selectedRequest.reviewedAt" class="form-section" disabled>
|
||||
<legend>Review History</legend>
|
||||
<div class="history-item">
|
||||
<div class="history-header">
|
||||
<strong>{{ selectedRequest.reviewerName }}</strong>
|
||||
<KsStatusTag
|
||||
:value="selectedRequest.status.toUpperCase()"
|
||||
:severity="selectedRequest.status === 'approved' ? 'success' : 'danger'"
|
||||
/>
|
||||
</div>
|
||||
<div class="history-date ks-financial-number">{{ formatDate(selectedRequest.reviewedAt) }}</div>
|
||||
<div v-if="selectedRequest.reviewComment" class="history-comment">
|
||||
{{ selectedRequest.reviewComment }}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<!-- Footer: Action Buttons -->
|
||||
<div v-if="canApprove || canReject" class="form-footer">
|
||||
<KsButton
|
||||
label="Approve [승인]"
|
||||
variant="primary"
|
||||
:disabled="!canApprove || !reviewComment.trim()"
|
||||
type="submit"
|
||||
/>
|
||||
<KsButton
|
||||
label="Reject [반려]"
|
||||
variant="secondary"
|
||||
:disabled="!canReject || !reviewComment.trim()"
|
||||
@click.prevent="handleReject"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="empty-state">
|
||||
<p>Select a request from list to review and approve</p>
|
||||
</div>
|
||||
|
||||
<!-- Request Details (old content - DELETE) -->
|
||||
<div v-if="selectedRequest" class="request-detail" style="display: none;">
|
||||
<!-- Request Info -->
|
||||
<div class="section">
|
||||
<h3>Request Details</h3>
|
||||
@@ -494,7 +575,8 @@ h3 {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
/* Approval Form (EditForm Pattern) */
|
||||
.approval-form {
|
||||
border: var(--border-width-1) solid var(--color-border-primary);
|
||||
border-radius: var(--border-radius-lg);
|
||||
background: var(--color-background-secondary);
|
||||
@@ -502,43 +584,97 @@ h3 {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-2);
|
||||
.form-header {
|
||||
padding: var(--spacing-3);
|
||||
border-bottom: var(--border-width-1) solid var(--color-border-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-header h2 {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
.form-header h2 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.empty-detail {
|
||||
text-align: center;
|
||||
padding: var(--spacing-8);
|
||||
.form-meta {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.request-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
.form-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-section:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.form-section legend {
|
||||
padding: 0 0 6px 0;
|
||||
margin: 0;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: var(--border-width-1) solid var(--color-border-secondary);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
border: var(--border-width-1) solid var(--color-input-border);
|
||||
border-radius: var(--border-radius-base);
|
||||
background: var(--color-input-background);
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-sans);
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
max-height: 80px;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--ks-color-action);
|
||||
box-shadow: 0 0 0 2px var(--ks-color-action);
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-3);
|
||||
border-top: var(--border-width-1) solid var(--color-border-secondary);
|
||||
flex-shrink: 0;
|
||||
background: var(--color-background-primary);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: var(--spacing-8);
|
||||
text-align: center;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.section {
|
||||
|
||||
Reference in New Issue
Block a user