diff --git a/CHANGELOG.md b/CHANGELOG.md index c51d7e2ce..1df391673 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## Unreleased +## Version 23.12.1 + +Released on 19.12.2023 + +- Feature: Add global searchbar and improve filters +- Feature: Add occupancies to booking form +- Feature: Set default times for bookings +- Feature: Do not show payment info when amount is not positive +- Improvement: Improve responsiveness of layout +- Bugfixes + ## Version 23.11.2 Released on 28.11.2023 diff --git a/Dockerfile b/Dockerfile index 481e00a0a..4378b6dd0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ### === base === ### FROM ruby:3.2.2-alpine AS base -RUN apk add --no-cache --update postgresql-dev tzdata libssl1.1 nodejs +RUN apk add --no-cache --update postgresql-dev tzdata nodejs RUN gem install bundler ### === development === ### diff --git a/Gemfile.lock b/Gemfile.lock index 5af5a9fe6..e420e09ed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,27 +77,27 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) annotate (3.2.0) activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) - aws-eventstream (1.2.0) - aws-partitions (1.853.0) - aws-sdk-core (3.187.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-eventstream (1.3.0) + aws-partitions (1.869.0) + aws-sdk-core (3.190.0) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.72.0) - aws-sdk-core (~> 3, >= 3.184.0) + aws-sdk-kms (1.75.0) + aws-sdk-core (~> 3, >= 3.188.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.137.0) - aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-s3 (1.141.0) + aws-sdk-core (~> 3, >= 3.189.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.6.1) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babel-source (5.8.35) babel-transpiler (0.7.0) @@ -108,14 +108,14 @@ GEM statsd-ruby (~> 1.1) base64 (0.2.0) bcrypt (3.1.20) - bigdecimal (3.1.4) + bigdecimal (3.1.5) blueprinter (0.30.0) bootsnap (1.17.0) msgpack (~> 1.2) bootstrap_form (5.4.0) actionpack (>= 6.1) activemodel (>= 6.1) - brakeman (6.0.1) + brakeman (6.1.0) builder (3.2.4) bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) @@ -140,7 +140,7 @@ GEM coderay (1.1.3) concurrent-ruby (1.2.2) connection_pool (2.4.1) - countries (5.7.0) + countries (5.7.1) unaccent (~> 0.3) country_select (8.0.3) countries (~> 5.0) @@ -185,7 +185,7 @@ GEM railties (>= 5.0.0) faker (3.2.2) i18n (>= 1.8.11, < 2) - faraday (2.7.11) + faraday (2.7.12) base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) @@ -218,19 +218,19 @@ GEM terminal-table (>= 1.5.1) i18n-tasks-csv (1.1) i18n-tasks (~> 0.9) - icalendar (2.10.0) + icalendar (2.10.1) ice_cube (~> 0.16) ice_cube (0.16.4) inline_svg (1.9.0) activesupport (>= 3.0) nokogiri (>= 1.6) interception (0.5) - io-console (0.6.0) - irb (1.9.0) + io-console (0.7.1) + irb (1.10.1) rdoc reline (>= 0.3.8) jmespath (1.6.2) - json (2.6.3) + json (2.7.1) kramdown (2.4.0) rexml language_server-protocol (3.17.0.3) @@ -263,7 +263,7 @@ GEM msgpack (1.7.2) multi_json (1.15.0) mutex_m (0.2.0) - net-imap (0.4.5) + net-imap (0.4.8) date net-protocol net-pop (0.1.2) @@ -272,11 +272,11 @@ GEM timeout net-smtp (0.4.0) net-protocol - nio4r (2.6.0) + nio4r (2.7.0) nokogiri (1.15.5-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) - parallel (1.23.0) + parallel (1.24.0) parser (3.2.2.4) ast (~> 2.4.1) racc @@ -302,7 +302,7 @@ GEM pry-rescue (1.5.2) interception (>= 0.5) pry (>= 0.12.0) - psych (5.1.1.1) + psych (5.1.2) stringio public_suffix (5.0.4) puma (6.4.0) @@ -310,7 +310,7 @@ GEM raabro (1.4.0) racc (1.7.3) rack (3.0.8) - rack-mini-profiler (3.1.1) + rack-mini-profiler (3.3.0) rack (>= 1.2.0) rack-proxy (0.7.7) rack @@ -360,7 +360,7 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - rdoc (6.6.0) + rdoc (6.6.2) psych (>= 4.0.0) react-rails (3.1.1) babel-transpiler (>= 0.7.0) @@ -370,10 +370,10 @@ GEM tilt redis (5.0.8) redis-client (>= 0.17.0) - redis-client (0.18.0) + redis-client (0.19.0) connection_pool - regexp_parser (2.8.2) - reline (0.4.0) + regexp_parser (2.8.3) + reline (0.4.1) io-console (~> 0.5) request_store (1.5.1) rack (>= 1.4) @@ -393,7 +393,7 @@ GEM rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.0.3) + rspec-rails (6.1.0) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -402,7 +402,7 @@ GEM rspec-mocks (~> 3.12) rspec-support (~> 3.12) rspec-support (3.12.1) - rubocop (1.57.2) + rubocop (1.59.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -410,7 +410,7 @@ GEM rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.30.0) @@ -419,10 +419,10 @@ GEM rubocop (~> 1.41) rubocop-factory_bot (2.24.0) rubocop (~> 1.33) - rubocop-performance (1.19.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) - rubocop-rails (2.22.2) + rubocop-performance (1.20.0) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) + rubocop-rails (2.23.0) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) @@ -434,7 +434,7 @@ GEM ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) rubyzip (2.3.2) - selenium-webdriver (4.15.0) + selenium-webdriver (4.16.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -449,7 +449,7 @@ GEM connection_pool (>= 2.3.0) rack (>= 2.2.4) redis-client (>= 0.14.0) - sidekiq-cron (1.11.0) + sidekiq-cron (1.12.0) fugit (~> 1.8) globalid (>= 1.0.1) sidekiq (>= 6) @@ -468,9 +468,9 @@ GEM railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) squasher (0.7.2) - statesman (11.0.0) + statesman (12.0.0) statsd-ruby (1.5.0) - stringio (3.0.9) + stringio (3.1.0) temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) diff --git a/VERSION b/VERSION index 22e7cc4b2..62dacea78 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -V23.11.2 +V23.12.1 diff --git a/app/concerns/timespanable.rb b/app/concerns/timespanable.rb index e3991f85c..9da0cba8f 100644 --- a/app/concerns/timespanable.rb +++ b/app/concerns/timespanable.rb @@ -59,7 +59,7 @@ def define_timespan_scopes def timespan_scope_lambda(arel_column) lambda do |before: nil, after: nil| - return where(arel_column.between(after..before)) if before.present? || after.present? + where(arel_column.between(after..before)) if before.present? || after.present? end end diff --git a/app/controllers/manage/designated_documents_controller.rb b/app/controllers/manage/designated_documents_controller.rb index e8223f0c0..15ecafc82 100644 --- a/app/controllers/manage/designated_documents_controller.rb +++ b/app/controllers/manage/designated_documents_controller.rb @@ -5,7 +5,7 @@ class DesignatedDocumentsController < BaseController load_and_authorize_resource :designated_document def index - @designated_documents = @designated_documents.where(organisation: current_organisation) + @designated_documents = @designated_documents.where(organisation: current_organisation).order(created_at: :ASC) respond_with :manage, @designated_documents end diff --git a/app/controllers/manage/tarifs_controller.rb b/app/controllers/manage/tarifs_controller.rb index fae2fc64a..2d50b1f34 100644 --- a/app/controllers/manage/tarifs_controller.rb +++ b/app/controllers/manage/tarifs_controller.rb @@ -6,6 +6,7 @@ class TarifsController < BaseController def index @tarifs = @tarifs.where(organisation: current_organisation).ordered + @tarifs = @tarifs.kept if params[:all].blank? respond_with :manage, @tarifs end diff --git a/app/controllers/public/homes_controller.rb b/app/controllers/public/homes_controller.rb index da66bf581..ec741a848 100644 --- a/app/controllers/public/homes_controller.rb +++ b/app/controllers/public/homes_controller.rb @@ -5,7 +5,7 @@ class HomesController < BaseController load_and_authorize_resource :home def index - @homes = @homes.where(organisation: current_organisation) + @homes = @homes.where(organisation: current_organisation).active respond_with :public, @homes end diff --git a/app/controllers/public/organisations_controller.rb b/app/controllers/public/organisations_controller.rb index 90bc676e7..9ab8d157d 100644 --- a/app/controllers/public/organisations_controller.rb +++ b/app/controllers/public/organisations_controller.rb @@ -3,10 +3,12 @@ module Public class OrganisationsController < BaseController before_action :set_organisation - # authorize_resource :organisation def show - redirect_to homes_path + respond_to do |format| + format.html { redirect_to homes_path } + format.json { render json: OrganisationSerializer.render_as_json(@organisation) } + end end protected diff --git a/app/domain/booking_actions/manage/email_contract.rb b/app/domain/booking_actions/manage/email_contract.rb index 13429e307..30f2375f1 100644 --- a/app/domain/booking_actions/manage/email_contract.rb +++ b/app/domain/booking_actions/manage/email_contract.rb @@ -7,7 +7,8 @@ class EmailContract < BookingActions::Base autodeliver: false) def call! - mail = MailTemplate.use!(:awaiting_contract_notification, booking, to: :tenant, contract:, invoices: deposits) + mail = MailTemplate.use!(:awaiting_contract_notification, booking, to: :tenant, booking:, contract:, + invoices: deposits) mail.attach :contract, :contract_documents, deposits mail.save! && contract.sent! && deposits.each(&:sent!) diff --git a/app/domain/booking_states/cancelled_request.rb b/app/domain/booking_states/cancelled_request.rb index da6103ed1..930167539 100644 --- a/app/domain/booking_states/cancelled_request.rb +++ b/app/domain/booking_states/cancelled_request.rb @@ -3,6 +3,7 @@ module BookingStates class CancelledRequest < Base templates << MailTemplate.define(:cancelled_request_notification, context: %i[booking]) + templates << MailTemplate.define(:manage_cancelled_request_notification, context: %i[booking]) def checklist [] @@ -20,6 +21,7 @@ def self.to_sym booking.free! booking.conclude booking.deadline&.clear + MailTemplate.use(:manage_cancelled_request_notification, booking, to: :administration, &:autodeliver) MailTemplate.use(:cancelled_request_notification, booking, to: booking.agent_booking ? :booking_agent : :tenant, &:autodeliver) end diff --git a/app/javascript/components/calendar/Calendar.tsx b/app/javascript/components/calendar/Calendar.tsx new file mode 100644 index 000000000..b5cc9932a --- /dev/null +++ b/app/javascript/components/calendar/Calendar.tsx @@ -0,0 +1,52 @@ +import { Dispatch, SetStateAction, createContext, useState } from "react"; +import MonthsCalendar from "./MonthsCalendar"; +import YearCalendar from "./YearCalendar"; +import * as React from "react"; +import { DateElementFactory } from "./CalendarDate"; +import { CalendarNav } from "./CalendarNav"; +import getYear from "date-fns/getYear"; +import addMonths from "date-fns/addMonths"; +import subMonths from "date-fns/subMonths"; +import { parseDate } from "../../services/date"; + +type CalendarProps = { + initialFirstDate?: string; + dateElementFactory: DateElementFactory; + defaultView?: ViewType; + months?: number; +}; + +export type ViewType = "months" | "year"; +type CalendarViewContextType = { + view: ViewType; + setView?: Dispatch>; +}; +export const CalendarViewContext = createContext({ view: "months" }); + +function Calendar({ initialFirstDate, defaultView, months, dateElementFactory }: CalendarProps) { + const [view, setView] = useState(defaultView || "months"); + const [firstDate, setFirstDate] = useState(parseDate(initialFirstDate)); + const gotoNextMonth = () => setFirstDate((prevFirstDate) => addMonths(prevFirstDate, 1)); + const gotoPrevMonth = () => setFirstDate((prevFirstDate) => subMonths(prevFirstDate, 1)); + const gotoToday = () => setFirstDate(new Date()); + + return ( + +
+ + {getYear(firstDate)} + + {({ months: MonthsCalendar, year: YearCalendar }[view] || MonthsCalendar)({ + firstDate, + months, + dateElementFactory, + })} + + {getYear(firstDate)} + +
+
+ ); +} + +export default Calendar; diff --git a/app/javascript/components/calendar/CalendarFormField.tsx b/app/javascript/components/calendar/CalendarFormField.tsx deleted file mode 100644 index 035afb614..000000000 --- a/app/javascript/components/calendar/CalendarFormField.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import * as React from "react"; -import { InputGroup, Form, Button, Modal, Row, Col } from "react-bootstrap"; -import { OccupancyWindow } from "../../models/OccupancyWindow"; -import { - formatISO, - parseISO, - parse, - isSameDay, - isValid, - setHours, - setMinutes, - getHours, - getMinutes, - isAfter, - isBefore, - addYears, -} from "date-fns/esm"; -import { getYear } from "date-fns"; -import OccupancyCalendar from "./OccupancyCalendar"; - -export type HourRange = number[] | string; - -const formatDate = new Intl.DateTimeFormat("de-CH", { - year: "numeric", - month: "2-digit", - day: "2-digit", -}).format; - -function valueAsDate(value: string | Date): Date | undefined { - if (!value) return; - if (value instanceof Date && isValid(value)) return value; - const dateValue = parseISO((value as string) || ""); - if (!isValid(dateValue)) return; - return dateValue; -} - -function dateToCalendarControlValue( - date: Date, - availableHours: number[], - availableMinutes: number[], -): CalendarControlValue { - const hours = closestNumber((date && getHours(date)) || 0, availableHours); - const minutes = closestNumber((date && getMinutes(date)) || 0, availableMinutes); - - if (date) { - date = setHours(date, hours); - date = setMinutes(date, minutes); - } - - return { - date: date, - hours: hours, - minutes: minutes, - text: (date && formatDate(date)) || "", - }; -} - -export const availableMinutes = [0, 15, 30, 45]; - -const closestNumber = (n: number, range: number[]) => { - return range.includes(n) ? n : range.reduce((a, b) => (Math.abs(b - n) < Math.abs(a - n) ? b : a)); -}; - -type CalendarControlValue = { - date?: Date; - hours: number; - minutes: number; - text: string; -}; - -interface CalendarInputProps { - value: string | Date; - name?: string; - id?: string; - required?: boolean; - disabled?: boolean; - onChange?(value: Date): void; - onBlur?(): void; - isInvalid?: boolean; - className?: string; - occupancyWindow?: OccupancyWindow; - minDate?: Date; - maxDate?: Date; - availableHours?: number[] | string; - calendarUrl?: string; -} - -export function CalendarInput({ - value = "", - name, - id, - required = false, - disabled = false, - onChange, - onBlur, - isInvalid = false, - minDate, - maxDate, - calendarUrl, -}: CalendarInputProps) { - const availableHours = [...Array(24)].map((_, i) => 0 + i); - const [showModal, setShowModal] = React.useState(false); - const [calendarControlValue, setCalendarControlValue] = React.useState({ - hours: Math.min(...availableHours), - minutes: Math.min(...availableMinutes), - text: "", - }); - - React.useEffect(() => { - const dateValue = valueAsDate(value); - if (dateValue && dateValue.toISOString() != calendarControlValue.date?.toISOString()) - setDateValue(dateToCalendarControlValue(dateValue, availableHours, availableMinutes)); - }, [value, onChange]); - - const setDateValue = ({ date, hours, minutes }: Partial) => { - date = date ?? calendarControlValue?.date; - if (date == undefined || date == null || !isValid(date)) return; - - date = setHours(date, hours ?? (calendarControlValue?.hours || Math.min(...availableHours))); - date = setMinutes(date, minutes ?? (calendarControlValue?.minutes || Math.min(...availableMinutes))); - setCalendarControlValue(dateToCalendarControlValue(date, availableHours, availableMinutes)); - onChange && onChange(date); - return date; - }; - - const handleClose = () => setShowModal(false); - const handleShow = () => setShowModal(true); - const handleClick = (event: React.MouseEvent) => { - if (disabled) return; - - const parsedValue = parseISO((event.currentTarget as HTMLInputElement).value); - if (!isValid(parsedValue)) return; - - setDateValue({ date: parsedValue }); - setShowModal(false); - }; - - const handleTextChange = (event: React.ChangeEvent) => { - if (disabled) return; - - const value = (event.target as HTMLInputElement).value; - let parsedValue = parse(value, "dd.MM.yyyy", new Date()); - if (getYear(parsedValue) < 100) parsedValue = addYears(parsedValue, 2000); - if (!setDateValue({ date: parsedValue })) { - setCalendarControlValue((prev) => ({ - ...prev, - text: (calendarControlValue?.date && formatDate(calendarControlValue.date)) || value, - })); - } - onBlur && onBlur(); - }; - - const classNameCallback = (date: Date) => - calendarControlValue?.date && isSameDay(date, calendarControlValue.date) ? "selected-date" : ""; - - const disabledCallback = (date: Date) => - (minDate && isBefore(date, minDate)) || (maxDate && isAfter(date, maxDate)) || false; - - return ( -
- - - - - { - const value = (e.target as HTMLInputElement).value; - setCalendarControlValue((prev) => ({ ...prev, text: value })); - }} - isInvalid={isInvalid} - required={required} - /> - - - - - - setDateValue({ - hours: closestNumber(parseInt((e.target as HTMLInputElement).value), availableHours), - }) - } - isInvalid={isInvalid} - disabled={disabled || !isValid(calendarControlValue.date)} - required={required} - as="select" - id={`${id}_hours`} - > - {availableHours.map((hour) => ( - - ))} - - - setDateValue({ - minutes: closestNumber(parseInt((e.target as HTMLInputElement).value), availableMinutes), - }) - } - isInvalid={isInvalid} - disabled={disabled || !isValid(calendarControlValue.date)} - required={required} - as="select" - id={`${id}_minutes`} - > - {availableMinutes.map((minutes) => ( - - ))} - - - - - - - - -
- ); -} - -interface CalendarFormField { - value: string; - name: string; - id?: string; - label: string; - required?: boolean; - disabled?: boolean; - isInvalid?: boolean; - invalidFeedback?: string; - calendarUrl?: string; -} - -export default function CalendarFormField({ - value, - name, - id, - label, - required = false, - disabled = false, - invalidFeedback, - isInvalid = !!invalidFeedback, - calendarUrl, -}: CalendarFormField) { - return ( - - {label} - - {invalidFeedback} - - ); -} diff --git a/app/javascript/components/calendar/CalendarNav.tsx b/app/javascript/components/calendar/CalendarNav.tsx index 4158e26ac..ae00e4076 100644 --- a/app/javascript/components/calendar/CalendarNav.tsx +++ b/app/javascript/components/calendar/CalendarNav.tsx @@ -1,18 +1,27 @@ -import { useContext } from "react"; -import { CalendarViewContext } from "./OccupancyCalendar"; +import { PropsWithChildren, useContext } from "react"; +import { CalendarViewContext } from "./Calendar"; interface CalendarNavProps { onPrev(): void; onNext(): void; - children?: React.ReactNode; + onToday(): void; } -export function CalendarNav({ onPrev, onNext, children }: CalendarNavProps) { +export function CalendarNav({ onPrev, onNext, onToday, children }: PropsWithChildren) { const { view, setView } = useContext(CalendarViewContext); return (