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.
Files changed (47) hide show
  1. package/lib/components/action/iconButton/iconButton.stories.tsx +11 -2
  2. package/lib/components/auth/signUp/signUp.stories.tsx +39 -1
  3. package/lib/components/chart/statsCard/README.md +24 -0
  4. package/lib/components/chart/statsCard/statsCard.stories.tsx +68 -0
  5. package/lib/components/chat/chatBase/chatBase.stories.tsx +1 -1
  6. package/lib/components/data/dataGraph/dataGraph.stories.tsx +5 -5
  7. package/lib/components/data/dataTemplate/README.md +204 -85
  8. package/lib/components/data/dataTemplate/dataTemplate.stories.tsx +307 -295
  9. package/lib/components/data/dataTemplate/drive.stories.tsx +197 -0
  10. package/lib/components/data/dataWrapper/README.md +109 -22
  11. package/lib/components/data/dataWrapper/dataWrapper.bareHooks.stories.tsx +951 -0
  12. package/lib/components/data/dataWrapper/dataWrapper.edit.stories.tsx +356 -5
  13. package/lib/components/data/dataWrapper/dataWrapper.view.stories.tsx +79 -5
  14. package/lib/components/display/calendar/calendar.stories.tsx +1 -1
  15. package/lib/components/display/detailList/README.md +11 -10
  16. package/lib/components/display/detailList/detailList.stories.tsx +16 -3
  17. package/lib/components/display/feeds/README.md +3 -3
  18. package/lib/components/display/feeds/feeds.stories.tsx +3 -3
  19. package/lib/components/display/filePreview/filePreview.stories.tsx +111 -0
  20. package/lib/components/display/modal/README.md +3 -0
  21. package/lib/components/display/modal/modal.stories.tsx +50 -1
  22. package/lib/components/display/newCalendar/README.md +0 -6
  23. package/lib/components/display/newCalendar/newCalendar.stories.tsx +1 -1
  24. package/lib/components/display/newFeeds/README.md +1 -5
  25. package/lib/components/display/newFeeds/newFeeds.stories.tsx +3 -3
  26. package/lib/components/display/newKanban/README.md +0 -7
  27. package/lib/components/display/newList/README.md +1 -4
  28. package/lib/components/display/newTable/README.md +29 -9
  29. package/lib/components/display/newTable/newTable.stories.tsx +120 -9
  30. package/lib/components/display/tag/README.md +1 -1
  31. package/lib/components/display/tag/tag.stories.tsx +7 -0
  32. package/lib/components/input/markdown/markdown.stories.tsx +3 -1
  33. package/lib/components/input/uploadInput/uploadInput.stories.tsx +41 -0
  34. package/lib/components/navigation/leftNav/leftNav.stories.tsx +175 -0
  35. package/lib/components/navigation/tabs/tabs.stories.tsx +23 -0
  36. package/lib/components/template/calendarView/calendarView.stories.tsx +4 -4
  37. package/lib/components/template/chatDashboard/chatDashboard.stories.tsx +2 -2
  38. package/lib/mcp/tools/getBackendComponent.d.ts.map +1 -1
  39. package/lib/mcp/tools/getBackendComponent.js +7 -3
  40. package/lib/mcp/tools/getBackendComponent.js.map +1 -1
  41. package/lib/mcp/tools/suggestTemplate.js +3 -3
  42. package/lib/mcp/tools/suggestTemplate.js.map +1 -1
  43. package/package.json +1 -1
  44. package/lib/components/data/dataTemplate/dataModule.stories.tsx +0 -191
  45. package/lib/components/data/dataWrapper/dataWrapper.consumer.stories.tsx +0 -2125
  46. package/lib/components/data/dataWrapper/dataWrapper.utils.stories.tsx +0 -576
  47. 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 &quot;preCheckList&quot;: 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
- &quot;four&quot;, bareUpdateMobileView, list inputs locked, the
1320
- caller&apos;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&apos;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&apos;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 = &quot;{DARK_PAGE}&quot; — the class
2004
- Meraque&apos;s pages use, so every DTW surface sits on the app&apos;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
- };