"use client";
import { useState, useEffect, useCallback } from "react";
import { Plus, X, ChevronDown, ChevronUp, Trash2, Users, CalendarDays, FileText, Star, ArrowLeft, Check, AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import {
fetchAppraisalCycles,
createAppraisalCycle,
fetchAppraisalTemplates,
createAppraisalTemplate,
deleteAppraisalTemplate,
fetchCycleAppraisals,
updateAppraisalCycleStatus,
} from "@/lib/supabase/appraisal-actions";
import PageSkeleton from "./PageSkeleton";
// ── Star Rating Component ──────────────────────────────────────
function StarRating({
value,
onChange,
readonly = false,
size = "sm",
}: {
value: number;
onChange?: (v: number) => void;
readonly?: boolean;
size?: "sm" | "md" | "lg";
}) {
const [hover, setHover] = useState(0);
const sizeClass = size === "lg" ? "w-7 h-7" : size === "md" ? "w-5 h-5" : "w-4 h-4";
return (
{[1, 2, 3, 4, 5].map((s) => (
))}
);
}
// ── Status Badge Helpers ───────────────────────────────────────
const CYCLE_STATUS_LABELS: Record = {
draft: "Draft",
active: "Active",
completed: "Completed",
};
const CYCLE_STATUS_COLORS: Record = {
draft: "bg-gray-100 text-gray-600 border-gray-200",
active: "bg-teal-100 text-teal-700 border-teal-200",
completed: "bg-green-100 text-green-700 border-green-200",
};
// ── Template Builder Modal ─────────────────────────────────────
function TemplateBuilderModal({
onClose,
onSuccess,
}: {
onClose: () => void;
onSuccess: () => void;
}) {
const [name, setName] = useState("");
const [questions, setQuestions] = useState<
{ id: string; question: string; type: string }[]
>([]);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const addQuestion = () => {
setQuestions((prev) => [
...prev,
{ id: crypto.randomUUID(), question: "", type: "rating" },
]);
};
const updateQuestion = (id: string, field: string, value: string) => {
setQuestions((prev) =>
prev.map((q) => (q.id === id ? { ...q, [field]: value } : q))
);
};
const removeQuestion = (id: string) => {
setQuestions((prev) => prev.filter((q) => q.id !== id));
};
const moveQuestion = (index: number, direction: "up" | "down") => {
setQuestions((prev) => {
const arr = [...prev];
const target = direction === "up" ? index - 1 : index + 1;
if (target < 0 || target >= arr.length) return prev;
[arr[index], arr[target]] = [arr[target], arr[index]];
return arr;
});
};
const handleSave = async () => {
if (!name.trim()) {
setError("Template name is required");
return;
}
if (questions.length === 0) {
setError("Add at least one question");
return;
}
if (questions.some((q) => !q.question.trim())) {
setError("All questions must have text");
return;
}
setSaving(true);
setError("");
const result = await createAppraisalTemplate(
name.trim(),
questions.map((q) => ({ ...q, question: q.question.trim() }))
);
setSaving(false);
if (result.success) {
onSuccess();
} else {
setError(result.error || "Failed to create template");
}
};
return (
e.stopPropagation()}
>
{/* Header */}
New Appraisal Template
Define questions for this appraisal template
{/* Body */}
setName(e.target.value)}
placeholder="e.g. Annual Performance Review 2026"
className="mt-1"
/>
{/* Questions */}
{questions.length === 0 && (
No questions yet
Click "Add Question" to start building your template
)}
{questions.map((q, i) => (
))}
{error && (
)}
{/* Footer */}
);
}
// ── New Cycle Modal ────────────────────────────────────────────
function NewCycleModal({
templates,
onClose,
onSuccess,
}: {
templates: { id: string; name: string }[];
onClose: () => void;
onSuccess: () => void;
}) {
const [name, setName] = useState("");
const [templateId, setTemplateId] = useState("");
const [periodStart, setPeriodStart] = useState("");
const [periodEnd, setPeriodEnd] = useState("");
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const handleSave = async () => {
if (!name.trim()) {
setError("Cycle name is required");
return;
}
if (!periodStart || !periodEnd) {
setError("Period dates are required");
return;
}
if (periodEnd <= periodStart) {
setError("End date must be after start date");
return;
}
setSaving(true);
setError("");
const result = await createAppraisalCycle({
name: name.trim(),
template_id: templateId || undefined,
period_start: periodStart,
period_end: periodEnd,
});
setSaving(false);
if (result.success) {
onSuccess();
} else {
setError(result.error || "Failed to create cycle");
}
};
return (
e.stopPropagation()}
>
New Appraisal Cycle
Create a review period and auto-generate appraisals
setName(e.target.value)}
placeholder="e.g. Q1 2026 Reviews"
className="mt-1"
/>
{error && (
)}
);
}
// ── Cycle Detail View ──────────────────────────────────────────
function CycleDetailView({
cycle,
onBack,
}: {
cycle: { id: string; name: string; status: string; period_start: string; period_end: string };
onBack: () => void;
}) {
const [appraisals, setAppraisals] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
fetchCycleAppraisals(cycle.id)
.then((a) => {
if (!cancelled) setAppraisals(a);
})
.catch(() => {})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [cycle.id]);
const handleActivate = async () => {
const result = await updateAppraisalCycleStatus(cycle.id, "active");
if (result.success) window.location.reload();
};
const handleComplete = async () => {
const result = await updateAppraisalCycleStatus(cycle.id, "completed");
if (result.success) window.location.reload();
};
if (loading) return ;
const totalAppraisals = appraisals.length;
const submittedCount = appraisals.filter(
(a) => a.status === "submitted"
).length;
const completionPct =
totalAppraisals > 0
? Math.round((submittedCount / totalAppraisals) * 100)
: 0;
return (
{/* Back button + header */}
{cycle.name}
{CYCLE_STATUS_LABELS[cycle.status] || cycle.status}
{new Date(cycle.period_start + "T00:00:00").toLocaleDateString(
"en-AU",
{ month: "long", day: "numeric", year: "numeric" }
)}{" "}
—{" "}
{new Date(cycle.period_end + "T00:00:00").toLocaleDateString(
"en-AU",
{ month: "long", day: "numeric", year: "numeric" }
)}
{cycle.status === "draft" && (
)}
{cycle.status === "active" && (
)}
{/* Progress */}
{submittedCount} of {totalAppraisals} appraisals submitted
{completionPct}%
{/* Appraisals Table */}
{appraisals.length === 0 ? (
No appraisals in this cycle
) : (
|
Employee
|
Type
|
Reviewer
|
Rating
|
Status
|
{appraisals.map((a) => (
|
{a.employee
? `${a.employee.first_name} ${a.employee.last_name}`
: "Unknown"}
|
{a.type === "self"
? "Self"
: a.type === "manager"
? "Manager"
: a.type || "Peer"}
|
{a.reviewer
? `${a.reviewer.first_name} ${a.reviewer.last_name}`
: "—"}
|
{a.overall_rating ? (
{[1, 2, 3, 4, 5].map((s) => (
))}
{a.overall_rating}/5
) : (
—
)}
|
{a.status === "submitted" ? "Submitted" : "Pending"}
|
))}
)}
);
}
// ── Main Performance Page ──────────────────────────────────────
export default function PerformancePage() {
const [activeTab, setActiveTab] = useState<"cycles" | "templates">("cycles");
const [cycles, setCycles] = useState([]);
const [templates, setTemplates] = useState([]);
const [loading, setLoading] = useState(true);
const [showNewCycleModal, setShowNewCycleModal] = useState(false);
const [showNewTemplateModal, setShowNewTemplateModal] = useState(false);
const [selectedCycle, setSelectedCycle] = useState(null);
const loadData = useCallback(async () => {
setLoading(true);
try {
const [c, t] = await Promise.all([
fetchAppraisalCycles(),
fetchAppraisalTemplates(),
]);
setCycles(c);
setTemplates(t);
} catch {
// ignore
}
setLoading(false);
}, []);
useEffect(() => {
loadData();
}, [loadData]);
if (loading) return ;
// ── Cycle Detail View ──
if (selectedCycle) {
return (
setSelectedCycle(null)}
/>
);
}
return (
{/* Tabs */}
360 Appraisals
Manage appraisal cycles and templates
{activeTab === "cycles" && (
)}
{activeTab === "templates" && (
)}
{/* ── Cycles Tab ── */}
{activeTab === "cycles" && (
<>
{cycles.length === 0 ? (
No appraisal cycles yet
Create your first cycle to start 360-degree reviews
) : (
|
Cycle Name
|
Period
|
Appraisals
|
Completion
|
Status
|
{cycles.map((c) => {
const appraisalCount =
Array.isArray(c.appraisals) && c.appraisals.length > 0
? c.appraisals[0]?.count || 0
: 0;
return (
setSelectedCycle(c)}
>
|
{c.name}
{c.template && (
{c.template.name}
)}
|
{new Date(
c.period_start + "T00:00:00"
).toLocaleDateString("en-AU", {
month: "short",
year: "numeric",
})}{" "}
—{" "}
{new Date(
c.period_end + "T00:00:00"
).toLocaleDateString("en-AU", {
month: "short",
year: "numeric",
})}
|
{appraisalCount}
|
0
? Math.min(
(appraisalCount /
Math.max(appraisalCount, 1)) *
100,
100
)
: 0
}%`,
}}
/>
|
{CYCLE_STATUS_LABELS[c.status] || c.status}
|
);
})}
)}
>
)}
{/* ── Templates Tab ── */}
{activeTab === "templates" && (
<>
{templates.length === 0 ? (
No appraisal templates yet
Create a template with questions to reuse across cycles
) : (
|
Template Name
|
Questions
|
Created
|
|
{templates.map((t) => {
const questionCount = t.questions
? JSON.parse(t.questions).length
: 0;
return (
|
{t.name}
|
{questionCount}
|
{new Date(t.created_at).toLocaleDateString("en-AU", {
month: "short",
day: "numeric",
year: "numeric",
})}
|
|
);
})}
)}
>
)}
{/* ── Modals ── */}
{showNewCycleModal && (
setShowNewCycleModal(false)}
onSuccess={() => {
setShowNewCycleModal(false);
loadData();
}}
/>
)}
{showNewTemplateModal && (
setShowNewTemplateModal(false)}
onSuccess={() => {
setShowNewTemplateModal(false);
loadData();
}}
/>
)}
);
}