@nuxt-customer-portal/planning 0.4.5 → 0.4.7
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 +19 -0
- package/app/components/PlanningPersonalCalendarSettings.vue +17 -1
- package/app/components/PlanningPolicyFields.vue +11 -12
- package/app/components/PlanningSettingsSynchronization.vue +5 -0
- package/app/composables/usePlanning.ts +1 -0
- package/i18n/locales/en.json +1 -0
- package/i18n/locales/nl.json +1 -0
- package/package.json +4 -4
- package/portal.manifest.mjs +1 -1
- package/server/api/planning/admin/jobs.get.ts +3 -1
- package/server/utils/adapters.ts +24 -2
- package/server/utils/jobs.ts +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Keep Google Calendar settings recoverable when stored authorization expires, and offer reconnection after a rejected token refresh.
|
|
8
|
+
- @nuxt-customer-portal/core@0.4.7
|
|
9
|
+
- @nuxt-customer-portal/products@0.4.7
|
|
10
|
+
- @nuxt-customer-portal/ui@0.4.7
|
|
11
|
+
|
|
12
|
+
## 0.4.6
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Allow authenticated Google Calendar channel notifications through the public webhook boundary, retain provider-returned availability event IDs, expose those IDs in synchronization diagnostics, and remove the planning policy lint warning.
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
- @nuxt-customer-portal/core@0.4.6
|
|
19
|
+
- @nuxt-customer-portal/products@0.4.6
|
|
20
|
+
- @nuxt-customer-portal/ui@0.4.6
|
|
21
|
+
|
|
3
22
|
## 0.4.5
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
|
@@ -46,7 +46,23 @@ async function load() {
|
|
|
46
46
|
busyCalendarIds: settings.busyCalendarIds,
|
|
47
47
|
writeCalendarId: settings.writeCalendarId || ''
|
|
48
48
|
})
|
|
49
|
-
|
|
49
|
+
if (googleConnected.value) {
|
|
50
|
+
try {
|
|
51
|
+
calendars.value = await api.calendars()
|
|
52
|
+
} catch (calendarError) {
|
|
53
|
+
const refreshedSettings = await api.provider().catch(() => null)
|
|
54
|
+
const connectionStillHealthy = refreshedSettings?.connections.some(
|
|
55
|
+
(connection) => connection.provider === 'google' && connection.healthy
|
|
56
|
+
)
|
|
57
|
+
if (connectionStillHealthy !== false) {
|
|
58
|
+
throw calendarError
|
|
59
|
+
}
|
|
60
|
+
googleConnected.value = false
|
|
61
|
+
calendars.value = []
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
calendars.value = []
|
|
65
|
+
}
|
|
50
66
|
ready.value = true
|
|
51
67
|
} catch {
|
|
52
68
|
error.value = t('planning.loadError')
|
|
@@ -10,17 +10,16 @@ const props = withDefaults(defineProps<{ prefix?: string; currencies?: string[];
|
|
|
10
10
|
compact: false
|
|
11
11
|
})
|
|
12
12
|
const { t } = useI18n()
|
|
13
|
-
|
|
14
|
-
'reservationMinutes'
|
|
15
|
-
'slotIntervalMinutes'
|
|
16
|
-
'displayIntervalMinutes'
|
|
17
|
-
'bookingHorizonDays'
|
|
18
|
-
'minimumNoticeMinutes'
|
|
19
|
-
'freeChanges'
|
|
20
|
-
'rescheduleCutoffMinutes'
|
|
21
|
-
'cancellationCutoffMinutes'
|
|
22
|
-
'refundPercentage'
|
|
23
|
-
] as const
|
|
13
|
+
type NumericPolicyKey =
|
|
14
|
+
| 'reservationMinutes'
|
|
15
|
+
| 'slotIntervalMinutes'
|
|
16
|
+
| 'displayIntervalMinutes'
|
|
17
|
+
| 'bookingHorizonDays'
|
|
18
|
+
| 'minimumNoticeMinutes'
|
|
19
|
+
| 'freeChanges'
|
|
20
|
+
| 'rescheduleCutoffMinutes'
|
|
21
|
+
| 'cancellationCutoffMinutes'
|
|
22
|
+
| 'refundPercentage'
|
|
24
23
|
const groups = [
|
|
25
24
|
{
|
|
26
25
|
key: 'schedule',
|
|
@@ -70,7 +69,7 @@ if (overrides.value !== undefined) {
|
|
|
70
69
|
}
|
|
71
70
|
overrides.value = next
|
|
72
71
|
}
|
|
73
|
-
function setNumber(key:
|
|
72
|
+
function setNumber(key: NumericPolicyKey, value: number | null | undefined) {
|
|
74
73
|
state.value = { ...state.value, [key]: value ?? 0 }
|
|
75
74
|
}
|
|
76
75
|
function setFee(currency: string, value: number | null | undefined) {
|
|
@@ -375,6 +375,11 @@ useAutoPagination({
|
|
|
375
375
|
<UIcon name="i-lucide-calendar" class="size-4 shrink-0" />
|
|
376
376
|
<span class="truncate">{{ job.entity.calendarId || t('planning.noDestinationCalendar') }}</span>
|
|
377
377
|
</p>
|
|
378
|
+
<p v-if="job.entity.calendarEventId" class="flex min-w-0 items-center gap-2 sm:col-span-2">
|
|
379
|
+
<UIcon name="i-lucide-fingerprint" class="size-4 shrink-0" />
|
|
380
|
+
<span class="shrink-0">{{ t('planning.calendarEventId') }}:</span>
|
|
381
|
+
<code class="truncate text-xs" :title="job.entity.calendarEventId">{{ job.entity.calendarEventId }}</code>
|
|
382
|
+
</p>
|
|
378
383
|
</div>
|
|
379
384
|
<div
|
|
380
385
|
v-else-if="job.entity?.type === 'appointment'"
|
package/i18n/locales/en.json
CHANGED
|
@@ -146,6 +146,7 @@
|
|
|
146
146
|
"jobAllProducts": "All products",
|
|
147
147
|
"noProducts": "No products",
|
|
148
148
|
"noDestinationCalendar": "No destination calendar selected",
|
|
149
|
+
"calendarEventId": "Google event ID",
|
|
149
150
|
"synchronizationPages": "Synchronization task pages",
|
|
150
151
|
"jobKinds": {
|
|
151
152
|
"appointment": "Appointment synchronization",
|
package/i18n/locales/nl.json
CHANGED
|
@@ -146,6 +146,7 @@
|
|
|
146
146
|
"jobAllProducts": "Alle producten",
|
|
147
147
|
"noProducts": "Geen producten",
|
|
148
148
|
"noDestinationCalendar": "Geen doelagenda geselecteerd",
|
|
149
|
+
"calendarEventId": "Google-afspraak-ID",
|
|
149
150
|
"synchronizationPages": "Pagina's met synchronisatietaken",
|
|
150
151
|
"jobKinds": {
|
|
151
152
|
"appointment": "Afspraaksynchronisatie",
|
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.7",
|
|
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.
|
|
44
|
-
"@nuxt-customer-portal/
|
|
45
|
-
"@nuxt-customer-portal/
|
|
43
|
+
"@nuxt-customer-portal/core": "^0.4.7",
|
|
44
|
+
"@nuxt-customer-portal/ui": "^0.4.7",
|
|
45
|
+
"@nuxt-customer-portal/products": "^0.4.7"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"nuxt": "^4.5.1",
|
package/portal.manifest.mjs
CHANGED
|
@@ -130,6 +130,7 @@ export default defineEventHandler(async (event) => {
|
|
|
130
130
|
providerName: string
|
|
131
131
|
providerEmail: string
|
|
132
132
|
calendarId: string | null
|
|
133
|
+
calendarEventId: string | null
|
|
133
134
|
data: {
|
|
134
135
|
date: string
|
|
135
136
|
endDate: string | null
|
|
@@ -142,7 +143,7 @@ export default defineEventHandler(async (event) => {
|
|
|
142
143
|
productTitles: string[]
|
|
143
144
|
}>(
|
|
144
145
|
`SELECT a.id::text AS id,u.name AS "providerName",u.email AS "providerEmail",
|
|
145
|
-
p.write_calendar_id AS "calendarId",a.data,
|
|
146
|
+
COALESCE(a.calendar_id,p.write_calendar_id) AS "calendarId",a.calendar_event_id AS "calendarEventId",a.data,
|
|
146
147
|
COALESCE((
|
|
147
148
|
SELECT array_agg(COALESCE(NULLIF(product.data->'content'->'en'->>'title',''),product.data->'content'->'nl'->>'title',product.id) ORDER BY product.id)
|
|
148
149
|
FROM products.product product
|
|
@@ -175,6 +176,7 @@ export default defineEventHandler(async (event) => {
|
|
|
175
176
|
providerName: entity.providerName,
|
|
176
177
|
providerEmail: entity.providerEmail,
|
|
177
178
|
calendarId: entity.calendarId,
|
|
179
|
+
calendarEventId: entity.calendarEventId,
|
|
178
180
|
date: entity.data.date,
|
|
179
181
|
endDate: entity.data.endDate,
|
|
180
182
|
startTime: entity.data.startTime,
|
package/server/utils/adapters.ts
CHANGED
|
@@ -54,6 +54,14 @@ export interface MeetingAdapter {
|
|
|
54
54
|
): Promise<{ id: string; url: string }>
|
|
55
55
|
remove(storeId: string, userId: string, id: string): Promise<void>
|
|
56
56
|
}
|
|
57
|
+
class AuthorizationExchangeError extends Error {
|
|
58
|
+
constructor(
|
|
59
|
+
public readonly status: number,
|
|
60
|
+
message = `Calendar/meeting authorization failed (${status})`
|
|
61
|
+
) {
|
|
62
|
+
super(message)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
57
65
|
export function oauthConfig(provider: 'google' | 'zoom') {
|
|
58
66
|
const prefix = `PLANNING_${provider.toUpperCase()}`
|
|
59
67
|
const clientId = process.env[`${prefix}_CLIENT_ID`],
|
|
@@ -84,7 +92,7 @@ export async function exchange(provider: 'google' | 'zoom', params: Record<strin
|
|
|
84
92
|
}
|
|
85
93
|
)
|
|
86
94
|
if (!response.ok) {
|
|
87
|
-
throw new
|
|
95
|
+
throw new AuthorizationExchangeError(response.status)
|
|
88
96
|
}
|
|
89
97
|
const value = (await response.json()) as { access_token: string; refresh_token?: string; expires_in: number }
|
|
90
98
|
if (!value.access_token || !value.expires_in) {
|
|
@@ -111,7 +119,21 @@ async function accessToken(storeId: string, userId: string, provider: 'google' |
|
|
|
111
119
|
}
|
|
112
120
|
let credentials = decrypt<Credentials>(connection.credentials)
|
|
113
121
|
if (credentials.expiresAt < Date.now() + 60000) {
|
|
114
|
-
|
|
122
|
+
try {
|
|
123
|
+
credentials = await exchange(provider, {
|
|
124
|
+
grant_type: 'refresh_token',
|
|
125
|
+
refresh_token: credentials.refresh_token
|
|
126
|
+
})
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof AuthorizationExchangeError && [400, 401].includes(error.status)) {
|
|
129
|
+
await rows(
|
|
130
|
+
'UPDATE planning.connection SET healthy=false,error=$4 WHERE store_id=$1 AND user_id=$2 AND provider=$3',
|
|
131
|
+
[storeId, userId, provider, 'Authorization expired; reconnect'],
|
|
132
|
+
client
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
throw error
|
|
136
|
+
}
|
|
115
137
|
await rows(
|
|
116
138
|
'UPDATE planning.connection SET credentials=$4,error=NULL WHERE store_id=$1 AND user_id=$2 AND provider=$3',
|
|
117
139
|
[storeId, userId, provider, encrypt(credentials)],
|
package/server/utils/jobs.ts
CHANGED
|
@@ -353,9 +353,18 @@ async function availabilityEffects(id: string) {
|
|
|
353
353
|
}
|
|
354
354
|
await rows('UPDATE planning.availability SET calendar_event_id=$2,calendar_id=$3 WHERE id=$1', [
|
|
355
355
|
id,
|
|
356
|
-
eventId,
|
|
356
|
+
written?.id || eventId,
|
|
357
357
|
provider.write_calendar_id
|
|
358
358
|
])
|
|
359
|
+
if (written?.wasMissing || written?.externalChangeKey) {
|
|
360
|
+
console.info('Planning availability synchronized', {
|
|
361
|
+
availabilityId: id,
|
|
362
|
+
calendarId: provider.write_calendar_id,
|
|
363
|
+
calendarEventId: written.id,
|
|
364
|
+
revision: window.revision,
|
|
365
|
+
result: written.externalChangeKey ? 'external-change-repaired' : 'event-created'
|
|
366
|
+
})
|
|
367
|
+
}
|
|
359
368
|
}
|
|
360
369
|
async function refund(orderId: string, amount: number, key: string) {
|
|
361
370
|
const order = await getOrder(orderId)
|