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
11 changes: 10 additions & 1 deletion apps/api/app/Http/Controllers/ReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ public function store(Request $request): JsonResponse
'layout.view_id' => 'nullable|uuid|exists:views,id',
'layout.show_headers_only' => 'nullable|boolean',
'layout.per_page' => 'nullable|integer|min:1|max:100',
'layout.card_columns' => 'nullable|integer|min:1|max:4',
'layout.orientation' => 'nullable|in:portrait,landscape',
'layout.compact_cards' => 'nullable|boolean',
]);

$report = Report::create($validated);
Expand Down Expand Up @@ -94,6 +97,9 @@ public function update(Request $request, Report $report): JsonResponse
'layout.view_id' => 'nullable|uuid|exists:views,id',
'layout.show_headers_only' => 'nullable|boolean',
'layout.per_page' => 'nullable|integer|min:1|max:100',
'layout.card_columns' => 'nullable|integer|min:1|max:4',
'layout.orientation' => 'nullable|in:portrait,landscape',
'layout.compact_cards' => 'nullable|boolean',
]);

$report->update($validated);
Expand Down Expand Up @@ -259,12 +265,15 @@ private function generatePdfResponse(string $reportName, array $columns, array $
{
$html = $this->renderReportHtml($reportName, $columns, $groups, $groupBy, $layout);

$orientation = ($layout['orientation'] ?? 'portrait') === 'landscape' ? 'landscape' : 'portrait';

$dompdf = new Dompdf([
'isHtml5ParserEnabled' => true,
'isPhpEnabled' => false,
'defaultPaperSize' => 'a4',
'defaultPaperOrientation' => 'portrait',
'defaultPaperOrientation' => $orientation,
]);
$dompdf->setPaper('a4', $orientation);
$dompdf->loadHtml($html);
$dompdf->render();

Expand Down
155 changes: 94 additions & 61 deletions apps/api/resources/views/reports/pdf.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
<meta charset="utf-8">
<title>{{ $reportName }}</title>
<style>
/* Dompdf takes the orientation from its constructor; this rule is what makes
the browser print the iframe preview on the same paper. */
@page {
size: A4 {{ ($layout['orientation'] ?? 'portrait') === 'landscape' ? 'landscape' : 'portrait' }};
}
body {
font-family: 'DejaVu Sans', sans-serif;
font-size: 9.5pt;
Expand Down Expand Up @@ -54,16 +59,27 @@
text-transform: uppercase;
border-top: 1px solid #e2e8f0;
}
.report-card-grid {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 0;
}
.report-card-cell {
vertical-align: top;
border: none;
padding: 0 5px 0 0;
}
.report-card {
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
padding: {{ ($layout['compact_cards'] ?? false) ? '6px' : '12px' }};
margin-bottom: {{ ($layout['compact_cards'] ?? false) ? '6px' : '12px' }};
background-color: #fff;
page-break-inside: avoid;
}
.report-card-title {
font-size: 11pt;
font-size: {{ ($layout['compact_cards'] ?? false) ? '9.5pt' : '11pt' }};
font-weight: bold;
color: #0f172a;
border-bottom: 1px solid #e2e8f0;
Expand All @@ -74,11 +90,11 @@
border: 1px dashed #cbd5e1;
border-radius: 4px;
background-color: #f8fafc;
padding: 8px;
min-height: 80px;
padding: {{ ($layout['compact_cards'] ?? false) ? '5px' : '8px' }};
min-height: {{ ($layout['compact_cards'] ?? false) ? '0' : '80px' }};
}
.report-card-field {
margin-bottom: 6px;
margin-bottom: {{ ($layout['compact_cards'] ?? false) ? '2px' : '6px' }};
}
.report-card-field-label {
font-size: 7.5pt;
Expand Down Expand Up @@ -115,64 +131,81 @@
@endif
@if (!($layout['show_headers_only'] ?? false))
@if ($view && isset($view->config['columns']))
@foreach ($group['records'] as $rec)
@php
$titleField = $view->table->fields->first(fn($f) => ($f->options['is_title'] ?? false) || $f->type === 'title');
$titleVal = null;
if ($titleField) {
foreach ($rec as $k => $v) {
if (strcasecmp($k, $titleField->name) === 0) {
$titleVal = $v;
break;
}
}
}
$columnsLayout = $view->config['columns'];
$colCount = count($columnsLayout);
$colWidth = $colCount > 1 ? '48%' : '100%';
@endphp
<div class="report-card">
@if ($titleVal)
<div class="report-card-title">{{ $titleVal }}</div>
@endif
<table style="width: 100%; border: none; margin: 0; border-collapse: collapse;">
<tr style="border: none;">
@foreach ($columnsLayout as $colFields)
<td style="width: {{ $colWidth }}; vertical-align: top; border: none; padding: 0 10px 0 0;">
<div class="report-card-col">
@foreach ($colFields as $fId)
@php
$cleanId = str_starts_with($fId, 'draft-') ? substr($fId, 6) : $fId;
$fieldDef = $view->table->fields->first(fn($f) => $f->id === $cleanId);
@endphp
@if ($fieldDef && $fieldDef->type !== 'title')
@php
$fieldVal = null;
foreach ($rec as $k => $v) {
if (strcasecmp($k, $fieldDef->name) === 0) {
$fieldVal = $v;
break;
}
}
@endphp
<div class="report-card-field">
<div class="report-card-field-label">{{ $fieldDef->name }}</div>
<div class="report-card-field-value">
@if (is_array($fieldVal))
{{ implode(', ', $fieldVal) }}
@else
{{ $fieldVal ?? '-' }}
@php
// Cards are laid out N per row; a wider grid fits far more records
// per sheet for concise views.
$cardColumns = max(1, min(4, (int) ($layout['card_columns'] ?? 1)));
$cardCellWidth = round(100 / $cardColumns, 2).'%';
@endphp
@foreach (array_chunk($group['records'], $cardColumns) as $cardRow)
<table class="report-card-grid">
<tr>
@foreach ($cardRow as $rec)
<td class="report-card-cell" style="width: {{ $cardCellWidth }};">
@php
$titleField = $view->table->fields->first(fn($f) => ($f->options['is_title'] ?? false) || $f->type === 'title');
$titleVal = null;
if ($titleField) {
foreach ($rec as $k => $v) {
if (strcasecmp($k, $titleField->name) === 0) {
$titleVal = $v;
break;
}
}
}
$columnsLayout = $view->config['columns'];
$innerColCount = count($columnsLayout);
$innerColWidth = $innerColCount > 1 ? round(100 / $innerColCount, 2).'%' : '100%';
@endphp
<div class="report-card">
@if ($titleVal)
<div class="report-card-title">{{ $titleVal }}</div>
@endif
<table style="width: 100%; border: none; margin: 0; border-collapse: collapse;">
<tr style="border: none;">
@foreach ($columnsLayout as $colFields)
<td style="width: {{ $innerColWidth }}; vertical-align: top; border: none; padding: 0 6px 0 0;">
<div class="report-card-col">
@foreach ($colFields as $fId)
@php
$cleanId = str_starts_with($fId, 'draft-') ? substr($fId, 6) : $fId;
$fieldDef = $view->table->fields->first(fn($f) => $f->id === $cleanId);
@endphp
@if ($fieldDef && $fieldDef->type !== 'title')
@php
$fieldVal = null;
foreach ($rec as $k => $v) {
if (strcasecmp($k, $fieldDef->name) === 0) {
$fieldVal = $v;
break;
}
}
@endphp
<div class="report-card-field">
<div class="report-card-field-label">{{ $fieldDef->name }}</div>
<div class="report-card-field-value">
@if (is_array($fieldVal))
{{ implode(', ', $fieldVal) }}
@else
{{ $fieldVal ?? '-' }}
@endif
</div>
</div>
@endif
</div>
@endforeach
</div>
@endif
</td>
@endforeach
</div>
</td>
@endforeach
</tr>
</table>
</div>
</tr>
</table>
</div>
</td>
@endforeach
@for ($filler = count($cardRow); $filler < $cardColumns; $filler++)
<td class="report-card-cell" style="width: {{ $cardCellWidth }};"></td>
@endfor
</tr>
</table>
@endforeach
@else
<table class="report-table">
Expand Down
125 changes: 125 additions & 0 deletions apps/api/tests/Feature/ReportFeatureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -516,3 +516,128 @@ function createAuthenticatedUser()
// Card markup, not the fallback table.
expect($response->getContent())->toContain('report-card');
});

test('layout orientation, card columns and compact flag survive a save', function () {
$setup = createAuthenticatedUser();
$user = $setup['user'];
$table = $setup['table'];
$view = View::factory()->create(['table_id' => $table->id]);

$created = $this->actingAs($user)->postJson('/api/v1/reports', [
'table_id' => $table->id,
'name' => 'Rapport paysage',
'query' => ['select' => ['Nom']],
'layout' => [
'fields' => [['name' => 'Nom', 'visible' => true, 'order' => 1]],
'view_id' => $view->id,
'orientation' => 'landscape',
'card_columns' => 3,
'compact_cards' => true,
],
]);

$created->assertStatus(201);

$report = Report::find($created->json('id'));
expect($report->layout['orientation'])->toBe('landscape');
expect($report->layout['card_columns'])->toBe(3);
expect($report->layout['compact_cards'])->toBeTrue();
});

test('card columns are rejected outside the supported range', function () {
$setup = createAuthenticatedUser();
$user = $setup['user'];
$table = $setup['table'];

$this->actingAs($user)->postJson('/api/v1/reports', [
'table_id' => $table->id,
'name' => 'Trop de colonnes',
'layout' => ['card_columns' => 9],
])->assertStatus(422);

$this->actingAs($user)->postJson('/api/v1/reports', [
'table_id' => $table->id,
'name' => 'Orientation inconnue',
'layout' => ['orientation' => 'diagonal'],
])->assertStatus(422);
});

test('card grid lays records out side by side and landscape changes the paper', function () {
$setup = createAuthenticatedUser();
$user = $setup['user'];
$table = $setup['table'];

$ville = Field::factory()->create(['table_id' => $table->id, 'name' => 'Ville', 'type' => 'text']);
$view = View::factory()->create([
'table_id' => $table->id,
'type' => 'card',
'config' => ['columns' => [[$ville->id]]],
]);

foreach (range(1, 6) as $i) {
Record::create([
'table_id' => $table->id,
'data' => ['Ville' => "Ville {$i}"],
'version' => 1,
]);
}

$payload = fn (array $extra) => [
'table_id' => $table->id,
'name' => 'Grille',
'query' => ['select' => ['Ville']],
'layout' => array_merge([
'fields' => [['name' => 'Ville', 'visible' => true, 'order' => 1]],
'view_id' => $view->id,
], $extra),
];

// One card per row: six rows of one cell.
$single = $this->actingAs($user)->postJson('/api/v1/reports/preview/html', $payload([]));
expect(substr_count($single->getContent(), '<table class="report-card-grid">'))->toBe(6);

// Three per row: two rows, still six cards.
$grid = $this->actingAs($user)->postJson('/api/v1/reports/preview/html', $payload(['card_columns' => 3]));
$gridHtml = $grid->getContent();
expect(substr_count($gridHtml, '<table class="report-card-grid">'))->toBe(2);
expect(substr_count($gridHtml, 'class="report-card"'))->toBe(6);

// Landscape must reach both the browser (@page) and Dompdf (paper size).
$landscape = $this->actingAs($user)->postJson('/api/v1/reports/preview/html', $payload(['orientation' => 'landscape']));
expect($landscape->getContent())->toContain('size: A4 landscape');

$pdf = $this->actingAs($user)->postJson('/api/v1/reports/preview/pdf', $payload(['orientation' => 'landscape']));
$pdf->assertStatus(200);
preg_match('/MediaBox\s*\[\s*[\d.]+\s+[\d.]+\s+([\d.]+)\s+([\d.]+)/', $pdf->getContent(), $box);
expect((float) $box[1])->toBeGreaterThan((float) $box[2]);
});

test('compact cards tighten the card chrome', function () {
$setup = createAuthenticatedUser();
$user = $setup['user'];
$table = $setup['table'];

$ville = Field::factory()->create(['table_id' => $table->id, 'name' => 'Ville', 'type' => 'text']);
$view = View::factory()->create([
'table_id' => $table->id,
'type' => 'card',
'config' => ['columns' => [[$ville->id]]],
]);

Record::create(['table_id' => $table->id, 'data' => ['Ville' => 'Québec'], 'version' => 1]);

$base = [
'table_id' => $table->id,
'name' => 'Condense',
'query' => ['select' => ['Ville']],
'layout' => ['view_id' => $view->id, 'fields' => [['name' => 'Ville', 'visible' => true, 'order' => 1]]],
];

$normal = $this->actingAs($user)->postJson('/api/v1/reports/preview/html', $base);
expect($normal->getContent())->toContain('min-height: 80px');

$base['layout']['compact_cards'] = true;
$compact = $this->actingAs($user)->postJson('/api/v1/reports/preview/html', $base);
expect($compact->getContent())->toContain('min-height: 0');
expect($compact->getContent())->not->toContain('min-height: 80px');
});
Loading