Skip to content

Commit 685e134

Browse files
feat: improve dashboard widgets with richer actionable status
Expands employee, manager, and admin dashboard widgets with clearer status summaries, absence visibility, and state-aware quick context so users can understand work and availability at a glance. Made-with: Cursor
1 parent 539a01d commit 685e134

19 files changed

Lines changed: 6856 additions & 4514 deletions

appinfo/routes.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@
2929
['name' => 'time_tracking#endBreak', 'url' => '/api/break/end', 'verb' => 'POST'],
3030
['name' => 'time_tracking#getBreakStatus', 'url' => '/api/break/status', 'verb' => 'GET'],
3131

32+
// Dashboard widget workspace + API routes
33+
['name' => 'dashboard_widget#workspace', 'url' => '/dashboard-widget-workspace', 'verb' => 'GET'],
34+
['name' => 'dashboard_widget#employeeData', 'url' => '/api/dashboard-widget/employee', 'verb' => 'GET'],
35+
['name' => 'dashboard_widget#managerData', 'url' => '/api/dashboard-widget/manager', 'verb' => 'GET'],
36+
['name' => 'dashboard_widget#adminData', 'url' => '/api/dashboard-widget/admin', 'verb' => 'GET'],
37+
['name' => 'dashboard_widget#clockIn', 'url' => '/api/dashboard-widget/clock/in', 'verb' => 'POST'],
38+
['name' => 'dashboard_widget#startBreak', 'url' => '/api/dashboard-widget/break/start', 'verb' => 'POST'],
39+
['name' => 'dashboard_widget#endBreak', 'url' => '/api/dashboard-widget/break/end', 'verb' => 'POST'],
40+
['name' => 'dashboard_widget#clockOut', 'url' => '/api/dashboard-widget/clock/out', 'verb' => 'POST'],
41+
3242
// Time entry management routes
3343
['name' => 'time_entry#index_api', 'url' => '/api/time-entries-legacy', 'verb' => 'GET'],
3444
['name' => 'time_entry#create', 'url' => '/time-entries/create', 'verb' => 'GET'],

