diff --git a/src/modules/admin/components/courses/LessonsTable.jsx b/src/modules/admin/components/courses/LessonsTable.jsx index af1a4f9..8b4e768 100644 --- a/src/modules/admin/components/courses/LessonsTable.jsx +++ b/src/modules/admin/components/courses/LessonsTable.jsx @@ -1,15 +1,13 @@ -import { useMemo, useRef, useState, useCallback } from "react"; +import { useMemo, useRef, useState, useCallback, 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 api from "@/utils/api.util"; import DataTable from "@/components/generic/Table/DataTable"; import { FilterSheet } from "@/components/generic/Sheet/FilterSheet"; import { ArchiveDialog } from "@/components/generic/Dialogs/ArchiveDialog"; -import AttachLessonsDialog from "../library/AttachLessonsDialog"; -import { Link2 } from "lucide-react"; import { buildDataColumns, columnPinning } from "../../config/courses/lessons/columns.config"; import { buildToolbarActions } from "../../config/courses/lessons/toolbar.config"; @@ -22,7 +20,6 @@ import { formatGeneratedBy } from "@/utils/generatedBy.util"; export default function LessonsTable({ courseId, unitId }) { const [archiveTarget, setArchiveTarget] = useState(null); const [archiveIds, setArchiveIds] = useState(null); - const [attachOpen, setAttachOpen] = useState(false); const tableRefsRef = useRef({ getFilters: () => [], @@ -44,6 +41,17 @@ export default function LessonsTable({ courseId, unitId }) { 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: lessons, attributes, @@ -62,35 +70,17 @@ export default function LessonsTable({ courseId, unitId }) { onArchive: (row) => setArchiveTarget(row), }), [courseId, unitId]); - const toolbarActions = [ - ...buildToolbarActions({ - fetchLessons: (params) => fetchLessons(courseId, unitId, params), - pagination, - exportConfig, - navigate, - courseId, - unitId, - getFilters: () => tableRefsRef.current.getFilters(), - getSort: () => tableRefsRef.current.getSort(), - getTableInstance: () => tableRefsRef.current.tableInstance, - }), - // Junction revamp — lessons live standalone in the library; attach without re-creating - { - key: "attach-existing", - type: "button", - label: "Attach Existing", - icon: , - variant: "outline", - onClick: () => setAttachOpen(true), - }, - ]; - - const handleAttachLessons = async (lessonIds) => { - for (const lesson_id of lessonIds) { - await api.post(`/admin/courses/${courseId}/units/${unitId}/lessons`, { lesson_id }); - } - fetchLessons(courseId, unitId, { page: 1, limit: pagination?.limit ?? 10 }); - }; + const toolbarActions = buildToolbarActions({ + fetchLessons: (params) => fetchLessons(courseId, unitId, params), + pagination, + exportConfig, + navigate, + courseId, + unitId, + getFilters: () => tableRefsRef.current.getFilters(), + getSort: () => tableRefsRef.current.getSort(), + getTableInstance: () => tableRefsRef.current.tableInstance, + }); const selectionActions = buildSelectionActions({ exportConfig, @@ -100,8 +90,8 @@ export default function LessonsTable({ courseId, unitId }) { }); const columns = useMemo( - () => buildDataColumns(attributes, rowActions), - [attributes, rowActions] + () => buildDataColumns(attributes, rowActions, tierMap, unit?.subscription, course?.subscription), + [attributes, rowActions, tierMap, unit?.subscription, course?.subscription] ); const handleArchiveSuccess = () => { @@ -163,15 +153,6 @@ export default function LessonsTable({ courseId, unitId }) { loading={loading} onSuccess={handleArchiveSuccess} /> - - {/* ── Attach existing library lessons ── */} - l.lesson_id)} - onAttach={handleAttachLessons} - loading={loading} - /> ); } diff --git a/src/modules/admin/components/courses/UnitsTable.jsx b/src/modules/admin/components/courses/UnitsTable.jsx index c400ad3..f5b9533 100644 --- a/src/modules/admin/components/courses/UnitsTable.jsx +++ b/src/modules/admin/components/courses/UnitsTable.jsx @@ -1,4 +1,4 @@ -import { useMemo, useRef, useState, useCallback } from "react"; +import { useMemo, useRef, useState, useCallback, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import api from "@/utils/api.util"; @@ -41,6 +41,17 @@ export default function UnitsTable({ courseId, returnTo }) { 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: units, attributes, @@ -79,8 +90,8 @@ export default function UnitsTable({ courseId, returnTo }) { }); const columns = useMemo( - () => buildDataColumns(attributes, rowActions), - [attributes, rowActions] + () => buildDataColumns(attributes, rowActions, tierMap, course?.subscription), + [attributes, rowActions, tierMap, course?.subscription] ); const handleArchiveSuccess = () => { diff --git a/src/modules/admin/config/courses/lessons/columns.config.jsx b/src/modules/admin/config/courses/lessons/columns.config.jsx index 8f51e87..8183960 100644 --- a/src/modules/admin/config/courses/lessons/columns.config.jsx +++ b/src/modules/admin/config/courses/lessons/columns.config.jsx @@ -2,11 +2,13 @@ // Column definitions and pinning config for the Users table. import { Badge } from "@/components/ui/badge"; -import { Clock } from "lucide-react"; +import { Clock, Tag } from "lucide-react"; +import * as LucideIcons from "lucide-react"; import { buildColumns } from "@/utils/table.util"; import { buildSelectionColumn } from "@/components/generic/Table/buildSelectionColumn"; import { buildRowActionsColumn } from "@/components/generic/Table/buildRowActionsColumn"; import { formatDuration } from "@/utils/timestamp.util"; +import { resolveTierBadge } from "@/utils/tierBadge.util"; export const columnPinning = { right: ["actions"], @@ -14,7 +16,8 @@ export const columnPinning = { }; // ─── Custom cell overrides ──────────────────────────────────────────────────── -const cellOverrides = { +function buildCellOverrides(tierMap, unitSubscription, courseSubscription) { + return { duration_seconds: (info) => { const seconds = parseInt(info.getValue() ?? 0, 10); @@ -27,17 +30,35 @@ const cellOverrides = { ); }, -}; + subscription: (info) => { + const slug = info.getValue() || unitSubscription || courseSubscription; + if (!slug) return -; + + const { label, cls } = resolveTierBadge(slug, tierMap); + const Icon = LucideIcons[tierMap[slug]?.badge_icon] ?? Tag; + return ( + + + {label} + + ); + }, + }; +} /** * Builds the full column array for the Users table. * - * @param {Array} attributes Field definitions from the server (drives data columns) - * @param {Array} rowActions Row-level kebab action definitions - * @returns {Array} TanStack column definitions + * @param {Array} attributes Field definitions from the server (drives data columns) + * @param {Array} rowActions Row-level kebab action definitions + * @param {Object} tierMap slug → tier category, for colored Subscription badges + * @param {string} unitSubscription Parent unit's tier, used when the lesson has no direct tier of its own + * @param {string} courseSubscription Parent course's tier, used when neither the lesson nor the unit has one + * @returns {Array} TanStack column definitions */ -export function buildDataColumns(attributes, rowActions) { +export function buildDataColumns(attributes, rowActions, tierMap = {}, unitSubscription = null, courseSubscription = null) { const visibleAttributes = attributes.filter((a) => !a.hidden); + const cellOverrides = buildCellOverrides(tierMap, unitSubscription, courseSubscription); return [ buildSelectionColumn(), diff --git a/src/modules/admin/config/courses/units/columns.config.jsx b/src/modules/admin/config/courses/units/columns.config.jsx index 1edd8bf..612810e 100644 --- a/src/modules/admin/config/courses/units/columns.config.jsx +++ b/src/modules/admin/config/courses/units/columns.config.jsx @@ -2,11 +2,13 @@ // Column definitions and pinning config for the Users table. import { Badge } from "@/components/ui/badge"; -import { Clock } from "lucide-react"; +import { Clock, Tag } from "lucide-react"; +import * as LucideIcons from "lucide-react"; import { buildColumns } from "@/utils/table.util"; import { buildSelectionColumn } from "@/components/generic/Table/buildSelectionColumn"; import { buildRowActionsColumn } from "@/components/generic/Table/buildRowActionsColumn"; import { formatDuration } from "@/utils/timestamp.util"; +import { resolveTierBadge } from "@/utils/tierBadge.util"; export const columnPinning = { right: ["actions"], @@ -14,7 +16,8 @@ export const columnPinning = { }; // ─── Custom cell overrides ──────────────────────────────────────────────────── -const cellOverrides = { +function buildCellOverrides(tierMap, courseSubscription) { + return { duration_seconds: (info) => { const seconds = parseInt(info.getValue() ?? 0, 10); @@ -27,17 +30,34 @@ const cellOverrides = { ); }, -}; + subscription: (info) => { + const slug = info.getValue() || courseSubscription; + if (!slug) return -; + + const { label, cls } = resolveTierBadge(slug, tierMap); + const Icon = LucideIcons[tierMap[slug]?.badge_icon] ?? Tag; + return ( + + + {label} + + ); + }, + }; +} /** * Builds the full column array for the Users table. * - * @param {Array} attributes Field definitions from the server (drives data columns) - * @param {Array} rowActions Row-level kebab action definitions - * @returns {Array} TanStack column definitions + * @param {Array} attributes Field definitions from the server (drives data columns) + * @param {Array} rowActions Row-level kebab action definitions + * @param {Object} tierMap slug → tier category, for colored Subscription badges + * @param {string} courseSubscription Parent course's tier, used when the unit has no direct tier of its own + * @returns {Array} TanStack column definitions */ -export function buildDataColumns(attributes, rowActions) { +export function buildDataColumns(attributes, rowActions, tierMap = {}, courseSubscription = null) { const visibleAttributes = attributes.filter((a) => !a.hidden); + const cellOverrides = buildCellOverrides(tierMap, courseSubscription); return [ buildSelectionColumn(), diff --git a/src/modules/admin/pages/courses/ViewCourse.jsx b/src/modules/admin/pages/courses/ViewCourse.jsx index f543dcb..cec8496 100644 --- a/src/modules/admin/pages/courses/ViewCourse.jsx +++ b/src/modules/admin/pages/courses/ViewCourse.jsx @@ -44,9 +44,9 @@ function SectionCard({ icon: Icon, title, children }) { } const LEVEL_BADGE = { - beginner: "secondary", + beginner: "secondary", intermediate: "outline", - advanced: "destructive", + advanced: "destructive", }; const SUBSCRIPTION_BADGE = { @@ -156,7 +156,7 @@ function CourseDetailsTab({ course, loading, instructors, achievementKeys, achie