+
{({ getRootProps, getInputProps, isDragActive }) => (
-
- {hasFile ? (
- previewSrc ? (
-

- ) : (
-
- )
- ) : (
-
-
-
+ {hasFile ? (
+ <>
+
+
+ {previewSrc ? (
+

+ ) : (
+
+ )}
+
+
+
+
+ {displayName}
+
+
+ {uploading ? 'Uploading...' : 'Drag and drop or click to replace'}
+
+
+
+
+ {uploading ? (
+
+
-
- )}
-
-
- {hasFile
- ? displayName
- : isImageField
- ? 'Drop or click to upload an image'
- : 'Drop or click to upload a file'}
-
-
- {uploading
- ? 'Uploading...'
- : hasFile
- ? localFile?.size
- ? formatBytes(localFile.size)
- : 'Uploaded'
- : `${exts.join(', ')} · 500MB max`}
-
+ ) : (
+
+ )}
+ >
+ ) : (
+
+
+
Drag and drop a file here or click
- {hasFile ? (
-
- ) : (
-
- )}
-
+ )}
)}
-
+
500MB Max File size
{error ?
{error}
: null}
);
diff --git a/src/components/table/DataTable.jsx b/src/components/table/DataTable.jsx
index 4d323e1..a237cd6 100644
--- a/src/components/table/DataTable.jsx
+++ b/src/components/table/DataTable.jsx
@@ -2,10 +2,11 @@ import { flexRender } from '@tanstack/react-table';
import clsx from 'clsx';
import { Table } from 'react-bootstrap';
import { TbArrowDown, TbArrowUp } from 'react-icons/tb';
+import { EmptyTableRow } from '@/components/EmptyState';
const DataTable = ({
table,
className = '',
- emptyMessage = 'Nothing found.',
+ emptyMessage = 'No data to show',
showHeaders = true
}) => {
const columns = table.getAllColumns();
@@ -30,11 +31,7 @@ const DataTable = ({
{table.getRowModel().rows?.length ? table.getRowModel().rows.map(row =>
{row.getVisibleCells().map(cell => | {flexRender(cell.column.columnDef.cell, cell.getContext())} | )}
-
) :
- |
- {emptyMessage}
- |
-
}
+ ) : }
;
diff --git a/src/helpers/metric.js b/src/helpers/metric.js
index 4797f38..6023685 100644
--- a/src/helpers/metric.js
+++ b/src/helpers/metric.js
@@ -6,6 +6,13 @@ export function splitHash(value) {
return [];
}
+export function joinHash(value) {
+ if (Array.isArray(value)) return value.filter(Boolean).join('#');
+ return value || '';
+}
+
+export const STATUS_VALUES = { active: 'Active', disabled: 'Disabled' };
+
export function splitTilde(value) {
if (Array.isArray(value)) return value.filter((part) => part !== null && part !== undefined && part !== '');
if (typeof value === 'string' && value.trim()) return value.split('~').filter(Boolean);
diff --git a/src/layouts/menu.js b/src/layouts/menu.js
index 6b60a25..3cd3f9a 100644
--- a/src/layouts/menu.js
+++ b/src/layouts/menu.js
@@ -48,7 +48,6 @@ export function getMenuItems({ permissions = [], roles = [], domain = '', email
'hierarchy_get_all',
'team_get_all',
'group_get_all',
- 'channel_get_all',
'domain_get_one',
])
) {
@@ -64,7 +63,6 @@ export function getMenuItems({ permissions = [], roles = [], domain = '', email
child('positions', 'Positions', '/positions', 'all_data_access'),
child('teams', 'Teams', '/teams', 'team_get_all'),
child('groups', 'Groups', '/groups', 'group_get_all'),
- child('channels', 'Channels', '/channels', 'channel_get_all'),
child('domain', 'Domain', '/domain/edit', 'domain_get_one'),
child('apps', 'Apps', '/apps', 'app_get_all'),
].filter((item) => !item.permission || permissions.includes(item.permission)),
@@ -79,7 +77,7 @@ export function getMenuItems({ permissions = [], roles = [], domain = '', email
});
}
- if (hasAny(permissions, ['contact_get_all', 'contactattribute_get_all'])) {
+ if (hasAny(permissions, ['contact_get_all', 'contactattribute_get_all', 'contact_list_get_all'])) {
items.push({
key: 'contacts',
label: 'Contact Management',
@@ -88,6 +86,7 @@ export function getMenuItems({ permissions = [], roles = [], domain = '', email
child('contact-list', 'Contacts', '/contacts', 'contact_get_all'),
child('contact-export', 'Contact Export', '/contactexports', 'contact_get_all'),
child('contact-attrs', 'Attributes', '/contactattributes', 'contactattribute_get_all'),
+ child('contact-lists', 'Contact Lists', '/contactlists', 'contact_list_get_all'),
].filter((item) => !item.permission || permissions.includes(item.permission)),
});
}
@@ -181,6 +180,19 @@ export function getMenuItems({ permissions = [], roles = [], domain = '', email
});
}
+ if (hasAny(permissions, ['courses_get_all', 'articles_bank_get_all', 'lms_quiz_get_all'])) {
+ items.push({
+ key: 'lms',
+ label: 'LMS',
+ icon: LuBookOpen,
+ children: [
+ child('courses', 'Courses', '/courses', 'courses_get_all'),
+ child('articlebanks', 'Article Banks', '/articlebanks', 'articles_bank_get_all'),
+ child('lmsquizs', 'Quiz', '/lmsquizs', 'lms_quiz_get_all'),
+ ].filter((item) => !item.permission || permissions.includes(item.permission)),
+ });
+ }
+
if (hasAny(permissions, ['rcpa_product_get_all', 'rcpa_reports'])) {
items.push({
key: 'rcpa',
@@ -196,6 +208,18 @@ export function getMenuItems({ permissions = [], roles = [], domain = '', email
});
}
+ if (permissions.includes('contest_get_all')) {
+ items.push({
+ key: 'detailing',
+ label: 'Detailing',
+ icon: LuBookOpen,
+ children: [
+ child('detailing-contests', 'Contests', '/detailingcontests'),
+ child('contest-parameters', 'Parameters', '/contestparameters'),
+ ],
+ });
+ }
+
if (hasAny(permissions, ['metric_get_all', 'injestion_mapping_get_all', 'metric_view_get_all'])) {
items.push({
key: 'metric',
@@ -246,6 +270,7 @@ export function getMenuItems({ permissions = [], roles = [], domain = '', email
'roles_leaderboard_get_all',
'workflow_definition_get_all',
'workflow_instance_get_all',
+ 'formproduct_get_all',
])
) {
const formChildren = [
@@ -257,6 +282,7 @@ export function getMenuItems({ permissions = [], roles = [], domain = '', email
child('formexports', 'Form Data Export', '/formexports', 'formexports_get_all'),
child('reminders', 'Reminders', '/reminders', 'reminder_get_all'),
child('leaderboards', 'Leaderboard', '/leaderboards', 'roles_leaderboard_get_all'),
+ child('formproducts', 'Products', '/formproducts', 'formproduct_get_all'),
].filter((item) => !item.permission || permissions.includes(item.permission));
if (hasAny(permissions, ['workflow_definition_get_all', 'workflow_instance_get_all'])) {
diff --git a/src/modules/articlebanks.js b/src/modules/articlebanks.js
new file mode 100644
index 0000000..ce648ae
--- /dev/null
+++ b/src/modules/articlebanks.js
@@ -0,0 +1,42 @@
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const articlebanks = {
+ service: 'lms',
+ module: 'articles_bank',
+ title: 'Article Bank',
+ subtitle: 'LMS',
+ baseUrl: '/articlebank',
+ listPath: '/articlebanks',
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'title', title: 'Title', type: 'text' },
+ { key: 'description', title: 'Description', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: () => [
+ { key: 'title', title: 'Title', type: 'text', required: true },
+ { key: 'description', title: 'Description', type: 'text', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'contents', title: 'Content', type: 'mapping', fullwidth: true },
+ { key: 'icon', title: 'Icon', type: 'file', required: true },
+ ],
+ beforeForm: (item) => ({
+ ...item,
+ contents: Array.isArray(item.contents)
+ ? item.contents.map((row) => ({
+ value: row.type || row.value || '',
+ display: row.content || row.display || '',
+ }))
+ : [],
+ }),
+ beforeSave: (data) => ({
+ ...data,
+ contents: (Array.isArray(data.contents) ? data.contents : []).map((row, index) => ({
+ order: index + 1,
+ type: row.value || row.type || '',
+ content: row.display || row.content || '',
+ })),
+ }),
+};
+
+export default articlebanks;
diff --git a/src/modules/articles.js b/src/modules/articles.js
new file mode 100644
index 0000000..fde4869
--- /dev/null
+++ b/src/modules/articles.js
@@ -0,0 +1,51 @@
+import { signedGet, isSuccess } from '@/api/client';
+
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const articles = {
+ service: 'lms',
+ module: 'articles',
+ title: 'Article',
+ subtitle: 'LMS',
+ baseUrl: '/article',
+ listPath: '/articles',
+ missingParentMessage: 'Lesson not found',
+ parentParam: { urlKey: 'id', apiKey: 'lessons_id' },
+ parentField: { urlKey: 'id', field: 'lessons_id' },
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'type', title: 'Type', type: 'text' },
+ { key: 'lessons_id', title: 'Lesson', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ listButtons: [
+ { title: 'Add New', to: '/article/addnew' },
+ { title: 'Back to Courses', to: '/courses' },
+ ],
+ formFields: async () => {
+ const [banksRes, quizRes] = await Promise.all([
+ signedGet('lms', 'articles_bank/get_all'),
+ signedGet('lms', 'lmsquiz/get_all'),
+ ]);
+ const entities = {};
+ (isSuccess(banksRes) ? banksRes.data?.items || [] : []).forEach((item) => {
+ if (item?.id) entities[item.id] = item.title || item.id;
+ });
+ (isSuccess(quizRes) ? quizRes.data?.items || [] : []).forEach((item) => {
+ if (item?.id) entities[item.id] = item.title || item.id;
+ });
+ return [
+ { key: 'type', title: 'Type', type: 'select', required: true, values: { article: 'Article', quiz: 'Quiz' } },
+ { key: 'entity_id', title: 'Select Form', type: 'select', required: true, values: entities },
+ { key: 'sort_order', title: 'Sequence Of Article', type: 'text', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'icon', title: 'Icon', type: 'file', required: true },
+ ];
+ },
+ beforeForm: (item, _mode, ctx) => ({
+ ...item,
+ course_id: item.course_id || ctx?.searchParams?.get('course_id') || '',
+ }),
+};
+
+export default articles;
diff --git a/src/modules/brands.js b/src/modules/brands.js
new file mode 100644
index 0000000..3373d70
--- /dev/null
+++ b/src/modules/brands.js
@@ -0,0 +1,35 @@
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const brands = {
+ service: 'catalog',
+ module: 'brand',
+ title: 'Brand',
+ subtitle: 'Catalog',
+ baseUrl: '/brand',
+ listPath: '/brands',
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'name', title: 'Brand Name', type: 'text' },
+ { key: 'type', title: 'Brand Type', type: 'text' },
+ { key: 'division', title: 'Division', type: 'text' },
+ { key: 'company', title: 'Company', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: () => [
+ { key: 'name', title: 'Brand Name', type: 'text', required: true },
+ { key: 'type', title: 'Brand Type', type: 'select', values: { MDI: 'MDI', DPI: 'DPI', Oral: 'Oral' } },
+ { key: 'division', title: 'Division', type: 'text' },
+ { key: 'company', title: 'Company', type: 'text' },
+ { key: 'focus_brand', title: 'Focus Brand', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ ],
+ beforeSave: (data) => ({
+ ...data,
+ type: data.type || '',
+ division: data.division || '',
+ company: data.company || '',
+ focus_brand: data.focus_brand || '',
+ }),
+};
+
+export default brands;
diff --git a/src/modules/contactlists.js b/src/modules/contactlists.js
new file mode 100644
index 0000000..41fb7c6
--- /dev/null
+++ b/src/modules/contactlists.js
@@ -0,0 +1,23 @@
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const contactlists = {
+ service: 'contact',
+ module: 'contact_list',
+ title: 'Contact List',
+ subtitle: 'Contact Management',
+ baseUrl: '/contactlist',
+ listPath: '/contactlists',
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'name', title: 'Name', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: () => [
+ { key: 'name', title: 'Name', type: 'text', required: true },
+ { key: 'type', title: 'Type', type: 'select', values: { normal: 'Normal', dynamic: 'Dynamic' }, required: true },
+ { key: 'conditions', title: 'Conditions', type: 'textarea', fullwidth: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ ],
+};
+
+export default contactlists;
diff --git a/src/modules/contestparameters.js b/src/modules/contestparameters.js
new file mode 100644
index 0000000..aa92e66
--- /dev/null
+++ b/src/modules/contestparameters.js
@@ -0,0 +1,24 @@
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const contestparameters = {
+ service: 'detailing',
+ module: 'parameter',
+ title: 'Contest Parameters',
+ subtitle: 'Detailing',
+ baseUrl: '/contestparameter',
+ listPath: '/contestparameters',
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'title', title: 'Title', type: 'text' },
+ { key: 'scale', title: 'Scale', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: () => [
+ { key: 'title', title: 'Title', type: 'text', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'description', title: 'Contest Description', type: 'textarea', required: true, fullwidth: true },
+ { key: 'scale', title: 'Scale', type: 'number', required: true },
+ ],
+};
+
+export default contestparameters;
diff --git a/src/modules/courses.js b/src/modules/courses.js
new file mode 100644
index 0000000..4646045
--- /dev/null
+++ b/src/modules/courses.js
@@ -0,0 +1,45 @@
+import { toDateInput } from '@/helpers/metric';
+
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const courses = {
+ service: 'lms',
+ module: 'courses',
+ title: 'Courses',
+ subtitle: 'LMS',
+ baseUrl: '/course',
+ listPath: '/courses',
+ listFields: [
+ {
+ key: 'actions',
+ title: 'Actions',
+ buttons: [
+ 'edit',
+ 'delete',
+ { name: 'lessons', to: (row) => `/lessons?id=${row.id}`, title: 'Lessons' },
+ ],
+ },
+ { key: 'title', title: 'Title', type: 'text' },
+ { key: 'description', title: 'Description', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: () => [
+ { key: 'title', title: 'Title', type: 'text', required: true },
+ { key: 'description', title: 'Description', type: 'text', required: true },
+ { key: 'is_scheduled', title: 'Is Scheduled', type: 'radio', required: true, values: { yes: 'Yes', no: 'No' } },
+ { key: 'is_sequential', title: 'Is Sequential', type: 'radio', required: true, values: { yes: 'Yes', no: 'No' } },
+ { key: 'start_date', title: 'Schedule start date', type: 'date', required: true },
+ { key: 'end_date', title: 'Schedule end date', type: 'date', required: true },
+ { key: 'deadline', title: 'Deadline', type: 'date', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'icon', title: 'Icon', type: 'file', required: true },
+ ],
+ beforeForm: (item) => ({
+ ...item,
+ start_date: toDateInput(item.start_date),
+ end_date: toDateInput(item.end_date),
+ deadline: toDateInput(item.deadline),
+ }),
+};
+
+export default courses;
diff --git a/src/modules/detailingcontestresponses.js b/src/modules/detailingcontestresponses.js
new file mode 100644
index 0000000..adc88f6
--- /dev/null
+++ b/src/modules/detailingcontestresponses.js
@@ -0,0 +1,36 @@
+const detailingcontestresponses = {
+ service: 'detailing',
+ module: 'contest_response',
+ title: 'Detailing Contest Responses',
+ subtitle: 'Detailing',
+ baseUrl: '/detailingcontestresponse',
+ listPath: '/detailingcontestresponses',
+ missingParentMessage: 'Contest not found',
+ parentParam: { urlKey: 'id', apiKey: 'con_id' },
+ parentField: { urlKey: 'id', field: 'con_id' },
+ skipEdit: true,
+ skipAdd: true,
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['delete'] },
+ { key: 'avatar', title: 'User Avatar', type: 'image' },
+ { key: 'user_name', title: 'User Name', type: 'text' },
+ { key: 'title', title: 'Media Name', type: 'text' },
+ { key: 'media_type', title: 'Media Type', type: 'text' },
+ { key: 'createdAt', title: 'Submitted On', type: 'date' },
+ { key: 'marks', title: 'Avg. Marks', type: 'text' },
+ { key: 'total', title: 'Avg. Total', type: 'text' },
+ { key: 'result', title: 'Result', type: 'text' },
+ { key: 'no_of_evaluations', title: 'No of Evaluations', type: 'text' },
+ { key: 'likes_count', title: 'Likes', type: 'text' },
+ { key: 'comments_count', title: 'Comments', type: 'text' },
+ ],
+ listButtons: [{ title: 'Back to Detailing Contests', to: '/detailingcontests' }],
+ mapItems: (items) =>
+ items.map((item) => ({
+ ...item,
+ user_name: item.user_name || item.user_info?.name || '',
+ avatar: item.avatar || item.user_info?.avatar || '',
+ })),
+};
+
+export default detailingcontestresponses;
diff --git a/src/modules/detailingcontests.js b/src/modules/detailingcontests.js
new file mode 100644
index 0000000..99815f1
--- /dev/null
+++ b/src/modules/detailingcontests.js
@@ -0,0 +1,126 @@
+import { dropdownMap } from '@/api/dropdowns';
+import { signedGet, isSuccess } from '@/api/client';
+import { joinHash, splitHash } from '@/helpers/metric';
+
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const detailingcontests = {
+ service: 'detailing',
+ module: 'contest',
+ title: 'Detailing Contests',
+ subtitle: 'Detailing',
+ baseUrl: '/detailingcontest',
+ listPath: '/detailingcontests',
+ listFields: [
+ {
+ key: 'actions',
+ title: 'Actions',
+ buttons: [
+ 'edit',
+ 'delete',
+ { name: 'responses', to: (row) => `/detailingcontestresponses?id=${row.id}`, title: 'Responses' },
+ ],
+ },
+ { key: 'name', title: 'Title', type: 'text' },
+ { key: 'marks', title: 'Marks', type: 'text' },
+ { key: 'no_of_participants', title: 'No of participants', type: 'text' },
+ { key: 'no_of_uploads', title: 'No of uploads', type: 'text' },
+ { key: 'no_of_evaluations', title: 'No of evaluations', type: 'text' },
+ { key: 'pass_count', title: 'Pass Count', type: 'text' },
+ { key: 'fail_count', title: 'Fail Count', type: 'text' },
+ { key: 'published_on', title: 'Published On', type: 'date' },
+ { key: 'is_published', title: 'Published', type: 'select' },
+ { key: 'is_result_published', title: 'Result Published', type: 'select' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: async () => {
+ const [groups, users, roles, paramsRes] = await Promise.all([
+ dropdownMap('ums', 'group', (item) => item.name),
+ dropdownMap('ums', 'users', (item) => `${item.firstname || ''} ${item.lastname || ''} (${item.employee_code || ''})`),
+ dropdownMap('ums', 'roles', (item) => item.name),
+ signedGet('detailing', 'parameter/get_all'),
+ ]);
+ const params = {};
+ (isSuccess(paramsRes) ? paramsRes.data?.items || [] : []).forEach((item) => {
+ if (item?.id) params[item.id] = `${item.title || item.name} (Scale: ${item.scale ?? ''})`;
+ });
+ return [
+ { key: 'name', title: 'Contest Name', type: 'text', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'description', title: 'Contest Description', type: 'textarea', required: true, fullwidth: true },
+ { key: 'groups', title: 'Groups', type: 'multiselect', required: true, values: groups },
+ { key: 'evaluators', title: 'Evaluators', type: 'multiselect', required: true, values: users },
+ { key: 'observers', title: 'Observers', type: 'multiselect', required: true, values: users },
+ { key: 'evaluation_parameters', title: 'Evaluation Parameters', type: 'multiselect', required: true, values: params },
+ { key: 'dashboard_image', title: 'Dashboard Image', type: 'image', required: true },
+ { key: 'cover_image', title: 'Cover Image', type: 'image', required: true },
+ { key: 'media_type', title: 'Media Type', type: 'select', required: true, values: { audio: 'Audio', video: 'Video', pdf: 'PDF', image: 'Image' } },
+ { key: 'media', title: 'Media', type: 'file', required: true },
+ { key: 'response_types', title: 'Response Types', type: 'checkbox', required: true, values: { audio: 'Audio', video: 'Video', pdf: 'PDF', image: 'Image' } },
+ { key: 'response_sources', title: 'Response Sources', type: 'multiselect', required: true, values: { CAMERA: 'CAMERA', GALLERY: 'GALLERY' } },
+ { key: 'limits_audio', title: 'Audio Duration Limit (in sec)', type: 'number', required: true },
+ { key: 'limits_video', title: 'Video Duration Limit (in sec)', type: 'number', required: true },
+ { key: 'limits_size', title: 'Max file Size (in MB)', type: 'number', required: true },
+ { key: 'response_self_delete', title: 'Can delete self Response before Evaluation', type: 'yesno', required: true },
+ { key: 'allow_self_evaluation', title: 'Is Self Evaluation Enabled', type: 'yesno', required: true },
+ { key: 'evaluation_workflow', title: 'Evaluator Type', type: 'select', required: true, values: { ANY: 'ANY', ALL: 'ALL' } },
+ { key: 'result_publish_count', title: 'Result Publish Count', type: 'number', required: true },
+ { key: 'marks', title: 'Marks', type: 'number', required: true },
+ { key: 'is_reattempt_allowed', title: 'Is Reattempt allowed', type: 'yesno', required: true },
+ { key: 'no_of_reattempts', title: 'No of Reattempts', type: 'number', required: true },
+ { key: 'validate_evaluations', title: 'Validate Evaluations', type: 'yesno', required: true },
+ {
+ key: 'validation_type',
+ title: 'Validation Type',
+ type: 'select',
+ required: true,
+ values: { none: 'None', warning: 'Warning', warning_for_top: 'Warning for top', stop: 'Stop', stop_for_top: 'Stop for top' },
+ },
+ {
+ key: 'additional_evaluators',
+ title: 'Additional Evaluators',
+ type: 'checkbox',
+ required: true,
+ values: { level_1_manager: '1st line manager', level_2_manager: '2nd line manager', random_manager: 'Random Manager' },
+ },
+ { key: 'response_roles', title: 'Accept Response from Roles', type: 'multiselect', required: true, values: roles },
+ { key: 'show_pass_fail_icon', title: 'Show Pass/Fail icons on Responses', type: 'yesno', required: true, fullwidth: true },
+ { key: 'pass_icon', title: 'Pass Icon', type: 'image', required: true },
+ { key: 'fail_icon', title: 'Fail Icon', type: 'image', required: true },
+ ];
+ },
+ beforeForm: (item) => ({
+ ...item,
+ groups: splitHash(item.groups),
+ evaluators: splitHash(item.evaluators),
+ observers: splitHash(item.observers),
+ response_roles: splitHash(item.response_roles),
+ evaluation_parameters: splitHash(item.evaluation_parameters),
+ response_types: splitHash(item.response_types),
+ response_sources: splitHash(item.response_sources),
+ additional_evaluators: splitHash(item.additional_evaluators),
+ limits_video: item.limits_video ?? item.limits?.video ?? 0,
+ limits_audio: item.limits_audio ?? item.limits?.audio ?? 0,
+ limits_size: item.limits_size ?? item.limits?.size ?? 0,
+ }),
+ beforeSave: (data) => {
+ const payload = { ...data };
+ payload.groups = joinHash(payload.groups);
+ payload.evaluators = joinHash(payload.evaluators);
+ payload.observers = joinHash(payload.observers);
+ payload.response_roles = joinHash(payload.response_roles);
+ payload.evaluation_parameters = joinHash(payload.evaluation_parameters);
+ payload.response_types = Array.isArray(payload.response_types) ? payload.response_types : splitHash(payload.response_types);
+ payload.response_sources = Array.isArray(payload.response_sources) ? payload.response_sources : splitHash(payload.response_sources);
+ payload.additional_evaluators = Array.isArray(payload.additional_evaluators)
+ ? payload.additional_evaluators
+ : splitHash(payload.additional_evaluators);
+ payload.limits = { video: payload.limits_video, audio: payload.limits_audio, size: payload.limits_size };
+ delete payload.limits_video;
+ delete payload.limits_audio;
+ delete payload.limits_size;
+ return payload;
+ },
+};
+
+export default detailingcontests;
diff --git a/src/modules/formentitys.js b/src/modules/formentitys.js
index 5870ec5..2540da1 100644
--- a/src/modules/formentitys.js
+++ b/src/modules/formentitys.js
@@ -48,6 +48,7 @@ const formentitys = {
'edit',
'delete',
{ name: 'questions', to: (row) => `/formquestions?id=${row.id}`, title: 'Questions' },
+ { name: 'personalizations', to: (row) => `/formpersonalizations?id=${row.id}`, title: 'Personalizations' },
],
},
{ key: 'title', title: 'Title', type: 'text' },
diff --git a/src/modules/formpersonalizations.js b/src/modules/formpersonalizations.js
new file mode 100644
index 0000000..b24b361
--- /dev/null
+++ b/src/modules/formpersonalizations.js
@@ -0,0 +1,49 @@
+import { signedGet, isSuccess } from '@/api/client';
+import { splitHash } from '@/helpers/metric';
+
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const formpersonalizations = {
+ service: 'form',
+ module: 'personalization',
+ title: 'Form Personalization',
+ subtitle: 'Data Gathering',
+ baseUrl: '/formpersonalization',
+ listPath: '/formpersonalizations',
+ missingParentMessage: 'Form not found',
+ parentParam: { urlKey: 'id', apiKey: 'entity_id' },
+ parentField: { urlKey: 'id', field: 'entity_id' },
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'icon', title: 'Icon', type: 'image' },
+ { key: 'title', title: 'Question', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ listButtons: [
+ { title: 'Add New', to: '/formpersonalization/addnew' },
+ { title: 'Back to Forms', to: '/formentitys' },
+ ],
+ formFields: async ({ item } = {}) => {
+ const entityId = item?.entity_id || '';
+ const questions = {};
+ if (entityId) {
+ const response = await signedGet('form', 'formquestion/get_all', { entity_id: entityId });
+ (isSuccess(response) ? response.data?.items || [] : []).forEach((q) => {
+ if (q?.id) questions[q.id] = q.question || q.title || q.id;
+ });
+ }
+ return [
+ { key: 'title', title: 'Title', type: 'text', required: true, fullwidth: true },
+ { key: 'questions', title: 'Select 1 or Multiple questions (to be included in this personalization)', type: 'checkbox', values: questions, required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'icon', title: 'Icon', type: 'image', required: true },
+ ];
+ },
+ beforeForm: (item, _mode, ctx) => ({
+ ...item,
+ entity_id: item.entity_id || ctx?.searchParams?.get('id') || '',
+ questions: splitHash(item.questions),
+ }),
+};
+
+export default formpersonalizations;
diff --git a/src/modules/formproducts.js b/src/modules/formproducts.js
new file mode 100644
index 0000000..2a05471
--- /dev/null
+++ b/src/modules/formproducts.js
@@ -0,0 +1,41 @@
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const formproducts = {
+ service: 'form',
+ module: 'formproduct',
+ title: 'Form Products',
+ subtitle: 'Data Gathering',
+ baseUrl: '/formproduct',
+ listPath: '/formproducts',
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'name', title: 'Name', type: 'text' },
+ { key: 'code', title: 'Code', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: () => [
+ { key: 'name', title: 'Name', type: 'text', required: true },
+ { key: 'code', title: 'Code', type: 'text', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'skus', title: 'Skus', type: 'mapping', fullwidth: true },
+ ],
+ beforeForm: (item) => ({
+ ...item,
+ skus: Array.isArray(item.skus)
+ ? item.skus.map((sku) => ({
+ value: sku.name || sku.value || '',
+ display: sku.code || sku.display || '',
+ }))
+ : [],
+ }),
+ beforeSave: (data) => ({
+ ...data,
+ skus: (Array.isArray(data.skus) ? data.skus : []).map((row) => ({
+ name: row.value || row.name || '',
+ code: row.display || row.code || '',
+ competitors: Array.isArray(row.competitors) ? row.competitors : String(row.competitors || '').split(',').map((part) => part.trim()).filter(Boolean),
+ })),
+ }),
+};
+
+export default formproducts;
diff --git a/src/modules/leaderboards.js b/src/modules/leaderboards.js
index 97fd655..7a4c2a6 100644
--- a/src/modules/leaderboards.js
+++ b/src/modules/leaderboards.js
@@ -24,7 +24,7 @@ const leaderboards = {
baseUrl: '/leaderboard',
listPath: '/leaderboards',
listFields: [
- { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete', { name: 'report', to: (row) => `/leaderboard/dashboard?id=${row.id}`, title: 'Dashboard' }] },
{ key: 'name', title: 'Name', type: 'text' },
{ key: 'sort_order', title: 'Sort Order', type: 'number' },
{ key: 'visible_roles_label', title: 'Visible Roles', type: 'text' },
diff --git a/src/modules/lessons.js b/src/modules/lessons.js
new file mode 100644
index 0000000..5ce3998
--- /dev/null
+++ b/src/modules/lessons.js
@@ -0,0 +1,55 @@
+import { signedGet, isSuccess } from '@/api/client';
+
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const lessons = {
+ service: 'lms',
+ module: 'lessons',
+ title: 'Lesson',
+ subtitle: 'LMS',
+ baseUrl: '/lesson',
+ listPath: '/lessons',
+ missingParentMessage: 'Course not found',
+ parentParam: { urlKey: 'id', apiKey: 'course_id' },
+ parentField: { urlKey: 'id', field: 'course_id' },
+ listFields: [
+ {
+ key: 'actions',
+ title: 'Actions',
+ buttons: [
+ 'edit',
+ 'delete',
+ { name: 'articles', to: (row) => `/articles?id=${row.id}&course_id=${row.course_id || ''}`, title: 'Articles' },
+ ],
+ },
+ { key: 'title', title: 'Title', type: 'text' },
+ { key: 'description', title: 'Description', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ listButtons: [
+ { title: 'Add New', to: '/lesson/addnew' },
+ { title: 'Back to Courses', to: '/courses' },
+ ],
+ mapItems: (items, _data, ctx) =>
+ items.map((item) => ({
+ ...item,
+ course_id: item.course_id || ctx?.parentValue || '',
+ })),
+ formFields: async () => {
+ const response = await signedGet('lms', 'courses/get_all');
+ const courses = {};
+ (isSuccess(response) ? response.data?.items || [] : []).forEach((item) => {
+ if (item?.id) courses[item.id] = item.title || item.id;
+ });
+ return [
+ { key: 'course_id', title: 'Select Course', type: 'select', required: true, values: courses },
+ { key: 'title', title: 'Title', type: 'text', required: true },
+ { key: 'description', title: 'Description', type: 'text', required: true },
+ { key: 'sort_order', title: 'Sequence Of Lesson', type: 'number', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'icon', title: 'Icon', type: 'file', required: true },
+ ];
+ },
+};
+
+export default lessons;
diff --git a/src/modules/lmsquizs.js b/src/modules/lmsquizs.js
new file mode 100644
index 0000000..87c2df6
--- /dev/null
+++ b/src/modules/lmsquizs.js
@@ -0,0 +1,29 @@
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const lmsquizs = {
+ service: 'lms',
+ module: 'lmsquiz',
+ title: 'LMS Quiz',
+ subtitle: 'LMS',
+ baseUrl: '/lmsquiz',
+ listPath: '/lmsquizs',
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'title', title: 'Name', type: 'text' },
+ { key: 'show_score', title: 'Show Score', type: 'text' },
+ { key: 'show_answers', title: 'Show Answers', type: 'text' },
+ { key: 'number_attempts', title: 'No. of Attempts', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: () => [
+ { key: 'title', title: 'Name', type: 'text', required: true, fullwidth: true },
+ { key: 'show_score', title: 'Show Score', type: 'select', required: true, values: { Yes: 'Yes', No: 'No' } },
+ { key: 'show_answers', title: 'Show Answers', type: 'select', required: true, values: { Yes: 'Yes', No: 'No' } },
+ { key: 'limit_attempts', title: 'Limit Attempts', type: 'select', required: true, values: { Yes: 'Yes', No: 'No' } },
+ { key: 'number_attempts', title: 'No. of attempts', type: 'number', required: true },
+ { key: 'passing_percent', title: 'Passing Percentage', type: 'number', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ ],
+};
+
+export default lmsquizs;
diff --git a/src/modules/productattributes.js b/src/modules/productattributes.js
new file mode 100644
index 0000000..f4deb9d
--- /dev/null
+++ b/src/modules/productattributes.js
@@ -0,0 +1,35 @@
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+const OPTION_TYPES = ['select', 'checkbox', 'radio'];
+
+const productattributes = {
+ service: 'catalog',
+ module: 'productattribute',
+ title: 'Product Attributes',
+ subtitle: 'Catalog',
+ baseUrl: '/productattribute',
+ listPath: '/productattributes',
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'attribute', title: 'Attribute', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: () => [
+ { key: 'attribute', title: 'Attribute', type: 'text', required: true },
+ { key: 'code', title: 'Code (small caps without spaces and special chars except "_")', type: 'text', required: true },
+ { key: 'required', title: 'Is Required', type: 'yesno', required: true },
+ { key: 'type', title: 'Attribute Type', type: 'select', required: true, values: { text: 'Text', number: 'Number', date: 'Date', email: 'Email', url: 'Url', select: 'Dropdown', checkbox: 'Checkbox', radio: 'Radio' } },
+ { key: 'sort_order', title: 'Sort Order', type: 'number', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'options', title: 'Options', type: 'mapping', fullwidth: true, showIf: { type: OPTION_TYPES } },
+ ],
+ beforeForm: (item) => ({
+ ...item,
+ options: Array.isArray(item.options) ? item.options : [],
+ }),
+ beforeSave: (data) => ({
+ ...data,
+ options: OPTION_TYPES.includes(data.type) ? data.options || [] : [],
+ }),
+};
+
+export default productattributes;
diff --git a/src/modules/products.js b/src/modules/products.js
new file mode 100644
index 0000000..9eee022
--- /dev/null
+++ b/src/modules/products.js
@@ -0,0 +1,59 @@
+import { signedGet, isSuccess } from '@/api/client';
+import { dropdownMap } from '@/api/dropdowns';
+
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+function optionValues(item) {
+ const values = {};
+ (item.options || []).forEach((option) => {
+ if (option && option.value !== undefined && option.value !== null) {
+ values[option.value] = option.display ?? option.value;
+ }
+ });
+ return values;
+}
+
+const products = {
+ service: 'catalog',
+ module: 'product',
+ title: 'Product',
+ subtitle: 'Catalog',
+ baseUrl: '/product',
+ listPath: '/products',
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'name', title: 'Name', type: 'text' },
+ { key: 'sku', title: 'SKU', type: 'text' },
+ { key: 'division', title: 'Division', type: 'text' },
+ { key: 'company', title: 'Company', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: async () => {
+ const [attrRes, brands] = await Promise.all([
+ signedGet('catalog', 'productattribute/get_all'),
+ dropdownMap('catalog', 'brand', (item) => item.name),
+ ]);
+ const fields = [
+ { key: 'name', title: 'Name', type: 'text', required: true },
+ { key: 'sku', title: 'SKU', type: 'text', required: true },
+ { key: 'division', title: 'Division', type: 'text', required: true },
+ { key: 'company', title: 'Company', type: 'text' },
+ { key: 'brand', title: 'Brand', type: 'select', required: true, values: brands },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ ];
+ const attributes = isSuccess(attrRes) ? attrRes.data?.items || [] : [];
+ attributes.forEach((item) => {
+ const needsOptions = ['select', 'radio', 'checkbox'].includes(item.type);
+ fields.push({
+ key: item.code,
+ title: item.attribute,
+ type: item.type || 'text',
+ required: item.required === 'yes',
+ values: needsOptions ? optionValues(item) : {},
+ });
+ });
+ return fields;
+ },
+};
+
+export default products;
diff --git a/src/modules/rcpacompetitors.js b/src/modules/rcpacompetitors.js
new file mode 100644
index 0000000..2cf076d
--- /dev/null
+++ b/src/modules/rcpacompetitors.js
@@ -0,0 +1,45 @@
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const rcpacompetitors = {
+ service: 'rcpa',
+ module: 'competitor',
+ title: 'RCPA Competitor',
+ subtitle: 'RCPA',
+ baseUrl: '/rcpacompetitor',
+ listPath: '/rcpacompetitors',
+ missingParentMessage: 'SKU not found',
+ parentParam: { urlKey: 'id', apiKey: 'sku_id' },
+ parentField: { urlKey: 'id', field: 'sku_id' },
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'title', title: 'Title', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ listButtons: [
+ { title: 'Add New', to: '/rcpacompetitor/addnew' },
+ { title: 'Back to Products', to: '/rcpaproducts' },
+ ],
+ formFields: () => [
+ { key: 'company', title: 'Company', type: 'text', required: true },
+ { key: 'brand', title: 'Brand', type: 'text', required: true },
+ { key: 'title', title: 'Product Name', type: 'text', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'description', title: 'Description', type: 'textarea', required: true, fullwidth: true },
+ { key: 'mrp_value', title: 'MRP Value', type: 'number', required: true },
+ { key: 'pts_value', title: 'PTS Value', type: 'number', required: true },
+ { key: 'ptr_value', title: 'PTR Value', type: 'number', required: true },
+ { key: 'nrv_value', title: 'NRV Value', type: 'number', required: true },
+ { key: 'sort_order', title: 'Sort Order', type: 'number', required: true },
+ ],
+ beforeForm: (item, _mode, ctx) => ({
+ ...item,
+ product_id: item.product_id || ctx?.searchParams?.get('product_id') || '',
+ }),
+ beforeSave: (data) => {
+ const payload = { ...data };
+ if (!payload.product_id) delete payload.product_id;
+ return payload;
+ },
+};
+
+export default rcpacompetitors;
diff --git a/src/modules/rcpaforms.js b/src/modules/rcpaforms.js
new file mode 100644
index 0000000..cf54750
--- /dev/null
+++ b/src/modules/rcpaforms.js
@@ -0,0 +1,85 @@
+import { signedGet, isSuccess } from '@/api/client';
+import { dropdownMap } from '@/api/dropdowns';
+import { splitHash } from '@/helpers/metric';
+
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+function asArray(value) {
+ if (Array.isArray(value)) return value.filter((part) => part !== null && part !== undefined && part !== '');
+ return splitHash(value);
+}
+
+function yesno(value, fallback = 'no') {
+ return value ? String(value).toLowerCase() : fallback;
+}
+
+const rcpaforms = {
+ service: 'rcpa',
+ module: 'form',
+ title: 'RCPA Form',
+ subtitle: 'RCPA',
+ baseUrl: '/rcpaform',
+ listPath: '/rcpaforms',
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'title', title: 'Title', type: 'text' },
+ { key: 'workflow', title: 'Workflow', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: async () => {
+ const [usersRes, roles, contactRes] = await Promise.all([
+ signedGet('ums', 'users/dropdown'),
+ dropdownMap('ums', 'roles', (item) => item.name),
+ signedGet('contact', 'contactattribute/get_all'),
+ ]);
+ const approvers = { l1manager: 'L1 Manager', l2manager: 'L2 Manager' };
+ const userItems = isSuccess(usersRes) ? usersRes.data?.items || [] : [];
+ userItems.forEach((user) => {
+ if (!user?.id) return;
+ approvers[user.id] = `${user.firstname || ''} ${user.lastname || ''} (${user.employee_code || ''})`.trim();
+ });
+ const contactAttributes = {};
+ (isSuccess(contactRes) ? contactRes.data?.items || [] : []).forEach((attr) => {
+ if (attr?.code) contactAttributes[attr.code] = attr.attribute || attr.code;
+ });
+ return [
+ { key: 'title', title: 'Title', type: 'text', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'metric_submission', title: 'Metric Submission', type: 'yesno', required: true },
+ { key: 'metric_authkey', title: 'Auth Key', type: 'text', required: true },
+ { key: 'manager_de_roles', title: 'Manager Data Entry Roles', type: 'multiselect', values: roles },
+ { key: 'contact_attributes', title: 'Include Doctor Attributes in Export', type: 'multiselect', values: contactAttributes },
+ { key: 'self_notification', title: 'Self Email Notification', type: 'yesno', required: true, fullwidth: true },
+ { key: 'manager1_email_notification', title: '1st Level Manager Email Notification', type: 'yesno', required: true },
+ { key: 'manager1_push_notification', title: '1st Level Manager Mobile Notification', type: 'yesno', required: true },
+ { key: 'manager2_email_notification', title: '2nd Level Manager Email Notification', type: 'yesno', required: true },
+ { key: 'manager2_push_notification', title: '2nd Level Manager Mobile Notification', type: 'yesno', required: true },
+ { key: 'roles_email_notification', title: 'Email Notification to Roles', type: 'multiselect', values: roles },
+ { key: 'roles_push_notification', title: 'Push Notification to Roles', type: 'multiselect', values: roles },
+ { key: 'allow_non_hierarchy_submission', title: 'Allow Non Hierarchy Submission', type: 'yesno', required: true },
+ { key: 'workflow', title: 'Enable Workflow', type: 'yesno', required: true },
+ { key: 'workflow_type', title: 'Workflow type', type: 'select', required: true, values: { single: 'Single Persoon', multiple: 'Multiple person', sequencial: 'Sequencial' } },
+ { key: 'approver_can_edit', title: 'Approver can edit entry', type: 'yesno', required: true },
+ { key: 'is_personalized', title: 'Is Personalization Needed', type: 'yesno', required: true },
+ { key: 'allow_delete_by_user', title: 'Allow Submission Delete by User', type: 'yesno', required: true },
+ { key: 'export_fields', title: 'Export Fields', type: 'textarea', fullwidth: true },
+ { key: 'workflow_approvers', title: 'Metric', type: 'mapping', required: true, approvers, fullwidth: true },
+ ];
+ },
+ beforeForm: (item) => ({
+ ...item,
+ self_notification: yesno(item.self_notification),
+ manager1_email_notification: yesno(item.manager1_email_notification),
+ manager1_push_notification: yesno(item.manager1_push_notification),
+ manager2_email_notification: yesno(item.manager2_email_notification),
+ manager2_push_notification: yesno(item.manager2_push_notification),
+ export_fields: item.export_fields || '',
+ manager_de_roles: asArray(item.manager_de_roles),
+ contact_attributes: asArray(item.contact_attributes),
+ roles_email_notification: asArray(item.roles_email_notification),
+ roles_push_notification: asArray(item.roles_push_notification),
+ workflow_approvers: Array.isArray(item.workflow_approvers) && item.workflow_approvers.length ? item.workflow_approvers : ['l1manager'],
+ }),
+};
+
+export default rcpaforms;
diff --git a/src/modules/rcpaproductattributes.js b/src/modules/rcpaproductattributes.js
new file mode 100644
index 0000000..24e2212
--- /dev/null
+++ b/src/modules/rcpaproductattributes.js
@@ -0,0 +1,85 @@
+import { signedGet, isSuccess } from '@/api/client';
+import { dropdownMap } from '@/api/dropdowns';
+import { splitHash } from '@/helpers/metric';
+
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+function asArray(value) {
+ if (Array.isArray(value)) return value.filter((part) => part !== null && part !== undefined && part !== '');
+ return splitHash(value);
+}
+
+function yesno(value, fallback = 'no') {
+ return value ? String(value).toLowerCase() : fallback;
+}
+
+const rcpaproductattributes = {
+ service: 'rcpa',
+ module: 'attribute',
+ title: 'RCPA Product Attribute',
+ subtitle: 'RCPA',
+ baseUrl: '/rcpaproductattribute',
+ listPath: '/rcpaproductattributes',
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'title', title: 'Title', type: 'text' },
+ { key: 'workflow', title: 'Workflow', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: async () => {
+ const [usersRes, roles, contactRes] = await Promise.all([
+ signedGet('ums', 'users/dropdown'),
+ dropdownMap('ums', 'roles', (item) => item.name),
+ signedGet('contact', 'contactattribute/get_all'),
+ ]);
+ const approvers = { l1manager: 'L1 Manager', l2manager: 'L2 Manager' };
+ const userItems = isSuccess(usersRes) ? usersRes.data?.items || [] : [];
+ userItems.forEach((user) => {
+ if (!user?.id) return;
+ approvers[user.id] = `${user.firstname || ''} ${user.lastname || ''} (${user.employee_code || ''})`.trim();
+ });
+ const contactAttributes = {};
+ (isSuccess(contactRes) ? contactRes.data?.items || [] : []).forEach((attr) => {
+ if (attr?.code) contactAttributes[attr.code] = attr.attribute || attr.code;
+ });
+ return [
+ { key: 'title', title: 'Title', type: 'text', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'metric_submission', title: 'Metric Submission', type: 'yesno', required: true },
+ { key: 'metric_authkey', title: 'Auth Key', type: 'text', required: true },
+ { key: 'manager_de_roles', title: 'Manager Data Entry Roles', type: 'multiselect', values: roles },
+ { key: 'contact_attributes', title: 'Include Doctor Attributes in Export', type: 'multiselect', values: contactAttributes },
+ { key: 'self_notification', title: 'Self Email Notification', type: 'yesno', required: true, fullwidth: true },
+ { key: 'manager1_email_notification', title: '1st Level Manager Email Notification', type: 'yesno', required: true },
+ { key: 'manager1_push_notification', title: '1st Level Manager Mobile Notification', type: 'yesno', required: true },
+ { key: 'manager2_email_notification', title: '2nd Level Manager Email Notification', type: 'yesno', required: true },
+ { key: 'manager2_push_notification', title: '2nd Level Manager Mobile Notification', type: 'yesno', required: true },
+ { key: 'roles_email_notification', title: 'Email Notification to Roles', type: 'multiselect', values: roles },
+ { key: 'roles_push_notification', title: 'Push Notification to Roles', type: 'multiselect', values: roles },
+ { key: 'allow_non_hierarchy_submission', title: 'Allow Non Hierarchy Submission', type: 'yesno', required: true },
+ { key: 'workflow', title: 'Enable Workflow', type: 'yesno', required: true },
+ { key: 'workflow_type', title: 'Workflow type', type: 'select', required: true, values: { single: 'Single Persoon', multiple: 'Multiple person', sequencial: 'Sequencial' } },
+ { key: 'approver_can_edit', title: 'Approver can edit entry', type: 'yesno', required: true },
+ { key: 'is_personalized', title: 'Is Personalization Needed', type: 'yesno', required: true },
+ { key: 'allow_delete_by_user', title: 'Allow Submission Delete by User', type: 'yesno', required: true },
+ { key: 'export_fields', title: 'Export Fields', type: 'textarea', fullwidth: true },
+ { key: 'workflow_approvers', title: 'Metric', type: 'mapping', required: true, approvers, fullwidth: true },
+ ];
+ },
+ beforeForm: (item) => ({
+ ...item,
+ self_notification: yesno(item.self_notification),
+ manager1_email_notification: yesno(item.manager1_email_notification),
+ manager1_push_notification: yesno(item.manager1_push_notification),
+ manager2_email_notification: yesno(item.manager2_email_notification),
+ manager2_push_notification: yesno(item.manager2_push_notification),
+ export_fields: item.export_fields || '',
+ manager_de_roles: asArray(item.manager_de_roles),
+ contact_attributes: asArray(item.contact_attributes),
+ roles_email_notification: asArray(item.roles_email_notification),
+ roles_push_notification: asArray(item.roles_push_notification),
+ workflow_approvers: Array.isArray(item.workflow_approvers) && item.workflow_approvers.length ? item.workflow_approvers : ['l1manager'],
+ }),
+};
+
+export default rcpaproductattributes;
diff --git a/src/modules/rcpaproducts.js b/src/modules/rcpaproducts.js
new file mode 100644
index 0000000..a0b0089
--- /dev/null
+++ b/src/modules/rcpaproducts.js
@@ -0,0 +1,31 @@
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const rcpaproducts = {
+ service: 'rcpa',
+ module: 'product',
+ title: 'RCPA Product',
+ subtitle: 'RCPA',
+ baseUrl: '/rcpaproduct',
+ listPath: '/rcpaproducts',
+ listFields: [
+ {
+ key: 'actions',
+ title: 'Actions',
+ buttons: [
+ 'edit',
+ 'delete',
+ { name: 'skus', to: (row) => `/rcpaskus?id=${row.id}`, title: 'SKUs' },
+ ],
+ },
+ { key: 'title', title: 'Title', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: () => [
+ { key: 'title', title: 'Title', type: 'text', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'description', title: 'Description', type: 'textarea', required: true, fullwidth: true },
+ { key: 'sort_order', title: 'Sort Order', type: 'number', required: true },
+ ],
+};
+
+export default rcpaproducts;
diff --git a/src/modules/rcpaskus.js b/src/modules/rcpaskus.js
new file mode 100644
index 0000000..7a8a8cf
--- /dev/null
+++ b/src/modules/rcpaskus.js
@@ -0,0 +1,47 @@
+const STATUS = { active: 'Active', disabled: 'Disabled' };
+
+const rcpaskus = {
+ service: 'rcpa',
+ module: 'sku',
+ title: 'RCPA Product SKU',
+ subtitle: 'RCPA',
+ baseUrl: '/rcpasku',
+ listPath: '/rcpaskus',
+ missingParentMessage: 'Product not found',
+ parentParam: { urlKey: 'id', apiKey: 'product_id' },
+ parentField: { urlKey: 'id', field: 'product_id' },
+ listFields: [
+ {
+ key: 'actions',
+ title: 'Actions',
+ buttons: [
+ 'edit',
+ 'delete',
+ { name: 'competitions', to: (row) => `/rcpacompetitors?id=${row.id}&product_id=${row.product_id || ''}`, title: 'Competitors' },
+ ],
+ },
+ { key: 'title', title: 'Title', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ listButtons: [
+ { title: 'Add New', to: '/rcpasku/addnew' },
+ { title: 'Back to Products', to: '/rcpaproducts' },
+ ],
+ mapItems: (items, _data, ctx) =>
+ items.map((item) => ({
+ ...item,
+ product_id: item.product_id || ctx?.parentValue || '',
+ })),
+ formFields: () => [
+ { key: 'title', title: 'Title', type: 'text', required: true },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
+ { key: 'description', title: 'Description', type: 'textarea', required: true, fullwidth: true },
+ { key: 'mrp_value', title: 'MRP Value', type: 'number', required: true },
+ { key: 'pts_value', title: 'PTS Value', type: 'number', required: true },
+ { key: 'ptr_value', title: 'PTR Value', type: 'number', required: true },
+ { key: 'nrv_value', title: 'NRV Value', type: 'number', required: true },
+ { key: 'sort_order', title: 'Sort Order', type: 'number', required: true },
+ ],
+};
+
+export default rcpaskus;
diff --git a/src/modules/sessions.js b/src/modules/sessions.js
new file mode 100644
index 0000000..08e7aa2
--- /dev/null
+++ b/src/modules/sessions.js
@@ -0,0 +1,95 @@
+import { dropdownMap } from '@/api/dropdowns';
+
+const sessions = {
+ service: 'events',
+ module: 'session',
+ title: 'Session',
+ subtitle: 'Events',
+ baseUrl: '/session',
+ listPath: '/sessions',
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'title', title: 'Session Title', type: 'text' },
+ { key: 'session_type', title: 'Type', type: 'select' },
+ { key: 'track_id', title: 'Track', type: 'text' },
+ { key: 'presenter', title: 'Presenter', type: 'text' },
+ { key: 'start_time', title: 'Start Time', type: 'datetime' },
+ { key: 'end_time', title: 'End Time', type: 'datetime' },
+ { key: 'room', title: 'Room', type: 'text' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: async () => {
+ const tracks = await dropdownMap('events', 'track', (item) => item.name);
+ return [
+ { key: 'track_id', title: 'Track', type: 'select', required: true, values: tracks },
+ { key: 'title', title: 'Session Title', type: 'text', required: true },
+ { key: 'description', title: 'Description', type: 'textarea' },
+ {
+ key: 'session_type',
+ title: 'Session Type',
+ type: 'select',
+ required: true,
+ values: {
+ presentation: 'Presentation',
+ workshop: 'Workshop',
+ panel: 'Panel Discussion',
+ networking: 'Networking',
+ break: 'Break',
+ lunch: 'Lunch',
+ keynote: 'Keynote',
+ demo: 'Demo',
+ qna: 'Q&A Session',
+ other: 'Other',
+ },
+ },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: { scheduled: 'Scheduled', live: 'Live', completed: 'Completed', cancelled: 'Cancelled' } },
+ { key: 'start_time', title: 'Start Time', type: 'datetime', required: true },
+ { key: 'end_time', title: 'End Time', type: 'datetime', required: true },
+ { key: 'duration', title: 'Duration (minutes)', type: 'number', default: 60 },
+ { key: 'presenter', title: 'Presenter Name', type: 'text' },
+ { key: 'presenter_title', title: 'Presenter Title', type: 'text' },
+ { key: 'presenter_bio', title: 'Presenter Bio', type: 'textarea' },
+ { key: 'presenter_avatar', title: 'Presenter Avatar', type: 'image' },
+ { key: 'max_attendees', title: 'Max Attendees', type: 'number', default: 0 },
+ { key: 'is_interactive', title: 'Interactive Session', type: 'yesno', required: true },
+ { key: 'requires_registration', title: 'Requires Registration', type: 'yesno', required: true },
+ { key: 'is_recorded', title: 'Record Session', type: 'yesno', required: true },
+ { key: 'room', title: 'Room/Venue', type: 'text' },
+ { key: 'virtual_link', title: 'Virtual Link', type: 'text' },
+ { key: 'presentation_url', title: 'Presentation URL', type: 'text' },
+ { key: 'tags', title: 'Tags', type: 'text', placeholder: 'Enter tags separated by commas' },
+ ];
+ },
+ beforeForm: (item) => {
+ const presenter = item.presenter && typeof item.presenter === 'object' ? item.presenter : {};
+ return {
+ ...item,
+ presenter: typeof item.presenter === 'string' ? item.presenter : presenter.name || '',
+ presenter_title: item.presenter_title || presenter.title || '',
+ presenter_bio: item.presenter_bio || presenter.bio || '',
+ presenter_avatar: item.presenter_avatar || presenter.avatar || '',
+ tags: Array.isArray(item.tags) ? item.tags.join(', ') : item.tags || '',
+ };
+ },
+ beforeSave: (data) => {
+ const payload = { ...data };
+ payload.tags = String(payload.tags || '')
+ .split(',')
+ .map((part) => part.trim())
+ .filter(Boolean);
+ payload.max_attendees = payload.max_attendees || 0;
+ payload.duration = payload.duration || 60;
+ payload.presenter = {
+ name: payload.presenter || '',
+ title: payload.presenter_title || '',
+ bio: payload.presenter_bio || '',
+ avatar: payload.presenter_avatar || '',
+ };
+ delete payload.presenter_title;
+ delete payload.presenter_bio;
+ delete payload.presenter_avatar;
+ return payload;
+ },
+};
+
+export default sessions;
diff --git a/src/modules/teams.js b/src/modules/teams.js
index 4a28c77..0edd236 100644
--- a/src/modules/teams.js
+++ b/src/modules/teams.js
@@ -1,7 +1,8 @@
import { dropdownMap } from '@/api/dropdowns';
const teams = {
- service: 'ums',
+ service: 'detailing',
+ getOneService: 'ums',
module: 'team',
title: 'Teams',
baseUrl: '/team',
diff --git a/src/modules/tracks.js b/src/modules/tracks.js
new file mode 100644
index 0000000..d824ea6
--- /dev/null
+++ b/src/modules/tracks.js
@@ -0,0 +1,52 @@
+import { dropdownMap } from '@/api/dropdowns';
+
+const tracks = {
+ service: 'events',
+ module: 'track',
+ title: 'Track',
+ subtitle: 'Events',
+ baseUrl: '/track',
+ listPath: '/tracks',
+ listFields: [
+ { key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
+ { key: 'name', title: 'Track Name', type: 'text' },
+ { key: 'track_type', title: 'Type', type: 'select' },
+ { key: 'event_id', title: 'Event', type: 'text' },
+ { key: 'room', title: 'Room', type: 'text' },
+ { key: 'max_attendees', title: 'Max Attendees', type: 'number' },
+ { key: 'order', title: 'Order', type: 'number' },
+ { key: 'status', title: 'Status', type: 'select' },
+ ],
+ formFields: async () => {
+ const events = await dropdownMap('events', 'event', (item) => item.name);
+ return [
+ { key: 'event_id', title: 'Event', type: 'select', required: true, values: events },
+ { key: 'name', title: 'Track Name', type: 'text', required: true },
+ { key: 'description', title: 'Description', type: 'textarea' },
+ {
+ key: 'track_type',
+ title: 'Track Type',
+ type: 'select',
+ required: true,
+ values: {
+ general: 'General',
+ technical: 'Technical',
+ business: 'Business',
+ networking: 'Networking',
+ workshop: 'Workshop',
+ panel: 'Panel Discussion',
+ keynote: 'Keynote',
+ other: 'Other',
+ },
+ },
+ { key: 'status', title: 'Status', type: 'select', required: true, values: { active: 'Active', inactive: 'Inactive', completed: 'Completed' } },
+ { key: 'max_attendees', title: 'Max Attendees', type: 'number', default: 0 },
+ { key: 'is_parallel', title: 'Parallel Track', type: 'yesno', required: true },
+ { key: 'order', title: 'Display Order', type: 'number', default: 0 },
+ { key: 'room', title: 'Room/Venue', type: 'text' },
+ { key: 'virtual_link', title: 'Virtual Link', type: 'text' },
+ ];
+ },
+};
+
+export default tracks;
diff --git a/src/modules/users.js b/src/modules/users.js
index 8c885d6..8dc1fc0 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -96,6 +96,33 @@ const users = {
}
return fields;
},
+ beforeForm: (item) => ({
+ ...item,
+ roles: asIdList(item?.roles ?? item?.role),
+ channels: asIdList(item?.channels ?? item?.channel),
+ }),
+ beforeSave: (data) => {
+ const payload = { ...data };
+ payload.roles = asIdList(payload.roles);
+ payload.channels = asIdList(payload.channels);
+ if (!payload.roles.length) delete payload.roles;
+ if (!payload.channels.length) delete payload.channels;
+ return payload;
+ },
};
+function asIdList(value) {
+ if (value == null || value === '') return [];
+ if (Array.isArray(value)) return value.flatMap(asIdList);
+ if (typeof value === 'object') {
+ if (value.id != null && value.id !== '') return [String(value.id)];
+ if (value.value != null && value.value !== '') return [String(value.value)];
+ return Object.values(value).flatMap(asIdList);
+ }
+ return String(value)
+ .split(/[#~,]/)
+ .map((part) => part.trim())
+ .filter(Boolean);
+}
+
export default users;
diff --git a/src/routes/index.jsx b/src/routes/index.jsx
index 1747142..3e47784 100644
--- a/src/routes/index.jsx
+++ b/src/routes/index.jsx
@@ -44,6 +44,27 @@ import formexports from '@/modules/formexports';
import reminders from '@/modules/reminders';
import leaderboards from '@/modules/leaderboards';
import workflows from '@/modules/workflows';
+import detailingcontests from '@/modules/detailingcontests';
+import contestparameters from '@/modules/contestparameters';
+import detailingcontestresponses from '@/modules/detailingcontestresponses';
+import contactlists from '@/modules/contactlists';
+import rcpaproducts from '@/modules/rcpaproducts';
+import rcpaskus from '@/modules/rcpaskus';
+import rcpacompetitors from '@/modules/rcpacompetitors';
+import rcpaforms from '@/modules/rcpaforms';
+import rcpaproductattributes from '@/modules/rcpaproductattributes';
+import brands from '@/modules/brands';
+import products from '@/modules/products';
+import productattributes from '@/modules/productattributes';
+import formproducts from '@/modules/formproducts';
+import formpersonalizations from '@/modules/formpersonalizations';
+import courses from '@/modules/courses';
+import lessons from '@/modules/lessons';
+import articles from '@/modules/articles';
+import articlebanks from '@/modules/articlebanks';
+import lmsquizs from '@/modules/lmsquizs';
+import sessions from '@/modules/sessions';
+import tracks from '@/modules/tracks';
const Login = lazy(() => import('@/views/auth/Login'));
const LoginOtp = lazy(() => import('@/views/auth/LoginOtp'));
@@ -63,12 +84,15 @@ const TeamPlayers = lazy(() => import('@/views/organisation/TeamPlayers'));
const MagMediaViewers = lazy(() => import('@/views/content/MagMediaViewers'));
const WorkflowEditor = lazy(() => import('@/views/workflow/Editor'));
const WorkflowMonitor = lazy(() => import('@/views/workflow/Monitor'));
+const LeaderboardDashboard = lazy(() => import('@/views/leaderboard/Dashboard'));
function moduleRoutes(config) {
const routes = [
{ path: config.listPath.replace(/^\//, ''), element:
},
- { path: `${config.baseUrl.replace(/^\//, '')}/addnew`, element:
},
];
+ if (!config.skipAdd) {
+ routes.push({ path: `${config.baseUrl.replace(/^\//, '')}/addnew`, element:
});
+ }
if (!config.skipEdit) {
routes.push({ path: `${config.baseUrl.replace(/^\//, '')}/edit`, element:
});
}
@@ -156,6 +180,28 @@ export const routes = [
...moduleRoutes(workflows),
{ path: '/workflow/editor', element:
},
{ path: '/workflow/monitor', element:
},
+ { path: '/leaderboard/dashboard', element:
},
+ ...moduleRoutes(detailingcontests),
+ ...moduleRoutes(contestparameters),
+ ...moduleRoutes(detailingcontestresponses),
+ ...moduleRoutes(contactlists),
+ ...moduleRoutes(rcpaproducts),
+ ...moduleRoutes(rcpaskus),
+ ...moduleRoutes(rcpacompetitors),
+ ...moduleRoutes(rcpaforms),
+ ...moduleRoutes(rcpaproductattributes),
+ ...moduleRoutes(brands),
+ ...moduleRoutes(products),
+ ...moduleRoutes(productattributes),
+ ...moduleRoutes(formproducts),
+ ...moduleRoutes(formpersonalizations),
+ ...moduleRoutes(courses),
+ ...moduleRoutes(lessons),
+ ...moduleRoutes(articles),
+ ...moduleRoutes(articlebanks),
+ ...moduleRoutes(lmsquizs),
+ ...moduleRoutes(sessions),
+ ...moduleRoutes(tracks),
{ path: '/metricreports', element:
},
{ path: '/metricreport/report', element:
},
{ path: '/coming-soon', element:
},
diff --git a/src/views/content/MagMediaViewers.jsx b/src/views/content/MagMediaViewers.jsx
index d4acd15..3b74707 100644
--- a/src/views/content/MagMediaViewers.jsx
+++ b/src/views/content/MagMediaViewers.jsx
@@ -4,6 +4,7 @@ import { Button, Card, Container, Spinner, Table } from 'react-bootstrap';
import { signedGet, isSuccess } from '@/api/client';
import PageBreadcrumb from '@/components/PageBreadcrumb';
import { ListTableFooter, ListTableToolbar } from '@/components/ListTableControls';
+import { EmptyTableRow } from '@/components/EmptyState';
import { downloadCsv } from '@/helpers/csv';
import { formatRoleNames, getRoleMap, loadRoleMap } from '@/helpers/roles';
@@ -137,11 +138,7 @@ export default function MagMediaViewers() {
{pageRows.length === 0 ? (
-
- |
- No viewers
- |
-
+
) : (
pageRows.map((user) => (
diff --git a/src/views/dashboard/EmptyChart.jsx b/src/views/dashboard/EmptyChart.jsx
index e31a91b..ed9b89c 100644
--- a/src/views/dashboard/EmptyChart.jsx
+++ b/src/views/dashboard/EmptyChart.jsx
@@ -1,10 +1,10 @@
-import ReactApexChart from 'react-apexcharts';
-import { CHART_COLORS } from '@/views/dashboard/helpers';
+import EmptyState from '@/components/EmptyState';
const EMPTY_COLOR = '#e9ecef';
export function emptyPieChart(label = 'No data') {
return {
+ empty: true,
type: 'pie',
height: 250,
width: 250,
@@ -23,6 +23,7 @@ export function emptyPieChart(label = 'No data') {
export function emptyRadialChart(label = 'No data', extras = {}) {
return {
+ empty: true,
type: 'radialBar',
height: 250,
series: [0],
@@ -44,20 +45,7 @@ export function emptyRadialChart(label = 'No data', extras = {}) {
};
}
-export default function EmptyChart({ type = 'radialBar', label = 'No data', height = 250 }) {
- const chart = type === 'pie' ? emptyPieChart(label) : emptyRadialChart(label);
- return (
-
- );
+export default function EmptyChart({ label = 'Not found' }) {
+ const description = !label || label === 'No data' ? 'No data to show' : label;
+ return ;
}
diff --git a/src/views/dashboard/MetricCard.jsx b/src/views/dashboard/MetricCard.jsx
index b514d4f..503de60 100644
--- a/src/views/dashboard/MetricCard.jsx
+++ b/src/views/dashboard/MetricCard.jsx
@@ -2,88 +2,12 @@ import { useMemo } from 'react';
import { Link } from 'react-router';
import { Card, Col } from 'react-bootstrap';
import ReactApexChart from 'react-apexcharts';
-import { CHART_COLORS } from '@/views/dashboard/helpers';
-import { emptyPieChart, emptyRadialChart } from '@/views/dashboard/EmptyChart';
+import { buildDashboardMetricChart } from '@/views/metricreport/buildChart';
+import { typeChartList } from '@/views/metricreport/helpers';
export default function MetricCard({ metric }) {
- const chart = useMemo(() => {
- const values = Array.isArray(metric.values) ? metric.values : [];
- const hasPieData = values.some((row) => Number(row?.value) > 0);
- const percent = Math.ceil(metric.max > 0 ? (Number(metric.value) / Number(metric.max)) * 100 : 0);
- const isGrouped = metric.type === 'grouped';
- const isVerses = metric.type === 'verses';
- const isSimple = metric.type === 'simple';
- const isTeam = metric.agg_type === 'team';
- const isIndividual = metric.agg_type === 'individual';
-
- if ((isIndividual || isTeam) && isGrouped) {
- if (!hasPieData) {
- return emptyPieChart(metric.label || 'No data');
- }
- return {
- type: 'pie',
- height: 250,
- width: 250,
- series: values.map((row) => Number(row.value) || 0),
- options: {
- chart: { type: 'pie', width: 250, animations: { enabled: true, easing: 'easeinout', speed: 2000 } },
- labels: values.map((row) => row.label),
- colors: CHART_COLORS,
- legend: { show: false },
- responsive: [{ breakpoint: 480, options: { chart: { width: 250 }, legend: { position: 'bottom' } } }],
- },
- };
- }
-
- if (isIndividual && isSimple) {
- return {
- type: 'radialBar',
- height: 250,
- series: [percent],
- options: {
- chart: { type: 'radialBar', height: 250, animations: { enabled: true, easing: 'easeinout', speed: 2000 } },
- plotOptions: { radialBar: { startAngle: -90, endAngle: 270, hollow: { size: '58%' } } },
- fill: {
- type: 'gradient',
- gradient: { shade: 'light', shadeIntensity: 0.15, inverseColors: false, opacityFrom: 1, opacityTo: 1, stops: [0, 50, 65, 91] },
- },
- stroke: { dashArray: 7 },
- colors: CHART_COLORS,
- labels: [metric.label || 'No data'],
- legend: { show: false },
- },
- };
- }
-
- if (isTeam && (isSimple || isVerses)) {
- return {
- type: 'radialBar',
- height: 250,
- series: [percent],
- options: {
- chart: { type: 'radialBar', height: 250, animations: { enabled: true, easing: 'easeinout', speed: 2000 } },
- plotOptions: {
- radialBar: {
- hollow: { size: '68%' },
- ...(isVerses
- ? { dataLabels: { name: { show: false }, value: { show: true, offsetY: 8, fontSize: '22px' } } }
- : {}),
- },
- },
- fill: {
- type: 'gradient',
- gradient: { shade: 'dark', shadeIntensity: 0.15, inverseColors: false, opacityFrom: 1, opacityTo: 1, stops: [0, 50, 65, 91] },
- },
- stroke: { dashArray: 4 },
- colors: CHART_COLORS,
- labels: [isVerses ? '' : metric.label || 'No data'],
- legend: { show: false },
- },
- };
- }
-
- return isGrouped ? emptyPieChart(metric.label || 'No data') : emptyRadialChart(metric.label || 'No data');
- }, [metric]);
+ const chart = useMemo(() => buildDashboardMetricChart(metric), [metric]);
+ const value = metric.value == null || metric.value === '' ? 0 : metric.value;
return (
@@ -104,19 +28,23 @@ export default function MetricCard({ metric }) {
{metric.label}
- {metric.value}
- {metric.type === 'verses' ? ` / ${metric.max}` : ''}
+ {value}
+ {metric.type === 'verses' ? ` / ${metric.max ?? 0}` : ''}
- {(metric.available_charts || []).map((item) => (
-
- {item.name} Chart
-
- ))}
+ {(metric.available_charts || []).map((item) => {
+ const chartName = typeChartList(item)[0] || item.chart;
+ if (!item.type || !chartName) return null;
+ return (
+
+ {item.name} Chart
+
+ );
+ })}
diff --git a/src/views/dashboard/helpers.js b/src/views/dashboard/helpers.js
index 0ef5c07..f22804e 100644
--- a/src/views/dashboard/helpers.js
+++ b/src/views/dashboard/helpers.js
@@ -46,6 +46,19 @@ export function userInitials(name) {
return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
}
+export function rankRoleId(rankRole) {
+ if (!rankRole) return '';
+ const role = rankRole.role ?? rankRole.id;
+ if (role && typeof role === 'object') return String(role.id || role.role || '');
+ return String(role || '');
+}
+
+export function rankRoleName(rankRole) {
+ if (!rankRole) return '';
+ if (rankRole.role && typeof rankRole.role === 'object') return String(rankRole.role.name || '');
+ return String(rankRole.name || '');
+}
+
export function rankRolePillLabel(title, limit, roleName) {
const label = String(title || '').trim() || String(roleName || '').trim() || 'Role';
if (/^\s*top\s+\d+/i.test(label)) return label;
diff --git a/src/views/dashboard/index.jsx b/src/views/dashboard/index.jsx
index 6211e83..b571806 100644
--- a/src/views/dashboard/index.jsx
+++ b/src/views/dashboard/index.jsx
@@ -4,6 +4,7 @@ import { Button, Card, Container, Row, Spinner } from 'react-bootstrap';
import { signedGet, isSuccess } from '@/api/client';
import PageBreadcrumb from '@/components/PageBreadcrumb';
import MetricCard from '@/views/dashboard/MetricCard';
+import EmptyState from '@/components/EmptyState';
import { resolveWidget } from '@/views/dashboard/widgets/registry';
function WidgetSlot({ widget }) {
@@ -71,6 +72,7 @@ export default function Dashboard() {
) : (
<>
+ {metrics.length === 0 && widgets.length === 0 ? : null}
{metrics.map((metric) => (
diff --git a/src/views/dashboard/widgets/MilestoneTeamLeaderboard.jsx b/src/views/dashboard/widgets/MilestoneTeamLeaderboard.jsx
index 31dd2d1..4503ca7 100644
--- a/src/views/dashboard/widgets/MilestoneTeamLeaderboard.jsx
+++ b/src/views/dashboard/widgets/MilestoneTeamLeaderboard.jsx
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { Link } from 'react-router';
import { Spinner } from 'react-bootstrap';
import { signedGet, isSuccess } from '@/api/client';
-import { asArray, rankRolePillLabel, userInitials } from '@/views/dashboard/helpers';
+import { asArray, rankRoleId, rankRoleName, rankRolePillLabel, userInitials } from '@/views/dashboard/helpers';
import EmptyChart from '@/views/dashboard/EmptyChart';
function podiumUsersFromRankRole(rankRole) {
@@ -126,11 +126,11 @@ export default function MilestoneTeamLeaderboard({ widget }) {
const mapped = leaderboards.map((board) => {
const rankRoles = asArray(board.rank_roles);
const pills = rankRoles.map((rankRole, index) => {
- const role = rankRole.role || {};
const limit = Number(rankRole.limit) > 0 ? Number(rankRole.limit) : 10;
+ const roleId = rankRoleId(rankRole);
return {
- label: rankRolePillLabel(rankRole.title, limit, role.name),
- url: `/leaderboard/dashboard?id=${encodeURIComponent(board.id || '')}&role=${encodeURIComponent(role.id || '')}`,
+ label: rankRolePillLabel(rankRole.title, limit, rankRoleName(rankRole)),
+ url: `/leaderboard/dashboard?id=${encodeURIComponent(board.id || '')}&role=${encodeURIComponent(roleId)}`,
podium_users: podiumUsersFromRankRole(rankRole),
active: index === 0,
};
diff --git a/src/views/dashboard/widgets/VivaCompatitionPointsTable.jsx b/src/views/dashboard/widgets/VivaCompatitionPointsTable.jsx
index e24370f..7868210 100644
--- a/src/views/dashboard/widgets/VivaCompatitionPointsTable.jsx
+++ b/src/views/dashboard/widgets/VivaCompatitionPointsTable.jsx
@@ -8,7 +8,7 @@ export default function VivaCompatitionPointsTable({ widget }) {
useEffect(() => {
const load = async () => {
- const response = await signedGet('ums', 'team/list');
+ const response = await signedGet('detailing', 'team/list');
setTeams(isSuccess(response) ? response.data?.items || [] : []);
};
load();
diff --git a/src/views/formaction/ConsoleList.jsx b/src/views/formaction/ConsoleList.jsx
index bf7b08e..bf7aca5 100644
--- a/src/views/formaction/ConsoleList.jsx
+++ b/src/views/formaction/ConsoleList.jsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { Card, Col, Container, Modal, Row, Spinner } from 'react-bootstrap';
import { signedGet, isSuccess } from '@/api/client';
import PageBreadcrumb from '@/components/PageBreadcrumb';
+import EmptyState from '@/components/EmptyState';
export default function ConsoleList() {
const [actions, setActions] = useState([]);
@@ -43,18 +44,20 @@ export default function ConsoleList() {
) : (
-
+
{actions.length === 0 ? (
- No quick actions available.
+
) : (
actions.map((action, index) => (
-
+
setActive(action)} onKeyDown={(event) => event.key === 'Enter' && setActive(action)}>
-
- {action.name}
+
+

+
+ {action.name}
diff --git a/src/views/leaderboard/Dashboard.jsx b/src/views/leaderboard/Dashboard.jsx
new file mode 100644
index 0000000..0066ca1
--- /dev/null
+++ b/src/views/leaderboard/Dashboard.jsx
@@ -0,0 +1,307 @@
+import { useEffect, useMemo, useState } from 'react';
+import { Link, useSearchParams } from 'react-router';
+import { Button, Card, Container, Form, Spinner, Table } from 'react-bootstrap';
+import { signedGet, isSuccess } from '@/api/client';
+import PageBreadcrumb from '@/components/PageBreadcrumb';
+import { downloadCsv } from '@/helpers/csv';
+import { asArray, rankRoleId, rankRoleName, rankRolePillLabel } from '@/views/dashboard/helpers';
+
+function uniquePeriods(periods) {
+ const seen = new Set();
+ return asArray(periods).filter((period) => {
+ const value = String(period?.value ?? '');
+ if (!value || seen.has(value)) return false;
+ seen.add(value);
+ return true;
+ });
+}
+
+function buildPills(leaderboardId, selectedRoleId, rankRoles, groups, period) {
+ let roles = asArray(rankRoles);
+ if (!roles.length) {
+ roles = asArray(groups).map((group) => group?.role || group);
+ }
+ roles = [...roles].sort((a, b) => Number(a?.sort_order || 0) - Number(b?.sort_order || 0));
+ return roles.map((rankRole, index) => {
+ const roleId = rankRoleId(rankRole);
+ const limit = Number(rankRole?.limit) > 0 ? Number(rankRole.limit) : 10;
+ const params = new URLSearchParams({ id: leaderboardId, role: roleId });
+ if (period) params.set('period', period);
+ return {
+ label: rankRolePillLabel(rankRole?.title, limit, rankRoleName(rankRole)),
+ to: `/leaderboard/dashboard?${params.toString()}`,
+ active: selectedRoleId ? roleId === selectedRoleId : index === 0,
+ roleId,
+ };
+ });
+}
+
+function shapeView({ id, roleId, period, dashboard, item }) {
+ const view = dashboard?.view || {};
+ const groups = asArray(dashboard?.leaderboard);
+ const periods = uniquePeriods(view.periods);
+ const selectedPeriod = String(period || view.interval || 'all') || 'all';
+ const pills = buildPills(id, roleId, item?.rank_roles, groups, period);
+
+ const selected =
+ groups.find((group) => {
+ const currentRoleId = rankRoleId(group?.role || group);
+ return !roleId || currentRoleId === roleId;
+ }) || null;
+ const selectedRole = selected ? rankRoleId(selected.role || selected) : roleId;
+
+ if (!selected) {
+ return {
+ error: 'No data found for this role.',
+ title: view.name || item?.name || 'Leaderboard',
+ periodLabel: '',
+ periods,
+ selectedPeriod,
+ users: [],
+ metrics: [],
+ pills,
+ selectedRole,
+ };
+ }
+
+ const roleConfig = selected.role || {};
+ const limit = Number(roleConfig.limit) > 0 ? Number(roleConfig.limit) : 0;
+ const roleTitle = String(roleConfig.title || '').trim() || 'Role';
+ const rows = asArray(selected.users);
+ const limited = limit > 0 ? rows.slice(0, limit) : rows;
+
+ const metrics = [];
+ const users = limited.map((row, index) => {
+ const info = row?.user || {};
+ const metricValues = {};
+ asArray(row?.metrics).forEach((metric, metricIndex) => {
+ const metricId = metric?.metric || metric?.title || `m${metricIndex}`;
+ const metricName = metric?.name || metric?.display || metric?.title || metricId;
+ if (index === 0) metrics.push({ id: metricId, name: metricName });
+ metricValues[metricId] = metric?.value ?? 0;
+ });
+ return {
+ rank: row?.rank ?? index + 1,
+ name: info.name || '',
+ avatar: info.avatar || '',
+ employee_code: info.employee_code || '',
+ designation: info.designation || '',
+ position_name: info.position_name || '',
+ score: row?.score ?? 0,
+ metric_values: metricValues,
+ };
+ });
+
+ const firstDay = view.firstDay || '';
+ const lastDay = view.lastDay || '';
+ const periodLabel = `${firstDay}${firstDay && lastDay ? ' – ' : ''}${lastDay}`.trim();
+
+ return {
+ error: '',
+ title: view.name || item?.name || 'Leaderboard',
+ periodLabel,
+ periods,
+ selectedPeriod,
+ users,
+ metrics,
+ pills,
+ selectedRole,
+ };
+}
+
+export default function LeaderboardDashboard() {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const id = String(searchParams.get('id') || '').trim();
+ const roleId = String(searchParams.get('role') || '').trim();
+ const period = String(searchParams.get('period') || '').trim();
+ const [view, setView] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ const load = async () => {
+ if (!id) {
+ setView(null);
+ setError('Leaderboard is required.');
+ setLoading(false);
+ return;
+ }
+ setLoading(true);
+ setError('');
+ try {
+ const params = { id };
+ if (period) params.period = period;
+ const [dashboardRes, oneRes] = await Promise.all([
+ signedGet('reports', 'roles_leaderboard/dashboard', params, { timeout: 120000 }),
+ signedGet('reports', 'roles_leaderboard/get_one', { id }),
+ ]);
+ if (!isSuccess(dashboardRes)) {
+ setView(
+ shapeView({
+ id,
+ roleId,
+ period,
+ dashboard: {},
+ item: isSuccess(oneRes) ? oneRes.data?.item : null,
+ }),
+ );
+ setError(dashboardRes?.message || 'Unable to load leaderboard.');
+ return;
+ }
+ const next = shapeView({
+ id,
+ roleId,
+ period,
+ dashboard: dashboardRes.data || {},
+ item: isSuccess(oneRes) ? oneRes.data?.item : null,
+ });
+ setView(next);
+ setError(next.error || '');
+ } catch (err) {
+ setView(null);
+ setError(err.message || 'Unable to load leaderboard.');
+ } finally {
+ setLoading(false);
+ }
+ };
+ load();
+ }, [id, roleId, period]);
+
+ const title = view?.title || 'Leaderboard';
+ const periods = view?.periods || [];
+ const pills = view?.pills || [];
+ const users = view?.users || [];
+ const metrics = view?.metrics || [];
+
+ const exportRows = useMemo(() => {
+ const header = ['Rank', 'User', 'Employee Code', 'Designation', 'Position', 'Score', ...metrics.map((metric) => metric.name)];
+ const body = users.map((user) => [
+ user.rank,
+ user.name,
+ user.employee_code,
+ user.designation,
+ user.position_name,
+ user.score,
+ ...metrics.map((metric) => user.metric_values?.[metric.id] ?? ''),
+ ]);
+ return [header, ...body];
+ }, [users, metrics]);
+
+ const onPeriodChange = (value) => {
+ const next = new URLSearchParams(searchParams);
+ if (value) next.set('period', value);
+ else next.delete('period');
+ setSearchParams(next, { replace: true });
+ };
+
+ const onExport = () => {
+ const filename = `${String(title || 'leaderboard')
+ .replace(/[^A-Za-z0-9_\-]+/g, '-')
+ .replace(/^-+|-+$/g, '') || 'leaderboard'}.csv`;
+ downloadCsv(exportRows, filename);
+ };
+
+ return (
+
+
+
+
+ Back to Dashboard
+
+ {!error && users.length ? (
+
+ ) : null}
+
+ {view?.periodLabel ? {view.periodLabel}
: null}
+
+
+ {pills.length || periods.length ? (
+
+
+ {pills.map((pill) => (
+
+ {pill.label}
+
+ ))}
+
+ {periods.length ? (
+
+
+
onPeriodChange(event.target.value)}
+ >
+ {periods.map((item) => (
+
+ ))}
+
+
+ ) : null}
+
+ ) : null}
+ {loading ? (
+
+
+
+ ) : error ? (
+ {error}
+ ) : !users.length ? (
+ No leaderboard data found.
+ ) : (
+
+
+
+
+ | Rank |
+ User |
+ Employee Code |
+ Designation / Position |
+ Score |
+ {metrics.map((metric) => (
+ {metric.name} |
+ ))}
+
+
+
+ {users.map((user) => (
+
+ | {user.rank} |
+
+
+ {user.avatar ? (
+ 
+ ) : null}
+ {user.name || '-'}
+
+ |
+ {user.employee_code || '-'} |
+
+ {user.designation || '-'}
+ {user.position_name ? (
+ <>
+
+ {user.position_name}
+ >
+ ) : null}
+ |
+ {user.score} |
+ {metrics.map((metric) => (
+ {user.metric_values?.[metric.id] ?? '—'} |
+ ))}
+
+ ))}
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/views/metricreport/List.jsx b/src/views/metricreport/List.jsx
index f6d9b6e..2877b8c 100644
--- a/src/views/metricreport/List.jsx
+++ b/src/views/metricreport/List.jsx
@@ -5,6 +5,7 @@ import { TbChartBar } from 'react-icons/tb';
import { signedGet, isSuccess } from '@/api/client';
import PageBreadcrumb from '@/components/PageBreadcrumb';
import { ListTableFooter, ListTableToolbar } from '@/components/ListTableControls';
+import { EmptyTableRow } from '@/components/EmptyState';
function formatDate(value) {
if (!value) return '-';
@@ -96,11 +97,7 @@ export default function MetricReportList() {
{pageRows.length === 0 ? (
-
- |
- No records
- |
-
+
) : (
pageRows.map((item) => (
diff --git a/src/views/metricreport/Report.jsx b/src/views/metricreport/Report.jsx
index 4e0ffc9..5297b78 100644
--- a/src/views/metricreport/Report.jsx
+++ b/src/views/metricreport/Report.jsx
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { Link, useSearchParams } from 'react-router';
import { Button, Card, Col, Container, Form, Modal, Row, Spinner } from 'react-bootstrap';
import Flatpickr from 'react-flatpickr';
-import { TbFilter } from 'react-icons/tb';
+import { TbFilter, TbArrowRight } from 'react-icons/tb';
import { LuCalendar, LuChartColumnIncreasing, LuUsers } from 'react-icons/lu';
import { signedGet, signedPost, isSuccess } from '@/api/client';
import PageBreadcrumb from '@/components/PageBreadcrumb';
@@ -14,11 +14,15 @@ import {
canExport,
normalizeLeaderboardMeRow,
parseMetricsParam,
- readBreadcrumbs,
titleCase,
+ chartTabLabel,
+ typeChartList,
toIsoDate,
+ uniqueBy,
updateBreadcrumbs,
writeBreadcrumbs,
+ isTeamReport,
+ isMyTeamType,
} from '@/views/metricreport/helpers';
import { loadRoleMap } from '@/helpers/roles';
@@ -40,6 +44,7 @@ export default function MetricReport() {
const [selectedMetrics, setSelectedMetrics] = useState([]);
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
+ const [pickingCustomDate, setPickingCustomDate] = useState(searchParams.get('period') === 'custom-date');
const [refreshToken, setRefreshToken] = useState(0);
const [roleMapVersion, setRoleMapVersion] = useState(0);
const refreshNext = useRef(searchParams.get('refresh') === '1' || searchParams.get('refresh') === 'true');
@@ -80,6 +85,13 @@ export default function MetricReport() {
return;
}
+ if (query.period === 'custom-date' && !query.dates) {
+ setLoading(false);
+ setError('');
+ setPickingCustomDate(true);
+ return;
+ }
+
const load = async () => {
setLoading(true);
setError('');
@@ -103,16 +115,12 @@ export default function MetricReport() {
}
const post = { ...(response.data.post || {}), ...body };
- let graph = normalizeLeaderboardMeRow(response.data.graph);
+ let graph = normalizeLeaderboardMeRow(response.data.graph, post);
graph = applyMetricVisibility(graph, parseMetricsParam(post.metrics));
let crumbs = [];
- if (graph.agg_type === 'team' && query.hid) {
+ if (isTeamReport(graph) && isMyTeamType(post.type) && query.hid) {
crumbs = updateBreadcrumbs(query.id, query.level || graph.current_level, query.hid, query.username);
- } else if (graph.agg_type === 'team') {
- crumbs = Object.entries(readBreadcrumbs(query.id))
- .sort((a, b) => Number(b[0]) - Number(a[0]))
- .map(([, value]) => value);
}
const codes = asArray(graph.contact_attributes);
@@ -154,14 +162,17 @@ export default function MetricReport() {
const onPeriodChange = (value) => {
if (value === 'custom-date') {
- updateQuery({ period: value });
+ setPickingCustomDate(true);
+ setError('');
return;
}
+ setPickingCustomDate(false);
updateQuery({ period: value }, ['dates']);
};
const onCustomDate = () => {
if (!startDate || !endDate) return;
+ setPickingCustomDate(false);
updateQuery({ period: 'custom-date', dates: `${startDate}--${endDate}` });
};
@@ -187,15 +198,18 @@ export default function MetricReport() {
const graph = payload?.graph;
const post = payload?.post || query;
- const periods = payload?.periods || [];
+ const periods = uniqueBy(payload?.periods, (item) => item?.value);
const charts = payload?.charts || [];
const currentType = post.type || query.type;
- const currentCharts = asArray(charts).find((item) => item.type === currentType)?.charts || [];
+ const typeTabs = uniqueBy(
+ asArray(charts).filter((item) => typeChartList(item).length > 0),
+ (item) => item.type,
+ );
+ const currentCharts = typeChartList(typeTabs.find((item) => item.type === currentType));
return (
- Metric Report
{error ? {error}
: null}
{loading || !graph ? (
@@ -233,43 +247,41 @@ export default function MetricReport() {
-
- {canExport(post, graph) ? (
-
- {rows.map((row, index) => {
+ {rows.length === 0 ? (
+
+ ) : (
+ rows.map((row, index) => {
const uid = row.user?.id;
if (uid && uid === lastId) return null;
lastId = uid;
@@ -104,14 +122,20 @@ function LeaderboardTable({ graph, post, breadcrumbs, onLevel, onChilds, onReset
{showChilds ? (
|
- onChilds(row.hierarchy || row.user?.hierarchy, row.user?.name, Number(graph.current_level) - 1)}>
-
-
+ onChilds(row.hierarchy || row.user?.hierarchy, row.user?.name, Number(graph.current_level) - 1)}
+ >
+
+ View
+
|
) : null}
{showRank ? (
-
+
|
) : null}
@@ -120,18 +144,19 @@ function LeaderboardTable({ graph, post, breadcrumbs, onLevel, onChilds, onReset
| {row.user?.name || ''} |
{row.user?.employee_code || '-'} |
{row.user?.designation || '-'} |
- {positionValue(row.user, 'Zone')} |
+ {positionValue(row.user, 'Zone', { countryFallback: true })} |
{positionValue(row.user, 'Region')} |
{positionValue(row.user, 'Area')} |
{positionValue(row.user, 'Territory')} |
- {graph.type === 'verses' ? null : {row.stats?.ucount ?? 0} | }
+ {graph.type === 'verses' ? '-' : (row.stats?.ucount ?? 0)} |
{metrics.map((metric) => {
const value = leaderboardStatValue(row.stats, metric);
return {value !== null && value !== undefined ? value : '-'} | ;
})}
);
- })}
+ })
+ )}
@@ -148,12 +173,16 @@ function SimpleTrendTable({ graph }) {
- {asArray(graph.values).map((row, index) => (
+ {asArray(graph.values).length === 0 ? (
+
+ ) : (
+ asArray(graph.values).map((row, index) => (
| {row.x} |
{row.y} |
- ))}
+ ))
+ )}
);
@@ -172,14 +201,18 @@ function GroupedTrendTable({ graph }) {