@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 +17 -0
- package/app/components/PlanningPersonalCalendarSettings.vue +10 -0
- package/app/composables/usePlanning.ts +1 -0
- package/i18n/locales/en.json +2 -0
- package/i18n/locales/nl.json +2 -0
- package/migrations/0005_availability_sync_enabled.sql +2 -0
- package/package.json +4 -4
- package/portal.manifest.mjs +1 -1
- package/server/db/schema/planning.ts +1 -0
- package/server/utils/jobs.ts +8 -10
- package/server/utils/management.ts +17 -6
- package/shared/types.ts +1 -0
- package/shared/validation.ts +1 -0
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')"
|
package/i18n/locales/en.json
CHANGED
|
@@ -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",
|
package/i18n/locales/nl.json
CHANGED
|
@@ -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",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxt-customer-portal/planning",
|
|
3
|
-
"version": "0.4.
|
|
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/
|
|
44
|
-
"@nuxt-customer-portal/
|
|
45
|
-
"@nuxt-customer-portal/products": "^0.4.
|
|
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",
|
package/portal.manifest.mjs
CHANGED
|
@@ -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()
|
package/server/utils/jobs.ts
CHANGED
|
@@ -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<{
|
|
285
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
package/shared/validation.ts
CHANGED
|
@@ -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)
|