@updraft-solutions/mcrit-sdk 0.4.0 → 0.6.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.
Files changed (47) hide show
  1. package/dist/chunk-EE4YY3AU.js +2620 -0
  2. package/dist/chunk-H4CJ2YEQ.cjs +2620 -0
  3. package/dist/courses/index.d.cts +2 -2
  4. package/dist/courses/index.d.ts +2 -2
  5. package/dist/index.cjs +201 -5
  6. package/dist/index.d.cts +3 -3
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.js +207 -11
  9. package/dist/schemas/index.cjs +198 -2
  10. package/dist/schemas/index.d.cts +3157 -128
  11. package/dist/schemas/index.d.ts +3157 -128
  12. package/dist/schemas/index.js +201 -5
  13. package/dist/training-C9rIBUeV.d.cts +101685 -0
  14. package/dist/training-C9rIBUeV.d.ts +101685 -0
  15. package/dist/types/index.cjs +1 -1
  16. package/dist/types/index.d.cts +278 -6
  17. package/dist/types/index.d.ts +278 -6
  18. package/dist/types/index.js +1 -1
  19. package/package.json +1 -1
  20. package/src/courses/index.ts +2 -2
  21. package/src/index.ts +42 -0
  22. package/src/schemas/common.ts +69 -5
  23. package/src/schemas/course-milestone.ts +78 -0
  24. package/src/schemas/equipment.ts +27 -0
  25. package/src/schemas/form-template.ts +416 -0
  26. package/src/schemas/index.ts +7 -1
  27. package/src/schemas/location-map.ts +175 -0
  28. package/src/schemas/models.ts +201 -100
  29. package/src/schemas/newsletter.ts +362 -0
  30. package/src/schemas/todo.ts +46 -0
  31. package/src/schemas/training.ts +500 -0
  32. package/src/types/ai.ts +85 -0
  33. package/src/types/aircraft-block.ts +23 -0
  34. package/src/types/api.ts +3 -1
  35. package/src/types/background-task.ts +28 -0
  36. package/src/types/compliance.ts +27 -3
  37. package/src/types/index.ts +5 -0
  38. package/src/types/landing-fee.ts +92 -0
  39. package/src/types/models.ts +46 -3
  40. package/src/types/roles.ts +26 -0
  41. package/dist/chunk-RC2BBT6H.cjs +0 -1384
  42. package/dist/chunk-WQLDSZNN.js +0 -1384
  43. package/dist/models-Bc5BKYuI.d.cts +0 -36898
  44. package/dist/models-Bc5BKYuI.d.ts +0 -36898
  45. package/src/schemas/fee.ts +0 -37
  46. /package/dist/{chunk-LXNCAKJZ.js → chunk-2WJHTRIE.js} +0 -0
  47. /package/dist/{chunk-MOOGM3DW.cjs → chunk-DCEOET63.cjs} +0 -0
