forked from MapComplete/MapComplete
		
	Refactoring: port opening hours visualisation to svelte
This commit is contained in:
		
							parent
							
								
									3b2c2462c5
								
							
						
					
					
						commit
						cc96df94e9
					
				
					 12 changed files with 290 additions and 285 deletions
				
			
		| 
						 | 
				
			
			@ -967,6 +967,52 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
 | 
			
		|||
        }
 | 
			
		||||
        return oh
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @param changeHours number of seconds 'till the start of the day, assuming sorted
 | 
			
		||||
     * @param changeHourText
 | 
			
		||||
     * @param maxDiff minimum required seconds between two items to be in the same group
 | 
			
		||||
     *
 | 
			
		||||
     * OH.partitionOHForDistance([0, 15, 3615], ["start", "15s", "1h15s"]) // => [{changeHours: [0, 3615], changeTexts: ["start", "1h15s"]}, {changeHours: [15], changeTexts: ["15 seconds"]}}]
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    public static partitionOHForDistance(changeHours: number[], changeHourText: string[], maxDiff = 3600): {
 | 
			
		||||
        changeHours: number[],
 | 
			
		||||
        changeTexts: string[]
 | 
			
		||||
    }[] {
 | 
			
		||||
        const partitionedHours: { changeHours: number[], changeTexts: string[] }[] = [
 | 
			
		||||
            { changeHours: [changeHours[0]], changeTexts: [changeHourText[0]] }
 | 
			
		||||
        ]
 | 
			
		||||
        for (let i = 1 /*skip the first one, inited ^*/; i < changeHours.length; i++) {
 | 
			
		||||
            const moment = changeHours[i]
 | 
			
		||||
            const text = changeHourText[i]
 | 
			
		||||
            let depth = 0
 | 
			
		||||
            while (depth < partitionedHours.length) {
 | 
			
		||||
                const candidate = partitionedHours[depth]
 | 
			
		||||
                const lastMoment = candidate.changeHours.at(-1)
 | 
			
		||||
                const diff = moment - lastMoment
 | 
			
		||||
                if (diff >= maxDiff) {
 | 
			
		||||
                    candidate.changeHours.push(moment)
 | 
			
		||||
                    candidate.changeTexts.push(text)
 | 
			
		||||
                    break
 | 
			
		||||
                }
 | 
			
		||||
                depth++
 | 
			
		||||
            }
 | 
			
		||||
            if (depth == partitionedHours.length) {
 | 
			
		||||
                // No candidate found - make a new list
 | 
			
		||||
                partitionedHours.push({
 | 
			
		||||
                    changeTexts: [text],
 | 
			
		||||
                    changeHours: [moment]
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        return partitionedHours
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ToTextualDescription {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,255 +0,0 @@
 | 
			
		|||
import { UIEventSource } from "../../Logic/UIEventSource"
 | 
			
		||||
import Combine from "../Base/Combine"
 | 
			
		||||
import { FixedUiElement } from "../Base/FixedUiElement"
 | 
			
		||||
import { OH, OpeningRange, ToTextualDescription } from "./OpeningHours"
 | 
			
		||||
import Translations from "../i18n/Translations"
 | 
			
		||||
import BaseUIElement from "../BaseUIElement"
 | 
			
		||||
import Toggle from "../Input/Toggle"
 | 
			
		||||
import { VariableUiElement } from "../Base/VariableUIElement"
 | 
			
		||||
import Table from "../Base/Table"
 | 
			
		||||
import { Translation } from "../i18n/Translation"
 | 
			
		||||
import Loading from "../Base/Loading"
 | 
			
		||||
import opening_hours from "opening_hours"
 | 
			
		||||
import Locale from "../i18n/Locale"
 | 
			
		||||
import SpecialCase from "./Visualisation/SpecialCase.svelte"
 | 
			
		||||
import SvelteUIElement from "../Base/SvelteUIElement"
 | 
			
		||||
import OpeningHoursRangeElement from "./Visualisation/OpeningHoursRangeElement.svelte"
 | 
			
		||||
 | 
			
		||||
export default class OpeningHoursVisualization extends Toggle {
 | 
			
		||||
    private static readonly weekdays: Translation[] = [
 | 
			
		||||
        Translations.t.general.weekdays.abbreviations.monday,
 | 
			
		||||
        Translations.t.general.weekdays.abbreviations.tuesday,
 | 
			
		||||
        Translations.t.general.weekdays.abbreviations.wednesday,
 | 
			
		||||
        Translations.t.general.weekdays.abbreviations.thursday,
 | 
			
		||||
        Translations.t.general.weekdays.abbreviations.friday,
 | 
			
		||||
        Translations.t.general.weekdays.abbreviations.saturday,
 | 
			
		||||
        Translations.t.general.weekdays.abbreviations.sunday,
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        tags: UIEventSource<Record<string, string>>,
 | 
			
		||||
        key: string,
 | 
			
		||||
        prefix = "",
 | 
			
		||||
        postfix = ""
 | 
			
		||||
    ) {
 | 
			
		||||
        const openingHoursStore = OH.CreateOhObjectStore(tags, key, prefix, postfix)
 | 
			
		||||
        const ohTable = new VariableUiElement(
 | 
			
		||||
            openingHoursStore.map((opening_hours_obj) => {
 | 
			
		||||
                if (opening_hours_obj === undefined) {
 | 
			
		||||
                    return new FixedUiElement("No opening hours defined with key " + key).SetClass(
 | 
			
		||||
                        "alert"
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (opening_hours_obj === "error") {
 | 
			
		||||
                    return Translations.t.general.opening_hours.error_loading
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const applicableWeek = OH.createRangesForApplicableWeek(opening_hours_obj)
 | 
			
		||||
                const textual = ToTextualDescription.createTextualDescriptionFor(
 | 
			
		||||
                    opening_hours_obj,
 | 
			
		||||
                    applicableWeek.ranges
 | 
			
		||||
                )
 | 
			
		||||
                const vis = OpeningHoursVisualization.CreateFullVisualisation(
 | 
			
		||||
                    opening_hours_obj,
 | 
			
		||||
                    applicableWeek.ranges,
 | 
			
		||||
                    applicableWeek.startingMonday
 | 
			
		||||
                )
 | 
			
		||||
                Locale.language.mapD((lng) => {
 | 
			
		||||
                    console.debug("Setting OH description to", lng, textual)
 | 
			
		||||
                    vis.ConstructElement().ariaLabel = textual?.textFor(lng)
 | 
			
		||||
                })
 | 
			
		||||
                return vis
 | 
			
		||||
            })
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        super(
 | 
			
		||||
            ohTable,
 | 
			
		||||
            new Loading(Translations.t.general.opening_hours.loadingCountry),
 | 
			
		||||
            tags.map((tgs) => tgs._country !== undefined)
 | 
			
		||||
        )
 | 
			
		||||
        this.SetClass("no-weblate")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static CreateFullVisualisation(
 | 
			
		||||
        oh: opening_hours,
 | 
			
		||||
        ranges: OpeningRange[][],
 | 
			
		||||
        lastMonday: Date
 | 
			
		||||
    ): BaseUIElement {
 | 
			
		||||
        // First, a small sanity check. The business might be permanently closed, 24/7 opened or be another special case
 | 
			
		||||
        if (ranges.some((range) => range.length > 0)) {
 | 
			
		||||
            // The normal case: we have items for the coming days
 | 
			
		||||
            return OpeningHoursVisualization.ConstructVizTable(oh, ranges, lastMonday)
 | 
			
		||||
        }
 | 
			
		||||
        // The special case that range is completely empty
 | 
			
		||||
        return new SvelteUIElement(SpecialCase, { oh })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ConstructVizTable(
 | 
			
		||||
        oh: any,
 | 
			
		||||
        ranges: {
 | 
			
		||||
            isOpen: boolean
 | 
			
		||||
            isSpecial: boolean
 | 
			
		||||
            comment: string
 | 
			
		||||
            startDate: Date
 | 
			
		||||
            endDate: Date
 | 
			
		||||
        }[][],
 | 
			
		||||
        rangeStart: Date
 | 
			
		||||
    ): BaseUIElement {
 | 
			
		||||
        const isWeekstable: boolean = oh.isWeekStable()
 | 
			
		||||
        const [changeHours, changeHourText] = OH.allChangeMoments(ranges)
 | 
			
		||||
        const today = new Date()
 | 
			
		||||
        today.setHours(0, 0, 0, 0)
 | 
			
		||||
 | 
			
		||||
        const todayIndex = Math.ceil(
 | 
			
		||||
            (today.getTime() - rangeStart.getTime()) / (1000 * 60 * 60 * 24)
 | 
			
		||||
        )
 | 
			
		||||
        // 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
 | 
			
		||||
        const earliestOpen = Math.min(8 * 60 * 60, ...changeHours)
 | 
			
		||||
        let latestclose = Math.max(...changeHours)
 | 
			
		||||
        // We always make sure there is 30m of leeway in order to give enough room for the closing entry
 | 
			
		||||
        latestclose = Math.max(19 * 60 * 60, latestclose + 30 * 60)
 | 
			
		||||
        const availableArea = latestclose - earliestOpen
 | 
			
		||||
 | 
			
		||||
        /*
 | 
			
		||||
         * The OH-visualisation is a table, consisting of 8 rows and 2 columns:
 | 
			
		||||
         * The first row is a header row (which is NOT passed as header but just as a normal row!) containing empty for the first column and one object giving all the end times
 | 
			
		||||
         * The other rows are one for each weekday: the first element showing 'mo', 'tu', ..., the second element containing the bars.
 | 
			
		||||
         * Note that the bars are actually an embedded <div> spanning the full width, containing multiple sub-elements
 | 
			
		||||
         * */
 | 
			
		||||
 | 
			
		||||
        const [header, headerHeight] = OpeningHoursVisualization.ConstructHeaderElement(
 | 
			
		||||
            availableArea,
 | 
			
		||||
            changeHours,
 | 
			
		||||
            changeHourText,
 | 
			
		||||
            earliestOpen
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        const weekdays = []
 | 
			
		||||
        const weekdayStyles = []
 | 
			
		||||
        for (let i = 0; i < 7; i++) {
 | 
			
		||||
            const day = OpeningHoursVisualization.weekdays[i].Clone()
 | 
			
		||||
            day.SetClass("w-full h-full flex")
 | 
			
		||||
 | 
			
		||||
            const rangesForDay = ranges[i].map((range) =>
 | 
			
		||||
                new SvelteUIElement(OpeningHoursRangeElement, {
 | 
			
		||||
                    availableArea,
 | 
			
		||||
                    earliestOpen,
 | 
			
		||||
                    latestclose,
 | 
			
		||||
                    range,
 | 
			
		||||
                    isWeekstable
 | 
			
		||||
                })
 | 
			
		||||
            )
 | 
			
		||||
            const allRanges = new Combine([
 | 
			
		||||
                ...OpeningHoursVisualization.CreateLinesAtChangeHours(
 | 
			
		||||
                    changeHours,
 | 
			
		||||
                    availableArea,
 | 
			
		||||
                    earliestOpen
 | 
			
		||||
                ),
 | 
			
		||||
                ...rangesForDay,
 | 
			
		||||
            ]).SetClass("w-full block")
 | 
			
		||||
 | 
			
		||||
            let extraStyle = ""
 | 
			
		||||
            if (todayIndex == i) {
 | 
			
		||||
                extraStyle = "background-color: var(--subtle-detail-color);"
 | 
			
		||||
                allRanges.SetClass("ohviz-today")
 | 
			
		||||
            } else if (i >= 5) {
 | 
			
		||||
                extraStyle = "background-color: rgba(230, 231, 235, 1);"
 | 
			
		||||
            }
 | 
			
		||||
            weekdays.push([day, allRanges])
 | 
			
		||||
            weekdayStyles.push([
 | 
			
		||||
                "padding-left: 0.5em;" + extraStyle,
 | 
			
		||||
                `position: relative;` + extraStyle,
 | 
			
		||||
            ])
 | 
			
		||||
        }
 | 
			
		||||
        return new Table(undefined, [[" ", header], ...weekdays], {
 | 
			
		||||
            contentStyle: [
 | 
			
		||||
                ["width: 5%", `position: relative; height: ${headerHeight}`],
 | 
			
		||||
                ...weekdayStyles,
 | 
			
		||||
            ],
 | 
			
		||||
        })
 | 
			
		||||
            .SetClass("w-full")
 | 
			
		||||
            .SetStyle(
 | 
			
		||||
                "border-collapse: collapse; word-break; word-break: normal; word-wrap: normal"
 | 
			
		||||
            )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static CreateLinesAtChangeHours(
 | 
			
		||||
        changeHours: number[],
 | 
			
		||||
        availableArea: number,
 | 
			
		||||
        earliestOpen: number
 | 
			
		||||
    ): BaseUIElement[] {
 | 
			
		||||
        const allLines: BaseUIElement[] = []
 | 
			
		||||
        for (const changeMoment of changeHours) {
 | 
			
		||||
            const offset = (100 * (changeMoment - earliestOpen)) / availableArea
 | 
			
		||||
            if (offset < 0 || offset > 100) {
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
            const el = new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line")
 | 
			
		||||
            allLines.push(el)
 | 
			
		||||
        }
 | 
			
		||||
        return allLines
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The OH-Visualization header element, a single bar with hours
 | 
			
		||||
     * @param availableArea
 | 
			
		||||
     * @param changeHours
 | 
			
		||||
     * @param changeHourText
 | 
			
		||||
     * @param earliestOpen
 | 
			
		||||
     * @constructor
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    private static ConstructHeaderElement(
 | 
			
		||||
        availableArea: number,
 | 
			
		||||
        changeHours: number[],
 | 
			
		||||
        changeHourText: string[],
 | 
			
		||||
        earliestOpen: number
 | 
			
		||||
    ): [BaseUIElement, string] {
 | 
			
		||||
        const header: BaseUIElement[] = []
 | 
			
		||||
 | 
			
		||||
        header.push(
 | 
			
		||||
            ...OpeningHoursVisualization.CreateLinesAtChangeHours(
 | 
			
		||||
                changeHours,
 | 
			
		||||
                availableArea,
 | 
			
		||||
                earliestOpen
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        let showHigher = false
 | 
			
		||||
        let showHigherUsed = false
 | 
			
		||||
        for (let i = 0; i < changeHours.length; i++) {
 | 
			
		||||
            const changeMoment = changeHours[i]
 | 
			
		||||
            const offset = (100 * (changeMoment - earliestOpen)) / availableArea
 | 
			
		||||
            if (offset < 0 || offset > 100) {
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (i > 0 && (changeMoment - changeHours[i - 1]) / (60 * 60) < 2) {
 | 
			
		||||
                // Quite close to the previous value
 | 
			
		||||
                // We alternate the heights
 | 
			
		||||
                showHigherUsed = true
 | 
			
		||||
                showHigher = !showHigher
 | 
			
		||||
            } else {
 | 
			
		||||
                showHigher = false
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const el = new Combine([
 | 
			
		||||
                new FixedUiElement(changeHourText[i])
 | 
			
		||||
                    .SetClass(
 | 
			
		||||
                        "relative bg-white pl-1 pr-1 h-3 font-sm rounded-xl border-2 border-black  border-opacity-50"
 | 
			
		||||
                    )
 | 
			
		||||
                    .SetStyle("left: -50%; word-break:initial"),
 | 
			
		||||
            ])
 | 
			
		||||
                .SetStyle(`left:${offset}%;margin-top: ${showHigher ? "1.4rem;" : "0.1rem"}`)
 | 
			
		||||
                .SetClass("block absolute top-0 m-0 h-full box-border ohviz-time-indication")
 | 
			
		||||
            header.push(el)
 | 
			
		||||
        }
 | 
			
		||||
        const headerElem = new Combine(header)
 | 
			
		||||
            .SetClass(`w-full absolute block ${showHigherUsed ? "h-16" : "h-8"}`)
 | 
			
		||||
            .SetStyle("margin-top: -1rem")
 | 
			
		||||
        const headerHeight = showHigherUsed ? "4rem" : "2rem"
 | 
			
		||||
        return [headerElem, headerHeight]
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								src/UI/OpeningHours/Visualisation/OpeningHours.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/UI/OpeningHours/Visualisation/OpeningHours.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  /**
 | 
			
		||||
   * Full opening hours visualisations table, dispatches to special cases
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  import { OH, ToTextualDescription } from "../OpeningHours"
 | 
			
		||||
  import opening_hours from "opening_hours"
 | 
			
		||||
  import { ariaLabel } from "../../../Utils/ariaLabel"
 | 
			
		||||
  import RegularOpeningHoursTable from "./RegularOpeningHoursTable.svelte"
 | 
			
		||||
  import SpecialCase from "./SpecialCase.svelte"
 | 
			
		||||
  import { Translation } from "../../i18n/Translation"
 | 
			
		||||
  import type { OpeningRange } from "../OpeningHours"
 | 
			
		||||
 | 
			
		||||
  export let opening_hours_obj: opening_hours
 | 
			
		||||
 | 
			
		||||
  let applicableWeek = OH.createRangesForApplicableWeek(opening_hours_obj)
 | 
			
		||||
  let oh = opening_hours_obj
 | 
			
		||||
 | 
			
		||||
  let textual: Translation = ToTextualDescription.createTextualDescriptionFor(oh, applicableWeek.ranges)
 | 
			
		||||
  let applicableWeekRanges: { ranges: OpeningRange[][]; startingMonday: Date } = OH.createRangesForApplicableWeek(oh)
 | 
			
		||||
  let ranges = applicableWeekRanges.ranges
 | 
			
		||||
  let lastMonday = applicableWeekRanges.startingMonday
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
<div use:ariaLabel={textual} class="no-weblate">
 | 
			
		||||
  <!-- First, a small sanity check. The business might be permanently closed, 24/7 opened or be another special case -->
 | 
			
		||||
  {#if ranges.some((range) => range.length > 0)}
 | 
			
		||||
    <!-- The normal case: we have items for the coming days -->
 | 
			
		||||
    <RegularOpeningHoursTable {ranges} rangeStart={lastMonday} oh={opening_hours_obj} />
 | 
			
		||||
  {:else}
 | 
			
		||||
    <!-- The special case that range is completely empty -->
 | 
			
		||||
    <SpecialCase oh={opening_hours_obj} />
 | 
			
		||||
  {/if}
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										38
									
								
								src/UI/OpeningHours/Visualisation/OpeningHoursHeader.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/UI/OpeningHours/Visualisation/OpeningHoursHeader.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  import BaseUIElement from "../../BaseUIElement"
 | 
			
		||||
  import Combine from "../../Base/Combine"
 | 
			
		||||
  import { FixedUiElement } from "../../Base/FixedUiElement"
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The element showing an "hour" in a bubble, above or below the opening hours table
 | 
			
		||||
   * Dumbly shows one row of what is given.
 | 
			
		||||
   *
 | 
			
		||||
   * Does not include lines
 | 
			
		||||
   */
 | 
			
		||||
  export let availableArea: number
 | 
			
		||||
  export let changeHours: number[]
 | 
			
		||||
  export let changeHourText: string[]
 | 
			
		||||
  export let earliestOpen: number
 | 
			
		||||
  export let todayChangeMoments: Set<number>
 | 
			
		||||
 | 
			
		||||
  function calcOffset(changeMoment: number) {
 | 
			
		||||
    return (100 * (changeMoment - earliestOpen)) / availableArea
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="w-full absolute block h-8" style="margin-top: -1rem">
 | 
			
		||||
  {#each changeHours as changeMoment, i}
 | 
			
		||||
    {#if calcOffset(changeMoment) >= 0 && calcOffset(changeMoment) <= 100}
 | 
			
		||||
      <div style={`left:${calcOffset(changeMoment)}%; margin-top: 0.1rem`}
 | 
			
		||||
           class="block absolute top-0 m-0 h-full box-border ohviz-time-indication">
 | 
			
		||||
        <div
 | 
			
		||||
          style="left: -50%; word-break: initial;"
 | 
			
		||||
          class:border-opacity-50={!todayChangeMoments?.has(changeMoment)}
 | 
			
		||||
          class="relative h-fit bg-white pl-1 pr-1 h-3 font-sm rounded-xl border-2 border-black">
 | 
			
		||||
          {changeHourText[i]}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    {/if}
 | 
			
		||||
  {/each}
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +19,7 @@
 | 
			
		|||
  startOfDay.setHours(0, 0, 0, 0)
 | 
			
		||||
  let startpoint = (range.startDate.getTime() - startOfDay.getTime()) / 1000 - earliestOpen
 | 
			
		||||
  // prettier-ignore
 | 
			
		||||
  let width = (100 * (range.endDate.getTime() - range.startDate.getTime()) / 1000) / (latestclose - earliestOpen)
 | 
			
		||||
  let width = (100 * (range.endDate.getTime() - range.startDate.getTime()) / 1000) / availableArea
 | 
			
		||||
  let startPercentage = (100 * startpoint) / availableArea
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
<script lang="ts">/**
 | 
			
		||||
 * Wrapper around 'OpeningHours' so that the latter can deal with the opening_hours object directly
 | 
			
		||||
 */
 | 
			
		||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
 | 
			
		||||
import type opening_hours from "opening_hours"
 | 
			
		||||
import Translations from "../../i18n/Translations"
 | 
			
		||||
import Loading from "../../Base/Loading.svelte"
 | 
			
		||||
import Tr from "../../Base/Tr.svelte"
 | 
			
		||||
import OpeningHours from "./OpeningHours.svelte"
 | 
			
		||||
 | 
			
		||||
export let tags: UIEventSource<Record<string, string>>
 | 
			
		||||
export let opening_hours_obj: Store<opening_hours | "error">
 | 
			
		||||
export let key: string
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{#if $tags._country === undefined}
 | 
			
		||||
  <Loading>
 | 
			
		||||
    <Tr t={Translations.t.general.opening_hours.loadingCountry} />
 | 
			
		||||
  </Loading>
 | 
			
		||||
{:else if $opening_hours_obj === undefined}
 | 
			
		||||
  <div class="alert">No opening hours defined with key {key}</div>
 | 
			
		||||
{:else if $opening_hours_obj === "error"}
 | 
			
		||||
  <Tr cls="alert" t={Translations.t.general.opening_hours.error_loading} />
 | 
			
		||||
{:else}
 | 
			
		||||
  <OpeningHours opening_hours_obj={$opening_hours_obj} />
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,129 @@
 | 
			
		|||
<script lang="ts">/**
 | 
			
		||||
 * The main visualisation which shows ranges, one or more top/bottom headers, ...
 | 
			
		||||
 * Does not handle the special cases
 | 
			
		||||
 */
 | 
			
		||||
import opening_hours from "opening_hours"
 | 
			
		||||
import OpeningHoursHeader from "./OpeningHoursHeader.svelte"
 | 
			
		||||
import { default as Transl } from "../../Base/Tr.svelte" /* The IDE confuses <tr> (table row) and <Tr> (translation) as they are normally case insensitive -> import under a different name */
 | 
			
		||||
import OpeningHoursRangeElement from "./OpeningHoursRangeElement.svelte"
 | 
			
		||||
import { Translation } from "../../i18n/Translation"
 | 
			
		||||
import Translations from "../../i18n/Translations"
 | 
			
		||||
import { OH } from "../OpeningHours"
 | 
			
		||||
import { Utils } from "../../../Utils"
 | 
			
		||||
 | 
			
		||||
export let oh: opening_hours
 | 
			
		||||
export let ranges: {
 | 
			
		||||
  isOpen: boolean
 | 
			
		||||
  isSpecial: boolean
 | 
			
		||||
  comment: string
 | 
			
		||||
  startDate: Date
 | 
			
		||||
  endDate: Date
 | 
			
		||||
}[][] // Per weekday
 | 
			
		||||
export let rangeStart: Date
 | 
			
		||||
let isWeekstable: boolean = oh.isWeekStable()
 | 
			
		||||
let today = new Date()
 | 
			
		||||
today.setHours(0, 0, 0, 0)
 | 
			
		||||
let todayIndex = Math.ceil((today.getTime() - rangeStart.getTime()) / (1000 * 60 * 60 * 24))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
let weekdayRanges = ranges.map(ranges => ranges.filter(r => r.startDate.getDay() != 0 && r.startDate.getDay() != 6))
 | 
			
		||||
let weekendRanges = ranges.map(ranges => ranges.filter(r => r.startDate.getDay() == 0 || r.startDate.getDay() == 6))
 | 
			
		||||
let todayRanges = ranges.map(((r, i) => r.filter(() => i === todayIndex)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const [changeHours, changeHourText] = OH.allChangeMoments(weekdayRanges)
 | 
			
		||||
const [changeHoursWeekend, changeHourTextWeekend] = OH.allChangeMoments(weekendRanges)
 | 
			
		||||
 | 
			
		||||
const weekdayHeaders: {
 | 
			
		||||
  changeHours: number[];
 | 
			
		||||
  changeTexts: string[]
 | 
			
		||||
}[] = OH.partitionOHForDistance(changeHours, changeHourText)
 | 
			
		||||
const weekendDayHeaders: {
 | 
			
		||||
  changeHours: number[];
 | 
			
		||||
  changeTexts: string[]
 | 
			
		||||
}[] = OH.partitionOHForDistance(changeHoursWeekend, changeHourTextWeekend)
 | 
			
		||||
 | 
			
		||||
let allChangeMoments: number[] = Utils.DedupT([...changeHours, ...changeHoursWeekend])
 | 
			
		||||
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
 | 
			
		||||
// Ofc, a bigger range is used if needed
 | 
			
		||||
let earliestOpen = Math.min(8 * 60 * 60, ...changeHours)
 | 
			
		||||
// 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 availableArea = latestclose - earliestOpen
 | 
			
		||||
 | 
			
		||||
function calcLineOffset(moment: number) {
 | 
			
		||||
  return 100 * (moment - earliestOpen) / availableArea
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let weekdays: Translation[] = [
 | 
			
		||||
  Translations.t.general.weekdays.abbreviations.monday,
 | 
			
		||||
  Translations.t.general.weekdays.abbreviations.tuesday,
 | 
			
		||||
  Translations.t.general.weekdays.abbreviations.wednesday,
 | 
			
		||||
  Translations.t.general.weekdays.abbreviations.thursday,
 | 
			
		||||
  Translations.t.general.weekdays.abbreviations.friday,
 | 
			
		||||
  Translations.t.general.weekdays.abbreviations.saturday,
 | 
			
		||||
  Translations.t.general.weekdays.abbreviations.sunday
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
<div class="w-full h-fit relative">
 | 
			
		||||
  {#each allChangeMoments as moment}
 | 
			
		||||
    <div class="w-full absolute h-full">
 | 
			
		||||
      <div class="w-full h-full flex">
 | 
			
		||||
        <div style="height: 5rem; width: 5%; min-width: 2.75rem" />
 | 
			
		||||
        <div class="grow">
 | 
			
		||||
 | 
			
		||||
          <div class="border-x h-full"
 | 
			
		||||
               style={`width: calc(  ${calcLineOffset(moment)}% ); border-color: ${todayChangeMoments.has(moment) ? "#000" : "#bbb"}`} />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  {/each}
 | 
			
		||||
 | 
			
		||||
  <table class="w-full" style="border-collapse: collapse; word-break: normal; word-wrap: normal">
 | 
			
		||||
    {#each weekdayHeaders as weekdayHeader}
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td style="width: 5%; min-width: 2.75rem;"></td>
 | 
			
		||||
        <td class="relative h-8">
 | 
			
		||||
          <OpeningHoursHeader {earliestOpen} {availableArea} changeHours={weekdayHeader.changeHours}
 | 
			
		||||
                              {todayChangeMoments}
 | 
			
		||||
                              changeHourText={weekdayHeader.changeTexts} />
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    {/each}
 | 
			
		||||
 | 
			
		||||
    {#each weekdays as weekday, i}
 | 
			
		||||
      <tr class:interactive={i >= 5}>
 | 
			
		||||
        <td style="width: 5%">
 | 
			
		||||
          <Transl t={weekday} />
 | 
			
		||||
        </td>
 | 
			
		||||
        <td class="relative p-0 m-0" class:ohviz-today={i===todayIndex}>
 | 
			
		||||
          <div class="w-full" style="margin-left: -0px">
 | 
			
		||||
            {#each ranges[i] as range}
 | 
			
		||||
              <OpeningHoursRangeElement
 | 
			
		||||
                {availableArea}
 | 
			
		||||
                {earliestOpen}
 | 
			
		||||
                {latestclose}
 | 
			
		||||
                {range}
 | 
			
		||||
                {isWeekstable}
 | 
			
		||||
              />
 | 
			
		||||
            {/each}
 | 
			
		||||
          </div>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
 | 
			
		||||
    {/each}
 | 
			
		||||
 | 
			
		||||
    {#each weekendDayHeaders as weekdayHeader}
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td style="width: 5%"></td>
 | 
			
		||||
        <td class="relative h-8">
 | 
			
		||||
          <OpeningHoursHeader {earliestOpen} {availableArea} changeHours={weekdayHeader.changeHours}
 | 
			
		||||
                              {todayChangeMoments}
 | 
			
		||||
                              changeHourText={weekdayHeader.changeTexts} />
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    {/each}
 | 
			
		||||
  </table>
 | 
			
		||||
</div>
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue