improve design for time entries index export

This commit is contained in:
Gregor Vostrak
2024-12-08 18:32:55 +01:00
committed by Constantin Graf
parent 4281736a6d
commit e308ca78b1
6 changed files with 196 additions and 81 deletions

View File

@@ -241,7 +241,9 @@ class TimeEntryController extends Controller
]); ]);
$request = Gotenberg::chromium(config('services.gotenberg.url')) $request = Gotenberg::chromium(config('services.gotenberg.url'))
->pdf() ->pdf()
->pdfa('PDF/A-3b') ->assets(
Stream::path(resource_path('pdf/Outfit-VariableFont_wght.ttf'), 'outfit.ttf'),
)
->margins(0.39, 0.78, 0.39, 0.39) ->margins(0.39, 0.78, 0.39, 0.39)
->paperSize('8.27', '11.7') // A4 ->paperSize('8.27', '11.7') // A4
->footer(Stream::string('footer', $footerHtml)) ->footer(Stream::string('footer', $footerHtml))

View File

@@ -33,7 +33,7 @@ class ColorService
private const string VALID_REGEX = '/^#[0-9a-f]{6}$/'; private const string VALID_REGEX = '/^#[0-9a-f]{6}$/';
public function getRandomColor(string $seed = null): string public function getRandomColor(?string $seed = null): string
{ {
if ($seed !== null) { if ($seed !== null) {
srand(crc32($seed)); srand(crc32($seed));

View File

@@ -198,7 +198,9 @@ async function downloadExport(format: ExportFormat) {
'Export successful', 'Export successful',
'Export failed' 'Export failed'
); );
window.open(response.download_url, '_blank')?.focus(); if (response?.download_url) {
window.open(response.download_url as string, '_blank')?.focus();
}
} }
} }
const { getNameForReportingRowEntry, emptyPlaceholder } = useReportingStore(); const { getNameForReportingRowEntry, emptyPlaceholder } = useReportingStore();

View File

@@ -233,7 +233,9 @@ async function downloadExport(format: ExportFormat) {
'Export successful', 'Export successful',
'Export failed' 'Export failed'
); );
window.open(response.download_url, '_self')?.focus(); if (response?.download_url) {
window.open(response.download_url as string, '_blank')?.focus();
}
} }
} }
</script> </script>
@@ -252,6 +254,7 @@ async function downloadExport(format: ExportFormat) {
<ReportingExportButton <ReportingExportButton
:download="downloadExport"></ReportingExportButton> :download="downloadExport"></ReportingExportButton>
</MainContainer> </MainContainer>
<div class="py-2.5 w-full border-b border-default-background-separator"> <div class="py-2.5 w-full border-b border-default-background-separator">
<MainContainer <MainContainer
class="sm:flex space-y-4 sm:space-y-0 justify-between"> class="sm:flex space-y-4 sm:space-y-0 justify-between">

View File

@@ -72,6 +72,10 @@
color: #18181b color: #18181b
} }
table {
font-size: 14px;
}
thead { thead {
border-bottom: 1px #d4d4d8 solid; border-bottom: 1px #d4d4d8 solid;
} }
@@ -90,8 +94,15 @@
background-color: #fafafa; background-color: #fafafa;
} }
.table-wrapper {
border: 1px solid #d4d4d8;
border-radius: 8px;
overflow: hidden;
width: calc(100% - 2px)
}
table tr { table tr {
border-bottom: 1px #e4e4e7 solid border-bottom: 1px #e4e4e7 solid;
} }
table tr:last-of-type { table tr:last-of-type {
@@ -104,18 +115,6 @@
padding: 6px 12px; padding: 6px 12px;
} }
.range {
font-size: 24px;
font-weight: bold;
}
.table-wrapper {
border: 1px solid #d4d4d8;
border-radius: 8px;
overflow: hidden;
width: calc(100% - 2px)
}
.data-table { .data-table {
break-after: auto; break-after: auto;
} }
@@ -147,7 +146,6 @@
<div class="table-wrapper"> <div class="table-wrapper">
<div <div
style="background-color: #fafafa; padding: 5px 14px; border-bottom: 1px #d4d4d8 solid; display: flex; gap: 20px;"> style="background-color: #fafafa; padding: 5px 14px; border-bottom: 1px #d4d4d8 solid; display: flex; gap: 20px;">
<div style="padding: 8px 12px; border-radius: 8px;"> <div style="padding: 8px 12px; border-radius: 8px;">
@@ -187,11 +185,18 @@
<tr> <tr>
<td style="display: flex; align-items: center;"> <td style="display: flex; align-items: center;">
<div style="width: 12px; height: 12px; border-radius: 50%; background-color: {{ <div style="width: 12px; height: 12px; border-radius: 50%; background-color: {{
$group1Entry['color'] ?? $group1Entry['key'] ? $colorService->getRandomColor($group1Entry['key']) : '#CCCCCC' $group1Entry['color'] ?? ($group1Entry['key'] ? $colorService->getRandomColor($group1Entry['key']) : '#CCCCCC')
}};"> }};">
</div> </div>
<span style="padding-left: 8px;"> <span style="padding-left: 8px;">
{{ $group1Entry['description'] ?? $group1Entry['key'] ?? 'No '.Str::lower($group->description()) }}
@if($group->is(\App\Enums\TimeEntryAggregationType::Billable))
{{ $group1Entry['key'] === '1' ? 'Billable' : 'Non-billable' }}
@else
{{ $group1Entry['description'] ?? $group1Entry['key'] ?? 'No '.Str::lower($group->description()) }}
@endif
</span> </span>
</td> </td>
<td style="text-align: left;"> <td style="text-align: left;">
@@ -225,18 +230,18 @@
@foreach($aggregatedData['grouped_data'] as $group1Entry) @foreach($aggregatedData['grouped_data'] as $group1Entry)
<div class="data-table"> <div class="data-table">
<h2 class="no-break" <h2 class="no-break"
style="padding-top: 16px; padding-bottom: 8px; font-size: 20px; font-weight: 600; padding-left: 6px; color: #3f3f46;"> style="padding-top: 16px; padding-bottom: 8px; font-size: 16px; font-weight: 600; padding-left: 6px; color: #3f3f46;">
@if($group1Entry['key']) @if($group->is(\App\Enums\TimeEntryAggregationType::Billable))
{{ $group1Entry['key'] === '1' ? 'Billable' : 'Non-billable' }}
@else
<span style="color: #a1a1aa;"> <span style="color: #a1a1aa;">
{{ $group->description() }}: {{ $group->description() }}:
</span> </span>
{{ $group1Entry['description'] ?? $group1Entry['key'] ?? 'No '.Str::lower($group->description()) }}
@endif @endif
{{ $group1Entry['description'] ?? $group1Entry['key'] ?? 'No '.Str::lower($group->description()) }}
</h2> </h2>
<div class="table-wrapper"> <div class="table-wrapper">
<table style="width: 100%;"> <table style="width: 100%;">
<thead> <thead>
<tr> <tr>
@@ -266,7 +271,11 @@
@endphp @endphp
<tr> <tr>
<td> <td>
{{ $group2Entry['description'] ?? $group2Entry['key'] ?? '-' }} @if($subGroup->is(\App\Enums\TimeEntryAggregationType::Billable))
{{ $group2Entry['key'] === '1' ? 'Billable' : 'Non-billable' }}
@else
{{ $group2Entry['description'] ?? $group2Entry['key'] ?? '-' }}
@endif
</td> </td>
<td> <td>
{{ $interval->format($duration) }} {{ $interval->format($duration) }}

View File

