0

So I'm trying to implement booking (reservations) on the website which offers online services and I want the dates and time (working days and hours) for the end user to be shown correctly, taking into consideration timezones, DST and and all that tricky stuff. The actual location of the service providers are on timezone +5, and the working hours are from 8am to 5pm. So what I want the actual user, for example, on timezone +4 to see is working hours being 7am - 4pm.

For the time being I'm using Angular Material Datepicker to store dates, and Angular Material Select with hardcoded hours.

angular material form inputs working hours hardcoded

But this is not optimal at all, and I could only get away with notifying users that the time shown is of specified timezone.

I also tried to follow this guide, but to no avail.

I have installed moment and moment-timezone but cannot figure it out yet.

I store booked dates and hours in firebase, and retrieve them with angular/fire like so

table: Table;
this.db.list('timetable').valueChanges()
      .subscribe(table => this.table = table);

Then I grab the value from the datepicker input and check which working hours are available

selectedDate: string;
hours = hours; // a list of objects with hardcoded working hours in it, like {hour: "8:00", booked: false}, {hour: "9:00", booked: true} etc.
selectDate(e: MatDatepickerInputEvent<Date>) {
    this.selectedDate = new Date(e.target.value).toLocaleDateString();
    const bookedHours: string[] = [];
    this.table.forEach((booking) => {
      if (this.selectedDate === booking.date) {
        bookedHours.push(booking.hour);
      }
    });

    this.hours.forEach(time => {
      if (bookedHours.includes(time.hour)) {time.booked = true;
      } else { time.booked = false; }
    });
  }

And if 10am is booked, for example, it looks like this: hour unavailable

I know that the implementation is poor and hacky and I'm open for suggestions on that as well.

Ruslan
  • 134
  • 1
  • 10

1 Answers1

1

As I posted above momentjs and moment-timezone were suboptimal and couldn't get them figure out well. I ended up using luxon, by far the easiest library to manipulate time and dates.

Apart from regular npm installation, typing files are also necessary:

npm i luxon --save
npm i @types/luxon --save-dev

I created a helper service in my angular app, added luxon:

import {DateTime, Interval} from 'luxon';

and the function that receives a JS date and returns working hours in the user's local time.

getHours(date: Date) {
  const hours: DateTime[] = [];
  // Convert user date to local date
  const userSelectedDate = this.userDate(date);
  const serviceLocalTime = userSelectedDate.toUTC().setZone(service_ZONE),
        // Set working hours for the date
        serviceWorkStart = serviceLocalTime.set(service_OBJECT),
        serviceWorkEnd = serviceLocalTime.set(service_OBJECT).plus({hour: TOTAL_WORKING_HOURS});
        // Convert back to user date with hours
  const userWorkStart = serviceWorkStart.toLocal(),
        userWorkEnd = serviceWorkEnd.toLocal(),
        userWorkingHours = Interval.fromDateTimes(userWorkStart, userWorkEnd).divideEqually(TOTAL_WORKING_HOURS);
  userWorkingHours.forEach(hour => {
    if (hour.start.day < userSelectedDate.day) {
      const dayUp = hour.start.plus({day: 1});
      if (dayUp.toUTC().setZone(service_ZONE).weekday === 3 || dayUp.toUTC().setZone(service_ZONE).weekday === 7) {
        // Day-offs are not added to the list
      } else { hours.push(dayUp); }
    } else {
      if (hour.start.toUTC().setZone(service_ZONE).weekday === 3 || hour.start.toUTC().setZone(service_ZONE).weekday === 7) {
        // Day-offs are not added to the list
      } else { hours.push(hour.start); }
    }
  });
  return hours.sort((a, b) => a.hour - b.hour);
}

The component's code was also refactored.

selectDate(e: MatDatepickerInputEvent<Date>) {
    this.selectedDateHours = [];
    if (this.bookForm.controls.date.invalid) {
      this.bookForm.controls.hours.reset();
      this.selectDisabled = true;
    } else {
      this.selectDisabled = false;
      const dateInput = this.dtService.getHours(e.target.value);
      dateInput.forEach(hour => {
        if (this.table.some(booking => booking.hour === hour.toUTC().toISO())) {
          this.selectedDateHours.push({hour: hour, booked: true});
        } else {
          this.selectedDateHours.push({hour: hour, booked: false});
        }
      });
    }
  }

If anybody has a more elegant solution, I'd be happy to know:)

Ruslan
  • 134
  • 1
  • 10
  • 1
    This is good but you could remove a lot of the zone fiddling: a) `toUTC().setZone()` is unecessary in call cases; just `setZone()` will do what you need, b) afaict, you don't really want the start and end times `toLocal()`ed, c) if you remove those `toLocal()` calls, you don't need to set the zones on the working hour pieces either, since the zone will be inherited from the start time of the interval. In the end, you can do this whole thing with just one `setZone` call and letting it flow through – snickersnack Apr 10 '19 at 08:04
  • Thank you so much, @snickersnack! I'm a bit confused about the c): How do I return user's local hours without calling toLocal()? I mean, if it was some backend logic, I understand what you mean, but I want users to see working hours in their local time. – Ruslan Apr 10 '19 at 08:29
  • 1
    Luxon times are always local to somewhere, it's just a question of where. `toLocal()` returns a Luxon instance that is set up to return hours in the zone of your system. It really should be called `toSystemZone()`, which is under consideration: https://github.com/moment/luxon/issues/469#issuecomment-475990782. At any rate, your `toLocal()` gets completely superseded by your `setZone()` calls, and the Interval logic in between is indifferent to zones. So the simplification is to remove both. Your times will be in the user's zone from that first `setZone()` on. – snickersnack Apr 10 '19 at 14:04
  • Rereading that, I see what you mean now: you want to show the times in the browser's local time, not the service zone, and your code does do that. What I'd do to do that is to do what I advised above, and then put `toLocal()` inside the `push` calls. Then the logic is setZone --> do all the work --> switch back to local, which will be cleaner, imo – snickersnack Apr 12 '19 at 04:43