mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-18 05:02:14 +01:00
improve design for time entries index export
This commit is contained in:
committed by
Constantin Graf
parent
4281736a6d
commit
e308ca78b1
@@ -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))
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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) }}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user