Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
642 changes: 642 additions & 0 deletions MASON_REFACTOR_PLAN.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
nielsdrost7 marked this conversation as resolved.
{
Expand All @@ -28,7 +26,7 @@ class ReportBuilder extends Page

public ReportTemplate $record;

public array $blocks = [];
public string $masonContent = '';

public string $selectedBlockId = '';

Expand All @@ -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
Expand Down Expand Up @@ -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();
}
}
177 changes: 177 additions & 0 deletions Modules/Core/Services/MasonStorageAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<?php

namespace Modules\Core\Services;

use Modules\Core\DTOs\BlockDTO;
use Modules\Core\DTOs\GridPositionDTO;

/**
* Adapter to convert between Mason JSON format and InvoicePlane Block structure.
*
* Mason stores its editor state as JSON with a specific structure. This adapter
* translates that format to/from our BlockDTO structure for filesystem persistence.
*/
class MasonStorageAdapter
{
/**
* Convert Mason JSON to Block DTOs for filesystem storage.
*
* @param string $masonJson Mason editor JSON state
* @return array<string, BlockDTO> 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<BlockDTO> $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();
Comment thread
nielsdrost7 marked this conversation as resolved.
$block->setId($id)
->setType($type)
->setSlug(null)
->setPosition($position)
->setConfig($config)
->setLabel($label)
->setIsCloneable(false)
Comment thread
nielsdrost7 marked this conversation as resolved.
->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(
'<div class="block-preview"><strong>%s</strong></div>',
htmlspecialchars($block->getLabel() ?? 'Block', ENT_QUOTES)
);
}
}
Loading