214 lines
7.2 KiB
Vue
214 lines
7.2 KiB
Vue
<template>
|
|
<div class="relative" ref="datePickerRef">
|
|
<!-- Input Display -->
|
|
<div class="flex gap-2 items-center">
|
|
<div class="flex-1">
|
|
<label v-if="label" class="text-D/80 block text-sm font-medium mb-1">{{ label }}</label>
|
|
<div
|
|
@click="toggleCalendar"
|
|
class="w-full px-3 py-2 bg-A text-D border border-B rounded-md cursor-pointer hover:border-C focus-within:border-C focus-within:ring focus-within:ring-D focus-within:ring-opacity-50 transition-colors"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<span v-if="displayText" class="text-sm">{{ displayText }}</span>
|
|
<span v-else class="text-sm text-D/60">{{ placeholder }}</span>
|
|
<i class="fas fa-calendar-alt text-D/60"></i>
|
|
</div>
|
|
</div>
|
|
<div v-if="errorMessage" class="text-red-500 text-xs mt-1">{{ errorMessage }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Calendar Popup (inline, no teleport) -->
|
|
<div
|
|
v-if="showCalendar"
|
|
ref="popupRef"
|
|
class="absolute z-[9999] bg-A border border-C rounded-lg shadow-xl p-4 min-w-[300px] mt-2"
|
|
:class="popupPositionClass"
|
|
>
|
|
<!-- Manual Date Inputs -->
|
|
<div class="mb-4">
|
|
<div class="text-sm font-medium text-D mb-2">Pilih Manual</div>
|
|
<div class="flex gap-3 items-center">
|
|
<div class="flex-1">
|
|
<label class="text-xs text-D/80 block mb-1">Dari</label>
|
|
<input
|
|
type="date"
|
|
v-model="tempStartDate"
|
|
@input="validateDates"
|
|
:max="maxDate"
|
|
class="w-full text-xs px-2 py-2 bg-A text-D border border-B rounded focus:border-C focus:outline-none transition-colors"
|
|
/>
|
|
</div>
|
|
<span class="text-D/50 text-sm">s/d</span>
|
|
<div class="flex-1">
|
|
<label class="text-xs text-D/80 block mb-1">Sampai</label>
|
|
<input
|
|
type="date"
|
|
v-model="tempEndDate"
|
|
@input="validateDates"
|
|
:min="tempStartDate"
|
|
:max="maxDate"
|
|
class="w-full text-xs px-2 py-2 bg-A text-D border border-B rounded focus:border-C focus:outline-none transition-colors"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<!-- Range Info -->
|
|
<div v-if="tempStartDate && tempEndDate" class="text-xs text-D/60 mt-2">
|
|
{{ rangeDaysText }} ({{ formatDisplayDate(tempStartDate) }} - {{ formatDisplayDate(tempEndDate) }})
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex justify-between items-center pt-3 border-t border-C">
|
|
<button
|
|
@click="clearDates"
|
|
class="px-3 py-1 text-xs text-D/60 hover:text-D transition-colors"
|
|
>
|
|
Bersihkan
|
|
</button>
|
|
<div class="flex gap-2">
|
|
<button
|
|
@click="cancel"
|
|
class="px-4 py-1 text-xs bg-gray-200 hover:bg-gray-300 text-gray-700 rounded transition-colors"
|
|
>
|
|
Batal
|
|
</button>
|
|
<button
|
|
@click="confirm"
|
|
:disabled="!isValidRange"
|
|
class="px-4 py-1 text-xs bg-C hover:bg-C/80 text-D rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Terapkan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
|
|
|
const props = defineProps({
|
|
modelValue: { type: Object, default: () => ({ start: '', end: '' }) },
|
|
label: { type: String, default: 'Pilih Periode' },
|
|
placeholder: { type: String, default: 'Pilih rentang tanggal' },
|
|
maxDays: { type: Number, default: 31 },
|
|
position: { type: String, default: 'left', validator: (v) => ['left', 'right'].includes(v) }
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue', 'change'])
|
|
|
|
const datePickerRef = ref(null)
|
|
const showCalendar = ref(false)
|
|
const tempStartDate = ref('')
|
|
const tempEndDate = ref('')
|
|
const errorMessage = ref('')
|
|
|
|
const maxDate = computed(() => {
|
|
const today = new Date()
|
|
const year = today.getFullYear()
|
|
const month = String(today.getMonth() + 1).padStart(2, '0')
|
|
const day = String(today.getDate()).padStart(2, '0')
|
|
return `${year}-${month}-${day}`
|
|
})
|
|
|
|
|
|
const displayText = computed(() => {
|
|
if (props.modelValue.start && props.modelValue.end) {
|
|
const startFormatted = formatDisplayDate(props.modelValue.start)
|
|
const endFormatted = formatDisplayDate(props.modelValue.end)
|
|
return props.modelValue.start === props.modelValue.end
|
|
? startFormatted
|
|
: `${startFormatted} - ${endFormatted}`
|
|
}
|
|
return ''
|
|
})
|
|
|
|
const isValidRange = computed(() => {
|
|
if (!tempStartDate.value || !tempEndDate.value) return false
|
|
const start = new Date(tempStartDate.value)
|
|
const end = new Date(tempEndDate.value)
|
|
if (start > end) return false
|
|
const diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
|
return diffDays <= props.maxDays
|
|
})
|
|
|
|
const rangeDaysText = computed(() => {
|
|
if (!tempStartDate.value || !tempEndDate.value) return ''
|
|
const start = new Date(tempStartDate.value)
|
|
const end = new Date(tempEndDate.value)
|
|
const diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
|
return diffDays > props.maxDays
|
|
? `⚠️ Maksimal ${props.maxDays} hari`
|
|
: `${diffDays} hari`
|
|
})
|
|
|
|
const popupPositionClass = computed(() => props.position === 'right' ? 'right-0' : 'left-0')
|
|
|
|
const formatDisplayDate = (dateString) => {
|
|
const date = new Date(dateString + 'T00:00:00')
|
|
return date.toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' })
|
|
}
|
|
|
|
const toggleCalendar = () => {
|
|
showCalendar.value = !showCalendar.value
|
|
if (showCalendar.value) {
|
|
tempStartDate.value = props.modelValue.start
|
|
tempEndDate.value = props.modelValue.end
|
|
errorMessage.value = ''
|
|
}
|
|
}
|
|
|
|
const validateDates = () => {
|
|
errorMessage.value = ''
|
|
if (!tempStartDate.value || !tempEndDate.value) return
|
|
const start = new Date(tempStartDate.value)
|
|
const end = new Date(tempEndDate.value)
|
|
if (start > end) {
|
|
errorMessage.value = 'Tanggal akhir harus setelah tanggal mulai'
|
|
return
|
|
}
|
|
const diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
|
if (diffDays > props.maxDays) {
|
|
errorMessage.value = `Maksimal ${props.maxDays} hari`
|
|
}
|
|
}
|
|
|
|
const clearDates = () => {
|
|
tempStartDate.value = ''
|
|
tempEndDate.value = ''
|
|
errorMessage.value = ''
|
|
}
|
|
|
|
const cancel = () => {
|
|
showCalendar.value = false
|
|
errorMessage.value = ''
|
|
}
|
|
|
|
const confirm = () => {
|
|
if (isValidRange.value) {
|
|
const newValue = { start: tempStartDate.value, end: tempEndDate.value }
|
|
emit('update:modelValue', newValue)
|
|
emit('change', newValue)
|
|
showCalendar.value = false
|
|
}
|
|
}
|
|
|
|
const handleClickOutside = (e) => {
|
|
if (datePickerRef.value && !datePickerRef.value.contains(e.target)) {
|
|
showCalendar.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => document.addEventListener('click', handleClickOutside))
|
|
onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
|
|
|
watch(() => props.modelValue, (newValue) => {
|
|
if (newValue.start !== tempStartDate.value || newValue.end !== tempEndDate.value) {
|
|
tempStartDate.value = newValue.start
|
|
tempEndDate.value = newValue.end
|
|
}
|
|
}, { deep: true })
|
|
</script>
|