Skip to content

Commit 19546c7

Browse files
fix: harden localized decimal handling for hour inputs
Ensure comma-based decimal values (e.g. 7,74) are parsed consistently in admin and time-entry flows, and allow two-decimal precision in relevant settings fields to avoid truncated weekly-hour calculations. Made-with: Cursor
1 parent 093bb34 commit 19546c7

7 files changed

Lines changed: 50 additions & 11 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Die App läuft vollständig innerhalb Ihrer selbst gehosteten Nextcloud‑Instan
66
### Kernfunktionen
77

88
- **Rechtskonforme Zeiterfassung**: Kommen/Gehen, Pausen, manuelle Einträge mit Begründung
9+
- Robuster Paused-Entry-Flow: pausierte Tages-Einträge werden bei erneutem Clock-In fortgesetzt (kein Duplikat), sind im Edit-Fenster wieder zugreifbar und werden bei Korrektur mit Endzeit sauber finalisiert
910
- **ArbZG‑Compliance**:
1011
- Max. tägliche Arbeitszeit (8h, erweiterbar auf 10h, reine Arbeitszeit ohne Pausen)
1112
- Wöchentliche 48‑Stunden‑Durchschnittsprüfung (6‑Monats‑Zeitraum, Manager‑Warnungen)

docs/User-Manual.de.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ Im Auswahldialog erscheinen nur Konten aus der Nextcloud-`admin`-Gruppe.
6060
## 4. Alltägliche Aufgaben
6161

6262
- **Kommen/Gehen und Pausen** über die Zeiterfassung; Korrekturen und Begründungen nach internen Regeln.
63+
- **Pausierter Eintrag (wieder aufnehmen oder korrigieren):**
64+
- Wenn ein Eintrag den Status **Pausiert** hat, zeigt das Dashboard bei **Clock In** die Aktion **Fortsetzen (nach Pause)** und setzt den gleichen Tages-Eintrag fort (statt einen Duplikat-Eintrag zu erzeugen).
65+
- Pausierte Einträge sind in den letzten 14 Tagen wieder normal **bearbeitbar/löschbar** (sofern nicht bereits genehmigt).
66+
- Beim Speichern mit Endzeit wird ein pausierter Eintrag automatisch als **Abgeschlossen** (`completed`) finalisiert.
6367
- **Abwesenheiten** beantragen und ggf. auf Freigabe warten. **Resturlaub** und Überträge werden angezeigt, wenn die Administration das gepflegt hat.
6468
- **App-Teams (empfohlene Einrichtung):** Wenn Ihre Organisation **App-Teams** nutzt und in der App **kein:e Vorgesetzte:r** für Ihr Team hinterlegt ist, werden Anträge **ohne** Vertretung beim Absenden **automatisch genehmigt**—es gäbe sonst niemanden mit Managerfreigabe. Mit **Vertretung** läuft zuerst der Vertretungs-Schritt. Die Oberfläche kann dazu einen kurzen Hinweis anzeigen.
6569
- **Älteres Gruppenmodell:** Verhalten folgt dem früheren „gleiche Gruppe“-Modell; die Administration sollte sicherstellen, dass Genehmigungen für Ihre Organisation weiterhin sinnvoll möglich sind.

docs/User-Manual.en.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ Only users in Nextcloud's `admin` group are eligible in this picker.
6161
## 4. Everyday tasks
6262

6363
- **Clock in / out** and **breaks**: Use the time tracking UI; follow your organization’s rules for corrections and comments.
64+
- **Paused entry (resume or fix):**
65+
- If an entry is in **Paused** state, the dashboard shows **Resume after break** on clock-in and continues the same-day entry instead of creating a duplicate.
66+
- Paused entries are editable/deletable again within the normal 14-day edit window (as long as they are not already approved).
67+
- When saved with an end time, a paused entry is finalized automatically as **Completed**.
6468
- **Absences**: Create requests; wait for approval if your workflow requires it. Vacation balances and carryover (**Resturlaub**) may be shown if your admin configured them.
6569
- **App teams (recommended setup):** If your organization uses **app-managed teams** and **no manager is assigned** to your team in the app, requests you submit **without** a substitute are **approved automatically** when you send them—there is nobody who could approve them in the manager workflow. If you **do** pick a substitute, the substitute step still runs first. The UI may show a short explanation when this applies.
6670
- **Legacy group-based setup:** Behavior follows the older “same group” model; your admin should ensure approvals remain workable for your organization.

js/admin-settings.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,19 @@
185185
: (Array.isArray(requireSubstituteRaw) ? requireSubstituteRaw : [requireSubstituteRaw]);
186186
delete formData['requireSubstituteTypes[]'];
187187