@@ -0,0 +1,416 @@
1
+ import { z } from "zod";
2
+
3
+ // Field types enum - matches backend FormFieldType
4
+ export enum FormFieldType {
5
+ Text = "text",
6
+ Textarea = "textarea",
7
+ Number = "number",
8
+ Date = "date",
9
+ Time = "time",
10
+ Duration = "duration",
11
+ Select = "select",
12
+ MultiSelect = "multi_select",
13
+ Checkbox = "checkbox",
14
+ Radio = "radio",
15
+ RatingScale = "rating_scale",
16
+ Signature = "signature",
17
+ PassFail = "pass_fail",
18
+ MembershipSelect = "membership_select",
19
+ InstructorSelect = "instructor_select",
20
+ AirplaneSelect = "airplane_select",
21
+ CourseSelect = "course_select",
22
+ MembershipSignature = "membership_signature",
23
+ FileUpload = "file_upload",
24
+ // Select with built-in mass unit options (kg/lbs/t) — see app/utils/massUnits.ts
25
+ MassUnitSelect = "mass_unit_select",
26
+ // Display-only text block (contract clauses, instructions). Carries its text in
27
+ // `content`, stores no value, never an input.
28
+ StaticText = "static_text",
29
+ }
30
+
31
+ // Submission status enum - matches backend FormSubmissionStatus
32
+ export enum FormSubmissionStatus {
33
+ Draft = "draft",
34
+ AwaitingSignatures = "awaiting_signatures",
35
+ Submitted = "submitted",
36
+ Approved = "approved",
37
+ Rejected = "rejected",
38
+ }
39
+
40
+ // Submission origin - matches backend FormSubmissionSource
41
+ export enum FormSubmissionSource {
42
+ Voluntary = "voluntary",
43
+ Onboarding = "onboarding",
44
+ AdHocRequest = "ad_hoc_request",
45
+ }
46
+
47
+ // Schema field label (i18n)
48
+ export const FieldLabelSchema = z.object({
49
+ de: z.string().optional(),
50
+ en: z.string(),
51
+ no: z.string().optional(),
52
+ });
53
+
54
+ // Conditional visibility
55
+ export const ShowWhenSchema = z.object({
56
+ field: z.string(),
57
+ operator: z.enum(["eq", "neq", "in", "not_in", "gt", "lt", "gte", "lte"]),
58
+ value: z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]),
59
+ });
60
+
61
+ // Schema field definition
62
+ export const SchemaFieldSchema = z.object({
63
+ key: z.string(),
64
+ type: z.nativeEnum(FormFieldType),
65
+ // Optional because static_text fields use `content` instead of `label`.
66
+ label: FieldLabelSchema.optional(),
67
+ // Display-only text for static_text fields (i18n), rendered with line breaks.
68
+ content: FieldLabelSchema.optional(),
69
+ required: z.boolean().optional().default(false),
70
+ validation: z.record(z.any()).optional(),
71
+ options: z.record(z.any()).optional(),
72
+ col_span: z.number().min(1).max(12).optional(),
73
+ order: z.number(),
74
+ show_when: ShowWhenSchema.optional(),
75
+ // Signature fields only: auto-binds the designated signer to the membership_id stored in the
76
+ // referenced field (no manual designation needed).
77
+ signer_membership_field: z.string().optional(),
78
+ // Signature fields only: group designation — any member holding this permission at the
79
+ // template's company may sign (e.g. "formSubmission.signAsCompany" for the company signature).
80
+ signer_permission: z.string().optional(),
81
+ // Signature fields only: bind the signer to the membership the form is attached to
82
+ // ("submittable") — the person the form is about signs, with no picker.
83
+ signer_source: z.string().optional(),
84
+ // Text fields only: server-side party prefill (company.name|address,
85
+ // member.name|address). Resolved & frozen into data at issuance — render
86
+ // read-only; the backend rejects edits to these keys.
87
+ prefill_source: z.string().optional(),
88
+ });
89
+
90
+ // Schema section definition
91
+ export const SchemaSectionSchema = z.object({
92
+ key: z.string(),
93
+ title: z.string(),
94
+ order: z.number(),
95
+ fields: z.array(SchemaFieldSchema),
96
+ });
97
+
98
+ // Full template schema
99
+ export const TemplateSchemaSchema = z.object({
100
+ sections: z.array(SchemaSectionSchema),
101
+ });
102
+
103
+ // User basic schema for relationships
104
+ const UserBasicSchema = z
105
+ .object({
106
+ id: z.number(),
107
+ name: z.string(),
108
+ email: z.string().optional(),
109
+ })
110
+ .nullable();
111
+
112
+ // Form Template
113
+ export const FormTemplateSchema = z.object({
114
+ type: z.literal("form_template"),
115
+ id: z.number(),
116
+ company_id: z.number().nullable(),
117
+ name: z.string(),
118
+ slug: z.string(),
119
+ description: z.string().nullable(),
120
+ schema: TemplateSchemaSchema,
121
+ schema_version: z.number(),
122
+ pdf_view: z.string().nullable(),
123
+ requires_review: z.boolean(),
124
+ is_active: z.boolean(),
125
+ is_singleton: z.boolean().optional().default(false),
126
+ // Sign-only: submissions skip the draft phase (issued straight into
127
+ // awaiting_signatures) and auto-submit once all signatures are collected.
128
+ is_sign_only: z.boolean().optional().default(false),
129
+ // Requirable: this company template may be referenced by requirement groups
130
+ // (a compliance document), vs a workflow form. System templates are always
131
+ // requirable. Defaults false.
132
+ is_requirable: z.boolean().optional().default(false),
133
+ is_onboarding: z.boolean().optional().default(false),
134
+ // Array of system role keys this onboarding template applies to.
135
+ // null/missing = applies to every member at the company.
136
+ onboarding_role_keys: z.array(z.string()).nullable().optional(),
137
+ // System templates: platform-provided, super-admin maintained, no owning
138
+ // company. Cannot be assigned/filled until merge Phase 2.
139
+ is_system: z.boolean().optional().default(false),
140
+ // Scope of a system template: null/"user" = a personal qualification;
141
+ // "aircraft" = an aircraft document (e.g. weight report) excluded from the
142
+ // user qualification picker.
143
+ applies_to: z.string().nullable().optional(),
144
+ // When true, submissions carry an expires_at (content validity, e.g. a
145
+ // medical's expiry). Distinct from due_date (deadline to fill).
146
+ has_expiry: z.boolean().optional().default(false),
147
+ default_notification_days: z.number().nullable().optional(),
148
+ created_by: z.number().nullable(),
149
+ creator: UserBasicSchema.optional(),
150
+ assignments: z.array(z.any()).optional(),
151
+ created_at: z.string(),
152
+ updated_at: z.string(),
153
+ });
154
+
155
+ // Form Template Assignment
156
+ export const FormTemplateAssignmentSchema = z.object({
157
+ type: z.literal("form_template_assignment"),
158
+ id: z.number(),
159
+ form_template_id: z.number(),
160
+ assignable_type: z.string(),
161
+ assignable_id: z.number(),
162
+ target_type: z.string(),
163
+ // `trigger` is soft-deprecated — column kept as a future escape hatch but no
164
+ // handler reads it and the UI no longer surfaces it. See docs/features/forms.md.
165
+ trigger: z.string().nullable(),
166
+ sort_order: z.number(),
167
+ form_template: FormTemplateSchema.optional(),
168
+ created_at: z.string(),
169
+ updated_at: z.string(),
170
+ });
171
+
172
+ // Signature row from the polymorphic signatures table; each carries its own audit packet.
173
+ export const SignatureSchema = z.object({
174
+ type: z.literal("signature").optional(),
175
+ id: z.number(),
176
+ signable_type: z.string().optional(),
177
+ signable_id: z.number().optional(),
178
+ field_key: z.string(),
179
+ status: z.enum(["pending", "signed", "revoked"]),
180
+ required_signer_membership_id: z.number().nullable(),
181
+ required_signer_name: z.string().nullable().optional(),
182
+ // Group designation: any member holding this permission at the template's
183
+ // company may sign (e.g. "formSubmission.signAsCompany").
184
+ required_signer_permission: z.string().nullable().optional(),
185
+ signed_by_user_id: z.number().nullable(),
186
+ signed_by_membership_id: z.number().nullable(),
187
+ signed_by_name: z.string().nullable().optional(),
188
+ is_current_user_designated: z.boolean().optional().default(false),
189
+ signed_at: z.string().nullable(),
190
+ image: z.string().nullable(),
191
+ data_hash: z.string().nullable(),
192
+ schema_version: z.number().nullable(),
193
+ schema_hash: z.string().nullable(),
194
+ consent_text_hash: z.string().nullable(),
195
+ auth_method: z.string().nullable(),
196
+ auth_verified_at: z.string().nullable(),
197
+ ip_address: z.string().nullable(),
198
+ user_agent: z.string().nullable(),
199
+ geo_country: z.string().nullable(),
200
+ signing_event_id: z.string().nullable(),
201
+ created_at: z.string().optional(),
202
+ updated_at: z.string().optional(),
203
+ });
204
+
205
+ // Spatie MediaResource subset used by file_upload fields.
206
+ export const SubmissionFileSchema = z.object({
207
+ id: z.number(),
208
+ file_name: z.string(),
209
+ size: z.number(),
210
+ mime_type: z.string().nullable().optional(),
211
+ url: z.string().nullable().optional(),
212
+ conversions: z.record(z.string(), z.string().nullable()).optional(),
213
+ });
214
+
215
+ // Form Submission Share — a user-owned submission shared with a company
216
+ // (replaces the legacy document-shares model in merge Phase 3b). Base shape
217
+ // without the nested submission so it can be embedded on FormSubmission
218
+ // itself (`shares` — the owner sees all of them on their own listing).
219
+ export const FormSubmissionShareBaseSchema = z.object({
220
+ type: z.literal("form_submission_share").optional(),
221
+ id: z.number(),
222
+ form_submission_id: z.number(),
223
+ company_id: z.number(),
224
+ shared_by: z.number(),
225
+ company: z
226
+ .object({
227
+ id: z.number(),
228
+ name: z.string(),
229
+ })
230
+ .optional(),
231
+ created_at: z.string(),
232
+ });
233
+
234
+ // Form Submission
235
+ export const FormSubmissionSchema = z.object({
236
+ type: z.literal("form_submission"),
237
+ id: z.number(),
238
+ form_template_id: z.number(),
239
+ schema_version: z.number(),
240
+ schema_snapshot: TemplateSchemaSchema,
241
+ submittable_type: z.string(),
242
+ submittable_id: z.number(),
243
+ // Who/what the submission is about — shown in the dialog header. Present
244
+ // when the backend loaded the submittable relation.
245
+ submittable_summary: z
246
+ .object({
247
+ type: z.string(),
248
+ id: z.number(),
249
+ name: z.string().nullable(),
250
+ email: z.string().nullable(),
251
+ })
252
+ .nullable()
253
+ .optional(),
254
+ submitted_by: z.number(),
255
+ data: z.record(z.any()),
256
+ data_resolved_names: z.record(z.string(), z.string()).optional().default({}),
257
+ required_signers: z.record(z.string(), z.number()).nullable().optional(),
258
+ signatures: z.array(SignatureSchema).optional(),
259
+ status: z.nativeEnum(FormSubmissionStatus),
260
+ submitted_at: z.string().nullable(),
261
+ reviewed_by: z.number().nullable(),
262
+ reviewed_at: z.string().nullable(),
263
+ review_comment: z.string().nullable(),
264
+ is_final: z.boolean().optional().default(false),
265
+ is_mutable: z.boolean().optional().default(false),
266
+ is_signable: z.boolean().optional().default(false),
267
+ has_signature_fields: z.boolean().optional().default(false),
268
+ all_signatures_collected: z.boolean().optional().default(true),
269
+ // Server-evaluated policy decisions per gate (delete, revertToDraft, update, …),
270
+ // read via hasModelPermission(submission, '<gate>'). Null when the fetch didn't
271
+ // request permissions; populated when it passes permissions[form_submission].
272
+ can: z.record(z.boolean().nullable()).optional().default({}),
273
+ // Request envelope for pre-issued drafts (onboarding / ad-hoc admin request).
274
+ // Voluntary submissions leave note/due_date/requester null.
275
+ source: z
276
+ .nativeEnum(FormSubmissionSource)
277
+ .optional()
278
+ .default(FormSubmissionSource.Voluntary),
279
+ due_date: z.string().nullable().optional(),
280
+ note: z.string().nullable().optional(),
281
+ requested_by_user_id: z.number().nullable().optional(),
282
+ is_overdue: z.boolean().optional().default(false),
283
+ days_until_due: z.number().nullable().optional(),
284
+ // Content validity (has_expiry templates only). ISO datetime.
285
+ expires_at: z.string().nullable().optional(),
286
+ notification_days_before: z.number().nullable().optional(),
287
+ notifications_enabled: z.boolean().optional().default(true),
288
+ is_expired: z.boolean().optional().default(false),
289
+ // { field_key: media | null } for every file_upload field in the snapshot.
290
+ files: z.record(z.string(), SubmissionFileSchema.nullable()).optional(),
291
+ form_template: FormTemplateSchema.optional(),
292
+ submitter: UserBasicSchema.optional(),
293
+ reviewer: UserBasicSchema.optional(),
294
+ requester: UserBasicSchema.optional(),
295
+ // Shares of this (user-owned) submission. Present on listings that
296
+ // eager-load them — the owner sees all, recipient staff only their own
297
+ // companies' rows (viewer-scoped in the backend resource).
298
+ shares: z.array(FormSubmissionShareBaseSchema).optional(),
299
+ created_at: z.string(),
300
+ updated_at: z.string(),
301
+ });
302
+
303
+ // Full share shape — `form_submission` is nested on the my-shares /
304
+ // company-inbox listings.
305
+ export const FormSubmissionShareSchema = FormSubmissionShareBaseSchema.extend({
306
+ form_submission: FormSubmissionSchema.optional(),
307
+ });
308
+
309
+ // Cross-link source metadata. Present on rows surfaced through the
310
+ // enrollment ← booking link; null when the row belongs to the queried
311
+ // submittable directly. See backend GetRequiredFormsForModel.
312
+ // Full crew of the source booking, incl. virtual roles (authorizing
313
+ // instructor) — role values are backend CrewMemberPosition snake_case.
314
+ export const RequiredFormViaCrewSchema = z.object({
315
+ name: z.string().nullable(),
316
+ role: z.string(),
317
+ is_pic: z.boolean(),
318
+ });
319
+
320
+ export const RequiredFormViaSchema = z.object({
321
+ type: z.literal("booking"),
322
+ booking_id: z.number(),
323
+ etdZ: z.string().nullable(),
324
+ route_display: z.string().nullable(),
325
+ airplane_registration: z.string().nullable(),
326
+ crew: z.array(RequiredFormViaCrewSchema).optional().default([]),
327
+ });
328
+
329
+ // Required form (convenience endpoint response)
330
+ export const RequiredFormSchema = z.object({
331
+ template: FormTemplateSchema,
332
+ assignment: FormTemplateAssignmentSchema,
333
+ // Latest submission (back-compat for callers that haven't migrated to the array).
334
+ submission: FormSubmissionSchema.nullable(),
335
+ // All submissions for this (template, submittable), newest first. For singleton templates
336
+ // length is 0 or 1; non-singleton templates can grow over time.
337
+ submissions: z.array(FormSubmissionSchema).optional().default([]),
338
+ is_complete: z.boolean(),
339
+ via: RequiredFormViaSchema.nullable().optional(),
340
+ });
341
+
342
+ // Type exports
343
+ export type FieldLabel = z.infer<typeof FieldLabelSchema>;
344
+ export type ShowWhen = z.infer<typeof ShowWhenSchema>;
345
+ export type SchemaField = z.infer<typeof SchemaFieldSchema>;
346
+ export type SchemaSection = z.infer<typeof SchemaSectionSchema>;
347
+ export type TemplateSchema = z.infer<typeof TemplateSchemaSchema>;
348
+ export type FormTemplate = z.infer<typeof FormTemplateSchema>;
349
+ export type FormTemplateAssignment = z.infer<
350
+ typeof FormTemplateAssignmentSchema
351
+ >;
352
+ export type FormSubmission = z.infer<typeof FormSubmissionSchema>;
353
+ export type FormSubmissionShare = z.infer<typeof FormSubmissionShareSchema>;
354
+ export type FormSubmissionShareBase = z.infer<
355
+ typeof FormSubmissionShareBaseSchema
356
+ >;
357
+ export type SubmissionFile = z.infer<typeof SubmissionFileSchema>;
358
+ export type RequiredForm = z.infer<typeof RequiredFormSchema>;
359
+ export type RequiredFormVia = z.infer<typeof RequiredFormViaSchema>;
360
+ export type RequiredFormViaCrew = z.infer<typeof RequiredFormViaCrewSchema>;
361
+ export type Signature = z.infer<typeof SignatureSchema>;
362
+
363
+ // Input types
364
+ export interface CreateFormTemplateInput {
365
+ name: string;
366
+ slug?: string;
367
+ description?: string | null;
368
+ schema: TemplateSchema;
369
+ pdf_view?: string | null;
370
+ requires_review?: boolean;
371
+ is_active?: boolean;
372
+ is_singleton?: boolean;
373
+ is_sign_only?: boolean;
374
+ is_onboarding?: boolean;
375
+ onboarding_role_keys?: string[] | null;
376
+ has_expiry?: boolean;
377
+ default_notification_days?: number | null;
378
+ }
379
+
380
+ export interface UpdateFormTemplateInput {
381
+ name?: string;
382
+ slug?: string;
383
+ description?: string | null;
384
+ schema?: TemplateSchema;
385
+ pdf_view?: string | null;
386
+ requires_review?: boolean;
387
+ is_active?: boolean;
388
+ is_singleton?: boolean;
389
+ is_sign_only?: boolean;
390
+ is_onboarding?: boolean;
391
+ onboarding_role_keys?: string[] | null;
392
+ has_expiry?: boolean;
393
+ default_notification_days?: number | null;
394
+ }
395
+
396
+ export interface CreateFormTemplateAssignmentInput {
397
+ assignable_type: "course" | "company";
398
+ assignable_id: number;
399
+ target_type: "course_enrollment" | "booking" | "membership";
400
+ // @deprecated trigger is accepted by the backend for back-compat but no
401
+ // handler reads it. UI does not send it. Kept on the input shape only.
402
+ trigger?: "on_completion" | "on_creation" | "before_solo" | "manual" | null;
403
+ sort_order?: number;
404
+ }
405
+
406
+ export interface CreateFormSubmissionInput {
407
+ form_template_id: number;
408
+ data: Record<string, unknown>;
409
+ expires_at?: string | null;
410
+ notification_days_before?: number | null;
411
+ }
412
+
413
+ export interface ReviewFormSubmissionInput {
414
+ decision: "approved" | "rejected";
415
+ review_comment?: string | null;
416
+ }
@@ -1,6 +1,12 @@
1
1
  export * from "./common";
