13 KiB
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
// 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
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
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
-
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:
conversationsymessages, lo cual es correcto desde el punto de vista de la base de datos. -
Campos de Resumen Faltantes: La entidad
Conversationno tiene campos paralastMessage,lastTime(oupdatedDateque podría servir para ordenar, pero no es lo mismo que la fecha del último mensaje), ni un contadorunread. 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
lastMessageylastTime. - Para CADA conversación, contar los mensajes donde el
readByno incluya al usuario actual para saber elunread. Esto es muy ineficiente (problema N+1).
-
Identificación de Participantes:
- El mock tiene
sender_idysender_name, lo que sugiere una conversación 1-a-1. - La entidad
Conversationtiene un campoparticipants(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 únicosender.
- El mock tiene
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:
# 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 campounreadCountaConversationes 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
Messagenuevo, se actualicen los camposlastMessage,lastMessageSenderIdylastMessageTimestampen laConversationpadre. 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
participantsen lugar desender_idysender_name. El nombre del otro participante en un chat 1-a-1 se puede obtener buscando el ID enparticipantsque 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
// 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
type Staff @table(name: "staffs") {
id: UUID!
employeeName: String!
# ... muchos otros campos relacionados con el trabajo
}
dataconnect/schema/user.gql
type User @table(name: "users") {
id: String! // Uid de Firebase Auth
email: String!
fullName: String!
# ...
}
Análisis y Discrepancias
-
Relación
User<->StaffFaltante: El mock contieneid(para el perfilStaff) yuser_id(para la entidadUser), lo cual es una arquitectura correcta. Sin embargo, la entidadStaffen el schema no tiene un campouserIdpara vincularlo con la tablaUser. Esta es una relación fundamental que debe existir. -
Campos de Gamificación (
xp,level): Estos campos son el núcleo de la funcionalidad del leaderboard, pero están completamente ausentes en la entidadStaff. -
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 deStaff. -
Convención de Nombres: El mock usa
photo_urlyuser_id(snake_case), mientras que la convención estándar de GraphQL y de los SDK generados por Data Connect escamelCase. La aplicación cliente debería usarphotoUrlyuserIdpara 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:
# 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:
userIddebe ser único: Se debería añadir una constraint a nivel de base de datos para asegurar que no pueda haber dos perfiles deStaffcon el mismouserId.- Alineación en la App: Recomendar al equipo de Flutter que estandarice el uso de
camelCaseen las claves de susMappara que coincidan con los nombres de campo de GraphQL (photoUrl,userId,employeeName). User.fullNamevsStaff.employeeName: El sistema debe decidir cuál es la fuente de verdad para el nombre del usuario. Generalmente,User.fullNamees el nombre legal/de registro, yStaff.employeeNamepodría ser un alias o nombre profesional. Para el leaderboard,employeeNameparece 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
// 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)
// 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
-
Entidad Faltante:
Timesheet: La entidadShiftrepresenta 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 llamarTimesheetoWorkLog, que registre las horas reales y su estado de aprobación y pago. -
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.
- Relación con el turno:
-
Datos Desnormalizados: El mock incluye
shift_title,client_nameylocationpara facilitar su visualización en la app. En el backend, estos datos no deben duplicarse en la tablaTimesheet. La nueva entidad solo debe contener elshiftId, y los datos relacionados se obtendrán mediante una query de GraphQL que una las entidadesTimesheet,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:
# 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
Timesheettendría una relación de uno-a-uno (o uno-a-muchos, si un turno puede tener varios registros de tiempo) con la entidadShift. - Automatización: La lógica para calcular
totalHoursytotalPaypodría vivir en el backend (una Cloud Function que se dispara al crear/modificar unTimesheet) para asegurar consistencia. - Alineación en la App: La app debe ser actualizada para hacer una query a
timesheetsy, a través de GraphQL, pedir los campos relacionados del turno (shift { shiftName event { client { name } } }).