@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,260 @@
|
|
|
1
|
+
{
|
|
2
|
+
"planning": {
|
|
3
|
+
"midnight": "middernacht",
|
|
4
|
+
"planning": "Planning",
|
|
5
|
+
"appointments": "Afspraken",
|
|
6
|
+
"emailNewAppointment": "Nieuwe afspraak",
|
|
7
|
+
"emailUpdatedAppointment": "Bijgewerkte afspraak",
|
|
8
|
+
"emailCanceledAppointment": "Geannuleerde afspraak",
|
|
9
|
+
"emailZoomLinkReady": "Zoom-link gereed",
|
|
10
|
+
"availability": "Beschikbaarheid",
|
|
11
|
+
"settings": "Planningsinstellingen",
|
|
12
|
+
"timezone": "Tijdzone",
|
|
13
|
+
"provider": "Medewerker",
|
|
14
|
+
"anyProvider": "Elke beschikbare medewerker",
|
|
15
|
+
"previousMonth": "Vorige maand",
|
|
16
|
+
"nextMonth": "Volgende maand",
|
|
17
|
+
"loading": "Beschikbaarheid laden…",
|
|
18
|
+
"loadError": "Planningsinformatie kon niet worden geladen. Probeer opnieuw.",
|
|
19
|
+
"noAvailability": "Geen beschikbare tijden. Kies een andere datum of medewerker.",
|
|
20
|
+
"slotUnavailable": "Dit tijdstip is niet meer beschikbaar. Kies een ander tijdstip.",
|
|
21
|
+
"reserveHint": "Een tijdstip kiezen reserveert het tijdens het afrekenen. De uiterste reserveringstijd staat bij het afrekenen.",
|
|
22
|
+
"continueToCheckout": "Verder naar afrekenen",
|
|
23
|
+
"changeDate": "Andere datum",
|
|
24
|
+
"offeringDetails": "Details van het aanbod",
|
|
25
|
+
"confirmTimeHint": "Controleer je tijdstip en ga verder om het te reserveren tijdens het afrekenen.",
|
|
26
|
+
"bookNow": "Boek nu",
|
|
27
|
+
"back": "Terug",
|
|
28
|
+
"duration": "{minutes} minuten",
|
|
29
|
+
"noAppointments": "Nog geen afspraken.",
|
|
30
|
+
"manage": "Afspraak beheren",
|
|
31
|
+
"confirmed": "Bevestigd",
|
|
32
|
+
"cancelled": "Geannuleerd",
|
|
33
|
+
"reschedule": "Afspraak wijzigen",
|
|
34
|
+
"cancel": "Afspraak annuleren",
|
|
35
|
+
"joinMeeting": "Deelnemen aan vergadering",
|
|
36
|
+
"changesUsed": "{used} wijzigingen gebruikt; {free} inbegrepen zonder kosten.",
|
|
37
|
+
"changeCosts": "Deze wijziging kost {amount}. Je huidige afspraak blijft bevestigd totdat het afrekenen is voltooid.",
|
|
38
|
+
"freeChange": "Deze wijziging is inbegrepen zonder kosten. Rond het afrekenen af om je nieuwe tijdstip te bevestigen.",
|
|
39
|
+
"cancelConfirm": "Deze afspraak annuleren? Je terugbetaling is {amount}.",
|
|
40
|
+
"confirmCancellation": "Annulering bevestigen",
|
|
41
|
+
"actionError": "Deze actie kon niet worden voltooid. Probeer opnieuw.",
|
|
42
|
+
"reservationMinutes": "Reserveringsduur (minuten)",
|
|
43
|
+
"slotIntervalMinutes": "Interval starttijden (minuten)",
|
|
44
|
+
"displayIntervalMinutes": "Maximale afstand tussen getoonde starttijden (minuten)",
|
|
45
|
+
"bookingHorizonDays": "Maximaal aantal dagen vooruit boeken",
|
|
46
|
+
"minimumNoticeMinutes": "Minimale tijd voor een boeking (minuten)",
|
|
47
|
+
"freeChanges": "Inbegrepen afspraakwijzigingen",
|
|
48
|
+
"rescheduleCutoffMinutes": "Wijzigingstermijn voor de afspraak (minuten)",
|
|
49
|
+
"cancellationCutoffMinutes": "Annuleringstermijn voor de afspraak (minuten)",
|
|
50
|
+
"refundPercentage": "Terugbetaling bij annulering (%)",
|
|
51
|
+
"cancellationEnabled": "Klantannuleringen toestaan",
|
|
52
|
+
"changeFeeCurrency": "Wijzigingskosten ({currency})",
|
|
53
|
+
"changeFees": "Wijzigingskosten",
|
|
54
|
+
"policyGroup": {
|
|
55
|
+
"schedule": {
|
|
56
|
+
"title": "Tijdsindeling afspraken",
|
|
57
|
+
"description": "Stel reserveringen en de afstand tussen aangeboden starttijden in."
|
|
58
|
+
},
|
|
59
|
+
"bookingWindow": {
|
|
60
|
+
"title": "Boekingsperiode",
|
|
61
|
+
"description": "Bepaal hoe vroeg en hoe ver vooruit klanten kunnen boeken."
|
|
62
|
+
},
|
|
63
|
+
"changes": {
|
|
64
|
+
"title": "Afspraakwijzigingen",
|
|
65
|
+
"description": "Stel inbegrepen wijzigingen, termijnen en eventuele kosten in."
|
|
66
|
+
},
|
|
67
|
+
"cancellations": {
|
|
68
|
+
"title": "Annuleringen en terugbetalingen",
|
|
69
|
+
"description": "Bepaal of klanten kunnen annuleren en welke terugbetaling zij ontvangen."
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"connections": "Agenda- en vergaderverbindingen",
|
|
73
|
+
"connected": "Verbonden",
|
|
74
|
+
"notConnected": "Niet verbonden",
|
|
75
|
+
"connect": "Verbinden of opnieuw verbinden",
|
|
76
|
+
"disconnect": "Verbinding verbreken",
|
|
77
|
+
"connectionError": "De verbinding met Zoom kon niet worden gestart. Je beheerder moet Zoom mogelijk instellen.",
|
|
78
|
+
"googleConnectionError": "Google Agenda is nog niet ingesteld voor dit portaal. Vraag een beheerder om de Google OAuth-gegevens toe te voegen.",
|
|
79
|
+
"connectMainCalendar": "Verbind je hoofdagenda",
|
|
80
|
+
"connectMainCalendarDescription": "Begin met de Google Agenda waarin je portaalafspraken moeten komen.",
|
|
81
|
+
"mainCalendarPermissions": "Je hoofdagenda heeft lees- en schrijftoegang nodig zodat het portaal conflicten kan controleren en afspraakgebeurtenissen kan beheren.",
|
|
82
|
+
"connectGoogleCalendar": "Google Agenda verbinden",
|
|
83
|
+
"mainCalendar": "Hoofdagenda",
|
|
84
|
+
"mainCalendarFixedDescription": "Deze agenda ontvangt beschikbaarheid en afspraken uit het portaal. Wisselen wordt een aparte, bewuste actie.",
|
|
85
|
+
"chooseMainCalendarDescription": "Kies de beschrijfbare agenda waarin portaalafspraken moeten komen. Deze keuze staat na de configuratie vast.",
|
|
86
|
+
"readWrite": "Lezen en schrijven",
|
|
87
|
+
"portalCalendarDestination": "Beschikbaarheid en afspraken uit het portaal",
|
|
88
|
+
"chooseMainCalendar": "Kies je hoofdagenda",
|
|
89
|
+
"additionalCalendars": "Aanvullende agenda's",
|
|
90
|
+
"additionalCalendarsDescription": "Controleer optioneel andere agenda's op conflicten. Het portaal schrijft hier nooit afspraakgebeurtenissen naartoe.",
|
|
91
|
+
"readOnly": "Alleen conflictcontrole",
|
|
92
|
+
"addConflictCalendars": "Agenda's toevoegen om te controleren",
|
|
93
|
+
"noAdditionalCalendars": "Er zijn geen andere agenda's beschikbaar in dit Google-account.",
|
|
94
|
+
"schedulingPreferences": "Planningsvoorkeuren",
|
|
95
|
+
"setMainCalendar": "Hoofdagenda instellen",
|
|
96
|
+
"calendarSettings": "Agenda-instellingen",
|
|
97
|
+
"graceMinutes": "Rusttijd na portaalafspraken (minuten)",
|
|
98
|
+
"availabilityCalendarTitle": "Titel van beschikbaarheid in agenda",
|
|
99
|
+
"availabilityCalendarTitleDescription": "Wordt als titel getoond bij beschikbaarheid in je hoofdagenda.",
|
|
100
|
+
"busyCalendars": "Agenda’s die beschikbaarheid blokkeren",
|
|
101
|
+
"writeCalendar": "Agenda voor beschikbaarheid en boekingen",
|
|
102
|
+
"save": "Opslaan",
|
|
103
|
+
"planningDisabled": "Planning staat voor jou uit. Je beheerder kan dit na het instellen inschakelen.",
|
|
104
|
+
"previousWeek": "Vorige week",
|
|
105
|
+
"nextWeek": "Volgende week",
|
|
106
|
+
"addWindow": "Beschikbaarheid toevoegen",
|
|
107
|
+
"allProducts": "Alle toegewezen planbare producten",
|
|
108
|
+
"selectedProducts": "Geselecteerde producten",
|
|
109
|
+
"conflict": "Agendaconflict",
|
|
110
|
+
"syncError": "Agenda- of vergadersynchronisatie vereist aandacht.",
|
|
111
|
+
"editOne": "Alleen dit moment wijzigen",
|
|
112
|
+
"date": "Datum",
|
|
113
|
+
"startTime": "Begintijd",
|
|
114
|
+
"endTime": "Eindtijd",
|
|
115
|
+
"weekly": "Wekelijks herhalen",
|
|
116
|
+
"endDate": "Laatste datum (optioneel)",
|
|
117
|
+
"products": "Producten",
|
|
118
|
+
"delete": "Beschikbaarheid verwijderen",
|
|
119
|
+
"deleteOccurrenceConfirm": "Dit moment verwijderen? Bevestigde afspraken blijven bestaan.",
|
|
120
|
+
"deleteSeriesConfirm": "Dit beschikbaarheidsblok of deze reeks verwijderen? Bevestigde afspraken blijven bestaan.",
|
|
121
|
+
"providers": "Medewerkers",
|
|
122
|
+
"organizationPolicy": "Boekingsbeleid organisatie",
|
|
123
|
+
"pendingTasks": "Synchronisatie en terugbetalingen",
|
|
124
|
+
"pendingTasksDescription": "Bekijk de geschiedenis van agenda-, afspraak-, checkout- en terugbetalingstaken voor deze organisatie.",
|
|
125
|
+
"noPendingTasks": "Alles is verwerkt.",
|
|
126
|
+
"noPendingTasksDescription": "Er zijn geen synchronisatie- of terugbetalingstaken die aandacht nodig hebben.",
|
|
127
|
+
"noMatchingTasks": "Geen overeenkomende taken",
|
|
128
|
+
"noMatchingTasksDescription": "Geen synchronisatie- of terugbetalingstaken komen overeen met deze filters.",
|
|
129
|
+
"attempts": "{count} pogingen",
|
|
130
|
+
"retry": "Openstaande taken opnieuw proberen",
|
|
131
|
+
"retryAll": "Alles opnieuw proberen",
|
|
132
|
+
"retryTask": "Taak opnieuw proberen",
|
|
133
|
+
"searchSynchronization": "Zoek taken, fouten of afspraken…",
|
|
134
|
+
"allSyncStatuses": "Alle statussen",
|
|
135
|
+
"allTaskTypes": "Alle taaktypen",
|
|
136
|
+
"taskType": "Taaktype",
|
|
137
|
+
"lastEvent": "Laatste gebeurtenis",
|
|
138
|
+
"lastEventValue": "Laatste gebeurtenis: {date}",
|
|
139
|
+
"nextRetryValue": "Volgende automatische poging: {date}",
|
|
140
|
+
"attemptCount": "Pogingen",
|
|
141
|
+
"failed": "Mislukt",
|
|
142
|
+
"queued": "In wachtrij",
|
|
143
|
+
"succeeded": "Geslaagd",
|
|
144
|
+
"scheduledCheck": "Geplande controle",
|
|
145
|
+
"taskCount": "{count} taken",
|
|
146
|
+
"jobAllProducts": "Alle producten",
|
|
147
|
+
"noProducts": "Geen producten",
|
|
148
|
+
"noDestinationCalendar": "Geen doelagenda geselecteerd",
|
|
149
|
+
"synchronizationPages": "Pagina's met synchronisatietaken",
|
|
150
|
+
"jobKinds": {
|
|
151
|
+
"appointment": "Afspraaksynchronisatie",
|
|
152
|
+
"availability": "Beschikbaarheidssynchronisatie",
|
|
153
|
+
"repair": "Afspraakherstel",
|
|
154
|
+
"refund": "Terugbetaling",
|
|
155
|
+
"cleanup": "Agenda opschonen",
|
|
156
|
+
"close-checkout": "Checkout opschonen",
|
|
157
|
+
"failed-booking": "Mislukte boeking herstellen",
|
|
158
|
+
"sync": "Agenda-afstemming",
|
|
159
|
+
"conflict": "Melding agendaconflict"
|
|
160
|
+
},
|
|
161
|
+
"product": "Product",
|
|
162
|
+
"status": "Status",
|
|
163
|
+
"overridePolicySection": "Deze sectie overschrijven",
|
|
164
|
+
"time": "Tijd",
|
|
165
|
+
"timezone": "Tijdzone",
|
|
166
|
+
"meetingUrl": "Vergaderlink",
|
|
167
|
+
"meetingDetails": "Vergadergegevens",
|
|
168
|
+
"instructions": "Instructies na aankoop",
|
|
169
|
+
"url": "Portaallink",
|
|
170
|
+
"pendingChange": "Een vervangend tijdstip is gereserveerd. Je huidige afspraak blijft bevestigd totdat de wijziging is voltooid.",
|
|
171
|
+
"abandonChange": "Lopende wijziging afbreken",
|
|
172
|
+
"planningDisabledAdmin": "Planning staat uit voor je account. Stel eerst je verbindingen in en schakel planning daarna in bij Planninginstellingen.",
|
|
173
|
+
"bookingPolicyTab": "Boekingsbeleid",
|
|
174
|
+
"synchronizationTab": "Synchronisatie",
|
|
175
|
+
"staffChangeNotice": "Wijzigingen door beheerders brengen geen klantkosten in rekening en gebruiken geen inbegrepen wijzigingen.",
|
|
176
|
+
"newAppointment": "Nieuwe afspraak",
|
|
177
|
+
"close": "Sluiten",
|
|
178
|
+
"chooseProduct": "Kies een product",
|
|
179
|
+
"newAppointmentNotice": "Kies een product, selecteer een tijdstip en reken af. De afspraak wordt na het afrekenen bevestigd.",
|
|
180
|
+
"cancelAction": "Annuleren",
|
|
181
|
+
"cancelAppointmentAction": "Deze afspraak annuleren",
|
|
182
|
+
"chooseTime": "Kies een tijdstip",
|
|
183
|
+
"staffAppointmentsSubtitle": "Alle afspraken binnen je organisatie.",
|
|
184
|
+
"customerAppointmentsSubtitle": "Bekijk en beheer je afspraken.",
|
|
185
|
+
"searchAppointments": "Zoek afspraken…",
|
|
186
|
+
"allStatuses": "Alle statussen",
|
|
187
|
+
"allAppointments": "Alle afspraken",
|
|
188
|
+
"host": "Host",
|
|
189
|
+
"customer": "Klant",
|
|
190
|
+
"meetingProvider": "Vergaderprovider",
|
|
191
|
+
"allHosts": "Alle hosts",
|
|
192
|
+
"unknownHost": "Onbekende host",
|
|
193
|
+
"conflictsOnly": "Alleen conflicten",
|
|
194
|
+
"hostTimezone": "Tijd van de host",
|
|
195
|
+
"customerTimezone": "Tijd van de klant",
|
|
196
|
+
"sortDate": "Datum",
|
|
197
|
+
"sortTitle": "Productnaam",
|
|
198
|
+
"sort": "Afspraken sorteren",
|
|
199
|
+
"sortAscending": "Oplopend sorteren",
|
|
200
|
+
"sortDescending": "Aflopend sorteren",
|
|
201
|
+
"noMatchingAppointments": "Geen overeenkomende afspraken",
|
|
202
|
+
"emptyAppointmentsDescription": "Afspraken verschijnen hier na het boeken en afrekenen.",
|
|
203
|
+
"editAppointment": "Afspraak beheren",
|
|
204
|
+
"manageAppointment": "Afspraak beheren",
|
|
205
|
+
"viewAppointment": "Afspraak bekijken",
|
|
206
|
+
"zoomMeeting": "Zoom-vergadering",
|
|
207
|
+
"zoomLinkReady": "Link gereed",
|
|
208
|
+
"zoomLinkPending": "De Zoom-link wordt voorbereid",
|
|
209
|
+
"bookingReference": "Boekingsreferentie",
|
|
210
|
+
"joinZoom": "Deelnemen aan Zoom-vergadering",
|
|
211
|
+
"dashboard": {
|
|
212
|
+
"hostUpcoming": "Je komende afspraken",
|
|
213
|
+
"clientUpcoming": "Je komende boekingen",
|
|
214
|
+
"upcomingDescription": "Je eerstvolgende bevestigde afspraken in één overzicht.",
|
|
215
|
+
"noUpcoming": "Je hebt geen komende afspraken.",
|
|
216
|
+
"viewAppointments": "Afspraken bekijken",
|
|
217
|
+
"overview": "Planningsoverzicht",
|
|
218
|
+
"hostOverviewDescription": "Je planning en de status van je agenda.",
|
|
219
|
+
"clientOverviewDescription": "Je huidige afsprakenplanning.",
|
|
220
|
+
"today": "Vandaag",
|
|
221
|
+
"upcoming": "Komend",
|
|
222
|
+
"conflicts": "Conflicten",
|
|
223
|
+
"viewConflicts": "Bekijk {count} conflicterende afspraken",
|
|
224
|
+
"manageAvailability": "Beschikbaarheid beheren",
|
|
225
|
+
"manageBookings": "Boekingen beheren"
|
|
226
|
+
},
|
|
227
|
+
"appointmentCount": "{count} afspraken",
|
|
228
|
+
"pagination": "Afspraakpagina’s",
|
|
229
|
+
"myAppointmentSettings": "Mijn afspraakinstellingen",
|
|
230
|
+
"meetingSettings": "Vergaderinstellingen",
|
|
231
|
+
"editAvailability": "Beschikbaarheid wijzigen",
|
|
232
|
+
"availabilityFormDescription": "Kies wanneer klanten je kunnen boeken. Tijden zijn in {timezone}.",
|
|
233
|
+
"weeklyHelp": "Herhaal dit tijdvak op dezelfde weekdag.",
|
|
234
|
+
"endDateHelp": "Optioneel. Laat leeg om te blijven herhalen.",
|
|
235
|
+
"allProductsHelp": "Geldt voor alle toegewezen producten, ook toekomstige toewijzingen. Zet uit om specifieke producten te kiezen.",
|
|
236
|
+
"saveAvailability": "Beschikbaarheid opslaan",
|
|
237
|
+
"selectAtLeastOneProduct": "Selecteer ten minste één product.",
|
|
238
|
+
"dragAvailabilityHelp": "Sleep in de kalender om beschikbaarheid toe te voegen. Selecteer een bestaand blok om het te wijzigen.",
|
|
239
|
+
"calendarAllProducts": "Alle producten",
|
|
240
|
+
"calendarTimezone": "Kalendertijdzone",
|
|
241
|
+
"timezoneRangeError": "Kies een tijdvak binnen één dag in de tijdzone van je kalenderinstellingen.",
|
|
242
|
+
"timezoneFavorites": "Favorieten",
|
|
243
|
+
"allTimezones": "Alle tijdzones",
|
|
244
|
+
"removeTimezoneFavorite": "Verwijder {timezone} uit favorieten",
|
|
245
|
+
"timezoneReadOnly": "Alleen bekijken. Schakel naar {timezone} om beschikbaarheid te wijzigen.",
|
|
246
|
+
"calendarReadOnly": "De kalender is alleen-lezen.",
|
|
247
|
+
"externalCalendarBusy": "Bezet in een andere agenda",
|
|
248
|
+
"allDay": "Hele dag",
|
|
249
|
+
"externalCalendarLoadError": "Beschikbaarheid uit je aanvullende agenda's kon niet worden geladen.",
|
|
250
|
+
"switchToUserTimezone": "Schakel naar mijn tijdzone",
|
|
251
|
+
"chooseAppointmentTime": "Kies je tijd",
|
|
252
|
+
"calendarDate": {
|
|
253
|
+
"available": "Beschikbare tijden",
|
|
254
|
+
"unavailable": "Geen tijden",
|
|
255
|
+
"past": "Datum verstreken",
|
|
256
|
+
"loading": "Tijden laden"
|
|
257
|
+
},
|
|
258
|
+
"slotTimesTimezone": "Tijden weergegeven in {timezone}"
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
CREATE SCHEMA IF NOT EXISTS planning;
|
|
2
|
+
CREATE EXTENSION IF NOT EXISTS btree_gist;
|
|
3
|
+
CREATE TABLE planning.settings (
|
|
4
|
+
store_id text PRIMARY KEY REFERENCES public.organization(id), policy jsonb NOT NULL
|
|
5
|
+
);
|
|
6
|
+
CREATE TABLE planning.provider (
|
|
7
|
+
store_id text NOT NULL REFERENCES public.organization(id), user_id text NOT NULL REFERENCES public."user"(id),
|
|
8
|
+
enabled boolean NOT NULL DEFAULT false, timezone text NOT NULL DEFAULT 'Europe/Amsterdam', grace_minutes integer NOT NULL DEFAULT 0 CHECK(grace_minutes BETWEEN 0 AND 1440),
|
|
9
|
+
busy_calendar_ids text[] NOT NULL DEFAULT '{}', write_calendar_id text, PRIMARY KEY(store_id,user_id)
|
|
10
|
+
);
|
|
11
|
+
CREATE TABLE planning.connection (
|
|
12
|
+
store_id text NOT NULL, user_id text NOT NULL, provider text NOT NULL CHECK(provider IN ('google','zoom')),
|
|
13
|
+
credentials text NOT NULL, external_user_id text NOT NULL, healthy boolean NOT NULL DEFAULT true, error text,
|
|
14
|
+
PRIMARY KEY(store_id,user_id,provider), FOREIGN KEY(store_id,user_id) REFERENCES planning.provider(store_id,user_id)
|
|
15
|
+
);
|
|
16
|
+
CREATE TABLE planning.oauth_state (
|
|
17
|
+
id text PRIMARY KEY, store_id text NOT NULL, user_id text NOT NULL, provider text NOT NULL,
|
|
18
|
+
expires_at timestamptz NOT NULL, FOREIGN KEY(store_id,user_id) REFERENCES planning.provider(store_id,user_id)
|
|
19
|
+
);
|
|
20
|
+
CREATE TABLE planning.availability (
|
|
21
|
+
id uuid PRIMARY KEY, store_id text NOT NULL, user_id text NOT NULL, data jsonb NOT NULL, revision integer NOT NULL DEFAULT 1,
|
|
22
|
+
deleted boolean NOT NULL DEFAULT false, calendar_event_id text, calendar_id text,
|
|
23
|
+
FOREIGN KEY(store_id,user_id) REFERENCES planning.provider(store_id,user_id)
|
|
24
|
+
);
|
|
25
|
+
CREATE TABLE planning.reservation (
|
|
26
|
+
id uuid PRIMARY KEY, store_id text NOT NULL, user_id text NOT NULL, product_id text NOT NULL REFERENCES products.product(id),
|
|
27
|
+
token_hash text NOT NULL UNIQUE, start_at timestamptz NOT NULL, end_at timestamptz NOT NULL, blocked_until timestamptz NOT NULL,
|
|
28
|
+
expires_at timestamptz NOT NULL, confirmed_at timestamptz, status text NOT NULL CHECK(status IN ('reserved','confirmed','expired','cancelled','superseded')),
|
|
29
|
+
order_id text UNIQUE REFERENCES products.orders(id), replaces_id uuid, snapshot jsonb NOT NULL,
|
|
30
|
+
FOREIGN KEY(store_id,user_id) REFERENCES planning.provider(store_id,user_id),
|
|
31
|
+
CHECK(start_at < end_at AND end_at <= blocked_until),
|
|
32
|
+
EXCLUDE USING gist (user_id WITH =, tstzrange(start_at, blocked_until, '[)') WITH &&) WHERE (status IN ('reserved','confirmed'))
|
|
33
|
+
);
|
|
34
|
+
CREATE INDEX reservation_expiry ON planning.reservation(expires_at) WHERE status='reserved';
|
|
35
|
+
CREATE TABLE planning.appointment (
|
|
36
|
+
id uuid PRIMARY KEY, store_id text NOT NULL, reservation_id uuid NOT NULL UNIQUE REFERENCES planning.reservation(id),
|
|
37
|
+
order_line_id text NOT NULL UNIQUE REFERENCES products.order_line(id), order_id text NOT NULL REFERENCES products.orders(id),
|
|
38
|
+
user_id text NOT NULL REFERENCES public."user"(id), product_id text NOT NULL REFERENCES products.product(id),
|
|
39
|
+
start_at timestamptz NOT NULL, end_at timestamptz NOT NULL, status text NOT NULL CHECK(status IN ('confirmed','cancelled')),
|
|
40
|
+
changes integer NOT NULL DEFAULT 0, revision integer NOT NULL DEFAULT 1, snapshot jsonb NOT NULL,
|
|
41
|
+
meeting_id text, meeting_url text, calendar_event_id text, calendar_id text, calendar_user_id text,
|
|
42
|
+
conflict boolean NOT NULL DEFAULT false, effects_error text
|
|
43
|
+
);
|
|
44
|
+
ALTER TABLE planning.reservation ADD FOREIGN KEY(replaces_id) REFERENCES planning.appointment(id);
|
|
45
|
+
CREATE TABLE planning.audit (
|
|
46
|
+
id uuid PRIMARY KEY, appointment_id uuid NOT NULL REFERENCES planning.appointment(id), actor_id text REFERENCES public."user"(id),
|
|
47
|
+
action text NOT NULL, data jsonb NOT NULL DEFAULT '{}', created_at timestamptz NOT NULL DEFAULT now()
|
|
48
|
+
);
|
|
49
|
+
CREATE TABLE planning.job (
|
|
50
|
+
id text PRIMARY KEY, kind text NOT NULL, payload jsonb NOT NULL, attempts integer NOT NULL DEFAULT 0,
|
|
51
|
+
available_at timestamptz NOT NULL DEFAULT now(), completed_at timestamptz, error text
|
|
52
|
+
);
|
|
53
|
+
CREATE INDEX job_pending ON planning.job(available_at) WHERE completed_at IS NULL;
|
|
54
|
+
CREATE TABLE planning.watch (
|
|
55
|
+
id uuid PRIMARY KEY, store_id text NOT NULL, user_id text NOT NULL, calendar_id text NOT NULL,
|
|
56
|
+
token_hash text NOT NULL, resource_id text, expires_at timestamptz NOT NULL,
|
|
57
|
+
FOREIGN KEY(store_id,user_id) REFERENCES planning.provider(store_id,user_id)
|
|
58
|
+
);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
ALTER TABLE planning.job
|
|
2
|
+
ADD COLUMN created_at timestamptz NOT NULL DEFAULT now(),
|
|
3
|
+
ADD COLUMN last_attempt_at timestamptz;
|
|
4
|
+
|
|
5
|
+
UPDATE planning.job
|
|
6
|
+
SET last_attempt_at = available_at - make_interval(secs => LEAST(3600, (5 * power(2::numeric, LEAST(attempts, 10)))::int))
|
|
7
|
+
WHERE attempts > 0;
|
|
8
|
+
|
|
9
|
+
CREATE INDEX job_pending_activity
|
|
10
|
+
ON planning.job (COALESCE(last_attempt_at, created_at) DESC)
|
|
11
|
+
WHERE completed_at IS NULL;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
WITH ranked AS (
|
|
2
|
+
SELECT id,
|
|
3
|
+
row_number() OVER (
|
|
4
|
+
PARTITION BY payload->>'id'
|
|
5
|
+
ORDER BY created_at DESC, id DESC
|
|
6
|
+
) AS position
|
|
7
|
+
FROM planning.job
|
|
8
|
+
WHERE completed_at IS NULL
|
|
9
|
+
AND kind = 'availability'
|
|
10
|
+
)
|
|
11
|
+
DELETE FROM planning.job job
|
|
12
|
+
USING ranked
|
|
13
|
+
WHERE job.id = ranked.id
|
|
14
|
+
AND ranked.position > 1;
|
|
15
|
+
|
|
16
|
+
CREATE UNIQUE INDEX job_pending_availability_target
|
|
17
|
+
ON planning.job ((payload->>'id'))
|
|
18
|
+
WHERE completed_at IS NULL
|
|
19
|
+
AND kind = 'availability';
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
|
|
3
|
+
export default defineNuxtConfig({
|
|
4
|
+
$meta: { name: 'nuxt-customer-portal-planning' },
|
|
5
|
+
compatibilityDate: '2025-10-24',
|
|
6
|
+
modules: ['@nuxtjs/i18n'],
|
|
7
|
+
components: [{ path: fileURLToPath(new URL('./app/components', import.meta.url)), global: true }],
|
|
8
|
+
nitro: { experimental: { tasks: true }, scheduledTasks: { '* * * * *': ['planning:reconcile'] } },
|
|
9
|
+
i18n: {
|
|
10
|
+
locales: [
|
|
11
|
+
{ code: 'en', file: 'en.json' },
|
|
12
|
+
{ code: 'nl', file: 'nl.json' }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nuxt-customer-portal/planning",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"homepage": "https://nuxt-customer-portal.com",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ludulicious/nuxt-customer-portal.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./nuxt.config.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"CHANGELOG.md",
|
|
15
|
+
"app",
|
|
16
|
+
"i18n",
|
|
17
|
+
"migrations",
|
|
18
|
+
"server",
|
|
19
|
+
"shared",
|
|
20
|
+
"nuxt.config.ts",
|
|
21
|
+
"portal.manifest.mjs",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./nuxt.config.ts",
|
|
26
|
+
"./feature": "./shared/feature.ts",
|
|
27
|
+
"./types": "./shared/types.ts",
|
|
28
|
+
"./schema": "./server/db/schema/planning.ts",
|
|
29
|
+
"./portal-manifest": "./portal.manifest.mjs",
|
|
30
|
+
"./app/*": "./app/*.ts",
|
|
31
|
+
"./server/*": "./server/*.ts",
|
|
32
|
+
"./shared/*": "./shared/*.ts"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@nuxtjs/i18n": "^10.6.0",
|
|
39
|
+
"@types/pg": "8.15.5",
|
|
40
|
+
"drizzle-orm": "^0.45.2",
|
|
41
|
+
"pg": "^8.22.0",
|
|
42
|
+
"zod": "^4.4.3",
|
|
43
|
+
"@nuxt-customer-portal/core": "^0.4.0",
|
|
44
|
+
"@nuxt-customer-portal/ui": "^0.4.0",
|
|
45
|
+
"@nuxt-customer-portal/products": "^0.4.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"nuxt": "^4.5.1",
|
|
49
|
+
"vue": "^3.5.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"nuxt": "^4.5.1",
|
|
53
|
+
"vue": "^3.5.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineEventHandler, getRouterParam } from 'h3'
|
|
2
|
+
import { planningAdmin } from '@nuxt-customer-portal/planning/server/utils/access'
|
|
3
|
+
import { rows } from '@nuxt-customer-portal/products/server/utils/database'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const context = await planningAdmin(event)
|
|
7
|
+
return rows(
|
|
8
|
+
'SELECT x.action,x.data,x.created_at,u.name AS actor FROM planning.audit x JOIN planning.appointment a ON a.id=x.appointment_id LEFT JOIN public."user" u ON u.id=x.actor_id WHERE a.store_id=$1 AND a.id::text=$2 ORDER BY x.created_at',
|
|
9
|
+
[context.organizationId, getRouterParam(event, 'id')]
|
|
10
|
+
)
|
|
11
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineEventHandler } from 'h3'
|
|
2
|
+
import { planningAdmin } from '@nuxt-customer-portal/planning/server/utils/access'
|
|
3
|
+
import { rows } from '@nuxt-customer-portal/products/server/utils/database'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const context = await planningAdmin(event)
|
|
7
|
+
return rows(
|
|
8
|
+
`SELECT a.id,a.start_at AS start,a.end_at AS end,a.status,a.snapshot->>'title' AS title,a.conflict,a.effects_error AS "effectsError",u.name AS "providerName",o.email FROM planning.appointment a JOIN public."user" u ON u.id=a.user_id JOIN products.orders o ON o.id=a.order_id WHERE a.store_id=$1 ORDER BY start_at DESC LIMIT 200`,
|
|
9
|
+
[context.organizationId]
|
|
10
|
+
)
|
|
11
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createError, defineEventHandler, getRouterParam } from 'h3'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { planningAdmin, sameOrigin } from '@nuxt-customer-portal/planning/server/utils/access'
|
|
4
|
+
import { getStore } from '@nuxt-customer-portal/products/server/utils/access'
|
|
5
|
+
import { rows } from '@nuxt-customer-portal/products/server/utils/database'
|
|
6
|
+
import { runJobs } from '@nuxt-customer-portal/planning/server/utils/jobs'
|
|
7
|
+
|
|
8
|
+
export default defineEventHandler(async (event) => {
|
|
9
|
+
sameOrigin(event)
|
|
10
|
+
await planningAdmin(event)
|
|
11
|
+
const store = await getStore()
|
|
12
|
+
const id = z.string().min(1).max(500).parse(getRouterParam(event, 'id'))
|
|
13
|
+
const updated = await rows<{ id: string }>(
|
|
14
|
+
`UPDATE planning.job j SET available_at=now() WHERE id=$1 AND completed_at IS NULL AND COALESCE(
|
|
15
|
+
j.payload->>'storeId',
|
|
16
|
+
(SELECT a.store_id FROM planning.appointment a WHERE a.id::text=j.payload->>'appointmentId'),
|
|
17
|
+
(SELECT w.store_id FROM planning.availability w WHERE w.id::text=j.payload->>'id'),
|
|
18
|
+
(SELECT o.store_id FROM products.orders o WHERE o.id::text=j.payload->>'orderId')
|
|
19
|
+
)=$2 RETURNING id`,
|
|
20
|
+
[id, store.organization_id]
|
|
21
|
+
)
|
|
22
|
+
if (!updated.length) {
|
|
23
|
+
throw createError({ statusCode: 404, message: 'Synchronization task not found' })
|
|
24
|
+
}
|
|
25
|
+
return runJobs(1, store.organization_id, id)
|
|
26
|
+
})
|