@onehat/ui 0.4.95 → 0.4.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/Components/Container/Container.js +47 -43
- package/src/Components/Form/Form.js +15 -7
- package/src/Components/Grid/Grid.js +116 -41
- package/src/Components/Hoc/withAlert.js +2 -1
- package/src/Components/Hoc/withEditor.js +37 -7
- package/src/Components/Hoc/withPresetButtons.js +5 -5
- package/src/Components/Panel/GridPanel.js +2 -1
- package/src/Components/Panel/TabPanel.js +6 -3
- package/src/Components/Panel/TreePanel.js +3 -2
- package/src/Components/Tree/Tree.js +38 -4
- package/src/Components/Tree/TreeNode.js +3 -0
- package/src/Components/Viewer/DateTimeViewer.js +25 -0
- package/src/Components/Viewer/DateViewer.js +25 -0
- package/src/Components/Viewer/PmCalcDebugViewer.js +299 -0
- package/src/Components/Viewer/PmStatusesViewer.js +51 -0
- package/src/Components/Viewer/TimeViewer.js +25 -0
- package/src/Components/Viewer/Viewer.js +29 -21
- package/src/Components/index.js +10 -0
- package/src/Constants/Dates.js +1 -0
- package/src/Constants/EditorModes.js +2 -0
- package/src/Constants/MeterSources.js +5 -0
- package/src/Constants/MeterTypes.js +4 -2
- package/src/Constants/PmEventTypes.js +11 -0
- package/src/Constants/PmScheduleModes.js +4 -0
- package/src/Constants/PmStatuses.js +7 -0
- package/src/PlatformImports/Web/Attachments.js +1 -0
|
@@ -30,12 +30,15 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
30
30
|
userCanView = true,
|
|
31
31
|
canEditorViewOnly = false, // whether the editor can *ever* change state out of 'View' mode
|
|
32
32
|
canProceedWithCrud, // fn returns bool on if the CRUD operation can proceed
|
|
33
|
+
canRecordBeEdited, // fn(selection) returns bool on if the current record(s) can be edited
|
|
34
|
+
canRecordBeDeleted, // fn(selection) returns bool on if the current record(s) can be deleted
|
|
33
35
|
disableAdd = false,
|
|
34
36
|
disableEdit = false,
|
|
35
37
|
disableDelete = false,
|
|
36
38
|
disableDuplicate = false,
|
|
37
39
|
disableView = false,
|
|
38
40
|
useRemoteDuplicate = false, // call specific copyToNew function on server, rather than simple duplicate on client
|
|
41
|
+
getDuplicateValues, // fn(entity) to get default values for duplication
|
|
39
42
|
getRecordIdentifier = (selection) => {
|
|
40
43
|
if (selection.length > 1) {
|
|
41
44
|
return 'records?';
|
|
@@ -87,6 +90,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
87
90
|
editorModeRef = useRef(initialEditorMode),
|
|
88
91
|
isIgnoreNextSelectionChangeRef = useRef(false),
|
|
89
92
|
isEditorShownRef = useRef(false),
|
|
93
|
+
defaultValuesRef = useRef(defaultValues),
|
|
94
|
+
canEditorBeInEditModeRef = useRef(true), // whether the editor is allowed to be in edit mode based on canRecordBeEdited
|
|
90
95
|
[currentRecord, setCurrentRecord] = useState(null),
|
|
91
96
|
[isAdding, setIsAdding] = useState(false),
|
|
92
97
|
[isSaving, setIsSaving] = useState(false),
|
|
@@ -108,6 +113,19 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
108
113
|
getIsEditorShown = () => {
|
|
109
114
|
return isEditorShownRef.current;
|
|
110
115
|
},
|
|
116
|
+
getDefaultValues = () => {
|
|
117
|
+
return defaultValuesRef.current;
|
|
118
|
+
},
|
|
119
|
+
setDefaultValues = (vals) => {
|
|
120
|
+
defaultValuesRef.current = vals;
|
|
121
|
+
},
|
|
122
|
+
setCanEditorBeInEditMode = (bool) => {
|
|
123
|
+
canEditorBeInEditModeRef.current = bool;
|
|
124
|
+
forceUpdate();
|
|
125
|
+
},
|
|
126
|
+
getCanEditorBeInEditMode = () => {
|
|
127
|
+
return canEditorBeInEditModeRef.current;
|
|
128
|
+
},
|
|
111
129
|
setIsWaitModalShown = (bool) => {
|
|
112
130
|
const
|
|
113
131
|
dispatch = UiGlobals.redux?.dispatch,
|
|
@@ -154,6 +172,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
154
172
|
getNewEntityDisplayValue = () => {
|
|
155
173
|
return newEntityDisplayValueRef.current;
|
|
156
174
|
},
|
|
175
|
+
setNewEntityDisplayValue = (val) => {
|
|
176
|
+
newEntityDisplayValueRef.current = val;
|
|
177
|
+
},
|
|
157
178
|
doAdd = async (e, values) => {
|
|
158
179
|
if (canUser && !canUser(ADD)) {
|
|
159
180
|
showPermissionsError(ADD);
|
|
@@ -181,8 +202,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
181
202
|
// 2. Use the repository's default values (defined on each property as 'defaultValue'), or
|
|
182
203
|
// 3. Individually override the repository's default values with submitted 'defaultValues' (given as a prop to this HOC)
|
|
183
204
|
let defaultValuesToUse = Repository.getSchema().getDefaultValues();
|
|
184
|
-
if (
|
|
185
|
-
_.merge(defaultValuesToUse,
|
|
205
|
+
if (getDefaultValues()) {
|
|
206
|
+
_.merge(defaultValuesToUse, getDefaultValues());
|
|
186
207
|
}
|
|
187
208
|
addValues = {...defaultValuesToUse};
|
|
188
209
|
}
|
|
@@ -448,7 +469,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
448
469
|
const
|
|
449
470
|
entity = selection[0],
|
|
450
471
|
idProperty = Repository.getSchema().model.idProperty,
|
|
451
|
-
rawValues = _.omit(entity.getOriginalData(), idProperty);
|
|
472
|
+
rawValues = getDuplicateValues ? getDuplicateValues(entity) : _.omit(entity.getOriginalData(), idProperty);
|
|
452
473
|
rawValues.id = null; // unset the id of the duplicate
|
|
453
474
|
|
|
454
475
|
setIsWaitModalShown(true);
|
|
@@ -624,6 +645,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
624
645
|
});
|
|
625
646
|
},
|
|
626
647
|
calculateEditorMode = () => {
|
|
648
|
+
if (!getCanEditorBeInEditMode()) { // this is a result of canRecordBeEdited returning false
|
|
649
|
+
return EDITOR_MODE__VIEW;
|
|
650
|
+
}
|
|
627
651
|
|
|
628
652
|
let isIgnoreNextSelectionChange = getIsIgnoreNextSelectionChange(),
|
|
629
653
|
doStayInEditModeOnSelectionChange = stayInEditModeOnSelectionChange;
|
|
@@ -691,7 +715,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
691
715
|
useEffect(() => {
|
|
692
716
|
|
|
693
717
|
if (editorType === EDITOR_TYPE__SIDE) {
|
|
694
|
-
if (selection?.length) { //
|
|
718
|
+
if (selection?.length) { // || isAdding
|
|
695
719
|
// there is a selection, so show the editor
|
|
696
720
|
setIsEditorShown(true);
|
|
697
721
|
} else {
|
|
@@ -700,6 +724,11 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
700
724
|
}
|
|
701
725
|
}
|
|
702
726
|
|
|
727
|
+
if (canRecordBeEdited && canRecordBeEdited(selection) === false) {
|
|
728
|
+
setCanEditorBeInEditMode(false);
|
|
729
|
+
} else {
|
|
730
|
+
setCanEditorBeInEditMode(true);
|
|
731
|
+
}
|
|
703
732
|
setEditorMode(calculateEditorMode());
|
|
704
733
|
setLastSelection(selection);
|
|
705
734
|
|
|
@@ -720,7 +749,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
720
749
|
self.duplicate = doDuplicate;
|
|
721
750
|
self.setIsEditorShown = setIsEditorShown;
|
|
722
751
|
}
|
|
723
|
-
|
|
752
|
+
setNewEntityDisplayValue(newEntityDisplayValue);
|
|
753
|
+
setDefaultValues(defaultValues);
|
|
724
754
|
|
|
725
755
|
if (lastSelection !== selection) {
|
|
726
756
|
// NOTE: If I don't calculate this on the fly for selection changes,
|
|
@@ -749,8 +779,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
749
779
|
setIsEditorShown={setIsEditorShown}
|
|
750
780
|
setIsIgnoreNextSelectionChange={setIsIgnoreNextSelectionChange}
|
|
751
781
|
onAdd={(!userCanEdit || disableAdd) ? null : doAdd}
|
|
752
|
-
onEdit={(!userCanEdit || disableEdit) ? null : doEdit}
|
|
753
|
-
onDelete={(!userCanEdit || disableDelete) ? null : doDelete}
|
|
782
|
+
onEdit={(!userCanEdit || disableEdit || (canRecordBeEdited && !canRecordBeEdited(selection))) ? null : doEdit}
|
|
783
|
+
onDelete={(!userCanEdit || disableDelete || (canRecordBeDeleted && !canRecordBeDeleted(selection))) ? null : doDelete}
|
|
754
784
|
onView={doView}
|
|
755
785
|
onDuplicate={doDuplicate}
|
|
756
786
|
onEditorSave={doEditorSave}
|
|
@@ -48,7 +48,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const {
|
|
51
|
-
//
|
|
51
|
+
// for local use
|
|
52
52
|
contextMenuItems = [],
|
|
53
53
|
additionalToolbarButtons = [],
|
|
54
54
|
useUploadDownload = false,
|
|
@@ -59,18 +59,18 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
59
59
|
downloadHeaders,
|
|
60
60
|
downloadParams,
|
|
61
61
|
onChangeColumnsConfig,
|
|
62
|
-
canRecordBeEdited,
|
|
63
|
-
canRecordBeDeleted,
|
|
64
|
-
canRecordBeDuplicated,
|
|
65
62
|
...propsToPass
|
|
66
63
|
} = props,
|
|
67
64
|
{
|
|
68
|
-
//
|
|
65
|
+
// extract and pass down
|
|
69
66
|
isEditor = false,
|
|
70
67
|
isTree = false,
|
|
71
68
|
canDeleteRootNode = false,
|
|
72
69
|
isSideEditor = false,
|
|
73
70
|
canEditorViewOnly = false,
|
|
71
|
+
canRecordBeEdited, // fn(selection) returns bool on if the current record(s) can be edited
|
|
72
|
+
canRecordBeDeleted, // fn(selection) returns bool on if the current record(s) can be deleted
|
|
73
|
+
canRecordBeDuplicated, // fn(selection) returns bool on if the current record(s) can be duplicated
|
|
74
74
|
disableAdd = !isEditor,
|
|
75
75
|
disableEdit = !isEditor,
|
|
76
76
|
disableDelete = !isEditor,
|
|
@@ -12,6 +12,7 @@ export function GridPanel(props) {
|
|
|
12
12
|
isEditor = false,
|
|
13
13
|
editorType = EDITOR_TYPE__WINDOWED,
|
|
14
14
|
_panel = {},
|
|
15
|
+
_grid = {},
|
|
15
16
|
} = props;
|
|
16
17
|
|
|
17
18
|
let WhichGrid = Grid;
|
|
@@ -30,7 +31,7 @@ export function GridPanel(props) {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
return <Panel {...props} {..._panel}>
|
|
33
|
-
<WhichGrid {...props} />
|
|
34
|
+
<WhichGrid {...props} {..._grid} />
|
|
34
35
|
</Panel>;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -3,8 +3,11 @@ import Panel from './Panel.js';
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
export default function TabPanel(props) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const {
|
|
7
|
+
_panel = {},
|
|
8
|
+
_tab = {},
|
|
9
|
+
} = props;
|
|
10
|
+
return <Panel {...props} className="w-full flex" {..._panel}>
|
|
11
|
+
<TabBar {...props} {..._tab} />
|
|
9
12
|
</Panel>;
|
|
10
13
|
}
|
|
@@ -11,6 +11,7 @@ export function TreePanel(props) {
|
|
|
11
11
|
isEditor = false,
|
|
12
12
|
editorType = EDITOR_TYPE__WINDOWED,
|
|
13
13
|
_panel = {},
|
|
14
|
+
_tree = {},
|
|
14
15
|
} = props;
|
|
15
16
|
|
|
16
17
|
let WhichTree = Tree;
|
|
@@ -25,8 +26,8 @@ export function TreePanel(props) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
return <Panel {..._panel}>
|
|
29
|
-
<WhichTree {...props} {...
|
|
29
|
+
return <Panel {...props} {..._panel}>
|
|
30
|
+
<WhichTree {...props} {..._tree} />
|
|
30
31
|
</Panel>;
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
HStack,
|
|
4
4
|
Pressable,
|
|
5
5
|
ScrollView,
|
|
6
|
+
Text,
|
|
6
7
|
VStack,
|
|
7
8
|
VStackNative,
|
|
8
9
|
} from '@project-components/Gluestack';
|
|
@@ -133,7 +134,17 @@ function TreeComponent(props) {
|
|
|
133
134
|
canNodeMoveInternally, // optional fn to customize whether each node can be dragged INternally
|
|
134
135
|
canNodeMoveExternally, // optional fn to customize whether each node can be dragged EXternally
|
|
135
136
|
canNodeAcceptDrop, // optional fn to customize whether each node can accept a dropped item: (targetItem, draggedItem) => boolean
|
|
136
|
-
|
|
137
|
+
dragProxyField,
|
|
138
|
+
getCustomDragProxy = (item, selection) => { // optional fn to render custom drag preview: (item, selection) => ReactElement
|
|
139
|
+
let selectionCount = selection?.length || 1,
|
|
140
|
+
displayText = dragProxyField ? item[dragProxyField] : (item.displayValue || 'Selected TreeNode');
|
|
141
|
+
return <VStack className="bg-white border border-gray-300 rounded-lg p-3 shadow-lg max-w-[200px]">
|
|
142
|
+
<Text className="font-semibold text-gray-800">{displayText}</Text>
|
|
143
|
+
{selectionCount > 1 &&
|
|
144
|
+
<Text className="text-sm text-gray-600">(+{selectionCount -1} more item{selectionCount > 2 ? 's' : ''})</Text>
|
|
145
|
+
}
|
|
146
|
+
</VStack>;
|
|
147
|
+
},
|
|
137
148
|
dragPreviewOptions, // optional object for drag preview positioning options
|
|
138
149
|
areNodesDragSource = false,
|
|
139
150
|
nodeDragSourceType,
|
|
@@ -643,9 +654,23 @@ function TreeComponent(props) {
|
|
|
643
654
|
return;
|
|
644
655
|
}
|
|
645
656
|
|
|
646
|
-
// Create datum for the new entity and add it to parent's children
|
|
647
657
|
const newDatum = buildTreeNodeDatum(entity);
|
|
648
|
-
|
|
658
|
+
|
|
659
|
+
let isInserted = false;
|
|
660
|
+
if (entity._originalData?.previousSiblingId) {
|
|
661
|
+
// Insert datum in the correct position among parent's children
|
|
662
|
+
const siblingDatum = getDatumById(entity._originalData.previousSiblingId);
|
|
663
|
+
if (siblingDatum) {
|
|
664
|
+
const siblingIndex = parentDatum.children.findIndex(child => child.item.id === siblingDatum.item.id);
|
|
665
|
+
parentDatum.children.splice(siblingIndex + 1, 0, newDatum);
|
|
666
|
+
isInserted = true;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (!isInserted) {
|
|
670
|
+
// Insert datum at end of parent's children
|
|
671
|
+
parentDatum.children.push(newDatum);
|
|
672
|
+
}
|
|
673
|
+
|
|
649
674
|
|
|
650
675
|
// Update parent to show it has children and expand if needed
|
|
651
676
|
if (!entity.parent.hasChildren) {
|
|
@@ -1041,7 +1066,16 @@ function TreeComponent(props) {
|
|
|
1041
1066
|
return items;
|
|
1042
1067
|
},
|
|
1043
1068
|
getFooterToolbarItems = () => {
|
|
1044
|
-
|
|
1069
|
+
// Process additionalToolbarButtons to evaluate getIsButtonDisabled functions
|
|
1070
|
+
const processedButtons = _.map(additionalToolbarButtons, (config) => {
|
|
1071
|
+
const processedConfig = { ...config };
|
|
1072
|
+
// If the button has an getIsButtonDisabled function, evaluate it with current selection
|
|
1073
|
+
if (_.isFunction(config.getIsButtonDisabled)) {
|
|
1074
|
+
processedConfig.isDisabled = config.getIsButtonDisabled(selection);
|
|
1075
|
+
}
|
|
1076
|
+
return processedConfig;
|
|
1077
|
+
});
|
|
1078
|
+
return _.map(processedButtons, (config, ix) => getIconButtonFromConfig(config, ix, self));
|
|
1045
1079
|
},
|
|
1046
1080
|
renderTreeNode = (datum) => {
|
|
1047
1081
|
if (!datum.isVisible) {
|
|
@@ -148,6 +148,9 @@ export default function TreeNode(props) {
|
|
|
148
148
|
if (props.className) {
|
|
149
149
|
className += ' ' + props.className;
|
|
150
150
|
}
|
|
151
|
+
if (nodeProps.className) {
|
|
152
|
+
className += ' ' + nodeProps.className;
|
|
153
|
+
}
|
|
151
154
|
|
|
152
155
|
return <HStackNative
|
|
153
156
|
{...testProps('node' + (isSelected ? '-selected' : ''))}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import DisplayField from '../Form/Field/DisplayField.js';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import {
|
|
4
|
+
MOMENT_DATE_FORMAT_1,
|
|
5
|
+
} from '../../Constants/Dates';
|
|
6
|
+
|
|
7
|
+
export default function DateTimeViewer(props) {
|
|
8
|
+
const {
|
|
9
|
+
moment,
|
|
10
|
+
} = props;
|
|
11
|
+
if (!moment || !moment.isValid()) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
let className = clsx(
|
|
15
|
+
'flex-none',
|
|
16
|
+
);
|
|
17
|
+
if (props.className) {
|
|
18
|
+
className += ' ' + props.className;
|
|
19
|
+
}
|
|
20
|
+
return <DisplayField
|
|
21
|
+
text={moment.format(MOMENT_DATE_FORMAT_1)}
|
|
22
|
+
{...props}
|
|
23
|
+
className={className}
|
|
24
|
+
/>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import DisplayField from '../Form/Field/DisplayField.js';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import {
|
|
4
|
+
MOMENT_DATE_FORMAT_4,
|
|
5
|
+
} from '../../Constants/Dates';
|
|
6
|
+
|
|
7
|
+
export default function DateViewer(props) {
|
|
8
|
+
const {
|
|
9
|
+
moment,
|
|
10
|
+
} = props;
|
|
11
|
+
if (!moment || !moment.isValid()) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
let className = clsx(
|
|
15
|
+
'flex-none',
|
|
16
|
+
);
|
|
17
|
+
if (props.className) {
|
|
18
|
+
className += ' ' + props.className;
|
|
19
|
+
}
|
|
20
|
+
return <DisplayField
|
|
21
|
+
text={moment.format(MOMENT_DATE_FORMAT_4)}
|
|
22
|
+
{...props}
|
|
23
|
+
className={className}
|
|
24
|
+
/>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Text,
|
|
3
|
+
VStack,
|
|
4
|
+
} from '@project-components/Gluestack';
|
|
5
|
+
import clsx from 'clsx';
|
|
6
|
+
import {
|
|
7
|
+
MOMENT_DATE_FORMAT_6,
|
|
8
|
+
} from '../../Constants/Dates.js';
|
|
9
|
+
import {
|
|
10
|
+
PM_SCHEDULE_MODES__HISTORICAL_USAGE,
|
|
11
|
+
PM_SCHEDULE_MODES__EXPECTED_USAGE,
|
|
12
|
+
PM_SCHEDULE_MODES__NO_ESTIMATION,
|
|
13
|
+
PM_SCHEDULE_MODES__WILL_CALL,
|
|
14
|
+
} from '../../Constants/PmScheduleModes.js';
|
|
15
|
+
import {
|
|
16
|
+
PM_STATUSES__OK,
|
|
17
|
+
PM_STATUSES__PM_DUE,
|
|
18
|
+
PM_STATUSES__DELAYED,
|
|
19
|
+
PM_STATUSES__WILL_CALL,
|
|
20
|
+
PM_STATUSES__SCHEDULED,
|
|
21
|
+
PM_STATUSES__OVERDUE,
|
|
22
|
+
PM_STATUSES__COMPLETED,
|
|
23
|
+
} from '../../Constants/PmStatuses.js';
|
|
24
|
+
import {
|
|
25
|
+
METER_TYPES__HOURS,
|
|
26
|
+
METER_TYPES__HOURS_UNITS,
|
|
27
|
+
METER_TYPES__HOURS_TEXT,
|
|
28
|
+
METER_TYPES__MILES,
|
|
29
|
+
METER_TYPES__MILES_UNITS,
|
|
30
|
+
METER_TYPES__MILES_TEXT,
|
|
31
|
+
} from '../../Constants/MeterTypes.js';
|
|
32
|
+
import Button from '../Buttons/Button.js';
|
|
33
|
+
import Json from '../Form/Field/Json.js';
|
|
34
|
+
import Panel from '../Panel/Panel.js';
|
|
35
|
+
import Footer from '../Layout/Footer.js';
|
|
36
|
+
import Viewer from '../Viewer/Viewer.js';
|
|
37
|
+
import testProps from '../../Functions/testProps.js';
|
|
38
|
+
import moment from 'moment';
|
|
39
|
+
import _ from 'lodash';
|
|
40
|
+
|
|
41
|
+
export default function PmCalcDebugViewer(props) {
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
metersPmSchedule,
|
|
45
|
+
onClose,
|
|
46
|
+
} = props,
|
|
47
|
+
json = metersPmSchedule?.properties.meters_pm_schedules__debug_json.json,
|
|
48
|
+
flatten = (obj, prefix = '', result = {}) => {
|
|
49
|
+
for (const key in obj) {
|
|
50
|
+
if (obj.hasOwnProperty(key)) {
|
|
51
|
+
const
|
|
52
|
+
newKey = prefix ? `${prefix}.${key}` : key,
|
|
53
|
+
value = obj[key];
|
|
54
|
+
|
|
55
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
56
|
+
// Recursively flatten nested objects
|
|
57
|
+
flatten(value, newKey, result);
|
|
58
|
+
} else if (Array.isArray(value)) {
|
|
59
|
+
// Flatten arrays using index as key
|
|
60
|
+
value.forEach((item, index) => {
|
|
61
|
+
const arrayKey = `${newKey}.${index}`;
|
|
62
|
+
if (item !== null && typeof item === 'object') {
|
|
63
|
+
flatten(item, arrayKey, result);
|
|
64
|
+
} else {
|
|
65
|
+
result[arrayKey] = item;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
// Assign primitive values and null
|
|
70
|
+
result[newKey] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
},
|
|
76
|
+
ucfirst = (string) => {
|
|
77
|
+
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
78
|
+
},
|
|
79
|
+
getPmScheduleMode = (pm_schedule_mode_id) => {
|
|
80
|
+
switch(pm_schedule_mode_id) {
|
|
81
|
+
case PM_SCHEDULE_MODES__HISTORICAL_USAGE:
|
|
82
|
+
return 'Historical Usage';
|
|
83
|
+
case PM_SCHEDULE_MODES__EXPECTED_USAGE:
|
|
84
|
+
return 'Expected Usage';
|
|
85
|
+
case PM_SCHEDULE_MODES__NO_ESTIMATION:
|
|
86
|
+
return 'No Estimation';
|
|
87
|
+
case PM_SCHEDULE_MODES__WILL_CALL:
|
|
88
|
+
return 'Will Call';
|
|
89
|
+
default:
|
|
90
|
+
return 'Unknown Mode';
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
flattenedJson = flatten(json),
|
|
94
|
+
formatDaysValue = (value, record, self) => {
|
|
95
|
+
if (value === null || value === undefined) {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
return parseInt(_.round(value), 10);
|
|
99
|
+
},
|
|
100
|
+
formatMeterValue = (value, record, self) => {
|
|
101
|
+
if (value === null || value === undefined) {
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
let ret;
|
|
105
|
+
const meterType = parseInt(record?.meter_type, 10);
|
|
106
|
+
switch(meterType) {
|
|
107
|
+
case METER_TYPES__HOURS:
|
|
108
|
+
ret = `${value} ${METER_TYPES__HOURS_UNITS}`;
|
|
109
|
+
break;
|
|
110
|
+
case METER_TYPES__MILES:
|
|
111
|
+
ret = `${value} ${METER_TYPES__MILES_UNITS}`;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
return ret;
|
|
115
|
+
},
|
|
116
|
+
formatDateValue = (value, record, self) => {
|
|
117
|
+
if (value === null || value === undefined) {
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// convert from datetime to pretty-printed date
|
|
122
|
+
return moment(value).format(MOMENT_DATE_FORMAT_6);
|
|
123
|
+
},
|
|
124
|
+
items = [
|
|
125
|
+
{
|
|
126
|
+
"type": "Column",
|
|
127
|
+
"flex": 1,
|
|
128
|
+
"defaults": {
|
|
129
|
+
labelWidth: 300,
|
|
130
|
+
},
|
|
131
|
+
"items": [
|
|
132
|
+
{
|
|
133
|
+
"type": "FieldSet",
|
|
134
|
+
"title": "General",
|
|
135
|
+
"reference": "general",
|
|
136
|
+
"defaults": {},
|
|
137
|
+
"items": [
|
|
138
|
+
{
|
|
139
|
+
label: 'PM Schedule',
|
|
140
|
+
name: 'pmSchedule.name',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
label: 'Status',
|
|
144
|
+
name: 'pm_status_name',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
label: 'Next PM Date',
|
|
148
|
+
name: 'nextPmDate',
|
|
149
|
+
viewerFormatter: formatDateValue,
|
|
150
|
+
},
|
|
151
|
+
json?.overduePms > 0 && {
|
|
152
|
+
label: 'Overdue PMs',
|
|
153
|
+
name: 'overduePms',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"type": "FieldSet",
|
|
159
|
+
"title": "Calculation Details",
|
|
160
|
+
"reference": "calcDetails",
|
|
161
|
+
"defaults": {},
|
|
162
|
+
"items": [
|
|
163
|
+
{
|
|
164
|
+
label: 'Controlling Method',
|
|
165
|
+
name: 'controllingMethod',
|
|
166
|
+
tooltip: 'Indicates whether the calculation was based on days or usage (meter). ' +
|
|
167
|
+
'If both methods are applicable, the one resulting in the earlier PM date is chosen.',
|
|
168
|
+
},
|
|
169
|
+
(json?.pm_status_id === PM_STATUSES__OVERDUE || json?.pm_status_id === PM_STATUSES__PM_DUE) && {
|
|
170
|
+
label: 'Grace Period End Date',
|
|
171
|
+
name: 'maxGracePeriodDateTime',
|
|
172
|
+
viewerFormatter: formatDateValue,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
label: 'Last Reset Date',
|
|
176
|
+
name: 'resetDate',
|
|
177
|
+
tooltip: 'Indicates whether the calculation was based on days or usage (meter). ' +
|
|
178
|
+
'If both methods are applicable, the one resulting in the earlier PM date is chosen.',
|
|
179
|
+
viewerFormatter: formatDateValue,
|
|
180
|
+
},
|
|
181
|
+
json?.workOrder?.title && {
|
|
182
|
+
label: 'Work Order',
|
|
183
|
+
name: 'workOrder.title',
|
|
184
|
+
},
|
|
185
|
+
json?.workOrder?.service_order && {
|
|
186
|
+
label: 'Work Order',
|
|
187
|
+
name: 'workOrder.service_order',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
label: 'Meter Accrued Since Last PM',
|
|
191
|
+
name: 'meterAccruedSinceLatestPm',
|
|
192
|
+
viewerFormatter: formatMeterValue,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
label: 'Meter Remaining Until Next PM',
|
|
196
|
+
name: 'meterRemainingUntilNextPm',
|
|
197
|
+
viewerFormatter: formatMeterValue,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
label: 'Days Since Last PM',
|
|
201
|
+
name: 'daysSinceLatestPm',
|
|
202
|
+
viewerFormatter: formatDaysValue,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
label: 'Days Left Until Next PM',
|
|
206
|
+
name: 'daysLeft',
|
|
207
|
+
viewerFormatter: formatDaysValue,
|
|
208
|
+
},
|
|
209
|
+
// json?.isDelayed && {
|
|
210
|
+
// label: 'Is Delayed',
|
|
211
|
+
// name: 'isDelayed',
|
|
212
|
+
// },
|
|
213
|
+
// json?.isOverride && {
|
|
214
|
+
// label: 'Is Override',
|
|
215
|
+
// name: 'isOverride',
|
|
216
|
+
// },
|
|
217
|
+
]
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"type": "FieldSet",
|
|
221
|
+
"title": "PM Schedule Config",
|
|
222
|
+
"reference": "pmSchedule",
|
|
223
|
+
"defaults": {},
|
|
224
|
+
"items": [
|
|
225
|
+
json?.pmSchedule?.interval_days && {
|
|
226
|
+
label: 'Interval Days',
|
|
227
|
+
name: 'pmSchedule.interval_days',
|
|
228
|
+
},
|
|
229
|
+
json?.pmSchedule?.interval_meter && {
|
|
230
|
+
label: 'Interval Meter',
|
|
231
|
+
name: 'pmSchedule.interval_meter',
|
|
232
|
+
viewerFormatter: formatMeterValue,
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
label: 'Mode',
|
|
236
|
+
name: 'pmScheduleMode',
|
|
237
|
+
},
|
|
238
|
+
]
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"type": "FieldSet",
|
|
242
|
+
"title": "Equipment",
|
|
243
|
+
"reference": "equipment",
|
|
244
|
+
"defaults": {},
|
|
245
|
+
"items": [
|
|
246
|
+
{
|
|
247
|
+
label: 'In Service Date',
|
|
248
|
+
name: 'inServiceDate',
|
|
249
|
+
viewerFormatter: formatDateValue,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
label: 'Latest Meter Reading',
|
|
253
|
+
name: 'latestMeterReading',
|
|
254
|
+
viewerFormatter: formatMeterValue,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
label: 'Avg Daily Meter',
|
|
258
|
+
name: 'avgDailyMeter',
|
|
259
|
+
viewerFormatter: formatMeterValue,
|
|
260
|
+
},
|
|
261
|
+
]
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
flattenedJson.pmScheduleMode = getPmScheduleMode(json?.pmSchedule.pm_schedule_mode_id);
|
|
269
|
+
|
|
270
|
+
return <Panel
|
|
271
|
+
title="PM Calculation Info"
|
|
272
|
+
className="PmCalcViewer-Panel"
|
|
273
|
+
isScrollable={true}
|
|
274
|
+
footer={<Footer className="justify-end">
|
|
275
|
+
<Button
|
|
276
|
+
{...testProps('closeBtn')}
|
|
277
|
+
key="closeBtn"
|
|
278
|
+
onPress={onClose}
|
|
279
|
+
className="text-white"
|
|
280
|
+
text="Close"
|
|
281
|
+
/>
|
|
282
|
+
</Footer>}
|
|
283
|
+
>
|
|
284
|
+
<Viewer
|
|
285
|
+
record={flattenedJson}
|
|
286
|
+
items={items}
|
|
287
|
+
/>
|
|
288
|
+
{/* <VStack className="PmCalcDebugViewer-VStack p-4">
|
|
289
|
+
|
|
290
|
+
<Text>Equipment: {metersPmSchedule.meters__nickname}</Text>
|
|
291
|
+
<Json
|
|
292
|
+
value={metersPmSchedule.meters_pm_schedules__debug_json}
|
|
293
|
+
displaySize="expanded"
|
|
294
|
+
editable={false}
|
|
295
|
+
collapsed={2}
|
|
296
|
+
/>
|
|
297
|
+
</VStack> */}
|
|
298
|
+
</Panel>;
|
|
299
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import DisplayField from '../Form/Field/DisplayField.js';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import {
|
|
4
|
+
PM_STATUSES__OK,
|
|
5
|
+
PM_STATUSES__PM_DUE,
|
|
6
|
+
PM_STATUSES__DELAYED,
|
|
7
|
+
PM_STATUSES__WILL_CALL,
|
|
8
|
+
PM_STATUSES__SCHEDULED,
|
|
9
|
+
PM_STATUSES__OVERDUE,
|
|
10
|
+
PM_STATUSES__COMPLETED,
|
|
11
|
+
} from '../../Constants/PmStatuses.js';
|
|
12
|
+
|
|
13
|
+
export default function PmStatusesViewer(props) {
|
|
14
|
+
let text = '';
|
|
15
|
+
switch(props.id) {
|
|
16
|
+
case PM_STATUSES__OK:
|
|
17
|
+
text = 'OK';
|
|
18
|
+
break;
|
|
19
|
+
case PM_STATUSES__PM_DUE:
|
|
20
|
+
text = 'Due';
|
|
21
|
+
break;
|
|
22
|
+
case PM_STATUSES__DELAYED:
|
|
23
|
+
text = 'Delayed';
|
|
24
|
+
break;
|
|
25
|
+
case PM_STATUSES__WILL_CALL:
|
|
26
|
+
text = 'Will Call';
|
|
27
|
+
break;
|
|
28
|
+
case PM_STATUSES__SCHEDULED:
|
|
29
|
+
text = 'Scheduled';
|
|
30
|
+
break;
|
|
31
|
+
case PM_STATUSES__OVERDUE:
|
|
32
|
+
text = 'Overdue';
|
|
33
|
+
break;
|
|
34
|
+
case PM_STATUSES__COMPLETED:
|
|
35
|
+
text = 'Completed';
|
|
36
|
+
break;
|
|
37
|
+
default:
|
|
38
|
+
text = 'Unknown';
|
|
39
|
+
}
|
|
40
|
+
let className = clsx(
|
|
41
|
+
'flex-none',
|
|
42
|
+
);
|
|
43
|
+
if (props.className) {
|
|
44
|
+
className += ' ' + props.className;
|
|
45
|
+
}
|
|
46
|
+
return <DisplayField
|
|
47
|
+
text={text}
|
|
48
|
+
{...props}
|
|
49
|
+
className={className}
|
|
50
|
+
/>;
|
|
51
|
+
}
|