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

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

  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:

# 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

// 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

  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:

# 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

// 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

  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:

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