Files
dockerfluxodecaixapro/rules/19_INTEGRACAO_GLOBALLISTS.md
2026-09-01 10:46:29 -03:00

9.2 KiB
Executable File

Integração com Módulo GlobalLists (Endereços)

Todos os módulos que utilizam formulários com campos de endereço (País, Estado, Cidade) DEVEM obrigatoriamente utilizar as funcionalidades do módulo globallists para padronização e consistência dos dados.

Esta regra visa eliminar a duplicação de tabelas de localidades e garantir uma experiência unificada de seleção de endereços em todo o sistema.

1. Banco de Dados

Migração

As tabelas do módulo devem possuir as colunas _country_id, _state_id e _city_id (prefixadas com o identificador da tabela do módulo) configuradas como unsignedInteger e nullable.

É obrigatório o uso de chaves estrangeiras (Foreign Keys) apontando para as tabelas lists_countries, lists_states e lists_cities.

Exemplo (supondo um módulo com prefixo events):

// Adição das colunas
$table->unsignedInteger('events_country_id')->nullable();
$table->unsignedInteger('events_state_id')->nullable();
$table->unsignedInteger('events_city_id')->nullable();

// Definição das Foreign Keys
$table->foreign('events_country_id')->references('countries_id')->on('lists_countries')->onDelete('set null');
$table->foreign('events_state_id')->references('states_id')->on('lists_states')->onDelete('set null');
$table->foreign('events_city_id')->references('cities_id')->on('lists_cities')->onDelete('set null');

2. Model

No Model do seu módulo, adicione os relacionamentos belongsTo e inclua as novas colunas no array $fillable.

use Modules\GlobalLists\Models\Country;
use Modules\GlobalLists\Models\State;
use Modules\GlobalLists\Models\City;

class Event extends Model
{
    protected $fillable = [
        // ... outros campos
        'events_country_id',
        'events_state_id',
        'events_city_id',
    ];

    public function country()
    {
        return $this->belongsTo(Country::class, 'events_country_id', 'countries_id');
    }

    public function state()
    {
        return $this->belongsTo(State::class, 'events_state_id', 'states_id');
    }

    public function city()
    {
        return $this->belongsTo(City::class, 'events_city_id', 'cities_id');
    }
}

3. Controller

Nos métodos responsáveis por exibir o formulário (create, edit), busque a lista completa de países. IMPORTANTE: Use o método sortBy com o acessor name para garantir que os países sejam listados em ordem alfabética de acordo com o idioma do sistema (o campo countries_name é um JSON).

use Modules\GlobalLists\Models\Country;
use Modules\GlobalLists\Models\State;
use Modules\GlobalLists\Models\City;

public function create(): Response
{
    // Ordenação correta por idioma
    $countries = Country::all()->sortBy(function ($country) {
        return $country->name;
    });
    
    return $this->renderAdminView('@seumodulo/form.twig', [
        'countries' => $countries,
        // ... outros dados
    ]);
}

public function edit($id): Response
{
    $item = SeuModelo::find($id);
    
    $countries = Country::all()->sortBy(function ($country) {
        return $country->name;
    });

    // Pre-carregar estados e cidades se já houver seleção
    $states = [];
    $cities = [];
    if ($item->prefixo_country_id) {
        $states = State::where('states_country_id', $item->prefixo_country_id)->orderBy('states_name')->get();
    }
    if ($item->prefixo_state_id) {
        $cities = City::where('cities_state_id', $item->prefixo_state_id)->orderBy('cities_name')->get();
    }

    return $this->renderAdminView('@seumodulo/form.twig', [
        'item' => $item,
        'countries' => $countries,
        'states' => $states,
        'cities' => $cities
    ]);
}

4. View (Twig)

Utilize a estrutura HTML abaixo para os selects. Substitua os IDs e Names conforme o seu módulo. Observe o uso de country.name para obter o nome traduzido.

