diff --git a/MASON_REFACTOR_PLAN.md b/MASON_REFACTOR_PLAN.md new file mode 100644 index 000000000..94d1cb594 --- /dev/null +++ b/MASON_REFACTOR_PLAN.md @@ -0,0 +1,642 @@ +# Mason Package Integration Plan + +## Status: READY FOR IMPLEMENTATION + +This document provides a comprehensive plan to refactor the InvoicePlane v2 ReportBuilder to use the `awcodes/mason` package instead of the current custom implementation. + +**Package Added**: `composer.json` has been updated to require `awcodes/mason:^3.0` + +**Next Step**: Install the package in a local environment (CI has GitHub auth limitations) and begin implementing the brick classes. + +## Overview +This document outlines the plan to refactor the InvoicePlane v2 ReportBuilder to use the `awcodes/mason` package instead of the current custom implementation. + +## Installation + +```bash +composer require awcodes/mason:^3.0 +``` + +### Theme Setup +Since mason requires a custom Filament theme, add to `resources/css/filament/admin/theme.css`: + +```css +@import '../../../../vendor/awcodes/mason/resources/css/plugin.css'; +``` + +And update `tailwind.config.js`: + +```js +content: [ + // ... existing content paths + './vendor/awcodes/mason/resources/**/*.blade.php', +], +``` + +## Architecture Changes + +### Current Structure +- `BlockDTO` - Data transfer object for blocks +- `ReportBuilder` Page - Custom drag-drop with Alpine.js +- `ReportTemplateService` - Manages block persistence to JSON files +- Block handlers for different block types + +### New Structure with Mason +- **Brick Classes** - Replace BlockDTO with Mason Brick classes +- **BricksCollection** - Group bricks by category +- **Mason Field** - Replace custom drag-drop interface +- **Blade Views** - Preview and render templates for each brick +- **Maintain JSON Storage** - Keep filesystem storage (not database) + +## Implementation Steps + +### 1. Create Brick Classes for Each Block Type + +Create directory: `app/Mason/Bricks/` + +Each current `ReportBlockType` becomes a Brick: + +#### Example: HeaderCompanyBrick.php +```php + $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.header-company.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_company_header')) + ->modalHeading(trans('ip.company_header_settings')) + ->icon('heroicon-o-building-office') + ->slideOver() + ->fillForm(fn (array $arguments): array => [ + 'show_logo' => $arguments['show_logo'] ?? true, + 'show_vat_id' => $arguments['show_vat_id'] ?? true, + 'show_phone' => $arguments['show_phone'] ?? true, + 'show_email' => $arguments['show_email'] ?? true, + 'font_size' => $arguments['font_size'] ?? 10, + ]) + ->schema([ + Checkbox::make('show_logo') + ->label(trans('ip.show_logo')) + ->default(true), + Checkbox::make('show_vat_id') + ->label(trans('ip.show_vat_id')) + ->default(true), + Checkbox::make('show_phone') + ->label(trans('ip.show_phone')) + ->default(true), + Checkbox::make('show_email') + ->label(trans('ip.show_email')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(10) + ->minValue(8) + ->maxValue(16), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} +``` + +#### Brick Classes Needed +Based on `ReportBlockType` enum: +- `HeaderCompanyBrick` - Company header +- `HeaderClientBrick` - Client header +- `HeaderInvoiceMetaBrick` - Invoice metadata +- `DetailItemsBrick` - Line items table +- `DetailItemTaxBrick` - Tax breakdown +- `FooterTotalsBrick` - Totals section +- `FooterNotesBrick` - Footer notes +- `FooterQrCodeBrick` - QR code +- `CustomTextBrick` - Custom text block +- `CustomImageBrick` - Custom image block + +### 2. Create Blade Views + +Create directory: `resources/views/mason/bricks/` + +For each brick, create two views: +- `preview.blade.php` - Shown in editor +- `index.blade.php` - Final render on PDF + +#### Example: header-company/preview.blade.php +```blade +@props([ + 'config' => [] +]) + +
+
+ @if($config['show_logo'] ?? true) +
+ {{ trans('ip.logo') }} +
+ @endif +
+

{{ trans('ip.company_name') }}

+

{{ trans('ip.company_address') }}

+ @if($config['show_phone'] ?? true) +

{{ trans('ip.phone') }}: +1 234 567 890

+ @endif + @if($config['show_email'] ?? true) +

{{ trans('ip.email') }}: info@company.com

+ @endif + @if($config['show_vat_id'] ?? true) +

{{ trans('ip.vat_id') }}: 12345678

