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>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Detail Panel -->
|
<!-- Detail Panel (EditForm Pattern) -->
|
||||||
<template #detail>
|
<template #detail>
|
||||||
<div class="detail-panel" aria-label="선택 요청 상세 및 검토">
|
<form v-if="selectedRequest" class="approval-form" @submit.prevent="handleApprove">
|
||||||
<!-- Header with Actions (fixed at top) -->
|
<!-- Header: Read-only Title -->
|
||||||
<div v-if="selectedRequest" class="detail-header">
|
<div class="form-header">
|
||||||
<h2>{{ selectedRequest.modelName }}</h2>
|
<h2>{{ selectedRequest.modelName }}</h2>
|
||||||
<div v-if="canApprove || canReject" class="header-actions">
|
<small class="form-meta">{{ getActionLabel(selectedRequest.action) }}</small>
|
||||||
<KsButton
|
|
||||||
label="Approve"
|
|
||||||
variant="primary"
|
|
||||||
:disabled="!canApprove"
|
|
||||||
@click="handleApprove"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<KsButton
|
|
||||||
label="Reject"
|
|
||||||
variant="secondary"
|
|
||||||
:disabled="!canReject"
|
|
||||||
@click="handleReject"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-detail">Select a request from list</div>
|
|
||||||
|
|
||||||
<!-- Scrollable Content -->
|
<!-- Content: Read-only Info + Form Input -->
|
||||||
<div v-if="selectedRequest" class="request-detail">
|
<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 -->
|
<!-- Request Info -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Request Details</h3>
|
<h3>Request Details</h3>
|
||||||
@@ -494,7 +575,8 @@ h3 {
|
|||||||
color: var(--color-text-tertiary);
|
color: var(--color-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-panel {
|
/* Approval Form (EditForm Pattern) */
|
||||||
|
.approval-form {
|
||||||
border: var(--border-width-1) solid var(--color-border-primary);
|
border: var(--border-width-1) solid var(--color-border-primary);
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
background: var(--color-background-secondary);
|
background: var(--color-background-secondary);
|
||||||
@@ -502,43 +584,97 @@ h3 {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-header {
|
.form-header {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--spacing-2);
|
|
||||||
padding: var(--spacing-3);
|
padding: var(--spacing-3);
|
||||||
border-bottom: var(--border-width-1) solid var(--color-border-secondary);
|
border-bottom: var(--border-width-1) solid var(--color-border-secondary);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-header h2 {
|
.form-header h2 {
|
||||||
flex: 1;
|
margin: 0 0 4px 0;
|
||||||
margin: 0;
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.form-meta {
|
||||||
display: flex;
|
font-size: var(--font-size-xs);
|
||||||
gap: var(--spacing-2);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-detail {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--spacing-8);
|
|
||||||
color: var(--color-text-tertiary);
|
color: var(--color-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.request-detail {
|
.form-content {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-2);
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: var(--spacing-3);
|
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 {
|
.section {
|
||||||
|
|||||||
Reference in New Issue
Block a user