mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
177 lines
5.8 KiB
React
177 lines
5.8 KiB
React
import { useMemo, useRef, useState, useEffect } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import api from "@/utils/api.util";
|
|
|
|
import { useCourses } from "@/contexts/AdminCoursesContext";
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
|
|
import DataTable from "@/components/generic/Table/DataTable";
|
|
import { FilterSheet } from "@/components/generic/Sheet/FilterSheet";
|
|
import { ArchiveDialog } from "@/components/generic/Dialogs/ArchiveDialog";
|
|
|
|
import { buildDataColumns, columnPinning } from "../../config/courses/columns.config";
|
|
import { buildToolbarActions } from "../../config/courses/toolbar.config";
|
|
import { buildSelectionActions } from "../../config/courses/selection.config";
|
|
import { buildRowActions } from "../../config/courses/rowActions.config";
|
|
|
|
import { getTimestamp } from "@/utils/timestamp.util";
|
|
import { formatGeneratedBy } from "@/utils/generatedBy.util";
|
|
|
|
export default function CoursesTable() {
|
|
const [archiveTarget, setArchiveTarget] = useState(null);
|
|
const [archiveIds, setArchiveIds] = useState(null);
|
|
|
|
const tableRefsRef = useRef({
|
|
getFilters: () => [],
|
|
getSort: () => [],
|
|
resetSelection: () => { },
|
|
setFilters: () => { },
|
|
});
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const { user: currentUser } = useAuth();
|
|
|
|
const { courses, attributes, pagination, setPagination, loading, fetchCourses, reorderCourses, archiveCourse, archiveCourses, fetchCourseFieldValues } = useCourses();
|
|
|
|
const handleRefsReady = (refs) => {
|
|
tableRefsRef.current = refs;
|
|
};
|
|
|
|
const [tierCategories, setTierCategories] = useState([]);
|
|
useEffect(() => {
|
|
api.get("/admin/tiers/categories")
|
|
.then(({ data }) => setTierCategories(data.data ?? []))
|
|
.catch(() => {});
|
|
}, []);
|
|
const tierMap = useMemo(
|
|
() => Object.fromEntries(tierCategories.map((c) => [c.slug, c])),
|
|
[tierCategories]
|
|
);
|
|
|
|
const exportConfig = {
|
|
allData: courses,
|
|
attributes,
|
|
filename: `${getTimestamp()}_Courses`,
|
|
sheetName: "Courses",
|
|
generatedBy: formatGeneratedBy(currentUser),
|
|
};
|
|
|
|
// Reorder — swaps this row with its neighbor in the currently displayed
|
|
// (order_index-sorted) list, then persists the new order_index set.
|
|
const handleMove = async (row, direction) => {
|
|
const idx = courses.findIndex((c) => c.course_id === row.course_id);
|
|
const swapIdx = idx + direction;
|
|
if (idx < 0 || swapIdx < 0 || swapIdx >= courses.length) return;
|
|
|
|
const reordered = [...courses];
|
|
[reordered[idx], reordered[swapIdx]] = [reordered[swapIdx], reordered[idx]];
|
|
|
|
const ok = await reorderCourses(reordered.map((c) => c.course_id));
|
|
if (ok) fetchCourses({ page: 1, limit: pagination?.limit ?? 10 });
|
|
};
|
|
|
|
const rowActions = buildRowActions({
|
|
onView: (row) => navigate(`/admin/courses/${row.course_id}/view`),
|
|
onEdit: (row) => navigate(`/admin/courses/${row.course_id}/edit`),
|
|
onArchive: (row) => setArchiveTarget(row),
|
|
onMoveUp: (row) => handleMove(row, -1),
|
|
onMoveDown: (row) => handleMove(row, 1),
|
|
courses,
|
|
});
|
|
|
|
const toolbarActions = buildToolbarActions({
|
|
fetchCourses,
|
|
pagination,
|
|
exportConfig,
|
|
navigate,
|
|
getFilters: () => tableRefsRef.current.getFilters(),
|
|
getSort: () => tableRefsRef.current.getSort(),
|
|
getTableInstance: () => tableRefsRef.current.tableInstance,
|
|
});
|
|
|
|
const selectionActions = buildSelectionActions({
|
|
exportConfig,
|
|
onArchive: (row) => setArchiveTarget(row),
|
|
onArchiveMany: (ids) => setArchiveIds(ids),
|
|
getTableInstance: () => tableRefsRef.current.tableInstance,
|
|
});
|
|
|
|
const columns = useMemo(
|
|
() => buildDataColumns(attributes, rowActions, tierMap),
|
|
[attributes, rowActions, tierMap]
|
|
);
|
|
|
|
const handleArchiveSuccess = () => {
|
|
setArchiveTarget(null);
|
|
setArchiveIds(null);
|
|
tableRefsRef.current.resetSelection?.();
|
|
fetchCourses({ page: 1, limit: pagination?.limit ?? 10 });
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<DataTable
|
|
title="Courses"
|
|
data={courses}
|
|
columns={columns}
|
|
attributes={attributes}
|
|
pagination={pagination}
|
|
setPagination={setPagination}
|
|
loading={loading}
|
|
onFetch={fetchCourses}
|
|
onFetchFilterData={fetchCourseFieldValues}
|
|
onRefsReady={handleRefsReady}
|
|
renderFilterSheet={({ open, onOpenChange, column, attr, data, loading }) => (
|
|
<FilterSheet
|
|
open={open}
|
|
onOpenChange={onOpenChange}
|
|
column={column}
|
|
attr={attr}
|
|
data={data}
|
|
loading={loading}
|
|
/>
|
|
)}
|
|
columnPinning={columnPinning}
|
|
toolbarActions={toolbarActions}
|
|
selectionActions={selectionActions}
|
|
recordLabel="course"
|
|
emptyMessage="No courses found."
|
|
/>
|
|
|
|
{/* ── Single archive ── */}
|
|
<ArchiveDialog
|
|
open={!!archiveTarget}
|
|
onOpenChange={(v) => !v && setArchiveTarget(null)}
|
|
entity={archiveTarget}
|
|
entityLabel="Course"
|
|
getName={(c) => c?.title}
|
|
onArchive={(c) => archiveCourse(c?.course_id)}
|
|
loading={loading}
|
|
onSuccess={handleArchiveSuccess}
|
|
onImpactCheck={async () => {
|
|
const { data } = await api.get(
|
|
`/admin/courses/${archiveTarget?.course_id}/archive-impact`
|
|
);
|
|
const { activeCount, totalCount } = data?.data ?? {};
|
|
return [
|
|
{ label: "student(s) are currently taking this course", count: activeCount ?? 0 },
|
|
{ label: "student(s) have progress in this course", count: totalCount ?? 0 },
|
|
];
|
|
}}
|
|
/>
|
|
|
|
{/* ── Bulk archive ── */}
|
|
<ArchiveDialog
|
|
open={!!archiveIds}
|
|
onOpenChange={(v) => !v && setArchiveIds(null)}
|
|
ids={archiveIds ?? []}
|
|
entityLabel="Course"
|
|
onArchive={(ids) => archiveCourses(ids)}
|
|
loading={loading}
|
|
onSuccess={handleArchiveSuccess}
|
|
/>
|
|
</>
|
|
);
|
|
}
|