Skip to content

Commit

Permalink
Merge pull request #17316 from opf/feature/59824-export-cost-report-a…
Browse files Browse the repository at this point in the history
…s-timesheet-pdf

PDF export for time sheets
  • Loading branch information
as-op authored Dec 11, 2024
2 parents 2991e18 + e276aee commit 3fc1250
Show file tree
Hide file tree
Showing 21 changed files with 1,113 additions and 90 deletions.
17 changes: 16 additions & 1 deletion app/models/work_package/pdf_export/common/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ module WorkPackage::PDFExport::Common::Common

private

def get_pdf(_language)
def get_pdf
::WorkPackage::PDFExport::Common::View.new(current_language)
end

Expand Down Expand Up @@ -164,6 +164,13 @@ def draw_text_multiline_part(line, text_style, x_position, y_position)
measure_text_height(line, text_style)
end

def ellipsis_if_longer(text, available_width, text_style)
title_text_width = measure_text_width(text, text_style)
return text if title_text_width < available_width

truncate_ellipsis(text, available_width, text_style)
end

def truncate_ellipsis(text, available_width, text_style)
line = text.dup
while line.present? && (measure_text_width("#{line}...", text_style) > available_width)
Expand Down Expand Up @@ -285,4 +292,12 @@ def footer_date
def current_page_nr
pdf.page_number + @page_count - (with_cover? ? 1 : 0)
end

def write_horizontal_line(y_position, height, color)
draw_horizontal_line(
y_position,
pdf.bounds.left, pdf.bounds.right,
height, color
)
end
end
2 changes: 1 addition & 1 deletion app/models/work_package/pdf_export/document_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def initialize(work_package, _options = {})
end

def setup_page!
self.pdf = get_pdf(current_language)
self.pdf = get_pdf
end

def export!
Expand Down
52 changes: 43 additions & 9 deletions app/models/work_package/pdf_export/export/cover.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,34 @@ def write_cover_page!
def write_cover_hero
max_width = pdf.bounds.width - styles.cover_hero_padding[:right_padding]
float_top = write_background_image
float_top -= write_hero_title(float_top, max_width) if project
float_top -= write_hero_title(float_top, max_width)
float_top -= write_hero_heading(float_top, max_width)
write_hero_subheading(float_top, max_width) unless User.current.nil?
float_top -= write_hero_dates(float_top, max_width)
write_hero_subheading(float_top, max_width)
end

def available_title_height(current_y)
current_y -
styles.cover_hero_title_max_height -
styles.cover_hero_title_spacing -
styles.cover_hero_heading_spacing -
styles.cover_hero_subheading_max_height
cover_hero_title_max_height -
cover_hero_heading_max_height -
cover_hero_dates_max_height -
cover_hero_subheading_max_height
end

def cover_hero_title_max_height
cover_page_title&.then { styles.cover_hero_title_max_height + styles.cover_hero_title_spacing } || 0
end

def cover_hero_heading_max_height
cover_page_heading&.then { styles.cover_hero_heading_spacing } || 0
end

def cover_hero_dates_max_height
cover_page_dates&.then { styles.cover_hero_dates_max_height } || 0
end

def cover_hero_subheading_max_height
cover_page_title&.then { styles.cover_hero_title_max_height + styles.cover_hero_title_spacing } || 0
end

def write_cover_hr
Expand Down Expand Up @@ -81,27 +98,44 @@ def validate_cover_text_color
end

def write_hero_title(top, width)
return 0 if cover_page_title.blank?

write_hero_text(
top:, width:,
text: project.name,
text: cover_page_title,
text_style: styles.cover_hero_title,
height: styles.cover_hero_title_max_height
) + styles.cover_hero_title_spacing
end

def write_hero_heading(top, width)
return 0 if cover_page_heading.blank?

write_hero_text(
top:, width:,
text: heading,
text: cover_page_heading,
text_style: styles.cover_hero_heading,
height: available_title_height(top)
) + styles.cover_hero_heading_spacing
end

def write_hero_dates(top, width)
return 0 if cover_page_dates.blank?

write_hero_text(
top:, width:,
text: cover_page_dates,
text_style: styles.cover_hero_dates,
height: styles.cover_hero_dates_max_height
) + styles.cover_hero_dates_spacing
end

def write_hero_subheading(top, width)
return 0 if cover_page_subheading.blank?

