diff --git a/kabinizer-front-end/api/index.ts b/kabinizer-front-end/api/index.ts index b1b6a3b..cfe6e86 100644 --- a/kabinizer-front-end/api/index.ts +++ b/kabinizer-front-end/api/index.ts @@ -9,7 +9,11 @@ export type { OpenAPIConfig } from './core/OpenAPI'; export type { BookingRequest } from './models/BookingRequest'; export type { CreateBookingRequestDto } from './models/CreateBookingRequestDto'; +export type { CreateDrawDto } from './models/CreateDrawDto'; +export type { Draw } from './models/Draw'; +export type { DrawPeriod } from './models/DrawPeriod'; export type { Period } from './models/Period'; export { BookingRequestService } from './services/BookingRequestService'; +export { DrawService } from './services/DrawService'; export { PeriodService } from './services/PeriodService'; diff --git a/kabinizer-front-end/api/models/BookingRequest.ts b/kabinizer-front-end/api/models/BookingRequest.ts index 978d977..a1a1e62 100644 --- a/kabinizer-front-end/api/models/BookingRequest.ts +++ b/kabinizer-front-end/api/models/BookingRequest.ts @@ -4,9 +4,8 @@ /* eslint-disable */ export type BookingRequest = { - id?: string; + bookingRequestId?: string; userId?: string; - fromDate?: string; - toDate?: string; + periodId?: string; }; diff --git a/kabinizer-front-end/api/models/CreateBookingRequestDto.ts b/kabinizer-front-end/api/models/CreateBookingRequestDto.ts index 42f3e7b..8efb255 100644 --- a/kabinizer-front-end/api/models/CreateBookingRequestDto.ts +++ b/kabinizer-front-end/api/models/CreateBookingRequestDto.ts @@ -4,8 +4,6 @@ /* eslint-disable */ export type CreateBookingRequestDto = { - userId?: string; - fromDate?: string; - toDate?: string; + periodId?: string; }; diff --git a/kabinizer-front-end/api/models/CreateDrawDto.ts b/kabinizer-front-end/api/models/CreateDrawDto.ts new file mode 100644 index 0000000..f169d92 --- /dev/null +++ b/kabinizer-front-end/api/models/CreateDrawDto.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { DrawPeriod } from './DrawPeriod'; + +export type CreateDrawDto = { + deadlineStart?: string; + deadlineEnd?: string; + title?: string | null; + drawPeriods?: Array | null; + isSpecial?: boolean; +}; + diff --git a/kabinizer-front-end/api/models/Draw.ts b/kabinizer-front-end/api/models/Draw.ts new file mode 100644 index 0000000..f00c417 --- /dev/null +++ b/kabinizer-front-end/api/models/Draw.ts @@ -0,0 +1,16 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Period } from './Period'; + +export type Draw = { + id?: string; + start?: string; + end?: string; + title?: string | null; + periods?: Array | null; + isSpecial?: boolean; +}; + diff --git a/kabinizer-front-end/api/models/DrawPeriod.ts b/kabinizer-front-end/api/models/DrawPeriod.ts new file mode 100644 index 0000000..80a9566 --- /dev/null +++ b/kabinizer-front-end/api/models/DrawPeriod.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type DrawPeriod = { + start?: string; + end?: string; + title?: string | null; +}; + diff --git a/kabinizer-front-end/api/models/Period.ts b/kabinizer-front-end/api/models/Period.ts index efaf951..72589c3 100644 --- a/kabinizer-front-end/api/models/Period.ts +++ b/kabinizer-front-end/api/models/Period.ts @@ -4,10 +4,10 @@ /* eslint-disable */ export type Period = { + id?: string; periodStart?: string; periodEnd?: string; - deadlineDate?: string; - isSpecialPeriod?: boolean; title?: string | null; + drawId?: string; }; diff --git a/kabinizer-front-end/api/services/BookingRequestService.ts b/kabinizer-front-end/api/services/BookingRequestService.ts index 4327ba7..f28625d 100644 --- a/kabinizer-front-end/api/services/BookingRequestService.ts +++ b/kabinizer-front-end/api/services/BookingRequestService.ts @@ -39,36 +39,18 @@ export class BookingRequestService { } /** - * @param bookingRequestId + * @param requestBody * @returns boolean Success * @throws ApiError */ public static deleteApiBookingRequest( - bookingRequestId: string, + requestBody: Array, ): CancelablePromise { return __request(OpenAPI, { method: 'DELETE', url: '/api/BookingRequest', - query: { - 'bookingRequestId': bookingRequestId, - }, - }); - } - - /** - * @param userId - * @returns BookingRequest Success - * @throws ApiError - */ - public static getApiBookingRequestUser( - userId: string, - ): CancelablePromise> { - return __request(OpenAPI, { - method: 'GET', - url: '/api/BookingRequest/user/{userId}', - path: { - 'userId': userId, - }, + body: requestBody, + mediaType: 'application/json', }); } diff --git a/kabinizer-front-end/api/services/DrawService.ts b/kabinizer-front-end/api/services/DrawService.ts new file mode 100644 index 0000000..6552f81 --- /dev/null +++ b/kabinizer-front-end/api/services/DrawService.ts @@ -0,0 +1,41 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CreateDrawDto } from '../models/CreateDrawDto'; +import type { Draw } from '../models/Draw'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class DrawService { + + /** + * @returns Draw Success + * @throws ApiError + */ + public static getApiDraw(): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/Draw', + }); + } + + /** + * @param requestBody + * @returns any Success + * @throws ApiError + */ + public static postApiDraw( + requestBody: CreateDrawDto, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/Draw', + body: requestBody, + mediaType: 'application/json', + }); + } + +} diff --git a/kabinizer-front-end/bun.lockb b/kabinizer-front-end/bun.lockb index 617c66b..c60dd19 100755 Binary files a/kabinizer-front-end/bun.lockb and b/kabinizer-front-end/bun.lockb differ diff --git a/kabinizer-front-end/index.html b/kabinizer-front-end/index.html index d77ff2d..bc11f11 100644 --- a/kabinizer-front-end/index.html +++ b/kabinizer-front-end/index.html @@ -1,4 +1,4 @@ - + @@ -9,6 +9,10 @@ href="https://fonts.googleapis.com/css2?family=Pacifico&display=swap" rel="stylesheet" /> + Kabinizer diff --git a/kabinizer-front-end/mock/draws.tsx b/kabinizer-front-end/mock/draws.tsx new file mode 100644 index 0000000..8f0f46a --- /dev/null +++ b/kabinizer-front-end/mock/draws.tsx @@ -0,0 +1,66 @@ +interface Period { + id: string; + periodStart: string; + periodEnd: string; + title: string; + drawId: string; +} + +interface Draw { + id: string; + start: string; + end: string; + title: string; + periods: Period[]; + isSpecial: boolean; +} + +function generateDraws(numDraws: number): Draw[] { + const draws: Draw[] = []; + + for (let i = 0; i < numDraws; i++) { + const id = Math.random().toString(36).substring(7); + const start = new Date().toISOString(); + const end = new Date().toISOString(); + const title = Math.random() < 0.5 ? "" : "Holiday"; + const isSpecial = Math.random() < 0.5; + + const periods: Period[] = []; + let periodStart = start; + let periodEnd = new Date( + new Date(start).getTime() + 7 * 24 * 60 * 60 * 1000, + ).toISOString(); + + while (new Date(periodEnd) <= new Date(end)) { + const periodId = Math.random().toString(36).substring(7); + const periodTitle = `Period ${periods.length + 1}`; + periods.push({ + id: periodId, + periodStart, + periodEnd, + title: periodTitle, + drawId: id, + }); + + periodStart = periodEnd; + periodEnd = new Date( + new Date(periodEnd).getTime() + 7 * 24 * 60 * 60 * 1000, + ).toISOString(); + } + + draws.push({ + id, + start, + end, + title, + periods, + isSpecial, + }); + } + + return draws; +} + +const draws = generateDraws(10); + +console.log(draws); diff --git a/kabinizer-front-end/mock/draws2.tsx b/kabinizer-front-end/mock/draws2.tsx new file mode 100644 index 0000000..6093921 --- /dev/null +++ b/kabinizer-front-end/mock/draws2.tsx @@ -0,0 +1,122 @@ +import { Draw } from "../api"; + +export const draws: Draw[] = [ + { + id: "1", + start: "2024-01-01", + end: "2024-01-14", + title: "", + periods: [ + { + id: "11", + periodStart: "2024-01-01", + periodEnd: "2024-01-07", + title: "", + drawId: "1", + }, + { + id: "12", + periodStart: "2024-01-08", + periodEnd: "2024-01-14", + title: "", + drawId: "1", + }, + { + id: "13", + periodStart: "2024-01-15", + periodEnd: "2024-01-21", + title: "", + drawId: "1", + }, + { + id: "14", + periodStart: "2024-01-22", + periodEnd: "2024-01-28", + title: "", + drawId: "1", + }, + { + id: "15", + periodStart: "2024-01-29", + periodEnd: "2024-02-04", + title: "", + drawId: "1", + }, + { + id: "16", + periodStart: "2024-02-05", + periodEnd: "2024-02-11", + title: "", + drawId: "1", + }, + { + id: "17", + periodStart: "2024-02-12", + periodEnd: "2024-02-18", + title: "", + drawId: "1", + }, + { + id: "18", + periodStart: "2024-02-19", + periodEnd: "2024-02-25", + title: "", + drawId: "1", + }, + ], + isSpecial: false, + }, + { + id: "2", + start: "2024-01-29", + end: "2024-01-21", + title: "Easter", + periods: [ + { + id: "21", + periodStart: "2024-02-26", + periodEnd: "2024-02-29", + title: "Part 1", + drawId: "2", + }, + { + id: "22", + periodStart: "2024-03-1", + periodEnd: "2024-03-03", + title: "Part 2", + drawId: "2", + }, + ], + isSpecial: true, + }, + { + id: "3", + start: "2024-01-22", + end: "2024-02-04", + title: "", + periods: [ + { + id: "31", + periodStart: "2024-03-04", + periodEnd: "2024-03-10", + title: "", + drawId: "3", + }, + { + id: "32", + periodStart: "2024-03-11", + periodEnd: "2024-03-17", + title: "", + drawId: "3", + }, + { + id: "33", + periodStart: "2024-03-18", + periodEnd: "2024-03-24", + title: "", + drawId: "3", + }, + ], + isSpecial: false, + }, +]; diff --git a/kabinizer-front-end/mock/periods.tsx b/kabinizer-front-end/mock/periods.tsx index 09501fd..435cc2a 100644 --- a/kabinizer-front-end/mock/periods.tsx +++ b/kabinizer-front-end/mock/periods.tsx @@ -5,42 +5,35 @@ export const MOCKED_PERIODS: Period[] = [ title: "", periodStart: "2024-01-01", periodEnd: "2024-02-25", - isSpecialPeriod: false, }, { title: "Vinterferie 1", periodStart: "2024-02-26", periodEnd: "2024-02-29", - isSpecialPeriod: true, }, { title: "Vinterferie 2", periodStart: "2024-03-01", periodEnd: "2024-03-03", - isSpecialPeriod: true, }, { title: "", periodStart: "2024-03-04", periodEnd: "2024-03-24", - isSpecialPeriod: false, }, { title: "Påskeferie 1", periodStart: "2024-03-25", periodEnd: "2024-03-28", - isSpecialPeriod: true, }, { title: "Påskeferie 2", periodStart: "2024-03-29", periodEnd: "2024-04-01", - isSpecialPeriod: true, }, { title: "", periodStart: "2024-04-02", periodEnd: "2024-06-02", - isSpecialPeriod: false, }, ]; diff --git a/kabinizer-front-end/package.json b/kabinizer-front-end/package.json index 60c11b4..777a4f2 100644 --- a/kabinizer-front-end/package.json +++ b/kabinizer-front-end/package.json @@ -21,6 +21,9 @@ "sass": "^1.68.0" }, "devDependencies": { + "@azure/msal-browser": "^3.6.0", + "@azure/msal-node": "^2.6.0", + "@azure/msal-react": "^2.0.8", "@types/react": "^18.2.15", "@types/react-date-range": "^1.4.5", "@types/react-dom": "^18.2.7", diff --git a/kabinizer-front-end/src/App.tsx b/kabinizer-front-end/src/App.tsx index 00d97f7..df48ede 100644 --- a/kabinizer-front-end/src/App.tsx +++ b/kabinizer-front-end/src/App.tsx @@ -1,62 +1,58 @@ -import { QueryClient, QueryClientProvider } from "react-query"; import "./App.css"; import Admin from "./pages/admin"; import Home from "./pages/home"; -import { OpenAPI } from "../api"; +import SelectPeriodsView from "./pages/selectPeriods"; +import Navigation from "./Modules/Navigation"; +import { + AuthenticatedTemplate, + UnauthenticatedTemplate, + useMsal, +} from "@azure/msal-react"; -OpenAPI.BASE = "https://app-kabinizer-dev.azurewebsites.net"; - -function App() { +const Pages = () => { const showHome = window.location.pathname === "/"; + const showSelectPeriods = window.location.pathname === "/select-periods"; const showAdmin = window.location.pathname === "/admin"; - const queryClient = new QueryClient(); + return ( + <> + {showHome && } + {showSelectPeriods && } + {showAdmin && } + + ); +}; +function App() { return ( - -
-
-
- - -
-

- Kabinizer -

-
-
- -
+
+ +
+ +
+
-
- {showHome && } - {showAdmin && } -
-
- + + + + +
); } +const LogIn = () => { + const { instance } = useMsal(); + + const handleLogin = () => { + instance.loginRedirect(); + }; + + return ( +
+ +
+ ); +}; + export default App; diff --git a/kabinizer-front-end/src/Modules/Navigation.tsx b/kabinizer-front-end/src/Modules/Navigation.tsx new file mode 100644 index 0000000..4d4c0da --- /dev/null +++ b/kabinizer-front-end/src/Modules/Navigation.tsx @@ -0,0 +1,105 @@ +import { useMsal } from "@azure/msal-react"; +import { useRef, useState, useEffect } from "react"; + +const admins = [ + "fredrik.wigsnes@miles.no", + "kjetil.husebo@miles.no", + "siri.pedersen@miles.no", + "kamilla.nyborg@miles.no", +]; + +function Navigation() { + const ref = useRef(null); + const { instance, accounts } = useMsal(); + const name = accounts[0]?.name ?? ""; + const username = accounts[0]?.username ?? ""; + const [openNav, setOpenNav] = useState(false); + + const handleLogOut = () => { + instance.logoutRedirect(); + }; + + const handleOutsideClick = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + setOpenNav(false); + } + }; + + useEffect(() => { + document.addEventListener("mousedown", handleOutsideClick); + + return () => { + document.removeEventListener("mousedown", handleOutsideClick); + }; + }, []); + + // Extract initials from name string + const initials = name + .split(" ") + .map((word) => word.charAt(0)) + .join(""); + + return ( +
+
+

+ Kabinizer +

+
+ +
+
+ {openNav && ( +
+
+
+
{initials}
{" "} + {name} +
+ +
+
+ )} +
+ ); +} + +export default Navigation; diff --git a/kabinizer-front-end/src/components/Button.tsx b/kabinizer-front-end/src/components/Button.tsx new file mode 100644 index 0000000..8787846 --- /dev/null +++ b/kabinizer-front-end/src/components/Button.tsx @@ -0,0 +1,24 @@ +import { ReactNode } from "react"; + +const Button = ({ + onClick, + children, + size, +}: { + onClick?: () => void; + children: ReactNode; + size: "small" | "medium" | "large"; +}) => { + return ( + + ); +}; + +export default Button; diff --git a/kabinizer-front-end/src/components/DateRangeOption.tsx b/kabinizer-front-end/src/components/DateRangeOption.tsx new file mode 100644 index 0000000..88b1558 --- /dev/null +++ b/kabinizer-front-end/src/components/DateRangeOption.tsx @@ -0,0 +1,65 @@ +import { ColorType } from "../types"; +import { NumberOfDays } from "../utils"; + +const DateRangeOption = ({ + colors, + from, + to, + isSpecial, + selected, + onClick, +}: { + colors: ColorType; + from: Date; + to: Date; + isSpecial: boolean; + selected: boolean; + onClick: () => void; +}) => { + const days = NumberOfDays(from, to) + 1; + + let backgroundColor = isSpecial ? colors.special : colors.primary; + + if (selected) { + backgroundColor = isSpecial ? colors.specialSelected : colors.selected; + } + + return ( + + ); +}; + +export default DateRangeOption; diff --git a/kabinizer-front-end/src/components/MonthVertical.tsx b/kabinizer-front-end/src/components/MonthVertical.tsx new file mode 100644 index 0000000..7c5ea08 --- /dev/null +++ b/kabinizer-front-end/src/components/MonthVertical.tsx @@ -0,0 +1,20 @@ +const MonthColumn = ({ month, color }: { month: string; color: string }) => { + return ( +
+ {month} +
+ ); +}; + +export default MonthColumn; diff --git a/kabinizer-front-end/src/components/Title.tsx b/kabinizer-front-end/src/components/Title.tsx new file mode 100644 index 0000000..d93c956 --- /dev/null +++ b/kabinizer-front-end/src/components/Title.tsx @@ -0,0 +1,13 @@ +type TitleProps = { + children?: React.ReactNode; +}; + +const Title = ({ children }: TitleProps) => { + return ( +

+ {children} +

+ ); +}; + +export default Title; diff --git a/kabinizer-front-end/src/components/WeekDayRow.tsx b/kabinizer-front-end/src/components/WeekDayRow.tsx new file mode 100644 index 0000000..3fc7865 --- /dev/null +++ b/kabinizer-front-end/src/components/WeekDayRow.tsx @@ -0,0 +1,32 @@ +const WeekDayRow = () => { + return ( +
+

Uke

+
+
+ M +
+
+ T +
+
+ O +
+
+ T +
+
+ F +
+
+ L +
+
+ S +
+
+
+ ); +}; + +export default WeekDayRow; diff --git a/kabinizer-front-end/src/components/WeekNumber.tsx b/kabinizer-front-end/src/components/WeekNumber.tsx new file mode 100644 index 0000000..cfcb9ae --- /dev/null +++ b/kabinizer-front-end/src/components/WeekNumber.tsx @@ -0,0 +1,12 @@ +const WeekNumber = ({ value }: { value: number }) => { + return ( +

+ {value} +

+ ); +}; + +export default WeekNumber; diff --git a/kabinizer-front-end/src/hooks/useUser.tsx b/kabinizer-front-end/src/hooks/useUser.tsx new file mode 100644 index 0000000..f6d72c7 --- /dev/null +++ b/kabinizer-front-end/src/hooks/useUser.tsx @@ -0,0 +1,9 @@ +import { useMsal } from "@azure/msal-react"; + +const useUser = () => { + const { accounts } = useMsal(); + + return accounts[0] ?? { tenantId: "" }; +}; + +export default useUser; diff --git a/kabinizer-front-end/src/index.css b/kabinizer-front-end/src/index.css index f4e302c..d307710 100644 --- a/kabinizer-front-end/src/index.css +++ b/kabinizer-front-end/src/index.css @@ -43,7 +43,7 @@ h1 { @media (prefers-color-scheme: light) { :root { color: #213547; - background-color: #ffffff; + background-color: #F1F2F5; } a:hover { color: #747bff; diff --git a/kabinizer-front-end/src/input.css b/kabinizer-front-end/src/input.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/kabinizer-front-end/src/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/kabinizer-front-end/src/main.tsx b/kabinizer-front-end/src/main.tsx index 3d7150d..baf2d37 100644 --- a/kabinizer-front-end/src/main.tsx +++ b/kabinizer-front-end/src/main.tsx @@ -1,10 +1,63 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import './index.css' - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -) +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.tsx"; +import "./index.css"; +import { + AuthenticationResult, + EventMessage, + EventType, + PublicClientApplication, +} from "@azure/msal-browser"; +import Providers from "./providers.tsx"; +import { OpenAPI } from "../api"; + +async function getToken() { + const currentAccount = msalInstance.getActiveAccount(); + const accessTokenRequest = { + scopes: ["api://bfd7362f-1032-4565-820c-cd98e1874056/ApiAccess"], + account: currentAccount ?? undefined, + }; + + if (currentAccount) { + const accessTokenResponse = + await msalInstance.acquireTokenSilent(accessTokenRequest); + return accessTokenResponse.accessToken; + } + return "undefined"; +} + +OpenAPI.BASE = "https://app-kabinizer-dev.azurewebsites.net"; +OpenAPI.TOKEN = () => getToken(); + +const msalInstance = new PublicClientApplication({ + auth: { + clientId: "7f2fc4da-ccf1-4c75-ab65-29654a5eaf53", + authority: "https://login.microsoftonline.com/organizations", + redirectUri: "/", + postLogoutRedirectUri: "/", + }, +}); + +msalInstance.initialize().then(() => { + // Account selection logic is app dependent. Adjust as needed for different use cases. + const accounts = msalInstance.getAllAccounts(); + if (accounts.length > 0) { + msalInstance.setActiveAccount(accounts[0]); + } + + msalInstance.addEventCallback((event: EventMessage) => { + if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) { + const payload = event.payload as AuthenticationResult; + const account = payload.account; + msalInstance.setActiveAccount(account); + } + }); + + ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + , + ); +}); diff --git a/kabinizer-front-end/src/pages/admin/components/OptionItem.tsx b/kabinizer-front-end/src/pages/admin/components/OptionItem.tsx deleted file mode 100644 index ef7a933..0000000 --- a/kabinizer-front-end/src/pages/admin/components/OptionItem.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { atom, useAtomValue, useSetAtom } from "jotai"; -import { Option } from "../../../types"; -import { getDays } from "../../../utils"; -import { useHover } from "react-aria"; - -const hoverAtom = atom(null); - -const OptionItem = ({ - option, - id, - halfDay, -}: { - id: number; - option: Option; - halfDay: boolean; -}) => { - const days = getDays(option.from, option.to); - - return ( - <> - {days.map((day, index) => { - const monday = day.getDay() === 1; - return !(halfDay && index === days.length - 1) ? ( - - ) : ( -
- - -
- ); - })} - - ); -}; - -const Item = ({ - id, - title, - special, - halfDay, - firstDay, - lastDay, -}: { - id: number; - title?: string | null; - special?: boolean; - halfDay?: boolean; - firstDay?: boolean; - lastDay?: boolean; -}) => { - const setHover = useSetAtom(hoverAtom); - const hover = useAtomValue(hoverAtom); - const isHover = hover === id; - const { hoverProps } = useHover({ - onHoverStart: () => setHover(id), - onHoverEnd: () => setHover(null), - }); - - return ( -
- {title && ( -

- {title} -

- )} -
- ); -}; - -export default OptionItem; diff --git a/kabinizer-front-end/src/pages/admin/components/Options.tsx b/kabinizer-front-end/src/pages/admin/components/Options.tsx deleted file mode 100644 index ce97aab..0000000 --- a/kabinizer-front-end/src/pages/admin/components/Options.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Option } from "../../../types"; -import OptionItem from "./OptionItem"; - -const Options = ({ options }: { options: Option[] }) => { - return ( -
- {options.map((option, index) => { - return ( - - ); - })} -
- ); -}; - -export default Options; diff --git a/kabinizer-front-end/src/pages/admin/index.tsx b/kabinizer-front-end/src/pages/admin/index.tsx index c84784b..9f45f94 100644 --- a/kabinizer-front-end/src/pages/admin/index.tsx +++ b/kabinizer-front-end/src/pages/admin/index.tsx @@ -1,20 +1,25 @@ -// import { useQuery } from "react-query"; -// import { PeriodService } from "../../../api"; -import { getOptions } from "../../utils"; -import Options from "./components/Options"; - -import { MOCKED_PERIODS } from "../../../mock/periods"; +import { useMutation } from "react-query"; +import Button from "../../components/Button"; +import { BookingRequestService } from "../../../api"; const Admin = () => { - // const { data = [] } = useQuery(["getApiPeriod"], () => - // PeriodService.getApiPeriod(), - // ); - - const options = getOptions(MOCKED_PERIODS); + const { mutate } = useMutation(() => + BookingRequestService.getApiBookingRequestExport(), + ); return ( -
- +
+
+

