@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.
- package/CHANGELOG.md +61 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/app/components/PlanningAppointmentCreateForm.vue +49 -0
- package/app/components/PlanningAppointmentDetails.vue +163 -0
- package/app/components/PlanningAppointmentDrawer.vue +109 -0
- package/app/components/PlanningAvailabilityCalendar.vue +525 -0
- package/app/components/PlanningDashboardOverview.vue +62 -0
- package/app/components/PlanningDashboardUpcomingAppointments.vue +81 -0
- package/app/components/PlanningPersonalCalendarSettings.vue +218 -0
- package/app/components/PlanningPersonalConnection.vue +68 -0
- package/app/components/PlanningPersonalMeetingSettings.vue +1 -0
- package/app/components/PlanningPolicyFields.vue +196 -0
- package/app/components/PlanningSettingsMembers.vue +50 -0
- package/app/components/PlanningSettingsPolicy.vue +46 -0
- package/app/components/PlanningSettingsSynchronization.vue +433 -0
- package/app/components/PlanningSlotPicker.vue +588 -0
- package/app/components/PlanningTimezoneReadOnlyHover.vue +172 -0
- package/app/components/PlanningTimezoneSelect.vue +101 -0
- package/app/composables/usePlanning.ts +180 -0
- package/app/composables/usePlanningDashboard.ts +4 -0
- package/app/composables/usePlanningTimeDisplay.ts +15 -0
- package/app/pages/admin/planning.vue +36 -0
- package/app/pages/appointments/[id].vue +13 -0
- package/app/pages/appointments/index.vue +384 -0
- package/app/pages/availability.vue +585 -0
- package/app/pages/booking/[slug].vue +298 -0
- package/app/pages/my-appointment-settings.vue +33 -0
- package/app/plugins/planning.ts +5 -0
- package/i18n/locales/en.json +260 -0
- package/i18n/locales/nl.json +260 -0
- package/migrations/0000_baseline.sql +58 -0
- package/migrations/0001_availability_calendar_title.sql +2 -0
- package/migrations/0002_job_activity.sql +11 -0
- package/migrations/0003_deduplicate_availability_jobs.sql +19 -0
- package/migrations/0004_connection_label.sql +2 -0
- package/nuxt.config.ts +15 -0
- package/package.json +55 -0
- package/portal.manifest.mjs +8 -0
- package/server/api/planning/admin/appointments/[id]/audit.get.ts +11 -0
- package/server/api/planning/admin/appointments.get.ts +11 -0
- package/server/api/planning/admin/jobs/[id]/retry.post.ts +26 -0
- package/server/api/planning/admin/jobs.get.ts +199 -0
- package/server/api/planning/admin/policy.get.ts +6 -0
- package/server/api/planning/admin/policy.put.ts +8 -0
- package/server/api/planning/admin/products/[id].put.ts +8 -0
- package/server/api/planning/admin/providers/[userId].patch.ts +8 -0
- package/server/api/planning/admin/providers.get.ts +6 -0
- package/server/api/planning/admin/retry.post.ts +31 -0
- package/server/api/planning/appointments/[id]/availability.get.ts +19 -0
- package/server/api/planning/appointments/[id]/cancel.post.ts +8 -0
- package/server/api/planning/appointments/[id]/files/[assetId].get.ts +17 -0
- package/server/api/planning/appointments/[id]/files.get.ts +28 -0
- package/server/api/planning/appointments/[id]/pending.delete.ts +8 -0
- package/server/api/planning/appointments/[id]/reschedule.put.ts +8 -0
- package/server/api/planning/appointments/[id].get.ts +6 -0
- package/server/api/planning/appointments.get.ts +4 -0
- package/server/api/planning/availability/[id].delete.ts +12 -0
- package/server/api/planning/availability/[id].put.ts +8 -0
- package/server/api/planning/availability.get.ts +6 -0
- package/server/api/planning/availability.post.ts +8 -0
- package/server/api/planning/calendar-busy.get.ts +29 -0
- package/server/api/planning/calendars.get.ts +8 -0
- package/server/api/planning/dashboard.get.ts +36 -0
- package/server/api/planning/google/notifications.post.ts +24 -0
- package/server/api/planning/oauth/[provider]/callback.get.ts +18 -0
- package/server/api/planning/oauth/[provider]/start.post.ts +8 -0
- package/server/api/planning/oauth/[provider].delete.ts +15 -0
- package/server/api/planning/products.get.ts +11 -0
- package/server/api/planning/provider-appointments.get.ts +11 -0
- package/server/api/planning/provider.get.ts +6 -0
- package/server/api/planning/provider.put.ts +8 -0
- package/server/api/store/planning/[productId]/availability.get.ts +8 -0
- package/server/api/store/planning/hold.delete.ts +11 -0
- package/server/api/store/planning/hold.get.ts +36 -0
- package/server/api/store/planning/holds.post.ts +10 -0
- package/server/db/schema/planning.ts +213 -0
- package/server/plugins/openapi.ts +36 -0
- package/server/plugins/planning.ts +6 -0
- package/server/tasks/planning/reconcile.ts +30 -0
- package/server/utils/access.ts +80 -0
- package/server/utils/adapters.ts +468 -0
- package/server/utils/appointments.ts +37 -0
- package/server/utils/booking.ts +619 -0
- package/server/utils/crypto.ts +23 -0
- package/server/utils/jobs.ts +671 -0
- package/server/utils/management.ts +427 -0
- package/server/utils/oauth.ts +104 -0
- package/server/utils/staff-appointments.ts +158 -0
- package/shared/appointment-calendar.ts +82 -0
- package/shared/availability.ts +140 -0
- package/shared/emails.ts +99 -0
- package/shared/feature.ts +78 -0
- package/shared/invitation.ts +58 -0
- package/shared/types.ts +110 -0
- package/shared/validation.ts +77 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HoldResult } from '../../composables/usePlanning'
|
|
3
|
+
import { resolveCheckoutQuery, resolveCheckoutReturnUrl } from '@nuxt-customer-portal/products/shared/checkout-query'
|
|
4
|
+
import { formatMoney } from '@nuxt-customer-portal/products/shared/money'
|
|
5
|
+
|
|
6
|
+
definePageMeta({ path: '/store/:slug/book', public: true, layout: 'store', key: (route) => `booking:${route.path}` })
|
|
7
|
+
const route = useRoute(),
|
|
8
|
+
{ t, locale, setLocale } = useI18n(),
|
|
9
|
+
query = resolveCheckoutQuery(route.query),
|
|
10
|
+
product = await useProducts().catalog(String(route.params.slug), query.locale, query.currency)
|
|
11
|
+
if (locale.value !== query.locale) {
|
|
12
|
+
await setLocale(query.locale)
|
|
13
|
+
}
|
|
14
|
+
if (!product.planningEnabled) {
|
|
15
|
+
await navigateTo({ path: `/store/${encodeURIComponent(product.slug)}`, query: route.query })
|
|
16
|
+
}
|
|
17
|
+
const returnUrl = computed(() => resolveCheckoutReturnUrl(route.query.returnUrl, product.checkoutAppearance.returnUrl))
|
|
18
|
+
const currency = ref(
|
|
19
|
+
product.prices.some((p) => p.currency === query.currency) ? query.currency! : product.prices[0]?.currency || 'EUR'
|
|
20
|
+
)
|
|
21
|
+
const appearance = computed(() => product.checkoutAppearance)
|
|
22
|
+
const themeStyle = computed(() => ({
|
|
23
|
+
'--checkout-paper': appearance.value.paperColor,
|
|
24
|
+
'--checkout-surface': appearance.value.surfaceColor,
|
|
25
|
+
'--checkout-ink': appearance.value.inkColor,
|
|
26
|
+
'--checkout-muted': appearance.value.mutedColor,
|
|
27
|
+
'--checkout-display': appearance.value.displayColor,
|
|
28
|
+
'--checkout-accent': appearance.value.accentColor,
|
|
29
|
+
'--checkout-action': appearance.value.actionColor,
|
|
30
|
+
'--checkout-focus': appearance.value.focusColor,
|
|
31
|
+
'--checkout-border': appearance.value.borderColor,
|
|
32
|
+
'--checkout-radius': `${appearance.value.radius}px`,
|
|
33
|
+
'--checkout-display-font': appearance.value.displayFontFamily,
|
|
34
|
+
'--checkout-body-font': appearance.value.bodyFontFamily
|
|
35
|
+
}))
|
|
36
|
+
const selectedPrice = computed(() => product.prices.find((price) => price.currency === currency.value))
|
|
37
|
+
const endpoint = String(useRuntimeConfig().public.productsImageKitUrlEndpoint || '').replace(/\/$/, '')
|
|
38
|
+
const imageSource = (url: string) => (endpoint && url.startsWith(`${endpoint}/`) ? url.slice(endpoint.length) : url)
|
|
39
|
+
const imageProvider = (url: string) => (endpoint && url.startsWith(`${endpoint}/`) ? 'imagekit' : undefined)
|
|
40
|
+
async function reserved(hold: HoldResult) {
|
|
41
|
+
await navigateTo({
|
|
42
|
+
path: `/store/${encodeURIComponent(product.slug)}`,
|
|
43
|
+
query: { ...route.query, currency: hold.currency, locale: product.locale, holdToken: hold.holdToken }
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
useHead({ title: `${t('planning.bookNow')} — ${product.title}`, meta: [{ name: 'referrer', content: 'no-referrer' }] })
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<main class="checkout-page booking-page" :style="themeStyle">
|
|
51
|
+
<div class="checkout-shell">
|
|
52
|
+
<header class="checkout-header">
|
|
53
|
+
<a v-if="returnUrl" :href="returnUrl" class="checkout-back">← {{ t('products.backToOffering') }}</a>
|
|
54
|
+
<img v-if="appearance.logoUrl" :src="appearance.logoUrl" :alt="appearance.hostName" class="checkout-logo" />
|
|
55
|
+
<span v-else class="checkout-wordmark">{{ appearance.hostName }}</span>
|
|
56
|
+
<span class="checkout-secure" :class="{ 'checkout-test': product.storeMode === 'sandbox' }">{{
|
|
57
|
+
t(product.storeMode === 'sandbox' ? 'products.testCheckoutLabel' : 'products.secureCheckout')
|
|
58
|
+
}}</span>
|
|
59
|
+
</header>
|
|
60
|
+
<div class="checkout-grid">
|
|
61
|
+
<aside class="order-card" aria-labelledby="order-title">
|
|
62
|
+
<p class="checkout-eyebrow">{{ t('products.yourSelection') }}</p>
|
|
63
|
+
<div class="order-product">
|
|
64
|
+
<NuxtImg
|
|
65
|
+
v-if="product.thumbnailImage"
|
|
66
|
+
:provider="imageProvider(product.thumbnailImage)"
|
|
67
|
+
:src="imageSource(product.thumbnailImage)"
|
|
68
|
+
:alt="product.title"
|
|
69
|
+
preset="productThumbnail"
|
|
70
|
+
width="112"
|
|
71
|
+
height="112"
|
|
72
|
+
class="order-thumbnail"
|
|
73
|
+
/>
|
|
74
|
+
<div class="min-w-0">
|
|
75
|
+
<h1 id="order-title">{{ product.title }}</h1>
|
|
76
|
+
<p v-if="product.subtitle" class="order-subtitle">{{ product.subtitle }}</p>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<p class="order-summary">{{ product.summary }}</p>
|
|
80
|
+
<details class="mobile-offering-details">
|
|
81
|
+
<summary>{{ t('planning.offeringDetails') }}</summary>
|
|
82
|
+
<p>{{ product.summary }}</p>
|
|
83
|
+
</details>
|
|
84
|
+
<p class="booking-duration">
|
|
85
|
+
<UIcon name="i-lucide-clock" aria-hidden="true" />{{
|
|
86
|
+
t('planning.duration', { minutes: product.durationMinutes })
|
|
87
|
+
}}
|
|
88
|
+
</p>
|
|
89
|
+
<div class="order-price">
|
|
90
|
+
<span>{{ t('products.total') }}</span
|
|
91
|
+
><strong>{{
|
|
92
|
+
product.isFree
|
|
93
|
+
? t('products.freeProduct')
|
|
94
|
+
: selectedPrice
|
|
95
|
+
? formatMoney(selectedPrice.amount, selectedPrice.currency, product.locale)
|
|
96
|
+
: ''
|
|
97
|
+
}}</strong>
|
|
98
|
+
</div>
|
|
99
|
+
<UFormField v-if="product.prices.length > 1" :label="t('products.price')" class="mt-5">
|
|
100
|
+
<USelect
|
|
101
|
+
v-model="currency"
|
|
102
|
+
class="w-full"
|
|
103
|
+
:items="
|
|
104
|
+
product.prices.map((p) => ({
|
|
105
|
+
value: p.currency,
|
|
106
|
+
label: formatMoney(p.amount, p.currency, product.locale)
|
|
107
|
+
}))
|
|
108
|
+
"
|
|
109
|
+
/>
|
|
110
|
+
</UFormField>
|
|
111
|
+
</aside>
|
|
112
|
+
<section class="checkout-form-panel booking-panel" aria-labelledby="booking-title">
|
|
113
|
+
<p class="checkout-eyebrow">{{ t('planning.bookNow') }}</p>
|
|
114
|
+
<h2 id="booking-title">{{ t('planning.chooseAppointmentTime') }}</h2>
|
|
115
|
+
<PlanningSlotPicker
|
|
116
|
+
:key="currency"
|
|
117
|
+
docked
|
|
118
|
+
:product-id="product.id"
|
|
119
|
+
:currency="currency"
|
|
120
|
+
:locale="product.locale"
|
|
121
|
+
@reserved="reserved"
|
|
122
|
+
/>
|
|
123
|
+
</section>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</main>
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<style scoped src="../../../../products/app/assets/css/checkout.css"></style>
|
|
130
|
+
|
|
131
|
+
<style scoped>
|
|
132
|
+
/* Hallmark · component: mobile booking · theme: existing checkout · tone: soft
|
|
133
|
+
* pre-emit critique: P4 H5 E4 S5 R5 V4
|
|
134
|
+
*/
|
|
135
|
+
.mobile-offering-details {
|
|
136
|
+
display: none;
|
|
137
|
+
}
|
|
138
|
+
.booking-page .booking-panel {
|
|
139
|
+
--booking-field-background: var(--checkout-surface);
|
|
140
|
+
background: var(--checkout-surface);
|
|
141
|
+
border: 1px solid var(--checkout-border);
|
|
142
|
+
border-radius: var(--checkout-radius);
|
|
143
|
+
padding: clamp(20px, 3vw, 32px);
|
|
144
|
+
}
|
|
145
|
+
@media (min-width: 961px) {
|
|
146
|
+
.booking-page .booking-panel > .checkout-eyebrow {
|
|
147
|
+
margin-bottom: 12px;
|
|
148
|
+
}
|
|
149
|
+
.booking-page .order-product {
|
|
150
|
+
align-items: start;
|
|
151
|
+
}
|
|
152
|
+
.booking-page #order-title,
|
|
153
|
+
.booking-page #booking-title {
|
|
154
|
+
line-height: 1.08;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
@media (max-width: 960px) {
|
|
158
|
+
.booking-page .booking-panel {
|
|
159
|
+
padding: 16px 12px;
|
|
160
|
+
}
|
|
161
|
+
.booking-page .checkout-grid {
|
|
162
|
+
grid-template-columns: minmax(0, 1fr);
|
|
163
|
+
}
|
|
164
|
+
.booking-page .order-card {
|
|
165
|
+
position: static;
|
|
166
|
+
}
|
|
167
|
+
.booking-page .checkout-shell {
|
|
168
|
+
padding-top: 8px;
|
|
169
|
+
}
|
|
170
|
+
.booking-page .checkout-header {
|
|
171
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
172
|
+
min-height: 48px;
|
|
173
|
+
margin-bottom: 12px;
|
|
174
|
+
}
|
|
175
|
+
.booking-page .checkout-secure {
|
|
176
|
+
display: none;
|
|
177
|
+
}
|
|
178
|
+
.booking-page .checkout-test {
|
|
179
|
+
display: block;
|
|
180
|
+
grid-column: 1 / -1;
|
|
181
|
+
}
|
|
182
|
+
.booking-page .checkout-logo {
|
|
183
|
+
max-height: 36px;
|
|
184
|
+
max-width: 120px;
|
|
185
|
+
}
|
|
186
|
+
.booking-page .checkout-back {
|
|
187
|
+
font-size: 0.75rem;
|
|
188
|
+
}
|
|
189
|
+
.booking-page .checkout-grid {
|
|
190
|
+
gap: 16px;
|
|
191
|
+
}
|
|
192
|
+
.booking-page .order-card {
|
|
193
|
+
padding: 12px;
|
|
194
|
+
display: grid;
|
|
195
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
196
|
+
align-items: center;
|
|
197
|
+
gap: 4px 12px;
|
|
198
|
+
}
|
|
199
|
+
.booking-page .order-product {
|
|
200
|
+
grid-column: 1 / -1;
|
|
201
|
+
}
|
|
202
|
+
.booking-page .order-card > .mt-5 {
|
|
203
|
+
grid-column: 1 / -1;
|
|
204
|
+
}
|
|
205
|
+
.booking-page .order-card > .checkout-eyebrow,
|
|
206
|
+
.booking-page .order-summary,
|
|
207
|
+
.booking-page .order-subtitle,
|
|
208
|
+
.booking-page .booking-panel > .checkout-eyebrow {
|
|
209
|
+
display: none;
|
|
210
|
+
}
|
|
211
|
+
.booking-page .order-product {
|
|
212
|
+
grid-template-columns: 48px minmax(0, 1fr);
|
|
213
|
+
gap: 12px;
|
|
214
|
+
}
|
|
215
|
+
.booking-page .order-thumbnail {
|
|
216
|
+
width: 48px;
|
|
217
|
+
height: 48px;
|
|
218
|
+
}
|
|
219
|
+
.booking-page h1 {
|
|
220
|
+
font-size: 1.25rem;
|
|
221
|
+
line-height: 1.15;
|
|
222
|
+
}
|
|
223
|
+
.booking-page .booking-duration {
|
|
224
|
+
margin: 0;
|
|
225
|
+
padding: 0;
|
|
226
|
+
border: 0;
|
|
227
|
+
background: none;
|
|
228
|
+
font-size: 0.85rem;
|
|
229
|
+
}
|
|
230
|
+
.booking-page .order-price {
|
|
231
|
+
padding-top: 0;
|
|
232
|
+
border: 0;
|
|
233
|
+
font-size: 0.85rem;
|
|
234
|
+
}
|
|
235
|
+
.booking-page .order-price > span {
|
|
236
|
+
display: none;
|
|
237
|
+
}
|
|
238
|
+
.booking-page .order-price strong {
|
|
239
|
+
font-size: 1rem;
|
|
240
|
+
}
|
|
241
|
+
.mobile-offering-details {
|
|
242
|
+
display: block;
|
|
243
|
+
margin-top: 0;
|
|
244
|
+
grid-column: 1 / -1;
|
|
245
|
+
grid-row: 3;
|
|
246
|
+
color: var(--checkout-muted);
|
|
247
|
+
font-size: 0.85rem;
|
|
248
|
+
}
|
|
249
|
+
.mobile-offering-details summary {
|
|
250
|
+
cursor: pointer;
|
|
251
|
+
min-height: 28px;
|
|
252
|
+
}
|
|
253
|
+
.mobile-offering-details p {
|
|
254
|
+
margin-block: 8px;
|
|
255
|
+
line-height: 1.55;
|
|
256
|
+
}
|
|
257
|
+
.booking-page .booking-panel > h2 {
|
|
258
|
+
font-size: 2rem;
|
|
259
|
+
margin-bottom: 12px;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
.booking-panel > h2 {
|
|
263
|
+
margin-bottom: 28px;
|
|
264
|
+
}
|
|
265
|
+
.booking-duration {
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: 10px;
|
|
269
|
+
margin: 20px 0;
|
|
270
|
+
padding: 14px 16px;
|
|
271
|
+
border: 1px solid color-mix(in srgb, var(--checkout-accent) 25%, transparent);
|
|
272
|
+
border-radius: calc(var(--checkout-radius) * 0.62);
|
|
273
|
+
background: color-mix(in srgb, var(--checkout-accent) 7%, var(--checkout-surface));
|
|
274
|
+
color: var(--checkout-display);
|
|
275
|
+
font-size: 1.15rem;
|
|
276
|
+
font-weight: 600;
|
|
277
|
+
}
|
|
278
|
+
.booking-duration :deep(svg),
|
|
279
|
+
.booking-duration :deep(.iconify) {
|
|
280
|
+
width: 22px;
|
|
281
|
+
height: 22px;
|
|
282
|
+
color: var(--checkout-accent);
|
|
283
|
+
}
|
|
284
|
+
.booking-panel :deep(h2) {
|
|
285
|
+
font-size: 1rem;
|
|
286
|
+
line-height: 1.5;
|
|
287
|
+
}
|
|
288
|
+
.booking-panel > h2 {
|
|
289
|
+
font-size: clamp(2.15rem, 5vw, 3.75rem);
|
|
290
|
+
line-height: 1.04;
|
|
291
|
+
}
|
|
292
|
+
.booking-panel :deep(.text-muted) {
|
|
293
|
+
color: var(--checkout-muted);
|
|
294
|
+
}
|
|
295
|
+
.booking-panel :deep(button) {
|
|
296
|
+
--ui-primary: var(--checkout-accent);
|
|
297
|
+
}
|
|
298
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({ path: '/planning/settings' })
|
|
3
|
+
const { t } = useI18n(),
|
|
4
|
+
route = useRoute()
|
|
5
|
+
const items = computed(() => [
|
|
6
|
+
{ label: t('planning.calendarSettings'), value: 'calendar', slot: 'calendar' },
|
|
7
|
+
{ label: t('planning.meetingSettings'), value: 'meeting', slot: 'meeting' }
|
|
8
|
+
])
|
|
9
|
+
const tab = computed({
|
|
10
|
+
get: () => (route.query.tab === 'meeting' ? 'meeting' : 'calendar'),
|
|
11
|
+
set: (value: string) => {
|
|
12
|
+
void navigateTo({ query: { ...route.query, tab: value === 'meeting' ? 'meeting' : undefined } }, { replace: true })
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="h-full min-h-0 overflow-y-auto">
|
|
19
|
+
<UContainer class="space-y-6 py-8">
|
|
20
|
+
<h1 class="text-2xl font-bold">{{ t('planning.myAppointmentSettings') }}</h1>
|
|
21
|
+
<UTabs
|
|
22
|
+
v-model="tab"
|
|
23
|
+
:items="items"
|
|
24
|
+
variant="link"
|
|
25
|
+
:unmount-on-hide="true"
|
|
26
|
+
:ui="{ list: 'justify-start', trigger: 'grow-0' }"
|
|
27
|
+
class="w-full"
|
|
28
|
+
>
|
|
29
|
+
<template #calendar><PlanningPersonalCalendarSettings class="pt-4" /></template>
|
|
30
|
+
<template #meeting><PlanningPersonalMeetingSettings class="pt-4" /></template> </UTabs
|
|
31
|
+
></UContainer>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
{
|
|
2
|
+
"planning": {
|
|
3
|
+
"midnight": "midnight",
|
|
4
|
+
"planning": "Planning",
|
|
5
|
+
"appointments": "Appointments",
|
|
6
|
+
"emailNewAppointment": "New appointment",
|
|
7
|
+
"emailUpdatedAppointment": "Updated appointment",
|
|
8
|
+
"emailCanceledAppointment": "Canceled appointment",
|
|
9
|
+
"emailZoomLinkReady": "Zoom link ready",
|
|
10
|
+
"availability": "Availability",
|
|
11
|
+
"settings": "Planning settings",
|
|
12
|
+
"timezone": "Timezone",
|
|
13
|
+
"provider": "Team member",
|
|
14
|
+
"anyProvider": "Any available team member",
|
|
15
|
+
"previousMonth": "Previous month",
|
|
16
|
+
"nextMonth": "Next month",
|
|
17
|
+
"loading": "Loading availability…",
|
|
18
|
+
"loadError": "Could not load planning information. Please try again.",
|
|
19
|
+
"noAvailability": "No available times. Choose another date or team member.",
|
|
20
|
+
"slotUnavailable": "This time is no longer available. Please choose another slot.",
|
|
21
|
+
"reserveHint": "Selecting a time reserves it while you complete checkout. The checkout shows your reservation deadline.",
|
|
22
|
+
"continueToCheckout": "Continue to checkout",
|
|
23
|
+
"changeDate": "Change date",
|
|
24
|
+
"offeringDetails": "Offering details",
|
|
25
|
+
"confirmTimeHint": "Review your time, then continue to reserve it while you complete checkout.",
|
|
26
|
+
"bookNow": "Book now",
|
|
27
|
+
"back": "Back",
|
|
28
|
+
"duration": "{minutes} minutes",
|
|
29
|
+
"noAppointments": "No appointments yet.",
|
|
30
|
+
"manage": "Manage appointment",
|
|
31
|
+
"confirmed": "Confirmed",
|
|
32
|
+
"cancelled": "Cancelled",
|
|
33
|
+
"reschedule": "Change appointment",
|
|
34
|
+
"cancel": "Cancel appointment",
|
|
35
|
+
"joinMeeting": "Join meeting",
|
|
36
|
+
"changesUsed": "{used} changes used; {free} included without charge.",
|
|
37
|
+
"changeCosts": "This change costs {amount}. Your existing appointment stays confirmed until checkout succeeds.",
|
|
38
|
+
"freeChange": "This change is included without charge. Complete checkout to confirm your new time.",
|
|
39
|
+
"cancelConfirm": "Cancel this appointment? Your eligible refund is {amount}.",
|
|
40
|
+
"confirmCancellation": "Confirm cancellation",
|
|
41
|
+
"actionError": "Could not complete this action. Please try again.",
|
|
42
|
+
"reservationMinutes": "Reservation length (minutes)",
|
|
43
|
+
"slotIntervalMinutes": "Start time interval (minutes)",
|
|
44
|
+
"displayIntervalMinutes": "Maximum displayed start-time spacing (minutes)",
|
|
45
|
+
"bookingHorizonDays": "Book up to this many days ahead",
|
|
46
|
+
"minimumNoticeMinutes": "Minimum booking notice (minutes)",
|
|
47
|
+
"freeChanges": "Included appointment changes",
|
|
48
|
+
"rescheduleCutoffMinutes": "Change cutoff before appointment (minutes)",
|
|
49
|
+
"cancellationCutoffMinutes": "Cancellation cutoff before appointment (minutes)",
|
|
50
|
+
"refundPercentage": "Cancellation refund (%)",
|
|
51
|
+
"cancellationEnabled": "Allow customer cancellations",
|
|
52
|
+
"changeFeeCurrency": "Change fee ({currency})",
|
|
53
|
+
"changeFees": "Change fees",
|
|
54
|
+
"policyGroup": {
|
|
55
|
+
"schedule": {
|
|
56
|
+
"title": "Appointment timing",
|
|
57
|
+
"description": "Set appointment holds and the spacing between offered start times."
|
|
58
|
+
},
|
|
59
|
+
"bookingWindow": {
|
|
60
|
+
"title": "Booking window",
|
|
61
|
+
"description": "Control how early and how far ahead customers can book."
|
|
62
|
+
},
|
|
63
|
+
"changes": {
|
|
64
|
+
"title": "Appointment changes",
|
|
65
|
+
"description": "Configure included changes, deadlines and any applicable fees."
|
|
66
|
+
},
|
|
67
|
+
"cancellations": {
|
|
68
|
+
"title": "Cancellations and refunds",
|
|
69
|
+
"description": "Choose whether customers can cancel and what refund they receive."
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"connections": "Calendar and meeting connections",
|
|
73
|
+
"connected": "Connected",
|
|
74
|
+
"notConnected": "Not connected",
|
|
75
|
+
"connect": "Connect or reconnect",
|
|
76
|
+
"disconnect": "Disconnect",
|
|
77
|
+
"connectionError": "Could not connect to Zoom. Your administrator may need to configure Zoom.",
|
|
78
|
+
"googleConnectionError": "Google Calendar is not configured for this portal yet. Ask an administrator to add the Google OAuth credentials.",
|
|
79
|
+
"connectMainCalendar": "Connect your main calendar",
|
|
80
|
+
"connectMainCalendarDescription": "Start with the Google Calendar that will receive your portal appointments.",
|
|
81
|
+
"mainCalendarPermissions": "Your main calendar needs read and write access so the portal can check conflicts and manage appointment events.",
|
|
82
|
+
"connectGoogleCalendar": "Connect Google Calendar",
|
|
83
|
+
"mainCalendar": "Main calendar",
|
|
84
|
+
"mainCalendarFixedDescription": "This calendar receives portal availability and appointments. Switching it will be a separate deliberate action.",
|
|
85
|
+
"chooseMainCalendarDescription": "Choose the writable calendar that should receive portal appointments. This choice becomes fixed after setup.",
|
|
86
|
+
"readWrite": "Read and write",
|
|
87
|
+
"portalCalendarDestination": "Portal availability and appointments",
|
|
88
|
+
"chooseMainCalendar": "Choose your main calendar",
|
|
89
|
+
"additionalCalendars": "Additional calendars",
|
|
90
|
+
"additionalCalendarsDescription": "Optionally check other calendars for conflicts. The portal never writes appointment events to them.",
|
|
91
|
+
"readOnly": "Conflict checking only",
|
|
92
|
+
"addConflictCalendars": "Add calendars to check",
|
|
93
|
+
"noAdditionalCalendars": "No other calendars are available in this Google account.",
|
|
94
|
+
"schedulingPreferences": "Scheduling preferences",
|
|
95
|
+
"setMainCalendar": "Set main calendar",
|
|
96
|
+
"calendarSettings": "Calendar settings",
|
|
97
|
+
"graceMinutes": "Grace time after portal appointments (minutes)",
|
|
98
|
+
"availabilityCalendarTitle": "Availability calendar event title",
|
|
99
|
+
"availabilityCalendarTitleDescription": "Shown as the title of availability events in your main calendar.",
|
|
100
|
+
"busyCalendars": "Calendars that block availability",
|
|
101
|
+
"writeCalendar": "Calendar for portal availability and bookings",
|
|
102
|
+
"save": "Save",
|
|
103
|
+
"planningDisabled": "Planning is switched off for you. Your administrator can enable it after setup.",
|
|
104
|
+
"previousWeek": "Previous week",
|
|
105
|
+
"nextWeek": "Next week",
|
|
106
|
+
"addWindow": "Add availability",
|
|
107
|
+
"allProducts": "All assigned plannable products",
|
|
108
|
+
"selectedProducts": "Selected products",
|
|
109
|
+
"conflict": "Calendar conflict",
|
|
110
|
+
"syncError": "Calendar or meeting synchronization needs attention.",
|
|
111
|
+
"editOne": "Edit only this occurrence",
|
|
112
|
+
"date": "Date",
|
|
113
|
+
"startTime": "Start time",
|
|
114
|
+
"endTime": "End time",
|
|
115
|
+
"weekly": "Repeat weekly",
|
|
116
|
+
"endDate": "Last date (optional)",
|
|
117
|
+
"products": "Products",
|
|
118
|
+
"delete": "Delete availability",
|
|
119
|
+
"deleteOccurrenceConfirm": "Remove this occurrence? Confirmed appointments are preserved.",
|
|
120
|
+
"deleteSeriesConfirm": "Remove this availability window or series? Confirmed appointments are preserved.",
|
|
121
|
+
"providers": "Team members",
|
|
122
|
+
"organizationPolicy": "Organization booking policy",
|
|
123
|
+
"pendingTasks": "Synchronization and refunds",
|
|
124
|
+
"pendingTasksDescription": "Review calendar, appointment, checkout and refund task history for this organization.",
|
|
125
|
+
"noPendingTasks": "Everything has been processed.",
|
|
126
|
+
"noPendingTasksDescription": "There are no synchronization or refund tasks requiring attention.",
|
|
127
|
+
"noMatchingTasks": "No matching tasks",
|
|
128
|
+
"noMatchingTasksDescription": "No synchronization or refund tasks match these filters.",
|
|
129
|
+
"attempts": "{count} attempts",
|
|
130
|
+
"retry": "Retry pending tasks",
|
|
131
|
+
"retryAll": "Retry all",
|
|
132
|
+
"retryTask": "Retry task",
|
|
133
|
+
"searchSynchronization": "Search tasks, errors or appointments…",
|
|
134
|
+
"allSyncStatuses": "All statuses",
|
|
135
|
+
"allTaskTypes": "All task types",
|
|
136
|
+
"taskType": "Task type",
|
|
137
|
+
"lastEvent": "Last event",
|
|
138
|
+
"lastEventValue": "Last event: {date}",
|
|
139
|
+
"nextRetryValue": "Next automatic retry: {date}",
|
|
140
|
+
"attemptCount": "Attempts",
|
|
141
|
+
"failed": "Failed",
|
|
142
|
+
"queued": "Queued",
|
|
143
|
+
"succeeded": "Succeeded",
|
|
144
|
+
"scheduledCheck": "Scheduled check",
|
|
145
|
+
"taskCount": "{count} tasks",
|
|
146
|
+
"jobAllProducts": "All products",
|
|
147
|
+
"noProducts": "No products",
|
|
148
|
+
"noDestinationCalendar": "No destination calendar selected",
|
|
149
|
+
"synchronizationPages": "Synchronization task pages",
|
|
150
|
+
"jobKinds": {
|
|
151
|
+
"appointment": "Appointment synchronization",
|
|
152
|
+
"availability": "Availability synchronization",
|
|
153
|
+
"repair": "Appointment repair",
|
|
154
|
+
"refund": "Refund",
|
|
155
|
+
"cleanup": "Calendar cleanup",
|
|
156
|
+
"close-checkout": "Checkout cleanup",
|
|
157
|
+
"failed-booking": "Failed booking recovery",
|
|
158
|
+
"sync": "Calendar reconciliation",
|
|
159
|
+
"conflict": "Calendar conflict notification"
|
|
160
|
+
},
|
|
161
|
+
"product": "Product",
|
|
162
|
+
"status": "Status",
|
|
163
|
+
"overridePolicySection": "Override this section",
|
|
164
|
+
"time": "Time",
|
|
165
|
+
"timezone": "Timezone",
|
|
166
|
+
"meetingUrl": "Meeting link",
|
|
167
|
+
"meetingDetails": "Meeting details",
|
|
168
|
+
"instructions": "Instructions after purchase",
|
|
169
|
+
"url": "Portal link",
|
|
170
|
+
"pendingChange": "A replacement time is reserved. Your current appointment stays confirmed until the change completes.",
|
|
171
|
+
"abandonChange": "Abandon pending change",
|
|
172
|
+
"planningDisabledAdmin": "Planning is switched off for your account. After setting up your connections, enable it in Planning settings.",
|
|
173
|
+
"bookingPolicyTab": "Booking policy",
|
|
174
|
+
"synchronizationTab": "Synchronization",
|
|
175
|
+
"staffChangeNotice": "Staff changes do not charge the customer or use their included changes.",
|
|
176
|
+
"newAppointment": "New appointment",
|
|
177
|
+
"close": "Close",
|
|
178
|
+
"chooseProduct": "Choose a product",
|
|
179
|
+
"newAppointmentNotice": "Choose a product, then select a time and complete checkout. The appointment is confirmed after checkout.",
|
|
180
|
+
"cancelAction": "Cancel",
|
|
181
|
+
"cancelAppointmentAction": "Cancel this appointment",
|
|
182
|
+
"chooseTime": "Choose a time",
|
|
183
|
+
"staffAppointmentsSubtitle": "All appointments in your organization.",
|
|
184
|
+
"customerAppointmentsSubtitle": "View and manage your appointments.",
|
|
185
|
+
"searchAppointments": "Search appointments…",
|
|
186
|
+
"allStatuses": "All statuses",
|
|
187
|
+
"allAppointments": "All appointments",
|
|
188
|
+
"host": "Host",
|
|
189
|
+
"customer": "Customer",
|
|
190
|
+
"meetingProvider": "Meeting provider",
|
|
191
|
+
"allHosts": "All hosts",
|
|
192
|
+
"unknownHost": "Unknown host",
|
|
193
|
+
"conflictsOnly": "Conflicts only",
|
|
194
|
+
"hostTimezone": "Host time",
|
|
195
|
+
"customerTimezone": "Customer time",
|
|
196
|
+
"sortDate": "Date",
|
|
197
|
+
"sortTitle": "Product name",
|
|
198
|
+
"sort": "Sort appointments",
|
|
199
|
+
"sortAscending": "Sort ascending",
|
|
200
|
+
"sortDescending": "Sort descending",
|
|
201
|
+
"noMatchingAppointments": "No matching appointments",
|
|
202
|
+
"emptyAppointmentsDescription": "Appointments appear here after booking and checkout.",
|
|
203
|
+
"editAppointment": "Manage appointment",
|
|
204
|
+
"manageAppointment": "Manage appointment",
|
|
205
|
+
"viewAppointment": "View appointment",
|
|
206
|
+
"zoomMeeting": "Zoom meeting",
|
|
207
|
+
"zoomLinkReady": "Link ready",
|
|
208
|
+
"zoomLinkPending": "The Zoom link is being prepared",
|
|
209
|
+
"bookingReference": "Booking reference",
|
|
210
|
+
"joinZoom": "Join Zoom Meeting",
|
|
211
|
+
"dashboard": {
|
|
212
|
+
"hostUpcoming": "Your upcoming appointments",
|
|
213
|
+
"clientUpcoming": "Your upcoming bookings",
|
|
214
|
+
"upcomingDescription": "Your next confirmed appointments at a glance.",
|
|
215
|
+
"noUpcoming": "You have no upcoming appointments.",
|
|
216
|
+
"viewAppointments": "View appointments",
|
|
217
|
+
"overview": "Planning overview",
|
|
218
|
+
"hostOverviewDescription": "Your schedule and calendar health.",
|
|
219
|
+
"clientOverviewDescription": "Your current appointment schedule.",
|
|
220
|
+
"today": "Today",
|
|
221
|
+
"upcoming": "Upcoming",
|
|
222
|
+
"conflicts": "Conflicts",
|
|
223
|
+
"viewConflicts": "View {count} conflicting appointments",
|
|
224
|
+
"manageAvailability": "Manage availability",
|
|
225
|
+
"manageBookings": "Manage bookings"
|
|
226
|
+
},
|
|
227
|
+
"appointmentCount": "{count} appointments",
|
|
228
|
+
"pagination": "Appointment pages",
|
|
229
|
+
"myAppointmentSettings": "My appointment settings",
|
|
230
|
+
"meetingSettings": "Meeting settings",
|
|
231
|
+
"editAvailability": "Edit availability",
|
|
232
|
+
"availabilityFormDescription": "Choose when customers can book you. Times are in {timezone}.",
|
|
233
|
+
"weeklyHelp": "Repeat this time window on the same weekday.",
|
|
234
|
+
"endDateHelp": "Optional. Leave empty to keep repeating.",
|
|
235
|
+
"allProductsHelp": "Includes all assigned products, including future assignments. Turn off to choose specific products.",
|
|
236
|
+
"saveAvailability": "Save availability",
|
|
237
|
+
"selectAtLeastOneProduct": "Select at least one product.",
|
|
238
|
+
"dragAvailabilityHelp": "Drag on the calendar to add availability. Select an existing block to edit it.",
|
|
239
|
+
"calendarAllProducts": "All products",
|
|
240
|
+
"calendarTimezone": "Calendar timezone",
|
|
241
|
+
"timezoneRangeError": "Choose a time range within one day in your calendar settings timezone.",
|
|
242
|
+
"timezoneFavorites": "Favorites",
|
|
243
|
+
"allTimezones": "All timezones",
|
|
244
|
+
"removeTimezoneFavorite": "Remove {timezone} from favorites",
|
|
245
|
+
"timezoneReadOnly": "Viewing only. Switch to {timezone} to edit availability.",
|
|
246
|
+
"calendarReadOnly": "Calendar is read-only.",
|
|
247
|
+
"externalCalendarBusy": "Busy on another calendar",
|
|
248
|
+
"allDay": "All day",
|
|
249
|
+
"externalCalendarLoadError": "Availability from your additional calendars could not be loaded.",
|
|
250
|
+
"switchToUserTimezone": "Switch to my timezone",
|
|
251
|
+
"chooseAppointmentTime": "Choose your time",
|
|
252
|
+
"calendarDate": {
|
|
253
|
+
"available": "Open slots",
|
|
254
|
+
"unavailable": "No slots",
|
|
255
|
+
"past": "Past date",
|
|
256
|
+
"loading": "Loading slots"
|
|
257
|
+
},
|
|
258
|
+
"slotTimesTimezone": "Times shown in {timezone}"
|
|
259
|
+
}
|
|
260
|
+
}
|