foundrycms 0.1.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/dist/__tests__/foundry.test.d.ts +2 -0
  4. package/dist/__tests__/foundry.test.d.ts.map +1 -0
  5. package/dist/__tests__/foundry.test.js +1013 -0
  6. package/dist/__tests__/foundry.test.js.map +1 -0
  7. package/dist/config-manager.d.ts +33 -0
  8. package/dist/config-manager.d.ts.map +1 -0
  9. package/dist/config-manager.js +169 -0
  10. package/dist/config-manager.js.map +1 -0
  11. package/dist/hook-system.d.ts +61 -0
  12. package/dist/hook-system.d.ts.map +1 -0
  13. package/dist/hook-system.js +114 -0
  14. package/dist/hook-system.js.map +1 -0
  15. package/dist/index.d.ts +47 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +82 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/page-builder/element-registry.d.ts +47 -0
  20. package/dist/page-builder/element-registry.d.ts.map +1 -0
  21. package/dist/page-builder/element-registry.js +98 -0
  22. package/dist/page-builder/element-registry.js.map +1 -0
  23. package/dist/page-builder/elements/index.d.ts +22 -0
  24. package/dist/page-builder/elements/index.d.ts.map +1 -0
  25. package/dist/page-builder/elements/index.js +770 -0
  26. package/dist/page-builder/elements/index.js.map +1 -0
  27. package/dist/page-builder/renderer.d.ts +14 -0
  28. package/dist/page-builder/renderer.d.ts.map +1 -0
  29. package/dist/page-builder/renderer.js +240 -0
  30. package/dist/page-builder/renderer.js.map +1 -0
  31. package/dist/page-builder/serializer.d.ts +1220 -0
  32. package/dist/page-builder/serializer.d.ts.map +1 -0
  33. package/dist/page-builder/serializer.js +111 -0
  34. package/dist/page-builder/serializer.js.map +1 -0
  35. package/dist/page-builder/template-studio.d.ts +37 -0
  36. package/dist/page-builder/template-studio.d.ts.map +1 -0
  37. package/dist/page-builder/template-studio.js +923 -0
  38. package/dist/page-builder/template-studio.js.map +1 -0
  39. package/dist/page-builder/types.d.ts +99 -0
  40. package/dist/page-builder/types.d.ts.map +1 -0
  41. package/dist/page-builder/types.js +5 -0
  42. package/dist/page-builder/types.js.map +1 -0
  43. package/dist/plugin-system.d.ts +128 -0
  44. package/dist/plugin-system.d.ts.map +1 -0
  45. package/dist/plugin-system.js +252 -0
  46. package/dist/plugin-system.js.map +1 -0
  47. package/dist/plugins/communication.d.ts +6 -0
  48. package/dist/plugins/communication.d.ts.map +1 -0
  49. package/dist/plugins/communication.js +922 -0
  50. package/dist/plugins/communication.js.map +1 -0
  51. package/dist/plugins/core.d.ts +6 -0
  52. package/dist/plugins/core.d.ts.map +1 -0
  53. package/dist/plugins/core.js +675 -0
  54. package/dist/plugins/core.js.map +1 -0
  55. package/dist/plugins/growth.d.ts +6 -0
  56. package/dist/plugins/growth.d.ts.map +1 -0
  57. package/dist/plugins/growth.js +668 -0
  58. package/dist/plugins/growth.js.map +1 -0
  59. package/dist/plugins/index.d.ts +8 -0
  60. package/dist/plugins/index.d.ts.map +1 -0
  61. package/dist/plugins/index.js +43 -0
  62. package/dist/plugins/index.js.map +1 -0
  63. package/dist/plugins/operations.d.ts +7 -0
  64. package/dist/plugins/operations.d.ts.map +1 -0
  65. package/dist/plugins/operations.js +930 -0
  66. package/dist/plugins/operations.js.map +1 -0
  67. package/dist/theme/presets.d.ts +8 -0
  68. package/dist/theme/presets.d.ts.map +1 -0
  69. package/dist/theme/presets.js +257 -0
  70. package/dist/theme/presets.js.map +1 -0
  71. package/dist/theme/types.d.ts +83 -0
  72. package/dist/theme/types.d.ts.map +1 -0
  73. package/dist/theme/types.js +5 -0
  74. package/dist/theme/types.js.map +1 -0
  75. package/package.json +38 -0
