style(comp): polish UI quality with 1-click clear, LOT pulse expiry badges, and VAT 10% auto-summary
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 11s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 11s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 19s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 9s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 9s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 11s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 11s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 19s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 9s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 9s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
<!-- Business Composite Component Layer: OrderLineEditor (WBS-COMP-4.3) -->
|
||||
<template>
|
||||
<div class="business-order-line-editor border border-slate-300 rounded-md bg-white overflow-hidden shadow-sm flex flex-col gap-2">
|
||||
<div class="business-order-line-editor border border-slate-300 rounded-md bg-white overflow-hidden shadow-sm flex flex-col gap-2 select-none">
|
||||
<!-- Editor Header Toolbar -->
|
||||
<div class="bg-slate-100 p-2.5 border-b border-slate-300 flex justify-between items-center text-xs">
|
||||
<div class="flex items-center gap-2 font-bold text-slate-800">
|
||||
<span>📦 주문 품목 라인 인라인 편집기</span>
|
||||
<span>📦 주문 품목 라인 고성능 인라인 편집기</span>
|
||||
<span class="px-2 py-0.5 rounded bg-blue-100 text-blue-800 font-mono">총 {{ lines.length }}개 라인</span>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<button type="button" class="bg-emerald-600 hover:bg-emerald-700 text-white font-bold px-2.5 py-1 rounded transition" @click="addLine">
|
||||
+ 행 추가
|
||||
<div class="flex gap-1.5">
|
||||
<button type="button" class="bg-blue-600 hover:bg-blue-700 text-white font-bold px-3 py-1 rounded transition shadow-xs flex items-center gap-1" @click="addLine">
|
||||
<span>+ 행 추가</span>
|
||||
</button>
|
||||
<button type="button" class="bg-rose-100 hover:bg-rose-200 text-rose-800 font-bold px-2 py-1 rounded transition" @click="clearSelected">
|
||||
선택 삭제
|
||||
<button type="button" class="bg-rose-100 hover:bg-rose-200 text-rose-800 font-bold px-2.5 py-1 rounded transition flex items-center gap-1" @click="clearSelected">
|
||||
<span>선택 삭제</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,27 +28,31 @@
|
||||
<th class="p-2 border-r text-right">수량</th>
|
||||
<th class="p-2 border-r text-right">단가 (KRW)</th>
|
||||
<th class="p-2 border-r text-right">공급가액 (KRW)</th>
|
||||
<th class="p-2 border-r text-right">VAT (10%)</th>
|
||||
<th class="p-2 text-center w-16">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(line, idx) in lines" :key="line.id" class="border-b hover:bg-slate-50">
|
||||
<tr v-for="(line, idx) in lines" :key="line.id" class="border-b hover:bg-blue-50/40 transition">
|
||||
<td class="p-2 border-r text-center"><input type="checkbox" v-model="line.selected" /></td>
|
||||
<td class="p-2 border-r font-mono">
|
||||
<input type="text" v-model="line.itemCode" class="w-full px-1.5 py-1 border rounded font-mono font-bold text-blue-700" />
|
||||
<input type="text" v-model="line.itemCode" class="w-full px-2 py-1 border border-slate-300 rounded font-mono font-bold text-slate-900 bg-white focus:ring-1 focus:ring-blue-500 uppercase" />
|
||||
</td>
|
||||
<td class="p-2 border-r">
|
||||
<input type="text" v-model="line.itemName" class="w-full px-1.5 py-1 border rounded" />
|
||||
<input type="text" v-model="line.itemName" class="w-full px-2 py-1 border border-slate-300 rounded text-slate-900 font-medium bg-white focus:ring-1 focus:ring-blue-500" />
|
||||
</td>
|
||||
<td class="p-2 border-r text-right font-mono">
|
||||
<input type="number" v-model.number="line.quantity" class="w-20 text-right px-1.5 py-1 border rounded font-mono" @input="recalculateLine(line)" />
|
||||
<input type="number" v-model.number="line.quantity" class="w-20 text-right px-2 py-1 border border-slate-300 rounded font-mono font-bold text-slate-900 bg-white focus:ring-1 focus:ring-blue-500" @input="recalculateLine(line)" />
|
||||
</td>
|
||||
<td class="p-2 border-r text-right font-mono">
|
||||
<input type="number" v-model.number="line.price" class="w-24 text-right px-1.5 py-1 border rounded font-mono" @input="recalculateLine(line)" />
|
||||
<input type="number" v-model.number="line.price" class="w-28 text-right px-2 py-1 border border-slate-300 rounded font-mono font-bold text-slate-900 bg-white focus:ring-1 focus:ring-blue-500" @input="recalculateLine(line)" />
|
||||
</td>
|
||||
<td class="p-2 border-r text-right font-mono font-bold text-slate-800">
|
||||
<td class="p-2 border-r text-right font-mono font-bold text-slate-900">
|
||||
{{ line.amount.toLocaleString() }}
|
||||
</td>
|
||||
<td class="p-2 border-r text-right font-mono text-slate-500">
|
||||
{{ Math.floor(line.amount * 0.1).toLocaleString() }}
|
||||
</td>
|
||||
<td class="p-2 text-center">
|
||||
<button type="button" class="text-rose-600 font-bold hover:underline" @click="removeLine(idx)">삭제</button>
|
||||
</td>
|
||||
@@ -59,9 +63,13 @@
|
||||
|
||||
<!-- Total Summary Strip -->
|
||||
<div class="bg-slate-50 p-2.5 border-t border-slate-300 flex justify-between items-center text-xs">
|
||||
<span class="text-slate-500">라인 재계산: <strong class="text-emerald-700">실시간 유효성 검증 완료</strong></span>
|
||||
<div class="font-bold text-slate-900">
|
||||
합계 금액: <span class="text-blue-700 font-mono text-sm ml-1">{{ totalAmountSummary.toLocaleString() }} KRW</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-slate-600">라인 실시간 상태: <strong class="text-emerald-700">✓ 자동 연산 정합성 보장</strong></span>
|
||||
<span class="text-slate-400">|</span>
|
||||
<span class="text-slate-600">부가세 합계: <strong class="font-mono text-slate-800">{{ totalVatSummary.toLocaleString() }} KRW</strong></span>
|
||||
</div>
|
||||
<div class="font-bold text-slate-900 text-sm">
|
||||
총 합계 금액 (VAT 포함): <span class="text-blue-700 font-mono text-base ml-1 font-extrabold">{{ totalWithVatSummary.toLocaleString() }} KRW</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,6 +103,14 @@ const totalAmountSummary = computed(() => {
|
||||
return lines.value.reduce((sum, item) => sum + (item.amount || 0), 0);
|
||||
});
|
||||
|
||||
const totalVatSummary = computed(() => {
|
||||
return Math.floor(totalAmountSummary.value * 0.1);
|
||||
});
|
||||
|
||||
const totalWithVatSummary = computed(() => {
|
||||
return totalAmountSummary.value + totalVatSummary.value;
|
||||
});
|
||||
|
||||
const recalculateLine = (line: OrderLineItem) => {
|
||||
line.amount = (line.quantity || 0) * (line.price || 0);
|
||||
emit('update:lines', lines.value);
|
||||
|
||||
@@ -1,38 +1,40 @@
|
||||
<!-- Domain Field Component Layer: LotField (WBS-COMP-3.4) -->
|
||||
<template>
|
||||
<div class="domain-lot-field flex flex-col gap-1 border border-slate-200 p-2.5 rounded-md bg-white">
|
||||
<div class="domain-lot-field flex flex-col gap-1 w-full bg-slate-50 p-2.5 rounded-md border border-slate-300">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<label class="font-bold text-slate-700">
|
||||
{{ label || '로트(LOT) 번호 및 FEFO 관리' }} <span v-if="required" class="text-red-500">*</span>
|
||||
<label class="font-bold text-slate-800 flex items-center gap-1">
|
||||
<span>🏷️ LOT 번호 / FEFO 유효기간</span>
|
||||
<span v-if="required" class="text-rose-600">*</span>
|
||||
</label>
|
||||
<span :class="['text-[10px] px-2 py-0.5 rounded font-bold font-mono', isExpired ? 'bg-rose-100 text-rose-800' : 'bg-emerald-100 text-emerald-800']">
|
||||
{{ isExpired ? '🚨 로트 만료' : '✓ 출고 가능 (FEFO)' }}
|
||||
<span v-if="remainingDays !== null" :class="['px-2 py-0.5 rounded text-[11px] font-bold font-mono shadow-xs', expiryStatusClass]">
|
||||
{{ expiryStatusText }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-1">
|
||||
<input
|
||||
type="text"
|
||||
:value="lotNumber"
|
||||
placeholder="LOT-20260726-001"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
class="flex-1 h-9 px-3 border border-slate-300 rounded font-mono text-xs text-slate-800 font-bold focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
@input="handleLotInput"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
:value="expirationDate"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
class="h-9 px-2 border border-slate-300 rounded bg-slate-50 text-xs font-mono font-bold text-slate-700"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center text-[11px] text-slate-500 mt-0.5">
|
||||
<span>권장 출고 순위: <strong class="text-blue-700 font-mono">FEFO #1 (유효기간 상위)</strong></span>
|
||||
<span>유효기간: <strong class="font-mono text-slate-700">{{ expirationDate || '미지정' }}</strong></span>
|
||||
<div class="grid grid-cols-2 gap-2 mt-1">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-[10px] font-bold text-slate-500">LOT 배치 번호</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="lotNumber"
|
||||
placeholder="LOT-20260725-01"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
class="h-8 px-2 border border-slate-300 rounded font-mono text-xs text-slate-900 font-bold bg-white focus:ring-2 focus:ring-blue-500 uppercase"
|
||||
@input="handleLotInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-[10px] font-bold text-slate-500">유효기간 (Expiration Date)</span>
|
||||
<input
|
||||
type="date"
|
||||
:value="expirationDate"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
class="h-8 px-2 border border-slate-300 rounded font-mono text-xs text-slate-900 font-bold bg-white focus:ring-2 focus:ring-blue-500"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -41,7 +43,6 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
label?: string;
|
||||
lotNumber: string;
|
||||
expirationDate: string;
|
||||
required?: boolean;
|
||||
@@ -51,15 +52,31 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits(['update:lotNumber', 'update:expirationDate']);
|
||||
|
||||
const isExpired = computed(() => {
|
||||
if (!props.expirationDate) return false;
|
||||
const remainingDays = computed(() => {
|
||||
if (!props.expirationDate) return null;
|
||||
const exp = new Date(props.expirationDate).getTime();
|
||||
const today = new Date().getTime();
|
||||
return exp < today;
|
||||
const diff = Math.ceil((exp - today) / (1000 * 3600 * 24));
|
||||
return diff;
|
||||
});
|
||||
|
||||
const expiryStatusClass = computed(() => {
|
||||
if (remainingDays.value === null) return 'bg-slate-100 text-slate-700';
|
||||
if (remainingDays.value <= 0) return 'bg-rose-600 text-white animate-pulse';
|
||||
if (remainingDays.value <= 30) return 'bg-rose-100 text-rose-800 border border-rose-300 font-bold';
|
||||
if (remainingDays.value <= 90) return 'bg-amber-100 text-amber-800 border border-amber-300 font-bold';
|
||||
return 'bg-emerald-100 text-emerald-800 border border-emerald-300 font-bold';
|
||||
});
|
||||
|
||||
const expiryStatusText = computed(() => {
|
||||
if (remainingDays.value === null) return '유효기간 미지정';
|
||||
if (remainingDays.value <= 0) return `🚨 만료됨 (D+${Math.abs(remainingDays.value.valueOf())})`;
|
||||
if (remainingDays.value <= 30) return `⚠️ 출하임계 (D-${remainingDays.value})`;
|
||||
return `✓ 안전 (D-${remainingDays.value})`;
|
||||
});
|
||||
|
||||
const handleLotInput = (e: Event) => {
|
||||
emit('update:lotNumber', (e.target as HTMLInputElement).value);
|
||||
emit('update:lotNumber', (e.target as HTMLInputElement).value.toUpperCase());
|
||||
};
|
||||
|
||||
const handleDateChange = (e: Event) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- Primitive UI Components Layer -->
|
||||
<!-- Primitive UI Components Layer: TextInput -->
|
||||
<template>
|
||||
<div class="primitive-input-wrapper w-full">
|
||||
<div class="primitive-input-wrapper relative w-full flex items-center">
|
||||
<input
|
||||
:id="id"
|
||||
:type="type || 'text'"
|
||||
@@ -8,9 +8,18 @@
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
class="primitive-input w-full h-9 px-3 border border-slate-300 rounded text-xs text-slate-900 font-medium bg-white placeholder:text-slate-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none disabled:bg-slate-100 disabled:text-slate-500 disabled:cursor-not-allowed readonly:bg-slate-50 readonly:text-slate-700"
|
||||
class="primitive-input w-full h-9 pl-3 pr-8 border border-slate-300 rounded text-xs text-slate-900 font-medium bg-white placeholder:text-slate-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none disabled:bg-slate-100 disabled:text-slate-500 disabled:cursor-not-allowed readonly:bg-slate-50 readonly:text-slate-700 transition shadow-xs"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="modelValue && !disabled && !readonly"
|
||||
type="button"
|
||||
class="absolute right-2 text-slate-400 hover:text-slate-600 text-xs font-bold w-4 h-4 rounded-full flex items-center justify-center bg-slate-100 hover:bg-slate-200"
|
||||
title="Clear text"
|
||||
@click="$emit('update:modelValue', '')"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user