# Análisis de Mocks de Staff App vs. Schema de Data Connect Este documento analiza las estructuras de datos "mock" encontradas en el código de la aplicación de Staff (Flutter) y las compara con los schemas GraphQL de Data Connect. El objetivo es identificar discrepancias y proponer cambios para alinear el backend con las necesidades del frontend. --- ## 1. Conversaciones y Mensajes (`messages_screen.dart`) El mock `_conversations` representa la lista de chats de un usuario. Se compara con las entidades `Conversation` y `Message` del schema. ### Estructura del Mock ```dart // Estructura de un elemento en la lista `_conversations` { 'sender_id': String, 'sender_name': String, 'lastMessage': String, 'lastTime': DateTime, 'unread': int, 'messages': [ { 'content': String, 'sender_id': String, } ] } ``` ### Entidades de Data Connect **`dataconnect/schema/conversation.gql`** ```graphql type Conversation @table(name: "conversations") { id: UUID! participants: String! // Se espera un array de IDs de usuario en formato JSON string conversationType: ConversationType! // ... otros campos } ``` **`dataconnect/schema/message.gql`** ```graphql type Message @table(name: "messages") { id: UUID! conversationId: UUID! senderName: String! content: String! readBy: String // Se espera un array de IDs de usuario que han leído el mensaje // ... otros campos } ``` ### Análisis y Discrepancias 1. **Modelo de Datos**: El mock está diseñado para una vista de "bandeja de entrada", donde cada item es una conversación y muestra un resumen (último mensaje, hora, contador de no leídos). El schema de Data Connect, en cambio, normaliza los datos en dos tablas: `conversations` y `messages`, lo cual es correcto desde el punto de vista de la base de datos. 2. **Campos de Resumen Faltantes**: La entidad `Conversation` no tiene campos para `lastMessage`, `lastTime` (o `updatedDate` que podría servir para ordenar, pero no es lo mismo que la fecha del último mensaje), ni un contador `unread`. Para construir la pantalla de la app, el cliente tendría que: * Obtener todas las conversaciones. * Para CADA conversación, obtener el último mensaje para mostrar `lastMessage` y `lastTime`. * Para CADA conversación, contar los mensajes donde el `readBy` no incluya al usuario actual para saber el `unread`. Esto es muy ineficiente (problema N+1). 3. **Identificación de Participantes**: * El mock tiene `sender_id` y `sender_name`, lo que sugiere una conversación 1-a-1. * La entidad `Conversation` tiene un campo `participants` (un JSON string de un array de IDs), que es más flexible y soporta chats grupales. El frontend deberá adaptar su lógica para manejar una lista de participantes en lugar de un único `sender`. ### Recomendaciones Para optimizar la carga de datos y alinear el backend con las necesidades de la UI, recomiendo **desnormalizar** la entidad `Conversation` añadiendo campos de resumen. **Archivo a modificar**: `dataconnect/schema/conversation.gql` **Cambios sugeridos**: ```graphql # EN: dataconnect/schema/conversation.gql type Conversation @table(name: "conversations") { id: UUID! @default(expr: "uuidV4()") participants: String! # participants (jsonb -> String, required array of strings) conversationType: ConversationType! relatedTo: UUID! # related_to (generic FK as string) status: ConversationStatus createdDate: Timestamp @default(expr: "request.time") updatedDate: Timestamp @default(expr: "request.time") createdBy: String @default(expr: "auth.uid") # --- CAMPOS AÑADIDOS PARA RESUMEN --- lastMessage: String # Contenido del último mensaje lastMessageSenderId: String # ID del remitente del último mensaje lastMessageTimestamp: Timestamp # Fecha y hora del último mensaje } ``` **Consideraciones Adicionales**: * **Contador de no leídos (`unread`)**: Añadir un campo `unreadCount` a `Conversation` es complejo porque su valor depende de *quién* está mirando la conversación. Una mejor estrategia sería manejar esto en el lado del cliente o con una query más específica si el rendimiento lo permite. Por ahora, los campos de resumen del último mensaje son la mejora más crítica. * **Lógica de Actualización**: Será necesario implementar una lógica (probablemente en una Cloud Function o en la mutación que crea mensajes) para que cada vez que se añada un `Message` nuevo, se actualicen los campos `lastMessage`, `lastMessageSenderId` y `lastMessageTimestamp` en la `Conversation` padre. Data Connect por sí solo no manejará esta lógica de desnormalización. * **Alineación en la App**: La app de Flutter deberá cambiar su mock y su lógica para usar `participants` en lugar de `sender_id` y `sender_name`. El nombre del otro participante en un chat 1-a-1 se puede obtener buscando el ID en `participants` que no sea el del usuario actual. --- ## 2. Perfil de Staff y Leaderboard (`leaderboard_screen.dart`) El mock `_profiles` se usa para mostrar la tabla de clasificación y parece ser un subconjunto de un perfil de trabajador (staff). Se compara con las entidades `Staff` y `User`. ### Estructura del Mock ```dart // Estructura de un elemento en la lista `_profiles` { 'id': String, // ID del perfil del trabajador (Staff) 'name': String, 'photo_url': String?, 'xp': int, // Puntos de experiencia 'level': String, // Nivel de gamificación, ej: 'Krower III' 'user_id': String, // ID de la entidad User (auth) } ``` ### Entidades de Data Connect **`dataconnect/schema/staff.gql`** ```graphql type Staff @table(name: "staffs") { id: UUID! employeeName: String! # ... muchos otros campos relacionados con el trabajo } ``` **`dataconnect/schema/user.gql`** ```graphql type User @table(name: "users") { id: String! // Uid de Firebase Auth email: String! fullName: String! # ... } ``` ### Análisis y Discrepancias 1. **Relación `User` <-> `Staff` Faltante**: El mock contiene `id` (para el perfil `Staff`) y `user_id` (para la entidad `User`), lo cual es una arquitectura correcta. Sin embargo, la entidad `Staff` en el schema **no tiene un campo `userId`** para vincularlo con la tabla `User`. Esta es una relación fundamental que debe existir. 2. **Campos de Gamificación (`xp`, `level`)**: Estos campos son el núcleo de la funcionalidad del leaderboard, pero están completamente ausentes en la entidad `Staff`. 3. **Foto de Perfil (`photo_url`)**: Un campo para la URL de la foto de perfil es estándar en cualquier sistema con perfiles de usuario y falta en el schema de `Staff`. 4. **Convención de Nombres**: El mock usa `photo_url` y `user_id` (snake_case), mientras que la convención estándar de GraphQL y de los SDK generados por Data Connect es `camelCase`. La aplicación cliente debería usar `photoUrl` y `userId` para evitar inconsistencias. ### Recomendaciones Para soportar la funcionalidad del leaderboard y establecer una arquitectura de datos robusta, se deben hacer los siguientes cambios. **Archivo a modificar**: `dataconnect/schema/staff.gql` **Cambios sugeridos**: ```graphql # EN: dataconnect/schema/staff.gql type Staff @table(name: "staffs") { id: UUID! @default(expr: "uuidV4()") userId: String! # --- AÑADIDO: Vínculo a la tabla User (FK a User.id) employeeName: String! @col(name: "employee_name") # ... (campos existentes) # --- CAMPOS AÑADIDOS PARA PERFIL Y GAMIFICACIÓN --- photoUrl: String # URL de la foto de perfil xp: Int @default(expr: "0") # Puntos de experiencia, con valor inicial de 0 level: String # Nivel de gamificación (ej: "Beginner", "Krower I") # ... (resto de campos existentes) createdDate: Timestamp @default(expr: "request.time") updatedDate: Timestamp @default(expr: "request.time") createdBy: String @default(expr: "auth.uid") } ``` **Consideraciones Adicionales**: * **`userId` debe ser único**: Se debería añadir una constraint a nivel de base de datos para asegurar que no pueda haber dos perfiles de `Staff` con el mismo `userId`. * **Alineación en la App**: Recomendar al equipo de Flutter que estandarice el uso de `camelCase` en las claves de sus `Map` para que coincidan con los nombres de campo de GraphQL (`photoUrl`, `userId`, `employeeName`). * **`User.fullName` vs `Staff.employeeName`**: El sistema debe decidir cuál es la fuente de verdad para el nombre del usuario. Generalmente, `User.fullName` es el nombre legal/de registro, y `Staff.employeeName` podría ser un alias o nombre profesional. Para el leaderboard, `employeeName` parece más apropiado. --- ## 3. Hojas de Tiempo (Timesheets) y Pagos (`time_card_screen.dart`) El mock `_timesheets` representa los registros de horas trabajadas que son la base para el cálculo de pagos. Esto es un concepto distinto al de un `Shift` (turno programado). ### Estructura del Mock ```dart // Estructura de un elemento en la lista `_timesheets` { 'id': String, 'shift_id': String, 'date': String, 'actual_start': String, // Hora de entrada real 'actual_end': String, // Hora de salida real 'total_hours': double, 'hourly_rate': double, 'total_pay': double, 'status': String, // ej: 'pending', 'approved', 'paid' 'shift_title': String, // Dato desnormalizado 'client_name': String, // Dato desnormalizado 'location': String, // Dato desnormalizado } ``` ### Entidad de Data Connect (`shift.gql`) ```graphql // dataconnect/schema/shift.gql type Shift @table(name: "shifts") { id: UUID! shiftName: String! startDate: Timestamp! // Hora de inicio programada endDate: Timestamp // Hora de fin programada assignedStaff: String # ... } ``` ### Análisis y Discrepancias 1. **Entidad Faltante: `Timesheet`**: La entidad `Shift` representa un evento *programado*. El mock `_timesheets`, sin embargo, describe el trabajo *realizado*. Es una práctica estándar separar estos dos conceptos. Se necesita una nueva entidad, que podríamos llamar `Timesheet` o `WorkLog`, que registre las horas reales y su estado de aprobación y pago. 2. **Campos del Mock**: El mock contiene todos los campos necesarios para la nueva entidad: * Relación con el turno: `shift_id`. * Tiempos reales: `actual_start`, `actual_end`. * Cálculos de pago: `total_hours`, `hourly_rate`, `total_pay`. * Ciclo de vida: `status`. 3. **Datos Desnormalizados**: El mock incluye `shift_title`, `client_name` y `location` para facilitar su visualización en la app. En el backend, estos datos no deben duplicarse en la tabla `Timesheet`. La nueva entidad solo debe contener el `shiftId`, y los datos relacionados se obtendrán mediante una query de GraphQL que una las entidades `Timesheet`, `Shift`, `Event`, `Client`, etc. ### Recomendaciones La recomendación principal es **crear una nueva entidad `Timesheet`**. **1. Crear nuevo archivo**: `dataconnect/schema/timesheet.gql` **2. Añadir el siguiente schema**: ```graphql # NUEVO ARCHIVO: dataconnect/schema/timesheet.gql enum TimesheetStatus { PENDING # El trabajador lo ha enviado, pendiente de aprobación APPROVED # Aprobado por un manager, listo para ser pagado DISPUTED # El trabajador o manager ha marcado una discrepancia PAID # Incluido en una nómina y pagado } type Timesheet @table(name: "timesheets") { id: UUID! @default(expr: "uuidV4()") shiftId: UUID! # FK a la tabla Shift staffId: UUID! # FK a la tabla Staff actualStart: Timestamp # Hora de entrada real registrada actualEnd: Timestamp # Hora de salida real registrada totalHours: Float # Horas totales calculadas hourlyRate: Float # Tarifa por hora (puede copiarla del turno por si cambia) totalPay: Float # Pago total calculado (totalHours * hourlyRate) status: TimesheetStatus! @default(expr: "'PENDING'") notes: String # Notas del trabajador o del manager createdDate: Timestamp @default(expr: "request.time") updatedDate: Timestamp @default(expr: "request.time") createdBy: String @default(expr: "auth.uid") } ``` **Consideraciones Adicionales**: * **Relación**: La entidad `Timesheet` tendría una relación de uno-a-uno (o uno-a-muchos, si un turno puede tener varios registros de tiempo) con la entidad `Shift`. * **Automatización**: La lógica para calcular `totalHours` y `totalPay` podría vivir en el backend (una Cloud Function que se dispara al crear/modificar un `Timesheet`) para asegurar consistencia. * **Alineación en la App**: La app debe ser actualizada para hacer una query a `timesheets` y, a través de GraphQL, pedir los campos relacionados del turno (`shift { shiftName event { client { name } } }`). ---