From 16c0ef827c5551199bc4de9b480e48c3cd7b5054 Mon Sep 17 00:00:00 2001 From: Amey Bhagwat Date: Mon, 28 Sep 2026 17:02:59 +0530 Subject: [PATCH] Changes done in several Modules ## Metric Views: - Added missing fields in the Metric view (edit or create) as Team Leaderboard Widget Roles, Include User Attributes - Added one field called stateheadwise_leaderboard:{ type: String, default: 'no' }, Radio button: Yes/No ## Metric Campaigns: - Added missing fields Show Units Export, Show Normal Metrics, Show Combined Metrics, Manager Details, Total Units Name in Add / Edit Campaign management. ## Recognition - Milestones: - Added missing fields Show Units Export, Show Normal Metrics, Show Combined Metrics, Manager Details, Total Units Name in Add / Edit Milestone View after Show Positions Type. - Days checkboxes are inline. - Added more missing fields like Report Subject, Report Body which are after Show Overall Average. ## Data Gathering - Forms: - Added missing User Attributes in Export field in Add/Edit form - In Add / Edit Form Questions, Add Option control should only visible for Question Type Dropdown, Checkbox, Radio, RadioMetrix and Multi Text Boxes - In Add / Edit Form Personalization, added missing major fields like Template and Positions and others. ## Form data export: - Resolved error while opening Form Data Export. - In Add / Edit Form Exports, Select Date Field must have options whose type is date for the selected form. Also Select Date Field should have Default Date (Recommended) option. ## Reminders: - In Add / Edit Form Reminder, added Notification Type field. ## Leaderboard: - In Add / Edit Leaderboard, updated the form fields same as Laravel. - Added missing fields like Show Units Export, Show Normal Metrics, Show Combined Metrics, Total Units Name. --- src/components/EntityForm.jsx | 109 +++++++++++-- src/components/form/DependentSelectField.jsx | 59 ++++--- src/components/form/ReminderRowsField.jsx | 12 ++ .../form/TemplatePositionsField.jsx | 123 +++++++++++++++ src/modules/formentitys.js | 3 +- src/modules/formexports.js | 2 + src/modules/formpersonalizations.js | 84 +++++++++- src/modules/formquestions.js | 5 +- src/modules/leaderboards.js | 148 ++++++++++++++++-- src/modules/metriccampaigns.js | 27 ++++ src/modules/metricviews.js | 27 +++- src/modules/milestones.js | 42 +++++ src/modules/reminders.js | 4 + 13 files changed, 575 insertions(+), 70 deletions(-) create mode 100644 src/components/form/TemplatePositionsField.jsx diff --git a/src/components/EntityForm.jsx b/src/components/EntityForm.jsx index 11a47c6..30d48b9 100644 --- a/src/components/EntityForm.jsx +++ b/src/components/EntityForm.jsx @@ -18,6 +18,7 @@ import RankRolesField from '@/components/form/RankRolesField'; import ReminderRowsField from '@/components/form/ReminderRowsField'; import DependentSelectField from '@/components/form/DependentSelectField'; import BigMultiSelectField from '@/components/form/BigMultiSelectField'; +import TemplatePositionsField from '@/components/form/TemplatePositionsField'; import S3FileField from '@/components/S3FileField'; import { parseCombinedMetrics } from '@/helpers/metric'; @@ -30,7 +31,7 @@ function defaultValue(field, item) { const raw = fromItem !== undefined && fromItem !== null ? fromItem : field.default; if (raw === undefined || raw === null) { if (field.type === 'multiselect' || field.type === 'bigmultiselect' || field.type === 'checkbox') return []; - if (field.type === 'mapping' || field.type === 'color_ranges' || field.type === 'ranking_attributes' || field.type === 'combined_metrics' || field.type === 'milestonemapping' || field.type === 'email_notifications' || field.type === 'rank_roles' || field.type === 'reminders') { + if (field.type === 'mapping' || field.type === 'color_ranges' || field.type === 'ranking_attributes' || field.type === 'combined_metrics' || field.type === 'milestonemapping' || field.type === 'email_notifications' || field.type === 'rank_roles' || field.type === 'reminders' || field.type === 'template_positions') { return []; } if (field.type === 'approverlist') return ['l1manager']; @@ -49,7 +50,7 @@ function defaultValue(field, item) { } return Array.isArray(raw) ? raw : []; } - if ((field.type === 'mapping' || field.type === 'color_ranges' || field.type === 'ranking_attributes' || field.type === 'milestonemapping' || field.type === 'rank_roles' || field.type === 'reminders') && !Array.isArray(raw)) { + if ((field.type === 'mapping' || field.type === 'color_ranges' || field.type === 'ranking_attributes' || field.type === 'milestonemapping' || field.type === 'rank_roles' || field.type === 'reminders' || field.type === 'template_positions') && !Array.isArray(raw)) { return []; } if (field.type === 'multiselect' || field.type === 'bigmultiselect') { @@ -94,12 +95,28 @@ function defaultValue(field, item) { return raw === undefined || raw === null ? '' : String(raw); } +function toColorPickerValue(value, fallback = '#ffffff') { + const raw = String(value || '').trim(); + if (/^#[0-9A-Fa-f]{6}$/.test(raw)) return raw; + if (/^#[0-9A-Fa-f]{3}$/.test(raw)) { + return `#${raw[1]}${raw[1]}${raw[2]}${raw[2]}${raw[3]}${raw[3]}`; + } + if (/^[0-9A-Fa-f]{6}$/.test(raw)) return `#${raw}`; + const fallbackRaw = String(fallback || '').trim(); + if (/^#[0-9A-Fa-f]{6}$/.test(fallbackRaw)) return fallbackRaw; + return '#ffffff'; +} + function isFieldVisible(field, values) { if (field.workflowOnly && values?.workflow !== 'yes') return false; if (!field.showIf) return true; return Object.entries(field.showIf).every(([key, expected]) => { const actual = values?.[key]; - if (Array.isArray(expected)) return expected.includes(actual); + if (Array.isArray(expected)) { + if (Array.isArray(actual)) return expected.some((value) => actual.map(String).includes(String(value))); + return expected.map(String).includes(String(actual)); + } + if (Array.isArray(actual)) return actual.map(String).includes(String(expected)); return actual === expected; }); } @@ -114,7 +131,7 @@ function applyExclusive(next, previous, exclusiveValue) { return next; } -function FieldControl({ field, control, register, moduleName, onUploadingChange }) { +function FieldControl({ field, control, register, watch, moduleName, onUploadingChange }) { const common = { className: 'form-control', id: `field-${field.key}` }; if (field.type === 'hidden') { return ; @@ -224,9 +241,10 @@ function FieldControl({ field, control, register, moduleName, onUploadingChange
rhf.onChange(e.target.value)} - style={{ width: 50, padding: 2 }} + style={{ width: 50, height: 38, padding: 2, colorScheme: 'light' }} /> - - -
+ ( +
+ rhf.onChange('yes')} + /> + rhf.onChange('no')} + /> +
+ )} + /> ); } if (field.type === 'radio') { @@ -264,11 +307,33 @@ function FieldControl({ field, control, register, moduleName, onUploadingChange } if (field.type === 'checkbox') { return ( -
- {Object.entries(field.values || {}).map(([value, label]) => ( - - ))} -
+ { + const selected = Array.isArray(rhf.value) ? rhf.value.map(String) : []; + return ( +
+ {Object.entries(field.values || {}).map(([value, label]) => ( + { + const next = event.target.checked + ? [...selected, value] + : selected.filter((item) => item !== String(value)); + rhf.onChange(next); + }} + /> + ))} +
+ ); + }} + /> ); } if (field.type === 'bigmultiselect') { @@ -411,6 +476,16 @@ function FieldControl({ field, control, register, moduleName, onUploadingChange if (field.type === 'reminders') { return ; } + if (field.type === 'template_positions') { + return ( + + ); + } if (field.type === 'dependent_select') { return ; } @@ -581,7 +656,7 @@ export default function EntityForm({ module: config }) { {fields.map((field) => { if (!isFieldVisible(field, watched)) return null; return field.type === 'hidden' ? ( - + ) : ( {field.hide_label ? null : ( @@ -589,7 +664,7 @@ export default function EntityForm({ module: config }) { {field.title} {field.required ? * : null} )} - + ); })} diff --git a/src/components/form/DependentSelectField.jsx b/src/components/form/DependentSelectField.jsx index 99e154e..15f0991 100644 --- a/src/components/form/DependentSelectField.jsx +++ b/src/components/form/DependentSelectField.jsx @@ -1,33 +1,44 @@ -import { Controller } from 'react-hook-form'; +import { useEffect } from 'react'; +import { useController } from 'react-hook-form'; import { Form } from 'react-bootstrap'; export default function DependentSelectField({ field, control, watch }) { const parentValue = watch(field.dependsOn); - const staticOptions = field.staticOptions || {}; - const questions = (field.questions || []).filter((question) => question.entity_id === parentValue); - const options = { ...staticOptions }; - questions.forEach((question) => { - if (question.code) options[question.code] = `${question.question || question.code} (Only Experts)`; + const staticOptions = Object.entries(field.staticOptions || {}).map(([value, label]) => ({ value, label })); + const allowedTypes = field.questionTypes || (field.questionType ? [field.questionType] : null); + const questions = (field.questions || []).filter((question) => { + const questionFormId = question.entity_id ?? question.form_id; + if (String(questionFormId ?? '') !== String(parentValue || '')) return false; + if (!allowedTypes) return true; + return allowedTypes.includes(String(question.type || '').toLowerCase()); }); + const options = [ + ...staticOptions, + ...questions + .filter((question) => question.code) + .map((question) => ({ + value: question.code, + label: `${question.question || question.code} (Only Experts)`, + })), + ]; + const optionValues = new Set(options.map((option) => String(option.value))); + const fallback = staticOptions[0]?.value || ''; + const { field: rhf } = useController({ name: field.key, control }); + const selected = optionValues.has(String(rhf.value || '')) ? rhf.value : fallback; + + useEffect(() => { + if (String(rhf.value || '') === String(selected || '')) return; + rhf.onChange(selected); + }, [parentValue, selected, rhf.value, rhf.onChange]); return ( - ( - rhf.onChange(e.target.value)} - > - - {Object.entries(options).map(([value, label]) => ( - - ))} - - )} - /> + rhf.onChange(e.target.value)}> + {staticOptions.length ? null : } + {options.map((option, index) => ( + + ))} + ); } diff --git a/src/components/form/ReminderRowsField.jsx b/src/components/form/ReminderRowsField.jsx index eec5548..f107bc3 100644 --- a/src/components/form/ReminderRowsField.jsx +++ b/src/components/form/ReminderRowsField.jsx @@ -31,6 +31,7 @@ function emptyRow() { checksubmission_days: '', message: '', plan_date_field: '', + notification_type: 'normal', }; } @@ -165,6 +166,17 @@ export default function ReminderRowsField({ field, control }) { onChange={(e) => rhf.onChange(patchRow(rows, index, { plan_date_field: e.target.value }))} /> + + Notification Type + rhf.onChange(patchRow(rows, index, { notification_type: e.target.value }))} + > + + + + ); diff --git a/src/components/form/TemplatePositionsField.jsx b/src/components/form/TemplatePositionsField.jsx new file mode 100644 index 0000000..f152850 --- /dev/null +++ b/src/components/form/TemplatePositionsField.jsx @@ -0,0 +1,123 @@ +import { Button, Col, Form, Row } from 'react-bootstrap'; +import { Controller } from 'react-hook-form'; +import S3FileField from '@/components/S3FileField'; + +const YES_NO = [ + { value: 'no', label: 'No' }, + { value: 'yes', label: 'Yes' }, +]; + +function emptyRow() { + return { + template: '', + createpdf: 'no', + addlink: 'no', + linkfield: '', + positions: '', + }; +} + +function patchRow(rows, index, patch) { + return rows.map((row, i) => (i === index ? { ...row, ...patch } : row)); +} + +export default function TemplatePositionsField({ field, control, moduleName, onUploadingChange }) { + const linkFields = field.values || field.linkFields || {}; + + return ( + { + const rows = Array.isArray(rhf.value) ? rhf.value : []; + return ( +
+
+ +
+ {rows.length === 0 ?
No templates
: null} + {rows.map((row, index) => ( +
+ + + rhf.onChange(patchRow(rows, index, { template: url }))} + moduleName={moduleName} + onUploadingChange={onUploadingChange} + /> + + + Include in PDF + rhf.onChange(patchRow(rows, index, { createpdf: e.target.value }))} + > + {YES_NO.map((option) => ( + + ))} + + Add Link to PDF + rhf.onChange(patchRow(rows, index, { addlink: e.target.value }))} + > + {YES_NO.map((option) => ( + + ))} + + Link Field + rhf.onChange(patchRow(rows, index, { linkfield: e.target.value }))} + > + + {Object.entries(linkFields).map(([value, label]) => ( + + ))} + + + + + + + rhf.onChange(patchRow(rows, index, { positions: e.target.value }))} + /> + + +
+ ))} +
+ ); + }} + /> + ); +} diff --git a/src/modules/formentitys.js b/src/modules/formentitys.js index 2540da1..6abbfcb 100644 --- a/src/modules/formentitys.js +++ b/src/modules/formentitys.js @@ -122,7 +122,6 @@ const formentitys = { type: 'multiselect', values: userAttributes, default: [], - showIf: { show_user_attributes: 'yes' }, }, { key: 'form_type', @@ -234,6 +233,7 @@ const formentitys = { next.contact_attributes = asArray(next.contact_attributes); next.show_user_attributes = yesno(next.show_user_attributes, 'no'); next.user_attributes = asArray(next.user_attributes); + if (next.user_attributes.length) next.show_user_attributes = 'yes'; next.roles_email_notification = asArray(next.roles_email_notification); next.roles_push_notification = asArray(next.roles_push_notification); next.workflow_approvers = Array.isArray(next.workflow_approvers) && next.workflow_approvers.length ? next.workflow_approvers : ['l1manager']; @@ -248,6 +248,7 @@ const formentitys = { payload.contact_attributes = asArray(payload.contact_attributes); payload.show_user_attributes = payload.show_user_attributes === 'yes' ? 'yes' : 'no'; payload.user_attributes = asArray(payload.user_attributes); + if (payload.user_attributes.length) payload.show_user_attributes = 'yes'; payload.enrollment_form_attributes = asArray(payload.enrollment_form_attributes); payload.roles_email_notification = asArray(payload.roles_email_notification); payload.roles_push_notification = asArray(payload.roles_push_notification); diff --git a/src/modules/formexports.js b/src/modules/formexports.js index 1f75284..c08f92a 100644 --- a/src/modules/formexports.js +++ b/src/modules/formexports.js @@ -44,6 +44,8 @@ const formexports = { required: true, dependsOn: 'form_id', questions, + questionType: 'date', + default: 'record_date', staticOptions: { record_date: 'Default Date (Recommended)' }, }, { key: 'start_date', title: 'Start Date', type: 'date', required: true }, diff --git a/src/modules/formpersonalizations.js b/src/modules/formpersonalizations.js index b24b361..52ae45d 100644 --- a/src/modules/formpersonalizations.js +++ b/src/modules/formpersonalizations.js @@ -3,6 +3,43 @@ import { splitHash } from '@/helpers/metric'; const STATUS = { active: 'Active', disabled: 'Disabled' }; +function asList(value) { + if (value == null || value === '') return []; + if (Array.isArray(value)) return value; + if (typeof value === 'object') return Object.values(value); + return [value]; +} + +function stringifyPositions(value) { + if (value == null || value === '') return ''; + if (typeof value === 'string') return value; + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +function flattenMappings(item) { + const templates = asList(item?.templates); + const positions = asList(item?.positions); + const createpdf = asList(item?.createpdf); + const addlink = asList(item?.addlink); + const linkfield = asList(item?.linkfield); + const length = Math.max(templates.length, positions.length, createpdf.length, addlink.length, linkfield.length); + const rows = []; + for (let index = 0; index < length; index += 1) { + rows.push({ + template: templates[index] || '', + createpdf: createpdf[index] === 'yes' ? 'yes' : 'no', + addlink: addlink[index] === 'yes' ? 'yes' : 'no', + linkfield: linkfield[index] || '', + positions: stringifyPositions(positions[index]), + }); + } + return rows; +} + const formpersonalizations = { service: 'form', module: 'personalization', @@ -26,24 +63,55 @@ const formpersonalizations = { formFields: async ({ item } = {}) => { const entityId = item?.entity_id || ''; const questions = {}; + const linkFields = {}; 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; + (isSuccess(response) ? response.data?.items || [] : []).forEach((question) => { + if (question?.id) questions[question.id] = question.question || question.title || question.id; + if (question?.code) linkFields[question.code] = question.question || question.title || question.code; }); } 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: '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 }, + { + key: 'mappings', + title: 'Template and Positions', + type: 'template_positions', + fullwidth: true, + values: linkFields, + }, ]; }, - beforeForm: (item, _mode, ctx) => ({ - ...item, - entity_id: item.entity_id || ctx?.searchParams?.get('id') || '', - questions: splitHash(item.questions), - }), + beforeForm: (item, _mode, ctx) => { + const next = { + ...item, + entity_id: item.entity_id || ctx?.searchParams?.get('id') || '', + questions: splitHash(item.questions), + }; + next.mappings = flattenMappings(next); + return next; + }, + beforeSave: (data) => { + const mappings = Array.isArray(data.mappings) ? data.mappings : []; + const payload = { ...data }; + payload.questions = Array.isArray(payload.questions) ? payload.questions.filter(Boolean) : []; + payload.templates = mappings.map((row) => row.template || ''); + payload.positions = mappings.map((row) => row.positions || ''); + payload.createpdf = mappings.map((row) => (row.createpdf === 'yes' ? 'yes' : 'no')); + payload.addlink = mappings.map((row) => (row.addlink === 'yes' ? 'yes' : 'no')); + payload.linkfield = mappings.map((row) => row.linkfield || ''); + delete payload.mappings; + return payload; + }, }; export default formpersonalizations; diff --git a/src/modules/formquestions.js b/src/modules/formquestions.js index fb5c714..db198a2 100644 --- a/src/modules/formquestions.js +++ b/src/modules/formquestions.js @@ -16,6 +16,7 @@ const FORM_QUESTION_TYPES = { }; const STATUS = { active: 'Active', disabled: 'Disabled' }; +const OPTION_TYPES = ['dropdown', 'checkbox', 'radio', 'radiometrix', 'multitextbox']; const formquestions = { service: 'form', @@ -49,7 +50,7 @@ const formquestions = { { key: 'required', title: 'Is Required', type: 'yesno', required: true }, { key: 'sort_order', title: 'Sort Order', type: 'number' }, { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS }, - { key: 'options', title: 'Options', type: 'mapping', variant: 'question_options', fullwidth: true }, + { key: 'options', title: 'Options', type: 'mapping', variant: 'question_options', fullwidth: true, showIf: { type: OPTION_TYPES } }, ], beforeForm: (item) => ({ ...item, @@ -59,7 +60,7 @@ const formquestions = { beforeSave: (data) => ({ ...data, sort_order: data.sort_order === '' || data.sort_order == null ? 0 : Number(data.sort_order), - options: Array.isArray(data.options) ? data.options : [], + options: OPTION_TYPES.includes(data.type) && Array.isArray(data.options) ? data.options : [], }), }; diff --git a/src/modules/leaderboards.js b/src/modules/leaderboards.js index 7a4c2a6..03e2682 100644 --- a/src/modules/leaderboards.js +++ b/src/modules/leaderboards.js @@ -1,21 +1,73 @@ +import { signedGet, isSuccess } from '@/api/client'; import { dropdownMap } from '@/api/dropdowns'; import { - DEFAULT_INTERVAL_VALUES, INTERVAL_VALUES, metricOptions, parseCombinedMetrics, splitHash, + stringifyCombinedMetrics, toDateInput, } from '@/helpers/metric'; const STATUS = { active: 'Active', disabled: 'Disabled' }; const YES_NO = { yes: 'Yes', no: 'No' }; +const MANAGER_LEVELS = { + l1manager: 'L1 Manager', + l2manager: 'L2 Manager', + l3manager: 'L3 Manager', + l4manager: 'L4 Manager', +}; +const DEFAULT_INTERVAL_VALUES = { + '': 'Overall / All', + all: 'Overall', + 'month-0': 'This Month', + 'month-1': 'Last Month', + 'month-3': 'Last 3 Months', + 'month-6': 'Last 6 Months', + 'month-12': 'Last 12 Months', + 'day-0': 'Today', + 'day-1': 'Yesterday', + 'day-7': 'Last 7 Days', + 'day-30': 'Last 30 Days', + 'week-0': 'This week', + 'week-1': 'Last week', + 'yearfy-0': 'This Financial Year', + 'yearfy-1': 'Last Financial Year', + 'yearcy-0': 'This Calender Year', + 'yearcy-1': 'Last Calender Year', +}; function asArray(value) { if (Array.isArray(value)) return value.filter((part) => part !== null && part !== undefined && part !== ''); return splitHash(value); } +function bySortOrder(left, right) { + return (Number(left?.sort_order) || 0) - (Number(right?.sort_order) || 0); +} + +function itemsToMap(items) { + const values = {}; + (items || []).forEach((item) => { + const id = item?.id; + if (id === undefined || id === null || id === '') return; + values[id] = item.name || item.title || id; + }); + return values; +} + +async function teamOptions() { + const fromDropdown = await dropdownMap('detailing', 'team', (item) => item.name); + if (Object.keys(fromDropdown).length) return fromDropdown; + for (const path of ['team/get_all', 'team/list']) { + const response = await signedGet('detailing', path); + const items = isSuccess(response) ? response.data?.items || response.data || [] : []; + const values = itemsToMap(Array.isArray(items) ? items : []); + if (Object.keys(values).length) return values; + } + return {}; +} + const leaderboards = { service: 'reports', module: 'roles_leaderboard', @@ -26,6 +78,7 @@ const leaderboards = { listFields: [ { 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: 'leaderboard_roles_type_label', title: 'Type', type: 'text' }, { key: 'sort_order', title: 'Sort Order', type: 'number' }, { key: 'visible_roles_label', title: 'Visible Roles', type: 'text' }, { key: 'validity_start', title: 'Validity start', type: 'text' }, @@ -36,18 +89,47 @@ const leaderboards = { (items || []).map((item) => ({ ...item, visible_roles_label: asArray(item.visible_roles).join(', '), + leaderboard_roles_type_label: item.leaderboard_roles_type === 'team' ? 'Team' : 'Roles', sort_order: item.sort_order ?? 0, })), formFields: async () => { - const [metrics, roles] = await Promise.all([metricOptions(), dropdownMap('ums', 'roles', (item) => item.name)]); + const [metrics, roles, teams] = await Promise.all([ + metricOptions(), + dropdownMap('ums', 'roles', (item) => item.name), + teamOptions(), + ]); return [ { key: 'name', title: 'Name', type: 'text', required: true }, { key: 'sort_order', title: 'Sort Order', type: 'number', required: true }, { key: 'validity_start', title: 'Validity start', type: 'date', required: true }, { key: 'validity_end', title: 'Validity end', type: 'date', required: true }, + { + key: 'leaderboard_roles_type', + title: 'Leaderboard Type', + type: 'select', + required: true, + values: { roles: 'Roles', team: 'Team' }, + }, + { + key: 'leaderboard_team_ids', + title: 'Teams', + type: 'multiselect', + values: teams, + fullwidth: true, + required: true, + showIf: { leaderboard_roles_type: 'team' }, + }, { key: 'visible_roles', title: 'Visible Roles', type: 'multiselect', values: roles, required: true }, { key: 'reporting_roles', title: 'Reporting Roles', type: 'multiselect', values: roles, required: true }, - { key: 'rank_roles', title: 'Rank Roles', type: 'rank_roles', roles, required: true, fullwidth: true }, + { + key: 'rank_roles', + title: 'Rank Roles', + type: 'rank_roles', + roles, + required: true, + fullwidth: true, + showIf: { leaderboard_roles_type: 'roles' }, + }, { key: 'metrics', title: 'Choose Metric', @@ -59,7 +141,7 @@ const leaderboards = { }, { key: 'combined_metrics', title: 'Combined Metrics (if any)', type: 'combined_metrics', fullwidth: true }, { key: 'intervals', title: 'Intervals', type: 'multiselect', required: true, values: INTERVAL_VALUES }, - { key: 'default_interval', title: 'Default Interval', type: 'select', values: { '': 'Overall / All', all: 'Overall', ...DEFAULT_INTERVAL_VALUES } }, + { key: 'default_interval', title: 'Default Interval', type: 'select', values: DEFAULT_INTERVAL_VALUES }, { key: 'show_positions', title: 'Show Positions', type: 'select', required: true, values: YES_NO }, { key: 'show_positions_type', @@ -68,40 +150,70 @@ const leaderboards = { required: true, values: { all: 'All', loggedin: 'Logged In' }, }, + { key: 'show_units_export', title: 'Show Units Export', type: 'select', values: YES_NO }, + { key: 'show_normal_metrics', title: 'Show Normal Metrics', type: 'select', values: YES_NO }, + { key: 'show_combined_metrics', title: 'Show Combined Metrics', type: 'select', values: YES_NO }, + { key: 'manager_details', title: 'Manager Details', type: 'multiselect', values: MANAGER_LEVELS }, + { key: 'total_units_name', title: 'Total Units Name', type: 'text', placeholder: 'Total Units' }, + { key: 'bg_color', title: 'Background Color', type: 'color' }, + { key: 'bg_image', title: 'Background Image', type: 'image', uploadModule: 'leaderboard' }, + { key: 'icon', title: 'Icon', type: 'image', uploadModule: 'leaderboard' }, { key: 'status', title: 'Status', type: 'select', required: true, values: STATUS }, ]; }, beforeForm: (item) => { const next = { ...item }; next.sort_order = next.sort_order ?? 0; - next.show_positions = next.show_positions || 'no'; + next.show_positions = String(next.show_positions || 'no').toLowerCase(); next.show_positions_type = next.show_positions_type || 'all'; + next.show_units_export = next.show_units_export === 'no' ? 'no' : 'yes'; + next.show_normal_metrics = next.show_normal_metrics === 'no' ? 'no' : 'yes'; + next.show_combined_metrics = next.show_combined_metrics === 'no' ? 'no' : 'yes'; + next.total_units_name = next.total_units_name || ''; next.status = next.status || 'active'; next.default_interval = next.default_interval ?? ''; + next.leaderboard_roles_type = next.leaderboard_roles_type === 'team' ? 'team' : 'roles'; + next.bg_color = next.bg_color || ''; + next.bg_image = next.bg_image || ''; + next.icon = next.icon || ''; next.validity_start = toDateInput(next.validity_start); next.validity_end = toDateInput(next.validity_end); next.visible_roles = asArray(next.visible_roles); next.reporting_roles = asArray(next.reporting_roles); next.intervals = asArray(next.intervals); - next.rank_roles = Array.isArray(next.rank_roles) ? next.rank_roles : []; - next.metrics = Array.isArray(next.metrics) - ? next.metrics.map((row) => ({ - metric: row.metric || '', - display: row.display || '', - sort_order: row.sort_order ?? 0, - weight: row.weight ?? 1, - list_view: row.list_view || 'yes', - })) - : []; + next.leaderboard_team_ids = asArray(next.leaderboard_team_ids); + next.manager_details = asArray(next.manager_details); + if (!next.manager_details.length) next.manager_details = ['l1manager']; + next.rank_roles = (Array.isArray(next.rank_roles) ? next.rank_roles : []).slice().sort(bySortOrder); + next.metrics = (Array.isArray(next.metrics) ? next.metrics : []) + .map((row) => ({ + metric: row.metric || '', + display: row.display || '', + sort_order: row.sort_order ?? 0, + weight: row.weight ?? 1, + list_view: row.list_view || 'yes', + })) + .sort(bySortOrder); next.combined_metrics = parseCombinedMetrics(next.combined_metrics); return next; }, beforeSave: (data) => { const payload = { ...data }; + const leaderboardType = payload.leaderboard_roles_type === 'team' ? 'team' : 'roles'; + payload.leaderboard_roles_type = leaderboardType; payload.sort_order = payload.sort_order === '' || payload.sort_order == null ? 0 : Number(payload.sort_order); payload.visible_roles = asArray(payload.visible_roles); payload.reporting_roles = asArray(payload.reporting_roles); payload.intervals = asArray(payload.intervals); + payload.leaderboard_team_ids = leaderboardType === 'team' ? asArray(payload.leaderboard_team_ids) : []; + payload.show_units_export = payload.show_units_export === 'no' ? 'no' : 'yes'; + payload.show_normal_metrics = payload.show_normal_metrics === 'no' ? 'no' : 'yes'; + payload.show_combined_metrics = payload.show_combined_metrics === 'no' ? 'no' : 'yes'; + payload.total_units_name = payload.total_units_name || ''; + payload.manager_details = asArray(payload.manager_details); + if (!payload.manager_details.length) payload.manager_details = ['l1manager']; + payload.bg_color = Array.isArray(payload.bg_color) ? '' : payload.bg_color || ''; + payload.default_interval = payload.default_interval ?? ''; payload.rank_roles = (Array.isArray(payload.rank_roles) ? payload.rank_roles : []) .filter((row) => row.title || row.role) .map((row) => ({ @@ -118,7 +230,11 @@ const leaderboards = { sort_order: row.sort_order === '' || row.sort_order == null ? 0 : Number(row.sort_order), weight: row.weight === '' || row.weight == null ? '1' : String(row.weight), })); - payload.combined_metrics = Array.isArray(payload.combined_metrics) ? payload.combined_metrics : []; + payload.combined_metrics = stringifyCombinedMetrics( + (Array.isArray(payload.combined_metrics) ? payload.combined_metrics : []).filter((row) => row?.type), + ); + if (!payload.bg_image) delete payload.bg_image; + if (!payload.icon) delete payload.icon; return payload; }, }; diff --git a/src/modules/metriccampaigns.js b/src/modules/metriccampaigns.js index 53146bd..2d54aa6 100644 --- a/src/modules/metriccampaigns.js +++ b/src/modules/metriccampaigns.js @@ -67,6 +67,12 @@ function normalizeCombined(row) { } const YES_NO = { yes: 'Yes', no: 'No' }; +const MANAGER_LEVELS = { + l1manager: 'L1 Manager', + l2manager: 'L2 Manager', + l3manager: 'L3 Manager', + l4manager: 'L4 Manager', +}; const metriccampaigns = { service: 'metric', @@ -171,6 +177,11 @@ const metriccampaigns = { required: true, values: { all: 'All', loggedin: 'Logged In' }, }, + { key: 'show_units_export', title: 'Show Units Export', type: 'select', values: YES_NO }, + { key: 'show_normal_metrics', title: 'Show Normal Metrics', type: 'select', values: YES_NO }, + { key: 'show_combined_metrics', title: 'Show Combined Metrics', type: 'select', values: YES_NO }, + { key: 'manager_details', title: 'Manager Details', type: 'multiselect', values: MANAGER_LEVELS }, + { key: 'total_units_name', title: 'Total Units Name', type: 'text', placeholder: 'Total Units' }, { key: 'highlight_top', title: 'Highlight Top', type: 'text', placeholder: 'e.g. 10, 20, 30' }, { key: 'highlight_color', title: 'Highlight Color', type: 'text', placeholder: 'R-G-B-A or CSS color' }, { @@ -202,11 +213,22 @@ const metriccampaigns = { showManagerTeamRank: 'yes', highlight_top: '', highlight_color: '', + show_units_export: 'yes', + show_normal_metrics: 'yes', + show_combined_metrics: 'yes', + manager_details: ['l1manager'], + total_units_name: '', ...item, }; next.campaign_visible_roles = asArray(next.campaign_visible_roles); next.reporting_roles = asArray(next.reporting_roles); next.intervals = asArray(next.intervals); + next.manager_details = asArray(next.manager_details); + if (!next.manager_details.length) next.manager_details = ['l1manager']; + next.show_units_export = next.show_units_export === 'no' ? 'no' : 'yes'; + next.show_normal_metrics = next.show_normal_metrics === 'no' ? 'no' : 'yes'; + next.show_combined_metrics = next.show_combined_metrics === 'no' ? 'no' : 'yes'; + next.total_units_name = next.total_units_name || ''; next.metrics = (Array.isArray(next.metrics) ? next.metrics : []).map(normalizeCampaignMetric); next.combined_metrics = parseCombinedMetrics(next.combined_metrics).map(normalizeCombined); next.ranking_attributes = normalizeRankingAttributes(next.ranking_attributes); @@ -253,6 +275,11 @@ const metriccampaigns = { campaign_visible_roles: asArray(data.campaign_visible_roles), reporting_roles: asArray(data.reporting_roles), intervals: asArray(data.intervals), + manager_details: asArray(data.manager_details).length ? asArray(data.manager_details) : ['l1manager'], + show_units_export: data.show_units_export === 'no' ? 'no' : 'yes', + show_normal_metrics: data.show_normal_metrics === 'no' ? 'no' : 'yes', + show_combined_metrics: data.show_combined_metrics === 'no' ? 'no' : 'yes', + total_units_name: data.total_units_name || '', sort_order: Number.parseInt(data.sort_order, 10) || 0, validity_start: toApiDate(data.validity_start), validity_end: toApiDate(data.validity_end), diff --git a/src/modules/metricviews.js b/src/modules/metricviews.js index bce992b..300b6e2 100644 --- a/src/modules/metricviews.js +++ b/src/modules/metricviews.js @@ -199,6 +199,12 @@ const metricviews = { values: { all: 'All', loggedin: 'Logged In' }, }, { key: 'contact_attributes', title: 'Include Doctor Attributes', type: 'multiselect', values: contactAttributes }, + { + key: 'team_leaderboard_widget_roles', + title: 'Team Leaderboard Widget Roles', + type: 'multiselect', + values: roles, + }, { key: 'show_user_attributes', title: 'Show User Attributes', type: 'yesno', required: true, default: 'no' }, { key: 'user_attributes', @@ -206,7 +212,13 @@ const metricviews = { type: 'multiselect', values: userAttributes, default: [], - showIf: { show_user_attributes: 'yes' }, + }, + { + key: 'stateheadwise_leaderboard', + title: 'Stateheadwise Leaderboard', + type: 'yesno', + required: true, + default: 'no', }, ]; }, @@ -219,9 +231,11 @@ const metricviews = { show_positions_type: 'all', show_user_attributes: 'no', user_attributes: [], + stateheadwise_leaderboard: 'no', list_chart_type: 'progressbar', value_views: ['value'], default_tab: '', + team_leaderboard_widget_roles: [], ...item, }; next.designations = asArray(next.designations); @@ -240,8 +254,15 @@ const metricviews = { next.default_tab = next.default_tab || ''; next.intervals = asArray(next.intervals); next.contact_attributes = asArray(next.contact_attributes); - next.show_user_attributes = next.show_user_attributes === 'yes' ? 'yes' : 'no'; + next.team_leaderboard_widget_roles = asArray( + next.team_leaderboard_widget_roles || next.leaderboard_widget_roles || next.widget_roles, + ); + next.show_user_attributes = ['yes', '1', 'true', true, 1].includes(next.show_user_attributes) ? 'yes' : 'no'; next.user_attributes = asArray(next.user_attributes); + if (next.user_attributes.length && next.show_user_attributes !== 'yes') { + next.show_user_attributes = 'yes'; + } + next.stateheadwise_leaderboard = next.stateheadwise_leaderboard === 'yes' ? 'yes' : 'no'; next.combined_metrics = parseCombinedMetrics(next.combined_metrics); next.validity_start = toDateInput(next.validity_start); next.validity_end = toDateInput(next.validity_end); @@ -309,8 +330,10 @@ const metricviews = { default_tab: data.default_tab || '', intervals: asArray(data.intervals), contact_attributes: asArray(data.contact_attributes), + team_leaderboard_widget_roles: asArray(data.team_leaderboard_widget_roles), show_user_attributes: data.show_user_attributes === 'yes' ? 'yes' : 'no', user_attributes: asArray(data.user_attributes), + stateheadwise_leaderboard: data.stateheadwise_leaderboard === 'yes' ? 'yes' : 'no', combined_metrics: stringifyCombinedMetrics( (data.combined_metrics || []) .filter((row) => row.type) diff --git a/src/modules/milestones.js b/src/modules/milestones.js index 1e52cf3..357bed2 100644 --- a/src/modules/milestones.js +++ b/src/modules/milestones.js @@ -4,6 +4,12 @@ import { metricOptions, toDateInput } from '@/helpers/metric'; const STATUS = { active: 'Active', disabled: 'Disabled' }; const YES_NO = { yes: 'Yes', no: 'No' }; +const MANAGER_LEVELS = { + l1manager: 'L1 Manager', + l2manager: 'L2 Manager', + l3manager: 'L3 Manager', + l4manager: 'L4 Manager', +}; const DAYS = { MON: 'Monday', TUE: 'Tuesday', @@ -195,6 +201,11 @@ const milestones = { required: true, values: { all: 'All', loggedin: 'Logged In' }, }, + { key: 'show_units_export', title: 'Show Units Export', type: 'select', values: YES_NO }, + { key: 'show_normal_metrics', title: 'Show Normal Metrics', type: 'select', values: YES_NO }, + { key: 'show_combined_metrics', title: 'Show Combined Metrics', type: 'select', values: YES_NO }, + { key: 'manager_details', title: 'Manager Details', type: 'multiselect', values: MANAGER_LEVELS }, + { key: 'total_units_name', title: 'Total Units Name', type: 'text', placeholder: 'Total Units' }, { key: 'send_report', title: 'Send Report', type: 'select', required: true, values: YES_NO }, { key: 'performance_period', @@ -276,6 +287,18 @@ const milestones = { values: YES_NO, showIf: { send_report: 'yes' }, }, + { + key: 'subject', + title: 'Report Subject', + type: 'text', + showIf: { send_report: 'yes' }, + }, + { + key: 'body', + title: 'Report Body', + type: 'textarea', + showIf: { send_report: 'yes' }, + }, { key: 'metrics', title: 'Metric Mapping', @@ -326,7 +349,15 @@ const milestones = { next.send_pending_users = fromBool(reportConfig.send_pending_users ?? false); next.show_target_average = fromBool(reportConfig.show_target_average ?? false); next.show_overall_average = fromBool(reportConfig.show_overall_average ?? false); + next.subject = reportConfig.subject || ''; + next.body = reportConfig.body || ''; next.send_report = fromBool(next.send_report ?? false); + next.show_units_export = next.show_units_export === 'no' ? 'no' : 'yes'; + next.show_normal_metrics = next.show_normal_metrics === 'no' ? 'no' : 'yes'; + next.show_combined_metrics = next.show_combined_metrics === 'no' ? 'no' : 'yes'; + next.manager_details = toStringList(next.manager_details); + if (!next.manager_details.length) next.manager_details = ['l1manager']; + next.total_units_name = next.total_units_name || ''; return next; }, beforeSave: (data) => { @@ -354,7 +385,16 @@ const milestones = { send_pending_users: toBool(payload.send_pending_users ?? 'no'), show_target_average: toBool(payload.show_target_average ?? 'no'), show_overall_average: toBool(payload.show_overall_average ?? 'no'), + subject: (payload.subject || '').trim(), + body: (payload.body || '').trim(), }; + payload.show_units_export = payload.show_units_export === 'no' ? 'no' : 'yes'; + payload.show_normal_metrics = payload.show_normal_metrics === 'no' ? 'no' : 'yes'; + payload.show_combined_metrics = payload.show_combined_metrics === 'no' ? 'no' : 'yes'; + payload.manager_details = toStringList(payload.manager_details).length + ? toStringList(payload.manager_details) + : ['l1manager']; + payload.total_units_name = payload.total_units_name || ''; delete payload.interval_start_date; delete payload.interval_end_date; @@ -370,6 +410,8 @@ const milestones = { delete payload.send_pending_users; delete payload.show_target_average; delete payload.show_overall_average; + delete payload.subject; + delete payload.body; return payload; }, }; diff --git a/src/modules/reminders.js b/src/modules/reminders.js index a42789c..5a0f803 100644 --- a/src/modules/reminders.js +++ b/src/modules/reminders.js @@ -42,6 +42,9 @@ function flattenReminders(rows) { checksubmission_days: item.checksubmission_days || '', message: item.message || '', plan_date_field: item.plan_date_field || '', + notification_type: ['normal', 'overlay_custom'].includes(item.notification_type) + ? item.notification_type + : 'normal', }; }); } @@ -108,6 +111,7 @@ const reminders = { checksubmission_days: row.checksubmission === 'yes' ? row.checksubmission_days || '' : '', message: row.message || '', plan_date_field: row.plan_date_field || '', + notification_type: row.notification_type === 'overlay_custom' ? 'overlay_custom' : 'normal', })); return payload; },