Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bookings (former Appointments) page TypeScript #600

Merged
merged 5 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ provide(fxaEditProfileUrlKey, import.meta.env?.VITE_FXA_EDIT_PROFILE);
const navItems = [
'calendar',
'schedule',
'appointments',
'bookings',
'settings',
];

Expand Down
108 changes: 55 additions & 53 deletions frontend/src/components/AppointmentModal.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
<script setup lang="ts">
import { BookingStatus } from '@/definitions';
import { timeFormat } from '@/utils';
import { computed, inject } from 'vue';
import { useI18n } from 'vue-i18n';
import { Appointment } from '@/models';

// icons
import {
IconCalendar,
IconCalendarEvent,
IconClock,
IconNotes,
IconUsers,
IconVideo,
IconX,
} from '@tabler/icons-vue';
import PrimaryButton from '@/elements/PrimaryButton.vue';
import CautionButton from '@/elements/CautionButton.vue';
import { useUserStore } from '@/stores/user-store';
import { dayjsKey } from "@/keys";

const user = useUserStore();

// component constants
const { t } = useI18n();
const dj = inject(dayjsKey);

// component properties
interface Props {
open: boolean, // modal state
appointment?: Appointment; // appointment data to display
};
const props = defineProps<Props>();

// attendees list
const attendeesSlots = computed(() => props.appointment.slots.filter((s) => s.attendee));

// calculate initials
const initials = (name: string) => name.split(' ').map((p) => p[0]).join('');

const confirmationUrl = computed(() => `${user.data.signedUrl}/confirm/${props.appointment.slots[0].id}/${props.appointment.slots[0].booking_tkn}/1`);
const denyUrl = computed(() => `${user.data.signedUrl}/confirm/${props.appointment.slots[0].id}/${props.appointment.slots[0].booking_tkn}/0`);

const answer = (isConfirmed: boolean) => {
window.location.href = isConfirmed ? confirmationUrl.value : denyUrl.value;
};

// component emits
const emit = defineEmits(['close']);

</script>

<template>
<transition>
<div
Expand Down Expand Up @@ -110,7 +163,7 @@
</div>
<div class="rounded-lg border border-gray-400 p-4 dark:border-gray-600">{{ appointment.details }}</div>
</div>
<div class="p-6" v-if="appointment?.slots[0].booking_status === bookingStatus.requested">
<div class="p-6" v-if="appointment?.slots[0].booking_status === BookingStatus.Requested">
<p>{{ attendeesSlots.map((s) => s.attendee.email).join(', ') }} have requested a booking at this time.</p>
<div class="mt-4 flex justify-center gap-4">
<primary-button class="btn-confirm" @click="answer(true)" :title="t('label.confirm')">
Expand All @@ -121,60 +174,9 @@
</caution-button>
</div>
</div>
<div class="p-6" v-if="appointment?.slots[0].booking_status === bookingStatus.booked">
<div class="p-6" v-if="appointment?.slots[0].booking_status === BookingStatus.Booked">
<p>This booking is confirmed.</p>
</div>
</div>
</transition>
</template>

<script setup>
import { bookingStatus } from '@/definitions';
import { timeFormat } from '@/utils';
import { computed, inject } from 'vue';
import { useI18n } from 'vue-i18n';

// icons
import {
IconCalendar,
IconCalendarEvent,
IconClock,
IconNotes,
IconUsers,
IconVideo,
IconX,
} from '@tabler/icons-vue';
import PrimaryButton from '@/elements/PrimaryButton.vue';
import CautionButton from '@/elements/CautionButton.vue';
import { useUserStore } from '@/stores/user-store';
import { dayjsKey } from "@/keys";

const user = useUserStore();

// component constants
const { t } = useI18n();
const dj = inject(dayjsKey);

// component properties
const props = defineProps({
open: Boolean, // modal state
appointment: Object, // appointment data to display
});

// attendees list
const attendeesSlots = computed(() => props.appointment.slots.filter((s) => s.attendee));

// calculate initials
const initials = (name) => name.split(' ').map((p) => p[0]).join('');

