diff --git a/.env.example b/.env.example index 740f234ce..dcf8ac845 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ CLIENT_ID= CLIENT_SECRET= SESSION_SECRET= +TIME_ZONE= TARGET_APP= CUSTOM_MUNICIPALITY_OPTIONS=[] diff --git a/.eslintrc b/.eslintrc index 809ded2bc..b1bdd4b72 100644 --- a/.eslintrc +++ b/.eslintrc @@ -63,7 +63,9 @@ "react/no-string-refs": "off", - "react/no-unused-prop-types": ["error", { "skipShapeProps": true }], + "react/no-unused-state": "warn", + + "react/no-unused-prop-types": ["warn", { "skipShapeProps": true }], "react/prefer-stateless-function": "off", @@ -89,6 +91,18 @@ "react/no-array-index-key": "off", - "comma-dangle": "off" + "comma-dangle": "off", + + "max-len": ["warn", { "code": 120}], + + "no-plusplus": "off", + + "no-undef": "warn", + + "arrow-body-style": "off", + + "quote-props": "off", + + "no-case-declarations": "off" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d875bf8..4da4f7ff7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,62 @@ +# 0.6.0 + **MAJOR CHANGES** + - Support for payments in Varaamo. + - Add option to configure time zone of calendar/resources. Defaults to `Europe/Helsinki`. + - Permission checking has been changed: We are now using unit authorizations instead of unit object permissions from respa admin. + + ***CALENDAR*** + - Add reusable FullCalendar component. + - Fix FullCalendar not auto-select `min_period` time slot when user select. Various fixes related to edit reservation calendar, drag and drop and select error handler. + - Fix missing `max_period` check when user select / resize calendar reservation. Show notification and revert to `max_period` if user select bigger amount of time slot than the limit. Various improvement for calendar is included as well. + - Add custom mobile view and stylings in calendar. + + **MINOR CHANGES** + - Replace failure message and add a return button for reservation payment. + - Fix manage reservation page only display `can_approve` reservation, now display *all* reservations. Add strict rules for staff to be able to edit/cancel specific reservation. + - Fix missing reservation metadata fields data in manage reservation view. Trim empty field row. + - Add `show_only` filter section to filter reservation list. This filter have `can_modify` as its default value. + - Anonymous users now see a log in button below the calendar on the resource page helping them understand that they need to log in to continue. + - Errors from respa backend are not swallowed in our ApiClient anymore. + - Allow user to select time slot which is already happening. + + **HOTFIX** + - Fix resource information headlines and icon. + - Fix default timezone being overrided and disappear from moment object by fullcalendar moment-timezone plugin. + + **CHANGELOG** + - [#995](https://github.com/City-of-Helsinki/varaamo/pull/995) Fix resource information headlines and icon. + - [#968](https://github.com/City-of-Helsinki/varaamo/pull/968) Support payments for Varaamo. + - [#999](https://github.com/City-of-Helsinki/varaamo/pull/968) Add reusable FullCalendar component. Used in resource page. + - [#1002](https://github.com/City-of-Helsinki/varaamo/pull/1002) Replace failure message and add a return button. + - [#1004](https://github.com/City-of-Helsinki/varaamo/pull/1004) Staff cannot see normal reservations. + - [#1005](https://github.com/City-of-Helsinki/varaamo/pull/1005) Reservation information modal is missing metadata fields. + - [#1006](https://github.com/City-of-Helsinki/varaamo/pull/1006) Manage reservation filter buttons. + - [#1026](https://github.com/City-of-Helsinki/varaamo/pull/1026) Add support for minPeriod auto-select. + - [#1027](https://github.com/City-of-Helsinki/varaamo/pull/1027) Improve resource page usability for anonymous users. + - [#1028](https://github.com/City-of-Helsinki/varaamo/pull/1028) Always fetch reservations with start and end filters. + - [#1029](https://github.com/City-of-Helsinki/varaamo/pull/1029) Add option to configure time zone for resources. + - [#1033](https://github.com/City-of-Helsinki/varaamo/pull/1033) Show errors to staff members if editing of reservations fail. + - [#1031](https://github.com/City-of-Helsinki/varaamo/pull/1031) Use resource's userPermissions.isAdmin flag to check if user isStaff. + - [#1032](https://github.com/City-of-Helsinki/varaamo/pull/1032) Better max period handling for FullCalendar. + - [#1035](https://github.com/City-of-Helsinki/varaamo/pull/1035) Daily / weekly view for mobile. + - [#1037](https://github.com/City-of-Helsinki/varaamo/pull/1037) Allow to select timeslot which is already happening. + - [#1039](https://github.com/City-of-Helsinki/varaamo/pull/1039) Fix moment-timezone default timezone. + # 0.4.2 **HOTFIX** - ``` - Fix various styling issue for date-picker [#991](https://github.com/City-of-Helsinki/varaamo/pull/991) - ``` + - Add `isAdmin` check for RecurringReservationControl to normal user will not able to make recurring reservation. [#993](https://github.com/City-of-Helsinki/varaamo/pull/993/) + # 0.4.1 **HOTFIX** - ``` - Fix security warnings for dependencies: react-select, jest, postcss-loader, codecov, node-sass, eslint - ``` # 0.4.0 **MAJOR CHANGES** - ``` - Add new purpose section for sauna and organize events. As well as mock placeholder icon. - Some technical improvements. - ``` + **CHANGELOG** - [#940](https://github.com/City-of-Helsinki/varaamo/pull/940) Upgrade fortawesome, add new temp purpose icon. - [#939](https://github.com/City-of-Helsinki/varaamo/pull/939) Rename all classnames imports to classNames @@ -23,7 +64,6 @@ # 0.3.0 **MAJOR CHANGES** - ``` - Add translation for date-picker, show date and month in currently selected language. - Set varaamo timezone to flexible base on user local timezone. @@ -31,7 +71,6 @@ - Add slotSize and minPeriod to reservation select, enable ability to reserve sauna slots with default amount of minPeriod. Time slot range equal with slotSize config from backend. - Show access-code pending text if the access-code is generated 24h before reservation starts. - ``` **CHANGELOG** @@ -50,12 +89,12 @@ - Add new selection field to sort filtered resources. Currently support to search by name, type, premise, people. - Temporarily only show warning messages in 3 languages for IE11 user. - - Ability to favourite resources straight on search view instead going to resource detail page. + - Ability to favorite resources straight on search view instead going to resource detail page. **CHANGELOG** - [#895](https://github.com/City-of-Helsinki/varaamo/pull/895) Add sort to sort filtered resources. - - [#904](https://github.com/City-of-Helsinki/varaamo/pull/904) Favourite Resource on search view. + - [#904](https://github.com/City-of-Helsinki/varaamo/pull/904) Favorite Resource on search view. - [#909](https://github.com/City-of-Helsinki/varaamo/pull/909) Show warning message for IE11 users. # 0.1.1 diff --git a/README.md b/README.md index e8bf86149..1229b9912 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ By default the running app can be found at `localhost:3000`. TARGET_APP API_URL CUSTOM_MUNICIPALITY_OPTIONS + TIME_ZONE ``` Environment's variable guideline: @@ -82,6 +83,8 @@ By default the running app can be found at `localhost:3000`. Without this config, default to use 3 central cities Helsinki, Espoo, Vantaa as options. + - `TIME_ZONE`: + The time zone of your resources. Bookings and calendars will be shown with this time zone no matter what the end users' browser is configured for. Defaults to Europe/Helsinki. 3. Then, start the development server: @@ -202,7 +205,7 @@ All setting was included under .vscode directory. - On Jest test: [Guideline](https://jestjs.io/docs/en/troubleshooting#debugging-in-vs-code). Setting was under `Vscode Jest debugger` name. - - Put breakpoint in test file `(*.spec.js)` + - Put breakpoint in test file `(*.test.js)` - Run command: diff --git a/app/actions/__tests__/reservationActions.spec.js b/app/actions/__tests__/reservationActions.test.js similarity index 93% rename from app/actions/__tests__/reservationActions.spec.js rename to app/actions/__tests__/reservationActions.test.js index bf8e47777..0d36e3334 100644 --- a/app/actions/__tests__/reservationActions.spec.js +++ b/app/actions/__tests__/reservationActions.test.js @@ -1,7 +1,7 @@ import simple from 'simple-mock'; -import * as apiUtils from 'utils/apiUtils'; -import * as reservationActions from 'actions/reservationActions'; +import * as apiUtils from '../../utils/apiUtils'; +import * as reservationActions from '../reservationActions'; describe('Actions: reservationActions', () => { const reservation = { diff --git a/app/actions/__tests__/resourceActions.spec.js b/app/actions/__tests__/resourceActions.test.js similarity index 87% rename from app/actions/__tests__/resourceActions.spec.js rename to app/actions/__tests__/resourceActions.test.js index 68580bf7e..4cf8f508a 100644 --- a/app/actions/__tests__/resourceActions.spec.js +++ b/app/actions/__tests__/resourceActions.test.js @@ -1,8 +1,7 @@ -import types from 'constants/ActionTypes'; - -import { favoriteResource, unfavoriteResource } from 'actions/resourceActions'; -import { buildAPIUrl } from 'utils/apiUtils'; -import { createApiTest } from 'utils/testUtils'; +import types from '../../constants/ActionTypes'; +import { favoriteResource, unfavoriteResource } from '../resourceActions'; +import { buildAPIUrl } from '../../utils/apiUtils'; +import { createApiTest } from '../../utils/testUtils'; describe('Actions: resourceActions', () => { describe('favoriteResource', () => { diff --git a/app/actions/__tests__/routeActions.spec.js b/app/actions/__tests__/routeActions.test.js similarity index 91% rename from app/actions/__tests__/routeActions.spec.js rename to app/actions/__tests__/routeActions.test.js index 974b7b6eb..58f855463 100644 --- a/app/actions/__tests__/routeActions.spec.js +++ b/app/actions/__tests__/routeActions.test.js @@ -1,7 +1,7 @@ import simple from 'simple-mock'; import * as reduxActions from 'redux-actions'; -import { updateRoute } from 'actions/routeActions'; +import { updateRoute } from '../routeActions'; describe('Actions: resourceActions', () => { describe('updateRoute', () => { diff --git a/app/actions/__tests__/searchActions.spec.js b/app/actions/__tests__/searchActions.test.js similarity index 93% rename from app/actions/__tests__/searchActions.spec.js rename to app/actions/__tests__/searchActions.test.js index 478dc1248..7c62856a4 100644 --- a/app/actions/__tests__/searchActions.spec.js +++ b/app/actions/__tests__/searchActions.test.js @@ -1,7 +1,7 @@ import simple from 'simple-mock'; -import { getPiwikActionName, searchResources } from 'actions/searchActions'; -import * as apiUtils from 'utils/apiUtils'; +import { getPiwikActionName, searchResources } from '../searchActions'; +import * as apiUtils from '../../utils/apiUtils'; describe('Actions: searchActions', () => { let getRequestTypeDescriptorMock; diff --git a/app/actions/__tests__/unitActions.spec.js b/app/actions/__tests__/unitActions.test.js similarity index 83% rename from app/actions/__tests__/unitActions.spec.js rename to app/actions/__tests__/unitActions.test.js index ef2f0c194..abf78279f 100644 --- a/app/actions/__tests__/unitActions.spec.js +++ b/app/actions/__tests__/unitActions.test.js @@ -1,9 +1,8 @@ -import types from 'constants/ActionTypes'; - import simple from 'simple-mock'; -import * as apiUtils from 'utils/apiUtils'; -import { fetchUnits } from 'actions/unitActions'; +import types from '../../constants/ActionTypes'; +import * as apiUtils from '../../utils/apiUtils'; +import { fetchUnits } from '../unitActions'; describe('Actions: unitActions', () => { let getRequestTypeDescriptorMock; diff --git a/app/actions/__tests__/userActions.spec.js b/app/actions/__tests__/userActions.test.js similarity index 86% rename from app/actions/__tests__/userActions.spec.js rename to app/actions/__tests__/userActions.test.js index c618cfa7f..5a2f514c2 100644 --- a/app/actions/__tests__/userActions.spec.js +++ b/app/actions/__tests__/userActions.test.js @@ -1,7 +1,7 @@ import simple from 'simple-mock'; -import * as apiUtils from 'utils/apiUtils'; -import { fetchUser } from 'actions/userActions'; +import * as apiUtils from '../../utils/apiUtils'; +import { fetchUser } from '../userActions'; describe('Actions: userActions', () => { let getRequestTypeDescriptorMock; diff --git a/app/actions/notificationsActions.js b/app/actions/notificationsActions.js deleted file mode 100644 index be3662b09..000000000 --- a/app/actions/notificationsActions.js +++ /dev/null @@ -1,13 +0,0 @@ -import types from 'constants/ActionTypes'; - -import { createAction } from 'redux-actions'; - - -const addNotification = createAction(types.UI.ADD_NOTIFICATION); - -const hideNotification = createAction(types.UI.HIDE_NOTIFICATION); - -export { - addNotification, - hideNotification, -}; diff --git a/app/actions/purposeActions.js b/app/actions/purposeActions.js deleted file mode 100644 index fdc175199..000000000 --- a/app/actions/purposeActions.js +++ /dev/null @@ -1,35 +0,0 @@ -import types from 'constants/ActionTypes'; - -import { RSAA } from 'redux-api-middleware'; - -import schemas from 'store/middleware/Schemas'; -import { - buildAPIUrl, - getErrorTypeDescriptor, - getHeadersCreator, - getRequestTypeDescriptor, - getSuccessTypeDescriptor, -} from 'utils/apiUtils'; - -function fetchPurposes() { - return { - [RSAA]: { - types: [ - getRequestTypeDescriptor(types.API.PURPOSES_GET_REQUEST), - getSuccessTypeDescriptor( - types.API.PURPOSES_GET_SUCCESS, - { schema: schemas.paginatedPurposesSchema } - ), - getErrorTypeDescriptor(types.API.PURPOSES_GET_ERROR), - ], - endpoint: buildAPIUrl('purpose'), - method: 'GET', - headers: getHeadersCreator(), - bailout: state => !state.api.shouldFetch.purposes, - }, - }; -} - -export { - fetchPurposes, -}; diff --git a/app/actions/reservationActions.js b/app/actions/reservationActions.js index 9d9a0d391..6d281b87b 100644 --- a/app/actions/reservationActions.js +++ b/app/actions/reservationActions.js @@ -1,18 +1,17 @@ -import types from 'constants/ActionTypes'; - -import pickBy from 'lodash/pickBy'; import { decamelizeKeys } from 'humps'; +import pickBy from 'lodash/pickBy'; import { RSAA } from 'redux-api-middleware'; -import schemas from 'store/middleware/Schemas'; +import types from '../constants/ActionTypes'; +import schemas from '../store/middleware/Schemas'; import { buildAPIUrl, getErrorTypeDescriptor, getHeadersCreator, getRequestTypeDescriptor, getSuccessTypeDescriptor, -} from 'utils/apiUtils'; -import { getMissingValues, isStaffEvent } from 'utils/reservationUtils'; +} from '../utils/apiUtils'; +import { getMissingValues, isStaffEvent } from '../utils/reservationUtils'; function commentReservation(reservation, resource, comments) { const missingValues = getMissingValues(reservation); diff --git a/app/actions/resourceActions.js b/app/actions/resourceActions.js index 245c24ab1..91599fd52 100644 --- a/app/actions/resourceActions.js +++ b/app/actions/resourceActions.js @@ -1,15 +1,14 @@ -import types from 'constants/ActionTypes'; - import { RSAA } from 'redux-api-middleware'; -import schemas from 'store/middleware/Schemas'; +import types from '../constants/ActionTypes'; +import schemas from '../store/middleware/Schemas'; import { buildAPIUrl, getErrorTypeDescriptor, getHeadersCreator, getRequestTypeDescriptor, getSuccessTypeDescriptor, -} from 'utils/apiUtils'; +} from '../utils/apiUtils'; function fetchFavoritedResources(timeAsMoment, source) { const params = { @@ -40,7 +39,6 @@ function fetchResource(id, params = {}) { function fetchResources(params = {}, source) { const fetchParams = Object.assign({}, params, { pageSize: 500 }); - return { [RSAA]: { types: [ diff --git a/app/actions/searchActions.js b/app/actions/searchActions.js index feecb17d9..99db6e2f9 100644 --- a/app/actions/searchActions.js +++ b/app/actions/searchActions.js @@ -1,19 +1,17 @@ -import types from 'constants/ActionTypes'; -import constants from 'constants/AppConstants'; - import { createAction } from 'redux-actions'; import { RSAA } from 'redux-api-middleware'; - -import schemas from 'store/middleware/Schemas'; +import types from '../constants/ActionTypes'; +import constants from '../constants/AppConstants'; +import schemas from '../store/middleware/Schemas'; import { buildAPIUrl, getErrorTypeDescriptor, getHeadersCreator, getRequestTypeDescriptor, getSuccessTypeDescriptor, -} from 'utils/apiUtils'; -import { getFetchParamsFromFilters } from 'utils/searchUtils'; +} from '../utils/apiUtils'; +import { getFetchParamsFromFilters } from '../utils/searchUtils'; const clearSearchResults = createAction(types.UI.CLEAR_SEARCH_FILTERS); const toggleMap = createAction(types.UI.TOGGLE_SEARCH_SHOW_MAP); diff --git a/app/actions/uiActions.js b/app/actions/uiActions.js index 83e4d339f..124fe1472 100644 --- a/app/actions/uiActions.js +++ b/app/actions/uiActions.js @@ -1,9 +1,8 @@ - -import types from 'constants/ActionTypes'; -import ModalTypes from 'constants/ModalTypes'; - import { createAction } from 'redux-actions'; +import types from '../constants/ActionTypes'; +import ModalTypes from '../constants/ModalTypes'; + const cancelReservationEdit = createAction(types.UI.CANCEL_RESERVATION_EDIT); const cancelReservationEditInInfoModal = createAction( @@ -78,8 +77,6 @@ const openReservationCommentModal = createAction( () => ModalTypes.RESERVATION_COMMENT ); -const selectReservationSlot = createAction(types.UI.SELECT_RESERVATION_SLOT); - const selectReservationToCancel = createAction(types.UI.SELECT_RESERVATION_TO_CANCEL); const selectReservationToEdit = createAction(types.UI.SELECT_RESERVATION_TO_EDIT); @@ -92,9 +89,7 @@ const showReservationInfoModal = createAction(types.UI.SHOW_RESERVATION_INFO_MOD const startReservationEditInInfoModal = createAction(types.UI.START_RESERVATION_EDIT_IN_INFO_MODAL); -const toggleTimeSlot = createAction(types.UI.TOGGLE_TIME_SLOT); - -const clearTimeSlots = createAction(types.UI.CLEAR_TIME_SLOTS); +const setSelectedTimeSlots = createAction(types.UI.SET_SELECTED_TIME_SLOTS); const toggleResourceMap = createAction(types.UI.TOGGLE_RESOURCE_SHOW_MAP); @@ -103,7 +98,6 @@ const unselectAdminResourceType = createAction(types.UI.UNSELECT_ADMIN_RESOURCE_ export { cancelReservationEdit, cancelReservationEditInInfoModal, - clearTimeSlots, changeAdminReservationFilters, changeAdminResourcesPageDate, changeSearchFilters, @@ -125,11 +119,10 @@ export { openResourceTermsModal, selectReservationToCancel, selectReservationToEdit, - selectReservationSlot, selectReservationToShow, showReservationInfoModal, startReservationEditInInfoModal, toggleResourceMap, - toggleTimeSlot, unselectAdminResourceType, + setSelectedTimeSlots, }; diff --git a/app/actions/unitActions.js b/app/actions/unitActions.js index ded90366c..8309733da 100644 --- a/app/actions/unitActions.js +++ b/app/actions/unitActions.js @@ -1,15 +1,14 @@ -import types from 'constants/ActionTypes'; - import { RSAA } from 'redux-api-middleware'; -import schemas from 'store/middleware/Schemas'; +import types from '../constants/ActionTypes'; +import schemas from '../store/middleware/Schemas'; import { buildAPIUrl, getErrorTypeDescriptor, getHeadersCreator, getRequestTypeDescriptor, getSuccessTypeDescriptor, -} from 'utils/apiUtils'; +} from '../utils/apiUtils'; function fetchUnits() { const fetchParams = { pageSize: 500, unit_has_resource: true }; diff --git a/app/actions/userActions.js b/app/actions/userActions.js index ce322bc6b..13dec6e96 100644 --- a/app/actions/userActions.js +++ b/app/actions/userActions.js @@ -1,14 +1,13 @@ -import types from 'constants/ActionTypes'; - import { RSAA } from 'redux-api-middleware'; +import types from '../constants/ActionTypes'; import { buildAPIUrl, getErrorTypeDescriptor, getHeadersCreator, getRequestTypeDescriptor, getSuccessTypeDescriptor, -} from 'utils/apiUtils'; +} from '../utils/apiUtils'; function fetchUser(id, params = {}) { return { diff --git a/app/assets/icons/comment.svg b/app/assets/icons/comment.svg new file mode 100644 index 000000000..3f836e351 --- /dev/null +++ b/app/assets/icons/comment.svg @@ -0,0 +1,16 @@ + + + + alert + Created with Sketch. + + + + + + + + + + + diff --git a/app/assets/icons/completed.svg b/app/assets/icons/completed.svg new file mode 100644 index 000000000..dabbf37a3 --- /dev/null +++ b/app/assets/icons/completed.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/icons/error.svg b/app/assets/icons/error.svg new file mode 100644 index 000000000..456b486d5 --- /dev/null +++ b/app/assets/icons/error.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/icons/hds-info.svg b/app/assets/icons/hds-info.svg new file mode 100644 index 000000000..db748d86f --- /dev/null +++ b/app/assets/icons/hds-info.svg @@ -0,0 +1,15 @@ + + + + Icon color Copy + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/icons/hds-success.svg b/app/assets/icons/hds-success.svg new file mode 100644 index 000000000..21fa43ed3 --- /dev/null +++ b/app/assets/icons/hds-success.svg @@ -0,0 +1,12 @@ + + + + Path Copy 2 + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/app/assets/icons/hds-warning.svg b/app/assets/icons/hds-warning.svg new file mode 100644 index 000000000..edf4a103d --- /dev/null +++ b/app/assets/icons/hds-warning.svg @@ -0,0 +1,18 @@ + + + + Icon color Copy + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/styles/main.scss b/app/assets/styles/main.scss index 43c088da4..3eeb254fc 100644 --- a/app/assets/styles/main.scss +++ b/app/assets/styles/main.scss @@ -10,16 +10,20 @@ @import '~react-notifications/lib/notifications.css'; @import '~react-toggle/style.css'; +@import '~@fullcalendar/core/main.css'; +@import '~@fullcalendar/daygrid/main.css'; +@import '~@fullcalendar/timegrid/main.css'; + @import './layout'; @import './mixins'; @import '../../pages/pages'; @import '../../shared/shared'; +@import '../../../src/domain/domain'; +@import '../../../src/common/common'; @import './elements'; @import './loader'; @import './modals'; @import './react-select'; @import './react-toggle'; - -@import './customization/espoo/customization'; diff --git a/app/constants/ActionTypes.js b/app/constants/ActionTypes.js index d34ed054e..d75a4b927 100644 --- a/app/constants/ActionTypes.js +++ b/app/constants/ActionTypes.js @@ -4,10 +4,6 @@ export default { AUTH_GET_REQUEST: 'AUTH_GET_REQUEST', AUTH_GET_SUCCESS: 'AUTH_GET_SUCCESS', - PURPOSES_GET_ERROR: 'PURPOSES_GET_ERROR', - PURPOSES_GET_REQUEST: 'PURPOSES_GET_REQUEST', - PURPOSES_GET_SUCCESS: 'PURPOSES_GET_SUCCESS', - RESERVATION_DELETE_ERROR: 'RESERVATION_DELETE_ERROR', RESERVATION_DELETE_REQUEST: 'RESERVATION_DELETE_REQUEST', RESERVATION_DELETE_SUCCESS: 'RESERVATION_DELETE_SUCCESS', @@ -61,7 +57,6 @@ export default { CHANGE_SEARCH_FILTERS: 'CHANGE_SEARCH_FILTERS', CLEAR_RESERVATIONS: 'CLEAR_RESERVATIONS', CLEAR_SEARCH_FILTERS: 'CLEAR_SEARCH_FILTERS', - CLEAR_TIME_SLOTS: 'CLEAR_TIME_SLOTS', CLOSE_MODAL: 'CLOSE_MODAL', DISABLE_GEOPOSITION: 'DISABLE_GEOPOSITION', DISABLE_TIME_RANGE: 'DISABLE_TIME_RANGE', @@ -75,7 +70,6 @@ export default { SEARCH_MAP_CLICK: 'SEARCH_MAP_CLICK', OPEN_MODAL: 'OPEN_MODAL', SELECT_SEARCH_RESULTS_UNIT: 'SELECT_SEARCH_RESULTS_UNIT', - SELECT_RESERVATION_SLOT: 'SELECT_RESERVATION_SLOT', SELECT_RESERVATION_TO_CANCEL: 'SELECT_RESERVATION_TO_CANCEL', SELECT_RESERVATION_TO_EDIT: 'SELECT_RESERVATION_TO_EDIT', SELECT_RESERVATION_TO_SHOW: 'SELECT_RESERVATION_TO_SHOW', @@ -83,7 +77,7 @@ export default { START_RESERVATION_EDIT_IN_INFO_MODAL: 'START_RESERVATION_EDIT_IN_INFO_MODAL', TOGGLE_RESOURCE_SHOW_MAP: 'TOGGLE_RESOURCE_SHOW_MAP', TOGGLE_SEARCH_SHOW_MAP: 'TOGGLE_SEARCH_SHOW_MAP', - TOGGLE_TIME_SLOT: 'TOGGLE_TIME_SLOT', UNSELECT_ADMIN_RESOURCE_TYPE: 'UNSELECT_ADMIN_RESOURCE_TYPE', + SET_SELECTED_TIME_SLOTS: 'SET_SELECTED_TIME_SLOTS', }, }; diff --git a/app/constants/AppConstants.js b/app/constants/AppConstants.js index b92dd8f1c..252d7ef9f 100644 --- a/app/constants/AppConstants.js +++ b/app/constants/AppConstants.js @@ -14,14 +14,8 @@ export default { timePeriod: 30, timePeriodType: 'minutes', }, - NOTIFICATION_DEFAULTS: { - message: '', - type: 'info', - timeOut: 5000, - hidden: false, - }, REQUIRED_API_HEADERS: { - Accept: 'application/json', + 'Accept': 'application/json', 'Accept-Language': 'fi', 'Content-Type': 'application/json', }, @@ -52,9 +46,7 @@ export default { freeOfCharge: '', date: '', distance: '', - duration: 0, municipality: [], - end: '', lat: '', lon: '', orderBy: '', @@ -62,18 +54,16 @@ export default { people: '', purpose: '', search: '', - start: '', unit: '', - useTimeRange: false, + availableBetween: '', }, TIME_FORMAT: 'H:mm', - TIME_SLOT_DEFAULT_LENGTH: 30, + TIME_ZONE: SETTINGS.TIME_ZONE, TRACKING: SETTINGS.TRACKING, SORT_BY_OPTIONS: { NAME: 'resource_name_lang', TYPE: 'type_name_lang', PREMISES: 'unit_name_lang', PEOPLE: 'people_capacity', - // TODO: sortby 'open now' should be implemented later after API support it } }; diff --git a/app/i18n/changeLocale.spec.js b/app/i18n/__tests__/changeLocale.test.js similarity index 74% rename from app/i18n/changeLocale.spec.js rename to app/i18n/__tests__/changeLocale.test.js index f7e1a8e33..b920cec2e 100644 --- a/app/i18n/changeLocale.spec.js +++ b/app/i18n/__tests__/changeLocale.test.js @@ -1,5 +1,5 @@ -import changeLocale from './changeLocale'; -import * as persistState from 'store/middleware/persistState'; +import changeLocale from '../changeLocale'; +import * as persistState from '../../store/middleware/persistState'; describe('changeLocale', () => { test('should invoke savePersistLocale', () => { diff --git a/app/i18n/changeLocale.js b/app/i18n/changeLocale.js index 198d980be..75e165f46 100644 --- a/app/i18n/changeLocale.js +++ b/app/i18n/changeLocale.js @@ -1,10 +1,10 @@ import moment from 'moment'; import { updateIntl } from 'react-intl-redux'; -import { savePersistLocale } from 'store/middleware/persistState'; -import enMessages from 'i18n/messages/en.json'; -import fiMessages from 'i18n/messages/fi.json'; -import svMessages from 'i18n/messages/sv.json'; +import { savePersistLocale } from '../store/middleware/persistState'; +import enMessages from './messages/en.json'; +import fiMessages from './messages/fi.json'; +import svMessages from './messages/sv.json'; const messages = { fi: fiMessages, @@ -12,14 +12,13 @@ const messages = { sv: svMessages, }; -function changeLocale(language) { - const locale = language === 'sv' ? 'se' : language; +function changeLocale(locale) { savePersistLocale(locale); - moment.locale(`varaamo-${locale}`); + return updateIntl({ locale, - messages: messages[language], + messages: messages[locale], }); } diff --git a/app/i18n/index.js b/app/i18n/index.js deleted file mode 100644 index add84bc5f..000000000 --- a/app/i18n/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import changeLocale from './changeLocale'; -import initI18n from './initI18n'; -import injectT from './injectT'; - -export { - changeLocale, - initI18n, - injectT, -}; diff --git a/app/i18n/initI18n.js b/app/i18n/initI18n.js index b95f6178a..9e0fea897 100644 --- a/app/i18n/initI18n.js +++ b/app/i18n/initI18n.js @@ -2,26 +2,24 @@ import 'moment/locale/en-gb'; import 'moment/locale/fi'; import 'moment/locale/sv'; import 'moment-timezone/builds/moment-timezone-with-data-10-year-range'; - -import constants from 'constants/AppConstants'; - import moment from 'moment'; import { addLocaleData } from 'react-intl'; import en from 'react-intl/locale-data/en'; import fi from 'react-intl/locale-data/fi'; -import se from 'react-intl/locale-data/se'; - -import { loadPersistedLocale } from 'store/middleware/persistState'; -import enMessages from 'i18n/messages/en.json'; -import fiMessages from 'i18n/messages/fi.json'; -import svMessages from 'i18n/messages/sv.json'; +import sv from 'react-intl/locale-data/sv'; +import constants from '../constants/AppConstants'; +import { loadPersistedLocale } from '../store/middleware/persistState'; +import enMessages from './messages/en.json'; +import fiMessages from './messages/fi.json'; +import svMessages from './messages/sv.json'; const messages = { en: enMessages, fi: fiMessages, - se: svMessages, + sv: svMessages, }; +moment.tz.setDefault(SETTINGS.TIME_ZONE); moment.defineLocale('varaamo-en', { parentLocale: 'en-gb', @@ -35,11 +33,11 @@ moment.defineLocale('varaamo-fi', { }, }); -moment.defineLocale('varaamo-se', { +moment.defineLocale('varaamo-sv', { parentLocale: 'sv', }); -addLocaleData([...en, ...fi, ...se]); +addLocaleData([...en, ...fi, ...sv]); function initI18n() { const persistedData = loadPersistedLocale(); diff --git a/app/i18n/injectT.js b/app/i18n/injectT.js index b48f59bb4..20250998c 100644 --- a/app/i18n/injectT.js +++ b/app/i18n/injectT.js @@ -17,7 +17,7 @@ function injectT(WrappedComponent) { } render() { - return ; + return ; } } diff --git a/app/i18n/messages/en.json b/app/i18n/messages/en.json index b97a6ec93..56cb01ad2 100644 --- a/app/i18n/messages/en.json +++ b/app/i18n/messages/en.json @@ -39,6 +39,22 @@ "common.addressZipLabel": "Postal code", "common.back": "Back", "common.billingAddressLabel": "Invoicing address", + "common.billingAddressCityLabel": "Billing address city", + "common.billingAddressStreetLabel": "Billing address street", + "common.billingAddressZipLabel": "Billing address zipcode", + "common.companyLabel": "Company", + "common.paymentInformationLabel": "Payment information", + "common.billingFirstNameLabel": "First name", + "common.billingLastNameLabel": "Last name", + "common.billingPhoneNumberLabel": "Phone", + "common.billingEmailAddressLabel": "Email", + "common.priceLabel": "Price", + "common.totalPriceLabel": "Total price", + "common.priceWithVAT": "{price}€ (VAT {vat}% inc.)", + "payment.title": "Payment interrupted", + "payment.text": "Payment interrupted.", + "payment.return": "Return", + "payment.link": "Back to your reservations", "common.cancel": "Cancel", "common.cancelled": "Cancelled", "common.cancelling": "Cancelling...", @@ -53,10 +69,15 @@ "common.numberOfParticipantsLabel": "Number of participants", "common.ok": "OK", "common.optionsAllLabel": "All", + "common.next": "Next", "common.previous": "Previous", "common.requested": "Processed", "common.reservationTimeLabel": "Time and date of the reservation", + "common.reservationExtraQuestions": "Reservation extra questions", "common.reserverAddressLabel": "Address", + "common.reserverAddressCityLabel": "City", + "common.reserverAddressStreetLabel": "Street address", + "common.reserverAddressZipLabel": "Postcode", "common.reserverEmailAddressLabel": "E-mail", "common.reserverIdLabel": "Business ID / social security number", "common.reserverNameLabel": "The person making the reservation / lessee", @@ -65,8 +86,11 @@ "common.save": "Save", "common.saving": "Saving...", "common.select": "Select", + "common.pay": "Pay", "common.userNameLabel": "Account name", "common.userEmailLabel": "Account email", + "paymentTerms.title": "Payment terms and conditions", + "paymentTerms.terms": "Payment terms and conditions", "SelectControl.noOptions": "No options", "ConfirmReservationModal.beforeText": "Original reservation time:", "ConfirmReservationModal.editTitle": "Changing reservation", @@ -79,10 +103,10 @@ "DatePickerControl.buttonLabel": "Date", "DatePickerControl.header": "Select date", "DatePickerControl.label": "When?", - "Footer.espooText": "Varaamo is the City of Helsinki’s reservation booking system, which is being trialled in certain premises provided by Espoo City Library. This is a pilot version, so please feel free to send us any feedback you may have.

