diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 9145900d8..50f32288e 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -273,9 +273,15 @@ export default { return null } }, + locale () { + return Intl.DateTimeFormat().resolvedOptions()?.locale + }, formattedDate () { if (this.field.type === 'date' && this.modelValue) { - return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(new Date(this.modelValue)) + return this.formatDate( + new Date(this.modelValue), + this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY') + ) } else { return '' } @@ -315,6 +321,36 @@ export default { } }, methods: { + formatDate (date, format) { + const monthFormats = { + M: 'numeric', + MM: '2-digit', + MMM: 'short', + MMMM: 'long' + } + + const dayFormats = { + D: 'numeric', + DD: '2-digit' + } + + const yearFormats = { + YYYY: 'numeric', + YY: '2-digit' + } + + const parts = new Intl.DateTimeFormat([], { + day: dayFormats[format.match(/D+/)], + month: monthFormats[format.match(/M+/)], + year: yearFormats[format.match(/Y+/)], + timeZone: 'UTC' + }).formatToParts(date) + + return format + .replace(/D+/, parts.find((p) => p.type === 'day').value) + .replace(/M+/, parts.find((p) => p.type === 'month').value) + .replace(/Y+/, parts.find((p) => p.type === 'year').value) + }, updateMultipleSelectValue (value) { if (this.modelValue?.includes(value)) { const newValue = [...this.modelValue] diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 351bec90f..c0be64f2d 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -474,6 +474,12 @@ export default { field.options = [{ value: '', uuid: v4() }] } + if (type === 'date') { + field.preferences = { + format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY' + } + } + this.drawField = field this.drawOption = null }, @@ -650,6 +656,12 @@ export default { field.options = [{ value: '', uuid: v4() }] } + if (field.type === 'date') { + field.preferences = { + format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY' + } + } + const fieldArea = { x: (area.x - 6) / area.maskW, y: area.y / area.maskH, diff --git a/app/javascript/template_builder/field.vue b/app/javascript/template_builder/field.vue index c5285095d..3fb00e76c 100644 --- a/app/javascript/template_builder/field.vue +++ b/app/javascript/template_builder/field.vue @@ -111,6 +111,33 @@ Default value +
+ + +
  • p.type === 'day').value) + .replace(/M+/, parts.find((p) => p.type === 'month').value) + .replace(/Y+/, parts.find((p) => p.type === 'year').value) + }, copyToAllPages (field) { const areaString = JSON.stringify(field.areas[0]) diff --git a/app/javascript/template_builder/fields.vue b/app/javascript/template_builder/fields.vue index c4e692fd1..b3f5e91c5 100644 --- a/app/javascript/template_builder/fields.vue +++ b/app/javascript/template_builder/fields.vue @@ -261,6 +261,12 @@ export default { field.options = [{ value: '', uuid: v4() }] } + if (type === 'date') { + field.preferences = { + format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY' + } + } + this.fields.push(field) if (['signature', 'initials', 'cells'].includes(type)) { diff --git a/app/views/submissions/_value.html.erb b/app/views/submissions/_value.html.erb index 52a030c8e..900b4e691 100644 --- a/app/views/submissions/_value.html.erb +++ b/app/views/submissions/_value.html.erb @@ -34,7 +34,7 @@ <% elsif field['type'] == 'date' %>
    - <%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %> + <%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %>
    <% else %> diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 03ead8ece..d390d633d 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -162,7 +162,7 @@ <% elsif field['type'] == 'checkbox' %> <%= svg_icon('check', class: 'w-6 h-6') %> <% elsif field['type'] == 'date' %> - <%= l(Date.parse(value), locale: @submission.template.account.locale, format: :long) %> + <%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.template.account.locale) %> <% else %> <%= Array.wrap(value).join(', ') %> <% end %> diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb index 2ac8a7e7d..ca54de588 100644 --- a/lib/submissions/generate_audit_trail.rb +++ b/lib/submissions/generate_audit_trail.rb @@ -228,7 +228,10 @@ def call(submission) elsif field['type'] == 'checkbox' composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0]) else - value = I18n.l(Date.parse(value), format: :long, locale: account.locale) if field['type'] == 'date' + if field['type'] == 'date' + value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale) + end + value = value.join(', ') if value.is_a?(Array) composer.formatted_text_box([{ text: value.to_s.presence || 'n/a' }], padding: [0, 0, 10, 0]) diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 42eb8550e..62b9806aa 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -164,7 +164,9 @@ def call(submitter) height - (area['y'] * height)) end else - value = I18n.l(Date.parse(value), format: :default, locale: account.locale) if field['type'] == 'date' + if field['type'] == 'date' + value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale) + end text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '), font: pdf.fonts.add(FONT_NAME), font_size:) @@ -268,7 +270,7 @@ def build_pdfs_index(submitter) Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter documents = latest_submitter&.documents&.preload(:blob).to_a.presence - documents ||= submitter.submission.template.schema_documents.preload(:blob) + documents ||= submitter.submission.template_schema_documents.preload(:blob) documents.to_h do |attachment| pdf = diff --git a/lib/time_utils.rb b/lib/time_utils.rb index c708ead5a..3c87ed5ff 100644 --- a/lib/time_utils.rb +++ b/lib/time_utils.rb @@ -1,6 +1,26 @@ # frozen_string_literal: true module TimeUtils + MONTH_FORMATS = { + 'M' => '%-m', + 'MM' => '%m', + 'MMM' => '%b', + 'MMMM' => '%B' + }.freeze + + DAY_FORMATS = { + 'D' => '%-d', + 'DD' => '%d' + }.freeze + + YEAR_FORMATS = { + 'YYYY' => '%Y', + 'YY' => '%y' + }.freeze + + DEFAULT_DATE_FORMAT_US = 'MM/DD/YYYY' + DEFAULT_DATE_FORMAT = 'DD/MM/YYYY' + module_function def timezone_abbr(timezone, time = Time.current) @@ -10,4 +30,20 @@ def timezone_abbr(timezone, time = Time.current) tz_info.abbreviation(time) end + + def format_date_string(string, format, locale) + date = Date.parse(string) + + format ||= locale.to_s.ends_with?('US') ? DEFAULT_DATE_FORMAT_US : DEFAULT_DATE_FORMAT + + i18n_format = format.sub(/D+/, DAY_FORMATS[format[/D+/]]) + .sub(/M+/, MONTH_FORMATS[format[/M+/]]) + .sub(/Y+/, YEAR_FORMATS[format[/Y+/]]) + + I18n.l(date, format: i18n_format, locale:) + rescue Date::Error => e + Rollbar.error(e) if defined?(Rollbar) + + string + end end