518 lines
20 KiB
TypeScript
518 lines
20 KiB
TypeScript
import {
|
|
ResizableDialog,
|
|
ResizableDialogContent,
|
|
ResizableDialogHeader,
|
|
ResizableDialogTitle,
|
|
ResizableDialogDescription,
|
|
ResizableDialogFooter,
|
|
} from "@/components/ui/resizable-dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Camera, X, Car, Wrench, Clock, Plus, Trash2 } from "lucide-react";
|
|
import { ProfileFormData } from "@/types/profile";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { VehicleRegisterData } from "@/lib/api/driver";
|
|
|
|
// 운전자 정보 타입
|
|
export interface DriverInfo {
|
|
vehicleNumber: string;
|
|
vehicleType: string | null;
|
|
licenseNumber: string;
|
|
phoneNumber: string;
|
|
vehicleStatus: string | null;
|
|
branchName: string | null;
|
|
}
|
|
|
|
// 알림 모달 컴포넌트
|
|
interface AlertModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
title: string;
|
|
message: string;
|
|
type?: "success" | "error" | "info";
|
|
}
|
|
|
|
function AlertModal({ isOpen, onClose, title, message, type = "info" }: AlertModalProps) {
|
|
const getTypeColor = () => {
|
|
switch (type) {
|
|
case "success":
|
|
return "text-green-600";
|
|
case "error":
|
|
return "text-destructive";
|
|
default:
|
|
return "text-primary";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ResizableDialog open={isOpen} onOpenChange={onClose}>
|
|
<ResizableDialogContent className="sm:max-w-md">
|
|
<ResizableDialogHeader>
|
|
<ResizableDialogTitle className={getTypeColor()}>{title}</ResizableDialogTitle>
|
|
</ResizableDialogHeader>
|
|
<div className="py-4">
|
|
<p className="text-sm text-muted-foreground">{message}</p>
|
|
</div>
|
|
<div className="flex justify-end">
|
|
<Button onClick={onClose} className="w-20">
|
|
확인
|
|
</Button>
|
|
</div>
|
|
</ResizableDialogContent>
|
|
</ResizableDialog>
|
|
);
|
|
}
|
|
|
|
// 운전자 폼 데이터 타입
|
|
export interface DriverFormData {
|
|
vehicleNumber: string;
|
|
vehicleType: string;
|
|
licenseNumber: string;
|
|
phoneNumber: string;
|
|
branchName: string;
|
|
}
|
|
|
|
interface ProfileModalProps {
|
|
isOpen: boolean;
|
|
user: any;
|
|
formData: ProfileFormData;
|
|
selectedImage: string;
|
|
isSaving: boolean;
|
|
departments: Array<{
|
|
deptCode: string;
|
|
deptName: string;
|
|
}>;
|
|
alertModal: {
|
|
isOpen: boolean;
|
|
title: string;
|
|
message: string;
|
|
type: "success" | "error" | "info";
|
|
};
|
|
// 운전자 관련 props (선택적)
|
|
isDriver?: boolean;
|
|
hasVehicle?: boolean;
|
|
driverInfo?: DriverInfo | null;
|
|
driverFormData?: DriverFormData;
|
|
onDriverFormChange?: (field: keyof DriverFormData, value: string) => void;
|
|
onDriverStatusChange?: (status: "off" | "maintenance") => void;
|
|
onDriverAccountDelete?: () => void;
|
|
// 차량 삭제/등록 관련 props
|
|
onDeleteVehicle?: () => void;
|
|
onOpenVehicleRegisterModal?: () => void;
|
|
// 새 차량 등록 모달 관련 props
|
|
isVehicleRegisterModalOpen?: boolean;
|
|
newVehicleData?: VehicleRegisterData;
|
|
onCloseVehicleRegisterModal?: () => void;
|
|
onNewVehicleDataChange?: (field: keyof VehicleRegisterData, value: string) => void;
|
|
onRegisterVehicle?: () => void;
|
|
onClose: () => void;
|
|
onFormChange: (field: keyof ProfileFormData, value: string) => void;
|
|
onImageSelect: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
onImageRemove: () => void;
|
|
onSave: () => void;
|
|
onAlertClose: () => void;
|
|
}
|
|
|
|
/**
|
|
* 프로필 수정 모달 컴포넌트
|
|
*/
|
|
export function ProfileModal({
|
|
isOpen,
|
|
user,
|
|
formData,
|
|
selectedImage,
|
|
isSaving,
|
|
departments,
|
|
alertModal,
|
|
isDriver = false,
|
|
hasVehicle = false,
|
|
driverInfo,
|
|
driverFormData,
|
|
onDriverFormChange,
|
|
onDriverStatusChange,
|
|
onDriverAccountDelete,
|
|
onDeleteVehicle,
|
|
onOpenVehicleRegisterModal,
|
|
isVehicleRegisterModalOpen = false,
|
|
newVehicleData,
|
|
onCloseVehicleRegisterModal,
|
|
onNewVehicleDataChange,
|
|
onRegisterVehicle,
|
|
onClose,
|
|
onFormChange,
|
|
onImageSelect,
|
|
onImageRemove,
|
|
onSave,
|
|
onAlertClose,
|
|
}: ProfileModalProps) {
|
|
// 차량 상태 한글 변환
|
|
const getStatusLabel = (status: string | null) => {
|
|
switch (status) {
|
|
case "off":
|
|
return "대기";
|
|
case "active":
|
|
return "운행중";
|
|
case "inactive":
|
|
return "공차";
|
|
case "maintenance":
|
|
return "정비";
|
|
default:
|
|
return status || "-";
|
|
}
|
|
};
|
|
return (
|
|
<>
|
|
<ResizableDialog open={isOpen} onOpenChange={onClose}>
|
|
<ResizableDialogContent className="sm:max-w-[500px]">
|
|
<ResizableDialogHeader>
|
|
<ResizableDialogTitle>프로필 수정</ResizableDialogTitle>
|
|
</ResizableDialogHeader>
|
|
|
|
<div className="grid gap-6 py-4">
|
|
{/* 프로필 사진 섹션 */}
|
|
<div className="flex flex-col items-center space-y-4">
|
|
<div className="relative">
|
|
<div className="relative flex h-24 w-24 shrink-0 overflow-hidden rounded-full">
|
|
{selectedImage && selectedImage.trim() !== "" ? (
|
|
<img
|
|
src={selectedImage}
|
|
alt="프로필 사진 미리보기"
|
|
className="aspect-square h-full w-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="flex h-full w-full items-center justify-center rounded-full bg-slate-200 text-2xl font-semibold text-slate-700">
|
|
{formData.userName?.substring(0, 1)?.toUpperCase() || "U"}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{selectedImage && selectedImage.trim() !== "" ? (
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
size="icon"
|
|
className="absolute -top-2 -right-2 h-6 w-6 rounded-full"
|
|
onClick={onImageRemove}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => document.getElementById("profile-image-input")?.click()}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<Camera className="h-4 w-4" />
|
|
사진 선택
|
|
</Button>
|
|
</div>
|
|
|
|
<input
|
|
id="profile-image-input"
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={onImageSelect}
|
|
className="hidden"
|
|
/>
|
|
</div>
|
|
|
|
{/* 사용자 정보 폼 */}
|
|
<div className="grid gap-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="userId">사용자 ID</Label>
|
|
<Input id="userId" value={user?.userId || ""} disabled className="bg-muted" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="userName">이름</Label>
|
|
<Input
|
|
id="userName"
|
|
value={formData.userName}
|
|
onChange={(e) => onFormChange("userName", e.target.value)}
|
|
placeholder="이름을 입력하세요"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">이메일</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
value={formData.email}
|
|
onChange={(e) => onFormChange("email", e.target.value)}
|
|
placeholder="이메일을 입력하세요"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="deptName">부서</Label>
|
|
<Select value={formData.deptName} onValueChange={(value) => onFormChange("deptName", value)}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="부서 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Array.isArray(departments) && departments.length > 0 ? (
|
|
departments.map((department) => (
|
|
<SelectItem key={department.deptCode} value={department.deptName}>
|
|
{department.deptName}
|
|
</SelectItem>
|
|
))
|
|
) : (
|
|
<SelectItem value="no-data" disabled>
|
|
부서 정보가 없습니다
|
|
</SelectItem>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="positionName">직급</Label>
|
|
<Input
|
|
id="positionName"
|
|
value={formData.positionName}
|
|
onChange={(e) => onFormChange("positionName", e.target.value)}
|
|
placeholder="직급을 입력하세요"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="locale">지역</Label>
|
|
<Select value={formData.locale || ""} onValueChange={(value) => onFormChange("locale", value)}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="선택해주세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="KR">한국어 (KR)</SelectItem>
|
|
<SelectItem value="US">English (US)</SelectItem>
|
|
<SelectItem value="JP">日本語 (JP)</SelectItem>
|
|
<SelectItem value="CN">中文 (CN)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 운전자 정보 섹션 (공차중계 사용자만) */}
|
|
{isDriver && (
|
|
<>
|
|
<Separator className="my-4" />
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Car className="h-5 w-5 text-primary" />
|
|
<h3 className="text-sm font-semibold">차량/운전자 정보</h3>
|
|
</div>
|
|
{/* 차량 유무에 따른 버튼 표시 */}
|
|
{hasVehicle ? (
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={onDeleteVehicle}
|
|
className="flex items-center gap-1"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
차량 삭제
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
type="button"
|
|
variant="default"
|
|
size="sm"
|
|
onClick={onOpenVehicleRegisterModal}
|
|
className="flex items-center gap-1"
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
새 차량 등록
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* 운전자 정보 (항상 수정 가능) */}
|
|
{driverFormData && onDriverFormChange && (
|
|
<>
|
|
{/* 차량 정보 - 차량이 있을 때만 수정 가능 */}
|
|
{hasVehicle ? (
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="vehicleNumber">차량번호</Label>
|
|
<Input
|
|
id="vehicleNumber"
|
|
value={driverFormData.vehicleNumber}
|
|
onChange={(e) => onDriverFormChange("vehicleNumber", e.target.value)}
|
|
placeholder="12가1234"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="vehicleType">차종</Label>
|
|
<Input
|
|
id="vehicleType"
|
|
value={driverFormData.vehicleType}
|
|
onChange={(e) => onDriverFormChange("vehicleType", e.target.value)}
|
|
placeholder="1톤 카고"
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* 차량이 없는 경우: 안내 메시지 */
|
|
<div className="text-center py-4 text-muted-foreground border rounded-md bg-muted/30">
|
|
<Car className="h-8 w-8 mx-auto mb-2 opacity-30" />
|
|
<p className="text-sm">등록된 차량이 없습니다.</p>
|
|
<p className="text-xs mt-1">새 차량 등록 버튼을 눌러 차량을 등록하세요.</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 운전자 개인 정보 - 항상 수정 가능 */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="driverPhone">연락처</Label>
|
|
<Input
|
|
id="driverPhone"
|
|
value={driverFormData.phoneNumber}
|
|
onChange={(e) => onDriverFormChange("phoneNumber", e.target.value)}
|
|
placeholder="010-1234-5678"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="licenseNumber">면허번호</Label>
|
|
<Input
|
|
id="licenseNumber"
|
|
value={driverFormData.licenseNumber}
|
|
onChange={(e) => onDriverFormChange("licenseNumber", e.target.value)}
|
|
placeholder="12-34-567890-12"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="branchName">소속 지점</Label>
|
|
<Input
|
|
id="branchName"
|
|
value={driverFormData.branchName}
|
|
onChange={(e) => onDriverFormChange("branchName", e.target.value)}
|
|
placeholder="서울 본점"
|
|
/>
|
|
</div>
|
|
|
|
{/* 차량 상태 - 차량이 있을 때만 표시 */}
|
|
{hasVehicle && driverInfo && onDriverStatusChange && (
|
|
<div className="space-y-2">
|
|
<Label>현재 차량 상태</Label>
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-sm font-medium px-3 py-1 rounded-full bg-muted">
|
|
{getStatusLabel(driverInfo.vehicleStatus)}
|
|
</span>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onDriverStatusChange("off")}
|
|
disabled={driverInfo.vehicleStatus === "off"}
|
|
className="flex items-center gap-1"
|
|
>
|
|
<Clock className="h-3 w-3" />
|
|
대기
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onDriverStatusChange("maintenance")}
|
|
disabled={driverInfo.vehicleStatus === "maintenance"}
|
|
className="flex items-center gap-1"
|
|
>
|
|
<Wrench className="h-3 w-3" />
|
|
정비
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
* 운행/공차 상태는 공차등록 화면에서 변경하세요
|
|
</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<ResizableDialogFooter>
|
|
<Button type="button" variant="outline" onClick={onClose} disabled={isSaving}>
|
|
취소
|
|
</Button>
|
|
<Button type="button" onClick={onSave} disabled={isSaving}>
|
|
{isSaving ? "저장 중..." : "저장"}
|
|
</Button>
|
|
</ResizableDialogFooter>
|
|
</ResizableDialogContent>
|
|
</ResizableDialog>
|
|
|
|
{/* 알림 모달 */}
|
|
<AlertModal
|
|
isOpen={alertModal.isOpen}
|
|
onClose={onAlertClose}
|
|
title={alertModal.title}
|
|
message={alertModal.message}
|
|
type={alertModal.type}
|
|
/>
|
|
|
|
{/* 새 차량 등록 모달 */}
|
|
{isVehicleRegisterModalOpen && newVehicleData && onNewVehicleDataChange && onRegisterVehicle && onCloseVehicleRegisterModal && (
|
|
<ResizableDialog open={isVehicleRegisterModalOpen} onOpenChange={onCloseVehicleRegisterModal}>
|
|
<ResizableDialogContent className="sm:max-w-[400px]">
|
|
<ResizableDialogHeader>
|
|
<ResizableDialogTitle>새 차량 등록</ResizableDialogTitle>
|
|
<ResizableDialogDescription>
|
|
새로운 차량 정보를 입력해주세요.
|
|
</ResizableDialogDescription>
|
|
</ResizableDialogHeader>
|
|
|
|
<div className="grid gap-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="newVehicleNumber">차량번호 *</Label>
|
|
<Input
|
|
id="newVehicleNumber"
|
|
value={newVehicleData.vehicleNumber}
|
|
onChange={(e) => onNewVehicleDataChange("vehicleNumber", e.target.value)}
|
|
placeholder="12가1234"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="newVehicleType">차종</Label>
|
|
<Input
|
|
id="newVehicleType"
|
|
value={newVehicleData.vehicleType || ""}
|
|
onChange={(e) => onNewVehicleDataChange("vehicleType", e.target.value)}
|
|
placeholder="1톤 카고"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<ResizableDialogFooter>
|
|
<Button type="button" variant="outline" onClick={onCloseVehicleRegisterModal}>
|
|
취소
|
|
</Button>
|
|
<Button type="button" onClick={onRegisterVehicle}>
|
|
등록
|
|
</Button>
|
|
</ResizableDialogFooter>
|
|
</ResizableDialogContent>
|
|
</ResizableDialog>
|
|
)}
|
|
</>
|
|
);
|
|
}
|