Files
grademaxxing/app/routes/periods.tsx
2026-01-17 13:37:57 +01:00

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>
);
}