blixify-ui-web-mcp 0.1.19 → 0.1.21
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/lib/components/action/iconButton/iconButton.stories.tsx +11 -2
- package/lib/components/auth/signUp/signUp.stories.tsx +39 -1
- package/lib/components/chart/statsCard/README.md +24 -0
- package/lib/components/chart/statsCard/statsCard.stories.tsx +68 -0
- package/lib/components/chat/chatBase/chatBase.stories.tsx +1 -1
- package/lib/components/data/dataGraph/dataGraph.stories.tsx +5 -5
- package/lib/components/data/dataTemplate/README.md +204 -85
- package/lib/components/data/dataTemplate/dataTemplate.stories.tsx +307 -295
- package/lib/components/data/dataTemplate/drive.stories.tsx +197 -0
- package/lib/components/data/dataWrapper/README.md +109 -22
- package/lib/components/data/dataWrapper/dataWrapper.bareHooks.stories.tsx +951 -0
- package/lib/components/data/dataWrapper/dataWrapper.edit.stories.tsx +356 -5
- package/lib/components/data/dataWrapper/dataWrapper.view.stories.tsx +79 -5
- package/lib/components/display/calendar/calendar.stories.tsx +1 -1
- package/lib/components/display/detailList/README.md +11 -10
- package/lib/components/display/detailList/detailList.stories.tsx +16 -3
- package/lib/components/display/feeds/README.md +3 -3
- package/lib/components/display/feeds/feeds.stories.tsx +3 -3
- package/lib/components/display/filePreview/filePreview.stories.tsx +111 -0
- package/lib/components/display/modal/README.md +3 -0
- package/lib/components/display/modal/modal.stories.tsx +50 -1
- package/lib/components/display/newCalendar/README.md +0 -6
- package/lib/components/display/newCalendar/newCalendar.stories.tsx +1 -1
- package/lib/components/display/newFeeds/README.md +1 -5
- package/lib/components/display/newFeeds/newFeeds.stories.tsx +3 -3
- package/lib/components/display/newKanban/README.md +0 -7
- package/lib/components/display/newList/README.md +1 -4
- package/lib/components/display/newTable/README.md +29 -9
- package/lib/components/display/newTable/newTable.stories.tsx +120 -9
- package/lib/components/display/tag/README.md +1 -1
- package/lib/components/display/tag/tag.stories.tsx +7 -0
- package/lib/components/input/markdown/markdown.stories.tsx +3 -1
- package/lib/components/input/uploadInput/uploadInput.stories.tsx +41 -0
- package/lib/components/navigation/leftNav/leftNav.stories.tsx +175 -0
- package/lib/components/navigation/tabs/tabs.stories.tsx +23 -0
- package/lib/components/template/calendarView/calendarView.stories.tsx +4 -4
- package/lib/components/template/chatDashboard/chatDashboard.stories.tsx +2 -2
- package/lib/mcp/tools/getBackendComponent.d.ts.map +1 -1
- package/lib/mcp/tools/getBackendComponent.js +7 -3
- package/lib/mcp/tools/getBackendComponent.js.map +1 -1
- package/lib/mcp/tools/suggestTemplate.js +3 -3
- package/lib/mcp/tools/suggestTemplate.js.map +1 -1
- package/package.json +1 -1
- package/lib/components/data/dataTemplate/dataModule.stories.tsx +0 -191
- package/lib/components/data/dataWrapper/dataWrapper.consumer.stories.tsx +0 -2125
- package/lib/components/data/dataWrapper/dataWrapper.utils.stories.tsx +0 -576
- package/lib/components/data/flexFilter/flexFilter.stories.tsx +0 -171
|
@@ -1,2125 +0,0 @@
|
|
|
1
|
-
// INFO: consumer fixtures — the DataTemplateWrapper configurations the apps
|
|
2
|
-
// (KPJ on Blixify Space, Verdant) run in production, copied field for field,
|
|
3
|
-
// so a library change can be checked against the real shapes before an app
|
|
4
|
-
// upgrades. Every story mocks its own API and opens without a server.
|
|
5
|
-
import React from "react";
|
|
6
|
-
import MockAdapter from "axios-mock-adapter";
|
|
7
|
-
import axios from "axios";
|
|
8
|
-
import { DataTemplateWrapper } from "./index";
|
|
9
|
-
import { Button } from "../../action/button";
|
|
10
|
-
import { ButtonGroup } from "../../action/buttonGroup";
|
|
11
|
-
import { Loading } from "../../display/loading";
|
|
12
|
-
import {
|
|
13
|
-
MeraqueWorkOrderSchema,
|
|
14
|
-
PostCheckListTestSchema,
|
|
15
|
-
VerdantSimulatorSchema,
|
|
16
|
-
} from "../testSchema";
|
|
17
|
-
import { useEffect, useRef, useState } from "react";
|
|
18
|
-
|
|
19
|
-
const meta = {
|
|
20
|
-
title: "Data/Data Wrapper/Consumer Fixtures",
|
|
21
|
-
// INFO: these stories are whole pages — Storybook's default padded frame
|
|
22
|
-
// would put a white border around the grey page
|
|
23
|
-
parameters: { layout: "fullscreen" },
|
|
24
|
-
decorators: [
|
|
25
|
-
(Story: () => React.ReactElement) => (
|
|
26
|
-
<div className="bg-gray-100 min-h-screen p-4">
|
|
27
|
-
<Story />
|
|
28
|
-
</div>
|
|
29
|
-
),
|
|
30
|
-
],
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export default meta;
|
|
34
|
-
|
|
35
|
-
const VERDANT_ENDPOINT = "https://verdant-fixture.blixify.invalid/api/data";
|
|
36
|
-
const IMAGE_COLLECTION = "verdant-installer-precheck-images";
|
|
37
|
-
|
|
38
|
-
const handleClone = (value: any) =>
|
|
39
|
-
value === null || value === undefined
|
|
40
|
-
? value
|
|
41
|
-
: JSON.parse(JSON.stringify(value));
|
|
42
|
-
|
|
43
|
-
const handleInstallMock = (
|
|
44
|
-
collection: string,
|
|
45
|
-
store: any[],
|
|
46
|
-
counters: { update: number },
|
|
47
|
-
onCount: () => void,
|
|
48
|
-
) => {
|
|
49
|
-
const mock = new MockAdapter(axios, {
|
|
50
|
-
onNoMatch: "passthrough",
|
|
51
|
-
delayResponse: 50,
|
|
52
|
-
});
|
|
53
|
-
const handleParseBody = (config: any) => {
|
|
54
|
-
try {
|
|
55
|
-
return JSON.parse(config?.data ?? "{}");
|
|
56
|
-
} catch (err) {
|
|
57
|
-
return {};
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
mock
|
|
61
|
-
.onPost(`${VERDANT_ENDPOINT}/${collection}/list`)
|
|
62
|
-
.reply(() => [200, { data: handleClone(store), count: store.length }]);
|
|
63
|
-
mock
|
|
64
|
-
.onPost(`${VERDANT_ENDPOINT}/${collection}/count`)
|
|
65
|
-
.reply(() => [200, { data: store.length, count: store.length }]);
|
|
66
|
-
mock.onPost(`${VERDANT_ENDPOINT}/${collection}/get`).reply((config) => {
|
|
67
|
-
const body = handleParseBody(config);
|
|
68
|
-
return [
|
|
69
|
-
200,
|
|
70
|
-
{
|
|
71
|
-
// INFO: clone like a real HTTP response would — the list view decorates
|
|
72
|
-
// the rows it receives, and a shared object would leak that into the
|
|
73
|
-
// read view
|
|
74
|
-
data: handleClone(
|
|
75
|
-
store.find((eachRow) => eachRow._id === body.id) ?? null,
|
|
76
|
-
),
|
|
77
|
-
},
|
|
78
|
-
];
|
|
79
|
-
});
|
|
80
|
-
mock.onPost(`${VERDANT_ENDPOINT}/${collection}/update`).reply((config) => {
|
|
81
|
-
counters.update += 1;
|
|
82
|
-
onCount();
|
|
83
|
-
const body = handleParseBody(config);
|
|
84
|
-
const index = store.findIndex((eachRow) => eachRow._id === body.id);
|
|
85
|
-
if (index >= 0) store[index] = { ...store[index], ...(body.data ?? {}) };
|
|
86
|
-
return [200, { success: true }];
|
|
87
|
-
});
|
|
88
|
-
return mock;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// INFO: Verdant passes its own isMobile flag to every list; the fixtures derive
|
|
92
|
-
// it from the viewport so the phone card list + floating search show up on a
|
|
93
|
-
// narrow tunnel
|
|
94
|
-
const useIsMobile = () => {
|
|
95
|
-
const [isMobile, setIsMobile] = useState<boolean>(
|
|
96
|
-
typeof window !== "undefined" && window.innerWidth < 768,
|
|
97
|
-
);
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
const handleResize = () => setIsMobile(window.innerWidth < 768);
|
|
100
|
-
window.addEventListener("resize", handleResize);
|
|
101
|
-
return () => window.removeEventListener("resize", handleResize);
|
|
102
|
-
}, []);
|
|
103
|
-
return isMobile;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const workspace = {
|
|
107
|
-
apiEndpointType: "mongoDB" as const,
|
|
108
|
-
apiEndpoint: VERDANT_ENDPOINT,
|
|
109
|
-
apiEndpointToken: "",
|
|
110
|
-
apiUserToken: "",
|
|
111
|
-
assetEndpoint: "",
|
|
112
|
-
assetUploadEndpoint: "",
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
// KPJ · Grant proposal form
|
|
117
|
-
// The shape of Blixify Space's kpj-projects module (a MOHE ReDI-style grant
|
|
118
|
-
// application): tabs on a left rail, sections with descriptions, a radio list,
|
|
119
|
-
// a pill boolean and a wide nested sheet — trimmed to three tabs.
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
const LONG_ENDPOINT = "https://longform.blixify.invalid/api/data";
|
|
122
|
-
const LONG_COLLECTION = "long-proposals";
|
|
123
|
-
|
|
124
|
-
const LongFormSchema = [
|
|
125
|
-
{
|
|
126
|
-
id: "title",
|
|
127
|
-
name: "Research Title",
|
|
128
|
-
type: "string" as const,
|
|
129
|
-
tabId: "A. Identification",
|
|
130
|
-
groupId: "Research Details",
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
id: "abbreviation",
|
|
134
|
-
name: "Title Abbreviation",
|
|
135
|
-
type: "string" as const,
|
|
136
|
-
optional: true,
|
|
137
|
-
tabId: "A. Identification",
|
|
138
|
-
groupId: "Research Details",
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
id: "status",
|
|
142
|
-
name: "Status",
|
|
143
|
-
type: "list" as const,
|
|
144
|
-
listSelection: ["Draft", "Submitted", "Approved"],
|
|
145
|
-
optional: true,
|
|
146
|
-
tabId: "A. Identification",
|
|
147
|
-
groupId: "Application Details",
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
id: "cluster",
|
|
151
|
-
name: "Research Cluster",
|
|
152
|
-
type: "list" as const,
|
|
153
|
-
listView: "radioview" as const,
|
|
154
|
-
listSelection: [
|
|
155
|
-
"Food Safety & Security",
|
|
156
|
-
"Basic Infrastructure",
|
|
157
|
-
"Climate Change & Environment",
|
|
158
|
-
"Health",
|
|
159
|
-
"Education & Civil Society",
|
|
160
|
-
"National Security",
|
|
161
|
-
"Social & Economic Wellbeing",
|
|
162
|
-
"Frontier Technologies",
|
|
163
|
-
],
|
|
164
|
-
tabId: "B. Classification",
|
|
165
|
-
groupId: "Alignment",
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
id: "humanSubjects",
|
|
169
|
-
name: "Involves human subjects",
|
|
170
|
-
type: "boolean" as const,
|
|
171
|
-
enableRadioButtons: true,
|
|
172
|
-
optional: true,
|
|
173
|
-
tabId: "B. Classification",
|
|
174
|
-
groupId: "Ethics",
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
id: "dayLog",
|
|
178
|
-
name: "Daily Log",
|
|
179
|
-
type: "listObject" as const,
|
|
180
|
-
listFlexTable: true,
|
|
181
|
-
listFlexCategories: [{ title: "Day 1" }, { title: "Day 2" }],
|
|
182
|
-
optional: true,
|
|
183
|
-
tabId: "C. Site Log",
|
|
184
|
-
groupId: "Checklist",
|
|
185
|
-
objectData: [
|
|
186
|
-
{
|
|
187
|
-
id: "panels",
|
|
188
|
-
name: "Panels installed",
|
|
189
|
-
type: "number" as const,
|
|
190
|
-
optional: true,
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
id: "inverters",
|
|
194
|
-
name: "Inverters mounted",
|
|
195
|
-
type: "number" as const,
|
|
196
|
-
optional: true,
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
id: "cablesMetres",
|
|
200
|
-
name: "Cable run (m)",
|
|
201
|
-
type: "number" as const,
|
|
202
|
-
optional: true,
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
id: "earthing",
|
|
206
|
-
name: "Earthing points",
|
|
207
|
-
type: "number" as const,
|
|
208
|
-
optional: true,
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
id: "brackets",
|
|
212
|
-
name: "Roof brackets",
|
|
213
|
-
type: "number" as const,
|
|
214
|
-
optional: true,
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
id: "optimisers",
|
|
218
|
-
name: "Optimisers fitted",
|
|
219
|
-
type: "number" as const,
|
|
220
|
-
optional: true,
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
id: "crew",
|
|
224
|
-
name: "Crew on site",
|
|
225
|
-
type: "number" as const,
|
|
226
|
-
optional: true,
|
|
227
|
-
},
|
|
228
|
-
],
|
|
229
|
-
},
|
|
230
|
-
];
|
|
231
|
-
|
|
232
|
-
const sampleRecord = {
|
|
233
|
-
_id: "long-01",
|
|
234
|
-
title: "",
|
|
235
|
-
abbreviation: "",
|
|
236
|
-
status: "Draft",
|
|
237
|
-
cluster: "",
|
|
238
|
-
humanSubjects: undefined,
|
|
239
|
-
dayLog: [
|
|
240
|
-
{
|
|
241
|
-
category: "Day 1",
|
|
242
|
-
title: "Morning shift",
|
|
243
|
-
panels: 24,
|
|
244
|
-
inverters: 1,
|
|
245
|
-
cablesMetres: 120,
|
|
246
|
-
earthing: 4,
|
|
247
|
-
brackets: 48,
|
|
248
|
-
optimisers: 24,
|
|
249
|
-
crew: 4,
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
category: "Day 2",
|
|
253
|
-
title: "Commissioning",
|
|
254
|
-
panels: 0,
|
|
255
|
-
inverters: 1,
|
|
256
|
-
cablesMetres: 30,
|
|
257
|
-
earthing: 2,
|
|
258
|
-
brackets: 0,
|
|
259
|
-
optimisers: 0,
|
|
260
|
-
crew: 2,
|
|
261
|
-
},
|
|
262
|
-
],
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
const handleInstallLongFormMock = (
|
|
266
|
-
store: any[],
|
|
267
|
-
counters: { update: number },
|
|
268
|
-
onCount: () => void,
|
|
269
|
-
) => {
|
|
270
|
-
const mock = new MockAdapter(axios, {
|
|
271
|
-
onNoMatch: "passthrough",
|
|
272
|
-
delayResponse: 50,
|
|
273
|
-
});
|
|
274
|
-
const handleParseBody = (config: any) => {
|
|
275
|
-
try {
|
|
276
|
-
return JSON.parse(config?.data ?? "{}");
|
|
277
|
-
} catch (err) {
|
|
278
|
-
return {};
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
mock
|
|
282
|
-
.onPost(`${LONG_ENDPOINT}/${LONG_COLLECTION}/list`)
|
|
283
|
-
.reply(() => [200, { data: handleClone(store), count: store.length }]);
|
|
284
|
-
mock.onPost(`${LONG_ENDPOINT}/${LONG_COLLECTION}/get`).reply((config) => {
|
|
285
|
-
const body = handleParseBody(config);
|
|
286
|
-
return [
|
|
287
|
-
200,
|
|
288
|
-
{ data: store.find((eachRow) => eachRow._id === body.id) ?? null },
|
|
289
|
-
];
|
|
290
|
-
});
|
|
291
|
-
mock.onPost(`${LONG_ENDPOINT}/${LONG_COLLECTION}/update`).reply((config) => {
|
|
292
|
-
counters.update += 1;
|
|
293
|
-
onCount();
|
|
294
|
-
const body = handleParseBody(config);
|
|
295
|
-
const index = store.findIndex((eachRow) => eachRow._id === body.id);
|
|
296
|
-
if (index >= 0) store[index] = { ...store[index], ...(body.data ?? {}) };
|
|
297
|
-
return [200, { success: true }];
|
|
298
|
-
});
|
|
299
|
-
return mock;
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
export const KPJGrantProposalForm = () => {
|
|
303
|
-
const [mount, setMount] = useState<boolean>(false);
|
|
304
|
-
const [, setTick] = useState<number>(0);
|
|
305
|
-
const [viewType, setViewType] = useState<"list" | "read" | "update">(
|
|
306
|
-
"update",
|
|
307
|
-
);
|
|
308
|
-
const [selectedId, setSelectedId] = useState<string | undefined>("long-01");
|
|
309
|
-
const storeRef = useRef<any[]>([JSON.parse(JSON.stringify(sampleRecord))]);
|
|
310
|
-
const countersRef = useRef({ update: 0 });
|
|
311
|
-
const mockRef = useRef<any>(null);
|
|
312
|
-
|
|
313
|
-
useEffect(() => {
|
|
314
|
-
mockRef.current = handleInstallLongFormMock(
|
|
315
|
-
storeRef.current,
|
|
316
|
-
countersRef.current,
|
|
317
|
-
() => setTick((previous) => previous + 1),
|
|
318
|
-
);
|
|
319
|
-
setMount(true);
|
|
320
|
-
return () => {
|
|
321
|
-
mockRef.current?.restore();
|
|
322
|
-
mockRef.current = null;
|
|
323
|
-
};
|
|
324
|
-
}, []);
|
|
325
|
-
|
|
326
|
-
const handleNavigate = (type: any, id: any) => {
|
|
327
|
-
setViewType(type);
|
|
328
|
-
setSelectedId(id);
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
if (!mount) return <Loading />;
|
|
332
|
-
|
|
333
|
-
return (
|
|
334
|
-
<div className="w-full">
|
|
335
|
-
<div className="font-semibold mb-1 text-gray-900">
|
|
336
|
-
KPJ · Grant Proposal Form
|
|
337
|
-
</div>
|
|
338
|
-
<div className="text-sm font-light mb-2 text-gray-500">
|
|
339
|
-
Three tabs on a left rail with numbered steps and completion ticks,
|
|
340
|
-
sections as white cards with descriptions, a step bar with a counter, an
|
|
341
|
-
eight-option radio list in two columns, a Yes / No pill boolean, and a
|
|
342
|
-
seven-column daily log that scrolls sideways with its first column
|
|
343
|
-
pinned.
|
|
344
|
-
</div>
|
|
345
|
-
<div className="mb-4 text-xs text-gray-600 font-mono">
|
|
346
|
-
update calls:{" "}
|
|
347
|
-
<span data-testid="longform-update-count">
|
|
348
|
-
{countersRef.current.update}
|
|
349
|
-
</span>
|
|
350
|
-
</div>
|
|
351
|
-
<DataTemplateWrapper
|
|
352
|
-
title="Grant Proposal"
|
|
353
|
-
type={viewType}
|
|
354
|
-
id={selectedId}
|
|
355
|
-
collectionId={LONG_COLLECTION}
|
|
356
|
-
schema={LongFormSchema}
|
|
357
|
-
workspace={{
|
|
358
|
-
apiEndpointType: "mongoDB",
|
|
359
|
-
apiEndpoint: LONG_ENDPOINT,
|
|
360
|
-
apiEndpointToken: "",
|
|
361
|
-
apiUserToken: "",
|
|
362
|
-
assetEndpoint: "",
|
|
363
|
-
assetUploadEndpoint: "",
|
|
364
|
-
}}
|
|
365
|
-
edit={{
|
|
366
|
-
allEditable: true,
|
|
367
|
-
disabledUpdate: false,
|
|
368
|
-
deleteBtn: false,
|
|
369
|
-
enableUpdateDraft: true,
|
|
370
|
-
}}
|
|
371
|
-
bareSettings={{ bareNewDTW: true, bareSaveOnTabChange: true }}
|
|
372
|
-
organise={{
|
|
373
|
-
breadcrumbType: "button",
|
|
374
|
-
tabAlign: "left",
|
|
375
|
-
tabDescriptions: {
|
|
376
|
-
"A. Identification":
|
|
377
|
-
"Core proposal metadata: title, abbreviation and application status.",
|
|
378
|
-
"B. Classification":
|
|
379
|
-
"Research alignment, cluster and ethics declarations.",
|
|
380
|
-
"C. Site Log": "Day-by-day installation counts.",
|
|
381
|
-
},
|
|
382
|
-
groupDescriptions: {
|
|
383
|
-
"Research Details": "What the study is called.",
|
|
384
|
-
"Application Details":
|
|
385
|
-
"Where the application sits in the pipeline.",
|
|
386
|
-
Checklist:
|
|
387
|
-
"One row per day; scroll sideways for the later columns.",
|
|
388
|
-
},
|
|
389
|
-
}}
|
|
390
|
-
handleNavigate={handleNavigate}
|
|
391
|
-
limit={10}
|
|
392
|
-
/>
|
|
393
|
-
</div>
|
|
394
|
-
);
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
// ---------------------------------------------------------------------------
|
|
398
|
-
// Verdant · Installer pre-installation checklist
|
|
399
|
-
// components/base/installer/InstallerChecklistComponent.tsx, checklistType
|
|
400
|
-
// "preCheckList", desktop tab titles. tabAlign "stepper", draft saves on every
|
|
401
|
-
// tab change, action bar at the bottom, no custom action component in the
|
|
402
|
-
// update view.
|
|
403
|
-
// ---------------------------------------------------------------------------
|
|
404
|
-
|
|
405
|
-
const PRECHECK_COLLECTION = "verdant-installer-precheck";
|
|
406
|
-
|
|
407
|
-
const TAB_BASIC = "Basic Information";
|
|
408
|
-
const TAB_MATERIAL = "Material & Equipment Check";
|
|
409
|
-
const TAB_PANEL = "Panel Layout & System Design";
|
|
410
|
-
const TAB_ROOF = "Roof Condition Verification";
|
|
411
|
-
const TAB_AGREEMENT = "Client Agreement & Briefing";
|
|
412
|
-
|
|
413
|
-
const imageField = {
|
|
414
|
-
type: "multipleImage" as const,
|
|
415
|
-
imageCollectionName: IMAGE_COLLECTION,
|
|
416
|
-
fileExtension: "image/*,video/*,.dng",
|
|
417
|
-
compressfileQuality: 1,
|
|
418
|
-
maxFileSize: 1.5,
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
const PreChecklistSchema: any[] = [
|
|
422
|
-
// renderBasicInformationSchema
|
|
423
|
-
{
|
|
424
|
-
id: "clientName",
|
|
425
|
-
name: "Client Name",
|
|
426
|
-
type: "string",
|
|
427
|
-
groupId: "1. Basic Information",
|
|
428
|
-
tabId: TAB_BASIC,
|
|
429
|
-
},
|
|
430
|
-
{
|
|
431
|
-
id: "address",
|
|
432
|
-
name: "Address",
|
|
433
|
-
type: "string",
|
|
434
|
-
groupId: "1. Basic Information",
|
|
435
|
-
tabId: TAB_BASIC,
|
|
436
|
-
},
|
|
437
|
-
{
|
|
438
|
-
id: "date",
|
|
439
|
-
name: "Actual Installation Date",
|
|
440
|
-
type: "date",
|
|
441
|
-
groupId: "1. Basic Information",
|
|
442
|
-
tabId: TAB_BASIC,
|
|
443
|
-
},
|
|
444
|
-
{
|
|
445
|
-
id: "systemSize",
|
|
446
|
-
name: "System Size (kWp)",
|
|
447
|
-
type: "number",
|
|
448
|
-
groupId: "1. Basic Information",
|
|
449
|
-
tabId: TAB_BASIC,
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
id: "panelNumbers",
|
|
453
|
-
name: "Total Panels to be Installed",
|
|
454
|
-
type: "number",
|
|
455
|
-
groupId: "1. Basic Information",
|
|
456
|
-
tabId: TAB_BASIC,
|
|
457
|
-
},
|
|
458
|
-
// renderSafetyCheckSchema
|
|
459
|
-
{
|
|
460
|
-
id: "safetyHarnessPrepared",
|
|
461
|
-
name: "Safety harness prepared and worn",
|
|
462
|
-
type: "boolean",
|
|
463
|
-
enableCheckbox: true,
|
|
464
|
-
defaultValue: false,
|
|
465
|
-
groupId: "1. Safety Check",
|
|
466
|
-
tabId: TAB_MATERIAL,
|
|
467
|
-
},
|
|
468
|
-
{
|
|
469
|
-
id: "rubberCoveredShoes",
|
|
470
|
-
name: "Rubber/Covered shoe prepared and worn",
|
|
471
|
-
type: "boolean",
|
|
472
|
-
enableCheckbox: true,
|
|
473
|
-
defaultValue: false,
|
|
474
|
-
groupId: "1. Safety Check",
|
|
475
|
-
tabId: TAB_MATERIAL,
|
|
476
|
-
},
|
|
477
|
-
{
|
|
478
|
-
id: "safetyCheckImage",
|
|
479
|
-
name: "Safety Attire Images",
|
|
480
|
-
...imageField,
|
|
481
|
-
groupId: "1. Safety Check",
|
|
482
|
-
tabId: TAB_MATERIAL,
|
|
483
|
-
},
|
|
484
|
-
// renderMaterialEquipmentCheckSchema
|
|
485
|
-
{
|
|
486
|
-
id: "componentsVerification",
|
|
487
|
-
name: "Material Verification",
|
|
488
|
-
type: "boolean",
|
|
489
|
-
enableCheckbox: true,
|
|
490
|
-
optional: true,
|
|
491
|
-
groupId: "2. Materials Specifications and Condition",
|
|
492
|
-
tabId: TAB_MATERIAL,
|
|
493
|
-
},
|
|
494
|
-
{
|
|
495
|
-
id: "componentsVerificationImage",
|
|
496
|
-
name: "Upload Material Images",
|
|
497
|
-
...imageField,
|
|
498
|
-
groupId: "2. Materials Specifications and Condition",
|
|
499
|
-
tabId: TAB_MATERIAL,
|
|
500
|
-
},
|
|
501
|
-
{
|
|
502
|
-
id: "componentsRemarks",
|
|
503
|
-
name: "Material Verification Remarks",
|
|
504
|
-
type: "textArea",
|
|
505
|
-
optional: true,
|
|
506
|
-
groupId: "2. Materials Specifications and Condition",
|
|
507
|
-
tabId: TAB_MATERIAL,
|
|
508
|
-
},
|
|
509
|
-
{
|
|
510
|
-
id: "wattageConfirmation",
|
|
511
|
-
name: "Panel Wattage Confirmation",
|
|
512
|
-
type: "boolean",
|
|
513
|
-
enableCheckbox: true,
|
|
514
|
-
optional: true,
|
|
515
|
-
groupId: "2. Materials Specifications and Condition",
|
|
516
|
-
tabId: TAB_MATERIAL,
|
|
517
|
-
},
|
|
518
|
-
{
|
|
519
|
-
id: "wattageRemarks",
|
|
520
|
-
name: "Panel Wattage Confirmation Remarks",
|
|
521
|
-
type: "textArea",
|
|
522
|
-
optional: true,
|
|
523
|
-
groupId: "2. Materials Specifications and Condition",
|
|
524
|
-
tabId: TAB_MATERIAL,
|
|
525
|
-
},
|
|
526
|
-
{
|
|
527
|
-
id: "panelBrand",
|
|
528
|
-
name: "Panel Brand",
|
|
529
|
-
type: "list",
|
|
530
|
-
listSelection: ["Jinko 580W", "Longi 590W", "Canadian Solar 575W"],
|
|
531
|
-
groupId: "2. Materials Specifications and Condition",
|
|
532
|
-
tabId: TAB_MATERIAL,
|
|
533
|
-
},
|
|
534
|
-
{
|
|
535
|
-
id: "systemType",
|
|
536
|
-
name: "System Type",
|
|
537
|
-
type: "list",
|
|
538
|
-
listSelection: [
|
|
539
|
-
"Microinverter",
|
|
540
|
-
"String Inverter",
|
|
541
|
-
"Microinverter + BESS",
|
|
542
|
-
"String Inverter + BESS",
|
|
543
|
-
],
|
|
544
|
-
groupId: "2. Materials Specifications and Condition",
|
|
545
|
-
tabId: TAB_MATERIAL,
|
|
546
|
-
},
|
|
547
|
-
{
|
|
548
|
-
id: "inverterModel",
|
|
549
|
-
name: "Inverter Model",
|
|
550
|
-
type: "string",
|
|
551
|
-
groupId: "2. Materials Specifications and Condition",
|
|
552
|
-
tabId: TAB_MATERIAL,
|
|
553
|
-
},
|
|
554
|
-
{
|
|
555
|
-
id: "dataLoggerSN",
|
|
556
|
-
name: "Data Logger SN",
|
|
557
|
-
type: "string",
|
|
558
|
-
groupId: "2. Materials Specifications and Condition",
|
|
559
|
-
tabId: TAB_MATERIAL,
|
|
560
|
-
},
|
|
561
|
-
{
|
|
562
|
-
id: "serialNumber",
|
|
563
|
-
name: "Inverter Serial Number",
|
|
564
|
-
type: "string",
|
|
565
|
-
groupId: "2. Materials Specifications and Condition",
|
|
566
|
-
tabId: TAB_MATERIAL,
|
|
567
|
-
},
|
|
568
|
-
{
|
|
569
|
-
id: "batterySerialNumber",
|
|
570
|
-
name: "Battery Serial Number",
|
|
571
|
-
type: "string",
|
|
572
|
-
groupId: "2. Materials Specifications and Condition",
|
|
573
|
-
tabId: TAB_MATERIAL,
|
|
574
|
-
hideWhen: [
|
|
575
|
-
{
|
|
576
|
-
type: "!in",
|
|
577
|
-
queryId: "systemType",
|
|
578
|
-
value: ["Microinverter + BESS", "String Inverter + BESS"],
|
|
579
|
-
},
|
|
580
|
-
],
|
|
581
|
-
},
|
|
582
|
-
// INFO: the string-settings sheet lives in the post-installation checklist
|
|
583
|
-
// (renderRoofAndPanelStructure); it sits here so the fixture also exercises
|
|
584
|
-
// the FlexTable inside a stepper form
|
|
585
|
-
{
|
|
586
|
-
id: "stringInverterSettings",
|
|
587
|
-
name: "String Inverter Settings",
|
|
588
|
-
type: "listObject",
|
|
589
|
-
listFlexTable: true,
|
|
590
|
-
listFlexCategories: [
|
|
591
|
-
{ title: "String Number", description: "String 1 to String 4" },
|
|
592
|
-
],
|
|
593
|
-
listFlexMaxRows: 4,
|
|
594
|
-
optional: true,
|
|
595
|
-
groupId: "2. Materials Specifications and Condition",
|
|
596
|
-
tabId: TAB_MATERIAL,
|
|
597
|
-
objectData: [
|
|
598
|
-
{
|
|
599
|
-
id: "voltageCheck",
|
|
600
|
-
name: "Voltage Check (VoC)",
|
|
601
|
-
type: "number",
|
|
602
|
-
minNumber: 0.1,
|
|
603
|
-
},
|
|
604
|
-
{
|
|
605
|
-
id: "cableLength",
|
|
606
|
-
name: "Cable Length (meters)",
|
|
607
|
-
type: "number",
|
|
608
|
-
minNumber: 1,
|
|
609
|
-
},
|
|
610
|
-
{ id: "tiltAngle", name: "Tilt Angle (°)", type: "number" },
|
|
611
|
-
{
|
|
612
|
-
id: "orientation",
|
|
613
|
-
name: "Orientation",
|
|
614
|
-
type: "list",
|
|
615
|
-
listSelection: [
|
|
616
|
-
"North",
|
|
617
|
-
"South",
|
|
618
|
-
"East",
|
|
619
|
-
"West",
|
|
620
|
-
"North-East",
|
|
621
|
-
"North-West",
|
|
622
|
-
"South-East",
|
|
623
|
-
"South-West",
|
|
624
|
-
],
|
|
625
|
-
listNewValue: true,
|
|
626
|
-
},
|
|
627
|
-
],
|
|
628
|
-
hideWhen: [
|
|
629
|
-
{
|
|
630
|
-
type: "!in",
|
|
631
|
-
queryId: "systemType",
|
|
632
|
-
value: ["String Inverter", "String Inverter + BESS"],
|
|
633
|
-
},
|
|
634
|
-
],
|
|
635
|
-
},
|
|
636
|
-
{
|
|
637
|
-
id: "clientDBCondition",
|
|
638
|
-
name: "Client DB Condition Verification",
|
|
639
|
-
type: "boolean",
|
|
640
|
-
enableCheckbox: true,
|
|
641
|
-
optional: true,
|
|
642
|
-
groupId: "3. Client DB Condition",
|
|
643
|
-
tabId: TAB_MATERIAL,
|
|
644
|
-
},
|
|
645
|
-
{
|
|
646
|
-
id: "conditionRemarks",
|
|
647
|
-
name: "Client DB Condition Verification Remarks",
|
|
648
|
-
type: "textArea",
|
|
649
|
-
optional: true,
|
|
650
|
-
groupId: "3. Client DB Condition",
|
|
651
|
-
tabId: TAB_MATERIAL,
|
|
652
|
-
},
|
|
653
|
-
{
|
|
654
|
-
id: "conditionPictures",
|
|
655
|
-
name: "Pictures of Condition",
|
|
656
|
-
...imageField,
|
|
657
|
-
groupId: "3. Client DB Condition",
|
|
658
|
-
tabId: TAB_MATERIAL,
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
id: "skyliftArrivalImage",
|
|
662
|
-
name: "Skylift Arrival Images",
|
|
663
|
-
...imageField,
|
|
664
|
-
optional: true,
|
|
665
|
-
groupId: "4. Skylift Arrival",
|
|
666
|
-
tabId: TAB_MATERIAL,
|
|
667
|
-
},
|
|
668
|
-
{
|
|
669
|
-
id: "skyliftArrivalTime",
|
|
670
|
-
name: "Skylift Arrival Time",
|
|
671
|
-
type: "date",
|
|
672
|
-
showTime: true,
|
|
673
|
-
optional: true,
|
|
674
|
-
groupId: "4. Skylift Arrival",
|
|
675
|
-
tabId: TAB_MATERIAL,
|
|
676
|
-
},
|
|
677
|
-
// renderPanelLayoutSystemDesignSchema
|
|
678
|
-
{
|
|
679
|
-
id: "placementAgreement",
|
|
680
|
-
name: "Client Agreement in Layout and Placement",
|
|
681
|
-
type: "boolean",
|
|
682
|
-
enableCheckbox: true,
|
|
683
|
-
optional: true,
|
|
684
|
-
groupId: "1. Panel Layout and Inverter Placement",
|
|
685
|
-
tabId: TAB_PANEL,
|
|
686
|
-
},
|
|
687
|
-
{
|
|
688
|
-
id: "placementRemarks",
|
|
689
|
-
name: "Client Agreement in Layout and Placement Remarks",
|
|
690
|
-
type: "textArea",
|
|
691
|
-
optional: true,
|
|
692
|
-
groupId: "1. Panel Layout and Inverter Placement",
|
|
693
|
-
tabId: TAB_PANEL,
|
|
694
|
-
},
|
|
695
|
-
{
|
|
696
|
-
id: "placementImage",
|
|
697
|
-
name: "Upload New Layout with Installer Draft Image (If disagreement with current layout)",
|
|
698
|
-
...imageField,
|
|
699
|
-
optional: true,
|
|
700
|
-
groupId: "1. Panel Layout and Inverter Placement",
|
|
701
|
-
tabId: TAB_PANEL,
|
|
702
|
-
},
|
|
703
|
-
{
|
|
704
|
-
id: "inverterLocationImage",
|
|
705
|
-
name: "Inverter Location Images",
|
|
706
|
-
...imageField,
|
|
707
|
-
groupId: "2. Inverter Location",
|
|
708
|
-
tabId: TAB_PANEL,
|
|
709
|
-
},
|
|
710
|
-
{
|
|
711
|
-
id: "routingLengthOptimization",
|
|
712
|
-
name: "Client has been briefed and agrees on cable and casing placement?",
|
|
713
|
-
type: "boolean",
|
|
714
|
-
enableCheckbox: true,
|
|
715
|
-
optional: true,
|
|
716
|
-
groupId: "3. Cable and Casing Placement",
|
|
717
|
-
tabId: TAB_PANEL,
|
|
718
|
-
},
|
|
719
|
-
{
|
|
720
|
-
id: "routingRemarks",
|
|
721
|
-
name: "Issues with cable and casing placement",
|
|
722
|
-
type: "textArea",
|
|
723
|
-
optional: true,
|
|
724
|
-
groupId: "3. Cable and Casing Placement",
|
|
725
|
-
tabId: TAB_PANEL,
|
|
726
|
-
},
|
|
727
|
-
{
|
|
728
|
-
id: "existingDamageNoted",
|
|
729
|
-
name: "No Existing Damage Noted",
|
|
730
|
-
type: "boolean",
|
|
731
|
-
enableCheckbox: true,
|
|
732
|
-
optional: true,
|
|
733
|
-
groupId: "4. Existing Damage in the House now",
|
|
734
|
-
tabId: TAB_PANEL,
|
|
735
|
-
},
|
|
736
|
-
{
|
|
737
|
-
id: "existingDamagePhoto",
|
|
738
|
-
name: "Upload Existing Damage Photo (If Noted)",
|
|
739
|
-
...imageField,
|
|
740
|
-
optional: true,
|
|
741
|
-
groupId: "4. Existing Damage in the House now",
|
|
742
|
-
tabId: TAB_PANEL,
|
|
743
|
-
},
|
|
744
|
-
{
|
|
745
|
-
id: "existingDamageRemarks",
|
|
746
|
-
name: "Existing Damage Noted Remarks",
|
|
747
|
-
type: "textArea",
|
|
748
|
-
optional: true,
|
|
749
|
-
groupId: "4. Existing Damage in the House now",
|
|
750
|
-
tabId: TAB_PANEL,
|
|
751
|
-
},
|
|
752
|
-
// renderRoofConditionVerificationSchema
|
|
753
|
-
{
|
|
754
|
-
id: "freeDamage",
|
|
755
|
-
name: "Free of Damage Confirmation",
|
|
756
|
-
type: "boolean",
|
|
757
|
-
enableCheckbox: true,
|
|
758
|
-
optional: true,
|
|
759
|
-
groupId: "1. Roof Condition Verification",
|
|
760
|
-
tabId: TAB_ROOF,
|
|
761
|
-
},
|
|
762
|
-
{
|
|
763
|
-
id: "freeDamageRemarks",
|
|
764
|
-
name: "Roof Condition Remarks",
|
|
765
|
-
type: "textArea",
|
|
766
|
-
optional: true,
|
|
767
|
-
groupId: "1. Roof Condition Verification",
|
|
768
|
-
tabId: TAB_ROOF,
|
|
769
|
-
},
|
|
770
|
-
{
|
|
771
|
-
id: "freeDamageImage",
|
|
772
|
-
name: "Roof Condition Images",
|
|
773
|
-
...imageField,
|
|
774
|
-
groupId: "1. Roof Condition Verification",
|
|
775
|
-
tabId: TAB_ROOF,
|
|
776
|
-
},
|
|
777
|
-
{
|
|
778
|
-
id: "roofStructureConfirmation",
|
|
779
|
-
name: "Roof Structure Confirmation",
|
|
780
|
-
type: "boolean",
|
|
781
|
-
enableCheckbox: true,
|
|
782
|
-
optional: true,
|
|
783
|
-
groupId: "3. Roof Structure",
|
|
784
|
-
tabId: TAB_ROOF,
|
|
785
|
-
},
|
|
786
|
-
{
|
|
787
|
-
id: "roofStructureRemarks",
|
|
788
|
-
name: "Roof Structure Confirmation Remarks",
|
|
789
|
-
type: "textArea",
|
|
790
|
-
optional: true,
|
|
791
|
-
groupId: "3. Roof Structure",
|
|
792
|
-
tabId: TAB_ROOF,
|
|
793
|
-
},
|
|
794
|
-
{
|
|
795
|
-
id: "roofMaterials",
|
|
796
|
-
name: "Roof Materials",
|
|
797
|
-
type: "list",
|
|
798
|
-
listSelection: [
|
|
799
|
-
"Clay Tiles",
|
|
800
|
-
"Concrete Tiles",
|
|
801
|
-
"Metal Deck",
|
|
802
|
-
"Asbestos",
|
|
803
|
-
"Other",
|
|
804
|
-
],
|
|
805
|
-
groupId: "3. Roof Structure",
|
|
806
|
-
tabId: TAB_ROOF,
|
|
807
|
-
},
|
|
808
|
-
{
|
|
809
|
-
id: "trussDimension",
|
|
810
|
-
name: "Truss Dimension",
|
|
811
|
-
type: "list",
|
|
812
|
-
listSelection: ["2x1", "2x2", "2x3", "Other"],
|
|
813
|
-
groupId: "3. Roof Structure",
|
|
814
|
-
tabId: TAB_ROOF,
|
|
815
|
-
},
|
|
816
|
-
{
|
|
817
|
-
id: "trussDimensionOther",
|
|
818
|
-
name: "Truss Dimension (Other)",
|
|
819
|
-
type: "string",
|
|
820
|
-
groupId: "3. Roof Structure",
|
|
821
|
-
tabId: TAB_ROOF,
|
|
822
|
-
hideWhen: [{ type: "!=", queryId: "trussDimension", value: "Other" }],
|
|
823
|
-
},
|
|
824
|
-
{
|
|
825
|
-
id: "ladderAccessPointImage",
|
|
826
|
-
name: "Ladder Access Point Images",
|
|
827
|
-
...imageField,
|
|
828
|
-
groupId: "3. Roof Structure",
|
|
829
|
-
tabId: TAB_ROOF,
|
|
830
|
-
},
|
|
831
|
-
// renderClientAgreementBriefingSchema
|
|
832
|
-
{
|
|
833
|
-
id: "briefingProvided",
|
|
834
|
-
name: "Client has been briefed on cable layout, remarks and damage found.",
|
|
835
|
-
type: "boolean",
|
|
836
|
-
enableCheckbox: true,
|
|
837
|
-
optional: true,
|
|
838
|
-
groupId: "1. Client Agreement & Briefing",
|
|
839
|
-
tabId: TAB_AGREEMENT,
|
|
840
|
-
},
|
|
841
|
-
{
|
|
842
|
-
id: "customerRemarks",
|
|
843
|
-
name: "Briefing Provided Remarks",
|
|
844
|
-
type: "textArea",
|
|
845
|
-
optional: true,
|
|
846
|
-
groupId: "1. Client Agreement & Briefing",
|
|
847
|
-
tabId: TAB_AGREEMENT,
|
|
848
|
-
},
|
|
849
|
-
{
|
|
850
|
-
id: "customerSignature",
|
|
851
|
-
name: "Customer's Signature",
|
|
852
|
-
type: "signature",
|
|
853
|
-
groupId: "1. Client Agreement & Briefing",
|
|
854
|
-
tabId: TAB_AGREEMENT,
|
|
855
|
-
},
|
|
856
|
-
];
|
|
857
|
-
|
|
858
|
-
const preChecklistRecord = {
|
|
859
|
-
_id: "precheck-01",
|
|
860
|
-
clientName: "Tan Wei Ling",
|
|
861
|
-
address: "12, Jalan Setia Impian 3, Setia Alam, 40170 Shah Alam",
|
|
862
|
-
date: new Date("2026-09-02T01:00:00.000Z"),
|
|
863
|
-
systemSize: 8.7,
|
|
864
|
-
panelNumbers: 15,
|
|
865
|
-
safetyHarnessPrepared: true,
|
|
866
|
-
rubberCoveredShoes: true,
|
|
867
|
-
componentsVerification: true,
|
|
868
|
-
panelBrand: "Jinko 580W",
|
|
869
|
-
systemType: "String Inverter",
|
|
870
|
-
inverterModel: "Huawei SUN2000-8KTL-L1",
|
|
871
|
-
dataLoggerSN: "DL-2026-00418",
|
|
872
|
-
serialNumber: "HV2260041812",
|
|
873
|
-
stringInverterSettings: [
|
|
874
|
-
{
|
|
875
|
-
category: "String Number",
|
|
876
|
-
title: "String 1",
|
|
877
|
-
voltageCheck: 412.5,
|
|
878
|
-
cableLength: 18,
|
|
879
|
-
tiltAngle: 15,
|
|
880
|
-
orientation: "South",
|
|
881
|
-
},
|
|
882
|
-
{
|
|
883
|
-
category: "String Number",
|
|
884
|
-
title: "String 2",
|
|
885
|
-
voltageCheck: 408.1,
|
|
886
|
-
cableLength: 22,
|
|
887
|
-
tiltAngle: 15,
|
|
888
|
-
orientation: "South-West",
|
|
889
|
-
},
|
|
890
|
-
],
|
|
891
|
-
clientDBCondition: true,
|
|
892
|
-
roofMaterials: "Concrete Tiles",
|
|
893
|
-
trussDimension: "2x2",
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
export const VerdantInstallerPreChecklist = () => {
|
|
897
|
-
const [mount, setMount] = useState<boolean>(false);
|
|
898
|
-
const isMobile = useIsMobile();
|
|
899
|
-
const [, setTick] = useState<number>(0);
|
|
900
|
-
const [viewType, setViewType] = useState<"list" | "read" | "update">(
|
|
901
|
-
"update",
|
|
902
|
-
);
|
|
903
|
-
const [selectedId, setSelectedId] = useState<string | undefined>(
|
|
904
|
-
"precheck-01",
|
|
905
|
-
);
|
|
906
|
-
const storeRef = useRef<any[]>([
|
|
907
|
-
JSON.parse(JSON.stringify(preChecklistRecord)),
|
|
908
|
-
]);
|
|
909
|
-
const countersRef = useRef({ update: 0 });
|
|
910
|
-
const mockRef = useRef<any>(null);
|
|
911
|
-
|
|
912
|
-
useEffect(() => {
|
|
913
|
-
mockRef.current = handleInstallMock(
|
|
914
|
-
PRECHECK_COLLECTION,
|
|
915
|
-
storeRef.current,
|
|
916
|
-
countersRef.current,
|
|
917
|
-
() => setTick((previous) => previous + 1),
|
|
918
|
-
);
|
|
919
|
-
setMount(true);
|
|
920
|
-
return () => {
|
|
921
|
-
mockRef.current?.restore();
|
|
922
|
-
mockRef.current = null;
|
|
923
|
-
};
|
|
924
|
-
}, []);
|
|
925
|
-
|
|
926
|
-
const handleNavigate = (type: any, id: any) => {
|
|
927
|
-
setViewType(type);
|
|
928
|
-
setSelectedId(id);
|
|
929
|
-
};
|
|
930
|
-
|
|
931
|
-
if (!mount) return <Loading />;
|
|
932
|
-
|
|
933
|
-
return (
|
|
934
|
-
<div className="w-full">
|
|
935
|
-
<div className="font-semibold mb-1 text-gray-900">
|
|
936
|
-
Verdant · Installer Pre-Installation Checklist
|
|
937
|
-
</div>
|
|
938
|
-
<div className="text-sm font-light mb-2 text-gray-500">
|
|
939
|
-
The production configuration of InstallerChecklistComponent for
|
|
940
|
-
checklistType "preCheckList": five stepper tabs, checkbox
|
|
941
|
-
booleans, image uploads, remarks, a signature, conditional fields
|
|
942
|
-
(Battery Serial Number shows for BESS system types; String Inverter
|
|
943
|
-
Settings for string inverters), draft saved on every tab change, action
|
|
944
|
-
bar at the bottom.
|
|
945
|
-
</div>
|
|
946
|
-
<div className="mb-4 text-xs text-gray-600 font-mono">
|
|
947
|
-
update calls:{" "}
|
|
948
|
-
<span data-testid="precheck-update-count">
|
|
949
|
-
{countersRef.current.update}
|
|
950
|
-
</span>
|
|
951
|
-
</div>
|
|
952
|
-
<DataTemplateWrapper
|
|
953
|
-
title="Pre-Installation Checklist"
|
|
954
|
-
description="Complete every section before the installation starts."
|
|
955
|
-
type={viewType}
|
|
956
|
-
id={selectedId}
|
|
957
|
-
collectionId={PRECHECK_COLLECTION}
|
|
958
|
-
schema={PreChecklistSchema}
|
|
959
|
-
workspace={workspace}
|
|
960
|
-
edit={{ enableUpdateDraft: true }}
|
|
961
|
-
organise={{
|
|
962
|
-
breadcrumbType: "hidden",
|
|
963
|
-
splitView: true,
|
|
964
|
-
groupRead: "groupId",
|
|
965
|
-
tabAlign: "stepper",
|
|
966
|
-
// INFO: the labels Verdant hand-swaps into the tabId on phones, as
|
|
967
|
-
// display-only short names instead
|
|
968
|
-
tabShortLabels: {
|
|
969
|
-
[TAB_BASIC]: "Basic Info",
|
|
970
|
-
[TAB_MATERIAL]: "Material Check",
|
|
971
|
-
[TAB_PANEL]: "Panel System",
|
|
972
|
-
[TAB_ROOF]: "Roof Cond.",
|
|
973
|
-
[TAB_AGREEMENT]: "Agreement",
|
|
974
|
-
},
|
|
975
|
-
}}
|
|
976
|
-
bareSettings={{
|
|
977
|
-
bareNewDTW: true,
|
|
978
|
-
bareCompactMode: true,
|
|
979
|
-
bareListMobileView: isMobile,
|
|
980
|
-
bareListMobileFloatingSearch: true,
|
|
981
|
-
bareSaveOnTabChange: true,
|
|
982
|
-
bareUpdateActionComponent: () => undefined,
|
|
983
|
-
bareUpdateActionBottom: true,
|
|
984
|
-
}}
|
|
985
|
-
handleNavigate={handleNavigate}
|
|
986
|
-
limit={1}
|
|
987
|
-
/>
|
|
988
|
-
</div>
|
|
989
|
-
);
|
|
990
|
-
};
|
|
991
|
-
|
|
992
|
-
// ---------------------------------------------------------------------------
|
|
993
|
-
// Verdant · Sales Leads list
|
|
994
|
-
// app/dashboard/proposalList/page.tsx — the Sales Leads table backed by
|
|
995
|
-
// mongoMasterLeadSchema (models/verdantMongo/MongoMasterLead.ts), rendered with
|
|
996
|
-
// bareNewDTW, scroll pagination and advanced sort.
|
|
997
|
-
// ---------------------------------------------------------------------------
|
|
998
|
-
|
|
999
|
-
const LEAD_COLLECTION = "verdant-master-leads";
|
|
1000
|
-
|
|
1001
|
-
const SALES_STATUS = [
|
|
1002
|
-
"Interested",
|
|
1003
|
-
"Site Survey",
|
|
1004
|
-
"Low Hanging Fruit",
|
|
1005
|
-
"Closed Case",
|
|
1006
|
-
"Future Case",
|
|
1007
|
-
"Drop",
|
|
1008
|
-
"New Lead",
|
|
1009
|
-
"Call Back Later",
|
|
1010
|
-
"Site Survey Completed",
|
|
1011
|
-
];
|
|
1012
|
-
const SALES_STATUS_COLOUR = [
|
|
1013
|
-
"indigo",
|
|
1014
|
-
"blue",
|
|
1015
|
-
"green",
|
|
1016
|
-
"green",
|
|
1017
|
-
"yellow",
|
|
1018
|
-
"red",
|
|
1019
|
-
"dark",
|
|
1020
|
-
"blue",
|
|
1021
|
-
"blue",
|
|
1022
|
-
];
|
|
1023
|
-
|
|
1024
|
-
const SalesLeadSchema: any[] = [
|
|
1025
|
-
{ id: "name", type: "string", name: "Name" },
|
|
1026
|
-
{ id: "sales", type: "string", name: "Sales", optional: true },
|
|
1027
|
-
{
|
|
1028
|
-
id: "salesStatus",
|
|
1029
|
-
type: "list",
|
|
1030
|
-
name: "Sales Status",
|
|
1031
|
-
listSelection: SALES_STATUS,
|
|
1032
|
-
listSelectionColour: SALES_STATUS_COLOUR,
|
|
1033
|
-
optional: true,
|
|
1034
|
-
},
|
|
1035
|
-
{
|
|
1036
|
-
id: "salesAssignedDate",
|
|
1037
|
-
type: "date",
|
|
1038
|
-
name: "Assigned Date",
|
|
1039
|
-
optional: true,
|
|
1040
|
-
},
|
|
1041
|
-
{ id: "phone", type: "contact", name: "Phone", optional: true },
|
|
1042
|
-
{ id: "firstContact", type: "date", name: "First Contact", optional: true },
|
|
1043
|
-
{ id: "appointment", type: "boolean", name: "Appointment", optional: true },
|
|
1044
|
-
{
|
|
1045
|
-
id: "appointmentDate",
|
|
1046
|
-
type: "date",
|
|
1047
|
-
name: "Appointment Date",
|
|
1048
|
-
optional: true,
|
|
1049
|
-
},
|
|
1050
|
-
{
|
|
1051
|
-
id: "rioSentiment",
|
|
1052
|
-
type: "list",
|
|
1053
|
-
name: "RIO Sentiment",
|
|
1054
|
-
listSelection: ["positive", "neutral", "negative"],
|
|
1055
|
-
listSelectionColour: ["green", "yellow", "red"],
|
|
1056
|
-
optional: true,
|
|
1057
|
-
},
|
|
1058
|
-
{ id: "followUpDate", type: "date", name: "Follow Up Date", optional: true },
|
|
1059
|
-
{ id: "closedDate", type: "date", name: "Closed Date", optional: true },
|
|
1060
|
-
{ id: "email", type: "email", name: "Email", optional: true },
|
|
1061
|
-
{ id: "location", type: "string", name: "Location", optional: true },
|
|
1062
|
-
{ id: "roofType", type: "string", name: "Roof Type", optional: true },
|
|
1063
|
-
{
|
|
1064
|
-
id: "electricPhase",
|
|
1065
|
-
type: "string",
|
|
1066
|
-
name: "Electric Phase",
|
|
1067
|
-
optional: true,
|
|
1068
|
-
},
|
|
1069
|
-
{ id: "propertyType", type: "string", name: "Property Type", optional: true },
|
|
1070
|
-
{
|
|
1071
|
-
id: "monthlyTnbBill",
|
|
1072
|
-
type: "string",
|
|
1073
|
-
name: "Avg Monthly Bill",
|
|
1074
|
-
optional: true,
|
|
1075
|
-
},
|
|
1076
|
-
{ id: "isOwner", type: "boolean", name: "Is Owner", optional: true },
|
|
1077
|
-
{ id: "product", type: "string", name: "Product", optional: true },
|
|
1078
|
-
];
|
|
1079
|
-
|
|
1080
|
-
const LEAD_NAMES = [
|
|
1081
|
-
"Tan Wei Ling",
|
|
1082
|
-
"Muhammad Aiman",
|
|
1083
|
-
"Priya Nair",
|
|
1084
|
-
"Lim Boon Keat",
|
|
1085
|
-
"Siti Nurhaliza",
|
|
1086
|
-
"Rajesh Kumar",
|
|
1087
|
-
"Chong Mei Yee",
|
|
1088
|
-
"Ahmad Faizal",
|
|
1089
|
-
"Ng Kah Wai",
|
|
1090
|
-
"Farah Hanim",
|
|
1091
|
-
"Kumaravel S.",
|
|
1092
|
-
"Wong Siew Lan",
|
|
1093
|
-
];
|
|
1094
|
-
const LEAD_LOCATIONS = [
|
|
1095
|
-
"Setia Alam",
|
|
1096
|
-
"Puchong",
|
|
1097
|
-
"Shah Alam",
|
|
1098
|
-
"Cyberjaya",
|
|
1099
|
-
"Seremban",
|
|
1100
|
-
"Klang",
|
|
1101
|
-
"Petaling Jaya",
|
|
1102
|
-
"Kajang",
|
|
1103
|
-
"Subang Jaya",
|
|
1104
|
-
"Rawang",
|
|
1105
|
-
"Nilai",
|
|
1106
|
-
"Ampang",
|
|
1107
|
-
];
|
|
1108
|
-
|
|
1109
|
-
const salesLeadRecords = LEAD_NAMES.map((name, index) => ({
|
|
1110
|
-
_id: `lead-${String(index + 1).padStart(2, "0")}`,
|
|
1111
|
-
name,
|
|
1112
|
-
sales: index % 3 === 0 ? "Hafiz" : index % 3 === 1 ? "Jasmine" : "Darren",
|
|
1113
|
-
salesStatus: SALES_STATUS[index % SALES_STATUS.length],
|
|
1114
|
-
salesAssignedDate: new Date(Date.UTC(2026, 7, 1 + index)),
|
|
1115
|
-
phone: `+6012${String(3456700 + index * 37).slice(-7)}`,
|
|
1116
|
-
firstContact:
|
|
1117
|
-
index % 4 === 0 ? undefined : new Date(Date.UTC(2026, 7, 3 + index)),
|
|
1118
|
-
appointment: index % 2 === 0,
|
|
1119
|
-
appointmentDate:
|
|
1120
|
-
index % 2 === 0 ? new Date(Date.UTC(2026, 7, 10 + index)) : undefined,
|
|
1121
|
-
rioSentiment: ["positive", "neutral", "negative"][index % 3],
|
|
1122
|
-
followUpDate:
|
|
1123
|
-
index % 3 === 1 ? new Date(Date.UTC(2026, 8, 1 + index)) : undefined,
|
|
1124
|
-
closedDate:
|
|
1125
|
-
SALES_STATUS[index % SALES_STATUS.length] === "Closed Case"
|
|
1126
|
-
? new Date(Date.UTC(2026, 7, 20 + index))
|
|
1127
|
-
: undefined,
|
|
1128
|
-
email: `${name.toLowerCase().replace(/[^a-z]+/g, ".")}@example.com`,
|
|
1129
|
-
location: LEAD_LOCATIONS[index],
|
|
1130
|
-
roofType: index % 2 === 0 ? "Concrete Tiles" : "Metal Deck",
|
|
1131
|
-
electricPhase: index % 3 === 0 ? "Single Phase" : "Three Phase",
|
|
1132
|
-
propertyType: index % 4 === 3 ? "Bungalow" : "Terrace",
|
|
1133
|
-
monthlyTnbBill: `RM ${350 + index * 45}`,
|
|
1134
|
-
isOwner: index % 5 !== 4,
|
|
1135
|
-
product: index % 2 === 0 ? "Residential Solar" : "Solar + BESS",
|
|
1136
|
-
}));
|
|
1137
|
-
|
|
1138
|
-
export const VerdantSalesLeads = () => {
|
|
1139
|
-
const [mount, setMount] = useState<boolean>(false);
|
|
1140
|
-
const isMobile = useIsMobile();
|
|
1141
|
-
const [, setTick] = useState<number>(0);
|
|
1142
|
-
const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
|
|
1143
|
-
const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
|
|
1144
|
-
const [viewId, setViewId] = useState<string | undefined>(undefined);
|
|
1145
|
-
const storeRef = useRef<any[]>(JSON.parse(JSON.stringify(salesLeadRecords)));
|
|
1146
|
-
const countersRef = useRef({ update: 0 });
|
|
1147
|
-
const mockRef = useRef<any>(null);
|
|
1148
|
-
|
|
1149
|
-
useEffect(() => {
|
|
1150
|
-
mockRef.current = handleInstallMock(
|
|
1151
|
-
LEAD_COLLECTION,
|
|
1152
|
-
storeRef.current,
|
|
1153
|
-
countersRef.current,
|
|
1154
|
-
() => setTick((previous) => previous + 1),
|
|
1155
|
-
);
|
|
1156
|
-
setMount(true);
|
|
1157
|
-
return () => {
|
|
1158
|
-
mockRef.current?.restore();
|
|
1159
|
-
mockRef.current = null;
|
|
1160
|
-
};
|
|
1161
|
-
}, []);
|
|
1162
|
-
|
|
1163
|
-
const handleNavigate = (type: any, id: any, newViewId?: string) => {
|
|
1164
|
-
setViewType(type);
|
|
1165
|
-
setSelectedId(id);
|
|
1166
|
-
if (newViewId) setViewId(newViewId);
|
|
1167
|
-
};
|
|
1168
|
-
|
|
1169
|
-
if (!mount) return <Loading />;
|
|
1170
|
-
|
|
1171
|
-
return (
|
|
1172
|
-
<div className="w-full">
|
|
1173
|
-
<div className="font-semibold mb-1 text-gray-900">
|
|
1174
|
-
Verdant · Sales Leads
|
|
1175
|
-
</div>
|
|
1176
|
-
<div className="text-sm font-light mb-4 text-gray-500">
|
|
1177
|
-
The Sales Leads table from the proposalList page: mongoMasterLeadSchema
|
|
1178
|
-
with coloured Sales Status and RIO Sentiment tags, contact / email /
|
|
1179
|
-
date / boolean columns, bareNewDTW, scroll pagination, advanced sort,
|
|
1180
|
-
rows that open in a new tab.
|
|
1181
|
-
</div>
|
|
1182
|
-
<DataTemplateWrapper
|
|
1183
|
-
title="Sales Leads"
|
|
1184
|
-
description="Your assigned leads from Sales Lead Board"
|
|
1185
|
-
type={viewType}
|
|
1186
|
-
id={selectedId ?? ""}
|
|
1187
|
-
collectionId={LEAD_COLLECTION}
|
|
1188
|
-
schema={SalesLeadSchema}
|
|
1189
|
-
workspace={workspace}
|
|
1190
|
-
edit={{ allEditable: false }}
|
|
1191
|
-
newTabUrl={{ default: "/dashboard/proposalList?leadId=", addons: "" }}
|
|
1192
|
-
organise={{
|
|
1193
|
-
paginationType: "scroll",
|
|
1194
|
-
disableSelectFullData: { table: true },
|
|
1195
|
-
viewId,
|
|
1196
|
-
// INFO: same view tabs as the Dark Mode fixture — the default tab is
|
|
1197
|
-
// the table
|
|
1198
|
-
extendedTabs: [
|
|
1199
|
-
{ name: "List", type: "list", viewType: "list" },
|
|
1200
|
-
{ name: "Grid", type: "list", viewType: "grid" },
|
|
1201
|
-
{
|
|
1202
|
-
name: "Feeds",
|
|
1203
|
-
type: "list",
|
|
1204
|
-
viewType: "feeds",
|
|
1205
|
-
feedsSettings: {
|
|
1206
|
-
title: "name",
|
|
1207
|
-
content: "salesStatus",
|
|
1208
|
-
desc: "salesAssignedDate",
|
|
1209
|
-
},
|
|
1210
|
-
},
|
|
1211
|
-
{
|
|
1212
|
-
name: "Kanban",
|
|
1213
|
-
type: "list",
|
|
1214
|
-
viewType: "kanban",
|
|
1215
|
-
kanbanSettings: { title: "salesStatus" },
|
|
1216
|
-
},
|
|
1217
|
-
],
|
|
1218
|
-
}}
|
|
1219
|
-
bareSettings={{
|
|
1220
|
-
bareNewDTW: true,
|
|
1221
|
-
bareReadListTabs: true,
|
|
1222
|
-
bareCompactMode: true,
|
|
1223
|
-
bareListMobileView: isMobile,
|
|
1224
|
-
bareListMobileFloatingSearch: true,
|
|
1225
|
-
bareFlexFilters: true,
|
|
1226
|
-
}}
|
|
1227
|
-
handleNavigate={handleNavigate}
|
|
1228
|
-
limit={15}
|
|
1229
|
-
/>
|
|
1230
|
-
</div>
|
|
1231
|
-
);
|
|
1232
|
-
};
|
|
1233
|
-
|
|
1234
|
-
// ---------------------------------------------------------------------------
|
|
1235
|
-
// Verdant · Simulator proposal form
|
|
1236
|
-
// components/base/CustomSolarProposals.tsx renderProposalModal — the proposal
|
|
1237
|
-
// editor inside the simulator: four-column update form on the full simulator
|
|
1238
|
-
// schema (system specs, financial results, price & maintenance, discounts),
|
|
1239
|
-
// list inputs locked, the caller's own (empty) action component at the bottom.
|
|
1240
|
-
// ---------------------------------------------------------------------------
|
|
1241
|
-
|
|
1242
|
-
const SIMULATOR_COLLECTION = "verdant-simulator-form";
|
|
1243
|
-
|
|
1244
|
-
const simulatorRecord = {
|
|
1245
|
-
_id: "sim-01",
|
|
1246
|
-
panelBrand: "Jinko 580W",
|
|
1247
|
-
inverterBrand: "Huawei",
|
|
1248
|
-
calcSolarPVInverterModel: "SUN2000-8KTL-L1",
|
|
1249
|
-
calcSolarSystemSize: "8.7 kWp",
|
|
1250
|
-
consumedRatio: 0.6,
|
|
1251
|
-
exportedRatio: 0.4,
|
|
1252
|
-
storedRatio: 0,
|
|
1253
|
-
batteryUnit: 0,
|
|
1254
|
-
calcNoPanels: 15,
|
|
1255
|
-
estimatedDrop: 12,
|
|
1256
|
-
calcAfterSolarBill: 98,
|
|
1257
|
-
calcMonthSaving: 412,
|
|
1258
|
-
calcEstSystemCost: 31800,
|
|
1259
|
-
roi: 15.5,
|
|
1260
|
-
paybackPeriod: 6.4,
|
|
1261
|
-
calcEasyPayInstallment: 620,
|
|
1262
|
-
calcNetInvestmentInsTerm: 60,
|
|
1263
|
-
addOnMaintenance: "2 Years",
|
|
1264
|
-
waiveMaintCost: false,
|
|
1265
|
-
addOnCostMaint: 800,
|
|
1266
|
-
addOnWorkmanship: "5 Years",
|
|
1267
|
-
waiveWorkmanshipCost: true,
|
|
1268
|
-
addOnCostWorkmanship: 0,
|
|
1269
|
-
optimizerUnit: 0,
|
|
1270
|
-
optimizerCost: 0,
|
|
1271
|
-
systemPriceBeforeInsurance: 31000,
|
|
1272
|
-
insuranceCoverYear: "1 Year",
|
|
1273
|
-
worth: 33200,
|
|
1274
|
-
totalSystemCost: 31800,
|
|
1275
|
-
discountRate: 3,
|
|
1276
|
-
verdantRebateAmount: 500,
|
|
1277
|
-
tngRebateApplied: 0,
|
|
1278
|
-
isReferral: true,
|
|
1279
|
-
referralDiscount: 300,
|
|
1280
|
-
promoItems: [],
|
|
1281
|
-
calcInstallmentTerm: "60",
|
|
1282
|
-
paymentTerm: "Full Payment",
|
|
1283
|
-
finalCost: 30046,
|
|
1284
|
-
remarks: "",
|
|
1285
|
-
createdAt: new Date("2026-08-20T02:00:00.000Z"),
|
|
1286
|
-
updatedAt: new Date("2026-08-21T02:00:00.000Z"),
|
|
1287
|
-
};
|
|
1288
|
-
|
|
1289
|
-
export const VerdantSimulatorForm = () => {
|
|
1290
|
-
const [mount, setMount] = useState<boolean>(false);
|
|
1291
|
-
const [, setTick] = useState<number>(0);
|
|
1292
|
-
const storeRef = useRef<any[]>([JSON.parse(JSON.stringify(simulatorRecord))]);
|
|
1293
|
-
const countersRef = useRef({ update: 0 });
|
|
1294
|
-
const mockRef = useRef<any>(null);
|
|
1295
|
-
|
|
1296
|
-
useEffect(() => {
|
|
1297
|
-
mockRef.current = handleInstallMock(
|
|
1298
|
-
SIMULATOR_COLLECTION,
|
|
1299
|
-
storeRef.current,
|
|
1300
|
-
countersRef.current,
|
|
1301
|
-
() => setTick((previous) => previous + 1),
|
|
1302
|
-
);
|
|
1303
|
-
setMount(true);
|
|
1304
|
-
return () => {
|
|
1305
|
-
mockRef.current?.restore();
|
|
1306
|
-
mockRef.current = null;
|
|
1307
|
-
};
|
|
1308
|
-
}, []);
|
|
1309
|
-
|
|
1310
|
-
if (!mount) return <Loading />;
|
|
1311
|
-
|
|
1312
|
-
return (
|
|
1313
|
-
<div className="w-full">
|
|
1314
|
-
<div className="font-semibold mb-1 text-gray-900">
|
|
1315
|
-
Verdant · Simulator Proposal Form
|
|
1316
|
-
</div>
|
|
1317
|
-
<div className="text-sm font-light mb-2 text-gray-500">
|
|
1318
|
-
The proposal editor inside the solar simulator: bareUpdateColumnType
|
|
1319
|
-
"four", bareUpdateMobileView, list inputs locked, the
|
|
1320
|
-
caller's own empty action component at the bottom, four sections
|
|
1321
|
-
(A–D) and Additional Information.
|
|
1322
|
-
</div>
|
|
1323
|
-
<div className="mb-4 text-xs text-gray-600 font-mono">
|
|
1324
|
-
update calls:{" "}
|
|
1325
|
-
<span data-testid="simulator-update-count">
|
|
1326
|
-
{countersRef.current.update}
|
|
1327
|
-
</span>
|
|
1328
|
-
</div>
|
|
1329
|
-
<div className="rounded-lg bg-white p-4">
|
|
1330
|
-
<DataTemplateWrapper
|
|
1331
|
-
title=""
|
|
1332
|
-
description=""
|
|
1333
|
-
type="update"
|
|
1334
|
-
id="sim-01"
|
|
1335
|
-
collectionId={SIMULATOR_COLLECTION}
|
|
1336
|
-
schema={VerdantSimulatorSchema}
|
|
1337
|
-
workspace={workspace}
|
|
1338
|
-
edit={{
|
|
1339
|
-
allEditable: true,
|
|
1340
|
-
listEditable: false,
|
|
1341
|
-
disabledUpdate: false,
|
|
1342
|
-
disableBatchUpdate: true,
|
|
1343
|
-
createBtn: true,
|
|
1344
|
-
}}
|
|
1345
|
-
organise={{ breadcrumbType: "hidden", disabledOnEnter: true }}
|
|
1346
|
-
bareSettings={{
|
|
1347
|
-
bareNewDTW: true,
|
|
1348
|
-
bareCompactMode: true,
|
|
1349
|
-
bareUpdateMobileView: true,
|
|
1350
|
-
bareUpdateColumnType: "four",
|
|
1351
|
-
bareUpdateActionComponent: () => <></>,
|
|
1352
|
-
bareUpdateActionBottom: true,
|
|
1353
|
-
}}
|
|
1354
|
-
handleComplete={() => {}}
|
|
1355
|
-
/>
|
|
1356
|
-
</div>
|
|
1357
|
-
</div>
|
|
1358
|
-
);
|
|
1359
|
-
};
|
|
1360
|
-
|
|
1361
|
-
// ---------------------------------------------------------------------------
|
|
1362
|
-
// Verdant · Installer post-installation checklist
|
|
1363
|
-
// verdant-pv-delivery-postchecklist — PostCheckListTestSchema: three tabs on a
|
|
1364
|
-
// left rail, two listFlexTable sheets (string inverter settings, additional
|
|
1365
|
-
// skylift days with image + time cells), a signature, compact mode.
|
|
1366
|
-
// ---------------------------------------------------------------------------
|
|
1367
|
-
|
|
1368
|
-
const POSTCHECK_COLLECTION = "verdant-pv-delivery-postchecklist";
|
|
1369
|
-
|
|
1370
|
-
const postChecklistRecord = {
|
|
1371
|
-
_id: "postcheck-01",
|
|
1372
|
-
clientName: "Tan Wei Ling",
|
|
1373
|
-
date: new Date("2026-09-02T01:00:00.000Z"),
|
|
1374
|
-
address: "12, Jalan Setia Impian 3, Setia Alam, 40170 Shah Alam",
|
|
1375
|
-
systemSize: 8.7,
|
|
1376
|
-
installerName: "Ahmad Faizal",
|
|
1377
|
-
pvDeliveryId: "PVD-2026-0418",
|
|
1378
|
-
systemType: "String Inverter",
|
|
1379
|
-
stringInverterSettings: [
|
|
1380
|
-
{
|
|
1381
|
-
category: "String Number",
|
|
1382
|
-
title: "String 1",
|
|
1383
|
-
voltageCheck: 412.5,
|
|
1384
|
-
cableLength: 18,
|
|
1385
|
-
tiltAngle: 15,
|
|
1386
|
-
orientation: "South",
|
|
1387
|
-
},
|
|
1388
|
-
{
|
|
1389
|
-
category: "String Number",
|
|
1390
|
-
title: "String 2",
|
|
1391
|
-
voltageCheck: 408.1,
|
|
1392
|
-
cableLength: 22,
|
|
1393
|
-
tiltAngle: 15,
|
|
1394
|
-
orientation: "South-West",
|
|
1395
|
-
},
|
|
1396
|
-
],
|
|
1397
|
-
panelInstallation: true,
|
|
1398
|
-
skyliftDepartureTime: new Date("2026-09-02T09:30:00.000Z"),
|
|
1399
|
-
additionalSkylift: [
|
|
1400
|
-
{
|
|
1401
|
-
category: "Days 2",
|
|
1402
|
-
title: "Day 2",
|
|
1403
|
-
arrivalTime: new Date("2026-09-03T00:30:00.000Z"),
|
|
1404
|
-
departureTime: new Date("2026-09-03T08:00:00.000Z"),
|
|
1405
|
-
},
|
|
1406
|
-
],
|
|
1407
|
-
finalPhotoDocumentation: false,
|
|
1408
|
-
};
|
|
1409
|
-
|
|
1410
|
-
export const VerdantInstallerPostChecklist = () => {
|
|
1411
|
-
const [mount, setMount] = useState<boolean>(false);
|
|
1412
|
-
const isMobile = useIsMobile();
|
|
1413
|
-
const [, setTick] = useState<number>(0);
|
|
1414
|
-
const [viewType, setViewType] = useState<"list" | "read" | "update">(
|
|
1415
|
-
"update",
|
|
1416
|
-
);
|
|
1417
|
-
const [selectedId, setSelectedId] = useState<string | undefined>(
|
|
1418
|
-
"postcheck-01",
|
|
1419
|
-
);
|
|
1420
|
-
const storeRef = useRef<any[]>([
|
|
1421
|
-
JSON.parse(JSON.stringify(postChecklistRecord)),
|
|
1422
|
-
]);
|
|
1423
|
-
const countersRef = useRef({ update: 0 });
|
|
1424
|
-
const mockRef = useRef<any>(null);
|
|
1425
|
-
|
|
1426
|
-
useEffect(() => {
|
|
1427
|
-
mockRef.current = handleInstallMock(
|
|
1428
|
-
POSTCHECK_COLLECTION,
|
|
1429
|
-
storeRef.current,
|
|
1430
|
-
countersRef.current,
|
|
1431
|
-
() => setTick((previous) => previous + 1),
|
|
1432
|
-
);
|
|
1433
|
-
setMount(true);
|
|
1434
|
-
return () => {
|
|
1435
|
-
mockRef.current?.restore();
|
|
1436
|
-
mockRef.current = null;
|
|
1437
|
-
};
|
|
1438
|
-
}, []);
|
|
1439
|
-
|
|
1440
|
-
const handleNavigate = (type: any, id: any) => {
|
|
1441
|
-
setViewType(type);
|
|
1442
|
-
setSelectedId(id);
|
|
1443
|
-
};
|
|
1444
|
-
|
|
1445
|
-
if (!mount) return <Loading />;
|
|
1446
|
-
|
|
1447
|
-
return (
|
|
1448
|
-
<div className="w-full">
|
|
1449
|
-
<div className="font-semibold mb-1 text-gray-900">
|
|
1450
|
-
Verdant · Installer Post-Installation Checklist
|
|
1451
|
-
</div>
|
|
1452
|
-
<div className="text-sm font-light mb-2 text-gray-500">
|
|
1453
|
-
Three tabs on a left rail, the string inverter settings sheet and the
|
|
1454
|
-
additional skylift sheet (image upload and time cells inside a
|
|
1455
|
-
FlexTable), a signature, compact mode.
|
|
1456
|
-
</div>
|
|
1457
|
-
<div className="mb-4 text-xs text-gray-600 font-mono">
|
|
1458
|
-
update calls:{" "}
|
|
1459
|
-
<span data-testid="postcheck-update-count">
|
|
1460
|
-
{countersRef.current.update}
|
|
1461
|
-
</span>
|
|
1462
|
-
</div>
|
|
1463
|
-
<DataTemplateWrapper
|
|
1464
|
-
title="Post Checklist"
|
|
1465
|
-
description="verdant-pv-delivery-postchecklist"
|
|
1466
|
-
type={viewType}
|
|
1467
|
-
id={selectedId}
|
|
1468
|
-
collectionId={POSTCHECK_COLLECTION}
|
|
1469
|
-
schema={PostCheckListTestSchema}
|
|
1470
|
-
workspace={workspace}
|
|
1471
|
-
edit={{
|
|
1472
|
-
allEditable: true,
|
|
1473
|
-
disabledUpdate: false,
|
|
1474
|
-
disableBatchUpdate: false,
|
|
1475
|
-
deleteBtn: true,
|
|
1476
|
-
enableUpdateDraft: true,
|
|
1477
|
-
}}
|
|
1478
|
-
organise={{
|
|
1479
|
-
breadcrumbType: "button",
|
|
1480
|
-
tabAlign: "left",
|
|
1481
|
-
showExport: true,
|
|
1482
|
-
}}
|
|
1483
|
-
bareSettings={{
|
|
1484
|
-
bareNewDTW: true,
|
|
1485
|
-
bareCompactMode: true,
|
|
1486
|
-
bareListMobileView: isMobile,
|
|
1487
|
-
bareListMobileFloatingSearch: true,
|
|
1488
|
-
}}
|
|
1489
|
-
handleNavigate={handleNavigate}
|
|
1490
|
-
limit={10}
|
|
1491
|
-
/>
|
|
1492
|
-
</div>
|
|
1493
|
-
);
|
|
1494
|
-
};
|
|
1495
|
-
|
|
1496
|
-
// ---------------------------------------------------------------------------
|
|
1497
|
-
// Meraque · Work order (offline)
|
|
1498
|
-
// app/dashboard/orders/page.tsx — the drone-spraying work order on
|
|
1499
|
-
// MeraqueWorkOrderSchema with bareOffline. The DTW prefetches the latest work
|
|
1500
|
-
// orders and the five reference collections into IndexedDB on mount; the app
|
|
1501
|
-
// owns the policy — here an Offline toggle, the banner and a Sync button, the
|
|
1502
|
-
// way Meraque wires it. Everything is mocked in the page, including the
|
|
1503
|
-
// reference collections, so the whole cycle runs with no server.
|
|
1504
|
-
// ---------------------------------------------------------------------------
|
|
1505
|
-
|
|
1506
|
-
const WORK_ORDER_COLLECTION = "meraque-work-orders";
|
|
1507
|
-
const MERAQUE_REFERENCES: Array<{ collectionId: string; limit?: number }> = [
|
|
1508
|
-
{ collectionId: "meraque-cycles" },
|
|
1509
|
-
{ collectionId: "meraque-teams" },
|
|
1510
|
-
{ collectionId: "meraque-users" },
|
|
1511
|
-
{ collectionId: "meraque-robots" },
|
|
1512
|
-
{ collectionId: "meraque-vehicles" },
|
|
1513
|
-
];
|
|
1514
|
-
|
|
1515
|
-
const meraqueReferenceStores: Record<string, any[]> = {
|
|
1516
|
-
"meraque-cycles": [
|
|
1517
|
-
{ _id: "cycle-01", title: "Cycle 12 · Ladang Sri Tanjung" },
|
|
1518
|
-
{ _id: "cycle-02", title: "Cycle 13 · Ladang Bukit Kemuning" },
|
|
1519
|
-
],
|
|
1520
|
-
"meraque-teams": [
|
|
1521
|
-
{ _id: "team-01", name: "Team Alpha" },
|
|
1522
|
-
{ _id: "team-02", name: "Team Bravo" },
|
|
1523
|
-
],
|
|
1524
|
-
"meraque-users": [
|
|
1525
|
-
{ _id: "user-01", name: "Ahmad Faizal" },
|
|
1526
|
-
{ _id: "user-02", name: "Kumaravel S." },
|
|
1527
|
-
{ _id: "user-03", name: "Chong Mei Yee" },
|
|
1528
|
-
{ _id: "user-04", name: "Siti Nurhaliza" },
|
|
1529
|
-
],
|
|
1530
|
-
"meraque-robots": [
|
|
1531
|
-
{ _id: "robot-01", chasisNo: "MQ-DR-2041" },
|
|
1532
|
-
{ _id: "robot-02", chasisNo: "MQ-DR-2058" },
|
|
1533
|
-
],
|
|
1534
|
-
"meraque-vehicles": [
|
|
1535
|
-
{ _id: "vehicle-01", plateNo: "WXY 4821" },
|
|
1536
|
-
{ _id: "vehicle-02", plateNo: "BPK 913" },
|
|
1537
|
-
],
|
|
1538
|
-
};
|
|
1539
|
-
|
|
1540
|
-
const workOrderRecords = Array.from({ length: 6 }, (_, index) => ({
|
|
1541
|
-
_id: `wo-${String(index + 1).padStart(2, "0")}`,
|
|
1542
|
-
title: `Spray run ${index + 1} · Block ${String.fromCharCode(65 + index)}`,
|
|
1543
|
-
verified: index % 2 === 0,
|
|
1544
|
-
cycleId: index % 2 === 0 ? "cycle-01" : "cycle-02",
|
|
1545
|
-
status: index < 4 ? "Completed" : "Not Completed",
|
|
1546
|
-
startDate: new Date(Date.UTC(2026, 7, 20 + index, 0, 30)),
|
|
1547
|
-
endDate:
|
|
1548
|
-
index < 4 ? new Date(Date.UTC(2026, 7, 20 + index, 9, 0)) : undefined,
|
|
1549
|
-
operationDay: index + 1,
|
|
1550
|
-
teamId: index % 2 === 0 ? "team-01" : "team-02",
|
|
1551
|
-
pilotId: ["user-01", "user-02"][index % 2],
|
|
1552
|
-
coPilotId: [["user-03"], ["user-04"], ["user-03", "user-04"]][index % 3],
|
|
1553
|
-
robotId: index % 2 === 0 ? "robot-01" : "robot-02",
|
|
1554
|
-
vehicleId: index % 2 === 0 ? "vehicle-01" : "vehicle-02",
|
|
1555
|
-
pesticideName: ["Glyphosate"],
|
|
1556
|
-
pesticideBefore: 120,
|
|
1557
|
-
pesticideAfter: 120 - 18 * (index + 1),
|
|
1558
|
-
pesticideUsed: 18 * (index + 1),
|
|
1559
|
-
areaSprayed: 12.5 + index * 3,
|
|
1560
|
-
totalFlight: 6 + index,
|
|
1561
|
-
totalHours: 4.5 + index * 0.5,
|
|
1562
|
-
submittedBy: "user-01",
|
|
1563
|
-
createdAt: new Date(Date.UTC(2026, 7, 20 + index, 10, 0)),
|
|
1564
|
-
}));
|
|
1565
|
-
|
|
1566
|
-
// INFO: the mock answers the work-order collection and every reference
|
|
1567
|
-
// collection the DTW prefetches or looks labels up in
|
|
1568
|
-
const handleInstallMeraqueMock = (
|
|
1569
|
-
store: any[],
|
|
1570
|
-
counters: { update: number; create: number },
|
|
1571
|
-
onCount: () => void,
|
|
1572
|
-
) => {
|
|
1573
|
-
const mock = handleInstallMock(
|
|
1574
|
-
WORK_ORDER_COLLECTION,
|
|
1575
|
-
store,
|
|
1576
|
-
counters,
|
|
1577
|
-
onCount,
|
|
1578
|
-
);
|
|
1579
|
-
mock
|
|
1580
|
-
.onPost(`${VERDANT_ENDPOINT}/${WORK_ORDER_COLLECTION}/create`)
|
|
1581
|
-
.reply((config) => {
|
|
1582
|
-
counters.create += 1;
|
|
1583
|
-
onCount();
|
|
1584
|
-
let body: any = {};
|
|
1585
|
-
try {
|
|
1586
|
-
body = JSON.parse(config?.data ?? "{}");
|
|
1587
|
-
} catch (err) {
|
|
1588
|
-
body = {};
|
|
1589
|
-
}
|
|
1590
|
-
const created = {
|
|
1591
|
-
...(body.data ?? {}),
|
|
1592
|
-
_id: body.id ?? body.data?._id ?? `wo-${store.length + 1}`,
|
|
1593
|
-
};
|
|
1594
|
-
store.push(created);
|
|
1595
|
-
return [200, { success: true, _id: created._id }];
|
|
1596
|
-
});
|
|
1597
|
-
Object.entries(meraqueReferenceStores).forEach(([collection, rows]) => {
|
|
1598
|
-
mock
|
|
1599
|
-
.onPost(`${VERDANT_ENDPOINT}/${collection}/list`)
|
|
1600
|
-
.reply(() => [200, { data: handleClone(rows), count: rows.length }]);
|
|
1601
|
-
mock
|
|
1602
|
-
.onPost(`${VERDANT_ENDPOINT}/${collection}/count`)
|
|
1603
|
-
.reply(() => [200, { data: rows.length, count: rows.length }]);
|
|
1604
|
-
// INFO: the read view resolves each reference through /get; without this
|
|
1605
|
-
// route the request fails and the label falls back to a placeholder
|
|
1606
|
-
mock.onPost(`${VERDANT_ENDPOINT}/${collection}/get`).reply((config) => {
|
|
1607
|
-
let body: any = {};
|
|
1608
|
-
try {
|
|
1609
|
-
body = JSON.parse(config?.data ?? "{}");
|
|
1610
|
-
} catch (err) {
|
|
1611
|
-
body = {};
|
|
1612
|
-
}
|
|
1613
|
-
return [
|
|
1614
|
-
200,
|
|
1615
|
-
{
|
|
1616
|
-
data: handleClone(
|
|
1617
|
-
rows.find((eachRow: any) => eachRow._id === body.id) ?? null,
|
|
1618
|
-
),
|
|
1619
|
-
},
|
|
1620
|
-
];
|
|
1621
|
-
});
|
|
1622
|
-
});
|
|
1623
|
-
return mock;
|
|
1624
|
-
};
|
|
1625
|
-
|
|
1626
|
-
export const MeraqueWorkOrderOffline = () => {
|
|
1627
|
-
const [mount, setMount] = useState<boolean>(false);
|
|
1628
|
-
const [, setTick] = useState<number>(0);
|
|
1629
|
-
const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
|
|
1630
|
-
const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
|
|
1631
|
-
// INFO: the app's policy state — Meraque keeps the same flag and banner
|
|
1632
|
-
const [offline, setOffline] = useState<boolean>(false);
|
|
1633
|
-
const [syncing, setSyncing] = useState<boolean>(false);
|
|
1634
|
-
const [cache, setCache] = useState<{
|
|
1635
|
-
main: number;
|
|
1636
|
-
pending: number;
|
|
1637
|
-
refs: Record<string, number>;
|
|
1638
|
-
}>({
|
|
1639
|
-
main: 0,
|
|
1640
|
-
pending: 0,
|
|
1641
|
-
refs: {},
|
|
1642
|
-
});
|
|
1643
|
-
const isMobile = useIsMobile();
|
|
1644
|
-
const dataRef = useRef<any>(null);
|
|
1645
|
-
const storeRef = useRef<any[]>(JSON.parse(JSON.stringify(workOrderRecords)));
|
|
1646
|
-
const countersRef = useRef({ update: 0, create: 0 });
|
|
1647
|
-
const mockRef = useRef<any>(null);
|
|
1648
|
-
|
|
1649
|
-
useEffect(() => {
|
|
1650
|
-
mockRef.current = handleInstallMeraqueMock(
|
|
1651
|
-
storeRef.current,
|
|
1652
|
-
countersRef.current,
|
|
1653
|
-
() => setTick((previous) => previous + 1),
|
|
1654
|
-
);
|
|
1655
|
-
setMount(true);
|
|
1656
|
-
return () => {
|
|
1657
|
-
mockRef.current?.restore();
|
|
1658
|
-
mockRef.current = null;
|
|
1659
|
-
};
|
|
1660
|
-
}, []);
|
|
1661
|
-
|
|
1662
|
-
// INFO: poll IndexedDB so the panel shows what the DTW prefetched and what
|
|
1663
|
-
// is still waiting to be synced
|
|
1664
|
-
useEffect(() => {
|
|
1665
|
-
if (!mount) return;
|
|
1666
|
-
const handleCheckCache = async () => {
|
|
1667
|
-
const { indexedDb } = await import("../dataTemplate/indexedDb");
|
|
1668
|
-
const mainResp = await indexedDb.getValue("local", WORK_ORDER_COLLECTION);
|
|
1669
|
-
const rows: any[] = Array.isArray(mainResp?.data) ? mainResp.data : [];
|
|
1670
|
-
const refs: Record<string, number> = {};
|
|
1671
|
-
for (const ref of MERAQUE_REFERENCES) {
|
|
1672
|
-
const refResp = await indexedDb.getValue(
|
|
1673
|
-
"local",
|
|
1674
|
-
`${ref.collectionId}-ref`,
|
|
1675
|
-
);
|
|
1676
|
-
refs[ref.collectionId] = Array.isArray(refResp?.data)
|
|
1677
|
-
? refResp.data.length
|
|
1678
|
-
: 0;
|
|
1679
|
-
}
|
|
1680
|
-
setCache({
|
|
1681
|
-
main: rows.length,
|
|
1682
|
-
pending: rows.filter(
|
|
1683
|
-
(row) => !(row.baseOnline === true && row.syncStatus === "synced"),
|
|
1684
|
-
).length,
|
|
1685
|
-
refs,
|
|
1686
|
-
});
|
|
1687
|
-
};
|
|
1688
|
-
handleCheckCache();
|
|
1689
|
-
const timer = setInterval(handleCheckCache, 1000);
|
|
1690
|
-
return () => clearInterval(timer);
|
|
1691
|
-
}, [mount]);
|
|
1692
|
-
|
|
1693
|
-
const handleSync = async () => {
|
|
1694
|
-
setSyncing(true);
|
|
1695
|
-
await dataRef.current?.handleUploadOfflineData?.();
|
|
1696
|
-
setSyncing(false);
|
|
1697
|
-
};
|
|
1698
|
-
|
|
1699
|
-
const handleNavigate = (type: any, id: any) => {
|
|
1700
|
-
setViewType(type);
|
|
1701
|
-
setSelectedId(id);
|
|
1702
|
-
};
|
|
1703
|
-
|
|
1704
|
-
if (!mount) return <Loading />;
|
|
1705
|
-
|
|
1706
|
-
const renderBanner = () => (
|
|
1707
|
-
<div
|
|
1708
|
-
data-testid="meraque-banner"
|
|
1709
|
-
className={`mb-3 flex flex-col gap-2 rounded-lg border px-3 py-2 text-sm sm:flex-row sm:items-center sm:justify-between ${
|
|
1710
|
-
offline
|
|
1711
|
-
? "border-amber-300 bg-amber-50 text-amber-800"
|
|
1712
|
-
: "border-gray-200 bg-white text-gray-700"
|
|
1713
|
-
}`}
|
|
1714
|
-
>
|
|
1715
|
-
<span>
|
|
1716
|
-
{offline
|
|
1717
|
-
? "You're in offline mode. Saves stay on this device."
|
|
1718
|
-
: cache.pending > 0
|
|
1719
|
-
? `${cache.pending} work order(s) waiting to sync.`
|
|
1720
|
-
: "Data synced. Ready for offline."}
|
|
1721
|
-
</span>
|
|
1722
|
-
<div className="flex flex-row gap-2">
|
|
1723
|
-
<Button
|
|
1724
|
-
id="meraque-toggle-offline"
|
|
1725
|
-
text={offline ? "Go online" : "Go offline"}
|
|
1726
|
-
type="light"
|
|
1727
|
-
size="small"
|
|
1728
|
-
onClick={() => setOffline(!offline)}
|
|
1729
|
-
/>
|
|
1730
|
-
<Button
|
|
1731
|
-
id="meraque-sync"
|
|
1732
|
-
text={syncing ? "Syncing…" : "Sync now"}
|
|
1733
|
-
type="normal"
|
|
1734
|
-
size="small"
|
|
1735
|
-
disable={offline || syncing || cache.pending === 0}
|
|
1736
|
-
onClick={handleSync}
|
|
1737
|
-
/>
|
|
1738
|
-
</div>
|
|
1739
|
-
</div>
|
|
1740
|
-
);
|
|
1741
|
-
|
|
1742
|
-
const renderCache = () => (
|
|
1743
|
-
<div className="mb-4 rounded-lg border border-gray-200 bg-white p-3 text-xs text-gray-600">
|
|
1744
|
-
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
|
1745
|
-
<span>
|
|
1746
|
-
IndexedDB {WORK_ORDER_COLLECTION}:{" "}
|
|
1747
|
-
<span
|
|
1748
|
-
data-testid="meraque-cache-main"
|
|
1749
|
-
className="font-medium text-gray-900"
|
|
1750
|
-
>
|
|
1751
|
-
{cache.main}
|
|
1752
|
-
</span>{" "}
|
|
1753
|
-
docs,{" "}
|
|
1754
|
-
<span
|
|
1755
|
-
data-testid="meraque-cache-pending"
|
|
1756
|
-
className="font-medium text-gray-900"
|
|
1757
|
-
>
|
|
1758
|
-
{cache.pending}
|
|
1759
|
-
</span>{" "}
|
|
1760
|
-
pending
|
|
1761
|
-
</span>
|
|
1762
|
-
{MERAQUE_REFERENCES.map((ref) => (
|
|
1763
|
-
<span key={ref.collectionId}>
|
|
1764
|
-
{ref.collectionId}:{" "}
|
|
1765
|
-
<span className="font-medium text-gray-900">
|
|
1766
|
-
{cache.refs[ref.collectionId] ?? 0}
|
|
1767
|
-
</span>
|
|
1768
|
-
</span>
|
|
1769
|
-
))}
|
|
1770
|
-
<span className="font-mono">
|
|
1771
|
-
server updates:{" "}
|
|
1772
|
-
<span data-testid="meraque-update-count">
|
|
1773
|
-
{countersRef.current.update}
|
|
1774
|
-
</span>
|
|
1775
|
-
</span>
|
|
1776
|
-
</div>
|
|
1777
|
-
</div>
|
|
1778
|
-
);
|
|
1779
|
-
|
|
1780
|
-
return (
|
|
1781
|
-
<div className="w-full">
|
|
1782
|
-
<div className="font-semibold mb-1 text-gray-900">
|
|
1783
|
-
Meraque · Work Order (Offline)
|
|
1784
|
-
</div>
|
|
1785
|
-
<div className="text-sm font-light mb-3 text-gray-500">
|
|
1786
|
-
bareOffline with prefetch of the latest work orders and five reference
|
|
1787
|
-
collections into IndexedDB on mount. Toggle offline, edit a work order
|
|
1788
|
-
and save — it stays on the device; go online and Sync now uploads it.
|
|
1789
|
-
The banner, toggle and sync button are the app's part, copied from
|
|
1790
|
-
Meraque.
|
|
1791
|
-
</div>
|
|
1792
|
-
{renderBanner()}
|
|
1793
|
-
{renderCache()}
|
|
1794
|
-
<DataTemplateWrapper
|
|
1795
|
-
ref={dataRef}
|
|
1796
|
-
title="Work Orders"
|
|
1797
|
-
type={viewType}
|
|
1798
|
-
id={selectedId}
|
|
1799
|
-
collectionId={WORK_ORDER_COLLECTION}
|
|
1800
|
-
schema={MeraqueWorkOrderSchema}
|
|
1801
|
-
workspace={workspace}
|
|
1802
|
-
edit={{
|
|
1803
|
-
allEditable: true,
|
|
1804
|
-
disabledUpdate: false,
|
|
1805
|
-
disableBatchUpdate: false,
|
|
1806
|
-
deleteBtn: true,
|
|
1807
|
-
}}
|
|
1808
|
-
organise={{
|
|
1809
|
-
sort: { sortId: "startDate", type: "desc" as const },
|
|
1810
|
-
breadcrumbType: "button",
|
|
1811
|
-
tabAlign: "left",
|
|
1812
|
-
}}
|
|
1813
|
-
bareSettings={{
|
|
1814
|
-
bareNewDTW: true,
|
|
1815
|
-
bareCompactMode: true,
|
|
1816
|
-
bareListMobileView: isMobile,
|
|
1817
|
-
bareListMobileFloatingSearch: true,
|
|
1818
|
-
bareOffline: true,
|
|
1819
|
-
// INFO: while "online" the DTW follows the network and writes to the
|
|
1820
|
-
// server; the Go offline toggle turns the listener off, which routes
|
|
1821
|
-
// every save to IndexedDB — the same effect a dropped connection has
|
|
1822
|
-
bareOfflineNetworkListener: !offline,
|
|
1823
|
-
bareOfflinePrefetchLimit: 20,
|
|
1824
|
-
bareOfflineReferences: MERAQUE_REFERENCES,
|
|
1825
|
-
}}
|
|
1826
|
-
handleNavigate={handleNavigate}
|
|
1827
|
-
limit={20}
|
|
1828
|
-
/>
|
|
1829
|
-
</div>
|
|
1830
|
-
);
|
|
1831
|
-
};
|
|
1832
|
-
|
|
1833
|
-
// ---------------------------------------------------------------------------
|
|
1834
|
-
// Action component cases
|
|
1835
|
-
// bareUpdateActionComponent is one callback used by the list, the read view
|
|
1836
|
-
// and the update form. Only the update form changed: the three things an app
|
|
1837
|
-
// can return from it, side by side on the same (non-tabbed) form.
|
|
1838
|
-
// ---------------------------------------------------------------------------
|
|
1839
|
-
|
|
1840
|
-
const ACTION_CASES = [
|
|
1841
|
-
{ id: "none", name: "Returns undefined" },
|
|
1842
|
-
{ id: "custom", name: "Returns own buttons" },
|
|
1843
|
-
{ id: "empty", name: "Returns <></>" },
|
|
1844
|
-
];
|
|
1845
|
-
|
|
1846
|
-
export const ActionComponentCases = () => {
|
|
1847
|
-
const [mount, setMount] = useState<boolean>(false);
|
|
1848
|
-
const [, setTick] = useState<number>(0);
|
|
1849
|
-
const [actionCase, setActionCase] = useState<string>("none");
|
|
1850
|
-
const storeRef = useRef<any[]>([JSON.parse(JSON.stringify(simulatorRecord))]);
|
|
1851
|
-
const countersRef = useRef({ update: 0 });
|
|
1852
|
-
const mockRef = useRef<any>(null);
|
|
1853
|
-
|
|
1854
|
-
useEffect(() => {
|
|
1855
|
-
mockRef.current = handleInstallMock(
|
|
1856
|
-
SIMULATOR_COLLECTION,
|
|
1857
|
-
storeRef.current,
|
|
1858
|
-
countersRef.current,
|
|
1859
|
-
() => setTick((previous) => previous + 1),
|
|
1860
|
-
);
|
|
1861
|
-
setMount(true);
|
|
1862
|
-
return () => {
|
|
1863
|
-
mockRef.current?.restore();
|
|
1864
|
-
mockRef.current = null;
|
|
1865
|
-
};
|
|
1866
|
-
}, []);
|
|
1867
|
-
|
|
1868
|
-
if (!mount) return <Loading />;
|
|
1869
|
-
|
|
1870
|
-
const handleActionComponent = (_data: any, formRef: any) => {
|
|
1871
|
-
if (actionCase === "custom") {
|
|
1872
|
-
return (
|
|
1873
|
-
<div className="flex flex-row items-center gap-2">
|
|
1874
|
-
<Button
|
|
1875
|
-
id="case-own-secondary"
|
|
1876
|
-
text="Send for review"
|
|
1877
|
-
type="light"
|
|
1878
|
-
size="small"
|
|
1879
|
-
onClick={() => {}}
|
|
1880
|
-
/>
|
|
1881
|
-
<Button
|
|
1882
|
-
id="case-own-submit"
|
|
1883
|
-
text="Approve & Submit"
|
|
1884
|
-
type="normal"
|
|
1885
|
-
size="small"
|
|
1886
|
-
onClick={() => formRef?.current?.handleSubmit?.()}
|
|
1887
|
-
/>
|
|
1888
|
-
</div>
|
|
1889
|
-
);
|
|
1890
|
-
}
|
|
1891
|
-
if (actionCase === "empty") return <></>;
|
|
1892
|
-
return undefined;
|
|
1893
|
-
};
|
|
1894
|
-
|
|
1895
|
-
return (
|
|
1896
|
-
<div className="w-full">
|
|
1897
|
-
<div className="font-semibold mb-1 text-gray-900">
|
|
1898
|
-
Action Component Cases
|
|
1899
|
-
</div>
|
|
1900
|
-
<div className="text-sm font-light mb-3 text-gray-500">
|
|
1901
|
-
The same update form under the three things bareUpdateActionComponent
|
|
1902
|
-
can return. Undefined → the library's Save · Submit in the floating
|
|
1903
|
-
bar. Own buttons → they sit in the bar instead. An empty fragment → no
|
|
1904
|
-
bar at all (the app owns its buttons elsewhere, e.g. a modal footer).
|
|
1905
|
-
List and read views are untouched in every case.
|
|
1906
|
-
</div>
|
|
1907
|
-
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
1908
|
-
<ButtonGroup
|
|
1909
|
-
selection={ACTION_CASES}
|
|
1910
|
-
selectedId={actionCase}
|
|
1911
|
-
size="small"
|
|
1912
|
-
onClick={(id) => setActionCase(id)}
|
|
1913
|
-
/>
|
|
1914
|
-
<span className="text-xs text-gray-600 font-mono">
|
|
1915
|
-
update calls:{" "}
|
|
1916
|
-
<span data-testid="cases-update-count">
|
|
1917
|
-
{countersRef.current.update}
|
|
1918
|
-
</span>
|
|
1919
|
-
</span>
|
|
1920
|
-
</div>
|
|
1921
|
-
<DataTemplateWrapper
|
|
1922
|
-
key={actionCase}
|
|
1923
|
-
title="Proposal"
|
|
1924
|
-
description="Non-tabbed update form"
|
|
1925
|
-
type="update"
|
|
1926
|
-
id="sim-01"
|
|
1927
|
-
collectionId={SIMULATOR_COLLECTION}
|
|
1928
|
-
schema={VerdantSimulatorSchema}
|
|
1929
|
-
workspace={workspace}
|
|
1930
|
-
edit={{
|
|
1931
|
-
allEditable: true,
|
|
1932
|
-
listEditable: false,
|
|
1933
|
-
disabledUpdate: false,
|
|
1934
|
-
disableBatchUpdate: true,
|
|
1935
|
-
enableUpdateDraft: true,
|
|
1936
|
-
}}
|
|
1937
|
-
organise={{ breadcrumbType: "hidden" }}
|
|
1938
|
-
bareSettings={{
|
|
1939
|
-
bareNewDTW: true,
|
|
1940
|
-
bareCompactMode: true,
|
|
1941
|
-
bareUpdateActionComponent: handleActionComponent,
|
|
1942
|
-
}}
|
|
1943
|
-
handleComplete={() => {}}
|
|
1944
|
-
/>
|
|
1945
|
-
</div>
|
|
1946
|
-
);
|
|
1947
|
-
};
|
|
1948
|
-
|
|
1949
|
-
// ---------------------------------------------------------------------------
|
|
1950
|
-
// Dark mode
|
|
1951
|
-
// The Sales Leads list and the KPJ grant form with organise.darkMode on, on a
|
|
1952
|
-
// Meraque-style page (bg-neutral-900). darkModeBg hands the app's own dark
|
|
1953
|
-
// surface to every panel DataTemplate paints — modals, tables, cards, the
|
|
1954
|
-
// floating bar, toasts — so nothing comes out navy on a black page.
|
|
1955
|
-
// ---------------------------------------------------------------------------
|
|
1956
|
-
|
|
1957
|
-
const DARK_PAGE = "bg-neutral-900";
|
|
1958
|
-
|
|
1959
|
-
export const DarkMode = () => {
|
|
1960
|
-
const [mount, setMount] = useState<boolean>(false);
|
|
1961
|
-
const [, setTick] = useState<number>(0);
|
|
1962
|
-
const [view, setView] = useState<"list" | "calendar" | "form">("list");
|
|
1963
|
-
const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
|
|
1964
|
-
const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
|
|
1965
|
-
const [viewId, setViewId] = useState<string | undefined>(undefined);
|
|
1966
|
-
const isMobile = useIsMobile();
|
|
1967
|
-
const storeRef = useRef<any[]>(JSON.parse(JSON.stringify(workOrderRecords)));
|
|
1968
|
-
const countersRef = useRef({ update: 0, create: 0 });
|
|
1969
|
-
const mockRef = useRef<any>(null);
|
|
1970
|
-
|
|
1971
|
-
useEffect(() => {
|
|
1972
|
-
mockRef.current = handleInstallMeraqueMock(
|
|
1973
|
-
storeRef.current,
|
|
1974
|
-
countersRef.current,
|
|
1975
|
-
() => setTick((previous) => previous + 1),
|
|
1976
|
-
);
|
|
1977
|
-
setMount(true);
|
|
1978
|
-
return () => {
|
|
1979
|
-
mockRef.current?.restore();
|
|
1980
|
-
mockRef.current = null;
|
|
1981
|
-
};
|
|
1982
|
-
}, []);
|
|
1983
|
-
|
|
1984
|
-
const handleNavigate = (type: any, id: any, newViewId?: string) => {
|
|
1985
|
-
setViewType(type);
|
|
1986
|
-
setSelectedId(id);
|
|
1987
|
-
if (newViewId) setViewId(newViewId);
|
|
1988
|
-
};
|
|
1989
|
-
|
|
1990
|
-
if (!mount) return <Loading />;
|
|
1991
|
-
|
|
1992
|
-
const darkOrganise = { darkMode: true, darkModeBg: DARK_PAGE };
|
|
1993
|
-
const handleSwitch = (next: "list" | "calendar" | "form") => {
|
|
1994
|
-
setView(next);
|
|
1995
|
-
setViewType(next === "form" ? "update" : "list");
|
|
1996
|
-
setSelectedId(next === "form" ? "wo-01" : undefined);
|
|
1997
|
-
};
|
|
1998
|
-
|
|
1999
|
-
return (
|
|
2000
|
-
<div className={`${DARK_PAGE} -m-4 min-h-screen p-4 text-white`}>
|
|
2001
|
-
<div className="font-semibold mb-1">Dark Mode · Meraque work orders</div>
|
|
2002
|
-
<div className="text-sm font-light mb-3 text-gray-400">
|
|
2003
|
-
organise.darkMode with darkModeBg = "{DARK_PAGE}" — the class
|
|
2004
|
-
Meraque's pages use, so every DTW surface sits on the app's
|
|
2005
|
-
own dark. The form has reference fields (cycle, team, pilot, robot,
|
|
2006
|
-
vehicle) whose pickers open the modal, plus dates and a time.
|
|
2007
|
-
</div>
|
|
2008
|
-
<div className="mb-4 flex flex-row flex-wrap gap-2">
|
|
2009
|
-
{(["list", "calendar", "form"] as const).map((eachView) => (
|
|
2010
|
-
<Button
|
|
2011
|
-
key={eachView}
|
|
2012
|
-
id={`dark-view-${eachView}`}
|
|
2013
|
-
text={
|
|
2014
|
-
eachView === "list"
|
|
2015
|
-
? "Work orders list"
|
|
2016
|
-
: eachView === "calendar"
|
|
2017
|
-
? "Calendar view"
|
|
2018
|
-
: "Work order form"
|
|
2019
|
-
}
|
|
2020
|
-
type={view === eachView ? "normal" : "light"}
|
|
2021
|
-
size="small"
|
|
2022
|
-
onClick={() => handleSwitch(eachView)}
|
|
2023
|
-
/>
|
|
2024
|
-
))}
|
|
2025
|
-
</div>
|
|
2026
|
-
{view === "list" && (
|
|
2027
|
-
<DataTemplateWrapper
|
|
2028
|
-
key="dark-list"
|
|
2029
|
-
title="Work Orders"
|
|
2030
|
-
description="Drone spraying work orders"
|
|
2031
|
-
type={viewType}
|
|
2032
|
-
id={selectedId ?? ""}
|
|
2033
|
-
collectionId={WORK_ORDER_COLLECTION}
|
|
2034
|
-
schema={MeraqueWorkOrderSchema}
|
|
2035
|
-
workspace={workspace}
|
|
2036
|
-
edit={{ allEditable: true, deleteBtn: true }}
|
|
2037
|
-
organise={{
|
|
2038
|
-
...darkOrganise,
|
|
2039
|
-
sort: { sortId: "startDate", type: "desc" as const },
|
|
2040
|
-
viewId,
|
|
2041
|
-
// INFO: one tab per list renderer so every dark surface can be
|
|
2042
|
-
// checked from the same story — list, grid, feeds, kanban (the default
|
|
2043
|
-
// tab is the table)
|
|
2044
|
-
extendedTabs: [
|
|
2045
|
-
{ name: "List", type: "list", viewType: "list" },
|
|
2046
|
-
{ name: "Grid", type: "list", viewType: "grid" },
|
|
2047
|
-
{
|
|
2048
|
-
name: "Feeds",
|
|
2049
|
-
type: "list",
|
|
2050
|
-
viewType: "feeds",
|
|
2051
|
-
feedsSettings: {
|
|
2052
|
-
title: "title",
|
|
2053
|
-
content: "status",
|
|
2054
|
-
desc: "startDate",
|
|
2055
|
-
},
|
|
2056
|
-
},
|
|
2057
|
-
{
|
|
2058
|
-
name: "Kanban",
|
|
2059
|
-
type: "list",
|
|
2060
|
-
viewType: "kanban",
|
|
2061
|
-
kanbanSettings: { title: "status" },
|
|
2062
|
-
},
|
|
2063
|
-
],
|
|
2064
|
-
}}
|
|
2065
|
-
bareSettings={{
|
|
2066
|
-
bareNewDTW: true,
|
|
2067
|
-
bareCompactMode: true,
|
|
2068
|
-
bareReadListTabs: true,
|
|
2069
|
-
bareListMobileView: isMobile,
|
|
2070
|
-
bareListMobileFloatingSearch: true,
|
|
2071
|
-
bareFlexFilters: true,
|
|
2072
|
-
}}
|
|
2073
|
-
handleNavigate={handleNavigate}
|
|
2074
|
-
limit={20}
|
|
2075
|
-
/>
|
|
2076
|
-
)}
|
|
2077
|
-
{view === "calendar" && (
|
|
2078
|
-
<DataTemplateWrapper
|
|
2079
|
-
key="dark-calendar"
|
|
2080
|
-
title="Spray schedule"
|
|
2081
|
-
description="Work orders by start date"
|
|
2082
|
-
type={viewType}
|
|
2083
|
-
id={selectedId ?? ""}
|
|
2084
|
-
collectionId={WORK_ORDER_COLLECTION}
|
|
2085
|
-
schema={MeraqueWorkOrderSchema}
|
|
2086
|
-
workspace={workspace}
|
|
2087
|
-
edit={{ allEditable: false }}
|
|
2088
|
-
organise={{ ...darkOrganise }}
|
|
2089
|
-
viewSettings={{
|
|
2090
|
-
calendarSettings: {
|
|
2091
|
-
title: "title",
|
|
2092
|
-
startDate: "startDate",
|
|
2093
|
-
groupId: "status",
|
|
2094
|
-
defaultView: "dayGridMonth",
|
|
2095
|
-
},
|
|
2096
|
-
}}
|
|
2097
|
-
bareSettings={{ bareNewDTW: true, bareCompactMode: true }}
|
|
2098
|
-
handleNavigate={handleNavigate}
|
|
2099
|
-
limit={50}
|
|
2100
|
-
/>
|
|
2101
|
-
)}
|
|
2102
|
-
{view === "form" && (
|
|
2103
|
-
<DataTemplateWrapper
|
|
2104
|
-
key="dark-form"
|
|
2105
|
-
title="Work Order"
|
|
2106
|
-
type={viewType}
|
|
2107
|
-
id={selectedId}
|
|
2108
|
-
collectionId={WORK_ORDER_COLLECTION}
|
|
2109
|
-
schema={MeraqueWorkOrderSchema}
|
|
2110
|
-
workspace={workspace}
|
|
2111
|
-
edit={{
|
|
2112
|
-
allEditable: true,
|
|
2113
|
-
disabledUpdate: false,
|
|
2114
|
-
deleteBtn: true,
|
|
2115
|
-
enableUpdateDraft: true,
|
|
2116
|
-
}}
|
|
2117
|
-
organise={{ ...darkOrganise, breadcrumbType: "button" }}
|
|
2118
|
-
bareSettings={{ bareNewDTW: true, bareCompactMode: true }}
|
|
2119
|
-
handleNavigate={handleNavigate}
|
|
2120
|
-
limit={10}
|
|
2121
|
-
/>
|
|
2122
|
-
)}
|
|
2123
|
-
</div>
|
|
2124
|
-
);
|
|
2125
|
-
};
|