pmtool / src /components /appraisal /MissingCycleDialog.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
17.2 kB
import { useState, useEffect, useRef } from "react";
import { format } from "date-fns";
import { CalendarIcon, Check, Info, ChevronDown } from "lucide-react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { cn } from "@/lib/utils";
import { AppraisalCycle, Appraisal } from "@/types";
import CustomDialog from "@/components/CustomDialog";
import { getEmployeesByRole } from "@/services/employeeApi";
import { useAuth } from "@/lib/auth-context";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { ScrollArea } from "@/components/ui/scroll-area";
const formSchema = z.object({
cycleId: z.string({
required_error: "Please select an appraisal cycle.",
}),
appraiserId: z.string({
required_error: "Please select a manager.",
}),
});
interface MissingCycleDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (cycleId: number, appraiserId: number) => void;
cycles: AppraisalCycle[];
currentAppraisals: Appraisal[];
isLoading?: boolean;
}
export function MissingCycleDialog({
open,
onOpenChange,
onSubmit,
cycles,
currentAppraisals,
isLoading = false,
}: MissingCycleDialogProps) {
const [managers, setManagers] = useState<Array<{ id: number; name: string }>>([]);
const [isLoadingManagers, setIsLoadingManagers] = useState(false);
const [error, setError] = useState<string | null>(null);
const [availableCycles, setAvailableCycles] = useState<AppraisalCycle[]>([]);
const { userData } = useAuth();
// Dropdown state
const [cycleDropdownOpen, setCycleDropdownOpen] = useState(false);
const [managerDropdownOpen, setManagerDropdownOpen] = useState(false);
// Search state
const [cycleSearch, setCycleSearch] = useState("");
const [managerSearch, setManagerSearch] = useState("");
// Refs for click outside handling
const cycleDropdownRef = useRef<HTMLDivElement>(null);
const managerDropdownRef = useRef<HTMLDivElement>(null);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
cycleId: "",
appraiserId: "",
},
});
// Handle click outside to close dropdowns
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (cycleDropdownRef.current && !cycleDropdownRef.current.contains(event.target as Node)) {
setCycleDropdownOpen(false);
}
if (managerDropdownRef.current && !managerDropdownRef.current.contains(event.target as Node)) {
setManagerDropdownOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
// Filter out cycles that the employee already has appraisals for
useEffect(() => {
if (cycles && currentAppraisals) {
const currentCycleIds = currentAppraisals.map(a => a.cycleId);
const filtered = cycles.filter(cycle => !currentCycleIds.includes(cycle.cycleId));
setAvailableCycles(filtered);
}
}, [cycles, currentAppraisals]);
// Load managers when component mounts
useEffect(() => {
const loadManagers = async () => {
setIsLoadingManagers(true);
try {
// Get employees with manager roles (assuming role IDs for managers are 1, 2, 3)
// In a real app, you would need to filter by appropriate manager roles
const managerRoleIds = [1, 2, 3]; // Example role IDs for managers
const promises = managerRoleIds.map(roleId => getEmployeesByRole(roleId));
const responses = await Promise.all(promises);
const allManagers: Array<{ id: number; name: string }> = [];
responses.forEach(response => {
if (response.success) {
const managers = response.data.map(employee => ({
id: employee.id,
name: `${employee.firstName} ${employee.lastName}`
}));
allManagers.push(...managers);
}
});
setManagers(allManagers);
} catch (error) {
console.error("Error loading managers:", error);
setError("Failed to load managers. Please try again.");
} finally {
setIsLoadingManagers(false);
}
};
if (open) {
loadManagers();
}
}, [open]);
function handleSubmit(values: z.infer<typeof formSchema>) {
setError(null);
const cycleId = parseInt(values.cycleId);
const appraiserId = parseInt(values.appraiserId);
if (isNaN(cycleId) || isNaN(appraiserId)) {
setError("Invalid selection. Please try again.");
return;
}
onSubmit(cycleId, appraiserId);
}
// Handle dialog closing
const handleClose = () => {
onOpenChange(false);
form.reset();
setCycleSearch("");
setManagerSearch("");
setCycleDropdownOpen(false);
setManagerDropdownOpen(false);
};
const isPastCycle = (cycle: AppraisalCycle) => {
const now = new Date();
const endDate = new Date(cycle.endDate);
return now > endDate;
};
// Find cycle name by id
const getCycleName = (cycleId: string) => {
if (!cycleId) return "";
const cycle = availableCycles.find(c => c.cycleId.toString() === cycleId);
return cycle ? cycle.name : "";
};
// Find manager name by id
const getManagerName = (managerId: string) => {
if (!managerId) return "";
const manager = managers.find(m => m.id.toString() === managerId);
return manager ? manager.name : "";
};
// Filter cycles based on search
const filteredCycles = availableCycles.filter(cycle =>
cycle.name.toLowerCase().includes(cycleSearch.toLowerCase())
);
// Filter managers based on search
const filteredManagers = managers.filter(manager =>
manager.name.toLowerCase().includes(managerSearch.toLowerCase())
);
return (
<CustomDialog
isOpen={open}
onClose={handleClose}
title="Request Missing Appraisal Cycle"
description="Request an appraisal for a cycle that you missed."
allowClose={!isLoading}
isPending={isLoading}
>
{availableCycles.length === 0 ? (
<Alert>
<Info className="h-4 w-4" />
<AlertTitle>No missing cycles</AlertTitle>
<AlertDescription>
You have appraisals for all available cycles.
</AlertDescription>
</Alert>
) : (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
<FormField
control={form.control}
name="cycleId"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Appraisal Cycle</FormLabel>
<div ref={cycleDropdownRef} className="relative">
<FormControl>
<Button
type="button"
variant="outline"
className={cn(
"w-full justify-between",
!field.value && "text-muted-foreground"
)}
disabled={isLoading || availableCycles.length === 0}
onClick={(e) => {
e.preventDefault();
setCycleDropdownOpen(!cycleDropdownOpen);
}}
>
{field.value
? getCycleName(field.value)
: "Select an appraisal cycle"}
<ChevronDown className={cn(
"ml-2 h-4 w-4 shrink-0 opacity-50",
cycleDropdownOpen && "transform rotate-180"
)} />
</Button>
</FormControl>
{cycleDropdownOpen && (
<div className="absolute z-50 w-full mt-1 bg-background border rounded-md shadow-lg">
<div className="p-2">
<Input
placeholder="Search cycles..."
value={cycleSearch}
onChange={(e) => setCycleSearch(e.target.value)}
onClick={(e) => e.stopPropagation()}
className="w-full mb-2"
/>
<div className="max-h-60 overflow-y-auto">
{filteredCycles.length === 0 ? (
<div className="text-center py-4 text-sm text-muted-foreground">
No cycles found
</div>
) : (
<div className="space-y-1">
{filteredCycles.map((cycle) => {
const isDisabled = !isPastCycle(cycle);
return (
<Button
key={cycle.cycleId}
type="button"
variant="ghost"
className={cn(
"w-full justify-start text-left font-normal",
cycle.cycleId.toString() === field.value && "bg-accent text-accent-foreground",
isDisabled && "opacity-50 cursor-not-allowed"
)}
disabled={isDisabled}
onClick={(e) => {
e.preventDefault();
if (!isDisabled) {
form.setValue("cycleId", cycle.cycleId.toString());
setCycleDropdownOpen(false);
}
}}
>
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
<span>{cycle.name}</span>
{isDisabled && (
<span className="ml-2 text-xs text-muted-foreground">
(Not yet completed)
</span>
)}
</div>
{cycle.cycleId.toString() === field.value && (
<Check className="h-4 w-4" />
)}
</div>
</Button>
);
})}
</div>
)}
</div>
</div>
</div>
)}
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appraiserId"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Manager</FormLabel>
<div ref={managerDropdownRef} className="relative">
<FormControl>
<Button
type="button"
variant="outline"
className={cn(
"w-full justify-between",
!field.value && "text-muted-foreground"
)}
disabled={isLoading || isLoadingManagers}
onClick={(e) => {
e.preventDefault();
setManagerDropdownOpen(!managerDropdownOpen);
}}
>
{field.value
? getManagerName(field.value)
: isLoadingManagers
? "Loading managers..."
: "Select your manager"}
<ChevronDown className={cn(
"ml-2 h-4 w-4 shrink-0 opacity-50",
managerDropdownOpen && "transform rotate-180"
)} />
</Button>
</FormControl>
{managerDropdownOpen && (
<div className="absolute z-50 w-full mt-1 bg-background border rounded-md shadow-lg">
<div className="p-2">
<Input
placeholder="Search managers..."
value={managerSearch}
onChange={(e) => setManagerSearch(e.target.value)}
onClick={(e) => e.stopPropagation()}
className="w-full mb-2"
/>
<div className="max-h-60 overflow-y-auto">
{isLoadingManagers ? (
<div className="text-center py-4 text-sm text-muted-foreground">
Loading managers...
</div>
) : filteredManagers.length === 0 ? (
<div className="text-center py-4 text-sm text-muted-foreground">
No managers found
</div>
) : (
<div className="space-y-1">
{filteredManagers.map((manager) => (
<Button
key={manager.id}
type="button"
variant="ghost"
className={cn(
"w-full justify-start text-left font-normal",
manager.id.toString() === field.value && "bg-accent text-accent-foreground"
)}
onClick={(e) => {
e.preventDefault();
form.setValue("appraiserId", manager.id.toString());
setManagerDropdownOpen(false);
}}
>
<div className="flex items-center justify-between w-full">
<span>{manager.name}</span>
{manager.id.toString() === field.value && (
<Check className="h-4 w-4" />
)}
</div>
</Button>
))}
</div>
)}
</div>
</div>
</div>
)}
</div>
<FormMessage />
</FormItem>
)}
/>
{error && <p className="text-sm font-medium text-destructive">{error}</p>}
<div className="flex justify-end space-x-2 pt-4">
<Button
type="button"
variant="outline"
onClick={handleClose}
disabled={isLoading}
>
Cancel
</Button>
<Button
type="submit"
disabled={isLoading || availableCycles.length === 0}
>
{isLoading ? "Submitting..." : "Request Appraisal"}
</Button>
</div>
</form>
</Form>
)}
</CustomDialog>
);
}