fix: [quotes] 견적 등록 UI 개선 — 레이아웃/스타일 정리, 입력 영역 높이 맞춤
This commit is contained in:
@@ -304,9 +304,9 @@ export function LocationDetailPanel({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full overflow-hidden">
|
<div className="flex flex-col h-full overflow-hidden">
|
||||||
{/* ②-1 개소 정보 영역 */}
|
{/* ②-1 개소 정보 영역 */}
|
||||||
<div className="bg-gray-50 border-b">
|
<div>
|
||||||
{/* 1행: 층, 부호, 가로, 세로, 제품코드 */}
|
{/* 입력 영역 (왼쪽 입력 폼과 높이 맞춤) */}
|
||||||
<div className="px-4 py-3 space-y-3">
|
<div className="bg-gray-100 p-4 space-y-3 border-b border-gray-200 min-h-[128px]">
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-gray-600">층</label>
|
<label className="text-xs text-gray-600">층</label>
|
||||||
@@ -315,7 +315,7 @@ export function LocationDetailPanel({
|
|||||||
value={location.floor}
|
value={location.floor}
|
||||||
onChange={(e) => handleFieldChange("floor", e.target.value)}
|
onChange={(e) => handleFieldChange("floor", e.target.value)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="h-8 text-sm"
|
className="h-8 text-sm border border-gray-300"
|
||||||
/>
|
/>
|
||||||
<datalist id="floorOptionListDetail">
|
<datalist id="floorOptionListDetail">
|
||||||
{FLOOR_OPTIONS.map((f) => (
|
{FLOOR_OPTIONS.map((f) => (
|
||||||
@@ -330,7 +330,7 @@ export function LocationDetailPanel({
|
|||||||
value={location.code}
|
value={location.code}
|
||||||
onChange={(e) => handleFieldChange("code", e.target.value)}
|
onChange={(e) => handleFieldChange("code", e.target.value)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="h-8 text-sm"
|
className="h-8 text-sm border border-gray-300"
|
||||||
/>
|
/>
|
||||||
<datalist id="locationCodeListDetail">
|
<datalist id="locationCodeListDetail">
|
||||||
{locationCodes.map((code) => (
|
{locationCodes.map((code) => (
|
||||||
@@ -344,7 +344,7 @@ export function LocationDetailPanel({
|
|||||||
value={location.openWidth}
|
value={location.openWidth}
|
||||||
onChange={(value) => handleFieldChange("openWidth", value ?? 0)}
|
onChange={(value) => handleFieldChange("openWidth", value ?? 0)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="h-8 text-sm"
|
className="h-8 text-sm border border-gray-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -353,7 +353,7 @@ export function LocationDetailPanel({
|
|||||||
value={location.openHeight}
|
value={location.openHeight}
|
||||||
onChange={(value) => handleFieldChange("openHeight", value ?? 0)}
|
onChange={(value) => handleFieldChange("openHeight", value ?? 0)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="h-8 text-sm"
|
className="h-8 text-sm border border-gray-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -372,7 +372,7 @@ export function LocationDetailPanel({
|
|||||||
}}
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-sm">
|
<SelectTrigger className="h-8 text-sm border border-gray-300">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -398,7 +398,7 @@ export function LocationDetailPanel({
|
|||||||
onValueChange={(value) => handleFieldChange("guideRailType", value)}
|
onValueChange={(value) => handleFieldChange("guideRailType", value)}
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-sm">
|
<SelectTrigger className="h-8 text-sm border border-gray-300">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -419,7 +419,7 @@ export function LocationDetailPanel({
|
|||||||
onValueChange={(value) => handleFieldChange("motorPower", value)}
|
onValueChange={(value) => handleFieldChange("motorPower", value)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-sm">
|
<SelectTrigger className="h-8 text-sm border border-gray-300">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -440,7 +440,7 @@ export function LocationDetailPanel({
|
|||||||
onValueChange={(value) => handleFieldChange("controller", value)}
|
onValueChange={(value) => handleFieldChange("controller", value)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-sm">
|
<SelectTrigger className="h-8 text-sm border border-gray-300">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -454,27 +454,28 @@ export function LocationDetailPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3행: 제작사이즈, 산출중량, 산출면적, 수량, 산출하기 */}
|
</div>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 text-sm pt-2 border-t border-gray-200">
|
{/* 결과행: 제작사이즈, 산출중량, 산출면적, 수량, 산출하기 */}
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 text-sm px-4 py-3 bg-gray-50 border-b border-gray-200">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xs text-gray-500">제작사이즈</span>
|
<span className="text-xs text-muted-foreground">제작사이즈</span>
|
||||||
<p className="font-semibold">
|
<p className="text-base font-bold">
|
||||||
{String(location.bomResult?.variables?.W1 ?? location.manufactureWidth ?? "-")}
|
{String(location.bomResult?.variables?.W1 ?? location.manufactureWidth ?? "-")}
|
||||||
X
|
X
|
||||||
{String(location.bomResult?.variables?.H1 ?? location.manufactureHeight ?? "-")}
|
{String(location.bomResult?.variables?.H1 ?? location.manufactureHeight ?? "-")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xs text-gray-500">산출중량</span>
|
<span className="text-xs text-muted-foreground">산출중량</span>
|
||||||
<p className="font-semibold">
|
<p className="text-base font-bold">
|
||||||
{(location.bomResult?.variables?.K ?? location.bomResult?.variables?.WEIGHT)
|
{(location.bomResult?.variables?.K ?? location.bomResult?.variables?.WEIGHT)
|
||||||
? `${Number(location.bomResult?.variables?.K ?? location.bomResult?.variables?.WEIGHT).toFixed(2)} kg`
|
? `${Number(location.bomResult?.variables?.K ?? location.bomResult?.variables?.WEIGHT).toFixed(2)} kg`
|
||||||
: "-"}
|
: "-"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xs text-gray-500">산출면적</span>
|
<span className="text-xs text-muted-foreground">산출면적</span>
|
||||||
<p className="font-semibold">
|
<p className="text-base font-bold">
|
||||||
{(location.bomResult?.variables?.M ?? location.bomResult?.variables?.AREA)
|
{(location.bomResult?.variables?.M ?? location.bomResult?.variables?.AREA)
|
||||||
? `${Number(location.bomResult?.variables?.M ?? location.bomResult?.variables?.AREA).toFixed(2)} m²`
|
? `${Number(location.bomResult?.variables?.M ?? location.bomResult?.variables?.AREA).toFixed(2)} m²`
|
||||||
: "-"}
|
: "-"}
|
||||||
@@ -493,7 +494,7 @@ export function LocationDetailPanel({
|
|||||||
totalPrice: unitPrice * newQty,
|
totalPrice: unitPrice * newQty,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="h-8 text-sm font-semibold"
|
className="h-8 text-sm font-semibold border border-gray-300"
|
||||||
min={1}
|
min={1}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
@@ -502,7 +503,7 @@ export function LocationDetailPanel({
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => onCalculateLocation?.(location.id)}
|
onClick={() => onCalculateLocation?.(location.id)}
|
||||||
disabled={disabled || isCalculating}
|
disabled={disabled || isCalculating}
|
||||||
className="w-full h-8 bg-orange-500 hover:bg-orange-600 text-white"
|
className="w-full h-8"
|
||||||
>
|
>
|
||||||
{isCalculating ? (
|
{isCalculating ? (
|
||||||
<>
|
<>
|
||||||
@@ -519,7 +520,6 @@ export function LocationDetailPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ②-2 품목 상세 영역 */}
|
{/* ②-2 품목 상세 영역 */}
|
||||||
<div className="flex-1 overflow-hidden flex flex-col">
|
<div className="flex-1 overflow-hidden flex flex-col">
|
||||||
@@ -531,7 +531,7 @@ export function LocationDetailPanel({
|
|||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
value={tab.value}
|
value={tab.value}
|
||||||
className="rounded-none border-b-2 border-transparent data-[state=active]:border-orange-500 data-[state=active]:bg-orange-50 data-[state=active]:text-orange-700 px-4 py-2 text-sm whitespace-nowrap"
|
className="rounded-none border-b-2 border-transparent data-[state=active]:border-gray-900 data-[state=active]:bg-gray-100 data-[state=active]:text-gray-900 data-[state=active]:font-semibold px-4 py-2 text-sm whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -546,10 +546,10 @@ export function LocationDetailPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent key={tab.value} value={tab.value} className="flex-1 overflow-auto m-0 p-0">
|
<TabsContent key={tab.value} value={tab.value} className="flex-1 overflow-auto m-0 p-0">
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded-lg m-3">
|
<div className="bg-gray-50 border border-gray-200 rounded-lg m-3">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-amber-100/50">
|
<TableRow>
|
||||||
<TableHead className="font-semibold">품목명</TableHead>
|
<TableHead className="font-semibold">품목명</TableHead>
|
||||||
<TableHead className="text-center font-semibold">규격</TableHead>
|
<TableHead className="text-center font-semibold">규격</TableHead>
|
||||||
{isBendingTab && (
|
{isBendingTab && (
|
||||||
@@ -738,11 +738,10 @@ export function LocationDetailPanel({
|
|||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
{/* 품목 추가 + 저장 버튼 */}
|
{/* 품목 추가 + 저장 버튼 */}
|
||||||
<div className="p-3 flex items-center justify-between border-t border-amber-200">
|
<div className="p-3 flex items-center justify-between border-t border-gray-200">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-blue-100 border-blue-300 text-blue-700 hover:bg-blue-200"
|
|
||||||
onClick={() => setItemSearchOpen(true)}
|
onClick={() => setItemSearchOpen(true)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
@@ -751,7 +750,6 @@ export function LocationDetailPanel({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
|
||||||
onClick={onSaveItems}
|
onClick={onSaveItems}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ export function LocationListPanel({
|
|||||||
<div className="border-r border-gray-200 flex flex-col">
|
<div className="border-r border-gray-200 flex flex-col">
|
||||||
{/* ① 입력 영역 (상단으로 이동) */}
|
{/* ① 입력 영역 (상단으로 이동) */}
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<div className="bg-gray-50 p-4 space-y-3 border-b border-gray-200">
|
<div className="bg-gray-100 p-4 space-y-3 border-b border-gray-200 min-h-[128px]">
|
||||||
{/* 1행: 층, 부호, 가로, 세로, 제품코드, 수량 */}
|
{/* 1행: 층, 부호, 가로, 세로, 제품코드, 수량 */}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-2">
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-2">
|
||||||
<div>
|
<div>
|
||||||
@@ -322,7 +322,7 @@ export function LocationListPanel({
|
|||||||
placeholder="예: 1F"
|
placeholder="예: 1F"
|
||||||
value={formData.floor}
|
value={formData.floor}
|
||||||
onChange={(e) => handleFormChange("floor", e.target.value)}
|
onChange={(e) => handleFormChange("floor", e.target.value)}
|
||||||
className="h-8 text-sm placeholder:text-gray-300"
|
className="h-8 text-sm border border-gray-300 placeholder:text-gray-400"
|
||||||
/>
|
/>
|
||||||
<datalist id="floorOptionList">
|
<datalist id="floorOptionList">
|
||||||
{FLOOR_OPTIONS.map((f) => (
|
{FLOOR_OPTIONS.map((f) => (
|
||||||
@@ -337,7 +337,7 @@ export function LocationListPanel({
|
|||||||
placeholder="예: FSS-01"
|
placeholder="예: FSS-01"
|
||||||
value={formData.code}
|
value={formData.code}
|
||||||
onChange={(e) => handleFormChange("code", e.target.value)}
|
onChange={(e) => handleFormChange("code", e.target.value)}
|
||||||
className="h-8 text-sm placeholder:text-gray-300"
|
className="h-8 text-sm border border-gray-300 placeholder:text-gray-400"
|
||||||
/>
|
/>
|
||||||
<datalist id="locationCodeList">
|
<datalist id="locationCodeList">
|
||||||
{locationCodes.map((code) => (
|
{locationCodes.map((code) => (
|
||||||
@@ -351,7 +351,7 @@ export function LocationListPanel({
|
|||||||
placeholder="예: 5000"
|
placeholder="예: 5000"
|
||||||
value={formData.openWidth ? Number(formData.openWidth) : undefined}
|
value={formData.openWidth ? Number(formData.openWidth) : undefined}
|
||||||
onChange={(value) => handleFormChange("openWidth", value?.toString() ?? "")}
|
onChange={(value) => handleFormChange("openWidth", value?.toString() ?? "")}
|
||||||
className="h-8 text-sm placeholder:text-gray-300"
|
className="h-8 text-sm border border-gray-300 placeholder:text-gray-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -360,7 +360,7 @@ export function LocationListPanel({
|
|||||||
placeholder="예: 3000"
|
placeholder="예: 3000"
|
||||||
value={formData.openHeight ? Number(formData.openHeight) : undefined}
|
value={formData.openHeight ? Number(formData.openHeight) : undefined}
|
||||||
onChange={(value) => handleFormChange("openHeight", value?.toString() ?? "")}
|
onChange={(value) => handleFormChange("openHeight", value?.toString() ?? "")}
|
||||||
className="h-8 text-sm placeholder:text-gray-300"
|
className="h-8 text-sm border border-gray-300 placeholder:text-gray-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -376,14 +376,13 @@ export function LocationListPanel({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-sm">
|
<SelectTrigger className="h-8 text-sm border border-gray-300">
|
||||||
<SelectValue placeholder="선택" />
|
<SelectValue placeholder="선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(() => {
|
{(() => {
|
||||||
// 기존 개소가 있으면 동일 모델 제품만 표시 (예: KSS01 → KSS01 변형만)
|
|
||||||
const existingCode = locations.length > 0 ? locations[0].productCode : null;
|
const existingCode = locations.length > 0 ? locations[0].productCode : null;
|
||||||
const existingModel = existingCode?.split('-')[1] ?? null; // FG-KSS01-... → KSS01
|
const existingModel = existingCode?.split('-')[1] ?? null;
|
||||||
const filtered = existingModel
|
const filtered = existingModel
|
||||||
? finishedGoods.filter((fg) => fg.item_code.split('-')[1] === existingModel)
|
? finishedGoods.filter((fg) => fg.item_code.split('-')[1] === existingModel)
|
||||||
: finishedGoods;
|
: finishedGoods;
|
||||||
@@ -401,14 +400,14 @@ export function LocationListPanel({
|
|||||||
<QuantityInput
|
<QuantityInput
|
||||||
value={formData.quantity ? Number(formData.quantity) : undefined}
|
value={formData.quantity ? Number(formData.quantity) : undefined}
|
||||||
onChange={(value) => handleFormChange("quantity", value?.toString() ?? "")}
|
onChange={(value) => handleFormChange("quantity", value?.toString() ?? "")}
|
||||||
className="h-8 text-sm"
|
className="h-8 text-sm border border-gray-300"
|
||||||
min={1}
|
min={1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2행: 가이드레일, 전원, 제어기, 추가 버튼 */}
|
{/* 2행: 가이드레일, 전원, 제어기, + 버튼 */}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 items-end">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-gray-600 flex items-center gap-1">
|
<label className="text-xs text-gray-600 flex items-center gap-1">
|
||||||
🔧 가이드레일
|
🔧 가이드레일
|
||||||
@@ -419,7 +418,7 @@ export function LocationListPanel({
|
|||||||
onValueChange={(value) => handleFormChange("guideRailType", value)}
|
onValueChange={(value) => handleFormChange("guideRailType", value)}
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-sm">
|
<SelectTrigger className="h-8 text-sm border border-gray-300">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -439,7 +438,7 @@ export function LocationListPanel({
|
|||||||
value={formData.motorPower}
|
value={formData.motorPower}
|
||||||
onValueChange={(value) => handleFormChange("motorPower", value)}
|
onValueChange={(value) => handleFormChange("motorPower", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-sm">
|
<SelectTrigger className="h-8 text-sm border border-gray-300">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -459,7 +458,7 @@ export function LocationListPanel({
|
|||||||
value={formData.controller}
|
value={formData.controller}
|
||||||
onValueChange={(value) => handleFormChange("controller", value)}
|
onValueChange={(value) => handleFormChange("controller", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-sm">
|
<SelectTrigger className="h-8 text-sm border border-gray-300">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -471,21 +470,23 @@ export function LocationListPanel({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="flex items-end">
|
||||||
onClick={handleAdd}
|
<Button
|
||||||
className="h-8 bg-green-500 hover:bg-green-600 px-4"
|
onClick={handleAdd}
|
||||||
>
|
className="w-full h-[36px] bg-green-500 hover:bg-green-600"
|
||||||
<Plus className="h-4 w-4" />
|
>
|
||||||
</Button>
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 발주 개소 목록 헤더 */}
|
{/* 발주 개소 목록 헤더 */}
|
||||||
<div className="bg-blue-100 px-4 py-3 border-b border-blue-200">
|
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||||
<h3 className="font-semibold text-blue-800">
|
<h3 className="font-semibold text-gray-700">
|
||||||
📋 발주 개소 목록 ({locations.length})
|
발주 개소 목록 ({locations.length})
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
@@ -527,13 +528,13 @@ export function LocationListPanel({
|
|||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-gray-800 text-white">
|
<TableRow>
|
||||||
<TableHead className="text-center text-white">층</TableHead>
|
<TableHead className="text-center">층</TableHead>
|
||||||
<TableHead className="text-center text-white">부호</TableHead>
|
<TableHead className="text-center">부호</TableHead>
|
||||||
<TableHead className="text-center text-white">사이즈</TableHead>
|
<TableHead className="text-center">사이즈</TableHead>
|
||||||
<TableHead className="text-center text-white">제품</TableHead>
|
<TableHead className="text-center">제품</TableHead>
|
||||||
<TableHead className="text-center text-white">수량</TableHead>
|
<TableHead className="text-center">수량</TableHead>
|
||||||
{!disabled && <TableHead className="w-[80px] text-center text-white"></TableHead>}
|
{!disabled && <TableHead className="w-[80px] text-center"></TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -548,7 +549,7 @@ export function LocationListPanel({
|
|||||||
<TableRow
|
<TableRow
|
||||||
key={loc.id}
|
key={loc.id}
|
||||||
className={`cursor-pointer hover:bg-blue-50 ${
|
className={`cursor-pointer hover:bg-blue-50 ${
|
||||||
selectedLocationId === loc.id ? "bg-orange-100 border-l-4 border-l-orange-500" : ""
|
selectedLocationId === loc.id ? "bg-gray-100 border-l-4 border-l-gray-800" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onSelectLocation(loc.id)}
|
onClick={() => onSelectLocation(loc.id)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,10 +8,11 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Save, Check, ArrowLeft, Loader2, FileText, Pencil, ClipboardList, Percent, Calculator } from "lucide-react";
|
import { Save, Check, X, ArrowLeft, Loader2, FileText, Pencil, ClipboardList, Percent, Calculator } from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { formatNumber } from "@/lib/utils/amount";
|
import { formatNumber } from "@/lib/utils/amount";
|
||||||
|
import { useMenuStore } from "@/stores/menuStore";
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Props
|
// Props
|
||||||
@@ -29,8 +30,10 @@ interface QuoteFooterBarProps {
|
|||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
/** 최종확정 */
|
/** 최종확정 */
|
||||||
onFinalize: () => void;
|
onFinalize: () => void;
|
||||||
/** 뒤로가기 */
|
/** 뒤로가기 (목록으로) */
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
|
/** 취소 (목록으로 이동) */
|
||||||
|
onCancel?: () => void;
|
||||||
/** 수정 */
|
/** 수정 */
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
/** 수주등록 */
|
/** 수주등록 */
|
||||||
@@ -68,6 +71,7 @@ export function QuoteFooterBar({
|
|||||||
onSave,
|
onSave,
|
||||||
onFinalize,
|
onFinalize,
|
||||||
onBack,
|
onBack,
|
||||||
|
onCancel,
|
||||||
onEdit,
|
onEdit,
|
||||||
onOrderRegister,
|
onOrderRegister,
|
||||||
onOrderView,
|
onOrderView,
|
||||||
@@ -81,28 +85,35 @@ export function QuoteFooterBar({
|
|||||||
disabled: _disabled = false,
|
disabled: _disabled = false,
|
||||||
isViewMode = false,
|
isViewMode = false,
|
||||||
}: QuoteFooterBarProps) {
|
}: QuoteFooterBarProps) {
|
||||||
return (
|
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
|
||||||
<div className="sticky bottom-0 bg-gradient-to-r from-blue-50 to-indigo-50 border-t border-blue-200 shadow-lg">
|
|
||||||
<div className="px-3 py-3 md:px-6 md:py-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-0">
|
|
||||||
{/* 왼쪽: 뒤로가기 + 금액 표시 */}
|
|
||||||
<div className="flex items-center gap-3 md:gap-6">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onBack}
|
|
||||||
size="sm"
|
|
||||||
className="md:size-default shrink-0"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-4 w-4 md:mr-2" />
|
|
||||||
<span className="hidden md:inline">목록으로</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="min-w-0">
|
return (
|
||||||
<p className="text-xs md:text-sm text-gray-600 whitespace-nowrap">예상 전체 견적금액</p>
|
<div className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[24px] ${sidebarCollapsed ? 'md:left-[120px]' : 'md:left-[280px]'}`}>
|
||||||
<p className="text-lg md:text-3xl font-bold text-blue-600 whitespace-nowrap">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-0">
|
||||||
{formatNumber(totalAmount)}
|
{/* 왼쪽: 취소 + 목록으로 */}
|
||||||
<span className="text-sm md:text-lg font-normal text-gray-500 ml-1">원</span>
|
<div className="flex items-center gap-2">
|
||||||
</p>
|
{!isViewMode && onCancel && (
|
||||||
</div>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onCancel}
|
||||||
|
size="sm"
|
||||||
|
className="md:size-default shrink-0"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 md:mr-2" />
|
||||||
|
<span className="hidden md:inline">취소</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{isViewMode && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onBack}
|
||||||
|
size="sm"
|
||||||
|
className="md:size-default shrink-0"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4 md:mr-2" />
|
||||||
|
<span className="hidden md:inline">목록으로</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 오른쪽: 버튼들 */}
|
{/* 오른쪽: 버튼들 */}
|
||||||
|
|||||||
@@ -890,8 +890,8 @@ export function QuoteRegistration({
|
|||||||
|
|
||||||
{/* 자동 견적 산출 섹션 */}
|
{/* 자동 견적 산출 섹션 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3 bg-orange-50 border-b border-orange-200">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-base font-semibold flex items-center gap-2 text-orange-800">
|
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
||||||
<Calculator className="h-5 w-5" />
|
<Calculator className="h-5 w-5" />
|
||||||
자동 견적 산출
|
자동 견적 산출
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
@@ -979,6 +979,7 @@ export function QuoteRegistration({
|
|||||||
locations={formData.locations}
|
locations={formData.locations}
|
||||||
selectedLocationId={selectedLocationId}
|
selectedLocationId={selectedLocationId}
|
||||||
onSelectLocation={setSelectedLocationId}
|
onSelectLocation={setSelectedLocationId}
|
||||||
|
discountedTotalAmount={discountedTotalAmount}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -992,6 +993,7 @@ export function QuoteRegistration({
|
|||||||
onSave={() => handleSave("temporary")}
|
onSave={() => handleSave("temporary")}
|
||||||
onFinalize={() => handleSave("final")}
|
onFinalize={() => handleSave("final")}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
|
onCancel={onBack}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onOrderRegister={onOrderRegister}
|
onOrderRegister={onOrderRegister}
|
||||||
onOrderView={onOrderView}
|
onOrderView={onOrderView}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ interface QuoteSummaryPanelProps {
|
|||||||
locations: LocationItem[];
|
locations: LocationItem[];
|
||||||
selectedLocationId: string | null;
|
selectedLocationId: string | null;
|
||||||
onSelectLocation: (id: string) => void;
|
onSelectLocation: (id: string) => void;
|
||||||
|
/** 할인 적용 후 총 금액 (할인이 있을 때만 표시) */
|
||||||
|
discountedTotalAmount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -54,6 +56,7 @@ export function QuoteSummaryPanel({
|
|||||||
locations,
|
locations,
|
||||||
selectedLocationId,
|
selectedLocationId,
|
||||||
onSelectLocation,
|
onSelectLocation,
|
||||||
|
discountedTotalAmount,
|
||||||
}: QuoteSummaryPanelProps) {
|
}: QuoteSummaryPanelProps) {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 계산된 값
|
// 계산된 값
|
||||||
@@ -135,9 +138,9 @@ export function QuoteSummaryPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border-gray-200">
|
<Card className="border-gray-200">
|
||||||
<CardHeader className="pb-3 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-blue-200">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
||||||
<Coins className="h-5 w-5 text-blue-600" />
|
<Coins className="h-5 w-5" />
|
||||||
견적 금액 요약
|
견적 금액 요약
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -147,7 +150,7 @@ export function QuoteSummaryPanel({
|
|||||||
{/* 왼쪽: 개소별 합계 */}
|
{/* 왼쪽: 개소별 합계 */}
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<span className="text-blue-500">📍</span>
|
<span>📍</span>
|
||||||
<h4 className="font-semibold text-gray-700">개소별 합계</h4>
|
<h4 className="font-semibold text-gray-700">개소별 합계</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -162,7 +165,7 @@ export function QuoteSummaryPanel({
|
|||||||
key={loc.id}
|
key={loc.id}
|
||||||
className={`flex items-center justify-between p-3 rounded-lg cursor-pointer transition-colors ${
|
className={`flex items-center justify-between p-3 rounded-lg cursor-pointer transition-colors ${
|
||||||
selectedLocationId === loc.id
|
selectedLocationId === loc.id
|
||||||
? "bg-blue-100 border border-blue-300"
|
? "bg-gray-100 border border-gray-300"
|
||||||
: "bg-gray-50 hover:bg-gray-100 border border-transparent"
|
: "bg-gray-50 hover:bg-gray-100 border border-transparent"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onSelectLocation(loc.id)}
|
onClick={() => onSelectLocation(loc.id)}
|
||||||
@@ -175,7 +178,7 @@ export function QuoteSummaryPanel({
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-xs text-gray-500">상세소계</p>
|
<p className="text-xs text-gray-500">상세소계</p>
|
||||||
<p className="font-bold text-blue-600">
|
<p className="font-bold text-gray-900">
|
||||||
{formatNumber(loc.totalPrice)}
|
{formatNumber(loc.totalPrice)}
|
||||||
</p>
|
</p>
|
||||||
{loc.unitPrice > 0 && (
|
{loc.unitPrice > 0 && (
|
||||||
@@ -193,7 +196,7 @@ export function QuoteSummaryPanel({
|
|||||||
{/* 오른쪽: 상세별 합계 */}
|
{/* 오른쪽: 상세별 합계 */}
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<span className="text-blue-500">✨</span>
|
<span>✨</span>
|
||||||
<h4 className="font-semibold text-gray-700">
|
<h4 className="font-semibold text-gray-700">
|
||||||
상세별 합계
|
상세별 합계
|
||||||
{selectedLocation && (
|
{selectedLocation && (
|
||||||
@@ -215,19 +218,19 @@ export function QuoteSummaryPanel({
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{detailTotals.map((category, index) => (
|
{detailTotals.map((category, index) => (
|
||||||
<div key={index} className="bg-blue-50 rounded-lg overflow-hidden border border-blue-200">
|
<div key={index} className="bg-gray-50 rounded-lg overflow-hidden border border-gray-200">
|
||||||
{/* 카테고리 헤더 */}
|
{/* 카테고리 헤더 */}
|
||||||
<div className="flex items-center justify-between px-3 py-2 bg-blue-100/50">
|
<div className="flex items-center justify-between px-3 py-2 bg-gray-100">
|
||||||
<span className="font-semibold text-gray-700">{category.label}</span>
|
<span className="font-semibold text-gray-700">{category.label}</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-gray-500">({category.count}개)</span>
|
<span className="text-xs text-gray-500">({category.count}개)</span>
|
||||||
<span className="font-bold text-blue-600">
|
<span className="font-bold text-gray-900">
|
||||||
{formatNumber(category.amount)}
|
{formatNumber(category.amount)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 품목 상세 목록 */}
|
{/* 품목 상세 목록 */}
|
||||||
<div className="divide-y divide-blue-100">
|
<div className="divide-y divide-gray-100">
|
||||||
{category.items.map((item, itemIndex) => (
|
{category.items.map((item, itemIndex) => (
|
||||||
<div key={itemIndex} className="flex items-center justify-between px-3 py-2 bg-white">
|
<div key={itemIndex} className="flex items-center justify-between px-3 py-2 bg-white">
|
||||||
<div>
|
<div>
|
||||||
@@ -236,7 +239,7 @@ export function QuoteSummaryPanel({
|
|||||||
수량: {item.quantity} × 단가: {formatNumber(item.unitPrice)}
|
수량: {item.quantity} × 단가: {formatNumber(item.unitPrice)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-blue-600">
|
<span className="text-sm font-medium text-gray-900">
|
||||||
{formatNumber(item.totalPrice)}
|
{formatNumber(item.totalPrice)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,23 +253,32 @@ export function QuoteSummaryPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 하단 바: 총 개소 수, 예상 견적금액, 견적 상태 */}
|
{/* 하단 바: 총 개소 수, 예상 견적금액, 견적 상태 */}
|
||||||
<div className="bg-gray-900 text-white px-4 py-4 md:px-6 md:py-5 flex flex-wrap items-center justify-between gap-3">
|
<div className="bg-gray-50 border-t px-4 py-4 md:px-6 md:py-5 flex flex-wrap items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-4 md:gap-10">
|
<div className="flex items-center gap-4 md:gap-10">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs md:text-sm text-gray-400">총 개소 수</p>
|
<p className="text-xs md:text-sm text-muted-foreground">총 개소 수</p>
|
||||||
<p className="text-2xl md:text-4xl font-bold">{locations.length}</p>
|
<p className="text-2xl md:text-4xl font-bold">{locations.length}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs md:text-sm text-gray-400">예상 견적금액</p>
|
<p className="text-xs md:text-sm text-muted-foreground">예상 견적금액</p>
|
||||||
<p className="text-2xl md:text-4xl font-bold text-blue-400">
|
<p className="text-2xl md:text-4xl font-bold">
|
||||||
{formatNumber(totalAmount)}
|
{formatNumber(totalAmount)}
|
||||||
<span className="text-base md:text-xl ml-1">원</span>
|
<span className="text-base md:text-xl font-normal text-muted-foreground ml-1">원</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{discountedTotalAmount !== undefined && discountedTotalAmount !== totalAmount && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs md:text-sm text-muted-foreground">예상 전체 견적금액 <span className="text-blue-500">(할인 적용)</span></p>
|
||||||
|
<p className="text-2xl md:text-4xl font-bold text-blue-600">
|
||||||
|
{formatNumber(discountedTotalAmount)}
|
||||||
|
<span className="text-base md:text-xl font-normal text-muted-foreground ml-1">원</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-xs md:text-sm text-gray-400">견적 상태</p>
|
<p className="text-xs md:text-sm text-muted-foreground">견적 상태</p>
|
||||||
<span className="inline-block bg-blue-500/20 text-blue-300 border border-blue-500/50 text-sm md:text-lg px-3 py-1 md:px-4 rounded">
|
<span className="inline-block bg-gray-200 text-gray-700 border border-gray-300 text-sm md:text-lg px-3 py-1 md:px-4 rounded">
|
||||||
작성중
|
작성중
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ export const quoteConfig: DetailConfig = {
|
|||||||
fields: [], // renderView/renderForm 사용으로 필드 정의 불필요
|
fields: [], // renderView/renderForm 사용으로 필드 정의 불필요
|
||||||
gridColumns: 2,
|
gridColumns: 2,
|
||||||
actions: {
|
actions: {
|
||||||
showBack: false, // QuoteFooterBar에서 처리
|
hideActions: true, // QuoteFooterBar에서 전체 처리
|
||||||
showDelete: false, // QuoteFooterBar에서 처리
|
|
||||||
showEdit: false, // QuoteFooterBar에서 처리
|
|
||||||
showSave: false, // QuoteFooterBar에서 처리
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,9 +35,7 @@ export const quoteCreateConfig: DetailConfig = {
|
|||||||
basePath: '/sales/quote-management',
|
basePath: '/sales/quote-management',
|
||||||
fields: [],
|
fields: [],
|
||||||
actions: {
|
actions: {
|
||||||
showBack: true,
|
hideActions: true, // QuoteFooterBar에서 전체 처리
|
||||||
showSave: false, // QuoteFooterBar에서 처리
|
|
||||||
backLabel: '목록',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,9 +49,7 @@ export const quoteEditConfig: DetailConfig = {
|
|||||||
basePath: '/sales/quote-management',
|
basePath: '/sales/quote-management',
|
||||||
fields: [],
|
fields: [],
|
||||||
actions: {
|
actions: {
|
||||||
showBack: true,
|
hideActions: true, // QuoteFooterBar에서 전체 처리
|
||||||
showSave: false, // QuoteFooterBar에서 처리
|
|
||||||
backLabel: '목록',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user