feat:견적/수주/공정관리 UI 개선
- QuoteFooterBar: 수주전환 버튼 및 상태 표시 개선 - QuoteRegistrationV2: 할인금액 입력 UI 추가 - QuoteManagementClient: 타입 수정 - OrderRegistration: 수주 등록 폼 개선 - StepDetail: 공정 단계 상세 UI 확장 - RuleModal: 규칙 모달 레이아웃 조정 - BadgeSm: 뱃지 컴포넌트 스타일 개선
This commit is contained in:
@@ -552,6 +552,7 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
|
||||
onOpenChange={handleModalClose}
|
||||
onAdd={handleSaveRule}
|
||||
editRule={editingRule}
|
||||
processId={initialData?.id}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -48,9 +48,11 @@ interface RuleModalProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onAdd: (rule: Omit<ClassificationRule, 'id' | 'createdAt'>) => void;
|
||||
editRule?: ClassificationRule;
|
||||
/** 현재 공정 ID (다른 공정에 이미 배정된 품목 제외용) */
|
||||
processId?: string;
|
||||
}
|
||||
|
||||
export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProps) {
|
||||
export function RuleModal({ open, onOpenChange, onAdd, editRule, processId }: RuleModalProps) {
|
||||
// 공통 상태
|
||||
const [registrationType, setRegistrationType] = useState<RuleRegistrationType>(
|
||||
editRule?.registrationType || 'pattern'
|
||||
@@ -85,10 +87,11 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp
|
||||
q: q || undefined,
|
||||
itemType: itemType === 'all' ? undefined : itemType,
|
||||
size: 1000, // 전체 품목 조회
|
||||
excludeProcessId: processId, // 다른 공정에 이미 배정된 품목 제외
|
||||
});
|
||||
setItemList(items);
|
||||
setIsItemsLoading(false);
|
||||
}, []);
|
||||
}, [processId]);
|
||||
|
||||
// 검색어 유효성 검사 함수
|
||||
const isValidSearchKeyword = (keyword: string): boolean => {
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
* - 완료 정보: 유형(선택 완료 시 완료/클릭 시 완료)
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, Edit } from 'lucide-react';
|
||||
import { ArrowLeft, Edit, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -18,6 +19,18 @@ import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { useMenuStore } from '@/store/menuStore';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { deleteProcessStep } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import type { ProcessStep } from '@/types/process';
|
||||
|
||||
interface StepDetailProps {
|
||||
@@ -28,7 +41,9 @@ interface StepDetailProps {
|
||||
export function StepDetail({ step, processId }: StepDetailProps) {
|
||||
const router = useRouter();
|
||||
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
|
||||
const { canUpdate } = usePermission();
|
||||
const { canUpdate, canDelete } = usePermission();
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(
|
||||
@@ -40,6 +55,24 @@ export function StepDetail({ step, processId }: StepDetailProps) {
|
||||
router.push(`/ko/master-data/process-management/${processId}`);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const result = await deleteProcessStep(processId, step.id);
|
||||
if (result.success) {
|
||||
toast.success('단계가 삭제되었습니다.');
|
||||
router.push(`/ko/master-data/process-management/${processId}`);
|
||||
} else {
|
||||
toast.error(result.error || '삭제에 실패했습니다.');
|
||||
}
|
||||
} catch {
|
||||
toast.error('삭제 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setShowDeleteDialog(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<PageHeader title="단계 상세" />
|
||||
@@ -131,13 +164,48 @@ export function StepDetail({ step, processId }: StepDetailProps) {
|
||||
<ArrowLeft className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">공정으로 돌아가기</span>
|
||||
</Button>
|
||||
{canUpdate && (
|
||||
<Button onClick={handleEdit} size="sm" className="md:size-default">
|
||||
<Edit className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">수정</span>
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{canDelete && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
size="sm"
|
||||
className="md:size-default"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">삭제</span>
|
||||
</Button>
|
||||
)}
|
||||
{canUpdate && (
|
||||
<Button onClick={handleEdit} size="sm" className="md:size-default">
|
||||
<Edit className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">수정</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>단계 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
'{step.stepName}' 단계를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? '삭제 중...' : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -620,10 +620,13 @@ interface GetItemListParams {
|
||||
q?: string;
|
||||
itemType?: string;
|
||||
size?: number;
|
||||
/** 해당 공정 외 다른 공정에 이미 배정된 품목 제외 (공정 ID) */
|
||||
excludeProcessId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 목록 조회 (분류 규칙용)
|
||||
* - excludeProcessId: 다른 공정에 이미 배정된 품목 제외 (중복 방지)
|
||||
*/
|
||||
export async function getItemList(params?: GetItemListParams): Promise<ItemOption[]> {
|
||||
try {
|
||||
@@ -631,6 +634,7 @@ export async function getItemList(params?: GetItemListParams): Promise<ItemOptio
|
||||
searchParams.set('size', String(params?.size || 1000));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.itemType) searchParams.set('item_type', params.itemType);
|
||||
if (params?.excludeProcessId) searchParams.set('exclude_process_id', params.excludeProcessId);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/items?${searchParams.toString()}`,
|
||||
|
||||
Reference in New Issue
Block a user