depor_os/docs/api-v2.md

1643 lines
39 KiB
Markdown
Raw Permalink Normal View History

2026-03-18 11:47:06 +00:00
# API V2 — Documentación de endpoints
**Base URL real:** `/2.0`
**Base URL mock:** `/2.0-mock` *(sin autenticación, datos de ejemplo)*
> Las rutas protegidas requieren el header `Authorization: Bearer {token}` obtenido en `/login`.
> Todas las respuestas son `Content-Type: application/json`.
---
## Índice
1. [Auth](#1-auth)
2. [People (abonado)](#2-people-abonado)
3. [Activities (actividades)](#3-activities-actividades)
4. [Booking (reservas — app)](#4-booking-reservas--app)
5. [Payments](#5-payments)
6. [Wallet (monedero)](#6-wallet-monedero)
7. [Search (búsqueda)](#7-search-búsqueda)
8. [Members (socios — admin)](#8-members-socios--admin)
9. [Courses (cursos — admin)](#9-courses-cursos--admin)
10. [Bills (recibos — admin)](#10-bills-recibos--admin)
11. [Booking Admin (reservas — admin)](#11-booking-admin-reservas--admin)
12. [Cash (caja — admin)](#12-cash-caja--admin)
13. [Dayoff / Celebrations](#13-dayoff--celebrations)
14. [Remittance (remesas especiales)](#14-remittance-remesas-especiales)
15. [Inscriptions (preinscripciones)](#15-inscriptions-preinscripciones)
16. [Admin Misc](#16-admin-misc)
17. [Printer](#17-printer)
18. [Hash (público)](#18-hash-público)
---
## 1. Auth
### `POST /login`
Autentica al usuario y devuelve un token Bearer.
**Body (form/JSON)**
| Campo | Tipo | Descripción |
|----------|--------|------------------|
| email | string | Email del abonado |
| password | string | Contraseña |
**Respuesta 200**
```json
{
"access_token": "4Km9xQzLp2VnRtYwEoJbFsUcXgHdAiMkNlOqPrSvTu"
}
```
**Respuesta 401**
```json
{
"message": "Contraseña incorrecta."
}
```
---
### `POST /forgot`
Envía un email de recuperación de contraseña.
**Body**
| Campo | Tipo |
|-------|--------|
| email | string |
**Respuesta 200**
```json
{
"error": "Se le ha enviado un email con instrucciones"
}
```
---
### `GET /logout` 🔒
Cierra la sesión del usuario autenticado.
**Respuesta 200**
```json
{
"logout": "ok"
}
```
---
## 2. People (abonado)
### `GET /people/me` 🔒
Devuelve los datos del abonado autenticado.
**Respuesta 200**
```json
{
"member": "00042",
"family": 1,
"first_name": "María",
"last_name": "García López",
"email": "maria@example.com",
"phone": "612345678",
"dni": "12345678A",
"birth_date": "15/03/1985",
"address": "Calle Mayor 12",
"city": "Madrid",
"zip": "28001",
"type": "FAMILIAR",
"balance": "12.50 €",
"is_unreg": false,
"b_paid": 0
}
```
---
### `POST /people/qr` 🔒
Genera un código QR temporal (válido 15 minutos) para el abonado.
**Body**
| Campo | Tipo | Descripción |
|--------|---------|--------------------------------------|
| family | integer | (opcional) Número de familiar de la unidad |
**Respuesta 200**
```json
{
"m": "00042",
"f": "01",
"code": "ABCDEF123456"
}
```
---
### `GET /people/card` 🔒
Devuelve el carnet digital del abonado en base64.
**Respuesta 200**
```json
{
"card": "iVBORw0KGgoAAAANSUhEUgAA...",
"me": 1
}
```
> `card` es una imagen PNG codificada en base64. Si no existe, devuelve `""`.
---
### `GET /people` 🔒
Devuelve todos los miembros de la unidad familiar del abonado autenticado.
**Respuesta 200**
```json
{
"people": [
{
"member": "00042",
"family": 1,
"first_name": "María",
"last_name": "García López",
"email": "maria@example.com",
"phone": "612345678",
"is_unreg": false
},
{
"member": "00042",
"family": 2,
"first_name": "Carlos",
"last_name": "García López",
"email": "carlos@example.com",
"phone": "623456789",
"is_unreg": false
}
],
"me": 1
}
```
> `me` indica el número de familiar del usuario autenticado dentro del array.
---
## 3. Activities (actividades)
### `GET /activities` 🔒
Devuelve el árbol completo de actividades/cursos disponibles.
**Respuesta 200**
```json
[
{
"id": "001",
"name": "PÁDEL",
"levels": [
{
"id": "00101",
"name": "Iniciación",
"groups": [
{
"id": "0010101",
"name": "Lunes 18h",
"monitor": "Carlos",
"place": "Pista 1",
"hour": "L 18:00-19:00",
"price": "25.00"
},
{
"id": "0010102",
"name": "Miércoles 19h",
"monitor": "Carlos",
"place": "Pista 2",
"hour": "X 19:00-20:00",
"price": "25.00"
}
]
},
{
"id": "00102",
"name": "Avanzado",
"groups": [
{
"id": "0010201",
"name": "Martes 20h",
"monitor": "Ana",
"place": "Pista 3",
"hour": "M 20:00-21:00",
"price": "30.00"
}
]
}
]
},
{
"id": "002",
"name": "NATACIÓN",
"levels": [
{
"id": "00201",
"name": "Adultos",
"groups": [
{
"id": "0020101",
"name": "Lunes y Miércoles",
"monitor": "María",
"place": "Piscina",
"hour": "L-X 07:30-08:30",
"price": "35.00"
}
]
}
]
}
]
```
---
### `POST /activities` 🔒
Preinscribe a un miembro de la familia en una actividad.
**Body**
| Campo | Tipo | Descripción |
|----------------|---------|--------------------------------------------|
| activity | string | Código de actividad (7 dígitos, ej. `0010101`) |
| family | integer | Número de familiar |
| date_reg | string | Fecha de alta `dd/mm/yyyy` (opcional) |
| test_run | string | `permanent` o `test` |
| comment | string | Comentario / cuestionario de salud |
| privacy_policy | boolean | Aceptación política de privacidad |
| privacy_sports | boolean | Aceptación cláusula deportiva |
**Respuesta 200 — éxito**
```json
{
"msg": "Su preinscripción está siendo procesada, cuando sea aceptado/a en el grupo le llegará un mensaje de confirmación.",
"error": "0"
}
```
**Respuesta 200 — error**
```json
{
"msg": "El abonado/a ya está inscrito en la actividad",
"error": "3"
}
```
> Códigos de error: `1` actividad no permitida · `2` ya en lista de espera · `3` ya inscrito · `4` error al guardar.
---
### `GET /activities/unreg` 🔒
Devuelve las actividades e inscripciones de lista de espera de todos los miembros de la familia para solicitar bajas.
**Respuesta 200**
```json
[
{
"fk_i_member_id": "00042",
"i_family": 1,
"s_first_name": "María",
"s_last_name": "García",
"courses": [
{ "i_activity": "0010101", "s_name": "Pádel Iniciación - Lunes 18h", "d_reg_date": "01/09/2025" },
{ "i_activity": "0030101", "s_name": "Fitness Mañanas", "d_reg_date": "15/09/2025" }
],
"waitlist": [
{ "i_activity": "0020101", "s_name": "Natación Adultos - L-X", "dt_date": "10/10/2025" }
]
}
]
```
---
### `POST /activities/unreg` 🔒
Solicita la baja de actividades e inscripciones de lista de espera.
**Body**
| Campo | Tipo | Descripción |
|---------|--------|---------------------------------------------------------------|
| acs | object | Mapa `"{family}-{activity}": 1` de actividades a dar de baja |
| wls | object | Mapa `"{family}-{activity}": 1` de listas de espera |
| comment | string | Motivo / comentario |
| reason | string | Razón de baja |
| deftemp | string | `definitiva` o `temporal` |
**Respuesta 200**
```json
{
"msg": "Su solicitud de baja ha sido procesada. Muchas gracias.",
"error": "0"
}
```
---
## 4. Booking (reservas — app)
### `GET /booking` 🔒
Devuelve la rejilla de disponibilidad de pistas para una actividad y fecha.
**Query params**
| Param | Tipo | Ejemplo |
|----------|--------|----------------|
| actividad | integer | `1` |
| fecha | string | `18/03/2026` |
**Respuesta 200**
```json
{
"columns": ["Horarios", "01", "02", "03", "04"],
"data": [
[
{ "row": 0, "hour_id": 1, "text": "0900-1000", "type": "H" },
{ "id": "", "paid": "", "hourText": "0900-1000", "courtText": "01", "hour": 1, "court": 0, "type": "E", "text": "" },
{ "id": 4521, "paid": 0, "hourText": "0900-1000", "courtText": "02", "hour": 1, "court": 1, "type": "R", "text": "R" },
{ "id": "", "paid": "", "hourText": "0900-1000", "courtText": "03", "hour": 1, "court": 2, "type": "E", "text": "" },
{ "id": "", "paid": "", "hourText": "0900-1000", "courtText": "04", "hour": 1, "court": 3, "type": "E", "text": "" }
]
],
"ac": { "pk_i_activity_id": 1, "s_name": "PÁDEL MURO", "i_courts": 4 },
"light": "1.00",
"extra": "1.50",
"amount": "6.00"
}
```
> Tipos de celda: `H` cabecera horario · `E` disponible · `R` ocupada (otro) · `I` tuya sin pagar · `P` tuya pagada.
---
### `GET /booking/book/{id}` 🔒
Devuelve el detalle de una reserva concreta.
**Respuesta 200**
```json
{
"pk_i_id": 1234,
"fk_i_activity_id": 1,
"d_date": "2026-03-18",
"fk_i_court_id": 2,
"fk_i_hour": 3,
"s_hour": "1100-1200",
"fk_i_member_id": 42,
"fk_i_family": 1,
"b_paid": 0,
"s_source": "APP"
}
```
---
### `GET /booking/checkout/{id}` 🔒
Devuelve los datos de pago para una reserva.
**Respuesta 200**
```json
{
"booking_id": 1234,
"amount": "6.00",
"status": "pending",
"payment_url": "https://example.com/checkout/1234"
}
```
---
### `POST /booking` 🔒
Realiza una reserva de pista.
**Body**
| Campo | Tipo | Descripción |
|-------------------|---------|------------------------------|
| fk_i_activity_id | integer | ID de actividad |
| hour | integer | ID de hora |
| d_date | string | Fecha `dd/mm/yyyy` |
| court | integer | Número de pista (base 0) |
| light | boolean | Solicitar luz |
**Respuesta 200**
```json
{
"msg": "Reserva realizada correctamente",
"error": "0",
"id": 5678
}
```
---
### `POST /booking/cancel` 🔒
Cancela una reserva existente.
**Body**
| Campo | Tipo |
|-------|---------|
| id | integer |
**Respuesta 200**
```json
{
"msg": "Reserva cancelada correctamente",
"error": "0"
}
```
---
### `POST /booking/edit` 🔒
Edita una reserva existente.
**Body**
| Campo | Tipo | Descripción |
|-------|---------|-------------|
| id | integer | ID de reserva |
| light | boolean | Activar/desactivar luz |
**Respuesta 200**
```json
{
"msg": "Reserva editada correctamente",
"error": "0"
}
```
---
### `POST /booking/refresh` 🔒
Refresca el estado de una reserva (actualiza pagos pendientes).
**Respuesta 200**
```json
{
"msg": "ok",
"error": "0"
}
```
---
## 5. Payments
### `GET /payments` 🔒
Devuelve el historial de pagos del abonado.
**Respuesta 200**
```json
{
"payments": [
{
"id": 1,
"description": "Cuota mensual",
"amount": "35.00",
"date": "01/03/2026",
"status": "Pagado",
"type": "REMESA"
},
{
"id": 2,
"description": "Actividad pádel",
"amount": "25.00",
"date": "15/02/2026",
"status": "Pagado",
"type": "CAJA"
}
],
"error": "0"
}
```
---
## 6. Wallet (monedero)
### `GET /wallet` 🔒
Devuelve el saldo y el historial de transacciones del monedero.
**Respuesta 200**
```json
{
"transactions": [
{
"id": 1,
"concept": "Recarga monedero",
"amount": "20.00",
"date": "10/03/2026 09:15",
"balance": "20.00"
},
{
"id": 2,
"concept": "Pago reserva pádel",
"amount": "-6.00",
"date": "12/03/2026 18:32",
"balance": "14.00"
}
],
"balance": "14.00 €",
"error": "0"
}
```
---
## 7. Search (búsqueda)
### `GET /search` 🔒
Búsqueda rápida de socios por nombre, apellido, número o DNI.
**Query params**
| Param | Tipo | Descripción |
|-------|--------|-------------------|
| q | string | Término de búsqueda |
**Respuesta 200**
```json
[
{ "id": "0004201", "member": "00042", "family": "01", "name": "García, María", "phone": "612345678" },
{ "id": "0012301", "member": "00123", "family": "01", "name": "García, Antonio", "phone": "623456789" }
]
```
---
### `GET /search/new` 🔒
Búsqueda avanzada con filtros de tipo y estado.
**Query params**
| Param | Tipo | Valores |
|--------|--------|-----------------------------|
| q | string | Término |
| type | string | `all` · `FAMILIAR` · `INDIVIDUAL` · `SABIC` |
| status | string | `all` · `active` · `inactive` |
**Respuesta 200**
```json
{
"results": [
{
"id": "0004201",
"member": "00042",
"family": "01",
"name": "García, María",
"phone": "612345678",
"email": "maria@example.com",
"dni": "12345678A",
"type": "FAMILIAR",
"is_unreg": false,
"unreg_reason": "",
"b_paid": 0
}
],
"total": 1
}
```
---
### `GET /search/doorlog` 🔒
Devuelve el log de accesos por torniquete de un abonado.
**Query params**
| Param | Tipo | Descripción |
|--------|---------|----------------------------|
| member | string | Número de socio (5 dígitos)|
| family | integer | Número de familiar (def. 1)|
| limit | integer | Máx. registros (def. 100) |
**Respuesta 200**
```json
[
{
"pk_i_id": 1,
"fk_i_member_id": "00042",
"fk_i_people_id": 1,
"dt_date": "2026-03-15 09:23:11",
"s_source": "TORNIQUETE"
},
{
"pk_i_id": 2,
"fk_i_member_id": "00042",
"fk_i_people_id": 1,
"dt_date": "2026-03-14 18:45:02",
"s_source": "APP"
}
]
```
---
## 8. Members (socios — admin)
> Todos los endpoints de esta sección requieren autenticación 🔒.
El objeto **member** tiene la siguiente estructura completa:
```json
{
"pk_i_id": 42,
"d_reg_date": "2018-03-15",
"s_address": "Calle Mayor 12",
"s_city": "Madrid",
"s_zip": "28001",
"s_email": "socio@example.com",
"s_phone": "612345678",
"s_bank": "0049",
"s_bank_office": "1500",
"s_bank_sec_code": "42",
"s_bank_account": "0123456789",
"s_bank_name": "Santander",
"s_mandate": "MNDT-00042",
"s_type": "FAMILIAR",
"s_comment": "",
"s_comment2": "",
"b_paid": 0,
"b_rem_special": 0,
"b_retenido": 0,
"d_unreg_date": null,
"s_unreg_reason": "",
"people": [
{
"fk_i_member_id": 42,
"i_family": 1,
"s_first_name": "María",
"s_last_name": "García",
"s_dni": "12345678A",
"d_birth_date": "1985-03-15",
"s_phone": "612345678",
"s_email": "maria@example.com",
"b_lopd": 1,
"b_newsletter": 1,
"b_authorized": 1,
"b_authorized2": 0,
"s_photo_path": null
}
],
"cautions": [],
"comments": []
}
```
---
### `GET /members/quick` — Búsqueda rápida por número
**Query:** `fk_i_member_id=42` · `i_family=1` (opcional)
**Respuesta:** objeto `member` completo
---
### `GET /members/first` — Primer socio
### `GET /members/last` — Último socio
### `GET /members/{id}` — Socio por ID
### `GET /members/{id}/prev` — Socio anterior
### `GET /members/{id}/next` — Socio siguiente
Todas devuelven el objeto `member` completo.
---
### `GET /members/search` — Autocompletar socios
**Query:** `term=garcia`
**Respuesta 200**
```json
{
"0004201": "(00042-01) García, María",
"0004202": "(00042-02) García, Carlos"
}
```
---
### `POST /members/edit`
**Body:** campos del objeto member a actualizar (`pk_i_id` requerido).
**Respuesta 200**
```json
{ "msg": "Socio editado correctamente", "member": { "...": "objeto member completo" } }
```
---
### `POST /members/paid-caution` — Toggle flag recibos pendientes
**Body:** `pk_i_id`
**Respuesta:** objeto `member` actualizado
---
### `POST /members/recover` — Reactivar socio dado de baja
**Body:** `pk_i_id`
**Respuesta:** objeto `member` actualizado
---
### `POST /members/pre-delete` — Pre-verificación antes de dar de baja
**Body:** `pk_i_id`
**Respuesta 200**
```json
{ "pendientes": 0, "devueltos": 2, "revision": 0 }
```
---
### `POST /members/delete` — Dar de baja a un socio
**Body:** `pk_i_id` · `d_unreg_date` · `s_unreg_reason`
**Respuesta:** objeto `member` actualizado
---
### `POST /members/change-type` — Cambiar tipo de socio
**Body:** `id` · `s_type`
**Respuesta 200:** `{ "error": 0, "msg": "ok" }`
---
### `POST /members/rem-special` — Toggle remesa especial
**Body:** `pk_i_id` · `b_rem_special`
**Respuesta:** objeto `member` con campo `error`
---
### `POST /members/retener` / `POST /members/retener-cancel`
Marca/desmarca al socio como retenido.
**Body:** `member`
**Respuesta:** `{ "msg": "...", "member": { ... } }`
---
### `POST /members/promotion` — Registrar promoción
**Body:** `pk_i_id` · `friend`
**Respuesta:** `{ "msg": "Promoción añadida correctamente", "member": { ... } }`
---
### `POST /members/edit-comment` — Editar comentario interno
**Body:** `pk_i_id` · `s_comment2`
**Respuesta:** `{ "msg": "...", "member": { ... } }`
---
### `POST /members/comment/add` — Añadir comentario
**Body:** `member` · `comment`
**Respuesta:** `{ "msg": "...", "member": { ... } }`
---
### `GET /members/comment/edit` — Obtener comentario para editar
**Query:** `id`
**Respuesta:** `{ "comment": { "id": 1, "comment": "texto", "created_at": "..." } }`
---
### `POST /members/comment/edit-save` — Guardar edición de comentario
**Body:** `id` · `comment` · `pk_i_id`
**Respuesta:** `{ "msg": "...", "member": { ... } }`
---
### `POST /members/comment/delete` — Eliminar comentario
**Body:** `id` (`"old"` para el comentario heredado) · `member_id`
**Respuesta:** `{ "msg": "Comentario borrado", "member": { ... } }`
---
### Familiares (people)
| Método | Ruta | Descripción |
|--------|------|-------------|
| `POST` | `/members/people/add` | Añadir familiar |
| `POST` | `/members/people/edit` | Editar familiar |
| `POST` | `/members/people/delete` | Eliminar familiar |
| `POST` | `/members/people/auth` | Toggle autorización acceso |
| `POST` | `/members/people/auth2` | Toggle autorización 2 |
| `POST` | `/members/people/picture/upload` | Subir foto (multipart `webcam`) |
| `POST` | `/members/people/picture/delete` | Eliminar foto |
| `GET` | `/members/people/card` | Tarjetas de acceso |
| `POST` | `/members/people/card/add` | Añadir tarjeta |
| `POST` | `/members/people/card/delete` | Eliminar tarjeta |
| `POST` | `/members/people/password` | Generar enlace de reset de contraseña |
| `GET` | `/members/people/card/preview` | Previsualizar carnet |
| `POST` | `/members/people/card/print` | Imprimir carnet |
| `POST` | `/members/people/minors-card` | Actualizar visibilidad carnets menores |
Todas las respuestas de escritura devuelven `{ "msg": "...", "member": { ... } }` o `{ "error": 0, "msg": "..." }`.
**`GET /members/people/card`** devuelve:
```json
{
"cards": [
{ "pk_i_id": 1, "fk_i_member_id": 42, "i_family": 1, "s_card": "ABCD1234EFGH5678" }
]
}
```
**`POST /members/people/password`** devuelve:
```json
{ "h": "https://app.example.com/password/reset/TOKEN?email=socio@example.com" }
```
---
## 9. Courses (cursos — admin)
### `GET /courses/levels` 🔒
**Query:** `course` (código de actividad)
**Respuesta 200**
```json
[
{ "i_code1": 1, "i_code2": 1, "i_code3": 0, "s_name": "Iniciación" },
{ "i_code1": 1, "i_code2": 2, "i_code3": 0, "s_name": "Avanzado" }
]
```
---
### `GET /courses/groups` 🔒
**Query:** `course` · `level`
**Respuesta 200**
```json
[
{
"i_code1": 1, "i_code2": 1, "i_code3": 1,
"s_name": "Lunes 18h-19h",
"f_price": "25.00",
"i_max_students": 15,
"s_hour": "L 18:00-19:00",
"s_monitor": "Carlos",
"s_place": "Pista 1",
"i_one_bill": 0,
"b_fitness": 0,
"b_pending_count": 0,
"s_comment": ""
}
]
```
---
### `GET /courses/group` 🔒
**Query:** `course` · `level` · `group`
**Respuesta:** objeto grupo (igual que el array anterior, un único elemento)
---
### `GET /courses/data-alumni` 🔒
Actividades en las que está inscrito un abonado.
**Query:** `code` (7 dígitos: `{member}{family}`)
**Respuesta 200**
```json
[
{ "i_activity": "0010101", "s_name": "Pádel Iniciación L18h" },
{ "i_activity": "0030101", "s_name": "Fitness Mañanas" }
]
```
---
### `GET /courses/alumnis` 🔒
Lista de alumnos de un grupo.
**Query:** `course` · `level` · `group` · `month` (opcional) · `year` (opcional)
**Respuesta 200**
```json
[
{
"pk_i_id": 1234,
"fk_i_member_id": "00042",
"fk_i_family": "01",
"i_activity": "0010101",
"s_last_name": "García",
"s_first_name": "María",
"d_reg_date": "01/09/2025",
"f_price": "25.00",
"mod_price": "25.00",
"ac_price": "25.00",
"s_type": "REMESA",
"s_comment": "",
"b_authorized": 1,
"b_authorized2": 0,
"i_door_log_count": 0
}
]
```
---
### `GET /courses/waitlist` 🔒
**Query:** `course` · `level` · `group`
**Respuesta:** igual que `/courses/alumnis` con campo adicional `dt_date`.
---
### `GET /courses/alumni/get-reg-comment` 🔒
**Query:** `m` · `f` · `a`
**Respuesta 200**
```json
{
"fk_i_member_id": "00042",
"fk_i_family": "01",
"i_activity": "0010101",
"s_comment": "Interesada en el grupo de tarde si hay plaza."
}
```
---
### `GET /courses/alumni/get-unreg-comment` 🔒
**Query:** `i` (ID del registro en unreg_requests)
**Respuesta 200**
```json
{ "id": 5, "comment": "Se marcha al extranjero temporalmente." }
```
---
### Escritura de cursos
| Método | Ruta | Body principal | Respuesta |
|--------|------|----------------|-----------|
| `POST` | `/courses/add` | `s_name` | Nuevo `i_code1` |
| `POST` | `/courses/edit` | `course` · `s_name` | `"ok"` |
| `POST` | `/courses/delete` | `course` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/level/add` | `course` · `s_name` | Nuevo `i_code2` |
| `POST` | `/courses/level/edit` | `course` · `level` · `s_name` | `"ok"` |
| `POST` | `/courses/level/delete` | `course` · `level` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/group/add` | `course` · `level` · `s_name` · `f_price` · `i_max_students` · `s_hour` · `s_monitor` · `s_place` | `"ok"` |
| `POST` | `/courses/group/edit` | `id` · `s_field` · `s_value` | `"ok"` |
| `POST` | `/courses/group/delete` | `course` · `group` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/group/comment` | `course` · `level` · `group` · `comment` | `{ "msg": "...", "error": 0 }` |
| `POST` | `/courses/alumni/add` | `course` · `level` · `group` · `fk_i_member_id` · `d_date` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/alumni/add-from-wait` | idem + `email` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/alumni/delete` | `course` · `level` · `group` · `fk_i_member_id` · `fk_i_family` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/alumni/delete-by-id` | `id` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/alumni/delete-by-ids` | `ids` (CSV) | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/alumni/delete-wait` | `course` · `level` · `group` · `fk_i_member_id` · `fk_i_family` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/alumni/empty` | `course` · `level` · `group` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/alumni/import` | `ocourse` · `olevel` · `ogroup` · `dcourse` · `dlevel` · `dgroup` · `date` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/alumni/auth` | `id` · `value` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/alumni/auth2` | `id` · `value` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/alumni/reg-comment` | `m` · `f` · `a` · `s_comment` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/courses/alumni/unreg-comment` | `id` · `s_comment` | `{ "error": 0, "msg": "..." }` |
---
## 10. Bills (recibos — admin)
### `GET /bills/member-search` 🔒
**Query:** `fk_i_member_id`
**Respuesta 200** — array de recibos:
```json
[
{
"alumni": 1234,
"member": "00042",
"family": "01",
"name": "García, María",
"ac_name": "Pádel Iniciación L18h",
"activity": "0010101",
"price": "25.00",
"user_price": "25.00",
"bill_price": "25.00",
"month": 3,
"year": 2026,
"month_name": "marzo",
"status": 0,
"type": "REMESA",
"bonus": 0,
"cash_date": ""
}
]
```
> Estado (`status`): `0` pendiente · `1` pagado · `2` eliminado · `4` devuelto.
---
### `GET /bills/alumni` 🔒
**Query:** `id` (ID de alumno)
**Respuesta 200**
```json
{
"pk_i_id": 1234,
"fk_i_member_id": "00042",
"fk_i_family": "01",
"i_activity": "0010101",
"f_price": "25.00",
"s_type": "REMESA",
"s_comment": "",
"person": { "s_first_name": "María", "s_last_name": "García" },
"next_bills": [
{ "i_month": 4, "i_year": 2026, "i_status": 0, "f_price": "25.00" },
{ "i_month": 5, "i_year": 2026, "i_status": 0, "f_price": "25.00" }
]
}
```
---
### `GET /bills/from-alumni` 🔒
**Query:** `id`
**Respuesta 200**
```json
{
"pk_i_id": 1234,
"i_activity": "0010101",
"person": { "s_first_name": "María", "s_last_name": "García" },
"bills": [
{ "i_month": 1, "i_year": 2026, "i_status": 1, "f_price": "25.00", "year": 0 },
{ "i_month": 2, "i_year": 2026, "i_status": 1, "f_price": "25.00", "year": 0 },
{ "i_month": 3, "i_year": 2026, "i_status": 0, "f_price": "25.00", "year": 0 }
]
}
```
---
### `GET /bills/number` 🔒
**Query:** `course` · `level` · `group` · `month` · `year` · `type` (`BANK`/`ALL`) · `alumni[]` (opcional)
**Respuesta 200**
```json
12
```
---
### `GET /bills/pending` 🔒
**Query:** `id` (member) · `complete` (0/1)
**Respuesta 200** — array de recibos pendientes (misma estructura que `/bills/member-search`)
---
### `GET /bills/search` 🔒
**Query:** `fk_i_member_id` · `i_month` · `status` · `ext`
**Respuesta 200**
```json
{
"iTotalRecords": 5,
"iTotalDisplayRecords": 5,
"aaData": [ { "...": "objeto recibo" } ]
}
```
---
### `GET /bills/remittance-return/member` 🔒
**Query:** `member`
**Respuesta 200** — mapa de miembros con recibos devueltos:
```json
{
"42": {
"pk_i_id": 42,
"people": [ { "...": "..." } ],
"months": { "3": 1 },
"bills": [ { "...": "..." } ],
"i_total": "25.00"
}
}
```
---
### Escritura de recibos
| Método | Ruta | Body | Respuesta |
|--------|------|------|-----------|
| `POST` | `/bills/alumni/edit` | `id` · `price` · `type` · `comment` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/bills/generate` | `id` · `month` · `year` | `{ "error": 0, "msg": "Recibo creado" }` |
| `POST` | `/bills/generate-next` | `course` · `level` · `group` · `month` · `year` | `{ "msg": "Recibos generados", "error": 0 }` |
| `POST` | `/bills/generate-multi` | `activity` · `id` · `months[]` | `{ "iTotalRecords": N, "aaData": [...] }` |
| `POST` | `/bills/generate-remittance` | `course` · `level` · `group` · `month` · `year` | `{ "error": 0, "msg": "...", "paid_bills": 12 }` |
| `POST` | `/bills/ticket` | `bills[]` | `{ "error": 0, "msg": "...", "ticket_number": "2025/000123", "lines": [] }` |
| `POST` | `/bills/cancel` | `acs[]` | `{ "error": 0, "msg": "OK", "lines": [] }` |
| `POST` | `/bills/delete` | `acs[]` | `{ "error": 0, "msg": "OK" }` |
| `POST` | `/bills/delete-alumni` | `ids` (CSV) · `month` · `year` | `{ "error": 0, "msg": "OK" }` |
| `POST` | `/bills/recover` | `acs[]` | `{ "error": 0, "msg": "OK" }` |
| `POST` | `/bills/remittance-return/add` | `member` · `month` · `year` | `{ "error": 0, "msg": "OK" }` |
| `POST` | `/bills/remittance-return/delete` | `alumni` · `month` · `year` · `id` · `special` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/bills/remittance-return/print` | `id` | `{ "msg": "...", "data": { "id": 1 }, "lines": [] }` |
---
## 11. Booking Admin (reservas — admin)
### `GET /booking-admin/grid` 🔒
**Query:** `fk_i_activity_id` · `d_date` (`dd/mm/yyyy`)
**Respuesta 200**
```json
{
"aaData": [
["<div id=\"0\">0900-1000</div>", "00042.1+", "", "00123.2", ""],
["<div id=\"1\">1000-1100</div>", "", "", "", "00456.1"]
],
"aoColumns": [
{ "sTitle": "Horarios", "sClass": "celda_horario", "sWidth": "65px" },
{ "sTitle": "01", "sClass": "celda_normal", "sWidth": "95px" },
{ "sTitle": "02", "sClass": "celda_normal", "sWidth": "95px" }
],
"ac": { "pk_i_activity_id": 1, "s_name": "PÁDEL MURO", "i_courts": 4 },
"light": 100,
"extra": 150,
"extra2": 150
}
```
> En `aaData`, la celda contiene `{member}.{family}[+]` si está ocupada, `""` si libre. El `+` indica pagado.
---
### `GET /booking-admin/shared` 🔒
**Query:** `fk_i_activity_id` · `d_date` · `hour` · `court`
**Respuesta 200** — array de reservas en la misma hora/pista (compartidas)
---
### `GET /booking-admin/single` 🔒
**Query:** `fk_i_activity_id` · `d_date` · `hour` · `aPos[1]`
**Respuesta 200**
```json
{ "fk_i_member_id": 42, "fk_i_family": 1, "d_date": "2026-03-18", "b_paid": 0 }
```
---
### `GET /booking-admin/single-by-id` 🔒
**Query:** `id`
**Respuesta 200**
```json
{ "pk_i_id": 1234, "fk_i_member_id": 42, "fk_i_family": 1, "d_date": "2026-03-18", "b_paid": 0 }
```
---
### `POST /booking-admin/do` 🔒
**Body:** `fk_i_activity_id` · `hour` · `d_date` · `court` · `member` · `aPos[]`
**Respuesta 200**
```json
{
"msg": "Pista reservada correctamente",
"error": 1,
"cell": "2x1",
"book": { "fk_i_member_id": 42 }
}
```
> `error`: `0` fallo · `1` OK · `2` ya pagada · `3` ocupada por otro · `4` recibos pendientes · `5` dado de baja.
---
### `POST /booking-admin/confirm` 🔒
**Body:** `id` (ID de reserva pendiente de confirmar)
**Respuesta 200** — misma estructura que `/booking-admin/do`
---
### `POST /booking-admin/empty` 🔒
**Body:** `fk_i_activity_id` · `hour` · `s_hour` · `d_date` · `aPos[]`
**Respuesta 200**
```json
{ "error": 2, "book": { "pk_i_id": 1234, "b_paid": 0, "text": "" } }
```
> `error`: `0` nada que vaciar · `1` estaba pagada (devolver ticket) · `2` eliminada (no pagada) · `3` eliminada (pagada sin ticket).
---
### `POST /booking-admin/empty-by-id` 🔒
**Body:** `id`
**Respuesta:** igual que `/booking-admin/empty`
---
## 12. Cash (caja — admin)
### `GET /cash/last-ticket` 🔒
**Query:** `iPage` (paginación, base 0)
**Respuesta 200**
```json
{
"ticket": [
{ "dt_date": "2026-03-18 10:23:45", "i_line": 1, "s_line": "--------------------------------" },
{ "dt_date": "2026-03-18 10:23:45", "i_line": 2, "s_line": "CLUB DEPORTIVO SANTA ANA" },
{ "dt_date": "2026-03-18 10:23:45", "i_line": 3, "s_line": "Reserva pádel 6,00 €" }
]
}
```
---
### `GET /cash/search` 🔒
**Query:** `searchQuery` · `paymentType` · `iPage`
**Respuesta 200**
```json
{
"items": [
{
"pk_i_id": 12345,
"pk_s_id": "2026/000042",
"d_year": 2026,
"dt_date": "2026-03-18 10:23:45",
"fk_s_activity_group_id":"AC",
"fk_i_activity_id": "01",
"fk_i_member_id": "00042",
"s_name": "García, María",
"s_concept": "Reserva pádel",
"f_price": 6000000,
"f_tax": 0,
"f_amount": "6.00",
"s_type": "CAJA"
}
]
}
```
---
### `GET /cash/ticket-from-booking` 🔒
**Query:** `ac` · `hour` · `court` · `date`
**Respuesta 200**
```json
{ "error": 0, "ticket": [ { "...": "objeto cash" } ] }
```
---
### Escritura de caja
| Método | Ruta | Body | Respuesta |
|--------|------|------|-----------|
| `POST` | `/cash/post` | `fk_i_member_id` · `items[]` · `prices[]` · `concepts[]` · `taxes[]` · `amounts[]` · `payment_type` · `dt_date` | `{ "msg": "...", "total": 6000000, "pk_s_id": "2026/000042", "data": "" }` |
| `POST` | `/cash/post-ticket` | datos del ticket | `{ "msg": "...", "data": "", "lines": [] }` |
| `POST` | `/cash/print-ticket` | `date` | `{ "lines": ["linea1", "linea2"] }` |
| `POST` | `/cash/delete` | `id` | `{ "error": 0, "msg": "..." }` |
| `POST` | `/cash/delete-mov` | `sdate` · `edate` | `{ "error": 0, "msg": "OK" }` |
| `POST` | `/cash/book-ticket` | datos reserva + pago | `{ "msg": "...", "data": "", "lines": [] }` |
| `POST` | `/cash/book-return` | datos devolución | `{ "error": 0, "book": {}, "lines": [] }` |
| `POST` | `/cash/bonus-ticket` | datos bono | `{ "error": 0, "lines": [] }` |
| `POST` | `/cash/bonus-ticket-nopaid` | datos bono | `{ "error": 0, "lines": [] }` |
| `POST` | `/cash/qr` | `member` · `familiar` | `{ "msg": "ok", "error": 0, "qr": "00042-01-ABCDEF123456" }` |
---
## 13. Dayoff / Celebrations
| Método | Ruta | Body | Respuesta |
|--------|------|------|-----------|
| `POST` | `/dayoff/add` | `d_date` (`dd/mm/yyyy`) · `s_name` · `type` | `1` |
| `POST` | `/dayoff/delete` | `d_date` · `s_name` | `1` |
| `POST` | `/celebration/add` | `date` (`dd/mm/yyyy`) · `name` · `member` · `comment` | `1` |
| `POST` | `/celebration/delete` | `id` | `1` |
---
## 14. Remittance (remesas especiales)
### `POST /remittance/member/add` 🔒
**Body:** `id` (remittance ID) · `member` · `amount` · `concept`
**Respuesta 200**
```json
{ "error": 0, "msg": "Socio añadido correctamente" }
```
---
### `POST /remittance/member/delete` 🔒
**Body:** `id`
**Respuesta 200**
```json
{ "error": 0, "msg": "Socio eliminado correctamente" }
```
---
### `POST /remittance/edit` 🔒
**Body:** `id` · `field` · `value`
**Respuesta 200**
```json
{ "error": 0, "msg": "Editado correctamente" }
```
---
### `POST /remittance/sepa` 🔒
**Body:** `id` (remittance ID)
**Respuesta 200**
```json
{
"error": 0,
"msg": "Remesa generada correctamente",
"file": "/storage/remesas/2026_sepa.xml",
"count": 42
}
```
---
## 15. Inscriptions (preinscripciones)
El objeto **inscription** tiene esta estructura:
```json
{
"id": 1,
"address": "Calle Mayor 12",
"city": "Madrid",
"zip_code": "28001",
"emergency_phone": "612345678",
"bank": "0049",
"bank_office": "1500",
"bank_sec_code": "42",
"bank_account": "0123456789",
"comment": "",
"know_us": "amigo",
"know_us_comment": "",
"know_us_by_who": "",
"created_at": "2026-03-01T10:00:00.000Z",
"people": [
{
"first_name": "Luis",
"last_name": "Pérez",
"email": "luis@example.com",
"phone": "634567890",
"birth_date": "15/04/1990",
"dni": "87654321B"
}
]
}
```
| Método | Ruta | Descripción | Respuesta |
|----------|------|-------------|-----------|
| `GET` | `/inscriptions/first` | Primera preinscripción | objeto inscription |
| `GET` | `/inscriptions/last` | Última preinscripción | objeto inscription |
| `GET` | `/inscriptions/{id}` | Preinscripción por ID | objeto inscription |
| `GET` | `/inscriptions/{id}/prev` | Anterior | objeto inscription |
| `GET` | `/inscriptions/{id}/next` | Siguiente | objeto inscription |
| `POST` | `/inscriptions` | Convertir en socio | `{ "error": 0, "msg": "Usuario creado correctamente", "id": 1234 }` |
| `DELETE` | `/inscriptions/{id}` | Eliminar preinscripción | objeto inscription siguiente |
**`POST /inscriptions` — Body**
| Campo | Tipo |
|----------------|---------|
| inscription_id | integer |
| s_type | string (`INDIVIDUAL` / `FAMILIAR` / `SABIC` / ...) |
| id | integer | (opcional, número de socio a asignar) |
| d_reg_date | string |
---
## 16. Admin Misc
### `POST /admin/quota` 🔒
Actualiza el precio/cupo de una actividad.
**Body:** `pk_s_group_id` · `pk_i_activity_id` · `s_field` · `s_value`
**Respuesta 200**
```json
1
```
---
### `POST /admin/reboot` 🔒
Reinicia la impresora.
**Respuesta 200**
```json
{ "error": 0, "msg": "Reiniciando impresora..." }
```
---
### `GET /admin/check-reboot` 🔒
**Respuesta 200**
```json
{ "status": "ok" }
```
---
### `GET /admin/bank-sec-code` 🔒
Calcula el dígito de control bancario.
**Query:** `bank` · `office` · `account`
**Respuesta 200**
```json
"42"
```
---
### `GET /admin/bank-name` 🔒
**Query:** `bank` (código 4 dígitos)
**Respuesta 200**
```json
{ "name": "Banco Santander" }
```
---
### `POST /admin/print-card` 🔒
**Body:** `member` · `family`
**Respuesta 200**
```json
{ "error": 0, "msg": "Carnet enviado a imprimir" }
```
---
### `POST /admin/set-printer` 🔒
**Body:** `printer` (nombre/IP)
**Respuesta 200**
```json
{ "error": 0, "msg": "Impresora configurada correctamente" }
```
---
### `POST /admin/sorteo` 🔒
Registra participaciones en sorteo.
**Body:** `data[]` (array de `{ member, name, email, phone, tickets }`)
**Respuesta 200**
```json
{ "error": 0, "msg": "Participación registrada" }
```
---
## 17. Printer
### `GET /printer/lines` 🔒
Devuelve líneas pendientes de imprimir.
**Respuesta 200**
```json
{ "error": 0, "lines": [] }
```
---
## 18. Hash (público)
### `GET /hash/{hash}`
Devuelve el hash BCrypt de un valor, en base64. Útil para generar contraseñas desde scripts.
**Ejemplo:** `GET /2.0/hash/mipassword`
**Respuesta 200**
```json
{ "h": "JDJ5JDEwJGFCQ0Rm..." }
```
---
## Códigos de error comunes
| Campo `error` | Significado |
|---------------|-------------|
| `"0"` / `0` | Operación correcta |
| `"1"` / `1` | Error genérico o entidad no encontrada |
| `"2"` | Ya existe / ya está pagado |
| `"3"` | Ocupado por otro usuario |
| `"4"` | Recibos pendientes — acceso restringido |
| `"5"` | Dado de baja — acceso restringido |
---
## Convenciones
- **Montos** se almacenan internamente en **millonésimas de euro** (`6000000` = `6.00 €`). La API devuelve el valor formateado como string (`"6.00"`) en los recursos de la app.
- **Fechas** en pantalla usan formato `dd/mm/yyyy`; en base de datos `yyyy-mm-dd`.
- **Códigos de actividad** son cadenas de 7 dígitos: `{course:3}{level:2}{group:2}``0010102`.
- **Identificador de abonado** combina miembro y familiar: `{member:5}{family:2}``0004201`.