const confirmationUrl = computed(() => `${user.data.signedUrl}/confirm/${props.appointment.slots[0].id}/${props.appointment.slots[0].booking_tkn}/1`);
const denyUrl = computed(() => `${user.data.signedUrl}/confirm/${props.appointment.slots[0].id}/${props.appointment.slots[0].booking_tkn}/0`);

const answer = (isConfirmed) => {
window.location.href = isConfirmed ? confirmationUrl.value : denyUrl.value;
};

// component emits
const emit = defineEmits(['close']);

</script>
43 changes: 22 additions & 21 deletions frontend/src/components/TabBar.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';

const { t } = useI18n();

// component properties
interface Props {
tabItems: Object, // list of tab items. Keys are used as lang keys (label.<key>), values as index
active: number, // value of active tab
disabled?: boolean, // flag for making toggle non changable
};
defineProps<Props>();

// component emits
const emit = defineEmits(['update']);

// handle click events
const activate = (key: string) => {
emit('update', key);
};
</script>

<template>
<div class="rounded-2xl bg-gray-200 dark:bg-gray-600">
<nav class="flex">
Expand All @@ -22,24 +44,3 @@
</nav>
</div>
</template>

<script setup>
import { useI18n } from 'vue-i18n';

const { t } = useI18n();

// component properties
defineProps({
tabItems: Object, // list of tab items. Keys are used as lang keys (label.<key>), values as index
active: Number, // value of active tab
disabled: Boolean, // flag for making toggle non changable
});

// component emits
const emit = defineEmits(['update']);