css/dashboard-widgets.css

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
/* ============================================================
2+
Dashboard Widget Workspace — scoped to .dz-workspace
3+
WCAG 2.1 AA | Nextcloud CSS variables only | Responsive
4+
============================================================ */
5+
6+
/* ── Workspace container ──────────────────────────────────────── */
7+
.dz-workspace {
8+
max-width: 800px;
9+
}
10+
11+
/* ── Page header ──────────────────────────────────────────────── */
12+
.dz-header-section {
13+
padding-bottom: 0;
14+
}
15+
16+
.dz-header__title {
17+
font-size: var(--arbeitszeitcheck-font-size-2xl, 1.5rem);
18+
font-weight: var(--arbeitszeitcheck-font-weight-bold, 700);
19+
color: var(--color-main-text);
20+
margin: 0 0 var(--space-2, 0.5rem) 0;
21+
}
22+
23+
.dz-header__desc {
24+
margin: 0;
25+
color: var(--color-text-maxcontrast);
26+
font-size: var(--arbeitszeitcheck-font-size-base, 1rem);
27+
}
28+
29+
/* ── Sections ─────────────────────────────────────────────────── */
30+
.dz-section {
31+
display: flex;
32+
flex-direction: column;
33+
gap: var(--space-4, 1rem);
34+
}
35+
36+
.dz-section__header {
37+
padding-bottom: var(--space-3, 0.75rem);
38+
border-bottom: 1px solid var(--color-border);
39+
}
40+
41+
.dz-section__title {
42+
font-size: var(--arbeitszeitcheck-font-size-lg, 1.125rem);
43+
font-weight: var(--arbeitszeitcheck-font-weight-semibold, 600);
44+
color: var(--color-main-text);
45+
margin: 0;
46+
}
47+
48+
/* ── Loading state ────────────────────────────────────────────── */
49+
/* Section pulses while aria-busy="true" to signal ongoing fetch */
50+
[aria-busy="true"].dz-section {
51+
opacity: 0.7;
52+
transition: opacity 0.2s ease;
53+
}
54+
55+
@media (prefers-reduced-motion: reduce) {
56+
[aria-busy="true"].dz-section {
57+
opacity: 1;
58+
}
59+
}
60+
61+
/* ── Screen-reader helpers ───────────────────────────────────── */
62+
.dz-sr-only {
63+
position: absolute;
64+
width: 1px;
65+
height: 1px;
66+
padding: 0;
67+
margin: -1px;
68+
overflow: hidden;
69+
clip-path: inset(100%);
70+
white-space: nowrap;
71+
border: 0;
72+
}
73+
74+
/* ── Status card ─────────────────────────────────────────────── */
75+
.dz-status-card {
76+
display: flex;
77+
flex-direction: column;
78+
gap: var(--space-4, 1rem);
79+
padding: var(--space-4, 1rem);
80+
border: 1px solid var(--color-border);
81+
border-radius: var(--radius-lg, 10px);
82+
background: var(--color-main-background);
83+
}
84+
85+
.dz-status-card[data-status="active"] { border-left: 4px solid var(--color-success); }
86+
.dz-status-card[data-status="break"] { border-left: 4px solid var(--color-warning); }
87+
.dz-status-card[data-status="paused"] { border-left: 4px solid var(--color-primary-element); }
88+
.dz-status-card[data-status="clocked_out"] { border-left: 4px solid var(--color-border); }
89+
.dz-status-card[data-status="completed"] { border-left: 4px solid var(--color-border); }
90+
91+
.dz-status-card__header {
92+
display: flex;
93+
align-items: flex-start;
94+
justify-content: space-between;
95+
gap: var(--space-3, 0.75rem);
96+
}
97+
98+
.dz-status-title-wrap {
99+
display: flex;
100+
align-items: center;
101+
gap: var(--space-2, 0.5rem);
102+
}
103+
104+
.dz-status-icon {
105+
width: 1.5rem;
106+
height: 1.5rem;
107+
display: inline-flex;
108+
align-items: center;
109+
justify-content: center;
110+
color: var(--color-text-maxcontrast);
111+
}
112+
113+
.dz-status-headings {
114+
display: flex;
115+
flex-direction: column;
116+
gap: 2px;
117+
}
118+
119+
.dz-status-eyebrow {
120+
margin: 0;
121+
font-size: var(--arbeitszeitcheck-font-size-xs, 0.75rem);
122+
color: var(--color-text-maxcontrast);
123+
}
124+
125+
/* ── Status badge ─────────────────────────────────────────────── */
126+
.dz-status-badge {
127+
display: inline-flex;
128+
align-items: center;
129+
justify-content: center;
130+
padding: var(--space-1, 0.25rem) var(--space-3, 0.75rem);
131+
border-radius: var(--radius-full, 9999px);
132+
font-size: var(--arbeitszeitcheck-font-size-sm, 0.875rem);
133+
font-weight: var(--arbeitszeitcheck-font-weight-semibold, 600);
134+
white-space: nowrap;
135+
flex-shrink: 0;
136+
background-color: var(--color-background-hover);
137+
color: var(--color-text-maxcontrast);
138+
border: 1px solid var(--color-border);
139+
}
140+
141+
.dz-status-badge[data-status="active"] {
142+
background-color: color-mix(in srgb, var(--color-success) 18%, var(--color-main-background));
143+
color: var(--color-main-text);
144+
border-color: color-mix(in srgb, var(--color-success) 50%, transparent);
145+
}
146+
147+
.dz-status-badge[data-status="break"] {
148+
background-color: color-mix(in srgb, var(--color-warning) 20%, var(--color-main-background));
149+
color: var(--color-main-text);
150+
border-color: color-mix(in srgb, var(--color-warning) 50%, transparent);
151+
}
152+
153+
.dz-status-badge[data-status="paused"] {
154+
background-color: color-mix(in srgb, var(--color-primary-element) 18%, var(--color-main-background));
155+
color: var(--color-main-text);
156+
border-color: color-mix(in srgb, var(--color-primary-element) 50%, transparent);
157+
}
158+
159+
.dz-status-badge[data-status="clocked_out"],
160+
.dz-status-badge[data-status="completed"] {
161+
background-color: var(--color-background-hover);
162+
color: var(--color-text-maxcontrast);
163+
border-color: var(--color-border);
164+
}
165+
166+
/* ── Status text line ─────────────────────────────────────────── */
167+
.dz-status-text {
168+
margin: 0;
169+
font-size: var(--arbeitszeitcheck-font-size-base, 1rem);
170+
color: var(--color-main-text);
171+
font-weight: var(--arbeitszeitcheck-font-weight-medium, 500);
172+
}
173+
174+
.dz-status-metrics {
175+
display: grid;
176+
grid-template-columns: repeat(2, minmax(0, 1fr));
177+
gap: var(--space-3, 0.75rem);
178+
}
179+
180+
.dz-metric {
181+
background: var(--color-background-dark);
182+
border: 1px solid var(--color-border);
183+
border-radius: var(--radius-base, 6px);
184+
padding: var(--space-3, 0.75rem);
185+
}
186+
187+
.dz-metric__label {
188+
margin: 0;
189+
font-size: var(--arbeitszeitcheck-font-size-xs, 0.75rem);
190+
color: var(--color-text-maxcontrast);
191+
}
192+
193+
.dz-metric__value {
194+
margin: var(--space-1, 0.25rem) 0 0;
195+
font-size: var(--arbeitszeitcheck-font-size-lg, 1.125rem);
196+
font-weight: var(--arbeitszeitcheck-font-weight-semibold, 600);
197+
color: var(--color-main-text);
198+
}
199+
200+
.dz-feedback,
201+
.dz-last-updated {
202+
margin: 0;
203+
font-size: var(--arbeitszeitcheck-font-size-sm, 0.875rem);
204+
color: var(--color-text-maxcontrast);
205+
}
206+
207+
/* ── Error display ────────────────────────────────────────────── */
208+
.dz-error {
209+
padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
210+
border: 1px solid color-mix(in srgb, var(--color-error) 40%, transparent);
211+
border-left: 4px solid var(--color-error);
212+
border-radius: var(--radius-base, 6px);
213+
background-color: color-mix(in srgb, var(--color-error) 10%, var(--color-main-background));
214+
color: var(--color-main-text);
215+
font-size: var(--arbeitszeitcheck-font-size-sm, 0.875rem);
216+
margin: 0;
217+
display: block;
218+
}
219+
220+
/* ── Action buttons ───────────────────────────────────────────── */
221+
.dz-button-row {
222+
display: flex;
223+
flex-wrap: wrap;
224+
gap: var(--space-3, 0.75rem);
225+
}
226+
227+
/*
228+
Disabled buttons: visually dimmed + not-allowed cursor.
229+
We rely on the `disabled` attribute (set by JS) rather than a CSS class
230+
so that assistive technologies correctly report the state.
231+
*/
232+
.dz-button-row .btn:disabled,
233+
.dz-button-row .btn[aria-disabled="true"] {
234+
opacity: 0.45;
235+
cursor: not-allowed;
236+
}
237+
238+
.dz-button-row .btn:focus-visible,
239+
.dz-link-row .btn:focus-visible {
240+
outline: 2px solid var(--color-primary-element);
241+
outline-offset: 2px;
242+
}
243+
244+
/* High-contrast mode: add a visible border so disabled is clear */
245+
@media (prefers-contrast: high) {
246+
.dz-button-row .btn:disabled {
247+
border: 2px dashed var(--color-border);
248+
}
249+
}
250+
251+
/* ── Link row ─────────────────────────────────────────────────── */
252+
.dz-link-row {
253+
margin-top: var(--space-2, 0.5rem);
254+
}
255+
256+
/* ── People list (team / company) ─────────────────────────────── */
257+
.dz-people-list {
258+
min-height: 2rem; /* Prevents layout jump while loading */
259+
}
260+
261+
.dz-list {
262+
list-style: none;
263+
margin: 0;
264+
padding: 0;
265+
display: flex;
266+
flex-direction: column;
267+
gap: var(--space-2, 0.5rem);
268+
}
269+
270+
.dz-list-item {
271+
display: flex;
272+
align-items: center;
273+
gap: var(--space-3, 0.75rem);
274+
padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
275+
border-radius: var(--radius-base, 6px);
276+
border: 1px solid var(--color-border);
277+
background: var(--color-main-background);
278+
transition: background-color 0.12s ease;
279+
}
280+
281+
.dz-list-item:hover {
282+
background: var(--color-background-hover);
283+
}
284+
285+
.dz-list-item__text {
286+
font-size: var(--arbeitszeitcheck-font-size-sm, 0.875rem);
287+
color: var(--color-main-text);
288+
flex: 1;
289+
min-width: 0; /* Prevents text overflow breaking layout */
290+
}
291+
292+
/* Badge within list items */
293+
.dz-list-item .dz-badge {
294+
font-size: var(--arbeitszeitcheck-font-size-xs, 0.75rem);
295+
padding: 2px var(--space-2, 0.5rem);
296+
border-radius: var(--radius-full, 9999px);
297+
white-space: nowrap;
298+
flex-shrink: 0;
299+
border: 1px solid var(--color-border);
300+
background-color: var(--color-background-hover);
301+
color: var(--color-text-maxcontrast);
302+
}
303+
304+
.dz-list-item .dz-badge[data-status="active"] {
305+
background-color: color-mix(in srgb, var(--color-success) 18%, var(--color-main-background));
306+
color: var(--color-main-text);
307+
border-color: color-mix(in srgb, var(--color-success) 50%, transparent);
308+
}
309+
310+
.dz-list-item .dz-badge[data-status="break"] {
311+
background-color: color-mix(in srgb, var(--color-warning) 20%, var(--color-main-background));
312+
color: var(--color-main-text);
313+
border-color: color-mix(in srgb, var(--color-warning) 50%, transparent);
314+
}
315+
316+
.dz-list-item .dz-badge[data-status="paused"] {
317+
background-color: color-mix(in srgb, var(--color-primary-element) 18%, var(--color-main-background));
318+
color: var(--color-main-text);
319+
border-color: color-mix(in srgb, var(--color-primary-element) 50%, transparent);
320+
}
321+
322+
/* ── Empty state ──────────────────────────────────────────────── */
323+
.dz-empty {
324+
color: var(--color-text-maxcontrast);
325+
font-size: var(--arbeitszeitcheck-font-size-sm, 0.875rem);
326+
margin: 0;
327+
padding: var(--space-3, 0.75rem) 0;
328+
}
329+
330+
/* ── Responsive ───────────────────────────────────────────────── */
331+
@media (max-width: 480px) {
332+
.dz-button-row {
333+
flex-direction: column;
334+
}
335+
336+
.dz-button-row .btn {
337+
width: 100%;
338+
}
339+
340+
.dz-status-card__header {
341+
flex-direction: column;
342+
align-items: flex-start;
343+
}
344+
345+
.dz-status-metrics {
346+
grid-template-columns: 1fr;
347+
}
348+
}
349+
350+
/* ── High contrast overrides ──────────────────────────────────── */
351+
@media (prefers-contrast: high) {
352+
.dz-status-badge,
353+
.dz-list-item .dz-badge {
354+
border-width: 2px;
355+
}
356+
357+
.dz-status-card {
358+
border-width: 2px;
359+
}
360+
}

0 commit comments

Comments
 (0)