+ @endif +
+
+
+``` + +#### Example: header-company/index.blade.php +```blade +@props([ + 'config' => [], + 'data' => [] +]) + +
+ + + @if($config['show_logo'] ?? true) + + @endif + + +
+ @if(isset($data['company']['logo_path'])) + Logo + @endif + + {{ $data['company']['name'] ?? '' }}
+ {{ $data['company']['address'] ?? '' }}
+ @if($config['show_phone'] ?? true) + {{ trans('ip.phone') }}: {{ $data['company']['phone'] ?? '' }}
+ @endif + @if($config['show_email'] ?? true) + {{ trans('ip.email') }}: {{ $data['company']['email'] ?? '' }}
+ @endif + @if($config['show_vat_id'] ?? true) + {{ trans('ip.vat_id') }}: {{ $data['company']['vat_id'] ?? '' }}
+ @endif +
+
+``` + +### 3. Create BricksCollection + +Create: `app/Mason/Collections/ReportBricksCollection.php` + +```php +record = $record; + } + + protected function getSchema(): Schema + { + return Schema::make([ + Mason::make('content') + ->label(trans('ip.report_layout')) + ->bricks(ReportBricksCollection::all()) + ->previewLayout('layouts.mason-preview') + ->doubleClickToEdit() + ->sortBricks() + ->displayActionsAsGrid() + ->extraInputAttributes(['style' => 'min-height: 40rem;']) + ->statePath('content'), + ]); + } + + public function save(): void + { + $data = $this->schema->getState(); + $service = app(ReportTemplateService::class); + + // Convert Mason JSON to our block structure and persist to filesystem + $blocks = $this->convertMasonDataToBlocks($data['content']); + $service->persistBlocks($this->record, $blocks); + + $this->dispatch('blocks-saved'); + } + + protected function convertMasonDataToBlocks(string $masonJson): array + { + $masonData = json_decode($masonJson, true); + $blocks = []; + + // Transform Mason's structure to our block structure + foreach ($masonData['content'] ?? [] as $item) { + if ($item['type'] === 'masonBrick') { + $attrs = $item['attrs']; + $blocks[$attrs['id']] = [ + 'id' => $attrs['id'], + 'type' => $this->extractBrickType($attrs['id']), + 'config' => $attrs['config'], + 'label' => $attrs['label'], + // ... map other properties + ]; + } + } + + return $blocks; + } + + protected function extractBrickType(string $brickId): string + { + // Extract type from brick ID (e.g., "header_company_xyz" -> "header_company") + return preg_replace('/_[a-z0-9]+$/', '', $brickId); + } +} +``` + +### 5. Create Preview Layout + +Create: `resources/views/layouts/mason-preview.blade.php` + +```blade + + + + + + {{ config('app.name') }} - {{ trans('ip.report_preview') }} + + @vite(['resources/css/app.css', 'resources/js/app.js']) + @masonStyles + + + + +
+ @include('mason::iframe-preview-content', ['blocks' => $blocks]) +
+ + +``` + +### 6. Update Blade Template View + +Update: `resources/views/core/filament/admin/resources/report-template-resource/pages/design-report-template.blade.php` + +Replace the current custom drag-drop interface with the Mason field from the schema. + +### 7. Maintain Filesystem Storage + +The key requirement is to **NOT** store blocks in the database. Continue using the current JSON file storage system via `ReportTemplateFileRepository`. + +**Adapter Pattern**: Create a service to convert between Mason's JSON format and our block structure: + +Create: `Modules/Core/Services/MasonStorageAdapter.php` + +```php +createBlockFromMasonBrick($item['attrs']); + $blocks[$block->getId()] = $block; + } + } + + return $blocks; + } + + /** + * Convert Block DTOs to Mason JSON for editor + */ + public function blocksToMason(array $blockDTOs): string + { + $content = []; + + foreach ($blockDTOs as $blockDTO) { + $content[] = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => $blockDTO->getId(), + 'config' => $blockDTO->getConfig(), + 'label' => $blockDTO->getLabel(), + 'preview' => base64_encode($this->generatePreview($blockDTO)), + ], + ]; + } + + return json_encode([ + 'type' => 'doc', + 'content' => $content, + ]); + } + + protected function createBlockFromMasonBrick(array $attrs): BlockDTO + { + // Transform Mason brick attrs to BlockDTO + // Implementation details... + } + + protected function generatePreview(BlockDTO $block): string + { + // Generate preview HTML for the block + // Implementation details... + } +} +``` + +### 8. Update Translation Keys + +Add to `resources/lang/en/ip.php`: + +```php +// Mason Report Builder +'report_layout' => 'Report Layout', +'report_preview' => 'Report Preview', +'configure_company_header' => 'Configure Company Header', +'company_header_settings' => 'Company Header Settings', +'show_logo' => 'Show Logo', +'show_vat_id' => 'Show VAT ID', +'show_phone' => 'Show Phone', +'show_email' => 'Show Email', +'font_size' => 'Font Size', +// ... add translations for all bricks +``` + +### 9. Testing Updates + +Update tests to work with Mason structure: + +- `ReportBuilderFieldCanvasIntegrationTest.php` +- `ReportBuilderBlockWidthTest.php` +- `ReportBuilderBlockEditTest.php` + +Use Mason's `Faker` helper for generating test data: + +```php +use Awcodes\Mason\Support\Faker; + +$content = Faker::make() + ->brick( + id: 'header_company', + config: [ + 'show_logo' => true, + 'show_vat_id' => true, + 'font_size' => 10, + ] + ) + ->asJson(); +``` + +## Migration Strategy + +### Phase 1: Install and Setup +1. Install mason package +2. Configure theme and assets +3. Create preview layout + +### Phase 2: Create Bricks +1. Create all brick classes +2. Create preview views +3. Create render views +4. Test each brick individually + +### Phase 3: Integration +1. Create BricksCollection +2. Update ReportBuilder page +3. Create MasonStorageAdapter +4. Wire up save functionality + +### Phase 4: Testing +1. Update all tests +2. Test drag-drop functionality +3. Test persistence to JSON +4. Test PDF generation with new structure + +### Phase 5: Cleanup +1. Remove old custom drag-drop code +2. Remove unused DTOs (if any) +3. Update documentation +4. Run linters and fix issues + +## Benefits + +1. **Drag-and-Drop**: Native, battle-tested drag-drop UI from mason +2. **Maintainability**: Less custom code to maintain +3. **Filament Integration**: Better integration with Filament ecosystem +4. **Extensibility**: Easy to add new brick types +5. **User Experience**: More polished UI with slideOver configs +6. **Testing**: Mason provides faker helpers for testing + +## Considerations + +1. **Learning Curve**: Team needs to understand Mason's API +2. **Migration**: Existing templates need conversion +3. **Customization**: Some custom features may need workarounds +4. **Bundle Size**: Mason adds additional JS/CSS assets +5. **Storage**: Need adapter to maintain JSON file storage + +## Next Steps + +1. Install package locally +2. Create first brick (HeaderCompanyBrick) +3. Test basic drag-drop functionality +4. Implement storage adapter +5. Iterate on remaining bricks diff --git a/Modules/Core/Filament/Admin/Resources/ReportTemplates/Pages/ReportBuilder.php b/Modules/Core/Filament/Admin/Resources/ReportTemplates/Pages/ReportBuilder.php index a671e110d..d9f06ce38 100644 --- a/Modules/Core/Filament/Admin/Resources/ReportTemplates/Pages/ReportBuilder.php +++ b/Modules/Core/Filament/Admin/Resources/ReportTemplates/Pages/ReportBuilder.php @@ -2,24 +2,22 @@ namespace Modules\Core\Filament\Admin\Resources\ReportTemplates\Pages; +use App\Mason\Collections\ReportBricksCollection; use BackedEnum; use Filament\Actions\Action; use Filament\Actions\Concerns\InteractsWithActions; +use Awcodes\Mason\Mason as MasonEditor; use Filament\Resources\Pages\Page; use Filament\Schemas\Concerns\InteractsWithSchemas; use Filament\Schemas\Schema; use Filament\Support\Enums\Width; use Illuminate\Support\Str; use Livewire\Attributes\On; -use Modules\Core\DTOs\BlockDTO; -use Modules\Core\DTOs\GridPositionDTO; use Modules\Core\Filament\Admin\Resources\ReportBlocks\Schemas\ReportBlockForm; use Modules\Core\Filament\Admin\Resources\ReportTemplates\ReportTemplateResource; use Modules\Core\Models\ReportBlock; use Modules\Core\Models\ReportTemplate; -use Modules\Core\Services\GridSnapperService; -use Modules\Core\Services\ReportTemplateService; -use Modules\Core\Transformers\BlockTransformer; +use Modules\Core\Services\MasonTemplateStorage; class ReportBuilder extends Page { @@ -28,7 +26,7 @@ class ReportBuilder extends Page public ReportTemplate $record; - public array $blocks = []; + public string $masonContent = ''; public string $selectedBlockId = ''; @@ -45,8 +43,22 @@ public function getMaxContentWidth(): string public function mount(ReportTemplate $record): void { + $this->authorize(); $this->record = $record; - $this->loadBlocks(); + $this->loadMasonContent(); + } + + /** + * Authorize access to the report builder. + * Only admin and superadmin roles can access. + */ + protected function authorize(): void + { + $user = auth()->user(); + + if (!$user || !($user->hasRole('admin') || $user->hasRole('superadmin'))) { + abort(403, 'Unauthorized access to Report Builder.'); + } } public function setCurrentBlockId(?string $blockId): void @@ -414,84 +426,47 @@ public function updateBlockConfig(string $blockId, array $config): void ); } - public function save($bands): void + public function save($content): void { - // $bands is already grouped by band from Alpine.js - $blocks = []; - foreach ($bands as $band) { - if ( ! isset($band['blocks'])) { - continue; - } - foreach ($band['blocks'] as $block) { - // Ensure the block data has all necessary fields before passing to service - if ( ! isset($block['type'])) { - $systemBlocks = app(ReportTemplateService::class)->getSystemBlocks(); - $type = str_replace('block_', '', $block['id']); - if (isset($systemBlocks[$type])) { - $block = BlockTransformer::toArray($systemBlocks[$type]); - } - } - - $block['band'] = $band['key'] ?? 'header'; - $blocks[$block['id']] = $block; - } + // Mason-based save: Store JSON directly + if (is_string($content)) { + $storage = app(MasonTemplateStorage::class); + $storage->save($this->record, $content); + + $this->masonContent = $content; + $this->dispatch('blocks-saved'); } - $this->blocks = $blocks; - $service = app(ReportTemplateService::class); - $service->persistBlocks($this->record, $this->blocks); - $this->dispatch('blocks-saved'); } - public function saveBlockConfiguration(string $blockType, array $config): void + /** + * Load Mason editor content from filesystem. + */ + protected function loadMasonContent(): void { - $service = app(ReportTemplateService::class); - $dbBlock = ReportBlock::where('block_type', $blockType)->first(); - - if ($dbBlock) { - $service->saveBlockConfig($dbBlock, $config); - $this->dispatch('block-config-saved'); - } + $storage = app(MasonTemplateStorage::class); + $this->masonContent = $storage->load($this->record); } - public function getAvailableFields(): array + /** + * Get Mason editor configuration. + */ + public function getMasonEditorSchema(): array { return [ - ['id' => 'company_name', 'label' => 'Company Name'], - ['id' => 'company_address', 'label' => 'Company Address'], - ['id' => 'company_phone', 'label' => 'Company Phone'], - ['id' => 'company_email', 'label' => 'Company Email'], - ['id' => 'company_vat_id', 'label' => 'Company VAT ID'], - ['id' => 'client_name', 'label' => 'Client Name'], - ['id' => 'client_address', 'label' => 'Client Address'], - ['id' => 'client_phone', 'label' => 'Client Phone'], - ['id' => 'client_email', 'label' => 'Client Email'], - ['id' => 'invoice_number', 'label' => 'Invoice Number'], - ['id' => 'invoice_date', 'label' => 'Invoice Date'], - ['id' => 'invoice_due_date', 'label' => 'Due Date'], - ['id' => 'invoice_subtotal', 'label' => 'Subtotal'], - ['id' => 'invoice_tax_total', 'label' => 'Tax Total'], - ['id' => 'invoice_total', 'label' => 'Invoice Total'], - ['id' => 'item_description', 'label' => 'Item Description'], - ['id' => 'item_quantity', 'label' => 'Item Quantity'], - ['id' => 'item_price', 'label' => 'Item Price'], - ['id' => 'item_tax_name', 'label' => 'Item Tax Name'], - ['id' => 'item_tax_rate', 'label' => 'Item Tax Rate'], - ['id' => 'footer_notes', 'label' => 'Notes'], + MasonEditor::make('masonContent') + ->label(trans('ip.report_layout')) + ->bricks(ReportBricksCollection::all()) + ->preview(route('mason.preview')) + ->dehydrated() + ->required(), ]; } /** - * Loads the template blocks from the filesystem via the service. + * Get available bricks for Mason editor. */ - protected function loadBlocks(): void + public function getAvailableBricks(): array { - $service = app(ReportTemplateService::class); - $blockDTOs = $service->loadBlocks($this->record); - - $this->blocks = []; - foreach ($blockDTOs as $blockDTO) { - $blockArray = BlockTransformer::toArray($blockDTO); - $this->blocks[$blockArray['id']] = $blockArray; - } + return ReportBricksCollection::all(); } } diff --git a/Modules/Core/Services/MasonStorageAdapter.php b/Modules/Core/Services/MasonStorageAdapter.php new file mode 100644 index 000000000..dd0ca83dd --- /dev/null +++ b/Modules/Core/Services/MasonStorageAdapter.php @@ -0,0 +1,177 @@ + Array of BlockDTOs keyed by block ID + */ + public function masonToBlocks(string $masonJson): array + { + $masonData = json_decode($masonJson, true); + $blocks = []; + + if (!isset($masonData['content']) || !is_array($masonData['content'])) { + return $blocks; + } + + foreach ($masonData['content'] as $item) { + if (($item['type'] ?? null) === 'masonBrick') { + $attrs = $item['attrs'] ?? []; + $block = $this->createBlockFromMasonBrick($attrs); + + if ($block) { + $blocks[$block->getId()] = $block; + } + } + } + + return $blocks; + } + + /** + * Convert Block DTOs to Mason JSON for editor. + * + * @param array $blockDTOs Array of BlockDTOs + * @return string Mason-compatible JSON + */ + public function blocksToMason(array $blockDTOs): string + { + $content = []; + + foreach ($blockDTOs as $blockDTO) { + $content[] = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => $blockDTO->getId(), + 'config' => $blockDTO->getConfig() ?? [], + 'label' => $blockDTO->getLabel() ?? $this->getLabelForType($blockDTO->getType()), + 'preview' => base64_encode($this->generatePreview($blockDTO)), + ], + ]; + } + + return json_encode([ + 'type' => 'doc', + 'content' => $content, + ], JSON_PRETTY_PRINT); + } + + /** + * Create BlockDTO from Mason brick attributes. + * + * @param array $attrs Mason brick attributes + * @return BlockDTO|null + */ + protected function createBlockFromMasonBrick(array $attrs): ?BlockDTO + { + $id = $attrs['id'] ?? null; + $config = $attrs['config'] ?? []; + $label = $attrs['label'] ?? ''; + + if (!$id) { + return null; + } + + // Extract type from brick ID (e.g., "header_company_xyz123" -> "header_company") + $type = $this->extractTypeFromId($id); + + // Create position DTO with defaults + $position = GridPositionDTO::create(0, 0, 12, 4); + + $block = new BlockDTO(); + $block->setId($id) + ->setType($type) + ->setSlug(null) + ->setPosition($position) + ->setConfig($config) + ->setLabel($label) + ->setIsCloneable(false) + ->setDataSource($this->getDataSourceForType($type)) + ->setIsCloned(false) + ->setClonedFrom(null); + + return $block; + } + + /** + * Extract block type from Mason brick ID. + * + * @param string $brickId Mason brick ID (e.g., "header_company_abc123") + * @return string Block type (e.g., "header_company") + */ + protected function extractTypeFromId(string $brickId): string + { + // Remove trailing random suffix if present + return preg_replace('/_[a-z0-9]+$/i', '', $brickId); + } + + /** + * Get human-readable label for a block type. + * + * @param string $type Block type + * @return string Label + */ + protected function getLabelForType(string $type): string + { + return match($type) { + 'header_company' => trans('ip.company_header'), + 'header_client' => trans('ip.client_header'), + 'header_invoice_meta' => trans('ip.invoice_metadata'), + 'detail_items' => trans('ip.line_items_table'), + 'footer_totals' => trans('ip.totals_section'), + 'footer_notes' => trans('ip.footer_notes'), + default => ucfirst(str_replace('_', ' ', $type)), + }; + } + + /** + * Get data source for a block type. + * + * @param string $type Block type + * @return string Data source + */ + protected function getDataSourceForType(string $type): string + { + return match(true) { + str_starts_with($type, 'header_company') => 'company', + str_starts_with($type, 'header_client') => 'client', + str_starts_with($type, 'header_invoice') => 'invoice', + str_starts_with($type, 'detail_') => 'items', + str_starts_with($type, 'footer_') => 'invoice', + default => 'custom', + }; + } + + /** + * Generate preview HTML for a block (placeholder implementation). + * + * @param BlockDTO $block Block DTO + * @return string Preview HTML + */ + protected function generatePreview(BlockDTO $block): string + { + // This would render the appropriate preview view for the block type + $type = $block->getType(); + $config = $block->getConfig() ?? []; + + // Simplified preview generation + return sprintf( + '
%s
', + htmlspecialchars($block->getLabel() ?? 'Block', ENT_QUOTES) + ); + } +} diff --git a/Modules/Core/Services/MasonTemplateStorage.php b/Modules/Core/Services/MasonTemplateStorage.php new file mode 100644 index 000000000..636e21588 --- /dev/null +++ b/Modules/Core/Services/MasonTemplateStorage.php @@ -0,0 +1,78 @@ +getTemplatePath($template); + Storage::disk('report_templates')->put($path, $masonJson); + } + + /** + * Load Mason editor content from filesystem. + */ + public function load(ReportTemplate $template): string + { + $path = $this->getTemplatePath($template); + + if (!Storage::disk('report_templates')->exists($path)) { + return $this->getEmptyTemplate(); + } + + return Storage::disk('report_templates')->get($path); + } + + /** + * Check if template exists. + */ + public function exists(ReportTemplate $template): bool + { + $path = $this->getTemplatePath($template); + return Storage::disk('report_templates')->exists($path); + } + + /** + * Delete template. + */ + public function delete(ReportTemplate $template): bool + { + $path = $this->getTemplatePath($template); + + if (!$this->exists($template)) { + return false; + } + + return Storage::disk('report_templates')->delete($path); + } + + /** + * Get the template file path. + */ + protected function getTemplatePath(ReportTemplate $template): string + { + return "{$template->company_id}/mason_{$template->slug}.json"; + } + + /** + * Get an empty Mason template structure. + */ + protected function getEmptyTemplate(): string + { + return json_encode([ + 'type' => 'doc', + 'content' => [], + ], JSON_PRETTY_PRINT); + } +} diff --git a/Modules/Core/Tests/Feature/ReportBuilderMasonIntegrationTest.php b/Modules/Core/Tests/Feature/ReportBuilderMasonIntegrationTest.php new file mode 100644 index 000000000..4f8b3da92 --- /dev/null +++ b/Modules/Core/Tests/Feature/ReportBuilderMasonIntegrationTest.php @@ -0,0 +1,320 @@ +company = Company::factory()->create(); + $this->service = app(ReportTemplateService::class); + $this->adapter = app(MasonStorageAdapter::class); + + $this->template = ReportTemplate::factory()->create([ + 'company_id' => $this->company->id, + 'slug' => 'test-invoice', + 'template_type' => ReportTemplateType::INVOICE, + ]); + } + + #[Test] + public function it_saves_mason_content_to_filesystem(): void + { + /* Arrange */ + $masonJson = json_encode([ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_company_test', + 'config' => ['show_vat_id' => true], + 'label' => 'Company Header', + ], + ], + ], + ]); + + /* Act */ + $blocks = $this->adapter->masonToBlocks($masonJson); + $this->service->persistBlocks($this->template, $blocks); + $reloadedBlocks = $this->service->loadBlocks($this->template); + $reloadedMasonJson = $this->adapter->blocksToMason($reloadedBlocks); + + /* Assert */ + $this->assertIsString($reloadedMasonJson); + + $originalDecoded = json_decode($masonJson, true); + $reloadedDecoded = json_decode($reloadedMasonJson, true); + + $this->assertIsArray($originalDecoded); + $this->assertIsArray($reloadedDecoded); + $this->assertSame('doc', $originalDecoded['type']); + $this->assertSame('doc', $reloadedDecoded['type']); + $this->assertNotEmpty($reloadedDecoded['content']); + } + + #[Test] + public function it_saves_and_loads_mason_json_via_mason_template_storage(): void + { + /* Arrange */ + $storage = app(\Modules\Core\Services\MasonTemplateStorage::class); + $masonJson = json_encode([ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_company_test', + 'config' => ['show_vat_id' => true], + 'label' => 'Company Header', + ], + ], + ], + ]); + + /* Act */ + $storage->save($this->template, $masonJson); + + /* Assert */ + $path = "{$this->company->id}/mason_{$this->template->slug}.json"; + Storage::disk('report_templates')->assertExists($path); + + $loadedJson = $storage->load($this->template); + $this->assertIsString($loadedJson); + + $originalDecoded = json_decode($masonJson, true); + $loadedDecoded = json_decode($loadedJson, true); + + $this->assertIsArray($originalDecoded); + $this->assertIsArray($loadedDecoded); + $this->assertSame('doc', $originalDecoded['type']); + $this->assertSame('doc', $loadedDecoded['type']); + $this->assertNotEmpty($loadedDecoded['content']); + $this->assertEquals($originalDecoded, $loadedDecoded); + } + + #[Test] + public function it_loads_blocks_and_converts_to_mason_format(): void + { + /* Arrange */ + $initialMasonJson = json_encode([ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_company_abc', + 'config' => ['show_phone' => true], + 'label' => 'Company', + ], + ], + ], + ]); + + $blocks = $this->adapter->masonToBlocks($initialMasonJson); + $this->service->persistBlocks($this->template, $blocks); + + /* Act */ + $loadedBlocks = $this->service->loadBlocks($this->template); + $convertedMason = $this->adapter->blocksToMason($loadedBlocks); + $decoded = json_decode($convertedMason, true); + + /* Assert */ + $this->assertIsArray($decoded); + $this->assertEquals('doc', $decoded['type']); + $this->assertNotEmpty($decoded['content']); + } + + #[Test] + public function it_preserves_block_configuration_through_roundtrip(): void + { + /* Arrange */ + $config = [ + 'show_vat_id' => true, + 'show_phone' => false, + 'font_size' => 12, + 'text_align' => 'right', + ]; + + $masonJson = json_encode([ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_company_roundtrip', + 'config' => $config, + 'label' => 'Test', + ], + ], + ], + ]); + + /* Act */ + $blocks = $this->adapter->masonToBlocks($masonJson); + $this->service->persistBlocks($this->template, $blocks); + $loadedBlocks = $this->service->loadBlocks($this->template); + $block = reset($loadedBlocks); + + /* Assert */ + $this->assertEquals($config, $block->getConfig()); + } + + #[Test] + public function it_handles_multiple_bricks_of_different_types(): void + { + /* Arrange */ + $masonJson = json_encode([ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_company_test', + 'config' => [], + 'label' => 'Company', + ], + ], + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_client_test', + 'config' => [], + 'label' => 'Client', + ], + ], + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'detail_items_test', + 'config' => [], + 'label' => 'Items', + ], + ], + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'footer_totals_test', + 'config' => [], + 'label' => 'Totals', + ], + ], + ], + ]); + + /* Act */ + $blocks = $this->adapter->masonToBlocks($masonJson); + $this->service->persistBlocks($this->template, $blocks); + $loadedBlocks = $this->service->loadBlocks($this->template); + + /* Assert */ + $this->assertCount(4, $loadedBlocks); + + $types = array_map(fn($block) => $block->getType(), $loadedBlocks); + $this->assertContains('header_company', $types); + $this->assertContains('header_client', $types); + $this->assertContains('detail_items', $types); + $this->assertContains('footer_totals', $types); + } + + #[Test] + public function it_maintains_block_order_through_persistence(): void + { + /* Arrange */ + $masonJson = json_encode([ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'first_block', + 'config' => [], + 'label' => 'First', + ], + ], + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'second_block', + 'config' => [], + 'label' => 'Second', + ], + ], + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'third_block', + 'config' => [], + 'label' => 'Third', + ], + ], + ], + ]); + + /* Act */ + $blocks = $this->adapter->masonToBlocks($masonJson); + $this->service->persistBlocks($this->template, $blocks); + $loadedBlocks = $this->service->loadBlocks($this->template); + $convertedMason = $this->adapter->blocksToMason($loadedBlocks); + $decoded = json_decode($convertedMason, true); + + /* Assert */ + $this->assertCount(3, $decoded['content']); + } + + #[Test] + public function it_assigns_correct_data_sources_to_blocks(): void + { + /* Arrange */ + $masonJson = json_encode([ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_company_src', + 'config' => [], + 'label' => 'Company', + ], + ], + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_invoice_meta_src', + 'config' => [], + 'label' => 'Invoice', + ], + ], + ], + ]); + + /* Act */ + $blocks = $this->adapter->masonToBlocks($masonJson); + + /* Assert */ + $companyBlock = $blocks['header_company_src']; + $invoiceBlock = $blocks['header_invoice_meta_src']; + + $this->assertEquals('company', $companyBlock->getDataSource()); + $this->assertEquals('invoice', $invoiceBlock->getDataSource()); + } +} diff --git a/Modules/Core/Tests/Unit/MasonBricksTest.php b/Modules/Core/Tests/Unit/MasonBricksTest.php new file mode 100644 index 000000000..6a9859761 --- /dev/null +++ b/Modules/Core/Tests/Unit/MasonBricksTest.php @@ -0,0 +1,288 @@ +assertEquals('header_company', $id); + } + + #[Test] + public function it_header_company_brick_generates_preview_html(): void + { + /* Arrange */ + $config = [ + 'show_vat_id' => true, + 'show_phone' => true, + 'font_size' => 10, + ]; + + /* Act */ + $html = HeaderCompanyBrick::toPreviewHtml($config); + + /* Assert */ + $this->assertIsString($html); + $this->assertStringContainsString('Company Name', $html); + } + + #[Test] + public function it_header_company_brick_generates_render_html(): void + { + /* Arrange */ + $config = ['show_vat_id' => true]; + $data = [ + 'company' => [ + 'name' => 'Test Company', + 'vat_id' => '123456', + ], + ]; + + /* Act */ + $html = HeaderCompanyBrick::toHtml($config, $data); + + /* Assert */ + $this->assertIsString($html); + $this->assertStringContainsString('Test Company', $html); + } + + #[Test] + public function it_header_client_brick_has_correct_id(): void + { + /* Act */ + $id = HeaderClientBrick::getId(); + + /* Assert */ + $this->assertEquals('header_client', $id); + } + + #[Test] + public function it_header_client_brick_generates_html(): void + { + /* Arrange */ + $config = ['show_phone' => true]; + $data = [ + 'client' => [ + 'name' => 'Test Client', + 'phone' => '555-1234', + ], + ]; + + /* Act */ + $html = HeaderClientBrick::toHtml($config, $data); + + /* Assert */ + $this->assertIsString($html); + $this->assertStringContainsString('Test Client', $html); + } + + #[Test] + public function it_header_invoice_meta_brick_has_correct_id(): void + { + /* Act */ + $id = HeaderInvoiceMetaBrick::getId(); + + /* Assert */ + $this->assertEquals('header_invoice_meta', $id); + } + + #[Test] + public function it_header_invoice_meta_brick_shows_configured_fields(): void + { + /* Arrange */ + $config = [ + 'show_invoice_number' => true, + 'show_invoice_date' => true, + 'show_due_date' => false, + ]; + $data = [ + 'invoice' => [ + 'number' => 'INV-001', + 'date' => '2024-01-01', + ], + ]; + + /* Act */ + $html = HeaderInvoiceMetaBrick::toHtml($config, $data); + + /* Assert */ + $this->assertIsString($html); + $this->assertStringContainsString('INV-001', $html); + } + + #[Test] + public function it_detail_items_brick_has_correct_id(): void + { + /* Act */ + $id = DetailItemsBrick::getId(); + + /* Assert */ + $this->assertEquals('detail_items', $id); + } + + #[Test] + public function it_detail_items_brick_renders_items_table(): void + { + /* Arrange */ + $config = [ + 'show_description' => true, + 'show_quantity' => true, + 'show_price' => true, + ]; + $data = [ + 'items' => [ + [ + 'description' => 'Item 1', + 'quantity' => 2, + 'price' => '100.00', + ], + ], + ]; + + /* Act */ + $html = DetailItemsBrick::toHtml($config, $data); + + /* Assert */ + $this->assertIsString($html); + $this->assertStringContainsString('Item 1', $html); + } + + #[Test] + public function it_footer_totals_brick_has_correct_id(): void + { + /* Act */ + $id = FooterTotalsBrick::getId(); + + /* Assert */ + $this->assertEquals('footer_totals', $id); + } + + #[Test] + public function it_footer_totals_brick_displays_configured_totals(): void + { + /* Arrange */ + $config = [ + 'show_subtotal' => true, + 'show_tax' => true, + 'show_total' => true, + ]; + $data = [ + 'totals' => [ + 'subtotal' => '100.00', + 'tax' => '10.00', + 'total' => '110.00', + ], + ]; + + /* Act */ + $html = FooterTotalsBrick::toHtml($config, $data); + + /* Assert */ + $this->assertIsString($html); + $this->assertStringContainsString('110.00', $html); + } + + #[Test] + public function it_footer_notes_brick_has_correct_id(): void + { + /* Act */ + $id = FooterNotesBrick::getId(); + + /* Assert */ + $this->assertEquals('footer_notes', $id); + } + + #[Test] + public function it_footer_notes_brick_renders_custom_content(): void + { + /* Arrange */ + $config = [ + 'footer_content' => '