", + "Footer.espooText": "Varaamo is the City of Helsinki’s reservation booking system, which is being trialled in certain premises provided by Espoo City Library. This is a pilot version, so please feel free to send us any feedback you may have.", "Footer.feedbackLink": "You can send your feedback here.", - "Footer.helsinkiText": "Varaamo is the City of Helsinki’s reservation booking system. This is a pilot version, so please feel free to send us any feedback you may have.

", - "Footer.vantaaText": "Varaamo is the City of Helsinki’s reservation booking system, which is also used by the cities of Espoo and Vantaa. This is a pilot version, so please feel free to send us any feedback you may have.

", + "Footer.helsinkiText": "Varaamo is the City of Helsinki’s reservation booking system. This is a pilot version, so please feel free to send us any feedback you may have.", + "Footer.vantaaText": "Varaamo is the City of Helsinki’s reservation booking system, which is also used by the cities of Espoo and Vantaa. This is a pilot version, so please feel free to send us any feedback you may have.", "HomePage.title": "Welcome", "HomePage.contentTitle": "Premises and equipment available for booking", "HomePage.contentSubTitle": "Through Varaamo, you can reserve public premises and equipment for your own use", @@ -110,6 +134,7 @@ "Navbar.language-swedish": "Swedish language", "Navbar.login": "Log in", "Navbar.logout": "Log out", + "Navbar.manageReservations": "Manage Reservations", "Navbar.search": "Search", "Navbar.userResources": "Your reservations", "NotFoundPage.checkAddress": "If you entered the address manually, please check it is written correctly.", @@ -151,6 +176,7 @@ "ReservationCalendarPickerLegend.booked": "Booked", "ReservationCalendarPickerLegend.busy": "Partially booked", "ReservationCalendarPickerLegend.free": "Free", + "ReservationCalendar.selectedTime.infoText": "{beginText} - {endText} ({durationText}) for {price}€", "ReservationCancelModal.cancelAllowedCancel": "Do not cancel the reservation", "ReservationCancelModal.cancelAllowedConfirm": "Cancel the reservation", "ReservationCancelModal.cancelAllowedTitle": "Confirming the cancellation", @@ -159,7 +185,7 @@ "ReservationCancelModal.lead": "Are you certain that you wish to cancel the following reservation:", "ReservationCancelModal.takeIntoAccount": "Please note that the reservation must be cancelled, at the latest, 5 days before the start of the reservation. You will be invoiced for unused reservations.", "ReservationConfirmation.feedbackText": "Thank you for using Varaamo! Send feedback.", - "ReservationConfirmation.reservationCreatedTitle": "Reservation saved", + "ReservationConfirmation.reservationCreatedTitle": "Reservation successful", "ReservationConfirmation.reservationEditedTitle": "Reservation updated", "ReservationConfirmation.resourceButton": "Back to premise", "ReservationConfirmation.ownReservationButton": "Back to your reservations", @@ -173,17 +199,21 @@ "ReservationEditForm.cancelEdit": "Cancel editing", "ReservationEditForm.saveChanges": "Save changes", "ReservationEditForm.startEdit": "Edit reservation", + "ReservationForm.reservationFieldsAsteriskExplanation": "Please fill in the following data for your preliminary reservation. The fields marked with an asterisk (*) are mandatory.", "ReservationForm.cancel": "Back", "ReservationForm.emailError": "Enter a valid e-mail address", "ReservationForm.eventSubjectInfo": "Event name is visible to staff only. Giving a name to the event makes it easier to guide the guests to the right space. Please remember to also tell the name to people arriving to the event.", + "ReservationForm.specificTermsTitle": "Specific terms", "ReservationForm.maxLengthError": "The maximum length of the field is {maxLength} characters", "ReservationForm.requiredError": "Mandatory information", "ReservationForm.staffEventHelp": "The Department’s own events are approved automatically and the only mandatory information is the name of the person making the reservation and a description of the event.", "ReservationForm.staffEventLabel": "The Department’s own events", "ReservationForm.termsAndConditionsError": "You must accept the user regulations of the premises in order to reserve them", + "ReservationForm.paymentTermsAndConditionsError": "You must accept the payment terms and conditions", "ReservationForm.termsAndConditionsHeader": "User regulations of the premises", "ReservationForm.termsAndConditionsLabel": "I have read and accepted the user regulations of the premises", - "ReservationInfo.loginMessage": "You must log in to reserve these premises.", + "ReservationInfo.loginMessage": "You must log in to reserve this premise.", + "ReservationInfo.loginText": "You must log in to reserve this premise.", "ReservationInfo.maxNumberOfReservations": "Maximum number of reservations per user:", "ReservationInfo.reservationMaxLength": "Maximum duration of the reservation:", "ReservationInfo.reservationMinLength": "Minimum duration of the reservation:", @@ -198,15 +228,19 @@ "ReservationInformationForm.eventInformationTitle": "Event information", "ReservationInformationForm.termsAndConditionsLabel": "I have read and accepted", "ReservationInformationForm.termsAndConditionsLink": "the user regulations of the premises", + "ReservationInformationForm.paymentTermsAndConditionsLabel": "I have read and accepted", + "ReservationInformationForm.paymentTermsAndConditionsLink": "payment terms and conditions", "ReservationListContainer.emptyMessage": "You do not have any reservations yet.", "ReservationListItem.accessCodeText": "PIN code of the premises:", "ReservationPage.detailsTime": "Time", - "ReservationPage.detailsTitle": "Reservation details", + "ReservationPage.detailsTitle": "Summary", "ReservationPage.editReservationTitle": "Edit reservation", "ReservationPage.newReservationTitle": "New reservation", + "ReservationPage.paymentText": "Moving to payment...", "ReservationPhase.timeTitle": "Reservation time", "ReservationPhase.informationTitle": "Reservation details", - "ReservationPhase.confirmationTitle": "Ready", + "ReservationPhase.confirmationTitle": "Complete", + "ReservationPhase.paymentTitle": "Payment", "ReservationPopover.selectionInfoHeader": "Select the reservation's end time", "ReservationSuccessModal.emailHelpText": "as well as on the reservation confirmation sent to the following e-mail address: {email}", "ReservationSuccessModal.failedReservationsHeader": "We were not able to make the following reservations:", @@ -228,6 +262,7 @@ "ResourceAvailability.reserved": "Reserved for the whole day", "ResourceAvailability.reservingRestricted": "Not available for reservation", "ResourceCard.peopleCapacity": "{people} people", + "ResourceEquipment.headingText": "Equipment", "ResourceHeader.backButton": "Back to search results", "ResourceHeader.maxPeriodDays": "max {days} days", "ResourceHeader.maxPeriodHours": "max {hours} h", @@ -238,36 +273,42 @@ "ResourceHeader.favoriteAddButton": "Add favorite", "ResourceHeader.favoriteRemoveButton": "Remove favorite", "ResourceIcons.free": "Free of charge", - "ResourceInfo.additionalInfoTitle": "Premise additional information", + "ResourceInfo.additionalInfoTitle": "More info", + "ResourceInfo.descriptionTitle": "Description", "ResourceInfo.equipmentHeader": "Premise equipment", "ResourceInfo.serviceMapLink": "Search route on Service Map", "ResourceInfo.loginMessage": "You must log in to reserve these premises.", - "ResourceInfo.reservationTitle": "Reservation information", + "ResourceInfo.reservationTitle": "Reservation details", "ResourceInfo.reserveTitle": "Reserve", "ResourceInfoContainer.unpublishedLabel": "unpublished", "ResourcePage.back": "Back to search results", "ResourcePage.reservationStatusHeader": "Reservation situation", "ResourcePage.reserveHeader": "Reserve the space", "ResourcePage.showMap": "Show map", + "ResourcePage.specificTerms": "Specific terms", "ResourceTypeFilter.title": "Show", + "SearchFilters.title": "What do you want to do?", + "SearchFilters.dateLabel": "When?", + "SearchFilters.advancedSearch": "Advanced search", + "SearchFilters.chargeLabel": "Premises free of charge", + "SearchFilters.peopleCapacityLabel": "Minimum number of people permitted\"", + "SearchFilters.municipalityLabel": "Municipality", + "SearchFilters.purposeLabel": "Purpose of use", + "SearchFilters.resetButton": "Reset filters", + "SearchFilters.searchButton": "Search", + "SearchFilters.unitLabel": "Premise", + "SearchFilters.searchLabel": "Search premises or equipment", + "SelectFilter.noOptionsMessage": "No options", "SearchBox.buttonText": "Search", "SearchBox.placeholder": "Search for premises by entering their information", - "SearchControlsContainer.title": "What do you want to do?", - "SearchControlsContainer.advancedSearch": "Advanced search", - "SearchControlsContainer.chargeLabel": "Premises free of charge", - "SearchControlsContainer.peopleCapacityLabel": "Minimum number of people permitted", - "SearchControlsContainer.municipalityLabel": "Municipality", - "SearchControlsContainer.purposeLabel": "Purpose of use", - "SearchControlsContainer.resetButton": "Reset filters", - "SearchControlsContainer.searchButton": "Search", - "SearchControlsContainer.unitLabel": "Premise", "SearchPage.title": "Search", + "SearchPage.emptyMessage": "No results, try to change search filters.", "ShowResourcesLink.text": "Show all premises and equipment", - "SortBy.label": "Sort By:", - "SortBy.name.label": "Name", - "SortBy.type.label": "Type", - "SortBy.premise.label": "Premise", - "SortBy.people.label": "People", + "SearchSort.label": "Sort By:", + "SearchSort.nameLabel": "Name", + "SearchSort.typeLabel": "Type", + "SearchSort.premiseLabel": "Premise", + "SearchSort.peopleLabel": "People", "TestSiteMessage.text": "This is the test version of Varaamo", "TimeRangeControl.timeRangeTitle": "Time range and minimum duration", "TimeRangeControl.title": "{date} at {start}-{end} {hours}h booking", @@ -289,5 +330,45 @@ "UserReservationsPage.preliminaryReservationsHeader": "Preliminary reservations", "UserReservationsPage.regularEmptyMessage": "No standard reservations", "UserReservationsPage.regularReservationsHeader": "Standard reservations", - "UserReservationsPage.title": "My reservations" + "UserReservationsPage.title": "My reservations", + "ManageReservationsPage.title": "Manage reservations", + "ManageReservationsList.subjectHeader": "Subject", + "ManageReservationsList.nameHeader": "Name", + "ManageReservationsList.emailHeader": "Email", + "ManageReservationsList.resourceHeader": "Resource", + "ManageReservationsList.premiseHeader": "Premise", + "ManageReservationsList.dateAndTimeHeader": "Date and time", + "ManageReservationsList.statusHeader": "Status", + "ManageReservationsList.pinHeader": "PIN", + "ManageReservationsList.actionsHeader": "Select action", + "ManageReservationsList.actionLabel.information": "Information", + "ManageReservationsList.actionLabel.approve": "Approve", + "ManageReservationsList.actionLabel.deny": "Deny", + "ManageReservationsList.actionLabel.edit": "Edit", + "ManageReservationsList.actionLabel.cancel": "Cancel", + "ManageReservationsFilters.searchLabel": "Search", + "ManageReservationsFilters.searchPlaceholder": "Search", + "ManageReservationsFilters.statusLabel": "Reservation status", + "ManageReservationsFilters.unitLabel": "Premise", + "ManageReservationsFilters.startDateLabel": "Start date", + "ManageReservationsFilters.endDateLabel": "End date", + "ManageReservationsFilters.startDatePlaceholder": "Start date", + "ManageReservationsFilters.endDatePlaceholder": "End date", + "ManageReservationsFilters.resetButton": "Reset filters", + + "ManageReservationsFilters.showOnly.title": "Show only", + "ManageReservationsFilters.showOnly.favoriteButtonLabel": "Favorite", + "ManageReservationsFilters.showOnly.canModifyButtonLabel": "Can modify", + + "Reservation.stateLabelCancelled": "Cancelled", + "Reservation.stateLabelConfirmed": "Approved", + "Reservation.stateLabelDenied": "Denied", + "Reservation.stateLabelRequested": "Requested", + "ResourceReservationCalendar.reserveButton": "Reserve", + "ResourceReservationCalendar.selectedDateLabel": "Selected time:", + "ResourceReservationCalendar.selectedDateValue": "{date} at {start} - at {end} ({duration})", + "ResourceReservationCalendar.selectedDateValueWithPrice": "{date} at {start} - at {end} ({duration}) for {price}€", + + "TimePickerCalendar.info.minPeriodText": "Reservation must be at least {duration}h long", + "TimePickerCalendar.info.maxPeriodText": "Reservation must be shorter than {duration}h" } diff --git a/app/i18n/messages/fi.json b/app/i18n/messages/fi.json index e71fdba94..a623acf7f 100644 --- a/app/i18n/messages/fi.json +++ b/app/i18n/messages/fi.json @@ -39,6 +39,22 @@ "common.addressZipLabel": "Postinumero", "common.back": "Takaisin", "common.billingAddressLabel": "Laskutusosoite", + "common.billingAddressCityLabel": "Laskun kaupunki", + "common.billingAddressStreetLabel": "Laskun osoite", + "common.billingAddressZipLabel": "Laskun postinumero", + "common.companyLabel": "Yritys", + "common.paymentInformationLabel": "Maksutiedot", + "common.billingFirstNameLabel": "Etunimi", + "common.billingLastNameLabel": "Sukunimi", + "common.billingPhoneNumberLabel": "Puhelin", + "common.billingEmailAddressLabel": "Sähköposti", + "common.priceLabel": "Hinta", + "common.totalPriceLabel": "Hinta yhteensä", + "common.priceWithVAT": "{price}€ (sis. alv. {vat}%)", + "payment.title": "Maksu keskeytetty", + "payment.text": "Maksu keskeytetty.", + "payment.return": "Palaa takaisin", + "payment.link": "Palaa omiin varauksiin", "common.cancel": "Peru", "common.cancelled": "Peruttu", "common.cancelling": "Perutaan...", @@ -53,10 +69,15 @@ "common.numberOfParticipantsLabel": "Osallistujamäärä", "common.ok": "Valmis", "common.optionsAllLabel": "Kaikki", + "common.next": "Seuraava", "common.previous": "Edellinen", "common.requested": "Käsiteltävänä", "common.reservationTimeLabel": "Varauksen ajankohta", + "common.reservationExtraQuestions": "Lisäkysymykset", "common.reserverAddressLabel": "Osoite", + "common.reserverAddressCityLabel": "Kaupunki", + "common.reserverAddressStreetLabel": "Osoite", + "common.reserverAddressZipLabel": "Postinumero", "common.reserverEmailAddressLabel": "Sähköposti", "common.reserverIdLabel": "Y-tunnus / henkilötunnus", "common.reserverNameLabel": "Varaaja / vuokraaja", @@ -65,24 +86,27 @@ "common.save": "Tallenna", "common.saving": "Tallennetaan...", "common.select": "Valitse", + "common.pay": "Maksa", "common.userNameLabel": "Tilin nimi", "common.userEmailLabel": "Tilin sähköposti", + "paymentTerms.title": "Maksuehdot", + "paymentTerms.terms": "Maksuehdot", "SelectControl.noOptions": "Ei valintoja", "ConfirmReservationModal.beforeText": "Alkuperäinen varausaika:", "ConfirmReservationModal.editTitle": "Varauksen muuttaminen", - "ConfirmReservationModal.formInfo": "Täytä vielä seuraavat tiedot alustavaa varausta varten. Tähdellä (*) merkityt tiedot ovat pakollisia.", - "ConfirmReservationModal.preliminaryReservationText": "Olet tekemässä alustavaa varausta {reservationsCount, plural, one {ajalle} other {ajoille}}:", - "ConfirmReservationModal.preliminaryReservationTitle": "Alustava varaus", - "ConfirmReservationModal.priceInfo": "Huomioi, että tilan käyttö voi olla maksullista. Tarkemmat hintatiedot löytyvät tilan tiedoista. Varaus on alustava ja käsitellään kahden arkipäivän kuluessa.", + "ConfirmReservationModal.formInfo": "Täytä vielä seuraavat tiedot käsiteltävää varausta varten. Tähdellä (*) merkityt tiedot ovat pakollisia.", + "ConfirmReservationModal.preliminaryReservationText": "Olet tekemässä käsiteltävää varausta {reservationsCount, plural, one {ajalle} other {ajoille}}:", + "ConfirmReservationModal.preliminaryReservationTitle": "Käsiteltävä varaus", + "ConfirmReservationModal.priceInfo": "Huomioi, että tilan käyttö voi olla maksullista. Tarkemmat hintatiedot löytyvät tilan tiedoista. Varaus käsitellään kahden arkipäivän kuluessa.", "ConfirmReservationModal.regularReservationText": "Oletko varma että haluat tehdä varauksen {reservationsCount, plural, one {ajalle} other {ajoille}}:", "ConfirmReservationModal.regularReservationTitle": "Varauksen tekeminen", "DatePickerControl.buttonLabel": "Päivämäärä", "DatePickerControl.header": "Valitse aika", "DatePickerControl.label": "Milloin?", - "Footer.espooText": "Varaamo on Helsingin kaupungin tilanvarauspalvelu, jota kokeillaan Espoon kaupunginkirjaston tiloissa. Kyseessä on pilottiversio, josta toivomme Sinulta palautetta.

