- 라우팅 react-router v7, 서버상태 TanStack Query - orval 자동 생성 API 클라이언트(src/api/generated) - 견적/상품/협력사/견적설정 페이지, shadcn UI, 토큰(tokens.css) - 실행: 루트 docker compose (front :3001), README 그에 맞게 정리 - AI Studio 스캐폴드 잔재 제거(metadata.json, 중복 DESIGN_TOKENS.md), 타이틀 NegoData, vite.config 죽은코드 제거 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
142 lines
5.1 KiB
TypeScript
142 lines
5.1 KiB
TypeScript
import { useState } from 'react';
|
|
import { Settings, Plus } from 'lucide-react';
|
|
import { PageContainer } from '@/components/layout/PageContainer';
|
|
import { PageToolbar, SearchInput } from '@/components/layout/PageToolbar';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { useQuotations } from '@/features/quotations/hooks/useQuotations';
|
|
import { useQuotationFilters } from '@/features/quotations/hooks/useQuotationFilters';
|
|
import { QuotationTable } from '@/features/quotations/components/QuotationTable';
|
|
import { QuotationDetailDrawer } from '@/features/quotations/components/QuotationDetailDrawer';
|
|
import { CreateQuotationWizard } from '@/features/quotations/components/CreateQuotationWizard';
|
|
import { QuotationSettingsModal } from '@/features/quotations/components/QuotationSettingsModal';
|
|
import { QUOTATION_STATUS_FILTERS } from '@/features/quotations/types';
|
|
|
|
export default function QuotationPage() {
|
|
const {
|
|
products,
|
|
partners,
|
|
cards,
|
|
quotations,
|
|
quotationSettings,
|
|
chatSessions,
|
|
stopNegotiation,
|
|
addSetting,
|
|
deleteSetting,
|
|
createQuotation,
|
|
} = useQuotations();
|
|
|
|
const { search, setSearch, statusFilter, setStatusFilter, typeFilter, setTypeFilter, filtered } =
|
|
useQuotationFilters(quotations);
|
|
|
|
const [detailId, setDetailId] = useState<string | null>(null);
|
|
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
|
|
const activeQuotation = quotations.find((e) => e.id === detailId) ?? null;
|
|
|
|
return (
|
|
<PageContainer>
|
|
<PageToolbar
|
|
actions={
|
|
<>
|
|
<button
|
|
id="quotation-settings-btn"
|
|
onClick={() => setIsSettingsOpen(true)}
|
|
className="flex items-center gap-2 px-3 py-2.5 bg-muted text-foreground border border-border text-xs font-semibold rounded hover:bg-muted-foreground/10 cursor-pointer transition-colors"
|
|
>
|
|
<Settings size={14} />
|
|
<span>견적 세팅</span>
|
|
</button>
|
|
|
|
<button
|
|
id="quotation-create-btn"
|
|
onClick={() => setIsCreateOpen(true)}
|
|
className="flex items-center gap-2 px-4 py-2.5 bg-primary text-primary-foreground text-xs font-bold rounded hover:opacity-95 cursor-pointer transition-colors animate-pulse"
|
|
>
|
|
<Plus size={15} />
|
|
<span>신규 협상견적 발의</span>
|
|
</button>
|
|
</>
|
|
}
|
|
>
|
|
<SearchInput
|
|
id="quotation-search"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
placeholder="견적명 또는 견적 번호로 실시간 서치..."
|
|
/>
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as string)}>
|
|
<SelectTrigger id="quotation-status-filter" className="font-bold">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ALL">전체 견적상태</SelectItem>
|
|
{QUOTATION_STATUS_FILTERS.map((s) => (
|
|
<SelectItem key={s} value={s}>{s}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select value={typeFilter} onValueChange={(v) => setTypeFilter(v as string)}>
|
|
<SelectTrigger id="quotation-type-filter">
|
|
<SelectValue>
|
|
{(value) => (value === 'ALL' ? '전체 유형' : value === 'RE_NEGOTIATION' ? '재협상' : '재견적')}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ALL">전체 유형</SelectItem>
|
|
<SelectItem value="RE_NEGOTIATION">재협상</SelectItem>
|
|
<SelectItem value="RE_ESTIMATE">재견적</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</PageToolbar>
|
|
|
|
<QuotationTable
|
|
data={filtered}
|
|
products={products}
|
|
onOpenDetail={setDetailId}
|
|
onStop={stopNegotiation}
|
|
/>
|
|
|
|
{activeQuotation && (
|
|
<QuotationDetailDrawer
|
|
key={activeQuotation.id}
|
|
estimate={activeQuotation}
|
|
products={products}
|
|
partners={partners}
|
|
cards={cards}
|
|
quotationSettings={quotationSettings}
|
|
sessions={chatSessions[activeQuotation.id ?? ''] ?? []}
|
|
onStop={stopNegotiation}
|
|
onClose={() => setDetailId(null)}
|
|
/>
|
|
)}
|
|
|
|
{isCreateOpen && (
|
|
<CreateQuotationWizard
|
|
open
|
|
products={products}
|
|
partners={partners}
|
|
cards={cards}
|
|
quotationSettings={quotationSettings}
|
|
onCreate={createQuotation}
|
|
onClose={() => setIsCreateOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{isSettingsOpen && (
|
|
<QuotationSettingsModal
|
|
open
|
|
settings={quotationSettings}
|
|
onAdd={addSetting}
|
|
onDelete={deleteSetting}
|
|
onClose={() => setIsSettingsOpen(false)}
|
|
/>
|
|
)}
|
|
</PageContainer>
|
|
);
|
|
}
|