@nuxt-customer-portal/planning 0.4.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 (96) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/LICENSE +21 -0
  3. package/README.md +115 -0
  4. package/app/components/PlanningAppointmentCreateForm.vue +49 -0
  5. package/app/components/PlanningAppointmentDetails.vue +163 -0
  6. package/app/components/PlanningAppointmentDrawer.vue +109 -0
  7. package/app/components/PlanningAvailabilityCalendar.vue +525 -0
  8. package/app/components/PlanningDashboardOverview.vue +62 -0
  9. package/app/components/PlanningDashboardUpcomingAppointments.vue +81 -0
  10. package/app/components/PlanningPersonalCalendarSettings.vue +218 -0
  11. package/app/components/PlanningPersonalConnection.vue +68 -0
  12. package/app/components/PlanningPersonalMeetingSettings.vue +1 -0
  13. package/app/components/PlanningPolicyFields.vue +196 -0
  14. package/app/components/PlanningSettingsMembers.vue +50 -0
  15. package/app/components/PlanningSettingsPolicy.vue +46 -0
  16. package/app/components/PlanningSettingsSynchronization.vue +433 -0
  17. package/app/components/PlanningSlotPicker.vue +588 -0
  18. package/app/components/PlanningTimezoneReadOnlyHover.vue +172 -0
  19. package/app/components/PlanningTimezoneSelect.vue +101 -0
  20. package/app/composables/usePlanning.ts +180 -0
  21. package/app/composables/usePlanningDashboard.ts +4 -0
  22. package/app/composables/usePlanningTimeDisplay.ts +15 -0
  23. package/app/pages/admin/planning.vue +36 -0
  24. package/app/pages/appointments/[id].vue +13 -0
  25. package/app/pages/appointments/index.vue +384 -0
  26. package/app/pages/availability.vue +585 -0
  27. package/app/pages/booking/[slug].vue +298 -0
  28. package/app/pages/my-appointment-settings.vue +33 -0
  29. package/app/plugins/planning.ts +5 -0
  30. package/i18n/locales/en.json +260 -0
  31. package/i18n/locales/nl.json +260 -0
  32. package/migrations/0000_baseline.sql +58 -0
  33. package/migrations/0001_availability_calendar_title.sql +2 -0
  34. package/migrations/0002_job_activity.sql +11 -0
  35. package/migrations/0003_deduplicate_availability_jobs.sql +19 -0
  36. package/migrations/0004_connection_label.sql +2 -0
  37. package/nuxt.config.ts +15 -0
  38. package/package.json +55 -0
  39. package/portal.manifest.mjs +8 -0
  40. package/server/api/planning/admin/appointments/[id]/audit.get.ts +11 -0
  41. package/server/api/planning/admin/appointments.get.ts +11 -0
  42. package/server/api/planning/admin/jobs/[id]/retry.post.ts +26 -0
  43. package/server/api/planning/admin/jobs.get.ts +199 -0
  44. package/server/api/planning/admin/policy.get.ts +6 -0
  45. package/server/api/planning/admin/policy.put.ts +8 -0
  46. package/server/api/planning/admin/products/[id].put.ts +8 -0
  47. package/server/api/planning/admin/providers/[userId].patch.ts +8 -0
  48. package/server/api/planning/admin/providers.get.ts +6 -0
  49. package/server/api/planning/admin/retry.post.ts +31 -0
  50. package/server/api/planning/appointments/[id]/availability.get.ts +19 -0
  51. package/server/api/planning/appointments/[id]/cancel.post.ts +8 -0
  52. package/server/api/planning/appointments/[id]/files/[assetId].get.ts +17 -0
  53. package/server/api/planning/appointments/[id]/files.get.ts +28 -0
  54. package/server/api/planning/appointments/[id]/pending.delete.ts +8 -0
  55. package/server/api/planning/appointments/[id]/reschedule.put.ts +8 -0
  56. package/server/api/planning/appointments/[id].get.ts +6 -0
  57. package/server/api/planning/appointments.get.ts +4 -0
  58. package/server/api/planning/availability/[id].delete.ts +12 -0
  59. package/server/api/planning/availability/[id].put.ts +8 -0
  60. package/server/api/planning/availability.get.ts +6 -0
  61. package/server/api/planning/availability.post.ts +8 -0
  62. package/server/api/planning/calendar-busy.get.ts +29 -0
  63. package/server/api/planning/calendars.get.ts +8 -0
  64. package/server/api/planning/dashboard.get.ts +36 -0
  65. package/server/api/planning/google/notifications.post.ts +24 -0
  66. package/server/api/planning/oauth/[provider]/callback.get.ts +18 -0
  67. package/server/api/planning/oauth/[provider]/start.post.ts +8 -0
  68. package/server/api/planning/oauth/[provider].delete.ts +15 -0
  69. package/server/api/planning/products.get.ts +11 -0
  70. package/server/api/planning/provider-appointments.get.ts +11 -0
  71. package/server/api/planning/provider.get.ts +6 -0
  72. package/server/api/planning/provider.put.ts +8 -0
  73. package/server/api/store/planning/[productId]/availability.get.ts +8 -0
  74. package/server/api/store/planning/hold.delete.ts +11 -0
  75. package/server/api/store/planning/hold.get.ts +36 -0
  76. package/server/api/store/planning/holds.post.ts +10 -0
  77. package/server/db/schema/planning.ts +213 -0
  78. package/server/plugins/openapi.ts +36 -0
  79. package/server/plugins/planning.ts +6 -0
  80. package/server/tasks/planning/reconcile.ts +30 -0
  81. package/server/utils/access.ts +80 -0
  82. package/server/utils/adapters.ts +468 -0
  83. package/server/utils/appointments.ts +37 -0
  84. package/server/utils/booking.ts +619 -0
  85. package/server/utils/crypto.ts +23 -0
  86. package/server/utils/jobs.ts +671 -0
  87. package/server/utils/management.ts +427 -0
  88. package/server/utils/oauth.ts +104 -0
  89. package/server/utils/staff-appointments.ts +158 -0
  90. package/shared/appointment-calendar.ts +82 -0
  91. package/shared/availability.ts +140 -0
  92. package/shared/emails.ts +99 -0
  93. package/shared/feature.ts +78 -0
  94. package/shared/invitation.ts +58 -0
  95. package/shared/types.ts +110 -0
  96. package/shared/validation.ts +77 -0
