diff --git a/.github/workflows/laravel.yml b/.github/workflows/laravel.yml
index ca244bb0..c61e62d3 100644
--- a/.github/workflows/laravel.yml
+++ b/.github/workflows/laravel.yml
@@ -31,8 +31,6 @@ jobs:
run: |
mkdir -p database
touch database/database.sqlite
- - name: Create Storage Folders
- run: mkdir -p storage/framework/{sessions,views,cache}
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Generate key
diff --git a/.gitignore b/.gitignore
index 9aeee6a6..bf8284a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,10 +4,14 @@
/public/hot
/public/storage
/storage/*.key
+/storage/pail
+/storage/framework/lsp-*.php
+/storage/media-library
/vendor
.env
.env.backup
.env.production
+.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
@@ -16,6 +20,13 @@ npm-debug.log
yarn-error.log
/.fleet
/.idea
+/.nova
/.vscode
+/.zed
.php-cs-fixer.php
-/storage
\ No newline at end of file
+
+# Backups e metadados de SO
+*.bak
+*:Zone.Identifier
+.DS_Store
+Thumbs.db
diff --git a/CLAUDE.md b/CLAUDE.md
index 6a22f47e..f7a31d33 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -70,7 +70,6 @@ CI (`.github/workflows/laravel.yml`) runs on push/PR to `main`. It uses PHP 8.3
- **`routes/web.php` overrides `/admin/login` with a dev auto-login** that logs in `User::first()`, marked "TODO: Remove". Never ship it to production. Don't rely on it in tests either.
- `AppServiceProvider::boot()` queries `settings` whenever the table exists, so a broken settings row affects every request.
-- The `*.php`/`*.py` scripts in the repo root (`modify_*.php`, `patch_*.py`, `refactor_wizard*.py`, `generate_*.php`, `check_brackets.py`, `update_resource.patch`) are one-off codemods that were used to rewrite Blade/Filament files. They are not part of the app. Don't run them unless asked, because they overwrite files in `resources/views/vendor/...` and in the resources.
-- `.bak` files exist in `app/Livewire` and `resources/views/livewire`. Ignore them.
-- Test coverage is thin (`tests/Feature`: Budget, Navigation, User).
+- Keep the repo root Laravel-standard: no one-off codemod scripts, patches or `.bak` files (git history is the backup). A reusable maintenance task becomes an Artisan command in `app/Console/Commands`.
+- `storage/` is tracked only through its per-folder `.gitignore` stubs, as in the Laravel skeleton. Never ignore `/storage` wholesale, because a fresh clone would come without the folders Laravel needs.
- `docs/` has product and design notes (roadmap, permissions, user guide), mostly in Portuguese.
diff --git a/app/Livewire/Budget.php.bak b/app/Livewire/Budget.php.bak
deleted file mode 100644
index f4996a55..00000000
--- a/app/Livewire/Budget.php.bak
+++ /dev/null
@@ -1,470 +0,0 @@
-form->fill();
- }
-
- public function form(Form $form): Form
- {
- return $form
- ->model(BudgetModel::class)
- ->schema([
- Hidden::make('code')->default(fn () => BudgetModel::generateUniqueCode()),
- Hidden::make('content.price'),
- Hidden::make('content.subtotal'),
- Hidden::make('content.total'),
- Hidden::make('content.quantity'),
- Hidden::make('content.shipping')->default(0),
-
- Wizard::make([
- Wizard\Step::make('Pedido')
- ->description('O que precisa?')
- ->icon('heroicon-o-shopping-bag')
- ->schema([
- Repeater::make('content.products')
- ->label('')
- ->schema([
- Grid::make(12)->schema([
- Select::make('product')
- ->label('Produto')
- ->options(fn () => $this->getAllProductsPlucked())
- ->required()
- ->validationAttribute('Produto')
- ->live()
- ->afterStateUpdated(function (Set $set, $state) {
- $set('product_option', null);
- $min = $this->getProductMinQuantity($state);
- $set('quantity', floatval($min));
- })
- ->columnSpan(5),
- Select::make('product_option')
- ->label('Opção / Traço')
- ->options(fn (Get $get) => $this->getProductOptions($get))
- ->required(fn (Get $get) => $this->getProductOptions($get)->isNotEmpty())
- ->validationAttribute('Opção / Traço')
- ->hidden(fn (Get $get) => $this->getProductOptions($get)->isEmpty())
- ->live()
- ->afterStateUpdated(fn (Get $get, Set $set, $state) => $this->updatePrice($get, $set, $state))
- ->columnSpan(5),
- TextInput::make('quantity')
- ->label('Quantidade')
- ->numeric()
- ->required()
- ->default(1)
- ->minValue(fn (Get $get) => $this->getProductMinQuantity($get('product')))
- ->validationAttribute('Quantidade')
- ->step(1)
- ->suffix(fn (Get $get) => $this->getUnitSuffix($get))
- ->live(onBlur: true)
- ->placeholder(fn (Get $get) => 'Mín: ' . floatval($this->getProductMinQuantity($get('product'))))
- ->afterStateUpdated(function (Get $get, Set $set, $state) {
- $min = $this->getProductMinQuantity($get('product'));
- $qty = max(floatval($state ?? $min), $min);
- $set('quantity', ceil($qty));
- $this->calculateItemSubtotal($get, $set);
- $this->calculateTotal($get, $set);
- })
- ->columnSpan(2),
- Hidden::make('price'),
- ]),
- ])
- ->addActionLabel('Adicionar Item')
- ->cloneable()
- ->collapsible()
- ->defaultItems(1), // Auto-adds the first item to reduce friction!
-
- \Filament\Forms\Components\ToggleButtons::make('content.location')
- ->label('Local da Obra')
- ->options(fn () => $this->getAllLocationsPlucked())
- ->inline()
- ->required()
- ->validationAttribute('Local da Obra')
- ->live()
- ->visible(function (Get $get) {
- $products = $get('content.products') ?? [];
- foreach ($products as $item) {
- if (!empty($item['product_option'])) {
- return true;
- }
- }
- return false;
- }),
-
- \Filament\Forms\Components\FileUpload::make('content.photos')
- ->label('Fotos da Obra (Opcional)')
- ->image()
- ->multiple()
- ->maxFiles(4)
- ->maxSize(4096)
- ->disk('public')
- ->directory('budget-documents')
- ->panelLayout('grid'),
- ]),
-
- Wizard\Step::make('Contato & Local')
- ->description('Seus dados')
- ->icon('heroicon-o-user')
- ->schema([
- Grid::make(3)->schema([
- TextInput::make('content.customer_name')
- ->label('Nome Completo')
- ->required()
- ->validationAttribute('Nome Completo')
- ->placeholder('Seu nome')
- ->extraInputAttributes(['class' => '!bg-white shadow-sm']),
- TextInput::make('content.customer_phone')
- ->label('WhatsApp')
- ->tel()
- ->mask('(99)99999-9999')
- ->required()
- ->validationAttribute('WhatsApp')
- ->placeholder('(00) 00000-0000')
- ->extraInputAttributes(['class' => '!bg-white shadow-sm']),
- TextInput::make('content.customer_email')
- ->label('E-mail')
- ->email()
- ->required()
- ->validationAttribute('E-mail')
- ->placeholder('Seu e-mail')
- ->extraInputAttributes(['class' => '!bg-white shadow-sm']),
- ]),
- Grid::make(4)->schema([
- TextInput::make('content.postcode')
- ->label('CEP da Obra')
- ->placeholder('00000-000')
- ->mask('99999-999')
- ->required()
- ->validationAttribute('CEP')
- ->live(onBlur: true)
- ->extraInputAttributes(['class' => '!bg-white shadow-sm'])
- ->rules($this->getPostcodeRules())
- ->afterStateUpdated(function ($state, Set $set, $livewire) {
- $set('content.street', null);
- $set('content.neighborhood', null);
- $set('content.city', null);
- $set('content.state', null);
- $set('content.shipping', 0);
- if (strlen($state ?? '') === 9) {
- try {
- $livewire->validateOnly('data.content.postcode');
- $postcode = new \App\Services\PostcodeFinderService($state, $set);
- $postcode->find();
- $livewire->applyShippingFromCep($state, $set);
- } catch (\Illuminate\Validation\ValidationException $e) {
- throw $e;
- }
- }
- }),
- TextInput::make('content.street')
- ->label('Rua/Av')
- ->columnSpan(2)
- ->disabled()
- ->dehydrated()
- ->extraInputAttributes(['class' => '!bg-zinc-100/80 !opacity-90 !cursor-not-allowed border-zinc-200']),
- TextInput::make('content.number')
- ->label('Nº')
- ->required()
- ->validationAttribute('Número')
- ->placeholder('Nº ou KM')
- ->extraInputAttributes(['class' => '!bg-white shadow-sm']),
- ]),
- Grid::make(12)
- ->schema([
- TextInput::make('content.neighborhood')->label('Bairro')->columnSpan(5)->disabled()->dehydrated()->extraInputAttributes(['class' => '!bg-zinc-100/80 !opacity-90 !cursor-not-allowed border-zinc-200']),
- TextInput::make('content.city')->label('Cidade')->columnSpan(5)->disabled()->dehydrated()->extraInputAttributes(['class' => '!bg-zinc-100/80 !opacity-90 !cursor-not-allowed border-zinc-200']),
- TextInput::make('content.state')->label('UF')->columnSpan(2)->disabled()->dehydrated()->extraInputAttributes(['class' => '!bg-zinc-100/80 !opacity-90 !cursor-not-allowed border-zinc-200 text-center']),
- ])
- ->visible(fn (Get $get) => filled($get('content.street'))),
- ]),
-
- Wizard\Step::make('Resumo')
- ->description('Revise e envie')
- ->icon('heroicon-o-check-circle')
- ->schema([
- Placeholder::make('summary')
- ->label('')
- ->content(fn (Get $get) => view('livewire.budget-checkout-summary', [
- 'data' => ['content' => $get('content')],
- 'canSubmit' => \App\Services\OperationAreaService::isCepInOperationArea((string) $get('content.postcode')),
- 'turnstileEnabled' => \App\Models\Setting::get('security.turnstile.enabled', false),
- 'turnstileSiteKey' => \App\Models\Setting::get('security.turnstile.site_key', '')
- ])),
- ])
- ])
- ->nextAction(
- fn (\Filament\Forms\Components\Actions\Action $action) => $action->extraAttributes(['class' => '!bg-primary !text-white hover:!bg-primary/90 !border-0'])
- )
- ->previousAction(
- fn (\Filament\Forms\Components\Actions\Action $action) => $action->extraAttributes(['class' => '!bg-secondary !text-secondary-foreground !border !border-border hover:!bg-accent'])
- )
- ->submitAction(
- new \Illuminate\Support\HtmlString(
- 'Solicitar Orçamento Grátis '
- )
- ),
- ])
- ->statePath('data');
- }
-
-
-
- protected function getCachedProduct($id)
- {
- if (!$id) return null;
- if (!array_key_exists($id, self::$productsCache)) {
- self::$productsCache[$id] = \Illuminate\Support\Facades\Cache::remember(
- 'product_' . $id,
- 86400,
- fn () => Product::find($id)
- );
- }
- return self::$productsCache[$id];
- }
-
- protected function getCachedProductOption($id)
- {
- if (!$id) return null;
- if (!array_key_exists($id, self::$productOptionsCache)) {
- self::$productOptionsCache[$id] = \Illuminate\Support\Facades\Cache::remember(
- 'product_option_' . $id,
- 86400,
- fn () => ProductOption::find($id)
- );
- }
- return self::$productOptionsCache[$id];
- }
-
- protected function getProductMinQuantity($id): float
- {
- if (!$id) return 1;
- $product = $this->getCachedProduct($id);
- return $product?->min_quantity ?? 1;
- }
-
- protected function getAllProductsPlucked(): array
- {
- // For schema options, caching the array of products
- static $productsPlucked = null;
- if ($productsPlucked === null) {
- $productsPlucked = \Illuminate\Support\Facades\Cache::remember(
- 'all_products_plucked',
- 86400,
- fn () => Product::pluck('name', 'id')->toArray()
- );
- }
- return $productsPlucked;
- }
-
- protected function getAllLocationsPlucked(): array
- {
- if (self::$locationsCache === null) {
- self::$locationsCache = \Illuminate\Support\Facades\Cache::remember(
- 'all_locations_plucked',
- 86400,
- fn () => \App\Models\Location::pluck('name', 'id')->toArray()
- );
- }
- return self::$locationsCache;
- }
-
- protected function getPostcodeRules(): array
- {
- return ['required', 'string', 'size:9', new CepInOperationAreaRule];
- }
-
- public function create(TurnstileService $turnstile): void
- {
- $this->form->validate();
-
- // Validação Cloudflare Turnstile
- if (!$turnstile->verify($this->turnstileToken, request()->ip())) {
- $this->addError('turnstileToken', 'A verificação anti-spam falhou. Por favor, tente novamente.');
- \Filament\Notifications\Notification::make()
- ->title('Erro de Validação')
- ->body('A verificação anti-spam falhou. Atualize a página e tente novamente.')
- ->danger()
- ->send();
- return;
- }
-
- $state = $this->form->getState();
-
- // Garantir que os totais estejam corretos antes de salvar e enviar e-mail
- $products = $state['content']['products'] ?? [];
- $shipping = (float) ($state['content']['shipping'] ?? 0);
- $result = BudgetCalculatorService::calculateTotal($products, $shipping, 0, 0);
-
- $state['content']['quantity'] = $result['quantity'];
- $state['content']['price'] = $result['price'];
- $state['content']['subtotal'] = $result['subtotal'];
- $state['content']['total'] = $result['total'];
-
- // Extract products for pivot table sync
- $products = $state['content']['products'] ?? [];
-
- $budget = BudgetModel::create($state);
-
- // Save items to the pivot table (snapshot)
- foreach ($products as $req) {
- $budget->budgetItems()->create([
- 'product_id' => $req['product'] ?? null,
- 'product_option_id' => $req['product_option'] ?? null,
- 'location_id' => $req['location'] ?? null,
- 'quantity' => $req['quantity'] ?? 1,
- 'price' => $req['price'] ?? 0,
- 'subtotal' => $req['subtotal'] ?? 0,
- ]);
- }
-
- if (!empty($state['content']['photos'])) {
- foreach ($state['content']['photos'] as $index => $photoPath) {
- $fullPath = storage_path('app/public/' . $photoPath);
- $budget->documents()->create([
- 'title' => 'Foto do Local (Anexo ' . ($index + 1) . ')',
- 'description' => 'Foto recebida via formulário de orçamento.',
- 'file_path' => $photoPath,
- 'file_name' => basename($photoPath),
- 'file_size' => file_exists($fullPath) ? filesize($fullPath) : 0,
- 'file_type' => file_exists($fullPath) ? mime_content_type($fullPath) : 'image/jpeg',
- 'uploaded_by' => null,
- ]);
- }
- }
-
- \Illuminate\Support\Facades\Mail::to(User::first()?->email ?? config('mail.from.address'))
- ->send(new \App\Mail\AdminNewBudgetMail($budget));
-
- $this->isSubmitted = true;
- }
-
- public function resetForm(): void
- {
- $this->form->fill();
- $this->isSubmitted = false;
- }
-
- public function render(TurnstileService $turnstile)
- {
- return view('livewire.budget', [
- 'turnstileEnabled' => $turnstile->isEnabled(),
- 'turnstileSiteKey' => $turnstile->getSiteKey(),
- ]);
- }
-
- private function getProductOptions(Get $get): Collection
- {
- $productId = $get('product');
- if (!$productId) return collect();
-
- // Cache the options list per product
- static $optionsListCache = [];
- if (!isset($optionsListCache[$productId])) {
- $optionsListCache[$productId] = \Illuminate\Support\Facades\Cache::remember(
- 'product_options_plucked_' . $productId,
- 86400,
- fn () => ProductOption::where('product_id', $productId)->pluck('name', 'id')
- );
- }
- return collect($optionsListCache[$productId]);
- }
-
- private function calculateTotal(Get|\Closure $get, Set $set): void
- {
- $products = $get('content.products') ?? [];
- $shipping = (float) ($get('content.shipping') ?? 0);
- $result = BudgetCalculatorService::calculateTotal($products, $shipping, 0, 0);
- $set('content.quantity', $result['quantity']);
- $set('content.price', $result['price']);
- $set('content.subtotal', $result['subtotal']);
- $set('content.total', $result['total']);
- }
-
- public function applyShippingFromCep(?string $postcode, Set $set): void
- {
- if (empty($postcode) || strlen(preg_replace('/\D/', '', $postcode)) < 8) return;
- $result = OperationAreaService::resultForCep($postcode);
- $fee = $result['shipping_fee'] ?? 0;
- $set('content.shipping', (string) $fee);
- $get = fn (string $key) => data_get($this->data, $key);
- $this->calculateTotal($get, $set);
- }
-
- private function updatePrice(Get $get, Set $set, $productId): void
- {
- $price = 0;
- if ($productId) {
- $option = $this->getCachedProductOption($productId);
- $price = $option?->price ?? 0;
- }
- $set('quantity', null);
- $set('price', $price);
- $this->calculateItemSubtotal($get, $set);
- $this->calculateTotal($get, $set);
- }
-
- private function getUnitSuffix(Get $get): string
- {
- $optionId = $get('product_option');
- if (!$optionId) return '';
- return $this->getCachedProductOption($optionId)?->unit?->value ?? '';
- }
-
- private function calculateItemSubtotal(Get $get, Set $set): void
- {
- $quantity = floatval($get('quantity') ?? 0);
- $price = floatval($get('price') ?? 0);
- $set('subtotal', BudgetCalculatorService::calculateItemSubtotal($quantity, $price));
- }
-}
diff --git a/check_brackets.py b/check_brackets.py
deleted file mode 100644
index d698ebad..00000000
--- a/check_brackets.py
+++ /dev/null
@@ -1,19 +0,0 @@
-with open('app/Filament/Resources/ContentSectionResource.php', 'r') as f:
- text = f.read()
-
-def check(s):
- stack = []
- for i, c in enumerate(s):
- if c in '([{':
- stack.append((c, i))
- elif c in ')]}':
- if not stack:
- return f"Unmatched {c} at {i}"
- top, pos = stack.pop()
- if (top == '(' and c != ')') or (top == '[' and c != ']') or (top == '{' and c != '}'):
- return f"Mismatched {c} at {i}, expected matching for {top} from {pos}"
- if stack:
- return f"Unclosed {stack[-1][0]} at {stack[-1][1]}"
- return "OK"
-
-print(check(text))
diff --git a/composer.json b/composer.json
index b4d2a4e5..9697fadf 100644
--- a/composer.json
+++ b/composer.json
@@ -1,10 +1,12 @@
{
- "name": "laravel/laravel",
+ "name": "codhous/codhous",
"type": "project",
"description": "Comprehensive management and storefront ecosystem for small construction companies, built with Laravel and Filament.",
"keywords": [
"laravel",
- "framework"
+ "filament",
+ "construction",
+ "budget"
],
"license": "MIT",
"require": {
diff --git a/composer.lock b/composer.lock
index c9e009a0..7a02b1ae 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "a136c2b120844cdf2b411e46cd79a067",
+ "content-hash": "b7b57d3e88fdccb6f509acf6a79f685b",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -9705,12 +9705,12 @@
"version": "v5.7.0",
"source": {
"type": "git",
- "url": "https://github.com/vlucas/phpdotenv.git",
+ "url": "https://github.com/phpdotenv/phpdotenv.git",
"reference": "301c07936b16d88628b126b01d082ba153cf4c40"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/301c07936b16d88628b126b01d082ba153cf4c40",
+ "url": "https://api.github.com/repos/phpdotenv/phpdotenv/zipball/301c07936b16d88628b126b01d082ba153cf4c40",
"reference": "301c07936b16d88628b126b01d082ba153cf4c40",
"shasum": ""
},
diff --git a/fix_sidebar_colors.php b/fix_sidebar_colors.php
deleted file mode 100644
index 9c38173a..00000000
--- a/fix_sidebar_colors.php
+++ /dev/null
@@ -1,34 +0,0 @@
-{{ $cat }}
-HTML;
-$replace = <<<'HTML'
-
{{ $cat }}
-HTML;
-$content = str_replace($search, $replace, $content);
-
-$search2 = <<<'HTML'
-
- {{ $block->getLabel() }}
-
-HTML;
-$replace2 = <<<'HTML'
-
- {{ $block->getLabel() }}
-
-HTML;
-$content = str_replace($search2, $replace2, $content);
-
-$search3 = <<<'HTML'
-
-HTML;
-$replace3 = <<<'HTML'
-
-HTML;
-$content = str_replace($search3, $replace3, $content);
-
-file_put_contents('resources/views/vendor/filament-forms/components/builder.blade.php', $content);
-echo "Sidebar colors fixed.\n";
diff --git a/generate_iframes.php b/generate_iframes.php
deleted file mode 100644
index a04023c8..00000000
--- a/generate_iframes.php
+++ /dev/null
@@ -1,41 +0,0 @@
- '$block',
- 'data' => get_defined_vars(),
- 'id' => \$id
- ])->render();
-@endphp
-
-
-
-BLADE;
- file_put_contents("resources/views/filament/block-previews/{$block}.blade.php", $content);
-}
-echo "Fixed double escaping.\n";
diff --git a/generate_previews.php b/generate_previews.php
deleted file mode 100644
index 13f6974a..00000000
--- a/generate_previews.php
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-BLADE;
- file_put_contents("resources/views/filament/block-previews/{$block}.blade.php", $content);
-}
-echo "Generated " . count($blocks) . " preview views.\n";
diff --git a/modify_block_picker.php b/modify_block_picker.php
deleted file mode 100644
index cd4bd271..00000000
--- a/modify_block_picker.php
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
-
Categorias
- @foreach($groupedBlocks as $cat => $catBlocks)
-
- {{ $cat }}
- {{ count($catBlocks) }}
-
- @endforeach
-
-
-
-
- @foreach($groupedBlocks as $cat => $catBlocks)
-
-
-
{{ $cat }}
-
-
-HTML;
-
-$replace = <<<'HTML'
-
-
-
-
Categorias
- @foreach($groupedBlocks as $cat => $catBlocks)
-
- {{ $cat }}
- {{ count($catBlocks) }}
-
- @endforeach
-
-
-
-
- @foreach($groupedBlocks as $cat => $catBlocks)
-
-
-
{{ $cat }}
-
-
-HTML;
-
-$content = str_replace($search, $replace, $content);
-
-// Now fix the card background color inside the grid loop
-$cardSearch = <<<'HTML'
-
-
-
-HTML;
-
-$cardReplace = <<<'HTML'
-
-
-
-HTML;
-
-$content = str_replace($cardSearch, $cardReplace, $content);
-
-file_put_contents('resources/views/vendor/filament-forms/components/builder/block-picker.blade.php', $content);
-echo "Modal Layout fixed.\n";
-
diff --git a/modify_builder.php b/modify_builder.php
deleted file mode 100644
index 320dbd68..00000000
--- a/modify_builder.php
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
Adicionar Bloco
-
Clique para adicionar no fim da página.
-
-
- @foreach($blockPickerBlocks as $block)
- @php
- $wireClickActionArguments = \Illuminate\Support\Js::from(['block' => $block->getName()]);
- $wireClickAction = "mountFormComponentAction('{$statePath}', '{$addAction->getName()}', {$wireClickActionArguments})";
- @endphp
-
-
-
-
-
- {{ $block->getLabel() }}
-
-
- @endforeach
-
-
-
-
-
-
merge($getExtraAttributes(), escape: false)
- ->class(['fi-fo-builder grid grid-cols-1 gap-y-4'])
- }}
- >
-HTML;
-
-$content = str_replace('
- merge($getExtraAttributes(), escape: false)
- ->class([\'fi-fo-builder grid grid-cols-1 gap-y-4\'])
- }}
- >', $sidebarHtml, $content);
-
-$content = str_replace('', "
\n \n", $content);
-
-file_put_contents('resources/views/vendor/filament-forms/components/builder.blade.php', $content);
-echo "Sidebar restored in builder.blade.php without categories.\n";
-
diff --git a/modify_schema.php b/modify_schema.php
deleted file mode 100644
index 9e332480..00000000
--- a/modify_schema.php
+++ /dev/null
@@ -1,59 +0,0 @@
-schema\(\[/', $line)) {
- // Replace `->schema([` with `->schema(self::getBlockTabs([`
- $lines[$i] = preg_replace('/->schema\(\[/', '->schema(self::getBlockTabs([', $line, 1);
- $schemaLineIndex = $i;
- $bracketDepth = 0;
- }
-
- if ($schemaLineIndex !== -1) {
- if (strpos($line, '[') !== false) {
- $bracketDepth += substr_count($line, '[');
- }
- if (strpos($line, ']') !== false) {
- $bracketDepth -= substr_count($line, ']');
- }
-
- // If we closed the main array
- if ($bracketDepth === 0) {
- // Find where the array was closed, it should be something like `]);`
- if (preg_match('/\]\);/', $lines[$i])) {
- $lines[$i] = preg_replace('/\]\);/', ']));', $lines[$i]);
- $schemaLineIndex = -1; // Reset for next block
- }
- }
- }
-
- if ($braceDepth === 0 && strpos($line, '}') !== false) {
- $inBlock = false;
- }
- }
-}
-
-file_put_contents('app/Filament/Resources/PageResource.php', implode("", $lines));
-echo "Done.\n";
diff --git a/modify_sections.php b/modify_sections.php
deleted file mode 100644
index f9d1a129..00000000
--- a/modify_sections.php
+++ /dev/null
@@ -1,37 +0,0 @@
- or
that represents the main block wrapper
- // Since each block might be slightly different, let's look for `
/', $lines[$i], $matches)) {
- // If it doesn't already have custom_css_classes
- if (strpos($lines[$i], "custom_css_classes") === false) {
- $classContent = $matches[1];
- // Add {{ $data['custom_css_classes'] ?? '' }} to class
- $newClassContent = $classContent . " {{ \$data['custom_css_classes'] ?? '' }}";
-
- // Rebuild the line
- $newLine = preg_replace('/class="[^"]+"/', 'class="' . $newClassContent . '"', $lines[$i]);
-
- // Now add ID if not exists
- if (strpos($newLine, ' id=') === false) {
- // Insert ID before class
- $newLine = preg_replace('/ with
-# We can do a regex replacement.
-content = re.sub(r'(<(?:x-section-|livewire:section-)[a-zA-Z0-9_-]+)', r'\1\n :bg-color="$data[\'background_color\'] ?? null"', content)
-
-# For hardcoded sections in render-block:
-content = re.sub(r' and replace bg-*
- # match ` `]*class="([^"]*?bg-(?:white|background|muted/50|slate-50)[^"]*)"', comp_content)
- if not m:
- return
-
- full_class = m.group(1)
-
- # We want to replace the `bg-*` part
- # Actually, simpler: replace the exact bg class found
- bg_match = re.search(r'(bg-(?:white|background|muted/50|slate-50))', full_class)
- if bg_match:
- orig_bg = bg_match.group(1)
- new_class = full_class.replace(orig_bg, f"{{{{ $bgColor ?? '{orig_bg}' }}}}")
- comp_content = comp_content.replace(f'class="{full_class}"', f'class="{new_class}"')
-
- # Now add bgColor to props
- if '@props' in comp_content:
- # insert into existing @props
- comp_content = re.sub(r'@props\(\[', r"@props([\n 'bgColor' => null,", comp_content, count=1)
- else:
- # add @props at the top
- comp_content = "@props(['bgColor' => null])\n" + comp_content
-
- with open(filepath, 'w') as f:
- f.write(comp_content)
-
-for root, _, files in os.walk('resources/views/components'):
- for file in files:
- if file.startswith('section-') and file.endswith('.blade.php'):
- patch_component(os.path.join(root, file))
-
-for root, _, files in os.walk('resources/views/livewire'):
- for file in files:
- if file.startswith('section-') and file.endswith('.blade.php'):
- patch_component(os.path.join(root, file))
-
-print("Patch complete.")
diff --git a/patch_text_color.py b/patch_text_color.py
deleted file mode 100644
index ba59421f..00000000
--- a/patch_text_color.py
+++ /dev/null
@@ -1,62 +0,0 @@
-import os
-import re
-
-render_block = 'resources/views/components/render-block.blade.php'
-with open(render_block, 'r') as f:
- content = f.read()
-
-# Replace :bg-color="..." with :bg-color="..."\n :text-color="$data['text_color'] ?? 'light'"
-content = re.sub(r'(:bg-color="[^"]*")', r"\1\n :text-color=\"$data['text_color'] ?? 'light'\"", content)
-
-# For hardcoded sections in render-block:
-# match ` 'light',", comp_content, count=1)
-
- # Replace `persistTabInQueryString()
- ->tabs(["""
- wizard_start = """ Forms\\Components\\Wizard::make(["""
- content = content.replace(tabs_start, wizard_start)
-
- # 2. Add Step 1 (Tipo de Seção) and replace Step 2 (Conteúdo)
- tab_conteudo = """ Forms\\Components\\Tabs\\Tab::make('Conteúdo Principal')
- ->icon('heroicon-o-document-text')
- ->schema(["""
-
- step1_and_2 = """ Forms\\Components\\Wizard\\Step::make('1. Tipo da Seção')
- ->icon('heroicon-o-squares-2x2')
- ->description('Escolha o formato e a função da seção.')
- ->schema([
- Forms\\Components\\Section::make('Identificação')
- ->description('Informações básicas de identificação e configuração da seção no sistema.')
- ->icon('heroicon-o-identification')
- ->schema([
- Forms\\Components\\ToggleButtons::make('type')
- ->label('Tipo da seção')
- ->options(ContentSection::typeLabels())
- ->icons([
- ContentSection::TYPE_HERO => 'heroicon-o-star',
- ContentSection::TYPE_PARTNERS => 'heroicon-o-building-office',
- ContentSection::TYPE_COMMERCIAL_PARTNERS => 'heroicon-o-briefcase',
- ContentSection::TYPE_SERVICES => 'heroicon-o-wrench-screwdriver',
- ContentSection::TYPE_FAQ => 'heroicon-o-question-mark-circle',
- ContentSection::TYPE_TESTIMONIALS => 'heroicon-o-chat-bubble-left-right',
- ContentSection::TYPE_COVERAGE => 'heroicon-o-map',
- ContentSection::TYPE_DIFFERENTIALS => 'heroicon-o-sparkles',
- ContentSection::TYPE_TIMELINE => 'heroicon-o-clock',
- ContentSection::TYPE_CTA_CONTACT => 'heroicon-o-phone',
- ContentSection::TYPE_CONTACT_BANNER => 'heroicon-o-megaphone',
- ContentSection::TYPE_BUDGET_FORM => 'heroicon-o-currency-dollar',
- ContentSection::TYPE_CALCULATOR => 'heroicon-o-calculator',
- ContentSection::TYPE_PAYMENT_OFFER => 'heroicon-o-credit-card',
- ContentSection::TYPE_SIMPLE_BANNER => 'heroicon-o-photo',
- ContentSection::TYPE_TEAM => 'heroicon-o-users',
- ContentSection::TYPE_SHOWCASE => 'heroicon-o-camera',
- ContentSection::TYPE_EQUIPMENT_SHOWCASE => 'heroicon-o-truck',
- ])
- ->inline()
- ->columnSpanFull()
- ->required()
- ->disabled(fn (string $operation): bool => $operation === 'edit')
- ->helperText('Define os campos de conteúdo disponíveis. Não pode ser alterado depois de criado.')
- ->live()
- ->afterStateUpdated(function (Forms\\Set $set, ?string $state) {
- if ($state) {
- $set('slug', ContentSection::slugForType($state));
- $set('name', ContentSection::typeLabels()[$state] ?? $state);
- }
- }),
- Forms\\Components\\TextInput::make('slug')
- ->label('Slug (identificador único)')
- ->required()
- ->disabled(fn (string $operation): bool => $operation === 'edit')
- ->unique(ignoreRecord: true)
- ->maxLength(255)
- ->helperText('Identificador no sistema. Não editável após criação.'),
- Forms\\Components\\TextInput::make('name')
- ->label('Nome (admin)')
- ->required()
- ->maxLength(255)
- ->helperText('Nome para identificação interna nesta lista.'),
- ])->columns(2),
- ]),
-
- Forms\\Components\\Wizard\\Step::make('2. Conteúdo Principal')
- ->icon('heroicon-o-document-text')
- ->description('Preencha os textos, imagens e dados da seção.')
- ->schema(["""
- content = content.replace(tab_conteudo, step1_and_2)
-
-
- # 3. Replace the old "Configurações" tab
- old_config_start = """ Forms\\Components\\Tabs\\Tab::make('Configurações')
- ->icon('heroicon-o-cog-6-tooth')
- ->schema(["""
-
- # We will find old_config_start, and then find the next Forms\Components\Section::make('Aparência Global')
- # and replace everything in between.
-
- idx_config_start = content.find(old_config_start)
- if idx_config_start == -1:
- print("Config start not found")
- sys.exit(1)
-
- idx_aparencia_start = content.find(" Forms\\Components\\Section::make('Aparência Global')", idx_config_start)
- if idx_aparencia_start == -1:
- print("Aparencia start not found")
- sys.exit(1)
-
- step3 = """ ]), // Fim do Passo 2
-
- Forms\\Components\\Wizard\\Step::make('3. Aparência e Publicação')
- ->icon('heroicon-o-sparkles')
- ->description('Ajuste o visual geral e publique sua seção.')
- ->schema([
- Forms\\Components\\Section::make('Publicação')
- ->icon('heroicon-o-globe-alt')
- ->schema([
- Forms\\Components\\Toggle::make('is_active')
- ->label('Ativo')
- ->onIcon('heroicon-m-check')->offIcon('heroicon-m-x-mark')
- ->default(true)
- ->helperText('Se inativo, a seção não aparece no site e usa o conteúdo estático.'),
- Forms\\Components\\TextInput::make('sort_order')
- ->label('Ordem')
- ->numeric()
- ->helperText('Ordem na listagem.')
- ->minValue(0),
- ])->columns(2),
-
-"""
-
- content = content[:idx_config_start] + step3 + content[idx_aparencia_start:]
-
- # 4. Replace the closing of the tabs with the closing of the wizard
- end_tabs = " ])->columnSpanFull()"
- end_wizard = " ])->skippable()->columnSpanFull()"
- content = content.replace(end_tabs, end_wizard)
-
- with open('app/Filament/Resources/ContentSectionResource.php', 'w') as f:
- f.write(content)
-
- print("Refactor complete.")
-
-if __name__ == "__main__":
- main()
diff --git a/refactor_wizard_exact.py b/refactor_wizard_exact.py
deleted file mode 100644
index 8d39f8fb..00000000
--- a/refactor_wizard_exact.py
+++ /dev/null
@@ -1,127 +0,0 @@
-import re
-
-def find_matching_bracket(text, start_index, open_bracket='[', close_bracket=']'):
- count = 1
- for i in range(start_index, len(text)):
- if text[i] == open_bracket:
- count += 1
- elif text[i] == close_bracket:
- count -= 1
- if count == 0:
- return i
- return -1
-
-def main():
- with open('app/Filament/Resources/ContentSectionResource.php', 'r') as f:
- text = f.read()
-
- # 1. Extract Conteúdo Principal
- conteudo_start_str = "Forms\\Components\\Tabs\\Tab::make('Conteúdo Principal')\n ->icon('heroicon-o-document-text')\n ->schema(["
- idx_conteudo_start = text.find(conteudo_start_str)
- if idx_conteudo_start == -1:
- print("Conteudo start not found")
- return
- idx_conteudo_start += len(conteudo_start_str)
- idx_conteudo_end = find_matching_bracket(text, idx_conteudo_start)
- conteudo_content = text[idx_conteudo_start:idx_conteudo_end]
-
- # 2. Extract Identificação schema
- identificacao_start_str = "Forms\\Components\\Section::make('Identificação')\n ->description('Informações básicas de identificação e configuração da seção no sistema.')\n ->icon('heroicon-o-identification')\n ->schema(["
- idx_ident_start = text.find(identificacao_start_str)
- idx_ident_start += len(identificacao_start_str)
- idx_ident_end = find_matching_bracket(text, idx_ident_start)
- ident_content = text[idx_ident_start:idx_ident_end]
-
- # 3. Extract Aparência Global schema
- aparencia_start_str = "Forms\\Components\\Section::make('Aparência Global')\n ->description('Configurações visuais gerais aplicadas a esta seção.')\n ->icon('heroicon-o-swatch')\n ->schema(["
- idx_apar_start = text.find(aparencia_start_str)
- idx_apar_start += len(aparencia_start_str)
- idx_apar_end = find_matching_bracket(text, idx_apar_start)
- apar_content = text[idx_apar_start:idx_apar_end]
-
- # Modify ident_content to use ToggleButtons
- ident_content = ident_content.replace(
- "Forms\\Components\\Select::make('type')",
- """Forms\\Components\\ToggleButtons::make('type')
- ->icons([
- ContentSection::TYPE_HERO => 'heroicon-o-star',
- ContentSection::TYPE_PARTNERS => 'heroicon-o-building-office',
- ContentSection::TYPE_COMMERCIAL_PARTNERS => 'heroicon-o-briefcase',
- ContentSection::TYPE_SERVICES => 'heroicon-o-wrench-screwdriver',
- ContentSection::TYPE_FAQ => 'heroicon-o-question-mark-circle',
- ContentSection::TYPE_TESTIMONIALS => 'heroicon-o-chat-bubble-left-right',
- ContentSection::TYPE_COVERAGE => 'heroicon-o-map',
- ContentSection::TYPE_DIFFERENTIALS => 'heroicon-o-sparkles',
- ContentSection::TYPE_TIMELINE => 'heroicon-o-clock',
- ContentSection::TYPE_CTA_CONTACT => 'heroicon-o-phone',
- ContentSection::TYPE_CONTACT_BANNER => 'heroicon-o-megaphone',
- ContentSection::TYPE_BUDGET_FORM => 'heroicon-o-currency-dollar',
- ContentSection::TYPE_CALCULATOR => 'heroicon-o-calculator',
- ContentSection::TYPE_PAYMENT_OFFER => 'heroicon-o-credit-card',
- ContentSection::TYPE_SIMPLE_BANNER => 'heroicon-o-photo',
- ContentSection::TYPE_TEAM => 'heroicon-o-users',
- ContentSection::TYPE_SHOWCASE => 'heroicon-o-camera',
- ContentSection::TYPE_EQUIPMENT_SHOWCASE => 'heroicon-o-truck',
- ])
- ->inline()
- ->columnSpanFull()"""
- )
-
- # Extract Ativo and Ordem from Ident_content
- # They are the last two fields in ident_content
- # We will just split by "Forms\Components\Toggle::make('is_active')"
- ident_parts = ident_content.split("Forms\\Components\\Toggle::make('is_active')")
- new_ident_content = ident_parts[0]
- publicacao_content = "Forms\\Components\\Toggle::make('is_active')" + ident_parts[1]
-
-
- # Now we find the start of the Tabs array and replace it entirely
- tabs_main_str = "Forms\\Components\\Tabs::make('Tabs')\n ->persistTabInQueryString()\n ->tabs(["
- idx_tabs_main = text.find(tabs_main_str)
-
- idx_tabs_content_start = idx_tabs_main + len(tabs_main_str)
- idx_tabs_content_end = find_matching_bracket(text, idx_tabs_content_start)
-
- # We will replace text[idx_tabs_main : idx_tabs_content_end + 25] (up to columnSpanFull())
- # Actually just replace from idx_tabs_main to text.find("->columnSpanFull()", idx_tabs_content_end) + len("->columnSpanFull()")
- idx_end_replace = text.find("->columnSpanFull()", idx_tabs_content_end) + len("->columnSpanFull()")
-
- wizard_code = f"""Forms\\Components\\Wizard::make([
- Forms\\Components\\Wizard\\Step::make('1. Tipo da Seção')
- ->icon('heroicon-o-squares-2x2')
- ->description('Escolha o formato e a função da seção.')
- ->schema([
- Forms\\Components\\Section::make('Identificação')
- ->description('Informações básicas de identificação e configuração da seção no sistema.')
- ->icon('heroicon-o-identification')
- ->schema([{new_ident_content}])->columns(2),
- ]),
- Forms\\Components\\Wizard\\Step::make('2. Conteúdo Principal')
- ->icon('heroicon-o-document-text')
- ->description('Preencha os textos, imagens e dados da seção.')
- ->schema([{conteudo_content}]),
- Forms\\Components\\Wizard\\Step::make('3. Aparência e Publicação')
- ->icon('heroicon-o-sparkles')
- ->description('Ajuste o visual geral e publique sua seção.')
- ->schema([
- Forms\\Components\\Section::make('Publicação')
- ->icon('heroicon-o-globe-alt')
- ->schema([
- {publicacao_content}
- ])->columns(2),
- Forms\\Components\\Section::make('Aparência Global')
- ->description('Configurações visuais gerais aplicadas a esta seção.')
- ->icon('heroicon-o-swatch')
- ->schema([{apar_content}])->columns(2),
- ]),
- ])->skippable()->columnSpanFull()"""
-
- new_text = text[:idx_tabs_main] + wizard_code + text[idx_end_replace:]
-
- with open('app/Filament/Resources/ContentSectionResource.php', 'w') as f:
- f.write(new_text)
-
- print("Success")
-
-if __name__ == "__main__":
- main()
diff --git a/resources/views/livewire/budget.blade.php.bak b/resources/views/livewire/budget.blade.php.bak
deleted file mode 100644
index a56af010..00000000
--- a/resources/views/livewire/budget.blade.php.bak
+++ /dev/null
@@ -1,55 +0,0 @@
-
- @php
- $isPrimaryBg = str_contains($bgColor ?? '', 'bg-primary');
- $accentText = $isPrimaryBg ? 'text-[color-mix(in_srgb,var(--primary),black_85%)]' : 'text-primary';
- $accentBg = $isPrimaryBg ? 'bg-[color-mix(in_srgb,var(--primary),black_85%)] text-white' : 'bg-primary text-primary-foreground';
- $accentLightBg = $isPrimaryBg ? 'bg-black/20' : 'bg-primary/5';
- $accentLightBorder = $isPrimaryBg ? 'border-black/30' : 'border-primary/20';
- @endphp
-
- @if($isSubmitted)
-
-
-
-
-
Pedido Recebido com Sucesso!
-
- Nossa equipe recebeu sua solicitação de orçamento. Em até 24 horas entraremos em contato via WhatsApp ou E-mail para finalizar os detalhes e agendar sua entrega.
-
-
-
- Fazer outro pedido
-
-
-
- @else
-
- @endif
-
- @push('scripts')
- @if($turnstileEnabled)
-
-
- @endif
- @endpush
-
-
-
\ No newline at end of file
diff --git a/storage/app/.gitignore b/storage/app/.gitignore
new file mode 100644
index 00000000..8f4803c0
--- /dev/null
+++ b/storage/app/.gitignore
@@ -0,0 +1,3 @@
+*
+!public/
+!.gitignore
diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/storage/app/public/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore
new file mode 100644
index 00000000..05c4471f
--- /dev/null
+++ b/storage/framework/.gitignore
@@ -0,0 +1,9 @@
+compiled.php
+config.php
+down
+events.scanned.php
+maintenance.php
+routes.php
+routes.scanned.php
+schedule-*
+services.json
diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore
new file mode 100644
index 00000000..01e4a6cd
--- /dev/null
+++ b/storage/framework/cache/.gitignore
@@ -0,0 +1,3 @@
+*
+!data/
+!.gitignore
diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/storage/framework/cache/data/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/storage/framework/sessions/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/storage/framework/testing/.gitignore b/storage/framework/testing/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/storage/framework/testing/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/storage/framework/views/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/storage/logs/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/tailwindcss-config.js b/tailwindcss-config.js
deleted file mode 100644
index c189a4a5..00000000
--- a/tailwindcss-config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-export default {
- content: [],
- theme: {
- extend: {},
- },
- plugins: [],
-}
-
diff --git a/update_builder.php b/update_builder.php
deleted file mode 100644
index 5dd4c22a..00000000
--- a/update_builder.php
+++ /dev/null
@@ -1,20 +0,0 @@
-blockPreviews() to the builder
-$content = str_replace(
- "->blockNumbers(false),",
- "->blockNumbers(false)\n ->blockPreviews(),",
- $content
-);
-
-// Add ->preview() to each block
-$content = preg_replace(
- "/(Forms\\\\Components\\\\Builder\\\\Block::make\('([^']+)'\))/m",
- "$1\n ->preview('filament.block-previews.$2')",
- $content
-);
-
-file_put_contents($file, $content);
-echo "Updated PageResource.php\n";
diff --git a/update_resource.patch b/update_resource.patch
deleted file mode 100644
index c5117f1c..00000000
--- a/update_resource.patch
+++ /dev/null
@@ -1,367 +0,0 @@
---- app/Filament/Resources/ContentSectionResource.php
-+++ app/Filament/Resources/ContentSectionResource.php
-@@ -30,9 +30,17 @@
- return $form
- ->schema([
-- Forms\Components\Section::make('Identificação')
-+ Forms\Components\Tabs::make('Tabs')
-+ ->tabs([
-+ Forms\Components\Tabs\Tab::make('Conteúdo Principal')
-+ ->icon('heroicon-o-document-text')
-+ ->schema([
-+
-+ Forms\Components\Section::make('Cabeçalho (opcional)')
-+ ->description('Subtítulo, título e descrição exibidos no topo da seção.')
-+ ->icon('heroicon-o-bars-3-bottom-left')
- ->schema([
-- Forms\Components\Select::make('type')
-- ->label('Tipo da seção')
-- ->options(ContentSection::typeLabels())
-- ->required()
-- ->live()
-- ->afterStateUpdated(function (Forms\Set $set, ?string $state) {
-- if ($state) {
-- $set('slug', ContentSection::slugForType($state));
-- $set('name', ContentSection::typeLabels()[$state] ?? $state);
-- }
-- }),
-- Forms\Components\TextInput::make('slug')
-- ->label('Slug (identificador único)')
-- ->required()
-- ->unique(ignoreRecord: true)
-- ->maxLength(255)
-- ->helperText('Usado no código para buscar os dados. Ex: faq, partners'),
-- Forms\Components\TextInput::make('name')
-- ->label('Nome (admin)')
-- ->required()
-- ->maxLength(255),
-- Forms\Components\Toggle::make('is_active')
-- ->label('Ativo')
-- ->default(true)
-- ->helperText('Se inativo, a seção não aparece no site e usa o conteúdo estático.'),
-- Forms\Components\TextInput::make('sort_order')
-- ->label('Ordem')
-- ->numeric()
-- ->minValue(0),
-- ])
-- ->columns(2),
--
-- Forms\Components\Section::make('Cabeçalho (opcional)')
-- ->description('Subtítulo, título e descrição exibidos no topo da seção.')
-- ->schema([
- Forms\Components\TextInput::make('content.header.subtitle')
- ->label('Subtítulo')
-+ ->helperText('Aparece com destaque acima do título.')
- ->maxLength(255),
- Forms\Components\TextInput::make('content.header.title')
- ->label('Título')
-+ ->helperText('Título principal da seção.')
- ->maxLength(255),
- Forms\Components\Textarea::make('content.header.description')
- ->label('Descrição')
-+ ->helperText('Texto explicativo ou subtítulo abaixo do título principal.')
- ->rows(2),
- ])
- ->columns(1)
- ->collapsible(),
-
--
--
- // FAQ
- Forms\Components\Section::make('Perguntas e respostas')
-+ ->icon('heroicon-o-question-mark-circle')
- ->schema([
- Forms\Components\Repeater::make('content.items')
- ->label('Itens FAQ')
- ->schema([
-- Forms\Components\TextInput::make('question')->label('Pergunta')->required(),
-- Forms\Components\Textarea::make('answer')->label('Resposta')->required()->rows(2),
-+ Forms\Components\TextInput::make('question')->label('Pergunta')->helperText('A dúvida do usuário.')->required(),
-+ Forms\Components\Textarea::make('answer')->label('Resposta')->helperText('A resposta para a dúvida.')->required()->rows(2),
- ])
- ->columns(1)
- ->itemLabel(fn (array $state): ?string => $state['question'] ?? null),
- ])
- ->visible(fn ($get): bool => $get('type') === ContentSection::TYPE_FAQ)
- ->collapsible(),
-
- // Partners
- Forms\Components\Section::make('Parceiros')
-+ ->icon('heroicon-o-building-office-2')
- ->schema([
- Forms\Components\Repeater::make('content.items')
- ->label('Empresas / Parceiros')
- ->schema([
-- Forms\Components\TextInput::make('name')->label('Nome')->required(),
-+ Forms\Components\TextInput::make('name')->label('Nome')->helperText('Nome do parceiro.')->required(),
- Forms\Components\TextInput::make('icon')
- ->label('Ícone (Lucide)')
-+ ->helperText('Nome do ícone correspondente.')
- ->placeholder('building-2, hard-hat, factory...')
- ->maxLength(50),
- ])
- ->columns(2)
- ->itemLabel(fn (array $state): ?string => $state['name'] ?? null),
- ])
- ->visible(fn ($get): bool => $get('type') === ContentSection::TYPE_PARTNERS)
- ->collapsible(),
-
- // Services
- Forms\Components\Section::make('Serviços')
-+ ->icon('heroicon-o-wrench-screwdriver')
- ->schema([
- Forms\Components\Repeater::make('content.items')
- ->label('Serviços')
- ->schema([
-- Forms\Components\TextInput::make('title')->label('Título')->required(),
-- Forms\Components\TextInput::make('subtitle')->label('Subtítulo (ex: por m³)'),
-- Forms\Components\Textarea::make('description')->label('Descrição')->rows(2),
-- Forms\Components\TextInput::make('icon')->label('Ícone Lucide')->placeholder('droplets, gauge, wrench'),
-- Forms\Components\TagsInput::make('bullets')->label('Lista de itens')->placeholder('Item'),
-- Forms\Components\TextInput::make('cta_label')->label('Texto do link')->default('Solicitar Orçamento'),
-- Forms\Components\TextInput::make('cta_url')->label('URL do link')->default('#orcamento'),
-+ Forms\Components\TextInput::make('title')->label('Título')->helperText('Nome do serviço.')->required(),
-+ Forms\Components\TextInput::make('subtitle')->label('Subtítulo (ex: por m³)')->helperText('Informação extra rápida.'),
-+ Forms\Components\Textarea::make('description')->label('Descrição')->helperText('Detalhes do serviço.')->rows(2),
-+ Forms\Components\TextInput::make('icon')->label('Ícone Lucide')->helperText('Ícone do serviço.')->placeholder('droplets, gauge, wrench'),
-+ Forms\Components\TagsInput::make('bullets')->label('Lista de itens')->helperText('Tags ou tópicos do serviço.')->placeholder('Item'),
-+ Forms\Components\TextInput::make('cta_label')->label('Texto do link')->helperText('O que vai escrito no botão.')->default('Solicitar Orçamento'),
-+ Forms\Components\TextInput::make('cta_url')->label('URL do link')->helperText('Para onde o botão leva.')->default('#orcamento'),
- ])
- ->columns(1)
- ->itemLabel(fn (array $state): ?string => $state['title'] ?? null),
- ])
- ->visible(fn ($get): bool => $get('type') === ContentSection::TYPE_SERVICES)
- ->collapsible(),
-
- // Testimonials
- Forms\Components\Section::make('Depoimentos')
-+ ->icon('heroicon-o-chat-bubble-left-right')
- ->schema([
- Forms\Components\Repeater::make('content.items')
- ->label('Depoimentos')
- ->schema([
-- Forms\Components\Textarea::make('quote')->label('Citação')->required()->rows(3),
-- Forms\Components\TextInput::make('author_name')->label('Nome do autor')->required(),
-- Forms\Components\TextInput::make('author_role')->label('Cargo / Obra'),
-- Forms\Components\TextInput::make('stars')->label('Estrelas (1-5)')->numeric()->minValue(1)->maxValue(5)->default(5),
-+ Forms\Components\Textarea::make('quote')->label('Citação')->helperText('O texto do depoimento.')->required()->rows(3),
-+ Forms\Components\TextInput::make('author_name')->label('Nome do autor')->helperText('Pessoa que deu o depoimento.')->required(),
-+ Forms\Components\TextInput::make('author_role')->label('Cargo / Obra')->helperText('Ex: Cliente Codhous'),
-+ Forms\Components\TextInput::make('stars')->label('Estrelas (1-5)')->helperText('Nota de 1 a 5.')->numeric()->minValue(1)->maxValue(5)->default(5),
- ])
- ->columns(1)
- ->itemLabel(fn (array $state): ?string => $state['author_name'] ?? null),
- ])
- ->visible(fn ($get): bool => $get('type') === ContentSection::TYPE_TESTIMONIALS)
- ->collapsible(),
-
- // Coverage
- Forms\Components\Section::make('Onde atuamos')
-+ ->icon('heroicon-o-map-pin')
- ->schema([
- Forms\Components\Select::make('content.cities')
- ->label('Cidades Atendidas')
- ->multiple()
- ->options(\App\Models\OperationArea::query()->where('is_active', true)->pluck('city', 'city'))
- ->helperText('Selecione as cidades que deseja destacar. Os dados vêm do módulo de Áreas de Operação.'),
- Forms\Components\Repeater::make('content.sidebar')
- ->label('Cards laterais')
- ->schema([
-- Forms\Components\TextInput::make('title')->label('Título')->required(),
-- Forms\Components\Textarea::make('description')->label('Descrição')->rows(2),
-+ Forms\Components\TextInput::make('title')->label('Título')->helperText('Título do card.')->required(),
-+ Forms\Components\Textarea::make('description')->label('Descrição')->helperText('Descrição do card.')->rows(2),
- ])
- ->columns(1)
- ->itemLabel(fn (array $state): ?string => $state['title'] ?? null),
- ])
- ->visible(fn ($get): bool => $get('type') === ContentSection::TYPE_COVERAGE)
- ->collapsible(),
-
- // Differentials
- Forms\Components\Section::make('Diferenciais')
-+ ->icon('heroicon-o-star')
- ->schema([
- Forms\Components\Repeater::make('content.items')
- ->label('Itens')
- ->schema([
-- Forms\Components\TextInput::make('icon')->label('Ícone Lucide')->placeholder('clock, microscope'),
-- Forms\Components\TextInput::make('title')->label('Título')->required(),
-- Forms\Components\Textarea::make('description')->label('Descrição')->rows(2),
-+ Forms\Components\TextInput::make('icon')->label('Ícone Lucide')->helperText('Ícone do diferencial.')->placeholder('clock, microscope'),
-+ Forms\Components\TextInput::make('title')->label('Título')->helperText('Nome do diferencial.')->required(),
-+ Forms\Components\Textarea::make('description')->label('Descrição')->helperText('O que isso significa.')->rows(2),
- ])
- ->columns(1)
- ->itemLabel(fn (array $state): ?string => $state['title'] ?? null),
- ])
- ->visible(fn ($get): bool => $get('type') === ContentSection::TYPE_DIFFERENTIALS)
- ->collapsible(),
-
- // Timeline
- Forms\Components\Section::make('Etapas (Como funciona)')
-+ ->icon('heroicon-o-clock')
- ->schema([
- Forms\Components\Repeater::make('content.steps')
- ->label('Etapas')
- ->schema([
-- Forms\Components\TextInput::make('step_label')->label('Rótulo (ex: Etapa 1)')->required(),
-- Forms\Components\TextInput::make('title')->label('Título')->required(),
-- Forms\Components\Textarea::make('description')->label('Descrição')->rows(2),
-- Forms\Components\TextInput::make('icon')->label('Ícone Lucide')->placeholder('message-square-text, truck'),
-+ Forms\Components\TextInput::make('step_label')->label('Rótulo (ex: Etapa 1)')->helperText('Indicador da etapa.')->required(),
-+ Forms\Components\TextInput::make('title')->label('Título')->helperText('Nome da etapa.')->required(),
-+ Forms\Components\Textarea::make('description')->label('Descrição')->helperText('Explicação do que ocorre.')->rows(2),
-+ Forms\Components\TextInput::make('icon')->label('Ícone Lucide')->helperText('Ícone da etapa.')->placeholder('message-square-text, truck'),
- ])
- ->columns(1)
- ->itemLabel(fn (array $state): ?string => $state['title'] ?? null),
- ])
- ->visible(fn ($get): bool => $get('type') === ContentSection::TYPE_TIMELINE)
- ->collapsible(),
-
- // Contato
- Forms\Components\Section::make('Contato')
-+ ->icon('heroicon-o-envelope')
- ->schema([
- Forms\Components\TextInput::make('content.title')
- ->label('Título Principal')
-+ ->helperText('Título do formulário de contato.')
- ->default('Entre em Contato'),
- Forms\Components\Textarea::make('content.description')
- ->label('Descrição / Subtítulo')
-+ ->helperText('Breve mensagem convidando para o contato.')
- ->rows(2),
- Forms\Components\TextInput::make('content.email_to')
- ->label('E-mail de Destino (opcional)')
- ->helperText('Se não preenchido, enviará para o e-mail padrão da empresa.'),
- Forms\Components\Fieldset::make('Botão Orçamento')
- ->schema([
- Forms\Components\Toggle::make('content.budget_btn_enabled')
- ->label('Mostrar botão')
-+ ->helperText('Liga ou desliga esse botão.')
- ->default(true),
- Forms\Components\TextInput::make('content.budget_btn_title')
- ->label('Título')
-+ ->helperText('Texto maior.')
- ->default('Orçamento Grátis Online'),
- Forms\Components\TextInput::make('content.budget_btn_subtitle')
- ->label('Subtítulo')
-+ ->helperText('Texto menor.')
- ->default('Faça uma cotação rápida agora'),
-+ Forms\Components\TextInput::make('content.budget_btn_url')
-+ ->label('Link do Botão')
-+ ->helperText('URL ou ID da página para redirecionar. Ex: /#orcamento')
-+ ->default('/#orcamento')
-+ ->columnSpanFull(),
- ])->columns(3),
- Forms\Components\Fieldset::make('Botões Adicionais')
- ->schema([
- Forms\Components\Toggle::make('content.whatsapp_btn_enabled')
- ->label('Mostrar WhatsApp')
-+ ->helperText('Puxa o número das configurações do site.')
- ->default(true),
- Forms\Components\Toggle::make('content.email_btn_enabled')
- ->label('Mostrar E-mail')
-+ ->helperText('Mostra o e-mail cadastrado acima.')
- ->default(true),
- Forms\Components\Toggle::make('content.phone_btn_enabled')
- ->label('Mostrar Telefone')
-+ ->helperText('Puxa do cadastro da empresa.')
- ->default(true),
- Forms\Components\Toggle::make('content.address_enabled')
- ->label('Mostrar Endereço')
-+ ->helperText('Puxa do cadastro da empresa.')
- ->default(true),
- ])->columns(4),
- ])
- ->visible(fn ($get): bool => $get('type') === ContentSection::TYPE_CTA_CONTACT)
- ->collapsible(),
-
- // Contact Banner (Atendimento)
- Forms\Components\Section::make('Banner de Atendimento')
-+ ->icon('heroicon-o-megaphone')
- ->schema([
- Forms\Components\TextInput::make('content.badge')
- ->label('Badge (Texto Superior)')
-+ ->helperText('Exibido pequeno acima do título.')
- ->placeholder('ATENDIMENTO')
- ->default('ATENDIMENTO'),
- Forms\Components\TextInput::make('content.title')
- ->label('Título')
-+ ->helperText('Chamada principal.')
- ->default('Fale conosco')
- ->required(),
- Forms\Components\Textarea::make('content.description')
- ->label('Descrição')
-+ ->helperText('Texto de apoio.')
- ->default('Dúvidas, orçamento ou suporte: estamos prontos para atender você por telefone, WhatsApp ou e-mail.')
- ->rows(2),
- Forms\Components\Grid::make(3)
- ->schema([
- Forms\Components\Toggle::make('content.whatsapp_enabled')
- ->label('Botão WhatsApp')
-+ ->helperText('Ativa o botão do WhatsApp.')
- ->default(true),
- Forms\Components\Toggle::make('content.call_enabled')
- ->label('Botão Ligar')
-+ ->helperText('Ativa o botão de ligação.')
- ->default(true),
- Forms\Components\Toggle::make('content.email_enabled')
- ->label('Botão E-mail')
-+ ->helperText('Ativa o botão de e-mail.')
- ->default(true),
- ]),
- ])
- ->visible(fn ($get): bool => $get('type') === ContentSection::TYPE_CONTACT_BANNER)
- ->collapsible(),
-+ ]),
-+
-+ Forms\Components\Tabs\Tab::make('Configurações')
-+ ->icon('heroicon-o-cog-6-tooth')
-+ ->schema([
-+ Forms\Components\Section::make('Identificação')
-+ ->icon('heroicon-o-identification')
-+ ->schema([
-+ Forms\Components\Select::make('type')
-+ ->label('Tipo da seção')
-+ ->options(ContentSection::typeLabels())
-+ ->required()
-+ ->disabled(fn (string $operation): bool => $operation === 'edit')
-+ ->helperText('Define os campos de conteúdo disponíveis. Não pode ser alterado depois de criado.')
-+ ->live()
-+ ->afterStateUpdated(function (Forms\Set $set, ?string $state) {
-+ if ($state) {
-+ $set('slug', ContentSection::slugForType($state));
-+ $set('name', ContentSection::typeLabels()[$state] ?? $state);
-+ }
-+ }),
-+ Forms\Components\TextInput::make('slug')
-+ ->label('Slug (identificador único)')
-+ ->required()
-+ ->disabled(fn (string $operation): bool => $operation === 'edit')
-+ ->unique(ignoreRecord: true)
-+ ->maxLength(255)
-+ ->helperText('Identificador no sistema. Não editável após criação.'),
-+ Forms\Components\TextInput::make('name')
-+ ->label('Nome (admin)')
-+ ->required()
-+ ->maxLength(255)
-+ ->helperText('Nome para identificação interna nesta lista.'),
-+ Forms\Components\Toggle::make('is_active')
-+ ->label('Ativo')
-+ ->default(true)
-+ ->helperText('Se inativo, a seção não aparece no site e usa o conteúdo estático.'),
-+ Forms\Components\TextInput::make('sort_order')
-+ ->label('Ordem')
-+ ->numeric()
-+ ->helperText('Ordem na listagem.')
-+ ->minValue(0),
-+ ])
-+ ->columns(2),
-+ ]),
-+ ])
-+ ->columnSpanFull(),
- ]);
- }