feat: institutional + support + training admin sections (backend + frontend)
Ship three fully-wired admin areas end-to-end with APIs, seeds, tests and docs. Backend (new `encoach_lms_api` addon + existing addons): - Institutional: academic years/terms, departments, admission registers & admissions, courses/batches, lessons, fees (terms + student fees + invoicing with income-account auto-wiring), gradebook (assignments/grades), library, facilities (encoach.asset), student leave, result templates + marksheets (incl. delete-with-cascade). - Support: `encoach.ticket` model + CRUD/assignee routes; payment records derived from `op.student.fees.details` and `account.move`; platform settings backed by `encoach.code` and `ir.config_parameter` (packages + grading config). - Training: `encoach.vocab.item` + `encoach.grammar.rule` (plus progress models) with CRUD, pagination, search/level filters, and upsert-style progress endpoints. Odoo 19 compatibility: `_sql_constraints` replaced with `@api.constrains`; `ValidationError`/`UserError` mapped to HTTP 400. Frontend: - Rewire institutional admin pages (Academic Year Manager, Admissions, Courses, Lessons, Fees, Gradebook, Library, Facilities, Student Leave, Marksheets, Taxonomy, Resources) to real APIs with React Query invalidation and dialogs. - New typed services: `payments.service.ts`, `platformSettings.service.ts`, `training.service.ts`. Updated `fees/gradebook/lms/courseware/taxonomy/ resources/student-progress/generation` services + related types. - Rewrite `VocabularyPage`, `GrammarPage`, `PaymentRecordPage`, `SettingsPage`, `TicketsPage` to consume live data with search/filter/progress/CRUD flows. - New shared components: `TaxonomyCascade`, `MaterialViewer`, `teacher/TeacherLibrary`. - Favicons/branding assets and misc. UX polish across teacher/student pages. Tooling & QA: - Seeders: `seed_demo.py`, `seed_demo_data.py`, `seed_institutional.py` (idempotent, covers institutional + support + training fixtures incl. income-account wiring). - API write-flow test suites: `test_write_flows.py` (institutional), `test_support_flows.py` (support), `test_training_flows.py` (training), `test_ai_full.py`. All suites pass end-to-end. - Docs: add `docs/PROJECT_SUMMARY.md` with per-section scope, artifacts and QA. - `.gitignore`: ignore `pgdata_bak_*/`, `frontend/.vite/`, `frontend/dist/`, `frontend/node_modules/`. Made-with: Cursor
This commit is contained in:
@@ -5,14 +5,16 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { useFacilities, useCreateFacility, useDeleteFacility, useAssets, useCreateAsset, useDeleteAsset } from "@/hooks/queries";
|
||||
import { Search, Plus, Trash2, Building } from "lucide-react";
|
||||
import { useFacilities, useCreateFacility, useUpdateFacility, useDeleteFacility, useAssets, useCreateAsset, useDeleteAsset } from "@/hooks/queries";
|
||||
import { Search, Plus, Trash2, Edit, Building } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
export default function AdminFacilities() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [section, setSection] = useState<"facilities" | "assets">("facilities");
|
||||
const [facOpen, setFacOpen] = useState(false);
|
||||
const [facEditOpen, setFacEditOpen] = useState(false);
|
||||
const [facEditId, setFacEditId] = useState<number | null>(null);
|
||||
const [assetOpen, setAssetOpen] = useState(false);
|
||||
const [facForm, setFacForm] = useState({ name: "", code: "" });
|
||||
const [assetForm, setAssetForm] = useState({ name: "", code: "" });
|
||||
@@ -21,6 +23,7 @@ export default function AdminFacilities() {
|
||||
const facQ = useFacilities();
|
||||
const assetQ = useAssets();
|
||||
const createFac = useCreateFacility();
|
||||
const updateFac = useUpdateFacility();
|
||||
const delFac = useDeleteFacility();
|
||||
const createAsset = useCreateAsset();
|
||||
const delAsset = useDeleteAsset();
|
||||
@@ -104,21 +107,34 @@ export default function AdminFacilities() {
|
||||
<TableCell className="font-medium">{f.name}</TableCell>
|
||||
<TableCell>{f.code}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-destructive"
|
||||
onClick={() => {
|
||||
if (!window.confirm("Delete this facility?")) return;
|
||||
delFac.mutate(f.id, {
|
||||
onSuccess: () => toast({ title: "Deleted" }),
|
||||
onError: (err: Error) =>
|
||||
toast({ title: "Error", description: err.message, variant: "destructive" }),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setFacEditId(f.id);
|
||||
setFacForm({ name: f.name || "", code: f.code || "" });
|
||||
setFacEditOpen(true);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-destructive"
|
||||
onClick={() => {
|
||||
if (!window.confirm("Delete this facility?")) return;
|
||||
delFac.mutate(f.id, {
|
||||
onSuccess: () => toast({ title: "Deleted" }),
|
||||
onError: (err: Error) =>
|
||||
toast({ title: "Error", description: err.message, variant: "destructive" }),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -179,13 +195,14 @@ export default function AdminFacilities() {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={createFac.isPending}
|
||||
disabled={createFac.isPending || !facForm.name.trim()}
|
||||
onClick={() =>
|
||||
createFac.mutate(
|
||||
{ name: facForm.name, code: facForm.code || undefined },
|
||||
{ name: facForm.name.trim(), code: facForm.code || undefined },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setFacOpen(false);
|
||||
setFacForm({ name: "", code: "" });
|
||||
toast({ title: "Created successfully" });
|
||||
},
|
||||
onError: (err: Error) =>
|
||||
@@ -200,6 +217,50 @@ export default function AdminFacilities() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={facEditOpen} onOpenChange={setFacEditOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit facility</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Name</Label>
|
||||
<Input value={facForm.name} onChange={(e) => setFacForm((f) => ({ ...f, name: e.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Code</Label>
|
||||
<Input value={facForm.code} onChange={(e) => setFacForm((f) => ({ ...f, code: e.target.value }))} />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setFacEditOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={updateFac.isPending || !facForm.name.trim() || !facEditId}
|
||||
onClick={() => {
|
||||
if (!facEditId) return;
|
||||
updateFac.mutate(
|
||||
{ id: facEditId, data: { name: facForm.name.trim(), code: facForm.code || undefined } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setFacEditOpen(false);
|
||||
setFacEditId(null);
|
||||
setFacForm({ name: "", code: "" });
|
||||
toast({ title: "Updated successfully" });
|
||||
},
|
||||
onError: (err: Error) =>
|
||||
toast({ title: "Error", description: err.message, variant: "destructive" }),
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={assetOpen} onOpenChange={setAssetOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -226,13 +287,14 @@ export default function AdminFacilities() {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={createAsset.isPending}
|
||||
disabled={createAsset.isPending || !assetForm.name.trim()}
|
||||
onClick={() =>
|
||||
createAsset.mutate(
|
||||
{ name: assetForm.name, code: assetForm.code || undefined },
|
||||
{ name: assetForm.name.trim(), code: assetForm.code || undefined },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setAssetOpen(false);
|
||||
setAssetForm({ name: "", code: "" });
|
||||
toast({ title: "Created successfully" });
|
||||
},
|
||||
onError: (err: Error) =>
|
||||
|
||||
Reference in New Issue
Block a user