write_hero_text(
top:, width:,
text: User.current.name,
text: cover_page_subheading,
text_style: styles.cover_hero_subheading,
height: styles.cover_hero_subheading_max_height
)
Expand Down
36 changes: 36 additions & 0 deletions app/models/work_package/pdf_export/export/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,42 @@
}
]
},
"dates" : {
"title" : "The dates block in the hero",
"type" : "object",
"x-example" : {
"heading" : {
"spacing" : 10,
"max_height" : 20,
"size" : 32,
"color" : "414d5f",
"styles" : [
"bold"
]
}
},
"properties" : {
"max_height" : {
"title" : "Maximum height of the block",
"examples" : [
30
],
"$ref" : "#/$defs/measurement"
},
"spacing" : {
"title" : "Minimum spacing between dates and subheading",
"examples" : [
10
],
"$ref" : "#/$defs/measurement"
}
},
"allOf" : [
{
"$ref" : "#/$defs/font"
}
]
},
"subheading" : {
"title" : "The last block in the hero",
"type" : "object",
Expand Down
9 changes: 7 additions & 2 deletions app/models/work_package/pdf_export/export/standard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,14 @@ cover:
styles:
- bold
size: 16
dates:
spacing: 4
max_height: 16
color: '414d5f'
size: 10
styles:
- bold
subheading:
max_height: 30
color: '414d5f'
styles:
- italic
size: 10
12 changes: 12 additions & 0 deletions app/models/work_package/pdf_export/export/style.rb
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,18 @@ def cover_hero_heading_spacing
resolve_pt(@styles.dig(:cover, :hero, :heading, :spacing), 0)
end

def cover_hero_dates
resolve_font(@styles.dig(:cover, :hero, :dates))
end

def cover_hero_dates_spacing
resolve_pt(@styles.dig(:cover, :hero, :dates, :spacing), 0)
end

def cover_hero_dates_max_height
resolve_pt(@styles.dig(:cover, :hero, :dates, :max_height), 0)
end

def cover_hero_subheading
resolve_font(@styles.dig(:cover, :hero, :subheading))
end
Expand Down
18 changes: 17 additions & 1 deletion app/models/work_package/pdf_export/work_package_list_to_pdf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def export!
private

def setup_page!
self.pdf = get_pdf(current_language)
self.pdf = get_pdf

configure_page_size!(wants_report? ? :portrait : :landscape)
end
Expand All @@ -111,6 +111,22 @@ def with_cover?
wants_report?
end

def cover_page_title
project&.name
end

def cover_page_heading
heading
end

def cover_page_subheading
User.current&.name
end

def cover_page_dates
nil
end

def render_work_packages_pdfs(work_packages, filename)
write_cover_page! if with_cover?
if wants_gantt?
Expand Down
2 changes: 1 addition & 1 deletion app/models/work_package/pdf_export/work_package_to_pdf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def export!
end

def setup_page!
self.pdf = get_pdf(current_language)
self.pdf = get_pdf
@page_count = 0
configure_page_size!(:portrait)
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* -- copyright
* OpenProject is an open source project management software.
* Copyright (C) the OpenProject GmbH
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 3.
*
* OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
* Copyright (C) 2006-2013 Jean-Philippe Lang
* Copyright (C) 2010-2013 the ChiliProject Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* See COPYRIGHT and LICENSE files for more details.
* ++
*/

import { Controller } from '@hotwired/stimulus';
import * as Turbo from '@hotwired/turbo';
import { HttpErrorResponse } from '@angular/common/http';

export default class ExportController extends Controller<HTMLLinkElement> {
static values = {
jobStatusDialogUrl: String,
};

declare jobStatusDialogUrlValue:string;

jobModalUrl(job_id:string):string {
return this.jobStatusDialogUrlValue.replace('_job_uuid_', job_id);
}

async showJobModal(job_id:string) {
const response = await fetch(this.jobModalUrl(job_id), {
method: 'GET',
headers: { Accept: 'text/vnd.turbo-stream.html' },
});
if (response.ok) {
Turbo.renderStreamMessage(await response.text());
} else {
throw new Error(response.statusText || 'Invalid response from server');
}
}

async requestExport(exportURL:string):Promise<string> {
const response = await fetch(exportURL, {
method: 'GET',
headers: { Accept: 'application/json' },
credentials: 'same-origin',
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json() as { job_id:string };
if (!result.job_id) {
throw new Error('Invalid response from server');
}
return result.job_id;
}

get href() {
return this.element.href;
}

download(evt:CustomEvent) {
evt.preventDefault(); // Don't follow the href
this.requestExport(this.href)
.then((job_id) => this.showJobModal(job_id))
.catch((error:HttpErrorResponse) => this.handleError(error));
}

private handleError(error:HttpErrorResponse) {
void window.OpenProject.getPluginContext().then((pluginContext) => {
pluginContext.services.notifications.addError(error);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,39 @@

if show_export_button?
header.with_action_button(scheme: :default,
aria: { label: t(:export_to_excel)},
aria: { label: t(:export_to_excel) },
title: t(:export_to_excel),
mobile_icon: "op-file-xls-descriptions",
mobile_label: t(:export_to_excel),
tag: :a,
href: url_for({ controller: "cost_reports" , action: :index, format: 'xls', project_id: @project })
href: url_for({ controller: "cost_reports", action: :index, format: 'xls', project_id: @project }),
data: {
controller: "costs--export",
"application-target": "dynamic",
"costs--export-job-status-dialog-url-value": job_status_dialog_path('_job_uuid_'),
action: "click->costs--export#download"
}
) do |button|
button.with_leading_visual_icon(icon: "op-file-xls-descriptions")
t(:export_to_excel)
end
header.with_action_button(scheme: :default,
aria: { label: t("export.timesheet.button") },
title: t("export.timesheet.button"),
mobile_icon: "op-file-xls-descriptions",
mobile_label: t("export.timesheet.button"),
tag: :a,
href: url_for({ controller: "cost_reports", action: :index, format: 'pdf', project_id: @project }),
data: {
controller: "costs--export",
"application-target": "dynamic",
"costs--export-job-status-dialog-url-value": job_status_dialog_path('_job_uuid_'),
action: "click->costs--export#download"
}
) do |button|
button.with_leading_visual_icon(icon: "op-file-xls-descriptions")
t("export.timesheet.button")
end
call_hook(:view_cost_report_toolbar)
end
end
Expand Down
Loading

0 comments on commit 3fc1250

Please sign in to comment.