Compare commits

...
Author SHA1 Message Date
Jeppe Bundgaard d435c6d60a Fix local date selection handling 2026-07-06 14:46:14 +02:00
31 changed files with 354 additions and 98 deletions
@@ -5,6 +5,7 @@ import { useWindowSize } from "@vueuse/core";
import { BMessage } from "buefy"; import { BMessage } from "buefy";
import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
type DateRange = { type DateRange = {
startDate: Date, startDate: Date,
@@ -115,23 +116,11 @@ const availableYears = computed(() => (
)); ));
const formatDateInputValue = (date: Date) => { const formatDateInputValue = (date: Date) => {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) { return formatLocalDateOnly(date);
return "";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}; };
const parseDateInputValue = (value: string) => { const parseDateInputValue = (value: string) => {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); return parseLocalDateOnly(value);
if (!match) {
return new Date(value);
}
return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
}; };
const isSameDateInputValue = (left: Date, right: Date) => ( const isSameDateInputValue = (left: Date, right: Date) => (
@@ -30,6 +30,7 @@ import PosLastScannedLicensePlatesV2 from "@/components/displays/department/pos/
import PosDesktopOrderBookingSelectorModal from "@/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue"; import PosDesktopOrderBookingSelectorModal from "@/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue";
import PosDesktopCustomerConflictModal from "@/components/displays/department/pos/steps/elements/PosDesktopCustomerConflictModal.vue"; import PosDesktopCustomerConflictModal from "@/components/displays/department/pos/steps/elements/PosDesktopCustomerConflictModal.vue";
import PosDesktopDuplicateWarning from "@/components/displays/department/pos/steps/elements/PosDesktopDuplicateWarning.vue"; import PosDesktopDuplicateWarning from "@/components/displays/department/pos/steps/elements/PosDesktopDuplicateWarning.vue";
import { todayLocalDateOnly } from "@/services/dateOnly.js";
import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { parsePosRouteSearch } from "@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js"; import { parsePosRouteSearch } from "@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js";
import { import {
@@ -377,8 +378,8 @@ const fetchDuplicateOrdersForContext = async (context) => {
try { try {
const response = await SessionUser.request(SessionUser.objects.orders.meta.endpoint, "GET", { const response = await SessionUser.request(SessionUser.objects.orders.meta.endpoint, "GET", {
filters: `reg_1:${normalizedContext.reg1},department_id:${department_id.value},created_at-date_from:${ filters: `reg_1:${normalizedContext.reg1},department_id:${department_id.value},created_at-date_from:${
new Date().toISOString().split("T")[0] todayLocalDateOnly()
},created_at-date_to:${new Date().toISOString().split("T")[0]}`, },created_at-date_to:${todayLocalDateOnly()}`,
limit: 5, limit: 5,
}); });
@@ -12,6 +12,7 @@ import PosDepartmentStepMobileButtonNextStep from "@/components/displays/departm
import SessionUser from "@/components/session/token/SessionUser.vue"; import SessionUser from "@/components/session/token/SessionUser.vue";
import { PosOrder } from "@/components/displays/department/pos/steps/mobile/objects/PosOrder.vue"; import { PosOrder } from "@/components/displays/department/pos/steps/mobile/objects/PosOrder.vue";
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue"; import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
import { todayLocalDateOnly } from "@/services/dateOnly.js";
// Define the close event to emit when the component is closed // Define the close event to emit when the component is closed
const emit = defineEmits(["close"]); const emit = defineEmits(["close"]);
/** Display variables */ /** Display variables */
@@ -126,7 +127,7 @@ const liveTransactions = ref(null);
const isLoading = ref(true); const isLoading = ref(true);
const syncListTransactionHistory = async () => { const syncListTransactionHistory = async () => {
let dateToday = new Date().toISOString().split("T")[0]; // Get today's date in YYYY-MM-DD format let dateToday = todayLocalDateOnly(); // Get today's date in YYYY-MM-DD format
isLoading.value = true; isLoading.value = true;
// Fetch the list of orders created today for the current department // Fetch the list of orders created today for the current department
SessionUser.objects.orders.get SessionUser.objects.orders.get
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { formatLocalDateOnly } from '@/services/dateOnly.js';
type startEndDate = { type startEndDate = {
start: Date; // The start date of the range start: Date; // The start date of the range
end: Date; // The end date of the range end: Date; // The end date of the range
@@ -27,7 +28,7 @@ const datePresetFunctions = {
month: { month: {
// Get the first day of the month based on the provided date first day at 00:00:01 // Get the first day of the month based on the provided date first day at 00:00:01
firstDayOfMonth: (date: Date): Date => { firstDayOfMonth: (date: Date): Date => {
return new Date(date.getFullYear(), date.getMonth(), 2, 0, 0, 1, 0); // Set to the first day of the month at 00:00:01 return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0); // Set to the first day of the month at 00:00:00
}, },
// Get the last day of the month based on the provided date last day at 23:59:59 // Get the last day of the month based on the provided date last day at 23:59:59
lastDayOfMonth: (date: Date): Date => { lastDayOfMonth: (date: Date): Date => {
@@ -116,7 +117,7 @@ export const datePresets = <datePreset[]>[
*/ */
const convertToISO = (date: Date): string => { const convertToISO = (date: Date): string => {
return date.toISOString().split('T')[0]; // Convert to YYYY-MM-DD format return formatLocalDateOnly(date); // Convert to YYYY-MM-DD format
}; };
export const dateFunctions = { export const dateFunctions = {
@@ -150,4 +151,4 @@ export default defineComponent({
}, },
}); });
</script> </script>
<template></template> <template></template>
@@ -50,6 +50,7 @@ import ShowErrorField from "@/components/global/ShowErrorField.vue";
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { BSwitch } from "buefy"; import { BSwitch } from "buefy";
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js"; import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n() const { t } = useI18n()
const router = useRouter(); const router = useRouter();
@@ -63,7 +64,7 @@ const parseInitialDate = (value) => {
return new Date(); return new Date();
} }
const parsed = new Date(`${value}T00:00:00`); const parsed = parseLocalDateOnly(value);
return Number.isNaN(parsed.getTime()) ? new Date() : parsed; return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
}; };
@@ -129,7 +130,7 @@ const dateFrom = ref(parseInitialDate(props.initialDateFrom));
const dateTo = ref(parseInitialDate(props.initialDateTo)); const dateTo = ref(parseInitialDate(props.initialDateTo));
const parsedDate = (date) => { const parsedDate = (date) => {
return new Date(date); return parseLocalDateOnly(date);
}; };
const reloadScheduled = ref(false); const reloadScheduled = ref(false);
@@ -148,14 +149,14 @@ const actions = {
from: { from: {
select: (date) => { select: (date) => {
dateFrom.value = parsedDate(date); dateFrom.value = parsedDate(date);
setFilter("StartTime-date_from", date.toISOString().split("T")[0], false); setFilter("StartTime-date_from", formatLocalDateOnly(date), false);
scheduleReload(); scheduleReload();
} }
}, },
to: { to: {
select: (date) => { select: (date) => {
dateTo.value = parsedDate(date); dateTo.value = parsedDate(date);
setFilter("StartTime-date_to", date.toISOString().split("T")[0], false); setFilter("StartTime-date_to", formatLocalDateOnly(date), false);
scheduleReload(); scheduleReload();
} }
}, },
@@ -109,8 +109,8 @@ import OrdersTable from "@/components/displays/department/pos/orders/ordersTable
import {SessionUser} from "@/components/session/token/SessionUser.vue"; import {SessionUser} from "@/components/session/token/SessionUser.vue";
import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue"; import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue";
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue"; import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
import {now} from "@vueuse/core";
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { formatLocalDateOnly, parseLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n() const { t } = useI18n()
const router = useRouter(); const router = useRouter();
@@ -176,8 +176,8 @@ if (props.autoLoad) {
const date_from = ref(null); const date_from = ref(null);
const date_to = ref(null); const date_to = ref(null);
const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => { const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => {
const formattedStartDate = newSelectionStartDate.toISOString().split("T")[0]; const formattedStartDate = formatLocalDateOnly(newSelectionStartDate);
const formattedEndDate = newSelectionToDate.toISOString().split("T")[0]; const formattedEndDate = formatLocalDateOnly(newSelectionToDate);
date_from.value = formattedStartDate; date_from.value = formattedStartDate;
date_to.value = formattedEndDate; date_to.value = formattedEndDate;
setFilter("created_at-date_from", formattedStartDate, true); setFilter("created_at-date_from", formattedStartDate, true);
@@ -361,7 +361,7 @@ const doesEndpointMatch = (matcher) => {
<!-- Shortcuts for date filters --> <!-- Shortcuts for date filters -->
<DatePeriodSelector :on-selection-change="onDateRangeSelected" <DatePeriodSelector :on-selection-change="onDateRangeSelected"
:visibility="{ showDailySelector: false, showWeeklySelector: false, showMultipleMonthWarning: false, showUpdateButton: false, showMonthSelector: false, showStartDate: false, showEndDate: false, showSelectionValidity: false, showYearSelector: false, showToLabel: false }" :visibility="{ showDailySelector: false, showWeeklySelector: false, showMultipleMonthWarning: false, showUpdateButton: false, showMonthSelector: false, showStartDate: false, showEndDate: false, showSelectionValidity: false, showYearSelector: false, showToLabel: false }"
v-bind:selection="{ startDate: date_from ? new Date(date_from) : new Date(now()), endDate: date_to ? new Date(date_to) : new Date(now()) }"/> v-bind:selection="{ startDate: parseLocalDateOnly(date_from || todayLocalDateOnly()), endDate: parseLocalDateOnly(date_to || todayLocalDateOnly()) }"/>
</div> </div>
</template> </template>
<template #default> <template #default>
@@ -28,6 +28,7 @@ import {ref, watch} from "vue";
import { Colors } from "@/ThemeConfig.vue"; import { Colors } from "@/ThemeConfig.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { todayLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n() const { t } = useI18n()
const router = useRouter(); const router = useRouter();
@@ -52,7 +53,7 @@ watch(() => getFilter("status"), (value) => {
// If the route starts with /user, set the endpoint to /user/bookings // If the route starts with /user, set the endpoint to /user/bookings
if (router.currentRoute.value.path.startsWith("/user")) { if (router.currentRoute.value.path.startsWith("/user")) {
setEndpoint("/user/bookings", false); setEndpoint("/user/bookings", false);
setFilter("date", new Date().toISOString().split("T")[0], false); setFilter("date", todayLocalDateOnly(), false);
setOrder("created_at", "desc"); setOrder("created_at", "desc");
} }
// If the route starts with /superuser, set the endpoint to /bookings // If the route starts with /superuser, set the endpoint to /bookings
@@ -81,7 +82,7 @@ loadList();
const showingToday = ref(true); const showingToday = ref(true);
watch(() => getFilter("date"), (value) => { watch(() => getFilter("date"), (value) => {
showingToday.value = value === new Date().toISOString().split("T")[0]; showingToday.value = value === todayLocalDateOnly();
}); });
// If the screen is mobile, set the is small variable to true // If the screen is mobile, set the is small variable to true
@@ -151,7 +152,7 @@ const showNewOrderBookingsPortal = () => {
<label class="label">{{ isSmall ? t('common.today') : t('pagination.show_only_today') }}</label> <label class="label">{{ isSmall ? t('common.today') : t('pagination.show_only_today') }}</label>
<div class="control"> <div class="control">
<div class="field"> <div class="field">
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? new Date().toISOString().split('T')[0] : '*') }" checked="checked" :class="{ 'is-link': showingToday }" /> <input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? todayLocalDateOnly() : '*') }" checked="checked" :class="{ 'is-link': showingToday }" />
<label for="today"></label> <label for="today"></label>
</div> </div>
</div> </div>
@@ -188,4 +189,4 @@ const showNewOrderBookingsPortal = () => {
<style scoped> <style scoped>
</style> </style>
@@ -8,6 +8,7 @@ import OrderBookingsTable from "@/views/dashboards/userDashboard/bookings/displa
import { departments, getDepartments } from "@/components/pagination/departmentTabs.vue"; import { departments, getDepartments } from "@/components/pagination/departmentTabs.vue";
import { Colors } from "@/ThemeConfig.vue"; import { Colors } from "@/ThemeConfig.vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { endOfLocalDate, startOfLocalDate, todayLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n(); const { t } = useI18n();
/** /**
@@ -52,11 +53,9 @@ const onOnlyTodayFilterChange = (event, autoLoadList = true) => {
setFilter("datetime-date_from", null, false); setFilter("datetime-date_from", null, false);
setFilter("datetime-date_to", null, false); setFilter("datetime-date_to", null, false);
} else { } else {
const startOfDay = new Date().setHours(0, 0, 0, 0);
const endOfDay = new Date().setHours(23, 59, 59, 999);
//setFilter('datetime', null, false); //setFilter('datetime', null, false);
setFilter("datetime-date_from", new Date(startOfDay).toISOString(), false); setFilter("datetime-date_from", startOfLocalDate(val).toISOString(), false);
setFilter("datetime-date_to", new Date(endOfDay).toISOString(), false); setFilter("datetime-date_to", endOfLocalDate(val).toISOString(), false);
} }
if (autoLoadList) { if (autoLoadList) {
loadList(); loadList();
@@ -95,7 +94,7 @@ onMounted(() => {
departmentFilter.value = value; departmentFilter.value = value;
} else if (key === "only_today" && value === true) { } else if (key === "only_today" && value === true) {
setFilterKey = false; // Since the only_today filter is handled separately setFilterKey = false; // Since the only_today filter is handled separately
onOnlyTodayFilterChange({ target: { value: new Date().toISOString().split("T")[0] } }, false); onOnlyTodayFilterChange({ target: { value: todayLocalDateOnly() } }, false);
} }
if (setFilterKey) { if (setFilterKey) {
setFilter(key, value, false); setFilter(key, value, false);
@@ -170,7 +169,7 @@ onMounted(() => {
<div class="select"> <div class="select">
<select :value="onlyTodayFilter" @change="onOnlyTodayFilterChange"> <select :value="onlyTodayFilter" @change="onOnlyTodayFilterChange">
<option value="*">{{ t("common.all") }}</option> <option value="*">{{ t("common.all") }}</option>
<option :value="new Date().toISOString().split('T')[0]">{{ t("common.yes") }}</option> <option :value="todayLocalDateOnly()">{{ t("common.yes") }}</option>
</select> </select>
</div> </div>
</div> </div>
@@ -193,7 +192,7 @@ onMounted(() => {
@change=" @change="
(event) => { (event) => {
onOnlyTodayFilterChange({ onOnlyTodayFilterChange({
target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' }, target: { value: event.target.checked ? todayLocalDateOnly() : '*' },
}); });
} }
" "
@@ -226,7 +225,7 @@ onMounted(() => {
@change=" @change="
(event) => { (event) => {
onOnlyTodayFilterChange( onOnlyTodayFilterChange(
{ target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' } }, { target: { value: event.target.checked ? todayLocalDateOnly() : '*' } },
false false
); );
onOrderIdFilterChange({ target: { value: event.target.checked ? 'is null' : '*' } }); onOrderIdFilterChange({ target: { value: event.target.checked ? 'is null' : '*' } });
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import PaginationDisplayItemColumn from "@/components/displays/pagination/PaginationDisplayItemColumn.vue"; import PaginationDisplayItemColumn from "@/components/displays/pagination/PaginationDisplayItemColumn.vue";
import { ref, computed } from "vue"; import { computed } from "vue";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
const props = defineProps({ const props = defineProps({
label: { label: {
type: String, type: String,
@@ -19,7 +20,7 @@ defineExpose({
}); });
const formattedDate = computed(() => { const formattedDate = computed(() => {
return date.value.toISOString().split('T')[0]; return formatLocalDateOnly(date.value);
}); });
// This component is used to display a date input in a pagination display item column. // This component is used to display a date input in a pagination display item column.
@@ -36,8 +37,7 @@ const formattedDate = computed(() => {
class="input" class="input"
:value="formattedDate" :value="formattedDate"
@input="(e) => { @input="(e) => {
const newDate = new Date(e.target.value); const newDate = parseLocalDateOnly(e.target.value);
console.log('Selected date:', newDate);
emit('update:date', newDate); emit('update:date', newDate);
}" }"
/> />
@@ -47,4 +47,4 @@ const formattedDate = computed(() => {
<style scoped> <style scoped>
</style> </style>
@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { SessionUser } from '@/components/session/token/SessionUser.vue'; import { SessionUser } from '@/components/session/token/SessionUser.vue';
@@ -10,7 +9,7 @@ import PaginationDisplayTemplateButton
from "@/components/displays/pagination/templates/PaginationDisplayTemplateButton.vue"; from "@/components/displays/pagination/templates/PaginationDisplayTemplateButton.vue";
import { datePresets , dateFunctions} from '@/components/displays/pagination/PaginationDisplayDates.vue'; import { datePresets , dateFunctions} from '@/components/displays/pagination/PaginationDisplayDates.vue';
const props = defineProps({ defineProps({
startDate: { startDate: {
type: Date, type: Date,
required: true, required: true,
@@ -46,6 +45,7 @@ function updateStartDate(date: Date) {
// Update the end date model when the date is changed // Update the end date model when the date is changed
function updateEndDate(date: Date) { function updateEndDate(date: Date) {
endDateModel.value = date; endDateModel.value = date;
emits('update:endDate', date);
} }
</script> </script>
@@ -79,4 +79,4 @@ function updateEndDate(date: Date) {
<style scoped> <style scoped>
</style> </style>
@@ -13,6 +13,7 @@ import { showDownloadWashCertificate } from "@/components/shop/DownloadWashCerti
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue"; import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import Swal from 'sweetalert2'; import Swal from 'sweetalert2';
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue"; import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
import { todayLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
const redirectBookingObjectPage = (objectId) => { const redirectBookingObjectPage = (objectId) => {
// Send the user to the object page // Send the user to the object page
window.location.href = `/user/bookings/${objectId}`; window.location.href = `/user/bookings/${objectId}`;
@@ -237,7 +238,8 @@ window.addEventListener('resize', () => {
* To organize the bookings, we want to show the bookings that are created today first, then yesterday, then all other days * To organize the bookings, we want to show the bookings that are created today first, then yesterday, then all other days
* @type {string} * @type {string}
*/ */
const currentDate = new Date().toISOString().split("T")[0]; const currentDate = todayLocalDateOnly();
const previousDate = yesterdayLocalDateOnly();
/** /**
* Sort the bookings by date * Sort the bookings by date
@@ -362,7 +364,7 @@ const canUserEditObject = (object) => {
</span> </span>
<span class="has-text-grey"> <span class="has-text-grey">
<!-- Human readable date (Today, Yesterday, etc.) --> <!-- Human readable date (Today, Yesterday, etc.) -->
{{ object.date === currentDate ? $t('tables.bookings.today') : object.date === new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split("T")[0] ? $t('tables.bookings.yesterday') : object.date }} {{ object.date === currentDate ? $t('tables.bookings.today') : object.date === previousDate ? $t('tables.bookings.yesterday') : object.date }}
<!-- Number of bookings on the date --> <!-- Number of bookings on the date -->
({{ $t('tables.bookings.bookings_count', { count: objects.filter((booking) => booking.date === object.date).length }) }}) ({{ $t('tables.bookings.bookings_count', { count: objects.filter((booking) => booking.date === object.date).length }) }})
</span> </span>
+4 -3
View File
@@ -4,6 +4,7 @@ import { ref, watch, onMounted } from "vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { isAccessibleVisibleNamedDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js"; import { isAccessibleVisibleNamedDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js";
import { todayLocalDateOnly } from "@/services/dateOnly.js";
// Get the department ID from the URL // Get the department ID from the URL
const route = useRoute(); const route = useRoute();
const department_id = route.params.departmentId || SessionUser.functions.getDepartmentIdFromUrl() || null; const department_id = route.params.departmentId || SessionUser.functions.getDepartmentIdFromUrl() || null;
@@ -73,9 +74,9 @@ const emphasis = ref({
"department:" + "department:" +
department_id + department_id +
",status:pending,date-date_from:" + ",status:pending,date-date_from:" +
new Date().toISOString().split("T")[0] + todayLocalDateOnly() +
",date-date_to:" + ",date-date_to:" +
new Date().toISOString().split("T")[0], todayLocalDateOnly(),
limit: 1, // Limit to 1 booking, we only need to know if there are any bookings or not limit: 1, // Limit to 1 booking, we only need to know if there are any bookings or not
page: 1, page: 1,
}) })
@@ -97,7 +98,7 @@ const emphasis = ref({
* Check if the department has a daily report for today * Check if the department has a daily report for today
*/ */
SessionUser.request(SessionUser.objects.department_daily_reports.meta.endpoint + "/get", "GET", { SessionUser.request(SessionUser.objects.department_daily_reports.meta.endpoint + "/get", "GET", {
date: new Date().toISOString().split("T")[0], date: todayLocalDateOnly(),
id: department_id, // This refers to the department ID id: department_id, // This refers to the department ID
}) })
.then((response) => { .then((response) => {
@@ -5,6 +5,7 @@ import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/Ob
import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {ref} from "vue"; import {ref} from "vue";
import i18n from '@/i18n'; import i18n from '@/i18n';
import { todayLocalDateOnly } from "@/services/dateOnly.js";
const t = (key) => i18n.global.t(key); const t = (key) => i18n.global.t(key);
@@ -241,7 +242,7 @@ export const Bookings = {
'/bookings', '/bookings',
'GET', 'GET',
{ {
filters: `department:${department},status:pending,date-date_from:${new Date().toISOString().split('T')[0]},date-date_to:${new Date().toISOString().split('T')[0]}`, filters: `department:${department},status:pending,date-date_from:${todayLocalDateOnly()},date-date_to:${todayLocalDateOnly()}`,
limit: 100, limit: 100,
page: 1 page: 1
} }
@@ -289,4 +290,4 @@ export const Bookings = {
); );
} }
}; };
</script> </script>
@@ -5,6 +5,7 @@ import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/Ob
import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {ref} from "vue"; import {ref} from "vue";
import i18n from '@/i18n'; import i18n from '@/i18n';
import { todayLocalDateOnly } from "@/services/dateOnly.js";
const t = (key) => i18n.global.t(key); const t = (key) => i18n.global.t(key);
@@ -80,7 +81,7 @@ const t = (key) => i18n.global.t(key);
water_usage_morning: parseInt(water_usage_morning), water_usage_morning: parseInt(water_usage_morning),
water_usage: parseInt(water_usage), water_usage: parseInt(water_usage),
notes: notes ? notes : 'Daily report', notes: notes ? notes : 'Daily report',
date: date ? date : new Date().toISOString().split('T')[0] date: date ? date : todayLocalDateOnly()
} }
); );
}, },
@@ -1,6 +1,7 @@
<script> <script>
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue"; import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { formatLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
/** /**
* The DepartmentTimeBookingsEntries object * The DepartmentTimeBookingsEntries object
@@ -230,10 +231,10 @@ export const DepartmentTimeBookingsEntries = {
if (!dateFrom) { if (!dateFrom) {
const lastWeek = new Date(); const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7); lastWeek.setDate(lastWeek.getDate() - 7);
dateFrom = lastWeek.toISOString().split('T')[0]; // Format as YYYY-MM-DD dateFrom = formatLocalDateOnly(lastWeek); // Format as YYYY-MM-DD
} }
if (!dateTo) { if (!dateTo) {
dateTo = new Date().toISOString().split('T')[0]; // Format as YYYY-MM-DD dateTo = todayLocalDateOnly(); // Format as YYYY-MM-DD
} }
console.log("Fetching public available times for department:", departmentId, "from", dateFrom, "to", dateTo); console.log("Fetching public available times for department:", departmentId, "from", dateFrom, "to", dateTo);
// Make the request to fetch public available times for the department // Make the request to fetch public available times for the department
@@ -279,4 +280,4 @@ export const DepartmentTimeBookingsEntries = {
); );
} }
}; };
</script> </script>
+84
View File
@@ -0,0 +1,84 @@
const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
const DATE_ONLY_PREFIX_PATTERN = /^(\d{4}-\d{2}-\d{2})/;
const padDatePart = (value) => String(value).padStart(2, "0");
export const isValidDate = (value) => value instanceof Date && !Number.isNaN(value.getTime());
export const formatLocalDateOnly = (value = new Date()) => {
if (typeof value === "string") {
const directMatch = value.trim().match(DATE_ONLY_PREFIX_PATTERN);
if (directMatch) {
return directMatch[1];
}
}
const date = value instanceof Date ? value : new Date(value);
if (!isValidDate(date)) {
return "";
}
return [
date.getFullYear(),
padDatePart(date.getMonth() + 1),
padDatePart(date.getDate()),
].join("-");
};
export const parseLocalDateOnly = (value) => {
if (value instanceof Date) {
return isValidDate(value)
? new Date(value.getFullYear(), value.getMonth(), value.getDate())
: new Date(Number.NaN);
}
const stringValue = String(value ?? "").trim();
const match = DATE_ONLY_PATTERN.exec(stringValue);
if (match) {
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const parsed = new Date(year, month - 1, day);
if (
parsed.getFullYear() === year
&& parsed.getMonth() === month - 1
&& parsed.getDate() === day
) {
return parsed;
}
return new Date(Number.NaN);
}
const parsed = new Date(stringValue);
return isValidDate(parsed)
? new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate())
: parsed;
};
export const todayLocalDateOnly = () => formatLocalDateOnly(new Date());
export const yesterdayLocalDateOnly = () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return formatLocalDateOnly(yesterday);
};
export const startOfLocalDate = (value) => {
const date = parseLocalDateOnly(value);
if (!isValidDate(date)) {
return date;
}
date.setHours(0, 0, 0, 0);
return date;
};
export const endOfLocalDate = (value) => {
const date = parseLocalDateOnly(value);
if (!isValidDate(date)) {
return date;
}
date.setHours(23, 59, 59, 999);
return date;
};
@@ -19,6 +19,7 @@ import DepartmentDashboardDailyReportNavigation from "@/views/dashboards/departm
import DepartmentDashboardDailyReportProductSales from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportProductSales.vue"; import DepartmentDashboardDailyReportProductSales from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportProductSales.vue";
import DepartmentDashboardDailyReportTodayForm from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportTodayForm.vue"; import DepartmentDashboardDailyReportTodayForm from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportTodayForm.vue";
import DepartmentWeather from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentWeather.vue"; import DepartmentWeather from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentWeather.vue";
import { parseLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
import { import {
complaints_metric_state, complaints_metric_state,
count_bookings, count_bookings,
@@ -131,7 +132,7 @@ onMounted(() => {
return [...accessibleDepartmentIds.value]; return [...accessibleDepartmentIds.value];
})(); })();
const initialDateFrom = typeof query.dateFrom === "string" ? query.dateFrom : new Date().toISOString().split("T")[0]; const initialDateFrom = typeof query.dateFrom === "string" ? query.dateFrom : todayLocalDateOnly();
const initialDateTo = typeof query.dateTo === "string" ? query.dateTo : initialDateFrom; const initialDateTo = typeof query.dateTo === "string" ? query.dateTo : initialDateFrom;
initializeDailyReportFilters({ initializeDailyReportFilters({
@@ -170,7 +171,7 @@ watch(
); );
const formatDateShort = (dateString) => { const formatDateShort = (dateString) => {
const date = new Date(dateString); const date = parseLocalDateOnly(dateString);
const dayName = date.toLocaleDateString("da-DK", { weekday: "long" }); const dayName = date.toLocaleDateString("da-DK", { weekday: "long" });
const dateValue = date.toLocaleDateString("da-DK", { day: "2-digit", month: "2-digit", year: "2-digit" }); const dateValue = date.toLocaleDateString("da-DK", { day: "2-digit", month: "2-digit", year: "2-digit" });
return `${dayName} d. ${dateValue}`; return `${dayName} d. ${dateValue}`;
@@ -178,7 +179,7 @@ const formatDateShort = (dateString) => {
const formatDateRangeSubtitle = (startDateString, endDateString) => { const formatDateRangeSubtitle = (startDateString, endDateString) => {
const dateOptions = { day: "2-digit", month: "2-digit", year: "2-digit" }; const dateOptions = { day: "2-digit", month: "2-digit", year: "2-digit" };
return `${new Date(startDateString).toLocaleDateString("da-DK", dateOptions)} - ${new Date(endDateString).toLocaleDateString("da-DK", dateOptions)}`; return `${parseLocalDateOnly(startDateString).toLocaleDateString("da-DK", dateOptions)} - ${parseLocalDateOnly(endDateString).toLocaleDateString("da-DK", dateOptions)}`;
}; };
const formatSubtitle = (subtitle) => { const formatSubtitle = (subtitle) => {
@@ -2,18 +2,16 @@
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { createEmptyOutsideHours, normalizeOutsideHours } from "@/views/dashboards/departmentDashboard/modules/daily-report/outsideHours.js"; import { createEmptyOutsideHours, normalizeOutsideHours } from "@/views/dashboards/departmentDashboard/modules/daily-report/outsideHours.js";
import { todayLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
const GENERATE_UNIQUE_ID = () => Math.random().toString(36).slice(2, 11); const GENERATE_UNIQUE_ID = () => Math.random().toString(36).slice(2, 11);
const DATE_TODAY = () => { const DATE_TODAY = () => {
const date = new Date(); return todayLocalDateOnly();
return date.toISOString().split("T")[0];
}; };
const DATE_YESTERDAY = () => { const DATE_YESTERDAY = () => {
const date = new Date(); return yesterdayLocalDateOnly();
date.setDate(date.getDate() - 1);
return date.toISOString().split("T")[0];
}; };
const date_shortcuts = ref({ const date_shortcuts = ref({
@@ -17,6 +17,7 @@ import {
isLoading as departmentsStoreLoading, isLoading as departmentsStoreLoading,
} from "@/components/pagination/departmentTabs.vue"; } from "@/components/pagination/departmentTabs.vue";
import { isAccessibleVisibleDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js"; import { isAccessibleVisibleDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
/** /**
* Props: * Props:
@@ -141,14 +142,7 @@ const toggleAllDepartments = () => {
}; };
const formatDateSelectionValue = (date) => { const formatDateSelectionValue = (date) => {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) { return formatLocalDateOnly(date);
return "";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}; };
const onDateSelectionChange = (startDate, endDate) => { const onDateSelectionChange = (startDate, endDate) => {
@@ -161,7 +155,7 @@ const onDateSelectionChange = (startDate, endDate) => {
<div class="column is-12" data-testid="daily-report-date-controls"> <div class="column is-12" data-testid="daily-report-date-controls">
<DatePeriodSelector <DatePeriodSelector
:on-selection-change="onDateSelectionChange" :on-selection-change="onDateSelectionChange"
:selection="{ startDate: new Date(selected_date), endDate: new Date(selected_date_to) }" :selection="{ startDate: parseLocalDateOnly(selected_date), endDate: parseLocalDateOnly(selected_date_to) }"
:visibility="{ :visibility="{
showDailySelector: true, showDailySelector: true,
showWeeklySelector: true, showWeeklySelector: true,
@@ -5,6 +5,7 @@ import { parseError, getError, clearErrors} from "@/components/request/HandleGlo
import ShowErrorField from "@/components/global/ShowErrorField.vue"; import ShowErrorField from "@/components/global/ShowErrorField.vue";
import { selected_date, onChangeCall, selected_department_id} from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue"; import { selected_date, onChangeCall, selected_department_id} from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import { parseLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
const props = defineProps({ const props = defineProps({
department_id: Number, department_id: Number,
@@ -23,7 +24,7 @@ const created_at = ref(null);
const id = ref(null); const id = ref(null);
const message = ref(''); const message = ref('');
const yesterday = ref({ const yesterday = ref({
date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split('T')[0], date: yesterdayLocalDateOnly(),
water_usage: 0, water_usage: 0,
water_usage_morning: 0, water_usage_morning: 0,
notes: null, notes: null,
@@ -104,7 +105,7 @@ const getDailyReport = async () => {
// Check if the date is today // Check if the date is today
const created_at_val = new Date(response.data.data.created_at); const created_at_val = new Date(response.data.data.created_at);
const today = new Date(); const today = new Date();
if (created_at_val.toDateString() === (selected_date.value ? new Date(selected_date.value) : today).toDateString()) { if (created_at_val.toDateString() === (selected_date.value ? parseLocalDateOnly(selected_date.value) : today).toDateString()) {
// The daily report is today // The daily report is today
// So we can set the values // So we can set the values
water_usage.value = parseInt(response.data.data.water_usage); water_usage.value = parseInt(response.data.data.water_usage);
@@ -212,4 +213,4 @@ onChangeCall(() => {
<style scoped> <style scoped>
</style> </style>
@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { timeBookingsNewScheduler } from '@/views/dashboards/departmentDashboard/modules/time-bookings/book/TimeBookingsNewScheduler.vue'; import { timeBookingsNewScheduler } from '@/views/dashboards/departmentDashboard/modules/time-bookings/book/TimeBookingsNewScheduler.vue';
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
const date = ref(); const date = ref();
// Set the initial date to the current date YYYY-MM-DD format // Set the initial date to the current date YYYY-MM-DD format
const formatDate = (date_input) => { const formatDate = (date_input) => {
return date_input.toISOString().split('T')[0]; return formatLocalDateOnly(date_input);
}; };
date.value = formatDate(new Date()); date.value = formatDate(new Date());
@@ -15,7 +16,7 @@ watch(timeBookingsNewScheduler.date.selected, (newDate) => {
watch(date, (newDate) => { watch(date, (newDate) => {
// Update the selected date in the scheduler when the date input changes // Update the selected date in the scheduler when the date input changes
timeBookingsNewScheduler.date.selected.value = new Date(newDate); timeBookingsNewScheduler.date.selected.value = parseLocalDateOnly(newDate);
}); });
</script> </script>
@@ -63,4 +64,4 @@ watch(date, (newDate) => {
<style scoped> <style scoped>
</style> </style>
@@ -1,6 +1,7 @@
<script setup> <script setup>
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { SessionUser } from '@/components/session/token/SessionUser.vue'; import { SessionUser } from '@/components/session/token/SessionUser.vue';
import { todayLocalDateOnly } from "@/services/dateOnly.js";
const props = defineProps({ const props = defineProps({
department_id: { department_id: {
@@ -68,8 +69,7 @@ const getBookings = async () => {
}; };
const getDate = () => { const getDate = () => {
const date = new Date(); return todayLocalDateOnly();
return date.toISOString().split('T')[0];
}; };
// Get the department // Get the department
@@ -2,6 +2,7 @@
import { selected_date, selected_date_to, selectDate as selectDailyReportDate } from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue"; import { selected_date, selected_date_to, selectDate as selectDailyReportDate } from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue"; import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
// Emits for parents that want to react to date changes (non-breaking if unused) // Emits for parents that want to react to date changes (non-breaking if unused)
const emit = defineEmits(['date-change']); const emit = defineEmits(['date-change']);
@@ -16,12 +17,12 @@ const selectDate = (startIso, endIso) => {
<template> <template>
<DatePeriodSelector <DatePeriodSelector
:on-selection-change="(startDate, endDate) => selectDate(startDate.toISOString().split('T')[0], endDate.toISOString().split('T')[0])" :on-selection-change="(startDate, endDate) => selectDate(formatLocalDateOnly(startDate), formatLocalDateOnly(endDate))"
:selection="{ startDate: new Date(selected_date), endDate: new Date(selected_date_to) }" :selection="{ startDate: parseLocalDateOnly(selected_date), endDate: parseLocalDateOnly(selected_date_to) }"
:visibility="{ showDailySelector: true, showWeeklySelector: true, showMultipleMonthWarning: false, showUpdateButton: false }" :visibility="{ showDailySelector: true, showWeeklySelector: true, showMultipleMonthWarning: false, showUpdateButton: false }"
:reverse-level-order="true" :reverse-level-order="true"
@update:selection="(newSelection) => { @update:selection="(newSelection) => {
selectDate(newSelection.startDate.toISOString().split('T')[0], newSelection.endDate.toISOString().split('T')[0]); selectDate(formatLocalDateOnly(newSelection.startDate), formatLocalDateOnly(newSelection.endDate));
}" }"
/> />
</template> </template>
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {defineComponent, ref, watch} from 'vue'; import {defineComponent, ref, watch} from 'vue';
import { todayLocalDateOnly } from "@/services/dateOnly.js";
type fetch = { type fetch = {
@@ -11,8 +12,8 @@ type fetch = {
}; };
/** Define the variables */ /** Define the variables */
export const show_this_week = ref(false); export const show_this_week = ref(false);
export const selected_date = ref(new Date().toISOString().split('T')[0]); export const selected_date = ref(todayLocalDateOnly());
export const selected_date_to = ref(new Date().toISOString().split('T')[0]); export const selected_date_to = ref(todayLocalDateOnly());
export const last_fetch_id = ref(0); export const last_fetch_id = ref(0);
export const fetches = ref<fetch[]>([]); export const fetches = ref<fetch[]>([]);
/** Define the functions */ /** Define the functions */
@@ -53,4 +54,4 @@ watch([selected_date, selected_date_to], () => {
export default defineComponent({ export default defineComponent({
name: 'DepartmentsOverviewObject' name: 'DepartmentsOverviewObject'
}); });
</script> </script>
@@ -13,6 +13,7 @@ import StatisticsDepartmentGoal
from "@/views/dashboards/superUserDashboard/statistics/displays/overview/StatisticsDepartmentGoal.vue"; from "@/views/dashboards/superUserDashboard/statistics/displays/overview/StatisticsDepartmentGoal.vue";
import {departments, getDepartments} from "@/components/pagination/departmentTabs.vue"; import {departments, getDepartments} from "@/components/pagination/departmentTabs.vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { todayLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n(); const { t } = useI18n();
@@ -70,7 +71,8 @@ getDepartments()
// Get the statistics // Get the statistics
const getStatistics = async () => { const getStatistics = async () => {
// Get the statistics // Get the statistics
const response = await authenticatedRequest('/statistics/income/departments?start_date=' + (new Date()).toISOString().split('T')[0] + '&end_date=' + (new Date()).toISOString().split('T')[0]) const today = todayLocalDateOnly();
const response = await authenticatedRequest('/statistics/income/departments?start_date=' + today + '&end_date=' + today)
// Get the sent data // Get the sent data
statistics_department_today.value = response.data.data statistics_department_today.value = response.data.data
// Log the data // Log the data
@@ -131,4 +133,4 @@ getStatistics()
<style scoped> <style scoped>
</style> </style>
@@ -4,6 +4,7 @@ import {ref} from 'vue';
import { Bar } from 'vue-chartjs'; import { Bar } from 'vue-chartjs';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js' import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js'
import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { todayLocalDateOnly } from "@/services/dateOnly.js";
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale) ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
@@ -16,7 +17,7 @@ const chartOptions = {
// Shortcut to statistics // Shortcut to statistics
// Set the time for the statistics to today // Set the time for the statistics to today
SessionUser.adminUser.statistics.set_time((new Date()).toISOString().split('T')[0], (new Date()).toISOString().split('T')[0]) SessionUser.adminUser.statistics.set_time(todayLocalDateOnly(), todayLocalDateOnly())
@@ -33,4 +34,4 @@ SessionUser.adminUser.statistics.set_time((new Date()).toISOString().split('T')[
}" }"
/> />
</div> </div>
</template> </template>
+34
View File
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { endOfLocalDate, formatLocalDateOnly, parseLocalDateOnly, startOfLocalDate } from "@/services/dateOnly.js";
describe("date-only helpers", () => {
it("formats Date objects from their local calendar date", () => {
expect(formatLocalDateOnly(new Date(2026, 2, 31, 0, 0, 0, 0))).toBe("2026-03-31");
});
it("parses date input values as local calendar dates", () => {
const parsed = parseLocalDateOnly("2026-03-31");
expect(parsed.getFullYear()).toBe(2026);
expect(parsed.getMonth()).toBe(2);
expect(parsed.getDate()).toBe(31);
expect(formatLocalDateOnly(parsed)).toBe("2026-03-31");
});
it("keeps date-only prefixes from API timestamps without timezone reinterpretation", () => {
expect(formatLocalDateOnly("2026-03-31T23:30:00.000Z")).toBe("2026-03-31");
});
it("rejects impossible date input values", () => {
expect(Number.isNaN(parseLocalDateOnly("2026-02-31").getTime())).toBe(true);
});
it("builds local day boundaries from the selected date", () => {
expect(startOfLocalDate("2026-03-31").getHours()).toBe(0);
expect(startOfLocalDate("2026-03-31").getMinutes()).toBe(0);
expect(endOfLocalDate("2026-03-31").getHours()).toBe(23);
expect(endOfLocalDate("2026-03-31").getMinutes()).toBe(59);
expect(formatLocalDateOnly(endOfLocalDate("2026-03-31"))).toBe("2026-03-31");
});
});
+26
View File
@@ -122,6 +122,32 @@ describe("DatePeriodSelector mobile layout", () => {
expect(formatLocalDate(onSelectionChange.mock.calls[0][0])).toBe("2026-03-01"); expect(formatLocalDate(onSelectionChange.mock.calls[0][0])).toBe("2026-03-01");
expect(formatLocalDate(onSelectionChange.mock.calls[0][1])).toBe("2026-03-05"); expect(formatLocalDate(onSelectionChange.mock.calls[0][1])).toBe("2026-03-05");
}); });
it("emits the exact clicked date for start and end inputs", async () => {
sharedState.width.value = 1024;
const onSelectionChange = vi.fn();
const wrapper = mount(DatePeriodSelector, {
props: {
selection: {
startDate: new Date(2026, 2, 1, 0, 0, 0, 0),
endDate: new Date(2026, 2, 3, 23, 59, 59, 999),
},
onSelectionChange,
visibility: {
showUpdateButton: false,
},
},
});
await wrapper.get("[data-testid='date-period-start']").setValue("2026-03-31");
await wrapper.get("[data-testid='date-period-end']").setValue("2026-04-02");
const emittedSelection = wrapper.emitted("update:selection")?.at(-1)?.[0];
expect(formatLocalDate(emittedSelection.startDate)).toBe("2026-03-31");
expect(formatLocalDate(emittedSelection.endDate)).toBe("2026-04-02");
expect(formatLocalDate(onSelectionChange.mock.calls.at(-1)[0])).toBe("2026-03-31");
expect(formatLocalDate(onSelectionChange.mock.calls.at(-1)[1])).toBe("2026-04-02");
});
}); });
describe("DatePeriodSelector month warning", () => { describe("DatePeriodSelector month warning", () => {
@@ -2,6 +2,7 @@
import { defineComponent, nextTick } from "vue"; import { defineComponent, nextTick } from "vue";
import { mount } from "@vue/test-utils"; import { mount } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { formatLocalDateOnly } from "@/services/dateOnly.js";
const selectDateMock = vi.hoisted(() => vi.fn()); const selectDateMock = vi.hoisted(() => vi.fn());
const sharedDateState = vi.hoisted(() => ({ const sharedDateState = vi.hoisted(() => ({
@@ -39,10 +40,10 @@ const DatePeriodSelectorStub = defineComponent({
emits: ["update:selection"], emits: ["update:selection"],
template: ` template: `
<div> <div>
<button class="trigger-on-selection-change" @click="onSelectionChange(new Date('2026-03-24T00:00:00.000Z'), new Date('2026-03-29T00:00:00.000Z'))"> <button class="trigger-on-selection-change" @click="onSelectionChange(new Date(2026, 2, 24), new Date(2026, 2, 29))">
onSelectionChange onSelectionChange
</button> </button>
<button class="trigger-update-selection" @click="$emit('update:selection', { startDate: new Date('2026-04-01T00:00:00.000Z'), endDate: new Date('2026-04-30T00:00:00.000Z') })"> <button class="trigger-update-selection" @click="$emit('update:selection', { startDate: new Date(2026, 3, 1), endDate: new Date(2026, 3, 30) })">
update:selection update:selection
</button> </button>
</div> </div>
@@ -105,7 +106,7 @@ describe("DepartmentDashboardOverviewNavigation", () => {
}); });
const selector = wrapper.findComponent(DatePeriodSelectorStub); const selector = wrapper.findComponent(DatePeriodSelectorStub);
expect(selector.props("selection").startDate.toISOString().split("T")[0]).toBe("2026-05-05"); expect(formatLocalDateOnly(selector.props("selection").startDate)).toBe("2026-05-05");
expect(selector.props("selection").endDate.toISOString().split("T")[0]).toBe("2026-05-11"); expect(formatLocalDateOnly(selector.props("selection").endDate)).toBe("2026-05-11");
}); });
}); });
@@ -0,0 +1,110 @@
// @vitest-environment jsdom
import { defineComponent } from "vue";
import { mount } from "@vue/test-utils";
import { describe, expect, it, vi } from "vitest";
vi.mock("vue-i18n", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
useI18n: () => ({
t: (value) => value,
}),
};
});
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
objects: {
global: {
language: {
date_from: "From",
date_to: "To",
},
},
},
},
}));
import { formatLocalDateOnly } from "@/services/dateOnly.js";
import PaginationDisplayTemplateDate from "@/components/displays/pagination/templates/PaginationDisplayTemplateDate.vue";
import PaginationDisplayTemplateDates from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
import { dateFunctions } from "@/components/displays/pagination/PaginationDisplayDates.vue";
const PaginationDisplayItemColumnStub = defineComponent({
name: "PaginationDisplayItemColumn",
props: {
label: {
type: String,
required: true,
},
},
template: "<div><slot name='control' /></div>",
});
describe("pagination date selection", () => {
it("renders Date model values as the same local date", () => {
const wrapper = mount(PaginationDisplayTemplateDate, {
props: {
label: "Date",
modelValue: new Date(2026, 2, 31, 0, 0, 0, 0),
},
global: {
stubs: {
PaginationDisplayItemColumn: PaginationDisplayItemColumnStub,
},
},
});
expect(wrapper.get("input[type='date']").element.value).toBe("2026-03-31");
});
it("emits the clicked input date as the same local date", async () => {
const wrapper = mount(PaginationDisplayTemplateDate, {
props: {
label: "Date",
modelValue: new Date(2026, 2, 1, 0, 0, 0, 0),
},
global: {
stubs: {
PaginationDisplayItemColumn: PaginationDisplayItemColumnStub,
},
},
});
await wrapper.get("input[type='date']").setValue("2026-03-31");
const emittedDate = wrapper.emitted("update:date")?.at(-1)?.[0];
expect(formatLocalDateOnly(emittedDate)).toBe("2026-03-31");
});
it("uses the real first day of the month for month presets", () => {
const firstDay = dateFunctions.datePresetFunctions.month.firstDayOfMonth(new Date(2026, 4, 15, 12, 0, 0, 0));
expect(formatLocalDateOnly(firstDay)).toBe("2026-05-01");
expect(firstDay.getHours()).toBe(0);
expect(firstDay.getMinutes()).toBe(0);
});
it("emits both start and end updates from the paired date selector", async () => {
const wrapper = mount(PaginationDisplayTemplateDates, {
props: {
startDate: new Date(2026, 2, 1, 0, 0, 0, 0),
endDate: new Date(2026, 2, 31, 0, 0, 0, 0),
},
global: {
stubs: {
PaginationDisplayItemColumn: PaginationDisplayItemColumnStub,
},
},
});
const inputs = wrapper.findAll("input[type='date']");
await inputs[0].setValue("2026-04-01");
await inputs[1].setValue("2026-04-30");
expect(formatLocalDateOnly(wrapper.emitted("update:startDate")?.at(-1)?.[0])).toBe("2026-04-01");
expect(formatLocalDateOnly(wrapper.emitted("update:endDate")?.at(-1)?.[0])).toBe("2026-04-30");
});
});
@@ -367,6 +367,8 @@ describe("Invoice orders pagination contract", () => {
expect(invoiceOrdersPaginationSource).toContain( expect(invoiceOrdersPaginationSource).toContain(
"const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => {" "const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => {"
); );
expect(invoiceOrdersPaginationSource).toContain("formatLocalDateOnly(newSelectionStartDate)");
expect(invoiceOrdersPaginationSource).toContain("formatLocalDateOnly(newSelectionToDate)");
expect(invoiceOrdersPaginationSource).toContain("date_from.value = formattedStartDate;"); expect(invoiceOrdersPaginationSource).toContain("date_from.value = formattedStartDate;");
expect(invoiceOrdersPaginationSource).toContain("date_to.value = formattedEndDate;"); expect(invoiceOrdersPaginationSource).toContain("date_to.value = formattedEndDate;");
expect(invoiceOrdersPaginationSource).toContain('setFilter("created_at-date_from", formattedStartDate, true);'); expect(invoiceOrdersPaginationSource).toContain('setFilter("created_at-date_from", formattedStartDate, true);');