Feature(opening_hours): correctly display "open ended" opening hours, see #2438

This commit is contained in:
Pieter Vander Vennet 2025-06-12 14:32:22 +02:00
parent a01be66592
commit 9689cdfb65
7 changed files with 125 additions and 45 deletions

View file

@ -1530,10 +1530,6 @@ input[type="range"].range-lg::-moz-range-thumb {
margin-left: 1rem; margin-left: 1rem;
} }
.ml-6 {
margin-left: 1.5rem;
}
.mr-0\.5 { .mr-0\.5 {
margin-right: 0.125rem; margin-right: 0.125rem;
} }

View file

@ -145,6 +145,12 @@
font-size: smaller; font-size: smaller;
} }
.open-end {
border-right: none !important;
border-radius: 0;
background: linear-gradient(to right, #99e7ffff, #99e7ff00 );
}
.ohviz-today .ohviz-range { .ohviz-today .ohviz-range {
border: 1.5px solid black; border: 1.5px solid black;
} }

View file

@ -1,4 +1,7 @@
<script lang="ts"> <script lang="ts">
/**
* The interactive table to select opening hours
*/
import { UIEventSource } from "../../../../Logic/UIEventSource" import { UIEventSource } from "../../../../Logic/UIEventSource"
import type { OpeningHour } from "../../../OpeningHours/OpeningHours" import type { OpeningHour } from "../../../OpeningHours/OpeningHours"
import { OH as OpeningHours } from "../../../OpeningHours/OpeningHours" import { OH as OpeningHours } from "../../../OpeningHours/OpeningHours"

View file

@ -15,6 +15,7 @@ export interface OpeningHour {
export interface OpeningRange { export interface OpeningRange {
isOpen: boolean isOpen: boolean
isSpecial: boolean isSpecial: boolean
openEnd: boolean
comment: string comment: string
startDate: Date startDate: Date
endDate: Date endDate: Date
@ -464,14 +465,10 @@ const changes = OH.allChangeMoments([[{isOpen: true, isSpecial: false, comment:
changes // => [[36000,61200], ["10:00", "17:00"]] changes // => [[36000,61200], ["10:00", "17:00"]]
*/ */
public static allChangeMoments( public static allChangeMoments(
ranges: { ranges: OpeningRange[][],
isOpen: boolean includeOpenEnds = false,
isSpecial: boolean
comment: string
startDate: Date
endDate: Date
}[][]
): [number[], string[]] { ): [number[], string[]] {
const changeHours: number[] = [] const changeHours: number[] = []
const changeHourText: string[] = [] const changeHourText: string[] = []
@ -480,7 +477,8 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
for (const weekday of ranges) { for (const weekday of ranges) {
for (const range of weekday) { for (const range of weekday) {
if (!range.isOpen && !range.isSpecial) {
if (!(range.openEnd || range.isOpen || range.isSpecial)) {
continue continue
} }
const startOfDay: Date = new Date(range.startDate) const startOfDay: Date = new Date(range.startDate)
@ -496,6 +494,10 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
) )
} }
if(range.openEnd && !includeOpenEnds){
continue
}
// The number of seconds till between the start of the day and closing // The number of seconds till between the start of the day and closing
const changeMomentEnd: number = const changeMomentEnd: number =
(range.endDate.getTime() - startOfDay.getTime()) / 1000 (range.endDate.getTime() - startOfDay.getTime()) / 1000
@ -558,7 +560,7 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
.mapD( .mapD(
(ohtext) => { (ohtext) => {
try { try {
return OH.CreateOhObject(<any>tags.data, ohtext, country.data) return OH.createOhObject(<any>tags.data, ohtext, country.data)
} catch (e) { } catch (e) {
return "error" return "error"
} }
@ -567,19 +569,18 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
) )
} }
public static CreateOhObject( public static createOhObject(
tags: Record<string, string> & { _lat: number; _lon: number; _country?: string }, tags: Record<string, string | number> & { _lat: number; _lon: number; _country?: string },
textToParse: string, textToParse: string,
country?: string country: string,
) { ) {
// noinspection JSPotentiallyInvalidConstructorUsage
return new opening_hours( return new opening_hours(
textToParse, textToParse,
{ {
lat: tags._lat, lat: tags._lat,
lon: tags._lon, lon: tags._lon,
address: { address: {
country_code: country.toLowerCase(), country_code: country?.toLowerCase(),
state: undefined, state: undefined,
}, },
}, },
@ -705,7 +706,7 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
} }
/* We calculate the ranges when it is opened! */ /* We calculate the ranges when it is opened! */
return { startingMonday: lastMonday, ranges: OH.GetRanges(oh, lastMonday, nextSunday) } return { startingMonday: lastMonday, ranges: OH.getRanges(oh, lastMonday, nextSunday) }
} }
public static weekdaysIdentical(openingRanges: OpeningRange[][], startday = 0, endday = 4) { public static weekdaysIdentical(openingRanges: OpeningRange[][], startday = 0, endday = 4) {
@ -742,8 +743,9 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
/** /**
* Calculates when the business is opened (or on holiday) between two dates. * Calculates when the business is opened (or on holiday) between two dates.
* Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ... * Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ...
*
*/ */
public static GetRanges(oh: opening_hours, from: Date, to: Date): OpeningRange[][] { public static getRanges(oh: opening_hours, from: Date, to: Date): OpeningRange[][] {
const values = [[], [], [], [], [], [], []] const values = [[], [], [], [], [], [], []]
const start = new Date(from) const start = new Date(from)
@ -752,25 +754,41 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
const iterator = oh.getIterator(start) const iterator = oh.getIterator(start)
let prevValue = undefined let prevValue: OpeningRange = undefined
while (iterator.advance(to)) { while (iterator.advance(to)) {
if (prevValue) { if (prevValue) {
prevValue.endDate = iterator.getDate() as Date prevValue.endDate = iterator.getDate() as Date
if (prevValue.openEnd) {
prevValue.endDate = new Date(prevValue.startDate.getTime())
prevValue.endDate.setHours(prevValue.endDate.getHours() + 3)
}
} }
const endDate = new Date(iterator.getDate()) as Date const endDate = new Date(iterator.getDate()) as Date
endDate.setHours(0, 0, 0, 0) endDate.setHours(0, 0, 0, 0)
endDate.setDate(endDate.getDate() + 1) endDate.setDate(endDate.getDate() + 1)
const value = { let comment = iterator.getComment()
// See https://github.com/opening-hours/opening_hours.js/
const openEnd = comment === "Specified as open end. Closing time was guessed."
if (openEnd) {
comment = undefined
}
const value: OpeningRange = {
isSpecial: iterator.getUnknown(), isSpecial: iterator.getUnknown(),
isOpen: iterator.getState(), isOpen: iterator.getState(),
comment: iterator.getComment(), comment,
startDate: iterator.getDate() as Date, openEnd,
endDate: endDate, // Should be overwritten by the next iteration startDate: iterator.getDate(),
endDate, // The end date gets overwritten in the next iteration, see the first lines of this loop
} }
prevValue = value prevValue = value
if (value.comment === undefined && !value.isOpen && !value.isSpecial) { if (value.comment === undefined && !value.isOpen && !value.isSpecial) {
// simply closed, nothing special here // siif (prevValue) {
prevValue.endDate = iterator.getDate() as Date
if (prevValue.openEnd) {
prevValue.endDate = new Date(prevValue.startDate.getTime())
prevValue.endDate.setHours(prevValue.endDate.getHours() + 3)
}
continue continue
} }
@ -780,6 +798,10 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
// Get day: sunday is 0, monday is 1. We move everything so that monday == 0 // Get day: sunday is 0, monday is 1. We move everything so that monday == 0
values[(value.startDate.getDay() + 6) % 7].push(value) values[(value.startDate.getDay() + 6) % 7].push(value)
} }
if (prevValue && prevValue.openEnd) {
prevValue.endDate = new Date(prevValue.startDate.getTime())
prevValue.endDate.setHours(prevValue.endDate.getHours() + 3)
}
return values return values
} }

View file

@ -1,29 +1,26 @@
<script lang="ts"> <script lang="ts">
import type { OpeningRange } from "../OpeningHours"
/** /**
* A single bar in the Opening-Hours visualisations table, eventually with a text * A single bar in the Opening-Hours visualisations table, eventually with a text
*/ */
export let availableArea: number export let availableArea: number
export let earliestOpen: number export let earliestOpen: number
export let latestclose: number export let latestclose: number
export let range: { export let range: OpeningRange
isOpen: boolean
isSpecial: boolean
comment: string
startDate: Date
endDate: Date
}
export let isWeekstable: boolean export let isWeekstable: boolean
let textToShow = range.comment ?? (isWeekstable ? "" : range.startDate.toLocaleDateString()) let textToShow = range.comment ?? (isWeekstable ? "" : range.startDate.toLocaleDateString())
let startOfDay: Date = new Date(range.startDate) let startOfDay: Date = new Date(range.startDate)
startOfDay.setHours(0, 0, 0, 0) startOfDay.setHours(0, 0, 0, 0)
let startpoint = (range.startDate.getTime() - startOfDay.getTime()) / 1000 - earliestOpen let startpoint = (range.startDate.getTime() - startOfDay.getTime()) / 1000 - earliestOpen
// prettier-ignore
let width = (100 * (range.endDate.getTime() - range.startDate.getTime()) / 1000) / availableArea let width = (100 * (range.endDate.getTime() - range.startDate.getTime()) / 1000) / availableArea
let startPercentage = (100 * startpoint) / availableArea let startPercentage = (100 * startpoint) / availableArea
console.log("Available area is", availableArea, "for", range.endDate.toISOString())
</script> </script>
{#if range.openEnd}
{#if !range.isOpen && !range.isSpecial} <div class="ohviz-range open-end" style={`left:${startPercentage}%; width:${width}%`}/>
{:else if !range.isOpen && !range.isSpecial}
<div class="ohviz-day-off">{textToShow}</div> <div class="ohviz-day-off">{textToShow}</div>
{:else} {:else}
<div class="ohviz-range" style={`left:${startPercentage}%; width:${width}%`}>{textToShow}</div> <div class="ohviz-range" style={`left:${startPercentage}%; width:${width}%`}>{textToShow}</div>

View file

@ -10,16 +10,12 @@
import { Translation } from "../../i18n/Translation" import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations" import Translations from "../../i18n/Translations"
import { OH } from "../OpeningHours" import { OH } from "../OpeningHours"
import type { OpeningRange } from "../OpeningHours"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
export let oh: opening_hours export let oh: opening_hours
export let ranges: { export let ranges: OpeningRange[][] // Per weekday
isOpen: boolean
isSpecial: boolean
comment: string
startDate: Date
endDate: Date
}[][] // Per weekday
export let rangeStart: Date export let rangeStart: Date
let isWeekstable: boolean = oh.isWeekStable() let isWeekstable: boolean = oh.isWeekStable()
let today = new Date() let today = new Date()
@ -34,8 +30,12 @@
) )
let todayRanges = ranges.map((r, i) => r.filter(() => i === todayIndex)) let todayRanges = ranges.map((r, i) => r.filter(() => i === todayIndex))
// For the header
const [changeHours, changeHourText] = OH.allChangeMoments(weekdayRanges) const [changeHours, changeHourText] = OH.allChangeMoments(weekdayRanges)
// For the header
const [changeHoursWeekend, changeHourTextWeekend] = OH.allChangeMoments(weekendRanges) const [changeHoursWeekend, changeHourTextWeekend] = OH.allChangeMoments(weekendRanges)
// To calculate the range to display
const [changeHoursIncludingOpenEnd] = OH.allChangeMoments(weekdayRanges, true)
const weekdayHeaders: { const weekdayHeaders: {
changeHours: number[] changeHours: number[]
@ -51,9 +51,9 @@
let todayChangeMoments: Set<number> = new Set(OH.allChangeMoments(todayRanges)[0]) let todayChangeMoments: Set<number> = new Set(OH.allChangeMoments(todayRanges)[0])
// By default, we always show the range between 8 - 19h, in order to give a stable impression // By default, we always show the range between 8 - 19h, in order to give a stable impression
// Ofc, a bigger range is used if needed // Ofc, a bigger range is used if needed
let earliestOpen = Math.min(8 * 60 * 60, ...changeHours) let earliestOpen = Math.min(8 * 60 * 60, ...changeHoursIncludingOpenEnd)
// We always make sure there is 30m of leeway in order to give enough room for the closing entry // We always make sure there is 30m of leeway in order to give enough room for the closing entry
let latestclose = Math.max(19 * 60 * 60, Math.max(...changeHours) + 30 * 60) let latestclose = Math.max(19 * 60 * 60, Math.max(...changeHoursIncludingOpenEnd) + 30 * 60)
let availableArea = latestclose - earliestOpen let availableArea = latestclose - earliestOpen
function calcLineOffset(moment: number) { function calcLineOffset(moment: number) {

View file

@ -0,0 +1,56 @@
import { describe, it } from "vitest"
import { REPORT_REASONS } from "panoramax-js"
import Translations from "../../src/UI/i18n/Translations"
import { OH, OpeningRange } from "../../src/UI/OpeningHours/OpeningHours"
import { expect } from "chai"
describe("OH", () => {
describe("getRanges", () => {
it("standard opening hours", () => {
const oh_obj = OH.createOhObject({
"opening_hours": "10:00-18:00",
_lat: 0, _lon: 0, _country: "be",
}, "10:00-18:00", "be")
const ranges = OH.getRanges(oh_obj, new Date("2025-06-10T00:00:00Z"), new Date("2025-06-11T00:00:00Z"))
// Deep equal compares the dates correctly
expect(ranges[1]).to.deep.equal([
{
"comment": undefined,
"endDate": new Date("2025-06-10T16:00:00.000Z"),
"isOpen": true,
"isSpecial": false,
"openEnd": false,
"startDate": new Date("2025-06-10T08:00:00.000Z"),
},
])
})
it("open ended opening hours", () => {
const oh_obj = OH.createOhObject({
"opening_hours": "10:00-18:00+",
_lat: 0, _lon: 0, _country: "be",
}, "10:00+", "be")
const ranges = OH.getRanges(oh_obj, new Date("2025-06-09T00:00:00Z"), new Date("2025-06-16T00:00:00Z"))
// Deep equal compares the dates correctly
expect(ranges[1]).to.deep.equal([
{
"comment": undefined,
"endDate": new Date("2025-06-10T11:00:00.000Z"),
"isOpen": false,
"isSpecial": true,
"openEnd": true,
"startDate": new Date("2025-06-10T08:00:00.000Z"),
},
])
expect(ranges.at(-1)).to.deep.equal([
{
"comment": undefined,
"endDate": new Date("2025-06-15T11:00:00.000Z"),
"isOpen": false,
"isSpecial": true,
"openEnd": true,
"startDate": new Date("2025-06-15T08:00:00.000Z"),
},
])
})
})
})