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) -->
|
<!-- Business Composite Component Layer: OrderLineEditor (WBS-COMP-4.3) -->
|
||||||
<template>
|
<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 -->
|
<!-- 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="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">
|
<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>
|
<span class="px-2 py-0.5 rounded bg-blue-100 text-blue-800 font-mono">총 {{ lines.length }}개 라인</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1.5">
|
||||||
<button type="button" class="bg-emerald-600 hover:bg-emerald-700 text-white font-bold px-2.5 py-1 rounded transition" @click="addLine">
|
<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>
|
||||||
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,27 +28,31 @@
|
|||||||
<th class="p-2 border-r text-right">수량</th>
|
<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">공급가액 (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>
|
<th class="p-2 text-center w-16">작업</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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 text-center"><input type="checkbox" v-model="line.selected" /></td>
|
||||||
<td class="p-2 border-r font-mono">
|
<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>
|
||||||
<td class="p-2 border-r">
|
<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>
|
||||||
<td class="p-2 border-r text-right font-mono">
|
<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>
|
||||||
<td class="p-2 border-r text-right font-mono">
|
<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>
|
||||||
<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() }}
|
{{ line.amount.toLocaleString() }}
|
||||||
</td>
|
</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">
|
<td class="p-2 text-center">
|
||||||
<button type="button" class="text-rose-600 font-bold hover:underline" @click="removeLine(idx)">삭제</button>
|
<button type="button" class="text-rose-600 font-bold hover:underline" @click="removeLine(idx)">삭제</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -59,9 +63,13 @@
|
|||||||
|
|
||||||
<!-- Total Summary Strip -->
|
<!-- Total Summary Strip -->
|
||||||
<div class="bg-slate-50 p-2.5 border-t border-slate-300 flex justify-between items-center text-xs">
|
<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="flex items-center gap-3">
|
||||||
<div class="font-bold text-slate-900">
|
<span class="text-slate-600">라인 실시간 상태: <strong class="text-emerald-700">✓ 자동 연산 정합성 보장</strong></span>
|
||||||
합계 금액: <span class="text-blue-700 font-mono text-sm ml-1">{{ totalAmountSummary.toLocaleString() }} KRW</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,6 +103,14 @@ const totalAmountSummary = computed(() => {
|
|||||||
return lines.value.reduce((sum, item) => sum + (item.amount || 0), 0);
|
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) => {
|
const recalculateLine = (line: OrderLineItem) => {
|
||||||
line.amount = (line.quantity || 0) * (line.price || 0);
|
line.amount = (line.quantity || 0) * (line.price || 0);
|
||||||
emit('update:lines', lines.value);
|
emit('update:lines', lines.value);
|
||||||
|
|||||||
@@ -1,38 +1,40 @@
|
|||||||
<!-- Domain Field Component Layer: LotField (WBS-COMP-3.4) -->
|
<!-- Domain Field Component Layer: LotField (WBS-COMP-3.4) -->
|
||||||
<template>
|
<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">
|
<div class="flex justify-between items-center text-xs">
|
||||||
<label class="font-bold text-slate-700">
|
<label class="font-bold text-slate-800 flex items-center gap-1">
|
||||||
{{ label || '로트(LOT) 번호 및 FEFO 관리' }} <span v-if="required" class="text-red-500">*</span>
|
<span>🏷️ LOT 번호 / FEFO 유효기간</span>
|
||||||
|
<span v-if="required" class="text-rose-600">*</span>
|
||||||
</label>
|
</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']">
|
<span v-if="remainingDays !== null" :class="['px-2 py-0.5 rounded text-[11px] font-bold font-mono shadow-xs', expiryStatusClass]">
|
||||||
{{ isExpired ? '🚨 로트 만료' : '✓ 출고 가능 (FEFO)' }}
|
{{ expiryStatusText }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 mt-1">
|
<div class="grid grid-cols-2 gap-2 mt-1">
|
||||||
<input
|
<div class="flex flex-col gap-0.5">
|
||||||
type="text"
|
<span class="text-[10px] font-bold text-slate-500">LOT 배치 번호</span>
|
||||||
:value="lotNumber"
|
<input
|
||||||
placeholder="LOT-20260726-001"
|
type="text"
|
||||||
:readonly="readonly"
|
:value="lotNumber"
|
||||||
:disabled="disabled"
|
placeholder="LOT-20260725-01"
|
||||||
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"
|
:readonly="readonly"
|
||||||
@input="handleLotInput"
|
: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
|
@input="handleLotInput"
|
||||||
type="date"
|
/>
|
||||||
:value="expirationDate"
|
</div>
|
||||||
:readonly="readonly"
|
<div class="flex flex-col gap-0.5">
|
||||||
:disabled="disabled"
|
<span class="text-[10px] font-bold text-slate-500">유효기간 (Expiration Date)</span>
|
||||||
class="h-9 px-2 border border-slate-300 rounded bg-slate-50 text-xs font-mono font-bold text-slate-700"
|
<input
|
||||||
@change="handleDateChange"
|
type="date"
|
||||||
/>
|
:value="expirationDate"
|
||||||
</div>
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
<div class="flex justify-between items-center text-[11px] text-slate-500 mt-0.5">
|
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"
|
||||||
<span>권장 출고 순위: <strong class="text-blue-700 font-mono">FEFO #1 (유효기간 상위)</strong></span>
|
@change="handleDateChange"
|
||||||
<span>유효기간: <strong class="font-mono text-slate-700">{{ expirationDate || '미지정' }}</strong></span>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -41,7 +43,6 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
label?: string;
|
|
||||||
lotNumber: string;
|
lotNumber: string;
|
||||||
expirationDate: string;
|
expirationDate: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@@ -51,15 +52,31 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits(['update:lotNumber', 'update:expirationDate']);
|
const emit = defineEmits(['update:lotNumber', 'update:expirationDate']);
|
||||||
|
|
||||||
const isExpired = computed(() => {
|
const remainingDays = computed(() => {
|
||||||
if (!props.expirationDate) return false;
|
if (!props.expirationDate) return null;
|
||||||
const exp = new Date(props.expirationDate).getTime();
|
const exp = new Date(props.expirationDate).getTime();
|
||||||
const today = new Date().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) => {
|
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) => {
|
const handleDateChange = (e: Event) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!-- Primitive UI Components Layer -->
|
<!-- Primitive UI Components Layer: TextInput -->
|
||||||
<template>
|
<template>
|
||||||
<div class="primitive-input-wrapper w-full">
|
<div class="primitive-input-wrapper relative w-full flex items-center">
|
||||||
<input
|
<input
|
||||||
:id="id"
|
:id="id"
|
||||||
:type="type || 'text'"
|
:type="type || 'text'"
|
||||||
@@ -8,9 +8,18 @@
|
|||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:readonly="readonly"
|
: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)"
|
@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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user