@nuxt-customer-portal/planning 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.9
4
+
5
+ ### Patch Changes
6
+
7
+ - @nuxt-customer-portal/core@0.4.9
8
+ - @nuxt-customer-portal/products@0.4.9
9
+ - @nuxt-customer-portal/ui@0.4.9
10
+
11
+ ## 0.4.8
12
+
13
+ ### Patch Changes
14
+
15
+ - Control Google Calendar availability synchronization with a provider appointment preference instead of the store environment.
16
+ - @nuxt-customer-portal/core@0.4.8
17
+ - @nuxt-customer-portal/products@0.4.8
18
+ - @nuxt-customer-portal/ui@0.4.8
19
+
3
20
  ## 0.4.7
4
21
 
5
22
  ### Patch Changes
@@ -11,6 +11,7 @@ const calendars = ref<Array<{ id: string; summary: string; accessRole: string }>
11
11
  const providerState = reactive({
12
12
  timezone: 'Europe/Amsterdam',
13
13
  graceMinutes: 0,
14
+ availabilitySyncEnabled: true,
14
15
  availabilityCalendarTitle: 'Portal availability',
15
16
  busyCalendarIds: [] as string[],
16
17
  writeCalendarId: ''
@@ -42,6 +43,7 @@ async function load() {
42
43
  Object.assign(providerState, {
43
44
  timezone: settings.timezone,
44
45
  graceMinutes: settings.graceMinutes,
46
+ availabilitySyncEnabled: settings.availabilitySyncEnabled,
45
47
  availabilityCalendarTitle: settings.availabilityCalendarTitle,
46
48
  busyCalendarIds: settings.busyCalendarIds,
47
49
  writeCalendarId: settings.writeCalendarId || ''
@@ -198,6 +200,14 @@ async function saveSettings() {
198
200
  ><h2 class="font-semibold">{{ t('planning.schedulingPreferences') }}</h2></template
199
201
  >
200
202
  <div class="grid gap-4 sm:grid-cols-2">
203
+ <UFormField
204
+ name="availabilitySyncEnabled"
205
+ :label="t('planning.availabilitySyncEnabled')"
206
+ :description="t('planning.availabilitySyncEnabledDescription')"
207
+ class="sm:col-span-2"
208
+ >
209
+ <USwitch v-model="providerState.availabilitySyncEnabled" />
210
+ </UFormField>
201
211
  <UFormField
202
212
  name="availabilityCalendarTitle"
203
213
  :label="t('planning.availabilityCalendarTitle')"
@@ -38,6 +38,7 @@ export interface ProviderConfiguration {
38
38
  enabled: boolean
39
39
  timezone: string
40
40
  graceMinutes: number
41
+ availabilitySyncEnabled: boolean
41
42
  availabilityCalendarTitle: string
42
43
  busyCalendarIds: string[]
43
44
  writeCalendarId: string | null
@@ -95,6 +95,8 @@
95
95
  "setMainCalendar": "Set main calendar",
96
96
  "calendarSettings": "Calendar settings",
97
97
  "graceMinutes": "Grace time after portal appointments (minutes)",
98
+ "availabilitySyncEnabled": "Sync availability to Google Calendar",
99
+ "availabilitySyncEnabledDescription": "Create and maintain calendar events for your portal availability. Turning this off removes existing availability events; appointments remain synchronized.",
98
100
  "availabilityCalendarTitle": "Availability calendar event title",
99
101
  "availabilityCalendarTitleDescription": "Shown as the title of availability events in your main calendar.",
100
102
  "busyCalendars": "Calendars that block availability",
@@ -95,6 +95,8 @@
95
95
  "setMainCalendar": "Hoofdagenda instellen",
96
96
  "calendarSettings": "Agenda-instellingen",
97
97
  "graceMinutes": "Rusttijd na portaalafspraken (minuten)",
98
+ "availabilitySyncEnabled": "Beschikbaarheid synchroniseren met Google Agenda",
99
+ "availabilitySyncEnabledDescription": "Maak en onderhoud agendagebeurtenissen voor je beschikbaarheid in het portaal. Als je dit uitschakelt, worden bestaande beschikbaarheidsgebeurtenissen verwijderd; afspraken blijven gesynchroniseerd.",
98
100
  "availabilityCalendarTitle": "Titel van beschikbaarheid in agenda",
99
101
  "availabilityCalendarTitleDescription": "Wordt als titel getoond bij beschikbaarheid in je hoofdagenda.",
100
102
  "busyCalendars": "Agenda’s die beschikbaarheid blokkeren",
@@ -0,0 +1,2 @@
1
+ ALTER TABLE planning.provider
2
+ ADD COLUMN availability_sync_enabled boolean NOT NULL DEFAULT true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxt-customer-portal/planning",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "license": "MIT",
5
5
  "homepage": "https://nuxt-customer-portal.com",
6
6
  "repository": {
@@ -40,9 +40,9 @@
40
40
  "drizzle-orm": "^0.45.2",
41
41
  "pg": "^8.22.0",
42
42
  "zod": "^4.4.3",
43
- "@nuxt-customer-portal/core": "^0.4.7",
44
- "@nuxt-customer-portal/ui": "^0.4.7",
45
- "@nuxt-customer-portal/products": "^0.4.7"
43
+ "@nuxt-customer-portal/ui": "^0.4.9",
44
+ "@nuxt-customer-portal/core": "^0.4.9",
45
+ "@nuxt-customer-portal/products": "^0.4.9"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "nuxt": "^4.5.1",
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  id: 'planning',
3
- version: '0.4.7',
3
+ version: '0.4.9',
4
4
  source: '@nuxt-customer-portal/planning',
5
5
  dependsOn: ['core', 'ui', 'products'],
6
6
  schema: './server/db/schema/planning.ts',
@@ -38,6 +38,7 @@ export const provider = schema.table(
38
38
  enabled: boolean('enabled').notNull().default(false),
39
39
  timezone: text('timezone').notNull().default('Europe/Amsterdam'),
40
40
  graceMinutes: integer('grace_minutes').notNull().default(0),
41
+ availabilitySyncEnabled: boolean('availability_sync_enabled').notNull().default(true),
41
42
  availabilityCalendarTitle: text('availability_calendar_title').notNull().default('Portal availability'),
42
43
  busyCalendarIds: text('busy_calendar_ids')
43
44
  .array()
@@ -11,7 +11,6 @@ import {
11
11
  } from '@nuxt-customer-portal/products/server/utils/orders'
12
12
  import { stripeProvider } from '@nuxt-customer-portal/products/server/utils/payments'
13
13
  import { baseUrl } from '@nuxt-customer-portal/products/server/utils/access'
14
- import { developmentSandboxEffectsEnabled } from '@nuxt-customer-portal/products/server/utils/development'
15
14
  import type { Appointment, AvailabilityWindow, Reservation } from '../../shared/types'
16
15
  import { calendarInvitation } from '../../shared/invitation'
17
16
  import { calendarWallDateTime, wallInstant } from '../../shared/availability'
@@ -274,20 +273,19 @@ async function availabilityEffects(id: string) {
274
273
  if (!window) {
275
274
  return
276
275
  }
277
- const [store] = await rows<{ mode: string }>('SELECT mode FROM products.store WHERE organization_id=$1', [
278
- window.store_id
279
- ])
280
- if (store?.mode === 'sandbox' && !developmentSandboxEffectsEnabled()) {
281
- return
282
- }
283
276
  const eventId = window.calendar_event_id || `a${id.replace(/-/g, '')}`
284
- const [provider] = await rows<{ write_calendar_id: string; availability_calendar_title: string }>(
285
- 'SELECT write_calendar_id,availability_calendar_title FROM planning.provider WHERE store_id=$1 AND user_id=$2',
277
+ const [provider] = await rows<{
278
+ write_calendar_id: string
279
+ availability_calendar_title: string
280
+ availability_sync_enabled: boolean
281
+ }>(
282
+ 'SELECT write_calendar_id,availability_calendar_title,availability_sync_enabled FROM planning.provider WHERE store_id=$1 AND user_id=$2',
286
283
  [window.store_id, window.user_id]
287
284
  )
288
- if (window.deleted) {
285
+ if (window.deleted || !provider?.availability_sync_enabled) {
289
286
  if (window.calendar_id) {
290
287
  await calendarAdapter().remove(window.store_id, window.user_id, window.calendar_id, eventId)
288
+ await rows('UPDATE planning.availability SET calendar_event_id=NULL,calendar_id=NULL WHERE id=$1', [id])
291
289
  }
292
290
  return
293
291
  }
@@ -7,7 +7,7 @@ import { stripeProvider } from '@nuxt-customer-portal/products/server/utils/paym
7
7
  import { reconcileCheckout, getOrder } from '@nuxt-customer-portal/products/server/utils/orders'
8
8
  import { getProduct } from '@nuxt-customer-portal/products/server/utils/catalog'
9
9
  import { productPlanningSchema, planningPolicySchema } from '@nuxt-customer-portal/products/shared/planning'
10
- import { canChange, changeFee, refundAmount } from '../../shared/availability'
10
+ import { canChange, changeFee, refundAmount, localParts } from '../../shared/availability'
11
11
  import { availabilityEditSchema, providerSettingsSchema } from '../../shared/validation'
12
12
  import type { Appointment, ProviderSettings, AvailabilityWindow } from '../../shared/types'
13
13
  import { planningAdmin, providerAccess, appointmentAccess } from './access'
@@ -18,7 +18,7 @@ import { auditLock } from './jobs'
18
18
  export async function listProviders(event: H3Event): Promise<ProviderSettings[]> {
19
19
  const context = await planningAdmin(event)
20
20
  return rows<ProviderSettings>(
21
- `SELECT u.id AS "userId",u.name,u.image,COALESCE(p.enabled,false) AS enabled,COALESCE(p.timezone,u.timezone,os.timezone,'Europe/Amsterdam') AS timezone,COALESCE(p.grace_minutes,0) AS "graceMinutes",COALESCE(p.busy_calendar_ids,'{}') AS "busyCalendarIds",p.write_calendar_id AS "writeCalendarId",EXISTS(SELECT 1 FROM planning.connection c WHERE c.store_id=m.organization_id AND c.user_id=u.id AND c.provider='google' AND c.healthy) AS "googleConnected",EXISTS(SELECT 1 FROM planning.connection c WHERE c.store_id=m.organization_id AND c.user_id=u.id AND c.provider='zoom' AND c.healthy) AS "zoomConnected" FROM public.member m JOIN public."user" u ON u.id=m.user_id LEFT JOIN planning.provider p ON p.store_id=m.organization_id AND p.user_id=u.id LEFT JOIN public.organization_settings os ON os.organization_id=m.organization_id WHERE m.organization_id=$1 ORDER BY u.name`,
21
+ `SELECT u.id AS "userId",u.name,u.image,COALESCE(p.enabled,false) AS enabled,COALESCE(p.timezone,u.timezone,os.timezone,'Europe/Amsterdam') AS timezone,COALESCE(p.grace_minutes,0) AS "graceMinutes",COALESCE(p.availability_sync_enabled,true) AS "availabilitySyncEnabled",COALESCE(p.busy_calendar_ids,'{}') AS "busyCalendarIds",p.write_calendar_id AS "writeCalendarId",EXISTS(SELECT 1 FROM planning.connection c WHERE c.store_id=m.organization_id AND c.user_id=u.id AND c.provider='google' AND c.healthy) AS "googleConnected",EXISTS(SELECT 1 FROM planning.connection c WHERE c.store_id=m.organization_id AND c.user_id=u.id AND c.provider='zoom' AND c.healthy) AS "zoomConnected" FROM public.member m JOIN public."user" u ON u.id=m.user_id LEFT JOIN planning.provider p ON p.store_id=m.organization_id AND p.user_id=u.id LEFT JOIN public.organization_settings os ON os.organization_id=m.organization_id WHERE m.organization_id=$1 ORDER BY u.name`,
22
22
  [context.organizationId]
23
23
  )
24
24
  }
@@ -48,6 +48,7 @@ export async function ownSettings(event: H3Event) {
48
48
  enabled: boolean
49
49
  timezone: string
50
50
  grace_minutes: number
51
+ availability_sync_enabled: boolean
51
52
  availability_calendar_title: string
52
53
  busy_calendar_ids: string[]
53
54
  write_calendar_id: string | null
@@ -69,6 +70,7 @@ export async function ownSettings(event: H3Event) {
69
70
  enabled: p!.enabled,
70
71
  timezone: p!.timezone,
71
72
  graceMinutes: p!.grace_minutes,
73
+ availabilitySyncEnabled: p!.availability_sync_enabled,
72
74
  availabilityCalendarTitle: p!.availability_calendar_title,
73
75
  busyCalendarIds: p!.busy_calendar_ids,
74
76
  writeCalendarId: p!.write_calendar_id,
@@ -89,7 +91,7 @@ export async function saveOwnSettings(event: H3Event, body: unknown) {
89
91
  await transaction(async (tx) => {
90
92
  await lockProvider(tx, userId)
91
93
  await tx.query(
92
- 'UPDATE planning.provider SET timezone=$3,grace_minutes=$4,busy_calendar_ids=$5,write_calendar_id=$6,availability_calendar_title=$7 WHERE store_id=$1 AND user_id=$2',
94
+ 'UPDATE planning.provider SET timezone=$3,grace_minutes=$4,busy_calendar_ids=$5,write_calendar_id=$6,availability_calendar_title=$7,availability_sync_enabled=$8 WHERE store_id=$1 AND user_id=$2',
93
95
  [
94
96
  storeId,
95
97
  userId,
@@ -97,12 +99,21 @@ export async function saveOwnSettings(event: H3Event, body: unknown) {
97
99
  input.graceMinutes,
98
100
  [...new Set([...input.busyCalendarIds, input.writeCalendarId])],
99
101
  input.writeCalendarId,
100
- input.availabilityCalendarTitle
102
+ input.availabilityCalendarTitle,
103
+ input.availabilitySyncEnabled
101
104
  ]
102
105
  )
106
+ const today = localParts(new Date(), input.timezone).date
103
107
  const windows = await rows<{ id: string; revision: number }>(
104
- "UPDATE planning.availability SET data=jsonb_set(data,'{timezone}',to_jsonb($3::text)),revision=revision+1 WHERE store_id=$1 AND user_id=$2 RETURNING id,revision",
105
- [storeId, userId, input.timezone],
108
+ `UPDATE planning.availability
109
+ SET data=jsonb_set(data,'{timezone}',to_jsonb($3::text)),revision=revision+1
110
+ WHERE store_id=$1 AND user_id=$2
111
+ AND ($4::boolean IS FALSE OR (NOT deleted AND (
112
+ data->>'date'>=$5 OR
113
+ (data->>'recurring'='true' AND (data->>'endDate' IS NULL OR data->>'endDate'>=$5))
114
+ )))
115
+ RETURNING id,revision`,
116
+ [storeId, userId, input.timezone, input.availabilitySyncEnabled, today],
106
117
  tx
107
118
  )
108
119
  for (const w of windows) {
package/shared/types.ts CHANGED
@@ -80,6 +80,7 @@ export interface ProviderSettings {
80
80
  enabled: boolean
81
81
  timezone: string
82
82
  graceMinutes: number
83
+ availabilitySyncEnabled: boolean
83
84
  availabilityCalendarTitle: string
84
85
  busyCalendarIds: string[]
85
86
  writeCalendarId: string | null
@@ -35,6 +35,7 @@ export const availabilitySchema = z.object(availabilityShape).superRefine(valida
35
35
  export const providerSettingsSchema = z.object({
36
36
  timezone: timezoneSchema,
37
37
  graceMinutes: z.number().int().min(0).max(1440),
38
+ availabilitySyncEnabled: z.boolean().default(true),
38
39
  availabilityCalendarTitle: z.string().trim().min(1).max(100),
39
40
  busyCalendarIds: z.array(z.string().min(1).max(500)).max(20),
40
41
  writeCalendarId: z.string().min(1).max(500)