first commit
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user