|
| 1 | +# Design: "Dagsoversigt" day-overview export sheet |
| 2 | + |
| 3 | +**Date:** 2026-06-09 |
| 4 | +**Plugin:** eform-angular-timeplanning-plugin (`TimePlanning.Pn`) |
| 5 | +**Dev mode:** Base dev mode — edit in the `eform-angular-frontend` host-app mirror, then `devgetchanges.sh` back to the source repo for committing. No base repo involved. |
| 6 | + |
| 7 | +## Summary |
| 8 | + |
| 9 | +Add a new **Dagsoversigt** ("Day overview") sheet as the **first tab** in both existing |
| 10 | +Excel exports of the timeplanning plugin: |
| 11 | + |
| 12 | +- **All-workers** export (`GET reports/file-all-workers`) — one **combined** Dagsoversigt |
| 13 | + sheet containing every worker's planning days, followed by the existing Total + |
| 14 | + per-site sheets. |
| 15 | +- **Single-worker** export (`GET reports/file`) — the same sheet for just that one |
| 16 | + worker, followed by the existing Dashboard sheet. |
| 17 | + |
| 18 | +The sheet replicates the layout and formatting of the reference file |
| 19 | +`/home/rene/Downloads/Dagsoversigt.xlsx`. |
| 20 | + |
| 21 | +## Motivation |
| 22 | + |
| 23 | +The current exports present data per-worker (Dashboard) or as a per-site breakdown |
| 24 | +(Total + per-site). There is no single flat day-by-day overview that lists each |
| 25 | +worker's shifts per day in one scannable table. The Dagsoversigt sheet fills that gap |
| 26 | +and is requested as the first thing the reader sees when opening either workbook. |
| 27 | + |
| 28 | +## Reference file analysis |
| 29 | + |
| 30 | +`Dagsoversigt.xlsx` is a single banded Excel Table (`A1:U11`), one row per |
| 31 | +*(employee × day)*: |
| 32 | + |
| 33 | +| Col | Header (da) | Resx key | Notes | |
| 34 | +|-----|-------------|----------|-------| |
| 35 | +| A | Medarbejder nr. | `Employee no` | worker employee number | |
| 36 | +| B | Medarbejder | `Worker` | worker / site name | |
| 37 | +| C | Ugedag | `DayOfWeek` | localized weekday, lowercase (`torsdag`) | |
| 38 | +| D | Dato | `Date` | real date, `dd/mm/yyyy` | |
| 39 | +| E | Uge nr | `Week number` | ISO-ish week number | |
| 40 | +| F–H | Skift 1: start / stop / pause | `Shift 1: start` / `Shift 1: end` / `Shift 1: pause` | real `h:mm` time values | |
| 41 | +| I–K | Skift 2: … | `Shift 2: …` | | |
| 42 | +| L–N | Skift 3: … | `Shift 3: …` | | |
| 43 | +| O–Q | Skift 4: … | `Shift 4: …` | | |
| 44 | +| R–T | Skift 5: … | `Shift 5: …` | | |
| 45 | +| U | Timer netto | `NettoHours` | net hours, `0.00` | |
| 46 | + |
| 47 | +Notes from the reference: |
| 48 | +- All 5 shift blocks are always present (fixed 21-column layout), even when only |
| 49 | + shift 1 has data. |
| 50 | +- Banded Excel Table with autofilter and a bold header row. |
| 51 | +- The reference's `Timer netto` cells hold placeholder text; this design instead |
| 52 | + fills the column with the export's real computed net-hours value. |
| 53 | + |
| 54 | +## Decisions (settled during brainstorming) |
| 55 | + |
| 56 | +1. **Both exports** get the sheet, as the **first tab**. |
| 57 | +2. **All-workers** → **one combined sheet** (not per-site), rows interleaved across all |
| 58 | + assigned, non-resigned, non-removed sites. |
| 59 | +3. **Sort order**: by **date ascending, then employee number ascending** (matches the |
| 60 | + reference grouping: all employees for a day, then the next day). |
| 61 | +4. **Shift columns**: **always all 5** (fixed 21 columns), regardless of each site's |
| 62 | + `Third/Fourth/FifthShiftActive` flags — unlike the existing Dashboard/Total sheets |
| 63 | + which hide inactive shift columns. |
| 64 | +5. **Row scope**: one row per **planning day** per worker — the same day-set the |
| 65 | + existing `Index(...)` already returns (no new query logic; reuse it). |
| 66 | +6. **Timer netto**: reuse the existing override-aware computed value |
| 67 | + (`NettoHoursOverrideActive ? NettoHoursOverride : NettoHours`). Days without a |
| 68 | + registration simply show `0.00`. |
| 69 | +7. **Formatting fidelity**: **match the reference exactly** — real `h:mm` time values, |
| 70 | + `dd/mm/yyyy` dates, `0.00` net hours, and a banded Excel Table with autofilter + |
| 71 | + bold header. |
| 72 | +8. **Translations**: reuse existing resx keys for all 21 headers; add exactly **one** |
| 73 | + new key for the sheet name, translated into **all 25 culture files** (+ neutral). |
| 74 | +9. `DayOfWeek` keeps the existing lowercase localized weekday |
| 75 | + (`planning.Date.ToString("dddd", culture)`). |
| 76 | + |
| 77 | +## Architecture |
| 78 | + |
| 79 | +### Existing structure (for context) |
| 80 | + |
| 81 | +- Service: `Services/TimePlanningWorkingHoursService/TimePlanningWorkingHoursService.cs` |
| 82 | + - Single worker: `GenerateExcelDashboard(TimePlanningWorkingHoursRequestModel)` (~line 2343) |
| 83 | + — creates one `"Dashboard"` worksheet. |
| 84 | + - All workers: `GenerateExcelDashboard(TimePlanningWorkingHoursReportForAllWorkersRequestModel)` |
| 85 | + (~line 2847) — creates a `"Total"` sheet + one sheet per site; pre-computes a |
| 86 | + `perSiteCache` (`AllWorkersSiteCache`) of working-hours/pay data. |
| 87 | + - Shared cell builders: `CreateCell`, `CreateNumericCell`, `CreateDateCell` |
| 88 | + (StyleIndex 2), `CreateWeekNumberCell`; row builder `FillDataRow`. |
| 89 | + - `CurrentUICulture` is set before headers are read (lines ~2375 and ~2877), so |
| 90 | + `Translations.X` resolves in the user's language. |
| 91 | +- Low-level OpenXML helper: `Infrastructure/Helpers/OpenXMLHelper.cs` |
| 92 | + - `GenerateWorkbookPart1Content(WorkbookPart, List<KeyValuePair<string,string>> sheets)` |
| 93 | + registers the workbook's `Sheet` elements (name → relationship id). |
| 94 | + - `GenerateWorkbookStylesPart1Content(...)` builds the stylesheet (`CellFormats`); |
| 95 | + date format is `StyleIndex 2`. |
| 96 | +- Library: `DocumentFormat.OpenXml` 3.5.1 (transitive via `Microting.eFormApi.BasePn`). |
| 97 | +- Translations: strongly-typed `Translations` (neutral `Resources/Translations.Designer.cs`) |
| 98 | + + per-culture `Resources/Translations.<culture>.resx` satellite assemblies. The |
| 99 | + per-culture `Translations_xx` Designer classes are unused by the export. |
| 100 | + |
| 101 | +### New units |
| 102 | + |
| 103 | +**1. `BuildDayOverviewWorksheet(...)` — shared private method (new)** |
| 104 | + |
| 105 | +Single, isolated builder responsible for the entire Dagsoversigt worksheet: |
| 106 | + |
| 107 | +- **Input**: an ordered list of row items, each carrying the data needed for one row: |
| 108 | + `(EmployeeNo, WorkerOrSiteName, Date, planning)` — enough to emit all 21 cells. |
| 109 | + Both export methods construct this list and pass it in. |
| 110 | +- **Output**: appends a fully-built `WorksheetPart` (worksheet + sheet data + table + |
| 111 | + autofilter + page margins) for the Dagsoversigt sheet. |
| 112 | +- **Responsibilities**: |
| 113 | + - Header row from the existing resx keys (fixed 21 columns). |
| 114 | + - One data row per item: employee no (string), worker/site name (string), localized |
| 115 | + weekday (string), date (real date cell, `dd/mm/yyyy`), week number (numeric), five |
| 116 | + shift blocks as real `h:mm` time cells, net hours (numeric, `0.00`). |
| 117 | + - The banded Excel `TableDefinitionPart` over the full range with autofilter and |
| 118 | + header row. |
| 119 | +- **Why isolated**: one clear purpose (render the overview), well-defined input, no |
| 120 | + dependency on the surrounding export method's local state beyond the row list and |
| 121 | + culture/language. Independently reasoned about and testable. |
| 122 | + |
| 123 | +**2. Time-value helper (new, small)** |
| 124 | + |
| 125 | +Convert a shift time into an OADate time-of-day fraction (minutes-since-midnight ÷ 1440) |
| 126 | +so cells are real `h:mm` time values rather than strings. Sourced from the same |
| 127 | +underlying shift data the existing `GetShiftTime(...)` reads; empty/absent shift times |
| 128 | +produce an empty cell (not `0:00`), matching the reference where unused shifts are blank. |
| 129 | + |
| 130 | +> Implementation note: confirm the unit of `planning.ShiftNStart/Stop/Pause` and how |
| 131 | +> `GetShiftTime` derives its `HH:mm` string, then convert from that same source to the |
| 132 | +> fraction. This is the main implementation-time investigation. |
| 133 | +
|
| 134 | +**3. New cell styles in `OpenXMLHelper` stylesheet** |
| 135 | + |
| 136 | +Add (or reuse, if already present) the `CellFormats` needed: |
| 137 | +- `h:mm` time format (for shift start/stop/pause cells), |
| 138 | +- `0.00` number format (for net hours), |
| 139 | +- `dd/mm/yyyy` date format (verify whether existing `StyleIndex 2` already is `dd/mm/yyyy`; |
| 140 | + if not, add a dedicated style and use it for the Dagsoversigt Date column). |
| 141 | + |
| 142 | +New `CellFormat` entries get new `StyleIndex` values; the builder references those |
| 143 | +indices. Existing styles/indices are left unchanged to avoid disturbing the other |
| 144 | +sheets. |
| 145 | + |
| 146 | +**4. Sheet registration changes** |
| 147 | + |
| 148 | +- Single-worker export: change the workbook sheet list from `[("Dashboard","rId1")]` |
| 149 | + to `[(Translations.DayOverview,"rId1"), ("Dashboard","rId2")]`, create the Dagsoversigt |
| 150 | + worksheet part as `rId1` and the Dashboard part as `rId2`. |
| 151 | +- All-workers export: prepend `(Translations.DayOverview, "rId?")` as the first sheet and |
| 152 | + shift the existing relationship ids (Total + per-site) accordingly. |
| 153 | +- Excel tab-name limit (31 chars) and illegal chars (`: \ / ? * [ ]`): "Dagsoversigt" |
| 154 | + (and the chosen translations) must be validated/sanitized the same way existing |
| 155 | + per-site names are truncated. "Dagsoversigt" is 12 chars and clean. |
| 156 | + |
| 157 | +### Data flow |
| 158 | + |
| 159 | +``` |
| 160 | +Single-worker GenerateExcelDashboard(model) |
| 161 | + Index(model) ──► per-day plannings (Skip(1)) |
| 162 | + └─► build row list (one worker) ──► BuildDayOverviewWorksheet |
| 163 | + └─► existing Dashboard sheet (unchanged) |
| 164 | +
|
| 165 | +All-workers GenerateExcelDashboard(model) |
| 166 | + perSiteCache (existing pre-pass over all sites) |
| 167 | + └─► flatten to row list (all workers × their planning days) |
| 168 | + └─► sort by (Date asc, EmployeeNo asc) ──► BuildDayOverviewWorksheet |
| 169 | + └─► existing Total + per-site sheets (unchanged) |
| 170 | +``` |
| 171 | + |
| 172 | +The all-workers builder reuses the already-computed `perSiteCache` so no extra DB query |
| 173 | +is introduced. The single-worker builder reuses its existing `Index(...)` result. |
| 174 | + |
| 175 | +### Translation |
| 176 | + |
| 177 | +Add one key, **`DayOverview`**: |
| 178 | + |
| 179 | +1. Neutral `Resources/Translations.resx`: `<data name="DayOverview"><value>Day overview</value></data>`. |
| 180 | +2. `Resources/Translations.da.resx`: `<value>Dagsoversigt</value>`. |
| 181 | +3. All other 24 culture `.resx` files: a translated value each (filled for every |
| 182 | + language; any lower-confidence translations flagged for review). |
| 183 | +4. Neutral `Resources/Translations.Designer.cs`: add accessor |
| 184 | + ```csharp |
| 185 | + internal static string DayOverview { |
| 186 | + get { return ResourceManager.GetString("DayOverview", resourceCulture); } |
| 187 | + } |
| 188 | + ``` |
| 189 | + (key string `"DayOverview"`, no spaces, so accessor name == key.) |
| 190 | +5. Use `Translations.DayOverview` as the sheet name in both export methods. |
| 191 | + `CurrentUICulture` is already set before this point in both methods. |
| 192 | + |
| 193 | +The per-culture `Translations_xx` Designer classes are unused by the export and are not |
| 194 | +edited. The `localization.json` / `ITimePlanningLocalizationService` path is not needed |
| 195 | +(headers/sheet names use the resx path). |
| 196 | + |
| 197 | +## Error handling |
| 198 | + |
| 199 | +- The builder follows the existing pattern: wrap row construction in try/catch, |
| 200 | + `SentrySdk.CaptureException`, log, rethrow (consistent with `FillDataRow`). |
| 201 | +- Both export methods already finalize with `ValidateExcel(...)` (`OpenXmlValidator`); |
| 202 | + the added worksheet + table part must pass validation. The Excel Table part is the |
| 203 | + highest-risk element for validation — it must declare correct `ref`, column count, |
| 204 | + unique table id/name, and matching autofilter range. |
| 205 | +- Empty shift times render as empty cells (not `0:00`). |
| 206 | +- A worker/day with no data still renders a row with `0.00` net hours (decision 6). |
| 207 | + |
| 208 | +## Testing |
| 209 | + |
| 210 | +- **Unit-style / focused**: a test that feeds `BuildDayOverviewWorksheet` a small known |
| 211 | + row list and asserts the produced sheet has 21 headers in the right order, the right |
| 212 | + number of data rows, correct date/time/number style indices, and a valid table part |
| 213 | + (passes `OpenXmlValidator`). |
| 214 | +- **Sort**: assert combined all-workers rows come out ordered by (date, employee no). |
| 215 | +- **Translation**: assert the sheet name resolves to "Dagsoversigt" under `da` and |
| 216 | + "Day overview" under the neutral/`en` culture. |
| 217 | +- **Integration**: run both endpoints against seed data and open the resulting files to |
| 218 | + confirm Dagsoversigt is the first tab, the table is banded with autofilter, times show |
| 219 | + as `h:mm`, dates as `dd/mm/yyyy`, net hours as `0.00`, and the existing sheets are |
| 220 | + unchanged. |
| 221 | +- Follow the plugin's existing test conventions (verify how the current export is |
| 222 | + tested, if at all, during implementation and match it). |
| 223 | + |
| 224 | +## Out of scope |
| 225 | + |
| 226 | +- No changes to the existing Dashboard / Total / per-site sheets' content or layout. |
| 227 | +- No new DB queries or schema changes (no base repo). |
| 228 | +- No frontend/Angular changes (the export is triggered by existing endpoints/buttons). |
| 229 | +- No new column data beyond the reference's 21 columns (PlanText, Flex, Message, |
| 230 | + Comments, pay-codes, etc. are intentionally excluded from this sheet). |
| 231 | + |
| 232 | +## Files to change |
| 233 | + |
| 234 | +All in the host-app mirror `eform-angular-frontend/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/` |
| 235 | +(mirrored back to the source repo via `devgetchanges.sh`): |
| 236 | + |
| 237 | +- `Services/TimePlanningWorkingHoursService/TimePlanningWorkingHoursService.cs` |
| 238 | + — new `BuildDayOverviewWorksheet` + time-value helper; wire both export methods to add |
| 239 | + the sheet as first tab and adjust relationship ids. |
| 240 | +- `Infrastructure/Helpers/OpenXMLHelper.cs` — add `h:mm`, `0.00`, and (if needed) |
| 241 | + `dd/mm/yyyy` cell styles. |
| 242 | +- `Resources/Translations.resx` + `Resources/Translations.<25 cultures>.resx` — new |
| 243 | + `DayOverview` key. |
| 244 | +- `Resources/Translations.Designer.cs` — new `DayOverview` accessor. |
0 commit comments