pmtool / src /components /tasks /SimpleAddTaskDialog.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
19.7 kB
import React, { useState, useEffect, useRef } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { CalendarIcon, Loader2, X, ChevronDown, Check, Search } from "lucide-react";
import { format } from "date-fns";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Calendar } from "@/components/ui/calendar";
import { toast } from "@/lib/custom-toast";
import { tasksApi, TaskCreateRequest } from "@/services/tasksApi";
import { Employee } from "@/types";
import { cn } from "@/lib/utils";
import SlateRichTextEditor from "@/components/custom/SlateRichTextEditor";
// Form schema
const formSchema = z.object({
title: z.string().min(1, "Title is required"),
description: z.string().min(1, "Description is required"),
type: z.string().min(1, "Type is required"),
priority: z.string().min(1, "Priority is required"),
status: z.string().min(1, "Status is required"),
assignedTo: z.number().nullable(),
estimates: z.string().optional(),
stateDate: z.date(),
endDate: z.date().nullable().optional(),
});
// Custom dropdown component
interface DropdownOption {
value: string;
label: string;
}
interface CustomDropdownProps {
options: DropdownOption[];
value: string;
onChange: (value: string) => void;
placeholder?: string;
}
const CustomDropdown: React.FC<CustomDropdownProps> = ({
options,
value,
onChange,
placeholder = "Select option"
}) => {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const dropdownRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
// Focus search input when dropdown opens
useEffect(() => {
if (isOpen && searchInputRef.current && options.length > 5) {
setTimeout(() => {
searchInputRef.current?.focus();
}, 10);
}
}, [isOpen, options.length]);
// Clear search when dropdown closes
useEffect(() => {
if (!isOpen) {
setSearchQuery('');
}
}, [isOpen]);
const selectedOption = options.find(option => option.value === value);
// Filter options based on search query
const filteredOptions = searchQuery
? options.filter(option =>
option.label.toLowerCase().includes(searchQuery.toLowerCase())
)
: options;
return (
<div className="relative w-full" ref={dropdownRef}>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<span className={selectedOption ? "" : "text-muted-foreground"}>
{selectedOption ? selectedOption.label : placeholder}
</span>
<ChevronDown className="h-4 w-4 opacity-50" />
</button>
{isOpen && (
<div className="absolute z-10 mt-1 w-full rounded-md border border-input bg-popover shadow-md">
{options.length > 5 && (
<div className="p-2 border-b border-input">
<div className="flex items-center gap-2 bg-background rounded-md border border-input px-2">
<Search className="h-3.5 w-3.5 opacity-70" />
<input
ref={searchInputRef}
type="text"
placeholder="Search..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onClick={e => e.stopPropagation()}
className="flex w-full h-8 bg-transparent py-1 text-sm outline-none placeholder:text-muted-foreground"
/>
{searchQuery && (
<button
type="button"
onClick={e => {
e.stopPropagation();
setSearchQuery('');
searchInputRef.current?.focus();
}}
className="h-4 w-4 p-0 opacity-70 hover:opacity-100"
>
<X className="h-3 w-3" />
<span className="sr-only">Clear search</span>
</button>
)}
</div>
</div>
)}
<ul className="max-h-[200px] overflow-auto py-1">
{filteredOptions.length === 0 ? (
<li className="px-3 py-2 text-sm text-muted-foreground text-center">No options found</li>
) : (
filteredOptions.map((option) => (
<li
key={option.value}
onClick={() => {
onChange(option.value);
setIsOpen(false);
}}
className={cn(
"flex items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-accent hover:text-accent-foreground",
value === option.value && "bg-accent/50"
)}
>
<span className="flex-grow">{option.label}</span>
{value === option.value && <Check className="h-4 w-4" />}
</li>
))
)}
</ul>
</div>
)}
</div>
);
};
// Custom DatePicker component
interface CustomDatePickerProps {
value: Date | null;
onChange: (date: Date | null) => void;
placeholder?: string;
}
const CustomDatePicker: React.FC<CustomDatePickerProps> = ({
value,
onChange,
placeholder = "Pick a date"
}) => {
const [isOpen, setIsOpen] = useState(false);
const datePickerRef = useRef<HTMLDivElement>(null);
// Close date picker when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (datePickerRef.current && !datePickerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div className="relative w-full" ref={datePickerRef}>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<span className={value ? "" : "text-muted-foreground"}>
{value ? format(value, "PPP") : placeholder}
</span>
<CalendarIcon className="h-4 w-4 opacity-50" />
</button>
{isOpen && (
<div className="fixed top-0 left-0 right-0 z-50 flex justify-center pt-4 px-4 pb-20 items-start bg-black/50 overflow-auto min-h-screen">
<div className="bg-white dark:bg-gray-900 rounded-lg p-4 shadow-lg">
<div className="flex justify-between items-center mb-2">
<h3 className="text-base font-medium">Select Date</h3>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => setIsOpen(false)}
>
<X className="h-4 w-4" />
</Button>
</div>
<Calendar
mode="single"
selected={value || undefined}
onSelect={(date) => {
onChange(date);
setIsOpen(false);
}}
initialFocus
className="rounded border"
/>
<div className="mt-2 flex justify-end">
<Button
size="sm"
variant="outline"
onClick={() => setIsOpen(false)}
>
Close
</Button>
</div>
</div>
</div>
)}
</div>
);
};
interface SimpleAddTaskDialogProps {
issueId: number;
onTaskAdded: () => void;
employees: Employee[];
isOpen: boolean;
onClose: () => void;
}
const SimpleAddTaskDialog: React.FC<SimpleAddTaskDialogProps> = ({
issueId,
onTaskAdded,
employees,
isOpen,
onClose,
}) => {
const [isSubmitting, setIsSubmitting] = useState(false);
console.log("Simple Dialog isOpen state:", isOpen); // Debug log
// Initialize the form
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
description: "",
type: "Task",
priority: "Medium",
status: "New",
assignedTo: null,
estimates: "",
stateDate: new Date(),
endDate: null,
},
});
// Reset form when dialog opens
useEffect(() => {
if (isOpen) {
console.log("Simple Dialog opened, resetting form"); // Debug log
form.reset();
}
}, [isOpen, form]);
// Handle form submission
const onSubmit = async (values: z.infer<typeof formSchema>) => {
setIsSubmitting(true);
try {
// Prepare task data
const taskData: TaskCreateRequest = {
title: values.title,
description: values.description,
type: values.type,
priority: values.priority,
status: values.status,
assignedTo: values.assignedTo,
estimates: values.estimates || "0",
esUnit: 1, // Default to hours
stateDate: values.stateDate.toISOString().split('T')[0],
endDate: values.endDate ? values.endDate.toISOString().split('T')[0] : null,
remainingHr: 0,
sprintName: null,
issuesId: issueId,
};
console.log("Creating task with data:", taskData); // Debug log
// Create the task
await tasksApi.create(taskData);
// Show success message
toast.success("Task created successfully");
// Close the dialog and refresh the tasks list
onClose();
onTaskAdded();
} catch (error) {
console.error("Error creating task:", error);
toast.error("Failed to create task. Please try again.");
} finally {
setIsSubmitting(false);
}
};
// Dropdown options
const typeOptions = [
{ value: "Task", label: "Task" }
];
const priorityOptions = [
{ value: "Low", label: "Low" },
{ value: "Medium", label: "Medium" },
{ value: "High", label: "High" },
{ value: "Critical", label: "Critical" },
];
const statusOptions = [
{ value: "New", label: "New" },
{ value: "In Progress", label: "In Progress" },
{ value: "In Review", label: "In Review" },
{ value: "Completed", label: "Completed" },
{ value: "Closed", label: "Closed" },
];
const assigneeOptions = [
{ value: "null", label: "Unassigned" },
...employees.map(emp => ({
value: emp.id.toString(),
label: `${emp.firstName} ${emp.lastName}`
}))
];
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
<div className="bg-white dark:bg-gray-900 rounded-lg w-full max-w-[550px] p-6 max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-lg font-semibold">Add New Task</h2>
<p className="text-sm text-muted-foreground">Create a new task linked to this issue.</p>
</div>
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="h-4 w-4" />
</Button>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Task title" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<SlateRichTextEditor
initialValue={field.value || ''}
onChange={(value) => field.onChange(value)}
placeholder="Task description"
minHeight="150px"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Type</FormLabel>
<FormControl>
<CustomDropdown
options={typeOptions}
value={field.value}
onChange={field.onChange}
placeholder="Select type"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem>
<FormLabel>Priority</FormLabel>
<FormControl>
<CustomDropdown
options={priorityOptions}
value={field.value}
onChange={field.onChange}
placeholder="Select priority"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<FormControl>
<CustomDropdown
options={statusOptions}
value={field.value}
onChange={field.onChange}
placeholder="Select status"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="assignedTo"
render={({ field }) => (
<FormItem>
<FormLabel>Assigned To</FormLabel>
<FormControl>
<CustomDropdown
options={assigneeOptions}
value={field.value ? field.value.toString() : "null"}
onChange={(value) => field.onChange(value === "null" ? null : parseInt(value))}
placeholder="Select assignee"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
control={form.control}
name="stateDate"
render={({ field }) => (
<FormItem>
<FormLabel>Start Date</FormLabel>
<FormControl>
<CustomDatePicker
value={field.value}
onChange={field.onChange}
placeholder="Pick a start date"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endDate"
render={({ field }) => (
<FormItem>
<FormLabel>Due Date</FormLabel>
<FormControl>
<CustomDatePicker
value={field.value}
onChange={field.onChange}
placeholder="Pick a due date"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="estimates"
render={({ field }) => (
<FormItem>
<FormLabel>Estimated Hours</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Estimated hours to complete"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2 pt-2">
<Button
type="button"
variant="outline"
onClick={onClose}
disabled={isSubmitting}
>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
"Create Task"
)}
</Button>
</div>
</form>
</Form>
</div>
</div>
);
};
export default SimpleAddTaskDialog;