188-
// Convert numbers (use defaults on invalid/empty)
189-
const num = (v, def) => { const n = parseFloat(v); return (Number.isFinite(n) ? n : def); };
188+
// Convert localized decimal numbers (use defaults on invalid/empty)
189+
const parseLocalizedNumber = (v) => {
190+
if (v === undefined || v === null || String(v).trim() === '') {
191+
return Number.NaN;
192+
}
193+
const normalized = String(v).trim().replace(/\s+/g, '').replace(',', '.');
194+
const n = Number(normalized);
195+
return Number.isFinite(n) ? n : Number.NaN;
196+
};
197+
const num = (v, def) => {
198+
const n = parseLocalizedNumber(v);
199+
return Number.isFinite(n) ? n : def;
200+
};
190201
const int = (v, def) => { const n = parseInt(String(v), 10); return (Number.isInteger(n) ? n : def); };
191202
formData.maxDailyHours = num(formData.maxDailyHours, 10);
192203
formData.minRestPeriod = num(formData.minRestPeriod, 11);
@@ -486,7 +497,8 @@
486497
* Validate individual field
487498
*/
488499
function validateField(field) {
489-
const value = parseFloat(field.value);
500+
const normalized = String(field.value || '').trim().replace(/\s+/g, '').replace(',', '.');
501+
const value = Number(normalized);
490502
const min = parseFloat(field.getAttribute('min'));
491503
const max = parseFloat(field.getAttribute('max'));
492504

lib/Controller/TimeEntryController.php

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,24 @@ private function parseDate(string $dateString): \DateTime
228228
}
229229
}
230230

231+
/**
232+
* Parse localized decimal input (supports comma and dot).
233+
*
234+
* Returns null for null/empty/non-numeric input so callers can keep legacy
235+
* optional semantics while avoiding PHP's locale-insensitive float cast.
236+
*/
237+
private function parseNullableDecimal(mixed $value): ?float
238+
{
239+
if ($value === null) {
240+
return null;
241+
}
242+
$normalized = str_replace(',', '.', trim((string)$value));
243+
if ($normalized === '' || !is_numeric($normalized)) {
244+
return null;
245+
}
246+
return (float)$normalized;
247+
}
248+
231249
/**
232250
* Get time entries endpoint
233251
*
@@ -1182,7 +1200,7 @@ public function updatePost(int $id): JSONResponse
11821200

11831201
// Old format: date, hours (backward compatibility)
11841202
$date = $params['date'] ?? null;
1185-
$hours = isset($params['hours']) ? (float)$params['hours'] : null;
1203+
$hours = $this->parseNullableDecimal($params['hours'] ?? null);
11861204
$description = $params['description'] ?? null;
11871205
$project_check_project_id = $params['project_check_project_id'] ?? $params['projectCheckProjectId'] ?? null;
11881206

@@ -1306,7 +1324,7 @@ public function requestCorrection(int $id): JSONResponse
13061324

13071325
// Backward compatibility: support old format (newDate, newHours, newDescription)
13081326
$newDate = $params['newDate'] ?? null;
1309-
$newHours = isset($params['newHours']) ? (float)$params['newHours'] : null;
1327+
$newHours = $this->parseNullableDecimal($params['newHours'] ?? null);
13101328
$newDescription = $params['newDescription'] ?? null;
13111329

13121330
// Require justification for correction request
@@ -1803,7 +1821,7 @@ public function apiStore(): JSONResponse
18031821
$endTime = $params['endTime'] ?? null;
18041822
$breakStartTime = $params['breakStartTime'] ?? null;
18051823
$breakEndTime = $params['breakEndTime'] ?? null;
1806-
$hours = isset($params['hours']) ? (float)$params['hours'] : null;
1824+
$hours = $this->parseNullableDecimal($params['hours'] ?? null);
18071825
$description = $params['description'] ?? null;
18081826
$project_check_project_id = $params['project_check_project_id'] ?? $params['projectCheckProjectId'] ?? null;
18091827

@@ -2146,7 +2164,7 @@ public function apiUpdate(int $id): JSONResponse
21462164
{
21472165
$params = $this->request->getParams();
21482166
$date = $params['date'] ?? null;
2149-
$hours = isset($params['hours']) ? (float)$params['hours'] : null;
2167+
$hours = $this->parseNullableDecimal($params['hours'] ?? null);
21502168
$description = $params['description'] ?? null;
21512169
$project_check_project_id = $params['project_check_project_id'] ?? $params['projectCheckProjectId'] ?? null;
21522170

@@ -2183,7 +2201,7 @@ public function apiUpdatePost(int $id): JSONResponse
21832201

21842202
// Old format: date, hours (backward compatibility)
21852203
$date = $params['date'] ?? null;
2186-
$hours = isset($params['hours']) ? (float)$params['hours'] : null;
2204+
$hours = $this->parseNullableDecimal($params['hours'] ?? null);
21872205
$description = $params['description'] ?? null;
21882206
$project_check_project_id = $params['project_check_project_id'] ?? $params['projectCheckProjectId'] ?? null;
21892207

templates/admin-settings.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -515,11 +515,11 @@ class="form-input"
515515
value="<?php p($settings['defaultWorkingHours'] ?? 8); ?>"
516516
min="1"
517517
max="24"
518-
step="0.1"
518+
step="0.01"
519519
required
520520
aria-describedby="defaultWorkingHours-help">
521521
<p id="defaultWorkingHours-help" class="form-help">
522-
<?php p($l->t('Default daily working hours. Used for new employees until individual models are set. Decimal hours are allowed (e.g. 7.7).')); ?>
522+
<?php p($l->t('Default daily working hours. Used for new employees until individual models are set. Decimal hours are allowed (e.g. 7.74).')); ?>
523523
</p>
524524
</div>
525525
</section>

templates/personal-settings.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class="form-input"
5656
class="form-input"
5757
min="1"
5858
max="24"
59-
step="0.1"
59+
step="0.01"
6060
value="8"
6161
aria-describedby="working-hours-help"
6262
>

0 commit comments

Comments
 (0)