", + "Footer.espooText": "Varaamo on Helsingin kaupungin tilanvarauspalvelu, jota kokeillaan Espoon kaupunginkirjaston tiloissa. Kyseessä on pilottiversio, josta toivomme Sinulta palautetta.", "Footer.feedbackLink": "Palautteesi voit lähettää täältä.", - "Footer.helsinkiText": "Varaamo on Helsingin kaupungin tilanvarauspalvelu. Kyseessä on pilottiversio, josta toivomme Sinulta palautetta.

", - "Footer.vantaaText": "Varaamo on Helsingin kaupungin tilanvarauspalvelu, jota kokeillaan Vantaalla alkuvaiheessa Tikkurilan kirjaston ja Pointin tiloissa. Kyseessä on pilottiversio, josta toivomme Sinulta palautetta.

", + "Footer.helsinkiText": "Varaamo on Helsingin kaupungin tilanvarauspalvelu. Kyseessä on pilottiversio, josta toivomme Sinulta palautetta.", + "Footer.vantaaText": "Varaamo on Helsingin kaupungin tilanvarauspalvelu, jota kokeillaan Vantaalla alkuvaiheessa Tikkurilan kirjaston ja Pointin tiloissa. Kyseessä on pilottiversio, josta toivomme Sinulta palautetta.", "HomePage.title": "Tervetuloa", "HomePage.contentTitle": "Tilat ja laitteet varattavana", "HomePage.contentSubTitle": "Varaamosta voit varata julkisia tiloja ja laitteita omaan käyttöösi", @@ -110,6 +134,7 @@ "Navbar.language-swedish": "Swedish language", "Navbar.login": "Kirjaudu sisään", "Navbar.logout": "Kirjaudu ulos", + "Navbar.manageReservations": " Hallitse Varauksia", "Navbar.search": "Haku", "Navbar.userResources": "Omat varaukset", "NotFoundPage.checkAddress": "Jos syötit sivun osoitteen käsin, tarkista että se on oikein.", @@ -151,6 +176,7 @@ "ReservationCalendarPickerLegend.booked": "Varattu", "ReservationCalendarPickerLegend.busy": "Osittain vapaa", "ReservationCalendarPickerLegend.free": "Vapaa", + "ReservationCalendar.selectedTime.infoText": "{beginText} - {endText} ({durationText}) yht {price}€", "ReservationCancelModal.cancelAllowedCancel": "Älä peru varausta", "ReservationCancelModal.cancelAllowedConfirm": "Peru varaus", "ReservationCancelModal.cancelAllowedTitle": "Perumisen vahvistus", @@ -173,17 +199,21 @@ "ReservationEditForm.cancelEdit": "Peruuta muokkaus", "ReservationEditForm.saveChanges": "Tallenna muutokset", "ReservationEditForm.startEdit": "Muokkaa tapahtumaa", + "ReservationForm.reservationFieldsAsteriskExplanation": "Täytä vielä seuraavat tiedot käsiteltävää varausta varten. Tähdellä (*) merkityt tiedot ovat pakollisia.", "ReservationForm.cancel": "Takaisin", "ReservationForm.emailError": "Syötä kunnollinen sähköpostiosoite", "ReservationForm.eventSubjectInfo": "Tilaisuuden nimen näkee ainoastaan henkilökunta. Antamalla tilaisuudelle nimen helpotat saapuvien vieraiden opastamista oikeaan tilaan. Muistathan ilmoittaa nimen myös tilaisuuteen saapuville.", + "ReservationForm.specificTermsTitle": "Resurssikohtaiset ehdot", "ReservationForm.maxLengthError": "Kentän maksimipituus on {maxLength} merkkiä", "ReservationForm.requiredError": "Pakollinen tieto", "ReservationForm.staffEventHelp": "Viraston oma tapahtuma hyväksytään automaattisesti ja ainoat pakolliset tiedot ovat varaajan nimi ja tilaisuuden kuvaus.", "ReservationForm.staffEventLabel": "Viraston oma tapahtuma", "ReservationForm.termsAndConditionsError": "Sinun on hyväksyttävä tilan käyttösäännöt varataksesi tilan", + "ReservationForm.paymentTermsAndConditionsError": "Sinun on hyväksyttävä maksuehdot varataksesi tilan", "ReservationForm.termsAndConditionsHeader": "Tilan käyttösäännöt", "ReservationForm.termsAndConditionsLabel": "Olen lukenut ja hyväksynyt tilan käyttösäännöt", "ReservationInfo.loginMessage": "Sinun täytyy kirjautua sisään, jotta voit tehdä varauksen tähän tilaan.", + "ReservationInfo.loginText": "Sinun täytyy kirjautua sisään, jotta voit tehdä varauksen tähän tilaan.", "ReservationInfo.maxNumberOfReservations": "Maksimimäärä varauksia per käyttäjä:", "ReservationInfo.reservationMaxLength": "Varauksen maksimipituus:", "ReservationInfo.reservationMinLength": "Varauksen vähimmäispituus:", @@ -198,21 +228,25 @@ "ReservationInformationForm.eventInformationTitle": "Tilaisuuden tiedot", "ReservationInformationForm.termsAndConditionsLabel": "Olen lukenut ja hyväksynyt", "ReservationInformationForm.termsAndConditionsLink": "tilan käyttösäännöt", + "ReservationInformationForm.paymentTermsAndConditionsLabel": "Olen lukenut ja hyväksynyt", + "ReservationInformationForm.paymentTermsAndConditionsLink": "maksuehdot", "ReservationListContainer.emptyMessage": "Sinulla ei vielä ole yhtään varausta.", "ReservationListItem.accessCodeText": "Tilan PIN-koodi:", "ReservationPage.detailsTime": "Ajankohta", "ReservationPage.detailsTitle": "Varauksen tiedot", "ReservationPage.editReservationTitle": "Muokkaa varausta", "ReservationPage.newReservationTitle": "Uusi varaus", + "ReservationPage.paymentText": "Siirrytään maksuun...", "ReservationPhase.timeTitle": "Varauksen ajankohta", "ReservationPhase.informationTitle": "Varauksen lisätiedot", "ReservationPhase.confirmationTitle": "Valmis", + "ReservationPhase.paymentTitle": "Maksu", "ReservationPopover.selectionInfoHeader": "Valitse varauksen päättymisajankohta", "ReservationSuccessModal.emailHelpText": "sekä varausvahvistuksesta, joka on lähetetty sähköpostiosoitteeseen: {email}", "ReservationSuccessModal.failedReservationsHeader": "Seuraavien varausten tekeminen ei onnistunut:", "ReservationSuccessModal.ownReservationsPageHelpText": "PIN-koodin voit tarkistaa jatkossa \"Omat varaukset\" -sivulta", - "ReservationSuccessModal.preliminaryReservationInfo": "Varaus käsitellään kahden arkipäivän kuluessa. Tarkemmat tiedot alustavasta varauksesta lähetetään varauksen yhteydessä annettuun sähköpostiosoitteeseen {email}.", - "ReservationSuccessModal.preliminaryReservationLead": "Olet tehnyt alustavan varauksen tilaan {resourceName}", + "ReservationSuccessModal.preliminaryReservationInfo": "Varaus käsitellään kahden arkipäivän kuluessa. Tarkemmat tiedot käsiteltävästä varauksesta lähetetään varauksen yhteydessä annettuun sähköpostiosoitteeseen {email}.", + "ReservationSuccessModal.preliminaryReservationLead": "Olet tehnyt käsiteltävän varauksen tilaan {resourceName}", "ReservationSuccessModal.preliminaryReservationTitle": "Varauspyyntösi on lähetetty", "ReservationSuccessModal.regularReservationLead": "Varaus tehty tilaan {resourceName}", "ReservationSuccessModal.regularReservationTitle": "Varauksen tekeminen onnistui", @@ -228,6 +262,7 @@ "ResourceAvailability.reserved": "Varattu koko päivän", "ResourceAvailability.reservingRestricted": "Ei varattavissa", "ResourceCard.peopleCapacity": "{people} henkilöä", + "ResourceEquipment.headingText": "Varustelu", "ResourceHeader.backButton": "Takaisin hakutuloksiin", "ResourceHeader.maxPeriodDays": "max {days}pv", "ResourceHeader.maxPeriodHours": "max {hours}h", @@ -238,18 +273,32 @@ "ResourceHeader.favoriteAddButton": "Lisää suosikiksi", "ResourceHeader.favoriteRemoveButton": "Poista suosikeista", "ResourceIcons.free": "Maksuton", - "ResourceInfo.additionalInfoTitle": "Tilan lisätiedot", + "ResourceInfo.additionalInfoTitle": "Lisätietoja", + "ResourceInfo.descriptionTitle": "Kuvaus", "ResourceInfo.equipmentHeader": "Tilan varusteet", "ResourceInfo.serviceMapLink": "Katso reitti Palvelukartasta", "ResourceInfo.loginMessage": "Sinun täytyy kirjautua sisään, jotta voit tehdä varauksen tähän tilaan.", "ResourceInfo.reservationTitle": "Varauksen tiedot", - "ResourceInfo.reserveTitle": "Varaa tila", + "ResourceInfo.reserveTitle": "Varaa", "ResourceInfoContainer.unpublishedLabel": "ei julkaistu", "ResourcePage.back": "Takaisin hakutuloksiin", "ResourcePage.reservationStatusHeader": "Varaustilanne", "ResourcePage.reserveHeader": "Varaa tila", "ResourcePage.showMap": "Näytä kartalla", + "ResourcePage.specificTerms": "Resurssikohtaiset ehdot", "ResourceTypeFilter.title": "Näytä", + "SearchFilters.title": "Mitä haluat tehdä?", + "SearchFilters.dateLabel": "Milloin?", + "SearchFilters.advancedSearch": "Tarkennettu haku", + "SearchFilters.chargeLabel": "Maksuton tila", + "SearchFilters.peopleCapacityLabel": "Tilan henkilömäärä vähintään", + "SearchFilters.municipalityLabel": "Kunta", + "SearchFilters.purposeLabel": "Tilan käyttötarkoitus", + "SearchFilters.resetButton": "Tyhjennä suodattimet", + "SearchFilters.searchButton": "Hae", + "SearchFilters.unitLabel": "Toimipiste", + "SearchFilters.searchLabel": "Etsi tilaa syöttämällä tilaan liittyvää tietoa", + "SelectFilter.noOptionsMessage": "Ei valintoja", "SearchBox.buttonText": "Hae", "SearchBox.placeholder": "Etsi tilaa syöttämällä tilaan liittyvää tietoa", "SearchControlsContainer.title": "Mitä haluat tehdä?", @@ -262,12 +311,13 @@ "SearchControlsContainer.searchButton": "Hae", "SearchControlsContainer.unitLabel": "Toimipiste", "SearchPage.title": "Haku", + "SearchPage.emptyMessage": "Ei yhtään hakutulosta, koita muuttaa hakuehtoja.", "ShowResourcesLink.text": "Näytä kaikki tilat ja laitteet", - "SortBy.label": "Järjestä:", - "SortBy.name.label": "Nimi", - "SortBy.type.label": "Tyyppi", - "SortBy.premise.label": "Toimipiste", - "SortBy.people.label": "Henkilömäärä", + "SearchSort.label": "Järjestä:", + "SearchSort.nameLabel": "Nimi", + "SearchSort.typeLabel": "Tyyppi", + "SearchSort.premiseLabel": "Toimipiste", + "SearchSort.peopleLabel": "Henkilömäärä", "TimeRangeControl.timeRangeTitle": "Käytä aikaväliä ja varauksen minimipituutta", "TimeRangeControl.title": "{date} klo {start}-{end} {hours}h varaus", "TestSiteMessage.text": "Tämä on Varaamon testiversio", @@ -285,9 +335,49 @@ "TimeSlots.selectedDate": "Valittu aika:", "TimeSlots.selectedTime": "klo {time}", "TimeSlots.time": "Aika", - "UserReservationsPage.preliminaryEmptyMessage": "Ei alustavia varauksia näytettäväksi.", - "UserReservationsPage.preliminaryReservationsHeader": "Alustavat varaukset", + "UserReservationsPage.preliminaryEmptyMessage": "Ei käsiteltäviä varauksia näytettäväksi.", + "UserReservationsPage.preliminaryReservationsHeader": "Käsiteltävät varaukset", "UserReservationsPage.regularEmptyMessage": "Ei tavallisia varauksia näytettäväksi.", "UserReservationsPage.regularReservationsHeader": "Tavalliset varaukset", - "UserReservationsPage.title": "Omat varaukset" + "UserReservationsPage.title": "Omat varaukset", + "ManageReservationsPage.title": "Hallitse varauksia", + "ManageReservationsList.subjectHeader": "Aihe", + "ManageReservationsList.nameHeader": "Nimi", + "ManageReservationsList.emailHeader": "Sähköposti", + "ManageReservationsList.resourceHeader": "Resurssi", + "ManageReservationsList.premiseHeader": "Toimipiste", + "ManageReservationsList.dateAndTimeHeader": "Milloin", + "ManageReservationsList.statusHeader": "Tila", + "ManageReservationsList.pinHeader": "PIN", + "ManageReservationsList.actionsHeader": "Valitse toiminta", + "ManageReservationsList.actionLabel.information": "Tiedot", + "ManageReservationsList.actionLabel.approve": "Hyväksy", + "ManageReservationsList.actionLabel.deny": "Hylkää", + "ManageReservationsList.actionLabel.edit": "Muokkaa", + "ManageReservationsList.actionLabel.cancel": "Peru", + "ManageReservationsFilters.searchLabel": "Hae", + "ManageReservationsFilters.searchPlaceholder": "Hae", + "ManageReservationsFilters.statusLabel": "Varauksen tila", + "ManageReservationsFilters.unitLabel": "Toimipiste", + "ManageReservationsFilters.startDateLabel": "Aloituspäivämäärä", + "ManageReservationsFilters.endDateLabel": "Lopetuspäivämäärä", + "ManageReservationsFilters.startDatePlaceholder": "Aloituspäivämäärä", + "ManageReservationsFilters.endDatePlaceholder": "Lopetuspäivämäärä", + "ManageReservationsFilters.resetButton": "Tyhjennä suodattimet", + + "ManageReservationsFilters.showOnly.title": "Näytä vain", + "ManageReservationsFilters.showOnly.favoriteButtonLabel": "Suosikit", + "ManageReservationsFilters.showOnly.canModifyButtonLabel": "Toimipisteeni", + + "Reservation.stateLabelCancelled": "Peruttu", + "Reservation.stateLabelConfirmed": "Hyväksytty", + "Reservation.stateLabelDenied": "Evätty", + "Reservation.stateLabelRequested": "Anottu", + "ResourceReservationCalendar.reserveButton": "Tee varaus", + "ResourceReservationCalendar.selectedDateLabel": "Valittu aika:", + "ResourceReservationCalendar.selectedDateValue": "{date} klo {start} - klo {end} ({duration})", + "ResourceReservationCalendar.selectedDateValueWithPrice": "{date} klo {start} - klo {end} ({duration}) yht {price}€", + + "TimePickerCalendar.info.minPeriodText": "Varauksen on kestettävä vähintään {duration} tuntia.", + "TimePickerCalendar.info.maxPeriodText": "Varaus saa kestää enimmillään {duration} tuntia." } diff --git a/app/i18n/messages/sv.json b/app/i18n/messages/sv.json index 092d84c32..1ef9c7456 100644 --- a/app/i18n/messages/sv.json +++ b/app/i18n/messages/sv.json @@ -39,6 +39,22 @@ "common.addressZipLabel": "Postnummer", "common.back": "Tillbaka", "common.billingAddressLabel": "Faktureringsadress", + "common.billingAddressCityLabel": "Faktureringsstad", + "common.billingAddressStreetLabel": "Faktureringsgatuadress", + "common.billingAddressZipLabel": "Faktureringspostnummer", + "common.companyLabel": "Företag", + "common.paymentInformationLabel": "Betalningsinformation", + "common.billingFirstNameLabel": "Förnamn", + "common.billingLastNameLabel": "Efternamn", + "common.billingPhoneNumberLabel": "Telefon", + "common.billingEmailAddressLabel": "E-post", + "common.priceLabel": "Pris", + "common.totalPriceLabel": "Total pris", + "common.priceWithVAT": "{price}€ (inkl. moms {vat}%)", + "payment.title": "Betalning avbruten", + "payment.text": "Betalning avbruten.", + "payment.return": "Tillbaka", + "payment.link": "Visa mina bokningar", "common.cancel": "Avbryt", "common.cancelled": "Avbokad", "common.cancelling": "Avbokas...", @@ -53,10 +69,15 @@ "common.numberOfParticipantsLabel": "Antal deltagare", "common.ok": "OK", "common.optionsAllLabel": "Alla", + "common.next": "Nästa", "common.previous": "Föregående", "common.requested": "Behandlas", "common.reservationTimeLabel": "Tidpunkt för bokning", + "common.reservationExtraQuestions": "Ytterligare frågor", "common.reserverAddressLabel": "Adress", + "common.reserverAddressCityLabel": "Stad", + "common.reserverAddressStreetLabel": "Gatuadress", + "common.reserverAddressZipLabel": "Postnummer", "common.reserverEmailAddressLabel": "E-post", "common.reserverIdLabel": "FO-nummer/personbeteckning", "common.reserverNameLabel": "Bokare/hyrestagare", @@ -65,8 +86,11 @@ "common.save": "Spara", "common.saving": "Sparas...", "common.select": "Välj", + "common.pay": "Betala", "common.userNameLabel": "Konto namn", "common.userEmailLabel": "Konto e-post", + "paymentTerms.title": "Betalningsvillkor", + "paymentTerms.terms": "Betalningsvillkor", "SelectControl.noOptions": "Inga val", "ConfirmReservationModal.beforeText": "Ursprunglig bokningstid", "ConfirmReservationModal.editTitle": "Ändra bokningen", @@ -81,10 +105,10 @@ "DatePickerControl.label": "När?", "DatePickerControl.timeRangeTitle": "Tidsurval och minsta reserveringstid", "DatePickerControl.title": "{date} kl. {start}-{end} {hours}h bokning", - "Footer.espooText": "Varaamo är Helsingfors stads utrymmesbokningstjänst, som kommer att provas i vissa av Esbo stadsbiblioteks utrymmen. Det är frågan om en pilotversion och vi hoppas att Du ger oss respons på den.

