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.
This commit is contained in:
Amey Bhagwat 2026-09-28 17:02:59 +05:30
parent 212cf0091d
commit 16c0ef827c
13 changed files with 575 additions and 70 deletions

View File

@ -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 <input type="hidden" {...register(field.key)} />;
@ -224,9 +241,10 @@ function FieldControl({ field, control, register, moduleName, onUploadingChange
<div className="d-flex gap-2 align-items-center">
<Form.Control
type="color"
value={rhf.value || '#000000'}
className="form-control-color"
value={toColorPickerValue(rhf.value, field.pickerDefault || '#ffffff')}
onChange={(e) => rhf.onChange(e.target.value)}
style={{ width: 50, padding: 2 }}
style={{ width: 50, height: 38, padding: 2, colorScheme: 'light' }}
/>
<Form.Control
value={rhf.value || ''}
@ -240,10 +258,35 @@ function FieldControl({ field, control, register, moduleName, onUploadingChange
}
if (field.type === 'yesno') {
return (
<Controller
name={field.key}
control={control}
rules={{ required: field.required }}
render={({ field: rhf }) => (
<div>
<Form.Check inline type="radio" label="Yes" value="yes" {...register(field.key, { required: field.required })} />
<Form.Check inline type="radio" label="No" value="no" {...register(field.key, { required: field.required })} />
<Form.Check
inline
type="radio"
id={`field-${field.key}-yes`}
name={field.key}
label="Yes"
value="yes"
checked={rhf.value === 'yes'}
onChange={() => rhf.onChange('yes')}
/>
<Form.Check
inline
type="radio"
id={`field-${field.key}-no`}
name={field.key}
label="No"
value="no"
checked={rhf.value === 'no'}
onChange={() => rhf.onChange('no')}
/>
</div>
)}
/>
);
}
if (field.type === 'radio') {
@ -264,12 +307,34 @@ function FieldControl({ field, control, register, moduleName, onUploadingChange
}
if (field.type === 'checkbox') {
return (
<div>
<Controller
name={field.key}
control={control}
render={({ field: rhf }) => {
const selected = Array.isArray(rhf.value) ? rhf.value.map(String) : [];
return (
<div className="d-flex flex-wrap align-items-center gap-3">
{Object.entries(field.values || {}).map(([value, label]) => (
<Form.Check key={value} type="checkbox" label={label} value={value} {...register(field.key)} />
<Form.Check
key={value}
inline
type="checkbox"
id={`field-${field.key}-${value}`}
label={label}
checked={selected.includes(String(value))}
onChange={(event) => {
const next = event.target.checked
? [...selected, value]
: selected.filter((item) => item !== String(value));
rhf.onChange(next);
}}
/>
))}
</div>
);
}}
/>
);
}
if (field.type === 'bigmultiselect') {
return <BigMultiSelectField field={field} control={control} />;
@ -411,6 +476,16 @@ function FieldControl({ field, control, register, moduleName, onUploadingChange
if (field.type === 'reminders') {
return <ReminderRowsField field={field} control={control} />;
}
if (field.type === 'template_positions') {
return (
<TemplatePositionsField
field={field}
control={control}
moduleName={moduleName}
onUploadingChange={onUploadingChange}
/>
);
}
if (field.type === 'dependent_select') {
return <DependentSelectField field={field} control={control} watch={watch} />;
}
@ -581,7 +656,7 @@ export default function EntityForm({ module: config }) {
{fields.map((field) => {
if (!isFieldVisible(field, watched)) return null;
return field.type === 'hidden' ? (
<FieldControl key={field.key} field={field} control={control} register={register} moduleName={config.module} onUploadingChange={onUploadingChange} />
<FieldControl key={field.key} field={field} control={control} register={register} watch={watch} moduleName={config.module} onUploadingChange={onUploadingChange} />
) : (
<Col md={field.fullwidth ? 12 : 6} className="mb-3 min-w-0" key={field.key}>
{field.hide_label ? null : (
@ -589,7 +664,7 @@ export default function EntityForm({ module: config }) {
{field.title} {field.required ? <span className="text-danger">*</span> : null}
</Form.Label>
)}
<FieldControl field={field} control={control} register={register} moduleName={config.module} onUploadingChange={onUploadingChange} />
<FieldControl field={field} control={control} register={register} watch={watch} moduleName={config.module} onUploadingChange={onUploadingChange} />
</Col>
);
})}

View File

@ -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 (
<Controller
name={field.key}
control={control}
render={({ field: rhf }) => (
<Form.Select
required={field.required}
value={rhf.value || ''}
onChange={(e) => rhf.onChange(e.target.value)}
>
<option value=""> -- Select {field.title} -- </option>
{Object.entries(options).map(([value, label]) => (
<option key={value} value={value}>
{label}
<Form.Select required={field.required} value={selected || ''} onChange={(e) => rhf.onChange(e.target.value)}>
{staticOptions.length ? null : <option value=""> -- Select {field.title} -- </option>}
{options.map((option, index) => (
<option key={`${option.value}-${index}`} value={option.value}>
{option.label}
</option>
))}
</Form.Select>
)}
/>
);
}

View File

@ -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 }))}
/>
</Col>
<Col md={2}>
<Form.Label>Notification Type</Form.Label>
<Form.Select
required
value={row.notification_type || 'normal'}
onChange={(e) => rhf.onChange(patchRow(rows, index, { notification_type: e.target.value }))}
>
<option value="normal">Normal</option>
<option value="overlay_custom">Overlay Custom</option>
</Form.Select>
</Col>
</Row>
</div>
);

View File

@ -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 (
<Controller
name={field.key}
control={control}
render={({ field: rhf }) => {
const rows = Array.isArray(rhf.value) ? rhf.value : [];
return (
<div>
<div className="d-flex justify-content-end mb-2">
<Button
variant="info"
size="sm"
type="button"
onClick={() => rhf.onChange([...rows, emptyRow()])}
>
Add New Template
</Button>
</div>
{rows.length === 0 ? <div className="text-muted fs-12">No templates</div> : null}
{rows.map((row, index) => (
<div key={index} className="border rounded p-3 mb-3">
<Row className="g-3">
<Col md={8}>
<S3FileField
field={{ type: 'image', allowedExtensions: ['png', 'jpg', 'jpeg'], uploadModule: 'personalization' }}
value={row.template || ''}
onChange={(url) => rhf.onChange(patchRow(rows, index, { template: url }))}
moduleName={moduleName}
onUploadingChange={onUploadingChange}
/>
</Col>
<Col md={3}>
<Form.Label>Include in PDF</Form.Label>
<Form.Select
className="mb-2"
value={row.createpdf || 'no'}
onChange={(e) => rhf.onChange(patchRow(rows, index, { createpdf: e.target.value }))}
>
{YES_NO.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Form.Select>
<Form.Label>Add Link to PDF</Form.Label>
<Form.Select
className="mb-2"
value={row.addlink || 'no'}
onChange={(e) => rhf.onChange(patchRow(rows, index, { addlink: e.target.value }))}
>
{YES_NO.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Form.Select>
<Form.Label>Link Field</Form.Label>
<Form.Select
value={row.linkfield || ''}
onChange={(e) => rhf.onChange(patchRow(rows, index, { linkfield: e.target.value }))}
>
<option value=""> -- Select Link Field -- </option>
{Object.entries(linkFields).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</Form.Select>
</Col>
<Col md={1} className="d-flex align-items-start pt-1">
<Button
variant="danger"
size="sm"
type="button"
onClick={() => rhf.onChange(rows.filter((_, i) => i !== index))}
>
×
</Button>
</Col>
<Col md={12}>
<Form.Control
as="textarea"
rows={4}
placeholder="Enter Position"
value={row.positions || ''}
onChange={(e) => rhf.onChange(patchRow(rows, index, { positions: e.target.value }))}
/>
</Col>
</Row>
</div>
))}
</div>
);
}}
/>
);
}

View File

@ -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);

View File

@ -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 },

View File

@ -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) => ({
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;

View File

@ -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 : [],
}),
};

View File

@ -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) => ({
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;
},
};

View File

@ -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),

View File

@ -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)

View File

@ -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;
},
};

View File

@ -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;
},