@@ -9,9 +9,61 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Report</title> <title>Report</title>
<style> <style>
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
vertical-align: baseline;
box-sizing: border-box;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body { body {
font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; line-height: 1;
color: #555; }
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
@font-face {
font-family: 'Outfit';
src: url('outfit.ttf');
}
body {
font-family: 'Outfit', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;
color: #18181b
} }
table { table {
@@ -22,71 +74,118 @@
background-color: #eee; background-color: #eee;
} }
h1 {
font-size: 35px; .table-wrapper table th {
font-weight: bold; background-color: #fafafa;
} }
h2 { .table-wrapper {
font-size: 20px; border: 1px solid #d4d4d8;
font-weight: bold; border-radius: 8px;
overflow: hidden;
width: calc(100% - 2px)
} }
.range { table {
font-size: 24px; border-collapse: collapse;
font-weight: bold; border-spacing: 0;
text-align: left;
} }
.data { thead {
margin-top: 40px; border-bottom: 1px #d4d4d8 solid;
} }
tfoot {
border-top: 1px #d4d4d8 solid;
}
table th, table tfoot td {
font-weight: 500;
padding: 6px 12px;
color: #18181b;
}
table td, table th{
font-size: 12px;
}
table tr {
border-bottom: 1px #e4e4e7 solid;
}
table tr:last-of-type {
border-bottom: none;
}
table tr td {
font-weight: 400;
color: #3f3f46;
padding: 6px 12px;
}
</style> </style>
</head> </head>
<body> <body>
<h1>Detailed Report</h1> <div>
<p style="font-size: 32px; font-weight: 600; margin-bottom: 5px;">Detailed Report</p>
<hr> <div style="font-size: 16px; font-weight: 600; color: #71717a;">
<span>{{ $start->format('d.m.Y') }} - {{ $end->format('d.m.Y') }}</span><br><br>
<div class="range"> </div>
<span>{{ $start->format('d.m.Y') }} - {{ $end->format('d.m.Y') }}</span><br><br>
</div> </div>
<div class="table-wrapper">
<div
style="background-color: #fafafa; border-bottom: 1px #d4d4d8 solid; padding: 5px 14px; display: flex; gap: 20px;">
<div style="padding: 8px 12px; border-radius: 8px;">
<div style="color: #71717a; font-weight: 600;">Duration</div>
<div
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ $interval->format(CarbonInterval::seconds($aggregatedData['seconds'])) }} </div>
</div>
<div style="padding: 8px 12px; border-radius: 8px;">
<div style="color: #71717a; font-weight: 600;">Total cost</div>
<div
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ Money::of(BigDecimal::ofUnscaledValue($aggregatedData['cost'], 2)->__toString(), $currency)->formatTo('en_US') }} </div>
</div>
<div class="properties"> </div>
<span>Duration: {{ $interval->format(CarbonInterval::seconds($aggregatedData['seconds'])) }}</span><br> <div>
<span>Total cost: {{ Money::of(BigDecimal::ofUnscaledValue($aggregatedData['cost'], 2)->__toString(), $currency)->formatTo('en_US') }}</span><br> <table style="width: 100%;">
</div> <thead>
<div class="data">
<table>
<thead>
<tr>
<th>Description</th>
<th>Task</th>
<th>Project</th>
<th>Client</th>
<th>User</th>
<th>Duration</th>
<th>Billable</th>
<th>Tags</th>
</tr>
</thead>
<tbody>
@foreach($timeEntries as $timeEntry)
<tr> <tr>
<td>{{ $timeEntry->description === '' ? '-' : $timeEntry->description }}</td> <th>Description</th>
<td>{{ $timeEntry->task?->name ?? '-' }}</td> <th>Task</th>
<td>{{ $timeEntry->project?->name ?? '-' }}</td> <th>Project</th>
<td>{{ $timeEntry->client?->name ?? '-' }}</td> <th>Client</th>
<td>{{ $timeEntry->user->name }}</td> <th>User</th>
<td> <th>Time</th>
{{ $timeEntry->start->format('Y-m-d H:i:s') }} - {{ $timeEntry->end->format('Y-m-d H:i:s') }} <th>Duration</th>
</td> <th>Billable</th>
<td>{{ $timeEntry->billable ? 'Yes' : 'No' }}</td> <th>Tags</th>
<td>{{ count($timeEntry->tagsRelation) === 0 ? '-' : $timeEntry->tagsRelation->implode('name', ', ') }}</td>
</tr> </tr>
@endforeach </thead>
</tbody> <tbody>
</table> @foreach($timeEntries as $timeEntry)
<tr>
<td>{{ $timeEntry->description === '' ? '-' : $timeEntry->description }}</td>
<td>{{ $timeEntry->task?->name ?? '-' }}</td>
<td>{{ $timeEntry->project?->name ?? '-' }}</td>
<td>{{ $timeEntry->client?->name ?? '-' }}</td>
<td>{{ $timeEntry->user->name }}</td>
<td>
{{ $timeEntry->start->format('Y-m-d H:i:s') }} - <br> {{ $timeEntry->end->format('Y-m-d H:i:s') }}
</td>
<td>
{{ $interval->format($timeEntry->getDuration()) }}
</td>
<td>{{ $timeEntry->billable ? 'Yes' : 'No' }}</td>
<td>{{ count($timeEntry->tagsRelation) === 0 ? '-' : $timeEntry->tagsRelation->implode('name', ', ') }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div> </div>
</body> </body>
</html> </html>