<div class="row">
    <!-- País -->
    <div class="col-md-4 mb-3">
        <label for="events_country_id" class="form-label">País</label>
        <select class="form-select" id="events_country_id" name="events_country_id" onchange="loadStates(this.value)" required>
            <option value="">Selecione...</option>
            {% for country in countries %}
                <option value="{{ country.countries_id }}" {{ item.events_country_id == country.countries_id ? 'selected' : '' }}>
                    {{ country.name }}
                </option>
            {% endfor %}
        </select>
    </div>

    <!-- Estado -->
    <div class="col-md-4 mb-3">
        <label for="events_state_id" class="form-label">Estado</label>
        <select class="form-select" id="events_state_id" name="events_state_id" onchange="loadCities(this.value)" required {{ not item.events_country_id ? 'disabled' : '' }}>
            <option value="">Selecione...</option>
            {% for state in states %}
                <option value="{{ state.states_id }}" {{ item.events_state_id == state.states_id ? 'selected' : '' }}>
                    {{ state.states_name }}
                </option>
            {% endfor %}
        </select>
    </div>

    <!-- Cidade -->
    <div class="col-md-4 mb-3">
        <label for="events_city_id" class="form-label">Cidade</label>
        <select class="form-select" id="events_city_id" name="events_city_id" required {{ not item.events_state_id ? 'disabled' : '' }}>
            <option value="">Selecione...</option>
            {% for city in cities %}
                <option value="{{ city.cities_id }}" {{ item.events_city_id == city.cities_id ? 'selected' : '' }}>
                    {{ city.cities_name }}
                </option>
            {% endfor %}
        </select>
    </div>
</div>

5. JavaScript (Vanilla JS Obrigatório)

Utilize EXCLUSIVAMENTE JavaScript Puro para esta lógica. O uso de Alpine.js é estritamente proibido para os selects de localização do GlobalLists devido a conflitos de reatividade e performance.

As rotas da API /api/globallists/states/{countryId} e /api/globallists/cities/{stateId} já retornam os dados formatados corretamente.

// Defina os valores salvos (caso seja edição)
const savedStateId = "{{ item.events_state_id }}";
const savedCityId = "{{ item.events_city_id }}";

document.addEventListener('DOMContentLoaded', function() {
    const countryId = document.getElementById('events_country_id').value;
    if (countryId) {
        // Carrega os estados e, em seguida, as cidades se houver um estado salvo
        loadStates(countryId, savedStateId);
    }
});

function loadStates(countryId, selectedStateId = null) {
    const stateSelect = document.getElementById('events_state_id');
    const citySelect = document.getElementById('events_city_id');
    
    // Reset e estado de loading
    stateSelect.innerHTML = '<option value="">Carregando...</option>';
    stateSelect.disabled = true;
    
    citySelect.innerHTML = '<option value="">Selecione o estado primeiro...</option>';
    citySelect.disabled = true;

    if (!countryId) {
        stateSelect.innerHTML = '<option value="">Selecione o país primeiro...</option>';
        return;
    }

    fetch(`/api/globallists/states/${countryId}`)
        .then(response => response.json())
        .then(data => {
            stateSelect.innerHTML = '<option value="">Selecione...</option>';
            data.forEach(state => {
                // A API retorna {id, name}
                const selected = selectedStateId && selectedStateId == state.id ? 'selected' : '';
                stateSelect.innerHTML += `<option value="${state.id}" ${selected}>${state.name}</option>`;
            });
            stateSelect.disabled = false;

            // Se houve um estado pré-selecionado, carrega as cidades
            if (selectedStateId) {
                loadCities(selectedStateId, savedCityId);
            }
        })
        .catch(error => {
            console.error('Error loading states:', error);
            stateSelect.innerHTML = '<option value="">Erro ao carregar</option>';
        });
}

function loadCities(stateId, selectedCityId = null) {
    const citySelect = document.getElementById('events_city_id');
    
    citySelect.innerHTML = '<option value="">Carregando...</option>';
    citySelect.disabled = true;

    if (!stateId) {
        citySelect.innerHTML = '<option value="">Selecione o estado primeiro...</option>';
        return;
    }

    fetch(`/api/globallists/cities/${stateId}`)
        .then(response => response.json())
        .then(data => {
            citySelect.innerHTML = '<option value="">Selecione...</option>';
            data.forEach(city => {
                // A API retorna {id, name}
                const selected = selectedCityId && selectedCityId == city.id ? 'selected' : '';
                citySelect.innerHTML += `<option value="${city.id}" ${selected}>${city.name}</option>`;
            });
            citySelect.disabled = false;
        })
        .catch(error => {
            console.error('Error loading cities:', error);
            citySelect.innerHTML = '<option value="">Erro ao carregar</option>';
        });
}