Browse Source

Maintenance: Desktop view - Enhance datetime selector to enter times quicker.

Dusan Vuckovic 1 month ago
parent
commit
0fc327faeb

+ 250 - 22
app/frontend/apps/desktop/components/Form/fields/FieldDate/FieldDateTimeInput.vue

@@ -1,18 +1,26 @@
 <!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
 
+<!-- eslint-disable zammad/zammad-detect-translatable-string -->
+
 <script setup lang="ts">
+import { getNode, type FormKitNode } from '@formkit/core'
 import VueDatePicker, { type DatePickerInstance } from '@vuepic/vue-datepicker'
+import { isValid, format, parse } from 'date-fns'
+import { isEqual } from 'lodash-es'
 import { storeToRefs } from 'pinia'
-import { computed, nextTick, ref, toRef } from 'vue'
+import { computed, nextTick, ref, toRef, watch } from 'vue'
+import { IMask, useIMask } from 'vue-imask'
 
 import useValue from '#shared/components/Form/composables/useValue.ts'
 import type { DateTimeContext } from '#shared/components/Form/fields/FieldDate/types.ts'
 import { useDateTime } from '#shared/components/Form/fields/FieldDate/useDateTime.ts'
+import dateRange from '#shared/form/validation/rules/date-range.ts'
 import { EnumTextDirection } from '#shared/graphql/types.ts'
 import { i18n } from '#shared/i18n.ts'
 import testFlags from '#shared/utils/testFlags.ts'
 
 import { useThemeStore } from '#desktop/stores/theme.ts'
