Changes
This commit is contained in:
parent
212cf0091d
commit
ef3bb15db5
|
|
@ -19,9 +19,11 @@ import {
|
||||||
TbListDetails,
|
TbListDetails,
|
||||||
TbPencil,
|
TbPencil,
|
||||||
TbPhoto,
|
TbPhoto,
|
||||||
|
TbPlayerPlay,
|
||||||
TbPlaylist,
|
TbPlaylist,
|
||||||
TbRefresh,
|
TbRefresh,
|
||||||
TbSchool,
|
TbSchool,
|
||||||
|
TbSitemap,
|
||||||
TbTrash,
|
TbTrash,
|
||||||
TbTrophy,
|
TbTrophy,
|
||||||
TbUsers,
|
TbUsers,
|
||||||
|
|
@ -96,6 +98,8 @@ const ACTION_ICONS = {
|
||||||
topic: TbBook,
|
topic: TbBook,
|
||||||
viewers: TbEye,
|
viewers: TbEye,
|
||||||
duplicate: TbCopy,
|
duplicate: TbCopy,
|
||||||
|
editor: TbSitemap,
|
||||||
|
monitor: TbPlayerPlay,
|
||||||
report: TbChartBar,
|
report: TbChartBar,
|
||||||
questions: TbHelpCircle,
|
questions: TbHelpCircle,
|
||||||
responses: TbPlaylist,
|
responses: TbPlaylist,
|
||||||
|
|
@ -238,6 +242,25 @@ export default function EntityList({ module: config }) {
|
||||||
};
|
};
|
||||||
}, [config.module]);
|
}, [config.module]);
|
||||||
|
|
||||||
|
const onDuplicate = async (row) => {
|
||||||
|
if (typeof config.duplicateItem !== 'function') return;
|
||||||
|
setBusyAction(`duplicate-${row.id}`);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const result = await config.duplicateItem(row);
|
||||||
|
if (!result?.ok) {
|
||||||
|
setError(result?.message || 'Unable to duplicate');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMessage(result.message || 'Duplicated');
|
||||||
|
load();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Unable to duplicate');
|
||||||
|
} finally {
|
||||||
|
setBusyAction('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onDelete = async (id) => {
|
const onDelete = async (id) => {
|
||||||
if (!window.confirm('Delete this record?')) return;
|
if (!window.confirm('Delete this record?')) return;
|
||||||
const path = `${config.module}/delete`;
|
const path = `${config.module}/delete`;
|
||||||
|
|
@ -404,6 +427,20 @@ export default function EntityList({ module: config }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (button.name === 'duplicate') {
|
if (button.name === 'duplicate') {
|
||||||
|
if (typeof config.duplicateItem === 'function') {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key="duplicate"
|
||||||
|
size="sm"
|
||||||
|
variant="soft-info"
|
||||||
|
title="Duplicate"
|
||||||
|
disabled={busyAction === `duplicate-${row.id}`}
|
||||||
|
onClick={() => onDuplicate(row)}
|
||||||
|
>
|
||||||
|
<TbCopy />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
const href =
|
const href =
|
||||||
typeof button.to === 'function' ? button.to(row) : button.to || `${config.baseUrl}/addnew?duplicate=${row.id}`;
|
typeof button.to === 'function' ? button.to(row) : button.to || `${config.baseUrl}/addnew?duplicate=${row.id}`;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ import {
|
||||||
|
|
||||||
const hasAny = (permissions, keys) => keys.some((key) => permissions.includes(key));
|
const hasAny = (permissions, keys) => keys.some((key) => permissions.includes(key));
|
||||||
|
|
||||||
|
const ZENOVA_DASHBOARD_ROLES = [
|
||||||
|
'1650360764-424692c0-654a-4af2-zenova-64ce55c66088', // HO
|
||||||
|
'1787808155-9945ee91-7a22-4be6-b184-733610e66ddc', // Admin
|
||||||
|
];
|
||||||
|
|
||||||
const child = (key, label, url, permission, extra = {}) => ({
|
const child = (key, label, url, permission, extra = {}) => ({
|
||||||
key,
|
key,
|
||||||
label,
|
label,
|
||||||
|
|
@ -109,6 +114,15 @@ export function getMenuItems({ permissions = [], roles = [], domain = '', email
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (domain === 'zenova' && roles.some((role) => ZENOVA_DASHBOARD_ROLES.includes(role))) {
|
||||||
|
items.push({
|
||||||
|
key: 'glipiq-sitazit',
|
||||||
|
label: 'Custom Dashboard',
|
||||||
|
icon: LuFileText,
|
||||||
|
url: '/zenovareport/dashboard',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (domain === 'pulmocare' && roles.includes('1650360330-5bf35594-674a-4f31-HO_Audit-47738d2151ab')) {
|
if (domain === 'pulmocare' && roles.includes('1650360330-5bf35594-674a-4f31-HO_Audit-47738d2151ab')) {
|
||||||
items.push({
|
items.push({
|
||||||
key: 'healthy-lung',
|
key: 'healthy-lung',
|
||||||
|
|
@ -266,23 +280,18 @@ export function getMenuItems({ permissions = [], roles = [], domain = '', email
|
||||||
'formaction_get_all',
|
'formaction_get_all',
|
||||||
'all_data_access',
|
'all_data_access',
|
||||||
'formexports_get_all',
|
'formexports_get_all',
|
||||||
'quiz_get_all',
|
|
||||||
'roles_leaderboard_get_all',
|
'roles_leaderboard_get_all',
|
||||||
'workflow_definition_get_all',
|
'workflow_definition_get_all',
|
||||||
'workflow_instance_get_all',
|
'workflow_instance_get_all',
|
||||||
'formproduct_get_all',
|
|
||||||
])
|
])
|
||||||
) {
|
) {
|
||||||
const formChildren = [
|
const formChildren = [
|
||||||
child('forms', 'Forms', '/formentitys', 'formentity_get_all'),
|
child('forms', 'Forms', '/formentitys', 'formentity_get_all'),
|
||||||
child('formhooks', 'Form Hook', '/formhooks', 'form_hooks_get_all'),
|
child('formhooks', 'Form Hook', '/formhooks', 'form_hooks_get_all'),
|
||||||
child('surveys', 'Survey', '/surveys', 'survey_get_all'),
|
|
||||||
child('quizs', 'Quiz', '/quizs', 'quiz_get_all'),
|
|
||||||
child('formactions', 'Quick Action', '/formactions', 'formaction_get_all'),
|
child('formactions', 'Quick Action', '/formactions', 'formaction_get_all'),
|
||||||
child('formexports', 'Form Data Export', '/formexports', 'formexports_get_all'),
|
child('formexports', 'Form Data Export', '/formexports', 'formexports_get_all'),
|
||||||
child('reminders', 'Reminders', '/reminders', 'reminder_get_all'),
|
child('reminders', 'Reminders', '/reminders', 'reminder_get_all'),
|
||||||
child('leaderboards', 'Leaderboard', '/leaderboards', 'roles_leaderboard_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));
|
].filter((item) => !item.permission || permissions.includes(item.permission));
|
||||||
|
|
||||||
if (hasAny(permissions, ['workflow_definition_get_all', 'workflow_instance_get_all'])) {
|
if (hasAny(permissions, ['workflow_definition_get_all', 'workflow_instance_get_all'])) {
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,7 @@ function splitHash(value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function formOptions() {
|
async function formOptions() {
|
||||||
const values = {};
|
return dropdownMap('form', 'formentity', (item) => item.title || item.name);
|
||||||
const [forms, surveys, quizzes] = await Promise.all([
|
|
||||||
dropdownMap('form', 'formentity', (item) => item.title || item.name),
|
|
||||||
dropdownMap('form', 'survey', (item) => `${item.title || item.name} (Survey)`),
|
|
||||||
dropdownMap('form', 'quiz', (item) => `${item.name || item.title} (Quiz)`),
|
|
||||||
]);
|
|
||||||
Object.assign(values, forms, surveys, quizzes);
|
|
||||||
return values;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const banners = {
|
const banners = {
|
||||||
|
|
@ -68,8 +61,6 @@ const banners = {
|
||||||
values: {
|
values: {
|
||||||
internal_action: 'Internal Action',
|
internal_action: 'Internal Action',
|
||||||
form: 'Web Form',
|
form: 'Web Form',
|
||||||
survey: 'Surveys',
|
|
||||||
quiz: 'Quizs',
|
|
||||||
doctorlist: 'Doctor List',
|
doctorlist: 'Doctor List',
|
||||||
external: 'External Webpage',
|
external: 'External Webpage',
|
||||||
external_with_token: 'External Webpage with Token',
|
external_with_token: 'External Webpage with Token',
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,7 @@ import { splitHash } from '@/helpers/metric';
|
||||||
const STATUS = { active: 'Active', disabled: 'Disabled' };
|
const STATUS = { active: 'Active', disabled: 'Disabled' };
|
||||||
|
|
||||||
async function formOptions() {
|
async function formOptions() {
|
||||||
const values = {};
|
return dropdownMap('form', 'formentity', (item) => item.title || item.name);
|
||||||
const [forms, surveys, quizzes] = await Promise.all([
|
|
||||||
dropdownMap('form', 'formentity', (item) => item.title || item.name),
|
|
||||||
dropdownMap('form', 'survey', (item) => `${item.title || item.name} (Survey)`),
|
|
||||||
dropdownMap('form', 'quiz', (item) => `${item.name || item.title} (Quiz)`),
|
|
||||||
]);
|
|
||||||
Object.assign(values, forms, surveys, quizzes);
|
|
||||||
return values;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinHash(value) {
|
function joinHash(value) {
|
||||||
|
|
@ -52,8 +45,6 @@ const formactions = {
|
||||||
required: true,
|
required: true,
|
||||||
values: {
|
values: {
|
||||||
form: 'Web Form',
|
form: 'Web Form',
|
||||||
survey: 'Surveys',
|
|
||||||
quiz: 'Quizs',
|
|
||||||
doctorlist: 'Doctor List',
|
doctorlist: 'Doctor List',
|
||||||
external: 'External Webpage',
|
external: 'External Webpage',
|
||||||
internal_action: 'Internal Action',
|
internal_action: 'Internal Action',
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { signedGet, isSuccess } from '@/api/client';
|
||||||
import { dropdownMap } from '@/api/dropdowns';
|
import { dropdownMap } from '@/api/dropdowns';
|
||||||
import {
|
import {
|
||||||
DEFAULT_INTERVAL_VALUES,
|
DEFAULT_INTERVAL_VALUES,
|
||||||
|
|
@ -10,6 +11,12 @@ import {
|
||||||
|
|
||||||
const STATUS = { active: 'Active', disabled: 'Disabled' };
|
const STATUS = { active: 'Active', disabled: 'Disabled' };
|
||||||
const YES_NO = { yes: 'Yes', no: 'No' };
|
const YES_NO = { yes: 'Yes', no: 'No' };
|
||||||
|
const MANAGER_LEVELS = {
|
||||||
|
l1manager: 'L1 Manager',
|
||||||
|
l2manager: 'L2 Manager',
|
||||||
|
l3manager: 'L3 Manager',
|
||||||
|
l4manager: 'L4 Manager',
|
||||||
|
};
|
||||||
|
|
||||||
function asArray(value) {
|
function asArray(value) {
|
||||||
if (Array.isArray(value)) return value.filter((part) => part !== null && part !== undefined && part !== '');
|
if (Array.isArray(value)) return value.filter((part) => part !== null && part !== undefined && part !== '');
|
||||||
|
|
@ -39,7 +46,17 @@ const leaderboards = {
|
||||||
sort_order: item.sort_order ?? 0,
|
sort_order: item.sort_order ?? 0,
|
||||||
})),
|
})),
|
||||||
formFields: async () => {
|
formFields: async () => {
|
||||||
const [metrics, roles] = await Promise.all([metricOptions(), dropdownMap('ums', 'roles', (item) => item.name)]);
|
const [metrics, roles, userAttrRes] = await Promise.all([
|
||||||
|
metricOptions(),
|
||||||
|
dropdownMap('ums', 'roles', (item) => item.name),
|
||||||
|
signedGet('ums', 'userattribute/get_all'),
|
||||||
|
]);
|
||||||
|
const userAttributes = {};
|
||||||
|
const userAttrItems = isSuccess(userAttrRes) ? userAttrRes.data?.items || [] : [];
|
||||||
|
userAttrItems.forEach((attr) => {
|
||||||
|
if (!attr?.code || attr.status === 'disabled') return;
|
||||||
|
userAttributes[attr.code] = attr.attribute || attr.code;
|
||||||
|
});
|
||||||
return [
|
return [
|
||||||
{ key: 'name', title: 'Name', type: 'text', required: true },
|
{ key: 'name', title: 'Name', type: 'text', required: true },
|
||||||
{ key: 'sort_order', title: 'Sort Order', type: 'number', required: true },
|
{ key: 'sort_order', title: 'Sort Order', type: 'number', required: true },
|
||||||
|
|
@ -68,6 +85,20 @@ const leaderboards = {
|
||||||
required: true,
|
required: true,
|
||||||
values: { all: 'All', loggedin: 'Logged In' },
|
values: { all: 'All', loggedin: 'Logged In' },
|
||||||
},
|
},
|
||||||
|
{ key: 'show_units_export', title: 'Show Units Export', type: 'select', required: true, values: YES_NO, default: 'yes' },
|
||||||
|
{ key: 'show_user_attributes', title: 'Show User Attributes', type: 'select', required: true, values: YES_NO, default: 'no' },
|
||||||
|
{
|
||||||
|
key: 'user_attributes',
|
||||||
|
title: 'Include User Attributes',
|
||||||
|
type: 'multiselect',
|
||||||
|
values: userAttributes,
|
||||||
|
default: [],
|
||||||
|
showIf: { show_user_attributes: 'yes' },
|
||||||
|
},
|
||||||
|
{ key: 'show_normal_metrics', title: 'Show Normal Metrics', type: 'select', required: true, values: YES_NO, default: 'yes' },
|
||||||
|
{ key: 'show_combined_metrics', title: 'Show Combined Metrics', type: 'select', required: true, values: YES_NO, default: 'yes' },
|
||||||
|
{ key: 'manager_details', title: 'Manager Details', type: 'multiselect', values: MANAGER_LEVELS, default: ['l1manager'] },
|
||||||
|
{ key: 'total_units_name', title: 'Total Units Name', type: 'text', default: '' },
|
||||||
{ key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
|
{ key: 'status', title: 'Status', type: 'select', required: true, values: STATUS },
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
@ -76,6 +107,14 @@ const leaderboards = {
|
||||||
next.sort_order = next.sort_order ?? 0;
|
next.sort_order = next.sort_order ?? 0;
|
||||||
next.show_positions = next.show_positions || 'no';
|
next.show_positions = next.show_positions || 'no';
|
||||||
next.show_positions_type = next.show_positions_type || 'all';
|
next.show_positions_type = next.show_positions_type || 'all';
|
||||||
|
next.show_units_export = next.show_units_export || 'yes';
|
||||||
|
next.show_user_attributes = next.show_user_attributes === 'yes' ? 'yes' : 'no';
|
||||||
|
next.user_attributes = asArray(next.user_attributes);
|
||||||
|
next.show_normal_metrics = next.show_normal_metrics || 'yes';
|
||||||
|
next.show_combined_metrics = next.show_combined_metrics || 'yes';
|
||||||
|
next.manager_details = asArray(next.manager_details);
|
||||||
|
if (!next.manager_details.length) next.manager_details = ['l1manager'];
|
||||||
|
next.total_units_name = next.total_units_name || '';
|
||||||
next.status = next.status || 'active';
|
next.status = next.status || 'active';
|
||||||
next.default_interval = next.default_interval ?? '';
|
next.default_interval = next.default_interval ?? '';
|
||||||
next.validity_start = toDateInput(next.validity_start);
|
next.validity_start = toDateInput(next.validity_start);
|
||||||
|
|
@ -119,6 +158,14 @@ const leaderboards = {
|
||||||
weight: row.weight === '' || row.weight == null ? '1' : String(row.weight),
|
weight: row.weight === '' || row.weight == null ? '1' : String(row.weight),
|
||||||
}));
|
}));
|
||||||
payload.combined_metrics = Array.isArray(payload.combined_metrics) ? payload.combined_metrics : [];
|
payload.combined_metrics = Array.isArray(payload.combined_metrics) ? payload.combined_metrics : [];
|
||||||
|
payload.show_units_export = payload.show_units_export || 'yes';
|
||||||
|
payload.show_user_attributes = payload.show_user_attributes === 'yes' ? 'yes' : 'no';
|
||||||
|
payload.user_attributes = asArray(payload.user_attributes);
|
||||||
|
payload.show_normal_metrics = payload.show_normal_metrics || 'yes';
|
||||||
|
payload.show_combined_metrics = payload.show_combined_metrics || 'yes';
|
||||||
|
payload.manager_details = asArray(payload.manager_details);
|
||||||
|
if (!payload.manager_details.length) payload.manager_details = ['l1manager'];
|
||||||
|
payload.total_units_name = payload.total_units_name || '';
|
||||||
return payload;
|
return payload;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,11 @@ import { signedGet, isSuccess } from '@/api/client';
|
||||||
import { dropdownMap } from '@/api/dropdowns';
|
import { dropdownMap } from '@/api/dropdowns';
|
||||||
|
|
||||||
const FILE_TYPES = ['image', 'video', 'pdf', 'audio', 'scorm'];
|
const FILE_TYPES = ['image', 'video', 'pdf', 'audio', 'scorm'];
|
||||||
const FORM_TYPES = ['form', 'quiz', 'survey'];
|
const FORM_TYPES = ['form'];
|
||||||
const LINK_TYPES = ['form', 'external'];
|
const LINK_TYPES = ['form', 'external'];
|
||||||
|
|
||||||
async function formOptions() {
|
async function formOptions() {
|
||||||
const values = {};
|
return dropdownMap('form', 'formentity', (item) => item.title || item.name);
|
||||||
const [forms, surveys, quizzes] = await Promise.all([
|
|
||||||
dropdownMap('form', 'formentity', (item) => item.title || item.name),
|
|
||||||
dropdownMap('form', 'survey', (item) => `${item.title || item.name} (Survey)`),
|
|
||||||
dropdownMap('form', 'quiz', (item) => `${item.name || item.title} (Quiz)`),
|
|
||||||
]);
|
|
||||||
Object.assign(values, forms, surveys, quizzes);
|
|
||||||
return values;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function metricOptions() {
|
async function metricOptions() {
|
||||||
|
|
@ -102,8 +95,6 @@ const magmedias = {
|
||||||
audio: 'Audio',
|
audio: 'Audio',
|
||||||
gallery: 'Gallery',
|
gallery: 'Gallery',
|
||||||
form: 'Form',
|
form: 'Form',
|
||||||
quiz: 'Quiz',
|
|
||||||
survey: 'Survey',
|
|
||||||
external: 'External Url',
|
external: 'External Url',
|
||||||
scorm: 'Scorm',
|
scorm: 'Scorm',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,12 @@ function normalizeCombined(row) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const YES_NO = { yes: 'Yes', no: 'No' };
|
const YES_NO = { yes: 'Yes', no: 'No' };
|
||||||
|
const MANAGER_LEVELS = {
|
||||||
|
l1manager: 'L1 Manager',
|
||||||
|
l2manager: 'L2 Manager',
|
||||||
|
l3manager: 'L3 Manager',
|
||||||
|
l4manager: 'L4 Manager',
|
||||||
|
};
|
||||||
|
|
||||||
const metriccampaigns = {
|
const metriccampaigns = {
|
||||||
service: 'metric',
|
service: 'metric',
|
||||||
|
|
@ -96,6 +102,7 @@ const metriccampaigns = {
|
||||||
signedGet('ums', 'userattribute/get_all'),
|
signedGet('ums', 'userattribute/get_all'),
|
||||||
]);
|
]);
|
||||||
const attributes = {};
|
const attributes = {};
|
||||||
|
const userAttributes = {};
|
||||||
const attributesMeta = {};
|
const attributesMeta = {};
|
||||||
const attrItems = isSuccess(attrsRes) ? attrsRes.data?.items || [] : [];
|
const attrItems = isSuccess(attrsRes) ? attrsRes.data?.items || [] : [];
|
||||||
attrItems.forEach((attr) => {
|
attrItems.forEach((attr) => {
|
||||||
|
|
@ -109,6 +116,7 @@ const metriccampaigns = {
|
||||||
options[value] = String(opt.display ?? value);
|
options[value] = String(opt.display ?? value);
|
||||||
});
|
});
|
||||||
attributes[code] = `${attr.attribute || code} (${code})`;
|
attributes[code] = `${attr.attribute || code} (${code})`;
|
||||||
|
userAttributes[code] = attr.attribute || code;
|
||||||
attributesMeta[code] = {
|
attributesMeta[code] = {
|
||||||
code,
|
code,
|
||||||
attribute: attr.attribute || code,
|
attribute: attr.attribute || code,
|
||||||
|
|
@ -171,6 +179,20 @@ const metriccampaigns = {
|
||||||
required: true,
|
required: true,
|
||||||
values: { all: 'All', loggedin: 'Logged In' },
|
values: { all: 'All', loggedin: 'Logged In' },
|
||||||
},
|
},
|
||||||
|
{ key: 'show_units_export', title: 'Show Units Export', type: 'select', required: true, values: YES_NO, default: 'yes' },
|
||||||
|
{ key: 'show_user_attributes', title: 'Show User Attributes', type: 'select', required: true, values: YES_NO, default: 'no' },
|
||||||
|
{
|
||||||
|
key: 'user_attributes',
|
||||||
|
title: 'Include User Attributes',
|
||||||
|
type: 'multiselect',
|
||||||
|
values: userAttributes,
|
||||||
|
default: [],
|
||||||
|
showIf: { show_user_attributes: 'yes' },
|
||||||
|
},
|
||||||
|
{ key: 'show_normal_metrics', title: 'Show Normal Metrics', type: 'select', required: true, values: YES_NO, default: 'yes' },
|
||||||
|
{ key: 'show_combined_metrics', title: 'Show Combined Metrics', type: 'select', required: true, values: YES_NO, default: 'yes' },
|
||||||
|
{ key: 'manager_details', title: 'Manager Details', type: 'multiselect', values: MANAGER_LEVELS, default: ['l1manager'] },
|
||||||
|
{ key: 'total_units_name', title: 'Total Units Name', type: 'text', default: '' },
|
||||||
{ key: 'highlight_top', title: 'Highlight Top', type: 'text', placeholder: 'e.g. 10, 20, 30' },
|
{ 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' },
|
{ key: 'highlight_color', title: 'Highlight Color', type: 'text', placeholder: 'R-G-B-A or CSS color' },
|
||||||
{
|
{
|
||||||
|
|
@ -214,6 +236,14 @@ const metriccampaigns = {
|
||||||
next.validity_end = toDateInput(next.validity_end);
|
next.validity_end = toDateInput(next.validity_end);
|
||||||
if (next.teams_rank) next.teams_rank = String(next.teams_rank).toLowerCase().slice(0, 2);
|
if (next.teams_rank) next.teams_rank = String(next.teams_rank).toLowerCase().slice(0, 2);
|
||||||
if (next.sort_order === '' || next.sort_order == null) next.sort_order = 0;
|
if (next.sort_order === '' || next.sort_order == null) next.sort_order = 0;
|
||||||
|
next.show_units_export = next.show_units_export || 'yes';
|
||||||
|
next.show_user_attributes = next.show_user_attributes === 'yes' ? 'yes' : 'no';
|
||||||
|
next.user_attributes = asArray(next.user_attributes);
|
||||||
|
next.show_normal_metrics = next.show_normal_metrics || 'yes';
|
||||||
|
next.show_combined_metrics = next.show_combined_metrics || 'yes';
|
||||||
|
next.manager_details = asArray(next.manager_details);
|
||||||
|
if (!next.manager_details.length) next.manager_details = ['l1manager'];
|
||||||
|
next.total_units_name = next.total_units_name || '';
|
||||||
return next;
|
return next;
|
||||||
},
|
},
|
||||||
beforeSave: (data) => {
|
beforeSave: (data) => {
|
||||||
|
|
@ -254,6 +284,13 @@ const metriccampaigns = {
|
||||||
reporting_roles: asArray(data.reporting_roles),
|
reporting_roles: asArray(data.reporting_roles),
|
||||||
intervals: asArray(data.intervals),
|
intervals: asArray(data.intervals),
|
||||||
sort_order: Number.parseInt(data.sort_order, 10) || 0,
|
sort_order: Number.parseInt(data.sort_order, 10) || 0,
|
||||||
|
show_units_export: data.show_units_export || 'yes',
|
||||||
|
show_user_attributes: data.show_user_attributes === 'yes' ? 'yes' : 'no',
|
||||||
|
user_attributes: asArray(data.user_attributes),
|
||||||
|
show_normal_metrics: data.show_normal_metrics || 'yes',
|
||||||
|
show_combined_metrics: data.show_combined_metrics || 'yes',
|
||||||
|
manager_details: asArray(data.manager_details).length ? asArray(data.manager_details) : ['l1manager'],
|
||||||
|
total_units_name: data.total_units_name || '',
|
||||||
validity_start: toApiDate(data.validity_start),
|
validity_start: toApiDate(data.validity_start),
|
||||||
validity_end: toApiDate(data.validity_end),
|
validity_end: toApiDate(data.validity_end),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -189,8 +189,10 @@ const metricviews = {
|
||||||
types: VIEW_COMBINED_TYPES,
|
types: VIEW_COMBINED_TYPES,
|
||||||
},
|
},
|
||||||
{ key: 'report_roles', title: 'Data Report from Roles', type: 'multiselect', required: true, values: roles },
|
{ key: 'report_roles', title: 'Data Report from Roles', type: 'multiselect', required: true, values: roles },
|
||||||
|
{ key: 'team_leaderboard_widget_roles', title: 'Team Leaderboard Widget Roles', type: 'multiselect', values: roles },
|
||||||
{ key: 'status', title: 'Status', type: 'select', required: true, values: { active: 'Active', disabled: 'Disabled' } },
|
{ key: 'status', title: 'Status', type: 'select', required: true, values: { active: 'Active', disabled: 'Disabled' } },
|
||||||
{ key: 'show_positions', title: 'Show Positions', type: 'select', required: true, values: { yes: 'Yes', no: 'No' } },
|
{ key: 'show_positions', title: 'Show Positions', type: 'select', required: true, values: { yes: 'Yes', no: 'No' } },
|
||||||
|
{ key: 'stateheadwise_leaderboard', title: 'Stateheadwise Leaderboard', type: 'select', required: true, values: { yes: 'Yes', no: 'No' }, default: 'no' },
|
||||||
{
|
{
|
||||||
key: 'show_positions_type',
|
key: 'show_positions_type',
|
||||||
title: 'Show Positions Type',
|
title: 'Show Positions Type',
|
||||||
|
|
@ -218,6 +220,7 @@ const metricviews = {
|
||||||
show_positions: 'yes',
|
show_positions: 'yes',
|
||||||
show_positions_type: 'all',
|
show_positions_type: 'all',
|
||||||
show_user_attributes: 'no',
|
show_user_attributes: 'no',
|
||||||
|
stateheadwise_leaderboard: 'no',
|
||||||
user_attributes: [],
|
user_attributes: [],
|
||||||
list_chart_type: 'progressbar',
|
list_chart_type: 'progressbar',
|
||||||
value_views: ['value'],
|
value_views: ['value'],
|
||||||
|
|
@ -226,6 +229,7 @@ const metricviews = {
|
||||||
};
|
};
|
||||||
next.designations = asArray(next.designations);
|
next.designations = asArray(next.designations);
|
||||||
next.report_roles = asArray(next.report_roles);
|
next.report_roles = asArray(next.report_roles);
|
||||||
|
next.team_leaderboard_widget_roles = asArray(next.team_leaderboard_widget_roles);
|
||||||
next.graphical_views = asArray(next.graphical_views);
|
next.graphical_views = asArray(next.graphical_views);
|
||||||
next.details_views = asArray(next.details_views);
|
next.details_views = asArray(next.details_views);
|
||||||
next.trend_views = asArray(next.trend_views);
|
next.trend_views = asArray(next.trend_views);
|
||||||
|
|
@ -241,6 +245,7 @@ const metricviews = {
|
||||||
next.intervals = asArray(next.intervals);
|
next.intervals = asArray(next.intervals);
|
||||||
next.contact_attributes = asArray(next.contact_attributes);
|
next.contact_attributes = asArray(next.contact_attributes);
|
||||||
next.show_user_attributes = next.show_user_attributes === 'yes' ? 'yes' : 'no';
|
next.show_user_attributes = next.show_user_attributes === 'yes' ? 'yes' : 'no';
|
||||||
|
next.stateheadwise_leaderboard = next.stateheadwise_leaderboard === 'yes' ? 'yes' : 'no';
|
||||||
next.user_attributes = asArray(next.user_attributes);
|
next.user_attributes = asArray(next.user_attributes);
|
||||||
next.combined_metrics = parseCombinedMetrics(next.combined_metrics);
|
next.combined_metrics = parseCombinedMetrics(next.combined_metrics);
|
||||||
next.validity_start = toDateInput(next.validity_start);
|
next.validity_start = toDateInput(next.validity_start);
|
||||||
|
|
@ -299,6 +304,7 @@ const metricviews = {
|
||||||
metrics,
|
metrics,
|
||||||
designations: asArray(data.designations),
|
designations: asArray(data.designations),
|
||||||
report_roles: asArray(data.report_roles),
|
report_roles: asArray(data.report_roles),
|
||||||
|
team_leaderboard_widget_roles: asArray(data.team_leaderboard_widget_roles),
|
||||||
graphical_views: asArray(data.graphical_views),
|
graphical_views: asArray(data.graphical_views),
|
||||||
details_views: asArray(data.details_views),
|
details_views: asArray(data.details_views),
|
||||||
trend_views: asArray(data.trend_views),
|
trend_views: asArray(data.trend_views),
|
||||||
|
|
@ -310,6 +316,7 @@ const metricviews = {
|
||||||
intervals: asArray(data.intervals),
|
intervals: asArray(data.intervals),
|
||||||
contact_attributes: asArray(data.contact_attributes),
|
contact_attributes: asArray(data.contact_attributes),
|
||||||
show_user_attributes: data.show_user_attributes === 'yes' ? 'yes' : 'no',
|
show_user_attributes: data.show_user_attributes === 'yes' ? 'yes' : 'no',
|
||||||
|
stateheadwise_leaderboard: data.stateheadwise_leaderboard === 'yes' ? 'yes' : 'no',
|
||||||
user_attributes: asArray(data.user_attributes),
|
user_attributes: asArray(data.user_attributes),
|
||||||
combined_metrics: stringifyCombinedMetrics(
|
combined_metrics: stringifyCombinedMetrics(
|
||||||
(data.combined_metrics || [])
|
(data.combined_metrics || [])
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@ import { metricOptions, toDateInput } from '@/helpers/metric';
|
||||||
|
|
||||||
const STATUS = { active: 'Active', disabled: 'Disabled' };
|
const STATUS = { active: 'Active', disabled: 'Disabled' };
|
||||||
const YES_NO = { yes: 'Yes', no: 'No' };
|
const YES_NO = { yes: 'Yes', no: 'No' };
|
||||||
|
const MANAGER_LEVELS = {
|
||||||
|
l1manager: 'L1 Manager',
|
||||||
|
l2manager: 'L2 Manager',
|
||||||
|
l3manager: 'L3 Manager',
|
||||||
|
l4manager: 'L4 Manager',
|
||||||
|
};
|
||||||
const DAYS = {
|
const DAYS = {
|
||||||
MON: 'Monday',
|
MON: 'Monday',
|
||||||
TUE: 'Tuesday',
|
TUE: 'Tuesday',
|
||||||
|
|
@ -153,7 +159,7 @@ const milestones = {
|
||||||
{ key: 'status', title: 'Status', type: 'select' },
|
{ key: 'status', title: 'Status', type: 'select' },
|
||||||
],
|
],
|
||||||
formFields: async () => {
|
formFields: async () => {
|
||||||
const [metrics, roles, badges, categories, images] = await Promise.all([
|
const [metrics, roles, badges, categories, images, userAttrRes] = await Promise.all([
|
||||||
metricOptions(),
|
metricOptions(),
|
||||||
dropdownMap('ums', 'roles', (item) => item.name),
|
dropdownMap('ums', 'roles', (item) => item.name),
|
||||||
getAllItems('recognition', 'badge_list/get_all').then(titleMap),
|
getAllItems('recognition', 'badge_list/get_all').then(titleMap),
|
||||||
|
|
@ -165,7 +171,14 @@ const milestones = {
|
||||||
});
|
});
|
||||||
return values;
|
return values;
|
||||||
}),
|
}),
|
||||||
|
signedGet('ums', 'userattribute/get_all'),
|
||||||
]);
|
]);
|
||||||
|
const userAttributes = {};
|
||||||
|
const userAttrItems = isSuccess(userAttrRes) ? userAttrRes.data?.items || [] : [];
|
||||||
|
userAttrItems.forEach((attr) => {
|
||||||
|
if (!attr?.code || attr.status === 'disabled') return;
|
||||||
|
userAttributes[attr.code] = attr.attribute || attr.code;
|
||||||
|
});
|
||||||
return [
|
return [
|
||||||
{ key: 'title', title: 'Title', type: 'text', required: true },
|
{ key: 'title', title: 'Title', type: 'text', required: true },
|
||||||
{ key: 'interval_start_date', title: 'Interval Start Date', type: 'date', required: true },
|
{ key: 'interval_start_date', title: 'Interval Start Date', type: 'date', required: true },
|
||||||
|
|
@ -195,6 +208,20 @@ const milestones = {
|
||||||
required: true,
|
required: true,
|
||||||
values: { all: 'All', loggedin: 'Logged In' },
|
values: { all: 'All', loggedin: 'Logged In' },
|
||||||
},
|
},
|
||||||
|
{ key: 'show_units_export', title: 'Show Units Export', type: 'select', required: true, values: YES_NO, default: 'yes' },
|
||||||
|
{ key: 'show_user_attributes', title: 'Show User Attributes', type: 'select', required: true, values: YES_NO, default: 'no' },
|
||||||
|
{
|
||||||
|
key: 'user_attributes',
|
||||||
|
title: 'Include User Attributes',
|
||||||
|
type: 'multiselect',
|
||||||
|
values: userAttributes,
|
||||||
|
default: [],
|
||||||
|
showIf: { show_user_attributes: 'yes' },
|
||||||
|
},
|
||||||
|
{ key: 'show_normal_metrics', title: 'Show Normal Metrics', type: 'select', required: true, values: YES_NO, default: 'yes' },
|
||||||
|
{ key: 'show_combined_metrics', title: 'Show Combined Metrics', type: 'select', required: true, values: YES_NO, default: 'yes' },
|
||||||
|
{ key: 'manager_details', title: 'Manager Details', type: 'multiselect', values: MANAGER_LEVELS, default: ['l1manager'] },
|
||||||
|
{ key: 'total_units_name', title: 'Total Units Name', type: 'text', default: '' },
|
||||||
{ key: 'send_report', title: 'Send Report', type: 'select', required: true, values: YES_NO },
|
{ key: 'send_report', title: 'Send Report', type: 'select', required: true, values: YES_NO },
|
||||||
{
|
{
|
||||||
key: 'performance_period',
|
key: 'performance_period',
|
||||||
|
|
@ -327,6 +354,14 @@ const milestones = {
|
||||||
next.show_target_average = fromBool(reportConfig.show_target_average ?? false);
|
next.show_target_average = fromBool(reportConfig.show_target_average ?? false);
|
||||||
next.show_overall_average = fromBool(reportConfig.show_overall_average ?? false);
|
next.show_overall_average = fromBool(reportConfig.show_overall_average ?? false);
|
||||||
next.send_report = fromBool(next.send_report ?? false);
|
next.send_report = fromBool(next.send_report ?? false);
|
||||||
|
next.show_units_export = next.show_units_export || 'yes';
|
||||||
|
next.show_user_attributes = next.show_user_attributes === 'yes' ? 'yes' : 'no';
|
||||||
|
next.user_attributes = toStringList(next.user_attributes);
|
||||||
|
next.show_normal_metrics = next.show_normal_metrics || 'yes';
|
||||||
|
next.show_combined_metrics = next.show_combined_metrics || '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;
|
return next;
|
||||||
},
|
},
|
||||||
beforeSave: (data) => {
|
beforeSave: (data) => {
|
||||||
|
|
@ -340,6 +375,14 @@ const milestones = {
|
||||||
payload.milestones = mapMilestonesForSave(payload.milestones);
|
payload.milestones = mapMilestonesForSave(payload.milestones);
|
||||||
|
|
||||||
const daysOrDates = payload.days_or_dates || 'days';
|
const daysOrDates = payload.days_or_dates || 'days';
|
||||||
|
payload.show_units_export = payload.show_units_export || 'yes';
|
||||||
|
payload.show_user_attributes = payload.show_user_attributes === 'yes' ? 'yes' : 'no';
|
||||||
|
payload.user_attributes = toStringList(payload.user_attributes);
|
||||||
|
payload.show_normal_metrics = payload.show_normal_metrics || 'yes';
|
||||||
|
payload.show_combined_metrics = payload.show_combined_metrics || 'yes';
|
||||||
|
payload.manager_details = toStringList(payload.manager_details);
|
||||||
|
if (!payload.manager_details.length) payload.manager_details = ['l1manager'];
|
||||||
|
payload.total_units_name = payload.total_units_name || '';
|
||||||
payload.send_report = toBool(payload.send_report ?? 'no');
|
payload.send_report = toBool(payload.send_report ?? 'no');
|
||||||
payload.report_config = {
|
payload.report_config = {
|
||||||
days_or_dates: daysOrDates,
|
days_or_dates: daysOrDates,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const roles = {
|
||||||
baseUrl: '/role',
|
baseUrl: '/role',
|
||||||
listPath: '/roles',
|
listPath: '/roles',
|
||||||
listFields: [
|
listFields: [
|
||||||
{ key: 'actions', title: 'Actions', buttons: ['edit', 'delete'] },
|
{ key: 'actions', title: 'Actions', buttons: ['edit', 'duplicate', 'delete'] },
|
||||||
{ key: 'name', title: 'Role Name', type: 'text' },
|
{ key: 'name', title: 'Role Name', type: 'text' },
|
||||||
{ key: 'code', title: 'Role Code', type: 'text' },
|
{ key: 'code', title: 'Role Code', type: 'text' },
|
||||||
{ key: 'status', title: 'Status', type: 'select' },
|
{ key: 'status', title: 'Status', type: 'select' },
|
||||||
|
|
@ -31,14 +31,20 @@ const roles = {
|
||||||
{ key: 'permissions', title: 'permissions', type: 'bigmultiselect', required: true, values: permissions, fullwidth: true },
|
{ key: 'permissions', title: 'permissions', type: 'bigmultiselect', required: true, values: permissions, fullwidth: true },
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
beforeForm: (item) => ({
|
beforeForm: (item, _mode, context) => {
|
||||||
...item,
|
const next = {
|
||||||
permissions: Array.isArray(item.permissions)
|
...item,
|
||||||
? item.permissions.map(String)
|
permissions: Array.isArray(item.permissions)
|
||||||
: String(item.permissions || '')
|
? item.permissions.map(String)
|
||||||
.split(/[#~,]/)
|
: String(item.permissions || '')
|
||||||
.filter(Boolean),
|
.split(/[#~,]/)
|
||||||
}),
|
.filter(Boolean),
|
||||||
|
};
|
||||||
|
if (context?.duplicate && next.code && !String(next.code).endsWith('-copy')) {
|
||||||
|
next.code = `${next.code}-copy`;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default roles;
|
export default roles;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { isSuccess, signedGet } from '@/api/client';
|
import { isSuccess, signedGet, signedPost } from '@/api/client';
|
||||||
|
|
||||||
const workflows = {
|
const workflows = {
|
||||||
service: 'workflow',
|
service: 'workflow',
|
||||||
|
|
@ -15,11 +15,13 @@ const workflows = {
|
||||||
'edit',
|
'edit',
|
||||||
'delete',
|
'delete',
|
||||||
{ name: 'editor', to: (row) => `/workflow/editor?id=${row.id}`, title: 'Editor' },
|
{ name: 'editor', to: (row) => `/workflow/editor?id=${row.id}`, title: 'Editor' },
|
||||||
{ name: 'monitor', to: (row) => `/workflow/monitor?definition_id=${row.id}`, title: 'Monitor' },
|
{ name: 'monitor', to: (row) => `/workflow/monitor?definition_id=${row.id}`, title: 'Runs' },
|
||||||
|
'duplicate',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ key: 'name', title: 'Name', type: 'text' },
|
{ key: 'name', title: 'Name', type: 'text' },
|
||||||
{ key: 'entity', title: 'Entity', type: 'text' },
|
{ key: 'entity', title: 'Entity', type: 'text' },
|
||||||
|
{ key: 'run_on', title: 'Run on', type: 'text' },
|
||||||
{ key: 'published_version', title: 'Published', type: 'text' },
|
{ key: 'published_version', title: 'Published', type: 'text' },
|
||||||
{ key: 'status', title: 'Status', type: 'text' },
|
{ key: 'status', title: 'Status', type: 'text' },
|
||||||
],
|
],
|
||||||
|
|
@ -54,6 +56,15 @@ const workflows = {
|
||||||
values: forms,
|
values: forms,
|
||||||
showIf: { entity: 'form_submission' },
|
showIf: { entity: 'form_submission' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'run_on',
|
||||||
|
title: 'Run on',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
values: { insert: 'Insert', update: 'Update', both: 'Both' },
|
||||||
|
default: 'insert',
|
||||||
|
showIf: { entity: 'form_submission' },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
beforeForm: (item) => ({
|
beforeForm: (item) => ({
|
||||||
|
|
@ -61,10 +72,33 @@ const workflows = {
|
||||||
entity: item.entity || 'manual',
|
entity: item.entity || 'manual',
|
||||||
description: item.description || '',
|
description: item.description || '',
|
||||||
form_id: item.form_id || '',
|
form_id: item.form_id || '',
|
||||||
|
run_on: item.id ? (item.run_on || '') : (item.run_on || 'insert'),
|
||||||
}),
|
}),
|
||||||
|
duplicateItem: async (row) => {
|
||||||
|
const response = await signedGet('workflow', 'workflow_definition/get_one', { id: row.id });
|
||||||
|
if (!isSuccess(response)) return { ok: false, message: response?.message || 'Unable to load workflow' };
|
||||||
|
const item = response.data?.item || {};
|
||||||
|
const created = await signedPost('workflow', 'workflow_definition/create', {
|
||||||
|
name: `${item.name || 'Workflow'} (Copy)`,
|
||||||
|
description: item.description || '',
|
||||||
|
entity: item.entity || 'manual',
|
||||||
|
form_id: item.entity === 'form_submission' ? item.form_id || '' : '',
|
||||||
|
...(item.entity === 'form_submission' ? { run_on: item.run_on || '' } : {}),
|
||||||
|
nodes: item.nodes || [],
|
||||||
|
edges: item.edges || [],
|
||||||
|
layout: item.layout || {},
|
||||||
|
});
|
||||||
|
if (!isSuccess(created)) return { ok: false, message: created?.message || 'Unable to duplicate workflow' };
|
||||||
|
return { ok: true, message: 'Workflow duplicated' };
|
||||||
|
},
|
||||||
beforeSave: (data) => {
|
beforeSave: (data) => {
|
||||||
const payload = { ...data };
|
const payload = { ...data };
|
||||||
if (payload.entity !== 'form_submission') payload.form_id = '';
|
if (payload.entity !== 'form_submission') {
|
||||||
|
payload.form_id = '';
|
||||||
|
delete payload.run_on;
|
||||||
|
} else if (!payload.run_on) {
|
||||||
|
delete payload.run_on;
|
||||||
|
}
|
||||||
return payload;
|
return payload;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,10 @@ import milestones from '@/modules/milestones';
|
||||||
import formentitys from '@/modules/formentitys';
|
import formentitys from '@/modules/formentitys';
|
||||||
import formquestions from '@/modules/formquestions';
|
import formquestions from '@/modules/formquestions';
|
||||||
import formhooks from '@/modules/formhooks';
|
import formhooks from '@/modules/formhooks';
|
||||||
import surveys from '@/modules/surveys';
|
// import surveys from '@/modules/surveys';
|
||||||
import surveyquestions from '@/modules/surveyquestions';
|
// import surveyquestions from '@/modules/surveyquestions';
|
||||||
import quizs from '@/modules/quizs';
|
// import quizs from '@/modules/quizs';
|
||||||
import quizquestions from '@/modules/quizquestions';
|
// import quizquestions from '@/modules/quizquestions';
|
||||||
import formactions from '@/modules/formactions';
|
import formactions from '@/modules/formactions';
|
||||||
import formexports from '@/modules/formexports';
|
import formexports from '@/modules/formexports';
|
||||||
import reminders from '@/modules/reminders';
|
import reminders from '@/modules/reminders';
|
||||||
|
|
@ -56,7 +56,7 @@ import rcpaproductattributes from '@/modules/rcpaproductattributes';
|
||||||
import brands from '@/modules/brands';
|
import brands from '@/modules/brands';
|
||||||
import products from '@/modules/products';
|
import products from '@/modules/products';
|
||||||
import productattributes from '@/modules/productattributes';
|
import productattributes from '@/modules/productattributes';
|
||||||
import formproducts from '@/modules/formproducts';
|
// import formproducts from '@/modules/formproducts';
|
||||||
import formpersonalizations from '@/modules/formpersonalizations';
|
import formpersonalizations from '@/modules/formpersonalizations';
|
||||||
import courses from '@/modules/courses';
|
import courses from '@/modules/courses';
|
||||||
import lessons from '@/modules/lessons';
|
import lessons from '@/modules/lessons';
|
||||||
|
|
@ -169,10 +169,10 @@ export const routes = [
|
||||||
...moduleRoutes(formentitys),
|
...moduleRoutes(formentitys),
|
||||||
...moduleRoutes(formquestions),
|
...moduleRoutes(formquestions),
|
||||||
...moduleRoutes(formhooks),
|
...moduleRoutes(formhooks),
|
||||||
...moduleRoutes(surveys),
|
// ...moduleRoutes(surveys),
|
||||||
...moduleRoutes(surveyquestions),
|
// ...moduleRoutes(surveyquestions),
|
||||||
...moduleRoutes(quizs),
|
// ...moduleRoutes(quizs),
|
||||||
...moduleRoutes(quizquestions),
|
// ...moduleRoutes(quizquestions),
|
||||||
...moduleRoutes(formactions),
|
...moduleRoutes(formactions),
|
||||||
...moduleRoutes(formexports),
|
...moduleRoutes(formexports),
|
||||||
...moduleRoutes(reminders),
|
...moduleRoutes(reminders),
|
||||||
|
|
@ -193,7 +193,7 @@ export const routes = [
|
||||||
...moduleRoutes(brands),
|
...moduleRoutes(brands),
|
||||||
...moduleRoutes(products),
|
...moduleRoutes(products),
|
||||||
...moduleRoutes(productattributes),
|
...moduleRoutes(productattributes),
|
||||||
...moduleRoutes(formproducts),
|
// ...moduleRoutes(formproducts),
|
||||||
...moduleRoutes(formpersonalizations),
|
...moduleRoutes(formpersonalizations),
|
||||||
...moduleRoutes(courses),
|
...moduleRoutes(courses),
|
||||||
...moduleRoutes(lessons),
|
...moduleRoutes(lessons),
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,50 @@ import {
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import { Button, Card, Col, Container, Form, Modal, Row, Spinner } from 'react-bootstrap';
|
import { Button, Card, Col, Container, Form, Modal, Row, Spinner } from 'react-bootstrap';
|
||||||
|
import Select from 'react-select';
|
||||||
import PageBreadcrumb from '@/components/PageBreadcrumb';
|
import PageBreadcrumb from '@/components/PageBreadcrumb';
|
||||||
import { isSuccess, signedGet, signedPost } from '@/api/client';
|
import { isSuccess, signedGet, signedPost } from '@/api/client';
|
||||||
import { defaultConfig, fromFlow, labelFor, toFlow, NODE_TYPES, edgeLabel } from '@/views/workflow/graph';
|
import { dropdownMap } from '@/api/dropdowns';
|
||||||
|
import { defaultConfig, fromFlow, labelFor, toFlow, NODE_TYPES, edgeLabel, contactMappings } from '@/views/workflow/graph';
|
||||||
|
|
||||||
function newId() {
|
function newId() {
|
||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MANAGER_LEVELS = [
|
||||||
|
{ value: 'l1manager', label: 'L1 Manager' },
|
||||||
|
{ value: 'l2manager', label: 'L2 Manager' },
|
||||||
|
{ value: 'l3manager', label: 'L3 Manager' },
|
||||||
|
{ value: 'l4manager', label: 'L4 Manager' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function approverGroups(roles, users) {
|
||||||
|
return [
|
||||||
|
{ label: 'Manager', options: MANAGER_LEVELS },
|
||||||
|
{ label: 'Roles', options: Object.entries(roles).map(([value, label]) => ({ value, label })) },
|
||||||
|
{ label: 'Users', options: Object.entries(users).map(([value, label]) => ({ value, label })) },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApproverSelect({ values, groups, onChange }) {
|
||||||
|
const known = groups.flatMap((group) => group.options || []);
|
||||||
|
const saved = (values || []).filter((value) => !known.some((option) => option.value === value)).map((value) => ({ value, label: value }));
|
||||||
|
const options = saved.length ? groups.concat({ label: 'Saved', options: saved }) : groups;
|
||||||
|
const byValue = new Map(known.concat(saved).map((option) => [option.value, option]));
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
isMulti
|
||||||
|
options={options}
|
||||||
|
classNamePrefix="react-select"
|
||||||
|
menuPosition="fixed"
|
||||||
|
menuPortalTarget={typeof document !== 'undefined' ? document.body : null}
|
||||||
|
styles={{ menuPortal: (base) => ({ ...base, zIndex: 2000 }) }}
|
||||||
|
value={(values || []).map((value) => byValue.get(value)).filter(Boolean)}
|
||||||
|
onChange={(selected) => onChange((selected || []).map((option) => option.value))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function WorkflowEditor() {
|
export default function WorkflowEditor() {
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
const id = params.get('id') || '';
|
const id = params.get('id') || '';
|
||||||
|
|
@ -35,6 +71,7 @@ export default function WorkflowEditor() {
|
||||||
const [contextText, setContextText] = useState('{}');
|
const [contextText, setContextText] = useState('{}');
|
||||||
const [hierarchyId, setHierarchyId] = useState('');
|
const [hierarchyId, setHierarchyId] = useState('');
|
||||||
const [testOpen, setTestOpen] = useState(false);
|
const [testOpen, setTestOpen] = useState(false);
|
||||||
|
const [approverOptions, setApproverOptions] = useState(() => approverGroups({}, {}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
|
|
@ -56,6 +93,27 @@ export default function WorkflowEditor() {
|
||||||
if (id) load();
|
if (id) load();
|
||||||
}, [id, setEdges, setNodes]);
|
}, [id, setEdges, setNodes]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadApprovers = async () => {
|
||||||
|
try {
|
||||||
|
const [roles, usersRes] = await Promise.all([
|
||||||
|
dropdownMap('ums', 'roles', (role) => role.name),
|
||||||
|
signedGet('ums', 'users/dropdown'),
|
||||||
|
]);
|
||||||
|
const users = {};
|
||||||
|
const items = isSuccess(usersRes) ? usersRes.data?.items || [] : [];
|
||||||
|
items.forEach((user) => {
|
||||||
|
if (!user?.id) return;
|
||||||
|
users[user.id] = `${user.firstname || ''} ${user.lastname || ''} (${user.employee_code || ''})`.trim();
|
||||||
|
});
|
||||||
|
setApproverOptions(approverGroups(roles || {}, users));
|
||||||
|
} catch {
|
||||||
|
setApproverOptions(approverGroups({}, {}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadApprovers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const selectedNode = useMemo(
|
const selectedNode = useMemo(
|
||||||
() => nodes.find((node) => node.id === selected && node.data),
|
() => nodes.find((node) => node.id === selected && node.data),
|
||||||
[nodes, selected]
|
[nodes, selected]
|
||||||
|
|
@ -251,15 +309,15 @@ export default function WorkflowEditor() {
|
||||||
<Background />
|
<Background />
|
||||||
{selectedNode || selectedEdge ? (
|
{selectedNode || selectedEdge ? (
|
||||||
<Panel position="top-right">
|
<Panel position="top-right">
|
||||||
<Card style={{ width: 320 }}>
|
<Card className="d-flex flex-column" style={{ width: 360, maxHeight: 'calc(70vh - 24px)', overflow: 'hidden' }}>
|
||||||
<Card.Header className="d-flex align-items-center justify-content-between">
|
<Card.Header className="d-flex align-items-center justify-content-between flex-shrink-0">
|
||||||
<span>{selectedNode ? selectedNode.data.label : 'Relation'}</span>
|
<span>{selectedNode ? selectedNode.data.label : 'Relation'}</span>
|
||||||
<button type="button" className="btn-close" aria-label="Close" onClick={() => setSelected(null)} />
|
<button type="button" className="btn-close" aria-label="Close" onClick={() => setSelected(null)} />
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Body>
|
<Card.Body className="overflow-auto" style={{ minHeight: 0 }}>
|
||||||
{selectedNode ? (
|
{selectedNode ? (
|
||||||
<>
|
<>
|
||||||
<NodeForm node={selectedNode} onChange={patchNode} />
|
<NodeForm node={selectedNode} approverOptions={approverOptions} onChange={patchNode} />
|
||||||
<Button variant="outline-danger" className="mt-3" onClick={() => deleteNode(selectedNode.id)}>
|
<Button variant="outline-danger" className="mt-3" onClick={() => deleteNode(selectedNode.id)}>
|
||||||
Delete node
|
Delete node
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -305,7 +363,7 @@ export default function WorkflowEditor() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NodeForm({ node, onChange }) {
|
function NodeForm({ node, approverOptions, onChange }) {
|
||||||
const config = node.data.config || {};
|
const config = node.data.config || {};
|
||||||
const type = node.data.nodeType;
|
const type = node.data.nodeType;
|
||||||
if (type === 'approval') {
|
if (type === 'approval') {
|
||||||
|
|
@ -321,11 +379,12 @@ function NodeForm({ node, onChange }) {
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group className="mb-2">
|
<Form.Group className="mb-2">
|
||||||
<Form.Label>Approvers</Form.Label>
|
<Form.Label>Approvers</Form.Label>
|
||||||
<Form.Control
|
<ApproverSelect
|
||||||
value={(config.approvers || []).join(', ')}
|
values={config.approvers || []}
|
||||||
onChange={(event) => onChange({ approvers: event.target.value.split(',').map((part) => part.trim()).filter(Boolean) })}
|
groups={approverOptions}
|
||||||
|
onChange={(approvers) => onChange({ approvers })}
|
||||||
/>
|
/>
|
||||||
<Form.Text>l1manager to l4manager, a role id, or a user id.</Form.Text>
|
<Form.Text>Pick manager levels, roles, or users. In Sequential mode, the order selected is the approval order.</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
label="Escalate when the approver seat is vacant"
|
label="Escalate when the approver seat is vacant"
|
||||||
|
|
@ -356,19 +415,195 @@ function NodeForm({ node, onChange }) {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (type === 'contact') {
|
||||||
|
const mappings = contactMappings(config);
|
||||||
|
const updateRow = (index, patch) => {
|
||||||
|
const next = mappings.map((row, rowIndex) => (rowIndex === index ? { ...row, ...patch } : row));
|
||||||
|
onChange({ mappings: next.map(({ contact, source }) => ({ contact, source })) });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="text-muted">Map each contact field to a field on the incoming object. If that field is not on the object, the text is saved as the value. Phone or email is enough. Application and domain come from this workflow. Position comes from the hierarchy, and user info is built from the user.</p>
|
||||||
|
{mappings.map((row, index) => (
|
||||||
|
<div className="mb-2" key={`${row.contact}-${index}`}>
|
||||||
|
<div className="d-flex gap-2">
|
||||||
|
<Form.Control
|
||||||
|
placeholder="Contact field"
|
||||||
|
value={row.contact || ''}
|
||||||
|
disabled={row.locked}
|
||||||
|
onChange={(event) => updateRow(index, { contact: event.target.value })}
|
||||||
|
/>
|
||||||
|
{row.contact === 'msl_code' ? (
|
||||||
|
<Form.Select
|
||||||
|
value={config.msl_mode === 'auto' ? 'auto' : 'map'}
|
||||||
|
onChange={(event) => onChange({ msl_mode: event.target.value })}
|
||||||
|
>
|
||||||
|
<option value="map">Map field</option>
|
||||||
|
<option value="auto">Autocreate</option>
|
||||||
|
</Form.Select>
|
||||||
|
) : (
|
||||||
|
<Form.Control
|
||||||
|
placeholder={row.optional ? 'Incoming field' : 'Incoming field *'}
|
||||||
|
value={row.source || ''}
|
||||||
|
onChange={(event) => updateRow(index, { source: event.target.value })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{row.locked && !row.removable ? <span style={{ width: 38 }} /> : (
|
||||||
|
<Button
|
||||||
|
variant="outline-danger"
|
||||||
|
onClick={() => onChange({ mappings: mappings.filter((_, rowIndex) => rowIndex !== index).map(({ contact, source }) => ({ contact, source })) })}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{row.contact === 'msl_code' && config.msl_mode === 'auto' ? (
|
||||||
|
<div className="d-flex gap-2 mt-2">
|
||||||
|
<Form.Control placeholder="Prefix" value={config.msl_prefix || ''} onChange={(event) => onChange({ msl_prefix: event.target.value })} />
|
||||||
|
<Form.Control placeholder="Suffix" value={config.msl_suffix || ''} onChange={(event) => onChange({ msl_suffix: event.target.value })} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{row.contact === 'msl_code' && config.msl_mode !== 'auto' ? (
|
||||||
|
<Form.Control
|
||||||
|
className="mt-2"
|
||||||
|
placeholder="Incoming field *"
|
||||||
|
value={row.source || ''}
|
||||||
|
onChange={(event) => updateRow(index, { source: event.target.value })}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button variant="outline-secondary" onClick={() => onChange({ mappings: mappings.concat({ contact: '', source: '' }).map(({ contact, source }) => ({ contact, source })) })}>
|
||||||
|
Add field
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'ocr') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Group className="mb-2">
|
||||||
|
<Form.Label>Document</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
placeholder="visiting_card"
|
||||||
|
value={config.source || ''}
|
||||||
|
onChange={(event) => onChange({ source: event.target.value })}
|
||||||
|
/>
|
||||||
|
<Form.Text>Incoming field or a document URL. Textract reads the file. A missing field is skipped.</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Check
|
||||||
|
className="mb-2"
|
||||||
|
label="Mandatory"
|
||||||
|
checked={config.mandatory === true}
|
||||||
|
onChange={(event) => onChange({ mandatory: event.target.checked })}
|
||||||
|
/>
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Label>Save text as</Form.Label>
|
||||||
|
<Form.Control value={config.output || 'ocr_text'} onChange={(event) => onChange({ output: event.target.value })} />
|
||||||
|
</Form.Group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'delay') {
|
||||||
|
return (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Label>Seconds</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={86400}
|
||||||
|
value={config.seconds ?? 60}
|
||||||
|
onChange={(event) => onChange({ seconds: Number(event.target.value) })}
|
||||||
|
/>
|
||||||
|
<Form.Text>The run waits at least this long. EventBridge fires on a one-minute grid, so it can take up to about a minute longer.</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'llm') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Group className="mb-2">
|
||||||
|
<Form.Label>Model</Form.Label>
|
||||||
|
<Form.Select value={config.model || 'meta.llama3-8b-instruct-v1:0'} onChange={(event) => onChange({ model: event.target.value })}>
|
||||||
|
<option value="meta.llama3-8b-instruct-v1:0">Llama 3 8B</option>
|
||||||
|
<option value="amazon.titan-text-express-v1">Titan Text Express</option>
|
||||||
|
<option value="anthropic.claude-3-haiku-20240307-v1:0">Claude 3 Haiku</option>
|
||||||
|
</Form.Select>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group className="mb-2">
|
||||||
|
<Form.Label>Prompt</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
as="textarea"
|
||||||
|
rows={6}
|
||||||
|
value={config.prompt || ''}
|
||||||
|
onChange={(event) => onChange({ prompt: event.target.value })}
|
||||||
|
/>
|
||||||
|
<Form.Text>Use {'{{doctor_name}}'} or {'{{ocr_text}}'} for a value from the incoming object.</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Label>Save answer as</Form.Label>
|
||||||
|
<Form.Control value={config.output || 'llm_text'} onChange={(event) => onChange({ output: event.target.value })} />
|
||||||
|
</Form.Group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (type === 'action') {
|
if (type === 'action') {
|
||||||
|
const mappings = Array.isArray(config.mappings) ? config.mappings : [];
|
||||||
|
const updateRow = (index, patch) => {
|
||||||
|
onChange({
|
||||||
|
mappings: mappings.map((row, rowIndex) => (rowIndex === index ? { ...row, ...patch } : row)),
|
||||||
|
});
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Group className="mb-2">
|
<Form.Group className="mb-2">
|
||||||
<Form.Label>Action</Form.Label>
|
<Form.Label>Action</Form.Label>
|
||||||
<Form.Select value={config.name || 'set_status'} onChange={(event) => onChange({ name: event.target.value })}>
|
<Form.Select
|
||||||
|
value={config.name || 'set_status'}
|
||||||
|
onChange={(event) => {
|
||||||
|
const name = event.target.value;
|
||||||
|
onChange(name === 'update_submission'
|
||||||
|
? { name, mappings: mappings.length ? mappings : [{ field: '', source: '' }] }
|
||||||
|
: { name });
|
||||||
|
}}
|
||||||
|
>
|
||||||
<option value="set_status">Set status</option>
|
<option value="set_status">Set status</option>
|
||||||
|
<option value="update_submission">Form submission update</option>
|
||||||
</Form.Select>
|
</Form.Select>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group>
|
{config.name === 'update_submission' ? (
|
||||||
<Form.Label>Status</Form.Label>
|
<>
|
||||||
<Form.Control value={config.status || ''} onChange={(event) => onChange({ status: event.target.value })} />
|
<p className="text-muted">Map each submission field to a field on this run. If that field is not on the run, the text is saved as the value. Use formdata.doctor_name for an answer, or workflow_status for the submission record.</p>
|
||||||
</Form.Group>
|
{mappings.map((row, index) => (
|
||||||
|
<div className="d-flex gap-2 mb-2" key={`submission-field-${index}`}>
|
||||||
|
<Form.Control
|
||||||
|
placeholder="Submission field"
|
||||||
|
value={row.field || ''}
|
||||||
|
onChange={(event) => updateRow(index, { field: event.target.value })}
|
||||||
|
/>
|
||||||
|
<Form.Control
|
||||||
|
placeholder="Incoming field"
|
||||||
|
value={row.source || ''}
|
||||||
|
onChange={(event) => updateRow(index, { source: event.target.value })}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline-danger"
|
||||||
|
onClick={() => onChange({ mappings: mappings.filter((_, rowIndex) => rowIndex !== index) })}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button variant="outline-secondary" onClick={() => onChange({ mappings: mappings.concat({ field: '', source: '' }) })}>
|
||||||
|
Add field
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Label>Status</Form.Label>
|
||||||
|
<Form.Control value={config.status || ''} onChange={(event) => onChange({ status: event.target.value })} />
|
||||||
|
</Form.Group>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -401,7 +636,7 @@ function EdgeForm({ edge, onChange }) {
|
||||||
<Form.Group className="mb-2">
|
<Form.Group className="mb-2">
|
||||||
<Form.Label>Operator</Form.Label>
|
<Form.Label>Operator</Form.Label>
|
||||||
<Form.Select value={when.op || 'eq'} onChange={(event) => onChange({ op: event.target.value })}>
|
<Form.Select value={when.op || 'eq'} onChange={(event) => onChange({ op: event.target.value })}>
|
||||||
{['eq', 'neq', 'in', 'gt', 'lt', 'exists'].map((op) => (
|
{['eq', 'neq', 'in', 'gt', 'lt', 'exists', 'empty'].map((op) => (
|
||||||
<option key={op} value={op}>{op}</option>
|
<option key={op} value={op}>{op}</option>
|
||||||
))}
|
))}
|
||||||
</Form.Select>
|
</Form.Select>
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,63 @@ export const NODE_TYPES = [
|
||||||
{ type: 'approval', label: 'Approval' },
|
{ type: 'approval', label: 'Approval' },
|
||||||
{ type: 'notify', label: 'Notify' },
|
{ type: 'notify', label: 'Notify' },
|
||||||
{ type: 'action', label: 'Action' },
|
{ type: 'action', label: 'Action' },
|
||||||
|
{ type: 'contact', label: 'Contact' },
|
||||||
|
{ type: 'ocr', label: 'OCR' },
|
||||||
|
{ type: 'llm', label: 'LLM' },
|
||||||
|
{ type: 'delay', label: 'Delay' },
|
||||||
{ type: 'end', label: 'End' },
|
{ type: 'end', label: 'End' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const CONTACT_FIELDS = [
|
||||||
|
{ contact: 'id', optional: true },
|
||||||
|
{ contact: 'name' },
|
||||||
|
{ contact: 'phone', optional: true },
|
||||||
|
{ contact: 'email', optional: true },
|
||||||
|
{ contact: 'msl_code', optional: true, removable: true },
|
||||||
|
{ contact: 'type' },
|
||||||
|
{ contact: 'hierarchy' },
|
||||||
|
{ contact: 'user_id' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function contactMappings(config) {
|
||||||
|
const saved = Array.isArray(config && config.mappings) ? config.mappings : [];
|
||||||
|
const required = CONTACT_FIELDS.filter((field) => !field.removable || saved.some((row) => row && row.contact === field.contact)).map((field) => {
|
||||||
|
const found = saved.find((row) => row && row.contact === field.contact);
|
||||||
|
return {
|
||||||
|
contact: field.contact,
|
||||||
|
source: (found && found.source) || '',
|
||||||
|
locked: true,
|
||||||
|
optional: Boolean(field.optional),
|
||||||
|
removable: Boolean(field.removable),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const extra = saved
|
||||||
|
.filter((row) => {
|
||||||
|
if (!row) return false;
|
||||||
|
const contact = String(row.contact || '').trim();
|
||||||
|
if (['position', 'user_info', 'domain', 'application'].includes(contact)) return false;
|
||||||
|
if (contact && CONTACT_FIELDS.some((field) => field.contact === contact)) return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((row) => ({ contact: row.contact || '', source: row.source || '', locked: false }));
|
||||||
|
return required.concat(extra);
|
||||||
|
}
|
||||||
|
|
||||||
export function labelFor(node) {
|
export function labelFor(node) {
|
||||||
const type = node.type || node.data?.nodeType;
|
const type = node.type || node.data?.nodeType;
|
||||||
const config = node.config || node.data?.config || {};
|
const config = node.config || node.data?.config || {};
|
||||||
if (type === 'approval') return `Approval (${config.mode || 'single'})`;
|
if (type === 'approval') return `Approval (${config.mode || 'single'})`;
|
||||||
if (type === 'end') return `End (${config.outcome || 'completed'})`;
|
if (type === 'end') return `End (${config.outcome || 'completed'})`;
|
||||||
if (type === 'notify') return 'Notify';
|
if (type === 'notify') return 'Notify';
|
||||||
if (type === 'action') return config.name || 'Action';
|
if (type === 'action') {
|
||||||
|
if (config.name === 'update_submission') return 'Form submission update';
|
||||||
|
if (config.name === 'set_status') return 'Set status';
|
||||||
|
return config.name || 'Action';
|
||||||
|
}
|
||||||
|
if (type === 'contact') return 'Update contact';
|
||||||
|
if (type === 'ocr') return 'OCR';
|
||||||
|
if (type === 'llm') return 'LLM';
|
||||||
|
if (type === 'delay') return `Delay (${config.seconds || 60}s)`;
|
||||||
if (type === 'condition') return 'Condition';
|
if (type === 'condition') return 'Condition';
|
||||||
return 'Start';
|
return 'Start';
|
||||||
}
|
}
|
||||||
|
|
@ -45,12 +92,44 @@ export function toFlow(definition) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function actionConfig(config) {
|
||||||
|
const current = config || {};
|
||||||
|
if (current.name === 'update_submission') {
|
||||||
|
const mappings = Array.isArray(current.mappings) ? current.mappings : [];
|
||||||
|
return {
|
||||||
|
name: 'update_submission',
|
||||||
|
mappings: mappings
|
||||||
|
.map((row) => ({
|
||||||
|
field: String((row && row.field) || '').trim(),
|
||||||
|
source: String((row && row.source) || '').trim(),
|
||||||
|
}))
|
||||||
|
.filter((row) => row.field || row.source),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: current.name || 'set_status',
|
||||||
|
status: current.status || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function fromFlow(nodes, edges) {
|
export function fromFlow(nodes, edges) {
|
||||||
return {
|
return {
|
||||||
nodes: nodes.map((node) => ({
|
nodes: nodes.map((node) => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
type: node.data.nodeType,
|
type: node.data.nodeType,
|
||||||
config: node.data.config || {},
|
config: node.data.nodeType === 'contact'
|
||||||
|
? {
|
||||||
|
...node.data.config,
|
||||||
|
msl_mode: node.data.config?.msl_mode === 'auto' ? 'auto' : 'map',
|
||||||
|
msl_prefix: node.data.config?.msl_prefix || '',
|
||||||
|
msl_suffix: node.data.config?.msl_suffix || '',
|
||||||
|
mappings: contactMappings(node.data.config)
|
||||||
|
.filter((row) => String(row.contact || '').trim())
|
||||||
|
.map(({ contact, source }) => ({ contact, source })),
|
||||||
|
}
|
||||||
|
: node.data.nodeType === 'action'
|
||||||
|
? actionConfig(node.data.config)
|
||||||
|
: node.data.config || {},
|
||||||
})),
|
})),
|
||||||
edges: edges.map((edge) => {
|
edges: edges.map((edge) => {
|
||||||
const when = edge.data && edge.data.when && edge.data.when.field ? edge.data.when : null;
|
const when = edge.data && edge.data.when && edge.data.when.field ? edge.data.when : null;
|
||||||
|
|
@ -69,6 +148,17 @@ export function defaultConfig(type) {
|
||||||
if (type === 'approval') return { mode: 'sequential', approvers: ['l1manager'], escalate_vacant: true };
|
if (type === 'approval') return { mode: 'sequential', approvers: ['l1manager'], escalate_vacant: true };
|
||||||
if (type === 'notify') return { recipients: ['l1manager'], subject: '', body: '' };
|
if (type === 'notify') return { recipients: ['l1manager'], subject: '', body: '' };
|
||||||
if (type === 'action') return { name: 'set_status', status: 'approved' };
|
if (type === 'action') return { name: 'set_status', status: 'approved' };
|
||||||
|
if (type === 'contact') {
|
||||||
|
return {
|
||||||
|
mappings: contactMappings({ mappings: [{ contact: 'msl_code', source: '' }] }).map(({ contact, source }) => ({ contact, source })),
|
||||||
|
msl_mode: 'map',
|
||||||
|
msl_prefix: '',
|
||||||
|
msl_suffix: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === 'ocr') return { source: '', output: 'ocr_text', mandatory: false };
|
||||||
|
if (type === 'llm') return { model: 'meta.llama3-8b-instruct-v1:0', prompt: '', output: 'llm_text' };
|
||||||
|
if (type === 'delay') return { seconds: 60 };
|
||||||
if (type === 'end') return { outcome: 'completed' };
|
if (type === 'end') return { outcome: 'completed' };
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue