Form Service: Performance Evaluation Implementation Guide

by Viktoria Ivanova 58 views

Hey guys! Let's dive into creating a form service for performance evaluations and getting it integrated smoothly. This article will cover the service setup, filtering, DTOs, and how to use it in your mobile app. We'll keep it casual and make sure everything is crystal clear. This will also cover SEO and make it unique.

Understanding the JSON Data

Before we jump into the code, let's break down the JSON data we're working with. This part is crucial because understanding the data structure helps us design our service and DTOs effectively.

JSON Structure Overview

From the provided data, we can identify four main records. Each record represents a form related to either a "Programa de excelencia" or an "Evaluación de Desempeño" (Performance Evaluation), and it's targeted at either the "Canal Detalle" (Retail Channel) or "Canal Mayoreo" (Wholesale Channel) in El Salvador.

  1. Programa de excelencia - Canal Detalle (El Salvador)
    • Type: programa_excelencia
    • Channel: detalle
  2. Evaluación de Desempeño en Campo - Canal Mayoreo (El Salvador)
    • Type: evaluacion_desempeƱo
    • Channel: mayoreo
  3. Programa de excelencia - Canal Mayoreo (El Salvador)
    • Type: programa_excelencia
    • Channel: mayoreo
  4. Evaluación de Desempeño en Campo - Canal Detalle (El Salvador)
    • Type: evaluacion_desempeƱo
    • Channel: detalle

Summary by Type

To get a clearer picture, let's summarize the forms by type:

  • programa_excelencia: 2 forms
    • detalle
    • mayoreo
  • evaluacion_desempeƱo: 2 forms
    • detalle
    • mayoreo

For our current flow, we'll focus on the evaluacion_desempeƱo forms. These are the ones we'll use to build our service and integrate into the mobile app.

Key Fields in the JSON

Let's look at the key fields in the JSON data. This will guide our DTO (Data Transfer Object) design and filtering logic.

  • id: A unique identifier for the form (String). This is essential for referencing specific forms within our application.
  • nombre: The name of the form (String). Helps in identifying the form in a human-readable format.
  • version: The version of the form (String). Important for tracking changes and updates.
  • canal: The channel the form is designed for (detalle or mayoreo, String). This is a key field for filtering forms based on the channel.
  • tipo: The type of form (evaluacion_desempeƱo or programa_excelencia, String). We will use this to filter for performance evaluation forms.
  • descripcion: A description of the form (String). Provides additional context about the form's purpose.
  • preguntas: An array of questions in the form (Array of Objects). Each question has its own structure, which we'll dive into later.
  • resultadoKPI: An optional object for KPI results (Object, nullable). We'll handle this in a later phase, but it's good to be aware of it now.
  • pais: The country the form is applicable to (String). Useful for filtering forms based on location if needed.
  • updatedAt: A timestamp indicating when the form was last updated (Integer). We'll use this to select the most recent version of a form.
  • activa: A boolean indicating whether the form is active (Boolean). We'll only use active forms.

Diving into the preguntas Array

The preguntas array is a critical part of the form structure. Each question object contains the following key fields:

  • name: A unique name for the question (String). Used for referencing the question in the application.
  • etiqueta: The label or text of the question (String). This is what the user sees.
  • section: The section the question belongs to (String). Useful for organizing questions within the form.
  • orden: The order of the question within the section (Integer). Important for displaying questions in the correct sequence.
  • tipoEntrada: The input type for the question (radio or text, String). Determines how the question is rendered in the UI.
  • ponderacion: The weight or score of the question (Integer, nullable). Used for calculating overall scores.
  • opciones: An optional array of options for radio-type questions (Array of Objects, nullable). Each option has a valor (value) and a puntuacion (score).

Example Question Structures

Let's look at a couple of example question structures:

Radio Type Question

{
  "etiqueta": "Paso 1: Preparación de la visita (1)",
  "ponderacion": 1,
  "opciones": [{"valor":"SĆ­","puntuacion":1},{"valor":"No","puntuacion":0}],
  "name": "preparacion_visita",
  "section": "PASOS DE LA VENTA",
  "orden": 1,
  "tipoEntrada": "radio"
}

This is a radio-type question with two options: "SĆ­" (score 1) and "No" (score 0). The ponderacion is 1, meaning this question has a weight of 1 in the overall score.

Text Type Question

{
  "name": "positivo",
  "etiqueta": "Retroalimentación positiva (texto)",
  "section": "RETROALIMENTACIƓN Y RECONOCIMIENTO",
  "orden": 13,
  "tipoEntrada": "text"
}

This is a text-type question where the user can enter free-form text. There are no opciones or ponderacion in this case.

Why This Matters

Understanding the JSON structure is super important for several reasons:

  • DTO Design: We need to create DTOs that accurately represent the data structure. This makes our code type-safe and easier to work with.
  • Filtering: We need to filter the forms based on tipo, canal, and activa. Knowing the data structure helps us write efficient filtering logic.
  • Rendering: We need to render the questions dynamically based on tipoEntrada. Understanding the question structure allows us to build a flexible rendering component.

By thoroughly understanding the JSON data, we can build a robust and maintainable form service. Now, let's move on to the next part: creating the GitHub issue and outlining the service and filtering requirements.

GitHub Issue: Form Service + Filter by Type and Channel

Now, let's outline the steps to create the service and filter forms. This will help us stay organized and ensure we cover all the necessary points.

Issue Title

🧩 Form Service (Performance Evaluation) + Basic Integration

Endpoint Base

.../planes_formularios/

Authentication

Bearer token (same scheme as the rest of the app)

Environment

Don't hardcode the URL! Get the baseUrl from shared/configuracion/ambiente_config.dart.

Objective

Create a service that queries the forms endpoint and returns exactly one form, filtered by:

  • tipo = evaluacion_desempeƱo
  • canal = detalle | mayoreo (based on a combo box in the view)

The returned form will be used for dynamic rendering in mobile/vistas/evaluacion_desempeƱo/evaluacion_desempeƱo_llenado.dart.

Tasks

  1. New FormulariosService in lib/shared/servicios/:
    • Construct the URL using AmbienteConfig: GET {baseUrl}/planes_formularios/
    • Include the Bearer token in the headers.
    • Filter client-side by tipo and canal.
    • If there are multiple matches, choose the most recent (updatedAt) and activa == true.
  2. DTOs in lib/shared/modelos/:
    • FormularioDTO { id, nombre, version, canal, tipo, descripcion, preguntas, resultadoKPI?, pais, updatedAt, activa }
    • PreguntaDTO { name, etiqueta, section, orden, tipoEntrada, ponderacion?, opciones? }
  3. Public Service Signature:
class FormulariosService {
  Future<FormularioDTO> obtenerFormulario({
    required String bearerToken,
    required String tipo,   // 'evaluacion_desempeƱo'
    required String canal,  // 'detalle' | 'mayoreo'
    String? pais,           // optional if filtering by country is needed
    bool forceRefresh = false,
  });
}
  1. (Optional, Recommended) Cache in Hive:
    • Use a key like form:{tipo}:{canal}:{pais?:*}:{version} with a short TTL.
    • Invalidate cache with forceRefresh.
  2. Minimal Integration in evaluacion_desempeƱo_llenado.dart:
    • Receive the channel from the main screen.
    • Call FormulariosService.obtenerFormulario(tipo:'evaluacion_desempeƱo', canal: canalSel, bearerToken: token).
    • Pass the FormularioDTO to the current renderer/builder.
  3. Initial Type Mapping:
    • radio → Options: Yes/No with weighting (use opciones).
    • text → TextFormField with placeholder + limit 300 (rule already defined).

Acceptance Criteria (QA)

  • The service doesn't have hardcoded URLs; it uses AmbienteConfig.
  • The call is made with a Bearer token and handles network errors (timeout, 4xx/5xx).
  • For canal=Detalle, it returns the form ā€œEvaluación de DesempeƱo en Campo - Canal Detalle (El Salvador)ā€.
  • For canal=Mayoreo, it returns the corresponding form for wholesale.
  • If there are no active matches, it returns a controlled error with a clear message.
  • In evaluacion_desempeƱo_llenado.dart, the questions from the returned form are rendered, respecting tipoEntrada.

Snippets Guide (Interface Only; Implementation by Claude)

Service

class FormulariosService {
  final AmbienteConfig _env;
  final HttpClientLike _http;
  FormulariosService(this._env, this._http);

  Future<FormularioDTO> obtenerFormulario({
    required String bearerToken,
    required String tipo,
    required String canal,
    String? pais,
    bool forceRefresh = false,
  }) async {
    final url = Uri.parse('${_env.apiBaseUrl}/planes_formularios/');
    final resp = await _http.get(url, headers: {
      'Authorization': 'Bearer $bearerToken',
      'Content-Type': 'application/json',
    });
    final List data = jsonDecode(resp.body) as List;
    final candidatos = data.where((m) =>
      (m['tipo']?.toString().toLowerCase() == tipo.toLowerCase()) &&
      (m['canal']?.toString().toLowerCase() == canal.toLowerCase()) &&
      (m['activa'] == true)
    );

    final seleccionado = candidatos
      .sorted((a,b) => (b['updatedAt'] ?? 0).compareTo(a['updatedAt'] ?? 0))
      .firstOrNull;

    if (seleccionado == null) {
      throw StateError('No se encontró formulario activo para tipo=$tipo canal=$canal');
    }
    return FormularioDTO.fromJson(seleccionado);
  }
}

Usage in evaluacion_desempeƱo_llenado.dart

final form = await _formulariosSvc.obtenerFormulario(
  bearerToken: token,
  tipo: 'evaluacion_desempeƱo',
  canal: canalSeleccionado, // 'detalle' | 'mayoreo'
);

// Dynamic rendering
FormRenderer(schema: form); // where schema uses form.preguntas

Note

For text, apply the previously agreed-upon validation (max. 300 characters).

Prompt for Claude Code – Form Service + Integration in ā€œPerformance Evaluation (Fill)ā€

Objective

Implement a service that queries the forms endpoint and returns the active form filtered by tipo = evaluacion_desempeƱo and canal = detalle|mayoreo, and use it for dynamic rendering in evaluacion_desempeƱo_llenado.dart.

Rules and Restrictions

  • Don't hardcode URLs. Take the base URL from shared/configuracion/ambiente_config.dart.
  • Auth: Bearer token (same pattern as existing services).
  • If there are multiple matches, choose the active one (activa == true) with the highest updatedAt.
  • Handle network/parsing errors with visible messages and an option to retry.

Files to Create/Modify

  • New Service:

    • lib/shared/servicios/formularios_service.dart
  • DTOs (Models):

    • lib/shared/modelos/formulario_dto.dart
    • lib/shared/modelos/pregunta_dto.dart (or another name consistent with the project)
  • View Integration (Fill):

    • package:diana_lc_front/mobile/vistas/evaluacion_desempeƱo/evaluacion_desempeƱo_llenado.dart

    The main screen already passes the selected channel. In the fill view, use it to request the form.

Contracts / Expected Signatures

Service
class FormulariosService {
  final AmbienteConfig _env;         // Inject the environment config
  final HttpClientLike _httpClient;  // Use the existing client from the project

  FormulariosService(this._env, this._httpClient);

  Future<FormularioDTO> obtenerFormulario({
    required String bearerToken,
    required String tipo,   // 'evaluacion_desempeƱo'
    required String canal,  // 'detalle' | 'mayoreo'
    String? pais,           // optional in this phase
    bool forceRefresh = false,
  });
}
Minimum DTOs
class FormularioDTO {
  final String id;
  final String nombre;
  final String version;
  final String canal;   // 'detalle' | 'mayoreo'
  final String tipo;    // 'evaluacion_desempeƱo'
  final String? descripcion;
  final bool activa;
  final int? updatedAt;
  final String? pais;
  final List<PreguntaDTO> preguntas;
  final Map<String, dynamic>? resultadoKPI;

