Skip to content

Commit 83e1419

Browse files
authored
Merge pull request #1601 from microting/feat/dagsoversigt-export-sheet
feat: add Dagsoversigt day-overview sheet to timeplanning Excel exports
2 parents 2a98aa9 + 0f5b899 commit 83e1419

36 files changed

Lines changed: 1750 additions & 19 deletions

.github/workflows/dotnet-core-master.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,9 @@ jobs:
256256
- name: f
257257
filter: "FullyQualifiedName=TimePlanning.Pn.Test.SettingsServiceExtendedTests|FullyQualifiedName=TimePlanning.Pn.Test.PlanRegistrationHelperReadBySiteAndDateTests|FullyQualifiedName=TimePlanning.Pn.Test.PlanRegistrationHelperTests|FullyQualifiedName=TimePlanning.Pn.Test.PushNotificationServiceTests"
258258
- name: g
259-
filter: "FullyQualifiedName=TimePlanning.Pn.Test.SettingsServicePhoneNumberTests|FullyQualifiedName=TimePlanning.Pn.Test.TimePlanningWorkingHoursExportTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningAbsenceRequestGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningAuthGrpcServiceTests"
259+
filter: "FullyQualifiedName=TimePlanning.Pn.Test.SettingsServicePhoneNumberTests|FullyQualifiedName=TimePlanning.Pn.Test.TimePlanningWorkingHoursExportTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningAbsenceRequestGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningAuthGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.DagsoversigtWorksheetExportTests"
260260
- name: h
261-
filter: "FullyQualifiedName=TimePlanning.Pn.Test.SettingsServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningContentHandoverGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningPlanningsGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningSettingsGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningWorkingHoursGrpcServiceTests"
261+
filter: "FullyQualifiedName=TimePlanning.Pn.Test.SettingsServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningContentHandoverGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningPlanningsGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningSettingsGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningWorkingHoursGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.WorkingHoursExcelExportE2ETests"
262262
steps:
263263
- uses: actions/checkout@v3
264264
- name: Create docker network

.github/workflows/dotnet-core-pr.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,9 @@ jobs:
245245
- name: f
246246
filter: "FullyQualifiedName=TimePlanning.Pn.Test.SettingsServiceExtendedTests|FullyQualifiedName=TimePlanning.Pn.Test.PlanRegistrationHelperReadBySiteAndDateTests|FullyQualifiedName=TimePlanning.Pn.Test.PlanRegistrationHelperTests|FullyQualifiedName=TimePlanning.Pn.Test.PushNotificationServiceTests"
247247
- name: g
248-
filter: "FullyQualifiedName=TimePlanning.Pn.Test.SettingsServicePhoneNumberTests|FullyQualifiedName=TimePlanning.Pn.Test.TimePlanningWorkingHoursExportTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningAbsenceRequestGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningAuthGrpcServiceTests"
248+
filter: "FullyQualifiedName=TimePlanning.Pn.Test.SettingsServicePhoneNumberTests|FullyQualifiedName=TimePlanning.Pn.Test.TimePlanningWorkingHoursExportTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningAbsenceRequestGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningAuthGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.DagsoversigtWorksheetExportTests"
249249
- name: h
250-
filter: "FullyQualifiedName=TimePlanning.Pn.Test.SettingsServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningContentHandoverGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningPlanningsGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningSettingsGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningWorkingHoursGrpcServiceTests"
250+
filter: "FullyQualifiedName=TimePlanning.Pn.Test.SettingsServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningContentHandoverGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningPlanningsGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningSettingsGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.GrpcServices.TimePlanningWorkingHoursGrpcServiceTests|FullyQualifiedName=TimePlanning.Pn.Test.WorkingHoursExcelExportE2ETests"
251251
steps:
252252
- uses: actions/checkout@v3
253253
- name: Create docker network

docs/superpowers/plans/2026-06-09-dagsoversigt-export-sheet.md

Lines changed: 684 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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

Comments
 (0)