Files
Krow-workspace/staff_app_schema_analysis.md
2025-12-26 15:14:51 -05:00

287 lines
13 KiB
Markdown

# 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 } } }`).
---