  // fromJson / toJson
}

class PreguntaDTO {
  final String name;
  final String etiqueta;
  final String section;
  final int orden;
  final String tipoEntrada; // 'radio' | 'text' | ...
  final int? ponderacion;
  final List<OpcionDTO>? opciones;

  // fromJson / toJson
}

class OpcionDTO {
  final String valor;
  final num? puntuacion;
  // fromJson / toJson
}

Requested Implementation

A) FormulariosService
  • Construct URL: ${_env.apiBaseUrl}/planes_formularios/
  • GET with Headers:
    • Authorization: Bearer {token}
    • Content-Type: application/json
  • Parse the returned list.
  • Filter:
    • registro['tipo'] == 'evaluacion_desempeƱo'
    • registro['canal'] matches canal (case-insensitive).
    • registro['activa'] == true
  • Select the one with the highest updatedAt (or createdAt if missing).
  • Map to FormularioDTO (and its preguntaDTO/opcionDTO).
B) Integration in evaluacion_desempeƱo_llenado.dart
  • Receive via ModalRoute.of(context)!.settings.arguments:

    • canal ('Detalle' | 'Mayoreo'). Normalize to lower: 'detalle'|'mayoreo'.
    • (In addition to liderId, asesorId, etc., which are already passed).
  • Get the current token (same method as the rest of the app).

  • Call:

    final form = await _formulariosService.obtenerFormulario(
      bearerToken: token,
      tipo: 'evaluacion_desempeƱo',
      canal: canalNormalizado,
    );
    
  • Dynamically render all questions from form.preguntas:

    • radio → construct options using opciones[].valor and, if applicable, preserve puntuacion.
    • text → TextFormField with placeholder ā€œ(max. 300 characters)ā€ and limit 300 (maxLength, MaxLengthEnforcement.enforced, LengthLimitingTextInputFormatter(300)), as previously agreed upon.
  • Loading + Error:

    • While loading → CircularProgress / skeleton.
    • Error → card with message and Retry button that triggers the load again.

Snippets Guide (You can adjust to the project architecture)

Filter and Selection

final List<dynamic> data = jsonDecode(resp.body) as List<dynamic>;
final candidatos = data.where((m) =>
  (m['tipo']?.toString().toLowerCase() == tipo.toLowerCase()) &&
  (m['canal']?.toString().toLowerCase() == canal.toLowerCase()) &&
  (m['activa'] == true)
);

if (candidatos.isEmpty()) {
  throw StateError('No hay formulario activo para tipo=$tipo canal=$canal');
}

candidatos.toList().sort((a, b) {
  final ua = (a['updatedAt'] ?? a['createdAt'] ?? 0) as int;
  final ub = (b['updatedAt'] ?? b['createdAt'] ?? 0) as int;
  return ub.compareTo(ua); // desc
});

final seleccionado = candidatos.first;
return FormularioDTO.fromJson(seleccionado as Map<String, dynamic>);

Usage in the View (Summary)

late Future<FormularioDTO> _futureForm;

@override
void initState() {
  super.initState();
  final args = ModalRoute.of(context)!.settings.arguments as Map?;
  final canalArg = (args?['canal'] as String?) ?? 'Detalle';
  final canal = canalArg.toLowerCase(); // detalle | mayoreo
  _futureForm = _formulariosService.obtenerFormulario(
    bearerToken: tokenActual,
    tipo: 'evaluacion_desempeƱo',
    canal: canal,
  );
}

