feat(WEB): 직급/직책 관리 API 연동 완료

- RankManagement: 직급 목록/생성/수정/삭제 API 연동
- TitleManagement: 직책 목록/생성/수정/삭제 API 연동
- RankDialog/TitleDialog 폼 유효성 검사 개선
- 정렬 순서, 활성화 상태 관리 기능 추가
This commit is contained in:
2025-12-30 17:18:13 +09:00
parent 1fcefb1d2b
commit a45ff9af28
6 changed files with 384 additions and 189 deletions

View File

@@ -11,6 +11,7 @@ import {
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Loader2 } from 'lucide-react';
import type { RankDialogProps } from './types';
/**
@@ -21,7 +22,8 @@ export function RankDialog({
onOpenChange,
mode,
rank,
onSubmit
onSubmit,
isLoading = false,
}: RankDialogProps) {
const [name, setName] = useState('');
@@ -70,10 +72,13 @@ export function RankDialog({
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isLoading}>
</Button>
<Button type="submit" disabled={!name.trim()}>
<Button type="submit" disabled={!name.trim() || isLoading}>
{isLoading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : null}
{submitText}
</Button>
</DialogFooter>

View File

@@ -1,9 +1,9 @@
'use client';
import { useState, useCallback } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Award, Plus, GripVertical, Pencil, Trash2 } from 'lucide-react';
import { Award, Plus, GripVertical, Pencil, Trash2, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent } from '@/components/ui/card';
@@ -18,28 +18,36 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { toast } from 'sonner';
import type { Rank } from './types';
import {
fetchRanks,
createRank,
updatePosition,
deletePosition,
reorderPositions,
type Position,
} from '@/lib/api/positions';
/**
* 기본 직급 데이터 (PDF 51페이지 기준)
* Position → Rank 변환
*/
const defaultRanks: Rank[] = [
{ id: 1, name: '사원', order: 1 },
{ id: 2, name: '대리', order: 2 },
{ id: 3, name: '과장', order: 3 },
{ id: 4, name: '차장', order: 4 },
{ id: 5, name: '부장', order: 5 },
{ id: 6, name: '이사', order: 6 },
{ id: 7, name: '상무', order: 7 },
{ id: 8, name: '전무', order: 8 },
{ id: 9, name: '부사장', order: 9 },
{ id: 10, name: '사장', order: 10 },
{ id: 11, name: '회장', order: 11 },
];
function positionToRank(position: Position): Rank {
return {
id: position.id,
name: position.name,
order: position.sort_order,
isActive: position.is_active,
createdAt: position.created_at,
updatedAt: position.updated_at,
};
}
export function RankManagement() {
// 직급 데이터
const [ranks, setRanks] = useState<Rank[]>(defaultRanks);
const [ranks, setRanks] = useState<Rank[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
// 입력 필드
const [newRankName, setNewRankName] = useState('');
@@ -56,19 +64,41 @@ export function RankManagement() {
// 드래그 상태
const [draggedItem, setDraggedItem] = useState<number | null>(null);
// 데이터 로드
const loadRanks = useCallback(async () => {
try {
setIsLoading(true);
const positions = await fetchRanks();
setRanks(positions.map(positionToRank));
} catch (error) {
console.error('직급 목록 조회 실패:', error);
toast.error('직급 목록을 불러오는데 실패했습니다.');
} finally {
setIsLoading(false);
}
}, []);
// 초기 데이터 로드
useEffect(() => {
loadRanks();
}, [loadRanks]);
// 직급 추가 (입력 필드에서 직접)
const handleQuickAdd = () => {
if (!newRankName.trim()) return;
const handleQuickAdd = async () => {
if (!newRankName.trim() || isSubmitting) return;
const newId = Math.max(...ranks.map(r => r.id), 0) + 1;
const newOrder = Math.max(...ranks.map(r => r.order), 0) + 1;
setRanks(prev => [...prev, {
id: newId,
name: newRankName.trim(),
order: newOrder,
}]);
setNewRankName('');
try {
setIsSubmitting(true);
const newPosition = await createRank({ name: newRankName.trim() });
setRanks(prev => [...prev, positionToRank(newPosition)]);
setNewRankName('');
toast.success('직급이 추가되었습니다.');
} catch (error) {
console.error('직급 추가 실패:', error);
toast.error('직급 추가에 실패했습니다.');
} finally {
setIsSubmitting(false);
}
};
// 직급 수정 다이얼로그 열기
@@ -85,20 +115,40 @@ export function RankManagement() {
};
// 삭제 실행
const confirmDelete = () => {
if (rankToDelete) {
const confirmDelete = async () => {
if (!rankToDelete || isSubmitting) return;
try {
setIsSubmitting(true);
await deletePosition(rankToDelete.id);
setRanks(prev => prev.filter(r => r.id !== rankToDelete.id));
toast.success('직급이 삭제되었습니다.');
} catch (error) {
console.error('직급 삭제 실패:', error);
toast.error('직급 삭제에 실패했습니다.');
} finally {
setIsSubmitting(false);
setDeleteDialogOpen(false);
setRankToDelete(null);
}
setDeleteDialogOpen(false);
setRankToDelete(null);
};
// 다이얼로그 제출
const handleDialogSubmit = (name: string) => {
const handleDialogSubmit = async (name: string) => {
if (dialogMode === 'edit' && selectedRank) {
setRanks(prev => prev.map(r =>
r.id === selectedRank.id ? { ...r, name } : r
));
try {
setIsSubmitting(true);
await updatePosition(selectedRank.id, { name });
setRanks(prev => prev.map(r =>
r.id === selectedRank.id ? { ...r, name } : r
));
toast.success('직급이 수정되었습니다.');
} catch (error) {
console.error('직급 수정 실패:', error);
toast.error('직급 수정에 실패했습니다.');
} finally {
setIsSubmitting(false);
}
}
setDialogOpen(false);
};
@@ -109,9 +159,26 @@ export function RankManagement() {
e.dataTransfer.effectAllowed = 'move';
};
// 드래그 종료
const handleDragEnd = () => {
// 드래그 종료 - 서버에 순서 저장
const handleDragEnd = async () => {
if (draggedItem === null) return;
setDraggedItem(null);
// 순서 변경 API 호출
try {
const items = ranks.map((rank, idx) => ({
id: rank.id,
sort_order: idx + 1,
}));
await reorderPositions(items);
toast.success('순서가 변경되었습니다.');
} catch (error) {
console.error('순서 변경 실패:', error);
toast.error('순서 변경에 실패했습니다.');
// 실패시 원래 순서로 복구
loadRanks();
}
};
// 드래그 오버
@@ -124,7 +191,7 @@ export function RankManagement() {
newRanks.splice(draggedItem, 1);
newRanks.splice(index, 0, draggedRank);
// 순서 업데이트
// 순서 업데이트 (로컬)
const reorderedRanks = newRanks.map((rank, idx) => ({
...rank,
order: idx + 1
@@ -160,9 +227,17 @@ export function RankManagement() {
onKeyDown={handleKeyDown}
placeholder="직급명을 입력하세요"
className="flex-1"
disabled={isSubmitting}
/>
<Button onClick={handleQuickAdd} disabled={!newRankName.trim()}>
<Plus className="h-4 w-4 mr-2" />
<Button
onClick={handleQuickAdd}
disabled={!newRankName.trim() || isSubmitting}
>
{isSubmitting ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Plus className="h-4 w-4 mr-2" />
)}
</Button>
</div>
@@ -172,59 +247,68 @@ export function RankManagement() {
{/* 직급 목록 */}
<Card>
<CardContent className="p-0">
<div className="divide-y">
{ranks.map((rank, index) => (
<div
key={rank.id}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragEnd={handleDragEnd}
onDragOver={(e) => handleDragOver(e, index)}
className={`flex items-center gap-3 px-4 py-3 hover:bg-muted/50 transition-colors cursor-move ${
draggedItem === index ? 'opacity-50 bg-muted' : ''
}`}
>
{/* 드래그 핸들 */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0" />
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground"> ...</span>
</div>
) : (
<div className="divide-y">
{ranks.map((rank, index) => (
<div
key={rank.id}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragEnd={handleDragEnd}
onDragOver={(e) => handleDragOver(e, index)}
className={`flex items-center gap-3 px-4 py-3 hover:bg-muted/50 transition-colors cursor-move ${
draggedItem === index ? 'opacity-50 bg-muted' : ''
}`}
>
{/* 드래그 핸들 */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0" />
{/* 순서 번호 */}
<span className="text-sm text-muted-foreground w-8">
{index + 1}
</span>
{/* 순서 번호 */}
<span className="text-sm text-muted-foreground w-8">
{index + 1}
</span>
{/* 직급명 */}
<span className="flex-1 font-medium">{rank.name}</span>
{/* 직급명 */}
<span className="flex-1 font-medium">{rank.name}</span>
{/* 액션 버튼 */}
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(rank)}
className="h-8 w-8 p-0"
>
<Pencil className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(rank)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
{/* 액션 버튼 */}
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(rank)}
className="h-8 w-8 p-0"
disabled={isSubmitting}
>
<Pencil className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(rank)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
disabled={isSubmitting}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
</div>
</div>
</div>
))}
))}
{ranks.length === 0 && (
<div className="px-4 py-8 text-center text-muted-foreground">
.
</div>
)}
</div>
{ranks.length === 0 && (
<div className="px-4 py-8 text-center text-muted-foreground">
.
</div>
)}
</div>
)}
</CardContent>
</Card>
@@ -241,6 +325,7 @@ export function RankManagement() {
mode={dialogMode}
rank={selectedRank}
onSubmit={handleDialogSubmit}
isLoading={isSubmitting}
/>
{/* 삭제 확인 다이얼로그 */}
@@ -249,7 +334,7 @@ export function RankManagement() {
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>
"{rankToDelete?.name}" ?
&quot;{rankToDelete?.name}&quot; ?
<br />
<span className="text-destructive">
.
@@ -257,11 +342,15 @@ export function RankManagement() {
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel disabled={isSubmitting}></AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
disabled={isSubmitting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isSubmitting ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : null}
</AlertDialogAction>
</AlertDialogFooter>
@@ -269,4 +358,4 @@ export function RankManagement() {
</AlertDialog>
</PageLayout>
);
}
}

View File

@@ -5,6 +5,7 @@ export interface Rank {
id: number;
name: string;
order: number;
isActive?: boolean;
createdAt?: string;
updatedAt?: string;
}
@@ -15,4 +16,5 @@ export interface RankDialogProps {
mode: 'add' | 'edit';
rank?: Rank;
onSubmit: (name: string) => void;
isLoading?: boolean;
}

View File

@@ -11,6 +11,7 @@ import {
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Loader2 } from 'lucide-react';
import type { TitleDialogProps } from './types';
/**
@@ -21,7 +22,8 @@ export function TitleDialog({
onOpenChange,
mode,
title,
onSubmit
onSubmit,
isLoading = false,
}: TitleDialogProps) {
const [name, setName] = useState('');
@@ -65,15 +67,19 @@ export function TitleDialog({
onChange={(e) => setName(e.target.value)}
placeholder="직책명을 입력하세요"
autoFocus
disabled={isLoading}
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isLoading}>
</Button>
<Button type="submit" disabled={!name.trim()}>
<Button type="submit" disabled={!name.trim() || isLoading}>
{isLoading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : null}
{submitText}
</Button>
</DialogFooter>

View File

@@ -1,9 +1,9 @@
'use client';
import { useState } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Briefcase, Plus, GripVertical, Pencil, Trash2 } from 'lucide-react';
import { Briefcase, Plus, GripVertical, Pencil, Trash2, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent } from '@/components/ui/card';
@@ -16,28 +16,38 @@ import {
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTitle as AlertTitle,
} from '@/components/ui/alert-dialog';
import { toast } from 'sonner';
import type { Title } from './types';
import {
fetchTitles,
createTitle,
updatePosition,
deletePosition,
reorderPositions,
type Position,
} from '@/lib/api/positions';
/**
* 기본 직책 데이터 (PDF 52페이지 기준)
* Position → Title 변환
*/
const defaultTitles: Title[] = [
{ id: 1, name: '없음(기본)', order: 1 },
{ id: 2, name: '팀장', order: 2 },
{ id: 3, name: '파트장', order: 3 },
{ id: 4, name: '실장', order: 4 },
{ id: 5, name: '부서장', order: 5 },
{ id: 6, name: '본부장', order: 6 },
{ id: 7, name: '센터장', order: 7 },
{ id: 8, name: '매니저', order: 8 },
{ id: 9, name: '리더', order: 9 },
];
function positionToTitle(position: Position): Title {
return {
id: position.id,
name: position.name,
order: position.sort_order,
isActive: position.is_active,
createdAt: position.created_at,
updatedAt: position.updated_at,
};
}
export function TitleManagement() {
// 직책 데이터
const [titles, setTitles] = useState<Title[]>(defaultTitles);
const [titles, setTitles] = useState<Title[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
// 입력 필드
const [newTitleName, setNewTitleName] = useState('');
@@ -54,19 +64,41 @@ export function TitleManagement() {
// 드래그 상태
const [draggedItem, setDraggedItem] = useState<number | null>(null);
// 데이터 로드
const loadTitles = useCallback(async () => {
try {
setIsLoading(true);
const positions = await fetchTitles();
setTitles(positions.map(positionToTitle));
} catch (error) {
console.error('직책 목록 조회 실패:', error);
toast.error('직책 목록을 불러오는데 실패했습니다.');
} finally {
setIsLoading(false);
}
}, []);
// 초기 데이터 로드
useEffect(() => {
loadTitles();
}, [loadTitles]);
// 직책 추가 (입력 필드에서 직접)
const handleQuickAdd = () => {
if (!newTitleName.trim()) return;
const handleQuickAdd = async () => {
if (!newTitleName.trim() || isSubmitting) return;
const newId = Math.max(...titles.map(t => t.id), 0) + 1;
const newOrder = Math.max(...titles.map(t => t.order), 0) + 1;
setTitles(prev => [...prev, {
id: newId,
name: newTitleName.trim(),
order: newOrder,
}]);
setNewTitleName('');
try {
setIsSubmitting(true);
const newPosition = await createTitle({ name: newTitleName.trim() });
setTitles(prev => [...prev, positionToTitle(newPosition)]);
setNewTitleName('');
toast.success('직책이 추가되었습니다.');
} catch (error) {
console.error('직책 추가 실패:', error);
toast.error('직책 추가에 실패했습니다.');
} finally {
setIsSubmitting(false);
}
};
// 직책 수정 다이얼로그 열기
@@ -83,20 +115,40 @@ export function TitleManagement() {
};
// 삭제 실행
const confirmDelete = () => {
if (titleToDelete) {
const confirmDelete = async () => {
if (!titleToDelete || isSubmitting) return;
try {
setIsSubmitting(true);
await deletePosition(titleToDelete.id);
setTitles(prev => prev.filter(t => t.id !== titleToDelete.id));
toast.success('직책이 삭제되었습니다.');
} catch (error) {
console.error('직책 삭제 실패:', error);
toast.error('직책 삭제에 실패했습니다.');
} finally {
setIsSubmitting(false);
setDeleteDialogOpen(false);
setTitleToDelete(null);
}
setDeleteDialogOpen(false);
setTitleToDelete(null);
};
// 다이얼로그 제출
const handleDialogSubmit = (name: string) => {
const handleDialogSubmit = async (name: string) => {
if (dialogMode === 'edit' && selectedTitle) {
setTitles(prev => prev.map(t =>
t.id === selectedTitle.id ? { ...t, name } : t
));
try {
setIsSubmitting(true);
await updatePosition(selectedTitle.id, { name });
setTitles(prev => prev.map(t =>
t.id === selectedTitle.id ? { ...t, name } : t
));
toast.success('직책이 수정되었습니다.');
} catch (error) {
console.error('직책 수정 실패:', error);
toast.error('직책 수정에 실패했습니다.');
} finally {
setIsSubmitting(false);
}
}
setDialogOpen(false);
};
@@ -107,9 +159,26 @@ export function TitleManagement() {
e.dataTransfer.effectAllowed = 'move';
};
// 드래그 종료
const handleDragEnd = () => {
// 드래그 종료 - 서버에 순서 저장
const handleDragEnd = async () => {
if (draggedItem === null) return;
setDraggedItem(null);
// 순서 변경 API 호출
try {
const items = titles.map((title, idx) => ({
id: title.id,
sort_order: idx + 1,
}));
await reorderPositions(items);
toast.success('순서가 변경되었습니다.');
} catch (error) {
console.error('순서 변경 실패:', error);
toast.error('순서 변경에 실패했습니다.');
// 실패시 원래 순서로 복구
loadTitles();
}
};
// 드래그 오버
@@ -122,7 +191,7 @@ export function TitleManagement() {
newTitles.splice(draggedItem, 1);
newTitles.splice(index, 0, draggedTitle);
// 순서 업데이트
// 순서 업데이트 (로컬)
const reorderedTitles = newTitles.map((title, idx) => ({
...title,
order: idx + 1
@@ -158,9 +227,17 @@ export function TitleManagement() {
onKeyDown={handleKeyDown}
placeholder="직책명을 입력하세요"
className="flex-1"
disabled={isSubmitting}
/>
<Button onClick={handleQuickAdd} disabled={!newTitleName.trim()}>
<Plus className="h-4 w-4 mr-2" />
<Button
onClick={handleQuickAdd}
disabled={!newTitleName.trim() || isSubmitting}
>
{isSubmitting ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Plus className="h-4 w-4 mr-2" />
)}
</Button>
</div>
@@ -170,59 +247,68 @@ export function TitleManagement() {
{/* 직책 목록 */}
<Card>
<CardContent className="p-0">
<div className="divide-y">
{titles.map((title, index) => (
<div
key={title.id}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragEnd={handleDragEnd}
onDragOver={(e) => handleDragOver(e, index)}
className={`flex items-center gap-3 px-4 py-3 hover:bg-muted/50 transition-colors cursor-move ${
draggedItem === index ? 'opacity-50 bg-muted' : ''
}`}
>
{/* 드래그 핸들 */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0" />
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground"> ...</span>
</div>
) : (
<div className="divide-y">
{titles.map((title, index) => (
<div
key={title.id}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragEnd={handleDragEnd}
onDragOver={(e) => handleDragOver(e, index)}
className={`flex items-center gap-3 px-4 py-3 hover:bg-muted/50 transition-colors cursor-move ${
draggedItem === index ? 'opacity-50 bg-muted' : ''
}`}
>
{/* 드래그 핸들 */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0" />
{/* 순서 번호 */}
<span className="text-sm text-muted-foreground w-8">
{index + 1}
</span>
{/* 순서 번호 */}
<span className="text-sm text-muted-foreground w-8">
{index + 1}
</span>
{/* 직책명 */}
<span className="flex-1 font-medium">{title.name}</span>
{/* 직책명 */}
<span className="flex-1 font-medium">{title.name}</span>
{/* 액션 버튼 */}
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(title)}
className="h-8 w-8 p-0"
>
<Pencil className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(title)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
{/* 액션 버튼 */}
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(title)}
className="h-8 w-8 p-0"
disabled={isSubmitting}
>
<Pencil className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(title)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
disabled={isSubmitting}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
</div>
</div>
</div>
))}
))}
{titles.length === 0 && (
<div className="px-4 py-8 text-center text-muted-foreground">
.
</div>
)}
</div>
{titles.length === 0 && (
<div className="px-4 py-8 text-center text-muted-foreground">
.
</div>
)}
</div>
)}
</CardContent>
</Card>
@@ -239,15 +325,16 @@ export function TitleManagement() {
mode={dialogMode}
title={selectedTitle}
onSubmit={handleDialogSubmit}
isLoading={isSubmitting}
/>
{/* 삭제 확인 다이얼로그 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertTitle> </AlertTitle>
<AlertDialogDescription>
"{titleToDelete?.name}" ?
&quot;{titleToDelete?.name}&quot; ?
<br />
<span className="text-destructive">
.
@@ -255,11 +342,15 @@ export function TitleManagement() {
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel disabled={isSubmitting}></AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
disabled={isSubmitting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isSubmitting ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : null}
</AlertDialogAction>
</AlertDialogFooter>

View File

@@ -5,6 +5,7 @@ export interface Title {
id: number;
name: string;
order: number;
isActive?: boolean;
createdAt?: string;
updatedAt?: string;
}
@@ -15,4 +16,5 @@ export interface TitleDialogProps {
mode: 'add' | 'edit';
title?: Title;
onSubmit: (name: string) => void;
isLoading?: boolean;
}