@seatsyoo/vue 0.0.0-stage → 1.1.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/LICENSE +21 -0
- package/README.md +365 -2
- package/dist/SeatsyooChart.d.ts +153 -0
- package/dist/SeatsyooChart.js +226 -0
- package/dist/SeatsyooDesigner.d.ts +71 -0
- package/dist/SeatsyooDesigner.js +138 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/opciones.d.ts +99 -0
- package/dist/opciones.js +210 -0
- package/package.json +38 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tikyoo, S.L.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,366 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @seatsyoo/vue
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
El visor de mapas de asientos de **Seatsyoo** como componente de Vue 3: el comprador ve el plano del recinto,
|
|
4
|
+
elige sus butacas (quedan retenidas a su nombre mientras paga) y ve en tiempo real lo que otros compran. Usa el
|
|
5
|
+
visor de [`@seatsyoo/visor`](https://www.npmjs.com/package/@seatsyoo/visor) y trae sus tipos de TypeScript.
|
|
6
|
+
|
|
7
|
+
Funciona con **Vue 3.3 o posterior** y con renderizado en el servidor (Nuxt, Vite SSR…). No necesita compilar
|
|
8
|
+
plantillas: está escrito con `defineComponent` y una función `render`.
|
|
9
|
+
|
|
10
|
+
*(English summary below.)*
|
|
11
|
+
|
|
12
|
+
## Instalación
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @seatsyoo/vue
|
|
16
|
+
# o: pnpm add @seatsyoo/vue · yarn add @seatsyoo/vue
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`vue` lo pones tú (es una dependencia «peer»); `@seatsyoo/visor` se instala solo.
|
|
20
|
+
|
|
21
|
+
## Uso
|
|
22
|
+
|
|
23
|
+
```vue
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import { SeatsyooChart, type SelectedObject } from "@seatsyoo/vue";
|
|
26
|
+
import { ref } from "vue";
|
|
27
|
+
|
|
28
|
+
const plano = ref<InstanceType<typeof SeatsyooChart> | null>(null);
|
|
29
|
+
const seleccion = ref<SelectedObject[]>([]);
|
|
30
|
+
|
|
31
|
+
async function comprar() {
|
|
32
|
+
// A tu servidor, solo el evento y el token de retención, nunca la lista de lo elegido: pregunta a Seatsyoo qué
|
|
33
|
+
// retiene ese token, cobra eso y, con el pago confirmado, lo reserva (ver «Vender»).
|
|
34
|
+
await fetch("/caja", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "content-type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ evento: "hamlet-2026-11-20", holdToken: plano.value?.chart?.holdToken }),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tiempoAgotado() {
|
|
42
|
+
alert("Se ha acabado el tiempo: vuelve a elegir.");
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<!-- El visor ocupa todo el <div>: dale altura. -->
|
|
48
|
+
<SeatsyooChart
|
|
49
|
+
ref="plano"
|
|
50
|
+
server="https://seatsyoo.example"
|
|
51
|
+
public-key="pk_tu_clave_publica"
|
|
52
|
+
event="hamlet-2026-11-20"
|
|
53
|
+
:pricing="{ platea: 30, anfiteatro: 22 }"
|
|
54
|
+
style="height: 600px"
|
|
55
|
+
@change="(elegidas: SelectedObject[]) => (seleccion = elegidas)"
|
|
56
|
+
@hold-expired="tiempoAgotado"
|
|
57
|
+
/>
|
|
58
|
+
<button :disabled="seleccion.length === 0" @click="comprar">Comprar</button>
|
|
59
|
+
</template>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Para tenerlo en todas las plantillas sin importarlo, registra el plugin:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { SeatsyooPlugin } from "@seatsyoo/vue";
|
|
66
|
+
|
|
67
|
+
createApp(App).use(SeatsyooPlugin).mount("#app");
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Para probarlo en un proyecto nuevo de Vite:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
npm create vite@latest entradas -- --template vue-ts
|
|
74
|
+
cd entradas
|
|
75
|
+
npm install @seatsyoo/vue
|
|
76
|
+
# pon el ejemplo de arriba en src/App.vue y arranca con:
|
|
77
|
+
npm run dev
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Props
|
|
81
|
+
|
|
82
|
+
Todas las opciones del visor que no son avisos (`server`, `public-key`, `event`, `season`, `link`, `document`,
|
|
83
|
+
`language`, `theme`, `max-selected`, `pricing`, `channels`, `hold-token`, `mode`, `status-styles`, `marks`, `view3d`…), con los mismos
|
|
84
|
+
nombres y tipos que en `@seatsyoo/visor` (en las plantillas, en camelCase o en kebab-case), salvo `container`, que
|
|
85
|
+
es el propio componente. `class`, `style`, `id` y el resto de atributos pasan al `<div>` que lo contiene. Los números
|
|
86
|
+
y booleanos, con `:` (`:max-selected="6"`, `:view3d="false"`).
|
|
87
|
+
|
|
88
|
+
Cómo reacciona a los cambios de props:
|
|
89
|
+
|
|
90
|
+
| Qué cambia | Qué pasa |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `server`, `publicKey`, `event`, `link`, `document`, `season`, `seasonEvents`, `channels`, `pricing`, `currency`, `locale`, `language`, `maxSelected`, `session`, `holdToken`, `mode`, `showLegend`, `showSelectionBar`, `floor`, `sectors`, `view3d`, `entrance`, `texts` | Se desmonta el visor y se monta otro con lo nuevo. Los arrays y objetos se comparan **por su valor**: `:channels="['web']"` no lo vuelve a montar en cada render, y cambiar un array reactivo en el sitio (`canales.push("taquilla")`) sí. |
|
|
93
|
+
| `theme`, `colorBy` | Se aplica al visor montado (`setTheme`, `setColorBy`), sin volver a montarlo. |
|
|
94
|
+
| `statusStyles`, `marks` | Solo con `mode="manager"` (un panel de gestión): cómo se ve cada estado y las marcas sobre plazas libres (ver «Estados propios y marcas»). Se aplican al visor montado (`setStatusStyles`, `setMarks`), sin volver a montarlo, solo si cambia su valor: se comparan como datos (da igual el prototipo, y una clave que vale `undefined` es como si no estuviera), así que un objeto nuevo con lo mismo no hace nada, y cambiar uno reactivo en el sitio (`marcas["Platea-A-1"] = "reducida"`) sí cuenta. Quitarlas es dejar el visor sin estilos ni marcas. |
|
|
95
|
+
| `load3d` | Se usa la última función; ponerlo o quitarlo vuelve a montar. |
|
|
96
|
+
|
|
97
|
+
El plano (`document`) no se copia, porque puede ser muy grande: un plano nuevo con lo mismo no vuelve a montar el
|
|
98
|
+
visor, pero si cambias el que ya pasaste (en el sitio), no se nota, ni aunque después pases una copia. Pasa un plano
|
|
99
|
+
nuevo sin tocar el anterior.
|
|
100
|
+
|
|
101
|
+
**Lo que el comprador tiene retenido al volver a montar.** Si es en el mismo evento y con `session: "continue"` (por
|
|
102
|
+
defecto), el visor nuevo lo recupera. Si el nuevo no puede recuperarlo (has cambiado de evento o de temporada, de
|
|
103
|
+
`server` o de `publicKey`, usas `session: "start"`, le pasas un `holdToken` o el modo `"manager"`), el componente lo
|
|
104
|
+
suelta antes, como `clearSelection()`: se emiten `deselect` y `change` con la selección vacía, y esas butacas quedan
|
|
105
|
+
libres para los demás en lugar de bloqueadas hasta que caduque la retención. Así, para cambiar de función basta con
|
|
106
|
+
cambiar `event`.
|
|
107
|
+
|
|
108
|
+
Lo retenido con un `holdToken` tuyo (creado por tu servidor) no se suelta nunca: es de tu token, y tu servidor lo
|
|
109
|
+
reserva o lo suelta. Es la forma de llevar un carrito con varias funciones. Al desmontar el componente (por ejemplo,
|
|
110
|
+
al pasar a la página de pago) tampoco se suelta nada: tu servidor lo reserva con el token, o el comprador lo
|
|
111
|
+
recupera al volver.
|
|
112
|
+
|
|
113
|
+
### Vender
|
|
114
|
+
|
|
115
|
+
A tu servidor la página le manda el evento y el `holdToken`, nada más: nunca la lista de lo elegido. En la caja, tu
|
|
116
|
+
servidor pregunta a Seatsyoo qué retiene ese token (`GET /api/v1/events/{evento}/statuses?holdToken=…`: lo suyo lleva
|
|
117
|
+
`mine: true` y, en una zona de pie, `mq` con las plazas del token; `mq` solo viene si en la zona hay más de una plaza
|
|
118
|
+
ocupada, contando las de otros compradores, y si no viene es 1), calcula el importe con sus precios (precio × plazas:
|
|
119
|
+
una mesa entera cuenta todas sus sillas, como el total del visor) y guarda esa lista con el pedido, cada objeto con su
|
|
120
|
+
cantidad (`{ key, quantity: mq ?? 1 }`). Con el pago confirmado, reserva exactamente lo guardado
|
|
121
|
+
(`POST /api/v1/events/{evento}/actions/book` con `holdToken` y tu número de pedido, `orderId`), sin volver a preguntar.
|
|
122
|
+
|
|
123
|
+
No reserves una lista que mande la página: `book` reserva también objetos libres, así que cualquiera podría añadirle
|
|
124
|
+
butacas que no ha retenido, y saltarse los topes de retención. Es lo que hace la tienda de ejemplo de `@seatsyoo/node`
|
|
125
|
+
(`ejemplo/tienda.ts`: `POST /caja` con `{ evento, holdToken }` y `POST /pagar` con `{ holdToken }`) y lo que cuenta la
|
|
126
|
+
guía de integración de la documentación (**/docs/integracion**).
|
|
127
|
+
|
|
128
|
+
### Precios
|
|
129
|
+
|
|
130
|
+
Seatsyoo no guarda precios: los pone tu web. Pásalos con `pricing`, por clave de categoría (`{ platea: 30 }`), con
|
|
131
|
+
`currency` y `locale` para escribirlos: el visor los enseña en la ficha de cada butaca, en la leyenda y en el total,
|
|
132
|
+
y salen en `price` de cada objeto de `change`, `select`… (sin `pricing`, `price` es `null`). `price` es lo que cuesta
|
|
133
|
+
cada unidad de `quantity` (por plaza; una mesa entera, con todas sus sillas), así que `price × quantity` es lo que suma
|
|
134
|
+
el total del visor. Cambiar `pricing` vuelve a montar el visor (en el mismo evento, el comprador no pierde lo elegido).
|
|
135
|
+
Lo que se cobra lo calcula tu servidor con sus precios, no con lo que diga la página.
|
|
136
|
+
|
|
137
|
+
### Eventos
|
|
138
|
+
|
|
139
|
+
| Evento | Con qué | Cuándo |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| `ready` | el visor (`SeatingChart`) | Ha cargado el plano y los estados. |
|
|
142
|
+
| `change` | la selección (`SelectedObject[]`) | La selección ha cambiado. |
|
|
143
|
+
| `select`, `deselect` | el objeto (`SelectedObject`) | Se ha elegido o soltado un objeto. |
|
|
144
|
+
| `hold-expired` | — | Se acabó el tiempo de la retención. |
|
|
145
|
+
| `hold-token-change` | `{ token, expiresAt }` o `null` | El token de retención ha cambiado. |
|
|
146
|
+
| `floor-change` | `{ id, name }` | Se muestra otra planta. |
|
|
147
|
+
| `error` | `{ code, message }` | Algo ha fallado, o se ha llegado a un tope de retención (ver «Topes de retención»). Sin oyente de `@error`, se escribe en la consola. |
|
|
148
|
+
|
|
149
|
+
Los eventos llegan siempre a los oyentes de ese momento: cambiarlos no vuelve a montar el visor. En TypeScript (y
|
|
150
|
+
en las funciones `render`), los oyentes son `onReady`, `onChange`, `onHoldExpired`, `onHoldTokenChange`…
|
|
151
|
+
|
|
152
|
+
### Lo que expone
|
|
153
|
+
|
|
154
|
+
Con una ref de plantilla, `chart` es el visor montado (un `SeatingChart`): `getSelection()`, `holdToken`,
|
|
155
|
+
`holdExpiresAt`, `clearSelection()`, `selectBestAvailable(n)`, `setFloor(id)`, `show3d()`, `zoomToFit()`,
|
|
156
|
+
`refresh()`… Vale `null` mientras no hay visor: en el servidor, antes de montarlo y después de desmontarlo. Si
|
|
157
|
+
cambia una opción que lo vuelve a montar, `chart` pasa a ser el visor nuevo.
|
|
158
|
+
|
|
159
|
+
### Errores
|
|
160
|
+
|
|
161
|
+
Lo que falla al cargar o al retener llega a `@error` como `{ code, message }`. Si faltan opciones para montarlo
|
|
162
|
+
(qué enseñar, la clave pública…), el componente no lanza: emite `error` con `code: "invalid-options"` (sin oyente,
|
|
163
|
+
lo escribe en la consola) y lo monta en cuanto las opciones valen.
|
|
164
|
+
|
|
165
|
+
### Topes de retención
|
|
166
|
+
|
|
167
|
+
Lo que el comprador retiene desde el visor tiene topes en el servidor. Al pasar uno, la butaca no se retiene y llega
|
|
168
|
+
el error a `@error`:
|
|
169
|
+
|
|
170
|
+
| Código | Qué tope |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `hold-limit` (409) | Las «Plazas por comprador» de tu espacio (`maxHoldPerToken`, 10 si no se cambia; lo da `GET /workspace`), por token y evento. Una mesa o un palco que se venden enteros cuentan todas sus plazas. |
|
|
173
|
+
| `hold-limit-ip` (409) | Lo retenido a la vez desde una misma conexión (IP) en un evento, sumando todas sus compras en curso: el 10 % del aforo, hasta 200, y nunca menos que `maxHoldPerToken`. |
|
|
174
|
+
| `hold-rate-limited` (429) | Lo que retiene una misma IP en poco tiempo: como mucho 600 plazas cada 10 minutos (soltar no cuenta). |
|
|
175
|
+
|
|
176
|
+
- `max-selected` cuenta una mesa o un palco enteros como 1, y el servidor, con todas sus plazas: con
|
|
177
|
+
`max-selected` y `maxHoldPerToken` en 10, dos mesas de 8 dan `hold-limit`. No pongas `max-selected` por encima de
|
|
178
|
+
`maxHoldPerToken` y, si vendes mesas o palcos enteros, cuenta con ese error o sube el ajuste.
|
|
179
|
+
- `selectBestAvailable(n)` pide de 1 a 100 plazas, y nunca más que `maxHoldPerToken` (si no, `hold-limit` antes de
|
|
180
|
+
buscar).
|
|
181
|
+
- Un `holdToken` que crea tu servidor con la clave secreta (`POST /api/v1/hold-tokens`) no lleva IP: no cuenta para
|
|
182
|
+
`hold-limit-ip`. Lo que el comprador retiene con él desde el visor sí cuenta para `hold-limit` y `hold-rate-limited`.
|
|
183
|
+
- Lo que hace tu servidor con la clave secreta (retener, reservar) no tiene ninguno de estos topes.
|
|
184
|
+
|
|
185
|
+
### Estados propios y marcas (modo «manager»)
|
|
186
|
+
|
|
187
|
+
En un panel de gestión (modo `"manager"`), `statusStyles` dice cómo se ve cada estado y `marks`, qué plazas libres
|
|
188
|
+
llevan una marca. Se sacan de la API con la clave secreta, así que los pide tu servidor y se los da a la página:
|
|
189
|
+
|
|
190
|
+
- `marks` es tal cual el `marks` de `GET /api/v1/events/{evento}/marks` (`{ "marks": { "Platea-A-1":
|
|
191
|
+
"visibilidad-reducida" } }`): objeto → clave del estado.
|
|
192
|
+
- `statusStyles` sale del catálogo de `GET /api/v1/statuses` (`{ statuses, system }`; con `?event={evento}`, solo los
|
|
193
|
+
que valen para ese evento), pero no es el mismo formato: por la clave de cada estado, `{ name, color, icon }`, y el
|
|
194
|
+
icono cambia de forma. `{ type: "text", text }` pasa a `{ text }` y `{ type: "image", src }`, a `{ image: src }`;
|
|
195
|
+
`{ type: "preset", name }` es de la colección del panel, que no se publica: sin icono, o con un trazado tuyo de 24×24
|
|
196
|
+
(`{ path }`). De `system` (los estados de base que tu espacio ha cambiado), solo los que traen `color`, y nunca
|
|
197
|
+
`free`. Un estado de base con `color: null` no pasa, así que el visor lo pinta como siempre y también pierde el
|
|
198
|
+
nombre y el icono propios que le hayas puesto, aunque el panel sí los enseñe (en el modo «manager», «Reservado» en
|
|
199
|
+
vez de tu «Vendida»). Si los quieres, ponle color propio en el panel. Pasar el catálogo tal cual no compila en
|
|
200
|
+
TypeScript y, en JavaScript, rompe el pintado.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import type { StatusStyle } from "@seatsyoo/vue";
|
|
204
|
+
|
|
205
|
+
/** Un icono del catálogo de estados (GET /api/v1/statuses). */
|
|
206
|
+
type IconoDeEstado = { type: "preset"; name: string } | { type: "text"; text: string } | { type: "image"; src: string };
|
|
207
|
+
/** Lo que responde GET /api/v1/statuses (lo que hace falta aquí). */
|
|
208
|
+
interface Catalogo {
|
|
209
|
+
statuses: { key: string; name: string; color: string; icon: IconoDeEstado | null }[];
|
|
210
|
+
system: { key: string; name: string | null; color: string | null; icon: IconoDeEstado | null }[];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function estilo(name: string | null, color: string, icon: IconoDeEstado | null): StatusStyle {
|
|
214
|
+
// «preset»: sin icono (o tu trazado de 24×24, { path: "M4 12h16" }).
|
|
215
|
+
const forma = icon?.type === "text" ? { text: icon.text } : icon?.type === "image" ? { image: icon.src } : null;
|
|
216
|
+
return { ...(name ? { name } : {}), color, ...(forma ? { icon: forma } : {}) };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** De GET /api/v1/statuses a la opción statusStyles. */
|
|
220
|
+
export function estilosDeEstados({ statuses, system }: Catalogo): Record<string, StatusStyle> {
|
|
221
|
+
const estilos: Record<string, StatusStyle> = {};
|
|
222
|
+
for (const s of statuses) estilos[s.key] = estilo(s.name, s.color, s.icon);
|
|
223
|
+
// De los de base, solo los que tienen color propio: sin él se quedan como los pinta el visor, también con su nombre
|
|
224
|
+
// y su icono de siempre (los tuyos se pierden). «free» no se pinta con estilo.
|
|
225
|
+
for (const s of system) if (s.key !== "free" && s.color) estilos[s.key] = estilo(s.name, s.color, s.icon);
|
|
226
|
+
return estilos;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Después, `<SeatsyooChart mode="manager" … :status-styles="estilos" :marks="marcas" />`, con `estilos =
|
|
231
|
+
estilosDeEstados(catalogo)` y las marcas tal cual (también reactivas, cambiadas en el sitio).
|
|
232
|
+
|
|
233
|
+
### Renderizado en el servidor
|
|
234
|
+
|
|
235
|
+
El visor se monta en `onMounted`: en el servidor el componente pinta solo un `<div>` vacío y en el navegador se
|
|
236
|
+
hidrata y monta el visor. En Nuxt funciona tal cual (también dentro de `<ClientOnly>`, si lo prefieres).
|
|
237
|
+
|
|
238
|
+
### La vista 3D
|
|
239
|
+
|
|
240
|
+
No va en el paquete: el visor la descarga de `${server}/sdk/butacas-3d.js` cuando alguien la abre. Si tu web
|
|
241
|
+
tiene una política de seguridad de contenidos (CSP), añade esa dirección a `script-src`, o pasa `:view3d="false"`.
|
|
242
|
+
|
|
243
|
+
## El diseñador de planos
|
|
244
|
+
|
|
245
|
+
`<SeatsyooDesigner>` abre en tu web el diseñador de planos de Seatsyoo, para que tus clientes dibujen sus recintos.
|
|
246
|
+
Tu servidor abre una sesión para un plano con su clave secreta (`POST /api/v1/charts/{plano}/designer-sessions`, o
|
|
247
|
+
`charts.designerSession` de `@seatsyoo/node`) y le da el token a tu web:
|
|
248
|
+
|
|
249
|
+
```vue
|
|
250
|
+
<script setup lang="ts">
|
|
251
|
+
import { SeatsyooDesigner } from "@seatsyoo/vue";
|
|
252
|
+
|
|
253
|
+
defineProps<{ token: string }>();
|
|
254
|
+
const emit = defineEmits<{ cerrar: [] }>();
|
|
255
|
+
// Tu servidor abre otra sesión del mismo plano y te da el token (aquí, una ruta tuya).
|
|
256
|
+
const pedirToken = (): Promise<string> =>
|
|
257
|
+
fetch("/mi-api/disenador/teatro", { method: "POST" }).then((r) => r.json()).then((s: { token: string }) => s.token);
|
|
258
|
+
</script>
|
|
259
|
+
|
|
260
|
+
<template>
|
|
261
|
+
<SeatsyooDesigner
|
|
262
|
+
server="https://seatsyoo.example"
|
|
263
|
+
:token="token"
|
|
264
|
+
:refresh-token="pedirToken"
|
|
265
|
+
style="height: 80vh"
|
|
266
|
+
@publish="({ version }) => console.log('Publicada la versión', version)"
|
|
267
|
+
@close="emit('cerrar')"
|
|
268
|
+
/>
|
|
269
|
+
</template>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Props: `server`, `token`, `language` y `refreshToken` (pide a tu servidor otro token del mismo plano antes de que
|
|
273
|
+
caduque: sin ella, al caducar deja de guardar). Eventos: `ready`, `save`, `publish`, `close` («←» sale si hay quien
|
|
274
|
+
escuche `@close` al montarse) y `error`. `class`, `style` e `id` van al `<div>`, que necesita alto. Un `token` nuevo se le
|
|
275
|
+
manda sin volver a montarlo; cambiar `server` o `language`, o pasar o quitar `refreshToken`, lo vuelve a montar. Con
|
|
276
|
+
una ref de plantilla, `disenador.value.designer` es el diseñador (`setToken()`, `iframe`). `app.use(SeatsyooPlugin)`
|
|
277
|
+
también lo registra. En el servidor (SSR) pinta solo el `<div>`.
|
|
278
|
+
|
|
279
|
+
La referencia completa de opciones, avisos y métodos está en la documentación de Seatsyoo (**/docs/visor**, **/docs/disenador** y
|
|
280
|
+
**/docs/frameworks**).
|
|
281
|
+
|
|
282
|
+
## Licencia
|
|
283
|
+
|
|
284
|
+
MIT (archivo `LICENSE`). El visor del que depende, `@seatsyoo/visor`, tiene su propia licencia: se usa gratis para
|
|
285
|
+
integrar Seatsyoo.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## English summary
|
|
290
|
+
|
|
291
|
+
`@seatsyoo/vue` is the **Seatsyoo** seating chart viewer as a Vue 3 component (Vue 3.3+, SSR-safe, no template
|
|
292
|
+
compiler needed). It wraps `@seatsyoo/visor` and ships TypeScript types.
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
npm install @seatsyoo/vue
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```vue
|
|
299
|
+
<SeatsyooChart
|
|
300
|
+
ref="chart"
|
|
301
|
+
server="https://seatsyoo.example"
|
|
302
|
+
public-key="pk_your_public_key"
|
|
303
|
+
event="hamlet-2026-11-20"
|
|
304
|
+
language="en"
|
|
305
|
+
style="height: 600px"
|
|
306
|
+
@change="(selection: SelectedObject[]) => (selected = selection)"
|
|
307
|
+
/>
|
|
308
|
+
<!-- chart.value.chart is the mounted SeatingChart: at checkout, send only the event and its holdToken to your server
|
|
309
|
+
(never the selected keys). -->
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
- **Embedded designer**: `<SeatsyooDesigner server :token :refresh-token @publish @close style="height: 80vh" />` opens
|
|
313
|
+
the Seatsyoo chart designer in your website, with a session token your server opens with its secret key
|
|
314
|
+
(`POST /api/v1/charts/{chart}/designer-sessions`).
|
|
315
|
+
- **Props**: every viewer option except `container` and the callbacks; `class`, `style`, `id` and other attributes
|
|
316
|
+
go to the wrapping `<div>`. `app.use(SeatsyooPlugin)` registers `<SeatsyooChart>` globally.
|
|
317
|
+
- **Events**: `ready`, `change`, `select`, `deselect`, `error`, `hold-expired`, `hold-token-change`,
|
|
318
|
+
`floor-change`. They always reach the current listeners; without an `@error` listener, errors go to the console.
|
|
319
|
+
- **Re-mounting**: changing `server`, `publicKey`, `event`, `link`, `document`, `season`, `seasonEvents`,
|
|
320
|
+
`channels`, `pricing`, `currency`, `locale`, `language`, `maxSelected`, `session`, `holdToken`, `mode`,
|
|
321
|
+
`showLegend`, `showSelectionBar`, `floor`, `sectors`, `view3d`, `entrance` or `texts` destroys the viewer and mounts a new one (arrays
|
|
322
|
+
and objects are compared by value). `theme` and `colorBy` are applied live (`setTheme`, `setColorBy`), and so
|
|
323
|
+
are `statusStyles` and `marks` (`"manager"` mode: `setStatusStyles`, `setMarks`), only when their value changes
|
|
324
|
+
(compared as data: the prototype doesn't matter and a key set to `undefined` counts as missing; a reactive object
|
|
325
|
+
changed in place counts too).
|
|
326
|
+
- **Prices**: Seatsyoo does not store prices. Pass yours with `pricing` (per category key, with `currency` and
|
|
327
|
+
`locale`): the viewer shows them and they come in `price` of each selected object (`null` without `pricing`).
|
|
328
|
+
`price` is what each unit of `quantity` costs (per place; a whole table, all its chairs), so `price × quantity` adds
|
|
329
|
+
up to the viewer's total.
|
|
330
|
+
- **Selling**: the page sends your server the event and the `holdToken`, nothing else. At checkout your server asks
|
|
331
|
+
Seatsyoo what that token holds (`GET /api/v1/events/{event}/statuses?holdToken=…`: `mine: true`, plus `mq` in a
|
|
332
|
+
standing area with the token's places, which only comes when more than one place is taken there, counting other
|
|
333
|
+
buyers'; when it is missing, it is 1), prices it (price × places: a whole table counts all its chairs, as in the
|
|
334
|
+
viewer's total) and stores that list with the order, each object with an explicit quantity
|
|
335
|
+
(`{ key, quantity: mq ?? 1 }`). Once paid, it books exactly that list (`POST …/actions/book` with
|
|
336
|
+
`holdToken` and your `orderId`). Never book a list sent by the page: `book` also books free objects, so anyone could
|
|
337
|
+
add seats they never held. The `@seatsyoo/node` example store (`ejemplo/tienda.ts`) does this; see
|
|
338
|
+
**/en/docs/integration**.
|
|
339
|
+
- **Hold limits** (errors in the `error` event): `hold-limit` (409, the workspace's "places per buyer",
|
|
340
|
+
`maxHoldPerToken`, 10 by default, per token and event; a whole table or box counts all its places), `hold-limit-ip` (409, per connection
|
|
341
|
+
and event: 10% of the capacity, up to 200, never below `maxHoldPerToken`) and `hold-rate-limited` (429, 600 places
|
|
342
|
+
held per IP every 10 minutes). `max-selected` counts a whole table or box as 1, so keep it at or below
|
|
343
|
+
`maxHoldPerToken` and expect `hold-limit` with whole tables. `selectBestAvailable(n)` takes 1 to 100 and never more
|
|
344
|
+
than `maxHoldPerToken`. A `holdToken` your server creates with the secret key has no IP, so it does not count for
|
|
345
|
+
`hold-limit-ip`, but what the buyer holds with it from the viewer still counts for `hold-limit` and
|
|
346
|
+
`hold-rate-limited`. Your server's own calls with the secret key have none of these limits.
|
|
347
|
+
- **Custom statuses and marks** (`"manager"` mode): `marks` is the `marks` of `GET /api/v1/events/{event}/marks` as
|
|
348
|
+
is. `statusStyles` is not the shape of `GET /api/v1/statuses`: per status key, `{ name, color, icon }` with the icon
|
|
349
|
+
converted (`text` → `{ text }`, `image` → `{ image: src }`, `preset` → no icon or your own 24×24 `{ path }`); from
|
|
350
|
+
`system`, only entries with a `color`, never `free` (one with `color: null` stays exactly as the viewer draws it:
|
|
351
|
+
its own name and icon are lost too, even if the panel shows them; give it its own colour if you want them). Both
|
|
352
|
+
need the secret key: your server fetches them. The `estilosDeEstados` function above does the conversion.
|
|
353
|
+
- **Holds on re-mount**: same event with `session: "continue"` (default) → the new viewer recovers the buyer's
|
|
354
|
+
held seats. If it can't (another event or season, `server`, `publicKey`, `session: "start"`, a `holdToken`,
|
|
355
|
+
`"manager"` mode), the component releases them first, like `clearSelection()` (`deselect` and an empty `change`
|
|
356
|
+
are emitted): switching shows is just changing `event`. Seats held with your own `holdToken` are never released
|
|
357
|
+
(your server books or releases them, e.g. a multi-show cart), and nothing is released on unmount.
|
|
358
|
+
- **`document`** is not copied: changing it in place goes unnoticed; pass a new object without mutating the old one.
|
|
359
|
+
- **Exposed**: `chart`, the mounted `SeatingChart` (or `null` on the server, before mounting and after unmounting).
|
|
360
|
+
- **Invalid options** don't throw: the `error` event gets `{ code: "invalid-options", message }`.
|
|
361
|
+
- **SSR**: renders an empty `<div>` on the server and mounts the viewer in `onMounted`.
|
|
362
|
+
- **3D view**: downloaded from `${server}/sdk/butacas-3d.js` when opened (allow it in your CSP `script-src`, or
|
|
363
|
+
pass `:view3d="false"`).
|
|
364
|
+
- Full reference: the Seatsyoo docs at **/en/docs/seat-map** and **/en/docs/frameworks**.
|
|
365
|
+
|
|
366
|
+
License: MIT. The viewer it depends on, `@seatsyoo/visor`, has its own license (free to use to integrate Seatsyoo).
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { type ChartDocument, type Language, type SeatingChart, type SelectedObject, type StatusStyle, type View3DModule } from "@seatsyoo/visor";
|
|
2
|
+
import { type ComponentOptionsMixin, type DefineComponent, type ExtractPublicPropTypes, type Plugin, type PropType } from "vue";
|
|
3
|
+
import { SeatsyooDesigner } from "./SeatsyooDesigner.js";
|
|
4
|
+
declare const props: {
|
|
5
|
+
/** URL base del servidor de Seatsyoo, p. ej. https://seatsyoo.example */
|
|
6
|
+
server: {
|
|
7
|
+
type: StringConstructor;
|
|
8
|
+
required: true;
|
|
9
|
+
};
|
|
10
|
+
/** Clave pública del espacio (obligatoria salvo con `link` o `document`). */
|
|
11
|
+
publicKey: StringConstructor;
|
|
12
|
+
/** Clave del evento. */
|
|
13
|
+
event: StringConstructor;
|
|
14
|
+
/** O la clave de un enlace público del plano: solo para ver. */
|
|
15
|
+
link: StringConstructor;
|
|
16
|
+
/** O el plano ya cargado: solo para ver. */
|
|
17
|
+
document: PropType<ChartDocument>;
|
|
18
|
+
/** O la clave de una temporada (abono). */
|
|
19
|
+
season: StringConstructor;
|
|
20
|
+
/** Abono parcial: los eventos de la temporada elegidos. */
|
|
21
|
+
seasonEvents: PropType<string[]>;
|
|
22
|
+
/** Canales de venta desde los que se compra. */
|
|
23
|
+
channels: PropType<string[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Los precios que pone tu web, por clave de categoría (`{ platea: 30 }`): el visor los enseña en la ficha, la leyenda
|
|
26
|
+
* y el total, y salen en `price` de lo elegido. Seatsyoo no guarda precios.
|
|
27
|
+
*/
|
|
28
|
+
pricing: PropType<Record<string, number>>;
|
|
29
|
+
/** Moneda de `pricing` (código ISO; si no, euros). */
|
|
30
|
+
currency: StringConstructor;
|
|
31
|
+
locale: StringConstructor;
|
|
32
|
+
language: PropType<Language>;
|
|
33
|
+
/** Máximo de plazas seleccionables (por defecto 10). */
|
|
34
|
+
maxSelected: NumberConstructor;
|
|
35
|
+
/** «continue» (por defecto) recupera la selección al recargar; «start» empieza de cero. */
|
|
36
|
+
session: PropType<"continue" | "start">;
|
|
37
|
+
/** Token de retención creado por tu servidor. */
|
|
38
|
+
holdToken: StringConstructor;
|
|
39
|
+
/** «buyer» (por defecto) o «manager». */
|
|
40
|
+
mode: PropType<"buyer" | "manager">;
|
|
41
|
+
/** Color base de los objetos (se aplica sin volver a montar). */
|
|
42
|
+
colorBy: PropType<"category" | "channel">;
|
|
43
|
+
/** Tema (se aplica sin volver a montar). */
|
|
44
|
+
theme: PropType<"light" | "dark" | "auto">;
|
|
45
|
+
/**
|
|
46
|
+
* Solo en el modo «manager»: nombre, color e icono de los estados, por su clave (los propios y los de base que se
|
|
47
|
+
* quieran cambiar). Se aplica sin volver a montar (setStatusStyles).
|
|
48
|
+
*/
|
|
49
|
+
statusStyles: PropType<Record<string, StatusStyle>>;
|
|
50
|
+
/**
|
|
51
|
+
* Solo en el modo «manager»: marcas sobre plazas que siguen libres, objeto → estado (su aspecto, en `statusStyles`).
|
|
52
|
+
* Se aplica sin volver a montar (setMarks).
|
|
53
|
+
*/
|
|
54
|
+
marks: PropType<Record<string, string>>;
|
|
55
|
+
showLegend: {
|
|
56
|
+
type: BooleanConstructor;
|
|
57
|
+
default: undefined;
|
|
58
|
+
};
|
|
59
|
+
showSelectionBar: {
|
|
60
|
+
type: BooleanConstructor;
|
|
61
|
+
default: undefined;
|
|
62
|
+
};
|
|
63
|
+
/** Planta que se muestra al abrir (id). */
|
|
64
|
+
floor: StringConstructor;
|
|
65
|
+
sectors: {
|
|
66
|
+
type: BooleanConstructor;
|
|
67
|
+
default: undefined;
|
|
68
|
+
};
|
|
69
|
+
/** Botón de vista 3D (por defecto sí). */
|
|
70
|
+
view3d: {
|
|
71
|
+
type: BooleanConstructor;
|
|
72
|
+
default: undefined;
|
|
73
|
+
};
|
|
74
|
+
/** Entrada animada al abrirse: el plano se descubre desde el escenario (por defecto, no). */
|
|
75
|
+
entrance: {
|
|
76
|
+
type: BooleanConstructor;
|
|
77
|
+
default: undefined;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Textos del plano que se enseñan cambiados (por ejemplo, traducidos), por el texto tal como está en el plano:
|
|
81
|
+
* `{ Platea: "Stalls" }`. Solo cambia lo que se ve: claves, selección y eventos siguen siendo los del plano.
|
|
82
|
+
*/
|
|
83
|
+
texts: PropType<Record<string, string>>;
|
|
84
|
+
/** Cómo cargar el módulo 3D (por defecto se pide al servidor). */
|
|
85
|
+
load3d: PropType<() => Promise<View3DModule>>;
|
|
86
|
+
};
|
|
87
|
+
declare const emits: {
|
|
88
|
+
/** El visor ha cargado el plano y los estados. */
|
|
89
|
+
ready: (chart: SeatingChart) => boolean;
|
|
90
|
+
/** La selección ha cambiado. */
|
|
91
|
+
change: (selection: SelectedObject[]) => boolean;
|
|
92
|
+
select: (object: SelectedObject) => boolean;
|
|
93
|
+
deselect: (object: SelectedObject) => boolean;
|
|
94
|
+
/** Algo ha fallado: `{ code, message }`. Sin oyente, se escribe en la consola. */
|
|
95
|
+
error: (error: {
|
|
96
|
+
code: string;
|
|
97
|
+
message: string;
|
|
98
|
+
}) => boolean;
|
|
99
|
+
/** Se acabó el tiempo de la retención (`@hold-expired`). */
|
|
100
|
+
holdExpired: () => boolean;
|
|
101
|
+
/** El token de retención ha cambiado (`@hold-token-change`). */
|
|
102
|
+
holdTokenChange: (token: {
|
|
103
|
+
token: string;
|
|
104
|
+
expiresAt: Date;
|
|
105
|
+
} | null) => boolean;
|
|
106
|
+
/** Se muestra otra planta (`@floor-change`). */
|
|
107
|
+
floorChange: (floor: {
|
|
108
|
+
id: string;
|
|
109
|
+
name: string;
|
|
110
|
+
}) => boolean;
|
|
111
|
+
};
|
|
112
|
+
/** Props de SeatsyooChart: las opciones del visor, sin `container` ni los avisos (que son eventos). */
|
|
113
|
+
export type SeatsyooChartProps = ExtractPublicPropTypes<typeof props>;
|
|
114
|
+
/** Lo que expone el componente a una ref de plantilla. */
|
|
115
|
+
export interface SeatsyooChartExposed {
|
|
116
|
+
/** El visor montado (`getSelection()`, `holdToken`…), o null mientras no hay ninguno. */
|
|
117
|
+
chart: SeatingChart | null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* El tipo público del componente, escrito a mano con los parámetros de DefineComponent que no han cambiado desde
|
|
121
|
+
* Vue 3.3: el que deduce TypeScript lleva los de la versión con que se compila (ShallowRef<T, S>, más
|
|
122
|
+
* parámetros…), que no existen en las anteriores.
|
|
123
|
+
*/
|
|
124
|
+
export type SeatsyooChartComponent = DefineComponent<typeof props, SeatsyooChartExposed, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, typeof emits>;
|
|
125
|
+
/**
|
|
126
|
+
* El visor de mapas de asientos de Seatsyoo.
|
|
127
|
+
*
|
|
128
|
+
* ```vue
|
|
129
|
+
* <SeatsyooChart
|
|
130
|
+
* ref="plano"
|
|
131
|
+
* server="https://seatsyoo.example"
|
|
132
|
+
* public-key="pk_tu_clave_publica"
|
|
133
|
+
* event="hamlet-2026-11-20"
|
|
134
|
+
* style="height: 600px"
|
|
135
|
+
* @change="(seleccion) => (elegidas = seleccion)"
|
|
136
|
+
* />
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* Con una ref de plantilla, `plano.value.chart` es el visor montado (`getSelection()`, `holdToken`…), o `null`
|
|
140
|
+
* mientras no hay ninguno.
|
|
141
|
+
*/
|
|
142
|
+
export declare const SeatsyooChart: SeatsyooChartComponent;
|
|
143
|
+
/** Plugin que registra <SeatsyooChart> y <SeatsyooDesigner> en toda la aplicación: `app.use(SeatsyooPlugin)`. */
|
|
144
|
+
export declare const SeatsyooPlugin: Plugin;
|
|
145
|
+
declare module "vue" {
|
|
146
|
+
interface GlobalComponents {
|
|
147
|
+
/** El visor de Seatsyoo (registrado con `app.use(SeatsyooPlugin)`). */
|
|
148
|
+
SeatsyooChart: typeof SeatsyooChart;
|
|
149
|
+
/** El diseñador de planos de Seatsyoo (registrado con `app.use(SeatsyooPlugin)`). */
|
|
150
|
+
SeatsyooDesigner: typeof SeatsyooDesigner;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export {};
|