first commit

This commit is contained in:
Your NamebaishaliHolocron
2026-06-15 12:57:03 +05:30
commit b9ac5ae0b2
398 changed files with 49583 additions and 0 deletions

View File

@@ -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>
);
}

View File

@@ -0,0 +1,37 @@
import { z } from "zod";
export const projectSchema = z
.object({
projectName: z.string().min(1, "Project name is required"),
projectDescription
: z.string("Description is required"),
projectStatus: z.string(),
projectManager: z
.string("Invalid"),
projectTeam: z
.array(z.string())
.min(1, "Select at least one team member"),
contractedStartDate: z.string().min(1, "Start date is required"),
contractedEndDate: z.string().min(1, "End date is required"),
source: z.string("manual").optional(),
productType: z.string().optional(),
})
.refine(
(data) =>
new Date(data.contractedEndDate) >=
new Date(data.contractedStartDate),
{
message: "End date must be after start date",
path: ["contractedEndDate"],
}
);
export type ProjectFormData = z.infer<typeof projectSchema>;