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

243 lines
9.2 KiB
Markdown
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`):
```php
// 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`.
```php
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).
```php
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.
```twig
<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.
```javascript
// 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>';
});
}
```