pmtool / src /components /todo /TodoForm.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
22.9 kB
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Todo, TodoCreateRequest, TodoUpdateRequest } from "@/types";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Calendar } from "@/components/ui/calendar";
import { format } from "date-fns";
import { CalendarIcon, Check, ChevronDown, Loader2, Search, User, Briefcase } from "lucide-react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { employeeService, projectService } from "@/lib/api";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
import { useAuth } from "@/lib/auth-context";
// Custom dropdown component
interface CustomDropdownProps {
value: string;
onChange: (value: string) => void;
options: { value: string; label: string; color?: string }[];
placeholder: string;
showBadge?: boolean;
}
function CustomDropdown({ value, onChange, options, placeholder, showBadge = false }: CustomDropdownProps) {
const [open, setOpen] = useState(false);
const getLabel = () => {
return options.find(option => option.value === value)?.label || placeholder;
};
const getColor = () => {
return options.find(option => option.value === value)?.color || "";
};
const handleButtonClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setOpen(!open);
};
const handleOptionClick = (optionValue: string) => {
onChange(optionValue);
setOpen(false);
};
return (
<div className="relative w-full">
<div
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer",
open && "ring-2 ring-ring ring-offset-2"
)}
onClick={handleButtonClick}
>
{showBadge && value ? (
<div className="flex items-center gap-2">
<Badge className={getColor()}>
{getLabel()}
</Badge>
</div>
) : (
<span className={!value ? "text-muted-foreground" : ""}>{getLabel()}</span>
)}
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
</div>
{open && (
<div className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
{options.map((option) => (
<div
key={option.value}
className={cn(
"flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 hover:bg-accent hover:text-accent-foreground",
value === option.value && "bg-accent text-accent-foreground"
)}
onClick={() => handleOptionClick(option.value)}
>
{showBadge ? (
<Badge className={option.color}>{option.label}</Badge>
) : (
option.label
)}
{value === option.value && <Check className="h-4 w-4" />}
</div>
))}
</div>
)}
</div>
);
}
// Custom Employee Dropdown component
interface CustomEmployeeDropdownProps {
value?: number;
onChange: (value: number) => void;
employees: Array<{id: number; firstName: string; lastName: string}>;
isLoading: boolean;
placeholder: string;
}
function CustomEmployeeDropdown({ value, onChange, employees, isLoading, placeholder }: CustomEmployeeDropdownProps) {
const [open, setOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const getEmployeeName = (id?: number) => {
if (!id) return placeholder;
const employee = employees.find(emp => emp.id === id);
return employee ? `${employee.firstName} ${employee.lastName}` : `Employee ${id}`;
};
const getInitials = (firstName: string, lastName: string) => {
return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase();
};
const handleButtonClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setOpen(!open);
};
const handleOptionClick = (employeeId: number) => {
onChange(employeeId);
setOpen(false);
};
// Filter employees based on search term
const filteredEmployees = searchTerm
? employees.filter(
emp =>
emp.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
emp.lastName.toLowerCase().includes(searchTerm.toLowerCase())
)
: employees;
return (
<div className="relative w-full">
<div
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer",
open && "ring-2 ring-ring ring-offset-2"
)}
onClick={handleButtonClick}
>
<div className="flex items-center">
{value && employees.find(e => e.id === value) ? (
<Avatar className="h-5 w-5 mr-2">
<AvatarFallback className="text-[10px]">
{getInitials(
employees.find(e => e.id === value)!.firstName,
employees.find(e => e.id === value)!.lastName
)}
</AvatarFallback>
</Avatar>
) : (
<User className="h-4 w-4 mr-2 opacity-50" />
)}
<span className={!value ? "text-muted-foreground truncate" : "truncate"}>
{getEmployeeName(value)}
</span>
</div>
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
</div>
{open && (
<div className="absolute z-50 mt-1 w-full overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
<div className="flex items-center border-b px-3 py-2">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<input
className="flex h-8 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Search employee..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onClick={(e) => e.stopPropagation()}
/>
</div>
<div className="max-h-60 overflow-auto py-1">
{isLoading ? (
<div className="flex items-center justify-center p-4">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="ml-2">Loading...</span>
</div>
) : filteredEmployees.length === 0 ? (
<div className="text-center p-2 text-sm text-muted-foreground">
No employee found
</div>
) : (
filteredEmployees.map((employee) => (
<div
key={employee.id}
className={cn(
"flex cursor-pointer items-center px-2 py-1.5 hover:bg-accent hover:text-accent-foreground",
value === employee.id && "bg-accent text-accent-foreground"
)}
onClick={() => handleOptionClick(employee.id)}
>
<Avatar className="h-5 w-5 mr-2">
<AvatarFallback className="text-[10px]">
{getInitials(employee.firstName, employee.lastName)}
</AvatarFallback>
</Avatar>
<span>{employee.firstName} {employee.lastName}</span>
{value === employee.id && (
<Check className="ml-auto h-4 w-4" />
)}
</div>
))
)}
</div>
</div>
)}
</div>
);
}
// Custom Project Dropdown component
interface CustomProjectDropdownProps {
value?: number;
onChange: (value: number) => void;
projects: Array<{id: number; projectName: string; projectCode: string}>;
isLoading: boolean;
placeholder: string;
}
function CustomProjectDropdown({ value, onChange, projects, isLoading, placeholder }: CustomProjectDropdownProps) {
const [open, setOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const getProjectName = (id?: number) => {
if (!id) return placeholder;
const project = projects.find(proj => proj.id === id);
return project ? project.projectName : `Project ${id}`;
};
const handleButtonClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setOpen(!open);
};
const handleOptionClick = (projectId: number) => {
onChange(projectId);
setOpen(false);
};
// Filter projects based on search term
const filteredProjects = searchTerm
? projects.filter(
proj =>
proj.projectName.toLowerCase().includes(searchTerm.toLowerCase()) ||
proj.projectCode.toLowerCase().includes(searchTerm.toLowerCase())
)
: projects;
return (
<div className="relative w-full">
<div
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer",
open && "ring-2 ring-ring ring-offset-2"
)}
onClick={handleButtonClick}
>
<div className="flex items-center">
{!value && <Briefcase className="h-4 w-4 mr-2 opacity-50" />}
{value && <Briefcase className="h-4 w-4 mr-2" />}
<span className={!value ? "text-muted-foreground truncate" : "truncate"}>
{getProjectName(value)}
</span>
</div>
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
</div>
{open && (
<div className="absolute z-50 mt-1 w-full overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
<div className="flex items-center border-b px-3 py-2">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<input
className="flex h-8 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Search project..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onClick={(e) => e.stopPropagation()}
/>
</div>
<div className="max-h-60 overflow-auto py-1">
{isLoading ? (
<div className="flex items-center justify-center p-4">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="ml-2">Loading...</span>
</div>
) : filteredProjects.length === 0 ? (
<div className="text-center p-2 text-sm text-muted-foreground">
No project found
</div>
) : (
filteredProjects.map((project) => (
<div
key={project.id}
className={cn(
"flex cursor-pointer items-center px-2 py-1.5 hover:bg-accent hover:text-accent-foreground",
value === project.id && "bg-accent text-accent-foreground"
)}
onClick={() => handleOptionClick(project.id)}
>
<Briefcase className="h-4 w-4 mr-2" />
<span>{project.projectName}</span>
<span className="ml-2 text-xs text-muted-foreground">
({project.projectCode})
</span>
{value === project.id && (
<Check className="ml-auto h-4 w-4" />
)}
</div>
))
)}
</div>
</div>
)}
</div>
);
}
// Custom DatePicker component
interface CustomDatePickerProps {
value: Date;
onChange: (date: Date) => void;
}
function CustomDatePicker({ value, onChange }: CustomDatePickerProps) {
const [open, setOpen] = useState(false);
const handleButtonClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setOpen(!open);
};
return (
<div className="relative w-full">
<div
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer",
open && "ring-2 ring-ring ring-offset-2"
)}
onClick={handleButtonClick}
>
{value ? format(value, "PPP") : <span className="text-muted-foreground">Pick a date</span>}
<CalendarIcon className="h-4 w-4 shrink-0 opacity-50" />
</div>
{open && (
<div className="absolute z-50 mt-1 overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
<Calendar
mode="single"
selected={value}
onSelect={(date) => {
if (date) {
onChange(date);
setOpen(false);
}
}}
initialFocus
/>
</div>
)}
</div>
);
}
const todoSchema = z.object({
title: z.string().min(3, { message: "Title must be at least 3 characters" }),
description: z.string().min(5, { message: "Description must be at least 5 characters" }),
status: z.enum(["Pending", "In Progress", "Completed"], {
required_error: "Please select a status",
}),
priority: z.enum(["Low", "Medium", "High"], {
required_error: "Please select a priority",
}),
dueDate: z.date({
required_error: "Due date is required",
}),
assigneeId: z.number().optional(),
projectId: z.number().optional(),
});
type TodoFormValues = z.infer<typeof todoSchema>;
type TodoFormProps = {
todo?: Todo;
onSubmit: (data: TodoCreateRequest | TodoUpdateRequest) => void;
onCancel: () => void;
isLoading?: boolean;
};
const statusOptions = [
{ value: "Pending", label: "Pending" },
{ value: "In Progress", label: "In Progress" },
{ value: "Completed", label: "Completed" },
];
const priorityOptions = [
{ value: "Low", label: "Low", color: "bg-green-100 text-green-800" },
{ value: "Medium", label: "Medium", color: "bg-yellow-100 text-yellow-800" },
{ value: "High", label: "High", color: "bg-red-100 text-red-800" },
];
type Employee = {
id: number;
firstName: string;
lastName: string;
email: string;
};
type Project = {
id: number;
projectName: string;
projectCode: string;
};
export function TodoForm({ todo, onSubmit, onCancel, isLoading = false }: TodoFormProps) {
const { userData } = useAuth();
const [employees, setEmployees] = useState<Employee[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
const [loadingEmployees, setLoadingEmployees] = useState(false);
const [loadingProjects, setLoadingProjects] = useState(false);
// Initialize form with default values or the provided todo
const form = useForm<TodoFormValues>({
resolver: zodResolver(todoSchema),
defaultValues: {
title: "",
description: "",
status: "Pending",
priority: "Medium",
dueDate: new Date(),
assigneeId: undefined,
projectId: undefined,
},
});
// Fetch employees and projects
useEffect(() => {
const fetchEmployees = async () => {
setLoadingEmployees(true);
try {
const response = await employeeService.getAll();
if (Array.isArray(response)) {
setEmployees(response);
}
} catch (error) {
console.error("Failed to fetch employees:", error);
} finally {
setLoadingEmployees(false);
}
};
const fetchProjects = async () => {
setLoadingProjects(true);
try {
const response = await projectService.getAll();
if (Array.isArray(response)) {
setProjects(response);
}
} catch (error) {
console.error("Failed to fetch projects:", error);
} finally {
setLoadingProjects(false);
}
};
fetchEmployees();
fetchProjects();
}, []);
// Update form values when todo changes
useEffect(() => {
if (todo) {
form.reset({
title: todo.title,
description: todo.description,
status: todo.status,
priority: todo.priority,
dueDate: new Date(todo.dueDate),
assigneeId: todo.assigneeId,
projectId: todo.projectId,
});
}
}, [todo, form]);
const handleSubmit = (values: TodoFormValues) => {
try {
const todoData = {
...values,
dueDate: format(values.dueDate, "yyyy-MM-dd"),
};
// If creating a new todo and no assignee is selected, use the current user's employeeId
if (!todo && !values.assigneeId && userData?.employeeId) {
todoData.assigneeId = userData.employeeId;
}
if (todo) {
onSubmit({
...todoData,
id: todo.id,
} as TodoUpdateRequest);
} else {
onSubmit(todoData as TodoCreateRequest);
}
} catch (error) {
console.error("Error submitting todo form:", error);
// You can add additional error handling here if needed
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Enter todo title" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Enter todo description"
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<FormControl>
<CustomDropdown
value={field.value}
onChange={field.onChange}
options={statusOptions}
placeholder="Select status"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem>
<FormLabel>Priority</FormLabel>
<FormControl>
<CustomDropdown
value={field.value}
onChange={field.onChange}
options={priorityOptions}
placeholder="Select priority"
showBadge={true}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="dueDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Due Date</FormLabel>
<FormControl>
<CustomDatePicker
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="assigneeId"
render={({ field }) => (
<FormItem>
<FormLabel>Assignee</FormLabel>
<FormControl>
<CustomEmployeeDropdown
value={field.value}
onChange={field.onChange}
employees={employees}
isLoading={loadingEmployees}
placeholder="Select assignee"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="projectId"
render={({ field }) => (
<FormItem>
<FormLabel>Project</FormLabel>
<FormControl>
<CustomProjectDropdown
value={field.value}
onChange={field.onChange}
projects={projects}
isLoading={loadingProjects}
placeholder="Select project"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex items-center justify-end space-x-4 pt-4">
<Button type="button" variant="outline" onClick={onCancel} disabled={isLoading}>
Cancel
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? "Saving..." : todo ? "Update Todo" : "Create Todo"}
</Button>
</div>
</form>
</Form>
);
}