Kasir/resources/js/components/DatePicker.vue
2025-09-19 13:26:53 +07:00

207 lines
7.0 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(() => new Date().toISOString().split('T')[0])
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>