Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ schermgroepering: gelijknamigen krijgen `#n`, naamlozen een surrogaat), daaronde
start; relaties staan bij dit type uit omdat een taak onder meerdere banden kan staan; optie "blad
per resource" = `PrintOptions.pageBreakBeforeGroups`
→ `RenderReportResult.forcedBreakOffsets` → `forcedBreakOffsetsPx` in `tileLayout`, waar een gedwongen
positie zonder vulgraaddrempel wint; `isGanttReportType()` bundelt beide Gantt-achtige typen), het
positie zonder vulgraaddrempel wint; `isGanttReportType()` bundelt beide Gantt-achtige typen; de voet
met legenda is sinds #113 net als de kop een herhaalbaar blok — `RenderReportResult.footerHeight` →
`repeatFooterHeightPx`/`repeatFooter`, instelling `repeatFooter` standaard aan; let op: de preview
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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 10 additions & 2 deletions public/docs/en/gids-rapporten-printen.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ A full, formatted printout of the Gantt bars — this is the only report type wi
independently of the zoom level above.
- **Repeat header on each page** — on by default; keeps the report header visible on every printed
page instead of only the first.
- **Repeat footer on each page** — on by default; puts the footer with project name, print date and
legend at the bottom of every page instead of only the last, so a sheet handed out on its own
carries its own legend — also when the timeline is spread over several pages side by side. If
the whole print fits on one page, the footer simply stays below the last row. The strip costs
space on every page: roughly two rows fewer per sheet, so occasionally one page more, and a
resource that just fitted on one sheet with *Each resource on a new page* can spill onto two.
Off = the footer on the last page only. The page number ("3 / 7") always sits in the bottom
margin, separate from the footer; the footer itself no longer carries a page number.
- **Timeline over** — spreads the Gantt timeline across 1 to 8 pages side by side; only available
with auto-fit on. Choose more pages when you want a less compressed timeline without reducing
the table text.
Expand Down Expand Up @@ -102,8 +110,8 @@ resource yourself first. Bands are per resource, not per name: two resources tha
name each get their own band (*Jan #1*, *Jan #2*), and a resource without a name gets a sequence
number. The summary block counts the resources, the assignments and the tasks without a resource —
that last count includes milestones and hammocks, because they are drawn here (the *Resource
assignments* table report counts real activities only). The footer with the legend and print date
sits, as in every multi-page print, on the last sheet. If there are no assignments yet, the preview
assignments* table report counts real activities only). With *Repeat footer on each page* (on by
default) every sheet you hand out carries its own legend. If there are no assignments yet, the preview
says so instead of showing an empty page; assigning happens on the **Resources** tab (see
[Resources & histogram](docs://gids-resources-histogram)).

Expand Down
12 changes: 10 additions & 2 deletions public/docs/nl/gids-rapporten-printen.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ instellingenblok:
van het zoomniveau hierboven.
- **Kop op elke pagina herhalen** — standaard aan; houdt de rapportkop zichtbaar op elke afgedrukte
pagina in plaats van alleen de eerste.
- **Voet op elke pagina herhalen** — standaard aan; zet de voet met projectnaam, afdrukdatum en
legenda onderaan elke pagina in plaats van alleen de laatste, zodat een los uitgedeeld vel zijn
eigen legenda heeft — ook wanneer de tijdlijn over meerdere pagina's naast elkaar staat. Past de
hele afdruk op één pagina, dan blijft de voet gewoon onder de laatste rij staan. De strook kost
per pagina ruimte: ongeveer twee rijen minder per vel, dus soms een pagina meer, en een resource
die met *Elke resource op een nieuwe pagina* nét op één vel paste kan daardoor over twee vellen
gaan. Uit = de voet alleen op de laatste pagina. Het paginanummer ("3 / 7") staat altijd in de
ondermarge, los van de voet; de voet zelf draagt geen paginanummer meer.
- **Tijdlijn over** — verdeelt de Gantt-tijdlijn over 1 tot 8 pagina's naast elkaar; alleen
beschikbaar met auto-fit aan. Kies meer pagina's wanneer je de tijdas minder wilt comprimeren
zonder de tabeltekst kleiner te maken.
Expand Down Expand Up @@ -105,8 +113,8 @@ niet eerst zelf op resource te groeperen. Banden zijn per resource, niet per naa
toevallig hetzelfde heten krijgen elk een eigen band (*Jan #1*, *Jan #2*), en een resource zonder
naam een volgnummer. Het overzichtsblok telt de resources, de toewijzingen en de taken zonder
resource — die laatste telling omvat ook mijlpalen en hammocks, want die worden hier getekend (het
tabelrapport *Resourcetoewijzingen* telt alleen echte activiteiten). De voet met legenda en
afdrukdatum staat, zoals bij elke meerpagina-afdruk, op het laatste vel. Zijn er nog geen
tabelrapport *Resourcetoewijzingen* telt alleen echte activiteiten). Met *Voet op elke pagina
herhalen* (standaard aan) heeft elk uitgedeeld vel zijn eigen legenda. Zijn er nog geen
toewijzingen, dan zegt het voorbeeld dat in plaats van een lege pagina te tonen; toewijzen gebeurt
op het tabblad **Resources** (zie [Resources & histogram](docs://gids-resources-histogram)).

Expand Down
57 changes: 37 additions & 20 deletions src/components/panels/ReportPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { computePreviewRasterLimits } from '@/services/print/previewSafety';
import { getLocalizedMonths, getLocalizedMonthsShort } from '@/i18n/dateFormat';
import { projectFileBase } from '@/utils/documents';
import { computeHighResScale } from '@/utils/miniPdf';
import { paginateCanvasToPdfBytes } from '@/services/print/paginate';
import { computeTileLayout } from '@/services/print/tileLayout';
import { paginateCanvasToPdfBytes, type PaginateOptions } from '@/services/print/paginate';
import { computeTileLayout, footerLayoutWidthFor } from '@/services/print/tileLayout';
import { ensureInterLoaded, getInterFontBytes, getArabicFontBytes } from '@/services/pdf/fontLoader';
import { RTL_LOCALES, type Locale } from '@/i18n/config';
import { Select } from '@/components/common/Select';
Expand Down Expand Up @@ -305,6 +305,9 @@ export function ReportPanel() {
// Bewust géén veld in `PrintOptions`: de kopherhaling is puur een pagineerder-zaak (raster:
// hoogte in px; vector: boolean), niet iets dat de render-zoom raakt.
const [repeatHeader, setRepeatHeader] = useState(DEFAULT_REPORT_SETTINGS.repeatHeader);
// Voet (projectnaam, afdrukdatum, legenda) op elke pagina — issue #113: een blad per persoon
// zonder legenda is onleesbaar. Zelfde pagineerder-zaak als de kop, dus óók geen PrintOptions-veld.
const [repeatFooter, setRepeatFooter] = useState(DEFAULT_REPORT_SETTINGS.repeatFooter);
// Issue #25 punt 5 — smeert de tijdlijn uit over N paginabreedtes (1 = oud gedrag, geen
// verrassing voor bestaande gebruikers). Alleen zinvol in fit-width-modus; daarom `disabled`
// wanneer `autoFit` uit staat (dan tegelt de export in 'actual'-modus toch al horizontaal).
Expand Down Expand Up @@ -387,6 +390,7 @@ export function ReportPanel() {
setPaperSize(s.paperSize);
setOrientation(s.orientation);
setRepeatHeader(s.repeatHeader);
setRepeatFooter(s.repeatFooter);
setTimelineColumns(s.timelineColumns);
setReportFontScale(s.reportFontScale);
setStatusLine(s.statusLine);
Expand Down Expand Up @@ -427,14 +431,14 @@ export function ReportPanel() {
void saveReportSettings({
reportType, showCritical, showFloat, showDeps, showWeekends, compressNonWorkdays: reportCompressNonWorkdays, showLegend,
showTaskNames, showCompletion, truncateTaskNames, taskNameColumnWidth, showBaselineOverlay, autoFit, customZoom,
paperSize, orientation, repeatHeader, timelineColumns, reportFontScale, statusLine, followView, previewQuality,
paperSize, orientation, repeatHeader, repeatFooter, timelineColumns, reportFontScale, statusLine, followView, previewQuality,
tableReports: tableOptions,
resourceGantt: resourceGanttOptions,
}).catch(() => {});
}, [reportType, showCritical, showFloat, showDeps, showWeekends, reportCompressNonWorkdays, showLegend, showTaskNames,
showCompletion, truncateTaskNames, taskNameColumnWidth, showBaselineOverlay, autoFit, customZoom, paperSize,
orientation, repeatHeader, timelineColumns, reportFontScale, statusLine, followView, previewQuality, tableOptions,
resourceGanttOptions]);
orientation, repeatHeader, repeatFooter, timelineColumns, reportFontScale, statusLine, followView, previewQuality,
tableOptions, resourceGanttOptions]);

// Resourcediagram (issue #113): dezelfde Gantt-render, maar de rijen komen uit de pure rekenmodule
// (per resource-identiteit een band, daaronder zijn taken) en niet van het scherm.
Expand Down Expand Up @@ -548,8 +552,6 @@ export function ReportPanel() {
duration: t('tableHeaders.duration'),
completion: t('tableHeaders.completion', { defaultValue: 'Volt.' }),
},
page: t('page', { defaultValue: 'Pagina' }),
of: t('of', { defaultValue: 'van' }),
today: t('today', { defaultValue: 'Vandaag' }),
statusDate: t('statusDateLabel', { defaultValue: 'Statusdatum' }),
progressDate: t('progressDateLabel', { defaultValue: 'Voortgangsdatum' }),
Expand Down Expand Up @@ -647,7 +649,7 @@ export function ReportPanel() {
const renderPreview = () => {
if (cancelled) return;
const {
width: logicalWidth, height: logicalHeight, tableWidth, headerHeight, breakOffsets, forcedBreakOffsets,
width: logicalWidth, height: logicalHeight, tableWidth, headerHeight, footerHeight, breakOffsets, forcedBreakOffsets,
} = measurePrintReport(tasks, sequences, calendar, projectName, options);
const lowerPaper = options.paperSize.toLowerCase() as 'a4' | 'a3' | 'a2' | 'a1';
const cssPageWidth = previewCssWidth;
Expand All @@ -665,6 +667,7 @@ export function ReportPanel() {
// Kop herhalen per pagina (issue #25 punt 1): de hoogte komt uit de render zelf; 0 = niet
// herhalen (oud gedrag). De raster-tak wil px, de vector-tak een boolean.
repeatHeaderHeightPx: repeatHeader ? headerHeight : 0,
repeatFooterHeightPx: repeatFooter ? footerHeight : 0,
timelineColumns: options.timelineColumns,
// Rij-bewuste paginering (issue #110): preview en export delen dezelfde breekposities;
// het resourcediagram (issue #113) ook zijn gedwongen overgangen per resource.
Expand All @@ -673,6 +676,9 @@ export function ReportPanel() {
supersample: previewLimits.pageSupersample,
};
const layout = computeTileLayout(tileOptions);
// De herhaalde voet wordt binnen één paginabreedte gelegd (meerdere kolommen ⇒ compleet op elk
// vel); zonder herhaling blijft de render exact de oude (voet over de volle canvasbreedte).
const pageOptions: PrintOptions = { ...options, footerLayoutWidth: footerLayoutWidthFor(layout) };
const total = layout.rows * layout.cols;
const root = previewViewportRef.current;
const anchor = root ? capturePreviewScrollAnchor(root) : { index: 0, offset: 0, scrollTop: 0 };
Expand Down Expand Up @@ -752,7 +758,7 @@ export function ReportPanel() {
const canvas = document.createElement('canvas');
let pendingObjectUrl: string | undefined;
try {
renderPrintPreviewPage(canvas, tasks, sequences, calendar, projectName, options, {
renderPrintPreviewPage(canvas, tasks, sequences, calendar, projectName, pageOptions, {
layout,
pageIndex: index,
rasterWidth: previewLimits.pageRasterWidth,
Expand Down Expand Up @@ -828,7 +834,7 @@ export function ReportPanel() {
// toevoegen zou iedere preview-state-update opnieuw laten rasteren.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reportSettingsHydrated, isGanttLike, tasks, sequences, calendar, projectName, previewOptionsSignature,
repeatHeader, previewCssWidth, previewQuality, replacePreviewPages]);
repeatHeader, repeatFooter, previewCssWidth, previewQuality, replacePreviewPages]);

// Eén stabiele observer per layout. Een nieuwe afbeelding verandert zijn dependencies niet en kan
// dus geen observer-rebuild/ping-pong veroorzaken. De queue dedupliceert callbacks.
Expand Down Expand Up @@ -919,20 +925,25 @@ export function ReportPanel() {
const exportRaster = (): Uint8Array => {
const exportCanvas = document.createElement('canvas');
const {
width: logicalWidth, height: logicalHeight, tableWidth, headerHeight, breakOffsets, forcedBreakOffsets,
width: logicalWidth, height: logicalHeight, tableWidth, headerHeight, footerHeight, breakOffsets, forcedBreakOffsets,
} = renderPrintCanvas(exportCanvas, tasks, sequences, calendar, projectName, options, 1);
const exportScale = computeHighResScale(logicalWidth, logicalHeight);
renderPrintCanvas(exportCanvas, tasks, sequences, calendar, projectName, options, exportScale);
return paginateCanvasToPdfBytes(exportCanvas, {
const rasterTile: PaginateOptions = {
paperSize: lowerPaper, orientation, mode,
logicalWidth, logicalHeight, frozenColumnWidthPx: tableWidth,
// Zelfde kopherhaling (px) en tijdlijn-spreiding als de preview en de vector-tak, zodat de
// raster-terugval WYSIWYG gelijk is aan beide (issue #25 punt 1 + 5).
// Zelfde kop-/voetherhaling (px) en tijdlijn-spreiding als de preview en de vector-tak,
// zodat de raster-terugval WYSIWYG gelijk is aan beide (issue #25 punt 1 + 5, #113).
repeatHeaderHeightPx: repeatHeader ? headerHeight : 0,
repeatFooterHeightPx: repeatFooter ? footerHeight : 0,
timelineColumns,
breakOffsetsPx: breakOffsets,
forcedBreakOffsetsPx: forcedBreakOffsets,
});
};
// De high-res render legt de voet binnen één paginabreedte (zie `footerLayoutWidth`) — alleen
// wanneer hij herhaald wordt; anders is dit letterlijk de oude render.
const exportScale = computeHighResScale(logicalWidth, logicalHeight);
renderPrintCanvas(exportCanvas, tasks, sequences, calendar, projectName,
{ ...options, footerLayoutWidth: footerLayoutWidthFor(computeTileLayout(rasterTile)) }, exportScale);
return paginateCanvasToPdfBytes(exportCanvas, rasterTile);
};

// Vector-tak (fase 2): échte vector-PDF met selecteerbare tekst + ingebedde Inter. Bij een fout
Expand All @@ -948,14 +959,16 @@ export function ReportPanel() {
getArabicFontBytes(700),
]);
pdfBytes = await paginateVectorToPdfBytes(
(make) => renderReport(make, tasks, sequences, calendar, projectName, options),
(make, footerLayoutWidth) => renderReport(make, tasks, sequences, calendar, projectName, { ...options, footerLayoutWidth }),
{
paperSize: lowerPaper,
orientation,
mode,
baseDir: exportBaseDir,
// Kop per pagina herhalen (issue #25 punt 1) + tijdlijn over N pagina's (punt 5).
// Kop per pagina herhalen (issue #25 punt 1) + tijdlijn over N pagina's (punt 5); voet
// per pagina (issue #113).
repeatHeader,
repeatFooter,
timelineColumns,
},
{ regular, bold },
Expand Down Expand Up @@ -1061,7 +1074,7 @@ export function ReportPanel() {

await writePdf(tablePdfBytes, `${fileBase}-${suffix}.pdf`);
}, [reportType, isGanttLike, projectName, fileBase, tasks, sequences, calendar, options, paperSize, orientation,
autoFit, repeatHeader, timelineColumns, writePdf, t, dd, milestoneRows, varianceResult, tableSpec]);
autoFit, repeatHeader, repeatFooter, timelineColumns, writePdf, t, dd, milestoneRows, varianceResult, tableSpec]);

// K7-guard: een stale planning eerst doorrekenen. NIET meteen daarna exporteren — `runExport`
// leest `tasks`/`options`/`tableSpec` uit de closure van de HUIDIGE render, en die kent de
Expand Down Expand Up @@ -1433,6 +1446,10 @@ export function ReportPanel() {
<input type="checkbox" checked={repeatHeader} onChange={e => setRepeatHeader(e.target.checked)} className="accent-accent flex-shrink-0" />
<span className="min-w-0">{t('repeatHeader')}</span>
</label>
<label className="flex items-center gap-2 min-w-0">
<input type="checkbox" checked={repeatFooter} onChange={e => setRepeatFooter(e.target.checked)} className="accent-accent flex-shrink-0" data-ops-report-repeat-footer />
<span className="min-w-0">{t('repeatFooter')}</span>
</label>

<label className="flex items-center gap-2 mt-1 min-w-0">
<input type="checkbox" checked={showTaskNames} onChange={e => setShowTaskNames(e.target.checked)} className="accent-accent flex-shrink-0" />
Expand Down
3 changes: 1 addition & 2 deletions src/i18n/locales/ar/report.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"zoom": "تكبير:",
"previewQuality": { "label": "جودة المعاينة", "standard": "قياسي", "high": "عالية", "maximum": "قصوى" },
"repeatHeader": "تكرار الرأس في كل صفحة",
"repeatFooter": "تكرار التذييل في كل صفحة",
"reportFontScaleLabel": "حجم الخط:",
"timelineColumnsLabel": "الجدول الزمني عبر:",
"timelineColumnsHint": "فقط مع الملاءمة التلقائية",
Expand All @@ -41,8 +42,6 @@
"exportPDF": "تصدير PDF",
"noTasks": "لا توجد مهام لعرضها",
"printed": "طبع في:",
"page": "صفحة",
"of": "من",
"today": "اليوم",
"previewPageLimit_zero": "… و{{count}} صفحة أخرى — صدّر للحصول على المستند الكامل",
"previewPageLimit_one": "… وصفحة واحدة أخرى — صدّر للحصول على المستند الكامل",
Expand Down
3 changes: 1 addition & 2 deletions src/i18n/locales/de/report.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"zoom": "Zoom:",
"previewQuality": { "label": "Vorschauqualität", "standard": "Standard", "high": "Hoch", "maximum": "Maximal" },
"repeatHeader": "Kopfzeile auf jeder Seite wiederholen",
"repeatFooter": "Fußzeile auf jeder Seite wiederholen",
"reportFontScaleLabel": "Schriftgröße:",
"timelineColumnsLabel": "Zeitleiste über:",
"timelineColumnsHint": "Nur bei automatischer Anpassung",
Expand All @@ -37,8 +38,6 @@
"exportPDF": "PDF exportieren",
"noTasks": "Keine Aufgaben vorhanden",
"printed": "Gedruckt:",
"page": "Seite",
"of": "von",
"today": "Heute",
"previewPageLimit_one": "… und {{count}} weitere Seite — für das vollständige Dokument exportieren",
"previewPageLimit_other": "… und {{count}} weitere Seiten — für das vollständige Dokument exportieren",
Expand Down
3 changes: 1 addition & 2 deletions src/i18n/locales/en/report.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"zoom": "Zoom:",
"previewQuality": { "label": "Preview quality", "standard": "Standard", "high": "High", "maximum": "Maximum" },
"repeatHeader": "Repeat header on each page",
"repeatFooter": "Repeat footer on each page",
"reportFontScaleLabel": "Font size:",
"timelineColumnsLabel": "Timeline over:",
"timelineColumnsHint": "Only with auto-fit",
Expand All @@ -37,8 +38,6 @@
"exportPDF": "Export PDF",
"noTasks": "No tasks to display",
"printed": "Printed:",
"page": "Page",
"of": "of",
"today": "Today",
"previewPageLimit_one": "… and {{count}} more page — export for the full document",
"previewPageLimit_other": "… and {{count}} more pages — export for the full document",
Expand Down
Loading