ERP-node/frontend/components/layout/ProfileModal.tsx

402 lines
14 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 } from "lucide-react";
import { ProfileFormData } from "@/types/profile";
import { Separator } from "@/components/ui/separator";
// 운전자 정보 타입
export interface DriverInfo {
vehicleNumber: string;
vehicleType: string | null;
licenseNumber: string;
phoneNumber: string;
vehicleStatus: 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;
}
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;
driverInfo?: DriverInfo | null;
driverFormData?: DriverFormData;
onDriverFormChange?: (field: keyof DriverFormData, value: string) => void;
onDriverStatusChange?: (status: "off" | "maintenance") => void;
onDriverAccountDelete?: () => 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,
driverInfo,
driverFormData,
onDriverFormChange,
onDriverStatusChange,
onDriverAccountDelete,
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 && driverFormData && onDriverFormChange && (
<>
<Separator className="my-4" />
<div className="space-y-4">
<div className="flex items-center gap-2">
<Car className="h-5 w-5 text-primary" />
<h3 className="text-sm font-semibold">/ </h3>
</div>
<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="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>
{/* 차량 상태 */}
{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}
/>
</>
);
}