merge new_starr_app@qas history into apps/web

git-subtree-dir: apps/web
git-subtree-mainline: eaa6d2276a
git-subtree-split: 459af9d46a
This commit is contained in:
2026-08-24 12:21:16 +08:00
699 changed files with 103969 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
// Action registry — label + badge colour per action key
export const ACTION_CONFIG = {
// ── Auth ────────────────────────────────────────────────────────────────────
login: { label: "Logged In", group: "auth" },
logout: { label: "Logged Out", group: "auth" },
register: { label: "Registered", group: "auth" },
password_change: { label: "Changed Password", group: "auth" },
// ── Profile ─────────────────────────────────────────────────────────────────
update_profile: { label: "Updated Profile", group: "profile" },
revoke_session: { label: "Revoked Session", group: "profile" },
// ── Learning ────────────────────────────────────────────────────────────────
lesson_read: { label: "Read Lesson", group: "learn" },
submit_task: { label: "Submitted Task", group: "learn" },
visit_link: { label: "Visited Link", group: "learn" },
// ── User management ─────────────────────────────────────────────────────────
create_staff: { label: "Created Staff", group: "mgmt" },
update_user: { label: "Updated User", group: "mgmt" },
deactivate_user: { label: "Deactivated User", group: "danger" },
restore_user: { label: "Restored User", group: "success" },
terminate_session: { label: "Terminated Session", group: "danger" },
set_user_status: { label: "Set User Status", group: "mgmt" },
// ── Courses ─────────────────────────────────────────────────────────────────
create_course: { label: "Created Course", group: "course" },
update_course: { label: "Updated Course", group: "course" },
archive_course: { label: "Archived Course", group: "course" },
restore_course: { label: "Restored Course", group: "course" },
bulk_archive_courses: { label: "Bulk Archived Courses", group: "course" },
bulk_restore_courses: { label: "Bulk Restored Courses", group: "course" },
sync_prerequisites: { label: "Synced Prerequisites", group: "course" },
sync_instructors: { label: "Synced Instructors", group: "course" },
// ── Units ───────────────────────────────────────────────────────────────────
create_unit: { label: "Created Unit", group: "course" },
update_unit: { label: "Updated Unit", group: "course" },
archive_unit: { label: "Archived Unit", group: "course" },
restore_unit: { label: "Restored Unit", group: "course" },
bulk_archive_units: { label: "Bulk Archived Units", group: "course" },
bulk_restore_units: { label: "Bulk Restored Units", group: "course" },
// ── Lessons ─────────────────────────────────────────────────────────────────
create_lesson: { label: "Created Lesson", group: "course" },
update_lesson: { label: "Updated Lesson", group: "course" },
archive_lesson: { label: "Archived Lesson", group: "course" },
restore_lesson: { label: "Restored Lesson", group: "course" },
bulk_archive_lessons: { label: "Bulk Archived Lessons", group: "course" },
bulk_restore_lessons: { label: "Bulk Restored Lessons", group: "course" },
upsert_lesson_page: { label: "Updated Lesson Page", group: "course" },
// ── Quizzes ─────────────────────────────────────────────────────────────────
create_quiz: { label: "Created Quiz", group: "course" },
update_quiz: { label: "Updated Quiz", group: "course" },
archive_quiz: { label: "Archived Quiz", group: "course" },
restore_quiz: { label: "Restored Quiz", group: "course" },
// ── Questions ───────────────────────────────────────────────────────────────
create_question: { label: "Created Question", group: "course" },
update_question: { label: "Updated Question", group: "course" },
archive_question: { label: "Archived Question", group: "course" },
restore_question: { label: "Restored Question", group: "course" },
bulk_archive_questions: { label: "Bulk Archived Questions", group: "course" },
bulk_restore_questions: { label: "Bulk Restored Questions", group: "course" },
// ── Assessments ─────────────────────────────────────────────────────────────
create_assessment: { label: "Created Assessment", group: "course" },
update_assessment: { label: "Updated Assessment", group: "course" },
archive_assessment: { label: "Archived Assessment", group: "course" },
restore_assessment: { label: "Restored Assessment", group: "course" },
// ── Task Lists ──────────────────────────────────────────────────────────────
create_task_list: { label: "Created Task List", group: "task" },
update_task_list: { label: "Updated Task List", group: "task" },
archive_task_list: { label: "Archived Task List", group: "task" },
restore_task_list: { label: "Restored Task List", group: "task" },
bulk_archive_task_lists: { label: "Bulk Archived Task Lists", group: "task" },
bulk_restore_task_lists: { label: "Bulk Restored Task Lists", group: "task" },
assign_groups: { label: "Assigned Groups", group: "task" },
unassign_groups: { label: "Unassigned Groups", group: "task" },
// ── Tasks ───────────────────────────────────────────────────────────────────
create_task: { label: "Created Task", group: "task" },
update_task: { label: "Updated Task", group: "task" },
archive_task: { label: "Archived Task", group: "task" },
restore_task: { label: "Restored Task", group: "task" },
bulk_archive_tasks: { label: "Bulk Archived Tasks", group: "task" },
bulk_restore_tasks: { label: "Bulk Restored Tasks", group: "task" },
// ── Task Completions ────────────────────────────────────────────────────────
archive_completion: { label: "Archived Completion", group: "task" },
restore_completion: { label: "Restored Completion", group: "task" },
bulk_archive_completions: { label: "Bulk Archived Completions", group: "task" },
bulk_restore_completions: { label: "Bulk Restored Completions", group: "task" },
// ── Groups ──────────────────────────────────────────────────────────────────
create_group: { label: "Created Group", group: "grp" },
update_group: { label: "Updated Group", group: "grp" },
deactivate_group: { label: "Deactivated Group", group: "grp" },
restore_group: { label: "Restored Group", group: "grp" },
bulk_deactivate_groups:{ label: "Bulk Deactivated Groups", group: "grp" },
bulk_restore_groups: { label: "Bulk Restored Groups", group: "grp" },
add_user_to_group: { label: "Added User to Group", group: "grp" },
remove_user_from_group:{ label: "Removed User from Group", group: "grp" },
// ── Subscriptions ───────────────────────────────────────────────────────────
create_tier_plan: { label: "Created Subscription", group: "commerce" },
update_tier_plan: { label: "Updated Subscription", group: "commerce" },
archive_tier_plan: { label: "Archived Subscription", group: "commerce" },
restore_tier_plan: { label: "Restored Subscription", group: "commerce" },
bulk_archive_tier_plans: { label: "Bulk Archived Subscriptions", group: "commerce" },
bulk_restore_tier_plans: { label: "Bulk Restored Subscriptions", group: "commerce" },
sync_plan_courses: { label: "Synced Plan Courses", group: "commerce" },
grant_tier: { label: "Granted Subscription", group: "success" },
revoke_tier: { label: "Revoked Subscription", group: "danger" },
// ── Products & Categories ───────────────────────────────────────────────────
upsert_course_product: { label: "Set Course Product", group: "commerce" },
remove_course_product: { label: "Removed Course Product", group: "commerce" },
sync_course_categories: { label: "Synced Course Categories", group: "commerce" },
create_category: { label: "Created Category", group: "commerce" },
update_category: { label: "Updated Category", group: "commerce" },
archive_category: { label: "Archived Category", group: "commerce" },
restore_category: { label: "Restored Category", group: "commerce" },
// ── Files ───────────────────────────────────────────────────────────────────
upload_asset: { label: "Uploaded File", group: "content" },
update_asset: { label: "Updated File", group: "content" },
archive_asset: { label: "Archived File", group: "content" },
restore_asset: { label: "Restored File", group: "content" },
bulk_archive_assets:{ label: "Bulk Archived Files", group: "content" },
bulk_restore_assets:{ label: "Bulk Restored Files", group: "content" },
// ── Advertisements ──────────────────────────────────────────────────────────
create_advertisement: { label: "Created Ad", group: "content" },
update_advertisement: { label: "Updated Ad", group: "content" },
archive_advertisement: { label: "Archived Ad", group: "content" },
restore_advertisement: { label: "Restored Ad", group: "content" },
bulk_archive_advertisements: { label: "Bulk Archived Ads", group: "content" },
bulk_restore_advertisements: { label: "Bulk Restored Ads", group: "content" },
};
// Badge class per group
const GROUP_STYLES = {
auth: "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-800",
profile: "bg-violet-50 text-violet-700 border-violet-200 dark:bg-violet-950 dark:text-violet-300 dark:border-violet-800",
learn: "bg-teal-50 text-teal-700 border-teal-200 dark:bg-teal-950 dark:text-teal-300 dark:border-teal-800",
mgmt: "bg-orange-50 text-orange-700 border-orange-200 dark:bg-orange-950 dark:text-orange-300 dark:border-orange-800",
danger: "bg-red-50 text-red-700 border-red-200 dark:bg-red-950 dark:text-red-300 dark:border-red-800",
success: "bg-green-50 text-green-700 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-800",
course: "bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-800",
task: "bg-indigo-50 text-indigo-700 border-indigo-200 dark:bg-indigo-950 dark:text-indigo-300 dark:border-indigo-800",
grp: "bg-sky-50 text-sky-700 border-sky-200 dark:bg-sky-950 dark:text-sky-300 dark:border-sky-800",
commerce: "bg-rose-50 text-rose-700 border-rose-200 dark:bg-rose-950 dark:text-rose-300 dark:border-rose-800",
content: "bg-slate-100 text-slate-600 border-slate-200 dark:bg-slate-800 dark:text-slate-400 dark:border-slate-700",
};
const FALLBACK = "bg-muted text-muted-foreground border-border";
export function getActionBadge(action) {
const cfg = ACTION_CONFIG[action];
return {
label: cfg?.label ?? action,
className: cfg ? (GROUP_STYLES[cfg.group] ?? FALLBACK) : FALLBACK,
};
}
+26
View File
@@ -0,0 +1,26 @@
// Centralizes all icon maps, link maps, and chart link maps for the
// admin dashboard. To add a new section (e.g. Tasks), just add a new
// export block here and import it in UsersDashboard.jsx.
import {
Users, UserCheck, UserMinus, ShieldCheck,
Archive, Layers,
} from "lucide-react";
// ─── Users ───────────────────────────────────────────────────────────────────
export const USER_STAT_MAP = {
total: { label: "Total Users", icon: <Users className="size-5 text-muted-foreground" /> },
active: { label: "Active", icon: <UserCheck className="size-5 text-green-500" /> },
inactive: { label: "Inactive", icon: <UserMinus className="size-5 text-red-400" /> },
verified: { label: "Verified", icon: <ShieldCheck className="size-5 text-blue-500" /> },
archived: { label: "Archived", icon: <Archive className="size-5 text-muted-foreground" /> },
};
// ─── User Groups ──────────────────────────────────────────────────────────────
export const GROUP_STAT_MAP = {
total: { label: "Total Groups", icon: <Layers className="size-5 text-muted-foreground" /> },
active: { label: "Active", icon: <UserCheck className="size-5 text-green-500" /> },
inactive: { label: "Inactive", icon: <UserMinus className="size-5 text-red-400" /> },
};
+56
View File
@@ -0,0 +1,56 @@
import { Users, GitFork, FolderOpen, BookText, BookCheck, FileText, ListCheck, ShieldCheck, Megaphone, Bell, Cog } from "lucide-react";
export const ADMIN_SECTIONS = [
{
id: "section-users",
tab: "User Management",
title: "User Management",
description: "Manage people across different systems",
tiles: [
{ key: "users", label: "Users", icon: Users, link: "/admin/users" },
{ key: "user-groups", label: "User Groups", icon: GitFork, link: "/admin/groups" },
],
},
{
id: "section-resources",
tab: "Resource Management",
title: "Resource Management",
description: "It includes files management and subscriptions.",
tiles: [
{ key: "assets", label: "Files", icon: FolderOpen, link: "/admin/assets" },
{ key: "tiers", label: "Subscriptions", icon: ShieldCheck, link: "/admin/tiers/plans" },
],
},
{
id: "section-courses",
tab: "Content Management",
title: "Content Management",
description: "Manage tasks and courses",
tiles: [
{ key: "courses", label: "Courses", icon: BookText, link: "/admin/courses" },
{ key: "units", label: "Units", icon: BookCheck, link: "/admin/units" },
{ key: "lessons", label: "Lessons", icon: FileText, link: "/admin/lessons" },
{ key: "tasks", label: "Tasks", icon: ListCheck, link: "/admin/taskList" },
],
},
{
id: "section-site",
tab: "Site Content",
title: "Site Content",
description: "Manage public-facing content",
tiles: [
{ key: "advertisements", label: "Ads", icon: Megaphone, link: "/admin/advertisements" },
{ key: "notifications", label: "Alerts", icon: Bell, link: "/admin/announcements" },
],
},
{
id: "section-system",
tab: "System",
title: "System",
description: "Background jobs and automation",
tiles: [
{ key: "jobs", label: "Jobs", icon: Cog, link: "/admin/jobs" },
],
},
];
+44
View File
@@ -0,0 +1,44 @@
// data/advertisements.data.js
import { Megaphone, Image } from "lucide-react";
// ─── Types ──────────────────────────────────────────────────────────────────
// Drives: filter dropdown options, type badge label/icon on each card,
// and which fields the Add/Edit form shows (hero needs headline/description/ctas,
// banner is closer to image-only).
export const ADVERTISEMENT_TYPES = [
{ value: "hero", label: "Hero", icon: Megaphone, description: "Large featured banner with headline, description, and CTAs" },
{ value: "banner", label: "Banner", icon: Image, description: "Simple image banner" },
];
export const ADVERTISEMENT_TYPE_MAP = Object.fromEntries(
ADVERTISEMENT_TYPES.map((t) => [t.value, t])
);
// ─── Statuses ───────────────────────────────────────────────────────────────
// Drives: filter dropdown options, status badge color/label on each card.
export const ADVERTISEMENT_STATUSES = [
{ value: "draft", label: "Draft", badgeClass: "bg-muted text-muted-foreground" },
{ value: "active", label: "Active", badgeClass: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400" },
{ value: "scheduled", label: "Scheduled", badgeClass: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400" },
{ value: "expired", label: "Expired", badgeClass: "bg-muted text-muted-foreground" },
];
export const ADVERTISEMENT_STATUS_MAP = Object.fromEntries(
ADVERTISEMENT_STATUSES.map((s) => [s.value, s])
);
// Filterable subset for the main (non-archived) list. "expired" is excluded —
// the expire_advertisements cron sweeps any ad past its end_date into the
// Archived table within a minute, so this list will practically never hold
// one; offering it as a filter here just returns an empty result.
export const ADVERTISEMENT_FILTERABLE_STATUSES = ADVERTISEMENT_STATUSES.filter(
(s) => s.value !== "expired"
);
// Max number of CTAs allowed per advertisement (matches backend normalizeCtas slice(0,2))
export const MAX_CTAS = 2;
// Max number of badge label chips allowed per advertisement (matches backend normalizeBadgeLabels slice(0,2))
export const MAX_BADGE_LABELS = 2;
+34
View File
@@ -0,0 +1,34 @@
// data/cronPresets.data.js
// Mirrors backend data/cronPresets.data.js — the only schedule options the UI offers.
export const CRON_PRESET_OPTIONS = [
{ value: "every_minute", label: "Every minute" },
{ value: "every_5_min", label: "Every 5 minutes" },
{ value: "every_15_min", label: "Every 15 minutes" },
{ value: "hourly", label: "Hourly" },
{ value: "every_6_hours", label: "Every 6 hours" },
{ value: "daily", label: "Daily at midnight" },
];
export const CRON_PRESET_MAP = Object.fromEntries(
CRON_PRESET_OPTIONS.map((p) => [p.value, p])
);
// job_name -> friendly copy (label/description come from the API too, but this
// is used as a fallback and for the settings icon).
export const JOB_LABELS = {
taskOverdue: { label: "Task Alerts (Admin)", description: "Automatically marks expired tasks as overdue or completed, and notifies admins." },
userNotifications: { label: "Task Alerts (Users)", description: "Notifies affected users when their tasks are automatically marked overdue or completed." },
issueCertificates: { label: "Certificate Issued", description: "Notifies users when a course certificate is ready." },
expireUserTiers: { label: "Subscription Expired", description: "Notifies users when their subscription expires." },
};
// The only jobs that support a configurable target_status, and the values it accepts.
export const TARGET_STATUS_OPTIONS = [
{ value: "overdue", label: "Overdue" },
{ value: "completed", label: "Completed" },
];
export const TARGET_STATUS_MAP = Object.fromEntries(
TARGET_STATUS_OPTIONS.map((s) => [s.value, s])
);
+6
View File
@@ -0,0 +1,6 @@
import { Mail, Globe } from "lucide-react";
export const ContactData = [
{ id: 1, icon: Mail, label: "Email:", value: "info@philproperties.ph" },
{ id: 2, icon: Globe, label: "Website:", value: "philproperties.ph" },
]
@@ -0,0 +1,32 @@
// data/notificationBroadcast.data.js
import { Shield, Users, Megaphone, ListCheck, BookText, ShieldCheck } from "lucide-react";
// ─── Target types ─────────────────────────────────────────────────────────────
// Drives: target select in the Add form, target badge on each card.
// needsTarget: true means the form must also show a picker for target_id.
export const TARGET_TYPE_OPTIONS = [
{ value: "admin", label: "Admins", icon: Shield, description: "Visible only in the admin notification bell", needsTarget: false },
{ value: "user", label: "Users", icon: Users, description: "Sent to every active user", needsTarget: false },
{ value: "both", label: "Admins & Users", icon: Megaphone, description: "Sent to admins and every active user", needsTarget: false },
{ value: "task_list", label: "Task List", icon: ListCheck, description: "Sent to everyone assigned to a specific task list", needsTarget: true },
{ value: "course", label: "Course", icon: BookText, description: "Sent to everyone with access to a specific course", needsTarget: true },
{ value: "tier_plan", label: "Subscription", icon: ShieldCheck, description: "Sent to everyone currently on a specific subscription plan", needsTarget: true },
];
export const TARGET_TYPE_MAP = Object.fromEntries(
TARGET_TYPE_OPTIONS.map((a) => [a.value, a])
);
// ─── Statuses ───────────────────────────────────────────────────────────────
// Drives: filter dropdown options, status badge color/label on each card.
export const BROADCAST_STATUSES = [
{ value: "draft", label: "Draft", badgeClass: "bg-muted text-muted-foreground" },
{ value: "sent", label: "Sent", badgeClass: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400" },
{ value: "archived", label: "Archived", badgeClass: "bg-muted text-muted-foreground" },
];
export const BROADCAST_STATUS_MAP = Object.fromEntries(
BROADCAST_STATUSES.map((s) => [s.value, s])
);
@@ -0,0 +1,12 @@
// A template is "sent" once it has a live title/message — that's the only
// content services/notificationTemplate.service.js's renderNotification() ever
// reads on the backend. Editing a sent template writes to draft_title/
// draft_message instead, so "pending changes" means there's a draft sitting on
// top of the live version. Mirrors emailTemplateStatus.data.js.
export const hasPendingChanges = (t) =>
t?.status === "sent" && (t?.draft_title != null || t?.draft_message != null);
export const STATUS_META = {
draft: { label: "Draft", badgeClass: "bg-muted text-muted-foreground border-border" },
sent: { label: "Sent", badgeClass: "bg-emerald-100 text-emerald-700 border-emerald-400 dark:bg-emerald-900/40 dark:text-emerald-400 dark:border-emerald-700" },
};
+24
View File
@@ -0,0 +1,24 @@
// data/placement.data.js
//
// Mirrors the backend placement registry (models/advertisements/advertisements.placements.js
// in new_starr). Each entry is a page + position slot; the format (hero/banner/popup/sidebar)
// is derived from the placement, never chosen independently. Keep this list in sync with the
// backend registry when adding a new placement — same pattern as ADVERTISEMENT_TYPES already
// mirroring the backend type ENUM.
export const PLACEMENTS = [
{ key: "dashboard.hero", format: "hero", page: "dashboard", pageLabel: "Dashboard", slotLabel: "Hero (top of page)" },
{ key: "tier_plans.banner", format: "banner", page: "tier_plans", pageLabel: "Subscriptions", slotLabel: "Banner (above plan cards)" },
{ key: "course_details.banner", format: "banner", page: "course_details", pageLabel: "Course Details", slotLabel: "Banner (below hero)" },
];
export const PLACEMENT_MAP = Object.fromEntries(PLACEMENTS.map((p) => [p.key, p]));
// Grouped by page — powers the cascading Page → Position picker in Add/EditAdvertisement.
export const AD_PAGES = Object.values(
PLACEMENTS.reduce((acc, p) => {
if (!acc[p.page]) acc[p.page] = { page: p.page, pageLabel: p.pageLabel, placements: [] };
acc[p.page].placements.push(p);
return acc;
}, {})
);
@@ -0,0 +1,44 @@
// data/placementLayouts.data.js
//
// Declarative "wireframe recipe" per placement — powers <PlacementSkeleton />
// in the advertisement creation wizard so admins can see roughly where their
// ad will land before saving. Keys mirror PLACEMENTS in placement.data.js.
// Adding a new placement: add one entry here, nothing else needs to change.
//
// Block kinds:
// nav — thin top bar stand-in
// bar — a rounded block; `size` controls height (sm/md/lg/xl);
// `highlight: true` marks it as the ad slot, `label` names it
// filters — a row of small pill shapes
// grid — a row of small equal boxes (card grid stand-in)
// list — a couple of thin stacked rows (table/list stand-in)
// row — a flex row of `columns`, each rendered like a `bar`
//
// `overlay` (optional, top-level) renders the base `blocks` dimmed with a
// centered floating highlighted box on top — used for popups.
export const PLACEMENT_LAYOUTS = {
"dashboard.hero": {
blocks: [
{ kind: "nav" },
{ kind: "bar", size: "xl", highlight: true, label: "Hero" },
{ kind: "list", label: "My Groups" },
{ kind: "grid", label: "Courses" },
],
},
"course_details.banner": {
blocks: [
{ kind: "nav" },
{ kind: "bar", size: "lg", label: "Course hero" },
{ kind: "bar", size: "sm", highlight: true, label: "Banner" },
{ kind: "list", label: "Course content" },
],
},
"tier_plans.banner": {
blocks: [
{ kind: "nav" },
{ kind: "bar", size: "md", highlight: true, label: "Banner" },
{ kind: "grid", label: "Plan cards" },
],
},
};
+13
View File
@@ -0,0 +1,13 @@
// ─── Config ───────────────────────────────────────────────────────────────────
export const ROLE_CONFIG = {
admin: { label: 'Administrator', variant: 'destructive' },
staff: { label: 'Staff', variant: 'secondary' },
client: { label: 'Client', variant: 'default' },
user: { label: 'Client', variant: 'default' }, // ← add this
}
export const AVATAR_COLORS = {
admin: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
staff: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
client: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
}