소스 검색

Enhance DateSelect component with date validation and formatting

- Integrated dayjs for improved date handling and validation within the DateSelect component.
- Added functions to validate ISO date formats and ensure date ranges are valid before saving.
- Updated the UI to reflect invalid states for date selections and disabled the save button when conditions are not met.
- Enhanced date display formatting to improve user experience.
0es 2 달 전
부모
커밋
a77094375b
2개의 변경된 파일61개의 추가작업 그리고 18개의 파일을 삭제
  1. 59 18
      app/components/popup/DateSelect.vue
  2. 2 0
      app/plugins/dayjs.ts

+ 59 - 18
app/components/popup/DateSelect.vue

@@ -2,6 +2,7 @@
 import { computed, ref, watch } from 'vue'
 import { useI18n } from 'vue-i18n'
 import type { PickerOption } from 'vant'
+import dayjs from 'dayjs'
 
 type DateRange = {
   start?: string // YYYY-MM-DD
@@ -10,6 +11,9 @@ type DateRange = {
 
 type PickerValue = [string, string, string] // day, month, year
 
+const ISO_DATE_FORMAT = 'YYYY-MM-DD'
+const DISPLAY_DATE_FORMAT = 'DD/MM/YYYY'
+
 const props = withDefaults(
   defineProps<{
     show: boolean
@@ -38,10 +42,16 @@ const isIsoDate = (value?: string): value is string => {
   return !!value && /^\d{4}-\d{2}-\d{2}$/.test(value)
 }
 
+const isValidIsoDate = (iso?: string) => {
+  if (!isIsoDate(iso)) return false
+  // Strict parse to avoid overflow dates like 2026-02-31 being treated as valid.
+  return dayjs(iso, ISO_DATE_FORMAT, true).isValid()
+}
+
 const toDisplay = (iso?: string) => {
   if (!isIsoDate(iso)) return ''
-  const [y, m, d] = iso.split('-')
-  return `${d}-${m}-${y}`
+
+  return dayjs(iso, ISO_DATE_FORMAT).format(DISPLAY_DATE_FORMAT)
 }
 
 const toIso = (d: string, m: string, y: string) => `${y}-${m}-${d}`
@@ -114,13 +124,16 @@ const formatter = (type: string, option: PickerOption) => {
   return option
 }
 
-const handleChange = (values: string[]) => {
-  // values order: day, month, year
-  const d = values[0]
-  const m = values[1]
-  const y = values[2]
+const handleChange = (values: unknown) => {
+  const { selectedValues } = values as { selectedValues: string[] }
+  const d = selectedValues[0]
+  const m = selectedValues[1]
+  const y = selectedValues[2]
+
   if (!d || !m || !y) return
+
   const iso = toIso(pad2(d), pad2(m), y)
+
   if (activeField.value === 'start') startDate.value = iso
   else endDate.value = iso
 }
@@ -135,16 +148,28 @@ const handleReset = () => {
   emit('reset')
 }
 
+const isRangeValid = computed(() => {
+  const start = startDate.value
+  const end = endDate.value
+
+  if (start && !isValidIsoDate(start)) return false
+  if (end && !isValidIsoDate(end)) return false
+
+  if (!start || !end) return true
+
+  const s = dayjs(start, ISO_DATE_FORMAT, true)
+  const e = dayjs(end, ISO_DATE_FORMAT, true)
+  if (!s.isValid() || !e.isValid()) return false
+
+  return !e.isBefore(s, 'day')
+})
+
+const canSave = computed(() => (startDate.value && endDate.value) && isRangeValid.value)
+
 const handleSave = () => {
+  if (!canSave.value) return
   const next: DateRange = { start: startDate.value, end: endDate.value }
 
-  // If both set and end < start, swap.
-  if (next.start && next.end && next.end < next.start) {
-    const tmp = next.start
-    next.start = next.end
-    next.end = tmp
-  }
-
   emit('update:value', next)
   emit('confirm', next)
   emit('update:show', false)
@@ -183,11 +208,15 @@ const handleUpdateShow = (value: boolean) => {
         <button
           type="button"
           class="date-select-popup__pill"
-          :class="{ 'date-select-popup__pill--active': activeField === 'start' }"
+          :class="{ 'date-select-popup__pill--active': activeField === 'start',
+                    'date-select-popup__pill--invalid': activeField === 'start' && !isRangeValid }"
           @click="activeField = 'start'"
         >
-          <span class="date-select-popup__pill-text">
-            {{ toDisplay(startDate) || t('wallet.dateSelect.startPlaceholder') }}
+          <span
+            class="date-select-popup__pill-text"
+            :class="{ 'date-select-popup__pill-text--placeholder': !startDate }"
+          >
+            {{ startDate ? toDisplay(startDate) : t('wallet.dateSelect.startPlaceholder') }}
           </span>
         </button>
 
@@ -196,7 +225,8 @@ const handleUpdateShow = (value: boolean) => {
         <button
           type="button"
           class="date-select-popup__pill"
-          :class="{ 'date-select-popup__pill--active': activeField === 'end' }"
+          :class="{ 'date-select-popup__pill--active': activeField === 'end',
+                    'date-select-popup__pill--invalid': activeField === 'end' && !isRangeValid }"
           @click="activeField = 'end'"
         >
           <span
@@ -222,6 +252,8 @@ const handleUpdateShow = (value: boolean) => {
         <button
           type="button"
           class="date-select-popup__save"
+          :class="{ 'date-select-popup__save--disabled': !canSave }"
+          :disabled="!canSave"
           @click="handleSave"
         >
           {{ t('wallet.dateSelect.save') }}
@@ -302,6 +334,10 @@ const handleUpdateShow = (value: boolean) => {
     border-color: #1789ff;
   }
 
+  &__pill--invalid {
+    border-color: #ff4d4f !important;
+  }
+
   &__pill-text {
     font-size: 14px;
     font-weight: 600;
@@ -351,6 +387,11 @@ const handleUpdateShow = (value: boolean) => {
     font-weight: 600;
     -webkit-tap-highlight-color: transparent;
   }
+
+  &__save--disabled,
+  &__save:disabled {
+    opacity: 0.5;
+  }
 }
 
 /* Vant picker style overrides */

+ 2 - 0
app/plugins/dayjs.ts

@@ -1,10 +1,12 @@
 import dayjs from 'dayjs'
 import utc from 'dayjs/plugin/utc'
 import timezone from 'dayjs/plugin/timezone'
+import customParseFormat from 'dayjs/plugin/customParseFormat'
 
 export default defineNuxtPlugin(() => {
   dayjs.extend(utc)
   dayjs.extend(timezone)
+  dayjs.extend(customParseFormat)
 
   // Set global default timezone to Jakarta (UTC+7)
   dayjs.tz.setDefault('Asia/Jakarta')