Skip to content

Commit

Permalink
Merge pull request #917 from eisbuk/feature/single-account-multiple-a…
Browse files Browse the repository at this point in the history
…thletes

Feature/single account multiple athletes
  • Loading branch information
silviot authored Jan 27, 2024
2 parents ff8410e + c677149 commit dbb336e
Show file tree
Hide file tree
Showing 51 changed files with 1,427 additions and 669 deletions.
599 changes: 303 additions & 296 deletions common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion common/config/rush/repo-state.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
"pnpmShrinkwrapHash": "c8781aa0eecdca84acfe762ce15c915e5bf08828",
"pnpmShrinkwrapHash": "fad0f80cd56291c19d54901d4bb76389a9449c1a",
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
}
2 changes: 2 additions & 0 deletions packages/client/src/AppContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import ErrorBoundaryPage from "@/pages/error_boundary";
import SlotsPage from "@/pages/slots";
import LoginPage from "@/pages/login";
import CustomerAreaPage from "@/pages/customer_area";
import SelectAccount from "@/pages/select_account";
import AttendancePrintable from "@/pages/attendance_printable";
import DebugPage from "@/pages/debug";
import AdminPreferencesPage from "@/pages/admin_preferences";
Expand Down Expand Up @@ -111,6 +112,7 @@ const AppContent: React.FC = () => {
>
<Redirect to={PrivateRoutes.Athletes} />
</PrivateRoute>
<Route path={Routes.SelectAccount} component={SelectAccount} />
<Route
path={`${Routes.CustomerArea}/:secretKey`}
component={CustomerAreaPage}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const AddToCalendar: React.FC = () => {

const { t } = useTranslation();

const { email } = useSelector(getBookingsCustomer) || {};
const { email } = useSelector(getBookingsCustomer(secretKey)) || {};
const hasBookingsForCalendar = useSelector(getHasBookingsForCalendar);

const { open: openModal } = useSendICSModal({ secretKey, email });
Expand Down
23 changes: 5 additions & 18 deletions packages/client/src/components/auth/LoginRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@ import React from "react";
import { Route, Redirect, RouteProps } from "react-router-dom";
import { useSelector } from "react-redux";

import { PrivateRoutes, Routes } from "@eisbuk/shared/ui";
import { PrivateRoutes } from "@eisbuk/shared/ui";

import {
getAllSecretKeys,
getIsAdmin,
getIsAuthEmpty,
getIsAuthLoaded,
} from "@/store/selectors/auth";
import { getIsAuthEmpty, getIsAuthLoaded } from "@/store/selectors/auth";
import Loading from "./Loading";

/**
Expand All @@ -21,8 +16,6 @@ import Loading from "./Loading";
const LoginRoute: React.FC<RouteProps> = (props) => {
const isAuthEmpty = useSelector(getIsAuthEmpty);
const isAuthLoaded = useSelector(getIsAuthLoaded);
const isAdmin = useSelector(getIsAdmin);
const [secretKey] = useSelector(getAllSecretKeys) || [];

switch (true) {
// Loading screen
Expand All @@ -31,16 +24,10 @@ const LoginRoute: React.FC<RouteProps> = (props) => {
// If auth empty, show login/register screen
case isAuthEmpty:
return <Route {...props} />;
// If admin, redirect to root page (attendance view)
case isAdmin:
return <Redirect to={PrivateRoutes.Root} />;
// If not admin, but has 'secretKey' redirect to customer area
case Boolean(secretKey):
return <Redirect to={`${Routes.CustomerArea}/${secretKey}`} />;
// Default: auth user exists, but is not admin nor is registered (doesn't have a 'secretKey'):
// Redirect to self registration form
// If auth exists, redirect to the default route
// Any further redirect will be handled by the PrivateRoute component there
default:
return <Redirect to={Routes.SelfRegister} />;
return <Redirect to={PrivateRoutes.Root} />;
}
};

Expand Down
11 changes: 9 additions & 2 deletions packages/client/src/components/auth/PrivateRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const PrivateRoute: React.FC<RouteProps> = (props) => {
const isAuthEmpty = useSelector(getIsAuthEmpty);
const isAdmin = useSelector(getIsAdmin);
const isAuthLoaded = useSelector(getIsAuthLoaded);
const [secretKey] = useSelector(getAllSecretKeys) || [];
const secretKeys = useSelector(getAllSecretKeys) || [];

switch (true) {
// Display loading state until initial auth is loaded
Expand All @@ -37,9 +37,16 @@ const PrivateRoute: React.FC<RouteProps> = (props) => {
case isAuthEmpty:
return <Redirect to={Routes.Login} />;

case Boolean(secretKey):
// If there's only one secret key, (user is managing only one account) redirect to that account
case secretKeys.length === 1:
const [secretKey] = secretKeys as string[];
return <Redirect to={[Routes.CustomerArea, secretKey].join("/")} />;

// If there are multiple secret keys (user is managing multiple accounts), redirect to account selection page
// Note: This is the private route - this should not affect the behaviour of routes like '/customer_area/:secretKey' or '/self_register'
case secretKeys.length > 1:
return <Redirect to={Routes.SelectAccount} />;

// The auth exists - user exists in firebase auth, but there's no secret
// key - redirect to complte registration
default:
Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/controllers/AthleteAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const AthletAvatatController: React.FC<AthleteAvatarMenuProps> = ({
content={
<AthleteAvatarMenu
{...props}
onAthleteClick={({ secretKey }) =>
history.push(`${Routes.CustomerArea}/${secretKey}`)
}
onAddAccount={() => history.push(Routes.SelfRegister)}
onLogout={async () => {
await signOut(getAuth());
history.push(Routes.Login);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ interface Props extends React.HTMLAttributes<HTMLElement> {
* state to the store (for bookings finalisation).
*/
const BookingsCountdownContainer: React.FC<Props> = (props) => {
const currentDate = useSelector(getCalendarDay);
const countdownProps = useSelector(getCountdownProps);
const isMonthEmpty = useSelector(getMonthEmptyForBooking);
const { id: customerId } = useSelector(getBookingsCustomer) || {};
const secretKey = useSelector(getSecretKey)!;
const currentDate = useSelector(getCalendarDay);
const countdownProps = useSelector(getCountdownProps(secretKey));
const isMonthEmpty = useSelector(getMonthEmptyForBooking(secretKey));
const { id: customerId } = useSelector(getBookingsCustomer(secretKey)) || {};

const { openWithProps: openFinalizeBookingsDialog } =
useFinalizeBooksingsModal();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { updateLocalDocuments } from "@eisbuk/react-redux-firebase-firestore";
import BookingsCountdownContainer from "../BookingsCountdownContainer";

import { getNewStore } from "@/store/createStore";
import { changeCalendarDate } from "@/store/actions/appActions";
import { changeCalendarDate, storeSecretKey } from "@/store/actions/appActions";

import { renderWithRedux } from "@/__testUtils__/wrappers";

Expand Down Expand Up @@ -60,13 +60,14 @@ describe("BookingsCountdown", () => {
})
);
store.dispatch(changeCalendarDate(month));
store.dispatch(storeSecretKey(saul.secretKey));
// With test state set up, 'finalize' button should be in the screen for
// provided 'month'
renderWithRedux(<BookingsCountdownContainer />, store);
screen.getByText(i18n.t(ActionButton.FinalizeBookings) as string).click();
const wantModal = {
component: "FinalizeBookingsDialog",
props: { customerId: saul.id, month },
props: expect.objectContaining({ customerId: saul.id, month }),
};
const mockDispatchCallPayload = mockDispatch.mock.calls[0][0].payload;
expect(mockDispatchCallPayload.component).toEqual(wantModal.component);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ const PrivacyPolicyToast: React.FC = () => {

const policyParams = useSelector(getPrivacyPolicy);

const bookingsCustomer = useSelector(getBookingsCustomer) || {};
const secretKey = useSelector(getSecretKey) || "";
const bookingsCustomer = useSelector(getBookingsCustomer(secretKey)) || {};

const policyAccepted = Boolean(bookingsCustomer.privacyPolicyAccepted);
const isAdmin = useSelector(getIsAdmin);
Expand Down
54 changes: 46 additions & 8 deletions packages/client/src/pages/customer_area/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import {
OrgSubCollection,
} from "@eisbuk/shared";
import { Routes } from "@eisbuk/shared/ui";
import { useFirestoreSubscribe } from "@eisbuk/react-redux-firebase-firestore";
import {
useFirestoreSubscribe,
useUpdateSubscription,
} from "@eisbuk/react-redux-firebase-firestore";

import { getOrganization } from "@/lib/getters";

Expand All @@ -22,14 +25,17 @@ import ProfileView from "./views/Profile";
import { useDate } from "./hooks";
import useSecretKey from "@/hooks/useSecretKey";

import Layout from "@/controllers/Layout";
import PrivacyPolicyToast from "@/controllers/PrivacyPolicyToast";
import AthleteAvatar from "@/controllers/AthleteAvatar";

import ErrorBoundary from "@/components/atoms/ErrorBoundary";

import Layout from "@/controllers/Layout";

import { getBookingsCustomer } from "@/store/selectors/bookings";
import {
getBookingsCustomer,
getOtherBookingsAccounts,
} from "@/store/selectors/bookings";
import { getAllSecretKeys } from "@/store/selectors/auth";

enum Views {
Book = "BookView",
Expand All @@ -50,21 +56,48 @@ const viewsLookup = {
const CustomerArea: React.FC = () => {
const secretKey = useSecretKey();

// We're providing a fallback [secretKey] as we have multiple ways of authenticating. If authenticating
// using firebase auth, the user will have all of their secret keys in the store (this is the preferred way).
// However, user can simply use a booking link (which includes the secret key). For this method, the user doesn't have
// to authenticate with firebase auth, no secret keys will be found in auth section of the store and 'getAllSecretKeys' selector
// will return 'undefined'
const secretKeysInStore = useSelector(getAllSecretKeys);
const secretKeys = secretKeysInStore?.length
? secretKeysInStore
: [secretKey];

// Subscribe to necessary collections
useFirestoreSubscribe(getOrganization(), [
{ collection: OrgSubCollection.SlotsByDay },
{ collection: OrgSubCollection.SlotBookingsCounts },
{ collection: Collection.PublicOrgInfo },
{ collection: OrgSubCollection.Bookings, meta: { secretKey } },
{
collection: OrgSubCollection.Bookings,
meta: { secretKeys },
},
{ collection: BookingSubCollection.BookedSlots, meta: { secretKey } },
{ collection: BookingSubCollection.AttendedSlots, meta: { secretKey } },
{ collection: BookingSubCollection.Calendar, meta: { secretKey } },
]);

useUpdateSubscription(
{ collection: OrgSubCollection.Bookings, meta: { secretKeys } },
[secretKeys]
);
useUpdateSubscription(
{ collection: BookingSubCollection.BookedSlots, meta: { secretKey } },
[secretKey]
);
useUpdateSubscription(
{ collection: BookingSubCollection.AttendedSlots, meta: { secretKey } },
[secretKey]
);

const calendarNavProps = useDate();

// Get customer data necessary for rendering/functionality
const userData = useSelector(getBookingsCustomer) || {};
const currentAthlete = useSelector(getBookingsCustomer(secretKey)) || {};
const otherAccounts = useSelector(getOtherBookingsAccounts(secretKey));

const [view, setView] = useState<keyof typeof viewsLookup>(Views.Book);
const CustomerView = viewsLookup[view];
Expand Down Expand Up @@ -95,14 +128,19 @@ const CustomerArea: React.FC = () => {
</>
);

if (secretKey && userData.deleted) {
if (secretKey && currentAthlete.deleted) {
return <Redirect to={`${Routes.Deleted}/${secretKey}`} />;
}

return (
<Layout
additionalButtons={additionalButtons}
userAvatar={<AthleteAvatar currentAthlete={userData} />}
userAvatar={
<AthleteAvatar
currentAthlete={currentAthlete}
otherAccounts={otherAccounts}
/>
}
>
{view !== "ProfileView" && (
<CalendarNav
Expand Down
7 changes: 4 additions & 3 deletions packages/client/src/pages/customer_area/views/Book.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import { createModal } from "@/features/modal/useModal";
const BookView: React.FC = () => {
const { t } = useTranslation();

const customer = useSelector(getBookingsCustomer);
const secretKey = useSelector(getSecretKey) || "";
const customer = useSelector(getBookingsCustomer(secretKey));
const orgEmail = useSelector(getOrgEmail);
const daysToRender = useSelector(getSlotsForBooking);
const daysToRender = useSelector(getSlotsForBooking(secretKey));
const date = useSelector(getCalendarDay);
const disabled = !useSelector(getIsBookingAllowed(date));
const disabled = !useSelector(getIsBookingAllowed(secretKey, date));

const { handleBooking, handleCancellation } = useBooking();

Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/pages/customer_area/views/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const CalendarView: React.FC = () => {
const currentDate = useSelector(getCalendarDay);
const secretKey = useSelector(getSecretKey)!;

const disabled = !useSelector(getIsBookingAllowed(currentDate));
const disabled = !useSelector(getIsBookingAllowed(secretKey, currentDate));

const bookedAndAttendedSlots = useSelector(
getBookedAndAttendedSlotsForCalendar
Expand Down
3 changes: 2 additions & 1 deletion packages/client/src/pages/customer_area/views/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ const CalendarView: React.FC = () => {
const secretKey = useSelector(getSecretKey)!;
const defaultCountryCode = useSelector(getDefaultCountryCode);

const customer = useSelector(getBookingsCustomer) || ({} as Customer);
const customer =
useSelector(getBookingsCustomer(secretKey)) || ({} as Customer);

return (
<CustomerForm.Profile
Expand Down
7 changes: 5 additions & 2 deletions packages/client/src/pages/deleted/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,12 @@ const Deleted: React.FC<Props> = ({ backgroundIndex }: Props) => {

const secretKey = useSecretKey();
useFirestoreSubscribe(getOrganization(), [
{ collection: OrgSubCollection.Bookings, meta: { secretKey } },
{
collection: OrgSubCollection.Bookings,
meta: { secretKeys: [secretKey] },
},
]);
const customer = useSelector(getBookingsCustomer);
const customer = useSelector(getBookingsCustomer(secretKey));

// If customer's not deleted - you shouldn't be here
// If there's no customer - the customer is either not yet loaded, or not found - both are valid reasons to stick around
Expand Down
68 changes: 68 additions & 0 deletions packages/client/src/pages/select_account/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from "react";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";

import {
useFirestoreSubscribe,
useUpdateSubscription,
} from "@eisbuk/react-redux-firebase-firestore";
import { OrgSubCollection } from "@eisbuk/shared";
import { Routes } from "@eisbuk/shared/ui";
import { LayoutContent } from "@eisbuk/ui";

import Layout from "@/controllers/Layout";

import ErrorBoundary from "@/components/atoms/ErrorBoundary";

import { getOrganization } from "@/lib/getters";

import { getAllSecretKeys } from "@/store/selectors/auth";
import { getOtherBookingsAccounts } from "@/store/selectors/bookings";

const SelectAccount: React.FC = () => {
const secretKeys = useSelector(getAllSecretKeys) || [];
const accounts = useSelector(getOtherBookingsAccounts(""));

// Subscribe to necessary collections
useFirestoreSubscribe(getOrganization(), [
{
collection: OrgSubCollection.Bookings,
meta: { secretKeys },
},
]);

useUpdateSubscription(
{ collection: OrgSubCollection.Bookings, meta: { secretKeys } },
[secretKeys]
);

return (
<Layout>
<LayoutContent>
<ErrorBoundary resetKeys={[]}>
<div className="px-[44px] py-4">
<h1 className="mt-20 text-2xl font-normal leading-none text-gray-700 cursor-normal select-none mb-12">
Select account
</h1>
<div className="w-full grid grid-cols-12 gap-5 ">
{accounts?.map((account) => (
<Link
className="rounded border border-gray-300 py-4 px-8 col-span-3 hover:bg-gray-50 hover:border-gray-700"
to={`${Routes.CustomerArea}/${account.secretKey}`}
>
<span className="text-lg text-gray-700">{account.name}</span>
<br />
<span className="text-lg text-gray-700">
{account.surname}
</span>
</Link>
))}
</div>
</div>
</ErrorBoundary>
</LayoutContent>
</Layout>
);
};

export default SelectAccount;
Loading

0 comments on commit dbb336e

Please sign in to comment.