Teren-app/resources/js/Components/DateRangePicker.vue
Simon Pocrnjič 63e0958b66 Dev branch
2025-11-02 12:31:01 +01:00

232 lines
6.3 KiB
Vue

<script setup>
import { computed, ref } from "vue";
import { Button } from "@/Components/ui/button";
import { RangeCalendar } from "@/Components/ui/range-calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/Components/ui/popover";
import { cn } from "@/lib/utils";
import { CalendarIcon } from "lucide-vue-next";
import { format } from "date-fns";
import { sl } from "date-fns/locale";
import { CalendarDate } from "@internationalized/date";
import { DateFormatter, getLocalTimeZone } from "@internationalized/date";
const props = defineProps({
modelValue: {
type: [Object, null],
default: null,
},
placeholder: {
type: String,
default: "Izberi datumski obseg",
},
format: {
type: String,
default: "dd.MM.yyyy",
},
disabled: {
type: Boolean,
default: false,
},
id: {
type: String,
default: undefined,
},
error: {
type: [String, Array],
default: undefined,
},
});
const emit = defineEmits(["update:modelValue"]);
// Convert string dates to CalendarDate objects
const toCalendarDate = (val) => {
if (!val) return null;
if (val instanceof Date) {
return new CalendarDate(
val.getFullYear(),
val.getMonth() + 1,
val.getDate()
);
}
if (typeof val === "string") {
if (/^\d{4}-\d{2}-\d{2}$/.test(val)) {
const [year, month, day] = val.split("-").map(Number);
return new CalendarDate(year, month, day);
}
const dateObj = new Date(val);
if (!isNaN(dateObj.getTime())) {
return new CalendarDate(
dateObj.getFullYear(),
dateObj.getMonth() + 1,
dateObj.getDate()
);
}
}
return null;
};
// Convert CalendarDate to ISO string (YYYY-MM-DD)
const fromCalendarDate = (calendarDate) => {
if (!calendarDate) return null;
return `${String(calendarDate.year).padStart(4, "0")}-${String(calendarDate.month).padStart(2, "0")}-${String(calendarDate.day).padStart(2, "0")}`;
};
// Convert ISO string range to DateRange (CalendarDate objects)
const toDateRange = (value) => {
if (!value) return { start: null, end: null };
const start = toCalendarDate(value.start);
const end = toCalendarDate(value.end);
// Always return an object, even if both are null
return { start: start || null, end: end || null };
};
// Convert DateRange to ISO string range
const fromDateRange = (dateRange) => {
if (!dateRange || (!dateRange.start && !dateRange.end)) return null;
const start = fromCalendarDate(dateRange.start);
const end = fromCalendarDate(dateRange.end);
// Return null if both dates are null/empty
if (!start && !end) return null;
return {
start: start || null,
end: end || null,
};
};
// Date formatter for display
const df = new DateFormatter("sl-SI", {
dateStyle: "short",
});
const dateRange = computed({
get: () => {
const range = toDateRange(props.modelValue);
// RangeCalendar expects an object with start and end, not null
return range || { start: null, end: null };
},
set: (value) => {
// Only emit if value has actual dates, otherwise emit null
if (value && (value.start || value.end)) {
const isoRange = fromDateRange(value);
emit("update:modelValue", isoRange);
} else {
emit("update:modelValue", null);
}
},
});
// Format for display using DateRange (CalendarDate objects)
const formattedDateRange = computed(() => {
const range = dateRange.value;
if (!range || (!range.start && !range.end)) {
return props.placeholder;
}
try {
if (range.start && range.end) {
// Use DateFormatter if available, otherwise fall back to date-fns
try {
const startStr = df.format(range.start.toDate(getLocalTimeZone()));
const endStr = df.format(range.end.toDate(getLocalTimeZone()));
return `${startStr} - ${endStr}`;
} catch {
// Fallback to date-fns
const formatDate = (calendarDate) => {
if (!calendarDate) return "";
const dateObj = new Date(
calendarDate.year,
calendarDate.month - 1,
calendarDate.day
);
const formatMap = {
"dd.MM.yyyy": "dd.MM.yyyy",
"yyyy-MM-dd": "yyyy-MM-dd",
};
const dateFormat = formatMap[props.format] || "dd.MM.yyyy";
return format(dateObj, dateFormat, { locale: sl });
};
return `${formatDate(range.start)} - ${formatDate(range.end)}`;
}
}
if (range.start) {
try {
return df.format(range.start.toDate(getLocalTimeZone()));
} catch {
const dateObj = new Date(
range.start.year,
range.start.month - 1,
range.start.day
);
return format(dateObj, props.format || "dd.MM.yyyy", { locale: sl });
}
}
if (range.end) {
try {
return df.format(range.end.toDate(getLocalTimeZone()));
} catch {
const dateObj = new Date(
range.end.year,
range.end.month - 1,
range.end.day
);
return format(dateObj, props.format || "dd.MM.yyyy", { locale: sl });
}
}
return props.placeholder;
} catch {
return props.placeholder;
}
});
const open = ref(false);
</script>
<template>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button
:id="id"
variant="outline"
:class="
cn(
'w-full justify-start text-left font-normal',
(!props.modelValue?.start && !props.modelValue?.end) && 'text-muted-foreground',
error && 'border-red-500 focus:border-red-500 focus:ring-red-500'
)
"
:disabled="disabled"
>
<CalendarIcon class="mr-2 h-4 w-4" />
<template v-if="dateRange?.start">
<template v-if="dateRange.end">
{{ formattedDateRange }}
</template>
<template v-else>
{{ formattedDateRange }}
</template>
</template>
<template v-else>
{{ props.placeholder }}
</template>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0" align="start">
<RangeCalendar
v-model="dateRange"
:disabled="disabled"
:initial-focus="true"
:number-of-months="2"
/>
</PopoverContent>
</Popover>
<p v-if="error" class="mt-1 text-sm text-red-600">
{{ Array.isArray(error) ? error[0] : error }}
</p>
</template>