@managespace/sdk 0.1.158-braintree → 0.1.158-extensions-v2

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 (78) hide show
  1. package/README.md +746 -2
  2. package/dist/extensibility/functions/project/billing.d.ts +0 -7
  3. package/dist/extensibility/functions/project/billing.d.ts.map +1 -1
  4. package/dist/extensibility/functions/project/billing.js +0 -5
  5. package/dist/extensibility/types/control.d.ts +1 -1
  6. package/dist/extensibility/types/control.d.ts.map +1 -1
  7. package/dist/extensions/host-bridge.d.ts +166 -0
  8. package/dist/extensions/host-bridge.d.ts.map +1 -0
  9. package/dist/extensions/host-bridge.js +259 -0
  10. package/dist/extensions/index.d.ts +40 -0
  11. package/dist/extensions/index.d.ts.map +1 -0
  12. package/dist/extensions/index.js +55 -0
  13. package/dist/extensions/types.d.ts +111 -0
  14. package/dist/extensions/types.d.ts.map +1 -0
  15. package/dist/extensions/types.js +2 -0
  16. package/dist/generated/apis/default-api.d.ts +1 -9
  17. package/dist/generated/apis/default-api.d.ts.map +1 -1
  18. package/dist/generated/apis/default-api.js +0 -28
  19. package/dist/generated/apis/extensions-api.d.ts +98 -0
  20. package/dist/generated/apis/extensions-api.d.ts.map +1 -0
  21. package/dist/generated/apis/extensions-api.js +295 -0
  22. package/dist/generated/apis/index.d.ts +1 -0
  23. package/dist/generated/apis/index.d.ts.map +1 -1
  24. package/dist/generated/apis/index.js +1 -0
  25. package/dist/generated/models/accrual-report-filters.d.ts +39 -0
  26. package/dist/generated/models/accrual-report-filters.d.ts.map +1 -0
  27. package/dist/generated/models/accrual-report-filters.js +55 -0
  28. package/dist/generated/models/extension-org.d.ts +64 -0
  29. package/dist/generated/models/extension-org.d.ts.map +1 -0
  30. package/dist/generated/models/extension-org.js +70 -0
  31. package/dist/generated/models/extension.d.ts +106 -0
  32. package/dist/generated/models/extension.d.ts.map +1 -0
  33. package/dist/generated/models/extension.js +98 -0
  34. package/dist/generated/models/get-journal-entries200-response.d.ts +41 -0
  35. package/dist/generated/models/get-journal-entries200-response.d.ts.map +1 -0
  36. package/dist/generated/models/get-journal-entries200-response.js +55 -0
  37. package/dist/generated/models/income-report-filters.d.ts +45 -0
  38. package/dist/generated/models/income-report-filters.d.ts.map +1 -0
  39. package/dist/generated/models/income-report-filters.js +57 -0
  40. package/dist/generated/models/index.d.ts +2 -1
  41. package/dist/generated/models/index.d.ts.map +1 -1
  42. package/dist/generated/models/index.js +2 -1
  43. package/dist/generated/models/journal-entry-entries.d.ts +63 -0
  44. package/dist/generated/models/journal-entry-entries.d.ts.map +1 -0
  45. package/dist/generated/models/journal-entry-entries.js +71 -0
  46. package/dist/generated/models/journal-entry.d.ts +106 -0
  47. package/dist/generated/models/journal-entry.d.ts.map +1 -0
  48. package/dist/generated/models/journal-entry.js +100 -0
  49. package/dist/generated/models/occupancy-statistics-report-filters.d.ts +45 -0
  50. package/dist/generated/models/occupancy-statistics-report-filters.d.ts.map +1 -0
  51. package/dist/generated/models/occupancy-statistics-report-filters.js +57 -0
  52. package/dist/generated/models/prepaid-rent-report-filters.d.ts +45 -0
  53. package/dist/generated/models/prepaid-rent-report-filters.d.ts.map +1 -0
  54. package/dist/generated/models/prepaid-rent-report-filters.js +57 -0
  55. package/dist/generated/models/refund-summary-report-filters.d.ts +45 -0
  56. package/dist/generated/models/refund-summary-report-filters.d.ts.map +1 -0
  57. package/dist/generated/models/refund-summary-report-filters.js +57 -0
  58. package/dist/generated/models/security-deposits-report-filters.d.ts +45 -0
  59. package/dist/generated/models/security-deposits-report-filters.d.ts.map +1 -0
  60. package/dist/generated/models/security-deposits-report-filters.js +57 -0
  61. package/dist/index.d.ts +1 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1 -0
  64. package/package.json +2 -2
  65. package/src/extensibility/functions/project/billing.ts +0 -6
  66. package/src/extensibility/types/control.ts +0 -1
  67. package/src/extensions/host-bridge.ts +274 -0
  68. package/src/extensions/index.ts +40 -0
  69. package/src/extensions/types.ts +123 -0
  70. package/src/generated/.openapi-generator/FILES +3 -1
  71. package/src/generated/apis/default-api.ts +0 -37
  72. package/src/generated/apis/extensions-api.ts +362 -0
  73. package/src/generated/apis/index.ts +1 -0
  74. package/src/generated/models/extension-org.ts +119 -0
  75. package/src/generated/models/extension.ts +182 -0
  76. package/src/generated/models/index.ts +2 -1
  77. package/src/index.ts +1 -0
  78. package/src/generated/models/payment-gateway-client-token-response.ts +0 -87
