@reekon-tools/boldr-utils 1.7.4 → 1.8.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.
- package/dist/calculator/categories.d.ts +76 -0
- package/dist/calculator/categories.js +160 -0
- package/dist/calculator/index.d.ts +1 -0
- package/dist/calculator/index.js +1 -0
- package/dist/calculator/schema.d.ts +12 -0
- package/dist/calculator/schema.js +3 -0
- package/dist/calculator/validate.d.ts +1 -0
- package/dist/calculator/validate.js +15 -0
- package/dist/export/buildJobCsv.d.ts +26 -0
- package/dist/export/buildJobCsv.js +189 -0
- package/dist/export/config.d.ts +18 -0
- package/dist/export/config.js +72 -0
- package/dist/export/desktopLinks.d.ts +11 -0
- package/dist/export/desktopLinks.js +45 -0
- package/dist/export/objectKinds.d.ts +3 -0
- package/dist/export/objectKinds.js +28 -0
- package/dist/exports.d.ts +5 -1
- package/dist/exports.js +7 -1
- package/dist/utils/groups.d.ts +2 -1
- package/dist/utils/groups.js +14 -0
- package/package.json +2 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { CalculatorDefinition } from './schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Category ids, as PERSISTED on `calculators/{id}.category`.
|
|
4
|
+
*
|
|
5
|
+
* Never rename a value — it is stored data. To retire a category, delete its
|
|
6
|
+
* entry from CALCULATOR_CATEGORIES and leave existing docs alone: they resolve
|
|
7
|
+
* to `undefined` and degrade into the "Other" bucket rather than breaking.
|
|
8
|
+
*/
|
|
9
|
+
export declare enum CalculatorCategoryId {
|
|
10
|
+
AreaAndVolume = "areaAndVolume",
|
|
11
|
+
Concrete = "concrete",
|
|
12
|
+
Electrical = "electrical",
|
|
13
|
+
Stairs = "stairs",
|
|
14
|
+
Framing = "framing",
|
|
15
|
+
FinishCarpentry = "finishCarpentry",
|
|
16
|
+
Hardscaping = "hardscaping",
|
|
17
|
+
Roofing = "roofing",
|
|
18
|
+
HVAC = "hvac",
|
|
19
|
+
Landscaping = "landscaping",
|
|
20
|
+
MaterialEstimator = "materialEstimator",
|
|
21
|
+
Optimizers = "optimizers",
|
|
22
|
+
Plumbing = "plumbing"
|
|
23
|
+
}
|
|
24
|
+
export interface CalculatorCategory {
|
|
25
|
+
id: CalculatorCategoryId;
|
|
26
|
+
/** Display label: the editor dropdown row AND the mobile category screen title. */
|
|
27
|
+
name: string;
|
|
28
|
+
/**
|
|
29
|
+
* Tile-glyph tint from the design. Uppercase 6-digit hex.
|
|
30
|
+
*
|
|
31
|
+
* This is the source of truth for the color: the icon generator bakes it
|
|
32
|
+
* into the per-app SVGs (see scripts/gen-category-icons.mjs), so a color
|
|
33
|
+
* change here needs a regenerate to reach the apps.
|
|
34
|
+
*/
|
|
35
|
+
color: string;
|
|
36
|
+
/**
|
|
37
|
+
* Glyph name in the legacy ROCK app's IcoMoon set, which is where this
|
|
38
|
+
* artwork comes from. Consumed only by the icon generator — clients load the
|
|
39
|
+
* emitted SVG by category id.
|
|
40
|
+
*/
|
|
41
|
+
glyph: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Canonical order, matching the design's tile grid.
|
|
45
|
+
*
|
|
46
|
+
* This array IS the ordering — there is deliberately no `sortIndex` field for
|
|
47
|
+
* it to drift out of sync with.
|
|
48
|
+
*/
|
|
49
|
+
export declare const CALCULATOR_CATEGORIES: readonly CalculatorCategory[];
|
|
50
|
+
export declare const CALCULATOR_CATEGORY_BY_ID: Readonly<Record<CalculatorCategoryId, CalculatorCategory>>;
|
|
51
|
+
/**
|
|
52
|
+
* Bucket key / route segment for calculators with no category, or with one
|
|
53
|
+
* that no longer exists. NOT a CalculatorCategoryId — guarded by a unit test,
|
|
54
|
+
* because a collision would silently swallow a real category.
|
|
55
|
+
*/
|
|
56
|
+
export declare const UNCATEGORIZED_CATEGORY_ID = "uncategorized";
|
|
57
|
+
export declare const UNCATEGORIZED_CATEGORY_NAME = "Other";
|
|
58
|
+
export type CalculatorCategoryBucketId = CalculatorCategoryId | typeof UNCATEGORIZED_CATEGORY_ID;
|
|
59
|
+
export declare const isCalculatorCategoryId: (value: unknown) => value is CalculatorCategoryId;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a stored category value. Returns undefined for missing, null, and
|
|
62
|
+
* retired ids — callers should treat all three as "Other" rather than trusting
|
|
63
|
+
* the raw field.
|
|
64
|
+
*/
|
|
65
|
+
export declare const findCalculatorCategory: (value: string | null | undefined) => CalculatorCategory | undefined;
|
|
66
|
+
export declare const calculatorCategoryOf: (calculator: Pick<CalculatorDefinition, "category">) => CalculatorCategory | undefined;
|
|
67
|
+
/** Display label for a stored value, falling back to "Other". */
|
|
68
|
+
export declare const calculatorCategoryName: (value: string | null | undefined) => string;
|
|
69
|
+
/**
|
|
70
|
+
* Group calculators by category id, collecting anything missing or
|
|
71
|
+
* unresolvable under UNCATEGORIZED_CATEGORY_ID.
|
|
72
|
+
*
|
|
73
|
+
* Keys exist only for non-empty buckets, which is what lets the mobile grid
|
|
74
|
+
* hide empty categories without a second pass.
|
|
75
|
+
*/
|
|
76
|
+
export declare const groupCalculatorsByCategory: <T extends Pick<CalculatorDefinition, "category">>(calculators: readonly T[]) => Map<CalculatorCategoryBucketId, T[]>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Calculator categories — the hard-coded taxonomy used to group published
|
|
3
|
+
// calculators in the mobile catalog and to label them in the web authoring
|
|
4
|
+
// tool.
|
|
5
|
+
//
|
|
6
|
+
// Deliberately NOT a Firestore collection: the set is authored here and ships
|
|
7
|
+
// inside the package, so both clients render the same list with no extra read,
|
|
8
|
+
// no rules surface, and no offline story to maintain.
|
|
9
|
+
//
|
|
10
|
+
// Distinct from `CalculatorDefinition.folderId`, which predates this and is
|
|
11
|
+
// still unused — `folderId` implies a hierarchy, a category is flat.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* Category ids, as PERSISTED on `calculators/{id}.category`.
|
|
15
|
+
*
|
|
16
|
+
* Never rename a value — it is stored data. To retire a category, delete its
|
|
17
|
+
* entry from CALCULATOR_CATEGORIES and leave existing docs alone: they resolve
|
|
18
|
+
* to `undefined` and degrade into the "Other" bucket rather than breaking.
|
|
19
|
+
*/
|
|
20
|
+
export var CalculatorCategoryId;
|
|
21
|
+
(function (CalculatorCategoryId) {
|
|
22
|
+
CalculatorCategoryId["AreaAndVolume"] = "areaAndVolume";
|
|
23
|
+
CalculatorCategoryId["Concrete"] = "concrete";
|
|
24
|
+
CalculatorCategoryId["Electrical"] = "electrical";
|
|
25
|
+
CalculatorCategoryId["Stairs"] = "stairs";
|
|
26
|
+
CalculatorCategoryId["Framing"] = "framing";
|
|
27
|
+
CalculatorCategoryId["FinishCarpentry"] = "finishCarpentry";
|
|
28
|
+
CalculatorCategoryId["Hardscaping"] = "hardscaping";
|
|
29
|
+
CalculatorCategoryId["Roofing"] = "roofing";
|
|
30
|
+
CalculatorCategoryId["HVAC"] = "hvac";
|
|
31
|
+
CalculatorCategoryId["Landscaping"] = "landscaping";
|
|
32
|
+
CalculatorCategoryId["MaterialEstimator"] = "materialEstimator";
|
|
33
|
+
CalculatorCategoryId["Optimizers"] = "optimizers";
|
|
34
|
+
CalculatorCategoryId["Plumbing"] = "plumbing";
|
|
35
|
+
})(CalculatorCategoryId || (CalculatorCategoryId = {}));
|
|
36
|
+
/**
|
|
37
|
+
* Canonical order, matching the design's tile grid.
|
|
38
|
+
*
|
|
39
|
+
* This array IS the ordering — there is deliberately no `sortIndex` field for
|
|
40
|
+
* it to drift out of sync with.
|
|
41
|
+
*/
|
|
42
|
+
export const CALCULATOR_CATEGORIES = [
|
|
43
|
+
{
|
|
44
|
+
id: CalculatorCategoryId.AreaAndVolume,
|
|
45
|
+
name: 'Area and Volume',
|
|
46
|
+
color: '#FF3A6E',
|
|
47
|
+
glyph: 'shape_calculator',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: CalculatorCategoryId.Concrete,
|
|
51
|
+
name: 'Concrete',
|
|
52
|
+
color: '#E2AB6B',
|
|
53
|
+
glyph: 'concrete',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: CalculatorCategoryId.Electrical,
|
|
57
|
+
name: 'Electrical',
|
|
58
|
+
color: '#60D8FE',
|
|
59
|
+
glyph: 'electrical',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: CalculatorCategoryId.Stairs,
|
|
63
|
+
name: 'Stairs',
|
|
64
|
+
color: '#D09C9C',
|
|
65
|
+
glyph: 'stair_tread',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: CalculatorCategoryId.Framing,
|
|
69
|
+
name: 'Framing',
|
|
70
|
+
color: '#FABC61',
|
|
71
|
+
glyph: 'frame',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: CalculatorCategoryId.FinishCarpentry,
|
|
75
|
+
name: 'Finish Carpentry',
|
|
76
|
+
color: '#2E9E82',
|
|
77
|
+
glyph: 'finish_carpentry',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: CalculatorCategoryId.Hardscaping,
|
|
81
|
+
name: 'Hardscaping',
|
|
82
|
+
color: '#6B77E2',
|
|
83
|
+
glyph: 'hardscaping',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: CalculatorCategoryId.Roofing,
|
|
87
|
+
name: 'Roofing',
|
|
88
|
+
color: '#E48787',
|
|
89
|
+
glyph: 'roofing',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: CalculatorCategoryId.HVAC,
|
|
93
|
+
name: 'HVAC',
|
|
94
|
+
color: '#FEF260',
|
|
95
|
+
glyph: 'hvac',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: CalculatorCategoryId.Landscaping,
|
|
99
|
+
name: 'Landscaping',
|
|
100
|
+
color: '#C892FF',
|
|
101
|
+
glyph: 'landscaping',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: CalculatorCategoryId.MaterialEstimator,
|
|
105
|
+
name: 'Material Estimator',
|
|
106
|
+
color: '#FFAD00',
|
|
107
|
+
glyph: 'raw_materials',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: CalculatorCategoryId.Optimizers,
|
|
111
|
+
name: 'Optimizers',
|
|
112
|
+
color: '#CA5726',
|
|
113
|
+
glyph: 'wall',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: CalculatorCategoryId.Plumbing,
|
|
117
|
+
name: 'Plumbing',
|
|
118
|
+
color: '#008EFD',
|
|
119
|
+
glyph: 'plumbing',
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
export const CALCULATOR_CATEGORY_BY_ID = Object.fromEntries(CALCULATOR_CATEGORIES.map((category) => [category.id, category]));
|
|
123
|
+
/**
|
|
124
|
+
* Bucket key / route segment for calculators with no category, or with one
|
|
125
|
+
* that no longer exists. NOT a CalculatorCategoryId — guarded by a unit test,
|
|
126
|
+
* because a collision would silently swallow a real category.
|
|
127
|
+
*/
|
|
128
|
+
export const UNCATEGORIZED_CATEGORY_ID = 'uncategorized';
|
|
129
|
+
export const UNCATEGORIZED_CATEGORY_NAME = 'Other';
|
|
130
|
+
export const isCalculatorCategoryId = (value) => typeof value === 'string' && value in CALCULATOR_CATEGORY_BY_ID;
|
|
131
|
+
/**
|
|
132
|
+
* Resolve a stored category value. Returns undefined for missing, null, and
|
|
133
|
+
* retired ids — callers should treat all three as "Other" rather than trusting
|
|
134
|
+
* the raw field.
|
|
135
|
+
*/
|
|
136
|
+
export const findCalculatorCategory = (value) => isCalculatorCategoryId(value) ? CALCULATOR_CATEGORY_BY_ID[value] : undefined;
|
|
137
|
+
export const calculatorCategoryOf = (calculator) => findCalculatorCategory(calculator.category);
|
|
138
|
+
/** Display label for a stored value, falling back to "Other". */
|
|
139
|
+
export const calculatorCategoryName = (value) => findCalculatorCategory(value)?.name ?? UNCATEGORIZED_CATEGORY_NAME;
|
|
140
|
+
/**
|
|
141
|
+
* Group calculators by category id, collecting anything missing or
|
|
142
|
+
* unresolvable under UNCATEGORIZED_CATEGORY_ID.
|
|
143
|
+
*
|
|
144
|
+
* Keys exist only for non-empty buckets, which is what lets the mobile grid
|
|
145
|
+
* hide empty categories without a second pass.
|
|
146
|
+
*/
|
|
147
|
+
export const groupCalculatorsByCategory = (calculators) => {
|
|
148
|
+
const buckets = new Map();
|
|
149
|
+
for (const calculator of calculators) {
|
|
150
|
+
const key = isCalculatorCategoryId(calculator.category)
|
|
151
|
+
? calculator.category
|
|
152
|
+
: UNCATEGORIZED_CATEGORY_ID;
|
|
153
|
+
const bucket = buckets.get(key);
|
|
154
|
+
if (bucket)
|
|
155
|
+
bucket.push(calculator);
|
|
156
|
+
else
|
|
157
|
+
buckets.set(key, [calculator]);
|
|
158
|
+
}
|
|
159
|
+
return buckets;
|
|
160
|
+
};
|
package/dist/calculator/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// definition validation. Pure logic (mathjs + zod only — no Skia/React), safe
|
|
4
4
|
// on web, native, and Node.
|
|
5
5
|
export * from './schema.js';
|
|
6
|
+
export * from './categories.js';
|
|
6
7
|
export * from './units.js';
|
|
7
8
|
export * from './evaluate.js';
|
|
8
9
|
export * from './solve.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ColumnType, type ColumnConfig, type ConversionTableColumnData, type InstructionsColumnData, type SelectColumnData, type DecimalTolerance, type FractionalTolerance } from '../types/firestore.js';
|
|
2
2
|
import type { AngleUnit, CalculatorUnit, FieldDimension, MeasurementDimension } from './units.js';
|
|
3
|
+
import type { CalculatorCategoryId } from './categories.js';
|
|
3
4
|
/**
|
|
4
5
|
* Version of the definition format itself. Bumped only on breaking schema
|
|
5
6
|
* changes; independent of the npm package version and of each calculator's
|
|
@@ -123,6 +124,17 @@ export interface CalculatorDefinition {
|
|
|
123
124
|
name: string;
|
|
124
125
|
description: string;
|
|
125
126
|
folderId: string | null;
|
|
127
|
+
/**
|
|
128
|
+
* Hard-coded category (see categories.ts) driving the mobile category grid
|
|
129
|
+
* and the web library badge.
|
|
130
|
+
*
|
|
131
|
+
* AUTHORING METADATA ONLY — never needed to run a calculator, so it stays
|
|
132
|
+
* optional and nullable: docs authored before categories existed have no
|
|
133
|
+
* value and must keep validating. Always resolve through
|
|
134
|
+
* findCalculatorCategory() rather than comparing the raw field, so a retired
|
|
135
|
+
* id degrades to "Other" instead of vanishing.
|
|
136
|
+
*/
|
|
137
|
+
category?: CalculatorCategoryId | null;
|
|
126
138
|
/** Inputs + outputs in one list, discriminated by `role`. */
|
|
127
139
|
fields: CalculatorField[];
|
|
128
140
|
equations: CalculatorEquation[];
|
|
@@ -122,6 +122,9 @@ export const createEmptyCalculatorDefinition = (id) => ({
|
|
|
122
122
|
name: '',
|
|
123
123
|
description: '',
|
|
124
124
|
folderId: null,
|
|
125
|
+
// `null`, never undefined — this object is spread straight into setDoc and
|
|
126
|
+
// Firestore rejects undefined field values.
|
|
127
|
+
category: null,
|
|
125
128
|
fields: [],
|
|
126
129
|
equations: [],
|
|
127
130
|
diagramFileId: null,
|
|
@@ -7,6 +7,7 @@ export declare const calculatorDefinitionSchema: z.ZodObject<{
|
|
|
7
7
|
name: z.ZodString;
|
|
8
8
|
description: z.ZodString;
|
|
9
9
|
folderId: z.ZodNullable<z.ZodString>;
|
|
10
|
+
category: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
10
11
|
fields: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
11
12
|
kind: z.ZodLiteral<ColumnType.Number>;
|
|
12
13
|
defaultValue: z.ZodOptional<z.ZodNumber>;
|
|
@@ -3,6 +3,7 @@ import { create, all } from 'mathjs';
|
|
|
3
3
|
import { ColumnType, DecimalTolerance, FractionalTolerance, } from '../types/firestore.js';
|
|
4
4
|
import { CALCULATOR_SCHEMA_VERSION, equationForField, isEquationTargetKind, isNumericFieldKind, } from './schema.js';
|
|
5
5
|
import { CALCULATOR_UNIT_INFO, unitDimension } from './units.js';
|
|
6
|
+
import { isCalculatorCategoryId } from './categories.js';
|
|
6
7
|
const math = create(all);
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Definition validation: zod for structure, plus the cross-field invariants
|
|
@@ -101,6 +102,14 @@ export const calculatorDefinitionSchema = z.object({
|
|
|
101
102
|
name: z.string(),
|
|
102
103
|
description: z.string(),
|
|
103
104
|
folderId: z.string().nullable(),
|
|
105
|
+
// Deliberately z.string(), NOT z.enum(CalculatorCategoryId): a safeParse
|
|
106
|
+
// failure returns early below, so a retired or typo'd id would suppress
|
|
107
|
+
// every other invariant and mark the whole calculator broken. Unknown ids
|
|
108
|
+
// are reported further down as an advisory warning instead.
|
|
109
|
+
//
|
|
110
|
+
// The key must be listed even though it's optional — zod strips unknown
|
|
111
|
+
// keys, so omitting it here would make the check below never fire.
|
|
112
|
+
category: z.string().nullish(),
|
|
104
113
|
fields: z.array(calculatorFieldSchema),
|
|
105
114
|
equations: z.array(calculatorEquationSchema),
|
|
106
115
|
diagramFileId: z.string().nullable().optional(),
|
|
@@ -187,6 +196,12 @@ export const validateCalculatorDefinition = (input) => {
|
|
|
187
196
|
if (def.schemaVersion > CALCULATOR_SCHEMA_VERSION) {
|
|
188
197
|
push('schema-version-unsupported', 'schemaVersion', `schemaVersion ${def.schemaVersion} is newer than this client supports (${CALCULATOR_SCHEMA_VERSION})`);
|
|
189
198
|
}
|
|
199
|
+
// A category the client doesn't know is a labelling problem, not a broken
|
|
200
|
+
// calculator — an older app build reading a newly-added category must still
|
|
201
|
+
// run it. Warning severity keeps `ok` true.
|
|
202
|
+
if (def.category != null && !isCalculatorCategoryId(def.category)) {
|
|
203
|
+
push('unknown-category', 'category', `Category "${def.category}" is not a known calculator category`, 'warning');
|
|
204
|
+
}
|
|
190
205
|
// --- Unique ids ---------------------------------------------------------
|
|
191
206
|
const fieldIds = new Set();
|
|
192
207
|
def.fields.forEach((field, i) => {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type DecimalTolerance, type Formula, type FractionalTolerance, type Group, type Measurement, type Section, type Units } from '../types/firestore.js';
|
|
2
|
+
import type { ExportConfig } from './config.js';
|
|
3
|
+
export type ExportScope = 'all' | 'activeGroup';
|
|
4
|
+
export type ExportObjectRow = {
|
|
5
|
+
name: string;
|
|
6
|
+
url: string | null;
|
|
7
|
+
};
|
|
8
|
+
export type BuildJobCsvInput = {
|
|
9
|
+
scope: ExportScope;
|
|
10
|
+
activeGroupId?: string | null;
|
|
11
|
+
config: ExportConfig;
|
|
12
|
+
groups: Group[];
|
|
13
|
+
sections: Section[];
|
|
14
|
+
formulas: Formula[];
|
|
15
|
+
measurementsByGroup: Map<string, Measurement[]>;
|
|
16
|
+
objectsByGroup: Map<string, ExportObjectRow[]>;
|
|
17
|
+
formatMeasurementValue: (value: number, device?: string) => string;
|
|
18
|
+
unitPrefs: {
|
|
19
|
+
defaultUnit?: Units;
|
|
20
|
+
fractionalTolerance?: FractionalTolerance;
|
|
21
|
+
decimalTolerance?: DecimalTolerance;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export declare const escapeCSVField: (field: string) => string;
|
|
25
|
+
export declare const hyperlinkCell: (url: string, label: string) => string;
|
|
26
|
+
export declare function buildJobCsv(input: BuildJobCsvInput): string;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { ColumnType, GroupType, } from '../types/firestore.js';
|
|
2
|
+
import { calculateFormula } from '../formulas/calculateFormula.js';
|
|
3
|
+
import { formatSelectedValues } from '../utils/selectValues.js';
|
|
4
|
+
import { isDefaultGroup, isFormGroupCompleted } from '../utils/groups.js';
|
|
5
|
+
export const escapeCSVField = (field) => `"${field.replace(/"/g, '""')}"`;
|
|
6
|
+
// Spreadsheet-formula hyperlink cell. Two escaping layers: quotes inside the
|
|
7
|
+
// formula's string literals are doubled (Excel/Sheets escaping), then the
|
|
8
|
+
// whole formula is CSV-escaped.
|
|
9
|
+
export const hyperlinkCell = (url, label) => escapeCSVField(`=HYPERLINK("${url.replace(/"/g, '""')}","${label.replace(/"/g, '""')}")`);
|
|
10
|
+
const objectCell = (obj) => obj.url ? hyperlinkCell(obj.url, obj.name) : escapeCSVField(obj.name);
|
|
11
|
+
// Pure CSV assembly for the job export, shared by rock-pro-app and
|
|
12
|
+
// rock-desktop so the two platforms emit identical files. With no objects
|
|
13
|
+
// selected and all measurement-data columns on, the measurement sections are
|
|
14
|
+
// identical to the legacy exportCSV output; objects add a trailing "Objects"
|
|
15
|
+
// column (list groups) or extend the form row (form groups), one skipped
|
|
16
|
+
// column after the existing cells.
|
|
17
|
+
export function buildJobCsv(input) {
|
|
18
|
+
const { scope, activeGroupId, config, groups, sections, formulas, measurementsByGroup, objectsByGroup, formatMeasurementValue, unitPrefs, } = input;
|
|
19
|
+
const csvRows = [];
|
|
20
|
+
let listGroups;
|
|
21
|
+
let formGroups;
|
|
22
|
+
if (scope === 'activeGroup') {
|
|
23
|
+
const active = groups.find((g) => g.id === activeGroupId);
|
|
24
|
+
listGroups = active?.type === GroupType.LIST ? [active] : [];
|
|
25
|
+
formGroups = active?.type === GroupType.FORM ? [active] : [];
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
listGroups = groups.filter((g) => g.type === GroupType.LIST &&
|
|
29
|
+
config.categories.includes(isDefaultGroup(g) ? 'defaultGroup' : 'groups'));
|
|
30
|
+
formGroups = config.categories.includes('forms')
|
|
31
|
+
? groups.filter((g) => g.type === GroupType.FORM)
|
|
32
|
+
: [];
|
|
33
|
+
}
|
|
34
|
+
const measurementPassesFilter = (m) => config.measurements === 'all' ||
|
|
35
|
+
(config.measurements === 'complete'
|
|
36
|
+
? m.isCompleted === true
|
|
37
|
+
: m.isCompleted !== true);
|
|
38
|
+
const includeField = (field) => config.measurementData.includes(field);
|
|
39
|
+
// --- List Groups ---
|
|
40
|
+
if (listGroups.length > 0) {
|
|
41
|
+
csvRows.push('List Groups');
|
|
42
|
+
csvRows.push('');
|
|
43
|
+
listGroups.forEach((group) => {
|
|
44
|
+
csvRows.push(`Group: ${group.name}`);
|
|
45
|
+
const objects = objectsByGroup.get(group.id) ?? [];
|
|
46
|
+
const headerCells = [
|
|
47
|
+
'',
|
|
48
|
+
...(includeField('measurement') ? ['Measurement'] : []),
|
|
49
|
+
...(includeField('label') ? ['Label'] : []),
|
|
50
|
+
...(includeField('notes') ? ['Notes'] : []),
|
|
51
|
+
].map(escapeCSVField);
|
|
52
|
+
if (objects.length > 0) {
|
|
53
|
+
headerCells.push(escapeCSVField(''), escapeCSVField('Objects'));
|
|
54
|
+
}
|
|
55
|
+
csvRows.push(headerCells.join(','));
|
|
56
|
+
const measurements = (measurementsByGroup.get(group.id) ?? []).filter(measurementPassesFilter);
|
|
57
|
+
// Objects fill a parallel column, one per row from the top; extra rows
|
|
58
|
+
// are emitted when a group has more objects than measurements.
|
|
59
|
+
const rowCount = Math.max(measurements.length, objects.length);
|
|
60
|
+
for (let i = 0; i < rowCount; i++) {
|
|
61
|
+
const measurement = measurements[i];
|
|
62
|
+
const row = [escapeCSVField('')];
|
|
63
|
+
if (includeField('measurement')) {
|
|
64
|
+
row.push(escapeCSVField(measurement
|
|
65
|
+
? formatMeasurementValue(measurement.value, measurement.device)
|
|
66
|
+
: ''));
|
|
67
|
+
}
|
|
68
|
+
if (includeField('label')) {
|
|
69
|
+
row.push(escapeCSVField(measurement?.label || ''));
|
|
70
|
+
}
|
|
71
|
+
if (includeField('notes')) {
|
|
72
|
+
row.push(escapeCSVField(measurement?.note || ''));
|
|
73
|
+
}
|
|
74
|
+
const obj = objects[i];
|
|
75
|
+
if (obj) {
|
|
76
|
+
row.push(escapeCSVField(''), objectCell(obj));
|
|
77
|
+
}
|
|
78
|
+
csvRows.push(row.join(','));
|
|
79
|
+
}
|
|
80
|
+
csvRows.push('');
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// --- Form Groups ---
|
|
84
|
+
if (formGroups.length > 0) {
|
|
85
|
+
csvRows.push('Form Groups');
|
|
86
|
+
csvRows.push('');
|
|
87
|
+
const sectionIds = [...new Set(formGroups.map((g) => g.sectionId))];
|
|
88
|
+
sectionIds.forEach((sectionId) => {
|
|
89
|
+
const section = sections.find((s) => s.id === sectionId);
|
|
90
|
+
if (!section)
|
|
91
|
+
return;
|
|
92
|
+
const sectionGroups = formGroups.filter((g) => g.sectionId === sectionId);
|
|
93
|
+
if (sectionGroups.length === 0)
|
|
94
|
+
return;
|
|
95
|
+
csvRows.push(`Section: ${section.name}`);
|
|
96
|
+
csvRows.push('');
|
|
97
|
+
const columnHeaders = [];
|
|
98
|
+
section.tableConfig.forEach((column) => {
|
|
99
|
+
columnHeaders.push(column.name);
|
|
100
|
+
if (column.type === ColumnType.Measurement ||
|
|
101
|
+
column.type === ColumnType.Angle) {
|
|
102
|
+
columnHeaders.push(`${column.name} Notes`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
csvRows.push(['Form Name', ...columnHeaders].map(escapeCSVField).join(','));
|
|
106
|
+
sectionGroups.forEach((group) => {
|
|
107
|
+
// The completeness filter drops whole rows (their objects with
|
|
108
|
+
// them); cell references below still resolve against the group's
|
|
109
|
+
// full measurement list.
|
|
110
|
+
if (config.measurements !== 'all') {
|
|
111
|
+
const completed = isFormGroupCompleted(group, section);
|
|
112
|
+
if (config.measurements === 'complete' ? !completed : completed) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const row = [];
|
|
117
|
+
section.tableConfig.forEach((column) => {
|
|
118
|
+
if (column.type === ColumnType.Formula) {
|
|
119
|
+
const formula = formulas.find((f) => f.id === column.columnData?.formulaId);
|
|
120
|
+
if (formula) {
|
|
121
|
+
try {
|
|
122
|
+
const result = calculateFormula(formula, formulas, group.columns, section.tableConfig, measurementsByGroup.get(group.id) ?? [], unitPrefs.defaultUnit, unitPrefs.fractionalTolerance, unitPrefs.decimalTolerance);
|
|
123
|
+
if (result !== null &&
|
|
124
|
+
result !== undefined &&
|
|
125
|
+
typeof result === 'number') {
|
|
126
|
+
row.push(escapeCSVField(formatMeasurementValue(result)));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
row.push(escapeCSVField(result !== null && result !== undefined
|
|
130
|
+
? String(result)
|
|
131
|
+
: ''));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
row.push('');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
row.push('');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (column.type === ColumnType.Angle) {
|
|
143
|
+
const measurementId = group.columns[column.id];
|
|
144
|
+
const measurements = measurementsByGroup.get(group.id);
|
|
145
|
+
const measurement = measurements?.find((m) => m.id === measurementId);
|
|
146
|
+
if (measurement) {
|
|
147
|
+
row.push(escapeCSVField(`${measurement.value}°`));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
row.push(escapeCSVField(formatSelectedValues(group.columns[column.id])));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (column.id in group.columns) {
|
|
154
|
+
const measurementId = group.columns[column.id];
|
|
155
|
+
const measurements = measurementsByGroup.get(group.id);
|
|
156
|
+
const measurement = measurements?.find((m) => m.id === measurementId);
|
|
157
|
+
if (measurement) {
|
|
158
|
+
row.push(escapeCSVField(formatMeasurementValue(measurement.value, measurement.device)));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
row.push(escapeCSVField(formatSelectedValues(group.columns[column.id])));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
row.push('');
|
|
166
|
+
}
|
|
167
|
+
if (column.type === ColumnType.Measurement ||
|
|
168
|
+
column.type === ColumnType.Angle) {
|
|
169
|
+
if (group.descriptions && group.descriptions[column.id]) {
|
|
170
|
+
row.push(escapeCSVField(group.descriptions[column.id]));
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
row.push('');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
// Objects extend the row past the table, one skipped column after
|
|
178
|
+
// the last cell.
|
|
179
|
+
const objects = objectsByGroup.get(group.id) ?? [];
|
|
180
|
+
if (objects.length > 0) {
|
|
181
|
+
row.push(escapeCSVField(''), ...objects.map(objectCell));
|
|
182
|
+
}
|
|
183
|
+
csvRows.push([escapeCSVField(group.name), ...row].join(','));
|
|
184
|
+
});
|
|
185
|
+
csvRows.push('');
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return csvRows.join('\n');
|
|
189
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type ExportCategory = 'defaultGroup' | 'groups' | 'forms';
|
|
2
|
+
export type ExportObjectType = 'calculators' | 'files' | 'photos' | 'cad' | 'labels' | 'notes';
|
|
3
|
+
export type ExportMeasurementFilter = 'all' | 'complete' | 'incomplete';
|
|
4
|
+
export type ExportImageFormat = 'hyperlink' | 'none';
|
|
5
|
+
export type ExportMeasurementDataField = 'measurement' | 'label' | 'notes';
|
|
6
|
+
export type ExportConfig = {
|
|
7
|
+
categories: ExportCategory[];
|
|
8
|
+
objects: ExportObjectType[];
|
|
9
|
+
measurements: ExportMeasurementFilter;
|
|
10
|
+
imageFormat: ExportImageFormat;
|
|
11
|
+
measurementData: ExportMeasurementDataField[];
|
|
12
|
+
};
|
|
13
|
+
export declare const ALL_EXPORT_CATEGORIES: ExportCategory[];
|
|
14
|
+
export declare const ALL_EXPORT_OBJECT_TYPES: ExportObjectType[];
|
|
15
|
+
export declare const ALL_EXPORT_MEASUREMENT_DATA_FIELDS: ExportMeasurementDataField[];
|
|
16
|
+
export declare const DEFAULT_EXPORT_CONFIG: ExportConfig;
|
|
17
|
+
export declare const EXPORT_EVERYTHING_CONFIG: ExportConfig;
|
|
18
|
+
export declare function normalizeExportConfig(raw: unknown): ExportConfig;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Shared model for the configurable job CSV export (rock-pro-app +
|
|
2
|
+
// rock-desktop). The config is persisted on the user doc (`exportConfig`) so
|
|
3
|
+
// it follows the user across devices and platforms; both apps must therefore
|
|
4
|
+
// read/write the same shape and tolerate values written by the other.
|
|
5
|
+
export const ALL_EXPORT_CATEGORIES = [
|
|
6
|
+
'defaultGroup',
|
|
7
|
+
'groups',
|
|
8
|
+
'forms',
|
|
9
|
+
];
|
|
10
|
+
export const ALL_EXPORT_OBJECT_TYPES = [
|
|
11
|
+
'calculators',
|
|
12
|
+
'files',
|
|
13
|
+
'photos',
|
|
14
|
+
'cad',
|
|
15
|
+
'labels',
|
|
16
|
+
'notes',
|
|
17
|
+
];
|
|
18
|
+
export const ALL_EXPORT_MEASUREMENT_DATA_FIELDS = ['measurement', 'label', 'notes'];
|
|
19
|
+
// Objects are opt-in: nothing ticked and images excluded until the user
|
|
20
|
+
// configures otherwise, so a default export is measurements-only.
|
|
21
|
+
export const DEFAULT_EXPORT_CONFIG = {
|
|
22
|
+
categories: [...ALL_EXPORT_CATEGORIES],
|
|
23
|
+
objects: [],
|
|
24
|
+
measurements: 'all',
|
|
25
|
+
imageFormat: 'none',
|
|
26
|
+
measurementData: [...ALL_EXPORT_MEASUREMENT_DATA_FIELDS],
|
|
27
|
+
};
|
|
28
|
+
// "Export All" (everything, ignoring the saved config): every category, every
|
|
29
|
+
// object kind as a hyperlink, all measurements, all list columns.
|
|
30
|
+
export const EXPORT_EVERYTHING_CONFIG = {
|
|
31
|
+
categories: [...ALL_EXPORT_CATEGORIES],
|
|
32
|
+
objects: [...ALL_EXPORT_OBJECT_TYPES],
|
|
33
|
+
measurements: 'all',
|
|
34
|
+
imageFormat: 'hyperlink',
|
|
35
|
+
measurementData: [...ALL_EXPORT_MEASUREMENT_DATA_FIELDS],
|
|
36
|
+
};
|
|
37
|
+
const isCategory = (v) => typeof v === 'string' && ALL_EXPORT_CATEGORIES.includes(v);
|
|
38
|
+
const isObjectType = (v) => typeof v === 'string' && ALL_EXPORT_OBJECT_TYPES.includes(v);
|
|
39
|
+
const isMeasurementFilter = (v) => v === 'all' || v === 'complete' || v === 'incomplete';
|
|
40
|
+
const isImageFormat = (v) => v === 'hyperlink' || v === 'none';
|
|
41
|
+
const isMeasurementDataField = (v) => typeof v === 'string' &&
|
|
42
|
+
ALL_EXPORT_MEASUREMENT_DATA_FIELDS.includes(v);
|
|
43
|
+
// Tolerant reader for the raw user-doc field. Missing/invalid fields fall
|
|
44
|
+
// back to defaults and unknown array members are dropped. Empty `categories`
|
|
45
|
+
// or `measurementData` fall back to all — the UIs can't save those, so treat
|
|
46
|
+
// them as corrupt. `objects` defaults to none (objects are opt-in), so
|
|
47
|
+
// missing and empty read the same.
|
|
48
|
+
export function normalizeExportConfig(raw) {
|
|
49
|
+
const r = raw && typeof raw === 'object' ? raw : {};
|
|
50
|
+
const categories = Array.isArray(r.categories)
|
|
51
|
+
? r.categories.filter(isCategory)
|
|
52
|
+
: [];
|
|
53
|
+
const objects = Array.isArray(r.objects)
|
|
54
|
+
? r.objects.filter(isObjectType)
|
|
55
|
+
: [];
|
|
56
|
+
const measurementData = Array.isArray(r.measurementData)
|
|
57
|
+
? r.measurementData.filter(isMeasurementDataField)
|
|
58
|
+
: [];
|
|
59
|
+
return {
|
|
60
|
+
categories: categories.length > 0 ? categories : [...ALL_EXPORT_CATEGORIES],
|
|
61
|
+
objects,
|
|
62
|
+
measurements: isMeasurementFilter(r.measurements)
|
|
63
|
+
? r.measurements
|
|
64
|
+
: DEFAULT_EXPORT_CONFIG.measurements,
|
|
65
|
+
imageFormat: isImageFormat(r.imageFormat)
|
|
66
|
+
? r.imageFormat
|
|
67
|
+
: DEFAULT_EXPORT_CONFIG.imageFormat,
|
|
68
|
+
measurementData: measurementData.length > 0
|
|
69
|
+
? measurementData
|
|
70
|
+
: [...ALL_EXPORT_MEASUREMENT_DATA_FIELDS],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type FileUpload } from '../types/firestore.js';
|
|
2
|
+
export type DesktopLinkContext = {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
orgId: string;
|
|
5
|
+
projectId: string;
|
|
6
|
+
jobId: string;
|
|
7
|
+
groupId: string;
|
|
8
|
+
isDefaultGroup: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function desktopBaseUrlForFirebaseProject(firebaseProjectId: string | undefined): string;
|
|
11
|
+
export declare function desktopFileEditorUrl(file: FileUpload, ctx: DesktopLinkContext): string | null;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { FileUploadType } from '../types/firestore.js';
|
|
2
|
+
// Maps the running mobile app's Firebase project onto the desktop
|
|
3
|
+
// deployment. Dev builds target a local rock-desktop dev server (`yarn dev`,
|
|
4
|
+
// Vite's default port) for testing.
|
|
5
|
+
export function desktopBaseUrlForFirebaseProject(firebaseProjectId) {
|
|
6
|
+
switch (firebaseProjectId) {
|
|
7
|
+
case 'rock-production-pro':
|
|
8
|
+
return 'https://app.boldrpro.com';
|
|
9
|
+
case 'rock-staging-pro':
|
|
10
|
+
return 'https://staging.app.boldrpro.com';
|
|
11
|
+
default:
|
|
12
|
+
return 'http://localhost:5173';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// Returns the desktop editor URL for an exported group file, or null when
|
|
16
|
+
// desktop has no editor for it (labels render inert there) — the CSV cell
|
|
17
|
+
// then degrades to the plain object name.
|
|
18
|
+
export function desktopFileEditorUrl(file, ctx) {
|
|
19
|
+
const { baseUrl, orgId, projectId, jobId, groupId, isDefaultGroup } = ctx;
|
|
20
|
+
const jobPath = `${baseUrl}/app/organizations/${orgId}/projects/${projectId}/jobs/${jobId}`;
|
|
21
|
+
const groupQuery = `groupId=${encodeURIComponent(groupId)}`;
|
|
22
|
+
switch (file.type) {
|
|
23
|
+
case FileUploadType.LayoutGroup:
|
|
24
|
+
return `${jobPath}/layout-group/${file.id}?${groupQuery}`;
|
|
25
|
+
case FileUploadType.Note:
|
|
26
|
+
return `${jobPath}/notes/${file.id}?${groupQuery}`;
|
|
27
|
+
case FileUploadType.Canvas:
|
|
28
|
+
return `${jobPath}/canvas/${file.id}?${groupQuery}`;
|
|
29
|
+
case FileUploadType.Annotation: {
|
|
30
|
+
// Pass a top-level fileType through when the doc carries one (desktop
|
|
31
|
+
// uses it to pick the 3D viewer); omit otherwise — the viewer defaults
|
|
32
|
+
// to the PDF path.
|
|
33
|
+
const rawFileType = file.fileType;
|
|
34
|
+
const fileTypeQuery = rawFileType
|
|
35
|
+
? `&fileType=${encodeURIComponent(rawFileType)}`
|
|
36
|
+
: '';
|
|
37
|
+
const defaultQuery = isDefaultGroup ? '&isDefault=true' : '';
|
|
38
|
+
return `${jobPath}/viewer?fileId=${encodeURIComponent(file.id)}&${groupQuery}${fileTypeQuery}${defaultQuery}`;
|
|
39
|
+
}
|
|
40
|
+
default:
|
|
41
|
+
// Labels (no desktop editor) and anything classifyFileForExport should
|
|
42
|
+
// already have excluded.
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { FileUploadType } from '../types/firestore.js';
|
|
2
|
+
// Maps a group file onto the export config's object kinds. `null` means the
|
|
3
|
+
// file is never exported: Calculator is a config placeholder for now (the
|
|
4
|
+
// checkbox persists but nothing exports until calculator export ships),
|
|
5
|
+
// Conversion/Template/Background are internal utility docs, and Annotation
|
|
6
|
+
// files flagged `isLabel` are label-print artifacts hidden from group grids.
|
|
7
|
+
export function classifyFileForExport(file) {
|
|
8
|
+
switch (file.type) {
|
|
9
|
+
case FileUploadType.LayoutGroup:
|
|
10
|
+
return 'cad';
|
|
11
|
+
case FileUploadType.Note:
|
|
12
|
+
return 'notes';
|
|
13
|
+
case FileUploadType.Label:
|
|
14
|
+
return 'labels';
|
|
15
|
+
case FileUploadType.Canvas:
|
|
16
|
+
return 'photos';
|
|
17
|
+
case FileUploadType.Annotation: {
|
|
18
|
+
const fileData = file.fileData;
|
|
19
|
+
if (fileData?.isLabel)
|
|
20
|
+
return null;
|
|
21
|
+
// Legacy Nutrient sketches count as photos for filtering, but still
|
|
22
|
+
// open in the document viewer (they have no Skia canvas state).
|
|
23
|
+
return fileData?.fileType === 'sketch' ? 'photos' : 'files';
|
|
24
|
+
}
|
|
25
|
+
default:
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/dist/exports.d.ts
CHANGED
|
@@ -7,7 +7,11 @@ export * from './types/firestore.js';
|
|
|
7
7
|
export * from './types/layout.js';
|
|
8
8
|
export * from './types/annotation.js';
|
|
9
9
|
export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, type ToleranceThreshold, type ToleranceConfig, } from './utils/tolerance.js';
|
|
10
|
-
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, } from './utils/groups.js';
|
|
10
|
+
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
|
|
11
|
+
export { ALL_EXPORT_CATEGORIES, ALL_EXPORT_MEASUREMENT_DATA_FIELDS, ALL_EXPORT_OBJECT_TYPES, DEFAULT_EXPORT_CONFIG, EXPORT_EVERYTHING_CONFIG, normalizeExportConfig, type ExportCategory, type ExportConfig, type ExportImageFormat, type ExportMeasurementDataField, type ExportMeasurementFilter, type ExportObjectType, } from './export/config.js';
|
|
12
|
+
export { classifyFileForExport } from './export/objectKinds.js';
|
|
13
|
+
export { buildJobCsv, escapeCSVField, hyperlinkCell, type BuildJobCsvInput, type ExportObjectRow, type ExportScope, } from './export/buildJobCsv.js';
|
|
14
|
+
export { desktopBaseUrlForFirebaseProject, desktopFileEditorUrl, type DesktopLinkContext, } from './export/desktopLinks.js';
|
|
11
15
|
export { numberToLetterIndex, formGroupInputLabel, listGroupMeasurementLabel, } from './utils/indexLabels.js';
|
|
12
16
|
export { toSelectedValues, formatSelectedValues, toStoredValue, } from './utils/selectValues.js';
|
|
13
17
|
export * from './calculator/index.js';
|
package/dist/exports.js
CHANGED
|
@@ -11,7 +11,13 @@ export * from './types/firestore.js';
|
|
|
11
11
|
export * from './types/layout.js';
|
|
12
12
|
export * from './types/annotation.js';
|
|
13
13
|
export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, } from './utils/tolerance.js';
|
|
14
|
-
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, } from './utils/groups.js';
|
|
14
|
+
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
|
|
15
|
+
// Configurable job CSV export, shared by rock-pro-app and rock-desktop so
|
|
16
|
+
// both platforms emit identical files.
|
|
17
|
+
export { ALL_EXPORT_CATEGORIES, ALL_EXPORT_MEASUREMENT_DATA_FIELDS, ALL_EXPORT_OBJECT_TYPES, DEFAULT_EXPORT_CONFIG, EXPORT_EVERYTHING_CONFIG, normalizeExportConfig, } from './export/config.js';
|
|
18
|
+
export { classifyFileForExport } from './export/objectKinds.js';
|
|
19
|
+
export { buildJobCsv, escapeCSVField, hyperlinkCell, } from './export/buildJobCsv.js';
|
|
20
|
+
export { desktopBaseUrlForFirebaseProject, desktopFileEditorUrl, } from './export/desktopLinks.js';
|
|
15
21
|
export { numberToLetterIndex, formGroupInputLabel, listGroupMeasurementLabel, } from './utils/indexLabels.js';
|
|
16
22
|
export { toSelectedValues, formatSelectedValues, toStoredValue, } from './utils/selectValues.js';
|
|
17
23
|
// Construction Calculator shared module (schema, units, evaluator, solver,
|
package/dist/utils/groups.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Group, type Section } from '../types/firestore.js';
|
|
2
2
|
export declare const DEFAULT_GROUP_INDEX = 0;
|
|
3
3
|
export declare const isDefaultGroup: (group: Pick<Group, "groupIndex"> | null | undefined) => boolean;
|
|
4
4
|
export declare const findDefaultGroup: <T extends Pick<Group, "groupIndex">>(groups: T[] | null | undefined) => T | null;
|
|
5
|
+
export declare const isFormGroupCompleted: (group: Group, section: Section) => boolean;
|
package/dist/utils/groups.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import { ColumnType } from '../types/firestore.js';
|
|
1
2
|
export const DEFAULT_GROUP_INDEX = 0;
|
|
2
3
|
export const isDefaultGroup = (group) => group?.groupIndex === DEFAULT_GROUP_INDEX;
|
|
3
4
|
export const findDefaultGroup = (groups) => groups?.find((g) => g.groupIndex === DEFAULT_GROUP_INDEX) ?? null;
|
|
5
|
+
// A FORM group (one row of its section's table) counts as complete when every
|
|
6
|
+
// Measurement column holds a value. Angle columns are intentionally excluded
|
|
7
|
+
// (mirrors the apps' header-completion computations), and a section with no
|
|
8
|
+
// Measurement columns is never "complete".
|
|
9
|
+
export const isFormGroupCompleted = (group, section) => {
|
|
10
|
+
const measurementColumns = section.tableConfig.filter((column) => column.type === ColumnType.Measurement);
|
|
11
|
+
if (measurementColumns.length === 0)
|
|
12
|
+
return false;
|
|
13
|
+
return measurementColumns.every((column) => {
|
|
14
|
+
const value = group.columns?.[column.id];
|
|
15
|
+
return value !== undefined && value !== '';
|
|
16
|
+
});
|
|
17
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reekon-tools/boldr-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
|
|
5
5
|
"author": "REEKON Tools",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"build": "tsc",
|
|
26
26
|
"prepack": "yarn run build",
|
|
27
27
|
"sync:local": "./scripts/sync-local.sh",
|
|
28
|
+
"gen:category-icons": "tsc && node ./scripts/gen-category-icons.mjs",
|
|
28
29
|
"test": "vitest",
|
|
29
30
|
"coverage": "vitest run --coverage",
|
|
30
31
|
"format": "prettier --write .",
|