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( - '' - ) - ), - ]) - ->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) - - @endforeach -
- - -
- @foreach($groupedBlocks as $cat => $catBlocks) -
-
-

{{ $cat }}

-
-
-HTML; - -$replace = <<<'HTML' -
- -
-
Categorias
- @foreach($groupedBlocks as $cat => $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 @@ - -
- - - - -
-
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. -

-
- -
-
- @else -
- {{ $this->form }} -
- @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(), - ]); - }