diff --git a/CLAUDE.md b/CLAUDE.md index 742a604f..1acb8f21 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -159,15 +159,20 @@ met legenda is sinds #113 net als de kop een herhaalbaar blok — `RenderReportR rendert per pagina één volledige `renderReport`-pass extra voor die strook, net als voor de kop), het mijlpalen- en variance-rapport (eigen DOM-component + `build*Columns` voor de PDF) en zeven **tabelrapporten** uit discussie #31 — look-ahead, kritiek/near-critical, voortgang, planningsgezondheid, -resourcebelasting per week, resourcetoewijzingen en WBS-samenvatting. Die zeven hebben een pure -rekenlaag in `src/engine/reports/` (één `ReportContext` in, rijen met rauwe waarden uit; headless -getest in `tests/planning/check-reports.ts`) en één presentatielaag: `useTableReportSpec.tsx` bouwt +resourcebelasting (per week of maand), resourcetoewijzingen en WBS-samenvatting. Die zeven hebben +een pure rekenlaag in `src/engine/reports/` (één `ReportContext` in, rijen met rauwe waarden uit; +headless getest in `tests/planning/check-reports.ts`) en één presentatielaag: `useTableReportSpec.tsx` bouwt per type een `TableReportSpec` (titel, meldingen, samenvatting, secties met een `ReportColumn`- lijst), `TableReportView.tsx` tekent daar de ``s uit en `makeSectionedRenderReport` (`pdfTable.ts`) de vector-PDF — dezelfde kolomspec, dus DOM en PDF kunnen niet uit elkaar lopen. Nieuw tabelrapport ⇒ engine-module, een `build*`-functie in `useTableReportSpec`, opties in `TableReportOptions` + `TableReportOptionsBlock`, sleutels onder `tableReports.*` in alle 14 -`report.json`-locales, en een sectie in `gids-rapporten-printen.md` (nl+en). +`report.json`-locales, en een sectie in `gids-rapporten-printen.md` (nl+en). Rapporten met een +tijdvenster (look-ahead, voortgang, belasting, toewijzingen) delen de **rapportageperiode** (issue +#120): een `ReportingPeriod` (preset rond de statusdatum, `project` of `custom` met twee ISO-dagen) +uit `src/engine/reports/reportingPeriod.ts`, per rapport opgeslagen in `TableReportOptions`, in de +UI het gedeelde `ReportingPeriodField`, en in de engine opgelost via `resolvePeriodFor(ctx, period)` +— nooit een eigen weken-getal erbij bouwen. ### State: één Zustand + Immer store, samengesteld uit slices diff --git a/public/docs/ar/gids-rapporten-printen.md b/public/docs/ar/gids-rapporten-printen.md index 1f75c300..42dbafb3 100644 --- a/public/docs/ar/gids-rapporten-printen.md +++ b/public/docs/ar/gids-rapporten-printen.md @@ -64,9 +64,25 @@ من آخر **حساب** (F5)، وتشير ملاحظة إلى جدول تغيّر منذ ذلك الحين، ويعيد تصدير PDF الحساب دائماً أولاً؛ لكل تقرير كتلة صغيرة **خيارات التقرير** تُحفظ بين الجلسات. تُختصر أيام العمل إلى *ي.ع*. +### فترة التقرير + +تعمل أربعة تقارير على نافذة زمنية: الاستشراف، والتقدّم، وتحميل الموارد، وتعيينات الموارد. وهي +تتشارك عنصر تحكّم واحداً *فترة التقرير* في خيارات التقرير، مع إعداد محفوظ لكل تقرير على حدة: + +- **الأسبوع القادم / الماضي، أو 2 أو 4 أو 6 أو 8 أو 12 أسبوعاً** و**الشهر القادم / الماضي** — تُحسب + من تاريخ حالة المشروع (أو من اليوم إن لم يُحدَّد). الإعداد المسبق شامل من الطرفين: *الأسابيع + الأربعة القادمة* يوم الخميس 10 سبتمبر تمتد حتى الأربعاء 7 أكتوبر. عند تغيير تاريخ الحالة تنتقل + النافذة معه. +- **المشروع بأكمله** — من أبكر بداية إلى آخر نهاية في الجدول. +- **مخصص** — تاريخان من اختيارك. يصبح الحقلان *من* و*إلى* قابلين للتحرير (كتابة أو منتقي تاريخ)؛ + ومع إعداد مسبق يعرضان التواريخ المحسوبة للقراءة فقط. تاريخ انتهاء يسبق تاريخ البداية أو حقل تاريخ فارغ يُميَّز + بالأحمر ولا يُطبَّق. عند العودة إلى إعداد مسبق تحلّ تواريخه محل نطاقك. + +تظهر الفترة المختارة كعنوان فرعي للتقرير وفي ملف PDF؛ ويعرضها تقرير التقدّم في الملخّص. + ### استشراف (Look-ahead) -قائمة اجتماع الموقع الأسبوعي: كل أنشطة الأسابيع الـ*N* القادمة (أربعة افتراضياً) — ما يبدأ وما يستمر وما +قائمة اجتماع الموقع الأسبوعي: كل أنشطة فترة التقرير (الشهر القادم افتراضياً) — ما يبدأ وما يستمر وما ينتهي — إضافة إلى ما كان يجب أن يحدث بالفعل. في كل صف: WBS، الاسم، البداية والنهاية، المدة المتبقية، الإنجاز، الفائض الكلي، حرج أو شبه حرج، الموارد المعيّنة وحالة: **يبدأ**، **قيد التنفيذ**، **كان يجب أن يبدأ** أو **متأخر**. يظهر أيضاً النشاط الذي يمتد على النافذة كلها. @@ -82,8 +98,10 @@ النظرة الدورية «أين نحن» عند تاريخ الحالة. يعطي الملخّص نهاية خط الأساس والنهاية المتوقعة مع الفرق بأيام العمل، والتقدّم **المخطط** مقابل **الفعلي** (كلاهما موزون بمدة المهام الورقية؛ المخطط على تواريخ خط الأساس النشط، وإلا على الجدول الحالي) والأعداد حسب الحالة. تحته خمسة أقسام: المكتمل في الفترة -الماضية، قيد التنفيذ، ما يبدأ في الفترة القادمة، المتأخر، والأنشطة الحرجة المفتوحة. الفترة (أسبوعان -افتراضياً) تنظر إلى الوراء بقدر ما تنظر إلى الأمام. +الماضية، قيد التنفيذ، ما يبدأ في الفترة القادمة، المتأخر، والأنشطة الحرجة المفتوحة. فترة التقرير (الشهر +الماضي افتراضياً) تحدد ما يُعدّ *مكتملاً في الفترة*؛ أما قسم *ما يبدأ في الفترة القادمة* فينظر إلى الأمام من +تاريخ الحالة — حتى نهاية الفترة إن كانت (جزئياً) بعد تاريخ الحالة، ومع إعداد مسبق *الماضي/الماضية …* إلى الأمام بقدر ما تنظر +الفترة إلى الوراء؛ ومع فترة مخصصة أو فترة المشروع بأكمله تقع كلها في الماضي يبقى القسم فارغاً. يعرض الملخّص كلا الحدّين. ### سلامة الجدول الزمني @@ -93,16 +111,19 @@ تأخيرات طويلة). الحدود في الخيارات؛ افتراضياً وفق DCMA: 44 يوم عمل للفائض المرتفع والمدة الطويلة، و10 للتأخيرات. الجدول النظيف فيه صفر أخطاء. -### تحميل الموارد أسبوعياً +### تحميل الموارد + +الصفوف مجمّعة حسب المورد (الاسم والنوع في الصف الأول من كل مجموعة فقط، كما في تعيينات الموارد)؛ ومن خلال *التجميع* تختار بين الأسابيع والأشهر التقويمية، وتحدد فترة التقرير أي الأسابيع أو الأشهر تظهر. -لكل مورد وأسبوع، المطلوب مقابل السعة المتاحة (بوحدة-يوم)، الفرق، ذروة اليوم، وهل الأسبوع محمّل بإفراط — -الحساب نفسه الذي يستخدمه المدرّج في علامة تبويب **الموارد**، في شكل جدول. تُدرج فقط الأسابيع التي فيها -طلب؛ ومع *الأسابيع المحمّلة بإفراط فقط* لا يبقى إلا الاختناقات. +لكل مورد وأسبوع أو شهر، المطلوب مقابل السعة المتاحة (بوحدة-يوم)، الفرق، ذروة اليوم، وهل الفترة محمّلة +بإفراط — الحساب نفسه الذي يستخدمه المدرّج في علامة تبويب **الموارد**، في شكل جدول. تُدرج فقط الفترات +التي فيها طلب؛ ومع *الفترات المحمّلة بإفراط فقط* لا يبقى إلا الاختناقات. إذا وقع فاصل صفحات في ملف PDF في +منتصف مجموعة، فلا يتكرر اسم المورد في الصفحة التالية. ### تعيينات الموارد لكل مورد الأنشطة المعيّنة له: WBS، الاسم، البداية والنهاية، المدة المتبقية، الوحدات في اليوم، الإنجاز، -الحرج والحالة. تُستبعد المهام المكتملة افتراضياً. وبنافذة بالأسابيع يصبح *استشراف الموارد*. يعدّ الملخّص +الحرج والحالة. تُستبعد المهام المكتملة افتراضياً. وبفترة تقرير (المشروع بأكمله افتراضياً) يصبح *استشراف الموارد*. يعدّ الملخّص أيضاً المهام بلا مورد. ### ملخّص هيكل تجزئة العمل diff --git a/public/docs/de/gids-rapporten-printen.md b/public/docs/de/gids-rapporten-printen.md index 60bdaa56..696a6ca6 100644 --- a/public/docs/de/gids-rapporten-printen.md +++ b/public/docs/de/gids-rapporten-printen.md @@ -71,9 +71,28 @@ rechnet der Bericht mit heute und sagt das; Termine und Puffer stammen aus der l jeder Bericht hat einen kleinen Block **Berichtsoptionen**, der zwischen Sitzungen gemerkt wird. Arbeitstage werden mit *AT* abgekürzt. +### Berichtszeitraum + +Vier Berichte arbeiten mit einem Zeitfenster: Vorschau, Fortschritt, Ressourcenauslastung und +Ressourcenzuweisungen. Sie teilen sich eine Auswahl *Berichtszeitraum* in den Berichtsoptionen, mit +einer eigenen gemerkten Einstellung pro Bericht: + +- **Nächste / letzte Woche, 2, 4, 6, 8 oder 12 Wochen** und **nächster / letzter Monat** — gerechnet + ab dem Statusdatum des Projekts (oder heute, wenn keins gesetzt ist). Eine Vorgabe ist an beiden + Enden inklusiv: *nächste 4 Wochen* am Donnerstag, 10. September, läuft bis Mittwoch, 7. Oktober. + Ändern Sie das Statusdatum, wandert das Fenster mit. +- **Gesamtes Projekt** — vom frühesten Start bis zum spätesten Ende im Terminplan. +- **Benutzerdefiniert** — zwei eigene Datumsangaben. Die Felder *Von* und *Bis* werden bearbeitbar (tippen + oder Datumsauswahl); bei einer Vorgabe zeigen sie die berechneten Daten schreibgeschützt. Ein + Enddatum vor dem Startdatum oder ein leeres Datumsfeld wird rot markiert und nicht übernommen. Wählen Sie danach wieder eine + Vorgabe, ersetzen deren Daten Ihren eigenen Bereich. + +Der gewählte Zeitraum steht als Untertitel im Bericht und im PDF; der Fortschrittsbericht zeigt ihn +in der Zusammenfassung. + ### Vorschau (Look-ahead) -Die Liste für die wöchentliche Baubesprechung: alle Vorgänge der nächsten *N* Wochen (Standard vier) +Die Liste für die wöchentliche Baubesprechung: alle Vorgänge im Berichtszeitraum (Standard: nächster Monat) — was beginnt, was weiterläuft, was endet — plus das, was bereits hätte passieren müssen. Je Zeile: PSP, Name, Start und Ende, Restdauer, Fertigstellung, Gesamtpuffer, kritisch/fast kritisch, die zugewiesenen Ressourcen und ein Status: **Beginnt**, **In Arbeit**, **Hätte starten müssen** oder @@ -93,8 +112,11 @@ Prognose-Ende mit der Differenz in Arbeitstagen, **geplanten** gegenüber **tats Fortschritt (beide dauergewichtet über die Blattvorgänge; geplant auf den Terminen des aktiven Basisplans, sonst auf der aktuellen Planung) und die Zählungen je Zustand. Darunter fünf Abschnitte: im vergangenen Zeitraum abgeschlossen, in Arbeit, Beginn im kommenden Zeitraum, -überfällig und offene kritische Vorgänge. Der Zeitraum (Standard zwei Wochen) blickt gleich weit -zurück wie voraus. +überfällig und offene kritische Vorgänge. Der Berichtszeitraum (Standard: letzter Monat) bestimmt, was als *im Zeitraum abgeschlossen* +zählt; der Abschnitt *Beginn im kommenden Zeitraum* blickt vom Statusdatum voraus — bis zum Ende +des Zeitraums, wenn dieser (teilweise) nach dem Statusdatum liegt, bei einer *Letzte …*-Vorgabe so weit +voraus, wie der Zeitraum zurückblickt; bei einem benutzerdefinierten oder Projekt-Zeitraum, der ganz +in der Vergangenheit liegt, bleibt der Abschnitt leer. Die Zusammenfassung zeigt beide Grenzen. ### Terminplan-Qualität @@ -106,18 +128,21 @@ Reihenfolge) und **Hinweise** (fast kritisch, hoher Puffer, lange Verzögerungen stehen in den Berichtsoptionen; Standard nach DCMA: 44 Arbeitstage für hohen Puffer und lange Dauer, 10 Arbeitstage für Verzögerungen. Eine saubere Planung hat null Fehler. -### Ressourcenauslastung pro Woche +### Ressourcenauslastung + +Die Zeilen sind pro Ressource gruppiert (Name und Typ nur in der ersten Zeile jeder Gruppe, wie bei den Ressourcenzuweisungen); mit *Aggregation* wählen Sie zwischen Kalenderwochen und Kalendermonaten, und der Berichtszeitraum bestimmt, welche Wochen oder Monate erscheinen. -Je Ressource und Woche der Bedarf gegenüber der verfügbaren Kapazität (in Einheiten-Tagen), die -Differenz, die Tagesspitze und ob die Woche überlastet ist — dieselbe Berechnung wie das Histogramm -auf der Registerkarte **Ressourcen**, aber als Tabelle. Nur Wochen mit Bedarf sind enthalten; mit -*Nur überlastete Wochen* bleiben nur die Engpässe. +Je Ressource und Woche oder Monat der Bedarf gegenüber der verfügbaren Kapazität (in Einheiten-Tagen), +die Differenz, die Tagesspitze und ob der Zeitraum überlastet ist — dieselbe Berechnung wie das +Histogramm auf der Registerkarte **Ressourcen**, aber als Tabelle. Nur Zeiträume mit Bedarf sind +enthalten; mit *Nur überlastete Zeiträume* bleiben nur die Engpässe. Fällt im PDF ein Seitenumbruch mitten +in eine Gruppe, wird der Ressourcenname auf der nächsten Seite nicht wiederholt. ### Ressourcenzuweisungen Je Ressource die zugewiesenen Vorgänge: PSP, Name, Start und Ende, Restdauer, Einheiten pro Tag, -Fertigstellung, kritisch und Status. Abgeschlossene Vorgänge fehlen standardmäßig. Mit einem Fenster -in Wochen wird daraus die *Ressourcen-Vorschau*. Die Zusammenfassung zählt auch die Vorgänge ohne +Fertigstellung, kritisch und Status. Abgeschlossene Vorgänge fehlen standardmäßig. Mit einem +Berichtszeitraum (Standard: gesamtes Projekt) wird daraus die *Ressourcen-Vorschau*. Die Zusammenfassung zählt auch die Vorgänge ohne Ressource. ### PSP-Zusammenfassung diff --git a/public/docs/en/gids-rapporten-printen.md b/public/docs/en/gids-rapporten-printen.md index e3471561..7e1fb41c 100644 --- a/public/docs/en/gids-rapporten-printen.md +++ b/public/docs/en/gids-rapporten-printen.md @@ -153,10 +153,29 @@ a few conventions: - A task can appear in several sections of one report when those sections each answer a different question (in progress and critical, for instance). +### Reporting period + +Four reports work on a time window: look-ahead, progress, resource loading and resource +assignments. They share one *Reporting period* control in the report options, with its own +remembered setting per report: + +- **Next / last week, 2, 4, 6, 8 or 12 weeks** and **next / last month** — counted from the + project's status date (or today if none is set). A preset is inclusive on both ends: *next 4 + weeks* on Thursday 10 September runs through Wednesday 7 October. Change the status date and the + window moves with it. +- **Project duration** — from the earliest start to the latest finish in the schedule. +- **Custom** — two dates of your own. The *From* and *To* fields become editable (type or use the + date picker); with a preset they show the calculated dates read-only. An end date before the + start date, or an empty date field, is highlighted in red and not applied. Pick a preset again and the preset dates + replace your custom range. + +The chosen period appears as the subtitle of the report and of the PDF; the progress report shows +it in its summary. + ### Look-ahead -The list for the weekly site meeting: every activity that is on the agenda for the next *N* weeks -(four by default) — what starts, what continues, what finishes — plus what should already have +The list for the weekly site meeting: every activity in the reporting period (the next month by +default) — what starts, what continues, what finishes — plus what should already have happened. Each row shows WBS, name, start and finish, remaining duration, completion, total float, whether the task is critical or near-critical, the assigned resources and a status: **Starting** (begins in the window), **In progress**, **Should have started** (start before the reference day, @@ -179,9 +198,13 @@ forecast finish with the difference in working days, **planned** versus **actual the counts per state. Both percentages are duration-weighted over the leaf tasks: a milestone weighs nothing, a month of work weighs a lot. Planned is measured on the dates of the active baseline (the agreement you measure against); without a baseline on the current schedule, and the -report says so. Below that, five sections: completed in the past period, in progress, starting in -the next period, overdue, and the open critical activities. The period (two weeks by default) -looks back as far as it looks ahead. +report says so. Below that, five sections: completed in the reporting period, in progress, starting in +the next period, overdue, and the open critical activities. The reporting period (the last +month by default) decides what counts as *completed in the period*; the *starting in the next +period* section looks ahead from the status date — to the end of the period when it lies (partly) +after the status date; for a *last …* preset as far ahead as the period looks back; for a custom or +project period that lies entirely in the past the section stays empty. The summary shows both +bounds. ### Schedule health @@ -199,20 +222,25 @@ The thresholds are in the report options. By default they follow DCMA: high floa duration above 44 working days; a lag above 10 working days. A clean schedule has zero errors; warnings and information are a reason to look, not necessarily to change. -### Resource loading per week +### Resource loading -Per resource and per week the required effort against the available capacity (in unit-days), the -difference, the peak load on a single day and whether the week is overloaded. It is the same -calculation as the histogram on the **Resources** tab, but as a table to lay side by side in a -staffing meeting. Only weeks with demand are listed; with *Overloaded weeks only* you keep just the -bottlenecks. +Per resource and per week or month the required effort against the available capacity (in +unit-days), the difference, the peak load on a single day and whether the period is overloaded. It +is the same calculation as the histogram on the **Resources** tab, but as a table to lay side by +side in a staffing meeting. Rows are grouped per resource — name and type appear on the first row +of each group only, just like the resource assignments report. Use *Aggregation* to choose between +calendar weeks and calendar months; the reporting period decides which weeks or months are +included (every week or month that touches the period, as a whole — so always the same number as +the histogram). Only periods with demand are listed; with *Overloaded periods only* you keep just +the bottlenecks. When a page break in the PDF falls inside a group, the resource name is not +repeated on the next page. ### Resource assignments Per resource which activities it is assigned to: WBS, name, start and finish, remaining duration, units per day, completion, critical and status. Completed tasks are excluded by default. With a -window in weeks it becomes the *resource look-ahead*: only what this crew or piece of equipment has -to do in the coming weeks, plus what is still open. The summary also counts the tasks without a +reporting period (the whole project by default) it becomes the *resource look-ahead*: only what +this crew or piece of equipment has to do in that period, plus what is still open. The summary also counts the tasks without a resource. ### WBS summary diff --git a/public/docs/es/gids-rapporten-printen.md b/public/docs/es/gids-rapporten-printen.md index f32a19f0..1fe9ef48 100644 --- a/public/docs/es/gids-rapporten-printen.md +++ b/public/docs/es/gids-rapporten-printen.md @@ -101,10 +101,29 @@ sin fecha de estado el informe usa hoy y lo indica; fechas y holguras vienen del recalcula antes; cada informe tiene un pequeño bloque **Opciones del informe** que se recuerda entre sesiones. Los días laborables se abrevian *dl*. +### Periodo del informe + +Cuatro informes trabajan sobre una ventana de tiempo: previsión, avance, carga de recursos y +asignaciones de recursos. Comparten un mismo control *Periodo del informe* en las opciones del +informe, con un ajuste recordado por informe: + +- **Próxima / última semana, 2, 4, 6, 8 o 12 semanas** y **próximo / último mes** — contados desde + la fecha de estado del proyecto (o hoy si no hay ninguna). Un preajuste es inclusivo en ambos + extremos: *próximas 4 semanas* el jueves 10 de septiembre llega hasta el miércoles 7 de octubre. + Cambie la fecha de estado y la ventana se mueve con ella. +- **Todo el proyecto** — desde el inicio más temprano hasta el fin más tardío del cronograma. +- **Personalizado** — dos fechas propias. Los campos *Desde* y *Hasta* pasan a ser editables + (escribir o selector de fecha); con un preajuste muestran las fechas calculadas en solo lectura. + Una fecha final anterior a la inicial, o un campo de fecha vacío, se marca en rojo y no se aplica. Si vuelve a un preajuste, + sus fechas sustituyen su rango. + +El periodo elegido aparece como subtítulo del informe y del PDF; el informe de avance lo muestra +en su resumen. + ### Previsión (look-ahead) -La lista para la reunión semanal de obra: todas las actividades de las próximas *N* semanas -(cuatro por defecto) — lo que empieza, continúa o termina — más lo que ya debería haber ocurrido. +La lista para la reunión semanal de obra: todas las actividades del periodo del informe +(el próximo mes por defecto) — lo que empieza, continúa o termina — más lo que ya debería haber ocurrido. Por fila: EDT, nombre, inicio y fin, duración restante, avance, holgura total, crítica o casi crítica, recursos asignados y un estado: **Empieza**, **En curso**, **Debió empezar** o **Atrasada**. Una actividad que abarca toda la ventana también aparece. @@ -123,7 +142,11 @@ y el fin previsto con la diferencia en días laborables, el avance **planificado (ambos ponderados por duración sobre las tareas hoja; planificado sobre las fechas de la línea base activa, si no sobre el cronograma actual) y los recuentos por estado. Debajo, cinco secciones: completadas en el periodo anterior, en curso, empiezan en el próximo periodo, atrasadas y -actividades críticas abiertas. El periodo (dos semanas por defecto) mira tanto atrás como adelante. +actividades críticas abiertas. El periodo del informe (el último mes por defecto) decide qué cuenta como *completado en el +periodo*; la sección *empiezan en el próximo periodo* mira adelante desde la fecha de estado: hasta el +fin del periodo si este queda (en parte) después de la fecha de estado, con un preajuste *último(s) …*, tan +lejos adelante como el periodo mira atrás; con un periodo personalizado o de todo el proyecto que +queda por completo en el pasado, la sección queda vacía. El resumen muestra ambos límites. ### Salud del cronograma @@ -135,18 +158,21 @@ fin abiertos, duración larga, adelantos, restricciones duras, avance fuera de s por defecto según DCMA: 44 días laborables para holgura alta y duración larga, 10 para retardos. Un cronograma limpio tiene cero errores. -### Carga de recursos por semana +### Carga de recursos + +Las filas se agrupan por recurso (nombre y tipo solo en la primera fila de cada grupo, como en las asignaciones de recursos); con *Agregación* elige entre semanas y meses naturales, y el periodo del informe determina qué semanas o meses aparecen. -Por recurso y semana, la demanda frente a la capacidad disponible (en unidades-día), la diferencia, -el pico diario y si la semana está sobrecargada — el mismo cálculo que el histograma de la pestaña -**Recursos**, en forma de tabla. Solo aparecen semanas con demanda; con *Solo semanas sobrecargadas* -quedan únicamente los cuellos de botella. +Por recurso y semana o mes, la demanda frente a la capacidad disponible (en unidades-día), la +diferencia, el pico diario y si el periodo está sobrecargado — el mismo cálculo que el histograma de +la pestaña **Recursos**, en forma de tabla. Solo aparecen periodos con demanda; con *Solo periodos +sobrecargados* quedan únicamente los cuellos de botella. Si un salto de página del PDF cae en medio +de un grupo, el nombre del recurso no se repite en la página siguiente. ### Asignaciones de recursos Por recurso, las actividades asignadas: EDT, nombre, inicio y fin, duración restante, unidades por -día, avance, crítica y estado. Las tareas completadas se excluyen por defecto. Con una ventana en -semanas se convierte en la *previsión por recurso*. El resumen cuenta también las tareas sin recurso. +día, avance, crítica y estado. Las tareas completadas se excluyen por defecto. Con un periodo del +informe (todo el proyecto por defecto) se convierte en la *previsión por recurso*. El resumen cuenta también las tareas sin recurso. ### Resumen EDT diff --git a/public/docs/fa/gids-rapporten-printen.md b/public/docs/fa/gids-rapporten-printen.md index fd47a570..841cba08 100644 --- a/public/docs/fa/gids-rapporten-printen.md +++ b/public/docs/fa/gids-rapporten-printen.md @@ -96,9 +96,28 @@ می‌شود و خروجی PDF همیشه اول دوباره محاسبه می‌کند؛ هر گزارش یک بلوک کوچک **گزینه‌های گزارش** دارد که بین نشست‌ها به خاطر سپرده می‌شود. روزهای کاری به *ر.ک* کوتاه می‌شوند. +### دورهٔ گزارش + +چهار گزارش روی یک بازهٔ زمانی کار می‌کنند: پیش‌نگر، پیشرفت، بار منابع و تخصیص منابع. این‌ها یک +کنترل مشترک *دورهٔ گزارش* در گزینه‌های گزارش دارند، با تنظیمی که برای هر گزارش جداگانه به خاطر +سپرده می‌شود: + +- **هفتهٔ آینده / گذشته، ۲، ۴، ۶، ۸ یا ۱۲ هفته** و **ماه آینده / گذشته** — از تاریخ وضعیت پروژه + (یا امروز اگر تعیین نشده) شمرده می‌شود. پیش‌تنظیم از هر دو طرف شامل است: *۴ هفتهٔ آینده* در + پنجشنبه ۱۰ سپتامبر تا چهارشنبه ۷ اکتبر ادامه دارد. با تغییر تاریخ وضعیت، بازه همراه آن جابه‌جا + می‌شود. +- **کل پروژه** — از زودترین شروع تا دیرترین پایان در برنامه. +- **سفارشی** — دو تاریخ دلخواه. فیلدهای *از* و *تا* قابل ویرایش می‌شوند (تایپ یا انتخابگر تاریخ)؛ + با یک پیش‌تنظیم، تاریخ‌های محاسبه‌شده را فقط‌خواندنی نشان می‌دهند. تاریخ پایانِ پیش از تاریخ + شروع یا فیلد تاریخ خالی قرمز می‌شود و اعمال نمی‌شود. با بازگشت به یک پیش‌تنظیم، تاریخ‌های آن جایگزین بازهٔ شما + می‌شوند. + +دورهٔ انتخاب‌شده به‌صورت زیرعنوان گزارش و PDF نمایش داده می‌شود؛ گزارش پیشرفت آن را در خلاصه نشان +می‌دهد. + ### پیش‌نگر (Look-ahead) -فهرست جلسهٔ هفتگی کارگاه: همهٔ فعالیت‌های *N* هفتهٔ آینده (پیش‌فرض چهار) — چه شروع می‌شود، چه ادامه +فهرست جلسهٔ هفتگی کارگاه: همهٔ فعالیت‌های دورهٔ گزارش (پیش‌فرض ماه آینده) — چه شروع می‌شود، چه ادامه دارد، چه تمام می‌شود — به‌علاوهٔ آنچه باید پیش‌تر انجام می‌شد. در هر سطر: WBS، نام، شروع و پایان، مدت باقی‌مانده، تکمیل، شناوری کل، بحرانی یا نزدیک به بحرانی، منابع تخصیص‌یافته و یک وضعیت: **شروع می‌شود**، **در حال انجام**، **باید شروع می‌شد** یا **عقب‌افتاده**. فعالیتی که کل بازه را می‌پوشاند نیز @@ -116,8 +135,11 @@ مرور دوره‌ای «کجا هستیم» در تاریخ وضعیت. خلاصه، پایان خط مبنا و پایان پیش‌بینی‌شده را با اختلاف به روز کاری، پیشرفت **برنامه‌ریزی‌شده** در برابر **واقعی** (هر دو وزن‌دار با مدت کارهای برگ؛ برنامه‌ریزی‌شده بر تاریخ‌های خط مبنای فعال، وگرنه بر برنامهٔ فعلی) و شمارش‌ها بر حسب وضعیت می‌دهد. زیر آن پنج بخش: -تکمیل‌شده در دورهٔ گذشته، در حال انجام، شروع در دورهٔ آینده، عقب‌افتاده و فعالیت‌های بحرانی باز. دوره -(پیش‌فرض دو هفته) به همان اندازه که به جلو نگاه می‌کند به عقب هم نگاه می‌کند. +تکمیل‌شده در دورهٔ گذشته، در حال انجام، شروع در دورهٔ آینده، عقب‌افتاده و فعالیت‌های بحرانی باز. دورهٔ +گزارش (پیش‌فرض ماه گذشته) تعیین می‌کند چه چیزی *تکمیل‌شده در دوره* به حساب می‌آید؛ بخش *شروع در +دورهٔ آینده* از تاریخ وضعیت به جلو نگاه می‌کند — تا پایان دوره اگر دوره (بخشی از آن) پس از تاریخ وضعیت +باشد، با پیش‌تنظیم *گذشته …* به همان اندازه که دوره به عقب +نگاه می‌کند؛ با دورهٔ سفارشی یا کل پروژه که کاملاً در گذشته است، این بخش خالی می‌ماند. خلاصه هر دو مرز را نشان می‌دهد. ### سلامت برنامه @@ -127,16 +149,19 @@ (نزدیک به بحرانی، شناوری زیاد، تأخیرهای طولانی). آستانه‌ها در گزینه‌ها هستند؛ پیش‌فرض طبق DCMA: ۴۴ روز کاری برای شناوری زیاد و مدت طولانی، ۱۰ برای تأخیرها. یک برنامهٔ پاک صفر خطا دارد. -### بار منابع در هفته +### بار منابع + +ردیف‌ها بر اساس منبع گروه‌بندی شده‌اند (نام و نوع فقط در اولین ردیف هر گروه، مانند تخصیص منابع)؛ با *تجمیع* بین هفته‌ها و ماه‌های تقویمی انتخاب می‌کنید و دورهٔ گزارش تعیین می‌کند کدام هفته‌ها یا ماه‌ها نمایش داده شوند. -برای هر منبع و هفته، نیاز در برابر ظرفیت در دسترس (به واحد-روز)، تفاوت، اوج روزانه و اینکه هفته -بیش‌بار است یا نه — همان محاسبهٔ هیستوگرام در برگهٔ **منابع**، به شکل جدول. فقط هفته‌های دارای نیاز -فهرست می‌شوند؛ با *فقط هفته‌های بیش‌بار* فقط گلوگاه‌ها می‌مانند. +برای هر منبع و هفته یا ماه، نیاز در برابر ظرفیت در دسترس (به واحد-روز)، تفاوت، اوج روزانه و اینکه +دوره بیش‌بار است یا نه — همان محاسبهٔ هیستوگرام در برگهٔ **منابع**، به شکل جدول. فقط دوره‌های دارای +نیاز فهرست می‌شوند؛ با *فقط دوره‌های بیش‌بار* فقط گلوگاه‌ها می‌مانند. اگر در PDF شکست صفحه وسط یک گروه بیفتد، نام منبع +در صفحهٔ بعد تکرار نمی‌شود. ### تخصیص منابع برای هر منبع فعالیت‌های تخصیص‌یافته: WBS، نام، شروع و پایان، مدت باقی‌مانده، واحد در روز، تکمیل، -بحرانی و وضعیت. کارهای تکمیل‌شده به‌طور پیش‌فرض کنار گذاشته می‌شوند. با بازه‌ای به هفته، این گزارش به +بحرانی و وضعیت. کارهای تکمیل‌شده به‌طور پیش‌فرض کنار گذاشته می‌شوند. با یک دورهٔ گزارش (پیش‌فرض کل پروژه)، این گزارش به *پیش‌نگر منابع* تبدیل می‌شود. خلاصه کارهای بدون منبع را هم می‌شمارد. ### خلاصهٔ WBS diff --git a/public/docs/fr/gids-rapporten-printen.md b/public/docs/fr/gids-rapporten-printen.md index 0fdd3eb5..c15d04ef 100644 --- a/public/docs/fr/gids-rapporten-printen.md +++ b/public/docs/fr/gids-rapporten-printen.md @@ -71,10 +71,30 @@ marges viennent du dernier **calcul** (F5), une note signale un planning modifi PDF recalcule toujours d'abord ; chaque rapport a un petit bloc **Options du rapport**, mémorisé entre les sessions. Les jours ouvrés sont abrégés en *jo*. +### Période de rapport + +Quatre rapports travaillent sur une fenêtre de temps : prévision, avancement, charge des ressources +et affectations des ressources. Ils partagent un même réglage *Période de rapport* dans les options +du rapport, avec un choix mémorisé par rapport : + +- **Semaine / 2, 4, 6, 8 ou 12 semaines prochaines ou dernières** et **mois prochain / dernier** — + comptés à partir de la date d'état du projet (ou d'aujourd'hui s'il n'y en a pas). Un préréglage + est inclusif aux deux bouts : *4 prochaines semaines* le jeudi 10 septembre va jusqu'au mercredi + 7 octobre. Changez la date d'état et la fenêtre suit. +- **Tout le projet** — du premier début à la dernière fin du planning. +- **Personnalisée** — deux dates de votre choix. Les champs *Du* et *Au* deviennent modifiables (saisie ou + sélecteur de date) ; avec un préréglage ils affichent les dates calculées en lecture seule. Une + date de fin antérieure à la date de début, ou un champ de date vide, est signalée en rouge et n'est + pas appliquée. En + revenant à un préréglage, ses dates remplacent votre plage. + +La période choisie apparaît en sous-titre du rapport et du PDF ; le rapport d'avancement l'affiche +dans sa synthèse. + ### Prévision (look-ahead) -La liste de la réunion de chantier hebdomadaire : toutes les activités des *N* prochaines semaines -(quatre par défaut) — ce qui démarre, continue ou se termine — plus ce qui aurait déjà dû se faire. +La liste de la réunion de chantier hebdomadaire : toutes les activités de la période de rapport +(le mois prochain par défaut) — ce qui démarre, continue ou se termine — plus ce qui aurait déjà dû se faire. Par ligne : WBS, nom, début et fin, durée restante, avancement, marge totale, critique ou quasi critique, ressources affectées et un statut : **Démarre**, **En cours**, **Aurait dû démarrer** ou **En retard**. Une activité qui couvre toute la fenêtre y figure aussi. @@ -94,7 +114,11 @@ et la fin prévue avec l'écart en jours ouvrés, l'avancement **prévu** contre pondérés par la durée des tâches feuilles ; prévu sur les dates de la référence active, sinon sur le planning actuel) et les comptages par état. Dessous, cinq sections : terminé pendant la période écoulée, en cours, démarre pendant la prochaine période, en retard, et activités critiques ouvertes. -La période (deux semaines par défaut) regarde autant en arrière qu'en avant. +La période de rapport (le mois dernier par défaut) détermine ce qui compte comme *terminé pendant la +période* ; la section *démarre pendant la prochaine période* regarde en avant à partir de la date +d'état — jusqu'à la fin de la période si elle se situe (en partie) après la date d'état, pour un préréglage *dernier(s) …* +aussi loin en avant que la période regarde en arrière ; pour une période personnalisée ou « tout le +projet » entièrement passée, la section reste vide. La synthèse affiche les deux bornes. ### Santé du planning @@ -106,18 +130,21 @@ marge élevée, décalages longs). Les seuils sont dans les options ; par défau ouvrés pour la marge élevée et la longue durée, 10 pour les décalages. Un planning propre a zéro erreur. -### Charge des ressources par semaine +### Charge des ressources + +Les lignes sont regroupées par ressource (nom et type uniquement sur la première ligne de chaque groupe, comme pour les affectations des ressources) ; *Agrégation* permet de choisir entre semaines et mois civils, et la période de rapport détermine quelles semaines ou quels mois apparaissent. -Par ressource et par semaine, le besoin face à la capacité disponible (en unités-jours), l'écart, -le pic journalier et si la semaine est surchargée — le même calcul que l'histogramme de l'onglet -**Ressources**, sous forme de tableau. Seules les semaines avec un besoin figurent ; *Semaines -surchargées uniquement* ne garde que les goulots. +Par ressource et par semaine ou par mois, le besoin face à la capacité disponible (en unités-jours), +l'écart, le pic journalier et si la période est surchargée — le même calcul que l'histogramme de +l'onglet **Ressources**, sous forme de tableau. Seules les périodes avec un besoin figurent ; +*Périodes surchargées uniquement* ne garde que les goulots. Si un saut de page du PDF tombe au +milieu d'un groupe, le nom de la ressource n'est pas répété sur la page suivante. ### Affectations des ressources Par ressource, les activités qui lui sont affectées : WBS, nom, début et fin, durée restante, unités par jour, avancement, critique et statut. Les tâches terminées sont exclues par défaut. Avec une -fenêtre en semaines, cela devient la *prévision par ressource*. La synthèse compte aussi les tâches +période de rapport (tout le projet par défaut), cela devient la *prévision par ressource*. La synthèse compte aussi les tâches sans ressource. ### Synthèse WBS diff --git a/public/docs/it/gids-rapporten-printen.md b/public/docs/it/gids-rapporten-printen.md index 445e14a2..00bfab6c 100644 --- a/public/docs/it/gids-rapporten-printen.md +++ b/public/docs/it/gids-rapporten-printen.md @@ -99,10 +99,29 @@ progetto — senza data di stato il rapporto usa oggi e lo segnala; date e slack sempre prima; ogni rapporto ha un piccolo blocco **Opzioni del rapporto**, ricordato tra le sessioni. I giorni lavorativi sono abbreviati in *gl*. +### Periodo del report + +Quattro report lavorano su una finestra temporale: previsione, avanzamento, carico risorse e +assegnazioni risorse. Condividono un unico controllo *Periodo del report* nelle opzioni del report, +con un'impostazione memorizzata per ciascun report: + +- **Prossima / ultima settimana, 2, 4, 6, 8 o 12 settimane** e **prossimo / ultimo mese** — + contati dalla data di stato del progetto (o da oggi se manca). Un preset è inclusivo a entrambi + gli estremi: *prossime 4 settimane* giovedì 10 settembre arriva a mercoledì 7 ottobre. Cambiando + la data di stato la finestra si sposta di conseguenza. +- **Intero progetto** — dall'inizio più anticipato alla fine più tardiva del programma. +- **Personalizzato** — due date a scelta. I campi *Dal* e *Al* diventano modificabili (digitazione + o selettore di data); con un preset mostrano le date calcolate in sola lettura. Una data finale + precedente a quella iniziale, o un campo data vuoto, viene evidenziata in rosso e non applicata. Tornando a un preset, + le sue date sostituiscono l'intervallo personalizzato. + +Il periodo scelto compare come sottotitolo del report e del PDF; il rapporto di avanzamento lo +mostra nel riepilogo. + ### Previsione (look-ahead) -L'elenco per la riunione settimanale di cantiere: tutte le attività delle prossime *N* settimane -(quattro per default) — cosa inizia, prosegue o finisce — più ciò che avrebbe già dovuto accadere. +L'elenco per la riunione settimanale di cantiere: tutte le attività del periodo del report +(il prossimo mese per default) — cosa inizia, prosegue o finisce — più ciò che avrebbe già dovuto accadere. Per riga: WBS, nome, inizio e fine, durata residua, avanzamento, slack totale, critica o quasi critica, risorse assegnate e uno stato: **Inizia**, **In corso**, **Doveva iniziare** o **In ritardo**. Compare anche un'attività che copre l'intera finestra. @@ -121,7 +140,11 @@ prevista con la differenza in giorni lavorativi, l'avanzamento **pianificato** c **effettivo** (entrambi pesati sulla durata delle attività foglia; pianificato sulle date della baseline attiva, altrimenti sul programma attuale) e i conteggi per stato. Sotto, cinque sezioni: completate nel periodo passato, in corso, iniziano nel prossimo periodo, in ritardo e attività -critiche aperte. Il periodo (due settimane per default) guarda indietro quanto avanti. +critiche aperte. Il periodo del report (l'ultimo mese per default) decide cosa conta come *completato nel periodo*; +la sezione *iniziano nel prossimo periodo* guarda avanti dalla data di stato: fino alla fine del +periodo se questo cade (in parte) dopo la data di stato, con un preset *ultimo/i …* tanto avanti +quanto il periodo guarda indietro; con un periodo personalizzato o dell'intero progetto che cade +del tutto nel passato, la sezione resta vuota. Il riepilogo mostra entrambi i limiti. ### Salute del programma @@ -132,18 +155,21 @@ anticipi, vincoli rigidi, avanzamento fuori sequenza) e **informazioni** (quasi elevato, ritardi lunghi). Le soglie sono nelle opzioni; default secondo DCMA: 44 giorni lavorativi per slack elevato e durata lunga, 10 per i ritardi. Un programma pulito ha zero errori. -### Carico risorse per settimana +### Carico risorse + +Le righe sono raggruppate per risorsa (nome e tipo solo sulla prima riga di ogni gruppo, come nelle assegnazioni risorse); con *Aggregazione* scegli tra settimane e mesi di calendario, e il periodo del report determina quali settimane o mesi compaiono. -Per risorsa e settimana, il fabbisogno rispetto alla capacità disponibile (in unità-giorno), la -differenza, il picco giornaliero e se la settimana è sovraccarica — lo stesso calcolo -dell'istogramma nella scheda **Risorse**, in forma di tabella. Compaiono solo le settimane con -fabbisogno; con *Solo settimane sovraccariche* restano solo i colli di bottiglia. +Per risorsa e settimana o mese, il fabbisogno rispetto alla capacità disponibile (in unità-giorno), +la differenza, il picco giornaliero e se il periodo è sovraccarico — lo stesso calcolo +dell'istogramma nella scheda **Risorse**, in forma di tabella. Compaiono solo i periodi con +fabbisogno; con *Solo periodi sovraccarichi* restano solo i colli di bottiglia. Se un'interruzione di pagina +del PDF cade a metà di un gruppo, il nome della risorsa non viene ripetuto nella pagina seguente. ### Assegnazioni risorse Per risorsa, le attività assegnate: WBS, nome, inizio e fine, durata residua, unità al giorno, -avanzamento, critica e stato. Le attività completate sono escluse per default. Con una finestra in -settimane diventa la *previsione per risorsa*. Il riepilogo conta anche le attività senza risorsa. +avanzamento, critica e stato. Le attività completate sono escluse per default. Con un periodo del +report (intero progetto per default) diventa la *previsione per risorsa*. Il riepilogo conta anche le attività senza risorsa. ### Riepilogo WBS diff --git a/public/docs/ja/gids-rapporten-printen.md b/public/docs/ja/gids-rapporten-printen.md index 0d4de6af..5b1f9ef2 100644 --- a/public/docs/ja/gids-rapporten-printen.md +++ b/public/docs/ja/gids-rapporten-printen.md @@ -66,9 +66,20 @@ 常に先に再計算します。各レポートには小さな**レポート設定**ブロックがあり、セッション間で記憶されます。 稼働日は「稼働日」と表記します。 +### 報告期間 + +先行計画、進捗、リソース負荷、リソース割当の 4 つのレポートは時間の窓を対象にします。これらはレポート +オプションの *報告期間* コントロールを共有し、設定はレポートごとに記憶されます。 + +- **今後 / 過去 1 週間、2、4、6、8、12 週間** および **来月 / 先月** — プロジェクトのステータス日(未設定なら今日)を起点に数えます。プリセットは両端を含みます。9 月 10 日(木)の *今後 4 週間* は 10 月 7 日(水)までです。ステータス日を変えると窓も一緒に動きます。 +- **プロジェクト全体** — スケジュール中の最も早い開始から最も遅い終了まで。 +- **カスタム** — 任意の 2 つの日付。*開始* と *終了* の欄が編集可能になります(入力または日付ピッカー)。プリセットではこの欄は計算された日付を読み取り専用で表示します。開始日より前の終了日や空の日付欄は赤く表示され、適用されません。再びプリセットを選ぶと、プリセットの日付がカスタム範囲を置き換えます。 + +選んだ期間はレポートと PDF の副題に表示されます。進捗レポートではサマリーに表示されます。 + ### 先行計画 -週次の現場会議用リスト。今後 *N* 週間(既定 4 週)のすべてのアクティビティ — 何が始まり、続き、 +週次の現場会議用リスト。報告期間(既定は来月)のすべてのアクティビティ — 何が始まり、続き、 終わるか — に加えて、すでに済んでいるべきものも含みます。各行に WBS、名称、開始と終了、残期間、完了率、 トータルフロート、クリティカル/準クリティカル、割当リソース、そして状態(**開始予定**、**進行中**、 **開始予定超過**、**遅延**)を表示します。期間全体にまたがるアクティビティも含まれます。 @@ -85,7 +96,9 @@ ステータス日時点の定期的な「現在地」の概要。サマリーはベースライン終了と予測終了とその差(稼働日)、 **計画**と**実績**の進捗(どちらもリーフタスクの期間で加重。計画は有効なベースラインの日付、なければ 現行スケジュール)、状態別の件数を示します。その下に 5 つのセクション:前期間に完了、進行中、次期間に -開始、遅延、未完了のクリティカル アクティビティ。期間(既定 2 週)は前後に同じだけ見ます。 +開始、遅延、未完了のクリティカル アクティビティ。報告期間(既定は先月)が「期間内に完了」に何を含めるかを決めます。「次期間に開始」セクションは +ステータス日から先を見ます — 期間が(一部でも)ステータス日より後にあればその期間の終わりまで、 +「過去 …」プリセットでは期間が遡るのと同じ長さだけ先まで。完全に過去にあるカスタムまたはプロジェクト全体の期間では、このセクションは空のままです。サマリーには両方の境界が表示されます。 ### スケジュール健全性 @@ -95,16 +108,19 @@ DCMA 14 項目評価の考え方に沿った自動スケジュールレビュー ラグ)。しきい値はレポート設定にあり、既定は DCMA に従い高フロートと長期間が 44 稼働日、ラグが 10 稼働日。クリーンなスケジュールはエラーがゼロです。 -### 週別リソース負荷 +### リソース負荷 + +行はリソースごとにグループ化されます(名前と種類は各グループの最初の行のみ、リソース割当と同じ)。*集計* で暦週と暦月を切り替え、報告期間がどの週または月を表示するかを決めます。 -リソースと週ごとに、必要量と利用可能な能力(単位・日)、差、日ピーク、過負荷の有無 — **リソース** -タブのヒストグラムと同じ計算を表にしたものです。必要量のある週だけを表示し、「過負荷の週のみ」で -ボトルネックだけを残せます。 +リソースと週または月ごとに、必要量と利用可能な能力(単位・日)、差、日ピーク、その期間の過負荷の有無 — +**リソース** タブのヒストグラムと同じ計算を表にしたものです。必要量のある期間だけを表示し、 +「過負荷の期間のみ」でボトルネックだけを残せます。PDF の改ページがグループの途中に入った場合、 +リソース名は次のページで繰り返されません。 ### リソース割当 リソースごとに割り当てられたアクティビティ:WBS、名称、開始と終了、残期間、1 日あたり単位、完了率、 -クリティカル、状態。完了タスクは既定で除外。週単位の期間を設定すると*リソース先行計画*になります。 +クリティカル、状態。完了タスクは既定で除外。報告期間(既定はプロジェクト全体)を設定すると*リソース先行計画*になります。 サマリーはリソース未割当のタスクも数えます。 ### WBS サマリー diff --git a/public/docs/ko/gids-rapporten-printen.md b/public/docs/ko/gids-rapporten-printen.md index e4a8bd2f..ca13f631 100644 --- a/public/docs/ko/gids-rapporten-printen.md +++ b/public/docs/ko/gids-rapporten-printen.md @@ -90,9 +90,20 @@ 계산합니다. 각 보고서에는 세션 간에 기억되는 작은 **보고서 옵션** 블록이 있습니다. 작업일은 “작업일”로 표기합니다. +### 보고 기간 + +선행 계획, 진척, 자원 부하, 자원 배정의 네 보고서는 시간 창을 기준으로 작동합니다. 이들은 보고서 +옵션의 *보고 기간* 컨트롤 하나를 공유하며, 설정은 보고서마다 따로 기억됩니다. + +- **향후 / 지난 1주, 2, 4, 6, 8, 12주** 및 **다음 달 / 지난달** — 프로젝트 상태 날짜(없으면 오늘)를 기준으로 셉니다. 사전 설정은 양 끝을 포함합니다. 9월 10일 목요일의 *향후 4주*는 10월 7일 수요일까지입니다. 상태 날짜를 바꾸면 창도 함께 이동합니다. +- **프로젝트 전체** — 일정에서 가장 이른 시작부터 가장 늦은 완료까지. +- **사용자 지정** — 직접 고른 두 날짜. *시작*과 *종료* 필드가 편집 가능해집니다(입력 또는 날짜 선택기). 사전 설정에서는 계산된 날짜를 읽기 전용으로 보여 줍니다. 시작일보다 앞선 종료일이나 비어 있는 날짜 필드는 빨갛게 표시되고 적용되지 않습니다. 다시 사전 설정을 고르면 그 날짜가 사용자 지정 범위를 대체합니다. + +선택한 기간은 보고서와 PDF의 부제목으로 표시됩니다. 진척 보고서는 요약에 표시합니다. + ### 선행 계획 -주간 현장 회의용 목록: 앞으로 *N*주(기본 4주)의 모든 활동 — 무엇이 시작되고, 계속되고, 끝나는지 — +주간 현장 회의용 목록: 보고 기간(기본 다음 달)의 모든 활동 — 무엇이 시작되고, 계속되고, 끝나는지 — 그리고 이미 이루어졌어야 할 것. 행마다 WBS, 이름, 시작과 완료, 잔여 기간, 진척률, 총 여유, 주공정 또는 준주공정 여부, 배정된 자원, 상태(**시작 예정**, **진행 중**, **시작했어야 함**, **지연**)를 표시합니다. 기간 전체에 걸친 활동도 포함됩니다. @@ -109,7 +120,10 @@ 상태 기준일 시점의 정기적인 “현재 위치” 개요. 요약은 기준선 완료와 예상 완료 및 작업일 차이, **계획** 대 **실적** 진척(둘 다 말단 작업 기간으로 가중; 계획은 활성 기준선의 날짜, 없으면 현재 일정 기준), 상태별 건수를 제공합니다. 아래에는 다섯 섹션: 지난 기간에 완료, 진행 중, 다음 기간에 -시작, 지연, 미완료 주공정 활동. 기간(기본 2주)은 앞뒤로 같은 만큼 봅니다. +시작, 지연, 미완료 주공정 활동. 보고 기간(기본 지난달)이 *기간 내 완료*로 무엇을 셀지 결정합니다. *다음 기간에 시작* 섹션은 상태 +기준일부터 앞을 봅니다 — 기간이 (일부라도) 상태 기준일 이후이면 기간 끝까지, *지난 …* 사전 설정이면 기간이 뒤로 +보는 만큼 앞으로 봅니다. 완전히 과거에 있는 사용자 지정 또는 프로젝트 전체 기간이면 이 섹션은 비어 +있습니다. 요약에는 두 경계가 모두 표시됩니다. ### 일정 건전성 @@ -119,16 +133,19 @@ DCMA 14점 평가의 취지에 따른 자동 일정 검토. 각 검사에 심각 임계값은 보고서 옵션에 있으며 기본값은 DCMA를 따릅니다: 높은 여유와 긴 기간 44 작업일, 지연 10 작업일. 깨끗한 일정은 오류가 0입니다. -### 주별 자원 부하 +### 자원 부하 + +행은 자원별로 그룹화됩니다(이름과 유형은 각 그룹의 첫 행에만 표시, 자원 배정 보고서와 동일). *집계*로 달력 주와 달력 월 중에서 선택하고, 보고 기간이 어떤 주 또는 월을 표시할지 결정합니다. -자원과 주별로 필요량 대 가용 능력(단위·일), 차이, 일 최대치, 과부하 여부 — **자원** 탭의 -히스토그램과 같은 계산을 표로 나타낸 것입니다. 필요량이 있는 주만 나열되며 “과부하 주만 표시”로 -병목만 남길 수 있습니다. +자원과 주 또는 월별로 필요량 대 가용 능력(단위·일), 차이, 일 최대치, 해당 기간의 과부하 여부 — +**자원** 탭의 히스토그램과 같은 계산을 표로 나타낸 것입니다. 필요량이 있는 기간만 나열되며 “과부하 +기간만 표시”로 병목만 남길 수 있습니다. PDF에서 페이지 나눔이 그룹 중간에 오면 자원 이름은 다음 +페이지에 반복되지 않습니다. ### 자원 배정 자원별로 배정된 활동: WBS, 이름, 시작과 완료, 잔여 기간, 일일 단위, 진척률, 주공정, 상태. 완료된 -작업은 기본적으로 제외됩니다. 주 단위 기간을 설정하면 *자원 선행 계획*이 됩니다. 요약은 자원이 +작업은 기본적으로 제외됩니다. 보고 기간(기본 프로젝트 전체)을 설정하면 *자원 선행 계획*이 됩니다. 요약은 자원이 없는 작업도 셉니다. ### WBS 요약 diff --git a/public/docs/nl/gids-rapporten-printen.md b/public/docs/nl/gids-rapporten-printen.md index 14a8cef7..6cab412a 100644 --- a/public/docs/nl/gids-rapporten-printen.md +++ b/public/docs/nl/gids-rapporten-printen.md @@ -155,10 +155,29 @@ delen een paar afspraken: - Een taak kan in meerdere secties van één rapport staan wanneer die secties elk een andere vraag beantwoorden (in uitvoering én kritiek, bijvoorbeeld). +### Rapportageperiode + +Vier rapporten werken op een tijdvenster: look-ahead, voortgang, resourcebelasting en +resourcetoewijzingen. Ze delen één *Rapportageperiode*-keuze in de rapportopties, met een eigen +onthouden instelling per rapport: + +- **Volgende / afgelopen week, 2, 4, 6, 8 of 12 weken** en **volgende / afgelopen maand** — gerekend + vanaf de statusdatum van het project (of vandaag als die ontbreekt). Een preset is inclusief aan + beide kanten: *volgende 4 weken* op donderdag 10 september loopt t/m woensdag 7 oktober. Wijzig je + de statusdatum, dan schuift het venster automatisch mee. +- **Hele project** — van de vroegste start tot het laatste einde in de planning. +- **Aangepast** — twee eigen datums. De velden *Van* en *Tot* worden dan bewerkbaar (typen of de + datumkiezer); bij een preset tonen ze alleen-lezen de berekende datums. Een einddatum vóór de + begindatum, of een leeg datumveld, wordt rood gemarkeerd en niet toegepast. Kies je daarna weer een preset, dan + vervangen de presetdatums je eigen bereik. + +De gekozen periode staat als ondertitel in het rapport en in de PDF; het voortgangsrapport toont +hem in de samenvatting. + ### Look-ahead -De lijst voor het weekoverleg op de bouw: alle activiteiten die de komende *N* weken (standaard -vier) aan de orde zijn — wat start, wat loopt door, wat eindigt — plus wat er al had moeten +De lijst voor het weekoverleg op de bouw: alle activiteiten in de rapportageperiode (standaard de +komende maand) — wat start, wat loopt door, wat eindigt — plus wat er al had moeten gebeuren. Per rij zie je WBS, naam, start en einde, de resterende duur, de voltooiing, de totale speling, of de taak kritiek of near-critical is, de toegewezen resources en een status: **Start** (begint in het venster), **In uitvoering**, **Had moeten starten** (start vóór de @@ -182,9 +201,13 @@ prognose-einde met het verschil in werkdagen, de **geplande** tegenover de **wer voortgang en de tellingen per staat. Beide percentages zijn duurgewogen over de bladtaken: een mijlpaal weegt niets, een maand werk weegt zwaar. Gepland wordt gemeten op de datums van de actieve baseline (de afspraak waartegen je meet); zonder baseline op de huidige planning, en dat -staat er dan bij. Daaronder vijf secties: voltooid in de afgelopen periode, in uitvoering, start -in de komende periode, achterstallig, en de open kritieke activiteiten. De periode (standaard twee -weken) kijkt evenveel terug als vooruit. +staat er dan bij. Daaronder vijf secties: voltooid in de rapportageperiode, in uitvoering, start +in de komende periode, achterstallig, en de open kritieke activiteiten. De rapportageperiode +(standaard de afgelopen maand) bepaalt wat als *voltooid in de periode* telt; de sectie *start +in de komende periode* kijkt vanaf de statusdatum vooruit — tot het einde van de periode als die +(deels) ná de statusdatum ligt; bij een *afgelopen …*-preset even ver vooruit als de periode +terugkijkt; bij een aangepaste of projectperiode die helemaal in het verleden ligt blijft de sectie +leeg. De samenvatting toont beide grenzen. ### Planningsgezondheid @@ -202,20 +225,25 @@ De drempels staan in de rapportopties. Standaard volgen ze DCMA: hoge speling en 44 werkdagen; een lag boven 10 werkdagen. Een schone planning heeft nul fouten; waarschuwingen en informatie zijn aanleiding om te kijken, niet per se om te veranderen. -### Resourcebelasting per week +### Resourcebelasting -Per resource en per week de gevraagde inzet tegenover de beschikbare capaciteit (in eenheid-dagen), -het verschil, de piekbelasting op één dag en of de week overbelast is. Het is dezelfde berekening -als het histogram op het tabblad **Resources**, maar dan als tabel om naast elkaar te leggen in -een bemensingsoverleg. Alleen weken met vraag staan erin; met de optie *Alleen overbelaste weken* -houd je uitsluitend de knelpunten over. +Per resource en per week of maand de gevraagde inzet tegenover de beschikbare capaciteit (in +eenheid-dagen), het verschil, de piekbelasting op één dag en of de periode overbelast is. Het is +dezelfde berekening als het histogram op het tabblad **Resources**, maar dan als tabel om naast +elkaar te leggen in een bemensingsoverleg. De rijen zijn per resource gegroepeerd — naam en type +staan alleen op de eerste rij van elke groep, net als bij de resourcetoewijzingen. Kies met +*Aggregatie* tussen kalenderweken en kalendermaanden; de rapportageperiode bepaalt welke weken of +maanden meedoen (elke week of maand die de periode raakt, als geheel — dus altijd hetzelfde getal +als in het histogram). Alleen periodes met vraag staan erin; met de optie *Alleen overbelaste +periodes* houd je uitsluitend de knelpunten over. Valt in de PDF een paginaovergang midden in een +groep, dan herhaalt de resourcenaam zich niet op de volgende pagina. ### Resourcetoewijzingen Per resource welke activiteiten eraan hangen: WBS, naam, start en einde, resterende duur, inzet in eenheden per dag, voltooiing, kritiek en status. Voltooide taken staan er standaard niet in. Met een -venster in weken wordt het de *resource-look-ahead*: alleen wat deze ploeg of dit materieel de -komende weken te doen heeft, plus wat er nog open staat. De samenvatting telt ook de taken zonder +rapportageperiode (standaard het hele project) wordt het de *resource-look-ahead*: alleen wat deze +ploeg of dit materieel in die periode te doen heeft, plus wat er nog open staat. De samenvatting telt ook de taken zonder resource. ### WBS-samenvatting diff --git a/public/docs/pl/gids-rapporten-printen.md b/public/docs/pl/gids-rapporten-printen.md index bf841d76..a908ccf7 100644 --- a/public/docs/pl/gids-rapporten-printen.md +++ b/public/docs/pl/gids-rapporten-printen.md @@ -100,10 +100,30 @@ statusu raport przyjmuje dzisiaj i o tym informuje; daty i zapasy pochodzą z os najpierw przelicza; każdy raport ma mały blok **Opcje raportu**, zapamiętywany między sesjami. Dni robocze skracane są do *dr*. +### Okres raportu + +Cztery raporty pracują na oknie czasowym: prognoza, postęp, obciążenie zasobów i przypisania +zasobów. Dzielą jeden wybór *Okres raportu* w opcjach raportu, z osobno zapamiętanym ustawieniem +dla każdego raportu: + +- **Następny / ostatni tydzień, 2, 4, 6, 8 lub 12 tygodni** oraz **następny / ostatni miesiąc** — + liczone od daty stanu projektu (lub od dziś, gdy jej brak). Ustawienie wstępne jest domknięte z + obu stron: *następne 4 tygodnie* w czwartek 10 września sięgają do środy 7 października. Zmień + datę stanu, a okno przesunie się razem z nią. +- **Cały projekt** — od najwcześniejszego startu do najpóźniejszego końca harmonogramu. +- **Niestandardowy** — dwie własne daty. Pola *Od* i *Do* stają się edytowalne (wpisanie lub + wybór z kalendarza); przy ustawieniu wstępnym pokazują obliczone daty tylko do odczytu. Data + końcowa wcześniejsza niż początkowa lub puste pole daty jest oznaczana na czerwono i nie jest + stosowana. Po powrocie + do ustawienia wstępnego jego daty zastępują własny zakres. + +Wybrany okres pojawia się jako podtytuł raportu i pliku PDF; raport postępu pokazuje go w +podsumowaniu. + ### Prognoza (look-ahead) -Lista na cotygodniową naradę budowy: wszystkie czynności najbliższych *N* tygodni (domyślnie -czterech) — co się zaczyna, trwa lub kończy — plus to, co już powinno było się wydarzyć. W wierszu: +Lista na cotygodniową naradę budowy: wszystkie czynności z okresu raportu (domyślnie +następny miesiąc) — co się zaczyna, trwa lub kończy — plus to, co już powinno było się wydarzyć. W wierszu: WBS, nazwa, start i koniec, pozostały czas, ukończenie, zapas całkowity, krytyczne lub prawie krytyczne, przydzielone zasoby i status: **Rozpoczyna się**, **W toku**, **Powinno się rozpocząć** lub **Zaległe**. Czynność obejmująca całe okno również się pojawia. @@ -122,7 +142,12 @@ prognozowany z różnicą w dniach roboczych, postęp **planowany** wobec **rzec czasem trwania zadań liściowych; planowany na datach aktywnego planu bazowego, w przeciwnym razie na bieżącym harmonogramie) oraz liczby według stanu. Poniżej pięć sekcji: ukończone w minionym okresie, w toku, rozpoczynające się w następnym okresie, zaległe i otwarte czynności krytyczne. Okres -(domyślnie dwa tygodnie) sięga tak samo wstecz, jak w przód. +raportu (domyślnie ostatni miesiąc) decyduje, co liczy się jako *ukończone w okresie*; sekcja +*rozpoczynające się w następnym okresie* patrzy w przód od daty stanu — do końca okresu, jeśli leży on +(częściowo) po dacie stanu, przy ustawieniu wstępnym *ostatni(e) …* tak daleko w +przód, jak okres sięga wstecz; przy okresie niestandardowym lub całego projektu leżącym w całości +w przeszłości sekcja pozostaje pusta. +Podsumowanie pokazuje obie granice. ### Kondycja harmonogramu @@ -133,18 +158,21 @@ trwania, wyprzedzenia, twarde ograniczenia, postęp poza kolejnością) i **info krytyczne, duży zapas, długie zwłoki). Progi są w opcjach; domyślnie według DCMA: 44 dni robocze dla dużego zapasu i długiego czasu trwania, 10 dla zwłok. Czysty harmonogram ma zero błędów. -### Obciążenie zasobów tygodniowo +### Obciążenie zasobów + +Wiersze są pogrupowane według zasobu (nazwa i typ tylko w pierwszym wierszu każdej grupy, jak w przypisaniach zasobów); opcja *Agregacja* pozwala wybrać tygodnie lub miesiące kalendarzowe, a okres raportu decyduje, które tygodnie lub miesiące się pojawią. -Dla zasobu i tygodnia zapotrzebowanie wobec dostępnej zdolności (w jednostko-dniach), różnica, -dzienny szczyt i czy tydzień jest przeciążony — to samo obliczenie co histogram na karcie **Zasoby**, -w postaci tabeli. Pokazywane są tylko tygodnie z zapotrzebowaniem; opcja *Tylko przeciążone -tygodnie* zostawia same wąskie gardła. +Dla zasobu i tygodnia lub miesiąca zapotrzebowanie wobec dostępnej zdolności (w jednostko-dniach), +różnica, dzienny szczyt i czy okres jest przeciążony — to samo obliczenie co histogram na karcie +**Zasoby**, w postaci tabeli. Pokazywane są tylko okresy z zapotrzebowaniem; opcja *Tylko +przeciążone okresy* zostawia same wąskie gardła. Gdy podział strony w PDF wypadnie w środku grupy, +nazwa zasobu nie powtarza się na następnej stronie. ### Przydziały zasobów Dla zasobu przydzielone czynności: WBS, nazwa, start i koniec, pozostały czas, jednostki na dzień, -ukończenie, krytyczność i status. Ukończone zadania są domyślnie pomijane. Z oknem w tygodniach -staje się to *prognozą zasobów*. Podsumowanie liczy też zadania bez zasobu. +ukończenie, krytyczność i status. Ukończone zadania są domyślnie pomijane. Z okresem raportu +(domyślnie cały projekt) staje się to *prognozą zasobów*. Podsumowanie liczy też zadania bez zasobu. ### Podsumowanie WBS diff --git a/public/docs/pt/gids-rapporten-printen.md b/public/docs/pt/gids-rapporten-printen.md index 2b20cd65..97562d3c 100644 --- a/public/docs/pt/gids-rapporten-printen.md +++ b/public/docs/pt/gids-rapporten-printen.md @@ -100,10 +100,29 @@ o relatório usa hoje e diz isso; datas e folgas vêm do último **cálculo** (F cronograma alterado desde então e a exportação PDF recalcula sempre primeiro; cada relatório tem um pequeno bloco **Opções do relatório**, lembrado entre sessões. Os dias úteis abreviam-se para *du*. +### Período do relatório + +Quatro relatórios trabalham sobre uma janela de tempo: previsão, progresso, carga de recursos e +atribuições de recursos. Partilham um único controlo *Período do relatório* nas opções do +relatório, com uma definição memorizada por relatório: + +- **Próxima / última semana, 2, 4, 6, 8 ou 12 semanas** e **próximo / último mês** — contados a + partir da data de estado do projeto (ou de hoje, se não existir). Um predefinido é inclusivo em + ambas as pontas: *próximas 4 semanas* na quinta-feira 10 de setembro vai até quarta-feira 7 de + outubro. Altere a data de estado e a janela acompanha. +- **Projeto inteiro** — do início mais cedo ao fim mais tardio do cronograma. +- **Personalizado** — duas datas à sua escolha. Os campos *De* e *Até* passam a ser editáveis + (escrever ou seletor de data); com um predefinido mostram as datas calculadas só de leitura. Uma + data final anterior à inicial, ou um campo de data vazio, é marcada a vermelho e não é aplicada. Ao voltar a um predefinido, + as datas dele substituem o seu intervalo. + +O período escolhido surge como subtítulo do relatório e do PDF; o relatório de progresso mostra-o +no resumo. + ### Previsão (look-ahead) -A lista para a reunião semanal de obra: todas as atividades das próximas *N* semanas (quatro por -defeito) — o que começa, continua ou termina — mais o que já devia ter acontecido. Por linha: EAP, +A lista para a reunião semanal de obra: todas as atividades do período do relatório (o próximo mês +por defeito) — o que começa, continua ou termina — mais o que já devia ter acontecido. Por linha: EAP, nome, início e fim, duração restante, progresso, folga total, crítica ou quase crítica, recursos atribuídos e um estado: **Começa**, **Em curso**, **Devia ter começado** ou **Atrasada**. Uma atividade que abrange toda a janela também aparece. @@ -121,8 +140,12 @@ O ponto de situação periódico «onde estamos» na data de estado. O resumo d e o fim previsto com a diferença em dias úteis, o progresso **planeado** face ao **real** (ambos ponderados pela duração das tarefas folha; planeado nas datas da linha de base ativa, senão no cronograma atual) e as contagens por estado. Por baixo, cinco secções: concluídas no período -anterior, em curso, começam no próximo período, atrasadas e atividades críticas em aberto. O período -(duas semanas por defeito) olha tanto para trás como para a frente. +anterior, em curso, começam no próximo período, atrasadas e atividades críticas em aberto. O período do +relatório (o último mês por defeito) decide o que conta como *concluído no período*; a secção +*começam no próximo período* olha para a frente a partir da data de estado: até ao fim do período se +este ficar (em parte) depois da data de estado, com um predefinido *último(s) …* tão longe para a +frente quanto o período olha para trás; com um período personalizado ou do projeto inteiro que fica +totalmente no passado, a secção fica vazia. O resumo mostra ambos os limites. ### Saúde do cronograma @@ -133,18 +156,21 @@ longa, avanços, restrições rígidas, progresso fora de sequência) e **inform folga alta, atrasos longos). Os limiares estão nas opções; por defeito segundo DCMA: 44 dias úteis para folga alta e duração longa, 10 para atrasos. Um cronograma limpo tem zero erros. -### Carga de recursos por semana +### Carga de recursos + +As linhas são agrupadas por recurso (nome e tipo apenas na primeira linha de cada grupo, como nas atribuições de recursos); com *Agregação* escolhe entre semanas e meses de calendário, e o período do relatório determina que semanas ou meses aparecem. -Por recurso e semana, a necessidade face à capacidade disponível (em unidades-dia), a diferença, o -pico diário e se a semana está sobrecarregada — o mesmo cálculo do histograma no separador -**Recursos**, em forma de tabela. Só aparecem semanas com necessidade; com *Apenas semanas -sobrecarregadas* ficam só os estrangulamentos. +Por recurso e semana ou mês, a necessidade face à capacidade disponível (em unidades-dia), a +diferença, o pico diário e se o período está sobrecarregado — o mesmo cálculo do histograma no +separador **Recursos**, em forma de tabela. Só aparecem períodos com necessidade; com *Apenas +períodos sobrecarregados* ficam só os estrangulamentos. Se uma quebra de página do PDF cair a meio +de um grupo, o nome do recurso não se repete na página seguinte. ### Atribuições de recursos Por recurso, as atividades atribuídas: EAP, nome, início e fim, duração restante, unidades por dia, -progresso, crítica e estado. As tarefas concluídas são excluídas por defeito. Com uma janela em -semanas torna-se a *previsão por recurso*. O resumo conta também as tarefas sem recurso. +progresso, crítica e estado. As tarefas concluídas são excluídas por defeito. Com um período do +relatório (projeto inteiro por defeito) torna-se a *previsão por recurso*. O resumo conta também as tarefas sem recurso. ### Resumo EAP diff --git a/public/docs/tr/gids-rapporten-printen.md b/public/docs/tr/gids-rapporten-printen.md index 425814d3..0b24d96d 100644 --- a/public/docs/tr/gids-rapporten-printen.md +++ b/public/docs/tr/gids-rapporten-printen.md @@ -71,9 +71,27 @@ program bir notla belirtilir ve PDF dışa aktarımı her zaman önce yeniden he oturumlar arasında hatırlanan küçük bir **Rapor seçenekleri** bloğu vardır. İş günleri *ig* olarak kısaltılır. +### Raporlama dönemi + +Dört rapor bir zaman penceresi üzerinde çalışır: ileriye bakış, ilerleme, kaynak yükü ve kaynak +atamaları. Rapor seçeneklerinde tek bir *Raporlama dönemi* denetimini paylaşırlar; her raporun kendi +hatırlanan ayarı vardır: + +- **Gelecek / geçen hafta, 2, 4, 6, 8 veya 12 hafta** ve **gelecek / geçen ay** — projenin durum + tarihinden (yoksa bugünden) sayılır. Bir ön ayar iki uçta da dahildir: 10 Eylül Perşembe günü + *gelecek 4 hafta*, 7 Ekim Çarşamba'ya kadar sürer. Durum tarihini değiştirirseniz pencere onunla + birlikte kayar. +- **Tüm proje** — programdaki en erken başlangıçtan en geç bitişe. +- **Özel** — kendi seçtiğiniz iki tarih. *Başlangıç* ve *Bitiş* alanları düzenlenebilir olur (yazın + veya tarih seçiciyi kullanın); bir ön ayarla hesaplanan tarihleri salt okunur gösterirler. + Başlangıçtan önceki bir bitiş tarihi veya boş bir tarih alanı kırmızıyla işaretlenir ve uygulanmaz. Yeniden bir ön ayar + seçtiğinizde ön ayarın tarihleri sizin aralığınızın yerini alır. + +Seçilen dönem raporun ve PDF'nin alt başlığında görünür; ilerleme raporu bunu özetinde gösterir. + ### İleriye bakış (look-ahead) -Haftalık şantiye toplantısının listesi: önümüzdeki *N* haftanın (varsayılan dört) tüm aktiviteleri — +Haftalık şantiye toplantısının listesi: raporlama dönemindeki (varsayılan gelecek ay) tüm aktiviteler — ne başlıyor, ne sürüyor, ne bitiyor — artı çoktan olmuş olması gerekenler. Satır başına: İKY, ad, başlangıç ve bitiş, kalan süre, tamamlanma, toplam bolluk, kritik veya kritiğe yakın, atanan kaynaklar ve bir durum: **Başlıyor**, **Devam ediyor**, **Başlamış olmalıydı** veya **Gecikmiş**. @@ -92,8 +110,11 @@ Durum tarihinde periyodik "neredeyiz" özeti. Özet, temel ve tahmini bitişi i **planlanan** ile **gerçekleşen** ilerlemeyi (her ikisi de yaprak görevlerin süresine göre ağırlıklı; planlanan etkin temel planın tarihlerinde, yoksa mevcut programda) ve duruma göre sayımları verir. Altında beş bölüm: geçen dönemde tamamlananlar, devam edenler, gelecek dönemde -başlayanlar, gecikmişler ve açık kritik aktiviteler. Dönem (varsayılan iki hafta) geriye baktığı -kadar ileriye de bakar. +başlayanlar, gecikmişler ve açık kritik aktiviteler. Raporlama dönemi (varsayılan geçen ay) neyin *dönemde tamamlanan* sayılacağını belirler; +*gelecek dönemde başlayanlar* bölümü durum tarihinden ileriye bakar — dönem (kısmen) durum tarihinden +sonraysa dönemin sonuna kadar, *Geçen/Son …* ön ayarında dönemin geriye baktığı kadar +ileriye; tamamen geçmişte kalan özel veya tüm proje döneminde bölüm boş kalır. Özet her iki sınırı da +gösterir. ### Program sağlığı @@ -104,18 +125,21 @@ tarih, ihlal edilen kısıt, tutarsız ilerleme), **uyarılar** (açık başlang gecikmeler). Eşikler seçeneklerdedir; varsayılan DCMA'ya göre: yüksek bolluk ve uzun süre için 44 iş günü, gecikmeler için 10. Temiz bir programda sıfır hata vardır. -### Haftalık kaynak yükü +### Kaynak yükü + +Satırlar kaynağa göre gruplanır (ad ve tür yalnızca her grubun ilk satırında, kaynak atamalarında olduğu gibi); *Toplama* ile takvim haftası veya takvim ayı seçersiniz ve raporlama dönemi hangi haftaların veya ayların görüneceğini belirler. -Kaynak ve hafta başına, mevcut kapasiteye (birim-gün) karşı gereksinim, fark, günlük tepe ve haftanın -aşırı yüklü olup olmadığı — **Kaynaklar** sekmesindeki histogramla aynı hesaplama, tablo halinde. -Yalnızca gereksinimi olan haftalar listelenir; *Yalnızca aşırı yüklü haftalar* ile sadece darboğazlar -kalır. +Kaynak ve hafta ya da ay başına, mevcut kapasiteye (birim-gün) karşı gereksinim, fark, günlük tepe ve +dönemin aşırı yüklü olup olmadığı — **Kaynaklar** sekmesindeki histogramla aynı hesaplama, tablo +halinde. Yalnızca gereksinimi olan dönemler listelenir; *Yalnızca aşırı yüklü dönemler* ile sadece +darboğazlar kalır. PDF'de sayfa sonu bir grubun ortasına denk gelirse kaynak adı sonraki sayfada +yinelenmez. ### Kaynak atamaları Kaynak başına atanan aktiviteler: İKY, ad, başlangıç ve bitiş, kalan süre, günlük birim, -tamamlanma, kritik ve durum. Tamamlanan görevler varsayılan olarak hariçtir. Hafta cinsinden bir -pencereyle *kaynak ileriye bakışı* olur. Özet, kaynaksız görevleri de sayar. +tamamlanma, kritik ve durum. Tamamlanan görevler varsayılan olarak hariçtir. Bir raporlama +dönemiyle (varsayılan tüm proje) *kaynak ileriye bakışı* olur. Özet, kaynaksız görevleri de sayar. ### İKY özeti diff --git a/public/docs/zh/gids-rapporten-printen.md b/public/docs/zh/gids-rapporten-printen.md index 2737f8ce..fbf9a40a 100644 --- a/public/docs/zh/gids-rapporten-printen.md +++ b/public/docs/zh/gids-rapporten-printen.md @@ -60,9 +60,20 @@ WBS 汇总中;吊床任务不出现);**基准日**是项目的状态日期 日期和浮时来自上次**计算**(F5),此后修改过的计划会显示提示,PDF 导出始终先重新计算;每个报告都有一个 小的**报告选项**块,会在会话之间记住。工作日缩写为“工日”。 +### 报告期间 + +四个报告基于时间窗口工作:前瞻计划、进度、资源负荷和资源分配。它们在报告选项中共用一个 +*报告期间* 控件,每个报告各自记住自己的设置: + +- **未来 / 过去一周、2、4、6、8 或 12 周** 以及 **下个月 / 上个月** — 从项目状态日期起算(未设置则从今天起算)。预设两端均包含:星期四 9 月 10 日的 *未来 4 周* 到星期三 10 月 7 日为止。更改状态日期后,窗口会随之移动。 +- **整个项目** — 从计划中最早的开始到最晚的完成。 +- **自定义** — 两个自选日期。此时 *从* 和 *至* 字段可编辑(输入或使用日期选择器);使用预设时它们只读显示计算出的日期。结束日期早于开始日期或日期字段为空时会标红且不会应用。再次选择预设时,预设日期会替换自定义范围。 + +所选期间会显示为报告和 PDF 的副标题;进度报告则在摘要中显示。 + ### 前瞻计划 -每周工地会议的清单:未来 *N* 周(默认四周)的全部活动——哪些开始、哪些继续、哪些完成——以及本应 +每周工地会议的清单:报告期间(默认下个月)内的全部活动——哪些开始、哪些继续、哪些完成——以及本应 已经发生的事项。每行显示 WBS、名称、开始和完成、剩余工期、完成率、总浮时、是否关键或近关键、分配的 资源和状态:**即将开始**、**进行中**、**应已开始**或**逾期**。跨越整个窗口的活动也会列出。 @@ -76,8 +87,8 @@ WBS 汇总中;吊床任务不出现);**基准日**是项目的状态日期 状态日期上的定期“我们在哪里”概览。摘要给出基线完成和预测完成及其工作日差异、**计划**与**实际** 进度(两者均按叶任务工期加权;计划按当前基线的日期计算,没有基线则按当前计划)以及各状态的数量。 -下方五个部分:上一期间完成、进行中、下一期间开始、逾期以及未完成的关键活动。期间(默认两周)向后 -看多远就向前看多远。 +下方五个部分:上一期间完成、进行中、下一期间开始、逾期以及未完成的关键活动。报告期间(默认上个月)决定哪些算作*期间内完成*;*下一期间开始*部分从状态日期向前看——若期间(部分) +在状态日期之后则看到期间结束,对于“过去 …”预设,向前看与期间向后看相同的长度;对于完全在过去的自定义或整个项目期间,该部分为空。摘要同时显示这两个边界。 ### 进度计划健康度 @@ -86,15 +97,17 @@ WBS 汇总中;吊床任务不出现);**基准日**是项目的状态日期 限制、顺序外进度)和**信息**(近关键、高浮时、长延时)。阈值在报告选项中;默认遵循 DCMA:高浮时和 长工期为 44 个工作日,延时为 10 个工作日。干净的计划错误为零。 -### 资源周负荷 +### 资源负荷 + +行按资源分组(名称和类型仅显示在每组的第一行,与资源分配报告相同);通过 *汇总* 在日历周和日历月之间选择,报告期间决定显示哪些周或月。 -按资源和周列出需求与可用产能(以单位·日计)、差额、单日峰值以及该周是否超负荷——与**资源**选项卡上 -的直方图相同的计算,以表格呈现。只列出有需求的周;使用“仅显示超负荷周”即可只保留瓶颈。 +按资源和周或月列出需求与可用产能(以单位·日计)、差额、单日峰值以及该期间是否超负荷——与**资源** +选项卡上的直方图相同的计算,以表格呈现。只列出有需求的期间;使用“仅显示超负荷期间”即可只保留瓶颈。PDF 中若分页恰好落在一个分组中间,资源名称不会在下一页重复。 ### 资源分配 按资源列出分配的活动:WBS、名称、开始和完成、剩余工期、每日单位、完成率、关键和状态。默认不包括 -已完成任务。设置以周为单位的窗口后,它就成为*资源前瞻计划*。摘要还会统计没有资源的任务。 +已完成任务。设置报告期间(默认整个项目)后,它就成为*资源前瞻计划*。摘要还会统计没有资源的任务。 ### WBS 汇总 diff --git a/src/components/panels/reports/ReportingPeriodField.tsx b/src/components/panels/reports/ReportingPeriodField.tsx new file mode 100644 index 00000000..161ee6bb --- /dev/null +++ b/src/components/panels/reports/ReportingPeriodField.tsx @@ -0,0 +1,143 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Select } from '@/components/common/Select'; +import { useAppStore } from '@/state/appStore'; +import { useDisplayDate } from '@/hooks/displayDate'; +import { formatDate } from '@/utils/dateUtils'; +import { + REPORTING_PERIOD_PRESETS, type ReportingPeriod, type ReportingPeriodPreset, type ResolvedPeriod, + isIsoDay, projectSpan, referenceDayOf, resolveReportingPeriod, +} from '@/engine/reports'; + +/** + * Het gedeelde rapportageperiode-control (issue #120): één preset-keuzelijst (volgende/afgelopen + * N weken of maand, hele project, aangepast) plus twee datumvelden. Bij een preset tonen de velden + * de berekende datums alleen-lezen als tekst in de datumnotatie van de app (`useDisplayDate`, net + * als de rapportondertitel); bij *Aangepast* worden het bewerkbare ``-velden — + * bewust de native invoer en niet `DateTextInput`, omdat het issue een kalenderkiezer vraagt; de + * browser toont daarin zijn eigen locale-notatie. Een omgekeerd bereik (tot < van) of een leeg/ + * onvolledig veld wordt niet doorgegeven maar rood gemarkeerd met een melding — het rapport blijft + * op de laatste geldige periode staan. Wisselen van *Aangepast* terug naar een preset laat de eigen + * datums vallen; wisselen náár *Aangepast* start met de datums van de preset die op dat moment gold. + * + * LAYOUT: de datumvelden staan op een eigen rij die bij de standaardbreedte van de instellingen- + * kolom (256 px) omslaat naar twee volle regels — een `` heeft ~120 px nodig om + * een datum plus kalenderknop te tonen (de app staat op 13 px root-fontsize, dus 8.5rem ≈ 110 px + * als ondergrens); onder het `w-32`-label ingesprongen bleef er 18 px over (reviewbevinding op de + * eerste versie). Het Van/Tot-label heeft bewust géén vaste breedte: "Başlangıç" (tr) is breder + * dan "Van" en werd met `w-8` afgekapt (reviewbevinding op de tweede versie). + * + * De opgeloste datums komen uit dezelfde pure functie als de engine (`resolveReportingPeriod`, + * tegen dezelfde referentiedag en projectspanne), zodat wat hier staat exact het venster is dat het + * rapport rekent — óók wanneer de statusdatum wijzigt. + */ +interface Props { + id: string; + value: ReportingPeriod; + onChange: (next: ReportingPeriod) => void; + /** `data-ops-report-option`-sleutel voor tests. */ + dataKey: string; +} + +/** De referentiedag en projectspanne uit de live store — dezelfde bron als `useReportContext`. */ +function useResolvedPeriod(period: ReportingPeriod): ResolvedPeriod { + const tasks = useAppStore(s => s.tasks); + const statusDate = useAppStore(s => s.project.statusDate); + const today = formatDate(new Date()); + return useMemo(() => { + const { day } = referenceDayOf(statusDate, today); + return resolveReportingPeriod(period, day, projectSpan(tasks)); + }, [period, tasks, statusDate, today]); +} + +export function ReportingPeriodField({ id, value, onChange, dataKey }: Props) { + const { t } = useTranslation('report'); + const dd = useDisplayDate(); + const resolved = useResolvedPeriod(value); + const isCustom = value.preset === 'custom'; + // Kladwaarden van de datumvelden: de gebruiker mag een datum half intypen zonder dat elke + // toetsaanslag het rapport herrekent of het veld terugspringt; commit zodra beide geldig zijn. + const [draftFrom, setDraftFrom] = useState(resolved.from); + const [draftTo, setDraftTo] = useState(resolved.to); + useEffect(() => { setDraftFrom(resolved.from); setDraftTo(resolved.to); }, [resolved.from, resolved.to]); + + const incomplete = isCustom && (!isIsoDay(draftFrom) || !isIsoDay(draftTo)); + const invalidRange = isCustom && !incomplete && draftTo < draftFrom; + const invalid = incomplete || invalidRange; + + const commitCustom = (from: string, to: string) => { + if (!isIsoDay(from) || !isIsoDay(to) || to < from) return; + if (from === value.from && to === value.to) return; + onChange({ preset: 'custom', from, to }); + }; + + const changePreset = (preset: ReportingPeriodPreset) => { + if (preset === value.preset) return; + // Naar Aangepast: start met de datums die de vorige keuze opleverde (die staan al in beeld). + if (preset === 'custom') onChange({ preset: 'custom', from: resolved.from, to: resolved.to }); + else onChange({ preset }); + }; + + const dateInput = (which: 'from' | 'to') => { + const draft = which === 'from' ? draftFrom : draftTo; + const setDraft = which === 'from' ? setDraftFrom : setDraftTo; + const label = t(which === 'from' ? 'tableReports.options.periodFrom' : 'tableReports.options.periodTo'); + if (!isCustom) { + return ( +
+ {label} + + {dd.date(which === 'from' ? resolved.from : resolved.to)} + +
+ ); + } + return ( + + ); + }; + + return ( +
+ {/* Label bóven de keuzelijst: naast een `w-32`-label bleven bij de standaardkolom 53 px over + en las élke preset als "Hele pr…" (reviewbevinding ronde 3). */} +
+ + onChange({ [key]: Number(v) })} + id="report-opt-resourceLoadBucket" + className="w-full min-w-0" + aria-label={t('tableReports.options.aggregation')} + value={options.resourceLoadBucket} + onChange={v => onChange({ resourceLoadBucket: v as ResourceLoadingBucket })} options={[ - ...(allowAll ? [{ value: '0', label: t('tableReports.options.allWeeks') }] : []), - ...range(L.weeks.min, L.weeks.max).map(n => ({ value: String(n), label: String(n) })), + { value: 'week', label: t('tableReports.options.aggregation_week') }, + { value: 'month', label: t('tableReports.options.aggregation_month') }, ]} />
@@ -111,13 +121,13 @@ export function TableReportOptionsBlock({ reportType, options, onChange, paperSi let body: React.ReactNode = null; switch (reportType) { case 'lookAhead': - body = <>{weeksRow('lookAheadWeeks', t('tableReports.options.lookAheadWeeks'), false)}{numberRow('nearCriticalDays', t('tableReports.options.nearCriticalDays'), L.nearCriticalDays.min, L.nearCriticalDays.max)}; + body = <>{periodRow('lookAheadPeriod')}{numberRow('nearCriticalDays', t('tableReports.options.nearCriticalDays'), L.nearCriticalDays.min, L.nearCriticalDays.max)}; break; case 'critical': body = numberRow('nearCriticalDays', t('tableReports.options.nearCriticalDays'), L.nearCriticalDays.min, L.nearCriticalDays.max); break; case 'progress': - body = <>{weeksRow('progressPeriodWeeks', t('tableReports.options.periodWeeks'), false)}{numberRow('nearCriticalDays', t('tableReports.options.nearCriticalDays'), L.nearCriticalDays.min, L.nearCriticalDays.max)}; + body = <>{periodRow('progressPeriod')}{numberRow('nearCriticalDays', t('tableReports.options.nearCriticalDays'), L.nearCriticalDays.min, L.nearCriticalDays.max)}; break; case 'health': body = ( @@ -130,10 +140,10 @@ export function TableReportOptionsBlock({ reportType, options, onChange, paperSi ); break; case 'resourceLoading': - body = checkRow('resourceLoadOnlyOverloaded', t('tableReports.options.onlyOverloaded')); + body = <>{periodRow('resourceLoadPeriod')}{aggregationRow}{checkRow('resourceLoadOnlyOverloaded', t('tableReports.options.onlyOverloaded'))}; break; case 'resourceAssignments': - body = <>{weeksRow('resourceAssignmentWeeks', t('tableReports.options.assignmentWeeks'), true)}{checkRow('resourceAssignmentIncludeCompleted', t('tableReports.options.includeCompleted'))}; + body = <>{periodRow('resourceAssignmentPeriod')}{checkRow('resourceAssignmentIncludeCompleted', t('tableReports.options.includeCompleted'))}; break; case 'wbsSummary': body = ( @@ -163,28 +173,35 @@ export function TableReportOptionsBlock({ reportType, options, onChange, paperSi

{t('tableReports.options.sectionTitle')}

-
- - onOrientation(v as ReportOrientation)} - options={[ - { value: 'landscape', label: t('landscape') }, - { value: 'portrait', label: t('portrait') }, - ]} - /> + {/* Papier en oriëntatie elk op een eigen rij met het label erboven: naast een `w-32`-label + (en ook in twee kolommen) bleef bij de standaardkolom ~53 px over en las "Landscape" + als "Landsc…" (dezelfde meting als bij de rapportageperiode, reviewronde 3). */} +
+
+ + onOrientation(v as ReportOrientation)} + options={[ + { value: 'landscape', label: t('landscape') }, + { value: 'portrait', label: t('portrait') }, + ]} + /> +
{body}
diff --git a/src/components/panels/reports/useTableReportSpec.tsx b/src/components/panels/reports/useTableReportSpec.tsx index 4bfa4e03..dfe888ec 100644 --- a/src/components/panels/reports/useTableReportSpec.tsx +++ b/src/components/panels/reports/useTableReportSpec.tsx @@ -5,7 +5,9 @@ import { useAppStore } from '@/state/appStore'; import { useDisplayDate } from '@/hooks/displayDate'; import type { ResourceType } from '@/types/resource'; import { formatDate } from '@/utils/dateUtils'; +import { makeMonthLabeler } from '@/utils/monthLabel'; import type { ReportType, TableReportOptions } from '@/utils/reportSettings'; +import type { ReportingPeriod } from '@/engine/reports'; import { isTableReportType } from '@/utils/reportSettings'; import { type ReportContext, type LookAheadRow, type CriticalRow, type ProgressRow, type HealthCheck, type HealthItem, @@ -122,8 +124,17 @@ function resourcesText(names: string[]): string { } // ── Rapportbouwers ─────────────────────────────────────────────────────────────────────────────── +/** + * "Periode: 10 sep 2026 – 7 okt 2026" — dezelfde ondertitel voor elk rapport met een venster; bij + * *Hele project* staat de presetnaam erachter, zodat je ziet wélke spanne dat is. + */ +function periodSubtitle(t: T, dd: DD, from: string, to: string, preset?: ReportingPeriod['preset']): string { + const base = `${t('tableReports.period')}: ${dd.date(from)} – ${dd.date(to)}`; + return preset === 'project' ? `${base} · ${t('tableReports.periodPresets.project')}` : base; +} + function buildLookAhead(ctx: ReportContext, o: TableReportOptions, t: T, dd: DD, stale: boolean): TableReportSpec { - const r = computeLookAhead(ctx, { weeks: o.lookAheadWeeks, nearCriticalDays: o.nearCriticalDays }); + const r = computeLookAhead(ctx, { period: o.lookAheadPeriod, nearCriticalDays: o.nearCriticalDays }); const p = 'tableReports.lookAhead'; const columns: ReportColumn[] = [ wbsCol(t), nameCol(t), @@ -135,7 +146,7 @@ function buildLookAhead(ctx: ReportContext, o: TableReportOptions, t: T, dd: DD, ]; return { title: t(`${p}.title`), - subtitle: `${t('tableReports.period')}: ${dd.date(r.from)} – ${dd.date(r.to)}`, + subtitle: periodSubtitle(t, dd, r.from, r.to, o.lookAheadPeriod.preset), notes: commonNotes(t, dd, ctx, stale, r.statusDateMissing), summary: [ { label: t(`${p}.total`), value: String(r.counts.total) }, @@ -179,7 +190,7 @@ function buildCritical(ctx: ReportContext, o: TableReportOptions, t: T, dd: DD, } function buildProgress(ctx: ReportContext, o: TableReportOptions, t: T, dd: DD, stale: boolean): TableReportSpec { - const r = computeProgressReport(ctx, { periodWeeks: o.progressPeriodWeeks, nearCriticalDays: o.nearCriticalDays }); + const r = computeProgressReport(ctx, { period: o.progressPeriod, nearCriticalDays: o.nearCriticalDays }); const p = 'tableReports.progress'; const s = r.summary; const columns: ReportColumn[] = [ @@ -196,6 +207,7 @@ function buildProgress(ctx: ReportContext, o: TableReportOptions, t: T, dd: DD, const summary: ReportSummaryItem[] = [ { label: t('tableReports.statusDate'), value: dd.date(s.statusDate) }, { label: t('tableReports.period'), value: `${dd.date(s.periodFrom)} – ${dd.date(s.periodTo)}` }, + { label: t(`${p}.lookAheadUntil`), value: dd.date(s.lookAheadTo) }, { label: t(`${p}.baselineFinish`), value: dd.date(s.baselineFinish) || '—' }, { label: t(`${p}.forecastFinish`), value: dd.date(s.forecastFinish) || '—' }, { label: t(`${p}.finishVariance`), value: s.finishVarianceDays === undefined ? '—' : signed(s.finishVarianceDays), color: varianceColor }, @@ -278,13 +290,37 @@ function buildHealth(ctx: ReportContext, o: TableReportOptions, t: T, dd: DD, st }; } -function buildResourceLoading(ctx: ReportContext, o: TableReportOptions, t: T, tCommon: TFunction<'common'>, dd: DD, stale: boolean): TableReportSpec { - const r = computeResourceLoading(ctx, { onlyOverloaded: o.resourceLoadOnlyOverloaded }); +/** + * Groepering per resource (issue #119): de resourcenaam en het type alleen op de eerste rij van elke + * groep, vet — DOM én PDF. Vooraf bepaald op de rijenlijst: een teller in `text()` zou bij een + * her-render (StrictMode, PDF ná DOM) met de vorige eindstand beginnen en de eerste groepsnaam + * laten wegvallen. + */ +function firstOfResourceGroup(rows: R[], keyOf: (r: R) => string): Set { + const first = new Set(); + let prev = ''; + for (const row of rows) { + if (row.resourceId !== prev) first.add(keyOf(row)); + prev = row.resourceId; + } + return first; +} + + +function buildResourceLoading(ctx: ReportContext, o: TableReportOptions, t: T, tCommon: TFunction<'common'>, dd: DD, stale: boolean, locale: string): TableReportSpec { + const r = computeResourceLoading(ctx, { period: o.resourceLoadPeriod, bucket: o.resourceLoadBucket, onlyOverloaded: o.resourceLoadOnlyOverloaded }); const p = 'tableReports.resourceLoading'; + const monthly = o.resourceLoadBucket === 'month'; + const rowKey = (row: ResourceLoadingRow) => `${row.resourceId}\u0000${row.bucketStart}`; + const firstOfGroup = firstOfResourceGroup(r.rows, rowKey); + // Eén formatter per rapport, niet per cel; Gregoriaans + Latijnse cijfers (zie `monthLabel.ts`). + const monthLabel = makeMonthLabeler(locale); const columns: ReportColumn[] = [ - { key: 'resource', header: t(`${p}.resource`), width: 180, align: 'left', text: r => r.resourceName }, - { key: 'type', header: t(`${p}.type`), width: 100, align: 'left', text: r => tCommon(RESOURCE_TYPE_KEY[r.resourceType]) }, - dateCol('week', t(`${p}.week`), r => r.weekStart, dd), + { key: 'resource', header: t(`${p}.resource`), width: 180, align: 'left', text: row => (firstOfGroup.has(rowKey(row)) ? row.resourceName : ''), bold: () => true }, + { key: 'type', header: t(`${p}.type`), width: 100, align: 'left', text: row => (firstOfGroup.has(rowKey(row)) ? tCommon(RESOURCE_TYPE_KEY[row.resourceType]) : '') }, + monthly + ? { key: 'month', header: t(`${p}.month`), width: 95, align: 'left', text: row => monthLabel(row.bucketStart) } + : dateCol('week', t(`${p}.week`), row => row.bucketStart, dd), { key: 'required', header: t(`${p}.required`), width: 85, align: 'right', text: r => num(r.required) }, { key: 'available', header: t(`${p}.available`), width: 90, align: 'right', text: r => num(r.available) }, { key: 'variance', header: t(`${p}.variance`), width: 80, align: 'right', text: r => signed(r.variance), color: r => (r.variance < 0 ? REPORT_COLORS.error : undefined), bold: r => r.variance < 0 }, @@ -292,14 +328,19 @@ function buildResourceLoading(ctx: ReportContext, o: TableReportOptions, t: T, t // Aantal overbelaste dagen als getal: geen taalkundig meervoud nodig (Pools "dni" was fout bij 1). { key: 'overloaded', header: t(`${p}.overloaded`), width: 100, align: 'right', text: r => (r.overloaded ? String(r.overloadedDays) : ''), color: r => (r.overloaded ? REPORT_COLORS.error : undefined), bold: r => r.overloaded }, ]; + const subtitleParts = [ + periodSubtitle(t, dd, r.from, r.to, o.resourceLoadPeriod.preset), + t(`tableReports.options.aggregation_${o.resourceLoadBucket}`), + ...(o.resourceLoadOnlyOverloaded ? [t('tableReports.options.onlyOverloaded')] : []), + ]; return { title: t(`${p}.title`), - subtitle: o.resourceLoadOnlyOverloaded ? t('tableReports.options.onlyOverloaded') : undefined, - notes: commonNotes(t, dd, ctx, stale), + subtitle: subtitleParts.join(' · '), + notes: commonNotes(t, dd, ctx, stale, r.statusDateMissing), summary: [ { label: t(`${p}.resources`), value: String(r.counts.resources) }, - { label: t(`${p}.weeks`), value: String(r.counts.weeks) }, - { label: t(`${p}.overloadedWeeks`), value: String(r.counts.overloadedWeeks), color: r.counts.overloadedWeeks ? REPORT_COLORS.error : REPORT_COLORS.ok }, + { label: t(monthly ? `${p}.months` : `${p}.weeks`), value: String(r.counts.buckets) }, + { label: t(monthly ? `${p}.overloadedMonths` : `${p}.overloadedWeeks`), value: String(r.counts.overloadedBuckets), color: r.counts.overloadedBuckets ? REPORT_COLORS.error : REPORT_COLORS.ok }, { label: t(`${p}.overloadedResources`), value: String(r.counts.overloadedResources), color: r.counts.overloadedResources ? REPORT_COLORS.error : undefined }, ], sections: [section({ key: 'rows', columns, rows: r.rows, emptyText: t(`${p}.empty`) })], @@ -308,20 +349,14 @@ function buildResourceLoading(ctx: ReportContext, o: TableReportOptions, t: T, t } function buildResourceAssignments(ctx: ReportContext, o: TableReportOptions, t: T, tCommon: TFunction<'common'>, dd: DD, stale: boolean): TableReportSpec { - const r = computeResourceAssignments(ctx, { weeks: o.resourceAssignmentWeeks, includeCompleted: o.resourceAssignmentIncludeCompleted }); + const r = computeResourceAssignments(ctx, { period: o.resourceAssignmentPeriod, includeCompleted: o.resourceAssignmentIncludeCompleted }); const p = 'tableReports.resourceAssignments'; - // Groepering per resource: de naam alleen op de eerste rij van elke groep (DOM én PDF). Vooraf - // bepaald op de rijenlijst — een teller in `text()` zou bij een her-render (StrictMode, PDF ná - // DOM) met de vorige eindstand beginnen en de eerste groepsnaam laten wegvallen. - const firstOfGroup = new Set(); - let prev = ''; - for (const row of r.rows) { - if (row.resourceId !== prev) firstOfGroup.add(row.assignmentId); - prev = row.resourceId; - } + // Groepering per resource: naam en type alleen op de eerste rij van elke groep (DOM én PDF) — + // dezelfde stijl als het belastingsrapport (issue #119). + const firstOfGroup = firstOfResourceGroup(r.rows, row => row.assignmentId); const columns: ReportColumn[] = [ { key: 'resource', header: t(`${p}.resource`), width: 170, align: 'left', text: row => (firstOfGroup.has(row.assignmentId) ? row.resourceName : ''), bold: () => true }, - { key: 'type', header: t('tableReports.resourceLoading.type'), width: 95, align: 'left', text: r => tCommon(RESOURCE_TYPE_KEY[r.resourceType]) }, + { key: 'type', header: t('tableReports.resourceLoading.type'), width: 95, align: 'left', text: row => (firstOfGroup.has(row.assignmentId) ? tCommon(RESOURCE_TYPE_KEY[row.resourceType]) : '') }, wbsCol(t), nameCol(t, 200), dateCol('start', t('tableReports.common.start'), r => r.start, dd), dateCol('finish', t('tableReports.common.finish'), r => r.finish, dd), @@ -333,8 +368,8 @@ function buildResourceAssignments(ctx: ReportContext, o: TableReportOptions, t: ]; return { title: t(`${p}.title`), - subtitle: r.from && r.to ? `${t('tableReports.period')}: ${dd.date(r.from)} – ${dd.date(r.to)}` : undefined, - notes: commonNotes(t, dd, ctx, stale, r.from ? r.statusDateMissing : false), + subtitle: periodSubtitle(t, dd, r.from, r.to, o.resourceAssignmentPeriod.preset), + notes: commonNotes(t, dd, ctx, stale, r.statusDateMissing), summary: [ { label: t(`${p}.resources`), value: String(r.counts.resources) }, { label: t(`${p}.assignments`), value: String(r.counts.assignments) }, @@ -379,10 +414,11 @@ function buildWbsSummary(ctx: ReportContext, o: TableReportOptions, t: T, dd: DD /** De spec voor `reportType`, of null wanneer het geen tabelrapport is (gantt/milestones/variance). */ export function useTableReportSpec(reportType: ReportType, options: TableReportOptions): TableReportSpec | null { - const { t } = useTranslation('report'); + const { t, i18n } = useTranslation('report'); const { t: tCommon } = useTranslation('common'); const dd = useDisplayDate(); const { ctx, stale } = useReportContext(); + const locale = i18n.language; return useMemo(() => { if (!isTableReportType(reportType)) return null; switch (reportType) { @@ -390,10 +426,10 @@ export function useTableReportSpec(reportType: ReportType, options: TableReportO case 'critical': return buildCritical(ctx, options, t, dd, stale); case 'progress': return buildProgress(ctx, options, t, dd, stale); case 'health': return buildHealth(ctx, options, t, dd, stale); - case 'resourceLoading': return buildResourceLoading(ctx, options, t, tCommon, dd, stale); + case 'resourceLoading': return buildResourceLoading(ctx, options, t, tCommon, dd, stale, locale); case 'resourceAssignments': return buildResourceAssignments(ctx, options, t, tCommon, dd, stale); case 'wbsSummary': return buildWbsSummary(ctx, options, t, dd, stale); default: return null; } - }, [reportType, options, ctx, stale, t, tCommon, dd]); + }, [reportType, options, ctx, stale, t, tCommon, dd, locale]); } diff --git a/src/engine/reports/index.ts b/src/engine/reports/index.ts index 4e9deca5..0456cc9b 100644 --- a/src/engine/reports/index.ts +++ b/src/engine/reports/index.ts @@ -1,4 +1,5 @@ export * from './reportCommon'; +export * from './reportingPeriod'; export * from './lookAhead'; export * from './criticalReport'; export * from './progressReport'; diff --git a/src/engine/reports/lookAhead.ts b/src/engine/reports/lookAhead.ts index 3c50a6ad..2b360c2f 100644 --- a/src/engine/reports/lookAhead.ts +++ b/src/engine/reports/lookAhead.ts @@ -1,17 +1,19 @@ import type { Task } from '@/types/task'; import { type ReportContext, assignedResourceNamesIndex, dayOf, isNearCritical, activityTasks, overlapsWindow, - progressState, referenceDay, remainingDays, taskFinish, taskStart, windowEnd, + progressState, remainingDays, resolvePeriodFor, taskFinish, taskStart, } from './reportCommon'; +import type { ReportingPeriod } from './reportingPeriod'; /** - * Look-ahead-rapport (discussie #31, rapport 2): de activiteiten van de komende N weken vanaf de - * statusdatum — het lijstje voor de weekvergadering op de bouw. + * Look-ahead-rapport (discussie #31, rapport 2): de activiteiten in de rapportageperiode (issue + * #120; standaard de komende maand vanaf de statusdatum) — het lijstje voor de weekvergadering + * op de bouw. * * Opgenomen worden de niet-voltooide bladtaken die het venster raken (interval-overlap, dus óók een * taak die het hele venster overspant — precies het gat dat discussie #32 aankaartte) PLUS de - * achterstallige taken van vóór het venster: wie het venster inplant moet weten wat er nog open - * staat. Status per rij: + * achterstallige taken van vóór de referentiedag: wie het venster inplant moet weten wat er nog + * open staat. Status per rij (altijd t.o.v. de referentiedag, niet t.o.v. het venster): * - `overdue` — niet voltooid en de (berekende) finish ligt vóór de referentiedag; * - `lateStart` — nog niet gestart terwijl de start vóór de referentiedag lag; * - `inProgress` — gestart, nog niet voltooid; @@ -42,8 +44,8 @@ export interface LookAheadRow { } export interface LookAheadOptions { - /** Vensterlengte in weken (kalenderweken vanaf de referentiedag). */ - weeks: number; + /** Rapportageperiode (issue #120); standaard `nextMonth`. */ + period: ReportingPeriod; /** Drempel voor near-critical (werkdagen); 0 = alleen de planningsoptie. */ nearCriticalDays: number; } @@ -66,14 +68,19 @@ function statusOf(t: Task, refDay: string): LookAheadStatus | null { } export function computeLookAhead(ctx: ReportContext, opts: LookAheadOptions): LookAheadResult { - const { day: from, statusDateMissing } = referenceDay(ctx); - const to = windowEnd(from, Math.max(1, Math.round(opts.weeks)) * 7); + const { from, to, refDay, statusDateMissing } = resolvePeriodFor(ctx, opts.period); + // Achterstallig en had-moeten-starten werk hoort bij elk venster dat de referentiedag raakt of + // erná ligt. Een venster dat helemaal in het verleden ligt (aangepast 2020) is een terugblik en + // sleept de actuele achterstand niet mee — dezelfde regel als de vooruitblik van het voortgangs- + // rapport (reviewbevinding ronde 3). + const includeBacklog = to >= refDay; const rows: LookAheadRow[] = []; const resourceNames = assignedResourceNamesIndex(ctx); for (const t of activityTasks(ctx.tasks)) { - const status = statusOf(t, from); + const status = statusOf(t, refDay); if (!status) continue; - if (status !== 'overdue' && !overlapsWindow(t, from, to) && !(status === 'lateStart')) continue; + const backlog = status === 'overdue' || status === 'lateStart'; + if (!overlapsWindow(t, from, to) && !(backlog && includeBacklog)) continue; rows.push({ taskId: t.id, wbs: t.wbsCode, diff --git a/src/engine/reports/progressReport.ts b/src/engine/reports/progressReport.ts index 63a7f01c..a7f98d72 100644 --- a/src/engine/reports/progressReport.ts +++ b/src/engine/reports/progressReport.ts @@ -2,8 +2,9 @@ import type { Task } from '@/types/task'; import { parseDate } from '@/utils/dateUtils'; import { type ReportContext, type ProgressState, dayOf, durationDays, isNearCritical, activityTasks, makeEngineCache, progressState, - referenceDay, remainingDays, round1, signedWorkDays, taskFinish, taskStart, windowEnd, windowStart, + remainingDays, resolvePeriodFor, round1, signedWorkDays, taskFinish, taskStart, windowEnd, } from './reportCommon'; +import { type ReportingPeriod, periodDays } from './reportingPeriod'; /** * Voortgangs-/statusrapport (discussie #31, rapport 4): het periodieke "waar staan we"-overzicht @@ -19,10 +20,19 @@ import { * waartegen je meet), anders op de huidige planning (`plannedBasis` vertelt welke). Werkelijk = * de ingevoerde completion, gewogen met dezelfde duur. * - * SECTIES — voltooid in de afgelopen periode, in uitvoering, start in de komende periode, + * SECTIES — voltooid in de rapportageperiode, in uitvoering, start in de komende periode, * achterstallig (had moeten starten/eindigen) en kritieke open activiteiten. Eén taak kan in * meerdere secties staan (in uitvoering én kritiek); dat is bewust — elke sectie beantwoordt * een eigen vraag. + * + * DE PERIODE (issue #120) is de rapportageperiode van het statusrapport — standaard de afgelopen + * maand t/m de statusdatum. "Voltooid" telt binnen die periode. "Start in de komende periode" kijkt + * vanaf de statusdatum VOORUIT: ligt de periode (deels) ná de statusdatum, dan tot het einde van de + * periode. Bij een "afgelopen …"-preset (die per definitie op de statusdatum eindigt) wordt de + * periode gespiegeld — even ver vooruit als hij terugkijkt; zo blijft "afgelopen 2 weken" + * byte-identiek aan de oude "periode (weken)"-optie (2 weken terug, 2 weken vooruit). Een + * aangepaste of project-periode die helemaal in het verleden ligt wordt NIET gespiegeld (een + * venster in 2020 zegt niets over de toekomst): de sectie blijft dan leeg. */ export type ProgressRowStatus = 'complete' | 'inProgress' | 'notStarted' | 'overdueStart' | 'overdueFinish'; @@ -48,8 +58,8 @@ export interface ProgressRow { } export interface ProgressReportOptions { - /** Rapportageperiode in weken: terugkijken (voltooid) en vooruitkijken (start binnenkort). */ - periodWeeks: number; + /** Rapportageperiode (issue #120); standaard `lastMonth`. */ + period: ReportingPeriod; nearCriticalDays: number; } @@ -58,6 +68,8 @@ export interface ProgressSummary { statusDateMissing: boolean; periodFrom: string; periodTo: string; + /** Einde van het vooruitkijkvenster van "start in de komende periode" (vanaf de statusdatum). */ + lookAheadTo: string; baselineFinish?: string; forecastFinish?: string; /** Werkdagen, getekend (+ = later dan de baseline). */ @@ -88,10 +100,14 @@ function overdueOf(t: Task, state: ProgressState, refDay: string): ProgressRow[' } export function computeProgressReport(ctx: ReportContext, opts: ProgressReportOptions): ProgressReportResult { - const { day: ref, statusDateMissing } = referenceDay(ctx); - const periodDays = Math.max(1, Math.round(opts.periodWeeks)) * 7; - const periodFrom = windowStart(ref, periodDays); - const periodTo = windowEnd(ref, periodDays + 1).slice(0, 10); // ref + periodDays kalenderdagen + const { from: periodFrom, to: periodTo, refDay: ref, statusDateMissing } = resolvePeriodFor(ctx, opts.period); + // Vooruitkijken vanaf de statusdatum: tot het periode-einde als dat erná ligt; een "afgelopen …"- + // preset wordt gespiegeld (2 weken terug ⇒ ref + 14 dagen, exact de oude `periodWeeks`-conventie); + // een aangepaste/project-periode in het verleden niet (zie de kop). + const mirrored = opts.period.preset.startsWith('last'); + const lookAheadTo = periodTo > ref ? periodTo + : mirrored ? windowEnd(ref, periodDays({ from: periodFrom, to: periodTo }) + 1) + : periodTo; const engineFor = makeEngineCache(ctx); const baseMap = new Map(ctx.baseline ? ctx.baseline.tasks.map(b => [b.taskId, b]) : []); const refDate = parseDate(ref); @@ -156,13 +172,13 @@ export function computeProgressReport(ctx: ReportContext, opts: ProgressReportOp const completedInPeriod = all.filter(r => { if (r.state !== 'complete') return false; const done = dayOf(r.actualFinish ?? r.finish); - return done >= periodFrom && done <= ref; + return done >= periodFrom && done <= periodTo; }); const inProgress = all.filter(r => r.state === 'inProgress'); const startingNext = all.filter(r => { if (r.state !== 'notStarted' || r.overdue) return false; const s = dayOf(taskStart(leafById.get(r.taskId)!)); - return s >= ref && s <= periodTo; + return s >= ref && s <= lookAheadTo; }); const overdue = all.filter(r => r.overdue !== undefined); const critical = all.filter(r => r.state !== 'complete' && r.isCritical); @@ -176,6 +192,7 @@ export function computeProgressReport(ctx: ReportContext, opts: ProgressReportOp statusDateMissing, periodFrom, periodTo, + lookAheadTo, baselineFinish, forecastFinish, finishVarianceDays, diff --git a/src/engine/reports/reportCommon.ts b/src/engine/reports/reportCommon.ts index 537ed3c5..02d13f16 100644 --- a/src/engine/reports/reportCommon.ts +++ b/src/engine/reports/reportCommon.ts @@ -10,6 +10,7 @@ import { calendarForEngine } from '@/utils/effectiveWorkTime'; import { effHoursPerDay, effectiveCalendarOf, taskDurationMinutes } from '@/utils/taskDuration'; import { taskDurationUnit } from '@/engine/scheduler/duration'; import { addCalendarDays, formatDate, parseDate } from '@/utils/dateUtils'; +import { type ReportingPeriod, type ResolvedPeriod, resolveReportingPeriod } from './reportingPeriod'; /** * Gedeelde bouwstenen van de tabelrapporten (discussie #31, manuvarkey — de rapportuitbreiding). @@ -66,8 +67,13 @@ export function activityTasks(tasks: readonly Task[]): Task[] { /** Referentiedag van het rapport: de statusdatum, anders vandaag. */ export function referenceDay(ctx: ReportContext): { day: string; statusDateMissing: boolean } { - if (ctx.statusDate) return { day: dayOf(ctx.statusDate), statusDateMissing: false }; - return { day: dayOf(ctx.today), statusDateMissing: true }; + return referenceDayOf(ctx.statusDate, ctx.today); +} + +/** Zelfde regel, zonder volledige context — voor de UI die het periodevenster vooraf toont. */ +export function referenceDayOf(statusDate: string | undefined, today: string): { day: string; statusDateMissing: boolean } { + if (statusDate) return { day: dayOf(statusDate), statusDateMissing: false }; + return { day: dayOf(today), statusDateMissing: true }; } export function taskStart(t: Task): string { @@ -151,6 +157,33 @@ export function windowStart(toDay: string, days: number): string { return formatDate(addCalendarDays(parseDate(toDay), -Math.max(0, days - 1))); } +/** + * De projectspanne uit de (berekende) taakdatums: vroegste start t/m laatste einde over alle taken, + * op dagniveau. Undefined bij een project zonder taken. + */ +export function projectSpan(tasks: readonly Task[]): ResolvedPeriod | undefined { + let from: string | undefined; + let to: string | undefined; + for (const t of tasks) { + const s = dayOf(taskStart(t)); + const f = dayOf(taskFinish(t)); + if (!s || !f) continue; + if (!from || s < from) from = s; + if (!to || f > to) to = f; + } + return from && to ? { from, to } : undefined; +} + +/** + * De rapportageperiode (issue #120) opgelost tegen de referentiedag van dit rapport — één plek, + * zodat look-ahead, voortgang, belasting en toewijzingen hetzelfde venster uit dezelfde keuze halen. + */ +export function resolvePeriodFor(ctx: ReportContext, period: ReportingPeriod): ResolvedPeriod & { refDay: string; statusDateMissing: boolean } { + const { day, statusDateMissing } = referenceDay(ctx); + const resolved = resolveReportingPeriod(period, day, projectSpan(ctx.tasks)); + return { ...resolved, refDay: day, statusDateMissing }; +} + /** Interval-overlap op dagniveau (inclusieve grenzen) — dezelfde test als het "Actief tussen"-filter. */ export function overlapsWindow(t: Task, fromDay: string, toDay: string): boolean { return dayOf(taskStart(t)) <= toDay && dayOf(taskFinish(t)) >= fromDay; diff --git a/src/engine/reports/reportingPeriod.ts b/src/engine/reports/reportingPeriod.ts new file mode 100644 index 00000000..fac99244 --- /dev/null +++ b/src/engine/reports/reportingPeriod.ts @@ -0,0 +1,103 @@ +import { addCalendarDays, addCalendarMonths, formatDate, parseDate } from '@/utils/dateUtils'; + +/** + * De rapportageperiode (issue #120, manuvarkey): één gedeeld periodemodel voor alle rapporten die + * op een tijdvenster werken — look-ahead, voortgang, resourcebelasting en resourcetoewijzingen. + * + * Een periode is een PRESET (relatief aan de referentiedag: statusdatum, anders vandaag) of een + * vaste keuze: `project` (de hele projectspanne) of `custom` (twee ISO-dagen). Alleen `custom` + * draagt eigen datums; een preset wordt bij elke berekening opnieuw opgelost, zodat een gewijzigde + * statusdatum het venster automatisch mee verschuift (aanbeveling uit het issue). + * + * Vensterconventie — inclusief aan beide kanten, en byte-identiek aan de oude "N weken"-opties: + * - `nextNWeeks` = [ref, ref + 7N − 1] (4 weken vanaf do 10 sep ⇒ t/m wo 7 okt, zoals het issue) + * - `lastNWeeks` = [ref − 7N + 1, ref] + * - `nextMonth` = [ref, ref + 1 maand − 1 dag]; `lastMonth` = [ref − 1 maand + 1 dag, ref] + * (maandrekenen klemt op de maandlengte: 31 jan → 28 feb, zie `addCalendarMonths`) + * + * Alles hier is puur: geen store, geen React, geen i18n. De UI labelt de presets via `t()`. + */ +export const REPORTING_PERIOD_PRESETS = [ + 'nextWeek', 'next2Weeks', 'next4Weeks', 'next6Weeks', 'next8Weeks', 'next12Weeks', 'nextMonth', + 'lastWeek', 'last2Weeks', 'last4Weeks', 'last6Weeks', 'last8Weeks', 'last12Weeks', 'lastMonth', + 'project', 'custom', +] as const; +export type ReportingPeriodPreset = (typeof REPORTING_PERIOD_PRESETS)[number]; + +export interface ReportingPeriod { + preset: ReportingPeriodPreset; + /** Alleen betekenisvol bij `custom` (ISO-dag, inclusief). */ + from?: string; + /** Alleen betekenisvol bij `custom` (ISO-dag, inclusief). */ + to?: string; +} + +/** Een opgelost venster: twee ISO-dagen, `from ≤ to`. */ +export interface ResolvedPeriod { + from: string; + to: string; +} + +const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/; + +/** Geldige ISO-dag (bestaand, `YYYY-MM-DD`) — `parseDate` accepteert ook 2026-02-31, deze niet. */ +export function isIsoDay(raw: unknown): raw is string { + if (typeof raw !== 'string' || !ISO_DAY.test(raw)) return false; + const d = parseDate(raw); + return !isNaN(d.getTime()) && formatDate(d) === raw; +} + +/** Een `custom`-periode is compleet én geordend; presets zijn altijd geldig. */ +export function isValidReportingPeriod(p: ReportingPeriod): boolean { + if (p.preset !== 'custom') return true; + return isIsoDay(p.from) && isIsoDay(p.to) && p.from <= p.to; +} + +/** Aantal weken van een `next*Weeks`/`last*Weeks`-preset, anders undefined. */ +function presetWeeks(preset: ReportingPeriodPreset): number | undefined { + const m = /^(next|last)(\d*)Weeks?$/.exec(preset); + if (!m) return undefined; + return m[2] === '' ? 1 : Number(m[2]); +} + +/** Vertaal een oude "N weken"-instelling naar de kleinste preset die die N dekt (1→1, 3→4, 9→12). */ +export function weeksToPreset(weeks: number, direction: 'next' | 'last'): ReportingPeriodPreset { + const steps = [1, 2, 4, 6, 8, 12]; + const n = steps.find(s => s >= weeks) ?? 12; + return (n === 1 ? `${direction}Week` : `${direction}${n}Weeks`) as ReportingPeriodPreset; +} + +/** + * Los een periode op tegen de referentiedag. `projectSpan` is het venster voor `project` (en de + * terugval voor een ongeldige `custom`); ontbreekt die (leeg project), dan valt het samen op de + * referentiedag zelf — een leeg rapport, geen exception. + */ +export function resolveReportingPeriod(period: ReportingPeriod, refDay: string, projectSpan?: ResolvedPeriod): ResolvedPeriod { + const ref = parseDate(refDay); + const weeks = presetWeeks(period.preset); + if (weeks !== undefined) { + const days = weeks * 7; + return period.preset.startsWith('next') + ? { from: refDay, to: formatDate(addCalendarDays(ref, days - 1)) } + : { from: formatDate(addCalendarDays(ref, -(days - 1))), to: refDay }; + } + switch (period.preset) { + case 'nextMonth': + return { from: refDay, to: formatDate(addCalendarDays(addCalendarMonths(ref, 1), -1)) }; + case 'lastMonth': + return { from: formatDate(addCalendarDays(addCalendarMonths(ref, -1), 1)), to: refDay }; + case 'custom': + if (isValidReportingPeriod(period)) return { from: period.from!, to: period.to! }; + return projectSpan ?? { from: refDay, to: refDay }; + case 'project': + default: + return projectSpan ?? { from: refDay, to: refDay }; + } +} + +/** Lengte van een venster in kalenderdagen (inclusief), minimaal 1. */ +export function periodDays(p: ResolvedPeriod): number { + const a = parseDate(p.from); + const b = parseDate(p.to); + return Math.max(1, Math.round((b.getTime() - a.getTime()) / 86400000) + 1); +} diff --git a/src/engine/reports/resourceAssignments.ts b/src/engine/reports/resourceAssignments.ts index 46debc60..e16907b6 100644 --- a/src/engine/reports/resourceAssignments.ts +++ b/src/engine/reports/resourceAssignments.ts @@ -1,14 +1,15 @@ import type { Resource } from '@/types/resource'; import { - type ReportContext, type ProgressState, activityTasks, dayOf, overlapsWindow, progressState, referenceDay, - remainingDays, taskFinish, taskStart, windowEnd, + type ReportContext, type ProgressState, activityTasks, dayOf, overlapsWindow, progressState, + remainingDays, resolvePeriodFor, taskFinish, taskStart, } from './reportCommon'; +import type { ReportingPeriod } from './reportingPeriod'; /** * Resourcetoewijzingen (discussie #31, rapport 9): per resource welke activiteiten eraan hangen — - * "wat doet deze ploeg/kraan/onderaannemer?". Met een look-ahead-venster in weken (0 = alles) - * wordt het meteen de "Resource Look-Ahead"-variant uit het voorstel. Rijen zijn gesorteerd op - * resource en dan op start; de UI groepeert per resource. + * "wat doet deze ploeg/kraan/onderaannemer?". Met een rapportageperiode (issue #120; standaard de + * hele projectspanne) wordt het meteen de "Resource Look-Ahead"-variant uit het voorstel. Rijen + * zijn gesorteerd op resource en dan op start; de UI groepeert per resource. */ export interface ResourceAssignmentRow { assignmentId: string; @@ -28,24 +29,23 @@ export interface ResourceAssignmentRow { } export interface ResourceAssignmentOptions { - /** Look-ahead-venster in weken; 0 = alle toewijzingen. */ - weeks: number; + /** Rapportageperiode; `project` = alle toewijzingen. */ + period: ReportingPeriod; includeCompleted: boolean; } export interface ResourceAssignmentResult { - from?: string; - to?: string; + from: string; + to: string; + /** Alleen relevant als de periode relatief aan de referentiedag is (geen `project`/`custom`). */ statusDateMissing: boolean; rows: ResourceAssignmentRow[]; counts: { resources: number; assignments: number; unassignedTasks: number }; } export function computeResourceAssignments(ctx: ReportContext, opts: ResourceAssignmentOptions): ResourceAssignmentResult { - const { day: ref, statusDateMissing } = referenceDay(ctx); - const weeks = Math.max(0, Math.round(opts.weeks)); - const from = weeks > 0 ? ref : undefined; - const to = weeks > 0 ? windowEnd(ref, weeks * 7) : undefined; + const { from, to, refDay, statusDateMissing } = resolvePeriodFor(ctx, opts.period); + const windowed = opts.period.preset !== 'project'; const taskById = new Map(ctx.tasks.map(t => [t.id, t])); const resById = new Map(ctx.resources.map(r => [r.id, r])); const rows: ResourceAssignmentRow[] = []; @@ -60,8 +60,11 @@ export function computeResourceAssignments(ctx: ReportContext, opts: ResourceAss assignedTaskIds.add(t.id); const state = progressState(t); if (!opts.includeCompleted && state === 'complete') continue; - // Venster: overlap, plus achterstallig werk vóór het venster (net als het look-ahead-rapport). - if (from && to && !overlapsWindow(t, from, to) && !(state !== 'complete' && dayOf(taskFinish(t)) < from)) continue; + // Venster: overlap, plus achterstallig werk van vóór de referentiedag (net als het look-ahead- + // rapport) — maar niet bij een venster dat helemaal in het verleden ligt (zie `lookAhead.ts`). + // `project` is bewust geen venster: dan telt élke toewijzing, ook buiten de taakdatums. + const backlog = to >= refDay && state !== 'complete' && dayOf(taskFinish(t)) < refDay; + if (windowed && !overlapsWindow(t, from, to) && !backlog) continue; rows.push({ assignmentId: a.id, resourceId: r.id, @@ -86,7 +89,9 @@ export function computeResourceAssignments(ctx: ReportContext, opts: ResourceAss || a.wbs.localeCompare(b.wbs)); const unassignedTasks = activityTasks(ctx.tasks).filter(t => !t.isMilestone && !assignedTaskIds.has(t.id)).length; return { - from, to, statusDateMissing, rows, + // De referentiedag stuurt bij élk venster (ook `custom`) de insluiting van achterstallig werk, + // dus zonder statusdatum hoort de melding er dan bij. + from, to, statusDateMissing: windowed && statusDateMissing, rows, counts: { resources: new Set(rows.map(r => r.resourceId)).size, assignments: rows.length, diff --git a/src/engine/reports/resourceLoading.ts b/src/engine/reports/resourceLoading.ts index 0c14c6e1..b56af043 100644 --- a/src/engine/reports/resourceLoading.ts +++ b/src/engine/reports/resourceLoading.ts @@ -3,24 +3,32 @@ import type { Task } from '@/types/task'; import type { Sequence } from '@/types/sequence'; import type { WorkCalendar } from '@/types/calendar'; import { computeHistogramReport } from '@/engine/scheduler/ResourceLoad'; -import { type ReportContext, round1 } from './reportCommon'; +import { type ReportContext, resolvePeriodFor, round1 } from './reportCommon'; +import type { ReportingPeriod } from './reportingPeriod'; /** - * Resourcebelasting per week (discussie #31, rapport 8, tabelvorm): per resource per week de - * gevraagde inzet tegenover de beschikbare capaciteit, met het verschil en een overbelastingsvlag. + * Resourcebelasting per week of maand (discussie #31, rapport 8, tabelvorm; issue #119/#120): per + * resource per periode de gevraagde inzet tegenover de beschikbare capaciteit, met het verschil en + * een overbelastingsvlag. * - * Rekenkern is `computeHistogramReport` (week-buckets) — exact dezelfde verdeling als het - * histogram op het Resources-tabblad, dus tabel en scherm spreken elkaar nooit tegen. Eenheid: - * eenheid-dagen per week (som over de werkdagen van units/dag). De histogramGRAFIEK zelf zit - * bewust niet in dit rapport: die staat al op het scherm, en een tabel is wat je in een - * bemensingsoverleg naast elkaar legt. + * Rekenkern is `computeHistogramReport` (week- of maandbuckets) — exact dezelfde dagverdeling als + * het histogram op het Resources-tabblad, dus een weekrij toont hetzelfde getal als de grafiek (de + * maandbucket kent het scherm niet; die is dezelfde verdeling, anders opgeteld). Eenheid: + * eenheid-dagen per bucket (som over de werkdagen van units/dag). De rapportageperiode kiest welke + * buckets meedoen: elke kalenderweek/-maand die de periode raakt, in z'n geheel — de buckets + * blijven hele kalenderweken/-maanden, net als in het histogram, zodat een rij altijd hetzelfde + * getal toont als de grafiek. De histogramGRAFIEK zelf zit bewust niet in dit rapport: die staat + * al op het scherm, en een tabel is wat je in een bemensingsoverleg naast elkaar legt. */ +export type ResourceLoadingBucket = 'week' | 'month'; + export interface ResourceLoadingRow { resourceId: string; resourceName: string; resourceType: Resource['type']; - weekStart: string; - weekEnd: string; + /** Eerste dag van de bucket (maandag resp. de 1e van de maand). */ + bucketStart: string; + bucketEnd: string; required: number; available: number; /** available − required (negatief = tekort). */ @@ -31,15 +39,24 @@ export interface ResourceLoadingRow { } export interface ResourceLoadingOptions { + /** Rapportageperiode (issue #120); `project` = de hele projectspanne. */ + period: ReportingPeriod; + /** Aggregatie per kalenderweek of per kalendermaand (issue #119). */ + bucket: ResourceLoadingBucket; onlyOverloaded: boolean; } export interface ResourceLoadingResult { + from: string; + to: string; + statusDateMissing: boolean; rows: ResourceLoadingRow[]; - counts: { resources: number; weeks: number; overloadedWeeks: number; overloadedResources: number }; + /** Alle tellingen gaan over de rijen in de tabel — dus ná het `onlyOverloaded`-filter. */ + counts: { resources: number; buckets: number; overloadedBuckets: number; overloadedResources: number }; } export function computeResourceLoading(ctx: ReportContext, opts: ResourceLoadingOptions): ResourceLoadingResult { + const { from, to, statusDateMissing } = resolvePeriodFor(ctx, opts.period); const report = computeHistogramReport({ tasks: ctx.tasks as Task[], sequences: ctx.sequences as Sequence[], @@ -48,29 +65,31 @@ export function computeResourceLoading(ctx: ReportContext, opts: ResourceLoading calendar: ctx.calendar, calendars: ctx.calendars as WorkCalendar[], cpmResult: ctx.cpmResult, - bucket: 'week', + from, + to, + bucket: opts.bucket === 'month' ? 'maand' : 'week', }); const byId = new Map(ctx.resources.map(r => [r.id, r])); const rows: ResourceLoadingRow[] = []; const overloadedResources = new Set(); - let weeks = 0; + let buckets = 0; for (const entry of report.resources) { const res = byId.get(entry.resourceId); if (!res) continue; for (const b of entry.buckets) { - // Alleen weken MET vraag: een lege week met capaciteit is geen belasting, en zou de tabel + // Alleen buckets MET vraag: een lege week met capaciteit is geen belasting, en zou de tabel // voor elke resource over de hele projectspanne volspoelen met nullen. if (b.load === 0) continue; - weeks++; const overloaded = b.overallocatedDays.length > 0; - if (overloaded) overloadedResources.add(res.id); if (opts.onlyOverloaded && !overloaded) continue; + buckets++; + if (overloaded) overloadedResources.add(res.id); rows.push({ resourceId: res.id, resourceName: res.name, resourceType: res.type, - weekStart: b.start, - weekEnd: b.end, + bucketStart: b.start, + bucketEnd: b.end, required: round1(b.load), available: round1(b.capacity), variance: round1(b.capacity - b.load), @@ -80,12 +99,16 @@ export function computeResourceLoading(ctx: ReportContext, opts: ResourceLoading }); } } + const relative = opts.period.preset !== 'project' && opts.period.preset !== 'custom'; return { + from, to, + statusDateMissing: relative && statusDateMissing, rows, counts: { - resources: report.resources.length, - weeks, - overloadedWeeks: rows.filter(r => r.overloaded).length, + // Resources in de tabel — dezelfde telling als het toewijzingenrapport (#119). + resources: new Set(rows.map(r => r.resourceId)).size, + buckets, + overloadedBuckets: rows.filter(r => r.overloaded).length, overloadedResources: overloadedResources.size, }, }; diff --git a/src/engine/scheduler/ResourceLoad.ts b/src/engine/scheduler/ResourceLoad.ts index ac508b29..121a6ba2 100644 --- a/src/engine/scheduler/ResourceLoad.ts +++ b/src/engine/scheduler/ResourceLoad.ts @@ -14,7 +14,7 @@ import { CONTOUR_SHAPE_VALUES, CURVE_TO_SHAPE, matchContoursToAssignments, periodsToWorkDaySlots, slotWeightsFromValues, } from '@/engine/contour/contourEngine'; -import { parseDate, formatDate, addCalendarDays, getWeekStart } from '@/utils/dateUtils'; +import { parseDate, formatDate, addCalendarDays, getMonthStart, getWeekStart } from '@/utils/dateUtils'; import { calendarForEngine } from '@/utils/effectiveWorkTime'; /** Controlepunten per curve: (t ∈ [0,1] = positie in de duur, gewicht). Lineair geïnterpoleerd @@ -467,8 +467,8 @@ export function maxUnitsOn(resource: Resource, iso: string): number { // // Een aparte, gescopete engine-pass bovenop dezelfde bouwstenen als `computeResourceLoad` // (`distributeUnits` + werkdag-enumeratie), met drie dingen die de UI-load-pass NIET levert: -// 1. Bucketing (dag/week) — weekbucket levert zowel de weeksom (`load`) als de piekdag -// (`peakDayLoad`), zodat een eendaagse piek niet in de weeksom verdwijnt. +// 1. Bucketing (dag/week/maand) — week- en maandbucket leveren zowel de periodesom (`load`) als +// de piekdag (`peakDayLoad`), zodat een eendaagse piek niet in de som verdwijnt. // 2. Venster-capaciteit — een EIGEN enumeratie over ÁLLE werkdagen van het bucketvenster (ook // onbelaste). De load-pass levert capaciteit enkel op belaste dagen; dat zou een week-som // onderschatten (review-bevinding). Kalender per resource: `resource.calendarId` → bibliotheek, @@ -491,7 +491,7 @@ export interface HistogramBucket { /** ISO-grenzen van het bucketvenster (inclusief). Dagbucket: start == end. */ start: string; end: string; - /** Weekbucket: som van de belasting over de week. Dagbucket: gelijk aan `peakDayLoad`. */ + /** Week-/maandbucket: som van de belasting over de periode. Dagbucket: gelijk aan `peakDayLoad`. */ load: number; /** Hoogste dag-belasting binnen het venster (dagbucket: gelijk aan `load`). */ peakDayLoad: number; @@ -520,7 +520,7 @@ export interface HistogramInput { /** Vensterstart/-einde (ISO). Default = projectspanne uit de taakdatums (min earlyStart..max earlyFinish). */ from?: string; to?: string; - bucket: 'dag' | 'week'; + bucket: 'dag' | 'week' | 'maand'; } /** @@ -587,11 +587,20 @@ export function computeHistogramReport(input: HistogramInput): HistogramReport { toIso = toIso ?? maxF; } - // 3. Bucketvensters — dichte tegeling van [from,to]. Week = ISO-week (ma..zo); dag = één dag. + // 3. Bucketvensters — dichte tegeling van [from,to]. Week = ISO-week (ma..zo); maand = + // kalendermaand; dag = één dag. const windows: Array<{ start: string; end: string }> = []; if (fromIso && toIso && fromIso <= toIso) { const toDate = parseDate(toIso); - if (bucket === 'week') { + if (bucket === 'maand') { + let ms = getMonthStart(parseDate(fromIso)); + let guard = 0; + while (ms <= toDate && guard++ < 100_000) { + const next = getMonthStart(addCalendarDays(ms, 32)); + windows.push({ start: formatDate(ms), end: formatDate(addCalendarDays(next, -1)) }); + ms = next; + } + } else if (bucket === 'week') { let ws = getWeekStart(parseDate(fromIso)); // maandag van de week rond `from` let guard = 0; while (ws <= toDate && guard++ < 100_000) { diff --git a/src/i18n/locales/ar/report.json b/src/i18n/locales/ar/report.json index c0253a50..f092c4dc 100644 --- a/src/i18n/locales/ar/report.json +++ b/src/i18n/locales/ar/report.json @@ -83,7 +83,7 @@ "critical": "حرج وشبه حرج", "progress": "تقرير التقدّم", "health": "سلامة الجدول الزمني", - "resourceLoading": "تحميل الموارد أسبوعياً", + "resourceLoading": "تحميل الموارد", "resourceAssignments": "تعيينات الموارد", "wbsSummary": "ملخّص هيكل تجزئة العمل" }, @@ -174,20 +174,42 @@ }, "options": { "sectionTitle": "خيارات التقرير", - "lookAheadWeeks": "النافذة (أسابيع):", "nearCriticalDays": "شبه حرج ≤ (ي.ع):", - "periodWeeks": "الفترة (أسابيع):", "highFloatDays": "فائض مرتفع > (ي.ع):", "longDurationDays": "مدة طويلة > (ي.ع):", "lagDays": "تأخير > (ي.ع):", - "onlyOverloaded": "الأسابيع المحمّلة بإفراط فقط", - "assignmentWeeks": "النافذة (أسابيع):", - "allWeeks": "الكل", + "reportingPeriod": "فترة التقرير:", + "periodFrom": "من", + "periodTo": "إلى", + "periodInvalid": "تاريخ الانتهاء يسبق تاريخ البداية.", + "periodIncomplete": "أدخل التاريخين معاً.", + "aggregation": "التجميع:", + "aggregation_week": "أسبوعياً", + "aggregation_month": "شهرياً", + "onlyOverloaded": "الفترات المحمّلة بإفراط فقط", "includeCompleted": "تضمين المهام المكتملة", "wbsLevel": "المستوى:", "wbsLevelAll": "الهيكل الكامل", "includeActivities": "إظهار الأنشطة" }, + "periodPresets": { + "nextWeek": "الأسبوع القادم", + "next2Weeks": "الأسبوعان القادمان", + "next4Weeks": "الأسابيع الأربعة القادمة", + "next6Weeks": "الأسابيع الستة القادمة", + "next8Weeks": "الأسابيع الثمانية القادمة", + "next12Weeks": "الأسابيع الاثنا عشر القادمة", + "nextMonth": "الشهر القادم", + "lastWeek": "الأسبوع الماضي", + "last2Weeks": "الأسبوعان الماضيان", + "last4Weeks": "الأسابيع الأربعة الماضية", + "last6Weeks": "الأسابيع الستة الماضية", + "last8Weeks": "الأسابيع الثمانية الماضية", + "last12Weeks": "الأسابيع الاثنا عشر الماضية", + "lastMonth": "الشهر الماضي", + "project": "المشروع بأكمله", + "custom": "مخصص" + }, "lookAhead": { "title": "استشراف (Look-ahead)", "total": "الأنشطة", @@ -212,6 +234,7 @@ "forecastFinish": "النهاية المتوقعة", "finishVariance": "Δ النهاية (ي.ع)", "planned": "المخطط", + "lookAheadUntil": "الاستشراف حتى", "actual": "الفعلي", "plannedBasis_baseline": "(خط الأساس)", "plannedBasis_current": "(الجدول الحالي)", @@ -272,10 +295,11 @@ "empty": "لا توجد نتائج." }, "resourceLoading": { - "title": "تحميل الموارد أسبوعياً", + "title": "تحميل الموارد", "resource": "المورد", "type": "النوع", "week": "أسبوع", + "month": "الشهر", "required": "المطلوب", "available": "المتاح", "variance": "الفرق", @@ -283,7 +307,9 @@ "overloaded": "محمّل بإفراط", "resources": "الموارد", "weeks": "الأسابيع", + "months": "الأشهر", "overloadedWeeks": "أسابيع محمّلة بإفراط", + "overloadedMonths": "أشهر محمّلة بإفراط", "overloadedResources": "موارد محمّلة بإفراط", "empty": "لا يوجد تحميل في هذه الفترة." }, diff --git a/src/i18n/locales/de/report.json b/src/i18n/locales/de/report.json index bc446182..8915a09e 100644 --- a/src/i18n/locales/de/report.json +++ b/src/i18n/locales/de/report.json @@ -71,7 +71,7 @@ "critical": "Kritisch & fast kritisch", "progress": "Fortschrittsbericht", "health": "Terminplan-Qualität", - "resourceLoading": "Ressourcenauslastung pro Woche", + "resourceLoading": "Ressourcenauslastung", "resourceAssignments": "Ressourcenzuweisungen", "wbsSummary": "PSP-Zusammenfassung" }, @@ -162,20 +162,42 @@ }, "options": { "sectionTitle": "Berichtsoptionen", - "lookAheadWeeks": "Fenster (Wochen):", "nearCriticalDays": "Fast kritisch ≤ (AT):", - "periodWeeks": "Zeitraum (Wochen):", "highFloatDays": "Hoher Puffer > (AT):", "longDurationDays": "Lange Dauer > (AT):", "lagDays": "Verzögerung > (AT):", - "onlyOverloaded": "Nur überlastete Wochen", - "assignmentWeeks": "Fenster (Wochen):", - "allWeeks": "Alle", + "reportingPeriod": "Berichtszeitraum:", + "periodFrom": "Von", + "periodTo": "Bis", + "periodInvalid": "Das Enddatum liegt vor dem Startdatum.", + "periodIncomplete": "Beide Daten ausfüllen.", + "aggregation": "Aggregation:", + "aggregation_week": "Wöchentlich", + "aggregation_month": "Monatlich", + "onlyOverloaded": "Nur überlastete Zeiträume", "includeCompleted": "Abgeschlossene Vorgänge einbeziehen", "wbsLevel": "Ebene:", "wbsLevelAll": "Vollständiger PSP", "includeActivities": "Vorgänge anzeigen" }, + "periodPresets": { + "nextWeek": "Nächste Woche", + "next2Weeks": "Nächste 2 Wochen", + "next4Weeks": "Nächste 4 Wochen", + "next6Weeks": "Nächste 6 Wochen", + "next8Weeks": "Nächste 8 Wochen", + "next12Weeks": "Nächste 12 Wochen", + "nextMonth": "Nächster Monat", + "lastWeek": "Letzte Woche", + "last2Weeks": "Letzte 2 Wochen", + "last4Weeks": "Letzte 4 Wochen", + "last6Weeks": "Letzte 6 Wochen", + "last8Weeks": "Letzte 8 Wochen", + "last12Weeks": "Letzte 12 Wochen", + "lastMonth": "Letzter Monat", + "project": "Gesamtes Projekt", + "custom": "Benutzerdefiniert" + }, "lookAhead": { "title": "Vorschau (Look-ahead)", "total": "Vorgänge", @@ -200,6 +222,7 @@ "forecastFinish": "Prognose-Ende", "finishVariance": "Δ Ende (AT)", "planned": "Geplant", + "lookAheadUntil": "Vorschau bis", "actual": "Ist", "plannedBasis_baseline": "(Basisplan)", "plannedBasis_current": "(aktueller Plan)", @@ -260,10 +283,11 @@ "empty": "Keine Befunde." }, "resourceLoading": { - "title": "Ressourcenauslastung pro Woche", + "title": "Ressourcenauslastung", "resource": "Ressource", "type": "Typ", "week": "Woche ab", + "month": "Monat", "required": "Bedarf", "available": "Verfügbar", "variance": "Differenz", @@ -271,7 +295,9 @@ "overloaded": "Überlastet", "resources": "Ressourcen", "weeks": "Wochen", + "months": "Monate", "overloadedWeeks": "Überlastete Wochen", + "overloadedMonths": "Überlastete Monate", "overloadedResources": "Überlastete Ressourcen", "empty": "Keine Auslastung in diesem Zeitraum." }, diff --git a/src/i18n/locales/en/report.json b/src/i18n/locales/en/report.json index 6d9c4a90..b7623ac1 100644 --- a/src/i18n/locales/en/report.json +++ b/src/i18n/locales/en/report.json @@ -71,7 +71,7 @@ "critical": "Critical & near-critical", "progress": "Progress report", "health": "Schedule health", - "resourceLoading": "Resource loading per week", + "resourceLoading": "Resource loading", "resourceAssignments": "Resource assignments", "wbsSummary": "WBS summary" }, @@ -162,20 +162,42 @@ }, "options": { "sectionTitle": "Report options", - "lookAheadWeeks": "Window (weeks):", "nearCriticalDays": "Near-critical ≤ (wd):", - "periodWeeks": "Period (weeks):", "highFloatDays": "High float > (wd):", "longDurationDays": "Long duration > (wd):", "lagDays": "Lag > (wd):", - "onlyOverloaded": "Overloaded weeks only", - "assignmentWeeks": "Window (weeks):", - "allWeeks": "All", + "reportingPeriod": "Reporting period:", + "periodFrom": "From", + "periodTo": "To", + "periodInvalid": "The end date is before the start date.", + "periodIncomplete": "Fill in both dates.", + "aggregation": "Aggregation:", + "aggregation_week": "Weekly", + "aggregation_month": "Monthly", + "onlyOverloaded": "Overloaded periods only", "includeCompleted": "Include completed tasks", "wbsLevel": "Level:", "wbsLevelAll": "Full WBS", "includeActivities": "Show activities" }, + "periodPresets": { + "nextWeek": "Next week", + "next2Weeks": "Next 2 weeks", + "next4Weeks": "Next 4 weeks", + "next6Weeks": "Next 6 weeks", + "next8Weeks": "Next 8 weeks", + "next12Weeks": "Next 12 weeks", + "nextMonth": "Next month", + "lastWeek": "Last week", + "last2Weeks": "Last 2 weeks", + "last4Weeks": "Last 4 weeks", + "last6Weeks": "Last 6 weeks", + "last8Weeks": "Last 8 weeks", + "last12Weeks": "Last 12 weeks", + "lastMonth": "Last month", + "project": "Project duration", + "custom": "Custom" + }, "lookAhead": { "title": "Look-ahead", "total": "Activities", @@ -200,6 +222,7 @@ "forecastFinish": "Forecast finish", "finishVariance": "Δ finish (wd)", "planned": "Planned", + "lookAheadUntil": "Look ahead until", "actual": "Actual", "plannedBasis_baseline": "(baseline)", "plannedBasis_current": "(current schedule)", @@ -260,10 +283,11 @@ "empty": "No findings." }, "resourceLoading": { - "title": "Resource loading per week", + "title": "Resource loading", "resource": "Resource", "type": "Type", "week": "Week of", + "month": "Month", "required": "Required", "available": "Available", "variance": "Variance", @@ -271,7 +295,9 @@ "overloaded": "Overloaded", "resources": "Resources", "weeks": "Weeks", + "months": "Months", "overloadedWeeks": "Overloaded weeks", + "overloadedMonths": "Overloaded months", "overloadedResources": "Overloaded resources", "empty": "No loading in this period." }, diff --git a/src/i18n/locales/es/report.json b/src/i18n/locales/es/report.json index 268a889d..348a71c2 100644 --- a/src/i18n/locales/es/report.json +++ b/src/i18n/locales/es/report.json @@ -74,7 +74,7 @@ "critical": "Crítico y casi crítico", "progress": "Informe de avance", "health": "Salud del cronograma", - "resourceLoading": "Carga de recursos por semana", + "resourceLoading": "Carga de recursos", "resourceAssignments": "Asignaciones de recursos", "wbsSummary": "Resumen EDT" }, @@ -165,20 +165,42 @@ }, "options": { "sectionTitle": "Opciones del informe", - "lookAheadWeeks": "Ventana (semanas):", "nearCriticalDays": "Casi crítico ≤ (dl):", - "periodWeeks": "Periodo (semanas):", "highFloatDays": "Holgura alta > (dl):", "longDurationDays": "Duración larga > (dl):", "lagDays": "Retardo > (dl):", - "onlyOverloaded": "Solo semanas sobrecargadas", - "assignmentWeeks": "Ventana (semanas):", - "allWeeks": "Todo", + "reportingPeriod": "Periodo del informe:", + "periodFrom": "Desde", + "periodTo": "Hasta", + "periodInvalid": "La fecha final es anterior a la inicial.", + "periodIncomplete": "Introduzca ambas fechas.", + "aggregation": "Agregación:", + "aggregation_week": "Semanal", + "aggregation_month": "Mensual", + "onlyOverloaded": "Solo periodos sobrecargados", "includeCompleted": "Incluir tareas completadas", "wbsLevel": "Nivel:", "wbsLevelAll": "EDT completa", "includeActivities": "Mostrar actividades" }, + "periodPresets": { + "nextWeek": "Próxima semana", + "next2Weeks": "Próximas 2 semanas", + "next4Weeks": "Próximas 4 semanas", + "next6Weeks": "Próximas 6 semanas", + "next8Weeks": "Próximas 8 semanas", + "next12Weeks": "Próximas 12 semanas", + "nextMonth": "Próximo mes", + "lastWeek": "Última semana", + "last2Weeks": "Últimas 2 semanas", + "last4Weeks": "Últimas 4 semanas", + "last6Weeks": "Últimas 6 semanas", + "last8Weeks": "Últimas 8 semanas", + "last12Weeks": "Últimas 12 semanas", + "lastMonth": "Último mes", + "project": "Todo el proyecto", + "custom": "Personalizado" + }, "lookAhead": { "title": "Previsión (look-ahead)", "total": "Actividades", @@ -203,6 +225,7 @@ "forecastFinish": "Fin previsto", "finishVariance": "Δ fin (dl)", "planned": "Planificado", + "lookAheadUntil": "Previsión hasta", "actual": "Real", "plannedBasis_baseline": "(línea base)", "plannedBasis_current": "(cronograma actual)", @@ -263,10 +286,11 @@ "empty": "Sin hallazgos." }, "resourceLoading": { - "title": "Carga de recursos por semana", + "title": "Carga de recursos", "resource": "Recurso", "type": "Tipo", "week": "Semana del", + "month": "Mes", "required": "Requerido", "available": "Disponible", "variance": "Diferencia", @@ -274,7 +298,9 @@ "overloaded": "Sobrecargado", "resources": "Recursos", "weeks": "Semanas", + "months": "Meses", "overloadedWeeks": "Semanas sobrecargadas", + "overloadedMonths": "Meses sobrecargados", "overloadedResources": "Recursos sobrecargados", "empty": "Sin carga en este periodo." }, diff --git a/src/i18n/locales/fa/report.json b/src/i18n/locales/fa/report.json index 33e6bee1..1859b27c 100644 --- a/src/i18n/locales/fa/report.json +++ b/src/i18n/locales/fa/report.json @@ -72,7 +72,7 @@ "critical": "بحرانی و نزدیک به بحرانی", "progress": "گزارش پیشرفت", "health": "سلامت برنامه", - "resourceLoading": "بار منابع در هفته", + "resourceLoading": "بار منابع", "resourceAssignments": "تخصیص منابع", "wbsSummary": "خلاصهٔ WBS" }, @@ -163,20 +163,42 @@ }, "options": { "sectionTitle": "گزینه‌های گزارش", - "lookAheadWeeks": "بازه (هفته):", "nearCriticalDays": "نزدیک به بحرانی ≤ (ر.ک):", - "periodWeeks": "دوره (هفته):", "highFloatDays": "شناوری زیاد > (ر.ک):", "longDurationDays": "مدت طولانی > (ر.ک):", "lagDays": "تأخیر > (ر.ک):", - "onlyOverloaded": "فقط هفته‌های بیش‌بار", - "assignmentWeeks": "بازه (هفته):", - "allWeeks": "همه", + "reportingPeriod": "دورهٔ گزارش:", + "periodFrom": "از", + "periodTo": "تا", + "periodInvalid": "تاریخ پایان پیش از تاریخ شروع است.", + "periodIncomplete": "هر دو تاریخ را وارد کنید.", + "aggregation": "تجمیع:", + "aggregation_week": "هفتگی", + "aggregation_month": "ماهانه", + "onlyOverloaded": "فقط دوره‌های بیش‌بار", "includeCompleted": "شامل کارهای تکمیل‌شده", "wbsLevel": "سطح:", "wbsLevelAll": "WBS کامل", "includeActivities": "نمایش فعالیت‌ها" }, + "periodPresets": { + "nextWeek": "هفتهٔ آینده", + "next2Weeks": "۲ هفتهٔ آینده", + "next4Weeks": "۴ هفتهٔ آینده", + "next6Weeks": "۶ هفتهٔ آینده", + "next8Weeks": "۸ هفتهٔ آینده", + "next12Weeks": "۱۲ هفتهٔ آینده", + "nextMonth": "ماه آینده", + "lastWeek": "هفتهٔ گذشته", + "last2Weeks": "۲ هفتهٔ گذشته", + "last4Weeks": "۴ هفتهٔ گذشته", + "last6Weeks": "۶ هفتهٔ گذشته", + "last8Weeks": "۸ هفتهٔ گذشته", + "last12Weeks": "۱۲ هفتهٔ گذشته", + "lastMonth": "ماه گذشته", + "project": "کل پروژه", + "custom": "سفارشی" + }, "lookAhead": { "title": "پیش‌نگر (Look-ahead)", "total": "فعالیت‌ها", @@ -201,6 +223,7 @@ "forecastFinish": "پایان پیش‌بینی‌شده", "finishVariance": "Δ پایان (ر.ک)", "planned": "برنامه‌ریزی‌شده", + "lookAheadUntil": "پیش‌نگری تا", "actual": "واقعی", "plannedBasis_baseline": "(خط مبنا)", "plannedBasis_current": "(برنامهٔ فعلی)", @@ -261,10 +284,11 @@ "empty": "یافته‌ای نیست." }, "resourceLoading": { - "title": "بار منابع در هفته", + "title": "بار منابع", "resource": "منبع", "type": "نوع", "week": "هفتهٔ", + "month": "ماه", "required": "مورد نیاز", "available": "در دسترس", "variance": "تفاوت", @@ -272,7 +296,9 @@ "overloaded": "بیش‌بار", "resources": "منابع", "weeks": "هفته‌ها", + "months": "ماه‌ها", "overloadedWeeks": "هفته‌های بیش‌بار", + "overloadedMonths": "ماه‌های بیش‌بار", "overloadedResources": "منابع بیش‌بار", "empty": "باری در این دوره نیست." }, diff --git a/src/i18n/locales/fr/report.json b/src/i18n/locales/fr/report.json index e4bec476..bb6f6a62 100644 --- a/src/i18n/locales/fr/report.json +++ b/src/i18n/locales/fr/report.json @@ -74,7 +74,7 @@ "critical": "Critique & quasi critique", "progress": "Rapport d'avancement", "health": "Santé du planning", - "resourceLoading": "Charge des ressources par semaine", + "resourceLoading": "Charge des ressources", "resourceAssignments": "Affectations des ressources", "wbsSummary": "Synthèse WBS" }, @@ -165,20 +165,42 @@ }, "options": { "sectionTitle": "Options du rapport", - "lookAheadWeeks": "Fenêtre (semaines) :", "nearCriticalDays": "Quasi critique ≤ (jo) :", - "periodWeeks": "Période (semaines) :", "highFloatDays": "Marge élevée > (jo) :", "longDurationDays": "Longue durée > (jo) :", "lagDays": "Décalage > (jo) :", - "onlyOverloaded": "Semaines surchargées uniquement", - "assignmentWeeks": "Fenêtre (semaines) :", - "allWeeks": "Tout", + "reportingPeriod": "Période de rapport :", + "periodFrom": "Du", + "periodTo": "Au", + "periodInvalid": "La date de fin précède la date de début.", + "periodIncomplete": "Renseignez les deux dates.", + "aggregation": "Agrégation :", + "aggregation_week": "Par semaine", + "aggregation_month": "Par mois", + "onlyOverloaded": "Périodes surchargées uniquement", "includeCompleted": "Inclure les tâches terminées", "wbsLevel": "Niveau :", "wbsLevelAll": "WBS complet", "includeActivities": "Afficher les activités" }, + "periodPresets": { + "nextWeek": "Semaine prochaine", + "next2Weeks": "2 prochaines semaines", + "next4Weeks": "4 prochaines semaines", + "next6Weeks": "6 prochaines semaines", + "next8Weeks": "8 prochaines semaines", + "next12Weeks": "12 prochaines semaines", + "nextMonth": "Mois prochain", + "lastWeek": "Semaine dernière", + "last2Weeks": "2 dernières semaines", + "last4Weeks": "4 dernières semaines", + "last6Weeks": "6 dernières semaines", + "last8Weeks": "8 dernières semaines", + "last12Weeks": "12 dernières semaines", + "lastMonth": "Mois dernier", + "project": "Tout le projet", + "custom": "Personnalisée" + }, "lookAhead": { "title": "Prévision (look-ahead)", "total": "Activités", @@ -203,6 +225,7 @@ "forecastFinish": "Fin prévue", "finishVariance": "Δ fin (jo)", "planned": "Prévu", + "lookAheadUntil": "Anticipation jusqu'au", "actual": "Réel", "plannedBasis_baseline": "(référence)", "plannedBasis_current": "(planning actuel)", @@ -263,10 +286,11 @@ "empty": "Aucun constat." }, "resourceLoading": { - "title": "Charge des ressources par semaine", + "title": "Charge des ressources", "resource": "Ressource", "type": "Type", "week": "Semaine du", + "month": "Mois", "required": "Requis", "available": "Disponible", "variance": "Écart", @@ -274,7 +298,9 @@ "overloaded": "Surchargé", "resources": "Ressources", "weeks": "Semaines", + "months": "Mois", "overloadedWeeks": "Semaines surchargées", + "overloadedMonths": "Mois surchargés", "overloadedResources": "Ressources surchargées", "empty": "Aucune charge sur cette période." }, diff --git a/src/i18n/locales/it/report.json b/src/i18n/locales/it/report.json index 8f373168..c0283846 100644 --- a/src/i18n/locales/it/report.json +++ b/src/i18n/locales/it/report.json @@ -74,7 +74,7 @@ "critical": "Critico e quasi critico", "progress": "Rapporto di avanzamento", "health": "Salute del programma", - "resourceLoading": "Carico risorse per settimana", + "resourceLoading": "Carico risorse", "resourceAssignments": "Assegnazioni risorse", "wbsSummary": "Riepilogo WBS" }, @@ -165,20 +165,42 @@ }, "options": { "sectionTitle": "Opzioni del rapporto", - "lookAheadWeeks": "Finestra (settimane):", "nearCriticalDays": "Quasi critico ≤ (gl):", - "periodWeeks": "Periodo (settimane):", "highFloatDays": "Slack elevato > (gl):", "longDurationDays": "Durata lunga > (gl):", "lagDays": "Ritardo > (gl):", - "onlyOverloaded": "Solo settimane sovraccariche", - "assignmentWeeks": "Finestra (settimane):", - "allWeeks": "Tutto", + "reportingPeriod": "Periodo del report:", + "periodFrom": "Dal", + "periodTo": "Al", + "periodInvalid": "La data finale precede quella iniziale.", + "periodIncomplete": "Inserisci entrambe le date.", + "aggregation": "Aggregazione:", + "aggregation_week": "Settimanale", + "aggregation_month": "Mensile", + "onlyOverloaded": "Solo periodi sovraccarichi", "includeCompleted": "Includi attività completate", "wbsLevel": "Livello:", "wbsLevelAll": "WBS completa", "includeActivities": "Mostra attività" }, + "periodPresets": { + "nextWeek": "Prossima settimana", + "next2Weeks": "Prossime 2 settimane", + "next4Weeks": "Prossime 4 settimane", + "next6Weeks": "Prossime 6 settimane", + "next8Weeks": "Prossime 8 settimane", + "next12Weeks": "Prossime 12 settimane", + "nextMonth": "Prossimo mese", + "lastWeek": "Ultima settimana", + "last2Weeks": "Ultime 2 settimane", + "last4Weeks": "Ultime 4 settimane", + "last6Weeks": "Ultime 6 settimane", + "last8Weeks": "Ultime 8 settimane", + "last12Weeks": "Ultime 12 settimane", + "lastMonth": "Ultimo mese", + "project": "Intero progetto", + "custom": "Personalizzato" + }, "lookAhead": { "title": "Previsione (look-ahead)", "total": "Attività", @@ -203,6 +225,7 @@ "forecastFinish": "Fine prevista", "finishVariance": "Δ fine (gl)", "planned": "Pianificato", + "lookAheadUntil": "Anteprima fino al", "actual": "Effettivo", "plannedBasis_baseline": "(baseline)", "plannedBasis_current": "(programma attuale)", @@ -263,10 +286,11 @@ "empty": "Nessun rilievo." }, "resourceLoading": { - "title": "Carico risorse per settimana", + "title": "Carico risorse", "resource": "Risorsa", "type": "Tipo", "week": "Settimana del", + "month": "Mese", "required": "Richiesto", "available": "Disponibile", "variance": "Differenza", @@ -274,7 +298,9 @@ "overloaded": "Sovraccarico", "resources": "Risorse", "weeks": "Settimane", + "months": "Mesi", "overloadedWeeks": "Settimane sovraccariche", + "overloadedMonths": "Mesi sovraccarichi", "overloadedResources": "Risorse sovraccariche", "empty": "Nessun carico in questo periodo." }, diff --git a/src/i18n/locales/ja/report.json b/src/i18n/locales/ja/report.json index 165c5993..9fac187b 100644 --- a/src/i18n/locales/ja/report.json +++ b/src/i18n/locales/ja/report.json @@ -68,7 +68,7 @@ "critical": "クリティカル/準クリティカル", "progress": "進捗レポート", "health": "スケジュール健全性", - "resourceLoading": "週別リソース負荷", + "resourceLoading": "リソース負荷", "resourceAssignments": "リソース割当", "wbsSummary": "WBS サマリー" }, @@ -159,20 +159,42 @@ }, "options": { "sectionTitle": "レポート設定", - "lookAheadWeeks": "期間 (週):", "nearCriticalDays": "準クリティカル ≤ (稼働日):", - "periodWeeks": "期間 (週):", "highFloatDays": "高フロート > (稼働日):", "longDurationDays": "長期間 > (稼働日):", "lagDays": "ラグ > (稼働日):", - "onlyOverloaded": "過負荷の週のみ", - "assignmentWeeks": "期間 (週):", - "allWeeks": "すべて", + "reportingPeriod": "報告期間:", + "periodFrom": "開始", + "periodTo": "終了", + "periodInvalid": "終了日が開始日より前です。", + "periodIncomplete": "両方の日付を入力してください。", + "aggregation": "集計:", + "aggregation_week": "週別", + "aggregation_month": "月別", + "onlyOverloaded": "過負荷の期間のみ", "includeCompleted": "完了タスクを含める", "wbsLevel": "レベル:", "wbsLevelAll": "WBS 全体", "includeActivities": "アクティビティを表示" }, + "periodPresets": { + "nextWeek": "来週", + "next2Weeks": "今後 2 週間", + "next4Weeks": "今後 4 週間", + "next6Weeks": "今後 6 週間", + "next8Weeks": "今後 8 週間", + "next12Weeks": "今後 12 週間", + "nextMonth": "来月", + "lastWeek": "先週", + "last2Weeks": "過去 2 週間", + "last4Weeks": "過去 4 週間", + "last6Weeks": "過去 6 週間", + "last8Weeks": "過去 8 週間", + "last12Weeks": "過去 12 週間", + "lastMonth": "先月", + "project": "プロジェクト全体", + "custom": "カスタム" + }, "lookAhead": { "title": "先行計画", "total": "アクティビティ", @@ -197,6 +219,7 @@ "forecastFinish": "予測終了", "finishVariance": "Δ 終了 (稼働日)", "planned": "計画", + "lookAheadUntil": "先読み期限", "actual": "実績", "plannedBasis_baseline": "(ベースライン)", "plannedBasis_current": "(現行スケジュール)", @@ -257,10 +280,11 @@ "empty": "指摘事項はありません。" }, "resourceLoading": { - "title": "週別リソース負荷", + "title": "リソース負荷", "resource": "リソース", "type": "種別", "week": "週 (開始日)", + "month": "月", "required": "必要", "available": "利用可能", "variance": "差", @@ -268,7 +292,9 @@ "overloaded": "過負荷", "resources": "リソース", "weeks": "週", + "months": "月数", "overloadedWeeks": "過負荷の週", + "overloadedMonths": "過負荷の月", "overloadedResources": "過負荷のリソース", "empty": "この期間に負荷はありません。" }, diff --git a/src/i18n/locales/ko/report.json b/src/i18n/locales/ko/report.json index 0988039c..45118c02 100644 --- a/src/i18n/locales/ko/report.json +++ b/src/i18n/locales/ko/report.json @@ -68,7 +68,7 @@ "critical": "주공정 및 준주공정", "progress": "진척 보고서", "health": "일정 건전성", - "resourceLoading": "주별 자원 부하", + "resourceLoading": "자원 부하", "resourceAssignments": "자원 배정", "wbsSummary": "WBS 요약" }, @@ -159,20 +159,42 @@ }, "options": { "sectionTitle": "보고서 옵션", - "lookAheadWeeks": "기간 (주):", "nearCriticalDays": "준주공정 ≤ (작업일):", - "periodWeeks": "기간 (주):", "highFloatDays": "높은 여유 > (작업일):", "longDurationDays": "긴 기간 > (작업일):", "lagDays": "지연 > (작업일):", - "onlyOverloaded": "과부하 주만 표시", - "assignmentWeeks": "기간 (주):", - "allWeeks": "전체", + "reportingPeriod": "보고 기간:", + "periodFrom": "시작", + "periodTo": "종료", + "periodInvalid": "종료일이 시작일보다 앞섭니다.", + "periodIncomplete": "두 날짜를 모두 입력하세요.", + "aggregation": "집계:", + "aggregation_week": "주별", + "aggregation_month": "월별", + "onlyOverloaded": "과부하 기간만 표시", "includeCompleted": "완료된 작업 포함", "wbsLevel": "수준:", "wbsLevelAll": "전체 WBS", "includeActivities": "활동 표시" }, + "periodPresets": { + "nextWeek": "다음 주", + "next2Weeks": "향후 2주", + "next4Weeks": "향후 4주", + "next6Weeks": "향후 6주", + "next8Weeks": "향후 8주", + "next12Weeks": "향후 12주", + "nextMonth": "다음 달", + "lastWeek": "지난주", + "last2Weeks": "지난 2주", + "last4Weeks": "지난 4주", + "last6Weeks": "지난 6주", + "last8Weeks": "지난 8주", + "last12Weeks": "지난 12주", + "lastMonth": "지난달", + "project": "프로젝트 전체", + "custom": "사용자 지정" + }, "lookAhead": { "title": "선행 계획", "total": "활동", @@ -197,6 +219,7 @@ "forecastFinish": "예상 완료", "finishVariance": "Δ 완료 (작업일)", "planned": "계획", + "lookAheadUntil": "선행 기간 종료", "actual": "실적", "plannedBasis_baseline": "(기준선)", "plannedBasis_current": "(현재 일정)", @@ -257,10 +280,11 @@ "empty": "발견 사항이 없습니다." }, "resourceLoading": { - "title": "주별 자원 부하", + "title": "자원 부하", "resource": "자원", "type": "유형", "week": "주 시작일", + "month": "월", "required": "필요", "available": "가용", "variance": "차이", @@ -268,7 +292,9 @@ "overloaded": "과부하", "resources": "자원", "weeks": "주", + "months": "월 수", "overloadedWeeks": "과부하 주", + "overloadedMonths": "과부하 월", "overloadedResources": "과부하 자원", "empty": "이 기간에 부하가 없습니다." }, diff --git a/src/i18n/locales/nl/report.json b/src/i18n/locales/nl/report.json index 348db8b4..d4d2c231 100644 --- a/src/i18n/locales/nl/report.json +++ b/src/i18n/locales/nl/report.json @@ -71,7 +71,7 @@ "critical": "Kritiek & near-critical", "progress": "Voortgangsrapport", "health": "Planningsgezondheid", - "resourceLoading": "Resourcebelasting per week", + "resourceLoading": "Resourcebelasting", "resourceAssignments": "Resourcetoewijzingen", "wbsSummary": "WBS-samenvatting" }, @@ -162,20 +162,42 @@ }, "options": { "sectionTitle": "Rapportopties", - "lookAheadWeeks": "Venster (weken):", "nearCriticalDays": "Near-critical ≤ (wd):", - "periodWeeks": "Periode (weken):", "highFloatDays": "Hoge speling > (wd):", "longDurationDays": "Lange duur > (wd):", "lagDays": "Lag > (wd):", - "onlyOverloaded": "Alleen overbelaste weken", - "assignmentWeeks": "Venster (weken):", - "allWeeks": "Alles", + "reportingPeriod": "Rapportageperiode:", + "periodFrom": "Van", + "periodTo": "Tot", + "periodInvalid": "De einddatum ligt vóór de begindatum.", + "periodIncomplete": "Vul beide datums in.", + "aggregation": "Aggregatie:", + "aggregation_week": "Per week", + "aggregation_month": "Per maand", + "onlyOverloaded": "Alleen overbelaste periodes", "includeCompleted": "Voltooide taken meenemen", "wbsLevel": "Niveau:", "wbsLevelAll": "Volledige WBS", "includeActivities": "Activiteiten tonen" }, + "periodPresets": { + "nextWeek": "Volgende week", + "next2Weeks": "Volgende 2 weken", + "next4Weeks": "Volgende 4 weken", + "next6Weeks": "Volgende 6 weken", + "next8Weeks": "Volgende 8 weken", + "next12Weeks": "Volgende 12 weken", + "nextMonth": "Volgende maand", + "lastWeek": "Afgelopen week", + "last2Weeks": "Afgelopen 2 weken", + "last4Weeks": "Afgelopen 4 weken", + "last6Weeks": "Afgelopen 6 weken", + "last8Weeks": "Afgelopen 8 weken", + "last12Weeks": "Afgelopen 12 weken", + "lastMonth": "Afgelopen maand", + "project": "Hele project", + "custom": "Aangepast" + }, "lookAhead": { "title": "Look-ahead", "total": "Activiteiten", @@ -200,6 +222,7 @@ "forecastFinish": "Prognose-einde", "finishVariance": "Δ einde (wd)", "planned": "Gepland", + "lookAheadUntil": "Vooruitblik t/m", "actual": "Werkelijk", "plannedBasis_baseline": "(baseline)", "plannedBasis_current": "(huidige planning)", @@ -260,10 +283,11 @@ "empty": "Geen bevindingen." }, "resourceLoading": { - "title": "Resourcebelasting per week", + "title": "Resourcebelasting", "resource": "Resource", "type": "Type", "week": "Week van", + "month": "Maand", "required": "Gevraagd", "available": "Beschikbaar", "variance": "Verschil", @@ -271,7 +295,9 @@ "overloaded": "Overbelast", "resources": "Resources", "weeks": "Weken", + "months": "Maanden", "overloadedWeeks": "Overbelaste weken", + "overloadedMonths": "Overbelaste maanden", "overloadedResources": "Overbelaste resources", "empty": "Geen belasting in deze periode." }, diff --git a/src/i18n/locales/pl/report.json b/src/i18n/locales/pl/report.json index 0e37087e..44f07b15 100644 --- a/src/i18n/locales/pl/report.json +++ b/src/i18n/locales/pl/report.json @@ -77,7 +77,7 @@ "critical": "Krytyczne i prawie krytyczne", "progress": "Raport postępu", "health": "Kondycja harmonogramu", - "resourceLoading": "Obciążenie zasobów tygodniowo", + "resourceLoading": "Obciążenie zasobów", "resourceAssignments": "Przydziały zasobów", "wbsSummary": "Podsumowanie WBS" }, @@ -168,20 +168,42 @@ }, "options": { "sectionTitle": "Opcje raportu", - "lookAheadWeeks": "Okno (tygodnie):", "nearCriticalDays": "Prawie krytyczne ≤ (dr):", - "periodWeeks": "Okres (tygodnie):", "highFloatDays": "Duży zapas > (dr):", "longDurationDays": "Długi czas trwania > (dr):", "lagDays": "Zwłoka > (dr):", - "onlyOverloaded": "Tylko przeciążone tygodnie", - "assignmentWeeks": "Okno (tygodnie):", - "allWeeks": "Wszystko", + "reportingPeriod": "Okres raportu:", + "periodFrom": "Od", + "periodTo": "Do", + "periodInvalid": "Data końcowa jest wcześniejsza niż początkowa.", + "periodIncomplete": "Wpisz obie daty.", + "aggregation": "Agregacja:", + "aggregation_week": "Tygodniowo", + "aggregation_month": "Miesięcznie", + "onlyOverloaded": "Tylko przeciążone okresy", "includeCompleted": "Uwzględnij ukończone zadania", "wbsLevel": "Poziom:", "wbsLevelAll": "Pełny WBS", "includeActivities": "Pokaż czynności" }, + "periodPresets": { + "nextWeek": "Następny tydzień", + "next2Weeks": "Następne 2 tygodnie", + "next4Weeks": "Następne 4 tygodnie", + "next6Weeks": "Następne 6 tygodni", + "next8Weeks": "Następne 8 tygodni", + "next12Weeks": "Następne 12 tygodni", + "nextMonth": "Następny miesiąc", + "lastWeek": "Ostatni tydzień", + "last2Weeks": "Ostatnie 2 tygodnie", + "last4Weeks": "Ostatnie 4 tygodnie", + "last6Weeks": "Ostatnie 6 tygodni", + "last8Weeks": "Ostatnie 8 tygodni", + "last12Weeks": "Ostatnie 12 tygodni", + "lastMonth": "Ostatni miesiąc", + "project": "Cały projekt", + "custom": "Niestandardowy" + }, "lookAhead": { "title": "Prognoza (look-ahead)", "total": "Czynności", @@ -206,6 +228,7 @@ "forecastFinish": "Prognozowany koniec", "finishVariance": "Δ koniec (dr)", "planned": "Planowane", + "lookAheadUntil": "Wybiegając do", "actual": "Rzeczywiste", "plannedBasis_baseline": "(plan bazowy)", "plannedBasis_current": "(bieżący harmonogram)", @@ -266,10 +289,11 @@ "empty": "Brak ustaleń." }, "resourceLoading": { - "title": "Obciążenie zasobów tygodniowo", + "title": "Obciążenie zasobów", "resource": "Zasób", "type": "Typ", "week": "Tydzień od", + "month": "Miesiąc", "required": "Wymagane", "available": "Dostępne", "variance": "Różnica", @@ -277,7 +301,9 @@ "overloaded": "Przeciążony", "resources": "Zasoby", "weeks": "Tygodnie", + "months": "Miesiące", "overloadedWeeks": "Przeciążone tygodnie", + "overloadedMonths": "Przeciążone miesiące", "overloadedResources": "Przeciążone zasoby", "empty": "Brak obciążenia w tym okresie." }, diff --git a/src/i18n/locales/pt/report.json b/src/i18n/locales/pt/report.json index 0603dd6e..b7bd207f 100644 --- a/src/i18n/locales/pt/report.json +++ b/src/i18n/locales/pt/report.json @@ -74,7 +74,7 @@ "critical": "Crítico e quase crítico", "progress": "Relatório de progresso", "health": "Saúde do cronograma", - "resourceLoading": "Carga de recursos por semana", + "resourceLoading": "Carga de recursos", "resourceAssignments": "Atribuições de recursos", "wbsSummary": "Resumo EAP" }, @@ -165,20 +165,42 @@ }, "options": { "sectionTitle": "Opções do relatório", - "lookAheadWeeks": "Janela (semanas):", "nearCriticalDays": "Quase crítico ≤ (du):", - "periodWeeks": "Período (semanas):", "highFloatDays": "Folga alta > (du):", "longDurationDays": "Duração longa > (du):", "lagDays": "Atraso > (du):", - "onlyOverloaded": "Apenas semanas sobrecarregadas", - "assignmentWeeks": "Janela (semanas):", - "allWeeks": "Tudo", + "reportingPeriod": "Período do relatório:", + "periodFrom": "De", + "periodTo": "Até", + "periodInvalid": "A data final é anterior à data inicial.", + "periodIncomplete": "Preencha ambas as datas.", + "aggregation": "Agregação:", + "aggregation_week": "Semanal", + "aggregation_month": "Mensal", + "onlyOverloaded": "Apenas períodos sobrecarregados", "includeCompleted": "Incluir tarefas concluídas", "wbsLevel": "Nível:", "wbsLevelAll": "EAP completa", "includeActivities": "Mostrar atividades" }, + "periodPresets": { + "nextWeek": "Próxima semana", + "next2Weeks": "Próximas 2 semanas", + "next4Weeks": "Próximas 4 semanas", + "next6Weeks": "Próximas 6 semanas", + "next8Weeks": "Próximas 8 semanas", + "next12Weeks": "Próximas 12 semanas", + "nextMonth": "Próximo mês", + "lastWeek": "Última semana", + "last2Weeks": "Últimas 2 semanas", + "last4Weeks": "Últimas 4 semanas", + "last6Weeks": "Últimas 6 semanas", + "last8Weeks": "Últimas 8 semanas", + "last12Weeks": "Últimas 12 semanas", + "lastMonth": "Último mês", + "project": "Projeto inteiro", + "custom": "Personalizado" + }, "lookAhead": { "title": "Previsão (look-ahead)", "total": "Atividades", @@ -203,6 +225,7 @@ "forecastFinish": "Fim previsto", "finishVariance": "Δ fim (du)", "planned": "Planeado", + "lookAheadUntil": "Previsão até", "actual": "Real", "plannedBasis_baseline": "(linha de base)", "plannedBasis_current": "(cronograma atual)", @@ -263,10 +286,11 @@ "empty": "Sem constatações." }, "resourceLoading": { - "title": "Carga de recursos por semana", + "title": "Carga de recursos", "resource": "Recurso", "type": "Tipo", "week": "Semana de", + "month": "Mês", "required": "Necessário", "available": "Disponível", "variance": "Diferença", @@ -274,7 +298,9 @@ "overloaded": "Sobrecarregado", "resources": "Recursos", "weeks": "Semanas", + "months": "Meses", "overloadedWeeks": "Semanas sobrecarregadas", + "overloadedMonths": "Meses sobrecarregados", "overloadedResources": "Recursos sobrecarregados", "empty": "Sem carga neste período." }, diff --git a/src/i18n/locales/tr/report.json b/src/i18n/locales/tr/report.json index 404581ea..cf25531a 100644 --- a/src/i18n/locales/tr/report.json +++ b/src/i18n/locales/tr/report.json @@ -71,7 +71,7 @@ "critical": "Kritik ve kritiğe yakın", "progress": "İlerleme raporu", "health": "Program sağlığı", - "resourceLoading": "Haftalık kaynak yükü", + "resourceLoading": "Kaynak yükü", "resourceAssignments": "Kaynak atamaları", "wbsSummary": "İKY özeti" }, @@ -162,20 +162,42 @@ }, "options": { "sectionTitle": "Rapor seçenekleri", - "lookAheadWeeks": "Pencere (hafta):", "nearCriticalDays": "Kritiğe yakın ≤ (ig):", - "periodWeeks": "Dönem (hafta):", "highFloatDays": "Yüksek bolluk > (ig):", "longDurationDays": "Uzun süre > (ig):", "lagDays": "Gecikme > (ig):", - "onlyOverloaded": "Yalnızca aşırı yüklü haftalar", - "assignmentWeeks": "Pencere (hafta):", - "allWeeks": "Tümü", + "reportingPeriod": "Raporlama dönemi:", + "periodFrom": "Başlangıç", + "periodTo": "Bitiş", + "periodInvalid": "Bitiş tarihi başlangıç tarihinden önce.", + "periodIncomplete": "Her iki tarihi de girin.", + "aggregation": "Toplama:", + "aggregation_week": "Haftalık", + "aggregation_month": "Aylık", + "onlyOverloaded": "Yalnızca aşırı yüklü dönemler", "includeCompleted": "Tamamlanan görevleri dahil et", "wbsLevel": "Seviye:", "wbsLevelAll": "Tam İKY", "includeActivities": "Aktiviteleri göster" }, + "periodPresets": { + "nextWeek": "Gelecek hafta", + "next2Weeks": "Gelecek 2 hafta", + "next4Weeks": "Gelecek 4 hafta", + "next6Weeks": "Gelecek 6 hafta", + "next8Weeks": "Gelecek 8 hafta", + "next12Weeks": "Gelecek 12 hafta", + "nextMonth": "Gelecek ay", + "lastWeek": "Geçen hafta", + "last2Weeks": "Son 2 hafta", + "last4Weeks": "Son 4 hafta", + "last6Weeks": "Son 6 hafta", + "last8Weeks": "Son 8 hafta", + "last12Weeks": "Son 12 hafta", + "lastMonth": "Geçen ay", + "project": "Tüm proje", + "custom": "Özel" + }, "lookAhead": { "title": "İleriye bakış (look-ahead)", "total": "Aktiviteler", @@ -200,6 +222,7 @@ "forecastFinish": "Tahmini bitiş", "finishVariance": "Δ bitiş (ig)", "planned": "Planlanan", + "lookAheadUntil": "İleriye bakış bitişi", "actual": "Gerçekleşen", "plannedBasis_baseline": "(temel plan)", "plannedBasis_current": "(mevcut program)", @@ -260,10 +283,11 @@ "empty": "Bulgu yok." }, "resourceLoading": { - "title": "Haftalık kaynak yükü", + "title": "Kaynak yükü", "resource": "Kaynak", "type": "Tür", "week": "Hafta", + "month": "Ay", "required": "Gereken", "available": "Mevcut", "variance": "Fark", @@ -271,7 +295,9 @@ "overloaded": "Aşırı yüklü", "resources": "Kaynaklar", "weeks": "Haftalar", + "months": "Aylar", "overloadedWeeks": "Aşırı yüklü haftalar", + "overloadedMonths": "Aşırı yüklü aylar", "overloadedResources": "Aşırı yüklü kaynaklar", "empty": "Bu dönemde yük yok." }, diff --git a/src/i18n/locales/zh/report.json b/src/i18n/locales/zh/report.json index ccc53e10..7e8c7eae 100644 --- a/src/i18n/locales/zh/report.json +++ b/src/i18n/locales/zh/report.json @@ -68,7 +68,7 @@ "critical": "关键与近关键", "progress": "进度报告", "health": "进度计划健康度", - "resourceLoading": "资源周负荷", + "resourceLoading": "资源负荷", "resourceAssignments": "资源分配", "wbsSummary": "WBS 汇总" }, @@ -159,20 +159,42 @@ }, "options": { "sectionTitle": "报告选项", - "lookAheadWeeks": "窗口 (周):", "nearCriticalDays": "近关键 ≤ (工日):", - "periodWeeks": "期间 (周):", "highFloatDays": "高浮时 > (工日):", "longDurationDays": "长工期 > (工日):", "lagDays": "延时 > (工日):", - "onlyOverloaded": "仅显示超负荷周", - "assignmentWeeks": "窗口 (周):", - "allWeeks": "全部", + "reportingPeriod": "报告期间:", + "periodFrom": "从", + "periodTo": "至", + "periodInvalid": "结束日期早于开始日期。", + "periodIncomplete": "请填写两个日期。", + "aggregation": "汇总:", + "aggregation_week": "按周", + "aggregation_month": "按月", + "onlyOverloaded": "仅显示超负荷期间", "includeCompleted": "包含已完成任务", "wbsLevel": "层级:", "wbsLevelAll": "完整 WBS", "includeActivities": "显示活动" }, + "periodPresets": { + "nextWeek": "下一周", + "next2Weeks": "未来 2 周", + "next4Weeks": "未来 4 周", + "next6Weeks": "未来 6 周", + "next8Weeks": "未来 8 周", + "next12Weeks": "未来 12 周", + "nextMonth": "下个月", + "lastWeek": "上一周", + "last2Weeks": "过去 2 周", + "last4Weeks": "过去 4 周", + "last6Weeks": "过去 6 周", + "last8Weeks": "过去 8 周", + "last12Weeks": "过去 12 周", + "lastMonth": "上个月", + "project": "整个项目", + "custom": "自定义" + }, "lookAhead": { "title": "前瞻计划", "total": "活动", @@ -197,6 +219,7 @@ "forecastFinish": "预测完成", "finishVariance": "Δ 完成 (工日)", "planned": "计划", + "lookAheadUntil": "前瞻至", "actual": "实际", "plannedBasis_baseline": "(基线)", "plannedBasis_current": "(当前计划)", @@ -257,10 +280,11 @@ "empty": "没有发现。" }, "resourceLoading": { - "title": "资源周负荷", + "title": "资源负荷", "resource": "资源", "type": "类型", "week": "周起", + "month": "月份", "required": "需求", "available": "可用", "variance": "差额", @@ -268,7 +292,9 @@ "overloaded": "超负荷", "resources": "资源", "weeks": "周数", + "months": "月数", "overloadedWeeks": "超负荷周", + "overloadedMonths": "超负荷月份", "overloadedResources": "超负荷资源", "empty": "此期间没有负荷。" }, diff --git a/src/services/mcp/tools/readTools.ts b/src/services/mcp/tools/readTools.ts index 56bdb78c..99537d41 100644 --- a/src/services/mcp/tools/readTools.ts +++ b/src/services/mcp/tools/readTools.ts @@ -699,9 +699,9 @@ function getResourceHistogram(ctx: McpContext, args: HistogramArgs) { // schakelde de tool stil naar de AGGREGAAT-modus — een heel andere respons dan gevraagd; // - een onbekend resource-id gaf een lege reeks die leest als "geen belasting". requireOnlyKeys(args, HISTOGRAM_KEYS, 'get_resource_histogram'); - if (args.bucket !== undefined && args.bucket !== 'dag' && args.bucket !== 'week') { + if (args.bucket !== undefined && args.bucket !== 'dag' && args.bucket !== 'week' && args.bucket !== 'maand') { throw new ToolError('VALIDATION', - `\`bucket\` moet 'dag' of 'week' zijn (Nederlandse waarden), kreeg '${String(args.bucket)}'.`); + `\`bucket\` moet 'dag', 'week' of 'maand' zijn (Nederlandse waarden), kreeg '${String(args.bucket)}'.`); } requireIsoDate(args.van, 'van'); requireIsoDate(args.tot, 'tot'); @@ -728,7 +728,7 @@ function getResourceHistogram(ctx: McpContext, args: HistogramArgs) { const fresh = ensureFreshSchedule(ctx.app); const s = ctx.app.store.getState(); // verse contextstate ná een eventuele recompute - const bucket: 'dag' | 'week' = args.bucket === 'dag' ? 'dag' : 'week'; + const bucket: 'dag' | 'week' | 'maand' = args.bucket === 'dag' ? 'dag' : args.bucket === 'maand' ? 'maand' : 'week'; const from = typeof args.van === 'string' ? args.van : undefined; const to = typeof args.tot === 'string' ? args.tot : undefined; @@ -1099,14 +1099,14 @@ export const readTools: McpToolDef[] = [ description: 'Belasting/capaciteit-histogram per resource. Params: `resourceIds` (weglaten = alle; een ' + 'onbekend id geeft een nette fout, geen lege reeks), `van`/`tot` (ISO-venster), `bucket` (exact ' + - '"dag" of "week" — Nederlandse waarden, default "week"). HERREKENT de planning vers wanneer die ' + + '"dag", "week" of "maand" — Nederlandse waarden, default "week"). HERREKENT de planning vers wanneer die ' + 'verouderd is of nog nooit is doorgerekend (en meldt dat via `recomputed`/`warning`). ' + 'DETAIL-OP-AANVRAAG: zónder venster ' + 'ÉN zónder resourceIds (de naïeve eerste call) levert de tool `mode:"aggregate"` — per resource ' + 'een samenvatting (peakLoad + peakDate, overallocatedDayCount, spanStart/spanEnd, loadSum, ' + 'capacitySum) mét `detailAvailable:true`; pieken blijven zo zichtbaar maar de respons is klein. ' + 'Geef `resourceIds` en/of `van`/`tot` voor `mode:"detail"` met de volledige bucket-arrays: per ' + - 'bucket `load` (weekbucket = som over de week), `peakDayLoad`, `capacity`, dag-granulaire ' + + 'bucket `load` (week-/maandbucket = som over de periode), `peakDayLoad`, `capacity`, dag-granulaire ' + '`overallocatedDays` en per overbelaste bucket de veroorzakende toewijzingen (`causes`). ' + 'LET OP — WEEKMODUS-OVERHANG: weekvensters snappen naar hele ISO-weken (ma..zo), dus een venster ' + 'kan aan de randen dagen buiten [van,tot] meenemen; de capaciteit telt álle werkdagen van het ' + @@ -1119,7 +1119,7 @@ export const readTools: McpToolDef[] = [ resourceIds: { type: 'array', items: { type: 'string' } }, van: { type: 'string', description: 'ISO-datum vensterstart' }, tot: { type: 'string', description: 'ISO-datum venstereinde' }, - bucket: { type: 'string', enum: ['dag', 'week'], description: 'Bucketbreedte (default week)' }, + bucket: { type: 'string', enum: ['dag', 'week', 'maand'], description: 'Bucketbreedte (default week)' }, }, additionalProperties: false, }, diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts index 0447b22a..12418318 100644 --- a/src/utils/dateUtils.ts +++ b/src/utils/dateUtils.ts @@ -135,6 +135,18 @@ export function getMonthStart(d: Date): Date { return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1)); } +/** + * Kalendermaanden optellen met klem op de maandlengte: 31 jan + 1 maand = 28/29 feb, niet 3 mrt. + * Zo blijft "een maand vanaf de statusdatum" altijd één kalendermaand en lekt de rapportageperiode + * (issue #120) nooit een paar dagen de volgende maand in. UTC-velden, net als de rest van dit bestand. + */ +export function addCalendarMonths(d: Date, months: number): Date { + const y = d.getUTCFullYear(); + const m = d.getUTCMonth() + months; + const lastDayOfTarget = new Date(Date.UTC(y, m + 1, 0)).getUTCDate(); + return new Date(Date.UTC(y, m, Math.min(d.getUTCDate(), lastDayOfTarget))); +} + /** Format a date for display (e.g., "2 Mar 2026") using Intl */ export function formatDisplayDate(d: Date, locale = 'en'): string { return new Intl.DateTimeFormat(locale, { diff --git a/src/utils/monthLabel.ts b/src/utils/monthLabel.ts new file mode 100644 index 00000000..0a0dd720 --- /dev/null +++ b/src/utils/monthLabel.ts @@ -0,0 +1,21 @@ +/** + * Maandlabel voor een maandbucket ("sep 2026") in de UI-taal — bewust met een GEFORCEERDE + * Gregoriaanse kalender en Latijnse cijfers. Zonder die twee unicode-extensies formatteert `Intl` + * in het Perzisch (`fa`) op de Solar-Hijri-kalender: "شهریور ۱۴۰۵" naast een ondertitel die + * "2026" zegt, in hetzelfde rapport (reviewbevinding ronde 3). Alle overige datums in de app + * lopen via `displayDate` (numeriek, Gregoriaans, ASCII-cijfers); dit label hoort op dezelfde + * kalender en hetzelfde cijferschrift te staan, alleen de maandnáám is vertaald. + */ +export function makeMonthLabeler(locale: string): (isoMonthStart: string) => string { + const base = locale.split('-u-')[0] || 'en'; + let fmt: Intl.DateTimeFormat; + try { + fmt = new Intl.DateTimeFormat(`${base}-u-ca-gregory-nu-latn`, { month: 'short', year: 'numeric', timeZone: 'UTC' }); + } catch { + fmt = new Intl.DateTimeFormat('en-u-ca-gregory-nu-latn', { month: 'short', year: 'numeric', timeZone: 'UTC' }); + } + return (iso) => { + const [y, m] = iso.split('-').map(Number); + return fmt.format(new Date(Date.UTC(y, m - 1, 1))); + }; +} diff --git a/src/utils/reportSettings.ts b/src/utils/reportSettings.ts index a0e31dd7..6bf1bb02 100644 --- a/src/utils/reportSettings.ts +++ b/src/utils/reportSettings.ts @@ -23,6 +23,10 @@ import { getSetting, setSetting } from '@/utils/settingsStore'; import { snapToChoice } from '@/utils/numberChoice'; import { NAME_COLUMN_WIDTH_DEFAULT, NAME_COLUMN_WIDTH_MAX, NAME_COLUMN_WIDTH_MIN, REPORT_FONT_SCALES, REPORT_MAX_ZOOM, REPORT_MIN_ZOOM } from '@/services/print/printPreview'; +import { + REPORTING_PERIOD_PRESETS, type ReportingPeriod, isIsoDay, weeksToPreset, +} from '@/engine/reports/reportingPeriod'; +import type { ResourceLoadingBucket } from '@/engine/reports/resourceLoading'; /** localStorage-sleutel (wordt door `setSetting` geprefixt tot `ops-reportSettings`). */ const STORAGE_KEY = 'reportSettings'; @@ -68,19 +72,28 @@ export function isTableReportType(type: ReportType): boolean { /** * Opties van de tabelrapporten — één object, samen bewaard met de rest van de rapportinstellingen. - * De drempels zijn werkdagen; de vensters kalenderweken. Defaults: look-ahead 4 weken (het - * gangbare "four-week look-ahead"), near-critical ≤ 5 wd, gezondheid volgens DCMA (44 wd). + * De drempels zijn werkdagen; de vensters zijn rapportageperiodes (issue #120: één gedeeld + * periodemodel met presets rond de statusdatum, de projectspanne of een eigen datumbereik — zie + * `src/engine/reports/reportingPeriod.ts`). Defaults volgen issue #120: look-ahead de komende maand, + * voortgang de afgelopen maand, belasting en toewijzingen de hele projectspanne; near-critical ≤ 5 wd, + * gezondheid volgens DCMA (44 wd). Bestaande gebruikers raken die defaults niet: hun opgeslagen + * weken-getal migreert naar de bijbehorende preset (zie `legacyWeeksPeriod`). De oude sleutels + * worden daarna NIET teruggeschreven — wie terugrolt naar een oudere appversie valt voor deze + * vier opties stil terug op de fabrieksdefault. Dat is aanvaardbaar voor een rapportvoorkeur + * (geen projectdata), maar het is een bewuste keuze, geen vergissing. */ export interface TableReportOptions { - lookAheadWeeks: number; + lookAheadPeriod: ReportingPeriod; nearCriticalDays: number; - progressPeriodWeeks: number; + progressPeriod: ReportingPeriod; healthHighFloatDays: number; healthLongDurationDays: number; healthLagDays: number; + resourceLoadPeriod: ReportingPeriod; + /** Aggregatie van het belastingsrapport: per kalenderweek of per kalendermaand (issue #119). */ + resourceLoadBucket: ResourceLoadingBucket; resourceLoadOnlyOverloaded: boolean; - /** 0 = alle toewijzingen. */ - resourceAssignmentWeeks: number; + resourceAssignmentPeriod: ReportingPeriod; resourceAssignmentIncludeCompleted: boolean; /** 0 = volledige WBS. */ wbsSummaryLevel: number; @@ -88,19 +101,25 @@ export interface TableReportOptions { } export const DEFAULT_TABLE_REPORT_OPTIONS: TableReportOptions = { - lookAheadWeeks: 4, + lookAheadPeriod: { preset: 'nextMonth' }, nearCriticalDays: 5, - progressPeriodWeeks: 2, + progressPeriod: { preset: 'lastMonth' }, healthHighFloatDays: 44, healthLongDurationDays: 44, healthLagDays: 10, + resourceLoadPeriod: { preset: 'project' }, + resourceLoadBucket: 'week', resourceLoadOnlyOverloaded: false, - resourceAssignmentWeeks: 0, + resourceAssignmentPeriod: { preset: 'project' }, resourceAssignmentIncludeCompleted: false, wbsSummaryLevel: 2, wbsSummaryIncludeActivities: false, }; +/** De periode-opties — één lijst, zodat UI en loader dezelfde velden kennen. */ +export const TABLE_REPORT_PERIOD_KEYS = ['lookAheadPeriod', 'progressPeriod', 'resourceLoadPeriod', 'resourceAssignmentPeriod'] as const; +export type TableReportPeriodKey = (typeof TABLE_REPORT_PERIOD_KEYS)[number]; + /** * Opties van het resourcediagram (issue #113). `pageBreakPerResource` = "een blad per persoon": * elke resource begint op een nieuwe pagina, zodat je per ploeg of medewerker één vel kunt @@ -119,8 +138,6 @@ export const DEFAULT_RESOURCE_GANTT_OPTIONS: ResourceGanttReportOptions = { /** Grenzen van de numerieke opties (de UI en de loader delen ze). */ export const TABLE_REPORT_LIMITS = { - weeks: { min: 1, max: 12 }, - assignmentWeeks: { min: 0, max: 12 }, nearCriticalDays: { min: 0, max: 60 }, thresholdDays: { min: 1, max: 365 }, lagDays: { min: 0, max: 365 }, @@ -242,20 +259,52 @@ function parseClampedInt(raw: unknown, min: number, max: number): number | undef return Math.min(max, Math.max(min, Math.round(raw))); } -function parseTableReportOptions(raw: unknown): TableReportOptions { +/** + * Een opgeslagen rapportageperiode: een geldige preset, bij `custom` met twee geordende ISO-dagen. + * Een `custom` zonder bruikbare datums valt terug op de default (niet op een halve periode). + */ +export function parseReportingPeriod(raw: unknown, fallback: ReportingPeriod): ReportingPeriod { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ...fallback }; + const s = raw as Record; + const preset = parseEnum(REPORTING_PERIOD_PRESETS, s.preset); + if (!preset) return { ...fallback }; + if (preset !== 'custom') return { preset }; + if (isIsoDay(s.from) && isIsoDay(s.to) && s.from <= s.to) return { preset, from: s.from, to: s.to }; + return { ...fallback }; +} + +/** + * Migratie van de oude "N weken"-getallen (vóór issue #120) naar een preset: de kleinste preset + * die N dekt (3 weken ⇒ 4 weken), 0 toewijzingsweken ⇒ hele project. Alleen gebruikt wanneer het + * nieuwe periodeveld ontbreekt; de oude sleutel wordt daarna niet meer teruggeschreven. + */ +function legacyWeeksPeriod(raw: unknown, direction: 'next' | 'last', zeroIsProject: boolean): ReportingPeriod | undefined { + if (typeof raw !== 'number' || !Number.isFinite(raw)) return undefined; + const n = Math.round(raw); + if (n <= 0) return zeroIsProject ? { preset: 'project' } : undefined; + return { preset: weeksToPreset(n, direction) }; +} + +const LOAD_BUCKETS: readonly ResourceLoadingBucket[] = ['week', 'month']; + +export function parseTableReportOptions(raw: unknown): TableReportOptions { const d = DEFAULT_TABLE_REPORT_OPTIONS; if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ...d }; const s = raw as Record; const L = TABLE_REPORT_LIMITS; + const period = (key: TableReportPeriodKey, legacy: ReportingPeriod | undefined): ReportingPeriod => + parseReportingPeriod(s[key], legacy ?? d[key]); return { - lookAheadWeeks: parseClampedInt(s.lookAheadWeeks, L.weeks.min, L.weeks.max) ?? d.lookAheadWeeks, + lookAheadPeriod: period('lookAheadPeriod', legacyWeeksPeriod(s.lookAheadWeeks, 'next', false)), nearCriticalDays: parseClampedInt(s.nearCriticalDays, L.nearCriticalDays.min, L.nearCriticalDays.max) ?? d.nearCriticalDays, - progressPeriodWeeks: parseClampedInt(s.progressPeriodWeeks, L.weeks.min, L.weeks.max) ?? d.progressPeriodWeeks, + progressPeriod: period('progressPeriod', legacyWeeksPeriod(s.progressPeriodWeeks, 'last', false)), healthHighFloatDays: parseClampedInt(s.healthHighFloatDays, L.thresholdDays.min, L.thresholdDays.max) ?? d.healthHighFloatDays, healthLongDurationDays: parseClampedInt(s.healthLongDurationDays, L.thresholdDays.min, L.thresholdDays.max) ?? d.healthLongDurationDays, healthLagDays: parseClampedInt(s.healthLagDays, L.lagDays.min, L.lagDays.max) ?? d.healthLagDays, + resourceLoadPeriod: period('resourceLoadPeriod', undefined), + resourceLoadBucket: parseEnum(LOAD_BUCKETS, s.resourceLoadBucket) ?? d.resourceLoadBucket, resourceLoadOnlyOverloaded: parseBoolean(s.resourceLoadOnlyOverloaded) ?? d.resourceLoadOnlyOverloaded, - resourceAssignmentWeeks: parseClampedInt(s.resourceAssignmentWeeks, L.assignmentWeeks.min, L.assignmentWeeks.max) ?? d.resourceAssignmentWeeks, + resourceAssignmentPeriod: period('resourceAssignmentPeriod', legacyWeeksPeriod(s.resourceAssignmentWeeks, 'next', true)), resourceAssignmentIncludeCompleted: parseBoolean(s.resourceAssignmentIncludeCompleted) ?? d.resourceAssignmentIncludeCompleted, wbsSummaryLevel: parseClampedInt(s.wbsSummaryLevel, L.wbsLevel.min, L.wbsLevel.max) ?? d.wbsSummaryLevel, wbsSummaryIncludeActivities: parseBoolean(s.wbsSummaryIncludeActivities) ?? d.wbsSummaryIncludeActivities, diff --git a/tests/browser/table-reports.spec.ts b/tests/browser/table-reports.spec.ts index 5bad978b..f0f9f2fd 100644 --- a/tests/browser/table-reports.spec.ts +++ b/tests/browser/table-reports.spec.ts @@ -22,8 +22,8 @@ test('table reports: rapporttype kiezen rendert tabel, optie ververst live, PDF- const report = page.locator('[data-ops-table-report="look-ahead"]'); await expect(report).toBeVisible(); - // Beide taken raken het 4-wekenvenster vanaf de statusdatum (14 sep – 11 okt): twee echte rijen - // (op naam, niet op aantal — de lege-staat is óók één
). + // Beide taken raken de standaardperiode "volgende maand" vanaf de statusdatum (14 sep – 13 okt): + // twee echte rijen (op naam, niet op aantal — de lege-staat is óók één ). const rows = report.locator('[data-ops-report-section="rows"] tbody tr'); await expect(rows).toHaveCount(2); await expect(rows.nth(0)).toContainText('Fundering'); @@ -31,12 +31,66 @@ test('table reports: rapporttype kiezen rendert tabel, optie ververst live, PDF- // Samenvatting: "Activities 2" als label-waardepaar, niet een los cijfer ergens in het blok. await expect(report.locator('[data-ops-report-summary] > div').first()).toHaveText(/^(Activities|Activiteiten)2$/); - // Venster naar 1 week: alleen de lopende taak (Fundering) blijft over; Casco valt eruit. - await page.getByLabel(/^(Window \(weeks\):|Venster \(weken\):)$/).first().click(); - await page.getByRole('option', { name: /^1$/ }).click(); + // Rapportageperiode (issue #120) naar "volgende week": alleen de lopende taak (Fundering) blijft + // over; Casco valt eruit. De datumvelden tonen het berekende venster alleen-lezen. + const periodField = page.locator('[data-ops-report-period="lookAheadPeriod"]'); + await periodField.getByLabel(/^(Reporting period:|Rapportageperiode:)$/).click(); + await page.getByRole('option', { name: /^(Next week|Volgende week)$/ }).click(); await expect(rows).toHaveCount(1); await expect(rows.first()).toContainText('Fundering'); await expect(report).not.toContainText('Casco'); + const fromInput = periodField.locator('[data-ops-report-option="lookAheadPeriod.from"]'); + const toInput = periodField.locator('[data-ops-report-option="lookAheadPeriod.to"]'); + // Bij een preset zijn het alleen-lezen teksten in de datumnotatie van de app (standaard d-m-j), + // geen invoervelden. + await expect(fromInput).toHaveAttribute('data-ops-readonly', 'true'); + await expect(fromInput).toHaveText('14-09-2026'); + await expect(toInput).toHaveText('20-09-2026'); + // Reviewbevinding: labels en waarden mogen bij de standaardbreedte van de instellingenkolom + // niet afkappen — gemeten op scroll- versus clientbreedte, niet alleen op de tekst. + const fitsOwnBox = (el: HTMLElement) => el.scrollWidth <= el.clientWidth + 1; + for (const el of await periodField.locator('[data-ops-report-period-label], [data-ops-readonly]').all()) { + expect(await el.evaluate(fitsOwnBox)).toBe(true); + } + // Ronde 3: óók de gekozen waarde in de keuzelijsten van het optieblok mag niet afkappen + // ("Hele pr…" bij een label naast de Select). + const optionValues = page.locator('[data-ops-report-options] .ops-select__value'); + expect(await optionValues.count()).toBeGreaterThan(0); + for (const el of await optionValues.all()) { + expect(await el.evaluate(fitsOwnBox)).toBe(true); + } + + // Aangepast: de velden worden bewerkbaar en starten op de presetdatums; een eigen bereik in + // oktober haalt Casco terug (Fundering blijft: die had vóór de statusdatum moeten starten en + // staat daarom altijd in de lijst). Een omgekeerd bereik wordt gemeld en NIET toegepast — het + // rapport blijft op het laatste geldige venster staan. + await periodField.getByLabel(/^(Reporting period:|Rapportageperiode:)$/).click(); + await page.getByRole('option', { name: /^(Custom|Aangepast)$/ }).click(); + await expect(fromInput).toBeEnabled(); + await expect(fromInput).toHaveValue('2026-09-14'); + // De invoervelden moeten een volledige datum plus kalenderknop kunnen tonen (eerste review: 18 px), + // en het label ervoor mag niet afkappen (tweede review: "Başlang" in het Turks). + for (const input of [fromInput, toInput]) { + expect((await input.boundingBox())?.width ?? 0).toBeGreaterThanOrEqual(120); + } + for (const el of await periodField.locator('[data-ops-report-period-label]').all()) { + expect(await el.evaluate(fitsOwnBox)).toBe(true); + } + await toInput.fill('2026-10-16'); + await fromInput.fill('2026-10-05'); + await expect(rows).toHaveCount(2); + await expect(report).toContainText('Casco'); + await toInput.fill('2026-10-01'); + await expect(periodField.getByRole('alert')).toBeVisible(); + await expect(rows).toHaveCount(2); + await toInput.fill('2026-10-16'); + await expect(periodField.getByRole('alert')).toHaveCount(0); + // Een leeggemaakt veld is óók ongeldig: gemeld, niet toegepast (het rapport houdt 5–16 okt). + await fromInput.fill(''); + await expect(periodField.getByRole('alert')).toBeVisible(); + await expect(report).toContainText('Casco'); + await fromInput.fill('2026-10-05'); + await expect(periodField.getByRole('alert')).toHaveCount(0); // Review-bevinding 1: een export op een VEROUDERDE planning rekent eerst door én exporteert pas // ná de re-render — het rapport op het scherm draagt dan geen "planning gewijzigd"-melding meer @@ -54,3 +108,52 @@ test('table reports: rapporttype kiezen rendert tabel, optie ververst live, PDF- expect(await page.evaluate(() => window.__OPS__!.store.getState().scheduleStale)).toBe(false); await expect(report.locator('[role=note]')).toHaveCount(0); }); + +// Resourcebelasting (issue #119): gegroepeerd per resource (naam alleen op de eerste rij van de +// groep) en schakelbaar tussen week- en maandaggregatie; de rapportageperiode beperkt de rijen. +test('table reports: resourcebelasting groepeert per resource en aggregeert per week of maand', async ({ page, ops: _ops }) => { + await seedProject(page, [ + { name: 'Fundering', start: '2026-09-07', finish: '2026-09-18', durationDays: 10 }, + { name: 'Casco', start: '2026-09-21', finish: '2026-10-16', durationDays: 20 }, + ], 'Belasting'); + await page.evaluate(() => { + const s = window.__OPS__!.store.getState(); + const [a, b] = s.tasks; + const crew = s.addResource({ name: 'Ploeg A', type: 'CREW', description: '', maxUnits: 1 }); + s.assignResource(a.id, crew, 1); + s.assignResource(b.id, crew, 1); + s.setStatusDate('2026-09-14'); + s.runCPM(); + }); + + await page.getByRole('button', { name: /^(Report|Rapport)$/ }).click(); + await page.getByLabel(/^(Report type|Rapporttype)$/).first().click(); + await page.getByRole('option', { name: /^(Resource loading|Resourcebelasting)$/ }).click(); + const report = page.locator('[data-ops-table-report="resourcebelasting"]'); + await expect(report).toBeVisible(); + const rows = report.locator('[data-ops-report-section="rows"] tbody tr'); + // Zes kalenderweken (7 sep t/m 12 okt) met vraag; de resourcenaam staat alleen op de eerste rij. + await expect(rows).toHaveCount(6); + await expect(rows.nth(0).locator('td').first()).toHaveText('Ploeg A'); + await expect(rows.nth(1).locator('td').first()).toHaveText(''); + + // Maandaggregatie: september en oktober — twee rijen, opnieuw gegroepeerd; de gekozen waarde + // en de maandlabels blijven leesbaar bij de standaardkolombreedte. + await page.getByLabel(/^(Aggregation:|Aggregatie:)$/).first().click(); + await page.getByRole('option', { name: /^(Monthly|Per maand)$/ }).click(); + await expect(rows).toHaveCount(2); + await expect(rows.nth(0).locator('td').nth(2)).toHaveText(/2026/); + for (const el of await page.locator('[data-ops-report-options] .ops-select__value').all()) { + expect(await el.evaluate((node: HTMLElement) => node.scrollWidth <= node.clientWidth + 1)).toBe(true); + } + await expect(rows.nth(0).locator('td').first()).toHaveText('Ploeg A'); + await expect(rows.nth(1).locator('td').first()).toHaveText(''); + await expect(report.locator('[data-ops-report-summary]')).toContainText(/^(Resources1(Months|Maanden)2)/); + + // Rapportageperiode "volgende week" vanaf de statusdatum (14–20 sep) ⇒ alleen september. + const periodField = page.locator('[data-ops-report-period="resourceLoadPeriod"]'); + await periodField.getByLabel(/^(Reporting period:|Rapportageperiode:)$/).click(); + await page.getByRole('option', { name: /^(Next week|Volgende week)$/ }).click(); + await expect(rows).toHaveCount(1); + await expect(report).toContainText(/(Period|Periode): /); +}); diff --git a/tests/mcp/cases-read.ts b/tests/mcp/cases-read.ts index ff0c151a..d46bb99d 100644 --- a/tests/mcp/cases-read.ts +++ b/tests/mcp/cases-read.ts @@ -365,6 +365,16 @@ test('get_resource_histogram: ongescopt (geen venster/resourceIds) ⇒ aggregaat assert(bytes < 20 * 1024, `aggregaat-payload ruim < 20 KB (kreeg ${(bytes / 1024).toFixed(1)} KB)`); }); +test('get_resource_histogram: bucket maand ⇒ kalendermaanden (issue #119)', () => { + const rId = S().resources[0].id; + const data = callOk('planner_get_resource_histogram', { bucket: 'maand', resourceIds: [rId] }); + assertEq(data.bucket, 'maand', 'bucket-echo'); + const res = data.resources.find((r: any) => r.resourceId === rId); + assert(!!res && res.buckets.length > 0, 'maandbuckets aanwezig'); + assert(res.buckets.every((b: any) => b.start.endsWith('-01')), 'elke maandbucket start op de 1e'); + assert(res.buckets.some((b: any) => b.load > 0), 'maandbucket met belasting'); +}); + test('get_resource_histogram: gescopt op resourceIds ⇒ volledig detail met bucket-arrays', () => { loadBenchmark(500); const rId = S().resources[0].id; diff --git a/tests/mcp/cases-schemavalidatie.ts b/tests/mcp/cases-schemavalidatie.ts index 949e036b..e86af47f 100644 --- a/tests/mcp/cases-schemavalidatie.ts +++ b/tests/mcp/cases-schemavalidatie.ts @@ -311,7 +311,7 @@ test('batch: een foute ENUM-waarde in een stap wordt geweigerd', async () => { registerAllTools(); store.getState().newProject(); const err = await expectBatchSchemaReject( - [{ tool: 'planner_get_resource_histogram', args: { bucket: 'maand' } }], + [{ tool: 'planner_get_resource_histogram', args: { bucket: 'jaar' } }], 1, 'planner_get_resource_histogram', 'bucket', ); assert(err.includes('moet één van'), `de toegestane waarden staan erin: ${err}`); diff --git a/tests/planning/check-reports.ts b/tests/planning/check-reports.ts index 30a5f80b..04c1a5fd 100644 --- a/tests/planning/check-reports.ts +++ b/tests/planning/check-reports.ts @@ -10,11 +10,15 @@ // Draait via run.sh. Exit 0 = alles groen. import { useAppStore } from '@/state/appStore'; import { - type ReportContext, + type ReportContext, type ReportingPeriod, computeLookAhead, computeCriticalReport, computeProgressReport, computeScheduleHealth, computeResourceLoading, computeResourceAssignments, computeResourceGanttRows, computeWbsSummary, progressState, remainingDays, resourceBandLabels, taskDepths, + isValidReportingPeriod, periodDays, projectSpan, resolveReportingPeriod, weeksToPreset, } from '@/engine/reports'; +import { addCalendarMonths, formatDate, parseDate } from '@/utils/dateUtils'; +import { makeMonthLabeler } from '@/utils/monthLabel'; +import { DEFAULT_TABLE_REPORT_OPTIONS, parseReportingPeriod, parseTableReportOptions } from '@/utils/reportSettings'; import type { Task } from '@/types/task'; import type { Resource, ResourceAssignment } from '@/types/resource'; @@ -97,7 +101,7 @@ eq('scenario: B rest = 6 wd (10 × 60%)', remainingDays(ctx, byId(B)), 6); // ── Look-ahead ─────────────────────────────────────────────────────────────────────────────────── { - const r = computeLookAhead(ctx, { weeks: 2, nearCriticalDays: 5 }); + const r = computeLookAhead(ctx, { period: { preset: 'next2Weeks' }, nearCriticalDays: 5 }); eq('lookAhead: venster start op de statusdatum', r.from, '2026-09-18'); eq('lookAhead: venster van 2 weken (14 dagen, inclusief)', r.to, '2026-10-01'); eq('lookAhead: geen statusdatum-melding', r.statusDateMissing, false); @@ -113,7 +117,7 @@ eq('scenario: B rest = 6 wd (10 × 60%)', remainingDays(ctx, byId(B)), 6); eq('lookAhead: near-critical geteld', r.counts.nearCritical, r.rows.filter(x => x.isNearCritical).length); // Zonder statusdatum ⇒ vandaag als referentie + melding. - const r2 = computeLookAhead({ ...ctx, statusDate: undefined, today: '2026-09-25' }, { weeks: 1, nearCriticalDays: 0 }); + const r2 = computeLookAhead({ ...ctx, statusDate: undefined, today: '2026-09-25' }, { period: { preset: 'nextWeek' }, nearCriticalDays: 0 }); eq('lookAhead: zonder statusdatum meldt het rapport dat', r2.statusDateMissing, true); eq('lookAhead: en rekent met vandaag', r2.from, '2026-09-25'); @@ -125,7 +129,7 @@ eq('scenario: B rest = 6 wd (10 × 60%)', remainingDays(ctx, byId(B)), 6); d.time.earlyStart = '2026-09-10'; d.time.earlyFinish = '2026-09-30'; const f = clone.find(t => t.id === F)!; f.time.isCritical = false; f.time.totalFloat = 4; - const r3 = computeLookAhead({ ...ctx, tasks: clone }, { weeks: 2, nearCriticalDays: 5 }); + const r3 = computeLookAhead({ ...ctx, tasks: clone }, { period: { preset: 'next2Weeks' }, nearCriticalDays: 5 }); eq('lookAhead: TF 4 ≤ 5 ⇒ near-critical-telling 1', r3.counts.nearCritical, 1); eq('lookAhead: finish vóór de statusdatum ⇒ achterstallig', r3.rows.find(x => x.taskId === G)?.status, 'overdue'); eq('lookAhead: start vóór de statusdatum, niet gestart ⇒ had moeten starten', r3.rows.find(x => x.taskId === D)?.status, 'lateStart'); @@ -153,11 +157,12 @@ eq('scenario: B rest = 6 wd (10 × 60%)', remainingDays(ctx, byId(B)), 6); // ── Voortgangsrapport ──────────────────────────────────────────────────────────────────────────── { - const r = computeProgressReport(ctx, { periodWeeks: 2, nearCriticalDays: 5 }); + const r = computeProgressReport(ctx, { period: { preset: 'last2Weeks' }, nearCriticalDays: 5 }); const s = r.summary; eq('progress: statusdatum', s.statusDate, '2026-09-18'); eq('progress: periode terug 14 dagen', s.periodFrom, '2026-09-05'); - eq('progress: periode vooruit 14 dagen', s.periodTo, '2026-10-02'); + eq('progress: periode t/m de statusdatum', s.periodTo, '2026-09-18'); + eq('progress: vooruitblik gespiegeld, 14 dagen (byte-identiek aan de oude 2-wekenoptie)', s.lookAheadTo, '2026-10-02'); eq('progress: gepland t.o.v. de baseline', s.plannedBasis, 'baseline'); eq('progress: 1 voltooid', s.counts.complete, 1); eq('progress: 1 in uitvoering', s.counts.inProgress, 1); @@ -175,7 +180,7 @@ eq('scenario: B rest = 6 wd (10 × 60%)', remainingDays(ctx, byId(B)), 6); // Zonder baseline: gepland t.o.v. de huidige planning; het gewicht is ALTIJD de huidige duur // (C = 8), ook mét baseline (review-bevinding 5: baseline.duration is voor uur-taken niet canoniek). - const r2 = computeProgressReport({ ...ctx, baseline: null }, { periodWeeks: 1, nearCriticalDays: 5 }); + const r2 = computeProgressReport({ ...ctx, baseline: null }, { period: { preset: 'lastWeek' }, nearCriticalDays: 5 }); eq('progress: zonder baseline ⇒ huidige planning als basis', r2.summary.plannedBasis, 'current'); near('progress: zonder baseline weegt C 8', r2.summary.actualPct, (9 / 74) * 100, 0.2); near('progress: mét baseline hetzelfde gewicht (huidige duur)', s.actualPct, (9 / 74) * 100, 0.2); @@ -187,7 +192,7 @@ eq('scenario: B rest = 6 wd (10 × 60%)', remainingDays(ctx, byId(B)), 6); const clone = ctx.tasks.map(t => ({ ...t, time: { ...t.time } })); const b = clone.find(t => t.id === B)!; b.time.earlyStart = '2026-09-07'; b.time.earlyFinish = '2026-09-11'; // einde vóór de statusdatum, 40% klaar - const r3 = computeProgressReport({ ...ctx, tasks: clone }, { periodWeeks: 2, nearCriticalDays: 5 }); + const r3 = computeProgressReport({ ...ctx, tasks: clone }, { period: { preset: 'last2Weeks' }, nearCriticalDays: 5 }); const c = r3.summary.counts; eq('progress: staat-tellingen sommeren tot het totaal', c.complete + c.inProgress + c.notStarted, c.total); ok('progress: te late lopende taak staat in "in uitvoering"', r3.inProgress.some(x => x.taskId === B)); @@ -201,8 +206,8 @@ eq('scenario: B rest = 6 wd (10 × 60%)', remainingDays(ctx, byId(B)), 6); const clone = ctx.tasks.map(t => ({ ...t, time: { ...t.time } })); const g = clone.find(t => t.id === G)!; g.time.durationUnit = 'hours'; g.time.durationMinutes = 4 * 60; g.time.scheduleDuration = 5; // 4 u = 0,5 wd - const withBase = computeProgressReport({ ...ctx, tasks: clone }, { periodWeeks: 2, nearCriticalDays: 5 }); - const noBase = computeProgressReport({ ...ctx, tasks: clone, baseline: null }, { periodWeeks: 2, nearCriticalDays: 5 }); + const withBase = computeProgressReport({ ...ctx, tasks: clone }, { period: { preset: 'last2Weeks' }, nearCriticalDays: 5 }); + const noBase = computeProgressReport({ ...ctx, tasks: clone, baseline: null }, { period: { preset: 'last2Weeks' }, nearCriticalDays: 5 }); near('progress: uur-taak weegt 0,5 wd — baseline aan/uit maakt geen verschil', withBase.summary.actualPct, noBase.summary.actualPct, 0.01); near('progress: gewicht G = 0,5 (A 5 + B 10 + C 8 + G 0,5 + D 3 + F 46 = 72,5)', noBase.summary.actualPct, (9 / 72.5) * 100, 0.2); } @@ -299,34 +304,145 @@ eq('scenario: B rest = 6 wd (10 × 60%)', remainingDays(ctx, byId(B)), 6); // ── Resourcebelasting ──────────────────────────────────────────────────────────────────────────── { - const r = computeResourceLoading(ctx, { onlyOverloaded: false }); + const weekly = { period: { preset: 'project' } as ReportingPeriod, bucket: 'week' as const }; + const r = computeResourceLoading(ctx, { ...weekly, onlyOverloaded: false }); ok('resourceLoading: rijen voor Ploeg 1', r.rows.length > 0 && r.rows.every(x => x.resourceName === 'Ploeg 1')); - ok('resourceLoading: weken chronologisch', r.rows.every((x, i) => i === 0 || x.weekStart > r.rows[i - 1].weekStart)); + ok('resourceLoading: weken chronologisch', r.rows.every((x, i) => i === 0 || x.bucketStart > r.rows[i - 1].bucketStart)); ok('resourceLoading: variance = beschikbaar − gevraagd', r.rows.every(x => Math.abs(x.variance - (x.available - x.required)) < 0.001)); ok('resourceLoading: alleen weken met vraag', r.rows.every(x => x.required > 0)); eq('resourceLoading: A (7–11 sep), B (14–25 sep, rest ná de statusdatum) en D ⇒ 3 weken', r.rows.length, 3); ok('resourceLoading: B en D overlappen ⇒ overbelaste week', r.rows.some(x => x.overloaded)); - eq('resourceLoading: aantal resources', r.counts.resources, 1); - eq('resourceLoading: weken = rijen zonder filter', r.counts.weeks, r.rows.length); - const r2 = computeResourceLoading(ctx, { onlyOverloaded: true }); + eq('resourceLoading: aantal resources = resources in de tabel (zelfde telling als toewijzingen)', r.counts.resources, 1); + eq('resourceLoading: een resource zonder rijen telt niet mee', computeResourceLoading({ ...ctx, resources: [...ctx.resources, { ...ctx.resources[0], id: 'leeg', name: 'Leeg' }] }, { ...weekly, onlyOverloaded: false }).counts.resources, 1); + eq('resourceLoading: weken = rijen zonder filter', r.counts.buckets, r.rows.length); + const r2 = computeResourceLoading(ctx, { ...weekly, onlyOverloaded: true }); ok('resourceLoading: filter houdt alleen overbelaste weken', r2.rows.every(x => x.overloaded)); - eq('resourceLoading: filter verandert de tellingen niet', r2.counts.weeks, r.counts.weeks); + eq('resourceLoading: tellingen gaan over de tabel — met filter tellen alleen overbelaste buckets', r2.counts.buckets, r2.rows.length); + eq('resourceLoading: … en overbelaste buckets blijven gelijk', r2.counts.overloadedBuckets, r.counts.overloadedBuckets); ok('resourceLoading: overbelaste rijen zijn een deelverzameling', r2.rows.length <= r.rows.length); + eq('resourceLoading: project-periode = projectspanne', [r.from, r.to], [projectSpan(ctx.tasks)!.from, projectSpan(ctx.tasks)!.to]); + eq('resourceLoading: project-periode meldt geen ontbrekende statusdatum', computeResourceLoading({ ...ctx, statusDate: undefined }, { ...weekly, onlyOverloaded: false }).statusDateMissing, false); + + // Issue #119: maandaggregatie — één bucket voor september die de drie weken samenneemt. + const m = computeResourceLoading(ctx, { period: { preset: 'project' }, bucket: 'month', onlyOverloaded: false }); + eq('resourceLoading: maandbucket ⇒ één rij (alles in september)', m.rows.map(x => [x.bucketStart, x.bucketEnd]), [['2026-09-01', '2026-09-30']]); + near('resourceLoading: maandsom = som van de weken', m.rows[0].required, r.rows.reduce((n, x) => n + x.required, 0), 0.11); + ok('resourceLoading: maand-capaciteit ≥ som van de weekcapaciteiten binnen de maand', m.rows[0].available >= r.rows.reduce((n, x) => n + x.available, 0) - 0.01); + eq('resourceLoading: overbelaste maand', m.counts.overloadedBuckets, 1); + ok('resourceLoading: piekdag van de maand = hoogste weekpiek', Math.abs(m.rows[0].peakDayLoad - Math.max(...r.rows.map(x => x.peakDayLoad))) < 0.01); + + // Issue #120: een rapportageperiode beperkt de buckets tot de weken die de periode raken. + const w = computeResourceLoading(ctx, { period: { preset: 'nextWeek' }, bucket: 'week', onlyOverloaded: false }); + eq('resourceLoading: "volgende week" vanaf vr 18 sep ⇒ [18 sep, 24 sep]', [w.from, w.to], ['2026-09-18', '2026-09-24']); + eq('resourceLoading: … raakt de weken van 14 en 21 sep', w.rows.map(x => x.bucketStart), ['2026-09-14', '2026-09-21']); + eq('resourceLoading: de weekrij is dezelfde hele kalenderweek als zonder periode', w.rows[0].required, r.rows.find(x => x.bucketStart === '2026-09-14')!.required); + const c = computeResourceLoading(ctx, { period: { preset: 'custom', from: '2026-09-07', to: '2026-09-11' }, bucket: 'week', onlyOverloaded: false }); + eq('resourceLoading: aangepaste periode ⇒ alleen de week van 7 sep', c.rows.map(x => x.bucketStart), ['2026-09-07']); + eq('resourceLoading: zonder statusdatum meldt een relatieve periode dat wél', computeResourceLoading({ ...ctx, statusDate: undefined }, { period: { preset: 'lastWeek' }, bucket: 'week', onlyOverloaded: false }).statusDateMissing, true); } // ── Resourcetoewijzingen ───────────────────────────────────────────────────────────────────────── { - const r = computeResourceAssignments(ctx, { weeks: 0, includeCompleted: false }); + const r = computeResourceAssignments(ctx, { period: { preset: 'project' }, includeCompleted: false }); eq('assignments: voltooide A weggelaten ⇒ B en D', r.rows.map(x => x.taskId).sort(), [B, D].sort()); - eq('assignments: geen venster', r.from, undefined); + eq('assignments: hele project ⇒ venster = projectspanne', [r.from, r.to], [projectSpan(ctx.tasks)!.from, projectSpan(ctx.tasks)!.to]); ok('assignments: gesorteerd op start binnen de resource', r.rows.every((x, i) => i === 0 || r.rows[i - 1].start <= x.start)); eq('assignments: taken zonder resource (C, F, G — E is mijlpaal)', r.counts.unassignedTasks, 3); - const r2 = computeResourceAssignments(ctx, { weeks: 0, includeCompleted: true }); + const r2 = computeResourceAssignments(ctx, { period: { preset: 'project' }, includeCompleted: true }); eq('assignments: met voltooide ⇒ ook A', r2.rows.length, 3); eq('assignments: A voltooid', r2.rows.find(x => x.taskId === A)?.state, 'complete'); - const r3 = computeResourceAssignments(ctx, { weeks: 1, includeCompleted: false }); + const r3 = computeResourceAssignments(ctx, { period: { preset: 'nextWeek' }, includeCompleted: false }); eq('assignments: venster van 1 week vanaf de statusdatum', [r3.from, r3.to], ['2026-09-18', '2026-09-24']); ok('assignments: B (in uitvoering) valt in het venster', r3.rows.some(x => x.taskId === B)); + const r4 = computeResourceAssignments(ctx, { period: { preset: 'custom', from: '2026-10-05', to: '2026-10-09' }, includeCompleted: false }); + ok('assignments: aangepast venster in oktober ⇒ D (30 sep – 2 okt) valt eruit', !r4.rows.some(x => x.taskId === D)); + // Ook bij een aangepast venster stuurt de referentiedag de insluiting van achterstallig werk. + eq('assignments: aangepast venster zonder statusdatum meldt dat wél', computeResourceAssignments({ ...ctx, statusDate: undefined }, { period: { preset: 'custom', from: '2026-10-05', to: '2026-10-09' }, includeCompleted: false }).statusDateMissing, true); + eq('assignments: hele project meldt geen ontbrekende statusdatum', computeResourceAssignments({ ...ctx, statusDate: undefined }, { period: { preset: 'project' }, includeCompleted: false }).statusDateMissing, false); +} + +// ── Rapportageperiode (issue #120) ─────────────────────────────────────────────────────────────── +{ + const ref = '2026-09-10'; // donderdag, het voorbeeld uit het issue + const span = { from: '2026-09-01', to: '2026-12-31' }; + const res = (p: ReportingPeriod) => resolveReportingPeriod(p, ref, span); + eq('period: volgende 4 weken = 10 sep t/m 7 okt (het voorbeeld uit het issue)', res({ preset: 'next4Weeks' }), { from: '2026-09-10', to: '2026-10-07' }); + eq('period: volgende week = 7 dagen inclusief', res({ preset: 'nextWeek' }), { from: '2026-09-10', to: '2026-09-16' }); + eq('period: afgelopen 2 weken eindigt op de referentiedag', res({ preset: 'last2Weeks' }), { from: '2026-08-28', to: '2026-09-10' }); + eq('period: volgende maand = t/m 9 okt', res({ preset: 'nextMonth' }), { from: '2026-09-10', to: '2026-10-09' }); + eq('period: afgelopen maand = vanaf 11 aug', res({ preset: 'lastMonth' }), { from: '2026-08-11', to: '2026-09-10' }); + eq('period: volgende maand vanaf 31 jan klemt op februari', resolveReportingPeriod({ preset: 'nextMonth' }, '2026-01-31', span), { from: '2026-01-31', to: '2026-02-27' }); + eq('period: addCalendarMonths klemt op de maandlengte', formatDate(addCalendarMonths(parseDate('2026-01-31'), 1)), '2026-02-28'); + eq('period: hele project = projectspanne', res({ preset: 'project' }), span); + eq('period: hele project zonder taken = de referentiedag', resolveReportingPeriod({ preset: 'project' }, ref, undefined), { from: ref, to: ref }); + eq('period: aangepast = de eigen datums', res({ preset: 'custom', from: '2026-10-01', to: '2026-10-31' }), { from: '2026-10-01', to: '2026-10-31' }); + eq('period: aangepast zonder datums valt terug op de projectspanne', res({ preset: 'custom' }), span); + eq('period: to < from is ongeldig', isValidReportingPeriod({ preset: 'custom', from: '2026-10-31', to: '2026-10-01' }), false); + eq('period: 2026-02-31 is ongeldig', isValidReportingPeriod({ preset: 'custom', from: '2026-02-31', to: '2026-03-01' }), false); + eq('period: lengte in dagen, inclusief', periodDays({ from: '2026-09-10', to: '2026-10-07' }), 28); + ok('period: alle 12 weekpresets sluiten aan op de referentiedag', ([1, 2, 4, 6, 8, 12] as const).every(n => { + const nx = res({ preset: weeksToPreset(n, 'next') }); const ls = res({ preset: weeksToPreset(n, 'last') }); + return nx.from === ref && periodDays(nx) === 7 * n && ls.to === ref && periodDays(ls) === 7 * n; + })); + + // Migratie van de oude "N weken"-getallen (reportSettings) naar presets. + eq('period: weeksToPreset 3 ⇒ 4 weken, 9 ⇒ 12, 1 ⇒ week', [weeksToPreset(3, 'next'), weeksToPreset(9, 'last'), weeksToPreset(1, 'next')], ['next4Weeks', 'last12Weeks', 'nextWeek']); + const migrated = parseTableReportOptions({ lookAheadWeeks: 3, progressPeriodWeeks: 1, resourceAssignmentWeeks: 0 }); + eq('settings: oude look-ahead 3 weken ⇒ volgende 4 weken', migrated.lookAheadPeriod, { preset: 'next4Weeks' }); + eq('settings: oude voortgang 1 week ⇒ afgelopen week', migrated.progressPeriod, { preset: 'lastWeek' }); + eq('settings: oude toewijzingen 0 ⇒ hele project', migrated.resourceAssignmentPeriod, { preset: 'project' }); + eq('settings: belasting krijgt de defaults', [migrated.resourceLoadPeriod, migrated.resourceLoadBucket], [{ preset: 'project' }, 'week']); + const kept = parseTableReportOptions({ lookAheadWeeks: 3, lookAheadPeriod: { preset: 'nextMonth' }, resourceLoadBucket: 'month' }); + eq('settings: het nieuwe periodeveld wint van het oude getal', kept.lookAheadPeriod, { preset: 'nextMonth' }); + eq('settings: maandaggregatie bewaard', kept.resourceLoadBucket, 'month'); + eq('settings: defaults volgen issue #120 (volgende maand / afgelopen maand)', [DEFAULT_TABLE_REPORT_OPTIONS.lookAheadPeriod, DEFAULT_TABLE_REPORT_OPTIONS.progressPeriod], [{ preset: 'nextMonth' }, { preset: 'lastMonth' }]); + eq('settings: aangepaste periode met omgekeerde datums ⇒ default', parseReportingPeriod({ preset: 'custom', from: '2026-10-31', to: '2026-10-01' }, DEFAULT_TABLE_REPORT_OPTIONS.lookAheadPeriod), { preset: 'nextMonth' }); + eq('settings: aangepaste periode met geldige datums blijft', parseReportingPeriod({ preset: 'custom', from: '2026-10-01', to: '2026-10-31' }, DEFAULT_TABLE_REPORT_OPTIONS.lookAheadPeriod), { preset: 'custom', from: '2026-10-01', to: '2026-10-31' }); + eq('settings: onbekende preset ⇒ default', parseReportingPeriod({ preset: 'nextYear' }, { preset: 'project' }), { preset: 'project' }); + + // Look-ahead en voortgang volgen dezelfde periode-keuze. + const la = computeLookAhead(ctx, { period: { preset: 'nextMonth' }, nearCriticalDays: 5 }); + eq('lookAhead: volgende maand vanaf 18 sep = t/m 17 okt', [la.from, la.to], ['2026-09-18', '2026-10-17']); + const laProject = computeLookAhead(ctx, { period: { preset: 'project' }, nearCriticalDays: 5 }); + ok('lookAhead: hele project ⇒ minstens zoveel open activiteiten als een maand', laProject.rows.length >= la.rows.length); + const pr = computeProgressReport(ctx, { period: { preset: 'next4Weeks' }, nearCriticalDays: 5 }); + eq('progress: periode ná de statusdatum ⇒ vooruitblik tot het periode-einde (niet gespiegeld)', [pr.summary.periodFrom, pr.summary.periodTo, pr.summary.lookAheadTo], ['2026-09-18', '2026-10-15', '2026-10-15']); + const pm = computeProgressReport(ctx, { period: { preset: 'lastMonth' }, nearCriticalDays: 5 }); + eq('progress: afgelopen maand ⇒ 19 aug t/m 18 sep, vooruitblik gespiegeld t/m 19 okt', [pm.summary.periodFrom, pm.summary.periodTo, pm.summary.lookAheadTo], ['2026-08-19', '2026-09-18', '2026-10-19']); + ok('progress: A (voltooid 11 sep) valt in de afgelopen maand', pm.completedInPeriod.some(x => x.taskId === A)); + const pc = computeProgressReport(ctx, { period: { preset: 'custom', from: '2026-09-14', to: '2026-09-15' }, nearCriticalDays: 5 }); + ok('progress: aangepaste periode 14–15 sep ⇒ A (klaar 11 sep) valt erbuiten', !pc.completedInPeriod.some(x => x.taskId === A)); + // Reviewbevinding ronde 2: een aangepaste periode in het verleden wordt NIET gespiegeld — een + // venster in 2020 zegt niets over de komende periode; de vooruitkijksectie blijft dan leeg. + const p2020 = computeProgressReport(ctx, { period: { preset: 'custom', from: '2020-01-01', to: '2020-12-31' }, nearCriticalDays: 5 }); + eq('progress: aangepaste periode in 2020 ⇒ vooruitblik t/m het periode-einde, niet een jaar vooruit', p2020.summary.lookAheadTo, '2020-12-31'); + eq('progress: … en de sectie "start in de komende periode" is leeg', p2020.startingNext.length, 0); + eq('progress: "afgelopen …"-preset spiegelt wél', computeProgressReport(ctx, { period: { preset: 'lastWeek' }, nearCriticalDays: 5 }).summary.lookAheadTo, '2026-09-25'); + + // Reviewbevinding ronde 3: een venster dat helemaal in het verleden ligt is een terugblik en sleept + // de actuele achterstand (overdue / had-moeten-starten) niet mee — look-ahead én toewijzingen. + { + const clone = ctx.tasks.map(t => ({ ...t, time: { ...t.time } })); + const g = clone.find(t => t.id === G)!; + g.time.earlyStart = '2026-09-08'; g.time.earlyFinish = '2026-09-09'; // achterstallig t.o.v. 18 sep + const past = { preset: 'custom' as const, from: '2020-01-01', to: '2020-12-31' }; + eq('lookAhead: venster in 2020 ⇒ geen rijen, ook geen achterstand', computeLookAhead({ ...ctx, tasks: clone }, { period: past, nearCriticalDays: 5 }).rows.length, 0); + ok('lookAhead: venster dat de statusdatum raakt ⇒ achterstand wél', computeLookAhead({ ...ctx, tasks: clone }, { period: { preset: 'custom', from: '2026-09-18', to: '2026-09-18' }, nearCriticalDays: 5 }).rows.some(x => x.taskId === G && x.status === 'overdue')); + eq('assignments: venster in 2020 ⇒ geen rijen', computeResourceAssignments({ ...ctx, tasks: clone }, { period: past, includeCompleted: false }).rows.length, 0); + } + + // Reviewbevinding ronde 3: het maandlabel is Gregoriaans met Latijnse cijfers in élke taal — + // zonder de unicode-extensies gaf `fa` "شهریور ۱۴۰۵" naast een ondertitel met 2026. + { + const persianDigits = /[\u06F0-\u06F9\u0660-\u0669]/; + for (const loc of ['nl', 'en', 'fa', 'ar', 'ja', 'zh', 'ko', 'de', 'tr', 'pl']) { + const label = makeMonthLabeler(loc)('2026-09-01'); + ok(`monthLabel: ${loc} bevat het Gregoriaanse jaar 2026 ("${label}")`, label.includes('2026')); + ok(`monthLabel: ${loc} gebruikt Latijnse cijfers`, !persianDigits.test(label)); + } + ok('monthLabel: fa is niet de Solar-Hijri-kalender', !makeMonthLabeler('fa')('2026-09-01').includes('1405')); + eq('monthLabel: onbekende locale valt terug op Engels', makeMonthLabeler('xx-YY')('2026-09-01'), 'Sep 2026'); + } } // ── WBS-samenvatting ─────────────────────────────────────────────────────────────────────────────