From fe03c32046af48a87bbd891941838cd89a98dea5 Mon Sep 17 00:00:00 2001 From: Abhijit Date: Mon, 19 Oct 2026 15:05:59 +0530 Subject: [PATCH] 1.added data designer 2.updated form export sort order --- .env.localapi | 2 + .gitignore | 2 + package.json | 1 + src/components/EntityForm.jsx | 6 + src/components/form/DataDesignerField.jsx | 334 ++++++++++++++++++++++ src/modules/formentitys.js | 10 + src/modules/formexports.js | 2 + vite.config.js | 28 +- 8 files changed, 372 insertions(+), 13 deletions(-) create mode 100644 .env.localapi create mode 100644 .gitignore create mode 100644 src/components/form/DataDesignerField.jsx diff --git a/.env.localapi b/.env.localapi new file mode 100644 index 0000000..451a99f --- /dev/null +++ b/.env.localapi @@ -0,0 +1,2 @@ +VITE_API_MODE=local +VITE_LARAVEL_ORIGIN=http://127.0.0.1:8000 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d15140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.cursor/ \ No newline at end of file diff --git a/package.json b/package.json index a2eb5ce..9a2293d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite", + "dev:local": "vite --mode localapi", "build": "vite build", "lint": "eslint .", "preview": "vite preview", diff --git a/src/components/EntityForm.jsx b/src/components/EntityForm.jsx index 7cf10a5..d06d4fd 100644 --- a/src/components/EntityForm.jsx +++ b/src/components/EntityForm.jsx @@ -14,6 +14,7 @@ import { } from '@/components/metric/MetricFieldControls'; import MilestoneMappingField from '@/components/recognition/MilestoneMappingField'; import EmailNotificationRulesField from '@/components/form/EmailNotificationRulesField'; +import { DataDesignerField, normalizeDataDesigner } from '@/components/form/DataDesignerField'; import RankRolesField from '@/components/form/RankRolesField'; import ReminderRowsField from '@/components/form/ReminderRowsField'; import DependentSelectField from '@/components/form/DependentSelectField'; @@ -36,6 +37,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 === 'data_designer') return { fields: [] }; 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 []; } @@ -44,6 +46,7 @@ function defaultValue(field, item) { return ''; } if (field.type === 'combined_metrics') return parseCombinedMetrics(raw); + if (field.type === 'data_designer') return normalizeDataDesigner(raw); if (field.type === 'email_notifications') { if (typeof raw === 'string' && raw.trim()) { try { @@ -475,6 +478,9 @@ function FieldControl({ field, control, register, watch, moduleName, onUploading if (field.type === 'email_notifications') { return ; } + if (field.type === 'data_designer') { + return ; + } if (field.type === 'rank_roles') { return ; } diff --git a/src/components/form/DataDesignerField.jsx b/src/components/form/DataDesignerField.jsx new file mode 100644 index 0000000..621b910 --- /dev/null +++ b/src/components/form/DataDesignerField.jsx @@ -0,0 +1,334 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router'; +import { Controller } from 'react-hook-form'; +import { Button, Dropdown, Form } from 'react-bootstrap'; +import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; +import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { signedGet, isSuccess } from '@/api/client'; + +const DEFAULT_HIERARCHY_LEVELS = ['Country', 'Zone', 'Region', 'Area', 'Territory']; +const MANAGER_LEVELS = [ + ['l1', 'L1'], + ['l2', 'L2'], + ['l3', 'L3'], +]; +const MANAGER_KEYS = [ + ['name', 'Name'], + ['email', 'Email'], + ['employee_code', 'Employee Code'], + ['position_name', 'Position Name'], +]; + +function emptyDesigner() { + return { fields: [] }; +} + +function asDesigner(raw) { + let value = raw; + if (typeof raw === 'string' && raw.trim()) { + try { + value = JSON.parse(raw); + } catch { + value = null; + } + } + const fields = Array.isArray(value?.fields) ? value.fields : []; + return { + fields: fields + .filter((row) => row && row.id && row.source && row.key) + .map((row) => ({ + id: String(row.id), + source: String(row.source), + key: String(row.key), + label: row.label || String(row.key), + level: row.level || undefined, + show_in_export: row.show_in_export !== false, + show_in_view: Boolean(row.show_in_view), + })), + }; +} + +export function normalizeDataDesigner(raw) { + return asDesigner(raw); +} + +const SOURCE_LABELS = { + user: 'User attributes', + hierarchy: 'Hierarchy', + manager: 'Manager info', + question: 'Form questions', + manual: 'Manual', +}; + +function managerStoredId(level, key) { + const number = String(level || '').replace('l', ''); + const suffix = key === 'employee_code' ? 'code' : key === 'position_name' ? 'position' : key; + return `manager${number}_${suffix}`; +} + +function option(id, source, key, label, extra = {}) { + return { value: id, label, row: { id, source, key, label, show_in_export: true, show_in_view: false, ...extra } }; +} + +function CheckDropdown({ title, count, options, taken, onToggle, emptyText, disabled, disabledText }) { + return ( + + + {disabled ? disabledText : `${title} (${count})`} + + + {options.length ? options.map((item) => ( + + onToggle(item, event.target.checked)} + /> + + )) :
{emptyText}
} +
+
+ ); +} + +function DesignerRow({ row, onPatch, onRemove }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: row.id }); + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.6 : 1, + }; + return ( +
+ +
+
{row.label}
+
{SOURCE_LABELS[row.source] || row.source}
+
+ onPatch(row.id, { show_in_export: event.target.checked })} + /> + onPatch(row.id, { show_in_view: event.target.checked })} + /> + +
+ ); +} + +function DataDesignerEditor({ value, onChange, userAttributes = {} }) { + const [searchParams] = useSearchParams(); + const formId = searchParams.get('id') || ''; + const [open, setOpen] = useState(false); + const [questions, setQuestions] = useState([]); + const [hierarchyLevels, setHierarchyLevels] = useState([]); + const [manualOpen, setManualOpen] = useState(false); + const [manualKey, setManualKey] = useState(''); + const [manualLabel, setManualLabel] = useState(''); + const designer = asDesigner(value); + const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } })); + + useEffect(() => { + if (!formId) { + setQuestions([]); + return undefined; + } + let cancelled = false; + signedGet('form', 'formquestion/get_all', { entity_id: formId }).then((response) => { + if (cancelled || !isSuccess(response)) return; + const items = response.data?.items || []; + setQuestions(Array.isArray(items) ? items : Object.values(items)); + }); + return () => { + cancelled = true; + }; + }, [formId]); + + useEffect(() => { + let cancelled = false; + signedGet('ums', 'domain/get_one').then((response) => { + if (cancelled) return; + const types = isSuccess(response) ? response.data?.item?.territory_types : []; + const levels = Array.isArray(types) ? types.map((type) => String(type || '').trim()).filter(Boolean) : []; + setHierarchyLevels(levels.length ? levels : DEFAULT_HIERARCHY_LEVELS); + }); + return () => { + cancelled = true; + }; + }, []); + + const taken = useMemo(() => new Set(designer.fields.map((row) => row.id)), [designer.fields]); + + const userOptions = useMemo(() => { + const fixed = [ + ['name', 'Name'], + ['email', 'Email'], + ['employee_code', 'Employee Code'], + ]; + const attributes = Object.entries(userAttributes); + const labelKey = (label) => String(label || '').trim().toLowerCase(); + const usedCodes = new Set(); + const usedLabels = new Set(); + const rows = []; + const addRow = (key, label) => { + const normalized = labelKey(label); + if (!key || usedCodes.has(key) || usedLabels.has(normalized)) return; + usedCodes.add(key); + usedLabels.add(normalized); + rows.push(option(key, 'user', key, label)); + }; + fixed.forEach(([key, label]) => { + const match = attributes.find(([code, attrLabel]) => code === key || labelKey(attrLabel) === labelKey(label)); + if (match) addRow(match[0], match[1]); + else addRow(key, label); + }); + attributes.forEach(([code, label]) => addRow(code, label)); + return rows; + }, [userAttributes]); + + const hierarchyOptions = hierarchyLevels.map((level) => option(String(level).trim().toLowerCase(), 'hierarchy', level, level)); + + const managerOptions = MANAGER_LEVELS.flatMap(([level, title]) => + MANAGER_KEYS.map(([key, name]) => + option(managerStoredId(level, key), 'manager', key, `${title} Manager ${name}`, { level }), + ), + ); + + const questionOptions = [ + ...questions + .filter((question) => question?.id && question.status !== 'disabled') + .map((question) => + option(question.code || String(question.id), 'question', question.code || String(question.id), question.question || question.code || question.id), + ), + option('submission_id', 'question', 'submission_id', 'Submission ID'), + option('rec_date', 'question', 'rec_date', 'Record Date'), + option('created_date', 'question', 'created_date', 'Submitted Date'), + option('updated_date', 'question', 'updated_date', 'Last Updated Date'), + option('workflow_status', 'question', 'workflow_status', 'Workflow Status'), + ]; + + const appendRows = (picked) => { + const rows = (picked || []).map((item) => item.row).filter((row) => row && !taken.has(row.id)); + if (!rows.length) return; + onChange({ fields: [...designer.fields, ...rows] }); + }; + + const patchRow = (id, patch) => { + onChange({ + fields: designer.fields.map((row) => (row.id === id ? { ...row, ...patch } : row)), + }); + }; + + const removeRow = (id) => { + onChange({ fields: designer.fields.filter((row) => row.id !== id) }); + }; + + const onDragEnd = (event) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + const from = designer.fields.findIndex((row) => row.id === active.id); + const to = designer.fields.findIndex((row) => row.id === over.id); + if (from < 0 || to < 0) return; + onChange({ fields: arrayMove(designer.fields, from, to) }); + }; + + const toggleOption = (item, checked) => { + if (checked) appendRows([item]); + else removeRow(item.value); + }; + + const countFor = (source) => designer.fields.filter((row) => row.source === source).length; + + const addManual = () => { + const key = manualKey.trim(); + const label = manualLabel.trim(); + if (!key || !label) return; + const id = key; + if (taken.has(id)) return; + onChange({ + fields: [...designer.fields, { id, source: 'manual', key, label, show_in_export: true, show_in_view: false }], + }); + setManualKey(''); + setManualLabel(''); + }; + + return ( +
+
+ Data Designer + +
+ {open ? ( +
+
+ + + + + +
+ {manualOpen ? ( +
+ + Field name + setManualKey(event.target.value)} placeholder="erdostyne_capsule_value" /> + + + Column title + setManualLabel(event.target.value)} placeholder="ERDOSTYNE CAPSULE 10s Value" /> + + +
+ ) : null} +
+ ) : null} + + row.id)} strategy={verticalListSortingStrategy}> + {designer.fields.map((row) => ( + + ))} + + + {!designer.fields.length ?

No fields selected.

: null} +
+ ); +} + +export function DataDesignerField({ field, control }) { + return ( + ( + + )} + /> + ); +} diff --git a/src/modules/formentitys.js b/src/modules/formentitys.js index e4bc811..bda4588 100644 --- a/src/modules/formentitys.js +++ b/src/modules/formentitys.js @@ -1,4 +1,5 @@ import { signedGet, signedPost, isSuccess } from '@/api/client'; +import { normalizeDataDesigner } from '@/components/form/DataDesignerField'; import { dropdownMap, fetchUserAttributes } from '@/api/dropdowns'; import { splitHash } from '@/helpers/metric'; @@ -201,6 +202,13 @@ const formentitys = { roles, users: notifyUsers, }, + { + key: 'data_designer', + type: 'data_designer', + fullwidth: true, + hide_label: true, + userAttributes, + }, ]; }, beforeForm: (item) => { @@ -237,6 +245,7 @@ const formentitys = { 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']; next.workflow_definition_id = next.workflow_definition_id || ''; + next.data_designer = normalizeDataDesigner(next.data_designer); return next; }, beforeSave: async (data, mode) => { @@ -245,6 +254,7 @@ const formentitys = { payload.export_manager_levels = asArray(payload.export_manager_levels); payload.manager_de_roles = asArray(payload.manager_de_roles); payload.contact_attributes = asArray(payload.contact_attributes); + payload.data_designer = normalizeDataDesigner(payload.data_designer); 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'; diff --git a/src/modules/formexports.js b/src/modules/formexports.js index c08f92a..6d4b4f5 100644 --- a/src/modules/formexports.js +++ b/src/modules/formexports.js @@ -29,6 +29,8 @@ const formexports = { { title: 'Add New', to: '/formexport/addnew' }, { title: 'Refresh', action: 'reload' }, ], + mapItems: (items) => + [...(items || [])].sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0)), formFields: async () => { const [forms, dropdown] = await Promise.all([ dropdownMap('form', 'formentity', (item) => item.title), diff --git a/vite.config.js b/vite.config.js index 396d000..1d7fbc9 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,4 +1,4 @@ -import { defineConfig } from 'vite'; +import { defineConfig , loadEnv } from 'vite'; import react from '@vitejs/plugin-react'; import { resolve } from 'node:path'; @@ -57,18 +57,20 @@ const laravelProxy = { }, }; -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - '@': resolve(__dirname, 'src'), + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + return { + plugins: [react()], + resolve: { + alias: { '@': resolve(__dirname, 'src') }, }, - }, - server: { - port: 5173, - proxy: { - ...laravelProxy, - ...(process.env.VITE_API_MODE === 'local' ? localProxy : productionProxy), + server: { + port: 5173, + proxy: { + ...laravelProxy, + ...(env.VITE_API_MODE === 'local' ? localProxy : productionProxy), + }, }, - }, + }; });