🚀 Sprint 1: Implementación de Persistencia con Cadences
Objetivo
Conectar el storefront imaging-clinic-template con el backend de Cadences.app para persistir datos reales en lugar de usar datos demo.
📋 Índice
- Resumen de la Arquitectura
- Requisitos Previos
- Tareas del Sprint
- Endpoints a Implementar
- Guía de Implementación
- Testing
- Checklist de Entrega
1. Resumen de la Arquitectura
┌────────────────────────────────────────────────────────────────────┐
│ cimad STOREFRONT │
│ (imaging-clinic-template) │
├────────────────────────────────────────────────────────────────────┤
│ │
│ Frontend (Astro + React) │
│ └── src/lib/cadences.ts ← Cliente API │
│ │
└────────────────────────────────────────────────────────────────────┘
│
│ HTTP REST
▼
┌────────────────────────────────────────────────────────────────────┐
│ CADENCES.APP (Backend) │
├────────────────────────────────────────────────────────────────────┤
│ functions/api/storefront/[id]/ │
│ ├── setup.js → POST: Crear DATA_TABLEs │
│ ├── data/[project].js → GET: Datos públicos │
│ └── admin/data.js → GET/POST/PUT/DELETE: CRUD admin │
│ │
│ Cloudflare D1 Database │
│ ├── organizations → Config del storefront │
│ ├── projects → DATA_TABLE definitions │
│ └── data_rows → Registros de cada tabla │
└────────────────────────────────────────────────────────────────────┘
Flujo de Datos
- Setup inicial: Se llama a
/api/storefront/{id}/setuppara crear los 11 proyectos DATA_TABLE - Frontend público: Lee datos de
/api/storefront/{id}/data/{project}(sin auth) - Panel admin: CRUD completo via
/api/storefront/{id}/admin/data(requiere auth)
2. Requisitos Previos
En Cadences Backend
- Crear organización para el storefront con
storefrontConfig.id = 'cimad' - Ejecutar setup para crear los 11 proyectos DATA_TABLE
- Configurar usuario admin con permisos
En el Storefront
- Configurar variables de entorno:
# .env
CADENCES_API_URL=https://cadences.pages.dev/api
CADENCES_CUSTOMER_ID=cust_xxx # ID del customer owner
CADENCES_ORGANIZATION_ID=org_xxx # ID de la organización
STOREFRONT_ID=cimad
3. Tareas del Sprint
Tarea 1: Crear Schema para Imaging Clinic
Archivo: functions/api/storefront/schemas.js
Añadir el schema imaging-clinic con los 11 proyectos:
// Añadir a STOREFRONT_SCHEMAS
'imaging-clinic': [
{ key: 'config', name: '⚙️ Configuración', ... },
{ key: 'locations', name: '🏥 Sedes', ... },
{ key: 'equipment', name: '🔬 Equipos', ... },
{ key: 'services', name: '📋 Servicios', ... },
{ key: 'staff', name: '👨⚕️ Personal', ... },
{ key: 'patients', name: '👥 Pacientes', ... },
{ key: 'appointments', name: '📅 Citas', ... },
{ key: 'studies', name: '🩻 Estudios', ... },
{ key: 'reports', name: '📝 Informes', ... },
{ key: 'templates', name: '📄 Plantillas', ... },
{ key: 'blog', name: '📰 Blog', ... },
]
Tarea 2: Actualizar Cliente Cadences
Archivo: src/lib/cadences.ts
Implementar funciones CRUD que faltan:
// Endpoints admin (requieren auth)
export async function getPatients(params) { ... }
export async function createPatient(data) { ... }
export async function updatePatient(id, data) { ... }
export async function deletePatient(id) { ... }
// Igual para: appointments, studies, reports, staff, etc.
Tarea 3: Reemplazar Datos Demo
Archivos a modificar:
src/data/demoData.ts→ Mantener como fallbacksrc/components/admin/PatientsList.tsx→ Usarcadences.getPatients()src/components/admin/Worklist.tsx→ Usarcadences.getStudies()src/components/admin/Agenda.tsx→ Usarcadences.getAppointments()
Tarea 4: Implementar Autenticación Admin
El panel admin requiere autenticación. Opciones:
- Google OAuth (recomendado) - via Cadences
- JWT propio - tokens generados por Cadences
- API Keys - para desarrollo
4. Endpoints a Implementar
Endpoints Públicos (sin auth)
| Método | Endpoint | Descripción |
|---|---|---|
| GET | /api/storefront/{id}/data/config |
Configuración pública |
| GET | /api/storefront/{id}/data/locations |
Sedes |
| GET | /api/storefront/{id}/data/services |
Servicios |
| GET | /api/storefront/{id}/data/staff |
Personal (público) |
| GET | /api/storefront/{id}/data/equipment |
Equipos |
| GET | /api/storefront/{id}/data/blog |
Posts del blog |
| POST | /api/storefront/{id}/public/appointment-request |
Solicitar cita |
Endpoints Admin (requieren auth)
| Método | Endpoint | Descripción |
|---|---|---|
| GET | /api/storefront/{id}/admin/data?project=patients |
Listar pacientes |
| POST | /api/storefront/{id}/admin/data?project=patients |
Crear paciente |
| PUT | /api/storefront/{id}/admin/data/{rowId}?project=patients |
Actualizar |
| DELETE | /api/storefront/{id}/admin/data/{rowId}?project=patients |
Eliminar |
Endpoints del Portal del Paciente
| Método | Endpoint | Descripción |
|---|---|---|
| POST | /api/storefront/{id}/portal/auth |
Login paciente |
| GET | /api/storefront/{id}/portal/appointments |
Mis citas |
| GET | /api/storefront/{id}/portal/studies |
Mis estudios |
| GET | /api/storefront/{id}/portal/studies/{id}/report |
Mi informe |
5. Guía de Implementación
Paso 1: Crear el Schema en Cadences
Crear archivo functions/api/storefront/schemas/imaging-clinic.js:
export const IMAGING_CLINIC_PROJECTS = [
{
key: 'config',
name: '⚙️ Configuración de la Clínica',
color: '#6366f1',
icon: '⚙️',
dataSchema: [
{ id: 'key', name: 'Clave', type: 'text', required: true },
{ id: 'value', name: 'Valor', type: 'text', required: true },
{ id: 'category', name: 'Categoría', type: 'select', options: ['General', 'Contacto', 'Branding', 'Features'] },
],
},
{
key: 'locations',
name: '🏥 Sedes',
color: '#8b5cf6',
icon: '🏥',
dataSchema: [
{ id: 'code', name: 'Código', type: 'text', required: true },
{ id: 'name', name: 'Nombre', type: 'text', required: true },
{ id: 'street', name: 'Dirección', type: 'text', required: true },
{ id: 'city', name: 'Ciudad', type: 'text', required: true },
{ id: 'postalCode', name: 'CP', type: 'text' },
{ id: 'phone', name: 'Teléfono', type: 'text' },
{ id: 'email', name: 'Email', type: 'email' },
{ id: 'lat', name: 'Latitud', type: 'number' },
{ id: 'lng', name: 'Longitud', type: 'number' },
{ id: 'active', name: 'Activo', type: 'boolean' },
{ id: 'order', name: 'Orden', type: 'number' },
],
},
{
key: 'equipment',
name: '🔬 Equipos',
color: '#06b6d4',
dataSchema: [
{ id: 'code', name: 'Código', type: 'text', required: true },
{ id: 'name', name: 'Nombre', type: 'text', required: true },
{ id: 'locationId', name: 'Sede', type: 'text', required: true },
{ id: 'modality', name: 'Modalidad', type: 'select', options: ['CT', 'MR', 'US', 'XR', 'MG', 'DX', 'NM', 'PT', 'DXA'] },
{ id: 'manufacturer', name: 'Fabricante', type: 'text' },
{ id: 'model', name: 'Modelo', type: 'text' },
{ id: 'aeTitle', name: 'AE Title', type: 'text' },
{ id: 'slotDurationMinutes', name: 'Duración Slot (min)', type: 'number' },
{ id: 'active', name: 'Activo', type: 'boolean' },
],
},
{
key: 'services',
name: '📋 Servicios',
color: '#10b981',
dataSchema: [
{ id: 'code', name: 'Código', type: 'text', required: true },
{ id: 'name', name: 'Nombre', type: 'text', required: true },
{ id: 'description', name: 'Descripción', type: 'textarea' },
{ id: 'modality', name: 'Modalidad', type: 'select', options: ['CT', 'MR', 'US', 'XR', 'MG'] },
{ id: 'bodyPart', name: 'Región', type: 'text' },
{ id: 'durationMinutes', name: 'Duración (min)', type: 'number' },
{ id: 'price', name: 'Precio', type: 'number' },
{ id: 'preparationInstructions', name: 'Preparación', type: 'textarea' },
{ id: 'requiresFasting', name: 'Ayuno', type: 'boolean' },
{ id: 'requiresContrast', name: 'Contraste', type: 'boolean' },
{ id: 'showOnWebsite', name: 'Mostrar en Web', type: 'boolean' },
{ id: 'active', name: 'Activo', type: 'boolean' },
],
},
{
key: 'staff',
name: '👨⚕️ Personal',
color: '#f59e0b',
dataSchema: [
{ id: 'firstName', name: 'Nombre', type: 'text', required: true },
{ id: 'lastName', name: 'Apellidos', type: 'text', required: true },
{ id: 'title', name: 'Título', type: 'text' },
{ id: 'role', name: 'Rol', type: 'select', options: ['radiologist', 'technician', 'admin', 'receptionist'] },
{ id: 'specialization', name: 'Especialidad', type: 'text' },
{ id: 'licenseNumber', name: 'Nº Colegiado', type: 'text' },
{ id: 'email', name: 'Email', type: 'email' },
{ id: 'phone', name: 'Teléfono', type: 'text' },
{ id: 'showOnWebsite', name: 'Mostrar en Web', type: 'boolean' },
{ id: 'photo', name: 'Foto URL', type: 'text' },
{ id: 'bio', name: 'Biografía', type: 'textarea' },
{ id: 'active', name: 'Activo', type: 'boolean' },
],
},
{
key: 'patients',
name: '👥 Pacientes',
color: '#ec4899',
dataSchema: [
{ id: 'mrn', name: 'MRN', type: 'text', required: true },
{ id: 'firstName', name: 'Nombre', type: 'text', required: true },
{ id: 'lastName', name: 'Apellidos', type: 'text', required: true },
{ id: 'dateOfBirth', name: 'Fecha Nacimiento', type: 'date' },
{ id: 'gender', name: 'Género', type: 'select', options: ['M', 'F', 'O'] },
{ id: 'nationalId', name: 'DNI/NIE', type: 'text' },
{ id: 'phone', name: 'Teléfono', type: 'text', required: true },
{ id: 'email', name: 'Email', type: 'email' },
{ id: 'allergies', name: 'Alergias', type: 'textarea' },
{ id: 'medicalConditions', name: 'Condiciones', type: 'textarea' },
{ id: 'insuranceCompany', name: 'Aseguradora', type: 'text' },
{ id: 'insurancePolicyNumber', name: 'Nº Póliza', type: 'text' },
],
},
{
key: 'appointments',
name: '📅 Citas',
color: '#ef4444',
dataSchema: [
{ id: 'patientId', name: 'Paciente ID', type: 'text', required: true },
{ id: 'serviceId', name: 'Servicio ID', type: 'text', required: true },
{ id: 'locationId', name: 'Sede ID', type: 'text', required: true },
{ id: 'equipmentId', name: 'Equipo ID', type: 'text' },
{ id: 'scheduledDateTime', name: 'Fecha y Hora', type: 'datetime', required: true },
{ id: 'durationMinutes', name: 'Duración', type: 'number' },
{ id: 'status', name: 'Estado', type: 'select', options: ['REQUESTED', 'SCHEDULED', 'CHECKED_IN', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', 'NO_SHOW'] },
{ id: 'priority', name: 'Prioridad', type: 'select', options: ['ROUTINE', 'URGENT', 'STAT'] },
{ id: 'clinicalIndication', name: 'Indicación', type: 'textarea' },
{ id: 'notes', name: 'Notas', type: 'textarea' },
],
},
{
key: 'studies',
name: '🩻 Estudios',
color: '#3b82f6',
dataSchema: [
{ id: 'accessionNumber', name: 'Nº Acceso', type: 'text', required: true },
{ id: 'patientId', name: 'Paciente ID', type: 'text', required: true },
{ id: 'appointmentId', name: 'Cita ID', type: 'text' },
{ id: 'serviceId', name: 'Servicio ID', type: 'text' },
{ id: 'modality', name: 'Modalidad', type: 'select', options: ['CT', 'MR', 'US', 'XR', 'MG'] },
{ id: 'bodyPart', name: 'Región', type: 'text' },
{ id: 'studyDescription', name: 'Descripción', type: 'text' },
{ id: 'status', name: 'Estado', type: 'select', options: ['SCHEDULED', 'IN_PROGRESS', 'COMPLETED', 'PENDING_REPORT', 'REPORTED', 'SIGNED', 'DELIVERED'] },
{ id: 'priority', name: 'Prioridad', type: 'select', options: ['ROUTINE', 'URGENT', 'STAT'] },
{ id: 'performedDate', name: 'Fecha Realización', type: 'datetime' },
{ id: 'technicianId', name: 'Técnico ID', type: 'text' },
{ id: 'radiologistId', name: 'Radiólogo ID', type: 'text' },
{ id: 'reportId', name: 'Informe ID', type: 'text' },
],
},
{
key: 'reports',
name: '📝 Informes',
color: '#14b8a6',
dataSchema: [
{ id: 'studyId', name: 'Estudio ID', type: 'text', required: true },
{ id: 'patientId', name: 'Paciente ID', type: 'text', required: true },
{ id: 'radiologistId', name: 'Radiólogo ID', type: 'text', required: true },
{ id: 'templateId', name: 'Plantilla ID', type: 'text' },
{ id: 'technique', name: 'Técnica', type: 'textarea' },
{ id: 'findings', name: 'Hallazgos', type: 'textarea' },
{ id: 'impression', name: 'Impresión', type: 'textarea' },
{ id: 'recommendations', name: 'Recomendaciones', type: 'textarea' },
{ id: 'status', name: 'Estado', type: 'select', options: ['DRAFT', 'PRELIMINARY', 'FINAL', 'AMENDED'] },
{ id: 'criticalFinding', name: 'Hallazgo Crítico', type: 'boolean' },
{ id: 'signedAt', name: 'Fecha Firma', type: 'datetime' },
{ id: 'signedBy', name: 'Firmado Por', type: 'text' },
],
},
{
key: 'templates',
name: '📄 Plantillas',
color: '#a855f7',
dataSchema: [
{ id: 'code', name: 'Código', type: 'text', required: true },
{ id: 'name', name: 'Nombre', type: 'text', required: true },
{ id: 'modality', name: 'Modalidad', type: 'select', options: ['CT', 'MR', 'US', 'XR', 'MG'] },
{ id: 'bodyPart', name: 'Región', type: 'text' },
{ id: 'category', name: 'Categoría', type: 'select', options: ['NORMAL', 'PATHOLOGY', 'FOLLOW_UP', 'GENERIC'] },
{ id: 'technique', name: 'Técnica', type: 'textarea' },
{ id: 'findings', name: 'Hallazgos', type: 'textarea' },
{ id: 'impression', name: 'Impresión', type: 'textarea' },
{ id: 'isDefault', name: 'Por Defecto', type: 'boolean' },
{ id: 'active', name: 'Activo', type: 'boolean' },
],
},
{
key: 'blog',
name: '📰 Blog',
color: '#64748b',
dataSchema: [
{ id: 'title', name: 'Título', type: 'text', required: true },
{ id: 'slug', name: 'Slug', type: 'text', required: true },
{ id: 'excerpt', name: 'Extracto', type: 'textarea' },
{ id: 'content', name: 'Contenido', type: 'textarea' },
{ id: 'featuredImage', name: 'Imagen', type: 'text' },
{ id: 'category', name: 'Categoría', type: 'text' },
{ id: 'tags', name: 'Tags', type: 'text' },
{ id: 'status', name: 'Estado', type: 'select', options: ['DRAFT', 'PUBLISHED', 'ARCHIVED'] },
{ id: 'publishedAt', name: 'Fecha Publicación', type: 'datetime' },
{ id: 'authorId', name: 'Autor ID', type: 'text' },
],
},
];
export const IMAGING_CLINIC_SEED = {
// Datos iniciales de ejemplo
config: [
{ key: 'name', value: 'cimad', category: 'General' },
{ key: 'phone', value: '+376 123 456', category: 'Contacto' },
{ key: 'email', value: 'info@cimad.com', category: 'Contacto' },
],
// ... más seeds
};
Paso 2: Actualizar el Cliente API
Modificar src/lib/cadences.ts para añadir funciones CRUD:
// ============================================
// ADMIN ENDPOINTS (requieren token JWT)
// ============================================
let authToken: string | null = null;
export function setAuthToken(token: string) {
authToken = token;
}
function getAdminHeaders(): Record<string, string> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Storefront-ID': STOREFRONT_ID,
};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
return headers;
}
// Genérico para CRUD admin
async function adminFetch<T>(
endpoint: string,
options: FetchOptions = {}
): Promise<APIResponse<T>> {
const { method = 'GET', body, headers = {} } = options;
const response = await fetch(`${API_URL}${endpoint}`, {
method,
headers: { ...getAdminHeaders(), ...headers },
body: body ? JSON.stringify(body) : undefined,
});
// ... manejar respuesta
}
// ============================================
// PACIENTES
// ============================================
export async function getPatients(params?: {
search?: string;
page?: number;
pageSize?: number;
}): Promise<APIResponse<Patient[]>> {
const searchParams = new URLSearchParams();
searchParams.set('project', 'patients');
if (params?.search) searchParams.set('search', params.search);
if (params?.page) searchParams.set('page', String(params.page));
if (params?.pageSize) searchParams.set('pageSize', String(params.pageSize));
return adminFetch(`/storefront/${STOREFRONT_ID}/admin/data?${searchParams}`);
}
export async function createPatient(data: Partial<Patient>): Promise<APIResponse<Patient>> {
return adminFetch(`/storefront/${STOREFRONT_ID}/admin/data?project=patients`, {
method: 'POST',
body: data,
});
}
export async function updatePatient(id: string, data: Partial<Patient>): Promise<APIResponse<Patient>> {
return adminFetch(`/storefront/${STOREFRONT_ID}/admin/data/${id}?project=patients`, {
method: 'PUT',
body: data,
});
}
export async function deletePatient(id: string): Promise<APIResponse<void>> {
return adminFetch(`/storefront/${STOREFRONT_ID}/admin/data/${id}?project=patients`, {
method: 'DELETE',
});
}
// ============================================
// ESTUDIOS
// ============================================
export async function getStudies(params?: {
status?: string;
modality?: string;
patientId?: string;
page?: number;
}): Promise<APIResponse<Study[]>> {
const searchParams = new URLSearchParams();
searchParams.set('project', 'studies');
if (params?.status) searchParams.set('filter', JSON.stringify({ status: params.status }));
// ...
return adminFetch(`/storefront/${STOREFRONT_ID}/admin/data?${searchParams}`);
}
// ... similar para appointments, reports, etc.
Paso 3: Modificar Componentes
Ejemplo para PatientsList.tsx:
// Antes (datos demo)
import { demoPatients } from '@data/demoData';
const patients = demoPatients;
// Después (API real)
import { getPatients, createPatient } from '@lib/cadences';
const [patients, setPatients] = useState<Patient[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function load() {
const result = await getPatients({ page: 1, pageSize: 50 });
if (result.success && result.data) {
setPatients(result.data);
}
setLoading(false);
}
load();
}, []);
6. Testing
Test 1: Setup del Storefront
# Crear los proyectos DATA_TABLE
curl -X POST https://cadences.pages.dev/api/storefront/cimad/setup \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type": "imaging-clinic"}'
Test 2: Verificar Proyectos Creados
curl https://cadences.pages.dev/api/storefront/cimad/setup
Test 3: Crear Paciente
curl -X POST https://cadences.pages.dev/api/storefront/cimad/admin/data?project=patients \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mrn": "MRN-001",
"firstName": "María",
"lastName": "García",
"phone": "+34 600 123 456"
}'
Test 4: Listar Servicios (público)
curl https://cadences.pages.dev/api/storefront/cimad/data/services
7. Checklist de Entrega
Backend (Cadences)
- Schema
imaging-clinicañadido aschemas.js - Seed data de ejemplo definido
- Setup endpoint soporta
type=imaging-clinic - Endpoints públicos funcionan sin auth
- Endpoints admin requieren auth correctamente
- CORS configurado para el dominio del storefront
Frontend (Storefront)
- Variables de entorno configuradas
- Cliente
cadences.tscon todas las funciones CRUD -
PatientsList.tsxusa API real -
Worklist.tsxusa API real -
Agenda.tsxusa API real -
ReportEditor.tsxguarda informes en API - Autenticación admin implementada
- Fallback a datos demo cuando no hay conexión
Documentación
- README actualizado con instrucciones de setup
- Variables de entorno documentadas
- Ejemplos de curl para testing
📚 Archivos Clave
| Archivo | Ubicación | Descripción |
|---|---|---|
cadences.ts |
src/lib/ |
Cliente API |
storefront.config.ts |
src/config/ |
Configuración del storefront |
models.ts |
src/data/ |
Tipos TypeScript |
setup.js |
functions/api/storefront/[id]/ |
Setup endpoint |
data/[project].js |
functions/api/storefront/[id]/ |
Datos públicos |
admin/data.js |
functions/api/storefront/[id]/ |
CRUD admin |
Sprint 1 - Enero 2026