287 lines
13 KiB
Markdown
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 } } }`).
|
|
|
|
---
|