+
 import '@vuepic/vue-datepicker/dist/main.css'
 
 interface Props {
@@ -48,7 +56,7 @@ const actionRow = computed(() => ({
   showSelect: false,
   showCancel: false,
   // Do not show 'Today' for range selection, because it will close the picker
-  //  even if only one date was selected.
+  //   even if only one date was selected.
   showNow: !props.context.range,
   showPreview: false,
 }))
@@ -63,6 +71,203 @@ const picker = ref<DatePickerInstance>()
 
 const { isDarkMode } = storeToRefs(useThemeStore())
 
+const localeFormat = computed(() => {
+  if (timePicker.value) return i18n.getDateTimeFormat()
+  return i18n.getDateFormat()
+})
+
+// Date/time placeholders used in the locale format:
+// - 'dd' - 2-digit day
+// - 'd' - day
+// - 'mm' - 2-digit month
+// - 'm' - month
+// - 'yyyy' - year
+// - 'yy' - last 2 digits of year
+// - 'SS' - 2-digit second
+// - 'MM' - 2-digit minute
+// - 'HH' - 2-digit hour (24h)
+// - 'l' - hour (12h)
+// - 'P' - Meridian indicator ('am' or 'pm')
+const inputFormat = computed(() =>
+  localeFormat.value
+    .replace(/MM/, '2DigitMinute') // 'MM' is used for both minute and month
+    .replace(/mm/, 'MM')
+    .replace(/m/, 'M')
+    .replace(/SS/, 'ss')
+    .replace(/2DigitMinute/, 'mm')
+    .replace(/l/, 'hh')
+    .replace(/P/, 'aaa'),
+)
+
+const maskOptions = computed(() => ({
+  mask: contextReactive.value.range
+    ? `${localeFormat.value} - ${localeFormat.value}`
+    : localeFormat.value,
+  blocks: {
+    d: {
+      mask: IMask.MaskedRange,
+      from: 1,
+      to: 31,
+      placeholderChar: 'D',
+    },
+    dd: {
+      mask: IMask.MaskedRange,
+      from: 1,
+      to: 31,
+      placeholderChar: 'D',
+    },
+    m: {
+      mask: IMask.MaskedRange,
+      from: 1,
+      to: 12,
+      placeholderChar: 'M',
+    },
+    mm: {
+      mask: IMask.MaskedRange,
+      from: 1,
+      to: 12,
+      placeholderChar: 'M',
+    },
+    yyyy: {
+      mask: IMask.MaskedRange,
+      from: 1900,
+      to: 2100,
+      placeholderChar: 'Y',
+    },
+    yy: {
+      mask: IMask.MaskedRange,
+      from: 0,
+      to: 99,
+      placeholderChar: 'Y',
+    },
+    ss: {
+      mask: IMask.MaskedRange,
+      from: 0,
+      to: 59,
+      placeholderChar: 's',
+    },
+    MM: {
+      mask: IMask.MaskedRange,
+      from: 0,
+      to: 59,
+      placeholderChar: 'm',
+    },
+    HH: {
+      mask: IMask.MaskedRange,
+      from: 0,
+      to: 23,
+      placeholderChar: 'h',
+    },
+    l: {
+      mask: IMask.MaskedRange,
+      from: 1,
+      to: 12,
+      placeholderChar: 'h',
+    },
+    P: {
+      mask: IMask.MaskedEnum,
+      enum: ['am', 'pm'],
+      placeholderChar: 'p',
+    },
+  },
+  autofix: true,
+  lazy: false,
+  overwrite: true,
+}))
+
+const { el, masked, unmasked } = useIMask(maskOptions)
+
+const parseValue = (value: string) =>
+  parse(value, valueFormat.value, new Date())
+
+const formatValue = (value: Date) => format(value, valueFormat.value)
+
+watch(
+  localValue,
+  (newValue) => {
+    if (!newValue) {
+      masked.value = '' // clear input
+      return
+    }
+
+    if (contextReactive.value.range) {
+      const [startValue, endValue] = newValue
+      if (!startValue || !endValue) return
+
+      const startDate = parseValue(startValue)
+      const endDate = parseValue(endValue)
+      if (!isValid(startDate) || !isValid(endDate)) return
+
+      const value = `${format(startDate, inputFormat.value)} - ${format(endDate, inputFormat.value)}`
+      if (masked.value === value) return
+
+      masked.value = `${format(startDate, inputFormat.value)} - ${format(endDate, inputFormat.value)}`
+
+      return
+    }
+
+    const newDate = parseValue(newValue)
+    const maskedDate = parse(masked.value, inputFormat.value, new Date())
+
+    if (
+      isValid(maskedDate) &&
+      maskedDate.toISOString() === newDate.toISOString()
+    )
+      return
+
+    masked.value = format(newDate, inputFormat.value)
+  },
+  {
+    immediate: true,
+  },
+)
+
+const dateRangeValidation = (value: (string | undefined)[]) => {
+  if (value.includes(undefined)) return false
+  if (dateRange.rule({ value } as FormKitNode<string[]>)) return true
+
+  const node = getNode(contextReactive.value.id)
+  if (!node) return
+
+  // Manually set validation error message.
+  node.setErrors(i18n.t(dateRange.localeMessage()))
+
+  return false
+}
+
+watch(masked, (newValue) => {
+  // empty input
+  if (localValue.value && (!newValue || !unmasked.value)) {
+    localValue.value = null
+    return
+  }
+
+  if (contextReactive.value.range) {
+    const newValues = newValue.split(' - ').map((value) => {
+      const date = parse(value, inputFormat.value, new Date())
+      if (!isValid(date)) return
+      return formatValue(date)
+    })
+
+    if (!dateRangeValidation(newValues) || isEqual(localValue.value, newValues))
+      return
+
+    localValue.value = newValues
+
+    return
+  }
+
+  const newDate = parse(newValue, inputFormat.value, new Date())
+
+  if (
+    !isValid(newDate) ||
+    (isValid(newDate) && localValue.value === formatValue(newDate))
+  )
+    return
+
+  localValue.value = formatValue(newDate)
+})
+
 const open = () => {
   nextTick(() => {
     testFlags.set('field-date-time.opened')
@@ -73,6 +278,40 @@ const closed = () => {
   nextTick(() => {
     testFlags.set('field-date-time.closed')
   })
+
+  if (!localValue.value && masked.value) {
+    masked.value = '' // clear input
+    return
+  }
+
+  if (contextReactive.value.range) {
+    const maskedValues = masked.value.split(' - ').map((value: string) => {
+      const date = parse(value, inputFormat.value, new Date())
+      if (!isValid(date)) return
+      return formatValue(date)
+    })
+
+    if (isEqual(localValue.value, maskedValues)) return
+
+    const [startValue, endValue] = localValue.value
+    if (!startValue || !endValue) return
+
+    const startDate = parseValue(startValue)
+    const endDate = parseValue(endValue)
+    if (!isValid(startDate) || !isValid(endDate)) return
+
+    masked.value = `${format(startDate, inputFormat.value)} - ${format(endDate, inputFormat.value)}`
+
+    return
+  }
+
+  const maskedDate = parse(masked.value, inputFormat.value, new Date())
+
+  if (isValid(maskedDate) && localValue.value === formatValue(maskedDate))
+    return
+
+  const newDate = parseValue(localValue.value)
+  masked.value = format(newDate, inputFormat.value)
 }
 </script>
 
@@ -105,43 +344,32 @@ const closed = () => {
       :action-row="actionRow"
       :config="config"
       :aria-labels="ariaLabels"
-      :text-input="{ openMenu: 'toggle' }"
+      :text-input="{ openMenu: 'open' }"
       auto-apply
       offset="12"
       @open="open"
       @closed="closed"
       @blur="context.handlers.blur"
     >
-      <template
-        #dp-input="{
-          value,
-          onInput,
-          onEnter,
-          onTab,
-          onBlur,
-          onKeypress,
-          onPaste,
-        }"
-      >
+      <template #dp-input>
         <input
           :id="context.id"
-          :value="value"
+          ref="el"
           :name="context.node.name"
           :class="context.classes.input"
           :disabled="context.disabled"
           :aria-describedby="context.describedBy"
           v-bind="context.attrs"
           type="text"
-          @input="onInput"
-          @keypress.enter="onEnter"
-          @keypress.tab="onTab"
-          @keypress="onKeypress"
-          @paste="onPaste"
-          @blur="onBlur"
         />
       </template>
       <template #input-icon>
-        <CommonIcon :name="inputIcon" size="tiny" decorative />
+        <CommonIcon
+          :name="inputIcon"
+          size="tiny"
+          decorative
+          @click.stop="picker?.toggleMenu()"
+        />
       </template>
       <template #clear-icon>
         <CommonIcon

+ 15 - 12
app/frontend/apps/desktop/components/Form/fields/FieldDate/__tests__/FieldDateTime.spec.ts

@@ -47,7 +47,7 @@ describe('Fields - FieldDate', () => {
 
       const input = view.getByLabelText('Date')
 
-      expect(input).toHaveDisplayValue('')
+      expect(input).toHaveDisplayValue('YYYY-MM-DD')
 
       await view.events.click(input)
       await view.events.click(view.getByText('12'))
@@ -63,7 +63,7 @@ describe('Fields - FieldDate', () => {
 
       const input = view.getByLabelText('Date')
 
-      expect(input).toHaveDisplayValue('')
+      expect(input).toHaveDisplayValue('YYYY-MM-DD')
 
       await view.events.type(input, '2021-04-12')
       await view.events.keyboard('{Enter}')
@@ -80,7 +80,8 @@ describe('Fields - FieldDate', () => {
       })
 
       const input = view.getByLabelText('Date')
-      expect(input).toHaveDisplayValue('')
+
+      expect(input).toHaveDisplayValue('YYYY-MM-DD - YYYY-MM-DD')
 
       await view.events.click(input)
 
@@ -102,7 +103,7 @@ describe('Fields - FieldDate', () => {
 
       const input = view.getByLabelText('Date')
 
-      expect(input).toHaveDisplayValue('')
+      expect(input).toHaveDisplayValue('YYYY-MM-DD - YYYY-MM-DD')
 
       await view.events.type(input, '2021-04-12 - 2021-04-14')
       await view.events.keyboard('{Enter}')
@@ -120,7 +121,7 @@ describe('Fields - FieldDate', () => {
 
       const input = view.getByLabelText('Date')
 
-      expect(input).toHaveDisplayValue('')
+      expect(input).toHaveDisplayValue('YYYY-MM-DD - YYYY-MM-DD')
 
       await view.events.type(input, '2021-04-28 - 2021-04-14')
       await view.events.keyboard('{Enter}')
@@ -137,7 +138,8 @@ describe('Fields - FieldDate', () => {
       const view = await renderDateField()
 
       const input = view.getByLabelText('Date')
-      expect(input).toHaveDisplayValue('')
+
+      expect(input).toHaveDisplayValue('YYYY-MM-DD')
 
       await view.events.click(input)
       await view.events.click(view.getByText('Today'))
@@ -173,7 +175,7 @@ describe('Fields - FieldDate', () => {
       const emittedInput = view.emitted().inputRaw as Array<Array<InputEvent>>
 
       expect(emittedInput[0][0]).toBeNull()
-      expect(input).toHaveDisplayValue('')
+      expect(input).toHaveDisplayValue('YYYY-MM-DD')
     })
 
     it("doesn't allow changing anything while disabled", async () => {
@@ -200,7 +202,7 @@ describe('Fields - FieldDate', () => {
       await view.events.click(input)
       await view.events.click(view.getByText('12'))
 
-      expect(input).toHaveDisplayValue('')
+      expect(input).toHaveDisplayValue('YYYY-MM-DD')
 
       await view.events.click(view.getByText('13'))
 
@@ -217,7 +219,7 @@ describe('Fields - FieldDate', () => {
       await view.events.click(input)
       await view.events.click(view.getByText('15'))
 
-      expect(input).toHaveDisplayValue('')
+      expect(input).toHaveDisplayValue('YYYY-MM-DD')
 
       await view.rerender({
         maxDate: '2021-04-15',
@@ -246,6 +248,7 @@ describe('Fields - FieldDate', () => {
       const input = view.getByLabelText('Date')
 
       await view.events.click(input)
+
       const dialog = view.getByRole('dialog')
 
       expect(dialog).toHaveClass('dp__theme_dark')
@@ -260,7 +263,7 @@ describe('Fields - FieldDate', () => {
 
       const input = view.getByLabelText('Date')
 
-      expect(input).toHaveDisplayValue('')
+      expect(input).toHaveDisplayValue('YYYY-MM-DD hh:mm')
 
       await view.events.click(input)
       await view.events.click(view.getByText('Today'))
@@ -278,7 +281,7 @@ describe('Fields - FieldDate', () => {
 
       const input = view.getByLabelText('Date')
 
-      expect(input).toHaveDisplayValue('')
+      expect(input).toHaveDisplayValue('YYYY-MM-DD hh:mm')
 
       await view.events.type(input, '2021-04-13 11:10')
       await view.events.keyboard('{Enter}')
@@ -300,7 +303,7 @@ describe('Fields - FieldDate', () => {
 
       const input = view.getByLabelText('Date')
 
-      expect(input).toHaveDisplayValue('')
+      expect(input).toHaveDisplayValue('MM/DD/YYYY hh:mm pp')
 
       await view.events.click(input)
       await view.events.click(view.getByText('Today'))

+ 1 - 56
app/frontend/apps/desktop/components/Form/fields/FieldDate/index.ts

@@ -7,63 +7,8 @@ import formUpdaterTrigger from '#shared/form/features/formUpdaterTrigger.ts'
 
 import FieldDateTimeInput from './FieldDateTimeInput.vue'
 
-import type { FormKitNode, FormKitProps } from '@formkit/core'
-
-const addDateRangeValidation = (node: FormKitNode) => {
-  const addDataRangeValidation = (props: Partial<FormKitProps>) => {
-    const { validation } = props
-    if (Array.isArray(validation)) {
-      validation.push(['date_range'])
-      return
-    }
-
-    if (!validation) {
-      props.validation = 'date_range'
-      return
-    }
-
-    if (!validation.includes('required')) {
-      props.validation = `${validation}|date_range`
-    }
-  }
-
-  const removeDataRangeValidation = (props: Partial<FormKitProps>) => {
-    const { validation } = props
-
-    if (!validation) return
-
-    if (Array.isArray(validation)) {
-      props.validation = validation.filter(([rule]) => rule !== 'date_range')
-      return
-    }
-
-    if (validation.includes('date_range')) {
-      props.validation = validation
-        .split('|')
-        .filter((rule: string) => !rule.includes('date_range'))
-        .join('|')
-    }
-  }
-
-  node.on('created', () => {
-    const { props } = node
-
-    if (props.range) {
-      addDataRangeValidation(props)
-    }
-
-    node.on('prop:range', ({ payload }) => {
-      if (payload) {
-        addDataRangeValidation(props)
-      } else {
-        removeDataRangeValidation(props)
-      }
-    })
-  })
-}
-
 const dateFieldDefinition = createInput(FieldDateTimeInput, dateFieldProps, {
-  features: [addLink, formUpdaterTrigger(), addDateRangeValidation],
+  features: [addLink, formUpdaterTrigger()],
 })
 
 export default [

+ 0 - 2
app/frontend/apps/desktop/pages/guided-setup/__tests__/guided-setup-manual-channel-email.spec.ts

@@ -685,8 +685,6 @@ describe('guided setup manual channel email', () => {
         '2025-01-01 00:00',
       )
 
-      await view.events.keyboard('{enter}')
-
       await getNode('channel-email-inbound-messages')?.settled
 
       await view.events.click(

+ 5 - 1
app/frontend/apps/desktop/pages/personal-setting/__tests__/personal-setting-out-of-office.spec.ts

@@ -278,7 +278,11 @@ describe('Out of Office page', () => {
       const view = await visitView('/personal-setting/out-of-office')
 
       expect(view.getByLabelText('Reason for absence')).toHaveValue('')
-      expect(view.getByLabelText('Start and end date')).toHaveValue('')
+
+      expect(view.getByLabelText('Start and end date')).toHaveValue(
+        'YYYY-MM-DD - YYYY-MM-DD',
+      )
+
       expect(view.getByLabelText('Replacement agent')).toHaveValue('')
       expect(view.getByLabelText('Active')).not.toBeChecked()
     })

+ 10 - 0
app/frontend/build/manualChunks.mjs

@@ -23,6 +23,11 @@ const matchers = [
     matcher: (id) => id.includes('@vue/apollo'),
     chunk: 'apollo',
   },
+  {
+    vendor: true,
+    matcher: (id) => id.includes('@vuepic/vue-datepicker'),
+    chunk: 'datepicker',
+  },
   {
     vendor: false,
     matcher: (id) => id.includes('frontend/shared/server'),
@@ -33,6 +38,11 @@ const matchers = [
     matcher: (id) => id.includes('node_modules/lodash-es'),
     chunk: 'lodash',
   },
+  {
+    vendor: true,
+    matcher: (id) => id.includes('node_modules/date-fns'),
+    chunk: 'date',
+  },
   {
     vendor: true,
     matcher: (id) => /node_modules\/@formkit/.test(id),

+ 1 - 1
app/frontend/shared/components/Form/fields/FieldDate/useDateTime.ts

@@ -12,7 +12,7 @@ export const useDateTime = (context: Ref<DateTimeContext>) => {
   const timePicker = computed(() => context.value.type === 'datetime')
 
   const valueFormat = computed(() => {
-    if (timePicker.value) return 'iso'
+    if (timePicker.value) return "yyyy-MM-dd'T'HH:mm:ss.SSSX"
     return 'yyyy-MM-dd'
   })
 

+ 10 - 10
i18n/zammad.pot

@@ -1260,7 +1260,7 @@ msgid "Agent idle timeout"
 msgstr ""
 
 #: app/models/role.rb:156
-#: app/models/user.rb:723
+#: app/models/user.rb:727
 msgid "Agent limit exceeded, please check your account settings."
 msgstr ""
 
@@ -1780,7 +1780,7 @@ msgstr ""
 msgid "Assignment timeout in minutes if assigned agent is not working on it. Ticket will be shown as unassigend."
 msgstr ""
 
-#: app/models/user.rb:626
+#: app/models/user.rb:630
 msgid "At least one identifier (firstname, lastname, phone, mobile or email) for user is required."
 msgstr ""
 
@@ -1793,7 +1793,7 @@ msgid "At least one object must be selected."
 msgstr ""
 
 #: app/models/role.rb:128
-#: app/models/user.rb:697
+#: app/models/user.rb:701
 msgid "At least one user needs to have admin permissions."
 msgstr ""
 
@@ -2883,7 +2883,7 @@ msgstr ""
 
 #: app/assets/javascripts/app/views/generic/searchable_select.jst.eco:56
 #: app/frontend/apps/desktop/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:679
-#: app/frontend/apps/desktop/components/Form/fields/FieldDate/FieldDateTimeInput.vue:153
+#: app/frontend/apps/desktop/components/Form/fields/FieldDate/FieldDateTimeInput.vue:381
 #: app/frontend/apps/desktop/components/Form/fields/FieldSelect/FieldSelectInput.vue:333
 #: app/frontend/apps/desktop/components/Form/fields/FieldTreeSelect/FieldTreeSelectInput.vue:382
 #: app/frontend/apps/mobile/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:155
@@ -5619,7 +5619,7 @@ msgstr ""
 msgid "Email address"
 msgstr ""
 
-#: app/models/user.rb:636
+#: app/models/user.rb:640
 msgid "Email address '%{email}' is already used for another user."
 msgstr ""
 
@@ -7968,7 +7968,7 @@ msgstr ""
 msgid "Invalid client_id received!"
 msgstr ""
 
-#: app/models/user.rb:581
+#: app/models/user.rb:585
 msgid "Invalid email '%{email}'"
 msgstr ""
 
@@ -9486,7 +9486,7 @@ msgstr ""
 msgid "More information can be found here."
 msgstr ""
 
-#: app/models/user.rb:649
+#: app/models/user.rb:653
 msgid "More than 250 secondary organizations are not allowed."
 msgstr ""
 
@@ -12818,7 +12818,7 @@ msgstr ""
 msgid "Secondary organizations"
 msgstr ""
 
-#: app/models/user.rb:643
+#: app/models/user.rb:647
 msgid "Secondary organizations are only allowed when the primary organization is given."
 msgstr ""
 
@@ -16582,7 +16582,7 @@ msgstr ""
 msgid "To work on Tickets."
 msgstr ""
 
-#: app/frontend/apps/desktop/components/Form/fields/FieldDate/FieldDateTimeInput.vue:50
+#: app/frontend/apps/desktop/components/Form/fields/FieldDate/FieldDateTimeInput.vue:58
 #: app/frontend/apps/mobile/components/Form/fields/FieldDate/FieldDateTimeInput.vue:109
 #: public/assets/chat/chat-no-jquery.coffee:215
 #: public/assets/chat/chat.coffee:213
@@ -19244,7 +19244,7 @@ msgstr ""
 msgid "is the wrong length (should be 1 character)"
 msgstr ""
 
-#: app/models/user.rb:847
+#: app/models/user.rb:851
 msgid "is too long"
 msgstr ""
 

+ 2 - 0
package.json

@@ -156,6 +156,7 @@
     "@vueuse/router": "^11.3.0",
     "@vueuse/shared": "^11.3.0",
     "async-mutex": "^0.5.0",
+    "date-fns": "^4.1.0",
     "graphql": "^16.10.0",
     "graphql-ruby-client": "^1.14.5",
     "graphql-tag": "^2.12.6",
@@ -177,6 +178,7 @@
     "vue": "^3.5.13",
     "vue-advanced-cropper": "^2.8.9",
     "vue-easy-lightbox": "1.19.0",
+    "vue-imask": "^7.6.1",
     "vue-router": "^4.5.0",
     "vue3-draggable-resizable": "^1.6.5",
     "workbox-core": "^7.3.0",

+ 46 - 0
pnpm-lock.yaml

@@ -172,6 +172,9 @@ importers:
       async-mutex:
         specifier: ^0.5.0
         version: 0.5.0
+      date-fns:
+        specifier: ^4.1.0
+        version: 4.1.0
       graphql:
         specifier: ^16.10.0
         version: 16.10.0
@@ -235,6 +238,9 @@ importers:
       vue-easy-lightbox:
         specifier: 1.19.0
         version: 1.19.0(vue@3.5.13(typescript@5.7.3))
+      vue-imask:
+        specifier: ^7.6.1
+        version: 7.6.1(vue@3.5.13(typescript@5.7.3))
       vue-router:
         specifier: ^4.5.0
         version: 4.5.0(vue@3.5.13(typescript@5.7.3))
@@ -1085,6 +1091,10 @@ packages:
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
+  '@babel/runtime-corejs3@7.26.0':
+    resolution: {integrity: sha512-YXHu5lN8kJCb1LOb9PgV6pvak43X2h4HvRApcN5SdWeaItQOzfn1hgP6jasD6KWQyJDBxrVmA9o9OivlnNJK/w==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/runtime@7.23.4':
     resolution: {integrity: sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==}
     engines: {node: '>=6.9.0'}
@@ -3059,6 +3069,9 @@ packages:
   core-js-compat@3.26.0:
     resolution: {integrity: sha512-piOX9Go+Z4f9ZiBFLnZ5VrOpBl0h7IGCkiFUN11QTe6LjAvOT3ifL/5TdoizMh99hcGy5SoLyWbapIY/PIb/3A==}
 
+  core-js-pure@3.39.0:
+    resolution: {integrity: sha512-7fEcWwKI4rJinnK+wLTezeg2smbFFdSBP6E2kQZNbnzM2s1rpKQ6aaRteZSSg7FLU3P0HGGVo/gbpfanU36urg==}
+
   cosmiconfig@8.2.0:
     resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==}
     engines: {node: '>=14'}
@@ -3150,6 +3163,9 @@ packages:
   date-fns@3.6.0:
     resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
 
+  date-fns@4.1.0:
+    resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
+
   de-indent@1.0.2:
     resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
 
@@ -4080,6 +4096,10 @@ packages:
     engines: {node: '>=0.10.0'}
     hasBin: true
 
+  imask@7.6.1:
+    resolution: {integrity: sha512-sJlIFM7eathUEMChTh9Mrfw/IgiWgJqBKq2VNbyXvBZ7ev/IlO6/KQTKlV/Fm+viQMLrFLG/zCuudrLIwgK2dg==}
+    engines: {npm: '>=4.0.0'}
+
   immutable@3.7.6:
     resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==}
     engines: {node: '>=0.8.0'}
@@ -6433,6 +6453,11 @@ packages:
     peerDependencies:
       eslint: '>=6.0.0'
 
+  vue-imask@7.6.1:
+    resolution: {integrity: sha512-/5ZVNerI9Dn6gZ/cSCYGiZK4JHdwsEBgHBTRpVwS2U0URxK/Jt5FlQuoL1DhbxC6t4ElcVMWYOvkE2hR8hdt1w==}
+    peerDependencies:
+      vue: '>=2.7'
+
   vue-router@4.5.0:
     resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==}
     peerDependencies:
@@ -7468,6 +7493,11 @@ snapshots:
       '@babel/types': 7.25.6
       esutils: 2.0.3
 
+  '@babel/runtime-corejs3@7.26.0':
+    dependencies:
+      core-js-pure: 3.39.0
+      regenerator-runtime: 0.14.1
+
   '@babel/runtime@7.23.4':
     dependencies:
       regenerator-runtime: 0.14.1
@@ -9872,6 +9902,8 @@ snapshots:
     dependencies:
       browserslist: 4.23.3
 
+  core-js-pure@3.39.0: {}
+
   cosmiconfig@8.2.0:
     dependencies:
       import-fresh: 3.3.0
@@ -9973,6 +10005,8 @@ snapshots:
 
   date-fns@3.6.0: {}
 
+  date-fns@4.1.0: {}
+
   de-indent@1.0.2: {}
 
   debounce@1.2.1: {}
@@ -11092,6 +11126,10 @@ snapshots:
 
   image-size@0.5.5: {}
 
+  imask@7.6.1:
+    dependencies:
+      '@babel/runtime-corejs3': 7.26.0
+
   immutable@3.7.6: {}
 
   immutable@5.0.2: {}
@@ -13497,6 +13535,14 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  vue-imask@7.6.1(vue@3.5.13(typescript@5.7.3)):
+    dependencies:
+      imask: 7.6.1
+      vue: 3.5.13(typescript@5.7.3)
+      vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3))
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+
   vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)):
     dependencies:
       '@vue/devtools-api': 6.6.4

Some files were not shown because too many files changed in this diff