257 lines
9.4 KiB
TypeScript
257 lines
9.4 KiB
TypeScript
import { useEffect, useState, type FormEvent } from "react";
|
|
import { Link } from "react-router";
|
|
import { ProtectedRoute } from "~/components/ProtectedRoute";
|
|
import { LoadingSpinner } from "~/components/LoadingSpinner";
|
|
import { Button } from "~/components/Button";
|
|
import { Input } from "~/components/Input";
|
|
import { api } from "~/api/client";
|
|
import type { ReportPeriod, ReportPeriodCreate, ReportPeriodUpdate } from "~/types/api";
|
|
|
|
export default function ReportPeriods() {
|
|
const [periods, setPeriods] = useState<ReportPeriod[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showAddForm, setShowAddForm] = useState(false);
|
|
const [editingPeriod, setEditingPeriod] = useState<ReportPeriod | null>(null);
|
|
const [name, setName] = useState("");
|
|
const [startDate, setStartDate] = useState("");
|
|
const [endDate, setEndDate] = useState("");
|
|
const [error, setError] = useState("");
|
|
|
|
useEffect(() => {
|
|
loadPeriods();
|
|
}, []);
|
|
|
|
const loadPeriods = async () => {
|
|
try {
|
|
const data = await api.getPeriods();
|
|
setPeriods(data);
|
|
} catch (error) {
|
|
console.error("Failed to load periods:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: FormEvent) => {
|
|
e.preventDefault();
|
|
setError("");
|
|
|
|
if (new Date(endDate) <= new Date(startDate)) {
|
|
setError("End date must be after start date");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (editingPeriod) {
|
|
// Update existing period
|
|
const updateData: ReportPeriodUpdate = {
|
|
name: name.trim(),
|
|
start_date: new Date(startDate).toISOString(),
|
|
end_date: new Date(endDate).toISOString(),
|
|
};
|
|
const updated = await api.updatePeriod(editingPeriod._id, updateData);
|
|
setPeriods(periods.map((p) => (p._id === editingPeriod._id ? updated : p)));
|
|
} else {
|
|
// Create new period
|
|
const periodData: ReportPeriodCreate = {
|
|
name: name.trim(),
|
|
start_date: new Date(startDate).toISOString(),
|
|
end_date: new Date(endDate).toISOString(),
|
|
};
|
|
const newPeriod = await api.createPeriod(periodData);
|
|
setPeriods([newPeriod, ...periods]);
|
|
}
|
|
setName("");
|
|
setStartDate("");
|
|
setEndDate("");
|
|
setShowAddForm(false);
|
|
setEditingPeriod(null);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : editingPeriod ? "Failed to update period" : "Failed to create period");
|
|
}
|
|
};
|
|
|
|
const handleEdit = (period: ReportPeriod) => {
|
|
setEditingPeriod(period);
|
|
setName(period.name);
|
|
setStartDate(new Date(period.start_date).toISOString().split("T")[0]);
|
|
setEndDate(new Date(period.end_date).toISOString().split("T")[0]);
|
|
setShowAddForm(true);
|
|
};
|
|
|
|
const handleCancelEdit = () => {
|
|
setEditingPeriod(null);
|
|
setName("");
|
|
setStartDate("");
|
|
setEndDate("");
|
|
setShowAddForm(false);
|
|
};
|
|
|
|
const handleDelete = async (periodId: string) => {
|
|
if (!confirm("Delete this report period?")) return;
|
|
|
|
try {
|
|
await api.deletePeriod(periodId);
|
|
setPeriods(periods.filter((p) => p._id !== periodId));
|
|
} catch (error) {
|
|
alert("Failed to delete period");
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<ProtectedRoute>
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<LoadingSpinner size="lg" />
|
|
</div>
|
|
</ProtectedRoute>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ProtectedRoute>
|
|
<div className="min-h-screen bg-gray-50">
|
|
<header className="bg-white shadow-sm border-b">
|
|
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<Link to="/dashboard" className="text-blue-600 hover:text-blue-700 text-sm">
|
|
← Back to Dashboard
|
|
</Link>
|
|
<h1 className="text-2xl font-bold text-gray-900 mt-1">Report Periods</h1>
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
Define time periods for your report cards (e.g., semesters, quarters)
|
|
</p>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{/* Add Period Button/Form */}
|
|
<div className="mb-6">
|
|
{!showAddForm ? (
|
|
<Button onClick={() => setShowAddForm(true)}>+ Add Period</Button>
|
|
) : (
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h3 className="text-lg font-semibold mb-4">{editingPeriod ? "Edit Report Period" : "Create Report Period"}</h3>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<Input
|
|
label="Period Name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="e.g., First Semester 2024/2025"
|
|
required
|
|
/>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<Input
|
|
label="Start Date"
|
|
type="date"
|
|
value={startDate}
|
|
onChange={(e) => setStartDate(e.target.value)}
|
|
required
|
|
/>
|
|
<Input
|
|
label="End Date"
|
|
type="date"
|
|
value={endDate}
|
|
onChange={(e) => setEndDate(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
<div className="flex gap-3">
|
|
<Button type="submit">{editingPeriod ? "Update Period" : "Create Period"}</Button>
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
onClick={handleCancelEdit}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Periods List */}
|
|
{periods.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{periods.map((period) => (
|
|
<div key={period._id} className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex justify-between items-start">
|
|
<div className="flex-1">
|
|
<h3 className="text-lg font-semibold text-gray-900">{period.name}</h3>
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
{new Date(period.start_date).toLocaleDateString()} -{" "}
|
|
{new Date(period.end_date).toLocaleDateString()}
|
|
</p>
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
Duration:{" "}
|
|
{Math.ceil(
|
|
(new Date(period.end_date).getTime() -
|
|
new Date(period.start_date).getTime()) /
|
|
(1000 * 60 * 60 * 24)
|
|
)}{" "}
|
|
days
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleEdit(period)}
|
|
className="text-blue-500 hover:text-blue-700"
|
|
aria-label="Edit period"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(period._id)}
|
|
className="text-red-500 hover:text-red-700"
|
|
aria-label="Delete period"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="bg-white rounded-lg shadow p-12 text-center">
|
|
<p className="text-gray-600 mb-4">
|
|
No report periods yet. Create your first period!
|
|
</p>
|
|
<Button onClick={() => setShowAddForm(true)}>+ Add Period</Button>
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
</ProtectedRoute>
|
|
);
|
|
}
|