@lateralus-ai/shipping-ui 1.2.0 → 1.3.0

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.
@@ -0,0 +1 @@
1
+ export { default as DocumentEditor } from "./DocumentEditor";
@@ -3,3 +3,4 @@ export * as InputPrompt from "./InputPrompt";
3
3
  export * as Sidebar from "./Sidebar";
4
4
  export * from "./PdfViewer/";
5
5
  export * from "./Tabs";
6
+ export * from "./DocumentEditor";
@@ -0,0 +1,287 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { DocumentEditor } from "../components/DocumentEditor";
3
+ import type { Schema } from "../types/documentEditor";
4
+
5
+ // Sample schema based on the defect report form
6
+ const sampleSchema: Schema = {
7
+ type: "object",
8
+ properties: {
9
+ shipName: {
10
+ type: "string",
11
+ description: "Name of the ship",
12
+ },
13
+ reportDate: {
14
+ type: "string",
15
+ format: "date",
16
+ description: "Date of the report",
17
+ },
18
+ reportNo: {
19
+ type: "string",
20
+ description: "Report number",
21
+ },
22
+ malfunctionDateTime: {
23
+ type: "string",
24
+ format: "date",
25
+ description: "Date the malfunction occurred",
26
+ },
27
+ malfunctionDescription: {
28
+ type: "string",
29
+ description: "Description of the malfunction or defect",
30
+ },
31
+ isCriticalEquipment: {
32
+ type: "boolean",
33
+ description: "Whether the equipment referenced is critical",
34
+ docx_mapping: {
35
+ type: "checkbox",
36
+ true_name: "Check1",
37
+ false_name: "Check2",
38
+ },
39
+ },
40
+ equipmentManufacturer: {
41
+ type: "string",
42
+ description: "The malfunctioning equipment manufacturer",
43
+ },
44
+ equipmentModel: {
45
+ type: "string",
46
+ description: "The malfunctioning equipment model",
47
+ },
48
+ equipmentSerial: {
49
+ type: "string",
50
+ description: "The malfunctioning equipment serial number",
51
+ },
52
+ equipmentManufacturerModelSerial: {
53
+ type: "string",
54
+ description: "Manufacturer, model, type, and serial number",
55
+ },
56
+ consequences: {
57
+ type: "string",
58
+ description: "Consequences of the malfunction",
59
+ },
60
+ rootCause: {
61
+ type: "string",
62
+ description: "Root cause of the defect (if known)",
63
+ },
64
+ actionsTaken: {
65
+ type: "string",
66
+ description: "Actions taken or suggestions for corrective actions",
67
+ },
68
+ isShoreAssistanceRequired: {
69
+ type: "boolean",
70
+ description: "Whether shore assistance is required",
71
+ docx_mapping: {
72
+ type: "checkbox",
73
+ true_name: "Check3",
74
+ false_name: "Check4",
75
+ },
76
+ },
77
+ shoreAssistanceRequisition: {
78
+ type: "string",
79
+ description: "Requisition details for shore assistance",
80
+ },
81
+ isEquipmentLandedAshore: {
82
+ type: "boolean",
83
+ description: "Whether equipment/machinery was landed ashore",
84
+ docx_mapping: {
85
+ type: "checkbox",
86
+ true_name: "Check5",
87
+ false_name: "Check6",
88
+ },
89
+ },
90
+ equipmentLandedRequisition: {
91
+ type: "string",
92
+ description: "Requisition details for equipment landed ashore",
93
+ },
94
+ areSparesAvailableOnboard: {
95
+ type: "boolean",
96
+ description: "Whether required spare parts are available onboard",
97
+ docx_mapping: {
98
+ type: "checkbox",
99
+ true_name: "Check7",
100
+ false_name: "Check8",
101
+ },
102
+ },
103
+ sparesAvailableRequisition: {
104
+ type: "string",
105
+ description: "Requisition details if spares are available onboard",
106
+ },
107
+ isNextDryDock: {
108
+ type: "boolean",
109
+ description: "If it's going to close out in next dry dock",
110
+ docx_mapping: {
111
+ type: "checkbox",
112
+ true_name: "Check9",
113
+ false_name: "Check10",
114
+ },
115
+ },
116
+ proposedCompletionDate: {
117
+ type: "string",
118
+ format: "date",
119
+ description: "Proposed date for completion",
120
+ },
121
+ chiefEngineerName: {
122
+ type: "string",
123
+ description: "Name of the Chief Engineer",
124
+ },
125
+ chiefEngineerSignature: {
126
+ type: "string",
127
+ description: "Signature of the Chief Engineer (surname only)",
128
+ },
129
+ chiefEngineerDate: {
130
+ type: "string",
131
+ format: "date",
132
+ description: "Date signed by the Chief Engineer",
133
+ },
134
+ masterName: {
135
+ type: "string",
136
+ description: "Name of the Master",
137
+ },
138
+ masterSignature: {
139
+ type: "string",
140
+ description: "Signature of the Master (surname only)",
141
+ },
142
+ masterDate: {
143
+ type: "string",
144
+ format: "date",
145
+ description: "Date signed by the Master",
146
+ },
147
+ },
148
+ required: [],
149
+ };
150
+
151
+ // Sample initial data for demonstration
152
+ const sampleInitialData = {
153
+ shipName: "MV Excellence",
154
+ reportNo: "DR-2024-001",
155
+ reportDate: "2024-03-15",
156
+ malfunctionDateTime: "2024-03-14",
157
+ malfunctionDescription:
158
+ "Main engine turbocharger failure during normal operation",
159
+ isCriticalEquipment: true,
160
+ equipmentManufacturer: "ABB",
161
+ equipmentModel: "TPL 85-B",
162
+ equipmentSerial: "TC-12345-2020",
163
+ consequences: "Reduced engine power, speed limited to 10 knots",
164
+ rootCause: "Bearing failure due to inadequate lubrication",
165
+ actionsTaken:
166
+ "Turbocharger isolated, vessel proceeding at reduced speed to nearest port",
167
+ isShoreAssistanceRequired: true,
168
+ shoreAssistanceRequisition:
169
+ "Service engineer required for turbocharger overhaul",
170
+ isEquipmentLandedAshore: false,
171
+ areSparesAvailableOnboard: false,
172
+ isNextDryDock: false,
173
+ proposedCompletionDate: "2024-03-20",
174
+ };
175
+
176
+ const DocumentEditorWrapper = () => {
177
+ const handleDataChange = (data: Record<string, any>) => {
178
+ console.log("Data changed:", data);
179
+ };
180
+
181
+ return (
182
+ <div className="w-full max-w-6xl mx-auto p-4">
183
+ <DocumentEditor
184
+ docxUrl="/sample-document.docx"
185
+ schema={sampleSchema}
186
+ onDataChange={handleDataChange}
187
+ initialData={sampleInitialData}
188
+ readonly={false}
189
+ />
190
+ </div>
191
+ );
192
+ };
193
+
194
+ const meta = {
195
+ title: "Design System/DocumentEditor",
196
+ component: DocumentEditorWrapper,
197
+ parameters: {
198
+ layout: "fullscreen",
199
+ docs: {
200
+ description: {
201
+ component: `
202
+ The DocumentEditor component provides an interactive Word document editor with inline field editing capabilities.
203
+
204
+ ## Features
205
+ - Renders Word documents using docx-preview
206
+ - Supports inline editing with various field types (text, boolean, date, dropdown)
207
+ - Tracks field changes with visual color coding
208
+ - Handles checkbox patterns (single, dual Yes/No, radio buttons)
209
+ - Preserves document formatting and layout
210
+
211
+ ## Usage
212
+ \`\`\`tsx
213
+ import { DocumentEditor } from '@lateralus-ai/shipping-ui';
214
+
215
+ <DocumentEditor
216
+ docxUrl="/path/to/document.docx"
217
+ schema={documentSchema}
218
+ onDataChange={(data) => console.log(data)}
219
+ initialData={initialValues}
220
+ readonly={false}
221
+ />
222
+ \`\`\`
223
+ `,
224
+ },
225
+ },
226
+ },
227
+ tags: ["autodocs"],
228
+ argTypes: {
229
+ docxUrl: {
230
+ control: "text",
231
+ description: "URL to the DOCX document template",
232
+ },
233
+ readonly: {
234
+ control: "boolean",
235
+ description: "Whether the document is in read-only mode",
236
+ },
237
+ },
238
+ } satisfies Meta<typeof DocumentEditorWrapper>;
239
+
240
+ export default meta;
241
+ type Story = StoryObj<typeof meta>;
242
+
243
+ export const Default: Story = {
244
+ render: () => {
245
+ return (
246
+ <div className="w-full max-w-6xl mx-auto p-4">
247
+ <DocumentEditor
248
+ docxUrl="/sample-document.docx"
249
+ schema={sampleSchema}
250
+ initialData={sampleInitialData}
251
+ onDataChange={(data) => console.log(data)}
252
+ />
253
+ </div>
254
+ );
255
+ },
256
+ };
257
+
258
+ export const ReadOnly: Story = {
259
+ render: () => {
260
+ return (
261
+ <div className="w-full max-w-6xl mx-auto p-4">
262
+ <DocumentEditor
263
+ docxUrl="/sample-document.docx"
264
+ schema={sampleSchema}
265
+ initialData={sampleInitialData}
266
+ readonly={true}
267
+ />
268
+ </div>
269
+ );
270
+ },
271
+ };
272
+
273
+ export const Empty: Story = {
274
+ render: () => {
275
+ return (
276
+ <div className="w-full max-w-6xl mx-auto p-4">
277
+ <DocumentEditor
278
+ docxUrl="/sample-document.docx"
279
+ schema={sampleSchema}
280
+ initialData={{}}
281
+ readonly={false}
282
+ onDataChange={(data) => console.log(data)}
283
+ />
284
+ </div>
285
+ );
286
+ },
287
+ };
@@ -0,0 +1,56 @@
1
+ // Type definitions for Document Editor components
2
+
3
+ export interface SchemaProperty {
4
+ type: "string" | "boolean" | "number" | "integer";
5
+ enum?: string[];
6
+ format?: "date" | "date-time" | string;
7
+ description?: string;
8
+ title?: string;
9
+ "ui:widget"?: string;
10
+ "ui:rows"?: number;
11
+ docx_mapping?: DocxMapping;
12
+ }
13
+
14
+ export interface DocxMapping {
15
+ type: "checkbox" | "radio" | "text";
16
+ name?: string;
17
+ true_name?: string;
18
+ false_name?: string;
19
+ mapping?: RadioMapping[];
20
+ }
21
+
22
+ export interface RadioMapping {
23
+ name: string;
24
+ value: string | boolean;
25
+ }
26
+
27
+ export interface Schema {
28
+ type: string;
29
+ properties: Record<string, SchemaProperty>;
30
+ required?: string[];
31
+ }
32
+
33
+ export interface CheckboxModuleOptions {
34
+ checkboxData?: Record<string, boolean>;
35
+ schema?: Schema;
36
+ finalGeneration?: boolean;
37
+ }
38
+
39
+ export interface DocumentEditorProps {
40
+ docxUrl: string;
41
+ schema: Schema;
42
+ onDataChange?: (data: Record<string, any>) => void;
43
+ initialData?: Record<string, any>;
44
+ onSaveRef?: React.MutableRefObject<(() => void) | null>;
45
+ readonly?: boolean;
46
+ }
47
+
48
+ export interface DocxtemplaterPart {
49
+ value?: string;
50
+ [key: string]: any;
51
+ }
52
+
53
+ export interface DocxtemplaterOptions {
54
+ contentType?: string;
55
+ [key: string]: any;
56
+ }
@@ -0,0 +1,242 @@
1
+ import type {
2
+ CheckboxModuleOptions,
3
+ Schema,
4
+ DocxtemplaterPart,
5
+ DocxtemplaterOptions,
6
+ } from "../types/documentEditor";
7
+
8
+ /**
9
+ * CheckboxModule for docxtemplater
10
+ *
11
+ * This module handles three distinct checkbox patterns in DOCX documents:
12
+ *
13
+ * 1. **Single Checkbox Pattern**: A field maps to a single checkbox
14
+ * - Schema: `docx_mapping: { type: "checkbox", name: "Check1" }`
15
+ * - Maps boolean field value directly to checkbox state
16
+ *
17
+ * 2. **Dual Checkbox Pattern**: Yes/No checkboxes for a single field
18
+ * - Schema: `docx_mapping: { type: "checkbox", true_name: "Check1", false_name: "Check2" }`
19
+ * - true_name checkbox checked when field is true
20
+ * - false_name checkbox checked when field is false
21
+ * - Used for Yes/No questions where both options have checkboxes
22
+ *
23
+ * 3. **Radio Button Pattern**: Multiple checkboxes acting as radio buttons
24
+ * - Schema: `docx_mapping: { type: "radio", mapping: [{ name: "Check1", value: "option1" }, ...] }`
25
+ * - Only the checkbox corresponding to the field's value is checked
26
+ * - Used for multiple choice questions
27
+ *
28
+ * The module operates in two modes:
29
+ * - **Preview Mode** (finalGeneration: false): Creates visual markers for inline editing
30
+ * - **Final Generation Mode** (finalGeneration: true): Updates actual Word form fields
31
+ */
32
+ class CheckboxModule {
33
+ private checkboxData: Record<string, boolean>;
34
+ private schema?: Schema;
35
+ private finalGeneration: boolean;
36
+
37
+ constructor(options: CheckboxModuleOptions = {}) {
38
+ this.checkboxData = options.checkboxData || {};
39
+ this.schema = options.schema;
40
+ this.finalGeneration = options.finalGeneration || false;
41
+ }
42
+
43
+ // Required docxtemplater module interface
44
+ name = "CheckboxModule";
45
+
46
+ parse(placeHolderContent: string) {
47
+ // Return null to indicate this module doesn't handle this placeholder
48
+ // This allows other modules/default processing to handle it
49
+ return null;
50
+ }
51
+
52
+ postparse(parsed: any) {
53
+ return parsed;
54
+ }
55
+
56
+ render(part: any, options: any) {
57
+ return part;
58
+ }
59
+
60
+ postrender(parts: DocxtemplaterPart[], options: DocxtemplaterOptions) {
61
+ const hasValidOptions = !!(
62
+ options &&
63
+ options.contentType &&
64
+ options.contentType ===
65
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"
66
+ );
67
+
68
+ return !hasValidOptions ? parts : (() => {
69
+ try {
70
+ // Convert parts to string for processing
71
+ const xmlContent = parts
72
+ .map((part) => {
73
+ const isString = typeof part === "string";
74
+ const hasValue = part && typeof part === "object" && part.value;
75
+
76
+ const getString = () => isString && part;
77
+ const getValue = () => !isString && hasValue && part.value;
78
+ const getStringified = () => !isString && !hasValue && String(part);
79
+
80
+ return getString() || getValue() || getStringified();
81
+ })
82
+ .join("");
83
+
84
+ // Process checkbox patterns
85
+ const processedContent = this.processCheckboxPatterns(xmlContent);
86
+
87
+ // Return the processed content maintaining the same structure as input
88
+ return [processedContent];
89
+ } catch (error) {
90
+ // Return original parts if processing fails
91
+ return parts;
92
+ }
93
+ })();
94
+ }
95
+
96
+ /**
97
+ * Process checkbox patterns in the document XML
98
+ * Identifies Word form field checkboxes by their bookmark names (Check1, Check2, etc.)
99
+ * and either updates their values or replaces them with visual markers
100
+ */
101
+ private processCheckboxPatterns(xmlContent: string): string {
102
+ try {
103
+ // Pattern 1: Simple checkbox bookmarks (most conservative approach)
104
+ // Matches: <w:bookmarkStart w:name="Check1" w:id="0"/>
105
+ const simpleBookmarkRegex =
106
+ /<w:bookmarkStart[^>]*w:name="(Check\d+)"[^>]*\/>/gi;
107
+
108
+ // Reset regex
109
+ simpleBookmarkRegex.lastIndex = 0;
110
+
111
+ const checkboxMatches: string[] = [];
112
+ let match;
113
+ while ((match = simpleBookmarkRegex.exec(xmlContent)) !== null) {
114
+ checkboxMatches.push(match[1]);
115
+ }
116
+
117
+ // Only process if we found actual checkbox bookmarks
118
+ const hasCheckboxMatches = checkboxMatches.length > 0;
119
+
120
+ return !hasCheckboxMatches ? xmlContent : (() => {
121
+ const processForFinalGeneration = () => {
122
+ // For final generation, update the checkbox default values directly in the XML
123
+ checkboxMatches.forEach((checkboxName) => {
124
+ const isChecked = this.getCheckboxState(checkboxName);
125
+ const checkedValue = isChecked ? "1" : "0";
126
+
127
+ // Update the default value in the checkbox XML
128
+ const checkboxRegex = new RegExp(
129
+ `(<w:fldChar w:fldCharType="begin">.*?<w:name w:val="${checkboxName}".*?<w:default w:val=")([01])(".*?</w:fldChar>)`,
130
+ "gs",
131
+ );
132
+
133
+ xmlContent = xmlContent.replace(checkboxRegex, `$1${checkedValue}$3`);
134
+ });
135
+ return xmlContent;
136
+ };
137
+
138
+ const processForPreview = () => {
139
+ // For preview mode, create visual markers
140
+ // Reset regex for replacement
141
+ simpleBookmarkRegex.lastIndex = 0;
142
+
143
+ return xmlContent.replace(
144
+ simpleBookmarkRegex,
145
+ (match, checkboxName) => {
146
+ try {
147
+ const isChecked = this.getCheckboxState(checkboxName);
148
+ const symbol = isChecked ? "☑" : "☐";
149
+ const marker = `|||${symbol}|||checkbox:${checkboxName}|||`;
150
+
151
+ // Replace just the bookmark start with our marker in a text run
152
+ return `<w:r><w:t>${marker}</w:t></w:r><w:bookmarkStart w:name="${checkboxName}" w:id="0"/>`;
153
+ } catch (error) {
154
+ // Return original if processing fails
155
+ return match;
156
+ }
157
+ },
158
+ );
159
+ };
160
+
161
+ const renderFinalGeneration = () => this.finalGeneration && processForFinalGeneration();
162
+ const renderPreview = () => !this.finalGeneration && processForPreview();
163
+
164
+ return renderFinalGeneration() || renderPreview();
165
+ })();
166
+ } catch (error) {
167
+ // Return original content if processing fails
168
+ return xmlContent;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Determine the state of a checkbox based on field mappings and data
174
+ * @param checkboxName - The name of the checkbox (e.g., "Check1")
175
+ * @returns true if checkbox should be checked, false otherwise
176
+ */
177
+ private getCheckboxState(checkboxName: string): boolean {
178
+ const hasSchema = !!this.schema;
179
+
180
+ const getSimpleLookup = () => !hasSchema && (this.checkboxData[checkboxName] || false);
181
+ const getSchemaBasedLookup = () => hasSchema && (() => {
182
+ // Check all fields in the schema for checkbox mappings
183
+ const matchedField = Object.entries(
184
+ this.schema!.properties || {},
185
+ ).find(([fieldName, fieldConfig]) => {
186
+ const config = fieldConfig as any;
187
+ const docxMapping = config.docx_mapping;
188
+ const hasCheckboxMapping = docxMapping && docxMapping.type === "checkbox";
189
+ const hasRadioMapping = docxMapping && docxMapping.type === "radio" && docxMapping.mapping;
190
+
191
+ // Pattern 1: Single checkbox with "name" property
192
+ const isSingleCheckbox = hasCheckboxMapping && docxMapping.name === checkboxName;
193
+
194
+ // Pattern 2: Dual checkbox with "true_name" and "false_name" properties
195
+ const isTrueCheckbox = hasCheckboxMapping && docxMapping.true_name === checkboxName;
196
+ const isFalseCheckbox = hasCheckboxMapping && docxMapping.false_name === checkboxName;
197
+
198
+ // Pattern 3: Radio button mapping
199
+ const radioMapping = hasRadioMapping && docxMapping.mapping.find(
200
+ (m: any) => m.name === checkboxName,
201
+ );
202
+
203
+ return isSingleCheckbox || isTrueCheckbox || isFalseCheckbox || !!radioMapping;
204
+ });
205
+
206
+ return matchedField ? (() => {
207
+ const [fieldName, fieldConfig] = matchedField;
208
+ const config = fieldConfig as any;
209
+ const docxMapping = config.docx_mapping;
210
+
211
+ // Pattern 1: Single checkbox with "name" property
212
+ const isSingleCheckbox = docxMapping && docxMapping.type === "checkbox" && docxMapping.name === checkboxName;
213
+
214
+ // Pattern 2: Dual checkbox with "true_name" and "false_name" properties
215
+ const isTrueCheckbox = docxMapping && docxMapping.type === "checkbox" && docxMapping.true_name === checkboxName;
216
+ const isFalseCheckbox = docxMapping && docxMapping.type === "checkbox" && docxMapping.false_name === checkboxName;
217
+
218
+ // Pattern 3: Radio button mapping
219
+ const radioMapping = docxMapping && docxMapping.type === "radio" && docxMapping.mapping &&
220
+ docxMapping.mapping.find((m: any) => m.name === checkboxName);
221
+
222
+ const checkSingleOrTrue = () => (isSingleCheckbox || isTrueCheckbox) && this.checkboxData[fieldName] === true;
223
+ const checkFalse = () => isFalseCheckbox && this.checkboxData[fieldName] === false;
224
+ const checkRadio = () => radioMapping && this.checkboxData[fieldName] === radioMapping.value;
225
+
226
+ return checkSingleOrTrue() || checkFalse() || checkRadio() || false;
227
+ })() : (this.checkboxData[checkboxName] || false);
228
+ })();
229
+
230
+ return getSimpleLookup() || getSchemaBasedLookup() || false;
231
+ }
232
+
233
+ /**
234
+ * Update checkbox data (can be called to change checkbox states)
235
+ * @param checkboxData - New checkbox data to merge with existing data
236
+ */
237
+ updateCheckboxData(checkboxData: Record<string, boolean>) {
238
+ this.checkboxData = { ...this.checkboxData, ...checkboxData };
239
+ }
240
+ }
241
+
242
+ export default CheckboxModule;