@@ -0,0 +1,110 @@
1
+ import type { PlanningPolicy } from '@nuxt-customer-portal/products/shared/planning'
2
+
3
+ export type { PlanningPolicy, ProductPlanning } from '@nuxt-customer-portal/products/shared/planning'
4
+ export interface Interval {
5
+ start: string
6
+ end: string
7
+ }
8
+ export interface AvailabilityWindow {
9
+ id: string
10
+ userId: string
11
+ timezone: string
12
+ date: string
13
+ endDate: string | null
14
+ startTime: string
15
+ endTime: string
16
+ recurring: boolean
17
+ productIds: string[] | null
18
+ exceptions: string[]
19
+ }
20
+ export interface Slot extends Interval {
21
+ providerUserId: string
22
+ providerName: string
23
+ }
24
+ export interface AppointmentSnapshot {
25
+ organizerEmail?: string
26
+ title: string
27
+ durationMinutes: number
28
+ graceMinutes: number
29
+ timezone: string
30
+ customerTimezone: string
31
+ policy: PlanningPolicy
32
+ currency: string
33
+ unitAmount: number
34
+ meetingProvider: 'none' | 'zoom'
35
+ customerNotificationRevision?: number
36
+ zoomLinkNotificationPending?: boolean
37
+ locale: 'en' | 'nl'
38
+ }
39
+ export interface Reservation {
40
+ id: string
41
+ store_id: string
42
+ user_id: string
43
+ product_id: string
44
+ token_hash: string
45
+ start_at: Date
46
+ end_at: Date
47
+ blocked_until: Date
48
+ confirmed_at: Date | null
49
+ expires_at: Date
50
+ status: 'reserved' | 'confirmed' | 'expired' | 'cancelled' | 'superseded'
51
+ order_id: string | null
52
+ replaces_id: string | null
53
+ snapshot: AppointmentSnapshot
54
+ }
55
+ export interface Appointment {
56
+ id: string
57
+ store_id: string
58
+ reservation_id: string
59
+ order_line_id: string
60
+ order_id: string
61
+ user_id: string
62
+ product_id: string
63
+ start_at: Date
64
+ end_at: Date
65
+ status: 'confirmed' | 'cancelled'
66
+ changes: number
67
+ revision: number
68
+ snapshot: AppointmentSnapshot
69
+ meeting_id: string | null
70
+ meeting_url: string | null
71
+ calendar_event_id: string | null
72
+ calendar_id: string | null
73
+ calendar_user_id: string | null
74
+ conflict: boolean
75
+ effects_error: string | null
76
+ }
77
+ export interface ProviderSettings {
78
+ userId: string
79
+ name: string
80
+ enabled: boolean
81
+ timezone: string
82
+ graceMinutes: number
83
+ availabilityCalendarTitle: string
84
+ busyCalendarIds: string[]
85
+ writeCalendarId: string | null
86
+ googleConnected: boolean
87
+ zoomConnected: boolean
88
+ }
89
+
90
+ export interface PlanningDashboardAppointment {
91
+ id: string
92
+ title: string
93
+ start: string
94
+ end: string
95
+ meetingProvider: 'none' | 'zoom'
96
+ meetingUrl: string | null
97
+ providerName: string
98
+ providerTimezone: string
99
+ customerName: string
100
+ customerTimezone: string
101
+ conflict: boolean
102
+ }
103
+
104
+ export interface PlanningDashboardDto {
105
+ access: { staff: boolean; canManage: boolean }
106
+ upcomingCount: number
107
+ todayCount: number
108
+ conflictCount: number
109
+ appointments: PlanningDashboardAppointment[]
110
+ }
@@ -0,0 +1,77 @@
1
+ import { z } from 'zod'
2
+ import { isValidTimezone } from '@nuxt-customer-portal/core/shared/timezone'
3
+
4
+ export { planningPolicySchema } from '@nuxt-customer-portal/products/shared/planning'
5
+ export const timezoneSchema = z.string().max(100).refine(isValidTimezone, 'Choose an IANA timezone')
6
+ const date = z
7
+ .string()
8
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
9
+ .refine((v) => Number.isFinite(Date.parse(v)) && new Date(v).toISOString().slice(0, 10) === v, 'Invalid date')
10
+ const time = z.string().regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/)
11
+ export const availabilitySchema = z
12
+ .object({
13
+ date,
14
+ endDate: date.nullable().default(null),
15
+ startTime: time,
16
+ endTime: z
17
+ .string()
18
+ .transform((value) => (value === '00:00' ? '24:00' : value))
19
+ .pipe(z.union([time, z.literal('24:00')])),
20
+ recurring: z.boolean().default(false),
21
+ productIds: z.array(z.string().min(1).max(100)).min(1).max(100).nullable().default(null),
22
+ exceptions: z.array(date).max(365).default([])
23
+ })
24
+ .superRefine((v, c) => {
25
+ if (v.endTime <= v.startTime) {
26
+ c.addIssue({ code: 'custom', path: ['endTime'], message: 'End must follow start on the same day' })
27
+ }
28
+ if (v.endDate && v.endDate < v.date) {
29
+ c.addIssue({ code: 'custom', path: ['endDate'], message: 'End date must follow start date' })
30
+ }
31
+ })
32
+ export const providerSettingsSchema = z.object({
33
+ timezone: timezoneSchema,
34
+ graceMinutes: z.number().int().min(0).max(1440),
35
+ availabilityCalendarTitle: z.string().trim().min(1).max(100),
36
+ busyCalendarIds: z.array(z.string().min(1).max(500)).min(1).max(20),
37
+ writeCalendarId: z.string().min(1).max(500)
38
+ })
39
+ export const availabilityQuerySchema = z
40
+ .object({
41
+ from: z.iso.datetime({ offset: true }),
42
+ to: z.iso.datetime({ offset: true }),
43
+ providerUserId: z.string().max(100).optional()
44
+ })
45
+ .refine(
46
+ (v) => Date.parse(v.to) > Date.parse(v.from) && Date.parse(v.to) - Date.parse(v.from) <= 31 * 86400000,
47
+ 'Request at most 31 days'
48
+ )
49
+ export const holdSchema = z.object({
50
+ productId: z.string().min(1).max(100),
51
+ providerUserId: z.string().min(1).max(100),
52
+ start: z.iso.datetime({ offset: true }),
53
+ customerTimezone: timezoneSchema,
54
+ currency: z.string().regex(/^[A-Z]{3}$/),
55
+ locale: z.enum(['en', 'nl']).default('en'),
56
+ replacesId: z.uuid().optional(),
57
+ previousHoldToken: z.string().min(32).max(200).optional()
58
+ })
59
+ export const holdCredentialSchema = z.object({ holdToken: z.string().min(32).max(200) })
60
+
61
+ export const availabilityEditSchema = availabilitySchema.safeExtend({ occurrenceDate: date.optional() })
62
+
63
+ export const appointmentListSchema = z.object({
64
+ search: z.string().trim().max(200).default(''),
65
+ status: z.enum(['all', 'confirmed', 'cancelled']).default('all'),
66
+ conflicts: z.enum(['all', 'conflicts']).default('all'),
67
+ host: z.string().trim().min(1).max(100).default('all'),
68
+ sortBy: z.enum(['start', 'title']).default('start'),
69
+ sortOrder: z.enum(['asc', 'desc']).default('desc'),
70
+ page: z.coerce.number().int().min(1).max(100000).default(1)
71
+ })
72
+ export const staffRescheduleSchema = z.object({
73
+ providerUserId: z.string().min(1).max(100),
74
+ start: z.iso.datetime({ offset: true }),
75
+ customerTimezone: timezoneSchema,
76
+ revision: z.number().int().min(0)
77
+ })