Módulos visuais
mad-sheet
mad-sheet-col
Combo com cascade e busca
mad-sheet zero PHP: categoria filtra produto por linha (depends-on + search).
| # | * |
|---|---|
|
DSTQQSS
|
|
| Σ |
resources/views/tutor/examples/modulos-sheet/cascade.blade.php
<mad-sheet model="TutorDemoSheetLancamento" database="business" rows-min="4">
<mad-sheet-col field="categoria_id" type="combo" model="TutorDemoSheetCategoria"
display="nome" label="Categoria" required width="200px" />
{{-- Cascade por linha: muda a categoria da linha e o produto da MESMA
linha limpa; busca server-side por causa do depends-on. --}}
<mad-sheet-col field="produto_id" type="combo" model="TutorDemoSheetProduto"
display="nome" label="Produto" required width="220px"
search depends-on="categoria_id" depends-column="categoria_id" />
<mad-sheet-col field="quantidade" type="number" label="Qtd" total="sum"
decimals="0" min="0" step="1" required />
</mad-sheet>
Lote completo (combo, número, moeda, data, calculado)
Todos os tipos de mad-sheet-col + quick register na célula + total persistido via beforeSaveRow.
| # | * |
|---|---|
|
DSTQQSS
|
|
| Σ |
resources/views/tutor/examples/modulos-sheet/lote-completo.blade.php
@php
// Partial de <x-tutor-demo> = escopo Blade ISOLADO — `$that` não
// atravessa esse aninhamento sozinho (ver lição em
// tutor.examples.grids-formatacao.agrupamento). Reabre aqui porque
// beforeSaveRow (recalcular o total no servidor) só existe no HOST de
// verdade (TutorModulosSheet, subclasse de MadSheet).
$that = \Mad\Component\MadRenderContext::getComponent();
@endphp
<mad-sheet model="TutorDemoSheetLancamento" database="business" rows-min="6" max-rows="500">
<mad-sheet-col field="categoria_id" type="combo" model="TutorDemoSheetCategoria"
display="nome" label="Categoria" required width="170px" />
<mad-sheet-col field="produto_id" type="combo" model="TutorDemoSheetProduto"
display="nome" label="Produto" required width="220px"
search depends-on="categoria_id" depends-column="categoria_id">
<mad-quick-form action="TutorModulosSheet::onQuickAddProduto" label="Adicionar"
message="Produto não encontrado — cadastre na hora:">
<mad-input-field name="nome" label="Nome" required />
</mad-quick-form>
</mad-sheet-col>
<mad-sheet-col field="quantidade" type="number" label="Qtd" total="sum"
decimals="0" min="0" step="1" required />
<mad-sheet-col field="valor" type="money" label="Valor unit." total="sum"
width="150px" prefix="R$" min="0" />
{{-- Coluna calculada: readonly, display-only — persistida no
beforeSaveRow (TutorModulosSheet::beforeSaveRow). --}}
<mad-sheet-col field="total" type="money" label="Total" total="sum"
width="150px" compute="{quantidade} * {valor}" />
{{-- :default = expressão PHP; default="{{ '{{ }}' }}" NÃO funciona aqui
(o compiler da tag roda antes do Blade). --}}
<mad-sheet-col field="data" type="date" label="Data" :default="date('Y-m-d')"
width="150px" min="2020-01-01" />
<mad-sheet-col field="obs" label="Observação" placeholder="opcional" maxlength="120" />
</mad-sheet>
app/control/Tutor/TutorModulosSheet.php
/**
* `compute` é display-only (nunca persiste) — recalcula o total no
* servidor antes de gravar. Contrato: SheetDevView::enrichComputedTotal.
*/
protected function beforeSaveRow(array $row, int $index): array
{
$row['total'] = round(((float) ($row['quantidade'] ?? 0)) * ((float) ($row['valor'] ?? 0)), 2);
return $row;
}
Código-fonte completo da página
app/control/Tutor/TutorModulosSheet.php
<?php
namespace App\Control\Tutor;
use App\Models\TutorDemo\TutorDemoSheetCategoria;
use App\Models\TutorDemo\TutorDemoSheetProduto;
use Illuminate\Support\Facades\Schema;
use Mad\Sheet\MadSheet;
/**
* TutorModulosSheet — página "Planilha (sheet)" do MAD Lab (catálogo
* modulos_sheet). Cobre: mad-sheet, mad-sheet-col.
*
* O demo pequeno (cascade.blade.php) NÃO reabre `$that` — cai num
* MadSheetStandalone novo por request (zero PHP, igual ao example_basic do
* mad_props). O demo grande (lote-completo.blade.php) reabre `$that` via
* MadRenderContext::getComponent() pra usar o HOST de verdade (esta classe),
* já que o hook `beforeSaveRow` só existe em subclasse. Diferente do
* mad-org-chart, o MadSheet NÃO cacheia dados carregados por instância (é
* entry-first — nasce vazio), então múltiplos <mad-sheet> no mesmo host não
* têm o bug de "roots desatualizados"; mesmo assim só o ÚLTIMO tag
* renderizado fica com o `shCfgKey` válido pro round-trip do wire (Salvar/
* Validar) — por isso o demo grande é sempre o último da página.
*/
class TutorModulosSheet extends MadSheet
{
protected static string $wrapper = self::INTERNAL;
protected string $model = 'TutorDemoSheetLancamento';
protected string $database = 'business';
public function mount(array $params = []): void
{
$this->ensureFixture();
}
/**
* `compute` é display-only (nunca persiste) — recalcula o total no
* servidor antes de gravar. Contrato: SheetDevView::enrichComputedTotal.
*/
protected function beforeSaveRow(array $row, int $index): array
{
$row['total'] = round(((float) ($row['quantidade'] ?? 0)) * ((float) ($row['valor'] ?? 0)), 2);
return $row;
}
/**
* Quick register do combo de produto (MadQuickRegisterService), chamado
* pelo <mad-quick-form action="TutorModulosSheet::onQuickAddProduto">
* dentro da coluna produto_id. Recebe {term, fields, field_name}; retorna
* {value, label[, toast]}.
*/
public static function onQuickAddProduto(array $params = []): array
{
$nome = trim((string) ($params['fields']['nome'] ?? $params['term'] ?? ''));
if ($nome === '') {
throw new \RuntimeException('Informe o nome do produto.');
}
$categoriaId = (int) ($params['fields']['categoria_id'] ?? 1);
$produto = TutorDemoSheetProduto::query()->create([
'nome' => $nome,
'categoria_id' => $categoriaId > 0 ? $categoriaId : 1,
]);
return ['value' => (string) $produto->id, 'label' => $nome, 'toast' => 'Produto cadastrado.'];
}
// ── Fixture ──────────────────────────────────────────────────────────
private function ensureFixture(): void
{
$schema = Schema::connection('business');
if (!$schema->hasTable('tutor_sheet_categorias')) {
$schema->create('tutor_sheet_categorias', function ($t) {
$t->increments('id');
$t->string('nome');
});
}
if (!$schema->hasTable('tutor_sheet_produtos')) {
$schema->create('tutor_sheet_produtos', function ($t) {
$t->increments('id');
$t->string('nome');
$t->integer('categoria_id')->default(0);
});
}
if (!$schema->hasTable('tutor_sheet_lancamentos')) {
$schema->create('tutor_sheet_lancamentos', function ($t) {
$t->increments('id');
$t->integer('categoria_id')->default(0);
$t->integer('produto_id')->default(0);
$t->decimal('quantidade', 12, 2)->default(0);
$t->decimal('valor', 12, 2)->default(0);
$t->decimal('total', 14, 2)->default(0);
$t->string('data', 10)->nullable();
$t->string('obs')->nullable();
});
}
if (TutorDemoSheetCategoria::query()->count() === 0) {
TutorDemoSheetCategoria::query()->insert([
['id' => 1, 'nome' => 'Bebidas'],
['id' => 2, 'nome' => 'Limpeza'],
]);
}
if (TutorDemoSheetProduto::query()->count() === 0) {
TutorDemoSheetProduto::query()->insert([
['id' => 1, 'nome' => 'Água mineral 500ml', 'categoria_id' => 1],
['id' => 2, 'nome' => 'Suco de laranja 1L', 'categoria_id' => 1],
['id' => 3, 'nome' => 'Detergente neutro', 'categoria_id' => 2],
['id' => 4, 'nome' => 'Água sanitária 1L', 'categoria_id' => 2],
]);
}
}
protected function view(): string|array
{
return ['tutor.modulos.sheet', []];
}
}
resources/views/tutor/modulos/sheet.blade.php
<x-tutor-page page="modulos_sheet">
<x-tutor-demo demo="tutor.examples.modulos-sheet.cascade"
title="Combo com cascade e busca" desc="mad-sheet zero PHP: categoria filtra produto por linha (depends-on + search)." />
<x-tutor-demo demo="tutor.examples.modulos-sheet.lote-completo"
title="Lote completo (combo, número, moeda, data, calculado)" desc="Todos os tipos de mad-sheet-col + quick register na célula + total persistido via beforeSaveRow."
php="TutorModulosSheet::beforeSaveRow" />
</x-tutor-page>