= Имплементација на кориснички сценарија == Општи информации Нашата апликација, како што е наведено и во претходните фази, се темели на клиент-сервер архитектура. Во овој случај клиентската страна е „претставена“ преку React апликација, додека пак серверската страна е Spring Boot апликација. Во основа на оваа архитектура е комуникација меѓу клиентот и серверот преку HTTP барања. За визуелизација и давање можност за интеракција на крајниот корисник, кој управува директно со апликацијата на клиентска страна, беше потребно да воспоставуваме конекција и соодветно да ги обработуваме простите податоци кои пристигнуваат од серверската страна. Ова го постигнавме преку Axios HTTP клиентот кој е инсталиран на клиенстката апликација и неговата инстанца во продолжение е онаа преку која ги испраќаме сите барања до серверот. {{{#!javascript import axios from "axios"; const instance = axios.create({ baseURL: "http://localhost:8080/", withCredentials: true, maxRedirects: 1, }) export default instance }}} На овој начин обезбедуваме при секое барање основниот URL да биде основниот URL на серверската апликација, додека пак со поставување на параметарот withCredentials овозможуваме препраќање на колачињата и XMLHttpRequest, што овозможува прибавување на нови податоци без целосно одновно вчитување на страницата. === Custom React Hook-и За поедноставно справување со HTTP GET и POST барањата, кои како што претходно споменавме се главната врска меѓу клиентската и серверската страна и практично не е можно да постои компонента каде не се употребуваат, креиравме наши React Hook-и кои се реискористливи и можат да се употребуваат во различни сценарија со едноставна промена на URL-то ==== useGet {{{#!javascript import axios from "../../axios"; import {useState, useEffect, useContext} from 'react'; const useGet = (url) => { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [changed, setChanged] = useState(0) const getData = async (uurl) => { await axios.get(uurl}).then((res) => { setData(res.data); }).catch((error) => { console.log(error) window.location.href = '/error' }) .finally(() => { setIsLoading(false); }); }; useEffect(() => { setIsLoading(true); getData(url); }, [dep, url, changed]); return { data, setData, isLoading, getData, setChanged }; }; export default useGet; }}} Како што и самото име кажува, овој Hook го користиме за GET барања кон серверската страна. При инстанцирање се наведува URL кон кое се праќаат барањата. За чување на податоците кои се добиваат како одговор се користи концептот на useState од React што овозможува ререндерирање на сите компоненти при промена на состојбата на објектот. На сличен начин се постапува и со знаменцето за вчитување на податоци, кое при испраќање на барањето се поставува на true, додека пак кога ќе заврши обработката се враќа на false. Истото служи како guard clause за компонентите кои ги користат податоците кои се чекаат од GET барањето, како би се избегнале исклучоци во случај кога рендерирањето на компонентата ќе заврши пред податоците да бидат вчитани. Асинхроната функција која ги прибавува податоците од серверот е ставена во useEffect callback, што овозможува нејзино реизвршување при промена на некој од елементите во низата зависности. Овој концепт го искористивме за да овозможиме на едноставен начин ререднерирање на компонентите во кои додаваме нови записи, преку вметнување на дополнителна вредност/состојба changed во низата зависноти која пак, при праќање на барање кое очекуваме да предизвика промена на податоците достапни на страницата, се менува и со тоа автоматски се испраќа ново GET барање и промените се одразуваат на компонените кои се зависни од нив. GET барањето испратено преку Axios HTTP клиентот враќа Promise со кој соодветно се справуваме на горенаведениот начин: - во then сегментот состојбата на data се поставува на податоците кои се вратени од серверот (ова значи дека барањето поминало со статус 200); - catch сегментот се извршува кога одговорот на барањето е со статус 4ХХ, односно грешка, па во овој случај корисникот се пренасочува до страница со грешка - finally сегментот се извршува во секој случај, односно означува дека комуникацијата е завршена, вратен е одговор од серверот, па тука се поставува знаменцето isLoading назад на false ==== usePost {{{#!javascript import axios from "../../axios"; const useCreate = () => { const createEntity = async (url, entity, getData) => { console.log(entity) await axios .post(url, null, { params: entity, }) .then((res) => { getData(prev => ++prev) }) .catch((err) => { console.log(err); }) .finally(() => { }); } return { createEntity }; } export default useCreate; }}} Овој Hook го користиме за POST барања кон серверската страна. Од него постојат неколку модифицирани верзии во зависоност од тоа дали парамтерите се испраќаат во телото на барањето, како URL параметри или како променливи во патеката, но во основа се работи за иста имплементација. За разлика од useGet, тука, URL-то се испраќа како аргумент на функцијата, заедно со податоците кои треба да се испратат како параметри и состојбата на зависната променлива за која зборувавме во претходната компонент. Откако ќе се испрати POST, se враќа Promise со кој соодветно се справуваме на горенаведениот начин, со единствна разлика што во then сегментот, кога статусот означува успех, ја менуваме состојбата на зависната променлива за да се ререндерираат зависните компоненти. ==== useFormData {{{#!javascript import { useState } from 'react'; const useFormData = (editData) => { const [formData, setData] = useState({ ...editData }); const onFormChange = (e) => { setData({ ...formData, [e.target.name]: e.target.value, }); }; const onCheckBoxChange = (e) => { setData((prevData) => ({ ...prevData, [e.target.name]: !prevData[e.target.name], })); } const setFormData = (newData) => { setData({...newData}); } return { formData, onFormChange, onCheckBoxChange, setFormData }; }; export default useFormData; }}} Во секој формулар каде што имаме кориснички влез потребно е да се справиме со него и да го проследиме до серверот во утврдената форма. Ова може да се постигне на стариот добар начин, со користење на концептот useState од React и посебна состојба за секое од input полињата. За да го поедноставиме ова, успешно имплементиравме општа функција за справување со корисничкиот влез која работи на следниот начин. Наместо посебни состојби за секое поле, чуваме состојба на еден објект, каде клучот е соодветно name атрибутот на полето, а вредноста е неговиот value. За процесирање на текстуалните полиња, при промена на вредноста одговорна е функцијата onFormChange, каде се менува состојбата на записот од објектот кој е засегнат според name атрибутот. Оваа функција се извршува на секој onChange настан испален од било која input компонента. onCheckBoxChange е ништо повеќе од специјална имплементација на onFormChange функцијата за checkbox input. Како и во претходно наведените Hook-и, и тука, референци до функциите и состојбите кои треба да бидат пристапени од другите компоненти се враќаат од функцијата. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// == '''Имплементација''' == Се најавува на системот [[Image(login.png)]] == Се регистрира на системот [[Image(registartion.png)]] == Корисник со основни привилегии === Листа сместувања [[Image(SearchHotel.png)]] {{{#!java public List getRoomsAvailibilityByDateAndLocation(String hotelLocation, Date dateFrom, Date dateTo, int numberOfBeds, Boolean flexible) { if(flexible) { Calendar calendar = Calendar.getInstance(); Calendar calendar1 = Calendar.getInstance(); calendar.setTime(dateTo); calendar1.setTime(dateFrom); return em.createQuery("SELECT hr FROM HotelRoomAvailable hr WHERE " + "((hr.dateFrom <= :dateTo AND hr.dateTo >= :dateFrom) OR " + "(hr.dateFrom <= :dateToMinus1 AND hr.dateTo >= :dateFromMinus1) OR " + "(hr.dateFrom <= :dateToMinus2 AND hr.dateTo >= :dateFromMinus2) OR " + "(hr.dateFrom <= :dateToMinus3 AND hr.dateTo >= :dateFromMinus3) OR " + "(hr.dateFrom <= :dateToPlus1 AND hr.dateTo >= :dateFromPlus1) OR " + "(hr.dateFrom <= :dateToPlus2 AND hr.dateTo >= :dateFromPlus2) OR " + "(hr.dateFrom <= :dateToPlus3 AND hr.dateTo >= :dateFromPlus3)) " + "AND hr.hotelRoom.hotel.hotelLocation LIKE :hotelLocation " + "AND hr.hotelRoom.numOfBeds >= :numBeds") .setParameter("hotelLocation", hotelLocation) .setParameter("dateFrom", dateFrom) .setParameter("dateTo", dateTo) .setParameter("dateToMinus1", subtractDays(calendar, 1)) .setParameter("dateToMinus2", subtractDays(calendar, 2)) .setParameter("dateToMinus3", subtractDays(calendar, 3)) .setParameter("dateToPlus1", addDays(calendar, 1)) .setParameter("dateToPlus2", addDays(calendar, 2)) .setParameter("dateToPlus3", addDays(calendar, 3)) .setParameter("dateFromMinus1", subtractDays(calendar1, 1)) .setParameter("dateFromMinus2", subtractDays(calendar1, 2)) .setParameter("dateFromMinus3", subtractDays(calendar1, 3)) .setParameter("dateFromPlus1", addDays(calendar1, 1)) .setParameter("dateFromPlus2", addDays(calendar1, 2)) .setParameter("dateFromPlus3", addDays(calendar1, 3)) .setParameter("numBeds", numberOfBeds) .getResultList(); } else { return em.createQuery("SELECT hr FROM HotelRoomAvailable hr WHERE " + "((hr.dateFrom <= :dateTo AND hr.dateTo >= :dateFrom)) " + "AND hr.hotelRoom.hotel.hotelLocation LIKE :hotelLocation " + "AND hr.hotelRoom.numOfBeds >= :numBeds") .setParameter("hotelLocation", hotelLocation) .setParameter("dateFrom", dateFrom) .setParameter("dateTo", dateTo) .setParameter("numBeds", numberOfBeds) .getResultList(); } } }}} Ова е кверито од кое се зимаат хотелите по криетриум, каде што првиот дел е додека флексибилноста од 3 дена има вредност true, па тогаш се листаат +/- (1,2,3) дена од бараните критериуми, а доколку има вредност false тогаш се листаат според точните дати кои се внесени. {{{#!java public List getRoomsAvailibilityByDateAndLocation(String hotelLocation, Date dateFrom, Date dateTo, int numberOfBeds, Boolean flexible) { long numberOfNights = Duration.between(dateFrom.toInstant(), dateTo.toInstant()).toDays(); List roomsAvailible = hotelDao.getRoomsAvailibilityByDateAndLocation(hotelLocation, dateFrom, dateTo, numberOfBeds, flexible); Map> roomsByHotels = roomsAvailible.stream().collect(Collectors.groupingBy(x -> x.getHotelRoom().getHotel())); List hotelsList = roomsByHotels.keySet().stream() .map(x -> new HotelDto( x.getHotelId(), x.getHotelName(), x.getHotelDescripiton(), x.getHotelLocation(), x.getHotelEDBS(), x.getParking(), x.getPetFriendly(), x.getInternetAvailable(), roomsByHotels.get(x).stream().mapToDouble(y -> y.getHotelRoom().getPrice()).min().getAsDouble() * numberOfNights, roomsByHotels.get(x), getReviewsForHotel(x.getHotelId()), getReviewsForHotel(x.getHotelId()).stream().mapToDouble(Reviews::getNumStar).average().orElse(0), getHotelImages(x.getHotelId()) )).toList(); return hotelsList; } }}} Во сервисот за да не ги користиме сите податоци од вистинската класа користиме класа HotelDto односно data transfer object. Со ова овозможуваме да за пренос на податоци меѓу backend и frontend. Во функцијата ги листаме сите слободни соби од сите хотели со наведените криетриуми. {{{#!java @GetMapping(path = "/hotel/search") public List searchAvailibleRooms(@RequestParam(name = "hotelLocation") String hotelLocation, @RequestParam(name = "dateFrom") @DateTimeFormat(pattern = "yyyy-MM-dd") Date dateFrom, @RequestParam(name = "dateTo") @DateTimeFormat(pattern = "yyyy-MM-dd") Date dateTo, @RequestParam(name = "numBeds") int numBeds, @RequestParam(name = "flexible") Boolean flexible) { return hotelManager.getRoomsAvailibilityByDateAndLocation(hotelLocation, dateFrom, dateTo, numBeds, flexible); } }}} Контролерот ги прима потребните податоци и ја повикува функцијата од сервисот. === Листа ресторани [[Image(RestaurantSearch.png)]] {{{#!java public List getTablesByDateAndLocation(String restaurantLocation, Date hourFrom, Date hourTo, int noSeats){ return em.createQuery("select hr from RestaurantsAvailible hr where hr.hourFrom <= :hourFrom and hr.hourTo >= :hourTo " + "and hr.restaurantTable.restaurant.restaurantLocation LIKE :restaurantLocation and hr.restaurantTable.noSeats >= :noSeats") .setParameter("restaurantLocation", restaurantLocation) .setParameter("hourFrom", hourFrom) .setParameter("hourTo", hourTo) .setParameter("noSeats", noSeats) .getResultList(); } }}} Ова е кверито кое ги листа сите маси во одредена локација, со одредено време на доаѓање, заминување и бројка колку луѓе ќе дојдат. {{{#!java public List getTablesByDateAndLocation(String restaurantLocation, Date date, String hourFrom, String hourTo, int noSeats) { Date dateFrom = date; Date dateTo = Date.from(date.toInstant()); String[] splittedFrom = hourFrom.split(":"); String[] splittedTo = hourTo.split(":"); dateFrom.setHours(Integer.parseInt(splittedFrom[0])); dateFrom.setMinutes(Integer.parseInt(splittedFrom[1])); dateTo.setHours(Integer.parseInt(splittedTo[0])); dateTo.setMinutes(Integer.parseInt(splittedTo[1])); List restaurantsAvailibles = restaurantDao.getTablesByDateAndLocation(restaurantLocation, hourFrom, hourTo, noSeats); Map> tablesByRestaurants = restaurantsAvailibles.stream().collect(Collectors.groupingBy(x -> x.getRestaurantTable().getRestaurant())); List restaurantsList = tablesByRestaurants.keySet().stream() .map(x -> new RestaurantDto( x.getRestaurantID(), x.getRestaurantName(), x.getRestaurantLocation(), x.getCousineType(), x.getRestaurantDescription(), x.getRestaurantEdbs(), x.getMenus(), tablesByRestaurants.get(x), getReviewsForRestaurant(x.getRestaurantID()), getReviewsForRestaurant(x.getRestaurantID()).stream().mapToDouble(Reviews::getNumStar).average().orElse(0), getRestaurantImages(x.getRestaurantID()), getMenuImagesByRestaurant(x.getRestaurantID()) )).toList(); return restaurantsList; } }}} Сервисот кој исто како и во хотели користи Dto притоа ги листа сите ресторани кои можат да го опслужат барањето од корисниците. Со тоа што времето од-до се интерпретира како текст, па тие ги средуваме во сервисот. {{{#!java @GetMapping(path = "/restaurant/search") public List searchAvailableRestaurant(@RequestParam(name = "restaurantLocation") String restaurantLocation, @RequestParam(name = "date") @DateTimeFormat(pattern = "yyyy-MM-dd") Date date, @RequestParam(name = "hourFrom") String hourFrom, @RequestParam(name = "hourTo") String hourTo, @RequestParam(name = "numPeople") int noSeats) { return restaurantManager.getTablesByDateAndLocation(restaurantLocation, date, dateFrom, dateTo, noSeats); } }}} Во контролерот се примаат податоци за локација, датум, време од до во форма на текст и број на седишта === Листа превоз [[Image(TransportSearch.png)]] {{{#!java public List getTransportsAvailableByFilters(String fromL, String toL, Date date, int numPassengers) { return em.createQuery("select h from TransportRoute h where h.from = :froml and h.to = :tol and h.freeSpace >= :nump") .setParameter("froml", fromL) .setParameter("tol", toL) .setParameter("nump", numPassengers) .getResultList(); } }}} Ова е кверито со кое се наоѓаат сите слобдни транспорти со одредена дата од-до одредено место и број на патници {{{#!java public List getTransportsAvailableByFilters(String from, String to, Date date, int numPassengers) { List transportAvailable = transportDao.getTransportsAvailableByFilters(from, to, date, numPassengers); Map> transportsByTransporter = transportAvailable.stream().collect(Collectors.groupingBy(x -> x.getParentRoute())); List transportList = transportsByTransporter.keySet().stream().toList().stream() .map(x -> new TransportListingDto( x.getTransportAvailibleId(), x.getFrom(), x.getTo(), x.getDate(), x.getFreeSpace(), x.getTime(), transportsByTransporter.get(x).stream().mapToDouble(y -> y.getPrice()).min().getAsDouble(), x.getRoutes(), x.getTransport(), getReviewsForTransport(x.getTransport().getTransportID()), getReviewsForTransport(x.getTransport().getTransportID()).stream().mapToDouble(Reviews::getNumStar).average().orElse(0) )).collect(Collectors.toList()); return transportList; } }}} Исто како и останатите имаме Dto и ги листаме сите слобдони транспорти со наведените критериуми. Притоа што доколку се внесе некој град кој е попатен во некоја рута се листа и таа со тоа што се зима главната рута на тој град. {{{#!comment Не знам дали вака види }}} {{{#!java @GetMapping(path = "/transport/search") public List searchAvailableTransport(@RequestParam(name = "from") String from, @RequestParam(name = "to") String to, @RequestParam(name = "date") @DateTimeFormat(pattern = "yyyy-MM-dd") Date date, @RequestParam(name = "numPassengers") int numPassengers){ return transportManager.getTransportsAvailableByFilters(from, to, date, numPassengers); } }}} Контролерот кој според влезните критериуми ги листаме сите слобдони транспорти за тие дестинации. == Најавен давател на услуги === Регистра бизнис [[Image(registerBusiness.png)]] ---- [[Image(RegisterBusinessForm.png)]] === Додава хотел ---- [[Image(MyResources1.png)]] ---- [[Image(MyResourcesHotel.png)]] ---- [[Image(myResourcesHotel1.png)]] ==== Додава соба во хотел '''За секој хотел се додаваат различни типови на соби, во нашиот случај се додава соба 'Single Room' и од таков тип хотелот има 10 соби. По креирање на собата може истата да се измени, и да се види, од кој до кој датум колку слободни соби има од конкретниот тип.''' ---- [[Image(MyResourcesHotelRooms.png)]] ---- [[Image(HotelRoomAdd.png)]] ---- [[Image(HotelRoom.png)]] ---- [[Image(HotelRoomFreeDates.png)]] ---- [[Image(HotelRoomEdit.png)]] ---- '''Покрај додавање на соби има и опција да се листаат резервациите кои се резервирани од страна на корисници заедно со нивните детали.''' ---- [[Image(HotelReservation.png)]] === Менува хотел '''За секој хотел што го имаме може да пристапиме до неговите информации и да ги измениме по потреба.''' ---- [[Image(MyResourcesHotelEdit.png)]] === Додава ресторан '''При додавање на ресторан, има форма во која може да се внесат детали за ресторанот. За него може да се додаваат слики што би се листале при разгледување на истиот од страна на корисник''' ---- [[Image(MyResourcesRestaurant.png)]] ---- [[Image(MyResourcesRestaurantAdd.png)]] === Менува ресторан '''Секој корисник кој поседува ресторан ја има опцијата на истиот да прави измени''' ---- [[Image(RestaurantEdit.png)]] ==== Додава мени '''За секој ресторан се додаваат различни оброци во менито. Менито може да се изменува, да се додаваат нови и да се менуваат стари ставки. За истите може да се додаваат слики кои корисниците ќе може да ги гледаат во деталите за ресторанот.''' ---- [[Image(RestaurantMenuAdd.png)]] ---- [[Image(RestaurantMenu.png)]] ---- [[Image(RestaurantMenuEdit.png)]] ==== Додава маси '''Покрај оброци се додаваат и маси кои ги поседува ресторанот. За масата се знае колку луѓе може да има на неа и истата може да се изменува и да се види достапност од-до во табелата.''' ---- [[Image(RestaurantTablesAdd.png)]] ---- [[Image(RestaurantTables.png)]] ---- [[Image(RestaurantTableAvailable.png)]] ---- [[Image(RestaurantTableEdit.png)]] ---- '''Исто како и кај хотел така и тука може да се прегледуваат резервациите за самиот ресторан.''' ---- [[Image(ReservationTable.png)]] === Додава транспорт '''При додавање на транспорт, има форма во која може да се внесат детали за транпосртот.''' ---- [[Image(MyResourcesTransport.png)]] ---- [[Image(MyResourcesTranposrtAdd.png)]] ---- [[Image(MyResourcesTransport1.png)]] ---- '''Истиот тој транспорт може да се изменува по потреба''' ---- [[Image(transportEdit.png)]] ==== Додава рути '''Секој транпосрт може да нуди различни рути, од кој до кој град патува патува и попатните градови низ кој поминува. За истите се додава и цена на чинење за секоја релација''' ---- [[Image(RouteAdd.png)]] ---- [[Image(RoutesAddExtended.png)]] ---- [[Image(route.png)]] ---- '''Покрај додавањето има опција и да се прегледуваат резервациите кои ги има одреден транспорт''' ---- [[Image(RouteReservation.png)]] == Најавен администратор '''Администраторот има улога да ги управува профилите, односно да ги одборува, а истото важи и за фирмите.''' === Управување со профили [[Image(profiles4.png)]] === Управува со фирми [[Image(profiles5.png)]] == Најавен корисник на услуги '''Откога корисникот ќе се реши за некоја понуда, тој може да ги види деталите за истата. При резервација на услугата се добива порака за успешна резервација. ''' === Резеревација ресторан [[Image(RestaurantDetails1-min.png)]] ---- [[Image(RestaurantDetails2.png)]] ---- [[Image(RestaurantReservation.png)]] === Резервира транспорт [[Image(TransportDetails.png)]] ---- [[Image(TransportDetails2.png)]] ---- [[Image(TransportReservation.png)]] === Резервира хотел [[Image(HotelDetails1-min.png)]] ---- [[Image(HotelReservationUser-min.png)]] === Откажува услуга '''Корисникот кој ги закажува резервациите има можност да ги прегледува истите. Има резервации кои се активни, односно што следат, но има и претходни резервации за кои може да остави некакво мислење''' ---- [[Image(ReservationsHotel.png)]]