309 lines
9.0 KiB
TypeScript
309 lines
9.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useRef } from "react";
|
|
import { useForm, Controller } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
Button,
|
|
Input,
|
|
Textarea,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
Label,
|
|
ComboboxItemProps,
|
|
} from "ikoncomponents";
|
|
import { Plus } from "lucide-react";
|
|
import { getUsersApi } from "@/app/utils/api/projectApi";
|
|
import { projectSchema, ProjectFormData } from "./zodProject";
|
|
import { useAppCache } from "@/app/utils/context/AppCacheContext";
|
|
|
|
interface User {
|
|
userId: string;
|
|
userName: string;
|
|
userEmail: string;
|
|
active: boolean;
|
|
}
|
|
|
|
interface CreateProjectModalProps {
|
|
open: boolean;
|
|
onClose: (open: boolean) => void;
|
|
onSubmit: (data: ProjectFormData) => void;
|
|
projectManagerDropdownOptions: ComboboxItemProps[];
|
|
}
|
|
|
|
export default function CreateProjectModal({
|
|
open,
|
|
onClose,
|
|
onSubmit,
|
|
projectManagerDropdownOptions,
|
|
}: CreateProjectModalProps) {
|
|
const {
|
|
control,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
setValue,
|
|
watch,
|
|
reset,
|
|
} = useForm<ProjectFormData>({
|
|
resolver: zodResolver(projectSchema),
|
|
defaultValues: {
|
|
projectName: "",
|
|
projectDescription: "",
|
|
projectStatus: "Active",
|
|
projectManager: "",
|
|
projectTeam: [],
|
|
contractedStartDate: "",
|
|
contractedEndDate: "",
|
|
source: "manual",
|
|
productType: "",
|
|
},
|
|
});
|
|
|
|
const projectTeam = watch("projectTeam");
|
|
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [loadingUsers, setLoadingUsers] = useState(false);
|
|
const [teamDropdownOpen, setTeamDropdownOpen] = useState(false);
|
|
const [managerDropdown, setManagerDropdown] = useState<ComboboxItemProps[]>(
|
|
[],
|
|
);
|
|
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Project Manager options come from the shared app-wide cache.
|
|
const { projectManagerOptions } = useAppCache();
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
reset();
|
|
return;
|
|
}
|
|
|
|
const fetchUsers = async () => {
|
|
setLoadingUsers(true);
|
|
try {
|
|
const data = await getUsersApi();
|
|
setUsers(data ?? []);
|
|
} finally {
|
|
setLoadingUsers(false);
|
|
}
|
|
};
|
|
|
|
fetchUsers();
|
|
}, [open, reset]);
|
|
|
|
// Keep the manager dropdown in sync with the cached Project Manager options.
|
|
useEffect(() => {
|
|
setManagerDropdown(
|
|
projectManagerOptions.map((opt) => ({
|
|
label: opt.label,
|
|
value: opt.value,
|
|
})),
|
|
);
|
|
}, [projectManagerOptions]);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
dropdownRef.current &&
|
|
!dropdownRef.current.contains(event.target as Node)
|
|
) {
|
|
setTeamDropdownOpen(false);
|
|
}
|
|
};
|
|
|
|
if (teamDropdownOpen) {
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
}
|
|
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, [teamDropdownOpen]);
|
|
|
|
const toggleTeamMember = (userId: string) => {
|
|
setValue(
|
|
"projectTeam",
|
|
projectTeam.includes(userId)
|
|
? projectTeam.filter((id) => id !== userId)
|
|
: [...projectTeam, userId],
|
|
{ shouldValidate: true },
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-3xl!">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-xl">Create New Project</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-5 pt-2">
|
|
{/* Project Name */}
|
|
<div className="space-y-1.5">
|
|
<Label>
|
|
Project Name<span className="ml-0.5 text-red-500">*</span>
|
|
</Label>
|
|
<Controller
|
|
control={control}
|
|
name="projectName"
|
|
render={({ field }) => <Input {...field} />}
|
|
/>
|
|
{errors.projectName && (
|
|
<p className="text-xs text-red-500">
|
|
{errors.projectName.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div className="space-y-1.5">
|
|
<Label>
|
|
Description<span className="ml-0.5 text-red-500">*</span>
|
|
</Label>
|
|
<Controller
|
|
control={control}
|
|
name="projectDescription"
|
|
render={({ field }) => <Textarea {...field} />}
|
|
/>
|
|
{errors.projectDescription && (
|
|
<p className="text-xs text-red-500">
|
|
{errors.projectDescription.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Dates */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-1.5">
|
|
<Label>
|
|
Start Date<span className="ml-0.5 text-red-500">*</span>
|
|
</Label>
|
|
<Controller
|
|
control={control}
|
|
name="contractedStartDate"
|
|
render={({ field }) => <Input type="date" {...field} />}
|
|
/>
|
|
{errors.contractedStartDate && (
|
|
<p className="text-xs text-red-500">
|
|
{errors.contractedStartDate.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label>
|
|
End Date<span className="ml-0.5 text-red-500">*</span>
|
|
</Label>
|
|
<Controller
|
|
control={control}
|
|
name="contractedEndDate"
|
|
render={({ field }) => <Input type="date" {...field} />}
|
|
/>
|
|
{errors.contractedEndDate && (
|
|
<p className="text-xs text-red-500">
|
|
{errors.contractedEndDate.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{/* Project Manager */}
|
|
<div className="space-y-1.5">
|
|
<Label>
|
|
Project Manager<span className="ml-0.5 text-red-500">*</span>
|
|
</Label>
|
|
|
|
<Controller
|
|
control={control}
|
|
name="projectManager"
|
|
render={({ field }) => (
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="Select manager" />
|
|
</SelectTrigger>
|
|
<SelectContent className="w-full">
|
|
{managerDropdown.map((option, index) => {
|
|
return (
|
|
<SelectItem key={index} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
);
|
|
})}
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
/>
|
|
|
|
{errors.projectManager && (
|
|
<p className="text-xs text-red-500">
|
|
{errors.projectManager.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Project Team */}
|
|
<div ref={dropdownRef} className="space-y-1.5">
|
|
<Label>
|
|
Project Team<span className="ml-0.5 text-red-500">*</span>
|
|
</Label>
|
|
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className="w-full justify-between"
|
|
onClick={() => setTeamDropdownOpen((p) => !p)}
|
|
>
|
|
{projectTeam.length
|
|
? `${projectTeam.length} member(s) selected`
|
|
: "Select team members"}
|
|
▾
|
|
</Button>
|
|
|
|
{teamDropdownOpen && (
|
|
<div className="mt-1.5 rounded-md border bg-background shadow-md">
|
|
<div className="max-h-56 space-y-1 overflow-y-auto p-2">
|
|
{users.map((u) => (
|
|
<label
|
|
key={u.userId}
|
|
className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-muted"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={projectTeam.includes(u.userId)}
|
|
onChange={() => toggleTeamMember(u.userId)}
|
|
/>
|
|
{u.userName}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{errors.projectTeam && (
|
|
<p className="text-xs text-red-500">
|
|
{errors.projectTeam.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="mt-2 flex justify-end gap-3 border-t pt-4">
|
|
<Button type="submit" className="flex items-center gap-2">
|
|
<Plus className="h-4 w-4" />
|
|
Create Project
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|