Feat/polish timesheet details (#10221)

* style(timesheet): size the page title and breadcrumb at 12px

* fix(timesheet): let each tab's card fill the page down to the footer

* style(timesheet): tighten the filter row on the Calendar, Weekly and Daily tabs

* style(timesheet): set all text on the Calendar tab to 12px

* fix(timesheet): stop hour rows showing through the calendar's sticky day header

* feat(timesheet): open the Daily selection actions in the filter row

* style(timesheet): set Daily row text to 12px

* refactor(timesheet): move the View page's inline styles into classes and drop a debug log

* fix(timesheet): align View page rows with their header and mark the selected log

* feat(time-log): lay out the Edit Time Log form on one shared grid

* fix(time-log): return to the View Time Log popup when the edit is cancelled

* fix(time-log): fit the View Time Log pane to its card so the backdrop closes it

* feat(time-log): redesign the View Time Logs popover with a header, aligned rows and a footer

* fix(timesheet): cap the Weekly tab's text at 12px like the Calendar tab

* feat(timesheet): redesign the screenshot slot popup with a header, summary strip and aligned time logs

* fix(timesheet): give the slot popup's screenshots an employee id so the viewer opens on the clicked one

* feat(timesheet): move the slot popup's summary into its header and put apps beside their title

* style(timesheet): fit the slot popup without a scrollbar and match its thumbnails and bar to the Recent Activities card

* fix(gallery): open the screenshot viewer without focusing its first button

* feat(gallery): redesign the screenshot viewer with a top bar, round nav buttons and a filmstrip

* style(gallery): darken and blur the viewer backdrop and dim missing-screenshot placeholders

* feat(gallery): sort the viewer by time, step with arrow keys and keep the active thumbnail in view

* feat(i18n): add screenshot labels that name each screenshot by its time or place in the set

* fix(timesheet): give the slot popup's screenshots alt text and name their info button

* fix(timesheet): let the Time Logs popover's time range wrap on narrow screens

* fix(gallery): name the viewer's info button, image and thumbnails and mark the selected thumbnail

* fix(gallery): scroll the filmstrip without animation when reduced motion is on
This commit is contained in:
joel kalema
2026-09-17 19:04:04 +02:00
committed by GitHub
parent 73e2318a7b
commit da19222a45
38 changed files with 2227 additions and 837 deletions
@@ -1,4 +1,4 @@
<nb-card [nbSpinner]="loading" nbSpinnerStatus="primary" nbSpinnerSize="large">
<nb-card class="calendar-card" [nbSpinner]="loading" nbSpinnerStatus="primary" nbSpinnerSize="large">
<nb-card-header class="p-0">
<div class="filters">
<div class="add-button">
@@ -3,9 +3,6 @@
:host nb-card {
background-color: var(--gauzy-card-2);
ngx-gauzy-filters {
margin: 0 -2rem;
}
nb-card-body {
height: calc(100vh - 21.75rem);
}
@@ -25,3 +22,99 @@
border-radius: nb-theme(button-rectangle-border-radius);
}
}
// Calendar and Weekly tabs: no text above 12px. Scoped to `.calendar-card` and
// `.weekly-card` (set in each component's template) because this sheet is
// forwarded into Weekly and from there into Daily, which keeps its own sizes.
:host .calendar-card,
:host .weekly-card {
font-size: 12px;
// The filter controls take their size from the table-header and select tokens
// (0.75rem, 0.8125rem once a value is picked), set by rules up to (0,4,1)
// inside the filter component — pinned here at a higher specificity instead.
// Named one by one rather than `*`, which would also shrink their chevrons.
ngx-gauzy-filters ::ng-deep {
.select-button,
.select-button.placeholder,
.activity-level-filter,
.popover-button[nbButton],
.clear-filters [nbButton] {
font-size: 12px;
}
}
}
// Calendar tab only: every piece of text in the calendar itself at exactly 12px.
:host .calendar-card {
// FullCalendar sizes its own text in `em` (a 1.75em title, events at .85em)
// and the shared overrides add a 1rem title, so nothing inherits one size.
// Every element is pinned instead, except the icon glyphs, which are sized
// off their font-size.
full-calendar ::ng-deep *:not(.fc-icon) {
font-size: 12px;
}
// The day-header row is sticky: the grid scrolls underneath it. The shared
// overrides unset its background, and every colour behind it is translucent,
// so the hour rows showed through it mid-scroll. It now paints, as one opaque
// background, the same stack it is seen against at rest — the three `card-2`
// layers (timesheet card, this card, its body) over the layout background — so
// it looks as before but covers what passes under it. The day cells keep their
// own translucent tint, now laid over an opaque row.
full-calendar ::ng-deep .fc-scrollgrid-section-sticky > * {
background:
linear-gradient(var(--gauzy-card-2), var(--gauzy-card-2)),
linear-gradient(var(--gauzy-card-2), var(--gauzy-card-2)),
linear-gradient(var(--gauzy-card-2), var(--gauzy-card-2)),
var(--layout-background-color) !important;
}
}
// The filter row that is the whole card header on the Calendar, Weekly and Daily
// tabs (Weekly forwards this sheet, Daily forwards Weekly). Its height is this
// padding plus one line of 2rem controls — no margin anywhere inside it adds to
// that. The inline 1rem ends the controls on the same inset as `+ Add Time`.
:host .filters {
margin: 0;
padding: 0.5rem 1rem;
ngx-gauzy-filters {
display: block;
margin: 0;
}
// The filter bar is authored as nested Bootstrap rows and columns, whose
// gutters are negative margins cancelled by padding, plus a 5px bottom margin
// on every filter. All of it is reset here and the spacing is a flex gap
// instead.
ngx-gauzy-filters ::ng-deep {
.main-wrapper {
width: 100%;
margin: 0;
padding: 0;
justify-content: flex-end;
}
.main-wrapper > .col-auto {
margin: 0 !important;
padding: 0;
}
// `align-items-end` is a Bootstrap utility (`!important`). Centred instead, so
// every control in the row — including Daily's selection actions — shares
// one axis.
.filter-item-list {
margin: 0;
align-items: center !important;
justify-content: flex-end;
gap: 0.5rem;
}
.filter-item-list > .col-auto,
.single-filter-wrapper {
margin: 0;
padding: 0 !important;
}
}
}
@@ -1,16 +1,9 @@
<nb-card>
<nb-card-header class="p-0">
<div class="filters">
<!-- `+ Add Time` stays on the tab strip, like Weekly and Calendar. -->
<div class="gauzy-action">
<ngx-gauzy-button-action
[hasLayoutSelector]="false"
[isDisable]="isRowSelected() ? false : !isCheckboxSelected()"
>
<ng-container
buttonTemplate
[ngTemplateOutlet]="actionButtons"
[ngTemplateOutletContext]="{ $implicit: selectedLog?.data }"
></ng-container>
<ngx-gauzy-button-action [hasLayoutSelector]="false">
<ng-container buttonTemplateVisible [ngTemplateOutlet]="visibleButtons"></ng-container>
</ngx-gauzy-button-action>
</div>
@@ -18,7 +11,22 @@
[isTimeFormat]="true"
[saveFilters]="(datePickerConfig$ | async).isSaveDatePicker"
(filtersChange)="filtersChange($event)"
></ngx-gauzy-filters>
>
<!-- The selection actions (View / Edit / Delete, Bulk action) open in the
filter row itself, leading the filter controls on the same line. -->
<div class="selection-actions">
<ngx-gauzy-button-action
[hasLayoutSelector]="false"
[isDisable]="isRowSelected() ? false : !isCheckboxSelected()"
>
<ng-container
buttonTemplate
[ngTemplateOutlet]="actionButtons"
[ngTemplateOutletContext]="{ $implicit: selectedLog?.data }"
></ng-container>
</ngx-gauzy-button-action>
</div>
</ngx-gauzy-filters>
</div>
</nb-card-header>
<nb-card-body>
@@ -12,6 +12,56 @@
align-content: center;
justify-content: flex-end;
}
// The filter row's own sizing (padding, no margins, centred flex gap) comes
// from the Calendar sheet at the base of this chain, shared with Weekly.
// View / Edit / Delete (and Bulk action), projected into the filter row ahead
// of the filter controls. Collapsed, `ngx-gauzy-button-action` takes no width,
// so the filters do not move until a row is selected; the gap to the first
// filter is the filter list's own flex gap.
.selection-actions {
display: flex;
align-items: center;
// Its standalone-row padding would push the header back open.
::ng-deep .actions-container {
padding: 0;
}
// No pill behind the strip here, and no 5px button margins from the shared
// table partial: the buttons stand in the row at the filter controls' height.
.actions {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0 !important;
background: none;
}
.actions button {
margin: 0;
height: 2rem;
min-height: 2rem;
padding-block: 0;
}
}
// Row text at 12px, the same size as the column labels above it.
nb-card-body .custom-body .content {
font-size: 12px;
// `.small` would take the To-do line down to ~10px.
.small {
font-size: inherit;
}
ngx-project ::ng-deep .project-render .name {
font-size: 12px;
line-height: 15px;
}
}
.log-container {
height: calc(100% - 50px);
}
@@ -5,3 +5,87 @@
background-color: var(--gauzy-card-2);
}
}
// Page title and breadcrumb at 12px, the size of every other piece of text on
// the timesheet tabs. The title reads `--gauzy-page-title-font-size` in both
// `ngx-header-title` and the global card-header heading rule, so the token is
// overridden for this header only; the breadcrumb sets its own 0.6875rem (11px).
:host > nb-card > nb-card-header {
--gauzy-page-title-font-size: 12px;
::ng-deep .ga-breadcrumb-item {
font-size: 12px;
}
}
// Hands the page frame's height down to the routed tab, so each tab's card fills
// the screen down to the footer instead of stopping wherever its own
// `calc(100vh - …)` estimate of the chrome above it ran out. Those heights are
// only a flex basis now: the chain below grows or shrinks them to the room
// actually left.
//
// `nb-route-tabset`'s template is the strip followed by a bare
// `<router-outlet>`, so the tab component is a SIBLING of the strip inside that
// host — which is why the chain can be closed here, once, for Daily, Weekly,
// Calendar and Approvals alike. Same chain as the dashboard and organization
// settings pages.
//
// The host itself CANNOT be the flex container: `pages.component.scss` sets
// `display: block` on every `router-outlet + *` from a (0,1,1) selector, which
// beats this `:host` (0,1,0) — see `projects/layout/layout.component.scss`. What
// the host does get, from the page frame in `styles/_overrides.scss`, is
// `flex: 1 1 auto` inside the content column, i.e. a definite height. So the
// card takes 100% of it, and the flex chain starts one level down.
:host {
min-height: 0;
> nb-card {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
> nb-card-body {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
}
}
}
:host ::ng-deep gz-dynamic-tabs {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
> nb-route-tabset {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
> .route-tabset {
flex: 0 0 auto;
}
// The outlet element is an anchor for the routed view, never a box of its
// own; without this it takes a share of the column as a flex item.
> router-outlet {
display: none;
}
> router-outlet ~ * {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
> nb-card {
flex: 1 1 auto;
min-height: 0;
}
}
}
}
@@ -3,7 +3,7 @@
<!-- `d-flex`, not `flex`: Bootstrap has no `.flex` utility, so the column
direction was being applied to a block. -->
<nb-card-header class="d-flex flex-column">
<div style="display: flex">
<div class="title-row">
<ngx-back-navigation></ngx-back-navigation>
<h4>
<!-- Every other page in this area routes its title through
@@ -39,7 +39,7 @@
<ng-template [ngxPermissionsOnly]="PermissionsEnum.ALLOW_DELETE_TIME">
<ng-template ngxTimeTrackingAuthorized [permission]="PermissionsEnum.ALLOW_DELETE_TIME">
<div class="col-auto">
<nb-checkbox status="basic" style="visibility: hidden"></nb-checkbox>
<nb-checkbox status="basic" class="checkbox-spacer"></nb-checkbox>
</div>
</ng-template>
</ng-template>
@@ -62,19 +62,17 @@
</div>
@if (dayLogs.value?.length > 0) {
<div class="custom-body">
@for (log of dayLogs.value; track log) {
@for (log of dayLogs.value; track log; let last = $last) {
<div
[class]="
dayLogs.value.at(-1) === log
? 'row m-0 py-3 align-items-center'
: 'row border-bottom m-0 py-3 align-items-center'
"
class="row m-0 py-3 align-items-center"
[class.border-bottom]="!last"
[class.selected]="selectedLog?.data?.id === log.id"
>
<ng-template [ngxPermissionsOnly]="PermissionsEnum.ALLOW_DELETE_TIME">
<ng-template ngxTimeTrackingAuthorized [permission]="PermissionsEnum.ALLOW_DELETE_TIME">
<div class="col-auto">
<nb-checkbox
[checked]="log.checked"
[checked]="selectedLog?.data?.id === log.id"
(checkedChange)="selectLog($event, log)"
status="basic"
[disabled]="log.isRunning"
@@ -118,7 +116,7 @@
</div>
</div>
<div class="col-1">
<span class="log">{{ log.logType }}</span>
<span class="log">{{ log.logType | titlecase }}</span>
</div>
<div class="col-1">
{{ log.duration | durationFormat }}
@@ -1,15 +1,18 @@
@use 'themes' as *;
// This page renders the same day/log rows as Daily, Weekly and Timesheet
// Approvals, but its stylesheet imported none of theirs and re-declared none of
// the classes the markup uses — so `.custom-header`, `.custom-body`, `.content`
// and `.log` were all unstyled here, and `.border-bottom` fell through to
// Bootstrap's own `1px solid #dee2e6`: a fixed light hairline under every row,
// which is a bright line on the near-black canvas.
// Approvals. It does not `@forward` their chain here, but it does not need to:
// the component's `styleUrls` already loads `daily.component.scss`, which
// forwards weekly and calendar. So `.content`, `.selected` and
// `.custom-header-container` are all styled on this page — the markup simply
// was not using them.
//
// The rules are spelled out here rather than pulled in with a `@forward` of the
// daily/weekly chain: that chain also carries `@shared/_pg-card` and a set of
// card-height overrides this page does not want.
// What this file is for is the handful of places where this page's structure
// differs from Daily's and the shared rules land wrong. Those overrides are
// nested under `.custom-card-body` so they out-rank the forwarded chain, whose
// own selectors (`nb-card-body .custom-body`) are more specific than a bare
// `:host .custom-body` and would otherwise win no matter what order they load
// in.
:host {
nb-card {
height: calc(100vh - 13rem);
@@ -19,49 +22,72 @@
background: unset;
}
}
}
.custom-card-body {
overflow: auto;
background-color: var(--gauzy-card-2);
.custom-card-body {
overflow: auto;
background-color: var(--gauzy-card-2);
// Column labels above each day's rows — same muted strip as Daily/Weekly.
.custom-header {
font-size: 12px;
font-weight: 600;
line-height: 15px;
letter-spacing: 0em;
text-align: left;
color: nb-theme(gauzy-text-color-2);
background: var(--gauzy-card-4);
border-radius: nb-theme(border-radius);
padding: 10px 10px 10px 12px;
flex-wrap: nowrap;
}
}
// Column labels above each day's rows — same muted strip as Daily/Weekly.
.custom-header {
font-size: 12px;
font-weight: 600;
line-height: 15px;
letter-spacing: 0em;
text-align: left;
color: nb-theme(gauzy-text-color-2);
background: var(--gauzy-card-4);
border-radius: nb-theme(border-radius);
padding: 10px 10px 10px 12px;
flex-wrap: nowrap;
}
// No `overflow`/`height` here on purpose: the card body above is already the
// scroll container, and one `.custom-body` is rendered per DAY, so making
// each of them scroll would put a second scrollbar inside the page's own.
.custom-body {
border-radius: nb-theme(border-radius);
margin-top: 6px;
@include nb-ltr(padding-right, 0.5rem);
@include nb-rtl(padding-left, 0.5rem);
.content {
// Daily and Weekly split this in two: `.custom-body` is the scroll
// container and `.content` inside it is the surface the rows sit on.
// This page has no scroll container of its own — the card body above is
// already scrolling, and one `.custom-body` is rendered per DAY, so
// making each of them scroll would put a second scrollbar inside the
// page's own. That leaves `.custom-body` playing the `.content` part,
// so it carries the panel's background and its 12px text inset; without
// them the rows sat transparent on the card and started 12px to the
// left of the header labels above them.
//
// No right-hand gutter either: that exists on Daily/Weekly to clear
// their scrollbar, and there is no scrollbar here to clear.
.custom-body {
overflow: visible;
height: auto;
background-color: var(--gauzy-card-3);
border-radius: var(--border-radius);
margin-top: 6px;
@include nb-ltr(padding-left, 12px);
@include nb-rtl(padding-right, 12px);
}
// Out-ranks Bootstrap's `.border-bottom` utility, which is `!important`.
.border-bottom {
border-bottom: 1px solid var(--gauzy-border-default-color) !important;
// Out-ranks Bootstrap's `.border-bottom` utility, which is `!important`.
.border-bottom {
border-bottom: 1px solid var(--gauzy-border-default-color) !important;
}
// Daily and Weekly drop the divider under the row that is marked
// selected, so the marker bar reads as one block. That rule is not
// `!important`, so it would lose to the one above; restate it here.
.selected.border-bottom {
border-bottom: none !important;
}
}
}
// The heading and its back arrow share a line.
.title-row {
display: flex;
}
// Holds the header strip's first column open so its labels line up with the
// rows' checkbox column below.
.checkbox-spacer {
visibility: hidden;
}
.project-name {
flex: 0 0 20%;
max-width: 20%;
@@ -121,7 +121,6 @@ export class TimesheetViewComponent extends TranslationBaseComponent implements
this.selectedLog.isSelected = this.disable;
this.selectedLog.data = log;
}
console.log(isChecked, log);
}
public clearData() {
@@ -1,4 +1,4 @@
<nb-card>
<nb-card class="weekly-card">
<nb-card-header class="p-0">
<div class="filters">
<div class="add-button">
@@ -102,6 +102,35 @@
max-width: 20%;
}
// Weekly tab only (Daily forwards this sheet): no text above 12px. The card and
// filter bar are set in the Calendar sheet's shared block; these are the pieces
// that size themselves — the project name (14px, and the whole column again at
// 14px below 1280px, see the media query at the end of this file) and the empty
// state's 18px heading and 14px message.
.weekly-card {
.content .project-name {
font-size: 12px;
}
ngx-project ::ng-deep .project-render .name {
font-size: 12px;
line-height: 15px;
}
// The day's total, the button that opens the Time Logs popover: 2px under
// the tab's 12px, and the same 10px the narrow-screen query below gives it.
.day-col [nbButton].appearance-ghost.status-basic {
font-size: 10px;
}
ngx-no-data-message ::ng-deep {
h5,
p {
font-size: 12px;
}
}
}
nb-checkbox ::ng-deep .custom-checkbox {
border-width: 2px;
}