Admin

+
+
+

Download selected periods

+ +
+
+
); }; diff --git a/kabinizer-front-end/src/pages/home/components/calender.tsx b/kabinizer-front-end/src/pages/home/components/calender.tsx deleted file mode 100644 index 9da5ec6..0000000 --- a/kabinizer-front-end/src/pages/home/components/calender.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Period } from "../../../../api"; -import { Option } from "../../../types"; -import { getWeeks } from "../../../utils"; -import CalenderItem from "./calenderItem"; - -const Calender = ({ - periods, - options, -}: { - periods: Period[]; - options: Option[]; -}) => { - const weeks = transformPeriodsToListOfWeeks(periods); - - return ( -
-
-
- {weeks.map((week, index) => ( -

- {week.from.getDate()} -

- ))} -
-
- {weeks.map((week, index) => ( - - ))} -
-
- {weeks.map((week, index) => ( -

- {week.to.getDate()} -

- ))} -
-
-
- ); -}; - -const transformPeriodsToListOfWeeks = (periods: Period[]) => { - const start = periods[0]?.periodStart ?? ""; - const end = periods[periods.length - 1]?.periodEnd ?? ""; - - return getWeeks(new Date(start), new Date(end)); -}; - -export default Calender; diff --git a/kabinizer-front-end/src/pages/home/components/calenderItem.tsx b/kabinizer-front-end/src/pages/home/components/calenderItem.tsx deleted file mode 100644 index f83a852..0000000 --- a/kabinizer-front-end/src/pages/home/components/calenderItem.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useQuery } from "react-query"; -import { BookingRequestService, Period } from "../../../../api"; -import { useButton, useHover, usePress } from "react-aria"; -import { useRef } from "react"; -import { atom, useAtomValue, useSetAtom } from "jotai"; -import { - isDateInBetweenDates, - isSameDay, - isSpecialPeriod, -} from "../../../utils"; -import { Option } from "../../../types"; - -const hoverAtom = atom(null); -const activeAtom = atom(null); - -type CalenderItemProps = { - periods: Period[]; - from: Date; - options: Option[]; -}; - -const CalenderItem = ({ periods, from, options }: CalenderItemProps) => { - return ( - <> - {[...Array(7)].map((_, index) => ( - - isDateInBetweenDates( - new Date(from.getTime() + index * 24 * 60 * 60 * 1000), - o.from, - o.to, - ), - )} - /> - ))} - - ); -}; - -const Square = ({ - date, - periods, - optionIndex, -}: { - date: Date; - periods: Period[]; - optionIndex: number; -}) => { - const ref = useRef(null); - const setActive = useSetAtom(activeAtom); - const active = useAtomValue(activeAtom); - const isActive = active === optionIndex; - const setHover = useSetAtom(hoverAtom); - const hover = useAtomValue(hoverAtom); - const isHover = hover === optionIndex; - - const { buttonProps } = useButton({}, ref); - - const { pressProps } = usePress({ - shouldCancelOnPointerExit: true, - onPressStart: () => { - setActive(optionIndex); - }, - onPressEnd: () => { - setActive(null); - }, - }); - - const { hoverProps } = useHover({ - onHoverStart: () => { - setHover(optionIndex); - if (optionIndex !== active) { - setActive(null); - } - }, - onHoverEnd: () => { - setHover(null); - }, - }); - - const { data = [] } = useQuery( - ["getApiBookingRequest"], - () => BookingRequestService.getApiBookingRequest(), - { - refetchOnWindowFocus: false, - }, - ); - - // const { mutate } = useMutation(() => - // BookingRequestService.postApiBookingRequest([ - // { - // userId: "0", - // fromDate: from.toString(), - // toDate: to.toString(), - // }, - // ]) - // ); - - const isSelected = data.some((br) => { - return isDateInBetweenDates( - date, - new Date(br.fromDate ?? ""), - new Date(br.toDate ?? ""), - ); - }); - - const special = isSpecialPeriod(date, periods); - - const color = isSelected - ? "border-green-400" - : special - ? "border-red-400" - : "border-blue-400"; - - const firstDay = - periods.some((p) => isSameDay(date, new Date(p.periodStart ?? ""))) || - (date.getDay() === 1 && !special); - - const lastDay = - periods.some((p) => isSameDay(date, new Date(p.periodEnd ?? ""))) || - (date.getDay() === 0 && !special); - - return ( -
- ); -}; - -export default CalenderItem; diff --git a/kabinizer-front-end/src/pages/home/index.tsx b/kabinizer-front-end/src/pages/home/index.tsx index e486536..05ea239 100644 --- a/kabinizer-front-end/src/pages/home/index.tsx +++ b/kabinizer-front-end/src/pages/home/index.tsx @@ -1,21 +1,21 @@ -import { useQuery } from "react-query"; -import { PeriodService } from "../../../api"; -import { getOptions } from "../../utils"; -import Calender from "./components/calender"; -import { MOCKED_PERIODS } from "../../../mock/periods"; - const Home = () => { - const { data = [] } = useQuery(["getApiPeriod"], () => - PeriodService.getApiPeriod(), - ); - - console.log(data); - - const options = getOptions(MOCKED_PERIODS); - return ( -
- + ); }; diff --git a/kabinizer-front-end/src/pages/hooks/useGetSelected.tsx b/kabinizer-front-end/src/pages/hooks/useGetSelected.tsx new file mode 100644 index 0000000..956b710 --- /dev/null +++ b/kabinizer-front-end/src/pages/hooks/useGetSelected.tsx @@ -0,0 +1,14 @@ +import { useQuery } from "react-query"; +import { BookingRequestService } from "../../../api"; +import useUser from "../../hooks/useUser"; + +const useGetSelected = () => { + const { tenantId } = useUser(); + const { data: selected = [] } = useQuery(["getApiBookingRequestUser"], () => + BookingRequestService.getApiBookingRequestUser(tenantId), + ); + + return selected; +}; + +export default useGetSelected; diff --git a/kabinizer-front-end/src/pages/options/index.ts b/kabinizer-front-end/src/pages/options/index.ts new file mode 100644 index 0000000..98ba58e --- /dev/null +++ b/kabinizer-front-end/src/pages/options/index.ts @@ -0,0 +1,33 @@ +import { ColorType } from "../../types"; + +export const MONTHS: Record = { + 0: "Januar", + 1: "Februar", + 2: "Mars", + 3: "April", + 4: "Mai", + 5: "Juni", + 6: "Juli", + 7: "August", + 8: "September", + 9: "Oktober", + 10: "November", + 11: "Desember", +}; + +export const COLORS: Record = { + 0: { + background: "#DDD4E9", + primary: "#AB98C5", + selected: "#5C447D", + special: "#FE757D", + specialSelected: "#ff303d", + }, + 1: { + background: "#B8C5DC", + primary: "#8497B8", + selected: "#354A71", + special: "#FE757D", + specialSelected: "#ff303d", + }, +}; diff --git a/kabinizer-front-end/src/pages/selectPeriods/Calendar.tsx b/kabinizer-front-end/src/pages/selectPeriods/Calendar.tsx new file mode 100644 index 0000000..cb8c3c1 --- /dev/null +++ b/kabinizer-front-end/src/pages/selectPeriods/Calendar.tsx @@ -0,0 +1,174 @@ +import { BookingRequest, Draw, Period } from "../../../api"; +import DateRangeOption from "../../components/DateRangeOption"; +import MonthColumn from "../../components/MonthVertical"; +import WeekNumber from "../../components/WeekNumber"; +import useUser from "../../hooks/useUser"; +import { MonthMapType, Option, WeekMapType } from "../../types"; +import { CompareDates, GetWeeksNum } from "../../utils"; +import { COLORS, MONTHS } from "../options"; +import Hump from "./Hump"; + +const getWeeklyPeriods = (periods: Period[], draws: Draw[]) => { + return periods.reduce((acc, cur) => { + const startDate = new Date(cur.periodStart ?? ""); + const endDate = new Date(cur.periodEnd ?? ""); + // Check if we are we within the same week + if (GetWeeksNum(startDate) === GetWeeksNum(endDate)) { + const weekDates = `${startDate.getDate()}-${endDate.getDate()}`; + acc.push({ + id: cur.id, + start: startDate.getDay() - 1, // 0 is monday + end: (endDate.getDay() + 6) % 7, // 6 is sunday + week: GetWeeksNum(startDate), + month: startDate.getMonth(), + label: cur.title ? `${cur.title} (${weekDates})` : weekDates, + from: startDate, + to: endDate, + isSpecial: + draws.find((draw) => draw.id === cur.drawId)?.isSpecial ?? false, + }); + } else { + // We have a period that breaks a week + acc.push( + { + id: cur.id, + start: startDate.getDay() - 1, + end: 6, + week: GetWeeksNum(startDate), + month: startDate.getMonth(), + label: cur.title ?? "", + from: startDate, + to: endDate, + isSpecial: + draws.find((draw) => draw.id === cur.drawId)?.isSpecial ?? false, + }, + { + id: cur.id, + start: 0, + end: (endDate.getDay() + 6) % 7, + week: GetWeeksNum(endDate), + month: startDate.getMonth(), + label: cur.title ?? "", + from: startDate, + to: endDate, + isSpecial: + draws.find((draw) => draw.id === cur.drawId)?.isSpecial ?? false, + }, + ); + } + + return acc; + }, []); +}; + +const Calendar = ({ + draws, + selected, + setSelected, +}: { + draws: Draw[]; + selected: BookingRequest[]; + setSelected: (selected: BookingRequest[]) => void; +}) => { + const { tenantId } = useUser(); + const periods = draws + .map((value) => value.periods ?? []) + .flat() + .sort((a, b) => { + return CompareDates(a, b); + }); + + const onClick = (periodId: string) => { + const checked = selected.find((s) => s.periodId === periodId); + + if (checked) { + setSelected(selected.filter((s) => s.periodId !== periodId)); + } else { + const period = periods.find((p) => p.id === periodId); + if (period) { + setSelected([ + ...selected, + { + periodId: periodId, + userId: tenantId, + }, + ]); + } + } + }; + + if (periods.length === 0) { + return
No periods
; + } + + const data = getWeeklyPeriods(periods, draws); + + const weekMap: WeekMapType = data.reduce<{ [key: number]: Option[] }>( + (acc, cur) => { + if (!acc[cur.week]) { + acc[cur.week] = []; + } + acc[cur.week].push(cur); + return acc; + }, + {}, + ); + + const monthMap: MonthMapType = Object.entries(weekMap).reduce<{ + [key: number]: { [key: number]: Option[] }; + }>((acc, [week, options]) => { + const month = options[0].month; + if (!acc[month]) { + acc[month] = {}; + } + acc[month][Number(week)] = options; + return acc; + }, {}); + + return ( +
+ {Object.entries(monthMap).map(([month, weeks]) => ( +
+ +
+ +
+ {Object.entries(weeks).map(([week, options]) => ( +
+ +
+ {options.map((option) => ( + s.periodId === option.id) + } + onClick={() => onClick(option.id ?? "")} + /> + ))} +
+
+ ))} +
+
+
+ ))} +
+ ); +}; + +export default Calendar; diff --git a/kabinizer-front-end/src/pages/selectPeriods/Deadline.tsx b/kabinizer-front-end/src/pages/selectPeriods/Deadline.tsx new file mode 100644 index 0000000..decb701 --- /dev/null +++ b/kabinizer-front-end/src/pages/selectPeriods/Deadline.tsx @@ -0,0 +1,29 @@ +import { Draw } from "../../../api"; + +type DeadlineProps = { + draws: Draw[]; +}; + +const Deadline = ({ draws }: DeadlineProps) => { + return ( +
+
+

Deadline

+
+
+ {draws.map((draw) => ( +
+

+ {draw.title} +

+

+ {draw.end} +

+
+ ))} +
+
+ ); +}; + +export default Deadline; diff --git a/kabinizer-front-end/src/pages/selectPeriods/Hump.tsx b/kabinizer-front-end/src/pages/selectPeriods/Hump.tsx new file mode 100644 index 0000000..b4ebe78 --- /dev/null +++ b/kabinizer-front-end/src/pages/selectPeriods/Hump.tsx @@ -0,0 +1,35 @@ +import { MonthMapType } from "../../types"; + +const Hump = ({ + month, + monthMap, + color, +}: { + month: number; + monthMap: MonthMapType; + color: string; +}) => { + if (monthMap[month - 1] === undefined) return null; + + const currentMonth = monthMap[month] + ? Object.entries(monthMap[month])[0] + : undefined; + + if (currentMonth === undefined) return null; + + const daysCur = currentMonth[1][0].from.getDate(); + + return ( +
+
+
+
+ ); +}; + +export default Hump; diff --git a/kabinizer-front-end/src/pages/selectPeriods/index.tsx b/kabinizer-front-end/src/pages/selectPeriods/index.tsx new file mode 100644 index 0000000..933c2cd --- /dev/null +++ b/kabinizer-front-end/src/pages/selectPeriods/index.tsx @@ -0,0 +1,153 @@ +import { useMutation, useQuery } from "react-query"; +import { + BookingRequest, + BookingRequestService, + DrawService, +} from "../../../api/index.ts"; +// import { draws } from "../../../mock/draws2.tsx"; + +import WeekDayRow from "../../components/WeekDayRow"; +import Calendar from "./Calendar"; +import { useState } from "react"; +import Button from "../../components/Button.tsx"; +import Deadline from "./Deadline.tsx"; +import Title from "../../components/Title.tsx"; + +const getDeletebookings = ( + bookings: BookingRequest[] | undefined = [], + selected: BookingRequest[], +) => { + return bookings.filter( + (b) => + !selected.map((s) => s.bookingRequestId).includes(b.bookingRequestId), + ); +}; + +const SelectPeriodsView = () => { + const [selected, setSelected] = useState([]); + + const { data = [] } = useQuery( + ["getApiDraw"], + () => DrawService.getApiDraw(), + // draws, + ); + + const { data: bookings, refetch } = useQuery( + ["getApiBookingRequest"], + () => BookingRequestService.getApiBookingRequest(), + { + onSuccess: (data) => { + setSelected( + data.map((d) => ({ + id: d.bookingRequestId, + periodId: d.periodId, + })), + ); + }, + onError: (error) => { + console.error(error); + }, + }, + ); + + const { mutateAsync: deleteBookings } = useMutation( + () => { + const deletedBookings = getDeletebookings(bookings, selected); + console.log("Deleted Booking Requests:"); + console.table(deletedBookings); + + return BookingRequestService.deleteApiBookingRequest( + deletedBookings.map((d) => d.bookingRequestId ?? "") ?? [], + ); + }, + { + onError: (error) => { + console.error(error); + }, + }, + ); + + const { mutate: addBookings } = useMutation( + () => BookingRequestService.postApiBookingRequest(selected), + { + onSuccess: () => { + console.log("Lagret"); + console.table(selected); + refetch(); + }, + onError: (error) => { + console.error(error); + }, + }, + ); + + const handleUpdate = async () => { + await deleteBookings(); + await addBookings(); + }; + + const handleSelectAll = () => { + const allPeriods = data.map((d) => d.periods ?? []).flat(); + if (selected.length === allPeriods.length) { + setSelected([]); + } else { + setSelected(allPeriods.map((p) => ({ periodId: p.id }))); + } + }; + + return ( +
+
+ Mine ønsker +
+ +
+
+
+
+ + setSelected(selected)} + /> +
+
+ +
+
+
+ ); +}; + +const Label = () => { + return ( +
+

Ønsker

+ + + +
+ ); +}; + +export default SelectPeriodsView; diff --git a/kabinizer-front-end/src/providers.tsx b/kabinizer-front-end/src/providers.tsx new file mode 100644 index 0000000..1cd9ca9 --- /dev/null +++ b/kabinizer-front-end/src/providers.tsx @@ -0,0 +1,26 @@ +import { QueryClient, QueryClientProvider } from "react-query"; +import "./App.css"; +import { OpenAPI } from "../api"; +import { MsalProvider } from "@azure/msal-react"; +import { ReactNode } from "react"; +import { PublicClientApplication } from "@azure/msal-browser"; + +OpenAPI.BASE = "https://app-kabinizer-dev.azurewebsites.net"; + +function Providers({ + children, + msalInstance, +}: { + children: ReactNode; + msalInstance: PublicClientApplication; +}) { + const queryClient = new QueryClient(); + + return ( + + {children} + + ); +} + +export default Providers; diff --git a/kabinizer-front-end/src/types/index.tsx b/kabinizer-front-end/src/types/index.tsx index dd2c5a5..bef5999 100644 --- a/kabinizer-front-end/src/types/index.tsx +++ b/kabinizer-front-end/src/types/index.tsx @@ -1,13 +1,23 @@ -export type Week = { - from: Date; - to: Date; +export type ColorType = { + background: string; + primary: string; + selected: string; + special: string; + specialSelected: string; }; export type Option = { + id?: string; + start: number; + end: number; + month: number; + week: number; + label: string; from: Date; to: Date; - deadline: Date; - isSpecialPeriod?: boolean | null; - title?: string | null; - halfDay?: boolean; + isSpecial: boolean; }; + +export type WeekMapType = Record; + +export type MonthMapType = Record; diff --git a/kabinizer-front-end/src/utils/index.tsx b/kabinizer-front-end/src/utils/index.tsx index 2a20338..bdcfb46 100644 --- a/kabinizer-front-end/src/utils/index.tsx +++ b/kabinizer-front-end/src/utils/index.tsx @@ -1,109 +1,34 @@ import { Period } from "../../api"; -import { Option, Week } from "../types"; const oneDay = 24 * 60 * 60 * 1000; -export const isSameDay = (date1: Date, date2: Date) => { - return ( - date1.getFullYear() === date2.getFullYear() && - date1.getMonth() === date2.getMonth() && - date1.getDate() === date2.getDate() - ); -}; - -export const isDateInBetweenDates = (date: Date, from: Date, to: Date) => { - return date.getTime() >= from.getTime() && date.getTime() <= to.getTime(); -}; - -export const isSpecialPeriod = (date: Date, periods: Period[]) => { - return periods.some((period) => { - const start = new Date(period.periodStart ?? ""); - const end = new Date(period.periodEnd ?? ""); - return ( - date.getTime() >= start.getTime() && - date.getTime() <= end.getTime() && - period.isSpecialPeriod - ); - }); -}; - -export const getWeeks = (start: Date, end: Date): Week[] => { - const weeks: Week[] = []; - - const diffTime = Math.abs(end.getTime() - start.getTime()); - const diffDays = Math.ceil(diffTime / oneDay); - const numberOfWeeks = Math.ceil(diffDays / 7); - - let from = new Date(start.getTime()); - for (let i = 0; i < numberOfWeeks; i++) { - const to = getEndOfWeek(from); - weeks.push({ - from, - to, - }); - from = new Date(to.getTime() + oneDay); - } - - return weeks; -}; - -// Get the sunday of the week -export const getEndOfWeek = (date: Date) => { - const copy = new Date(date); - const day = copy.getDay(); - const diff = copy.getDate() - day + (day === 0 ? -6 : 1); - return new Date(copy.setDate(diff + 6)); -}; - -export const getOptions = (periods: Period[]): Option[] => { - const options: Option[] = []; - - periods.forEach((period, index) => { - if (period.isSpecialPeriod) { - const wasHalfDay = periods[index - 1]?.periodEnd === period.periodStart; - const isHalfDay = periods[index + 1]?.periodStart === period.periodEnd; - options.push({ - from: new Date( - new Date(period.periodStart ?? "").getTime() + - (wasHalfDay ? oneDay : 0) - ), - to: new Date(period.periodEnd ?? ""), - deadline: new Date(period.deadlineDate ?? ""), - isSpecialPeriod: period.isSpecialPeriod, - title: period.title, - halfDay: isHalfDay, - }); - } else { - const start = new Date(period.periodStart ?? ""); - const end = new Date(period.periodEnd ?? ""); - const weeks = getWeeks(start, end); - weeks.forEach((week) => { - options.push({ - from: week.from, - to: week.to, - deadline: new Date(period.deadlineDate ?? ""), - isSpecialPeriod: period.isSpecialPeriod, - title: period.title, - }); - }); - } - }); - - return options; -}; - -export const numberOfDays = (from: Date, to: Date) => { +export const NumberOfDays = (from: Date, to: Date) => { return Math.round(Math.abs((from.getTime() - to.getTime()) / oneDay)); }; -export const getDays = (from: Date, to: Date) => { - const days: Date[] = []; - const numDays = numberOfDays(from, to); - - for (let i = 0; i <= numDays; i++) { - const date = new Date(from.getTime() + i * oneDay); - days.push(date); - } +export const CompareDates = (a: Period, b: Period) => { + const dateA = new Date(a.periodStart ?? "").getTime(); + const dateB = new Date(b.periodStart ?? "").getTime(); + return dateA === dateB ? 0 : dateA > dateB ? 1 : -1; +}; - return days; +export const GetWeeksNum = (date: Date) => { + // Copy date so don't modify original + date = new Date(+date); + // Set hours to 0 to take the time part out of the comparison + date.setHours(0, 0, 0, 0); + // Thursday in current week decides the year. + date.setDate(date.getDate() + 3 - ((date.getDay() + 6) % 7)); + // January 4 is always in week 1. + const week1 = new Date(date.getFullYear(), 0, 4); + // Adjust to Thursday in week 1 and count number of weeks from date to week1. + return ( + 1 + + Math.round( + ((date.getTime() - week1.getTime()) / 86400000 - + 3 + + ((week1.getDay() + 6) % 7)) / + 7, + ) + ); }; diff --git a/kabinizer-front-end/tailwind.config.js b/kabinizer-front-end/tailwind.config.js index 2e6dd27..c60a48e 100644 --- a/kabinizer-front-end/tailwind.config.js +++ b/kabinizer-front-end/tailwind.config.js @@ -4,7 +4,14 @@ export default { theme: { extend: { fontFamily: { - pacifico: ["Pacifico", "cursive"], + poppins: ["poppins", "sans-serif"], + }, + fontWeight: { + bold: 600, + text: 400, + }, + colors: { + title: "#354A71", }, }, },