", + "Footer.espooText": "Varaamo är Helsingfors stads utrymmesbokningstjänst, som kommer att provas i vissa av Esbo stadsbiblioteks utrymmen. Det är frågan om en pilotversion och vi hoppas att Du ger oss respons på den.", "Footer.feedbackLink": "Du kan ge din respons här.", - "Footer.helsinkiText": "Varaamo är Helsingfors stads utrymmesbokningstjänst. Det är frågan om en pilotversion och vi hoppas att Du ger oss respons på den.

", - "Footer.vantaaText": "Varaamo är Helsingfors stads utrymmesbokningstjänst, som kommer att provas under ett års tid i vissa av Vanda biblioteks utrymmen. Det är frågan om en pilotversion och vi hoppas att Du ger oss respons på den.

", + "Footer.helsinkiText": "Varaamo är Helsingfors stads utrymmesbokningstjänst. Det är frågan om en pilotversion och vi hoppas att Du ger oss respons på den.", + "Footer.vantaaText": "Varaamo är Helsingfors stads utrymmesbokningstjänst, som kommer att provas under ett års tid i vissa av Vanda biblioteks utrymmen. Det är frågan om en pilotversion och vi hoppas att Du ger oss respons på den.", "HomePage.title": "Välkommen", "HomePage.contentTitle": "Utrymmen och apparater som kan bokas", "HomePage.contentSubTitle": "Med Varaamo kan du boka offentliga utrymmen och apparater för privat bruk", @@ -112,6 +136,7 @@ "Navbar.language-swedish": "Svenska språket", "Navbar.login": "Logga in", "Navbar.logout": "Logga ut", + "Navbar.manageReservations": "Administrera Bokningar", "Navbar.search": "Sök", "Navbar.userResources": "Mina bokningar", "NotFoundPage.checkAddress": "Om du matade in webbadressen manuellt, kontrollera att den är korrekt.", @@ -153,6 +178,7 @@ "ReservationCalendarPickerLegend.booked": "Reserverad", "ReservationCalendarPickerLegend.busy": "Delvis ledig", "ReservationCalendarPickerLegend.free": "Ledig", + "ReservationCalendar.selectedTime.infoText": "{beginText} - {endText} ({durationText}) för {price}€", "ReservationCancelModal.cancelAllowedCancel": "Avboka inte bokningen", "ReservationCancelModal.cancelAllowedConfirm": "Avboka bokningen", "ReservationCancelModal.cancelAllowedTitle": "Bekräfta avbokningen", @@ -175,17 +201,21 @@ "ReservationEditForm.cancelEdit": "Avbryta", "ReservationEditForm.saveChanges": "Spara ändringar", "ReservationEditForm.startEdit": "Ändra bokningstiden", + "ReservationForm.reservationFieldsAsteriskExplanation": "Fyll ännu i följande uppgifter för den preliminära bokningen. Uppgifterna markerade med en asterisk (*) är obligatoriska.", "ReservationForm.cancel": "Tillbaka", "ReservationForm.emailError": "Mata in en giltig e-postadress", "ReservationForm.eventSubjectInfo": "Evenemangets namn är synbar enbart for personalen. Genom att ge evenemanget ett namn så kan vi lotsa besökarna till de rätta utrymmen. Kom ihåg att berätta evenemangets namn även till besökarna.", + "ReservationForm.specificTermsTitle": "Specifika villkor", "ReservationForm.maxLengthError": "Den maximala längden på fältet är {maxLength} tecken", "ReservationForm.requiredError": "Obligatorisk information", "ReservationForm.staffEventHelp": "Verkets egna evenemang godkänns automatiskt och de enda obligatoriska uppgifterna är namnet på personen som bokar utrymmet och beskrivningen av evenemanget.", "ReservationForm.staffEventLabel": "Verkets eget evenemang", "ReservationForm.termsAndConditionsError": "För att kunna boka utrymmet måste du godkänna utrymmets användningsregler.", + "ReservationForm.paymentTermsAndConditionsError": "För att kunna boka utrymmet måste du måste godkänna betalningsvillkoren", "ReservationForm.termsAndConditionsHeader": "Utrymmets användningsregler", "ReservationForm.termsAndConditionsLabel": "Jag har läst och godkänner utrymmets användningsregler", "ReservationInfo.loginMessage": "För att kunna boka detta utrymme måste du logga in.", + "ReservationInfo.loginText": "För att kunna boka detta utrymme måste du logga in.", "ReservationInfo.maxNumberOfReservations": "Maximalt antal bokningar per användare:", "ReservationInfo.reservationMaxLength": "Bokningens maximala längd:", "ReservationInfo.reservationMinLength": "Minsta längd av bokningen:", @@ -200,15 +230,19 @@ "ReservationInformationForm.eventInformationTitle": "Evenemangets uppgifter", "ReservationInformationForm.termsAndConditionsLabel": "Jag har läst och godkänner", "ReservationInformationForm.termsAndConditionsLink": "utrymmets användningsregler", + "ReservationInformationForm.paymentTermsAndConditionsLabel": "Jag har läst och godkänner", + "ReservationInformationForm.paymentTermsAndConditionsLink": "betalningsvillkoren", "ReservationListContainer.emptyMessage": "Du har ännu inte några bokningar.", "ReservationListItem.accessCodeText": "Utrymmets PIN-kod:", "ReservationPage.detailsTime": "Tidpunkt", "ReservationPage.detailsTitle": "Bokningens uppgifter", "ReservationPage.editReservationTitle": "Ändra din bokning", "ReservationPage.newReservationTitle": "Ny bokning", + "ReservationPage.paymentText": "Far till betalning...", "ReservationPhase.timeTitle": "Bokningens tidpunkt", "ReservationPhase.informationTitle": "Bokningens detaljer", "ReservationPhase.confirmationTitle": "Färdig", + "ReservationPhase.paymentTitle": "Betalning", "ReservationPopover.selectionInfoHeader": "Välj evenemangets sluttid", "ReservationSuccessModal.emailHelpText": "samt på bokningsbekräftelsen som skickats till e-postadressen: {email}", "ReservationSuccessModal.failedReservationsHeader": "Följande bokningar misslyckades:", @@ -230,6 +264,7 @@ "ResourceAvailability.reserved": "Bokat hela dagen", "ResourceAvailability.reservingRestricted": "Kan inte bokas", "ResourceCard.peopleCapacity": "{people} personer", + "ResourceEquipment.headingText": "Utrustning", "ResourceHeader.backButton": "Tillbaka till sökresultat", "ResourceHeader.maxPeriodDays": "max {days} dag", "ResourceHeader.maxPeriodHours": "max {hours}h", @@ -240,36 +275,41 @@ "ResourceHeader.favoriteAddButton": "Spara som favorit", "ResourceHeader.favoriteRemoveButton": "Radera favorit", "ResourceIcons.free": "Avgiftsfri", - "ResourceInfo.additionalInfoTitle": "Utrymmets närmare uppgifter", + "ResourceInfo.additionalInfoTitle": "Mer information", + "ResourceInfo.descriptionTitle": "Beskrivning", "ResourceInfo.equipmentHeader": "Utrustning", "ResourceInfo.serviceMapLink": "Reseplanering i Servicekarta", "ResourceInfo.loginMessage": "För att kunna boka detta utrymme måste du logga in.", "ResourceInfo.reservationTitle": "Bokningsinformation", - "ResourceInfo.reserveTitle": "Boka utrymme", + "ResourceInfo.reserveTitle": "Boka", "ResourceInfoContainer.unpublishedLabel": "opublicerad", "ResourcePage.back": "Tillbaka till sökresultat", "ResourcePage.reservationStatusHeader": "Bokningsläget", "ResourcePage.reserveHeader": "Boka utrymme", "ResourcePage.showMap": "Visa på kartan", + "ResourcePage.specificTerms": "Specifika villkor", "ResourceTypeFilter.title": "Visa", "SearchBox.buttonText": "Sök", "SearchBox.placeholder": "Sök rum genom att mata in uppgifter om rummet", - "SearchControlsContainer.title": "Vad vill du göra?", - "SearchControlsContainer.advancedSearch": "Avancerad sökning", - "SearchControlsContainer.chargeLabel": "Avgiftsfri utrym", - "SearchControlsContainer.peopleCapacityLabel": "Minsta personantal i utrymmet", - "SearchControlsContainer.municipalityLabel": "Kommun", - "SearchControlsContainer.purposeLabel": "Utrymmets användningsändamål", - "SearchControlsContainer.resetButton": "Tömma sökfilter", - "SearchControlsContainer.searchButton": "Sök", - "SearchControlsContainer.unitLabel": "Utrymmet", + "SearchFilters.title": "Vad vill du göra?", + "SearchFilters.advancedSearch": "Avancerad sökning", + "SearchFilters.chargeLabel": "Avgiftsfri utrym", + "SearchFilters.peopleCapacityLabel": "Minsta personantal i utrymmet", + "SearchFilters.municipalityLabel": "Kommun", + "SearchFilters.purposeLabel": "Utrymmets användningsändamål", + "SearchFilters.resetButton": "Tömma sökfilter", + "SearchFilters.searchButton": "Sök", + "SearchFilters.unitLabel": "Utrymmet", + "SearchFilters.dateLabel": "När?", + "SearchFilters.searchLabel": "Sök utrymmen eller ar", + "SelectFilter.noOptionsMessage": "Inga val", "SearchPage.title": "Sök", "ShowResourcesLink.text": "Visa alla utrymmen och apparater", - "SortBy.label": "Sortera efter:", - "SortBy.name.label": "Namn", - "SortBy.type.label": "Typ", - "SortBy.premise.label": "Lokal", - "SortBy.people.label": "Antal personer", + "SearchSort.label": "Sortera efter:", + "SearchSort.nameLabel": "Namn", + "SearchSort.typeLabel": "Typ", + "SearchSort.premiseLabel": "Lokal", + "SearchSort.peopleLabel": "Antal personer", "TestSiteMessage.text": "Detta är Varaamo testversion", "TimeRangeControl.timeRangeTitle": "Tidsurval och minsta reserveringstid", "TimeRangeControl.title": "{date} kl. {start}-{end} {hours}h bokning", @@ -291,5 +331,45 @@ "UserReservationsPage.preliminaryReservationsHeader": "Preliminärbokningar", "UserReservationsPage.regularEmptyMessage": "Det finns inga vanliga bokningar att visa.", "UserReservationsPage.regularReservationsHeader": "Vanliga bokningar", - "UserReservationsPage.title": "Mina bokningar" + "UserReservationsPage.title": "Mina bokningar", + "ManageReservationsPage.title": "Hantera reservationer", + "ManageReservationsList.subjectHeader": "Ämne", + "ManageReservationsList.nameHeader": "Namn", + "ManageReservationsList.emailHeader": "E-post", + "ManageReservationsList.resourceHeader": "Resurs", + "ManageReservationsList.premiseHeader": "Utrymmet", + "ManageReservationsList.dateAndTimeHeader": "När", + "ManageReservationsList.statusHeader": "Status", + "ManageReservationsList.pinHeader": "PIN", + "ManageReservationsList.actionsHeader": "Välj åtgärd", + "ManageReservationsList.actionLabel.information": "Information", + "ManageReservationsList.actionLabel.approve": "Godkänn", + "ManageReservationsList.actionLabel.deny": "Förneka", + "ManageReservationsList.actionLabel.edit": "Redigera", + "ManageReservationsList.actionLabel.cancel": "Avbryt", + "ManageReservationsFilters.searchLabel": "Sök", + "ManageReservationsFilters.searchPlaceholder": "Sök", + "ManageReservationsFilters.statusLabel": "Bokningsstatus", + "ManageReservationsFilters.unitLabel": "Utrymmet", + "ManageReservationsFilters.startDateLabel": "Startdatum", + "ManageReservationsFilters.endDateLabel": "Slutdatum", + "ManageReservationsFilters.startDatePlaceholder": "Startdatum", + "ManageReservationsFilters.endDatePlaceholder": "Slutdatum", + "ManageReservationsFilters.resetButton": "Återställ sökfilter", + + "ManageReservationsFilters.showOnly.title": "Visa bara", + "ManageReservationsFilters.showOnly.favoriteButtonLabel": "Mina favoriter", + "ManageReservationsFilters.showOnly.canModifyButtonLabel": "Mina utrymmen", + + "Reservation.stateLabelCancelled": "Avbokad", + "Reservation.stateLabelConfirmed": "Godkänd", + "Reservation.stateLabelDenied": "Nekad", + "Reservation.stateLabelRequested": "Begärda", + "ResourceReservationCalendar.reserveButton": "Boka", + "ResourceReservationCalendar.selectedDateLabel": "Utvald tidpunkt:", + "ResourceReservationCalendar.selectedDateValue": "{date} kl. {start} - kl. {end} ({duration})", + "ResourceReservationCalendar.selectedDateValueWithPrice": "{date} kl. {start} - kl. {end} ({duration}) for {price}€", + + "TimePickerCalendar.info.minPeriodText": "Bokningen måste vara minst {duration} timmar lång.", + "TimePickerCalendar.info.maxPeriodText": "Bokningen får vara högst {duration} timmar lång." } diff --git a/app/pages/AppContainer.js b/app/pages/AppContainer.js index 6f9a7b336..e7e5f0a52 100644 --- a/app/pages/AppContainer.js +++ b/app/pages/AppContainer.js @@ -7,15 +7,15 @@ import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; import { createStructuredSelector } from 'reselect'; import { withRouter } from 'react-router-dom'; +import { NotificationContainer } from 'react-notifications'; -import { fetchUser } from 'actions/userActions'; -import { enableGeoposition } from 'actions/uiActions'; -import Favicon from 'shared/favicon'; -import Footer from 'shared/footer'; -import Header from 'shared/header'; -import TestSiteMessage from 'shared/test-site-message'; -import Notifications from 'shared/notifications'; -import { getCustomizationClassName } from 'utils/customizationUtils'; +import { fetchUser } from '../actions/userActions'; +import { enableGeoposition } from '../actions/uiActions'; +import Favicon from '../shared/favicon/Favicon'; +import Footer from '../../src/domain/footer/Footer'; +import Header from '../../src/domain/header/Header'; +import TestSiteMessage from '../shared/test-site-message/TestSiteMessage'; +import { getCustomizationClassName } from '../utils/customizationUtils'; const userIdSelector = state => state.auth.userId; @@ -65,7 +65,7 @@ export class UnconnectedAppContainer extends Component {
- + {this.props.children}
diff --git a/app/pages/PageWrapper.js b/app/pages/PageWrapper.js index 0aa0a5dea..d4f3301f1 100644 --- a/app/pages/PageWrapper.js +++ b/app/pages/PageWrapper.js @@ -4,9 +4,13 @@ import React from 'react'; import Grid from 'react-bootstrap/lib/Grid'; import { Helmet } from 'react-helmet'; -function PageWrapper({ - children, className, fluid = false, title, transparent = false -}) { +const PageWrapper = ({ + children, + className, + fluid = false, + title, + transparent = false, +}) => { return (
); -} +}; PageWrapper.propTypes = { children: PropTypes.node.isRequired, diff --git a/app/pages/AppContainer.spec.js b/app/pages/__tests__/AppContainer.test.js similarity index 93% rename from app/pages/AppContainer.spec.js rename to app/pages/__tests__/AppContainer.test.js index 236fe4dd0..fc4bc480c 100644 --- a/app/pages/AppContainer.spec.js +++ b/app/pages/__tests__/AppContainer.test.js @@ -2,11 +2,10 @@ import React from 'react'; import { shallow } from 'enzyme'; import simple from 'simple-mock'; -import Header from 'shared/header'; -import Notifications from 'shared/notifications'; -import { getState } from 'utils/testUtils'; -import * as customizationUtils from 'utils/customizationUtils'; -import { selector, UnconnectedAppContainer as AppContainer } from './AppContainer'; +import Header from '../../../src/domain/header/Header'; +import { getState } from '../../utils/testUtils'; +import * as customizationUtils from '../../utils/customizationUtils'; +import { selector, UnconnectedAppContainer as AppContainer } from '../AppContainer'; describe('pages/AppContainer', () => { function getWrapper(props) { @@ -73,10 +72,6 @@ describe('pages/AppContainer', () => { expect(getWrapper().find(Header)).toHaveLength(1); }); - test('renders Notifications', () => { - expect(getWrapper().find(Notifications)).toHaveLength(1); - }); - test('renders props.children', () => { const children = wrapper.find('#child-div'); expect(children).toHaveLength(1); diff --git a/app/pages/PageWrapper.spec.js b/app/pages/__tests__/PageWrapper.test.js similarity index 96% rename from app/pages/PageWrapper.spec.js rename to app/pages/__tests__/PageWrapper.test.js index 7b54f5900..ef2c041fd 100644 --- a/app/pages/PageWrapper.spec.js +++ b/app/pages/__tests__/PageWrapper.test.js @@ -2,7 +2,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import Grid from 'react-bootstrap/lib/Grid'; -import PageWrapper from './PageWrapper'; +import PageWrapper from '../PageWrapper'; describe('pages/PageWrapper', () => { const defaultProps = { diff --git a/app/pages/_pages.scss b/app/pages/_pages.scss index 7e8a44a50..7ac96e923 100644 --- a/app/pages/_pages.scss +++ b/app/pages/_pages.scss @@ -1,9 +1,7 @@ -@import './about/about-page'; @import './admin-resources/admin-resources-page'; -@import './home/home-page'; @import './reservation/reservation-page'; @import './resource/resource-page'; -@import './search/search-page'; +@import './search/controls/search-controls'; @import './user-reservations/user-reservations-page'; .app-PageWrapper { diff --git a/app/pages/about/AboutPage.js b/app/pages/about/AboutPage.js deleted file mode 100644 index 5a44edf33..000000000 --- a/app/pages/about/AboutPage.js +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import PageWrapper from 'pages/PageWrapper'; -import { injectT } from 'i18n'; -import AboutPageContent from './AboutPageContent'; - -function AboutPage({ t }) { - return ( - - - - ); -} - -AboutPage.propTypes = { - t: PropTypes.func.isRequired, -}; - -export default injectT(AboutPage); diff --git a/app/pages/about/AboutPage.spec.js b/app/pages/about/AboutPage.spec.js deleted file mode 100644 index 88c642ade..000000000 --- a/app/pages/about/AboutPage.spec.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -import PageWrapper from 'pages/PageWrapper'; -import { shallowWithIntl } from 'utils/testUtils'; -import AboutPage from './AboutPage'; -import AboutPageContent from './AboutPageContent'; - -describe('pages/about/AboutPage', () => { - function getWrapper() { - return shallowWithIntl(); - } - - test('renders PageWrapper with correct props', () => { - const pageWrapper = getWrapper().find(PageWrapper); - expect(pageWrapper).toHaveLength(1); - expect(pageWrapper.prop('className')).toBe('about-page'); - expect(pageWrapper.prop('title')).toBe('AboutPage.title'); - }); - - test('renders AboutPageContent component', () => { - expect(getWrapper().find(AboutPageContent).length).toBe(1); - }); -}); diff --git a/app/pages/about/AboutPageContent.js b/app/pages/about/AboutPageContent.js deleted file mode 100644 index 27dc89b75..000000000 --- a/app/pages/about/AboutPageContent.js +++ /dev/null @@ -1,77 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { FormattedHTMLMessage } from 'react-intl'; - -import FeedbackLink from 'shared/feedback-link'; -import { injectT } from 'i18n'; -import { getCurrentCustomization } from 'utils/customizationUtils'; -import AboutPartners from './AboutPartners'; - -const defaultTranslationKeys = { - header: 'AboutPageContent.defaultHeader', - lead: 'AboutPageContent.defaultLead', - reservable: 'AboutPageContent.defaultReservableParagraph', -}; - -const customizedTranslationKeys = { - ESPOO: { - header: 'AboutPageContent.espooHeader', - lead: 'AboutPageContent.espooLead', - reservable: 'AboutPageContent.espooReservableParagraph', - partners: 'AboutPageContent.espooPartnersHeader', - }, - VANTAA: { - header: 'AboutPageContent.vantaaHeader', - lead: 'AboutPageContent.vantaaLead', - reservable: 'AboutPageContent.vantaaReservableParagraph', - partners: 'AboutPageContent.vantaaPartnersHeader', - }, -}; - -function AboutPageContent({ t }) { - const customization = getCurrentCustomization(); - let translationKeys = defaultTranslationKeys; - if (customization) { - translationKeys = customizedTranslationKeys[customization]; - } - - return ( -
-

{t(translationKeys.header)}

-

{t(translationKeys.lead)}

-

{t('AboutPageContent.pilotParagraph')}

-

{t(translationKeys.reservable)}

-

-

{t('AboutPageContent.developmentParagraph')}

-

{t('AboutPageContent.goalParagraph')}

-

- {t('AboutPageContent.feedbackParagraph')} - {' '} - {t('AboutPageContent.feedbackLink')} -

- {translationKeys.partners && ( -
-

{t(translationKeys.partners)}

- -
- )} - -

{t('AboutPageContent.customerRegisterHeader')}

-

- {t('AboutPageContent.customerRegisterParagraph')} - {' '} - - {t('AboutPageContent.customerRegisterLink')} - -

-
-
- ); -} - -AboutPageContent.propTypes = { - t: PropTypes.func.isRequired, -}; - - -export default injectT(AboutPageContent); diff --git a/app/pages/about/AboutPageContent.spec.js b/app/pages/about/AboutPageContent.spec.js deleted file mode 100644 index 2b8f81449..000000000 --- a/app/pages/about/AboutPageContent.spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import simple from 'simple-mock'; - -import * as customizationUtils from 'utils/customizationUtils'; -import { shallowWithIntl } from 'utils/testUtils'; -import AboutPageContent from './AboutPageContent'; - -describe('Component: customization/AboutPageContent', () => { - function getWrapper() { - return shallowWithIntl(); - } - - describe('When there is no customization in use', () => { - let content; - - beforeAll(() => { - content = getWrapper(); - }); - - test('renders header for Helsinki', () => { - expect(content.find('h1').text()).toContain('AboutPageContent.defaultHeader'); - }); - }); - - describe('When Espoo customization is used', () => { - let content; - - beforeAll(() => { - simple.mock(customizationUtils, 'getCurrentCustomization').returnWith('ESPOO'); - content = getWrapper(); - }); - - afterAll(() => { - simple.restore(); - }); - - test('renders header for Espoo', () => { - expect(content.find('h1').text()).toContain('AboutPageContent.espooHeader'); - }); - }); - - describe('When Vantaa customization is used', () => { - let content; - - beforeAll(() => { - simple.mock(customizationUtils, 'getCurrentCustomization').returnWith('VANTAA'); - content = getWrapper(); - }); - - afterAll(() => { - simple.restore(); - }); - - test('renders header for Vantaa', () => { - expect(content.find('h1').text()).toContain('AboutPageContent.vantaaHeader'); - }); - }); -}); diff --git a/app/pages/about/index.js b/app/pages/about/index.js deleted file mode 100644 index 384a08504..000000000 --- a/app/pages/about/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import AboutPage from './AboutPage'; - -export default AboutPage; diff --git a/app/pages/admin-resources/AdminResourcesPage.js b/app/pages/admin-resources/AdminResourcesPage.js index 21fa05fdc..dbf6218ab 100644 --- a/app/pages/admin-resources/AdminResourcesPage.js +++ b/app/pages/admin-resources/AdminResourcesPage.js @@ -5,20 +5,20 @@ import Loader from 'react-loader'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { fetchFavoritedResources } from 'actions/resourceActions'; +import { fetchFavoritedResources } from '../../actions/resourceActions'; import { changeAdminResourcesPageDate, selectAdminResourceType, openConfirmReservationModal, unselectAdminResourceType, -} from 'actions/uiActions'; -import { injectT } from 'i18n'; -import PageWrapper from 'pages/PageWrapper'; -import AvailabilityView from 'shared/availability-view'; -import ResourceTypeFilter from 'shared/resource-type-filter'; -import ReservationSuccessModal from 'shared/modals/reservation-success'; -import ReservationConfirmationContainer from 'shared/reservation-confirmation'; -import recurringReservations from 'state/recurringReservations'; +} from '../../actions/uiActions'; +import injectT from '../../i18n/injectT'; +import PageWrapper from '../PageWrapper'; +import AvailabilityView from '../../shared/availability-view/AvailabilityView'; +import ResourceTypeFilter from '../../shared/resource-type-filter/ResourceTypeFilterContainer'; +import ReservationSuccessModal from '../../shared/modals/reservation-success/ReservationSuccessModalContainer'; +import ReservationConfirmationContainer from '../../shared/reservation-confirmation/ReservationConfirmationContainer'; +import recurringReservations from '../../state/recurringReservations'; import adminResourcesPageSelector from './adminResourcesPageSelector'; class UnconnectedAdminResourcesPage extends Component { diff --git a/app/pages/admin-resources/AdminResourcesPage.spec.js b/app/pages/admin-resources/__tests__/AdminResourcesPage.test.js similarity index 96% rename from app/pages/admin-resources/AdminResourcesPage.spec.js rename to app/pages/admin-resources/__tests__/AdminResourcesPage.test.js index 60d86f949..f2bdda088 100644 --- a/app/pages/admin-resources/AdminResourcesPage.spec.js +++ b/app/pages/admin-resources/__tests__/AdminResourcesPage.test.js @@ -2,11 +2,11 @@ import React from 'react'; import Loader from 'react-loader'; import simple from 'simple-mock'; -import PageWrapper from 'pages/PageWrapper'; -import AvailabilityView from 'shared/availability-view'; -import ResourceTypeFilter from 'shared/resource-type-filter'; -import { shallowWithIntl } from 'utils/testUtils'; -import { UnconnectedAdminResourcesPage as AdminResourcesPage } from './AdminResourcesPage'; +import PageWrapper from '../../PageWrapper'; +import AvailabilityView from '../../../shared/availability-view/AvailabilityView'; +import ResourceTypeFilter from '../../../shared/resource-type-filter/ResourceTypeFilterContainer'; +import { shallowWithIntl } from '../../../utils/testUtils'; +import { UnconnectedAdminResourcesPage as AdminResourcesPage } from '../AdminResourcesPage'; describe('pages/admin-resources/AdminResourcesPage', () => { const changeAdminResourcesPageDate = simple.stub(); diff --git a/app/pages/admin-resources/adminResourcesPageSelector.spec.js b/app/pages/admin-resources/__tests__/adminResourcesPageSelector.test.js similarity index 96% rename from app/pages/admin-resources/adminResourcesPageSelector.spec.js rename to app/pages/admin-resources/__tests__/adminResourcesPageSelector.test.js index d04db3a0d..59a3bb4ff 100644 --- a/app/pages/admin-resources/adminResourcesPageSelector.spec.js +++ b/app/pages/admin-resources/__tests__/adminResourcesPageSelector.test.js @@ -1,7 +1,7 @@ import moment from 'moment'; -import { getState } from 'utils/testUtils'; -import adminResourcesPageSelector from './adminResourcesPageSelector'; +import { getState } from '../../../utils/testUtils'; +import adminResourcesPageSelector from '../adminResourcesPageSelector'; describe('pages/admin-resources/adminResourcesPageSelector', () => { function getSelected(extraState) { diff --git a/app/pages/admin-resources/adminResourcesPageSelector.js b/app/pages/admin-resources/adminResourcesPageSelector.js index 722b2c667..e295ccc3f 100644 --- a/app/pages/admin-resources/adminResourcesPageSelector.js +++ b/app/pages/admin-resources/adminResourcesPageSelector.js @@ -1,14 +1,13 @@ -import ActionTypes from 'constants/ActionTypes'; - import includes from 'lodash/includes'; import sortBy from 'lodash/sortBy'; import uniq from 'lodash/uniq'; import moment from 'moment'; import { createSelector, createStructuredSelector } from 'reselect'; -import { isAdminSelector, isLoggedInSelector } from 'state/selectors/authSelectors'; -import { resourcesSelector } from 'state/selectors/dataSelectors'; -import requestIsActiveSelectorFactory from 'state/selectors/factories/requestIsActiveSelectorFactory'; +import ActionTypes from '../../constants/ActionTypes'; +import { isAdminSelector, isLoggedInSelector } from '../../state/selectors/authSelectors'; +import { resourcesSelector } from '../../state/selectors/dataSelectors'; +import requestIsActiveSelectorFactory from '../../state/selectors/factories/requestIsActiveSelectorFactory'; const dateSelector = state => state.ui.pages.adminResources.date || moment().format('YYYY-MM-DD'); const resourceIdsSelector = state => state.ui.pages.adminResources.resourceIds; diff --git a/app/pages/admin-resources/index.js b/app/pages/admin-resources/index.js deleted file mode 100644 index b189f03cd..000000000 --- a/app/pages/admin-resources/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import AdminResourcesPage from './AdminResourcesPage'; - -export default AdminResourcesPage; diff --git a/app/pages/browser-warning/BrowserWarning.js b/app/pages/browser-warning/BrowserWarning.js index 20609b135..5152eabb6 100644 --- a/app/pages/browser-warning/BrowserWarning.js +++ b/app/pages/browser-warning/BrowserWarning.js @@ -4,36 +4,48 @@ function BrowserWarning() { return (

+ Currently, Varaamo does not support Internet Explorer. We are investigating this issue and finding a solution. Meanwhile, use another browser (such as Chrome + , Firefox + or Edge + ).

+ Varaamo ei tue Internet Explorer selainta tällä hetkellä. Selvitämme ongelmaa sen ratkaisemiseksi. Sillä välin, käytä toista selainta (kuten Chrome + , Firefox + tai Edge + ).

+ Varaamo fungerar inte längre med Internet Explorer. Vi arbetar med att lösa problemet. Under tiden så var vänlig och använd någon annan browser (t.ex Chrome + , Firefox + eller Edge + ).

diff --git a/app/pages/browser-warning/BrowserWarning.spec.js b/app/pages/browser-warning/__tests__/BrowserWarning.test.js similarity index 83% rename from app/pages/browser-warning/BrowserWarning.spec.js rename to app/pages/browser-warning/__tests__/BrowserWarning.test.js index 25aabf17e..5297b076c 100644 --- a/app/pages/browser-warning/BrowserWarning.spec.js +++ b/app/pages/browser-warning/__tests__/BrowserWarning.test.js @@ -1,7 +1,7 @@ import React from 'react'; -import BrowserWarning from './BrowserWarning'; -import { shallowWithIntl } from 'utils/testUtils'; +import BrowserWarning from '../BrowserWarning'; +import { shallowWithIntl } from '../../../utils/testUtils'; describe('pages/browser-warning/BrowserWarning', () => { function getWrapper() { diff --git a/app/pages/browser-warning/index.js b/app/pages/browser-warning/index.js deleted file mode 100644 index 2cb4f92e4..000000000 --- a/app/pages/browser-warning/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import BrowserWarning from './BrowserWarning'; - -export default BrowserWarning; diff --git a/app/pages/home/HomePage.js b/app/pages/home/HomePage.js deleted file mode 100644 index 24f1469c3..000000000 --- a/app/pages/home/HomePage.js +++ /dev/null @@ -1,122 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Button, Col, Row } from 'react-bootstrap'; -import Loader from 'react-loader'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import camelCase from 'lodash/camelCase'; -import Link from 'react-router-dom/Link'; -import { faHotTub as iconSauna, faCalendarAlt as iconOrganizeEvents } from '@fortawesome/free-solid-svg-icons'; - -// TODO: VAR-80 | VAR-81 Replace those icon with designed icon. -import { fetchPurposes } from 'actions/purposeActions'; -import { injectT } from 'i18n'; -import PageWrapper from 'pages/PageWrapper'; -import HomeSearchBox from './HomeSearchBox'; -import homePageSelector from './homePageSelector'; -import iconManufacturing from './images/frontpage_build.svg'; -import iconPhotoAndAudio from './images/frontpage_music.svg'; -import iconSports from './images/frontpage_sport.svg'; -import iconGuidance from './images/frontpage_guidance.svg'; -import iconMeetingsAndWorking from './images/frontpage_work.svg'; -import FAIcon from 'shared/fontawesome-icon'; - -const purposeIcons = { - photoAndAudio: iconPhotoAndAudio, - sports: iconSports, - guidance: iconGuidance, - manufacturing: iconManufacturing, - meetingsAndWorking: iconMeetingsAndWorking, - events: iconOrganizeEvents, - sauna: iconSauna -}; - -class UnconnectedHomePage extends Component { - constructor(props) { - super(props); - this.handleSearch = this.handleSearch.bind(this); - this.renderPurposeBanner = this.renderPurposeBanner.bind(this); - } - - componentDidMount() { - this.props.actions.fetchPurposes(); - } - - handleSearch(value = '') { - this.props.history.push(`/search?search=${value}`); - } - - renderPurposeBanner(purpose) { - const { t } = this.props; - const image = purposeIcons[camelCase(purpose.value)]; - - return ( - - -
- {typeof image === 'string' ? {purpose.label} - // TODO: VAR-80 | VAR-81 Replace those icon with designed icon. - - : } -
- -
{purpose.label}
-
- -
- - - ); - } - - render() { - const { isFetchingPurposes, purposes, t } = this.props; - return ( -
-
-

Varaamo –

-

{t('HomePage.contentTitle')}

-
{t('HomePage.contentSubTitle')}
- -
-
- -

{t('HomePage.bannersTitle')}

- -
- - {purposes.map(this.renderPurposeBanner)} - -
-
-
-
- ); - } -} - -UnconnectedHomePage.propTypes = { - actions: PropTypes.object.isRequired, - isFetchingPurposes: PropTypes.bool.isRequired, - purposes: PropTypes.array.isRequired, - history: PropTypes.object.isRequired, - t: PropTypes.func.isRequired, -}; - -UnconnectedHomePage = injectT(UnconnectedHomePage); // eslint-disable-line - -function mapDispatchToProps(dispatch) { - const actionCreators = { fetchPurposes }; - return { actions: bindActionCreators(actionCreators, dispatch) }; -} - -export { UnconnectedHomePage }; -export default connect( - homePageSelector, - mapDispatchToProps -)(UnconnectedHomePage); diff --git a/app/pages/home/HomePage.spec.js b/app/pages/home/HomePage.spec.js deleted file mode 100644 index ee88667e3..000000000 --- a/app/pages/home/HomePage.spec.js +++ /dev/null @@ -1,133 +0,0 @@ -import React from 'react'; -import Loader from 'react-loader'; -import simple from 'simple-mock'; -import Link from 'react-router-dom/Link'; - -import PageWrapper from 'pages/PageWrapper'; -import { shallowWithIntl } from 'utils/testUtils'; -import { UnconnectedHomePage as HomePage } from './HomePage'; -import HomeSearchBox from './HomeSearchBox'; - -describe('pages/home/HomePage', () => { - const history = { - push: () => {}, - }; - - const defaultProps = { - history, - actions: { - fetchPurposes: simple.stub(), - }, - isFetchingPurposes: false, - purposes: [ - { - label: 'Purpose 1', - value: 'purpose-1', - }, - { - label: 'Purpose 2', - value: 'purpose-2', - }, - { - label: 'Purpose 3', - value: 'purpose-3', - }, - { - label: 'Purpose 4', - value: 'purpose-4', - }, - ], - }; - - function getWrapper(extraProps) { - return shallowWithIntl(); - } - - describe('render', () => { - test('renders PageWrapper with correct props', () => { - const pageWrapper = getWrapper().find(PageWrapper); - expect(pageWrapper).toHaveLength(1); - expect(pageWrapper.prop('className')).toBe('app-HomePageContent'); - expect(pageWrapper.prop('title')).toBe('HomePage.title'); - }); - - test('renders HomeSearchBox with correct props', () => { - const wrapper = getWrapper(); - const instance = wrapper.instance(); - const homeSearchBox = wrapper.find(HomeSearchBox); - expect(homeSearchBox).toHaveLength(1); - expect(homeSearchBox.prop('onSearch')).toBe(instance.handleSearch); - }); - - describe('Loader', () => { - test('renders Loader with correct props when not fetching purposes', () => { - const loader = getWrapper().find(Loader); - expect(loader.length).toBe(1); - expect(loader.at(0).prop('loaded')).toBe(true); - }); - - test('renders Loader with correct props when fetching purposes', () => { - const loader = getWrapper({ isFetchingPurposes: true }).find(Loader); - expect(loader.length).toBe(1); - expect(loader.at(0).prop('loaded')).toBe(false); - }); - - test('renders purpose banners', () => { - const banners = getWrapper().find('.app-HomePageContent__banner'); - expect(banners.length).toBe(defaultProps.purposes.length); - }); - }); - - describe('Purpose banners', () => { - let wrapper; - - beforeAll(() => { - wrapper = getWrapper(); - }); - - afterAll(() => { - simple.restore(); - }); - - test(' have at least a Link component', () => { - expect(wrapper.find(Link)).toHaveLength(defaultProps.purposes.length); - expect(wrapper.find(Link).first().prop('to')).toContain(defaultProps.purposes[0].value); - }); - }); - }); - - describe('componentDidMount', () => { - function callComponentDidMount(props, extraActions) { - const actions = { ...defaultProps.actions, ...extraActions }; - const instance = getWrapper({ ...props, actions }).instance(); - instance.componentDidMount(); - } - - test('fetches purposes', () => { - const fetchPurposes = simple.mock(); - callComponentDidMount({}, { fetchPurposes }); - expect(fetchPurposes.callCount).toBe(1); - }); - }); - - describe('handleSearch', () => { - const value = 'some value'; - const expectedPath = `/search?search=${value}`; - let historyMock; - - beforeAll(() => { - const instance = getWrapper().instance(); - historyMock = simple.mock(history, 'push'); - instance.handleSearch(value); - }); - - afterAll(() => { - simple.restore(); - }); - - test('calls browserHistory push with correct path', () => { - expect(historyMock.callCount).toBe(1); - expect(historyMock.lastCall.args).toEqual([expectedPath]); - }); - }); -}); diff --git a/app/pages/home/homePageSelector.js b/app/pages/home/homePageSelector.js deleted file mode 100644 index 4b9ce1b1c..000000000 --- a/app/pages/home/homePageSelector.js +++ /dev/null @@ -1,28 +0,0 @@ -import ActionTypes from 'constants/ActionTypes'; - -import sortBy from 'lodash/sortBy'; -import values from 'lodash/values'; -import { createSelector, createStructuredSelector } from 'reselect'; - -import { purposesSelector } from 'state/selectors/dataSelectors'; -import requestIsActiveSelectorFactory from 'state/selectors/factories/requestIsActiveSelectorFactory'; - -const purposeOptionsSelector = createSelector( - purposesSelector, - (purposes) => { - const purposeOptions = values(purposes) - .filter(purpose => purpose.parent === null) - .map(purpose => ({ - value: purpose.id, - label: purpose.name, - })); - return sortBy(purposeOptions, 'label'); - } -); - -const homePageSelector = createStructuredSelector({ - isFetchingPurposes: requestIsActiveSelectorFactory(ActionTypes.API.PURPOSES_GET_REQUEST), - purposes: purposeOptionsSelector, -}); - -export default homePageSelector; diff --git a/app/pages/home/homePageSelector.spec.js b/app/pages/home/homePageSelector.spec.js deleted file mode 100644 index f65287109..000000000 --- a/app/pages/home/homePageSelector.spec.js +++ /dev/null @@ -1,64 +0,0 @@ -import keyBy from 'lodash/keyBy'; - -import Purpose from 'utils/fixtures/Purpose'; -import { getDefaultRouterProps, getState } from 'utils/testUtils'; -import homePageSelector from './homePageSelector'; - -describe('pages/home/homePageSelector', () => { - function getSelected(extraState) { - const state = getState(extraState); - const props = getDefaultRouterProps(); - return homePageSelector(state, props); - } - - test('returns isFetchingPurposes', () => { - expect(getSelected().isFetchingPurposes).toBeDefined(); - }); - - describe('purposes', () => { - function getPurposes(purposes) { - return getSelected({ - 'data.purposes': keyBy(purposes, 'id'), - }).purposes; - } - - test('returns an empty array if state contains no purposes', () => { - expect(getPurposes([])).toEqual([]); - }); - - test('returns an option object for each purpose without a parent', () => { - const purposes = [ - Purpose.build({ parent: null }), - Purpose.build({ parent: null }), - ]; - expect(getPurposes(purposes)).toHaveLength(purposes.length); - }); - - test('Does not return an option object for purposes with a parent', () => { - const purposes = [ - Purpose.build({ parent: 'some parent' }), - ]; - expect(getPurposes(purposes)).toHaveLength(0); - }); - - describe('a returned option object', () => { - const purpose = Purpose.build({ parent: null }); - function getPurpose() { - return getPurposes([purpose])[0]; - } - - test('has purpose.id as its value property', () => { - expect(getPurpose().value).toBe(purpose.id); - }); - - test('has purpose.name as its label property', () => { - expect(getPurpose().label).toBe(purpose.name); - }); - - test('does not contain other properties than value and label', () => { - const expected = { value: purpose.id, label: purpose.name }; - expect(getPurpose()).toEqual(expected); - }); - }); - }); -}); diff --git a/app/pages/home/index.js b/app/pages/home/index.js deleted file mode 100644 index d91fb083d..000000000 --- a/app/pages/home/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import HomePage from './HomePage'; - -export default HomePage; diff --git a/app/pages/not-found/NotFoundPage.js b/app/pages/not-found/NotFoundPage.js index 41a534f43..c734e956a 100644 --- a/app/pages/not-found/NotFoundPage.js +++ b/app/pages/not-found/NotFoundPage.js @@ -4,9 +4,9 @@ import Well from 'react-bootstrap/lib/Well'; import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; -import PageWrapper from 'pages/PageWrapper'; -import { injectT } from 'i18n'; -import { getSearchPageUrl } from 'utils/searchUtils'; +import PageWrapper from '../PageWrapper'; +import injectT from '../../i18n/injectT'; +import { getSearchPageUrl } from '../../utils/searchUtils'; function NotFoundPage({ t }) { return ( diff --git a/app/pages/not-found/NotFoundPage.spec.js b/app/pages/not-found/__tests__/NotFoundPage.test.js similarity index 83% rename from app/pages/not-found/NotFoundPage.spec.js rename to app/pages/not-found/__tests__/NotFoundPage.test.js index 8b916765b..10b911e57 100644 --- a/app/pages/not-found/NotFoundPage.spec.js +++ b/app/pages/not-found/__tests__/NotFoundPage.test.js @@ -1,8 +1,8 @@ import React from 'react'; -import PageWrapper from 'pages/PageWrapper'; -import { shallowWithIntl } from 'utils/testUtils'; -import NotFoundPage from './NotFoundPage'; +import PageWrapper from '../../PageWrapper'; +import { shallowWithIntl } from '../../../utils/testUtils'; +import NotFoundPage from '../NotFoundPage'; describe('pages/not-found/NotFoundPage', () => { function getWrapper() { diff --git a/app/pages/not-found/index.js b/app/pages/not-found/index.js deleted file mode 100644 index a4684e21b..000000000 --- a/app/pages/not-found/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import NotFoundPage from './NotFoundPage'; - -export default NotFoundPage; diff --git a/app/pages/reservation/PaymentFailed.js b/app/pages/reservation/PaymentFailed.js new file mode 100644 index 000000000..2fa330c6a --- /dev/null +++ b/app/pages/reservation/PaymentFailed.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import { Button } from 'react-bootstrap'; + +import injectT from '../../i18n/injectT'; + +function PaymentFailed({ + t, + resourceId +}) { + return ( +
+

{t('payment.title')}

+

{t('payment.text')}

+ {resourceId && ( + + + + )} + + + +
+ ); +} + +PaymentFailed.propTypes = { + t: PropTypes.func, + resourceId: PropTypes.string, +}; + +export default injectT(PaymentFailed); diff --git a/app/pages/reservation/PaymentSuccess.js b/app/pages/reservation/PaymentSuccess.js new file mode 100644 index 000000000..dccbb9333 --- /dev/null +++ b/app/pages/reservation/PaymentSuccess.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { injectIntl } from 'react-intl'; +import flow from 'lodash/flow'; +import get from 'lodash/get'; +import isPlainObject from 'lodash/isPlainObject'; + +import { currentUserSelector } from '../../state/selectors/authSelectors'; +import ReservationConfirmation from './reservation-confirmation/ReservationConfirmation'; + +const translateEntity = (entity, locale) => ( + Object + .entries(entity) + .reduce((acc, [key, value]) => { + const localizedValue = get(value, locale, value); + acc[key] = isPlainObject(localizedValue) ? null : localizedValue; + return acc; + }, {}) +); + +function PaymentSuccess({ + reservation, + resource, + user, + intl: { locale }, +}) { + const translatedReservation = translateEntity(reservation, locale); + const translatedResource = translateEntity(resource, locale); + return ( +
+ +
+ ); +} + +PaymentSuccess.propTypes = { + reservation: PropTypes.object, + resource: PropTypes.object, + user: PropTypes.object, + intl: PropTypes.object, +}; + +export default flow( + connect( + state => ({ + user: currentUserSelector(state) + }) + ), + injectIntl, +)(PaymentSuccess); diff --git a/app/pages/reservation/ReservationPage.js b/app/pages/reservation/ReservationPage.js index 446b25218..b3159638b 100644 --- a/app/pages/reservation/ReservationPage.js +++ b/app/pages/reservation/ReservationPage.js @@ -1,6 +1,8 @@ import first from 'lodash/first'; import isEmpty from 'lodash/isEmpty'; import last from 'lodash/last'; +import get from 'lodash/get'; +import has from 'lodash/has'; import moment from 'moment'; import React, { Component } from 'react'; import Loader from 'react-loader'; @@ -9,20 +11,25 @@ import { bindActionCreators } from 'redux'; import PropTypes from 'prop-types'; import queryString from 'query-string'; -import { postReservation, putReservation } from 'actions/reservationActions'; -import { fetchResource } from 'actions/resourceActions'; +import { postReservation, putReservation } from '../../actions/reservationActions'; +import { fetchResource } from '../../actions/resourceActions'; import { clearReservations, closeReservationSuccessModal, openResourceTermsModal, -} from 'actions/uiActions'; -import PageWrapper from 'pages/PageWrapper'; -import { injectT } from 'i18n'; + setSelectedTimeSlots, +} from '../../actions/uiActions'; +import PageWrapper from '../PageWrapper'; +import injectT from '../../i18n/injectT'; import ReservationConfirmation from './reservation-confirmation/ReservationConfirmation'; import ReservationInformation from './reservation-information/ReservationInformation'; import ReservationPhases from './reservation-phases/ReservationPhases'; import ReservationTime from './reservation-time/ReservationTime'; import reservationPageSelector from './reservationPageSelector'; +import { hasProducts } from '../../utils/resourceUtils'; +import RecurringReservationControls from '../../shared/recurring-reservation-controls/RecurringReservationControls'; +import CompactReservationList from '../../shared/compact-reservation-list/CompactReservationList'; +import recurringReservationsConnector from '../../state/recurringReservations'; class UnconnectedReservationPage extends Component { constructor(props) { @@ -69,11 +76,14 @@ class UnconnectedReservationPage extends Component { (!isEmpty(nextCreated) || !isEmpty(nextEdited)) && (nextCreated !== reservationCreated || nextEdited !== reservationEdited) ) { - // TODO: fix this lint + // Reservation created for resource with product/order: proceed to payment! + if (has(nextCreated, 'order.paymentUrl')) { + const paymentUrl = get(nextCreated, 'order.paymentUrl'); + window.location = paymentUrl; + return; + } // eslint-disable-next-line react/no-will-update-set-state - this.setState({ - view: 'confirmation', - }); + this.setState({ view: 'confirmation' }); window.scrollTo(0, 0); } } @@ -104,10 +114,17 @@ class UnconnectedReservationPage extends Component { window.scrollTo(0, 0); }; + createPaymentReturnUrl = () => { + const { protocol, hostname } = window.location; + const port = window.location.port ? `:${window.location.port}` : ''; + return `${protocol}//${hostname}${port}/reservation-payment-return`; + }; + handleReservation = (values = {}) => { const { - actions, reservationToEdit, resource, selected + actions, reservationToEdit, resource, selected, recurringReservations = [] } = this.props; + if (!isEmpty(selected)) { const { begin } = first(selected); const { end } = last(selected); @@ -121,31 +138,88 @@ class UnconnectedReservationPage extends Component { end, }); } else { - actions.postReservation({ + const allReservations = [...recurringReservations, { begin, end }]; + + const isOrder = hasProducts(resource); + const order = isOrder + ? { + order: { + order_lines: [{ + product: get(resource, 'products[0].id'), + }], + return_url: this.createPaymentReturnUrl(), + } + } : {}; + + if (isOrder) { + this.setState({ view: 'payment' }); + } + allReservations.forEach(reservation => actions.postReservation({ ...values, - begin, - end, + ...order, + begin: reservation.begin, + end: reservation.end, resource: resource.id, - }); + })); } } }; fetchResource() { - const { actions, date, resource } = this.props; + const { + actions, date, resource, location + } = this.props; + + const start = moment(date) + .subtract(2, 'M') + .startOf('month') + .format(); + const end = moment(date) + .add(2, 'M') + .endOf('month') + .format(); + + const params = queryString.parse(location.search); + if (!isEmpty(resource)) { - const start = moment(date) - .subtract(2, 'M') - .startOf('month') - .format(); - const end = moment(date) - .add(2, 'M') - .endOf('month') - .format(); actions.fetchResource(resource.id, { start, end }); + } else if (params.resource) { + actions.fetchResource(params.resource, { start, end }); + // Fetch resource by id if there are resource id exist in query but not in redux. + // TODO: Always invoke actually API call for fetching resource by ID, will fix later. } } + renderRecurringReservations = () => { + const { + resource, + actions, + recurringReservations, + selectedReservations, + t, + } = this.props; + + const reservationsCount = selectedReservations.length + recurringReservations.length; + const introText = resource.needManualConfirmation + ? t('ConfirmReservationModal.preliminaryReservationText', { reservationsCount }) + : t('ConfirmReservationModal.regularReservationText', { reservationsCount }); + + return ( + <> + {/* Recurring selection dropdown */} + + {

{introText}

} + + {/* Selected recurring info */} + + + ); + } + render() { const { actions, @@ -153,8 +227,6 @@ class UnconnectedReservationPage extends Component { isStaff, isFetchingResource, isMakingReservations, - location, - match, reservationCreated, reservationEdited, reservationToEdit, @@ -164,6 +236,7 @@ class UnconnectedReservationPage extends Component { unit, user, history, + failedReservations, } = this.props; const { view } = this.state; @@ -185,48 +258,60 @@ class UnconnectedReservationPage extends Component { const title = t( `ReservationPage.${isEditing || isEdited ? 'editReservationTitle' : 'newReservationTitle'}` ); - const params = queryString.parse(location.search); return (
-

{title}

+

+ {title} +

- + {view === 'time' && isEditing && ( )} {view === 'information' && selectedTime && ( - + <> + {isAdmin && this.renderRecurringReservations()} + + + )} + {view === 'payment' && ( +
+

{t('ReservationPage.paymentText')}

+
)} {view === 'confirmation' && (reservationCreated || reservationEdited) && ( apiClient.get(`reservation/${id}`).then(({ data }) => camelizeKeysDeep(data)); +const loadResource = id => apiClient.get(`resource/${id}`).then(({ data }) => camelizeKeysDeep(data)); + +class ReservationPaymentReturnPage extends Component { + state = { + isLoading: true, + reservation: null, + resource: null, + }; + + componentDidMount() { + this.loadData(); + } + + loadData = () => { + const reservationId = this.getQueryParam('reservation_id'); + const reservationPromise = loadReservation(reservationId); + const resourcePromise = reservationPromise.then(r => loadResource(r.resource)); + Promise.all([reservationPromise, resourcePromise]) + .then(([reservation, resource]) => { + this.setState({ + reservation, + resource, + isLoading: false, + }); + }) + .catch(() => this.setState({ isLoading: false })); + }; + + getQueryParam = (paramName) => { + const { location } = this.props; + const params = queryString.parse(location.search); + const param = get(params, paramName); + return param; + }; + + render() { + const { t } = this.props; + const { + reservation, + resource, + isLoading, + } = this.state; + const status = this.getQueryParam('payment_status'); + const title = t('ReservationPage.newReservationTitle'); + const steps = stepIds.map(msgId => t(msgId)); + const completedSteps = status === 'success' ? steps : [t('ReservationPhase.informationTitle')]; + return ( + +
+
+

+ {title} +

+ + + {status === 'success' && ( + + )} + {status === 'failure' && } + +
+
+
+ ); + } +} + +ReservationPaymentReturnPage.propTypes = { + t: PropTypes.func, + location: PropTypes.object, +}; + +export default injectT(ReservationPaymentReturnPage); diff --git a/app/pages/reservation/ReservationPage.spec.js b/app/pages/reservation/__tests__/ReservationPage.test.js similarity index 87% rename from app/pages/reservation/ReservationPage.spec.js rename to app/pages/reservation/__tests__/ReservationPage.test.js index 840e0438d..491196f8b 100644 --- a/app/pages/reservation/ReservationPage.spec.js +++ b/app/pages/reservation/__tests__/ReservationPage.test.js @@ -3,17 +3,19 @@ import Loader from 'react-loader'; import Immutable from 'seamless-immutable'; import simple from 'simple-mock'; -import PageWrapper from 'pages/PageWrapper'; -import { shallowWithIntl } from 'utils/testUtils'; -import Reservation from 'utils/fixtures/Reservation'; -import Resource from 'utils/fixtures/Resource'; -import Unit from 'utils/fixtures/Unit'; -import User from 'utils/fixtures/User'; -import ReservationConfirmation from './reservation-confirmation/ReservationConfirmation'; -import ReservationInformation from './reservation-information/ReservationInformation'; -import ReservationPhases from './reservation-phases/ReservationPhases'; -import ReservationTime from './reservation-time/ReservationTime'; -import { UnconnectedReservationPage as ReservationPage } from './ReservationPage'; +import PageWrapper from '../../PageWrapper'; +import { shallowWithIntl } from '../../../utils/testUtils'; +import Reservation from '../../../utils/fixtures/Reservation'; +import Resource from '../../../utils/fixtures/Resource'; +import Unit from '../../../utils/fixtures/Unit'; +import User from '../../../utils/fixtures/User'; +import ReservationConfirmation from '../reservation-confirmation/ReservationConfirmation'; +import ReservationInformation from '../reservation-information/ReservationInformation'; +import ReservationPhases from '../reservation-phases/ReservationPhases'; +import ReservationTime from '../reservation-time/ReservationTime'; +import { UnconnectedReservationPage as ReservationPage } from '../ReservationPage'; +import RecurringReservationControls from '../../../shared/recurring-reservation-controls/RecurringReservationControls'; +import CompactReservationList from '../../../shared/compact-reservation-list/CompactReservationList'; describe('pages/reservation/ReservationPage', () => { const resource = Immutable(Resource.build()); @@ -31,6 +33,7 @@ describe('pages/reservation/ReservationPage', () => { postReservation: simple.mock(), }, date: '2016-10-10', + failedReservations: [], isAdmin: false, isStaff: false, isFetchingResource: false, @@ -40,6 +43,8 @@ describe('pages/reservation/ReservationPage', () => { reservationToEdit: null, reservationCreated: null, reservationEdited: null, + recurringReservations: [], + selectedReservations: [], resource, selected: [ { @@ -149,6 +154,28 @@ describe('pages/reservation/ReservationPage', () => { }); }); + describe('RecurringReservation', () => { + test('render RecurringReservationControls component if user is admin', () => { + const controls = getWrapper({ isAdmin: true }).find(RecurringReservationControls); + expect(controls).toHaveLength(1); + }); + + test('render CompactReservationList if user is admin', () => { + const list = getWrapper({ isAdmin: true }).find(CompactReservationList); + expect(list).toHaveLength(1); + }); + + test('does not render RecurringReservationControls component if user is not admin', () => { + const controls = getWrapper({ isAdmin: false }).find(RecurringReservationControls); + expect(controls).toHaveLength(0); + }); + + test('does not render CompactReservationList if user is not admin', () => { + const list = getWrapper({ isAdmin: false }).find(CompactReservationList); + expect(list).toHaveLength(0); + }); + }); + describe('ReservationInformation', () => { test( 'renders ReservationInformation when view is information and selected not empty', diff --git a/app/pages/reservation/reservationPageSelector.spec.js b/app/pages/reservation/__tests__/reservationPageSelector.test.js similarity index 91% rename from app/pages/reservation/reservationPageSelector.spec.js rename to app/pages/reservation/__tests__/reservationPageSelector.test.js index c0cd33f34..90b7b7a5f 100644 --- a/app/pages/reservation/reservationPageSelector.spec.js +++ b/app/pages/reservation/__tests__/reservationPageSelector.test.js @@ -1,11 +1,11 @@ import keyBy from 'lodash/keyBy'; import Immutable from 'seamless-immutable'; -import Reservation from 'utils/fixtures/Reservation'; -import Resource from 'utils/fixtures/Resource'; -import Unit from 'utils/fixtures/Unit'; -import User from 'utils/fixtures/User'; -import reservationPageSelector from './reservationPageSelector'; +import Reservation from '../../../utils/fixtures/Reservation'; +import Resource from '../../../utils/fixtures/Resource'; +import Unit from '../../../utils/fixtures/Unit'; +import User from '../../../utils/fixtures/User'; +import reservationPageSelector from '../reservationPageSelector'; const defaultUnit = Unit.build(); const defaultReservation = Reservation.build(); @@ -40,6 +40,9 @@ function getState(resources = [], units = [], user = defaultUser) { toShowEdited: [defaultReservation], }, }), + recurringReservations: { + reservations: [] + } }; } diff --git a/app/pages/reservation/_reservation-page.scss b/app/pages/reservation/_reservation-page.scss index 5372872fa..413f43fd2 100644 --- a/app/pages/reservation/_reservation-page.scss +++ b/app/pages/reservation/_reservation-page.scss @@ -3,9 +3,67 @@ @import './reservation-phases/reservation-phases'; @import './reservation-time/reservation-time'; -.app-ReservationPage { - .btn.btn-warning { - background-color: $yellow; - color: $black; +.app-ReservationPage__title { + font-weight: $font-weight-regular; + font-size: 22px; + &--big { + font-size: 28px; } } + +.app-ReservationPage__formfield { + margin-bottom: 20px; + label { + display: block; + font-size: 14px; + font-weight: $font-weight-regular; + } + input, + textarea { + border: 1px solid $medium-gray; + display: block; + font-weight: normal; + padding: 10px; + width: 100%; + &:focus { + border: 2px solid $blue; + padding: 9px; + } + } +} + +.app-ReservationPage__error { + color: $red; + display: block; + font-size: 14px; +} + +.app-ReservationFailedPage { + &__contentbox { + background: $light-gray; + margin: 0 auto; + padding: 10px 30px 30px; + text-align: center; + + @media (min-width: $screen-sm-min) { + max-width: 700px; + } + } + + &__link { + display: inline-block; + margin-top: 20px; + margin-right: 10px; + + &--last { + margin-right: 0; + } + } + + &__progress-steps { + margin: 0 auto 30px; + @media (min-width: $screen-sm-min) { + max-width: 700px; + } + } +} \ No newline at end of file diff --git a/app/pages/reservation/index.js b/app/pages/reservation/index.js deleted file mode 100644 index 59bcc28b9..000000000 --- a/app/pages/reservation/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import ReservationPage from './ReservationPage'; - -export default ReservationPage; diff --git a/app/pages/reservation/reservation-confirmation/ReservationConfirmation.js b/app/pages/reservation/reservation-confirmation/ReservationConfirmation.js index 702b5a1e7..37d59b63a 100644 --- a/app/pages/reservation/reservation-confirmation/ReservationConfirmation.js +++ b/app/pages/reservation/reservation-confirmation/ReservationConfirmation.js @@ -1,42 +1,50 @@ -import constants from 'constants/AppConstants'; - import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { FormattedHTMLMessage } from 'react-intl'; import Button from 'react-bootstrap/lib/Button'; import Col from 'react-bootstrap/lib/Col'; import Row from 'react-bootstrap/lib/Row'; -import Well from 'react-bootstrap/lib/Well'; import iconHome from 'hel-icons/dist/shapes/home.svg'; +import { Link } from 'react-router-dom'; -import { injectT } from 'i18n'; -import ReservationDate from 'shared/reservation-date'; +import constants from '../../../constants/AppConstants'; +import injectT from '../../../i18n/injectT'; +import ReservationDate from '../../../shared/reservation-date/ReservationDate'; +import { hasProducts } from '../../../utils/resourceUtils'; +import { getReservationPrice, getReservationPricePerPeriod } from '../../../utils/reservationUtils'; +import apiClient from '../../../../src/common/api/client'; +import CompactReservationList from '../../../shared/compact-reservation-list/CompactReservationList'; class ReservationConfirmation extends Component { static propTypes = { + failedReservations: PropTypes.array.isRequired, isEdited: PropTypes.bool, reservation: PropTypes.object.isRequired, resource: PropTypes.object.isRequired, t: PropTypes.func.isRequired, user: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, }; - handleReservationsButton() { - this.props.history.replace('/my-reservations'); + state = { + reservationPrice: null, + } + + componentDidMount() { + const { reservation, resource } = this.props; + if (hasProducts(resource)) { + getReservationPrice(apiClient, reservation.begin, reservation.end, resource.products) + .then(reservationPrice => this.setState({ reservationPrice })); + } } renderField(field, label, value) { return ( - - - {label} + + + {label} - - {value} + + {value} ); @@ -44,8 +52,9 @@ class ReservationConfirmation extends Component { render() { const { - isEdited, reservation, resource, t, user + failedReservations, isEdited, reservation, resource, t, user } = this.props; + const { reservationPrice } = this.state; const refUrl = window.location.href; const href = `${constants.FEEDBACK_URL}?ref=${refUrl}`; let email = ''; @@ -60,44 +69,76 @@ class ReservationConfirmation extends Component { return ( - -

+
+

{t(`ReservationConfirmation.reservation${isEdited ? 'Edited' : 'Created'}Title`)}

- -

- {resource.name} + - {resource.name} -

- {!isEdited && ( -

- + {resource.name} + {resource.name}

+
+ {!isEdited && ( +

+ +

)}

+ + {Array.isArray(failedReservations) && Boolean(failedReservations.length) + && ( +
+
+ {t('ReservationSuccessModal.failedReservationsHeader')} +
+ +
+ ) + } +

- + + +

- +

- -

{t('ReservationConfirmation.reservationDetailsTitle')}

+
+

{t('ReservationConfirmation.reservationDetailsTitle')}

+ {reservationPrice + && this.renderField( + 'pricePerPeriod', + t('common.priceLabel'), + getReservationPricePerPeriod(resource) + )} + {reservationPrice + && this.renderField( + 'reservationPrice', + t('common.totalPriceLabel'), + `${reservationPrice}€` + )} {reservation.reserverName && this.renderField( 'reserverName', @@ -159,6 +200,30 @@ class ReservationConfirmation extends Component { {reservation.billingAddressStreet && ( {t('common.billingAddressLabel')} )} + {reservation.billingFirstName + && this.renderField( + 'billingFirstName', + t('common.billingFirstNameLabel'), + reservation.billingFirstName + )} + {reservation.billingLastName + && this.renderField( + 'billingLastName', + t('common.billingLastNameLabel'), + reservation.billingLastName + )} + {reservation.billingPhoneNumber + && this.renderField( + 'billingPhoneNumber', + t('common.billingPhoneNumberLabel'), + reservation.billingPhoneNumber + )} + {reservation.billingEmailAddress + && this.renderField( + 'billingEmailAddress', + t('common.billingEmailAddressLabel'), + reservation.billingEmailAddress + )} {reservation.billingAddressStreet && this.renderField( 'billingAddressStreet', @@ -177,7 +242,7 @@ class ReservationConfirmation extends Component { t('common.addressCityLabel'), reservation.billingAddressCity )} - +
); diff --git a/app/pages/reservation/reservation-confirmation/ReservationConfirmation.spec.js b/app/pages/reservation/reservation-confirmation/__tests__/ReservationConfirmation.test.js similarity index 70% rename from app/pages/reservation/reservation-confirmation/ReservationConfirmation.spec.js rename to app/pages/reservation/reservation-confirmation/__tests__/ReservationConfirmation.test.js index 53356537d..d02fc759e 100644 --- a/app/pages/reservation/reservation-confirmation/ReservationConfirmation.spec.js +++ b/app/pages/reservation/reservation-confirmation/__tests__/ReservationConfirmation.test.js @@ -1,24 +1,19 @@ import React from 'react'; import { FormattedHTMLMessage } from 'react-intl'; import Immutable from 'seamless-immutable'; -import simple from 'simple-mock'; import Button from 'react-bootstrap/lib/Button'; import Row from 'react-bootstrap/lib/Row'; -import ReservationDate from 'shared/reservation-date'; -import Reservation from 'utils/fixtures/Reservation'; -import Resource from 'utils/fixtures/Resource'; -import User from 'utils/fixtures/User'; -import { shallowWithIntl } from 'utils/testUtils'; -import ReservationConfirmation from './ReservationConfirmation'; +import ReservationDate from '../../../../shared/reservation-date/ReservationDate'; +import Reservation from '../../../../utils/fixtures/Reservation'; +import Resource from '../../../../utils/fixtures/Resource'; +import User from '../../../../utils/fixtures/User'; +import { shallowWithIntl } from '../../../../utils/testUtils'; +import ReservationConfirmation from '../ReservationConfirmation'; describe('pages/reservation/reservation-confirmation/ReservationConfirmation', () => { - const history = { - replace: () => {}, - }; - const defaultProps = { - history, + failedReservations: [], isEdited: false, reservation: Immutable(Reservation.build({ user: User.build() })), resource: Immutable(Resource.build()), @@ -95,10 +90,9 @@ describe('pages/reservation/reservation-confirmation/ReservationConfirmation', ( expect(email.prop('values')).toEqual({ email: user.email }); }); - test('renders Button with correct props', () => { + test('renders Button', () => { const button = getWrapper().find(Button); expect(button).toHaveLength(1); - expect(typeof button.prop('onClick')).toBe('function'); }); test('renders reserverName', () => { @@ -122,52 +116,4 @@ describe('pages/reservation/reservation-confirmation/ReservationConfirmation', ( const fields = getWrapper({ reservation }).find('.app-ReservationConfirmation__field'); expect(fields).toHaveLength(14); }); - - describe('Button onClick', () => { - let button; - let instance; - - beforeAll(() => { - const wrapper = getWrapper(); - button = wrapper.find(Button); - instance = wrapper.instance(); - instance.handleReservationsButton = simple.mock(); - }); - - afterEach(() => { - instance.handleReservationsButton.reset(); - }); - - afterAll(() => { - simple.restore(); - }); - - test('calls handleReservationsButton', () => { - expect(button).toHaveLength(1); - expect(typeof button.prop('onClick')).toBe('function'); - button.prop('onClick')(); - expect(instance.handleReservationsButton.callCount).toBe(1); - }); - }); - - describe('handleReservationsButton', () => { - const expectedPath = '/my-reservations'; - let instance; - let historyMock; - - beforeAll(() => { - instance = getWrapper().instance(); - historyMock = simple.mock(history, 'replace'); - instance.handleReservationsButton(); - }); - - afterAll(() => { - simple.restore(); - }); - - test('calls browserHistory replace with correct path', () => { - expect(historyMock.callCount).toBe(1); - expect(historyMock.lastCall.args).toEqual([expectedPath]); - }); - }); }); diff --git a/app/pages/reservation/reservation-confirmation/_reservation-confirmation.scss b/app/pages/reservation/reservation-confirmation/_reservation-confirmation.scss index e02b7f4e1..700630ef1 100644 --- a/app/pages/reservation/reservation-confirmation/_reservation-confirmation.scss +++ b/app/pages/reservation/reservation-confirmation/_reservation-confirmation.scss @@ -10,24 +10,34 @@ text-align: right; } - &__field { - padding-top: 12px; - } a { color: $blue; } &__icon { height: 20px; - margin-right: 6px; + margin-right: 7px; + } + + &__highlight { + background: darken($light-gray, 5%); + padding: 5px 0; + margin-bottom: 20px; } - .reservation-date { + &__reservation-date { .reservation-date__content { - margin: 0; + margin: 20px auto; } h3 { font-size: 1.6rem; - text-align: left; + font-weight: normal; } } + &__resource-name { + text-align: center; + } + + &__error-msg-title { + font-weight: $font-weight-light; + } } diff --git a/app/pages/reservation/reservation-information/Error.js b/app/pages/reservation/reservation-information/Error.js new file mode 100644 index 000000000..55d3a63ec --- /dev/null +++ b/app/pages/reservation/reservation-information/Error.js @@ -0,0 +1,14 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +function Error({ error }) { + return ( + {error} + ); +} + +Error.propTypes = { + error: PropTypes.string.isRequired, +}; + +export default Error; diff --git a/app/pages/reservation/reservation-information/Input.js b/app/pages/reservation/reservation-information/Input.js new file mode 100644 index 000000000..4ef0e0fae --- /dev/null +++ b/app/pages/reservation/reservation-information/Input.js @@ -0,0 +1,32 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Error from './Error'; +import injectT from '../../../i18n/injectT'; + +function Input({ + input, + meta: { error, touched }, + t, + label, +}) { + return ( +
+ + {touched && error && } +
+ ); +} + +Input.propTypes = { + input: PropTypes.object.isRequired, + meta: PropTypes.object.isRequired, + t: PropTypes.func.isRequired, + label: PropTypes.string.isRequired, +}; + +export default injectT(Input); diff --git a/app/pages/reservation/reservation-information/ReservationInformation.js b/app/pages/reservation/reservation-information/ReservationInformation.js index b5a9952e6..03d4acbcc 100644 --- a/app/pages/reservation/reservation-information/ReservationInformation.js +++ b/app/pages/reservation/reservation-information/ReservationInformation.js @@ -1,17 +1,18 @@ import pick from 'lodash/pick'; import uniq from 'lodash/uniq'; import camelCase from 'lodash/camelCase'; +import get from 'lodash/get'; import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import Col from 'react-bootstrap/lib/Col'; import Row from 'react-bootstrap/lib/Row'; -import Well from 'react-bootstrap/lib/Well'; import moment from 'moment'; -import { injectT } from 'i18n'; -import { isStaffEvent } from 'utils/reservationUtils'; -import { getTermsAndConditions } from 'utils/resourceUtils'; +import injectT from '../../../i18n/injectT'; +import { isStaffEvent, getReservationPrice, getReservationPricePerPeriod } from '../../../utils/reservationUtils'; +import { getTermsAndConditions, hasProducts } from '../../../utils/resourceUtils'; import ReservationInformationForm from './ReservationInformationForm'; +import apiClient from '../../../../src/common/api/client'; class ReservationInformation extends Component { static propTypes = { @@ -30,6 +31,25 @@ class ReservationInformation extends Component { unit: PropTypes.object.isRequired, }; + state = { + reservationPrice: null, + } + + componentDidMount() { + if (!hasProducts(this.props.resource)) { + return; + } + const products = get(this.props.resource, 'products'); + const { + begin, + end, + } = this.props.selectedTime; + + getReservationPrice(apiClient, begin, end, products) + .then(price => this.setState({ reservationPrice: price })) + .catch(() => this.setState({ reservationPrice: null })); + } + onConfirm = (values) => { const { onConfirm } = this.props; onConfirm(values); @@ -60,6 +80,10 @@ class ReservationInformation extends Component { formFields.push('termsAndConditions'); } + if (hasProducts(resource)) { + formFields.push('paymentTermsAndConditions'); + } + return uniq(formFields); } @@ -85,6 +109,10 @@ class ReservationInformation extends Component { requiredFormFields.push('termsAndConditions'); } + if (hasProducts(resource)) { + requiredFormFields.push('paymentTermsAndConditions'); + } + return requiredFormFields; } @@ -112,6 +140,11 @@ class ReservationInformation extends Component { t, unit, } = this.props; + const { + reservationPrice, + } = this.state; + + const taxPercentage = get(resource, 'products[0].price.taxPercentage'); const termsAndConditions = getTermsAndConditions(resource); const beginText = moment(selectedTime.begin).format('D.M.YYYY HH:mm'); @@ -137,27 +170,63 @@ class ReservationInformation extends Component { /> - -

{t('ReservationPage.detailsTitle')}

+
+

{t('ReservationPage.detailsTitle')}

- - {t('common.resourceLabel')} + + + {t('common.resourceLabel')} + - - {resource.name} -
- {unit.name} + + + {resource.name} +
+ {unit.name} +
+ {hasProducts(resource) && ( + + + + + {t('common.priceLabel')} + + + + + {getReservationPricePerPeriod(resource)} + + + + + + + {t('common.totalPriceLabel')} + + + + + {t('common.priceWithVAT', { price: reservationPrice, vat: taxPercentage })} + + + + + )} - - {t('ReservationPage.detailsTime')} + + + {t('ReservationPage.detailsTime')} + - - {`${beginText}–${endText} (${hours} h)`} + + + {`${beginText}–${endText} (${hours} h)`} + - +
); diff --git a/app/pages/reservation/reservation-information/ReservationInformationForm.js b/app/pages/reservation/reservation-information/ReservationInformationForm.js index 741822fd4..58f25e215 100644 --- a/app/pages/reservation/reservation-information/ReservationInformationForm.js +++ b/app/pages/reservation/reservation-information/ReservationInformationForm.js @@ -1,6 +1,3 @@ -import constants from 'constants/AppConstants'; -import FormTypes from 'constants/FormTypes'; - import includes from 'lodash/includes'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; @@ -10,11 +7,14 @@ import Well from 'react-bootstrap/lib/Well'; import { Field, reduxForm } from 'redux-form'; import isEmail from 'validator/lib/isEmail'; - -import ReduxFormField from 'shared/form-fields/ReduxFormField'; -import TermsField from 'shared/form-fields/TermsField'; -import { injectT } from 'i18n'; -import ReservationTermsModal from 'shared/modals/reservation-terms'; +import TermsField from '../../../shared/form-fields/TermsField'; +import constants from '../../../constants/AppConstants'; +import FormTypes from '../../../constants/FormTypes'; +import ReservationMetadataField from './ReservationMetadataField'; +import injectT from '../../../i18n/injectT'; +import ReservationTermsModal from '../../../shared/modals/reservation-terms/ReservationTermsModal'; +import PaymentTermsModal from '../../../shared/modals/payment-terms/PaymentTermsModal'; +import { hasProducts } from '../../../utils/resourceUtils'; const validators = { reserverEmailAddress: (t, { reserverEmailAddress }) => { @@ -23,12 +23,22 @@ const validators = { } return null; }, + billingEmailAddress: (t, { billingEmailAddress }) => { + if (billingEmailAddress && !isEmail(billingEmailAddress)) { + return t('ReservationForm.emailError'); + } + return null; + } }; const maxLengths = { billingAddressCity: 100, billingAddressStreet: 100, billingAddressZip: 30, + billingEmailAddress: 100, + billingFirstName: 100, + billingLastName: 100, + billingPhoneNumber: 30, company: 100, numberOfParticipants: 100, reserverAddressCity: 100, @@ -40,6 +50,17 @@ const maxLengths = { reserverPhoneNumber: 30, }; +function isTermsAndConditionsField(field) { + return field === 'termsAndConditions' + || field === 'paymentTermsAndConditions'; +} + +function getTermsAndConditionsError(field) { + return field === 'paymentTermsAndConditions' + ? 'ReservationForm.paymentTermsAndConditionsError' + : 'ReservationForm.termsAndConditionsError'; +} + export function validate(values, { fields, requiredFields, t }) { const errors = {}; const currentRequiredFields = values.staffEvent @@ -61,8 +82,8 @@ export function validate(values, { fields, requiredFields, t }) { if (includes(currentRequiredFields, field)) { if (!values[field]) { errors[field] = ( - field === 'termsAndConditions' - ? t('ReservationForm.termsAndConditionsError') + isTermsAndConditionsField(field) + ? t(getTermsAndConditionsError(field)) : t('ReservationForm.requiredError') ); } @@ -72,7 +93,19 @@ export function validate(values, { fields, requiredFields, t }) { } class UnconnectedReservationInformationForm extends Component { - renderField(name, type, label, controlProps = {}, help = null, info = null) { + state = { + isPaymentTermsModalOpen: false, + } + + closePaymentTermsModal = () => { + this.setState({ isPaymentTermsModalOpen: false }); + } + + openPaymentTermsModal = () => { + this.setState({ isPaymentTermsModalOpen: true }); + } + + renderField(name, type, label, help = null) { if (!includes(this.props.fields, name)) { return null; } @@ -80,10 +113,8 @@ class UnconnectedReservationInformationForm extends Component { return ( ); } - render() { + renderPaymentTermsField = () => { + const { t } = this.props; + return ( + + ); + } + + renderSaveButton() { + const { + isMakingReservations, + handleSubmit, + onConfirm, + t, + } = this.props; + return ( + + ); + } + + renderPayButton() { const { - isEditing, isMakingReservations, handleSubmit, + onConfirm, + t, + } = this.props; + return ( + + ); + } + + render() { + const { + isEditing, + fields, onBack, onCancel, - onConfirm, requiredFields, resource, staffEventSelected, t, termsAndConditions, } = this.props; + const { + isPaymentTermsModalOpen, + } = this.state; this.requiredFields = staffEventSelected ? constants.REQUIRED_STAFF_EVENT_FIELDS @@ -128,11 +214,16 @@ class UnconnectedReservationInformationForm extends Component { return (
-
- { includes(this.props.fields, 'reserverName') && ( -

{t('ReservationInformationForm.reserverInformationTitle')}

+ +

+ {t('ReservationForm.reservationFieldsAsteriskExplanation')} +

+ { includes(fields, 'reserverName') && ( +

+ {t('ReservationInformationForm.reserverInformationTitle')} +

)} - { includes(this.props.fields, 'staffEvent') && ( + { includes(fields, 'staffEvent') && ( {this.renderField( 'staffEvent', @@ -147,82 +238,102 @@ class UnconnectedReservationInformationForm extends Component { 'reserverName', 'text', t('common.reserverNameLabel'), - { placeholder: t('common.reserverNameLabel') } )} {this.renderField( 'reserverId', 'text', t('common.reserverIdLabel'), - { placeholder: t('common.reserverIdLabel') } )} {this.renderField( 'reserverPhoneNumber', 'text', t('common.reserverPhoneNumberLabel'), - { placeholder: t('common.reserverPhoneNumberLabel') } )} {this.renderField( 'reserverEmailAddress', 'email', t('common.reserverEmailAddressLabel'), - { placeholder: t('common.reserverEmailAddressLabel') } )} - {includes(this.props.fields, 'reserverAddressStreet') + {includes(fields, 'reserverAddressStreet') && this.renderField( 'reserverAddressStreet', 'text', t('common.addressStreetLabel'), - { placeholder: t('common.addressStreetLabel') } )} - {includes(this.props.fields, 'reserverAddressZip') + {includes(fields, 'reserverAddressZip') && this.renderField( 'reserverAddressZip', 'text', t('common.addressZipLabel'), - { placeholder: t('common.addressZipLabel') } )} - {includes(this.props.fields, 'reserverAddressCity') + {includes(fields, 'reserverAddressCity') && this.renderField( 'reserverAddressCity', 'text', t('common.addressCityLabel'), - { placeholder: t('common.addressCityLabel') } ) } - {includes(this.props.fields, 'billingAddressStreet') - &&

{t('common.billingAddressLabel')}

+ {includes(fields, 'billingAddressStreet') + &&

{t('common.billingAddressLabel')}

} - {includes(this.props.fields, 'billingAddressStreet') + {includes(fields, 'billingAddressStreet') && this.renderField( 'billingAddressStreet', 'text', t('common.addressStreetLabel'), - { placeholder: t('common.addressStreetLabel') } ) } - {includes(this.props.fields, 'billingAddressZip') + {includes(fields, 'billingAddressZip') && this.renderField( 'billingAddressZip', 'text', t('common.addressZipLabel'), - { placeholder: t('common.addressZipLabel') } ) } - {includes(this.props.fields, 'billingAddressCity') + {includes(fields, 'billingAddressCity') && this.renderField( 'billingAddressCity', 'text', t('common.addressCityLabel'), - { placeholder: t('common.addressCityLabel') } ) } -

{t('ReservationInformationForm.eventInformationTitle')}

+ {includes(fields, 'billingFirstName') + &&

{t('common.paymentInformationLabel')}

+ } + {includes(fields, 'billingFirstName') + && this.renderField( + 'billingFirstName', + 'text', + t('common.billingFirstNameLabel'), + ) + } + {includes(fields, 'billingLastName') + && this.renderField( + 'billingLastName', + 'text', + t('common.billingLastNameLabel'), + ) + } + {includes(fields, 'billingPhoneNumber') + && this.renderField( + 'billingPhoneNumber', + 'tel', + t('common.billingPhoneNumberLabel'), + ) + } + {includes(fields, 'billingEmailAddress') + && this.renderField( + 'billingEmailAddress', + 'email', + t('common.billingEmailAddressLabel'), + ) + } +

{t('ReservationInformationForm.eventInformationTitle')}

{this.renderField( 'eventSubject', 'text', t('common.eventSubjectLabel'), {}, - null, t('ReservationForm.eventSubjectInfo'), )} {this.renderField( @@ -241,17 +352,22 @@ class UnconnectedReservationInformationForm extends Component { 'comments', 'textarea', t('common.commentsLabel'), - { - placeholder: t('common.commentsPlaceholder'), - rows: 5, - } + { rows: 5 } )} {termsAndConditions && this.renderTermsField('termsAndConditions') } -
+ {resource.specificTerms && ( +
+

{t('ReservationForm.specificTermsTitle')}

+

{resource.specificTerms}

+
+ )} + {includes(fields, 'paymentTermsAndConditions') + && this.renderPaymentTermsField() + } +
) } - + {hasProducts(resource) + ? this.renderPayButton() + : this.renderSaveButton() + }
+
); } diff --git a/app/pages/reservation/reservation-information/ReservationMetadataField.js b/app/pages/reservation/reservation-information/ReservationMetadataField.js new file mode 100644 index 000000000..6a0c472e1 --- /dev/null +++ b/app/pages/reservation/reservation-information/ReservationMetadataField.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Input from './Input'; +import Textarea from './Textarea'; +import Terms from './Terms'; + +function ReservationMetadataField({ type, ...rest }) { + if (type === 'textarea') { + return