trypost/resources/js/components/DatePicker.vue
Paulo Castellano 07ddedceae fix: resolve all TypeScript errors
- Fix Auth type: add currentWorkspace, workspaces, FlashData
- Fix User.id type: number → string (UUID)
- Fix trans() number params: wrap with String()
- Fix debounce NodeJS.Timeout → ReturnType<typeof setTimeout>
- Fix DatePicker name prop: required → optional
- Fix DatePicker emit typing
- Fix storeStep1 → storeRole import in onboarding/Role
- Fix duplicate id in AcceptInvite interface
- Fix formatDate → formatDateTime in ApiKeys
- Add @unovis/vue and @unovis/ts type declarations
- Install @unovis/vue @unovis/ts dev dependencies
- Add type casts for Inertia/Reka UI component props
2026-03-31 09:34:39 -03:00

215 lines
No EOL
6.2 KiB
Vue

<script setup lang="ts">
import { parseDate } from '@internationalized/date';
import { IconCalendar } from '@tabler/icons-vue';
import { ref, watch, computed } from 'vue';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import dayjs from '@/dayjs';
const props = defineProps({
name: {
type: String,
default: '',
},
modelValue: {
type: String,
default: '',
},
align: {
type: String as () => 'start' | 'center' | 'end',
default: 'end',
validator: (value: string) => ['start', 'center', 'end'].includes(value),
},
disabled: {
type: Boolean,
default: false,
},
showTime: {
type: Boolean,
default: true,
},
placeholder: {
type: String,
default: '',
},
});
const emit = defineEmits<{
'update:modelValue': [value: string | null];
}>();
// Parse input value into date
const parseInput = (value: string) => {
if (!value) return undefined;
try {
const date = dayjs(value);
if (date.isValid()) {
return parseDate(date.format('YYYY-MM-DD'));
}
} catch {
return undefined;
}
return undefined;
};
const internalDate = ref(parseInput(props.modelValue));
const popoverOpen = ref(false);
// Time state (24-hour format)
const selectedHour = ref(
props.modelValue && dayjs(props.modelValue).isValid()
? dayjs(props.modelValue).format('HH')
: '09',
);
const selectedMinute = ref(
props.modelValue && dayjs(props.modelValue).isValid()
? dayjs(props.modelValue).format('mm')
: '00',
);
// Generate hours (00-23)
const hours = computed(() => {
return Array.from({ length: 24 }, (_, i) => {
return i.toString().padStart(2, '0');
});
});
// Generate minutes (00-59 in 5-minute intervals)
const minutes = computed(() => {
return Array.from({ length: 12 }, (_, i) => {
return (i * 5).toString().padStart(2, '0');
});
});
// Build full datetime string
const buildDateTime = (dateStr: string | null): string | null => {
if (!dateStr) return null;
if (!props.showTime) {
return dateStr;
}
const timeStr = `${selectedHour.value}:${selectedMinute.value}:00`;
return `${dateStr}T${timeStr}`;
};
const onTimeChange = () => {
if (internalDate.value) {
const dateStr = internalDate.value.toString();
emit('update:modelValue', buildDateTime(dateStr));
}
};
// Parse input value into date component
const isInternalUpdate = ref(false);
watch(
() => props.modelValue,
(newVal) => {
if (isInternalUpdate.value) {
isInternalUpdate.value = false;
return;
}
internalDate.value = parseInput(newVal);
if (newVal) {
const parsed = dayjs(newVal);
if (parsed.isValid()) {
if (props.showTime) {
selectedHour.value = parsed.format('HH');
selectedMinute.value = parsed.format('mm');
}
}
} else {
selectedHour.value = '09';
selectedMinute.value = '00';
}
},
{ immediate: true },
);
// Watch internal date changes from calendar
watch(internalDate, (newDate) => {
if (!newDate || isInternalUpdate.value) return;
isInternalUpdate.value = true;
const formatted = newDate.toString();
emit('update:modelValue', buildDateTime(formatted));
if (!props.showTime) {
popoverOpen.value = false;
}
});
// Display string for the button
const displayText = computed(() => {
if (!props.modelValue || !dayjs(props.modelValue).isValid()) {
return null;
}
const parsed = dayjs(props.modelValue);
if (props.showTime) {
return parsed.format('MMM D, YYYY [at] HH:mm');
}
return parsed.format('MMM D, YYYY');
});
</script>
<template>
<Popover v-model:open="popoverOpen">
<PopoverTrigger as-child :disabled="disabled">
<Button :id="name" type="button" variant="outline" class="w-full justify-between text-left font-normal"
:class="[{ 'text-muted-foreground': !displayText }, $attrs.class]" :disabled="disabled">
<span>{{ displayText || placeholder || $t('common.date_picker.select') }}</span>
<IconCalendar class="h-4 w-4 shrink-0" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0" :align="align">
<Calendar v-model="internalDate as any" :placeholder="(internalDate as any)" layout="month-and-year" locale="en"
calendar-label="Date picker" initial-focus />
<!-- Time Picker -->
<div v-if="showTime" class="border-t p-3">
<div class="flex items-center gap-2">
<Select v-model="selectedHour" @update:model-value="onTimeChange">
<SelectTrigger class="w-[70px]">
<SelectValue placeholder="HH" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="h in hours" :key="h" :value="h">
{{ h }}
</SelectItem>
</SelectContent>
</Select>
<span class="text-muted-foreground">:</span>
<Select v-model="selectedMinute" @update:model-value="onTimeChange">
<SelectTrigger class="w-[70px]">
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="m in minutes" :key="m" :value="m">
{{ m }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</PopoverContent>
</Popover>
</template>