2
2
  export * from "./models";
3
3
  export * from "./faq";
4
- export * from "./fee";
5
4
  export * from "./safety-report";
6
5
  export * from "./report";
6
+ export * from "./form-template";
7
+ export * from "./training";
8
+ export * from "./course-milestone";
9
+ export * from "./equipment";
10
+ export * from "./newsletter";
11
+ export * from "./location-map";
12
+ export * from "./todo";
@@ -0,0 +1,175 @@
1
+ import { z } from "zod";
2
+
3
+ // Minimal GeoJSON types (we don't depend on @types/geojson directly).
4
+
5
+ export type GeoJsonGeometry =
6
+ | { type: "Point"; coordinates: number[] }
7
+ | { type: "MultiPoint"; coordinates: number[][] }
8
+ | { type: "LineString"; coordinates: number[][] }
9
+ | { type: "MultiLineString"; coordinates: number[][][] }
10
+ | { type: "Polygon"; coordinates: number[][][] }
11
+ | { type: "MultiPolygon"; coordinates: number[][][][] };
12
+
13
+ export interface GeoJsonFeature<P = Record<string, unknown>> {
14
+ type: "Feature";
15
+ id?: number | string;
16
+ geometry: GeoJsonGeometry;
17
+ properties: P | null;
18
+ }
19
+
20
+ export interface GeoJsonFeatureCollection<P = Record<string, unknown>> {
21
+ type: "FeatureCollection";
22
+ features: GeoJsonFeature<P>[];
23
+ }
24
+
25
+ // ── Enums (mirror backend) ──
26
+
27
+ export enum LocationMapKind {
28
+ Facility = "facility",
29
+ TrainingArea = "training_area",
30
+ Parking = "parking",
31
+ Procedural = "procedural",
32
+ }
33
+
34
+ export enum LocationFeatureVisibility {
35
+ Public = "public",
36
+ Members = "members",
37
+ }
38
+
39
+ export const ALLOWED_GEOMETRY_TYPES = [
40
+ "Point",
41
+ "MultiPoint",
42
+ "LineString",
43
+ "MultiLineString",
44
+ "Polygon",
45
+ "MultiPolygon",
46
+ ] as const;
47
+
48
+ export type AllowedGeometryType = (typeof ALLOWED_GEOMETRY_TYPES)[number];
49
+
50
+ // ── Viewport ──
51
+
52
+ export const ViewportSchema = z.object({
53
+ center_lng: z.number().min(-180).max(180),
54
+ center_lat: z.number().min(-90).max(90),
55
+ zoom: z.number().min(0).max(24),
56
+ pitch: z.number().min(0).max(85).optional(),
57
+ bearing: z.number().min(-360).max(360).optional(),
58
+ });
59
+
60
+ export type Viewport = z.infer<typeof ViewportSchema>;
61
+
62
+ // ── Geometry (loose GeoJSON-compatible) ──
63
+
64
+ export const GeometrySchema = z.object({
65
+ type: z.enum(ALLOWED_GEOMETRY_TYPES),
66
+ coordinates: z.array(z.unknown()),
67
+ });
68
+
69
+ // ── LocationMap ──
70
+
71
+ export interface LocationMap {
72
+ type: "location_map";
73
+ id: number;
74
+ company_id: number;
75
+ name: string;
76
+ slug: string;
77
+ kind: string;
78
+ description: string | null;
79
+ default_viewport: Viewport | null;
80
+ is_active: boolean;
81
+ created_by: number | null;
82
+ creator?: { id: number; firstname?: string; lastname?: string } | null;
83
+ features?: LocationMapFeature[];
84
+ created_at: string;
85
+ updated_at: string;
86
+ }
87
+
88
+ export const CreateLocationMapSchema = z.object({
89
+ name: z.string().min(1).max(255),
90
+ slug: z.string().min(1).max(255).optional(),
91
+ kind: z.string().max(64).optional(),
92
+ description: z.string().nullable().optional(),
93
+ default_viewport: ViewportSchema.nullable().optional(),
94
+ is_active: z.boolean().optional(),
95
+ });
96
+ export type CreateLocationMapInput = z.infer<typeof CreateLocationMapSchema>;
97
+
98
+ export const UpdateLocationMapSchema = CreateLocationMapSchema.partial();
99
+ export type UpdateLocationMapInput = z.infer<typeof UpdateLocationMapSchema>;
100
+
101
+ // ── LocationMapFeature ──
102
+
103
+ export interface LocationMapFeature {
104
+ type: "location_map_feature";
105
+ id: number;
106
+ location_map_id: number;
107
+ name: string | null;
108
+ kind: string;
109
+ visibility: LocationFeatureVisibility | string;
110
+ geometry: GeoJsonGeometry;
111
+ geometry_type: AllowedGeometryType | string;
112
+ properties: Record<string, unknown>;
113
+ display_order: number;
114
+ created_at: string;
115
+ updated_at: string;
116
+ }
117
+
118
+ export const CreateLocationMapFeatureSchema = z.object({
119
+ name: z.string().max(255).nullable().optional(),
120
+ kind: z.string().min(1).max(64),
121
+ visibility: z.nativeEnum(LocationFeatureVisibility).optional(),
122
+ geometry: GeometrySchema,
123
+ properties: z.record(z.string(), z.unknown()).nullable().optional(),
124
+ display_order: z.number().int().min(0).optional(),
125
+ });
126
+ export type CreateLocationMapFeatureInput = z.infer<
127
+ typeof CreateLocationMapFeatureSchema
128
+ >;
129
+
130
+ export const UpdateLocationMapFeatureSchema =
131
+ CreateLocationMapFeatureSchema.partial().extend({
132
+ geometry: GeometrySchema.optional(),
133
+ });
134
+ export type UpdateLocationMapFeatureInput = z.infer<
135
+ typeof UpdateLocationMapFeatureSchema
136
+ >;
137
+
138
+ // ── Bulk import ──
139
+
140
+ export const ImportFeaturesSchema = z.object({
141
+ type: z.literal("FeatureCollection"),
142
+ features: z
143
+ .array(
144
+ z.object({
145
+ type: z.literal("Feature"),
146
+ geometry: GeometrySchema,
147
+ properties: z.record(z.string(), z.unknown()).nullable().optional(),
148
+ }),
149
+ )
150
+ .min(1)
151
+ .max(500),
152
+ defaults: z
153
+ .object({
154
+ kind: z.string().max(64).optional(),
155
+ visibility: z.nativeEnum(LocationFeatureVisibility).optional(),
156
+ })
157
+ .optional(),
158
+ });
159
+ export type ImportFeaturesInput = z.infer<typeof ImportFeaturesSchema>;
160
+
161
+ // ── Viewer (GeoJSON FeatureCollection returned by API) ──
162
+
163
+ export interface LocationMapGeoJsonProperties {
164
+ id: number;
165
+ name: string | null;
166
+ kind: string;
167
+ visibility: string;
168
+ display_order: number;
169
+ [key: string]: unknown;
170
+ }
171
+
172
+ export type LocationMapGeoJson =
173
+ GeoJsonFeatureCollection<LocationMapGeoJsonProperties>;
174
+ export type LocationMapFeatureGeoJson =
175
+ GeoJsonFeature<LocationMapGeoJsonProperties>;