diff --git a/app/javascript/submission_form/accessibility_areas.vue b/app/javascript/submission_form/accessibility_areas.vue
index 8f05d4c93..6f593f7ff 100644
--- a/app/javascript/submission_form/accessibility_areas.vue
+++ b/app/javascript/submission_form/accessibility_areas.vue
@@ -125,6 +125,11 @@ export default {
required: false,
default: () => []
},
+ fieldsUuidIndex: {
+ type: Object,
+ required: false,
+ default: () => ({})
+ },
values: {
type: Object,
required: false,
@@ -195,7 +200,7 @@ export default {
},
isMobileContainer: FieldAreas.computed.isMobileContainer,
isInlineSize: FieldAreas.computed.isInlineSize,
- fieldsUuidIndex () {
+ formulaFieldsUuidIndex () {
return this.formulaFields.reduce((acc, field) => {
acc[field.uuid] = field
@@ -289,6 +294,7 @@ export default {
methods: {
normalizeFormula: FormulaAreas.methods.normalizeFormula,
calculateFormula: FormulaAreas.methods.calculateFormula,
+ dateFormulaValue: FormulaAreas.methods.dateFormulaValue,
evalTextFormula: FormulaAreas.methods.evalTextFormula,
scrollInContainer: FieldAreas.methods.scrollInContainer,
scrollIntoArea: FieldAreas.methods.scrollIntoArea,
diff --git a/app/javascript/submission_form/calculator.js b/app/javascript/submission_form/calculator.js
index 55cc185f2..a1fc446ee 100644
--- a/app/javascript/submission_form/calculator.js
+++ b/app/javascript/submission_form/calculator.js
@@ -633,6 +633,32 @@ defineFunction('contains', {
return String(v[0]).indexOf(String(v[1])) !== -1
}
})
+defineFunction('today', {
+ min: 0,
+ max: 0,
+ call: () => {
+ const now = new Date()
+ return Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()) / 86400000
+ }
+})
+defineFunction('datedif', {
+ min: 3,
+ max: 3,
+ call: (v) => {
+ if (v[0] == null || v[1] == null) return null
+ const start = toNumber(v[0])
+ const end = toNumber(v[1])
+ const unit = String(v[2]).toLowerCase()
+ if (unit === 'd') return end - start
+ const from = new Date(Math.min(start, end) * 86400000)
+ const to = new Date(Math.max(start, end) * 86400000)
+ const months = (to.getUTCFullYear() - from.getUTCFullYear()) * 12 + to.getUTCMonth() - from.getUTCMonth() - (to.getUTCDate() < from.getUTCDate() ? 1 : 0)
+ const sign = end < start ? -1 : 1
+ if (unit === 'm') return sign * months
+ if (unit === 'y') return sign * Math.floor(months / 12)
+ return null
+ }
+})
const OP_CLASSES = {
add: Addition,
diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue
index a3cb83ad4..695d3385b 100644
--- a/app/javascript/submission_form/form.vue
+++ b/app/javascript/submission_form/form.vue
@@ -31,6 +31,7 @@
:readonly-conditional-fields="readonlyConditionalFields"
:readonly-conditional-field-values="readonlyConditionalFieldValues"
:formula-fields="formulaFields"
+ :fields-uuid-index="fieldsUuidIndex"
:values="values"
:readonly-values="readonlyFieldValues"
:submitter="submitter"
@@ -71,6 +72,7 @@
@@ -560,6 +562,7 @@
:field="currentField"
:submitter-slug="submitterSlug"
:fields="formulaFields"
+ :fields-uuid-index="fieldsUuidIndex"
:values="values"
:readonly-values="readonlyFieldValues"
:fetch-options="fetchOptions"
diff --git a/app/javascript/submission_form/formula_areas.vue b/app/javascript/submission_form/formula_areas.vue
index 77e633f09..1b2199889 100644
--- a/app/javascript/submission_form/formula_areas.vue
+++ b/app/javascript/submission_form/formula_areas.vue
@@ -48,6 +48,11 @@ export default {
type: Object,
required: false,
default: () => ({})
+ },
+ fieldsUuidIndex: {
+ type: Object,
+ required: false,
+ default: () => ({})
}
},
data () {
@@ -59,7 +64,7 @@ export default {
isInlineSize () {
return CSS.supports('container-type: size')
},
- fieldsUuidIndex () {
+ formulaFieldsUuidIndex () {
return this.fields.reduce((acc, field) => {
acc[field.uuid] = field
@@ -82,8 +87,8 @@ export default {
if (depth > 10) return formula
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
- if (this.fieldsUuidIndex[uuid]) {
- return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
+ if (this.formulaFieldsUuidIndex[uuid]) {
+ return `(${this.normalizeFormula(this.formulaFieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
} else {
return match
}
@@ -91,16 +96,36 @@ export default {
},
calculateFormula (field) {
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
- return this.readonlyValues[uuid] || this.values[uuid] || 0.0
+ const value = this.readonlyValues[uuid] || this.values[uuid]
+
+ if (this.fieldsUuidIndex[uuid]?.type === 'date') {
+ return this.dateFormulaValue(value)
+ }
+
+ return value || 0.0
})
- return this.math.evaluate(transformedFormula.toLowerCase())
+ const value = this.math.evaluate(transformedFormula.toLowerCase())
+
+ if (field.type === 'date') {
+ return Number.isFinite(value) ? new Date(Math.floor(value) * 86400000).toISOString().slice(0, 10) : ''
+ }
+
+ return value
+ },
+ dateFormulaValue (value) {
+ if (!value) return 'null'
+ if (value === '{{date}}') return 'today()'
+
+ const [year, month, day = 1] = value.slice(0, 10).split('-').map(Number)
+
+ return Date.UTC(year, month - 1, day) / 86400000
},
evalTextFormula (field, depth = 0) {
if (depth > 10) return ''
return field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
- const formulaField = this.fieldsUuidIndex[uuid]
+ const formulaField = this.formulaFieldsUuidIndex[uuid]
if (formulaField?.preferences?.formula) {
if (formulaField.type === 'text') {
diff --git a/app/javascript/submission_form/payment_step.vue b/app/javascript/submission_form/payment_step.vue
index ab0159542..44d516bdc 100644
--- a/app/javascript/submission_form/payment_step.vue
+++ b/app/javascript/submission_form/payment_step.vue
@@ -104,6 +104,11 @@ export default {
required: false,
default: () => ({})
},
+ fieldsUuidIndex: {
+ type: Object,
+ required: false,
+ default: () => ({})
+ },
values: {
type: Object,
required: true
@@ -137,7 +142,7 @@ export default {
}
},
computed: {
- fieldsUuidIndex () {
+ formulaFieldsUuidIndex () {
return this.fields.reduce((acc, field) => {
acc[field.uuid] = field
@@ -216,17 +221,31 @@ export default {
},
calculateFormula () {
const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
- return this.readonlyValues[uuid] || this.values[uuid] || 0.0
+ const value = this.readonlyValues[uuid] || this.values[uuid]
+
+ if (this.fieldsUuidIndex[uuid]?.type === 'date') {
+ return this.dateFormulaValue(value)
+ }
+
+ return value || 0.0
})
return this.math.evaluate(transformedFormula.toLowerCase())
},
+ dateFormulaValue (value) {
+ if (!value) return 'null'
+ if (value === '{{date}}') return 'today()'
+
+ const [year, month, day = 1] = value.slice(0, 10).split('-').map(Number)
+
+ return Date.UTC(year, month - 1, day) / 86400000
+ },
normalizeFormula (formula, depth = 0) {
if (depth > 10) return formula
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
- if (this.fieldsUuidIndex[uuid]) {
- return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
+ if (this.formulaFieldsUuidIndex[uuid]) {
+ return `(${this.normalizeFormula(this.formulaFieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
} else {
return match
}
diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue
index 655cccb9f..1ee213493 100644
--- a/app/javascript/template_builder/builder.vue
+++ b/app/javascript/template_builder/builder.vue
@@ -1799,10 +1799,28 @@ export default {
},
calculateFormula (field) {
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
+ if (this.fieldsUuidIndex[uuid]?.type === 'date') {
+ return this.dateFormulaValue(this.fieldsUuidIndex[uuid].default_value)
+ }
+
return this.fieldsUuidIndex[uuid]?.default_value || 0.0
})
- return this.math.evaluate(transformedFormula.toLowerCase())
+ const value = this.math.evaluate(transformedFormula.toLowerCase())
+
+ if (field.type === 'date') {
+ return Number.isFinite(value) ? new Date(Math.floor(value) * 86400000).toISOString().slice(0, 10) : ''
+ }
+
+ return value
+ },
+ dateFormulaValue (value) {
+ if (!value) return 'null'
+ if (value === '{{date}}') return 'today()'
+
+ const [year, month, day = 1] = value.slice(0, 10).split('-').map(Number)
+
+ return Date.UTC(year, month - 1, day) / 86400000
},
evalTextFormula (field, depth = 0) {
if (depth > 10) return ''
diff --git a/app/javascript/template_builder/field_context_menu.vue b/app/javascript/template_builder/field_context_menu.vue
index b85bad1c3..929ef0de6 100644
--- a/app/javascript/template_builder/field_context_menu.vue
+++ b/app/javascript/template_builder/field_context_menu.vue
@@ -611,7 +611,7 @@ export default {
return this.withCondition && (this.withConditions || this.nativePlatform !== 'ios') && !['stamp', 'heading'].includes(this.field.type)
},
showFormula () {
- return (this.field.type === 'number' || this.field.type === 'payment' || !!this.field.preferences?.formula) && (this.withFormula || this.nativePlatform !== 'ios')
+ return (this.field.type === 'number' || this.field.type === 'date' || this.field.type === 'payment' || !!this.field.preferences?.formula) && (this.withFormula || this.nativePlatform !== 'ios')
},
showRequired () {
return this.withRequired && !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
diff --git a/app/javascript/template_builder/field_settings.vue b/app/javascript/template_builder/field_settings.vue
index 1d4580e4f..0b5f0cdd6 100644
--- a/app/javascript/template_builder/field_settings.vue
+++ b/app/javascript/template_builder/field_settings.vue
@@ -500,7 +500,7 @@