first commit
This commit is contained in:
256
app/routes/periods.tsx
Normal file
256
app/routes/periods.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user