package/README.md CHANGED
@@ -1,6 +1,750 @@
1
1
  # @managespace/sdk
2
2
 
3
- update version number in package.json
4
- npm i
3
+ ManageSpace SDK for building integrations and extensions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @managespace/sdk
9
+ ```
10
+
11
+ ---
12
+
13
+ # Extension Development Guide
14
+
15
+ This guide explains how to build extensions that run within the ManageSpace platform.
16
+
17
+ ## Overview
18
+
19
+ ManageSpace extensions are standalone web applications that run in an iframe within the ManageSpace UI. They have access to:
20
+
21
+ - **Authentication context** - User's auth token, org/site IDs
22
+ - **ManageSpace APIs** - Full access to the REST API
23
+ - **Host navigation** - Ability to navigate the main application
24
+ - **Toast notifications** - Display feedback to users
25
+ - **Entity events** - React to changes in ManageSpace data
26
+
27
+ ## Quick Start
28
+
29
+ ### 1. Create Project Structure
30
+
31
+ ```
32
+ my-extension/
33
+ ├── package.json
34
+ ├── tsconfig.json
35
+ ├── vite.config.ts
36
+ ├── manifest.json
37
+ ├── index.html
38
+ └── src/
39
+ ├── main.tsx
40
+ ├── App.tsx
41
+ └── styles.css
42
+ ```
43
+
44
+ ### 2. package.json
45
+
46
+ **Important:** The `bundle` script is required to create the uploadable ZIP package. It builds the extension, copies the manifest, and creates the ZIP file.
47
+
48
+ ```json
49
+ {
50
+ "name": "my-extension",
51
+ "version": "1.0.0",
52
+ "private": true,
53
+ "type": "module",
54
+ "scripts": {
55
+ "dev": "vite",
56
+ "build": "tsc && vite build",
57
+ "bundle": "bun run build && cp manifest.json dist/ && cd dist && zip -r ../bundle.zip ."
58
+ },
59
+ "dependencies": {
60
+ "@managespace/sdk": "^0.1.0",
61
+ "react": "^18.3.0",
62
+ "react-dom": "^18.3.0"
63
+ },
64
+ "devDependencies": {
65
+ "@types/react": "^18.3.0",
66
+ "@types/react-dom": "^18.3.0",
67
+ "@vitejs/plugin-react": "^4.3.0",
68
+ "typescript": "^5.6.0",
69
+ "vite": "^5.4.0"
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### 3. tsconfig.json
75
+
76
+ ```json
77
+ {
78
+ "compilerOptions": {
79
+ "target": "ES2020",
80
+ "useDefineForClassFields": true,
81
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
82
+ "module": "ESNext",
83
+ "skipLibCheck": true,
84
+ "moduleResolution": "bundler",
85
+ "allowImportingTsExtensions": true,
86
+ "resolveJsonModule": true,
87
+ "isolatedModules": true,
88
+ "noEmit": true,
89
+ "jsx": "react-jsx",
90
+ "strict": true,
91
+ "noUnusedLocals": true,
92
+ "noUnusedParameters": true,
93
+ "noFallthroughCasesInSwitch": true
94
+ },
95
+ "include": ["src"]
96
+ }
97
+ ```
98
+
99
+ ### 4. vite.config.ts
100
+
101
+ ```typescript
102
+ import react from '@vitejs/plugin-react';
103
+ import { defineConfig } from 'vite';
104
+
105
+ export default defineConfig({
106
+ plugins: [react()],
107
+ build: {
108
+ outDir: 'dist',
109
+ assetsDir: 'assets',
110
+ rollupOptions: {
111
+ output: {
112
+ entryFileNames: 'assets/[name].js',
113
+ chunkFileNames: 'assets/[name].js',
114
+ assetFileNames: 'assets/[name].[ext]',
115
+ },
116
+ },
117
+ },
118
+ });
119
+ ```
120
+
121
+ ### 5. manifest.json
122
+
123
+ ```json
124
+ {
125
+ "name": "My Extension",
126
+ "version": "1.0.0",
127
+ "author": "Your Name",
128
+ "description": "Description of what your extension does",
129
+ "navLabel": "My Extension",
130
+ "navIcon": "Layout",
131
+ "navRoute": "/extensions/my-extension",
132
+ "entryPoint": "index.html"
133
+ }
134
+ ```
135
+
136
+ **Manifest Fields:**
137
+
138
+ | Field | Required | Description |
139
+ | ------------- | -------- | -------------------------------------------------------- |
140
+ | `name` | Yes | Display name of the extension |
141
+ | `version` | Yes | Semantic version (e.g., "1.0.0") |
142
+ | `author` | Yes | Author or company name |
143
+ | `description` | No | Short description |
144
+ | `navLabel` | Yes | Label shown in navigation menu |
145
+ | `navIcon` | Yes | Lucide icon name (e.g., "Users", "Settings", "BarChart") |
146
+ | `navRoute` | Yes | Route path, must start with "/extensions/" |
147
+ | `entryPoint` | Yes | Entry HTML file (usually "index.html") |
148
+ | `bff.url` | No | URL of your Backend for Frontend server |
149
+
150
+ ### 6. index.html
151
+
152
+ ```html
153
+ <!DOCTYPE html>
154
+ <html lang="en">
155
+ <head>
156
+ <meta charset="UTF-8" />
157
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
158
+ <title>My Extension</title>
159
+ </head>
160
+ <body>
161
+ <div id="root"></div>
162
+ <script type="module" src="/src/main.tsx"></script>
163
+ </body>
164
+ </html>
165
+ ```
166
+
167
+ ### 7. src/main.tsx
168
+
169
+ ```typescript
170
+ import React from 'react';
171
+ import ReactDOM from 'react-dom/client';
172
+ import App from './App';
173
+ import { signalReady } from '@managespace/sdk/extensions';
174
+ import './styles.css';
175
+
176
+ // Signal to host that extension is ready to receive context
177
+ signalReady();
178
+
179
+ ReactDOM.createRoot(document.getElementById('root')!).render(
180
+ <React.StrictMode>
181
+ <App />
182
+ </React.StrictMode>
183
+ );
184
+ ```
185
+
186
+ ### 8. src/App.tsx
187
+
188
+ ```typescript
189
+ import { useEffect, useState } from 'react';
190
+ import {
191
+ getContext,
192
+ createApiFetch,
193
+ navigate,
194
+ showToast,
195
+ type ExtensionContext,
196
+ } from '@managespace/sdk/extensions';
197
+
198
+ export default function App() {
199
+ const [context, setContext] = useState<ExtensionContext | null>(null);
200
+ const [loading, setLoading] = useState(true);
201
+
202
+ useEffect(() => {
203
+ getContext()
204
+ .then(setContext)
205
+ .finally(() => setLoading(false));
206
+ }, []);
207
+
208
+ if (loading) {
209
+ return <div>Loading...</div>;
210
+ }
211
+
212
+ if (!context) {
213
+ return <div>Failed to receive context from host</div>;
214
+ }
215
+
216
+ return (
217
+ <div>
218
+ <h1>My Extension</h1>
219
+ <p>Organisation: {context.orgId}</p>
220
+ <p>User: {context.userId}</p>
221
+ {/* Your extension UI here */}
222
+ </div>
223
+ );
224
+ }
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Extension SDK API
230
+
231
+ Import from `@managespace/sdk/extensions`:
232
+
233
+ ### getContext()
234
+
235
+ Get the extension context from the ManageSpace host.
236
+
237
+ ```typescript
238
+ import { getContext } from '@managespace/sdk/extensions';
239
+
240
+ const context = await getContext();
241
+ // context.orgId - Current organisation ID
242
+ // context.userId - Current user ID
243
+ // context.siteId - Current site ID
244
+ // context.apiBaseUrl - Base URL for API calls
245
+ // context.bffUrl - Your BFF URL (if configured)
246
+ ```
247
+
248
+ ### signalReady()
249
+
250
+ Signal to the host that your extension is ready to receive context. Call this early in your extension's lifecycle.
251
+
252
+ ```typescript
253
+ import { signalReady } from '@managespace/sdk/extensions';
254
+
255
+ signalReady();
256
+ ```
257
+
258
+ ### navigate(path)
259
+
260
+ Navigate the ManageSpace host to a specific path.
261
+
262
+ ```typescript
263
+ import { navigate } from '@managespace/sdk/extensions';
264
+
265
+ // Navigate to a customer profile
266
+ navigate('/customer/abc-123');
267
+
268
+ // Navigate to the assets page
269
+ navigate('/assets');
270
+ ```
271
+
272
+ ### showToast(message, variant?)
273
+
274
+ Show a toast notification in the host application.
275
+
276
+ ```typescript
277
+ import { showToast } from '@managespace/sdk/extensions';
278
+
279
+ showToast('Operation successful');
280
+ showToast('Something went wrong', 'error');
281
+ ```
282
+
283
+ ### createApiFetch(context)
284
+
285
+ Create a fetch function configured for ManageSpace API calls.
286
+
287
+ ```typescript
288
+ import { getContext, createApiFetch } from '@managespace/sdk/extensions';
289
+
290
+ const context = await getContext();
291
+ const apiFetch = createApiFetch(context);
292
+
293
+ const response = await apiFetch('/api/crm/customers/queries', {
294
+ method: 'POST',
295
+ body: JSON.stringify({
296
+ pageOptions: { offset: 0, limit: 20 },
297
+ }),
298
+ });
299
+ const data = await response.json();
300
+ ```
301
+
302
+ ### createBffFetch(context)
303
+
304
+ Create a fetch function for calling your extension's Backend for Frontend.
305
+
306
+ ```typescript
307
+ import { getContext, createBffFetch } from '@managespace/sdk/extensions';
308
+
309
+ const context = await getContext();
310
+ const bffFetch = createBffFetch(context);
311
+
312
+ if (bffFetch) {
313
+ const response = await bffFetch('/api/my-endpoint');
314
+ const data = await response.json();
315
+ }
316
+ ```
317
+
318
+ ### onEntityEvent(handler)
319
+
320
+ Subscribe to entity events from the ManageSpace host.
321
+
322
+ ```typescript
323
+ import { onEntityEvent } from '@managespace/sdk/extensions';
324
+
325
+ const unsubscribe = onEntityEvent((event) => {
326
+ console.log(`${event.entityType} ${event.entityId} was ${event.action}`);
327
+ // event.entityType: 'customer', 'asset', 'subscription', etc.
328
+ // event.action: 'created', 'updated', 'deleted'
329
+ });
330
+
331
+ // To stop listening:
332
+ unsubscribe();
333
+ ```
334
+
335
+ ---
336
+
337
+ ## ManageSpace API Reference
338
+
339
+ Use `createApiFetch(context)` to call these endpoints. All requests include authentication automatically.
340
+
341
+ ### Customers
342
+
343
+ #### List/Query Customers
344
+
345
+ ```typescript
346
+ const response = await apiFetch('/api/crm/customers/queries', {
347
+ method: 'POST',
348
+ body: JSON.stringify({
349
+ pageOptions: { offset: 0, limit: 20 },
350
+ filters: [{ field: 'name', operator: 'contains', value: 'Smith' }],
351
+ }),
352
+ });
353
+
354
+ // Response: { results: Customer[], total: number }
355
+ ```
356
+
357
+ **Customer Type:**
358
+
359
+ ```typescript
360
+ interface Customer {
361
+ id: string;
362
+ name: string;
363
+ description: string | null;
364
+ orgId: string;
365
+ createdAt: Date;
366
+ updatedAt: Date | null;
367
+ customerStatusId: string;
368
+ commercial: boolean;
369
+ contacts?: Contact[];
370
+ // ... additional fields
371
+ }
372
+ ```
373
+
374
+ #### Get Customer by ID
375
+
376
+ ```typescript
377
+ const response = await apiFetch(`/api/crm/customers/${customerId}`);
378
+ const customer = await response.json();
379
+ ```
380
+
381
+ ### Assets
382
+
383
+ #### List Assets
384
+
385
+ ```typescript
386
+ const response = await apiFetch('/api/assets');
387
+ // Response: { results: Asset[], total: number }
388
+ ```
389
+
390
+ #### Get Asset by ID
391
+
392
+ ```typescript
393
+ const response = await apiFetch(`/api/assets/${assetId}`);
394
+ const asset = await response.json();
395
+ ```
396
+
397
+ **Asset Type:**
398
+
399
+ ```typescript
400
+ interface Asset {
401
+ id: string;
402
+ name: string;
403
+ siteId: string;
404
+ assetClassId: string;
405
+ status: string;
406
+ width: number | null;
407
+ height: number | null;
408
+ depth: number | null;
409
+ // ... additional fields
410
+ }
411
+ ```
412
+
413
+ ### Subscriptions
414
+
415
+ #### List Subscriptions
416
+
417
+ ```typescript
418
+ const response = await apiFetch('/api/subscriptions/queries', {
419
+ method: 'POST',
420
+ body: JSON.stringify({
421
+ pageOptions: { offset: 0, limit: 20 },
422
+ }),
423
+ });
424
+ // Response: { results: Subscription[], total: number }
425
+ ```
426
+
427
+ **Subscription Type:**
428
+
429
+ ```typescript
430
+ interface Subscription {
431
+ id: string;
432
+ customerId: string;
433
+ assetId: string;
434
+ planId: string;
435
+ status: string;
436
+ startDate: Date;
437
+ endDate: Date | null;
438
+ // ... additional fields
439
+ }
440
+ ```
441
+
442
+ ### Sites
443
+
444
+ #### List Sites
445
+
446
+ ```typescript
447
+ const response = await apiFetch('/api/sites');
448
+ // Response: { results: Site[], total: number }
449
+ ```
450
+
451
+ ### Invoices
452
+
453
+ #### List Invoices
454
+
455
+ ```typescript
456
+ const response = await apiFetch('/api/billing/invoices/queries', {
457
+ method: 'POST',
458
+ body: JSON.stringify({
459
+ pageOptions: { offset: 0, limit: 20 },
460
+ }),
461
+ });
462
+ // Response: { results: Invoice[], total: number }
463
+ ```
464
+
465
+ ### Payments
466
+
467
+ #### List Payments
468
+
469
+ ```typescript
470
+ const response = await apiFetch('/api/billing/payments/queries', {
471
+ method: 'POST',
472
+ body: JSON.stringify({
473
+ pageOptions: { offset: 0, limit: 20 },
474
+ }),
475
+ });
476
+ // Response: { results: Payment[], total: number }
477
+ ```
478
+
479
+ ---
480
+
481
+ ## Building a Backend for Frontend (BFF)
482
+
483
+ If your extension needs to call external APIs, perform complex data processing, or keep secrets secure, you can create a BFF.
484
+
485
+ ### 1. Update manifest.json
486
+
487
+ ```json
488
+ {
489
+ "name": "My Extension",
490
+ "version": "1.0.0",
491
+ "author": "Your Name",
492
+ "navLabel": "My Extension",
493
+ "navIcon": "Layout",
494
+ "navRoute": "/extensions/my-extension",
495
+ "entryPoint": "index.html",
496
+ "bff": {
497
+ "url": "https://my-bff.example.com"
498
+ }
499
+ }
500
+ ```
501
+
502
+ ### 2. Create BFF Server
503
+
504
+ Using Hono (recommended):
505
+
506
+ ```typescript
507
+ // server.ts
508
+ import { Hono } from 'hono';
509
+ import { cors } from 'hono/cors';
510
+
511
+ const app = new Hono();
512
+
513
+ // Enable CORS for your ManageSpace instance
514
+ app.use(
515
+ '*',
516
+ cors({
517
+ origin: ['https://your-managespace-instance.com'],
518
+ credentials: true,
519
+ }),
520
+ );
521
+
522
+ app.get('/api/my-endpoint', async (c) => {
523
+ // Get auth cookie forwarded from extension
524
+ const cookie = c.req.header('Cookie');
525
+
526
+ if (!cookie) {
527
+ return c.json({ error: 'Not authenticated' }, 401);
528
+ }
529
+
530
+ // Call ManageSpace API with forwarded credentials
531
+ const response = await fetch(
532
+ 'https://your-instance/api/crm/customers/queries',
533
+ {
534
+ method: 'POST',
535
+ headers: {
536
+ Cookie: cookie,
537
+ 'Content-Type': 'application/json',
538
+ },
539
+ body: JSON.stringify({ pageOptions: { offset: 0, limit: 20 } }),
540
+ },
541
+ );
542
+
543
+ const data = await response.json();
544
+
545
+ // Enrich or transform data
546
+ const enrichedData = data.results.map((customer) => ({
547
+ ...customer,
548
+ customField: 'added by BFF',
549
+ }));
550
+
551
+ return c.json({ customers: enrichedData });
552
+ });
553
+
554
+ export default {
555
+ port: 4000,
556
+ fetch: app.fetch,
557
+ };
558
+ ```
559
+
560
+ ### 3. Call BFF from Extension
561
+
562
+ ```typescript
563
+ import { getContext, createBffFetch } from '@managespace/sdk/extensions';
564
+
565
+ const context = await getContext();
566
+ const bffFetch = createBffFetch(context);
567
+
568
+ if (bffFetch) {
569
+ const response = await bffFetch('/api/my-endpoint');
570
+ const data = await response.json();
571
+ }
572
+ ```
573
+
574
+ ---
575
+
576
+ ## Building and Packaging
577
+
578
+ ### Development
579
+
580
+ ```bash
581
+ bun run dev
582
+ ```
583
+
584
+ This starts a local dev server. During development, the extension runs standalone and waits for context from the host.
585
+
586
+ ### Build
587
+
588
+ ```bash
589
+ bun run build
590
+ ```
591
+
592
+ Creates a production build in `dist/`.
593
+
594
+ ### Package for Upload (Required)
595
+
596
+ Every extension must include a `bundle` script in package.json:
597
+
598
+ ```json
599
+ {
600
+ "scripts": {
601
+ "bundle": "bun run build && cp manifest.json dist/ && cd dist && zip -r ../bundle.zip ."
602
+ }
603
+ }
604
+ ```
605
+
606
+ Run it to create the uploadable package:
607
+
608
+ ```bash
609
+ bun run bundle
610
+ ```
611
+
612
+ This creates `bundle.zip` containing:
613
+
614
+ - `index.html` - Entry point
615
+ - `assets/` - JS, CSS bundles
616
+ - `manifest.json` - Extension metadata (copied from project root)
617
+
618
+ Upload this ZIP file through the ManageSpace Admin > Extensions page.
619
+
620
+ ---
621
+
622
+ ## Available Lucide Icons
623
+
624
+ For `navIcon` in manifest.json, use any Lucide icon name:
625
+
626
+ **Common icons:**
627
+
628
+ - `Users` - People/customers
629
+ - `Building` - Properties/sites
630
+ - `Box` - Assets/inventory
631
+ - `CreditCard` - Payments/billing
632
+ - `FileText` - Documents/invoices
633
+ - `Settings` - Configuration
634
+ - `BarChart` - Analytics/reports
635
+ - `Calendar` - Scheduling
636
+ - `Bell` - Notifications
637
+ - `Search` - Search functionality
638
+ - `Layout` - Dashboard/overview
639
+ - `Truck` - Delivery/logistics
640
+ - `Key` - Access/security
641
+ - `Mail` - Communications
642
+
643
+ See https://lucide.dev/icons for the full list.
644
+
645
+ ---
646
+
647
+ ## TypeScript Types
648
+
649
+ All types are exported from `@managespace/sdk/extensions`:
650
+
651
+ ```typescript
652
+ import type {
653
+ ExtensionContext,
654
+ ExtensionManifest,
655
+ HostMessage,
656
+ ExtensionMessage,
657
+ EntityEvent,
658
+ EntityEventHandler,
659
+ } from '@managespace/sdk/extensions';
660
+ ```
661
+
662
+ Entity types are available from the main SDK:
663
+
664
+ ```typescript
665
+ import type {
666
+ Customer,
667
+ Asset,
668
+ Subscription,
669
+ Site,
670
+ Invoice,
671
+ Payment,
672
+ Contact,
673
+ } from '@managespace/sdk';
674
+ ```
675
+
676
+ ---
677
+
678
+ ## Example: Customer List Extension
679
+
680
+ Complete example showing customers with click-to-navigate:
681
+
682
+ ```typescript
683
+ // src/App.tsx
684
+ import { useEffect, useState } from 'react';
685
+ import {
686
+ getContext,
687
+ createApiFetch,
688
+ navigate,
689
+ showToast,
690
+ type ExtensionContext,
691
+ } from '@managespace/sdk/extensions';
692
+ import type { Customer } from '@managespace/sdk';
693
+
694
+ export default function App() {
695
+ const [context, setContext] = useState<ExtensionContext | null>(null);
696
+ const [customers, setCustomers] = useState<Customer[]>([]);
697
+ const [loading, setLoading] = useState(true);
698
+
699
+ useEffect(() => {
700
+ getContext().then(setContext);
701
+ }, []);
702
+
703
+ useEffect(() => {
704
+ if (!context) return;
705
+
706
+ const apiFetch = createApiFetch(context);
707
+
708
+ apiFetch('/api/crm/customers/queries', {
709
+ method: 'POST',
710
+ body: JSON.stringify({
711
+ pageOptions: { offset: 0, limit: 20 }
712
+ })
713
+ })
714
+ .then(res => res.json())
715
+ .then(data => setCustomers(data.results || []))
716
+ .catch(() => showToast('Failed to load customers', 'error'))
717
+ .finally(() => setLoading(false));
718
+ }, [context]);
719
+
720
+ if (loading) return <div>Loading...</div>;
721
+
722
+ return (
723
+ <div>
724
+ <h1>Customers</h1>
725
+ <ul>
726
+ {customers.map(customer => (
727
+ <li
728
+ key={customer.id}
729
+ onClick={() => navigate(`/customer/${customer.id}`)}
730
+ style={{ cursor: 'pointer' }}
731
+ >
732
+ {customer.name}
733
+ </li>
734
+ ))}
735
+ </ul>
736
+ </div>
737
+ );
738
+ }
739
+ ```
740
+
741
+ ---
742
+
743
+ ## Publishing the SDK
744
+
745
+ ```bash
746
+ # Update version in package.json
747
+ npm install
5
748
  npm run build
6
749
  npm publish
750
+ ```