add stamp field type
This commit is contained in:
@@ -42,6 +42,11 @@
|
|||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-else-if="field.type === 'stamp' && stamp"
|
||||||
|
class="object-contain mx-auto"
|
||||||
|
:src="stamp.url"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="field.type === 'signature' && signature"
|
v-else-if="field.type === 'signature' && signature"
|
||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
@@ -158,7 +163,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -225,6 +230,7 @@ export default {
|
|||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
|
stamp: 'Stamp',
|
||||||
initials: 'Initials',
|
initials: 'Initials',
|
||||||
file: 'File',
|
file: 'File',
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
@@ -246,6 +252,7 @@ export default {
|
|||||||
select: IconSelect,
|
select: IconSelect,
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
|
stamp: IconRubberStamp,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
phone: IconPhoneCheck,
|
phone: IconPhoneCheck,
|
||||||
@@ -259,6 +266,13 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
stamp () {
|
||||||
|
if (this.field.type === 'stamp') {
|
||||||
|
return this.attachmentsIndex[this.modelValue]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
signature () {
|
signature () {
|
||||||
if (this.field.type === 'signature') {
|
if (this.field.type === 'signature') {
|
||||||
return this.attachmentsIndex[this.modelValue]
|
return this.attachmentsIndex[this.modelValue]
|
||||||
|
|||||||
@@ -206,7 +206,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ImageStep
|
<ImageStep
|
||||||
v-else-if="currentField.type === 'image'"
|
v-else-if="currentField.type === 'image' || currentField.type === 'stamp'"
|
||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
|||||||
@@ -493,6 +493,10 @@ export default {
|
|||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'date') {
|
if (type === 'date') {
|
||||||
field.preferences = {
|
field.preferences = {
|
||||||
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
@@ -675,6 +679,10 @@ export default {
|
|||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
if (field.type === 'date') {
|
if (field.type === 'date') {
|
||||||
field.preferences = {
|
field.preferences = {
|
||||||
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
@@ -707,7 +715,7 @@ export default {
|
|||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
||||||
}
|
}
|
||||||
} else if (field.type === 'signature') {
|
} else if (field.type === 'signature' || field.type === 'stamp') {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
@@ -92,8 +92,9 @@ export default {
|
|||||||
multiple: 'Multiple',
|
multiple: 'Multiple',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Cells',
|
cells: 'Cells',
|
||||||
phone: 'Phone',
|
stamp: 'Stamp',
|
||||||
payment: 'Payment'
|
payment: 'Payment',
|
||||||
|
phone: 'Phone'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -104,13 +105,14 @@ export default {
|
|||||||
date: IconCalendarEvent,
|
date: IconCalendarEvent,
|
||||||
image: IconPhoto,
|
image: IconPhoto,
|
||||||
file: IconPaperclip,
|
file: IconPaperclip,
|
||||||
select: IconSelect,
|
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
|
select: IconSelect,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
phone: IconPhoneCheck,
|
stamp: IconRubberStamp,
|
||||||
payment: IconCreditCard
|
payment: IconCreditCard,
|
||||||
|
phone: IconPhoneCheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -261,6 +261,10 @@ export default {
|
|||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'date') {
|
if (type === 'date') {
|
||||||
field.preferences = {
|
field.preferences = {
|
||||||
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
||||||
<% if field['type'].in?(['signature', 'image', 'initials']) %>
|
<% if field['type'].in?(['signature', 'image', 'initials', 'stamp']) %>
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'].in?(['file', 'payment']) %>
|
<% elsif field['type'].in?(['file', 'payment']) %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
<div class="w-full bg-base-300 py-1">
|
<div class="w-full bg-base-300 py-1">
|
||||||
<img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'image' %>
|
<% elsif field['type'].in?(['image', 'stamp']) %>
|
||||||
<img class="object-contain mx-auto max-h-28" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto max-h-28" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'] == 'file' || field['type'] == 'payment' %>
|
<% elsif field['type'] == 'file' || field['type'] == 'payment' %>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
|
|||||||
+15
-3
@@ -9,14 +9,26 @@ module PdfIcons
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
def check_io
|
def check_io
|
||||||
@check_io ||= StringIO.new(PATH.join('check.png').read)
|
StringIO.new(check_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def paperclip_io
|
def paperclip_io
|
||||||
@paperclip_io ||= StringIO.new(PATH.join('paperclip.png').read)
|
StringIO.new(paperclip_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def logo_io
|
def logo_io
|
||||||
@logo_io ||= StringIO.new(PATH.join('logo.png').read)
|
StringIO.new(logo_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_data
|
||||||
|
@check_data ||= PATH.join('check.png').read
|
||||||
|
end
|
||||||
|
|
||||||
|
def paperclip_data
|
||||||
|
@paperclip_data ||= PATH.join('paperclip.png').read
|
||||||
|
end
|
||||||
|
|
||||||
|
def logo_data
|
||||||
|
@logo_data ||= PATH.join('logo.png').read
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ module Submissions
|
|||||||
}
|
}
|
||||||
].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0]
|
].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0]
|
||||||
),
|
),
|
||||||
if field['type'].in?(%w[image signature initials])
|
if field['type'].in?(%w[image signature initials stamp])
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||||
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
|
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ module Submissions
|
|||||||
canvas.font(FONT_NAME, size: font_size)
|
canvas.font(FONT_NAME, size: font_size)
|
||||||
|
|
||||||
case field['type']
|
case field['type']
|
||||||
when 'image', 'signature', 'initials'
|
when 'image', 'signature', 'initials', 'stamp'
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||||
|
|
||||||
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
|
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submitters
|
||||||
|
module CreateStampAttachment
|
||||||
|
WIDTH = 400
|
||||||
|
HEIGHT = 200
|
||||||
|
|
||||||
|
TRANSPARENT_PIXEL = "\x89PNG\r\n\u001A\n\u0000\u0000\u0000\rIHDR\u0000" \
|
||||||
|
"\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0004" \
|
||||||
|
"\u0000\u0000\u0000\xB5\u001C\f\u0002\u0000\u0000\u0000" \
|
||||||
|
"\vIDATx\xDAc\xFC_\u000F\u0000\u0002\x83\u0001\x804\xC3ڨ" \
|
||||||
|
"\u0000\u0000\u0000\u0000IEND\xAEB`\x82"
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(submitter)
|
||||||
|
image = generate_stamp_image(submitter)
|
||||||
|
|
||||||
|
image_data = image.write_to_buffer('.png')
|
||||||
|
|
||||||
|
checksum = Digest::MD5.base64digest(image_data)
|
||||||
|
|
||||||
|
attachment = submitter.attachments.joins(:blob).find_by(blob: { checksum: })
|
||||||
|
|
||||||
|
attachment || ActiveStorage::Attachment.create!(
|
||||||
|
blob: ActiveStorage::Blob.create_and_upload!(io: StringIO.new(image_data), filename: 'stamp.png'),
|
||||||
|
metadata: { analyzed: true, identified: true, width: image.width, height: image.height },
|
||||||
|
name: 'attachments',
|
||||||
|
record: submitter
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
|
def generate_stamp_image(submitter)
|
||||||
|
logo = Vips::Image.new_from_buffer(load_logo(submitter).read, '')
|
||||||
|
|
||||||
|
logo = logo.resize([WIDTH / logo.width.to_f, HEIGHT / logo.height.to_f].min)
|
||||||
|
|
||||||
|
base_layer = Vips::Image.black(WIDTH, HEIGHT).new_from_image([255, 255, 255]).copy(interpretation: :srgb)
|
||||||
|
|
||||||
|
opacity_layer = Vips::Image.new_from_buffer(TRANSPARENT_PIXEL, '').resize(WIDTH)
|
||||||
|
|
||||||
|
text = build_text_image(submitter)
|
||||||
|
|
||||||
|
text_layer = text.new_from_image([0, 0, 0]).copy(interpretation: :srgb)
|
||||||
|
text_layer = text_layer.bandjoin(text)
|
||||||
|
|
||||||
|
base_layer = base_layer.composite(logo, 'over',
|
||||||
|
x: (WIDTH - logo.width) / 2,
|
||||||
|
y: (HEIGHT - logo.height) / 2)
|
||||||
|
|
||||||
|
base_layer = base_layer.composite(opacity_layer, 'over')
|
||||||
|
|
||||||
|
base_layer.composite(text_layer, 'over',
|
||||||
|
x: (WIDTH - text_layer.width) / 2,
|
||||||
|
y: (HEIGHT - text_layer.height) / 2)
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
|
def build_text_image(submitter)
|
||||||
|
time = I18n.l(submitter.completed_at.in_time_zone(submitter.account.timezone), format: :long,
|
||||||
|
locale: submitter.account.locale)
|
||||||
|
|
||||||
|
timezone = TimeUtils.timezone_abbr(submitter.account.timezone, submitter.completed_at)
|
||||||
|
|
||||||
|
name = if submitter.name.present? && submitter.email.present?
|
||||||
|
"#{submitter.name} #{submitter.email}"
|
||||||
|
else
|
||||||
|
submitter.name || submitter.email || submitter.phone
|
||||||
|
end
|
||||||
|
|
||||||
|
role = if submitter.submission.template_submitters.size > 1
|
||||||
|
item = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }
|
||||||
|
|
||||||
|
"Role: #{item['name']}\n"
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
|
text = %(<span size="90">Digitally signed by: <b>#{name}</b>\n#{role}#{time} #{timezone}</span>)
|
||||||
|
|
||||||
|
Vips::Image.text(text, width: WIDTH, height: HEIGHT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_logo(_submitter)
|
||||||
|
PdfIcons.logo_io
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -74,6 +74,12 @@ module Submitters
|
|||||||
default_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
|
default_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
|
||||||
next if field['submitter_uuid'] != submitter.uuid
|
next if field['submitter_uuid'] != submitter.uuid
|
||||||
|
|
||||||
|
if field['type'] == 'stamp'
|
||||||
|
acc[field['uuid']] ||= Submitters::CreateStampAttachment.call(submitter).uuid
|
||||||
|
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
value = field['default_value']
|
value = field['default_value']
|
||||||
|
|
||||||
next if value.blank?
|
next if value.blank?
|
||||||
|
|||||||
Reference in New Issue
Block a user