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 @@