@tvsgroup/appointment-form 0.1.1

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/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @tvsgroup/appointment-form
2
+
3
+ Multi-step appointment booking form for TVS Engineering, shipped as the `<tvs-appointment-form>` web component (Vue 3 inside a shadow root) with an optional typed Vue wrapper. Consumable three ways:
4
+
5
+ 1. **Web component** from any framework or plain HTML (Astro, WordPress, …)
6
+ 2. **Vue wrapper component** (`AppointmentForm`) for Vue apps / Astro with `@astrojs/vue`
7
+ 3. **Standalone bundle** (`dist/appointment-form.es.js`) served from S3 — the legacy embed, unchanged
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ npm install @tvsgroup/appointment-form
13
+ ```
14
+
15
+ Vue 3 is a peer dependency (npm ≥7 installs it automatically). All other runtime dependencies (Pinia, intl-tel-input, js-datepicker, …) are bundled.
16
+
17
+ ## Attributes / Props
18
+
19
+ | Custom-element attribute | Vue wrapper prop | Type (wrapper) | Default | Description |
20
+ |---|---|---|---|---|
21
+ | `lang` | `lang` | `string` | `'nl'` | UI language (`nl` or `en`) |
22
+ | `aws_url` | `awsUrl` | `string` | — (required) | Base URL of the appointment API |
23
+ | `vacation_mode` | `vacationMode` | `boolean` | `false` | Holiday closure: disables queue/direct lanes, shows closure banner. Attribute form accepts `"true"` / `"1"` / `"yes"` / bare attribute |
24
+ | `vacation_start` | `vacationStart` | `string` | `''` | First closed day, ISO `YYYY-MM-DD` |
25
+ | `vacation_end` | `vacationEnd` | `string` | `''` | Last closed day, ISO `YYYY-MM-DD` |
26
+ | `vacation_reopen` | `vacationReopen` | `string` | `''` | First day back at work, ISO `YYYY-MM-DD` |
27
+
28
+ ## Usage
29
+
30
+ ### Astro — web component (no framework integration needed)
31
+
32
+ ```astro
33
+ ---
34
+ // src/pages/afspraak.astro
35
+ ---
36
+ <tvs-appointment-form
37
+ lang="nl"
38
+ aws_url="https://<api-id>.execute-api.us-east-1.amazonaws.com"
39
+ ></tvs-appointment-form>
40
+
41
+ <script>
42
+ import '@tvsgroup/appointment-form/register';
43
+ </script>
44
+ ```
45
+
46
+ The `/register` import defines the element on the client (idempotent, SSR-safe).
47
+
48
+ ### Astro + `@astrojs/vue` — Vue wrapper
49
+
50
+ ```astro
51
+ ---
52
+ import { AppointmentForm } from '@tvsgroup/appointment-form';
53
+ ---
54
+ <AppointmentForm
55
+ client:only="vue"
56
+ lang="nl"
57
+ awsUrl="https://<api-id>.execute-api.us-east-1.amazonaws.com"
58
+ vacationMode={false}
59
+ />
60
+ ```
61
+
62
+ `client:only="vue"` is recommended — the form is fully interactive and gains nothing from SSR.
63
+
64
+ ### Plain HTML (standalone S3 bundle — legacy embed)
65
+
66
+ ```html
67
+ <script type="module" src="https://<bucket>/appointment-form.es.js"></script>
68
+
69
+ <tvs-appointment-form
70
+ lang="nl"
71
+ aws_url="https://<api-id>.execute-api.us-east-1.amazonaws.com"
72
+ ></tvs-appointment-form>
73
+ ```
74
+
75
+ ### Host-page notes
76
+
77
+ - **Multiple instances supported.** Each `<tvs-appointment-form>` element gets its own Pinia instance (installed per element via `configureApp`), so several forms on one page keep fully independent state.
78
+ - **Fonts**: the component inherits the host page's font stack; the TVS look expects [Inter](https://fonts.google.com/specimen/Inter) to be loaded by the host.
79
+ - **Styles** are encapsulated in a shadow root — the host page's CSS (and Tailwind config) won't clash with the form's.
80
+ - **Runtime network**: the phone input loads the intl-tel-input utils script from jsDelivr and does a geo-IP lookup (ipapi.co) for the default country flag.
81
+
82
+ ## Package layout
83
+
84
+ | Entry | File | Vue |
85
+ |---|---|---|
86
+ | `@tvsgroup/appointment-form` | `dist/index.js` — `AppointmentForm`, `register()`, `TAG_NAME`, types | external (peer) |
87
+ | `@tvsgroup/appointment-form/register` | `dist/register.js` — side-effect: defines the element | external (peer) |
88
+ | standalone | `dist/appointment-form.es.js` — self-contained, self-registering | bundled |
89
+
90
+ ## Development
91
+
92
+ ```sh
93
+ npm install
94
+ npm run dev # dev harness (src/main.ts + App.vue)
95
+ npm run build # standalone bundle + npm library (dist/)
96
+ npm run test:unit # vitest
97
+ npm run e2e:dry # Playwright suite in dry-run mode
98
+ npm run type-check
99
+ ```
100
+
101
+ Deployment of the standalone bundle to S3 is handled by `.github/workflows/deploy-*.yml` + `s3-sync.sh`. Publishing to a registry is not wired up yet; `npm run build` runs automatically via `prepublishOnly`.
102
+
103
+ ## Known Issues / Pending Fixes
104
+
105
+ These items are tracked here so the team has a single source of truth for outstanding tech debt. Update this list as items are resolved or new issues are discovered.
106
+
107
+ ### Cross-repo: Lambda `createAppointment` (`tvs-cloud-services/services/rest/src/createAppointment.ts`)
108
+
109
+ 1. **`loanCar` contract mismatch (worked around in this repo).** The Lambda types `loanCar` as `string` (scalar) and forwards it directly to Frappe's scalar fields `loan_car` and `custom_loan_car`. This webcomponent originally sent `loanCar` as `string[]` (multiselect), which caused Frappe to throw `frappe.exceptions.ValidationError: Waar voor Loan car kan geen lijst worden` (HTTP 417).
110
+ - **Current workaround**: `buildSubmitPayload` in `src/stores/appointmentForm.ts` serializes the array to a CSV string before fetch.
111
+ - **Proper fix**: keep `loanCar: string[]` end-to-end and let the Lambda do the join. The transformation belongs in the anti-corruption layer (Lambda), not the UI.
112
+
113
+ 2. **Bug at `createAppointment.ts:152` (latent).** In the PUT branch (when a project already exists), the field assignment is `custom_is_loan_car: data.loanCar` — it should be `data.needsLoanCar` (boolean). Today it never fires because the flow always goes through POST, but it will break the moment a customer updates an existing appointment.
114
+
115
+ ### Internal: this repo
116
+
117
+ 3. **Duplicate `LOAN_CAR_LABELS` mapping.** The map `golf6 → "GOLF 6 DSG"`, `up → "VW UP!"`, `caddy → "VW CADDY"` lives in **two** places:
118
+ - `src/stores/appointmentForm.ts` — used by `buildSubmitPayload` for the wire payload.
119
+ - `src/views/steps/ConfirmView.vue` — used by `formatLoanCars` for the confirmation summary shown to the customer.
120
+
121
+ If a label changes in only one place, the customer will see one value on Confirm but a different one will land in ERPNext. Candidate to dedupe into a shared util / constants module.
@@ -0,0 +1,78 @@
1
+ export interface AppointmentFormProps {
2
+ /** UI language, e.g. "nl" or "en". Defaults to "nl". */
3
+ lang?: string;
4
+ /** Base URL of the appointment API (required). */
5
+ awsUrl: string;
6
+ /** Holiday closure switch. Defaults to false. */
7
+ vacationMode?: boolean;
8
+ /** First closed day (ISO `YYYY-MM-DD`). */
9
+ vacationStart?: string;
10
+ /** Last closed day (ISO `YYYY-MM-DD`). */
11
+ vacationEnd?: string;
12
+ /** First day back at work (ISO `YYYY-MM-DD`). */
13
+ vacationReopen?: string;
14
+ }
15
+ /**
16
+ * Vue wrapper around the <tvs-appointment-form> custom element with typed,
17
+ * camelCase props. Rendered via a render function so consumers don't need
18
+ * `isCustomElement` compiler configuration.
19
+ */
20
+ export declare const AppointmentForm: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
21
+ lang: {
22
+ type: StringConstructor;
23
+ default: string;
24
+ };
25
+ awsUrl: {
26
+ type: StringConstructor;
27
+ required: true;
28
+ };
29
+ vacationMode: {
30
+ type: BooleanConstructor;
31
+ default: boolean;
32
+ };
33
+ vacationStart: {
34
+ type: StringConstructor;
35
+ default: string;
36
+ };
37
+ vacationEnd: {
38
+ type: StringConstructor;
39
+ default: string;
40
+ };
41
+ vacationReopen: {
42
+ type: StringConstructor;
43
+ default: string;
44
+ };
45
+ }>, () => import('vue').VNode<import('vue').RendererNode, import('vue').RendererElement, {
46
+ [key: string]: any;
47
+ }>, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
48
+ lang: {
49
+ type: StringConstructor;
50
+ default: string;
51
+ };
52
+ awsUrl: {
53
+ type: StringConstructor;
54
+ required: true;
55
+ };
56
+ vacationMode: {
57
+ type: BooleanConstructor;
58
+ default: boolean;
59
+ };
60
+ vacationStart: {
61
+ type: StringConstructor;
62
+ default: string;
63
+ };
64
+ vacationEnd: {
65
+ type: StringConstructor;
66
+ default: string;
67
+ };
68
+ vacationReopen: {
69
+ type: StringConstructor;
70
+ default: string;
71
+ };
72
+ }>> & Readonly<{}>, {
73
+ lang: string;
74
+ vacationMode: boolean;
75
+ vacationStart: string;
76
+ vacationEnd: string;
77
+ vacationReopen: string;
78
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;