@@ -0,0 +1,930 @@
1
+ import { z } from 'zod';
2
+ // ---------------------------------------------------------------------------
3
+ // Roster — Appointments
4
+ // ---------------------------------------------------------------------------
5
+ export const rosterPlugin = {
6
+ id: '@foundry/roster',
7
+ name: 'Roster',
8
+ version: '1.0.0',
9
+ description: 'Appointment scheduling with booking widget, calendar sync, reminders, and timezone handling',
10
+ adminPages: [
11
+ {
12
+ path: 'roster',
13
+ label: 'Appointments',
14
+ icon: 'calendar',
15
+ component: 'RosterDashboardPage',
16
+ order: 30,
17
+ permission: 'roster:view',
18
+ children: [
19
+ {
20
+ path: 'calendar',
21
+ label: 'Calendar',
22
+ component: 'RosterCalendarPage',
23
+ permission: 'roster:view',
24
+ },
25
+ {
26
+ path: 'bookings',
27
+ label: 'Bookings',
28
+ component: 'RosterBookingsPage',
29
+ permission: 'roster:view',
30
+ },
31
+ {
32
+ path: 'availability',
33
+ label: 'Availability',
34
+ component: 'RosterAvailabilityPage',
35
+ permission: 'roster:manage',
36
+ },
37
+ {
38
+ path: 'services',
39
+ label: 'Services',
40
+ component: 'RosterServicesPage',
41
+ permission: 'roster:manage',
42
+ },
43
+ ],
44
+ },
45
+ ],
46
+ apiRoutes: [
47
+ {
48
+ method: 'GET',
49
+ path: '/api/roster/availability',
50
+ handler: 'roster.getAvailability',
51
+ description: 'Get available time slots for a date range (public)',
52
+ },
53
+ {
54
+ method: 'POST',
55
+ path: '/api/roster/bookings',
56
+ handler: 'roster.createBooking',
57
+ description: 'Create a new booking (public)',
58
+ },
59
+ {
60
+ method: 'GET',
61
+ path: '/api/roster/bookings',
62
+ handler: 'roster.listBookings',
63
+ middleware: ['auth', 'permission:roster:view'],
64
+ description: 'List all bookings',
65
+ },
66
+ {
67
+ method: 'PATCH',
68
+ path: '/api/roster/bookings/:id',
69
+ handler: 'roster.updateBooking',
70
+ middleware: ['auth', 'permission:roster:manage'],
71
+ description: 'Update a booking (confirm, cancel, reschedule)',
72
+ },
73
+ {
74
+ method: 'DELETE',
75
+ path: '/api/roster/bookings/:id',
76
+ handler: 'roster.cancelBooking',
77
+ middleware: ['auth'],
78
+ description: 'Cancel a booking',
79
+ },
80
+ {
81
+ method: 'GET',
82
+ path: '/api/roster/services',
83
+ handler: 'roster.listServices',
84
+ description: 'List bookable services (public)',
85
+ },
86
+ ],
87
+ pageElements: [
88
+ {
89
+ type: 'booking-widget',
90
+ name: 'Booking Widget',
91
+ category: 'interactive',
92
+ component: 'RosterBookingWidget',
93
+ settingsSchema: z.object({
94
+ serviceId: z.string().optional(),
95
+ showAllServices: z.boolean().default(true),
96
+ theme: z.enum(['light', 'dark', 'auto']).default('auto'),
97
+ primaryColor: z.string().default('#6366f1'),
98
+ showTimezone: z.boolean().default(true),
99
+ maxAdvanceDays: z.number().min(1).max(365).default(60),
100
+ }),
101
+ },
102
+ ],
103
+ widgets: [
104
+ {
105
+ id: 'roster-upcoming',
106
+ name: 'Upcoming Appointments',
107
+ component: 'RosterUpcomingWidget',
108
+ areas: ['dashboard', 'sidebar'],
109
+ defaultSize: { width: 4, height: 3 },
110
+ },
111
+ {
112
+ id: 'roster-today',
113
+ name: "Today's Schedule",
114
+ component: 'RosterTodayWidget',
115
+ areas: ['dashboard'],
116
+ defaultSize: { width: 3, height: 4 },
117
+ },
118
+ ],
119
+ hooks: [
120
+ {
121
+ hook: 'content:before_save',
122
+ handler: (payload) => {
123
+ if (payload.content?.type === 'booking') {
124
+ // Validate booking times
125
+ const start = new Date(payload.content.startTime);
126
+ const end = new Date(payload.content.endTime);
127
+ if (isNaN(start.getTime()) || isNaN(end.getTime())) {
128
+ throw new Error('Roster: Invalid booking time format');
129
+ }
130
+ if (end <= start) {
131
+ throw new Error('Roster: End time must be after start time');
132
+ }
133
+ if (start < new Date()) {
134
+ throw new Error('Roster: Cannot book in the past');
135
+ }
136
+ // Calculate duration in minutes
137
+ payload.content.durationMinutes = Math.round((end.getTime() - start.getTime()) / 60000);
138
+ // Set status
139
+ if (!payload.content.status) {
140
+ payload.content.status = 'pending';
141
+ }
142
+ }
143
+ return payload;
144
+ },
145
+ priority: 5,
146
+ },
147
+ {
148
+ hook: 'content:after_save',
149
+ handler: (payload) => {
150
+ if (payload.content?.type === 'booking' && payload.content?.status === 'confirmed') {
151
+ // Queue reminder notifications
152
+ // This would connect to the relay plugin for email/SMS
153
+ }
154
+ },
155
+ priority: 10,
156
+ },
157
+ ],
158
+ settingsSchema: z.object({
159
+ timezone: z.string().default('Europe/London'),
160
+ slotDuration: z.number().min(5).max(480).default(30),
161
+ bufferBetweenSlots: z.number().min(0).max(60).default(10),
162
+ workingHours: z.object({
163
+ monday: z.object({ start: z.string(), end: z.string(), enabled: z.boolean() }).default({ start: '09:00', end: '17:00', enabled: true }),
164
+ tuesday: z.object({ start: z.string(), end: z.string(), enabled: z.boolean() }).default({ start: '09:00', end: '17:00', enabled: true }),
165
+ wednesday: z.object({ start: z.string(), end: z.string(), enabled: z.boolean() }).default({ start: '09:00', end: '17:00', enabled: true }),
166
+ thursday: z.object({ start: z.string(), end: z.string(), enabled: z.boolean() }).default({ start: '09:00', end: '17:00', enabled: true }),
167
+ friday: z.object({ start: z.string(), end: z.string(), enabled: z.boolean() }).default({ start: '09:00', end: '17:00', enabled: true }),
168
+ saturday: z.object({ start: z.string(), end: z.string(), enabled: z.boolean() }).default({ start: '10:00', end: '14:00', enabled: false }),
169
+ sunday: z.object({ start: z.string(), end: z.string(), enabled: z.boolean() }).default({ start: '10:00', end: '14:00', enabled: false }),
170
+ }).default({}),
171
+ requireConfirmation: z.boolean().default(true),
172
+ sendReminders: z.boolean().default(true),
173
+ reminderHoursBefore: z.number().default(24),
174
+ maxBookingsPerDay: z.number().min(1).max(100).default(20),
175
+ }),
176
+ async onActivate(core) {
177
+ core.hooks.on('content:after_save', (payload) => {
178
+ if (payload.content?.type === 'booking' && payload.content?.status === 'confirmed') {
179
+ // Auto-create CRM contact from booking if compass is active
180
+ if (core.plugins.isActive('@foundry/compass') && payload.content.clientEmail) {
181
+ core.hooks.emit('content:before_save', {
182
+ content: {
183
+ type: 'contact',
184
+ email: payload.content.clientEmail,
185
+ name: payload.content.clientName,
186
+ source: 'booking',
187
+ lifecycleStage: 'lead',
188
+ },
189
+ });
190
+ }
191
+ }
192
+ });
193
+ },
194
+ async onDeactivate(_core) { },
195
+ };
196
+ // ---------------------------------------------------------------------------
197
+ // Depot — Inventory Management
198
+ // ---------------------------------------------------------------------------
199
+ export const depotPlugin = {
200
+ id: '@foundry/depot',
201
+ name: 'Depot',
202
+ version: '1.0.0',
203
+ description: 'Inventory management with stock tracking, low-stock alerts, suppliers, and purchase orders',
204
+ adminPages: [
205
+ {
206
+ path: 'depot',
207
+ label: 'Inventory',
208
+ icon: 'package',
209
+ component: 'DepotDashboardPage',
210
+ order: 35,
211
+ permission: 'depot:view',
212
+ children: [
213
+ {
214
+ path: 'items',
215
+ label: 'Items',
216
+ component: 'DepotItemsPage',
217
+ permission: 'depot:view',
218
+ },
219
+ {
220
+ path: 'items/:id',
221
+ label: 'Item Detail',
222
+ component: 'DepotItemDetailPage',
223
+ permission: 'depot:view',
224
+ },
225
+ {
226
+ path: 'categories',
227
+ label: 'Categories',
228
+ component: 'DepotCategoriesPage',
229
+ permission: 'depot:manage',
230
+ },
231
+ {
232
+ path: 'suppliers',
233
+ label: 'Suppliers',
234
+ component: 'DepotSuppliersPage',
235
+ permission: 'depot:manage',
236
+ },
237
+ {
238
+ path: 'orders',
239
+ label: 'Purchase Orders',
240
+ component: 'DepotOrdersPage',
241
+ permission: 'depot:orders',
242
+ },
243
+ {
244
+ path: 'stock-adjustments',
245
+ label: 'Adjustments',
246
+ component: 'DepotAdjustmentsPage',
247
+ permission: 'depot:manage',
248
+ },
249
+ ],
250
+ },
251
+ ],
252
+ apiRoutes: [
253
+ {
254
+ method: 'GET',
255
+ path: '/api/depot/items',
256
+ handler: 'depot.listItems',
257
+ middleware: ['auth'],
258
+ description: 'List inventory items with filters',
259
+ },
260
+ {
261
+ method: 'POST',
262
+ path: '/api/depot/items',
263
+ handler: 'depot.createItem',
264
+ middleware: ['auth', 'permission:depot:manage'],
265
+ description: 'Add new inventory item',
266
+ },
267
+ {
268
+ method: 'PATCH',
269
+ path: '/api/depot/items/:id/stock',
270
+ handler: 'depot.adjustStock',
271
+ middleware: ['auth', 'permission:depot:manage'],
272
+ description: 'Adjust stock level for an item',
273
+ },
274
+ {
275
+ method: 'GET',
276
+ path: '/api/depot/low-stock',
277
+ handler: 'depot.getLowStock',
278
+ middleware: ['auth'],
279
+ description: 'Get items below minimum stock threshold',
280
+ },
281
+ {
282
+ method: 'POST',
283
+ path: '/api/depot/purchase-orders',
284
+ handler: 'depot.createPurchaseOrder',
285
+ middleware: ['auth', 'permission:depot:orders'],
286
+ description: 'Create a purchase order',
287
+ },
288
+ ],
289
+ widgets: [
290
+ {
291
+ id: 'depot-stock-summary',
292
+ name: 'Stock Summary',
293
+ component: 'DepotStockSummaryWidget',
294
+ areas: ['dashboard'],
295
+ defaultSize: { width: 3, height: 2 },
296
+ },
297
+ {
298
+ id: 'depot-low-stock-alerts',
299
+ name: 'Low Stock Alerts',
300
+ component: 'DepotLowStockWidget',
301
+ areas: ['dashboard', 'sidebar'],
302
+ defaultSize: { width: 3, height: 3 },
303
+ },
304
+ ],
305
+ hooks: [
306
+ {
307
+ hook: 'content:before_save',
308
+ handler: (payload) => {
309
+ if (payload.content?.type === 'inventory_item') {
310
+ // Validate SKU format
311
+ if (payload.content.sku && !/^[A-Z0-9\-]{3,30}$/i.test(payload.content.sku)) {
312
+ throw new Error('Depot: SKU must be 3-30 alphanumeric characters or hyphens');
313
+ }
314
+ // Ensure non-negative stock
315
+ if (typeof payload.content.stockQuantity === 'number' && payload.content.stockQuantity < 0) {
316
+ throw new Error('Depot: Stock quantity cannot be negative');
317
+ }
318
+ // Track stock changes
319
+ if (payload.content._previousQuantity !== undefined) {
320
+ payload.content._stockChange = payload.content.stockQuantity - payload.content._previousQuantity;
321
+ }
322
+ }
323
+ return payload;
324
+ },
325
+ priority: 5,
326
+ },
327
+ {
328
+ hook: 'content:after_save',
329
+ handler: (payload) => {
330
+ if (payload.content?.type === 'inventory_item') {
331
+ const item = payload.content;
332
+ // Check for low stock alert
333
+ if (item.stockQuantity <= (item.minimumStock ?? 0)) {
334
+ // This would trigger a notification via the relay plugin
335
+ }
336
+ }
337
+ },
338
+ priority: 10,
339
+ },
340
+ ],
341
+ settingsSchema: z.object({
342
+ defaultMinimumStock: z.number().min(0).default(5),
343
+ enableLowStockAlerts: z.boolean().default(true),
344
+ lowStockNotifyEmail: z.string().email().optional(),
345
+ trackCostPrice: z.boolean().default(true),
346
+ enableBarcodes: z.boolean().default(false),
347
+ defaultUnit: z.string().default('units'),
348
+ stockValuationMethod: z.enum(['fifo', 'lifo', 'average']).default('average'),
349
+ }),
350
+ async onActivate(core) {
351
+ // Wire up stock deduction when market plugin sells a product
352
+ core.hooks.on('content:after_save', (payload) => {
353
+ if (payload.content?.type === 'order' && payload.content?.status === 'paid') {
354
+ // Deduct stock for each order line item
355
+ for (const item of payload.content.items ?? []) {
356
+ if (item.inventoryItemId) {
357
+ core.hooks.emit('content:before_save', {
358
+ content: {
359
+ type: 'inventory_item',
360
+ id: item.inventoryItemId,
361
+ stockQuantity: -item.quantity, // relative adjustment
362
+ _adjustment: true,
363
+ reason: `Order ${payload.content.id}`,
364
+ },
365
+ });
366
+ }
367
+ }
368
+ }
369
+ });
370
+ },
371
+ async onDeactivate(_core) { },
372
+ };
373
+ // ---------------------------------------------------------------------------
374
+ // Stage — Events
375
+ // ---------------------------------------------------------------------------
376
+ export const stagePlugin = {
377
+ id: '@foundry/stage',
378
+ name: 'Stage',
379
+ version: '1.0.0',
380
+ description: 'Event management with ticketing, RSVP, event pages, and attendee tracking',
381
+ adminPages: [
382
+ {
383
+ path: 'stage',
384
+ label: 'Events',
385
+ icon: 'calendar-check',
386
+ component: 'StageDashboardPage',
387
+ order: 32,
388
+ permission: 'stage:view',
389
+ children: [
390
+ {
391
+ path: 'events',
392
+ label: 'All Events',
393
+ component: 'StageEventsPage',
394
+ permission: 'stage:view',
395
+ },
396
+ {
397
+ path: 'events/new',
398
+ label: 'Create Event',
399
+ component: 'StageEventEditorPage',
400
+ permission: 'stage:create',
401
+ },
402
+ {
403
+ path: 'attendees',
404
+ label: 'Attendees',
405
+ component: 'StageAttendeesPage',
406
+ permission: 'stage:view',
407
+ },
408
+ {
409
+ path: 'tickets',
410
+ label: 'Ticket Types',
411
+ component: 'StageTicketsPage',
412
+ permission: 'stage:manage',
413
+ },
414
+ ],
415
+ },
416
+ ],
417
+ apiRoutes: [
418
+ {
419
+ method: 'GET',
420
+ path: '/api/stage/events',
421
+ handler: 'stage.listEvents',
422
+ description: 'List upcoming events (public)',
423
+ },
424
+ {
425
+ method: 'GET',
426
+ path: '/api/stage/events/:slug',
427
+ handler: 'stage.getEvent',
428
+ description: 'Get event details by slug (public)',
429
+ },
430
+ {
431
+ method: 'POST',
432
+ path: '/api/stage/events',
433
+ handler: 'stage.createEvent',
434
+ middleware: ['auth', 'permission:stage:create'],
435
+ description: 'Create a new event',
436
+ },
437
+ {
438
+ method: 'POST',
439
+ path: '/api/stage/events/:id/rsvp',
440
+ handler: 'stage.rsvp',
441
+ description: 'RSVP to an event (public)',
442
+ },
443
+ {
444
+ method: 'POST',
445
+ path: '/api/stage/events/:id/tickets/purchase',
446
+ handler: 'stage.purchaseTicket',
447
+ description: 'Purchase tickets for an event',
448
+ },
449
+ {
450
+ method: 'GET',
451
+ path: '/api/stage/events/:id/attendees',
452
+ handler: 'stage.listAttendees',
453
+ middleware: ['auth', 'permission:stage:view'],
454
+ description: 'List event attendees',
455
+ },
456
+ ],
457
+ pageElements: [
458
+ {
459
+ type: 'event-card',
460
+ name: 'Event Card',
461
+ category: 'content',
462
+ component: 'StageEventCard',
463
+ settingsSchema: z.object({
464
+ eventId: z.string().optional(),
465
+ showDate: z.boolean().default(true),
466
+ showLocation: z.boolean().default(true),
467
+ showTicketPrice: z.boolean().default(true),
468
+ showAvailability: z.boolean().default(true),
469
+ ctaText: z.string().default('Register Now'),
470
+ }),
471
+ },
472
+ {
473
+ type: 'event-list',
474
+ name: 'Event List',
475
+ category: 'content',
476
+ component: 'StageEventList',
477
+ settingsSchema: z.object({
478
+ limit: z.number().min(1).max(20).default(6),
479
+ layout: z.enum(['grid', 'list', 'timeline']).default('grid'),
480
+ showPast: z.boolean().default(false),
481
+ category: z.string().optional(),
482
+ }),
483
+ },
484
+ ],
485
+ widgets: [
486
+ {
487
+ id: 'stage-upcoming-events',
488
+ name: 'Upcoming Events',
489
+ component: 'StageUpcomingEventsWidget',
490
+ areas: ['dashboard', 'sidebar'],
491
+ defaultSize: { width: 4, height: 3 },
492
+ },
493
+ ],
494
+ hooks: [
495
+ {
496
+ hook: 'content:before_save',
497
+ handler: (payload) => {
498
+ if (payload.content?.type === 'event') {
499
+ const event = payload.content;
500
+ // Validate event dates
501
+ if (event.startDate && event.endDate) {
502
+ const start = new Date(event.startDate);
503
+ const end = new Date(event.endDate);
504
+ if (end < start) {
505
+ throw new Error('Stage: Event end date must be after start date');
506
+ }
507
+ }
508
+ // Generate slug
509
+ if (event.title && !event.slug) {
510
+ event.slug = event.title
511
+ .toLowerCase()
512
+ .replace(/[^a-z0-9]+/g, '-')
513
+ .replace(/^-|-$/g, '');
514
+ }
515
+ // Calculate ticket availability
516
+ if (event.ticketTypes) {
517
+ event.totalCapacity = event.ticketTypes.reduce((sum, t) => sum + (t.maxQuantity ?? 0), 0);
518
+ event.ticketsSold = event.ticketTypes.reduce((sum, t) => sum + (t.sold ?? 0), 0);
519
+ event.ticketsAvailable = event.totalCapacity - event.ticketsSold;
520
+ }
521
+ }
522
+ return payload;
523
+ },
524
+ priority: 5,
525
+ },
526
+ ],
527
+ settingsSchema: z.object({
528
+ defaultTimezone: z.string().default('Europe/London'),
529
+ enableTicketing: z.boolean().default(true),
530
+ enableRsvp: z.boolean().default(true),
531
+ requireApproval: z.boolean().default(false),
532
+ maxAttendeesDefault: z.number().min(1).default(100),
533
+ sendConfirmationEmail: z.boolean().default(true),
534
+ calendarExport: z.boolean().default(true),
535
+ }),
536
+ async onActivate(core) {
537
+ core.hooks.on('content:after_save', (payload) => {
538
+ if (payload.content?.type === 'event_registration') {
539
+ // Update attendee count and trigger confirmation
540
+ if (core.plugins.isActive('@foundry/compass')) {
541
+ core.hooks.emit('content:before_save', {
542
+ content: {
543
+ type: 'contact',
544
+ email: payload.content.attendeeEmail,
545
+ name: payload.content.attendeeName,
546
+ source: 'event',
547
+ lifecycleStage: 'lead',
548
+ },
549
+ });
550
+ }
551
+ }
552
+ });
553
+ },
554
+ async onDeactivate(_core) { },
555
+ };
556
+ // ---------------------------------------------------------------------------
557
+ // Blueprint — Project Management
558
+ // ---------------------------------------------------------------------------
559
+ export const blueprintPlugin = {
560
+ id: '@foundry/blueprint',
561
+ name: 'Blueprint',
562
+ version: '1.0.0',
563
+ description: 'Project management with task boards, time tracking, milestones, and client project portals',
564
+ adminPages: [
565
+ {
566
+ path: 'blueprint',
567
+ label: 'Projects',
568
+ icon: 'layout',
569
+ component: 'BlueprintDashboardPage',
570
+ order: 12,
571
+ permission: 'blueprint:view',
572
+ children: [
573
+ {
574
+ path: 'projects',
575
+ label: 'All Projects',
576
+ component: 'BlueprintProjectsPage',
577
+ permission: 'blueprint:view',
578
+ },
579
+ {
580
+ path: 'projects/:id',
581
+ label: 'Project Detail',
582
+ component: 'BlueprintProjectDetailPage',
583
+ permission: 'blueprint:view',
584
+ },
585
+ {
586
+ path: 'board',
587
+ label: 'Task Board',
588
+ component: 'BlueprintBoardPage',
589
+ permission: 'blueprint:view',
590
+ },
591
+ {
592
+ path: 'time-tracking',
593
+ label: 'Time Tracking',
594
+ component: 'BlueprintTimeTrackingPage',
595
+ permission: 'blueprint:time',
596
+ },
597
+ {
598
+ path: 'milestones',
599
+ label: 'Milestones',
600
+ component: 'BlueprintMilestonesPage',
601
+ permission: 'blueprint:manage',
602
+ },
603
+ ],
604
+ },
605
+ ],
606
+ apiRoutes: [
607
+ {
608
+ method: 'GET',
609
+ path: '/api/blueprint/projects',
610
+ handler: 'blueprint.listProjects',
611
+ middleware: ['auth'],
612
+ description: 'List all projects',
613
+ },
614
+ {
615
+ method: 'POST',
616
+ path: '/api/blueprint/projects',
617
+ handler: 'blueprint.createProject',
618
+ middleware: ['auth', 'permission:blueprint:create'],
619
+ description: 'Create a new project',
620
+ },
621
+ {
622
+ method: 'GET',
623
+ path: '/api/blueprint/projects/:id/tasks',
624
+ handler: 'blueprint.listTasks',
625
+ middleware: ['auth'],
626
+ description: 'List tasks for a project',
627
+ },
628
+ {
629
+ method: 'POST',
630
+ path: '/api/blueprint/tasks',
631
+ handler: 'blueprint.createTask',
632
+ middleware: ['auth', 'permission:blueprint:edit'],
633
+ description: 'Create a task',
634
+ },
635
+ {
636
+ method: 'PATCH',
637
+ path: '/api/blueprint/tasks/:id',
638
+ handler: 'blueprint.updateTask',
639
+ middleware: ['auth', 'permission:blueprint:edit'],
640
+ description: 'Update a task (status, assignment, etc.)',
641
+ },
642
+ {
643
+ method: 'POST',
644
+ path: '/api/blueprint/time-entries',
645
+ handler: 'blueprint.logTime',
646
+ middleware: ['auth', 'permission:blueprint:time'],
647
+ description: 'Log time against a task',
648
+ },
649
+ {
650
+ method: 'GET',
651
+ path: '/api/blueprint/time-entries',
652
+ handler: 'blueprint.listTimeEntries',
653
+ middleware: ['auth'],
654
+ description: 'List time entries with filters',
655
+ },
656
+ ],
657
+ widgets: [
658
+ {
659
+ id: 'blueprint-active-projects',
660
+ name: 'Active Projects',
661
+ component: 'BlueprintActiveProjectsWidget',
662
+ areas: ['dashboard'],
663
+ defaultSize: { width: 4, height: 3 },
664
+ },
665
+ {
666
+ id: 'blueprint-my-tasks',
667
+ name: 'My Tasks',
668
+ component: 'BlueprintMyTasksWidget',
669
+ areas: ['dashboard', 'sidebar'],
670
+ defaultSize: { width: 3, height: 4 },
671
+ },
672
+ {
673
+ id: 'blueprint-time-summary',
674
+ name: 'Time This Week',
675
+ component: 'BlueprintTimeSummaryWidget',
676
+ areas: ['dashboard', 'sidebar'],
677
+ defaultSize: { width: 2, height: 2 },
678
+ },
679
+ ],
680
+ hooks: [
681
+ {
682
+ hook: 'content:before_save',
683
+ handler: (payload) => {
684
+ if (payload.content?.type === 'task') {
685
+ const task = payload.content;
686
+ // Validate priority
687
+ if (task.priority && !['low', 'medium', 'high', 'urgent'].includes(task.priority)) {
688
+ throw new Error('Blueprint: Invalid task priority');
689
+ }
690
+ // Auto-set due date based on project milestone if not set
691
+ if (!task.dueDate && task.milestoneId) {
692
+ task._inheritMilestoneDueDate = true;
693
+ }
694
+ // Track status transitions
695
+ if (task._previousStatus && task.status !== task._previousStatus) {
696
+ task._statusChangedAt = new Date().toISOString();
697
+ if (task.status === 'done') {
698
+ task.completedAt = new Date().toISOString();
699
+ }
700
+ }
701
+ }
702
+ // Time entry validation
703
+ if (payload.content?.type === 'time_entry') {
704
+ const entry = payload.content;
705
+ if (!entry.taskId && !entry.projectId) {
706
+ throw new Error('Blueprint: Time entry must be linked to a task or project');
707
+ }
708
+ if (typeof entry.minutes !== 'number' || entry.minutes <= 0) {
709
+ throw new Error('Blueprint: Time entry must have positive minutes');
710
+ }
711
+ entry.hours = Math.round((entry.minutes / 60) * 100) / 100;
712
+ }
713
+ return payload;
714
+ },
715
+ priority: 5,
716
+ },
717
+ ],
718
+ settingsSchema: z.object({
719
+ taskStatuses: z.array(z.object({
720
+ id: z.string(),
721
+ label: z.string(),
722
+ color: z.string(),
723
+ })).default([
724
+ { id: 'backlog', label: 'Backlog', color: '#94a3b8' },
725
+ { id: 'todo', label: 'To Do', color: '#6366f1' },
726
+ { id: 'in-progress', label: 'In Progress', color: '#f59e0b' },
727
+ { id: 'review', label: 'Review', color: '#8b5cf6' },
728
+ { id: 'done', label: 'Done', color: '#22c55e' },
729
+ ]),
730
+ defaultView: z.enum(['board', 'list', 'timeline']).default('board'),
731
+ enableTimeTracking: z.boolean().default(true),
732
+ hourlyRate: z.number().min(0).default(150),
733
+ currency: z.string().default('GBP'),
734
+ enableClientPortal: z.boolean().default(false),
735
+ }),
736
+ async onActivate(core) {
737
+ // Wire up project completion to invoicing
738
+ core.hooks.on('content:after_save', (payload) => {
739
+ if (payload.content?.type === 'project' && payload.content?.status === 'completed') {
740
+ // If ledger plugin is active, prompt for final invoice generation
741
+ if (core.plugins.isActive('@foundry/ledger')) {
742
+ core.hooks.emit('content:before_save', {
743
+ content: {
744
+ type: 'invoice',
745
+ action: 'generate_from_project',
746
+ projectId: payload.content.id,
747
+ clientId: payload.content.clientId,
748
+ },
749
+ });
750
+ }
751
+ }
752
+ });
753
+ },
754
+ async onDeactivate(_core) { },
755
+ };
756
+ // ---------------------------------------------------------------------------
757
+ // Pulse — Team Dashboard
758
+ // ---------------------------------------------------------------------------
759
+ export const pulsePlugin = {
760
+ id: '@foundry/pulse',
761
+ name: 'Pulse',
762
+ version: '1.0.0',
763
+ description: 'Team dashboard with status updates, activity feed, announcements, and KPI widgets',
764
+ adminPages: [
765
+ {
766
+ path: 'pulse',
767
+ label: 'Team',
768
+ icon: 'activity',
769
+ component: 'PulseDashboardPage',
770
+ order: 40,
771
+ permission: 'pulse:view',
772
+ children: [
773
+ {
774
+ path: 'feed',
775
+ label: 'Activity Feed',
776
+ component: 'PulseFeedPage',
777
+ permission: 'pulse:view',
778
+ },
779
+ {
780
+ path: 'status',
781
+ label: 'Team Status',
782
+ component: 'PulseStatusPage',
783
+ permission: 'pulse:view',
784
+ },
785
+ {
786
+ path: 'announcements',
787
+ label: 'Announcements',
788
+ component: 'PulseAnnouncementsPage',
789
+ permission: 'pulse:announce',
790
+ },
791
+ {
792
+ path: 'kpis',
793
+ label: 'KPIs',
794
+ component: 'PulseKpisPage',
795
+ permission: 'pulse:manage',
796
+ },
797
+ ],
798
+ },
799
+ ],
800
+ apiRoutes: [
801
+ {
802
+ method: 'GET',
803
+ path: '/api/pulse/feed',
804
+ handler: 'pulse.getFeed',
805
+ middleware: ['auth'],
806
+ description: 'Get team activity feed',
807
+ },
808
+ {
809
+ method: 'POST',
810
+ path: '/api/pulse/status',
811
+ handler: 'pulse.updateStatus',
812
+ middleware: ['auth'],
813
+ description: 'Update your team status',
814
+ },
815
+ {
816
+ method: 'GET',
817
+ path: '/api/pulse/status',
818
+ handler: 'pulse.getTeamStatus',
819
+ middleware: ['auth'],
820
+ description: 'Get all team member statuses',
821
+ },
822
+ {
823
+ method: 'POST',
824
+ path: '/api/pulse/announcements',
825
+ handler: 'pulse.createAnnouncement',
826
+ middleware: ['auth', 'permission:pulse:announce'],
827
+ description: 'Post a team announcement',
828
+ },
829
+ {
830
+ method: 'GET',
831
+ path: '/api/pulse/kpis',
832
+ handler: 'pulse.getKpis',
833
+ middleware: ['auth'],
834
+ description: 'Get KPI dashboard data',
835
+ },
836
+ ],
837
+ widgets: [
838
+ {
839
+ id: 'pulse-activity-feed',
840
+ name: 'Activity Feed',
841
+ component: 'PulseActivityFeedWidget',
842
+ areas: ['dashboard'],
843
+ defaultSize: { width: 4, height: 4 },
844
+ },
845
+ {
846
+ id: 'pulse-team-status',
847
+ name: 'Team Status',
848
+ component: 'PulseTeamStatusWidget',
849
+ areas: ['dashboard', 'sidebar'],
850
+ defaultSize: { width: 3, height: 3 },
851
+ },
852
+ {
853
+ id: 'pulse-announcements',
854
+ name: 'Announcements',
855
+ component: 'PulseAnnouncementsWidget',
856
+ areas: ['dashboard'],
857
+ defaultSize: { width: 4, height: 2 },
858
+ },
859
+ {
860
+ id: 'pulse-kpi-card',
861
+ name: 'KPI Card',
862
+ component: 'PulseKpiCardWidget',
863
+ areas: ['dashboard'],
864
+ defaultSize: { width: 2, height: 2 },
865
+ },
866
+ ],
867
+ hooks: [
868
+ {
869
+ hook: 'content:after_save',
870
+ handler: (payload) => {
871
+ // Log all content changes to the activity feed
872
+ if (payload.content && !payload.content._silent) {
873
+ const activity = {
874
+ type: 'activity',
875
+ action: payload.content._isNew ? 'created' : 'updated',
876
+ entityType: payload.content.type,
877
+ entityId: payload.content.id,
878
+ entityTitle: payload.content.title || payload.content.name || payload.content.id,
879
+ userId: payload.content._userId,
880
+ timestamp: new Date().toISOString(),
881
+ };
882
+ // Store activity entry (handled by the Pulse internal store)
883
+ _activityBuffer.push(activity);
884
+ if (_activityBuffer.length > 100) {
885
+ _activityBuffer.shift();
886
+ }
887
+ }
888
+ },
889
+ priority: 99, // run late to capture final state
890
+ },
891
+ {
892
+ hook: 'plugin:activated',
893
+ handler: (payload) => {
894
+ _activityBuffer.push({
895
+ type: 'activity',
896
+ action: 'plugin_activated',
897
+ entityType: 'plugin',
898
+ entityId: payload.plugin.id,
899
+ entityTitle: payload.plugin.name,
900
+ timestamp: new Date().toISOString(),
901
+ });
902
+ },
903
+ priority: 50,
904
+ },
905
+ ],
906
+ settingsSchema: z.object({
907
+ feedMaxItems: z.number().min(10).max(500).default(100),
908
+ enableStatusUpdates: z.boolean().default(true),
909
+ statusOptions: z.array(z.object({
910
+ id: z.string(),
911
+ label: z.string(),
912
+ emoji: z.string(),
913
+ })).default([
914
+ { id: 'available', label: 'Available', emoji: '🟢' },
915
+ { id: 'busy', label: 'Busy', emoji: '🔴' },
916
+ { id: 'away', label: 'Away', emoji: '🟡' },
917
+ { id: 'focus', label: 'Focus Mode', emoji: '🔵' },
918
+ ]),
919
+ enableKpis: z.boolean().default(true),
920
+ }),
921
+ async onActivate(_core) {
922
+ // Initialize activity buffer
923
+ _activityBuffer.length = 0;
924
+ },
925
+ async onDeactivate(_core) {
926
+ _activityBuffer.length = 0;
927
+ },
928
+ };
929
+ const _activityBuffer = [];
930
+ //# sourceMappingURL=operations.js.map