// handle click events
const activate = (key) => {
emit('update', key);
};
</script>
75 changes: 38 additions & 37 deletions frontend/src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,21 @@ export enum BookingCalendarViews {

/**
* Booking status for slots. This mirrors models.BookingStatus on the backend
* @enum
* @readonly
* @deprecated Can be removed if all occurences are TS-ified.
*/
export const bookingStatus = {
none: 1,
requested: 2,
booked: 3,
};
/**
* Booking status for slots. This mirrors models.BookingStatus on the backend
*/
export enum BookingStatus {
None = 1,
Requested = 2,
Booked = 3,
};

/**
* Status to indicate if an invite code ist still valid or no longer valid
Expand All @@ -106,52 +113,45 @@ export enum InviteStatus {
Revoked = 2,
};

// available appointment views
export const appointmentViews = {
booked: 1,
pending: 2,
past: 3,
all: 4,
/**
* Available appointment views
*/
export enum BookingsViews {
Booked = 1,
Pending = 2,
Past = 3,
All = 4,
};

/**
* List columns for bookings page
* @enum
* @readonly
*/
export const listColumns = {
title: 1,
status: 2,
// active: 3,
calendar: 3,
time: 4,
// bookingLink: 4,
// replies: 4,
export enum BookingsTableColumns {
Title = 1,
Status = 2,
Calendar = 3,
Time = 4,
};

/**
* Filter options for bookings page
* @enum
* @readonly
*/
export const filterOptions = {
allAppointments: 1,
appointmentsToday: 2,
appointmentsNext7Days: 3,
appointmentsNext14Days: 4,
appointmentsNext31Days: 5,
appointmentsInMonth: 6,
allFutureAppointments: 7,
export enum BookingsTableFilterOptions {
AllAppointments = 1,
AppointmentsToday = 2,
AppointmentsNext7Days = 3,
AppointmentsNext14Days = 4,
AppointmentsNext31Days = 5,
AppointmentsInMonth = 6,
AllFutureAppointments = 7,
};

/**
* View types for the bookings page
* @enum
* @readonly
*/
export const viewTypes = {
list: 1,
grid: 2,
export enum BookingsViewTypes {
List = 1,
Grid = 2,
};

/**
Expand Down Expand Up @@ -302,18 +302,20 @@ export const waitingListAction = {
export default {
AlertSchemes,
appointmentCreationState,
appointmentViews,
BookingCalendarViews,
BookingsTableColumns,
BookingsTableFilterOptions,
bookingStatus,
BookingStatus,
BookingsViews,
BookingsViewTypes,
calendarManagementType,
calendarViews,
ColorSchemes,
dateFormatStrings,
defaultSlotDuration,
filterOptions,
ftueStep,
InviteStatus,
listColumns,
locationTypes,
loginRedirectKey,
meetingLinkProviderType,
Expand All @@ -325,6 +327,5 @@ export default {
tableDataButtonType,
tableDataType,
tooltipPosition,
viewTypes,
waitingListAction,
};
6 changes: 3 additions & 3 deletions frontend/src/elements/AppointmentGridItem.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { bookingStatus } from '@/definitions';
import { BookingStatus } from '@/definitions';
import { inject, computed } from 'vue';
import { keyByValue, timeFormat } from '@/utils';
import { useI18n } from 'vue-i18n';
Expand Down Expand Up @@ -28,7 +28,7 @@ const props = defineProps<Props>();
const isPast = computed(() => props.appointment.slots[0].start < dj());

// true if a pending appointment was given
const isPending = computed(() => props.appointment.slots[0].booking_status === bookingStatus.requested);
const isPending = computed(() => props.appointment.slots[0].booking_status === BookingStatus.Requested);
</script>

<template>
Expand All @@ -50,7 +50,7 @@ const isPending = computed(() => props.appointment.slots[0].booking_status === b
<div>{{ appointment.title }}</div>
<div class="flex items-center gap-1 text-sm">
<icon-bulb class="size-4 shrink-0 fill-transparent stroke-gray-500 stroke-2"/>
{{ t('label.' + keyByValue(bookingStatus, appointment?.slots[0].booking_status ?? 'Unknown')) }}
{{ t('label.' + keyByValue(BookingStatus, appointment?.slots[0].booking_status ?? 'Unknown', true)) }}
</div>
<div class="flex items-center gap-1 text-sm">
<icon-calendar class="size-4 shrink-0 fill-transparent stroke-gray-500 stroke-2"/>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/elements/calendar/CalendarEvent.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { bookingStatus } from '@/definitions';
import { BookingStatus } from '@/definitions';
import { computed, inject, ref, toRefs } from 'vue';
import { timeFormat, initialEventPopupData, showEventPopup } from '@/utils';
import CalendarEventPlaceholder from '@/elements/calendar/CalendarEventPlaceholder.vue';
Expand Down Expand Up @@ -31,7 +31,7 @@ const { event, timeSlotDuration, timeSlotHeight } = toRefs(props);

const eventData = event.value.customData;
const elementHeight = computed(() => (eventData.duration / timeSlotDuration.value) * timeSlotHeight.value);
const isBusy = computed(() => eventData.slot_status === bookingStatus.booked);
const isBusy = computed(() => eventData.slot_status === BookingStatus.Booked);

// component emits
const emit = defineEmits(['eventSelected']);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const callKey = Symbol('call') as InjectionKey<Fetch>;
export const refreshKey = Symbol('refresh') as InjectionKey<Refresh>;

// Provides functionality to paint background of event objects
type PaintBackgroundType = (element: Event, hexColor: string, hexTransparency?: string, reset?: boolean) => string;
type PaintBackgroundType = (element: Event, hexColor: string, hexTransparency?: string, reset?: boolean) => void;
export const paintBackgroundKey = Symbol('paintBackground') as InjectionKey<PaintBackgroundType>;

// Provides duration data in human friendly form
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ const routes: RouteRecordRaw[] = [
{
path: '/appointments/:view?/:slug?',
name: 'appointments',
redirect: { name: 'bookings' },
},
{
path: '/bookings/:view?/:slug?',
name: 'bookings',
component: AppointmentsView,
},
{
Expand Down Expand Up @@ -155,7 +160,7 @@ const router = createRouter({
});

router.beforeEach((to, from) => {
if (!to.meta?.isPublic && !['setup', 'contact', undefined].includes(to.name)) {
if (!to.meta?.isPublic && !['setup', 'contact', 'undefined'].includes(String(to.name))) {
const user = useUserStore();
if (user && user.data?.email && !user.data.isSetup) {
return { ...to, name: 'setup' };
Expand Down
Loading