@override
Widget build(BuildContext context) {
  return FutureBuilder<FormularioDTO>(
    future: _futureForm,
    builder: (context, snap) {
      if (snap.connectionState != ConnectionState.done) {
        return const Center(child: CircularProgressIndicator());
      }
      if (snap.hasError) {
        return ErrorCard(
          message: 'No fue posible cargar el formulario',
          onRetry: () => setState(() {}),
        );
      }
      final form = snap.data!;
      return FormRenderer(schema: form); // use your current renderer/builder
    },
  );
}

Tests / QA (Acceptance Criteria)

  • The service constructs the URL with AmbienteConfig (no hardcoding).
  • The request uses a Bearer token and handles 4xx/5xx errors with a message and retry.
  • With canal = Detalle, it returns the form ā€œEvaluación de DesempeƱo en Campo - Canal Detalle ā€¦ā€.
  • With canal = Mayoreo, it returns the corresponding form for wholesale.
  • If there is more than one, the most recent (updatedAt) is used.
  • In evaluacion_desempeƱo_llenado.dart, all questions are rendered:
    • radio with its options.
    • text with placeholder and limit 300 (typing and pasting).
  • The screen shows loading; on error, it shows a message and a retry button.

Note

Next phase (don't implement yet): we'll connect saving responses (autosave/draft/finished) and KPI calculation using resultadoKPI from the form when applicable.

🧠 Rules for Selection (Source of Truth = API)

To ensure our service works correctly, we need to follow specific rules when selecting forms from the API response. These rules are crucial for filtering and choosing the right form.

  1. Type: The tipo field must be exactly evaluacion_desempeƱo. This ensures we are only fetching performance evaluation forms.
  2. Channel: The canal field should be either detalle (retail) or mayoreo (wholesale). This value will come from the UI, so we need to compare it case-insensitive to handle variations in input.
  3. Active: The activa field must be true. We only want to use forms that are currently active.
  4. Multiple Matches: If we find multiple forms that match the criteria, we need to select the most recent one. This means choosing the form with the highest updatedAt value. If updatedAt is not available, we can fallback to using createdAt.
  5. URL Base: The base URL for the API endpoint should be taken from AmbienteConfig. This ensures we are using the correct environment (e.g., development, staging, production) and avoids hardcoding URLs.
  6. Endpoint: The specific endpoint we are targeting is /planes_formularios/.
  7. Authentication: We need to include a Bearer token in the request headers for authentication. This follows the same pattern used by other services in the app.

šŸŽÆ Why Include This Snippet

Including a detailed snippet like this in the issue is beneficial for several reasons:

  • Contract Definition: It provides Claude (or any developer) with a clear contract for the data structure, including field names, types, and nesting. This eliminates ambiguity and ensures everyone is on the same page.
  • DTO Design: The snippet helps in designing DTOs (Data Transfer Objects) that accurately represent the data. This makes the code type-safe and easier to maintain.
  • Mapping and Validations: With a clear understanding of the data structure, it's easier to design mapping logic and validation rules.
  • Lightweight Issue: By using <details> to hide the full JSON sample, we keep the issue lightweight and easy to navigate. The relevant information is still available, but it doesn't clutter the main issue description.

By providing these rules and snippets, we set a solid foundation for implementing the form service. This approach minimizes misunderstandings and helps ensure a smooth development process.

Creating the FormulariosService

Alright, let's get into the nitty-gritty of building the FormulariosService. This service will be responsible for fetching, filtering, and returning the correct form based on our criteria.

Service Structure

First, let's outline the structure of our service. We'll need to inject the AmbienteConfig and an HTTP client. This allows us to construct the URL dynamically and make the API request.

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../modelos/formulario_dto.dart';
import '../configuracion/ambiente_config.dart';

class FormulariosService {
  final AmbienteConfig _env;
  final http.Client _httpClient;

  FormulariosService(this._env, this._httpClient);

  Future<FormularioDTO> obtenerFormulario({
    required String bearerToken,
    required String tipo,
    required String canal,
    String? pais,
    bool forceRefresh = false,
  }) async {
    // Implementation here
  }
}

Constructing the URL

The first step in our obtenerFormulario method is to construct the URL. We'll use the baseUrl from AmbienteConfig and append the /planes_formularios/ endpoint.

final url = Uri.parse('${_env.apiBaseUrl}/planes_formularios/');

Making the API Request

Next, we'll make a GET request to the API endpoint. We need to include the Bearer token in the Authorization header and set the Content-Type to application/json.

final response = await _httpClient.get(
  url,
  headers: {
    'Authorization': 'Bearer $bearerToken',
    'Content-Type': 'application/json',
  },
);

if (response.statusCode < 200 || response.statusCode >= 300) {
  throw Exception('Failed to load form: ${response.statusCode}');
}

Parsing the Response

Once we have the response, we need to parse the JSON data. We'll use jsonDecode to convert the response body into a list of dynamic objects.

final List<dynamic> data = jsonDecode(response.body) as List<dynamic>;

Filtering the Forms

Now comes the crucial part: filtering the forms based on our rules. We need to filter by tipo, canal, and activa.

final candidatos = data.where((m) =>
    (m['tipo']?.toString().toLowerCase() == tipo.toLowerCase()) &&
    (m['canal']?.toString().toLowerCase() == canal.toLowerCase()) &&
    (m['activa'] == true)).toList();

This code snippet filters the list based on the following conditions:

  • m['tipo'] must be equal to the provided tipo (case-insensitive).
  • m['canal'] must be equal to the provided canal (case-insensitive).
  • m['activa'] must be true.

Selecting the Most Recent Form

If we have multiple matching forms, we need to select the most recent one. We'll sort the forms by updatedAt in descending order and take the first element.

candidatos.sort((a, b) {
  final ua = (a['updatedAt'] ?? a['createdAt'] ?? 0) as int;
  final ub = (b['updatedAt'] ?? b['createdAt'] ?? 0) as int;
  return ub.compareTo(ua); // Descending order
});

final seleccionado = candidatos.isNotEmpty ? candidatos.first : null;

Handling No Matching Forms

If we don't find any matching forms, we should throw an error with a clear message.

if (seleccionado == null) {
  throw StateError('No active form found for type=$tipo and channel=$canal');
}

Mapping to FormularioDTO

Finally, we need to map the selected form data to our FormularioDTO. This involves creating an instance of FormularioDTO and populating it with the data from the JSON object.

return FormularioDTO.fromJson(seleccionado as Map<String, dynamic>);

Complete obtenerFormulario Method

Here's the complete obtenerFormulario method:

Future<FormularioDTO> obtenerFormulario({
  required String bearerToken,
  required String tipo,
  required String canal,
  String? pais,
  bool forceRefresh = false,
}) async {
  final url = Uri.parse('${_env.apiBaseUrl}/planes_formularios/');
  final response = await _httpClient.get(
    url,
    headers: {
      'Authorization': 'Bearer $bearerToken',
      'Content-Type': 'application/json',
    },
  );

  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw Exception('Failed to load form: ${response.statusCode}');
  }

  final List<dynamic> data = jsonDecode(response.body) as List<dynamic>;
  final candidatos = data.where((m) =>
      (m['tipo']?.toString().toLowerCase() == tipo.toLowerCase()) &&
      (m['canal']?.toString().toLowerCase() == canal.toLowerCase()) &&
      (m['activa'] == true)).toList();

  candidatos.sort((a, b) {
    final ua = (a['updatedAt'] ?? a['createdAt'] ?? 0) as int;
    final ub = (b['updatedAt'] ?? b['createdAt'] ?? 0) as int;
    return ub.compareTo(ua); // Descending order
  });

  final seleccionado = candidatos.isNotEmpty ? candidatos.first : null;

  if (seleccionado == null) {
    throw StateError('No active form found for type=$tipo and channel=$canal');
  }

  return FormularioDTO.fromJson(seleccionado as Map<String, dynamic>);
}

Caching (Optional)

For better performance, we can implement caching. We'll use Hive to cache the forms with a short TTL (Time-To-Live). The cache key will be based on tipo, canal, pais, and version.

Integration in evaluacion_desempeƱo_llenado.dart

Now that we have our FormulariosService, let's see how we can integrate it into evaluacion_desempeƱo_llenado.dart. This is where we'll use the service to fetch the form and render it dynamically.

Receiving the Channel

First, we need to receive the channel from the main screen. We can do this by accessing the ModalRoute arguments.

final args = ModalRoute.of(context)!.settings.arguments as Map?;
final canalArg = (args?['canal'] as String?) ?? 'Detalle';
final canal = canalArg.toLowerCase(); // Normalize to 'detalle' or 'mayoreo'

Fetching the Token

Next, we need to get the current token. This will depend on how your app manages authentication. Assuming you have a method to get the token, you can call it here.

final token = await getToken(); // Replace with your actual token retrieval method

Calling the Service

Now, we can call our FormulariosService to fetch the form. We'll pass the bearerToken, tipo, and canal.

final form = await _formulariosService.obtenerFormulario(
  bearerToken: token,
  tipo: 'evaluacion_desempeƱo',
  canal: canal,
);

Rendering the Form

Once we have the form, we can render it dynamically. This involves iterating over the form.preguntas list and creating widgets based on the tipoEntrada.