Custom payment terms

', + ]; + $data = []; + + /* Act */ + $html = FooterNotesBrick::toHtml($config, $data); + + /* Assert */ + $this->assertIsString($html); + $this->assertStringContainsString('Custom payment terms', $html); + } + + #[Test] + public function it_all_bricks_have_unique_ids(): void + { + /* Arrange */ + $bricks = [ + HeaderCompanyBrick::class, + HeaderClientBrick::class, + HeaderInvoiceMetaBrick::class, + DetailItemsBrick::class, + FooterTotalsBrick::class, + FooterNotesBrick::class, + ]; + + /* Act */ + $ids = array_map(fn($brick) => $brick::getId(), $bricks); + + /* Assert */ + $this->assertCount(6, array_unique($ids)); + $this->assertCount(6, $ids); + } + + #[Test] + public function it_all_bricks_return_labels(): void + { + /* Arrange */ + $bricks = [ + HeaderCompanyBrick::class, + HeaderClientBrick::class, + HeaderInvoiceMetaBrick::class, + DetailItemsBrick::class, + FooterTotalsBrick::class, + FooterNotesBrick::class, + ]; + + /* Act & Assert */ + foreach ($bricks as $brick) { + $label = $brick::getLabel(); + $this->assertIsString($label); + $this->assertNotEmpty($label); + } + } + + #[Test] + public function it_all_bricks_return_icons(): void + { + /* Arrange */ + $bricks = [ + HeaderCompanyBrick::class, + HeaderClientBrick::class, + HeaderInvoiceMetaBrick::class, + DetailItemsBrick::class, + FooterTotalsBrick::class, + FooterNotesBrick::class, + ]; + + /* Act & Assert */ + foreach ($bricks as $brick) { + $icon = $brick::getIcon(); + $this->assertNotNull($icon); + } + } +} diff --git a/Modules/Core/Tests/Unit/MasonStorageAdapterTest.php b/Modules/Core/Tests/Unit/MasonStorageAdapterTest.php new file mode 100644 index 000000000..0103fdfc6 --- /dev/null +++ b/Modules/Core/Tests/Unit/MasonStorageAdapterTest.php @@ -0,0 +1,267 @@ +adapter = new MasonStorageAdapter(); + } + + #[Test] + public function it_converts_mason_json_to_block_dtos(): void + { + /* Arrange */ + $masonJson = json_encode([ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_company_abc123', + 'config' => [ + 'show_vat_id' => true, + 'show_phone' => true, + 'font_size' => 10, + ], + 'label' => 'Company Header', + ], + ], + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'detail_items_xyz789', + 'config' => [ + 'show_description' => true, + 'show_quantity' => true, + ], + 'label' => 'Line Items', + ], + ], + ], + ]); + + /* Act */ + $blocks = $this->adapter->masonToBlocks($masonJson); + + /* Assert */ + $this->assertIsArray($blocks); + $this->assertCount(2, $blocks); + $this->assertInstanceOf(BlockDTO::class, $blocks['header_company_abc123']); + $this->assertInstanceOf(BlockDTO::class, $blocks['detail_items_xyz789']); + } + + #[Test] + public function it_extracts_correct_type_from_mason_brick_id(): void + { + /* Arrange */ + $masonJson = json_encode([ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_company_abc123', + 'config' => [], + 'label' => 'Company Header', + ], + ], + ], + ]); + + /* Act */ + $blocks = $this->adapter->masonToBlocks($masonJson); + $block = reset($blocks); + + /* Assert */ + $this->assertEquals('header_company', $block->getType()); + } + + #[Test] + public function it_preserves_config_from_mason_brick(): void + { + /* Arrange */ + $expectedConfig = [ + 'show_vat_id' => true, + 'show_phone' => false, + 'font_size' => 12, + 'text_align' => 'center', + ]; + + $masonJson = json_encode([ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_company_test', + 'config' => $expectedConfig, + 'label' => 'Test Block', + ], + ], + ], + ]); + + /* Act */ + $blocks = $this->adapter->masonToBlocks($masonJson); + $block = reset($blocks); + + /* Assert */ + $this->assertEquals($expectedConfig, $block->getConfig()); + } + + #[Test] + public function it_converts_block_dtos_to_mason_json(): void + { + /* Arrange */ + $position = GridPositionDTO::create(0, 0, 12, 4); + + $block1 = new BlockDTO(); + $block1->setId('header_company_abc') + ->setType('header_company') + ->setPosition($position) + ->setConfig(['show_vat_id' => true]) + ->setLabel('Company Header') + ->setIsCloneable(false) + ->setDataSource('company') + ->setIsCloned(false) + ->setClonedFrom(null); + + $block2 = new BlockDTO(); + $block2->setId('footer_totals_xyz') + ->setType('footer_totals') + ->setPosition($position) + ->setConfig(['show_tax' => true]) + ->setLabel('Totals') + ->setIsCloneable(false) + ->setDataSource('invoice') + ->setIsCloned(false) + ->setClonedFrom(null); + + /* Act */ + $masonJson = $this->adapter->blocksToMason([$block1, $block2]); + $decoded = json_decode($masonJson, true); + + /* Assert */ + $this->assertIsArray($decoded); + $this->assertEquals('doc', $decoded['type']); + $this->assertCount(2, $decoded['content']); + $this->assertEquals('masonBrick', $decoded['content'][0]['type']); + $this->assertEquals('header_company_abc', $decoded['content'][0]['attrs']['id']); + } + + #[Test] + public function it_returns_empty_array_for_invalid_mason_json(): void + { + /* Arrange */ + $invalidJson = 'not valid json'; + + /* Act */ + $blocks = $this->adapter->masonToBlocks($invalidJson); + + /* Assert */ + $this->assertIsArray($blocks); + $this->assertEmpty($blocks); + } + + #[Test] + public function it_returns_empty_array_for_mason_json_without_content(): void + { + /* Arrange */ + $masonJson = json_encode([ + 'type' => 'doc', + ]); + + /* Act */ + $blocks = $this->adapter->masonToBlocks($masonJson); + + /* Assert */ + $this->assertIsArray($blocks); + $this->assertEmpty($blocks); + } + + #[Test] + public function it_assigns_correct_data_source_based_on_type(): void + { + /* Arrange */ + $masonJson = json_encode([ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_company_test', + 'config' => [], + 'label' => 'Company', + ], + ], + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'header_client_test', + 'config' => [], + 'label' => 'Client', + ], + ], + [ + 'type' => 'masonBrick', + 'attrs' => [ + 'id' => 'detail_items_test', + 'config' => [], + 'label' => 'Items', + ], + ], + ], + ]); + + /* Act */ + $blocks = $this->adapter->masonToBlocks($masonJson); + + /* Assert */ + $this->assertEquals('company', $blocks['header_company_test']->getDataSource()); + $this->assertEquals('client', $blocks['header_client_test']->getDataSource()); + $this->assertEquals('items', $blocks['detail_items_test']->getDataSource()); + } + + #[Test] + public function it_roundtrip_conversion_preserves_data(): void + { + /* Arrange */ + $position = GridPositionDTO::create(0, 0, 12, 4); + + $originalBlock = new BlockDTO(); + $originalBlock->setId('header_company_test') + ->setType('header_company') + ->setPosition($position) + ->setConfig([ + 'show_vat_id' => true, + 'font_size' => 10, + ]) + ->setLabel('Company Header') + ->setIsCloneable(false) + ->setDataSource('company') + ->setIsCloned(false) + ->setClonedFrom(null); + + /* Act */ + $masonJson = $this->adapter->blocksToMason([$originalBlock]); + $blocks = $this->adapter->masonToBlocks($masonJson); + $convertedBlock = reset($blocks); + + /* Assert */ + $this->assertEquals($originalBlock->getId(), $convertedBlock->getId()); + $this->assertEquals($originalBlock->getType(), $convertedBlock->getType()); + $this->assertEquals($originalBlock->getConfig(), $convertedBlock->getConfig()); + $this->assertEquals($originalBlock->getLabel(), $convertedBlock->getLabel()); + } +} diff --git a/Modules/Core/Tests/Unit/ReportBricksCollectionTest.php b/Modules/Core/Tests/Unit/ReportBricksCollectionTest.php new file mode 100644 index 000000000..e4358941f --- /dev/null +++ b/Modules/Core/Tests/Unit/ReportBricksCollectionTest.php @@ -0,0 +1,115 @@ +assertIsArray($bricks); + $this->assertCount(17, $bricks); + } + + #[Test] + public function it_returns_header_bricks(): void + { + /* Act */ + $headerBricks = ReportBricksCollection::header(); + + /* Assert */ + $this->assertIsArray($headerBricks); + $this->assertCount(5, $headerBricks); + $this->assertContains(HeaderCompanyBrick::class, $headerBricks); + $this->assertContains(HeaderClientBrick::class, $headerBricks); + $this->assertContains(HeaderInvoiceMetaBrick::class, $headerBricks); + $this->assertContains(HeaderQuoteMetaBrick::class, $headerBricks); + $this->assertContains(HeaderProjectBrick::class, $headerBricks); + } + + #[Test] + public function it_returns_detail_bricks(): void + { + /* Act */ + $detailBricks = ReportBricksCollection::detail(); + + /* Assert */ + $this->assertIsArray($detailBricks); + $this->assertCount(8, $detailBricks); + $this->assertContains(DetailItemsBrick::class, $detailBricks); + $this->assertContains(DetailTasksBrick::class, $detailBricks); + $this->assertContains(DetailInvoiceProductBrick::class, $detailBricks); + $this->assertContains(DetailInvoiceProjectBrick::class, $detailBricks); + $this->assertContains(DetailQuoteProductBrick::class, $detailBricks); + $this->assertContains(DetailQuoteProjectBrick::class, $detailBricks); + $this->assertContains(DetailCustomerAgingBrick::class, $detailBricks); + $this->assertContains(DetailExpenseBrick::class, $detailBricks); + } + + #[Test] + public function it_returns_footer_bricks(): void + { + /* Act */ + $footerBricks = ReportBricksCollection::footer(); + + /* Assert */ + $this->assertIsArray($footerBricks); + $this->assertCount(4, $footerBricks); + $this->assertContains(FooterTotalsBrick::class, $footerBricks); + $this->assertContains(FooterNotesBrick::class, $footerBricks); + $this->assertContains(FooterTermsBrick::class, $footerBricks); + $this->assertContains(FooterSummaryBrick::class, $footerBricks); + } + + #[Test] + public function it_all_method_combines_all_sections(): void + { + /* Arrange */ + $headerCount = count(ReportBricksCollection::header()); + $detailCount = count(ReportBricksCollection::detail()); + $footerCount = count(ReportBricksCollection::footer()); + + /* Act */ + $allBricks = ReportBricksCollection::all(); + + /* Assert */ + $this->assertCount($headerCount + $detailCount + $footerCount, $allBricks); + } + + #[Test] + public function it_all_bricks_are_valid_class_names(): void + { + /* Act */ + $allBricks = ReportBricksCollection::all(); + + /* Assert */ + foreach ($allBricks as $brick) { + $this->assertTrue(class_exists($brick), "Class {$brick} should exist"); + } + } + + #[Test] + public function it_no_duplicate_bricks_in_collection(): void + { + /* Act */ + $allBricks = ReportBricksCollection::all(); + + /* Assert */ + $uniqueBricks = array_unique($allBricks); + $this->assertCount(count($allBricks), $uniqueBricks); + } +} diff --git a/app/Mason/Bricks/DetailCustomerAgingBrick.php b/app/Mason/Bricks/DetailCustomerAgingBrick.php new file mode 100644 index 000000000..7e29898b9 --- /dev/null +++ b/app/Mason/Bricks/DetailCustomerAgingBrick.php @@ -0,0 +1,126 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.customer_aging_details'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.detail-customer-aging.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.detail-customer-aging.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_customer_aging')) + ->modalHeading(trans('ip.customer_aging_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_invoice_number') + ->label(trans('ip.show_invoice_number')) + ->default(true), + Checkbox::make('show_invoice_date') + ->label(trans('ip.show_invoice_date')) + ->default(true), + Checkbox::make('show_due_date') + ->label(trans('ip.show_due_date')) + ->default(true), + Checkbox::make('show_current') + ->label(trans('ip.show_current')) + ->default(true), + Checkbox::make('show_30_days') + ->label(trans('ip.show_30_days')) + ->default(true), + Checkbox::make('show_60_days') + ->label(trans('ip.show_60_days')) + ->default(true), + Checkbox::make('show_90_days') + ->label(trans('ip.show_90_days')) + ->default(true), + Checkbox::make('show_over_90_days') + ->label(trans('ip.show_over_90_days')) + ->default(true), + Checkbox::make('show_total_due') + ->label(trans('ip.show_total_due')) + ->default(true), + Checkbox::make('highlight_overdue') + ->label(trans('ip.highlight_overdue')) + ->default(true), + Checkbox::make('alternating_rows') + ->label(trans('ip.alternating_rows')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(9) + ->minValue(7) + ->maxValue(14), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/DetailExpenseBrick.php b/app/Mason/Bricks/DetailExpenseBrick.php new file mode 100644 index 000000000..e8976c200 --- /dev/null +++ b/app/Mason/Bricks/DetailExpenseBrick.php @@ -0,0 +1,116 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.expense_details'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.detail-expense.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.detail-expense.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_expense_details')) + ->modalHeading(trans('ip.expense_details_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_expense_number') + ->label(trans('ip.show_expense_number')) + ->default(true), + Checkbox::make('show_expense_date') + ->label(trans('ip.show_expense_date')) + ->default(true), + Checkbox::make('show_category') + ->label(trans('ip.show_category')) + ->default(true), + Checkbox::make('show_vendor') + ->label(trans('ip.show_vendor')) + ->default(false), + Checkbox::make('show_description') + ->label(trans('ip.show_description')) + ->default(true), + Checkbox::make('show_amount') + ->label(trans('ip.show_amount')) + ->default(true), + Checkbox::make('show_status') + ->label(trans('ip.show_status')) + ->default(true), + Checkbox::make('alternating_rows') + ->label(trans('ip.alternating_rows')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(9) + ->minValue(7) + ->maxValue(14), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/DetailInvoiceProductBrick.php b/app/Mason/Bricks/DetailInvoiceProductBrick.php new file mode 100644 index 000000000..7d088556f --- /dev/null +++ b/app/Mason/Bricks/DetailInvoiceProductBrick.php @@ -0,0 +1,116 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.invoice_product_details'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.detail-invoice-product.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.detail-invoice-product.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_invoice_product_details')) + ->modalHeading(trans('ip.invoice_product_details_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_sku') + ->label(trans('ip.show_sku')) + ->default(true), + Checkbox::make('show_description') + ->label(trans('ip.show_description')) + ->default(true), + Checkbox::make('show_quantity') + ->label(trans('ip.show_quantity')) + ->default(true), + Checkbox::make('show_unit_price') + ->label(trans('ip.show_unit_price')) + ->default(true), + Checkbox::make('show_tax') + ->label(trans('ip.show_tax')) + ->default(true), + Checkbox::make('show_discount') + ->label(trans('ip.show_discount')) + ->default(false), + Checkbox::make('show_total') + ->label(trans('ip.show_total')) + ->default(true), + Checkbox::make('alternating_rows') + ->label(trans('ip.alternating_rows')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(9) + ->minValue(7) + ->maxValue(14), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/DetailInvoiceProjectBrick.php b/app/Mason/Bricks/DetailInvoiceProjectBrick.php new file mode 100644 index 000000000..78cc9544f --- /dev/null +++ b/app/Mason/Bricks/DetailInvoiceProjectBrick.php @@ -0,0 +1,116 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.invoice_project_details'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.detail-invoice-project.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.detail-invoice-project.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_invoice_project_details')) + ->modalHeading(trans('ip.invoice_project_details_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_project_name') + ->label(trans('ip.show_project_name')) + ->default(true), + Checkbox::make('show_task_name') + ->label(trans('ip.show_task_name')) + ->default(true), + Checkbox::make('show_description') + ->label(trans('ip.show_description')) + ->default(true), + Checkbox::make('show_hours') + ->label(trans('ip.show_hours')) + ->default(true), + Checkbox::make('show_rate') + ->label(trans('ip.show_rate')) + ->default(true), + Checkbox::make('show_total') + ->label(trans('ip.show_total')) + ->default(true), + Checkbox::make('group_by_project') + ->label(trans('ip.group_by_project')) + ->default(true), + Checkbox::make('alternating_rows') + ->label(trans('ip.alternating_rows')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(9) + ->minValue(7) + ->maxValue(14), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/DetailItemsBrick.php b/app/Mason/Bricks/DetailItemsBrick.php new file mode 100644 index 000000000..0b71eb3e0 --- /dev/null +++ b/app/Mason/Bricks/DetailItemsBrick.php @@ -0,0 +1,110 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.line_items_table'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.detail-items.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.detail-items.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_line_items')) + ->modalHeading(trans('ip.line_items_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_description') + ->label(trans('ip.show_description')) + ->default(true), + Checkbox::make('show_quantity') + ->label(trans('ip.show_quantity')) + ->default(true), + Checkbox::make('show_price') + ->label(trans('ip.show_price')) + ->default(true), + Checkbox::make('show_tax') + ->label(trans('ip.show_tax')) + ->default(true), + Checkbox::make('show_total') + ->label(trans('ip.show_total')) + ->default(true), + Checkbox::make('alternating_rows') + ->label(trans('ip.alternating_rows')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(9) + ->minValue(7) + ->maxValue(14), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/DetailQuoteProductBrick.php b/app/Mason/Bricks/DetailQuoteProductBrick.php new file mode 100644 index 000000000..e59b8ed41 --- /dev/null +++ b/app/Mason/Bricks/DetailQuoteProductBrick.php @@ -0,0 +1,116 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.quote_product_details'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.detail-quote-product.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.detail-quote-product.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_quote_product_details')) + ->modalHeading(trans('ip.quote_product_details_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_sku') + ->label(trans('ip.show_sku')) + ->default(true), + Checkbox::make('show_description') + ->label(trans('ip.show_description')) + ->default(true), + Checkbox::make('show_quantity') + ->label(trans('ip.show_quantity')) + ->default(true), + Checkbox::make('show_unit_price') + ->label(trans('ip.show_unit_price')) + ->default(true), + Checkbox::make('show_tax') + ->label(trans('ip.show_tax')) + ->default(true), + Checkbox::make('show_discount') + ->label(trans('ip.show_discount')) + ->default(false), + Checkbox::make('show_total') + ->label(trans('ip.show_total')) + ->default(true), + Checkbox::make('alternating_rows') + ->label(trans('ip.alternating_rows')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(9) + ->minValue(7) + ->maxValue(14), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/DetailQuoteProjectBrick.php b/app/Mason/Bricks/DetailQuoteProjectBrick.php new file mode 100644 index 000000000..516c0dc1a --- /dev/null +++ b/app/Mason/Bricks/DetailQuoteProjectBrick.php @@ -0,0 +1,116 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.quote_project_details'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.detail-quote-project.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.detail-quote-project.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_quote_project_details')) + ->modalHeading(trans('ip.quote_project_details_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_project_name') + ->label(trans('ip.show_project_name')) + ->default(true), + Checkbox::make('show_task_name') + ->label(trans('ip.show_task_name')) + ->default(true), + Checkbox::make('show_description') + ->label(trans('ip.show_description')) + ->default(true), + Checkbox::make('show_hours') + ->label(trans('ip.show_hours')) + ->default(true), + Checkbox::make('show_rate') + ->label(trans('ip.show_rate')) + ->default(true), + Checkbox::make('show_total') + ->label(trans('ip.show_total')) + ->default(true), + Checkbox::make('group_by_project') + ->label(trans('ip.group_by_project')) + ->default(true), + Checkbox::make('alternating_rows') + ->label(trans('ip.alternating_rows')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(9) + ->minValue(7) + ->maxValue(14), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/DetailTasksBrick.php b/app/Mason/Bricks/DetailTasksBrick.php new file mode 100644 index 000000000..0b6cedfe8 --- /dev/null +++ b/app/Mason/Bricks/DetailTasksBrick.php @@ -0,0 +1,119 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.tasks_table'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.detail-tasks.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.detail-tasks.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_tasks')) + ->modalHeading(trans('ip.tasks_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_task_number') + ->label(trans('ip.show_task_number')) + ->default(true), + Checkbox::make('show_task_name') + ->label(trans('ip.show_task_name')) + ->default(true), + Checkbox::make('show_description') + ->label(trans('ip.show_description')) + ->default(true), + Checkbox::make('show_due_at') + ->label(trans('ip.show_due_at')) + ->default(false), + Checkbox::make('show_task_price') + ->label(trans('ip.show_task_price')) + ->default(true), + Checkbox::make('show_task_status') + ->label(trans('ip.show_task_status')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(9) + ->minValue(6) + ->maxValue(12), + Select::make('header_style') + ->label(trans('ip.header_style')) + ->options([ + 'normal' => trans('ip.normal'), + 'bold' => trans('ip.bold'), + 'italic' => trans('ip.italic'), + ]) + ->default('bold'), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/FooterNotesBrick.php b/app/Mason/Bricks/FooterNotesBrick.php new file mode 100644 index 000000000..bccb82d00 --- /dev/null +++ b/app/Mason/Bricks/FooterNotesBrick.php @@ -0,0 +1,102 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.footer'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.footer-notes.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.footer-notes.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_notes')) + ->modalHeading(trans('ip.notes_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + RichEditor::make('footer_content') + ->label(trans('ip.footer_content')) + ->columnSpanFull() + ->toolbarButtons([ + 'bold', + 'italic', + 'underline', + 'bulletList', + 'orderedList', + ]), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(8) + ->minValue(6) + ->maxValue(12), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/FooterSummaryBrick.php b/app/Mason/Bricks/FooterSummaryBrick.php new file mode 100644 index 000000000..6023b9879 --- /dev/null +++ b/app/Mason/Bricks/FooterSummaryBrick.php @@ -0,0 +1,102 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.summary'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.footer-summary.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.footer-summary.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_summary')) + ->modalHeading(trans('ip.summary_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + RichEditor::make('summary_content') + ->label(trans('ip.summary_content')) + ->columnSpanFull() + ->toolbarButtons([ + 'bold', + 'italic', + 'underline', + 'bulletList', + 'orderedList', + ]), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(9) + ->minValue(6) + ->maxValue(14), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/FooterTermsBrick.php b/app/Mason/Bricks/FooterTermsBrick.php new file mode 100644 index 000000000..c62177651 --- /dev/null +++ b/app/Mason/Bricks/FooterTermsBrick.php @@ -0,0 +1,102 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.terms_conditions'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.footer-terms.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.footer-terms.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_terms')) + ->modalHeading(trans('ip.terms_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + RichEditor::make('terms_content') + ->label(trans('ip.terms_content')) + ->columnSpanFull() + ->toolbarButtons([ + 'bold', + 'italic', + 'underline', + 'bulletList', + 'orderedList', + ]), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(8) + ->minValue(6) + ->maxValue(12), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/FooterTotalsBrick.php b/app/Mason/Bricks/FooterTotalsBrick.php new file mode 100644 index 000000000..b2d843616 --- /dev/null +++ b/app/Mason/Bricks/FooterTotalsBrick.php @@ -0,0 +1,119 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.totals_section'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.footer-totals.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.footer-totals.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_totals')) + ->modalHeading(trans('ip.totals_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_subtotal') + ->label(trans('ip.show_subtotal')) + ->default(true), + Checkbox::make('show_tax') + ->label(trans('ip.show_tax')) + ->default(true), + Checkbox::make('show_total') + ->label(trans('ip.show_total')) + ->default(true), + Checkbox::make('show_paid') + ->label(trans('ip.show_paid')) + ->default(false), + Checkbox::make('show_balance') + ->label(trans('ip.show_balance')) + ->default(false), + Checkbox::make('highlight_total') + ->label(trans('ip.highlight_total')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(10) + ->minValue(8) + ->maxValue(16), + Select::make('text_align') + ->label(trans('ip.text_align')) + ->options([ + 'left' => trans('ip.align_left'), + 'center' => trans('ip.align_center'), + 'right' => trans('ip.align_right'), + ]) + ->default('right'), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/HeaderClientBrick.php b/app/Mason/Bricks/HeaderClientBrick.php new file mode 100644 index 000000000..b13db3dfe --- /dev/null +++ b/app/Mason/Bricks/HeaderClientBrick.php @@ -0,0 +1,110 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.client_header'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.header-client.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.header-client.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_client_header')) + ->modalHeading(trans('ip.client_header_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_phone') + ->label(trans('ip.show_phone')) + ->default(true), + Checkbox::make('show_email') + ->label(trans('ip.show_email')) + ->default(true), + Checkbox::make('show_address') + ->label(trans('ip.show_address')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(10) + ->minValue(8) + ->maxValue(16), + Select::make('text_align') + ->label(trans('ip.text_align')) + ->options([ + 'left' => trans('ip.align_left'), + 'center' => trans('ip.align_center'), + 'right' => trans('ip.align_right'), + ]) + ->default('right'), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/HeaderCompanyBrick.php b/app/Mason/Bricks/HeaderCompanyBrick.php new file mode 100644 index 000000000..f4c06c941 --- /dev/null +++ b/app/Mason/Bricks/HeaderCompanyBrick.php @@ -0,0 +1,121 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.company_header'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.header-company.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.header-company.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_company_header')) + ->modalHeading(trans('ip.company_header_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_vat_id') + ->label(trans('ip.show_vat_id')) + ->default(true), + Checkbox::make('show_phone') + ->label(trans('ip.show_phone')) + ->default(true), + Checkbox::make('show_email') + ->label(trans('ip.show_email')) + ->default(true), + Checkbox::make('show_address') + ->label(trans('ip.show_address')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(10) + ->minValue(8) + ->maxValue(16), + Select::make('font_weight') + ->label(trans('ip.font_weight')) + ->options([ + 'normal' => trans('ip.font_weight_normal'), + 'bold' => trans('ip.font_weight_bold'), + ]) + ->default('bold'), + Select::make('text_align') + ->label(trans('ip.text_align')) + ->options([ + 'left' => trans('ip.align_left'), + 'center' => trans('ip.align_center'), + 'right' => trans('ip.align_right'), + ]) + ->default('left'), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/HeaderInvoiceMetaBrick.php b/app/Mason/Bricks/HeaderInvoiceMetaBrick.php new file mode 100644 index 000000000..1c9319000 --- /dev/null +++ b/app/Mason/Bricks/HeaderInvoiceMetaBrick.php @@ -0,0 +1,113 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.invoice_metadata'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.header-invoice-meta.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.header-invoice-meta.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_invoice_metadata')) + ->modalHeading(trans('ip.invoice_metadata_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_invoice_number') + ->label(trans('ip.show_invoice_number')) + ->default(true), + Checkbox::make('show_invoice_date') + ->label(trans('ip.show_invoice_date')) + ->default(true), + Checkbox::make('show_due_date') + ->label(trans('ip.show_due_date')) + ->default(true), + Checkbox::make('show_po_number') + ->label(trans('ip.show_po_number')) + ->default(false), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(10) + ->minValue(8) + ->maxValue(16), + Select::make('text_align') + ->label(trans('ip.text_align')) + ->options([ + 'left' => trans('ip.align_left'), + 'center' => trans('ip.align_center'), + 'right' => trans('ip.align_right'), + ]) + ->default('right'), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/HeaderProjectBrick.php b/app/Mason/Bricks/HeaderProjectBrick.php new file mode 100644 index 000000000..f634aea5e --- /dev/null +++ b/app/Mason/Bricks/HeaderProjectBrick.php @@ -0,0 +1,116 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.project_header'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.header-project.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.header-project.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_project')) + ->modalHeading(trans('ip.project_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_project_number') + ->label(trans('ip.show_project_number')) + ->default(true), + Checkbox::make('show_project_name') + ->label(trans('ip.show_project_name')) + ->default(true), + Checkbox::make('show_start_date') + ->label(trans('ip.show_start_date')) + ->default(true), + Checkbox::make('show_end_date') + ->label(trans('ip.show_end_date')) + ->default(true), + Checkbox::make('show_status') + ->label(trans('ip.show_status')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(10) + ->minValue(6) + ->maxValue(16), + Select::make('text_align') + ->label(trans('ip.text_align')) + ->options([ + 'left' => trans('ip.align_left'), + 'center' => trans('ip.align_center'), + 'right' => trans('ip.align_right'), + ]) + ->default('left'), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Bricks/HeaderQuoteMetaBrick.php b/app/Mason/Bricks/HeaderQuoteMetaBrick.php new file mode 100644 index 000000000..8a6a444fa --- /dev/null +++ b/app/Mason/Bricks/HeaderQuoteMetaBrick.php @@ -0,0 +1,113 @@ +'); + } + + public static function getPreviewLabel(array $config): string + { + return trans('ip.quote_metadata'); + } + + public static function toPreviewHtml(array $config): ?string + { + return view('mason.bricks.header-quote-meta.preview', [ + 'config' => $config, + ])->render(); + } + + public static function toHtml(array $config, array $data): ?string + { + return view('mason.bricks.header-quote-meta.index', [ + 'config' => $config, + 'data' => $data, + ])->render(); + } + + public static function configureBrickAction(Action $action): Action + { + return $action + ->label(trans('ip.configure_quote_meta')) + ->modalHeading(trans('ip.quote_meta_settings')) + ->slideOver() + ->fillForm(fn (array $arguments): ?array => $arguments['config'] ?? null) + ->schema([ + Checkbox::make('show_quote_number') + ->label(trans('ip.show_quote_number')) + ->default(true), + Checkbox::make('show_quoted_at') + ->label(trans('ip.show_quoted_at')) + ->default(true), + Checkbox::make('show_expires_at') + ->label(trans('ip.show_expires_at')) + ->default(true), + Checkbox::make('show_status') + ->label(trans('ip.show_status')) + ->default(true), + TextInput::make('font_size') + ->label(trans('ip.font_size')) + ->numeric() + ->default(10) + ->minValue(6) + ->maxValue(16), + Select::make('text_align') + ->label(trans('ip.text_align')) + ->options([ + 'left' => trans('ip.align_left'), + 'center' => trans('ip.align_center'), + 'right' => trans('ip.align_right'), + ]) + ->default('right'), + ]) + ->action(function (array $arguments, array $data, \Awcodes\Mason\Mason $component) { + $brick = $component->getBrick($arguments['id']); + + if (blank($brick)) { + return; + } + + $brickContent = [ + 'type' => 'masonBrick', + 'attrs' => [ + 'config' => $data, + 'id' => $arguments['id'], + 'label' => $brick::getPreviewLabel($data), + 'preview' => base64_encode($brick::toPreviewHtml($data)), + ], + ]; + + $component->runCommands([ + \Awcodes\Mason\Actions\EditorCommand::make( + 'insertContentAt', + arguments: [ + $arguments['dragPosition'], + $brickContent, + ], + ), + ]); + }); + } +} diff --git a/app/Mason/Collections/ReportBricksCollection.php b/app/Mason/Collections/ReportBricksCollection.php new file mode 100644 index 000000000..8f8e35539 --- /dev/null +++ b/app/Mason/Collections/ReportBricksCollection.php @@ -0,0 +1,94 @@ + + */ + public static function all(): array + { + return [ + ...self::header(), + ...self::detail(), + ...self::footer(), + ]; + } + + /** + * Get header section bricks. + * + * @return array + */ + public static function header(): array + { + return [ + HeaderCompanyBrick::class, + HeaderClientBrick::class, + HeaderInvoiceMetaBrick::class, + HeaderQuoteMetaBrick::class, + HeaderProjectBrick::class, + ]; + } + + /** + * Get detail section bricks. + * + * @return array + */ + public static function detail(): array + { + return [ + DetailItemsBrick::class, + DetailTasksBrick::class, + DetailInvoiceProductBrick::class, + DetailInvoiceProjectBrick::class, + DetailQuoteProductBrick::class, + DetailQuoteProjectBrick::class, + DetailCustomerAgingBrick::class, + DetailExpenseBrick::class, + ]; + } + + /** + * Get footer section bricks. + * + * @return array + */ + public static function footer(): array + { + return [ + FooterTotalsBrick::class, + FooterNotesBrick::class, + FooterTermsBrick::class, + FooterSummaryBrick::class, + ]; + } +} diff --git a/composer.json b/composer.json index e7025f92c..4bc44386f 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "prefer-stable": true, "require": { "php": "^8.2", + "awcodes/mason": "^3.0", "doctrine/dbal": ">=4.4", "filament/actions": ">=4.5", "filament/filament": ">=4.5", diff --git a/composer.lock b/composer.lock index 28bfbb8c8..3be6031f4 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": "d3e176925b783ce2b0dce382cf24512b", + "content-hash": "b010743517f9f27a5a5d48829f35be63", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -72,6 +72,86 @@ }, "time": "2025-12-04T13:38:21+00:00" }, + { + "name": "awcodes/mason", + "version": "v3.0.4", + "source": { + "type": "git", + "url": "https://github.com/awcodes/mason.git", + "reference": "3bda91a31994857800394d47d64662f0e2d8bf73" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awcodes/mason/zipball/3bda91a31994857800394d47d64662f0e2d8bf73", + "reference": "3bda91a31994857800394d47d64662f0e2d8bf73", + "shasum": "" + }, + "require": { + "filament/filament": "^4.0|^5.0", + "php": "^8.2", + "spatie/laravel-package-tools": "^1.15.0" + }, + "require-dev": { + "larastan/larastan": "^3.0", + "laravel/pint": "^1.0", + "nunomaduro/collision": "^8.0", + "orchestra/testbench": "^9.0|^10.0", + "pestphp/pest": "^3.0|^4.3", + "pestphp/pest-plugin-laravel": "^3.0|^4.0", + "pestphp/pest-plugin-livewire": "^3.0|^4.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "rector/rector": "^2.0", + "spatie/laravel-ray": "^1.26" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Awcodes\\Mason\\MasonServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Awcodes\\Mason\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Adam Weston", + "email": "awcodes1@gmail.com", + "role": "Developer" + } + ], + "description": "A simple block based drag and drop page / document builder field for Filament.", + "homepage": "https://github.com/awcodes/mason", + "keywords": [ + "awcodes", + "filamentphp", + "laravel", + "mason" + ], + "support": { + "issues": "https://github.com/awcodes/mason/issues", + "source": "https://github.com/awcodes/mason" + }, + "funding": [ + { + "url": "https://github.com/awcodes", + "type": "github" + } + ], + "time": "2026-02-05T17:53:32+00:00" + }, { "name": "blade-ui-kit/blade-heroicons", "version": "2.6.0", diff --git a/resources/lang/en/ip.php b/resources/lang/en/ip.php index 11520d0d6..54444a495 100644 --- a/resources/lang/en/ip.php +++ b/resources/lang/en/ip.php @@ -1049,5 +1049,169 @@ 'report_block_type_metadata_desc' => 'Block for dates, notes, QR codes, and other metadata', 'report_block_type_totals' => 'Totals Block', 'report_block_type_totals_desc' => 'Block for displaying subtotals, taxes, and grand totals', + + // Mason Report Builder + 'report_layout' => 'Report Layout', + 'report_preview' => 'Report Preview', + 'company_header' => 'Company Header', + 'client_header' => 'Client Header', + 'invoice_metadata' => 'Invoice Metadata', + 'line_items_table' => 'Line Items Table', + 'totals_section' => 'Totals Section', + 'footer_notes' => 'Footer Notes', + 'configure_company_header' => 'Configure Company Header', + 'company_header_settings' => 'Company Header Settings', + 'configure_client_header' => 'Configure Client Header', + 'client_header_settings' => 'Client Header Settings', + 'configure_invoice_metadata' => 'Configure Invoice Metadata', + 'invoice_metadata_settings' => 'Invoice Metadata Settings', + 'configure_line_items' => 'Configure Line Items', + 'line_items_settings' => 'Line Items Settings', + 'configure_totals' => 'Configure Totals', + 'totals_settings' => 'Totals Settings', + 'configure_notes' => 'Configure Notes', + 'notes_settings' => 'Notes Settings', + 'show_logo' => 'Show Logo', + 'show_vat_id' => 'Show VAT ID', + 'show_phone' => 'Show Phone', + 'show_email' => 'Show Email', + 'show_address' => 'Show Address', + 'font_size' => 'Font Size', + 'font_weight' => 'Font Weight', + 'font_weight_normal' => 'Normal', + 'font_weight_bold' => 'Bold', + 'text_align' => 'Text Alignment', + 'align_left' => 'Left', + 'align_center' => 'Center', + 'align_right' => 'Right', + 'show_invoice_number' => 'Show Invoice Number', + 'show_invoice_date' => 'Show Invoice Date', + 'show_due_date' => 'Show Due Date', + 'show_po_number' => 'Show PO Number', + 'show_description' => 'Show Description', + 'show_quantity' => 'Show Quantity', + 'show_price' => 'Show Price', + 'show_tax' => 'Show Tax', + 'show_total' => 'Show Total', + 'alternating_rows' => 'Alternating Row Colors', + 'show_subtotal' => 'Show Subtotal', + 'show_paid' => 'Show Paid Amount', + 'show_balance' => 'Show Balance Due', + 'highlight_total' => 'Highlight Total Row', + 'notes_content' => 'Notes Content', + 'footer_notes_placeholder' => 'Add notes or terms and conditions here...', + 'bill_to' => 'Bill To', + 'company_address' => 'Company Address', + 'client_address' => 'Client Address', + 'logo' => 'Logo', + 'balance_due' => 'Balance Due', + 'mason_item' => 'Item', + + // New Mason Bricks for Multiple Entities + 'project_header' => 'Project Header', + 'quote_metadata' => 'Quote Metadata', + 'tasks_table' => 'Tasks Table', + 'terms_conditions' => 'Terms & Conditions', + 'mason_summary' => 'Summary', + 'footer' => 'Footer', + 'configure_project' => 'Configure Project', + 'project_settings' => 'Project Settings', + 'show_project_number' => 'Show Project Number', + 'show_project_name' => 'Show Project Name', + 'show_start_date' => 'Show Start Date', + 'show_end_date' => 'Show End Date', + 'show_status' => 'Show Status', + 'configure_quote_meta' => 'Configure Quote Metadata', + 'quote_meta_settings' => 'Quote Metadata Settings', + 'show_quote_number' => 'Show Quote Number', + 'show_quoted_at' => 'Show Quote Date', + 'show_expires_at' => 'Show Expiry Date', + 'configure_tasks' => 'Configure Tasks', + 'tasks_settings' => 'Tasks Settings', + 'show_task_number' => 'Show Task Number', + 'show_task_name' => 'Show Task Name', + 'show_due_at' => 'Show Due Date', + 'show_task_price' => 'Show Task Price', + 'show_task_status' => 'Show Task Status', + 'header_style' => 'Header Style', + 'normal' => 'Normal', + 'bold' => 'Bold', + 'italic' => 'Italic', + 'configure_terms' => 'Configure Terms', + 'terms_settings' => 'Terms Settings', + 'terms_content' => 'Terms Content', + 'terms_placeholder' => 'Add terms and conditions here...', + 'configure_summary' => 'Configure Summary', + 'summary_settings' => 'Summary Settings', + 'summary_content' => 'Summary Content', + 'summary_placeholder' => 'Add summary or description here...', + 'footer_content' => 'Footer Content', + 'footer_placeholder' => 'Add footer notes here...', + + // New brick translations + 'invoice_product_details' => 'Invoice Product Details', + 'configure_invoice_product_details' => 'Configure Invoice Product Details', + 'invoice_product_details_settings' => 'Invoice Product Details Settings', + 'invoice_project_details' => 'Invoice Project Details', + 'configure_invoice_project_details' => 'Configure Invoice Project Details', + 'invoice_project_details_settings' => 'Invoice Project Details Settings', + 'quote_product_details' => 'Quote Product Details', + 'configure_quote_product_details' => 'Configure Quote Product Details', + 'quote_product_details_settings' => 'Quote Product Details Settings', + 'quote_project_details' => 'Quote Project Details', + 'configure_quote_project_details' => 'Configure Quote Project Details', + 'quote_project_details_settings' => 'Quote Project Details Settings', + 'customer_aging_details' => 'Customer Aging Details', + 'configure_customer_aging' => 'Configure Customer Aging', + 'customer_aging_settings' => 'Customer Aging Settings', + 'expense_details' => 'Expense Details', + 'configure_expense_details' => 'Configure Expense Details', + 'expense_details_settings' => 'Expense Details Settings', + 'show_sku' => 'Show SKU', + 'show_unit_price' => 'Show Unit Price', + 'show_discount' => 'Show Discount', + 'show_project_name' => 'Show Project Name', + 'show_task_name' => 'Show Task Name', + 'show_hours' => 'Show Hours', + 'show_rate' => 'Show Rate', + 'group_by_project' => 'Group by Project', + 'show_invoice_date' => 'Show Invoice Date', + 'show_30_days' => 'Show 30 Days', + 'show_60_days' => 'Show 60 Days', + 'show_90_days' => 'Show 90 Days', + 'show_over_90_days' => 'Show Over 90 Days', + 'show_total_due' => 'Show Total Due', + 'highlight_overdue' => 'Highlight Overdue', + 'days_30' => '1-30 Days', + 'days_60' => '31-60 Days', + 'days_90' => '61-90 Days', + 'over_90' => 'Over 90 Days', + 'total_due' => 'Total Due', + 'show_expense_number' => 'Show Expense Number', + 'show_expense_date' => 'Show Expense Date', + 'show_category' => 'Show Category', + 'show_vendor' => 'Show Vendor', + 'show_amount' => 'Show Amount', + 'expense_number' => 'Expense Number', + 'expense_description' => 'Expense Description', + 'sku' => 'SKU', + 'unit_price' => 'Unit Price', + 'hours' => 'Hours', + 'rate' => 'Rate', + 'vendor' => 'Vendor', + 'task_description' => 'Task Description', + + 'project_number' => 'Project Number', + 'project_name' => 'Project Name', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'in_progress' => 'In Progress', + 'quote_number' => 'Quote Number', + 'quoted_at' => 'Quote Date', + 'expires_at' => 'Expiry Date', + 'draft' => 'Draft', + 'task_name' => 'Task Name', + 'pending' => 'Pending', + 'number' => 'Number', #endregion ]; diff --git a/resources/views/layouts/mason-preview.blade.php b/resources/views/layouts/mason-preview.blade.php new file mode 100644 index 000000000..d540fad1d --- /dev/null +++ b/resources/views/layouts/mason-preview.blade.php @@ -0,0 +1,42 @@ + + + + + + {{ config('app.name') }} - {{ trans('ip.report_preview') }} + + @vite(['resources/css/app.css', 'resources/js/app.js']) + @masonStyles + + + + +
+
+ @include('mason::iframe-preview-content', ['blocks' => $blocks]) +
+
+ + diff --git a/resources/views/mason/bricks/detail-customer-aging/index.blade.php b/resources/views/mason/bricks/detail-customer-aging/index.blade.php new file mode 100644 index 000000000..c3bbd0832 --- /dev/null +++ b/resources/views/mason/bricks/detail-customer-aging/index.blade.php @@ -0,0 +1,98 @@ +@props([ + 'config' => [], + 'data' => [] +]) + +
+ + + + @if($config['show_invoice_number'] ?? true) + + @endif + @if($config['show_invoice_date'] ?? true) + + @endif + @if($config['show_due_date'] ?? true) + + @endif + @if($config['show_current'] ?? true) + + @endif + @if($config['show_30_days'] ?? true) + + @endif + @if($config['show_60_days'] ?? true) + + @endif + @if($config['show_90_days'] ?? true) + + @endif + @if($config['show_over_90_days'] ?? true) + + @endif + @if($config['show_total_due'] ?? true) + + @endif + + + + @foreach(($data['aging_items'] ?? []) as $index => $item) + + @if($config['show_invoice_number'] ?? true) + + @endif + @if($config['show_invoice_date'] ?? true) + + @endif + @if($config['show_due_date'] ?? true) + + @endif + @if($config['show_current'] ?? true) + + @endif + @if($config['show_30_days'] ?? true) + + @endif + @if($config['show_60_days'] ?? true) + + @endif + @if($config['show_90_days'] ?? true) + + @endif + @if($config['show_over_90_days'] ?? true) + + @endif + @if($config['show_total_due'] ?? true) + + @endif + + @endforeach + + @if(!empty($data['aging_totals'])) + + + + @if($config['show_current'] ?? true) + + @endif + @if($config['show_30_days'] ?? true) + + @endif + @if($config['show_60_days'] ?? true) + + @endif + @if($config['show_90_days'] ?? true) + + @endif + @if($config['show_over_90_days'] ?? true) + + @endif + @if($config['show_total_due'] ?? true) + + @endif + + + @endif +
{{ trans('ip.invoice') }}{{ trans('ip.date') }}{{ trans('ip.due_date') }}{{ trans('ip.current') }}{{ trans('ip.days_30') }}{{ trans('ip.days_60') }}{{ trans('ip.days_90') }}{{ trans('ip.over_90') }}{{ trans('ip.total_due') }}
{{ $item['invoice_number'] ?? '' }}{{ $item['invoice_date'] ?? '' }}{{ $item['due_date'] ?? '' }}{{ $item['current'] ?? '-' }}{{ $item['days_30'] ?? '-' }}{{ $item['days_60'] ?? '-' }}{{ $item['days_90'] ?? '-' }}{{ $item['over_90'] ?? '-' }}{{ $item['total_due'] ?? '0.00' }}
{{ trans('ip.total') }}{{ $data['aging_totals']['current'] ?? '0.00' }}{{ $data['aging_totals']['days_30'] ?? '0.00' }}{{ $data['aging_totals']['days_60'] ?? '0.00' }}{{ $data['aging_totals']['days_90'] ?? '0.00' }}{{ $data['aging_totals']['over_90'] ?? '0.00' }}{{ $data['aging_totals']['total_due'] ?? '0.00' }}
+
diff --git a/resources/views/mason/bricks/detail-customer-aging/preview.blade.php b/resources/views/mason/bricks/detail-customer-aging/preview.blade.php new file mode 100644 index 000000000..35e8b762b --- /dev/null +++ b/resources/views/mason/bricks/detail-customer-aging/preview.blade.php @@ -0,0 +1,95 @@ +@props([ + 'config' => [] +]) + +
+ + + + @if($config['show_invoice_number'] ?? true) + + @endif + @if($config['show_invoice_date'] ?? true) + + @endif + @if($config['show_due_date'] ?? true) + + @endif + @if($config['show_current'] ?? true) + + @endif + @if($config['show_30_days'] ?? true) + + @endif + @if($config['show_60_days'] ?? true) + + @endif + @if($config['show_90_days'] ?? true) + + @endif + @if($config['show_over_90_days'] ?? true) + + @endif + @if($config['show_total_due'] ?? true) + + @endif + + + + @for($i = 1; $i <= 3; $i++) + + @if($config['show_invoice_number'] ?? true) + + @endif + @if($config['show_invoice_date'] ?? true) + + @endif + @if($config['show_due_date'] ?? true) + + @endif + @if($config['show_current'] ?? true) + + @endif + @if($config['show_30_days'] ?? true) + + @endif + @if($config['show_60_days'] ?? true) + + @endif + @if($config['show_90_days'] ?? true) + + @endif + @if($config['show_over_90_days'] ?? true) + + @endif + @if($config['show_total_due'] ?? true) + + @endif + + @endfor + + + + + @if($config['show_current'] ?? true) + + @endif + @if($config['show_30_days'] ?? true) + + @endif + @if($config['show_60_days'] ?? true) + + @endif + @if($config['show_90_days'] ?? true) + + @endif + @if($config['show_over_90_days'] ?? true) + + @endif + @if($config['show_total_due'] ?? true) + + @endif + + +
{{ trans('ip.invoice') }}{{ trans('ip.date') }}{{ trans('ip.due_date') }}{{ trans('ip.current') }}{{ trans('ip.days_30') }}{{ trans('ip.days_60') }}{{ trans('ip.days_90') }}{{ trans('ip.over_90') }}{{ trans('ip.total_due') }}
INV-{{ str_pad($i, 4, '0', STR_PAD_LEFT) }}{{ now()->subDays($i * 30)->format('Y-m-d') }}{{ now()->subDays(($i * 30) - 30)->format('Y-m-d') }}{{ $i == 1 ? '$1,500.00' : '-' }}{{ $i == 2 ? '$2,300.00' : '-' }}{{ $i == 3 ? '$800.00' : '-' }}--{{ $i == 1 ? '$1,500.00' : ($i == 2 ? '$2,300.00' : '$800.00') }}
{{ trans('ip.total') }}$1,500.00$2,300.00$800.00$0.00$0.00$4,600.00
+
diff --git a/resources/views/mason/bricks/detail-expense/index.blade.php b/resources/views/mason/bricks/detail-expense/index.blade.php new file mode 100644 index 000000000..c70aab6e7 --- /dev/null +++ b/resources/views/mason/bricks/detail-expense/index.blade.php @@ -0,0 +1,61 @@ +@props([ + 'config' => [], + 'data' => [] +]) + +
+ + + + @if($config['show_expense_number'] ?? true) + + @endif + @if($config['show_expense_date'] ?? true) + + @endif + @if($config['show_category'] ?? true) + + @endif + @if($config['show_vendor'] ?? false) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_amount'] ?? true) + + @endif + @if($config['show_status'] ?? true) + + @endif + + + + @foreach(($data['expense_items'] ?? []) as $index => $item) + + @if($config['show_expense_number'] ?? true) + + @endif + @if($config['show_expense_date'] ?? true) + + @endif + @if($config['show_category'] ?? true) + + @endif + @if($config['show_vendor'] ?? false) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_amount'] ?? true) + + @endif + @if($config['show_status'] ?? true) + + @endif + + @endforeach + +
{{ trans('ip.expense_number') }}{{ trans('ip.date') }}{{ trans('ip.category') }}{{ trans('ip.vendor') }}{{ trans('ip.description') }}{{ trans('ip.amount') }}{{ trans('ip.status') }}
{{ $item['expense_number'] ?? '' }}{{ $item['expense_date'] ?? '' }}{{ $item['category'] ?? '' }}{{ $item['vendor'] ?? '' }}{{ $item['description'] ?? '' }}{{ $item['amount'] ?? '0.00' }}{{ $item['status'] ?? '' }}
+
diff --git a/resources/views/mason/bricks/detail-expense/preview.blade.php b/resources/views/mason/bricks/detail-expense/preview.blade.php new file mode 100644 index 000000000..f11a14fcf --- /dev/null +++ b/resources/views/mason/bricks/detail-expense/preview.blade.php @@ -0,0 +1,60 @@ +@props([ + 'config' => [] +]) + +
+ + + + @if($config['show_expense_number'] ?? true) + + @endif + @if($config['show_expense_date'] ?? true) + + @endif + @if($config['show_category'] ?? true) + + @endif + @if($config['show_vendor'] ?? false) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_amount'] ?? true) + + @endif + @if($config['show_status'] ?? true) + + @endif + + + + @for($i = 1; $i <= 3; $i++) + + @if($config['show_expense_number'] ?? true) + + @endif + @if($config['show_expense_date'] ?? true) + + @endif + @if($config['show_category'] ?? true) + + @endif + @if($config['show_vendor'] ?? false) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_amount'] ?? true) + + @endif + @if($config['show_status'] ?? true) + + @endif + + @endfor + +
{{ trans('ip.expense_number') }}{{ trans('ip.date') }}{{ trans('ip.category') }}{{ trans('ip.vendor') }}{{ trans('ip.description') }}{{ trans('ip.amount') }}{{ trans('ip.status') }}
EXP-{{ str_pad($i, 4, '0', STR_PAD_LEFT) }}{{ now()->subDays($i * 5)->format('Y-m-d') }}{{ trans('ip.category') }} {{ $i }}{{ trans('ip.vendor') }} {{ $i }}{{ trans('ip.expense_description') }}${{ $i * 250 }}.00{{ $i % 2 == 0 ? trans('ip.paid') : trans('ip.pending') }}
+
diff --git a/resources/views/mason/bricks/detail-invoice-product/index.blade.php b/resources/views/mason/bricks/detail-invoice-product/index.blade.php new file mode 100644 index 000000000..5f0c6ed41 --- /dev/null +++ b/resources/views/mason/bricks/detail-invoice-product/index.blade.php @@ -0,0 +1,61 @@ +@props([ + 'config' => [], + 'data' => [] +]) + +
+ + + + @if($config['show_sku'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_quantity'] ?? true) + + @endif + @if($config['show_unit_price'] ?? true) + + @endif + @if($config['show_tax'] ?? true) + + @endif + @if($config['show_discount'] ?? false) + + @endif + @if($config['show_total'] ?? true) + + @endif + + + + @foreach(($data['invoice_items'] ?? []) as $index => $item) + + @if($config['show_sku'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_quantity'] ?? true) + + @endif + @if($config['show_unit_price'] ?? true) + + @endif + @if($config['show_tax'] ?? true) + + @endif + @if($config['show_discount'] ?? false) + + @endif + @if($config['show_total'] ?? true) + + @endif + + @endforeach + +
{{ trans('ip.sku') }}{{ trans('ip.description') }}{{ trans('ip.quantity') }}{{ trans('ip.unit_price') }}{{ trans('ip.tax') }}{{ trans('ip.discount') }}{{ trans('ip.total') }}
{{ $item['sku'] ?? '' }}{{ $item['description'] ?? '' }}{{ $item['quantity'] ?? 0 }}{{ $item['unit_price'] ?? '0.00' }}{{ $item['tax'] ?? '0.00' }}{{ $item['discount'] ?? '0.00' }}{{ $item['total'] ?? '0.00' }}
+
diff --git a/resources/views/mason/bricks/detail-invoice-product/preview.blade.php b/resources/views/mason/bricks/detail-invoice-product/preview.blade.php new file mode 100644 index 000000000..cfd8c75d9 --- /dev/null +++ b/resources/views/mason/bricks/detail-invoice-product/preview.blade.php @@ -0,0 +1,60 @@ +@props([ + 'config' => [] +]) + +
+ + + + @if($config['show_sku'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_quantity'] ?? true) + + @endif + @if($config['show_unit_price'] ?? true) + + @endif + @if($config['show_tax'] ?? true) + + @endif + @if($config['show_discount'] ?? false) + + @endif + @if($config['show_total'] ?? true) + + @endif + + + + @for($i = 1; $i <= 3; $i++) + + @if($config['show_sku'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_quantity'] ?? true) + + @endif + @if($config['show_unit_price'] ?? true) + + @endif + @if($config['show_tax'] ?? true) + + @endif + @if($config['show_discount'] ?? false) + + @endif + @if($config['show_total'] ?? true) + + @endif + + @endfor + +
{{ trans('ip.sku') }}{{ trans('ip.description') }}{{ trans('ip.quantity') }}{{ trans('ip.unit_price') }}{{ trans('ip.tax') }}{{ trans('ip.discount') }}{{ trans('ip.total') }}
SKU-{{ str_pad($i, 3, '0', STR_PAD_LEFT) }}{{ trans('ip.product') }} {{ $i }}{{ $i }}$100.00$10.00$0.00$110.00
+
diff --git a/resources/views/mason/bricks/detail-invoice-project/index.blade.php b/resources/views/mason/bricks/detail-invoice-project/index.blade.php new file mode 100644 index 000000000..fb64eb12a --- /dev/null +++ b/resources/views/mason/bricks/detail-invoice-project/index.blade.php @@ -0,0 +1,65 @@ +@props([ + 'config' => [], + 'data' => [] +]) + +
+ + + + @if($config['show_project_name'] ?? true) + + @endif + @if($config['show_task_name'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_hours'] ?? true) + + @endif + @if($config['show_rate'] ?? true) + + @endif + @if($config['show_total'] ?? true) + + @endif + + + + @php + $currentProject = null; + $items = $data['project_items'] ?? []; + @endphp + @foreach($items as $index => $item) + @if(($config['group_by_project'] ?? true) && $currentProject !== ($item['project_name'] ?? '')) + @php $currentProject = $item['project_name'] ?? ''; @endphp + + + + @endif + + @if($config['show_project_name'] ?? true) + + @endif + @if($config['show_task_name'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_hours'] ?? true) + + @endif + @if($config['show_rate'] ?? true) + + @endif + @if($config['show_total'] ?? true) + + @endif + + @endforeach + +
{{ trans('ip.project') }}{{ trans('ip.task') }}{{ trans('ip.description') }}{{ trans('ip.hours') }}{{ trans('ip.rate') }}{{ trans('ip.total') }}
{{ $currentProject }}
{{ $item['project_name'] ?? '' }}{{ $item['task_name'] ?? '' }}{{ $item['description'] ?? '' }}{{ $item['hours'] ?? 0 }}{{ $item['rate'] ?? '0.00' }}{{ $item['total'] ?? '0.00' }}
+
diff --git a/resources/views/mason/bricks/detail-invoice-project/preview.blade.php b/resources/views/mason/bricks/detail-invoice-project/preview.blade.php new file mode 100644 index 000000000..2ceccfb28 --- /dev/null +++ b/resources/views/mason/bricks/detail-invoice-project/preview.blade.php @@ -0,0 +1,54 @@ +@props([ + 'config' => [] +]) + +
+ + + + @if($config['show_project_name'] ?? true) + + @endif + @if($config['show_task_name'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_hours'] ?? true) + + @endif + @if($config['show_rate'] ?? true) + + @endif + @if($config['show_total'] ?? true) + + @endif + + + + @for($i = 1; $i <= 3; $i++) + + @if($config['show_project_name'] ?? true) + + @endif + @if($config['show_task_name'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_hours'] ?? true) + + @endif + @if($config['show_rate'] ?? true) + + @endif + @if($config['show_total'] ?? true) + + @endif + + @endfor + +
{{ trans('ip.project') }}{{ trans('ip.task') }}{{ trans('ip.description') }}{{ trans('ip.hours') }}{{ trans('ip.rate') }}{{ trans('ip.total') }}
{{ trans('ip.project') }} {{ $i }}{{ trans('ip.task') }} {{ $i }}{{ trans('ip.task_description') }}{{ $i * 5 }}$75.00${{ $i * 5 * 75 }}.00
+
diff --git a/resources/views/mason/bricks/detail-items/index.blade.php b/resources/views/mason/bricks/detail-items/index.blade.php new file mode 100644 index 000000000..3ec656329 --- /dev/null +++ b/resources/views/mason/bricks/detail-items/index.blade.php @@ -0,0 +1,49 @@ +@props([ + 'config' => [], + 'data' => [] +]) + +
+ + + + @if($config['show_description'] ?? true) + + @endif + @if($config['show_quantity'] ?? true) + + @endif + @if($config['show_price'] ?? true) + + @endif + @if($config['show_tax'] ?? true) + + @endif + @if($config['show_total'] ?? true) + + @endif + + + + @foreach(($data['items'] ?? []) as $index => $item) + + @if($config['show_description'] ?? true) + + @endif + @if($config['show_quantity'] ?? true) + + @endif + @if($config['show_price'] ?? true) + + @endif + @if($config['show_tax'] ?? true) + + @endif + @if($config['show_total'] ?? true) + + @endif + + @endforeach + +
{{ trans('ip.description') }}{{ trans('ip.quantity') }}{{ trans('ip.price') }}{{ trans('ip.tax') }}{{ trans('ip.total') }}
{{ $item['description'] ?? '' }}{{ $item['quantity'] ?? 0 }}{{ $item['price'] ?? '0.00' }}{{ $item['tax'] ?? '0.00' }}{{ $item['total'] ?? '0.00' }}
+
diff --git a/resources/views/mason/bricks/detail-items/preview.blade.php b/resources/views/mason/bricks/detail-items/preview.blade.php new file mode 100644 index 000000000..43e43e8e0 --- /dev/null +++ b/resources/views/mason/bricks/detail-items/preview.blade.php @@ -0,0 +1,48 @@ +@props([ + 'config' => [] +]) + +
+ + + + @if($config['show_description'] ?? true) + + @endif + @if($config['show_quantity'] ?? true) + + @endif + @if($config['show_price'] ?? true) + + @endif + @if($config['show_tax'] ?? true) + + @endif + @if($config['show_total'] ?? true) + + @endif + + + + @for($i = 1; $i <= 3; $i++) + + @if($config['show_description'] ?? true) + + @endif + @if($config['show_quantity'] ?? true) + + @endif + @if($config['show_price'] ?? true) + + @endif + @if($config['show_tax'] ?? true) + + @endif + @if($config['show_total'] ?? true) + + @endif + + @endfor + +
{{ trans('ip.description') }}{{ trans('ip.quantity') }}{{ trans('ip.price') }}{{ trans('ip.tax') }}{{ trans('ip.total') }}
{{ trans('ip.item') }} {{ $i }}{{ $i }}$100.00$10.00$110.00
+
diff --git a/resources/views/mason/bricks/detail-quote-product/index.blade.php b/resources/views/mason/bricks/detail-quote-product/index.blade.php new file mode 100644 index 000000000..58320fa00 --- /dev/null +++ b/resources/views/mason/bricks/detail-quote-product/index.blade.php @@ -0,0 +1,61 @@ +@props([ + 'config' => [], + 'data' => [] +]) + +
+ + + + @if($config['show_sku'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_quantity'] ?? true) + + @endif + @if($config['show_unit_price'] ?? true) + + @endif + @if($config['show_tax'] ?? true) + + @endif + @if($config['show_discount'] ?? false) + + @endif + @if($config['show_total'] ?? true) + + @endif + + + + @foreach(($data['quote_items'] ?? []) as $index => $item) + + @if($config['show_sku'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_quantity'] ?? true) + + @endif + @if($config['show_unit_price'] ?? true) + + @endif + @if($config['show_tax'] ?? true) + + @endif + @if($config['show_discount'] ?? false) + + @endif + @if($config['show_total'] ?? true) + + @endif + + @endforeach + +
{{ trans('ip.sku') }}{{ trans('ip.description') }}{{ trans('ip.quantity') }}{{ trans('ip.unit_price') }}{{ trans('ip.tax') }}{{ trans('ip.discount') }}{{ trans('ip.total') }}
{{ $item['sku'] ?? '' }}{{ $item['description'] ?? '' }}{{ $item['quantity'] ?? 0 }}{{ $item['unit_price'] ?? '0.00' }}{{ $item['tax'] ?? '0.00' }}{{ $item['discount'] ?? '0.00' }}{{ $item['total'] ?? '0.00' }}
+
diff --git a/resources/views/mason/bricks/detail-quote-product/preview.blade.php b/resources/views/mason/bricks/detail-quote-product/preview.blade.php new file mode 100644 index 000000000..cfd8c75d9 --- /dev/null +++ b/resources/views/mason/bricks/detail-quote-product/preview.blade.php @@ -0,0 +1,60 @@ +@props([ + 'config' => [] +]) + +
+ + + + @if($config['show_sku'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_quantity'] ?? true) + + @endif + @if($config['show_unit_price'] ?? true) + + @endif + @if($config['show_tax'] ?? true) + + @endif + @if($config['show_discount'] ?? false) + + @endif + @if($config['show_total'] ?? true) + + @endif + + + + @for($i = 1; $i <= 3; $i++) + + @if($config['show_sku'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_quantity'] ?? true) + + @endif + @if($config['show_unit_price'] ?? true) + + @endif + @if($config['show_tax'] ?? true) + + @endif + @if($config['show_discount'] ?? false) + + @endif + @if($config['show_total'] ?? true) + + @endif + + @endfor + +
{{ trans('ip.sku') }}{{ trans('ip.description') }}{{ trans('ip.quantity') }}{{ trans('ip.unit_price') }}{{ trans('ip.tax') }}{{ trans('ip.discount') }}{{ trans('ip.total') }}
SKU-{{ str_pad($i, 3, '0', STR_PAD_LEFT) }}{{ trans('ip.product') }} {{ $i }}{{ $i }}$100.00$10.00$0.00$110.00
+
diff --git a/resources/views/mason/bricks/detail-quote-project/index.blade.php b/resources/views/mason/bricks/detail-quote-project/index.blade.php new file mode 100644 index 000000000..d96494f44 --- /dev/null +++ b/resources/views/mason/bricks/detail-quote-project/index.blade.php @@ -0,0 +1,65 @@ +@props([ + 'config' => [], + 'data' => [] +]) + +
+ + + + @if($config['show_project_name'] ?? true) + + @endif + @if($config['show_task_name'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_hours'] ?? true) + + @endif + @if($config['show_rate'] ?? true) + + @endif + @if($config['show_total'] ?? true) + + @endif + + + + @php + $currentProject = null; + $items = $data['project_items'] ?? []; + @endphp + @foreach($items as $index => $item) + @if(($config['group_by_project'] ?? true) && $currentProject !== ($item['project_name'] ?? '')) + @php $currentProject = $item['project_name'] ?? ''; @endphp + + + + @endif + + @if($config['show_project_name'] ?? true) + + @endif + @if($config['show_task_name'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_hours'] ?? true) + + @endif + @if($config['show_rate'] ?? true) + + @endif + @if($config['show_total'] ?? true) + + @endif + + @endforeach + +
{{ trans('ip.project') }}{{ trans('ip.task') }}{{ trans('ip.description') }}{{ trans('ip.hours') }}{{ trans('ip.rate') }}{{ trans('ip.total') }}
{{ $currentProject }}
{{ $item['project_name'] ?? '' }}{{ $item['task_name'] ?? '' }}{{ $item['description'] ?? '' }}{{ $item['hours'] ?? 0 }}{{ $item['rate'] ?? '0.00' }}{{ $item['total'] ?? '0.00' }}
+
diff --git a/resources/views/mason/bricks/detail-quote-project/preview.blade.php b/resources/views/mason/bricks/detail-quote-project/preview.blade.php new file mode 100644 index 000000000..2ceccfb28 --- /dev/null +++ b/resources/views/mason/bricks/detail-quote-project/preview.blade.php @@ -0,0 +1,54 @@ +@props([ + 'config' => [] +]) + +
+ + + + @if($config['show_project_name'] ?? true) + + @endif + @if($config['show_task_name'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_hours'] ?? true) + + @endif + @if($config['show_rate'] ?? true) + + @endif + @if($config['show_total'] ?? true) + + @endif + + + + @for($i = 1; $i <= 3; $i++) + + @if($config['show_project_name'] ?? true) + + @endif + @if($config['show_task_name'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_hours'] ?? true) + + @endif + @if($config['show_rate'] ?? true) + + @endif + @if($config['show_total'] ?? true) + + @endif + + @endfor + +
{{ trans('ip.project') }}{{ trans('ip.task') }}{{ trans('ip.description') }}{{ trans('ip.hours') }}{{ trans('ip.rate') }}{{ trans('ip.total') }}
{{ trans('ip.project') }} {{ $i }}{{ trans('ip.task') }} {{ $i }}{{ trans('ip.task_description') }}{{ $i * 5 }}$75.00${{ $i * 5 * 75 }}.00
+
diff --git a/resources/views/mason/bricks/detail-tasks/index.blade.php b/resources/views/mason/bricks/detail-tasks/index.blade.php new file mode 100644 index 000000000..e8e6beea3 --- /dev/null +++ b/resources/views/mason/bricks/detail-tasks/index.blade.php @@ -0,0 +1,48 @@ + + + + @if($config['show_task_number'] ?? true) + + @endif + @if($config['show_task_name'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_due_at'] ?? false) + + @endif + @if($config['show_task_price'] ?? true) + + @endif + @if($config['show_task_status'] ?? true) + + @endif + + + + @foreach(($data['tasks'] ?? []) as $task) + + @if($config['show_task_number'] ?? true) + + @endif + @if($config['show_task_name'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_due_at'] ?? false) + + @endif + @if($config['show_task_price'] ?? true) + + @endif + @if($config['show_task_status'] ?? true) + + @endif + + @endforeach + +
{{ trans('ip.number') }}{{ trans('ip.task_name') }}{{ trans('ip.description') }}{{ trans('ip.due_date') }}{{ trans('ip.price') }}{{ trans('ip.status') }}
{{ $task['task_number'] ?? '' }}{{ $task['task_name'] ?? '' }}{{ $task['description'] ?? '' }}{{ $task['due_at'] ?? '' }}{{ $task['task_price'] ?? '' }}{{ $task['task_status'] ?? '' }}
diff --git a/resources/views/mason/bricks/detail-tasks/preview.blade.php b/resources/views/mason/bricks/detail-tasks/preview.blade.php new file mode 100644 index 000000000..24b9bedf6 --- /dev/null +++ b/resources/views/mason/bricks/detail-tasks/preview.blade.php @@ -0,0 +1,49 @@ +
+
{{ trans('ip.tasks_table') }}
+ + + + @if($config['show_task_number'] ?? true) + + @endif + @if($config['show_task_name'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_due_at'] ?? false) + + @endif + @if($config['show_task_price'] ?? true) + + @endif + @if($config['show_task_status'] ?? true) + + @endif + + + + + @if($config['show_task_number'] ?? true) + + @endif + @if($config['show_task_name'] ?? true) + + @endif + @if($config['show_description'] ?? true) + + @endif + @if($config['show_due_at'] ?? false) + + @endif + @if($config['show_task_price'] ?? true) + + @endif + @if($config['show_task_status'] ?? true) + + @endif + + +
{{ trans('ip.number') }}{{ trans('ip.task_name') }}{{ trans('ip.description') }}{{ trans('ip.due_date') }}{{ trans('ip.price') }}{{ trans('ip.status') }}
TASK-001Sample TaskTask description{{ now()->addDays(7)->format('Y-m-d') }}$100.00{{ trans('ip.pending') }}
+
diff --git a/resources/views/mason/bricks/footer-notes/index.blade.php b/resources/views/mason/bricks/footer-notes/index.blade.php new file mode 100644 index 000000000..f79b54aac --- /dev/null +++ b/resources/views/mason/bricks/footer-notes/index.blade.php @@ -0,0 +1,16 @@ +@props([ + 'config' => [], + 'data' => [] +]) + + diff --git a/resources/views/mason/bricks/footer-notes/preview.blade.php b/resources/views/mason/bricks/footer-notes/preview.blade.php new file mode 100644 index 000000000..968a770e8 --- /dev/null +++ b/resources/views/mason/bricks/footer-notes/preview.blade.php @@ -0,0 +1,15 @@ +@props([ + 'config' => [] +]) + +
+
+ @if(!empty($config['footer_content'])) +
+ {{ $config['footer_content'] }} +
+ @else +

{{ trans('ip.footer_placeholder') }}

+ @endif +
+
diff --git a/resources/views/mason/bricks/footer-summary/index.blade.php b/resources/views/mason/bricks/footer-summary/index.blade.php new file mode 100644 index 000000000..911131c69 --- /dev/null +++ b/resources/views/mason/bricks/footer-summary/index.blade.php @@ -0,0 +1,7 @@ +
+ @if(!empty($config['summary_content'])) + {{ $config['summary_content'] }} + @elseif(!empty($data['summary'])) + {{ $data['summary'] }} + @endif +
diff --git a/resources/views/mason/bricks/footer-summary/preview.blade.php b/resources/views/mason/bricks/footer-summary/preview.blade.php new file mode 100644 index 000000000..e938edcf2 --- /dev/null +++ b/resources/views/mason/bricks/footer-summary/preview.blade.php @@ -0,0 +1,10 @@ +
+
{{ trans('ip.summary') }}
+
+ @if(!empty($config['summary_content'])) + {{ $config['summary_content'] }} + @else +

{{ trans('ip.summary_placeholder') }}

+ @endif +
+
diff --git a/resources/views/mason/bricks/footer-terms/index.blade.php b/resources/views/mason/bricks/footer-terms/index.blade.php new file mode 100644 index 000000000..3d5768603 --- /dev/null +++ b/resources/views/mason/bricks/footer-terms/index.blade.php @@ -0,0 +1,7 @@ +
+ @if(!empty($config['terms_content'])) + {{ $config['terms_content'] }} + @elseif(!empty($data['terms'])) + {{ $data['terms'] }} + @endif +
diff --git a/resources/views/mason/bricks/footer-terms/preview.blade.php b/resources/views/mason/bricks/footer-terms/preview.blade.php new file mode 100644 index 000000000..463ac695e --- /dev/null +++ b/resources/views/mason/bricks/footer-terms/preview.blade.php @@ -0,0 +1,10 @@ +
+
{{ trans('ip.terms_conditions') }}
+
+ @if(!empty($config['terms_content'])) + {{ $config['terms_content'] }} + @else +

{{ trans('ip.terms_placeholder') }}

+ @endif +
+
diff --git a/resources/views/mason/bricks/footer-totals/index.blade.php b/resources/views/mason/bricks/footer-totals/index.blade.php new file mode 100644 index 000000000..c21eb0b18 --- /dev/null +++ b/resources/views/mason/bricks/footer-totals/index.blade.php @@ -0,0 +1,39 @@ +@props([ + 'config' => [], + 'data' => [] +]) + +
+ + @if($config['show_subtotal'] ?? true) + + + + + @endif + @if($config['show_tax'] ?? true) + + + + + @endif + @if($config['show_total'] ?? true) + + + + + @endif + @if(($config['show_paid'] ?? false) && isset($data['totals']['paid'])) + + + + + @endif + @if(($config['show_balance'] ?? false) && isset($data['totals']['balance'])) + + + + + @endif +
{{ trans('ip.subtotal') }}:{{ $data['totals']['subtotal'] ?? '0.00' }}
{{ trans('ip.tax') }}:{{ $data['totals']['tax'] ?? '0.00' }}
{{ trans('ip.total') }}:{{ $data['totals']['total'] ?? '0.00' }}
{{ trans('ip.paid') }}:{{ $data['totals']['paid'] }}
{{ trans('ip.balance_due') }}:{{ $data['totals']['balance'] }}
+
diff --git a/resources/views/mason/bricks/footer-totals/preview.blade.php b/resources/views/mason/bricks/footer-totals/preview.blade.php new file mode 100644 index 000000000..5ad1c1e3e --- /dev/null +++ b/resources/views/mason/bricks/footer-totals/preview.blade.php @@ -0,0 +1,40 @@ +@props([ + 'config' => [] +]) + +
+
+ + @if($config['show_subtotal'] ?? true) + + + + + @endif + @if($config['show_tax'] ?? true) + + + + + @endif + @if($config['show_total'] ?? true) + + + + + @endif + @if($config['show_paid'] ?? false) + + + + + @endif + @if($config['show_balance'] ?? false) + + + + + @endif +
{{ trans('ip.subtotal') }}:$300.00
{{ trans('ip.tax') }}:$30.00
{{ trans('ip.total') }}:$330.00
{{ trans('ip.paid') }}:$0.00
{{ trans('ip.balance_due') }}:$330.00
+
+
diff --git a/resources/views/mason/bricks/header-client/index.blade.php b/resources/views/mason/bricks/header-client/index.blade.php new file mode 100644 index 000000000..a65b3e2d0 --- /dev/null +++ b/resources/views/mason/bricks/header-client/index.blade.php @@ -0,0 +1,19 @@ +@props([ + 'config' => [], + 'data' => [] +]) + +
+ {{ trans('ip.bill_to') }}
+ {{ $data['client']['name'] ?? '' }}
+ @if($config['show_address'] ?? true) + {{ $data['client']['address'] ?? '' }}
+ {{ $data['client']['city'] ?? '' }} {{ $data['client']['postal_code'] ?? '' }}
+ @endif + @if($config['show_phone'] ?? true) + {{ trans('ip.phone') }}: {{ $data['client']['phone'] ?? '' }}
+ @endif + @if($config['show_email'] ?? true) + {{ trans('ip.email') }}: {{ $data['client']['email'] ?? '' }}
+ @endif +
diff --git a/resources/views/mason/bricks/header-client/preview.blade.php b/resources/views/mason/bricks/header-client/preview.blade.php new file mode 100644 index 000000000..cf0178b29 --- /dev/null +++ b/resources/views/mason/bricks/header-client/preview.blade.php @@ -0,0 +1,19 @@ +@props([ + 'config' => [] +]) + +
+
+

{{ trans('ip.bill_to') }}

+

{{ trans('ip.client_name') }}

+ @if($config['show_address'] ?? true) +

{{ trans('ip.client_address') }}

+ @endif + @if($config['show_phone'] ?? true) +

{{ trans('ip.phone') }}: +1 555 123 4567

+ @endif + @if($config['show_email'] ?? true) +

{{ trans('ip.email') }}: client@example.com

+ @endif +
+
diff --git a/resources/views/mason/bricks/header-company/index.blade.php b/resources/views/mason/bricks/header-company/index.blade.php new file mode 100644 index 000000000..557ed5ee5 --- /dev/null +++ b/resources/views/mason/bricks/header-company/index.blade.php @@ -0,0 +1,32 @@ +@props([ + 'config' => [], + 'data' => [] +]) + +
+ + + @if(($config['show_logo'] ?? true) && isset($data['company']['logo_path'])) + + @endif + + +
+ {{ trans('ip.logo') }} + + {{ $data['company']['name'] ?? '' }}
+ @if($config['show_address'] ?? true) + {{ $data['company']['address'] ?? '' }}
+ {{ $data['company']['city'] ?? '' }} {{ $data['company']['postal_code'] ?? '' }}
+ @endif + @if($config['show_phone'] ?? true) + {{ trans('ip.phone') }}: {{ $data['company']['phone'] ?? '' }}
+ @endif + @if($config['show_email'] ?? true) + {{ trans('ip.email') }}: {{ $data['company']['email'] ?? '' }}
+ @endif + @if($config['show_vat_id'] ?? true) + {{ trans('ip.vat_id') }}: {{ $data['company']['vat_id'] ?? '' }}
+ @endif +
+
diff --git a/resources/views/mason/bricks/header-company/preview.blade.php b/resources/views/mason/bricks/header-company/preview.blade.php new file mode 100644 index 000000000..9f49ac144 --- /dev/null +++ b/resources/views/mason/bricks/header-company/preview.blade.php @@ -0,0 +1,30 @@ +@props([ + 'config' => [] +]) + +
+
+ @if($config['show_logo'] ?? true) +
+ + + +
+ @endif +
+

{{ trans('ip.company_name') }}

+ @if($config['show_address'] ?? true) +

{{ trans('ip.company_address') }}

+ @endif + @if($config['show_phone'] ?? true) +

{{ trans('ip.phone') }}: +1 234 567 890

+ @endif + @if($config['show_email'] ?? true) +

{{ trans('ip.email') }}: info@company.com

+ @endif + @if($config['show_vat_id'] ?? true) +

{{ trans('ip.vat_id') }}: 12345678

+ @endif +
+
+
diff --git a/resources/views/mason/bricks/header-invoice-meta/index.blade.php b/resources/views/mason/bricks/header-invoice-meta/index.blade.php new file mode 100644 index 000000000..c66d92637 --- /dev/null +++ b/resources/views/mason/bricks/header-invoice-meta/index.blade.php @@ -0,0 +1,33 @@ +@props([ + 'config' => [], + 'data' => [] +]) + + diff --git a/resources/views/mason/bricks/header-invoice-meta/preview.blade.php b/resources/views/mason/bricks/header-invoice-meta/preview.blade.php new file mode 100644 index 000000000..5ff202eda --- /dev/null +++ b/resources/views/mason/bricks/header-invoice-meta/preview.blade.php @@ -0,0 +1,34 @@ +@props([ + 'config' => [] +]) + +
+
+ + @if($config['show_invoice_number'] ?? true) + + + + + @endif + @if($config['show_invoice_date'] ?? true) + + + + + @endif + @if($config['show_due_date'] ?? true) + + + + + @endif + @if($config['show_po_number'] ?? false) + + + + + @endif +
{{ trans('ip.invoice_number') }}:INV-2024-001
{{ trans('ip.invoice_date') }}:{{ date('Y-m-d') }}
{{ trans('ip.due_date') }}:{{ date('Y-m-d', strtotime('+30 days')) }}
{{ trans('ip.po_number') }}:PO-12345
+
+
diff --git a/resources/views/mason/bricks/header-project/index.blade.php b/resources/views/mason/bricks/header-project/index.blade.php new file mode 100644 index 000000000..678cd90fe --- /dev/null +++ b/resources/views/mason/bricks/header-project/index.blade.php @@ -0,0 +1,17 @@ +
+ @if($config['show_project_number'] ?? true) +
{{ trans('ip.project_number') }}: {{ $data['project']['project_number'] ?? '' }}
+ @endif + @if($config['show_project_name'] ?? true) +
{{ trans('ip.project_name') }}: {{ $data['project']['project_name'] ?? '' }}
+ @endif + @if($config['show_start_date'] ?? true) +
{{ trans('ip.start_date') }}: {{ $data['project']['start_at'] ?? '' }}
+ @endif + @if($config['show_end_date'] ?? true) +
{{ trans('ip.end_date') }}: {{ $data['project']['end_at'] ?? '' }}
+ @endif + @if($config['show_status'] ?? true) +
{{ trans('ip.status') }}: {{ $data['project']['project_status'] ?? '' }}
+ @endif +
diff --git a/resources/views/mason/bricks/header-project/preview.blade.php b/resources/views/mason/bricks/header-project/preview.blade.php new file mode 100644 index 000000000..105cbb428 --- /dev/null +++ b/resources/views/mason/bricks/header-project/preview.blade.php @@ -0,0 +1,20 @@ +
+
{{ trans('ip.project_header') }}
+
+ @if($config['show_project_number'] ?? true) +
{{ trans('ip.project_number') }}: PROJECT-001
+ @endif + @if($config['show_project_name'] ?? true) +
{{ trans('ip.project_name') }}: Sample Project
+ @endif + @if($config['show_start_date'] ?? true) +
{{ trans('ip.start_date') }}: {{ now()->format('Y-m-d') }}
+ @endif + @if($config['show_end_date'] ?? true) +
{{ trans('ip.end_date') }}: {{ now()->addDays(30)->format('Y-m-d') }}
+ @endif + @if($config['show_status'] ?? true) +
{{ trans('ip.status') }}: {{ trans('ip.in_progress') }}
+ @endif +
+
diff --git a/resources/views/mason/bricks/header-quote-meta/index.blade.php b/resources/views/mason/bricks/header-quote-meta/index.blade.php new file mode 100644 index 000000000..851ecf7f3 --- /dev/null +++ b/resources/views/mason/bricks/header-quote-meta/index.blade.php @@ -0,0 +1,14 @@ +
+ @if($config['show_quote_number'] ?? true) +
{{ trans('ip.quote_number') }}: {{ $data['quote']['quote_number'] ?? '' }}
+ @endif + @if($config['show_quoted_at'] ?? true) +
{{ trans('ip.quoted_at') }}: {{ $data['quote']['quoted_at'] ?? '' }}
+ @endif + @if($config['show_expires_at'] ?? true) +
{{ trans('ip.expires_at') }}: {{ $data['quote']['quote_expires_at'] ?? '' }}
+ @endif + @if($config['show_status'] ?? true) +
{{ trans('ip.status') }}: {{ $data['quote']['quote_status'] ?? '' }}
+ @endif +
diff --git a/resources/views/mason/bricks/header-quote-meta/preview.blade.php b/resources/views/mason/bricks/header-quote-meta/preview.blade.php new file mode 100644 index 000000000..ca6b5909d --- /dev/null +++ b/resources/views/mason/bricks/header-quote-meta/preview.blade.php @@ -0,0 +1,17 @@ +
+
{{ trans('ip.quote_metadata') }}
+
+ @if($config['show_quote_number'] ?? true) +
{{ trans('ip.quote_number') }}: QUO-001
+ @endif + @if($config['show_quoted_at'] ?? true) +
{{ trans('ip.quoted_at') }}: {{ now()->format('Y-m-d') }}
+ @endif + @if($config['show_expires_at'] ?? true) +
{{ trans('ip.expires_at') }}: {{ now()->addDays(30)->format('Y-m-d') }}
+ @endif + @if($config['show_status'] ?? true) +
{{ trans('ip.status') }}: {{ trans('ip.draft') }}
+ @endif +
+