FormRenderer(schema: form);

Handling Loading and Errors

It's essential to handle loading and error states. While the form is loading, we can show a CircularProgressIndicator. If there's an error, we can display an error message with a retry button.

late Future<FormularioDTO> _futureForm;

@override
void initState() {
  super.initState();
  final args = ModalRoute.of(context)!.settings.arguments as Map?;
  final canalArg = (args?['canal'] as String?) ?? 'Detalle';
  final canal = canalArg.toLowerCase(); // detalle | mayoreo
  _futureForm = _formulariosService.obtenerFormulario(
    bearerToken: tokenActual,
    tipo: 'evaluacion_desempeƱo',
    canal: canal,
  );
}

@override
Widget build(BuildContext context) {
  return FutureBuilder<FormularioDTO>(
    future: _futureForm,
    builder: (context, snap) {
      if (snap.connectionState != ConnectionState.done) {
        return const Center(child: CircularProgressIndicator());
      }
      if (snap.hasError) {
        return ErrorCard(
          message: 'Failed to load form',
          onRetry: () => setState(() {}),
        );
      }
      final form = snap.data!;
      return FormRenderer(schema: form); // Use your form renderer
    },
  );
}

Mapping Input Types

We need to map the tipoEntrada values to the appropriate widgets.

  • radio → Construct options using opciones[].valor and preserve puntuacion.
  • text → TextFormField with placeholder ā€œ(max. 300 characters)ā€ and limit 300.

Acceptance Criteria and QA

To ensure our service and integration meet the requirements, we need to verify the following:

  1. The service constructs the URL using AmbienteConfig (no hardcoding).
  2. The request uses a Bearer token and handles 4xx/5xx errors with a message and retry.
  3. With canal = Detalle, it returns the correct form.
  4. With canal = Mayoreo, it returns the correct form.
  5. If there is more than one matching form, the most recent one (updatedAt) is used.
  6. In evaluacion_desempeƱo_llenado.dart, all questions are rendered correctly:
    • radio questions with their options.
    • text questions with the placeholder and 300-character limit.
  7. The screen displays a loading indicator while the form is being fetched.
  8. If there's an error, a message and retry button are displayed.

SEO Optimization

To optimize this article for SEO, we've included relevant keywords throughout the text, such as "form service," "performance evaluation," "DTOs," "API integration," and "dynamic rendering." We've also used headings and subheadings to structure the content logically, making it easier for search engines to understand the article's main points.

Conclusion

Creating a form service and integrating it into a mobile app can seem daunting, but by breaking it down into smaller tasks, it becomes much more manageable. We've covered everything from understanding the JSON data to building the service, integrating it into the view, and handling loading and error states. Remember, clear communication and a solid understanding of the requirements are key to success. Keep coding, and you'll nail it!