mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-17 11:54:22 +01:00
(feat) places cache for slots
This commit is contained in:
parent
9f9a2e616f
commit
986a663c40
@ -57,6 +57,7 @@
|
|||||||
- Fix a security issue: updated rack to 2.2.6.2 to fix [CVE-2022-44571](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-44571)
|
- Fix a security issue: updated rack to 2.2.6.2 to fix [CVE-2022-44571](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-44571)
|
||||||
- Fix a security issue: updated globalid to 1.0.1 to fix [CVE-2023-22799](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-22799)
|
- Fix a security issue: updated globalid to 1.0.1 to fix [CVE-2023-22799](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-22799)
|
||||||
- [TODO DEPLOY] `rails fablab:fix:invoice_items_in_error` THEN `rails fablab:fix_invoice_items` THEN `rails db:migrate`
|
- [TODO DEPLOY] `rails fablab:fix:invoice_items_in_error` THEN `rails fablab:fix_invoice_items` THEN `rails db:migrate`
|
||||||
|
- [TODO DEPLOY] `rails fablab:fix_availabilities` THEN `rails fablab:setup:build_places_cache`
|
||||||
|
|
||||||
## v5.6.5 2023 January 9
|
## v5.6.5 2023 January 9
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change cart's customer by admin/manger
|
* Change cart's customer by admin/manager
|
||||||
*/
|
*/
|
||||||
const handleChangeMember = (user: User): void => {
|
const handleChangeMember = (user: User): void => {
|
||||||
CartAPI.setCustomer(cart, user.id).then(setCart).catch(onError);
|
CartAPI.setCustomer(cart, user.id).then(setCart).catch(onError);
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { IApplication } from '../../models/application';
|
||||||
|
import { Loader } from '../base/loader';
|
||||||
|
import { react2angular } from 'react2angular';
|
||||||
|
import { Slot } from '../../models/slot';
|
||||||
|
import { ReservableType } from '../../models/reservation';
|
||||||
|
import { User } from '../../models/user';
|
||||||
|
import { MemberSelect } from '../user/member-select';
|
||||||
|
import CartAPI from '../../api/cart';
|
||||||
|
import useCart from '../../hooks/use-cart';
|
||||||
|
import { ReservationsSummary } from './reservations-summary';
|
||||||
|
import UserLib from '../../lib/user';
|
||||||
|
|
||||||
|
declare const Application: IApplication;
|
||||||
|
|
||||||
|
interface OngoingReservationPanelProps {
|
||||||
|
selectedSlot: Slot,
|
||||||
|
reservableId: number,
|
||||||
|
reservableType: ReservableType,
|
||||||
|
onError: (message: string) => void,
|
||||||
|
operator: User,
|
||||||
|
onSlotAdded: (slot: Slot) => void,
|
||||||
|
onSlotRemoved: (slot: Slot) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Panel to manage the ongoing reservation (select the member, show the price, etc)
|
||||||
|
*/
|
||||||
|
export const OngoingReservationPanel: React.FC<OngoingReservationPanelProps> = ({ selectedSlot, reservableId, reservableType, onError, operator, onSlotAdded, onSlotRemoved }) => {
|
||||||
|
const { cart, setCart } = useCart(operator);
|
||||||
|
|
||||||
|
const [noMemberError, setNoMemberError] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedSlot && !cart.user) {
|
||||||
|
setNoMemberError(true);
|
||||||
|
onError('please select a member first');
|
||||||
|
}
|
||||||
|
}, [selectedSlot]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The admin/manager can change the cart's customer
|
||||||
|
*/
|
||||||
|
const handleChangeMember = (user: User): void => {
|
||||||
|
CartAPI.setCustomer(cart, user.id).then(setCart).catch(onError);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ongoing-reservation-panel">
|
||||||
|
{new UserLib(operator).hasPrivilegedRole() &&
|
||||||
|
<MemberSelect onSelected={handleChangeMember}
|
||||||
|
defaultUser={cart?.user as User}
|
||||||
|
hasError={noMemberError} />
|
||||||
|
}
|
||||||
|
<ReservationsSummary reservableId={reservableId}
|
||||||
|
reservableType={reservableType}
|
||||||
|
onError={onError}
|
||||||
|
cart={cart}
|
||||||
|
setCart={setCart}
|
||||||
|
customer={cart?.user as User}
|
||||||
|
onSlotAdded={onSlotAdded}
|
||||||
|
onSlotRemoved={onSlotRemoved}
|
||||||
|
slot={selectedSlot} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OngoingReservationPanelWrapper: React.FC<OngoingReservationPanelProps> = (props) => (
|
||||||
|
<Loader>
|
||||||
|
<OngoingReservationPanel {...props} />
|
||||||
|
</Loader>
|
||||||
|
);
|
||||||
|
|
||||||
|
Application.Components.component('ongoingReservationPanel', react2angular(OngoingReservationPanelWrapper, ['selectedSlot', 'reservableId', 'reservableType', 'onError', 'operator', 'onSlotRemoved', 'onSlotAdded']));
|
@ -1,20 +1,18 @@
|
|||||||
import { IApplication } from '../../models/application';
|
import React, { ReactNode, useEffect, useState } from 'react';
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import { Loader } from '../base/loader';
|
|
||||||
import { react2angular } from 'react2angular';
|
|
||||||
import type { Slot } from '../../models/slot';
|
import type { Slot } from '../../models/slot';
|
||||||
import { useImmer } from 'use-immer';
|
import { useImmer } from 'use-immer';
|
||||||
import FormatLib from '../../lib/format';
|
import FormatLib from '../../lib/format';
|
||||||
import { FabButton } from '../base/fab-button';
|
import { FabButton } from '../base/fab-button';
|
||||||
import { ShoppingCart } from 'phosphor-react';
|
import { ShoppingCart } from 'phosphor-react';
|
||||||
import CartAPI from '../../api/cart';
|
import CartAPI from '../../api/cart';
|
||||||
import useCart from '../../hooks/use-cart';
|
|
||||||
import type { User } from '../../models/user';
|
import type { User } from '../../models/user';
|
||||||
import Switch from 'react-switch';
|
import Switch from 'react-switch';
|
||||||
import { CartItemReservation } from '../../models/cart_item';
|
import { CartItemReservation } from '../../models/cart_item';
|
||||||
import { ReservableType } from '../../models/reservation';
|
import { ReservableType } from '../../models/reservation';
|
||||||
|
import { Order } from '../../models/order';
|
||||||
declare const Application: IApplication;
|
import PriceAPI from '../../api/price';
|
||||||
|
import { PaymentMethod } from '../../models/payment';
|
||||||
|
import { ComputePriceResult } from '../../models/price';
|
||||||
|
|
||||||
interface ReservationsSummaryProps {
|
interface ReservationsSummaryProps {
|
||||||
slot: Slot,
|
slot: Slot,
|
||||||
@ -22,15 +20,19 @@ interface ReservationsSummaryProps {
|
|||||||
reservableId: number,
|
reservableId: number,
|
||||||
reservableType: ReservableType,
|
reservableType: ReservableType,
|
||||||
onError: (error: string) => void,
|
onError: (error: string) => void,
|
||||||
|
cart: Order,
|
||||||
|
setCart: (cart: Order) => void,
|
||||||
|
onSlotAdded: (slot: Slot) => void,
|
||||||
|
onSlotRemoved: (slot: Slot) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a summary of the selected slots, and ask for confirmation before adding them to the cart
|
* Display a summary of the selected slots, and ask for confirmation before adding them to the cart
|
||||||
*/
|
*/
|
||||||
const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, customer, reservableId, reservableType, onError }) => {
|
export const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, customer, reservableId, reservableType, onError, cart, setCart, onSlotAdded, onSlotRemoved }) => {
|
||||||
const { cart, setCart } = useCart(customer);
|
|
||||||
const [pendingSlots, setPendingSlots] = useImmer<Array<Slot>>([]);
|
const [pendingSlots, setPendingSlots] = useImmer<Array<Slot>>([]);
|
||||||
const [offeredSlots, setOfferedSlots] = useImmer<Map<number, boolean>>(new Map());
|
const [offeredSlots, setOfferedSlots] = useImmer<Map<number, boolean>>(new Map());
|
||||||
|
const [price, setPrice] = useState<ComputePriceResult>(null);
|
||||||
const [reservation, setReservation] = useImmer<CartItemReservation>({
|
const [reservation, setReservation] = useImmer<CartItemReservation>({
|
||||||
reservation: {
|
reservation: {
|
||||||
reservable_id: reservableId,
|
reservable_id: reservableId,
|
||||||
@ -42,18 +44,30 @@ const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, custome
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (slot) {
|
if (slot) {
|
||||||
if (pendingSlots.find(s => s.slot_id === slot.slot_id)) {
|
if (pendingSlots.find(s => s.slot_id === slot.slot_id)) {
|
||||||
setPendingSlots(draft => draft.filter(s => s.slot_id !== slot.slot_id));
|
removeSlot(slot)();
|
||||||
} else {
|
} else {
|
||||||
setPendingSlots(draft => { draft.push(slot); });
|
addSlot(slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [slot]);
|
}, [slot]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (customer && cart) {
|
if (!customer) return;
|
||||||
CartAPI.setCustomer(cart, customer.id).then(setCart).catch(onError);
|
|
||||||
}
|
PriceAPI.compute({
|
||||||
}, [customer]);
|
customer_id: customer.id,
|
||||||
|
items: [reservation],
|
||||||
|
payment_method: PaymentMethod.Other
|
||||||
|
}).then(setPrice).catch(onError);
|
||||||
|
}, [reservation]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new slot to the pending list
|
||||||
|
*/
|
||||||
|
const addSlot = (slot: Slot) => {
|
||||||
|
setPendingSlots(draft => { draft.push(slot); });
|
||||||
|
if (typeof onSlotAdded === 'function') onSlotAdded(slot);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the product to cart
|
* Add the product to cart
|
||||||
@ -69,6 +83,33 @@ const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, custome
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the reservation contains the given slot
|
||||||
|
*/
|
||||||
|
const isSlotInReservation = (slot: Slot): boolean => {
|
||||||
|
return reservation.reservation.slots_reservations_attributes.filter(s => s.slot_id === slot.slot_id).length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given slot from the reservation (if applicable) and trigger the onSlotRemoved callback
|
||||||
|
*/
|
||||||
|
const removeSlot = (slot: Slot) => {
|
||||||
|
return () => {
|
||||||
|
if (isSlotInReservation(slot)) {
|
||||||
|
setReservation(draft => {
|
||||||
|
return {
|
||||||
|
reservation: {
|
||||||
|
...draft.reservation,
|
||||||
|
slots_reservations_attributes: draft.reservation.slots_reservations_attributes.filter(sr => sr.slot_id !== slot.slot_id)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setPendingSlots(draft => draft.filter(s => s.slot_id !== slot.slot_id));
|
||||||
|
if (typeof onSlotRemoved === 'function') onSlotRemoved(slot);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build / validate the reservation at server-side, then add it to the cart.
|
* Build / validate the reservation at server-side, then add it to the cart.
|
||||||
*/
|
*/
|
||||||
@ -93,26 +134,42 @@ const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, custome
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the price of the given slot, if known
|
||||||
|
*/
|
||||||
|
const slotPrice = (slot: Slot): string => {
|
||||||
|
if (!price) return '';
|
||||||
|
|
||||||
|
const slotPrice = price.details.slots.find(s => s.slot_id === slot.slot_id);
|
||||||
|
if (!slotPrice) return '';
|
||||||
|
|
||||||
|
return FormatLib.price(slotPrice.price);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the total price for the current reservation
|
||||||
|
*/
|
||||||
|
const total = (): ReactNode => {
|
||||||
|
if (!price || reservation.reservation.slots_reservations_attributes.length === 0) return '';
|
||||||
|
|
||||||
|
return <span>TOTAL: {FormatLib.price(price?.price)}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ul>{pendingSlots.map(slot => (
|
<ul>{pendingSlots.map(slot => (
|
||||||
<li key={slot.slot_id}>
|
<li key={slot.slot_id}>
|
||||||
<span>{FormatLib.date(slot.start)} {FormatLib.time(slot.start)} - {FormatLib.time(slot.end)}</span>
|
<span>{FormatLib.date(slot.start)} {FormatLib.time(slot.start)} - {FormatLib.time(slot.end)}</span>
|
||||||
<label>offered? <Switch checked={offeredSlots.get(slot.slot_id)} onChange={offerSlot(slot)} /></label>
|
<label>offered? <Switch checked={offeredSlots.get(slot.slot_id) || false} onChange={offerSlot(slot)} /></label>
|
||||||
<FabButton onClick={addSlotToReservation(slot)}>validate this slot</FabButton>
|
<span className="price">{slotPrice(slot)}</span>
|
||||||
|
{!isSlotInReservation(slot) && <FabButton onClick={addSlotToReservation(slot)}>Ajouter à la réservation</FabButton>}
|
||||||
|
<FabButton onClick={removeSlot(slot)}>Enlever</FabButton>
|
||||||
</li>
|
</li>
|
||||||
))}</ul>
|
))}</ul>
|
||||||
|
{total()}
|
||||||
{reservation.reservation.slots_reservations_attributes.length > 0 && <div>
|
{reservation.reservation.slots_reservations_attributes.length > 0 && <div>
|
||||||
<FabButton onClick={addReservationToCart}><ShoppingCart size={24}/>Ajouter au panier</FabButton>
|
<FabButton onClick={addReservationToCart}><ShoppingCart size={24}/>Ajouter au panier</FabButton>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReservationsSummaryWrapper: React.FC<ReservationsSummaryProps> = (props) => (
|
|
||||||
<Loader>
|
|
||||||
<ReservationsSummary {...props} />
|
|
||||||
</Loader>
|
|
||||||
);
|
|
||||||
|
|
||||||
Application.Components.component('reservationsSummary', react2angular(ReservationsSummaryWrapper, ['slot', 'customer', 'reservableId', 'reservableType', 'onError']));
|
|
||||||
|
@ -15,7 +15,7 @@ interface MemberSelectProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the member select for manager.
|
* This component allows privileged users (managers/admins) to select a user on whose behalf to act.
|
||||||
*/
|
*/
|
||||||
export const MemberSelect: React.FC<MemberSelectProps> = ({ defaultUser, value, onSelected, noHeader, hasError }) => {
|
export const MemberSelect: React.FC<MemberSelectProps> = ({ defaultUser, value, onSelected, noHeader, hasError }) => {
|
||||||
const { t } = useTranslation('public');
|
const { t } = useTranslation('public');
|
||||||
|
@ -469,21 +469,23 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
|||||||
/**
|
/**
|
||||||
* Change the last selected slot's appearance to looks like 'added to cart'
|
* Change the last selected slot's appearance to looks like 'added to cart'
|
||||||
*/
|
*/
|
||||||
$scope.markSlotAsAdded = function () {
|
$scope.markSlotAsAdded = function (slot) {
|
||||||
$scope.selectedEvent.backgroundColor = FREE_SLOT_BORDER_COLOR;
|
let calSlot = { ...slot };
|
||||||
$scope.selectedEvent.oldTitle = $scope.selectedEvent.title;
|
calSlot.backgroundColor = FREE_SLOT_BORDER_COLOR;
|
||||||
$scope.selectedEvent.title = _t('app.logged.machines_reserve.i_reserve');
|
calSlot.oldTitle = $scope.selectedEvent.title;
|
||||||
updateEvents($scope.selectedEvent);
|
calSlot.title = _t('app.logged.machines_reserve.i_reserve');
|
||||||
|
updateEvents(calSlot);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the last selected slot's appearance to looks like 'never added to cart'
|
* Change the last selected slot's appearance to looks like 'never added to cart'
|
||||||
*/
|
*/
|
||||||
$scope.markSlotAsRemoved = function (slot) {
|
$scope.markSlotAsRemoved = function (slot) {
|
||||||
slot.backgroundColor = 'white';
|
let calSlot = { ...slot };
|
||||||
slot.borderColor = FREE_SLOT_BORDER_COLOR;
|
calSlot.backgroundColor = 'white';
|
||||||
slot.title = slot.oldTitle;
|
calSlot.borderColor = FREE_SLOT_BORDER_COLOR;
|
||||||
updateEvents(slot);
|
calSlot.title = calSlot.oldTitle;
|
||||||
|
updateEvents(calSlot);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -529,7 +529,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
|||||||
}
|
}
|
||||||
// slot is not in the cart, so we add it
|
// slot is not in the cart, so we add it
|
||||||
$scope.events.reserved.push($scope.slot);
|
$scope.events.reserved.push($scope.slot);
|
||||||
if (typeof $scope.onSlotAddedToCart === 'function') { $scope.onSlotAddedToCart(); }
|
if (typeof $scope.onSlotAddedToCart === 'function') { $scope.onSlotAddedToCart($scope.slot); }
|
||||||
} else {
|
} else {
|
||||||
// slot is in the cart, remove it
|
// slot is in the cart, remove it
|
||||||
$scope.removeSlot($scope.slot, index);
|
$scope.removeSlot($scope.slot, index);
|
||||||
|
@ -20,6 +20,13 @@ export default class UserLib {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current user has a privileged role
|
||||||
|
*/
|
||||||
|
hasPrivilegedRole = (): boolean => {
|
||||||
|
return (this.user?.role === 'admin' || this.user?.role === 'manager');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter social networks from the user's profile
|
* Filter social networks from the user's profile
|
||||||
*/
|
*/
|
||||||
|
@ -23,6 +23,7 @@ export interface ComputePriceResult {
|
|||||||
price_without_coupon: number,
|
price_without_coupon: number,
|
||||||
details?: {
|
details?: {
|
||||||
slots: Array<{
|
slots: Array<{
|
||||||
|
slot_id: number,
|
||||||
start_at: TDateISO,
|
start_at: TDateISO,
|
||||||
price: number,
|
price: number,
|
||||||
promo: boolean
|
promo: boolean
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
@import "variables/animations";
|
@import "variables/animations";
|
||||||
@import "variables/colors";
|
@import "variables/colors";
|
||||||
|
@import "variables/component";
|
||||||
@import "variables/decoration";
|
@import "variables/decoration";
|
||||||
@import "variables/layout";
|
@import "variables/layout";
|
||||||
@import "variables/typography";
|
@import "variables/typography";
|
||||||
|
@ -174,18 +174,9 @@
|
|||||||
|
|
||||||
aside {
|
aside {
|
||||||
& > div {
|
& > div {
|
||||||
margin-bottom: 3.2rem;
|
@include component-border;
|
||||||
padding: 1.6rem;
|
h3 {
|
||||||
background-color: var(--gray-soft-lightest);
|
@include component-title;
|
||||||
border: 1px solid var(--gray-soft-dark);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
h3,
|
|
||||||
.member-select-title {
|
|
||||||
margin: 0 0 2.4rem;
|
|
||||||
padding-bottom: 1.2rem;
|
|
||||||
border-bottom: 1px solid var(--gray-hard);
|
|
||||||
@include title-base;
|
|
||||||
color: var(--gray-hard-dark) !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.checkout {
|
.checkout {
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
.member-select {
|
.member-select {
|
||||||
|
@include component-border;
|
||||||
|
&-title {
|
||||||
|
@include component-title;
|
||||||
|
}
|
||||||
&.error {
|
&.error {
|
||||||
.select-input > div:first-of-type {
|
.select-input > div:first-of-type {
|
||||||
border-color: var(--alert);
|
border-color: var(--alert);
|
||||||
|
15
app/frontend/src/stylesheets/variables/component.scss
Normal file
15
app/frontend/src/stylesheets/variables/component.scss
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
@mixin component-border {
|
||||||
|
margin-bottom: 3.2rem;
|
||||||
|
padding: 1.6rem;
|
||||||
|
background-color: var(--gray-soft-lightest);
|
||||||
|
border: 1px solid var(--gray-soft-dark);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin component-title {
|
||||||
|
margin: 0 0 2.4rem;
|
||||||
|
padding-bottom: 1.2rem;
|
||||||
|
border-bottom: 1px solid var(--gray-hard);
|
||||||
|
@include title-base;
|
||||||
|
color: var(--gray-hard-dark) !important;
|
||||||
|
}
|
@ -31,9 +31,14 @@
|
|||||||
|
|
||||||
<div class="col-sm-12 col-md-12 col-lg-3">
|
<div class="col-sm-12 col-md-12 col-lg-3">
|
||||||
|
|
||||||
<div ng-if="isAuthorized(['admin', 'manager'])">
|
<ongoing-reservation-panel selected-slot="selectedEvent"
|
||||||
<select-member></select-member>
|
on-slot-added="markSlotAsAdded"
|
||||||
</div>
|
on-slot-removed="markSlotAsRemoved"
|
||||||
|
on-error="onError"
|
||||||
|
reservable-id="machine.id"
|
||||||
|
reservable-type="'Machine'"
|
||||||
|
operator="currentUser">
|
||||||
|
</ongoing-reservation-panel>
|
||||||
|
|
||||||
<packs-summary item="machine"
|
<packs-summary item="machine"
|
||||||
item-type="'Machine'"
|
item-type="'Machine'"
|
||||||
@ -45,7 +50,9 @@
|
|||||||
refresh="afterPaymentPromise">
|
refresh="afterPaymentPromise">
|
||||||
</packs-summary>
|
</packs-summary>
|
||||||
|
|
||||||
<reservations-summary slot="selectedEvent" customer="ctrl.member" on-error="onError" reservable-id="machine.id" reservable-type="'Machine'"></reservations-summary>
|
<div ng-if="isAuthorized(['admin', 'manager'])">
|
||||||
|
<select-member></select-member>
|
||||||
|
</div>
|
||||||
|
|
||||||
<cart slot="selectedEvent"
|
<cart slot="selectedEvent"
|
||||||
slot-selection-time="selectionTime"
|
slot-selection-time="selectionTime"
|
||||||
|
@ -67,4 +67,8 @@ class CartItem::EventReservation < CartItem::Reservation
|
|||||||
def type
|
def type
|
||||||
'event'
|
'event'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def total_tickets
|
||||||
|
(normal_tickets || 0) + (cart_item_event_reservation_tickets.map(&:booked).reduce(:+) || 0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
# A generic reservation added to the shopping cart
|
# A generic reservation added to the shopping cart
|
||||||
class CartItem::Reservation < CartItem::BaseItem
|
class CartItem::Reservation < CartItem::BaseItem
|
||||||
|
self.table_name = 'cart_item_reservations'
|
||||||
|
|
||||||
MINUTES_PER_HOUR = 60.0
|
MINUTES_PER_HOUR = 60.0
|
||||||
SECONDS_PER_MINUTE = 60.0
|
SECONDS_PER_MINUTE = 60.0
|
||||||
|
|
||||||
@ -104,20 +106,26 @@ class CartItem::Reservation < CartItem::BaseItem
|
|||||||
0
|
0
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
|
||||||
# Group the slots by date, if the extended_prices_in_same_day option is set to true
|
# Group the slots by date, if the extended_prices_in_same_day option is set to true
|
||||||
##
|
# @return Hash{Symbol => Array<CartItem::ReservationSlot>}
|
||||||
def grouped_slots
|
def grouped_slots
|
||||||
return { all: cart_item_reservation_slots } unless Setting.get('extended_prices_in_same_day')
|
return { all: cart_item_reservation_slots } unless Setting.get('extended_prices_in_same_day')
|
||||||
|
|
||||||
cart_item_reservation_slots.group_by { |slot| slot.slot[:start_at].to_date }
|
cart_item_reservation_slots.group_by { |slot| slot.slot[:start_at].to_date }
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
|
||||||
# Compute the price of a single slot, according to the list of applicable prices.
|
# Compute the price of a single slot, according to the list of applicable prices.
|
||||||
# @param prices {{ prices: Array<{price: Price, duration: number}> }} list of prices to use with the current reservation
|
# @param prices [Hash{Symbol => Array<Hash{Symbol => Number,Price}>}] list of prices to use with the current reservation,
|
||||||
# @see get_slot_price
|
# as returned by #get_slot_price
|
||||||
##
|
# @option prices [Array<Hash{Symbol => Number,Price}>}] :prices => {price: Price, duration: number}
|
||||||
|
# @param slot_reservation [CartItem::ReservationSlot]
|
||||||
|
# @param is_privileged [Boolean]
|
||||||
|
# @param options [Hash] optional parameters, allowing the following options:
|
||||||
|
# @option options [Array] :elements if provided the resulting price will be append into elements.slots
|
||||||
|
# @option options [Boolean] :has_credits true if the user still has credits for the given slot, false if not provided
|
||||||
|
# @option options [Boolean] :is_division false if the slot covers a full availability, true if it is a subdivision (default)
|
||||||
|
# @option options [Number] :prepaid_minutes number of remaining prepaid minutes for the customer
|
||||||
|
# @return [Float]
|
||||||
def get_slot_price_from_prices(prices, slot_reservation, is_privileged, options = {})
|
def get_slot_price_from_prices(prices, slot_reservation, is_privileged, options = {})
|
||||||
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||||
|
|
||||||
@ -134,19 +142,17 @@ class CartItem::Reservation < CartItem::BaseItem
|
|||||||
real_price
|
real_price
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
|
||||||
# Compute the price of a single slot, according to the base price and the ability for an admin
|
# Compute the price of a single slot, according to the base price and the ability for an admin
|
||||||
# to offer the slot.
|
# to offer the slot.
|
||||||
# @param hourly_rate {Number} base price of a slot
|
# @param hourly_rate [Float] base price of a slot
|
||||||
# @param slot_reservation {CartItem::ReservationSlot}
|
# @param slot_reservation [CartItem::ReservationSlot]
|
||||||
# @param is_privileged {Boolean} true if the current user has a privileged role (admin or manager)
|
# @param is_privileged [Boolean] true if the current user has a privileged role (admin or manager)
|
||||||
# @param [options] {Hash} optional parameters, allowing the following options:
|
# @param options [Hash] optional parameters, allowing the following options:
|
||||||
# - elements {Array} if provided the resulting price will be append into elements.slots
|
# @option options [Array] :elements if provided the resulting price will be append into elements.slots
|
||||||
# - has_credits {Boolean} true if the user still has credits for the given slot, false if not provided
|
# @option options [Boolean] :has_credits true if the user still has credits for the given slot, false if not provided
|
||||||
# - is_division {boolean} false if the slot covers a full availability, true if it is a subdivision (default)
|
# @option options [Boolean] :is_division false if the slot covers a full availability, true if it is a subdivision (default)
|
||||||
# - prepaid_minutes {Number} number of remaining prepaid minutes for the customer
|
# @option options [Number] :prepaid_minutes number of remaining prepaid minutes for the customer
|
||||||
# @return {Number} price of the slot
|
# @return [Float] price of the slot
|
||||||
##
|
|
||||||
def get_slot_price(hourly_rate, slot_reservation, is_privileged, options = {})
|
def get_slot_price(hourly_rate, slot_reservation, is_privileged, options = {})
|
||||||
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||||
|
|
||||||
@ -158,16 +164,12 @@ class CartItem::Reservation < CartItem::BaseItem
|
|||||||
else
|
else
|
||||||
slot_rate
|
slot_rate
|
||||||
end
|
end
|
||||||
# subtract free minutes from prepaid packs
|
|
||||||
if real_price.positive? && options[:prepaid][:minutes]&.positive?
|
real_price = handle_prepaid_pack_price(real_price, slot_minutes, slot_rate, options)
|
||||||
consumed = slot_minutes
|
|
||||||
consumed = options[:prepaid][:minutes] if slot_minutes > options[:prepaid][:minutes]
|
|
||||||
real_price = (Rational(slot_minutes - consumed) * (Rational(slot_rate) / Rational(MINUTES_PER_HOUR))).to_f
|
|
||||||
options[:prepaid][:minutes] -= consumed
|
|
||||||
end
|
|
||||||
|
|
||||||
unless options[:elements].nil?
|
unless options[:elements].nil?
|
||||||
options[:elements][:slots].push(
|
options[:elements][:slots].push(
|
||||||
|
slot_id: slot_reservation.slot_id,
|
||||||
start_at: slot_reservation.slot[:start_at],
|
start_at: slot_reservation.slot[:start_at],
|
||||||
price: real_price,
|
price: real_price,
|
||||||
promo: (slot_rate != hourly_rate)
|
promo: (slot_rate != hourly_rate)
|
||||||
@ -176,15 +178,35 @@ class CartItem::Reservation < CartItem::BaseItem
|
|||||||
real_price
|
real_price
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param price [Float, Integer]
|
||||||
|
# @param slot_minutes [Number]
|
||||||
|
# @param options [Hash] optional parameters, allowing the following options:
|
||||||
|
# @option options [Array] :elements if provided the resulting price will be append into elements.slots
|
||||||
|
# @option options [Boolean] :has_credits true if the user still has credits for the given slot, false if not provided
|
||||||
|
# @option options [Boolean] :is_division false if the slot covers a full availability, true if it is a subdivision (default)
|
||||||
|
# @option options [Number] :prepaid_minutes number of remaining prepaid minutes for the customer
|
||||||
|
# @return [Float, Integer]
|
||||||
|
def handle_prepaid_pack_price(price, slot_minutes, slot_rate, options)
|
||||||
|
return price unless price.positive? && options[:prepaid][:minutes]&.positive?
|
||||||
|
|
||||||
|
# subtract free minutes from prepaid packs
|
||||||
|
consumed = slot_minutes
|
||||||
|
consumed = options[:prepaid][:minutes] if slot_minutes > options[:prepaid][:minutes]
|
||||||
|
options[:prepaid][:minutes] -= consumed
|
||||||
|
(Rational(slot_minutes - consumed) * (Rational(slot_rate) / Rational(MINUTES_PER_HOUR))).to_f
|
||||||
|
end
|
||||||
|
|
||||||
# We determine the list of prices applicable to current reservation
|
# We determine the list of prices applicable to current reservation
|
||||||
# The longest available price is always used in priority.
|
# The longest available price is always used in priority.
|
||||||
# Eg. If the reservation is for 12 hours, and there are prices for 3 hours, 7 hours,
|
# Eg. If the reservation is for 12 hours, and there are prices for 3 hours, 7 hours,
|
||||||
# and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price twice (7+3+1+1 = 12).
|
# and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price twice (7+3+1+1 = 12).
|
||||||
# All these prices are returned to be applied to the reservation.
|
# All these prices are returned to be applied to the reservation.
|
||||||
|
# @param slots_reservations [Array<CartItem::ReservationSlot>]
|
||||||
|
# @return [Hash{Symbol => Array<Hash{Symbol => Number, Price}>}]
|
||||||
def applicable_prices(slots_reservations)
|
def applicable_prices(slots_reservations)
|
||||||
total_duration = slots_reservations.map do |slot|
|
total_duration = slots_reservations.map do |slot|
|
||||||
(slot.slot[:end_at].to_time - slot.slot[:start_at].to_time) / SECONDS_PER_MINUTE
|
(slot.slot[:end_at].to_time - slot.slot[:start_at].to_time) / SECONDS_PER_MINUTE
|
||||||
end.reduce(:+)
|
end.reduce(:+) || 0
|
||||||
rates = { prices: [] }
|
rates = { prices: [] }
|
||||||
|
|
||||||
remaining_duration = total_duration
|
remaining_duration = total_duration
|
||||||
|
@ -18,4 +18,39 @@ class CartItem::ReservationSlot < ApplicationRecord
|
|||||||
|
|
||||||
belongs_to :slot
|
belongs_to :slot
|
||||||
belongs_to :slots_reservation
|
belongs_to :slots_reservation
|
||||||
|
|
||||||
|
after_create :add_to_places_cache
|
||||||
|
after_update :remove_from_places_cache, if: :canceled?
|
||||||
|
|
||||||
|
before_destroy :remove_from_places_cache
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def add_to_places_cache
|
||||||
|
update_places_cache(:+)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_from_places_cache
|
||||||
|
update_places_cache(:-)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param operation [Symbol] :+ or :-
|
||||||
|
def update_places_cache(operation)
|
||||||
|
user_method = operation == :+ ? :add_users : :remove_users
|
||||||
|
if cart_item_type == 'CartItem::EventReservation'
|
||||||
|
Slots::PlacesCacheService.change_places(slot, 'Event', cart_item.event_id, cart_item.total_tickets, operation)
|
||||||
|
Slots::PlacesCacheService.send(user_method,
|
||||||
|
slot,
|
||||||
|
'Event',
|
||||||
|
cart_item.event_id,
|
||||||
|
[cart_item.customer_profile.user_id])
|
||||||
|
else
|
||||||
|
Slots::PlacesCacheService.change_places(slot, cart_item.reservable_type, cart_item.reservable_id, 1, operation)
|
||||||
|
Slots::PlacesCacheService.send(user_method,
|
||||||
|
slot,
|
||||||
|
cart_item.reservable_type,
|
||||||
|
cart_item.reservable_id,
|
||||||
|
[cart_item.customer_profile.user_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -22,6 +22,7 @@ class ShoppingCart
|
|||||||
end
|
end
|
||||||
|
|
||||||
# compute the price details of the current shopping cart
|
# compute the price details of the current shopping cart
|
||||||
|
# @return [Hash]
|
||||||
def total
|
def total
|
||||||
total_amount = 0
|
total_amount = 0
|
||||||
all_elements = { slots: [] }
|
all_elements = { slots: [] }
|
||||||
|
@ -12,17 +12,25 @@ class Slot < ApplicationRecord
|
|||||||
|
|
||||||
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy
|
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy
|
||||||
|
|
||||||
|
after_create_commit :create_places_cache
|
||||||
|
|
||||||
attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids, :current_user_pending_reservations_ids
|
attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids, :current_user_pending_reservations_ids
|
||||||
|
|
||||||
|
# @param reservable [Machine, Space, Training, Event, NilClass]
|
||||||
def full?(reservable = nil)
|
def full?(reservable = nil)
|
||||||
availability_places = availability.available_places_per_slot(reservable)
|
availability_places = availability.available_places_per_slot(reservable)
|
||||||
return false if availability_places.nil?
|
return false if availability_places.nil?
|
||||||
|
|
||||||
if reservable.nil?
|
reserved_places = if reservable.nil?
|
||||||
slots_reservations.where(canceled_at: nil).count >= availability_places
|
places.pluck('reserved_places').reduce(:+)
|
||||||
else
|
else
|
||||||
slots_reservations.includes(:reservation).where(canceled_at: nil).where('reservations.reservable': reservable).count >= availability_places
|
rp = places.detect do |p|
|
||||||
end
|
p['reservable_type'] == reservable.class.name && p['reservable_id'] == reservable&.id
|
||||||
|
end
|
||||||
|
rp['reserved_places']
|
||||||
|
end
|
||||||
|
|
||||||
|
reserved_places >= availability_places
|
||||||
end
|
end
|
||||||
|
|
||||||
def empty?(reservable = nil)
|
def empty?(reservable = nil)
|
||||||
@ -36,4 +44,10 @@ class Slot < ApplicationRecord
|
|||||||
def duration
|
def duration
|
||||||
(end_at - start_at).seconds
|
(end_at - start_at).seconds
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_places_cache
|
||||||
|
Slots::PlacesCacheService.refresh(self)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -7,11 +7,15 @@ class SlotsReservation < ApplicationRecord
|
|||||||
belongs_to :reservation
|
belongs_to :reservation
|
||||||
has_one :cart_item_reservation_slot, class_name: 'CartItem::ReservationSlot', dependent: :nullify
|
has_one :cart_item_reservation_slot, class_name: 'CartItem::ReservationSlot', dependent: :nullify
|
||||||
|
|
||||||
|
after_create :add_to_places_cache
|
||||||
after_update :set_ex_start_end_dates_attrs, if: :slot_changed?
|
after_update :set_ex_start_end_dates_attrs, if: :slot_changed?
|
||||||
after_update :notify_member_and_admin_slot_is_modified, if: :slot_changed?
|
after_update :notify_member_and_admin_slot_is_modified, if: :slot_changed?
|
||||||
|
|
||||||
after_update :notify_member_and_admin_slot_is_canceled, if: :canceled?
|
after_update :notify_member_and_admin_slot_is_canceled, if: :canceled?
|
||||||
after_update :update_event_nb_free_places, if: :canceled?
|
after_update :update_event_nb_free_places, if: :canceled?
|
||||||
|
after_update :remove_from_places_cache, if: :canceled?
|
||||||
|
|
||||||
|
before_destroy :remove_from_places_cache
|
||||||
|
|
||||||
def set_ex_start_end_dates_attrs
|
def set_ex_start_end_dates_attrs
|
||||||
update_columns(ex_start_at: previous_slot.start_at, ex_end_at: previous_slot.end_at) # rubocop:disable Rails/SkipsModelValidations
|
update_columns(ex_start_at: previous_slot.start_at, ex_end_at: previous_slot.end_at) # rubocop:disable Rails/SkipsModelValidations
|
||||||
@ -37,6 +41,29 @@ class SlotsReservation < ApplicationRecord
|
|||||||
reservation.update_event_nb_free_places
|
reservation.update_event_nb_free_places
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_to_places_cache
|
||||||
|
update_places_cache(:+)
|
||||||
|
Slots::PlacesCacheService.add_users(slot, reservation.reservable_type, reservation.reservable_id, [reservation.statistic_profile.user_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_from_places_cache
|
||||||
|
update_places_cache(:-)
|
||||||
|
Slots::PlacesCacheService.remove_users(slot, reservation.reservable_type, reservation.reservable_id, [reservation.statistic_profile.user_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param operation [Symbol] :+ or :-
|
||||||
|
def update_places_cache(operation)
|
||||||
|
if reservation.reservable_type == 'Event'
|
||||||
|
Slots::PlacesCacheService.change_places(slot,
|
||||||
|
reservation.reservable_type,
|
||||||
|
reservation.reservable_id,
|
||||||
|
reservation.total_booked_seats,
|
||||||
|
operation)
|
||||||
|
else
|
||||||
|
Slots::PlacesCacheService.change_places(slot, reservation.reservable_type, reservation.reservable_id, 1, operation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def notify_member_and_admin_slot_is_modified
|
def notify_member_and_admin_slot_is_modified
|
||||||
NotificationCenter.call type: 'notify_member_slot_is_modified',
|
NotificationCenter.call type: 'notify_member_slot_is_modified',
|
||||||
receiver: reservation.user,
|
receiver: reservation.user,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
# Provides helper methods checking reservation status of any availabilities
|
# Provides helper methods checking reservation status of any availabilities
|
||||||
class Availabilities::StatusService
|
class Availabilities::StatusService
|
||||||
|
# @param current_user_role [String]
|
||||||
def initialize(current_user_role)
|
def initialize(current_user_role)
|
||||||
@current_user_role = current_user_role
|
@current_user_role = current_user_role
|
||||||
@show_name = (%w[admin manager].include?(@current_user_role) || (current_user_role && Setting.get('display_name_enable')))
|
@show_name = (%w[admin manager].include?(@current_user_role) || (current_user_role && Setting.get('display_name_enable')))
|
||||||
@ -10,7 +11,7 @@ class Availabilities::StatusService
|
|||||||
# check that the provided slot is reserved for the given reservable (machine, training or space).
|
# check that the provided slot is reserved for the given reservable (machine, training or space).
|
||||||
# Mark it accordingly for display in the calendar
|
# Mark it accordingly for display in the calendar
|
||||||
# @param slot [Slot]
|
# @param slot [Slot]
|
||||||
# @param user [User]
|
# @param user [User] the customer
|
||||||
# @param reservables [Array<Machine, Space, Training, Event>]
|
# @param reservables [Array<Machine, Space, Training, Event>]
|
||||||
# @return [Slot]
|
# @return [Slot]
|
||||||
def slot_reserved_status(slot, user, reservables)
|
def slot_reserved_status(slot, user, reservables)
|
||||||
@ -19,24 +20,24 @@ class Availabilities::StatusService
|
|||||||
"#{reservables.map(&:class).map(&:name).uniq} , with slot #{slot.id}")
|
"#{reservables.map(&:class).map(&:name).uniq} , with slot #{slot.id}")
|
||||||
end
|
end
|
||||||
|
|
||||||
slots_reservations, user_slots_reservations = slots_reservations(slot.slots_reservations, reservables, user)
|
places = places(slot, reservables)
|
||||||
|
is_reserved = places.any? { |p| p['reserved_places'].positive? }
|
||||||
pending_reserv_slot_ids = slot.cart_item_reservation_slots.select('id').map(&:id)
|
is_reserved_by_user = is_reserved && places.select { |p| p['user_ids'].include?(user.id) }.length.positive?
|
||||||
pending_reservations, user_pending_reservations = pending_reservations(pending_reserv_slot_ids, reservables, user)
|
|
||||||
|
|
||||||
is_reserved = slots_reservations.count.positive? || pending_reservations.count.positive?
|
|
||||||
slot.is_reserved = is_reserved
|
slot.is_reserved = is_reserved
|
||||||
slot.title = slot_title(slots_reservations, user_slots_reservations, user_pending_reservations, reservables)
|
slot.title = slot_title(slot, is_reserved, is_reserved_by_user, reservables)
|
||||||
slot.can_modify = true if %w[admin manager].include?(@current_user_role) || is_reserved
|
slot.can_modify = true if %w[admin manager].include?(@current_user_role) || is_reserved
|
||||||
slot.current_user_slots_reservations_ids = user_slots_reservations.select('id').map(&:id)
|
if is_reserved_by_user
|
||||||
slot.current_user_pending_reservations_ids = user_pending_reservations.select('id').map(&:id)
|
user_reservations = Slots::ReservationsService.user_reservations(slot, user, reservables.first.class.name)
|
||||||
|
|
||||||
|
slot.current_user_slots_reservations_ids = user_reservations[:reservations].select('id').map(&:id)
|
||||||
|
slot.current_user_pending_reservations_ids = user_reservations[:pending].select('id').map(&:id)
|
||||||
|
end
|
||||||
slot
|
slot
|
||||||
end
|
end
|
||||||
|
|
||||||
# check that the provided ability is reserved by the given user
|
# check that the provided ability is reserved by the given user
|
||||||
# @param availability [Availability]
|
# @param availability [Availability]
|
||||||
# @param user [User]
|
# @param user [User] the customer
|
||||||
# @param reservables [Array<Machine, Space, Training, Event>]
|
# @param reservables [Array<Machine, Space, Training, Event>]
|
||||||
# @return [Availability]
|
# @return [Availability]
|
||||||
def availability_reserved_status(availability, user, reservables)
|
def availability_reserved_status(availability, user, reservables)
|
||||||
@ -45,85 +46,54 @@ class Availabilities::StatusService
|
|||||||
"#{reservables.map(&:class).map(&:name).uniq}, with availability #{availability.id}")
|
"#{reservables.map(&:class).map(&:name).uniq}, with availability #{availability.id}")
|
||||||
end
|
end
|
||||||
|
|
||||||
slots_reservations, user_slots_reservations = slots_reservations(availability.slots_reservations, reservables, user)
|
slots = availability.slots.map do |slot|
|
||||||
|
slot_reserved_status(slot, user, reservables)
|
||||||
|
end
|
||||||
|
|
||||||
pending_reserv_slot_ids = availability.joins(slots: :cart_item_reservation_slots)
|
availability.is_reserved = slots.any?(&:is_reserved)
|
||||||
.select('cart_item_reservation_slots.id as cirs_id')
|
availability.current_user_slots_reservations_ids = slots.map(&:current_user_slots_reservations_ids).flatten
|
||||||
pending_reservations, user_pending_reservations = pending_reservations(pending_reserv_slot_ids, reservables, user)
|
availability.current_user_pending_reservations_ids = slots.map(&:current_user_pending_reservations_ids).flatten
|
||||||
|
|
||||||
availability.is_reserved = slots_reservations.count.positive? || pending_reservations.count.positive?
|
|
||||||
availability.current_user_slots_reservations_ids = user_slots_reservations.select('id').map(&:id)
|
|
||||||
availability.current_user_pending_reservations_ids = user_pending_reservations.select('id').map(&:id)
|
|
||||||
availability
|
availability
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# @param slots_reservations [ActiveRecord::Relation<SlotsReservation>]
|
# @param slot [Slot]
|
||||||
# @param user_slots_reservations [ActiveRecord::Relation<SlotsReservation>] same as slots_reservations but filtered by the current user
|
|
||||||
# @param user_pending_reservations [ActiveRecord::Relation<CartItem::ReservationSlot>]
|
|
||||||
# @param reservables [Array<Machine, Space, Training, Event>]
|
# @param reservables [Array<Machine, Space, Training, Event>]
|
||||||
def slot_title(slots_reservations, user_slots_reservations, user_pending_reservations, reservables)
|
# @return [Array<Hash>]
|
||||||
|
def places(slot, reservables)
|
||||||
|
places = []
|
||||||
|
reservables.each do |reservable|
|
||||||
|
places.push(slot.places.detect { |p| p['reservable_type'] == reservable.class.name && p['reservable_id'] == reservable.id })
|
||||||
|
end
|
||||||
|
places
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param slot [Slot]
|
||||||
|
# @param is_reserved [Boolean]
|
||||||
|
# @param is_reserved_by_user [Boolean]
|
||||||
|
# @param reservables [Array<Machine, Space, Training, Event>]
|
||||||
|
def slot_title(slot, is_reserved, is_reserved_by_user, reservables)
|
||||||
name = reservables.map(&:name).join(', ')
|
name = reservables.map(&:name).join(', ')
|
||||||
if user_slots_reservations.count.zero? && slots_reservations.count.zero?
|
if !is_reserved && !is_reserved_by_user
|
||||||
name
|
name
|
||||||
elsif user_slots_reservations.count.zero? && slots_reservations.count.positive?
|
elsif is_reserved && !is_reserved_by_user
|
||||||
"#{name} #{@show_name ? "- #{slot_users_names(slots_reservations)}" : ''}"
|
"#{name} #{@show_name ? "- #{slot_users_names(slot, reservables)}" : ''}"
|
||||||
elsif user_pending_reservations.count.positive?
|
|
||||||
"#{name} - #{I18n.t('availabilities.reserving')}"
|
|
||||||
else
|
else
|
||||||
"#{name} - #{I18n.t('availabilities.i_ve_reserved')}"
|
"#{name} - #{I18n.t('availabilities.i_ve_reserved')}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param slots_reservations [ActiveRecord::Relation<SlotsReservation>]
|
# @param slot [Slot]
|
||||||
|
# @param reservables [Array<Machine, Space, Training, Event>]
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def slot_users_names(slots_reservations)
|
def slot_users_names(slot, reservables)
|
||||||
slots_reservations.map(&:reservation)
|
user_ids = slot.places
|
||||||
.map(&:user)
|
.select { |p| p['reservable_type'] == reservables.first.class.name && reservables.map(&:id).includes?(p['reservable_id']) }
|
||||||
.map { |u| u&.profile&.full_name || I18n.t('availabilities.deleted_user') }
|
.pluck('user_ids')
|
||||||
.join(', ')
|
.flatten
|
||||||
end
|
User.where(id: user_ids).includes(:profile)
|
||||||
|
.map { |u| u&.profile&.full_name || I18n.t('availabilities.deleted_user') }
|
||||||
# @param slot_ids [Array<number>]
|
.join(', ')
|
||||||
# @param reservables [Array<Machine, Space, Training, Event>]
|
|
||||||
# @param user [User]
|
|
||||||
# @return [Array<ActiveRecord::Relation<CartItem::ReservationSlot>>]
|
|
||||||
def pending_reservations(slot_ids, reservables, user)
|
|
||||||
reservable_types = reservables.map(&:class).map(&:name).uniq
|
|
||||||
if reservable_types.size > 1
|
|
||||||
raise TypeError("[Availabilities::StatusService#pending_reservations] reservables have differents types: #{reservable_types}")
|
|
||||||
end
|
|
||||||
|
|
||||||
relation = "cart_item_#{reservable_types.first&.downcase}_reservation"
|
|
||||||
table = reservable_types.first == 'Event' ? 'cart_item_event_reservations' : 'cart_item_reservations'
|
|
||||||
pending_reservations = CartItem::ReservationSlot.where(id: slot_ids)
|
|
||||||
.includes(relation.to_sym)
|
|
||||||
.where(table => { reservable_type: reservable_types })
|
|
||||||
.where(table => { reservable_id: reservables.map(&:id) })
|
|
||||||
|
|
||||||
user_pending_reservations = pending_reservations.where(table => { customer_profile_id: user&.invoicing_profile&.id })
|
|
||||||
|
|
||||||
[pending_reservations, user_pending_reservations]
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param slots_reservations [ActiveRecord::Relation<SlotsReservation>]
|
|
||||||
# @param reservables [Array<Machine, Space, Training, Event>]
|
|
||||||
# @param user [User]
|
|
||||||
# @return [Array<ActiveRecord::Relation<SlotsReservation>>]
|
|
||||||
def slots_reservations(slots_reservations, reservables, user)
|
|
||||||
reservable_types = reservables.map(&:class).map(&:name).uniq
|
|
||||||
if reservable_types.size > 1
|
|
||||||
raise TypeError("[Availabilities::StatusService#slot_reservations] reservables have differents types: #{reservable_types}")
|
|
||||||
end
|
|
||||||
|
|
||||||
reservations = slots_reservations.includes(:reservation)
|
|
||||||
.where('reservations.reservable_type': reservable_types)
|
|
||||||
.where('reservations.reservable_id': reservables.map(&:id))
|
|
||||||
.where('slots_reservations.canceled_at': nil)
|
|
||||||
|
|
||||||
user_slots_reservations = reservations.where('reservations.statistic_profile_id': user&.statistic_profile&.id)
|
|
||||||
|
|
||||||
[reservations, user_slots_reservations]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
134
app/services/slots/places_cache_service.rb
Normal file
134
app/services/slots/places_cache_service.rb
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Services around slots
|
||||||
|
module Slots; end
|
||||||
|
|
||||||
|
# Maintain the cache of reserved places for a slot
|
||||||
|
class Slots::PlacesCacheService
|
||||||
|
class << self
|
||||||
|
# @param slot [Slot]
|
||||||
|
def refresh(slot)
|
||||||
|
return if slot.nil?
|
||||||
|
|
||||||
|
reservables = case slot.availability.available_type
|
||||||
|
when 'machines'
|
||||||
|
slot.availability.machines
|
||||||
|
when 'training'
|
||||||
|
slot.availability.trainings
|
||||||
|
when 'space'
|
||||||
|
slot.availability.spaces
|
||||||
|
when 'event'
|
||||||
|
Event.where(id: slot.availability.event.id)
|
||||||
|
else
|
||||||
|
Rails.logger.warn "[Slots::PlacesCacheService#update] Availability #{slot.availability_id} with unknown " \
|
||||||
|
"type #{slot.availability.available_type}"
|
||||||
|
[nil]
|
||||||
|
end
|
||||||
|
browser = reservables.respond_to?(:find_each) ? :find_each : :each
|
||||||
|
places = []
|
||||||
|
reservables.try(browser) do |reservable|
|
||||||
|
reservations = Slots::ReservationsService.reservations(slot.slots_reservations, [reservable])
|
||||||
|
pending = Slots::ReservationsService.pending_reservations(slot.cart_item_reservation_slots.map(&:id), [reservable])
|
||||||
|
|
||||||
|
places.push({
|
||||||
|
reservable_type: reservable.class.name,
|
||||||
|
reservable_id: reservable.try(&:id),
|
||||||
|
reserved_places: (reservations[:reservations].count || 0) + (pending[:reservations].count || 0),
|
||||||
|
user_ids: reservations[:user_ids] + pending[:user_ids]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
slot.update(places: places)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param slot [Slot]
|
||||||
|
# @param reservable_type [String]
|
||||||
|
# @param reservable_id [Number]
|
||||||
|
# @param places [Number]
|
||||||
|
# @param operation [Symbol] :+ OR :-
|
||||||
|
def change_places(slot, reservable_type, reservable_id, places, operation = :+)
|
||||||
|
return if slot.nil?
|
||||||
|
|
||||||
|
ActiveRecord::Base.connection.execute <<-SQL.squish
|
||||||
|
with reservable_places as (
|
||||||
|
select ('{'||index-1||',reserved_places}')::text[] as path
|
||||||
|
,(place->>'reserved_places')::decimal as reserved_places
|
||||||
|
from slots
|
||||||
|
,jsonb_array_elements(places) with ordinality arr(place, index)
|
||||||
|
where place->>'reservable_type' = '#{reservable_type}'
|
||||||
|
and place->>'reservable_id' = '#{reservable_id}'
|
||||||
|
and id = #{slot.id}
|
||||||
|
)
|
||||||
|
update slots
|
||||||
|
set places = jsonb_set(places, reservable_places.path, (reservable_places.reserved_places #{operation} #{places})::varchar::jsonb, true)
|
||||||
|
from reservable_places
|
||||||
|
where id = #{slot.id};
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param slot [Slot]
|
||||||
|
# @param reservable_type [String]
|
||||||
|
# @param reservable_id [Number]
|
||||||
|
# @param user_ids [Array<Number>]
|
||||||
|
def remove_users(slot, reservable_type, reservable_id, user_ids)
|
||||||
|
return if slot.nil?
|
||||||
|
|
||||||
|
ActiveRecord::Base.connection.execute <<-SQL.squish
|
||||||
|
with users as (
|
||||||
|
select ('{'||index-1||',user_ids}')::text[] as path
|
||||||
|
,place->>'user_ids' as user_ids
|
||||||
|
from slots
|
||||||
|
,jsonb_array_elements(places) with ordinality arr(place, index)
|
||||||
|
where place->>'reservable_type' = '#{reservable_type}'
|
||||||
|
and place->>'reservable_id' = '#{reservable_id}'
|
||||||
|
and id = #{slot.id}
|
||||||
|
),
|
||||||
|
all_users as (
|
||||||
|
select (ids.id)::text::int as all_ids
|
||||||
|
from users
|
||||||
|
,jsonb_array_elements(users.user_ids) with ordinality ids(id, index)
|
||||||
|
),
|
||||||
|
remaining_users as (
|
||||||
|
SELECT array_to_json(array(SELECT unnest(array_agg(all_users.all_ids)) EXCEPT SELECT unnest('{#{user_ids.to_s.gsub(/\]| |\[|/, '')}}'::int[])))::jsonb as ids
|
||||||
|
from all_users
|
||||||
|
)
|
||||||
|
update slots
|
||||||
|
set places = jsonb_set(places, users.path, remaining_users.ids, false)
|
||||||
|
from users, remaining_users
|
||||||
|
where id = #{slot.id};
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param slot [Slot]
|
||||||
|
# @param reservable_type [String]
|
||||||
|
# @param reservable_id [Number]
|
||||||
|
# @param user_ids [Array<Number>]
|
||||||
|
def add_users(slot, reservable_type, reservable_id, user_ids)
|
||||||
|
return if slot.nil?
|
||||||
|
|
||||||
|
ActiveRecord::Base.connection.execute <<-SQL.squish
|
||||||
|
with users as (
|
||||||
|
select ('{'||index-1||',user_ids}')::text[] as path
|
||||||
|
,place->>'user_ids' as user_ids
|
||||||
|
from slots
|
||||||
|
,jsonb_array_elements(places) with ordinality arr(place, index)
|
||||||
|
where place->>'reservable_type' = '#{reservable_type}'
|
||||||
|
and place->>'reservable_id' = '#{reservable_id}'
|
||||||
|
and id = #{slot.id}
|
||||||
|
),
|
||||||
|
all_users as (
|
||||||
|
select (ids.id)::text::int as all_ids
|
||||||
|
from users
|
||||||
|
,jsonb_array_elements(users.user_ids::jsonb) with ordinality ids(id, index)
|
||||||
|
),
|
||||||
|
new_users as (
|
||||||
|
SELECT array_to_json(array_cat(array_agg(all_users.all_ids), '{#{user_ids.to_s.gsub(/\]| |\[|/, '')}}'::int[]))::jsonb as ids
|
||||||
|
from all_users
|
||||||
|
)
|
||||||
|
update slots
|
||||||
|
set places = jsonb_set(places, users.path, new_users.ids, false)
|
||||||
|
from users, new_users
|
||||||
|
where id = #{slot.id};
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
93
app/services/slots/reservations_service.rb
Normal file
93
app/services/slots/reservations_service.rb
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Services around slots
|
||||||
|
module Slots; end
|
||||||
|
|
||||||
|
# Check the reservation status of a slot
|
||||||
|
class Slots::ReservationsService
|
||||||
|
class << self
|
||||||
|
# @param slots_reservations [ActiveRecord::Relation<SlotsReservation>]
|
||||||
|
# @param reservables [Array<Machine, Space, Training, Event, NilClass>]
|
||||||
|
# @return [Hash{Symbol=>ActiveRecord::Relation<SlotsReservation>,Array<Integer>}]
|
||||||
|
def reservations(slots_reservations, reservables)
|
||||||
|
reservable_types = reservables.map(&:class).map(&:name).uniq
|
||||||
|
if reservable_types.size > 1
|
||||||
|
raise TypeError("[Availabilities::StatusService#slot_reservations] reservables have differents types: #{reservable_types}")
|
||||||
|
end
|
||||||
|
|
||||||
|
reservations = slots_reservations.includes(:reservation)
|
||||||
|
.where('reservations.reservable_type': reservable_types)
|
||||||
|
.where('reservations.reservable_id': reservables.map { |r| r.try(:id) })
|
||||||
|
.where('slots_reservations.canceled_at': nil)
|
||||||
|
|
||||||
|
user_ids = reservations.includes(reservation: :statistic_profile)
|
||||||
|
.map(&:reservation)
|
||||||
|
.map(&:statistic_profile)
|
||||||
|
.map(&:user_id)
|
||||||
|
.filter { |id| !id.nil? }
|
||||||
|
|
||||||
|
{
|
||||||
|
reservations: reservations,
|
||||||
|
user_ids: user_ids
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param cart_item_reservation_slot_ids [Array<number>]
|
||||||
|
# @param reservables [Array<Machine, Space, Training, Event, NilClass>]
|
||||||
|
# @return [Hash{Symbol=>ActiveRecord::Relation<CartItem::ReservationSlot>,Array<Integer>}]
|
||||||
|
def pending_reservations(cart_item_reservation_slot_ids, reservables)
|
||||||
|
reservable_types = reservables.map(&:class).map(&:name).uniq
|
||||||
|
if reservable_types.size > 1
|
||||||
|
raise TypeError("[Slots::StatusService#pending_reservations] reservables have differents types: #{reservable_types}")
|
||||||
|
end
|
||||||
|
|
||||||
|
relation = "cart_item_#{reservable_types.first&.downcase}_reservation".to_sym
|
||||||
|
pending_reservations = case reservable_types.first
|
||||||
|
when 'Event'
|
||||||
|
CartItem::ReservationSlot.where(id: cart_item_reservation_slot_ids)
|
||||||
|
.includes(relation)
|
||||||
|
.where(cart_item_event_reservations: { event_id: reservables.map(&:id) })
|
||||||
|
when 'NilClass'
|
||||||
|
[]
|
||||||
|
else
|
||||||
|
CartItem::ReservationSlot.where(id: cart_item_reservation_slot_ids)
|
||||||
|
.includes(relation)
|
||||||
|
.where(cart_item_reservations: { reservable_type: reservable_types })
|
||||||
|
.where(cart_item_reservations: { reservable_id: reservables.map { |r| r.try(:id) } })
|
||||||
|
end
|
||||||
|
|
||||||
|
user_ids = if reservable_types.first == 'NilClass'
|
||||||
|
[]
|
||||||
|
else
|
||||||
|
pending_reservations.includes(relation => :customer_profile)
|
||||||
|
.map(&:cart_item)
|
||||||
|
.map(&:customer_profile)
|
||||||
|
.map(&:user_id)
|
||||||
|
.filter { |id| !id.nil? }
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
reservations: pending_reservations,
|
||||||
|
user_ids: user_ids
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param slot [Slot]
|
||||||
|
# @param user [User]
|
||||||
|
# @param reservable_type [String] 'Machine' | 'Space' | 'Training' | 'Event'
|
||||||
|
# @return [Hash{Symbol=>ActiveRecord::Relation<SlotsReservation>,ActiveRecord::Relation<CartItem::ReservationSlot>}]
|
||||||
|
def user_reservations(slot, user, reservable_type)
|
||||||
|
reservations = SlotsReservation.includes(:reservation)
|
||||||
|
.where(slot_id: slot.id, reservations: { statistic_profile_id: user.statistic_profile.id })
|
||||||
|
relation = "cart_item_#{reservable_type&.downcase}_reservation".to_sym
|
||||||
|
table = (reservable_type == 'Event' ? 'cart_item_event_reservations' : 'cart_item_reservations').to_sym
|
||||||
|
pending = CartItem::ReservationSlot.includes(relation)
|
||||||
|
.where(slot_id: slot.id, table => { customer_profile_id: user.invoicing_profile.id })
|
||||||
|
|
||||||
|
{
|
||||||
|
reservations: reservations,
|
||||||
|
pending: pending
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -5,6 +5,7 @@ json.price_without_coupon @amount[:before_coupon] / 100.00
|
|||||||
if @amount[:elements]
|
if @amount[:elements]
|
||||||
json.details do
|
json.details do
|
||||||
json.slots @amount[:elements][:slots] do |slot|
|
json.slots @amount[:elements][:slots] do |slot|
|
||||||
|
json.slot_id slot[:slot_id]
|
||||||
json.start_at slot[:start_at]
|
json.start_at slot[:start_at]
|
||||||
json.price slot[:price] / 100.00
|
json.price slot[:price] / 100.00
|
||||||
json.promo slot[:promo]
|
json.promo slot[:promo]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# From this migration, ths object_type and object_id columns in InvoiceItem won't be able to be null anymore
|
# From this migration, the object_type and object_id columns in InvoiceItem won't be able to be null anymore
|
||||||
# This will prevent issues while building the accounting data, and ensure data integrity
|
# This will prevent issues while building the accounting data, and ensure data integrity
|
||||||
class AddNotNullToInvoiceItemsObject < ActiveRecord::Migration[5.2]
|
class AddNotNullToInvoiceItemsObject < ActiveRecord::Migration[5.2]
|
||||||
def change
|
def change
|
||||||
|
13
db/migrate/20230112151631_add_places_cache_to_slot.rb
Normal file
13
db/migrate/20230112151631_add_places_cache_to_slot.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# From this migration, we will save the number of reserved places for each slots, for each reservable
|
||||||
|
# This will improved performance because of computing the number of reserved seats on each request in very resource demanding
|
||||||
|
#
|
||||||
|
# The places field is a jsonb object, with the following structure:
|
||||||
|
# {reservable_type: string, reservable_id: number, reserved_places: number, user_ids: number[]}
|
||||||
|
class AddPlacesCacheToSlot < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :slots, :places, :jsonb, null: false, default: []
|
||||||
|
add_index :slots, :places, using: :gin
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# From this migration, the slot_id and reservation_id of the SlotsReservation won't be allowed to be null,
|
||||||
|
# otherwise this could result in error
|
||||||
|
class AddNotNullToSlotsReservation < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
change_column_null :slots_reservations, :slot_id, false
|
||||||
|
change_column_null :slots_reservations, :reservation_id, false
|
||||||
|
change_column_null :slots, :availability_id, false
|
||||||
|
end
|
||||||
|
end
|
18
db/schema.rb
18
db/schema.rb
@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
enable_extension "unaccent"
|
enable_extension "unaccent"
|
||||||
|
|
||||||
create_table "abuses", id: :serial, force: :cascade do |t|
|
create_table "abuses", id: :serial, force: :cascade do |t|
|
||||||
t.integer "signaled_id"
|
|
||||||
t.string "signaled_type"
|
t.string "signaled_type"
|
||||||
|
t.integer "signaled_id"
|
||||||
t.string "first_name"
|
t.string "first_name"
|
||||||
t.string "last_name"
|
t.string "last_name"
|
||||||
t.string "email"
|
t.string "email"
|
||||||
@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
t.string "locality"
|
t.string "locality"
|
||||||
t.string "country"
|
t.string "country"
|
||||||
t.string "postal_code"
|
t.string "postal_code"
|
||||||
t.integer "placeable_id"
|
|
||||||
t.string "placeable_type"
|
t.string "placeable_type"
|
||||||
|
t.integer "placeable_id"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
end
|
end
|
||||||
@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "assets", id: :serial, force: :cascade do |t|
|
create_table "assets", id: :serial, force: :cascade do |t|
|
||||||
t.integer "viewable_id"
|
|
||||||
t.string "viewable_type"
|
t.string "viewable_type"
|
||||||
|
t.integer "viewable_id"
|
||||||
t.string "attachment"
|
t.string "attachment"
|
||||||
t.string "type"
|
t.string "type"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
@ -281,8 +281,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "credits", id: :serial, force: :cascade do |t|
|
create_table "credits", id: :serial, force: :cascade do |t|
|
||||||
t.integer "creditable_id"
|
|
||||||
t.string "creditable_type"
|
t.string "creditable_type"
|
||||||
|
t.integer "creditable_id"
|
||||||
t.integer "plan_id"
|
t.integer "plan_id"
|
||||||
t.integer "hours"
|
t.integer "hours"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
@ -524,15 +524,15 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
|
|
||||||
create_table "notifications", id: :serial, force: :cascade do |t|
|
create_table "notifications", id: :serial, force: :cascade do |t|
|
||||||
t.integer "receiver_id"
|
t.integer "receiver_id"
|
||||||
t.integer "attached_object_id"
|
|
||||||
t.string "attached_object_type"
|
t.string "attached_object_type"
|
||||||
|
t.integer "attached_object_id"
|
||||||
t.integer "notification_type_id"
|
t.integer "notification_type_id"
|
||||||
t.boolean "is_read", default: false
|
t.boolean "is_read", default: false
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.string "receiver_type"
|
t.string "receiver_type"
|
||||||
t.boolean "is_send", default: false
|
t.boolean "is_send", default: false
|
||||||
t.jsonb "meta_data", default: {}
|
t.jsonb "meta_data", default: "{}"
|
||||||
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
|
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
|
||||||
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
||||||
end
|
end
|
||||||
@ -772,8 +772,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
create_table "prices", id: :serial, force: :cascade do |t|
|
create_table "prices", id: :serial, force: :cascade do |t|
|
||||||
t.integer "group_id"
|
t.integer "group_id"
|
||||||
t.integer "plan_id"
|
t.integer "plan_id"
|
||||||
t.integer "priceable_id"
|
|
||||||
t.string "priceable_type"
|
t.string "priceable_type"
|
||||||
|
t.integer "priceable_id"
|
||||||
t.integer "amount"
|
t.integer "amount"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
@ -976,8 +976,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
t.text "message"
|
t.text "message"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.integer "reservable_id"
|
|
||||||
t.string "reservable_type"
|
t.string "reservable_type"
|
||||||
|
t.integer "reservable_id"
|
||||||
t.integer "nb_reserve_places"
|
t.integer "nb_reserve_places"
|
||||||
t.integer "statistic_profile_id"
|
t.integer "statistic_profile_id"
|
||||||
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
|
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
|
||||||
@ -986,8 +986,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
|
|
||||||
create_table "roles", id: :serial, force: :cascade do |t|
|
create_table "roles", id: :serial, force: :cascade do |t|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.integer "resource_id"
|
|
||||||
t.string "resource_type"
|
t.string "resource_type"
|
||||||
|
t.integer "resource_id"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
|
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
|
||||||
|
@ -82,3 +82,4 @@ This is currently not supported, because of some PostgreSQL specific instruction
|
|||||||
- `db/migrate/20200623141305_update_search_vector_of_projects.rb` defines a PL/pgSQL function (`fill_search_vector_for_project()`) and create an SQL trigger for this function;
|
- `db/migrate/20200623141305_update_search_vector_of_projects.rb` defines a PL/pgSQL function (`fill_search_vector_for_project()`) and create an SQL trigger for this function;
|
||||||
- `db/migrate/20200629123011_update_pg_trgm.rb` is using [ALTER EXTENSION](https://www.postgresql.org/docs/10/sql-alterextension.html);
|
- `db/migrate/20200629123011_update_pg_trgm.rb` is using [ALTER EXTENSION](https://www.postgresql.org/docs/10/sql-alterextension.html);
|
||||||
- `db/migrate/20201027101809_create_payment_schedule_items.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html);
|
- `db/migrate/20201027101809_create_payment_schedule_items.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html);
|
||||||
|
- `db/migrate/20230112151631_add_places_cache_to_slot.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html);
|
||||||
|
88
lib/tasks/fablab/fix_availabilities.rake
Normal file
88
lib/tasks/fablab/fix_availabilities.rake
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# This take will ensure data integrity for availbilities.
|
||||||
|
# For an unknown reason, some slots are associated with unexisting availabilities. This script will try to re-create them
|
||||||
|
namespace :fablab do
|
||||||
|
desc 'regenerate missing availabilities'
|
||||||
|
task fix_availabilities: :environment do |_task, _args|
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
Slot.find_each do |slot|
|
||||||
|
next unless slot.availability.nil?
|
||||||
|
|
||||||
|
other_slots = Slot.where(availability_id: slot.availability_id)
|
||||||
|
reservations = SlotsReservation.where(slot_id: other_slots.map(&:id))
|
||||||
|
|
||||||
|
a = Availability.new(
|
||||||
|
id: slot.availability_id,
|
||||||
|
start_at: other_slots.group('id').select('min(start_at) as min').first[:min],
|
||||||
|
end_at: other_slots.group('id').select('max(end_at) as max').first[:max],
|
||||||
|
available_type: available_type(reservations),
|
||||||
|
machine_ids: machines_ids(reservations, slot.availability_id),
|
||||||
|
space_ids: space_ids(reservations, slot.availability_id),
|
||||||
|
training_ids: training_ids(reservations, slot.availability_id)
|
||||||
|
)
|
||||||
|
raise StandardError, "unable to save availability for slot #{slot.id}: #{a.errors.full_messages}" unless a.save(validate: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# @param reservations [ActiveRecord::Relation<SlotsReservation>]
|
||||||
|
def available_type(reservations)
|
||||||
|
return 'unknown' if reservations.count.zero?
|
||||||
|
|
||||||
|
type = reservations.first&.reservation&.reservable_type
|
||||||
|
case type
|
||||||
|
when 'Training', 'Space', 'Event'
|
||||||
|
type&.downcase
|
||||||
|
else
|
||||||
|
'machines'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param reservations [ActiveRecord::Relation<SlotsReservation>]
|
||||||
|
# @param availability_id [Number]
|
||||||
|
def machines_ids(reservations, availability_id)
|
||||||
|
type = reservations.first&.reservation&.reservable_type
|
||||||
|
return [] unless type == 'Machine'
|
||||||
|
|
||||||
|
ma = MachinesAvailability.where(availability_id: availability_id).map(&:machine_id)
|
||||||
|
return ma unless ma.empty?
|
||||||
|
|
||||||
|
rv = reservations.map(&:reservation).map(&:reservable_id)
|
||||||
|
return rv unless rv.empty?
|
||||||
|
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param reservations [ActiveRecord::Relation<SlotsReservation>]
|
||||||
|
# @param availability_id [Number]
|
||||||
|
def space_ids(reservations, availability_id)
|
||||||
|
type = reservations.first&.reservation&.reservable_type
|
||||||
|
return [] unless type == 'Space'
|
||||||
|
|
||||||
|
sa = SpacesAvailability.where(availability_id: availability_id).map(&:machine_id)
|
||||||
|
return sa unless sa.empty?
|
||||||
|
|
||||||
|
rv = reservations.map(&:reservation).map(&:reservable_id)
|
||||||
|
return rv unless rv.empty?
|
||||||
|
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param reservations [ActiveRecord::Relation<SlotsReservation>]
|
||||||
|
# @param availability_id [Number]
|
||||||
|
def training_ids(reservations, availability_id)
|
||||||
|
type = reservations.first&.reservation&.reservable_type
|
||||||
|
return [] unless type == 'Training'
|
||||||
|
|
||||||
|
ta = TrainingsAvailability.where(availability_id: availability_id).map(&:machine_id)
|
||||||
|
return ta unless ta.empty?
|
||||||
|
|
||||||
|
rv = reservations.map(&:reservation).map(&:reservable_id)
|
||||||
|
return rv unless rv.empty?
|
||||||
|
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
@ -134,6 +134,17 @@ namespace :fablab do
|
|||||||
puts '-> Done'
|
puts '-> Done'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc 'build the reserved places cache for all slots'
|
||||||
|
task build_places_cache: :environment do
|
||||||
|
puts 'Builing the places cache. This may take some time...'
|
||||||
|
total = Slot.maximum(:id)
|
||||||
|
Slot.order(id: :asc).find_each do |slot|
|
||||||
|
puts "#{slot.id} / #{total}"
|
||||||
|
Slots::PlacesCacheService.refresh(slot)
|
||||||
|
end
|
||||||
|
puts '-> Done'
|
||||||
|
end
|
||||||
|
|
||||||
def select_group(groups)
|
def select_group(groups)
|
||||||
groups.each do |g|
|
groups.each do |g|
|
||||||
print "#{g.id}) #{g.name}\n"
|
print "#{g.id}) #{g.name}\n"
|
||||||
|
4
test/fixtures/reservations.yml
vendored
4
test/fixtures/reservations.yml
vendored
@ -5,7 +5,7 @@ reservation_1:
|
|||||||
message:
|
message:
|
||||||
created_at: 2012-03-12 11:03:31.651441000 Z
|
created_at: 2012-03-12 11:03:31.651441000 Z
|
||||||
updated_at: 2012-03-12 11:03:31.651441000 Z
|
updated_at: 2012-03-12 11:03:31.651441000 Z
|
||||||
reservable_id: 2
|
reservable_id: 3
|
||||||
reservable_type: Training
|
reservable_type: Training
|
||||||
nb_reserve_places:
|
nb_reserve_places:
|
||||||
|
|
||||||
@ -15,6 +15,6 @@ reservation_2:
|
|||||||
message:
|
message:
|
||||||
created_at: 2015-06-10 11:20:01.341130000 Z
|
created_at: 2015-06-10 11:20:01.341130000 Z
|
||||||
updated_at: 2015-06-10 11:20:01.341130000 Z
|
updated_at: 2015-06-10 11:20:01.341130000 Z
|
||||||
reservable_id: 4
|
reservable_id: 2
|
||||||
reservable_type: Machine
|
reservable_type: Machine
|
||||||
nb_reserve_places:
|
nb_reserve_places:
|
||||||
|
73
test/fixtures/slots.yml
vendored
73
test/fixtures/slots.yml
vendored
@ -6,6 +6,7 @@ slot_1:
|
|||||||
created_at: '2012-03-12 13:40:22.342717'
|
created_at: '2012-03-12 13:40:22.342717'
|
||||||
updated_at: '2012-03-12 13:40:22.342717'
|
updated_at: '2012-03-12 13:40:22.342717'
|
||||||
availability_id: 12
|
availability_id: 12
|
||||||
|
places: [{"user_ids": [7], "reservable_id": 3, "reservable_type": "Training", "reserved_places": 1}]
|
||||||
|
|
||||||
slot_2:
|
slot_2:
|
||||||
id: 2
|
id: 2
|
||||||
@ -14,6 +15,7 @@ slot_2:
|
|||||||
created_at: '2015-06-10 11:20:01.341130'
|
created_at: '2015-06-10 11:20:01.341130'
|
||||||
updated_at: '2015-06-10 11:20:01.341130'
|
updated_at: '2015-06-10 11:20:01.341130'
|
||||||
availability_id: 13
|
availability_id: 13
|
||||||
|
places: [{"user_ids": [3], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 1}]
|
||||||
|
|
||||||
slot_9:
|
slot_9:
|
||||||
id: 9
|
id: 9
|
||||||
@ -22,6 +24,7 @@ slot_9:
|
|||||||
created_at: '2022-07-12 15:18:43.880751'
|
created_at: '2022-07-12 15:18:43.880751'
|
||||||
updated_at: '2022-07-12 15:18:43.880751'
|
updated_at: '2022-07-12 15:18:43.880751'
|
||||||
availability_id: 3
|
availability_id: 3
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_10:
|
slot_10:
|
||||||
id: 10
|
id: 10
|
||||||
@ -30,6 +33,7 @@ slot_10:
|
|||||||
created_at: '2022-07-12 15:18:43.882957'
|
created_at: '2022-07-12 15:18:43.882957'
|
||||||
updated_at: '2022-07-12 15:18:43.882957'
|
updated_at: '2022-07-12 15:18:43.882957'
|
||||||
availability_id: 3
|
availability_id: 3
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_11:
|
slot_11:
|
||||||
id: 11
|
id: 11
|
||||||
@ -38,6 +42,7 @@ slot_11:
|
|||||||
created_at: '2022-07-12 15:18:43.884691'
|
created_at: '2022-07-12 15:18:43.884691'
|
||||||
updated_at: '2022-07-12 15:18:43.884691'
|
updated_at: '2022-07-12 15:18:43.884691'
|
||||||
availability_id: 3
|
availability_id: 3
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_12:
|
slot_12:
|
||||||
id: 12
|
id: 12
|
||||||
@ -46,6 +51,7 @@ slot_12:
|
|||||||
created_at: '2022-07-12 15:18:43.886431'
|
created_at: '2022-07-12 15:18:43.886431'
|
||||||
updated_at: '2022-07-12 15:18:43.886431'
|
updated_at: '2022-07-12 15:18:43.886431'
|
||||||
availability_id: 3
|
availability_id: 3
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_13:
|
slot_13:
|
||||||
id: 13
|
id: 13
|
||||||
@ -54,6 +60,7 @@ slot_13:
|
|||||||
created_at: '2022-07-12 15:18:43.888074'
|
created_at: '2022-07-12 15:18:43.888074'
|
||||||
updated_at: '2022-07-12 15:18:43.888074'
|
updated_at: '2022-07-12 15:18:43.888074'
|
||||||
availability_id: 3
|
availability_id: 3
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_14:
|
slot_14:
|
||||||
id: 14
|
id: 14
|
||||||
@ -62,6 +69,7 @@ slot_14:
|
|||||||
created_at: '2022-07-12 15:18:43.889691'
|
created_at: '2022-07-12 15:18:43.889691'
|
||||||
updated_at: '2022-07-12 15:18:43.889691'
|
updated_at: '2022-07-12 15:18:43.889691'
|
||||||
availability_id: 3
|
availability_id: 3
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_15:
|
slot_15:
|
||||||
id: 15
|
id: 15
|
||||||
@ -70,6 +78,7 @@ slot_15:
|
|||||||
created_at: '2022-07-12 15:18:43.893096'
|
created_at: '2022-07-12 15:18:43.893096'
|
||||||
updated_at: '2022-07-12 15:18:43.893096'
|
updated_at: '2022-07-12 15:18:43.893096'
|
||||||
availability_id: 4
|
availability_id: 4
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_16:
|
slot_16:
|
||||||
id: 16
|
id: 16
|
||||||
@ -78,6 +87,7 @@ slot_16:
|
|||||||
created_at: '2022-07-12 15:18:43.894777'
|
created_at: '2022-07-12 15:18:43.894777'
|
||||||
updated_at: '2022-07-12 15:18:43.894777'
|
updated_at: '2022-07-12 15:18:43.894777'
|
||||||
availability_id: 4
|
availability_id: 4
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_17:
|
slot_17:
|
||||||
id: 17
|
id: 17
|
||||||
@ -86,6 +96,7 @@ slot_17:
|
|||||||
created_at: '2022-07-12 15:18:43.896423'
|
created_at: '2022-07-12 15:18:43.896423'
|
||||||
updated_at: '2022-07-12 15:18:43.896423'
|
updated_at: '2022-07-12 15:18:43.896423'
|
||||||
availability_id: 4
|
availability_id: 4
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_18:
|
slot_18:
|
||||||
id: 18
|
id: 18
|
||||||
@ -94,6 +105,7 @@ slot_18:
|
|||||||
created_at: '2022-07-12 15:18:43.898021'
|
created_at: '2022-07-12 15:18:43.898021'
|
||||||
updated_at: '2022-07-12 15:18:43.898021'
|
updated_at: '2022-07-12 15:18:43.898021'
|
||||||
availability_id: 4
|
availability_id: 4
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_19:
|
slot_19:
|
||||||
id: 19
|
id: 19
|
||||||
@ -102,6 +114,7 @@ slot_19:
|
|||||||
created_at: '2022-07-12 15:18:43.899592'
|
created_at: '2022-07-12 15:18:43.899592'
|
||||||
updated_at: '2022-07-12 15:18:43.899592'
|
updated_at: '2022-07-12 15:18:43.899592'
|
||||||
availability_id: 4
|
availability_id: 4
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_20:
|
slot_20:
|
||||||
id: 20
|
id: 20
|
||||||
@ -110,6 +123,7 @@ slot_20:
|
|||||||
created_at: '2022-07-12 15:18:43.900938'
|
created_at: '2022-07-12 15:18:43.900938'
|
||||||
updated_at: '2022-07-12 15:18:43.900938'
|
updated_at: '2022-07-12 15:18:43.900938'
|
||||||
availability_id: 4
|
availability_id: 4
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_21:
|
slot_21:
|
||||||
id: 21
|
id: 21
|
||||||
@ -118,6 +132,7 @@ slot_21:
|
|||||||
created_at: '2022-07-12 15:18:43.904013'
|
created_at: '2022-07-12 15:18:43.904013'
|
||||||
updated_at: '2022-07-12 15:18:43.904013'
|
updated_at: '2022-07-12 15:18:43.904013'
|
||||||
availability_id: 5
|
availability_id: 5
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_22:
|
slot_22:
|
||||||
id: 22
|
id: 22
|
||||||
@ -126,6 +141,7 @@ slot_22:
|
|||||||
created_at: '2022-07-12 15:18:43.905470'
|
created_at: '2022-07-12 15:18:43.905470'
|
||||||
updated_at: '2022-07-12 15:18:43.905470'
|
updated_at: '2022-07-12 15:18:43.905470'
|
||||||
availability_id: 5
|
availability_id: 5
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_23:
|
slot_23:
|
||||||
id: 23
|
id: 23
|
||||||
@ -134,6 +150,7 @@ slot_23:
|
|||||||
created_at: '2022-07-12 15:18:43.907030'
|
created_at: '2022-07-12 15:18:43.907030'
|
||||||
updated_at: '2022-07-12 15:18:43.907030'
|
updated_at: '2022-07-12 15:18:43.907030'
|
||||||
availability_id: 5
|
availability_id: 5
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_24:
|
slot_24:
|
||||||
id: 24
|
id: 24
|
||||||
@ -142,6 +159,7 @@ slot_24:
|
|||||||
created_at: '2022-07-12 15:18:43.908585'
|
created_at: '2022-07-12 15:18:43.908585'
|
||||||
updated_at: '2022-07-12 15:18:43.908585'
|
updated_at: '2022-07-12 15:18:43.908585'
|
||||||
availability_id: 5
|
availability_id: 5
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_25:
|
slot_25:
|
||||||
id: 25
|
id: 25
|
||||||
@ -150,6 +168,7 @@ slot_25:
|
|||||||
created_at: '2022-07-12 15:18:43.910138'
|
created_at: '2022-07-12 15:18:43.910138'
|
||||||
updated_at: '2022-07-12 15:18:43.910138'
|
updated_at: '2022-07-12 15:18:43.910138'
|
||||||
availability_id: 5
|
availability_id: 5
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_26:
|
slot_26:
|
||||||
id: 26
|
id: 26
|
||||||
@ -158,6 +177,7 @@ slot_26:
|
|||||||
created_at: '2022-07-12 15:18:43.911643'
|
created_at: '2022-07-12 15:18:43.911643'
|
||||||
updated_at: '2022-07-12 15:18:43.911643'
|
updated_at: '2022-07-12 15:18:43.911643'
|
||||||
availability_id: 5
|
availability_id: 5
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_27:
|
slot_27:
|
||||||
id: 27
|
id: 27
|
||||||
@ -166,6 +186,7 @@ slot_27:
|
|||||||
created_at: '2022-07-12 15:18:43.914664'
|
created_at: '2022-07-12 15:18:43.914664'
|
||||||
updated_at: '2022-07-12 15:18:43.914664'
|
updated_at: '2022-07-12 15:18:43.914664'
|
||||||
availability_id: 6
|
availability_id: 6
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_28:
|
slot_28:
|
||||||
id: 28
|
id: 28
|
||||||
@ -174,6 +195,7 @@ slot_28:
|
|||||||
created_at: '2022-07-12 15:18:43.916047'
|
created_at: '2022-07-12 15:18:43.916047'
|
||||||
updated_at: '2022-07-12 15:18:43.916047'
|
updated_at: '2022-07-12 15:18:43.916047'
|
||||||
availability_id: 6
|
availability_id: 6
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_29:
|
slot_29:
|
||||||
id: 29
|
id: 29
|
||||||
@ -182,6 +204,7 @@ slot_29:
|
|||||||
created_at: '2022-07-12 15:18:43.917304'
|
created_at: '2022-07-12 15:18:43.917304'
|
||||||
updated_at: '2022-07-12 15:18:43.917304'
|
updated_at: '2022-07-12 15:18:43.917304'
|
||||||
availability_id: 6
|
availability_id: 6
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_30:
|
slot_30:
|
||||||
id: 30
|
id: 30
|
||||||
@ -190,6 +213,7 @@ slot_30:
|
|||||||
created_at: '2022-07-12 15:18:43.918798'
|
created_at: '2022-07-12 15:18:43.918798'
|
||||||
updated_at: '2022-07-12 15:18:43.918798'
|
updated_at: '2022-07-12 15:18:43.918798'
|
||||||
availability_id: 6
|
availability_id: 6
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_31:
|
slot_31:
|
||||||
id: 31
|
id: 31
|
||||||
@ -198,6 +222,7 @@ slot_31:
|
|||||||
created_at: '2022-07-12 15:18:43.920194'
|
created_at: '2022-07-12 15:18:43.920194'
|
||||||
updated_at: '2022-07-12 15:18:43.920194'
|
updated_at: '2022-07-12 15:18:43.920194'
|
||||||
availability_id: 6
|
availability_id: 6
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_32:
|
slot_32:
|
||||||
id: 32
|
id: 32
|
||||||
@ -206,6 +231,7 @@ slot_32:
|
|||||||
created_at: '2022-07-12 15:18:43.921662'
|
created_at: '2022-07-12 15:18:43.921662'
|
||||||
updated_at: '2022-07-12 15:18:43.921662'
|
updated_at: '2022-07-12 15:18:43.921662'
|
||||||
availability_id: 6
|
availability_id: 6
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_33:
|
slot_33:
|
||||||
id: 33
|
id: 33
|
||||||
@ -214,6 +240,7 @@ slot_33:
|
|||||||
created_at: '2022-07-12 15:18:43.924285'
|
created_at: '2022-07-12 15:18:43.924285'
|
||||||
updated_at: '2022-07-12 15:18:43.924285'
|
updated_at: '2022-07-12 15:18:43.924285'
|
||||||
availability_id: 7
|
availability_id: 7
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_34:
|
slot_34:
|
||||||
id: 34
|
id: 34
|
||||||
@ -222,6 +249,7 @@ slot_34:
|
|||||||
created_at: '2022-07-12 15:18:43.925669'
|
created_at: '2022-07-12 15:18:43.925669'
|
||||||
updated_at: '2022-07-12 15:18:43.925669'
|
updated_at: '2022-07-12 15:18:43.925669'
|
||||||
availability_id: 7
|
availability_id: 7
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_35:
|
slot_35:
|
||||||
id: 35
|
id: 35
|
||||||
@ -230,6 +258,7 @@ slot_35:
|
|||||||
created_at: '2022-07-12 15:18:43.927038'
|
created_at: '2022-07-12 15:18:43.927038'
|
||||||
updated_at: '2022-07-12 15:18:43.927038'
|
updated_at: '2022-07-12 15:18:43.927038'
|
||||||
availability_id: 7
|
availability_id: 7
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_36:
|
slot_36:
|
||||||
id: 36
|
id: 36
|
||||||
@ -238,6 +267,7 @@ slot_36:
|
|||||||
created_at: '2022-07-12 15:18:43.928407'
|
created_at: '2022-07-12 15:18:43.928407'
|
||||||
updated_at: '2022-07-12 15:18:43.928407'
|
updated_at: '2022-07-12 15:18:43.928407'
|
||||||
availability_id: 7
|
availability_id: 7
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_37:
|
slot_37:
|
||||||
id: 37
|
id: 37
|
||||||
@ -246,6 +276,7 @@ slot_37:
|
|||||||
created_at: '2022-07-12 15:18:43.929907'
|
created_at: '2022-07-12 15:18:43.929907'
|
||||||
updated_at: '2022-07-12 15:18:43.929907'
|
updated_at: '2022-07-12 15:18:43.929907'
|
||||||
availability_id: 7
|
availability_id: 7
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_38:
|
slot_38:
|
||||||
id: 38
|
id: 38
|
||||||
@ -254,6 +285,7 @@ slot_38:
|
|||||||
created_at: '2022-07-12 15:18:43.931295'
|
created_at: '2022-07-12 15:18:43.931295'
|
||||||
updated_at: '2022-07-12 15:18:43.931295'
|
updated_at: '2022-07-12 15:18:43.931295'
|
||||||
availability_id: 7
|
availability_id: 7
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_39:
|
slot_39:
|
||||||
id: 39
|
id: 39
|
||||||
@ -262,6 +294,7 @@ slot_39:
|
|||||||
created_at: '2022-07-12 15:18:43.934465'
|
created_at: '2022-07-12 15:18:43.934465'
|
||||||
updated_at: '2022-07-12 15:18:43.934465'
|
updated_at: '2022-07-12 15:18:43.934465'
|
||||||
availability_id: 13
|
availability_id: 13
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_40:
|
slot_40:
|
||||||
id: 40
|
id: 40
|
||||||
@ -270,6 +303,7 @@ slot_40:
|
|||||||
created_at: '2022-07-12 15:18:43.935716'
|
created_at: '2022-07-12 15:18:43.935716'
|
||||||
updated_at: '2022-07-12 15:18:43.935716'
|
updated_at: '2022-07-12 15:18:43.935716'
|
||||||
availability_id: 13
|
availability_id: 13
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_41:
|
slot_41:
|
||||||
id: 41
|
id: 41
|
||||||
@ -278,6 +312,7 @@ slot_41:
|
|||||||
created_at: '2022-07-12 15:18:43.937025'
|
created_at: '2022-07-12 15:18:43.937025'
|
||||||
updated_at: '2022-07-12 15:18:43.937025'
|
updated_at: '2022-07-12 15:18:43.937025'
|
||||||
availability_id: 13
|
availability_id: 13
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_42:
|
slot_42:
|
||||||
id: 42
|
id: 42
|
||||||
@ -286,6 +321,7 @@ slot_42:
|
|||||||
created_at: '2022-07-12 15:18:43.938379'
|
created_at: '2022-07-12 15:18:43.938379'
|
||||||
updated_at: '2022-07-12 15:18:43.938379'
|
updated_at: '2022-07-12 15:18:43.938379'
|
||||||
availability_id: 13
|
availability_id: 13
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_43:
|
slot_43:
|
||||||
id: 43
|
id: 43
|
||||||
@ -294,6 +330,7 @@ slot_43:
|
|||||||
created_at: '2022-07-12 15:18:43.939737'
|
created_at: '2022-07-12 15:18:43.939737'
|
||||||
updated_at: '2022-07-12 15:18:43.939737'
|
updated_at: '2022-07-12 15:18:43.939737'
|
||||||
availability_id: 13
|
availability_id: 13
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_44:
|
slot_44:
|
||||||
id: 44
|
id: 44
|
||||||
@ -302,6 +339,7 @@ slot_44:
|
|||||||
created_at: '2022-07-12 15:18:43.942392'
|
created_at: '2022-07-12 15:18:43.942392'
|
||||||
updated_at: '2022-07-12 15:18:43.942392'
|
updated_at: '2022-07-12 15:18:43.942392'
|
||||||
availability_id: 14
|
availability_id: 14
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_45:
|
slot_45:
|
||||||
id: 45
|
id: 45
|
||||||
@ -310,6 +348,7 @@ slot_45:
|
|||||||
created_at: '2022-07-12 15:18:43.943779'
|
created_at: '2022-07-12 15:18:43.943779'
|
||||||
updated_at: '2022-07-12 15:18:43.943779'
|
updated_at: '2022-07-12 15:18:43.943779'
|
||||||
availability_id: 14
|
availability_id: 14
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_46:
|
slot_46:
|
||||||
id: 46
|
id: 46
|
||||||
@ -318,6 +357,7 @@ slot_46:
|
|||||||
created_at: '2022-07-12 15:18:43.945154'
|
created_at: '2022-07-12 15:18:43.945154'
|
||||||
updated_at: '2022-07-12 15:18:43.945154'
|
updated_at: '2022-07-12 15:18:43.945154'
|
||||||
availability_id: 14
|
availability_id: 14
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_47:
|
slot_47:
|
||||||
id: 47
|
id: 47
|
||||||
@ -326,6 +366,7 @@ slot_47:
|
|||||||
created_at: '2022-07-12 15:18:43.946515'
|
created_at: '2022-07-12 15:18:43.946515'
|
||||||
updated_at: '2022-07-12 15:18:43.946515'
|
updated_at: '2022-07-12 15:18:43.946515'
|
||||||
availability_id: 14
|
availability_id: 14
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_48:
|
slot_48:
|
||||||
id: 48
|
id: 48
|
||||||
@ -334,6 +375,7 @@ slot_48:
|
|||||||
created_at: '2022-07-12 15:18:43.949178'
|
created_at: '2022-07-12 15:18:43.949178'
|
||||||
updated_at: '2022-07-12 15:18:43.949178'
|
updated_at: '2022-07-12 15:18:43.949178'
|
||||||
availability_id: 15
|
availability_id: 15
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_49:
|
slot_49:
|
||||||
id: 49
|
id: 49
|
||||||
@ -342,6 +384,7 @@ slot_49:
|
|||||||
created_at: '2022-07-12 15:18:43.950348'
|
created_at: '2022-07-12 15:18:43.950348'
|
||||||
updated_at: '2022-07-12 15:18:43.950348'
|
updated_at: '2022-07-12 15:18:43.950348'
|
||||||
availability_id: 15
|
availability_id: 15
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_50:
|
slot_50:
|
||||||
id: 50
|
id: 50
|
||||||
@ -350,6 +393,7 @@ slot_50:
|
|||||||
created_at: '2022-07-12 15:18:43.951535'
|
created_at: '2022-07-12 15:18:43.951535'
|
||||||
updated_at: '2022-07-12 15:18:43.951535'
|
updated_at: '2022-07-12 15:18:43.951535'
|
||||||
availability_id: 15
|
availability_id: 15
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_51:
|
slot_51:
|
||||||
id: 51
|
id: 51
|
||||||
@ -358,6 +402,7 @@ slot_51:
|
|||||||
created_at: '2022-07-12 15:18:43.952864'
|
created_at: '2022-07-12 15:18:43.952864'
|
||||||
updated_at: '2022-07-12 15:18:43.952864'
|
updated_at: '2022-07-12 15:18:43.952864'
|
||||||
availability_id: 15
|
availability_id: 15
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_52:
|
slot_52:
|
||||||
id: 52
|
id: 52
|
||||||
@ -366,6 +411,7 @@ slot_52:
|
|||||||
created_at: '2022-07-12 15:18:43.955443'
|
created_at: '2022-07-12 15:18:43.955443'
|
||||||
updated_at: '2022-07-12 15:18:43.955443'
|
updated_at: '2022-07-12 15:18:43.955443'
|
||||||
availability_id: 16
|
availability_id: 16
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_53:
|
slot_53:
|
||||||
id: 53
|
id: 53
|
||||||
@ -374,6 +420,7 @@ slot_53:
|
|||||||
created_at: '2022-07-12 15:18:43.956657'
|
created_at: '2022-07-12 15:18:43.956657'
|
||||||
updated_at: '2022-07-12 15:18:43.956657'
|
updated_at: '2022-07-12 15:18:43.956657'
|
||||||
availability_id: 16
|
availability_id: 16
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_54:
|
slot_54:
|
||||||
id: 54
|
id: 54
|
||||||
@ -382,6 +429,7 @@ slot_54:
|
|||||||
created_at: '2022-07-12 15:18:43.957811'
|
created_at: '2022-07-12 15:18:43.957811'
|
||||||
updated_at: '2022-07-12 15:18:43.957811'
|
updated_at: '2022-07-12 15:18:43.957811'
|
||||||
availability_id: 16
|
availability_id: 16
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_55:
|
slot_55:
|
||||||
id: 55
|
id: 55
|
||||||
@ -390,6 +438,7 @@ slot_55:
|
|||||||
created_at: '2022-07-12 15:18:43.959063'
|
created_at: '2022-07-12 15:18:43.959063'
|
||||||
updated_at: '2022-07-12 15:18:43.959063'
|
updated_at: '2022-07-12 15:18:43.959063'
|
||||||
availability_id: 16
|
availability_id: 16
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_56:
|
slot_56:
|
||||||
id: 56
|
id: 56
|
||||||
@ -398,6 +447,7 @@ slot_56:
|
|||||||
created_at: '2022-07-12 15:18:43.961319'
|
created_at: '2022-07-12 15:18:43.961319'
|
||||||
updated_at: '2022-07-12 15:18:43.961319'
|
updated_at: '2022-07-12 15:18:43.961319'
|
||||||
availability_id: 17
|
availability_id: 17
|
||||||
|
places: [{"user_ids": [], "reservable_id": 4, "reservable_type": "Event", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_112:
|
slot_112:
|
||||||
id: 112
|
id: 112
|
||||||
@ -406,6 +456,7 @@ slot_112:
|
|||||||
created_at: '2022-07-12 15:18:44.038089'
|
created_at: '2022-07-12 15:18:44.038089'
|
||||||
updated_at: '2022-07-12 15:18:44.038089'
|
updated_at: '2022-07-12 15:18:44.038089'
|
||||||
availability_id: 18
|
availability_id: 18
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Space", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_113:
|
slot_113:
|
||||||
id: 113
|
id: 113
|
||||||
@ -414,6 +465,7 @@ slot_113:
|
|||||||
created_at: '2022-07-12 15:18:44.039392'
|
created_at: '2022-07-12 15:18:44.039392'
|
||||||
updated_at: '2022-07-12 15:18:44.039392'
|
updated_at: '2022-07-12 15:18:44.039392'
|
||||||
availability_id: 18
|
availability_id: 18
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Space", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_114:
|
slot_114:
|
||||||
id: 114
|
id: 114
|
||||||
@ -422,6 +474,7 @@ slot_114:
|
|||||||
created_at: '2022-07-12 15:18:44.040522'
|
created_at: '2022-07-12 15:18:44.040522'
|
||||||
updated_at: '2022-07-12 15:18:44.040522'
|
updated_at: '2022-07-12 15:18:44.040522'
|
||||||
availability_id: 18
|
availability_id: 18
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Space", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_115:
|
slot_115:
|
||||||
id: 115
|
id: 115
|
||||||
@ -430,6 +483,7 @@ slot_115:
|
|||||||
created_at: '2022-07-12 15:18:44.041937'
|
created_at: '2022-07-12 15:18:44.041937'
|
||||||
updated_at: '2022-07-12 15:18:44.041937'
|
updated_at: '2022-07-12 15:18:44.041937'
|
||||||
availability_id: 18
|
availability_id: 18
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Space", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_116:
|
slot_116:
|
||||||
id: 116
|
id: 116
|
||||||
@ -438,6 +492,7 @@ slot_116:
|
|||||||
created_at: '2022-07-12 15:18:44.044421'
|
created_at: '2022-07-12 15:18:44.044421'
|
||||||
updated_at: '2022-07-12 15:18:44.044421'
|
updated_at: '2022-07-12 15:18:44.044421'
|
||||||
availability_id: 19
|
availability_id: 19
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_117:
|
slot_117:
|
||||||
id: 117
|
id: 117
|
||||||
@ -446,6 +501,7 @@ slot_117:
|
|||||||
created_at: '2022-07-12 15:18:44.045689'
|
created_at: '2022-07-12 15:18:44.045689'
|
||||||
updated_at: '2022-07-12 15:18:44.045689'
|
updated_at: '2022-07-12 15:18:44.045689'
|
||||||
availability_id: 19
|
availability_id: 19
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_118:
|
slot_118:
|
||||||
id: 118
|
id: 118
|
||||||
@ -454,6 +510,7 @@ slot_118:
|
|||||||
created_at: '2022-07-12 15:18:44.047009'
|
created_at: '2022-07-12 15:18:44.047009'
|
||||||
updated_at: '2022-07-12 15:18:44.047009'
|
updated_at: '2022-07-12 15:18:44.047009'
|
||||||
availability_id: 19
|
availability_id: 19
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_119:
|
slot_119:
|
||||||
id: 119
|
id: 119
|
||||||
@ -462,6 +519,7 @@ slot_119:
|
|||||||
created_at: '2022-07-12 15:18:44.048272'
|
created_at: '2022-07-12 15:18:44.048272'
|
||||||
updated_at: '2022-07-12 15:18:44.048272'
|
updated_at: '2022-07-12 15:18:44.048272'
|
||||||
availability_id: 19
|
availability_id: 19
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_120:
|
slot_120:
|
||||||
id: 120
|
id: 120
|
||||||
@ -470,6 +528,7 @@ slot_120:
|
|||||||
created_at: '2022-07-12 15:18:44.049599'
|
created_at: '2022-07-12 15:18:44.049599'
|
||||||
updated_at: '2022-07-12 15:18:44.049599'
|
updated_at: '2022-07-12 15:18:44.049599'
|
||||||
availability_id: 19
|
availability_id: 19
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_121:
|
slot_121:
|
||||||
id: 121
|
id: 121
|
||||||
@ -478,6 +537,7 @@ slot_121:
|
|||||||
created_at: '2022-07-12 15:18:44.050947'
|
created_at: '2022-07-12 15:18:44.050947'
|
||||||
updated_at: '2022-07-12 15:18:44.050947'
|
updated_at: '2022-07-12 15:18:44.050947'
|
||||||
availability_id: 19
|
availability_id: 19
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_122:
|
slot_122:
|
||||||
id: 122
|
id: 122
|
||||||
@ -486,6 +546,7 @@ slot_122:
|
|||||||
created_at: '2022-07-12 15:18:44.052817'
|
created_at: '2022-07-12 15:18:44.052817'
|
||||||
updated_at: '2022-07-12 15:18:44.052817'
|
updated_at: '2022-07-12 15:18:44.052817'
|
||||||
availability_id: 19
|
availability_id: 19
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_123:
|
slot_123:
|
||||||
id: 123
|
id: 123
|
||||||
@ -494,6 +555,7 @@ slot_123:
|
|||||||
created_at: '2022-07-12 15:18:44.054966'
|
created_at: '2022-07-12 15:18:44.054966'
|
||||||
updated_at: '2022-07-12 15:18:44.054966'
|
updated_at: '2022-07-12 15:18:44.054966'
|
||||||
availability_id: 19
|
availability_id: 19
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_124:
|
slot_124:
|
||||||
id: 124
|
id: 124
|
||||||
@ -502,6 +564,7 @@ slot_124:
|
|||||||
created_at: '2022-07-12 15:18:44.057217'
|
created_at: '2022-07-12 15:18:44.057217'
|
||||||
updated_at: '2022-07-12 15:18:44.057217'
|
updated_at: '2022-07-12 15:18:44.057217'
|
||||||
availability_id: 19
|
availability_id: 19
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_125:
|
slot_125:
|
||||||
id: 125
|
id: 125
|
||||||
@ -510,6 +573,7 @@ slot_125:
|
|||||||
created_at: '2022-07-12 15:18:44.059135'
|
created_at: '2022-07-12 15:18:44.059135'
|
||||||
updated_at: '2022-07-12 15:18:44.059135'
|
updated_at: '2022-07-12 15:18:44.059135'
|
||||||
availability_id: 19
|
availability_id: 19
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_126:
|
slot_126:
|
||||||
id: 126
|
id: 126
|
||||||
@ -518,6 +582,7 @@ slot_126:
|
|||||||
created_at: '2022-07-12 15:18:44.061887'
|
created_at: '2022-07-12 15:18:44.061887'
|
||||||
updated_at: '2022-07-12 15:18:44.061887'
|
updated_at: '2022-07-12 15:18:44.061887'
|
||||||
availability_id: 1
|
availability_id: 1
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Training", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_127:
|
slot_127:
|
||||||
id: 127
|
id: 127
|
||||||
@ -526,6 +591,7 @@ slot_127:
|
|||||||
created_at: '2022-07-12 15:18:44.063528'
|
created_at: '2022-07-12 15:18:44.063528'
|
||||||
updated_at: '2022-07-12 15:18:44.063528'
|
updated_at: '2022-07-12 15:18:44.063528'
|
||||||
availability_id: 2
|
availability_id: 2
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Training", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_128:
|
slot_128:
|
||||||
id: 128
|
id: 128
|
||||||
@ -534,6 +600,7 @@ slot_128:
|
|||||||
created_at: '2022-07-12 15:18:44.065114'
|
created_at: '2022-07-12 15:18:44.065114'
|
||||||
updated_at: '2022-07-12 15:18:44.065114'
|
updated_at: '2022-07-12 15:18:44.065114'
|
||||||
availability_id: 8
|
availability_id: 8
|
||||||
|
places: [{"user_ids": [], "reservable_id": 4, "reservable_type": "Training", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_129:
|
slot_129:
|
||||||
id: 129
|
id: 129
|
||||||
@ -542,6 +609,7 @@ slot_129:
|
|||||||
created_at: '2022-07-12 15:18:44.066837'
|
created_at: '2022-07-12 15:18:44.066837'
|
||||||
updated_at: '2022-07-12 15:18:44.066837'
|
updated_at: '2022-07-12 15:18:44.066837'
|
||||||
availability_id: 9
|
availability_id: 9
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Event", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_130:
|
slot_130:
|
||||||
id: 130
|
id: 130
|
||||||
@ -550,6 +618,7 @@ slot_130:
|
|||||||
created_at: '2022-07-12 15:18:44.068259'
|
created_at: '2022-07-12 15:18:44.068259'
|
||||||
updated_at: '2022-07-12 15:18:44.068259'
|
updated_at: '2022-07-12 15:18:44.068259'
|
||||||
availability_id: 10
|
availability_id: 10
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Event", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_131:
|
slot_131:
|
||||||
id: 131
|
id: 131
|
||||||
@ -558,6 +627,7 @@ slot_131:
|
|||||||
created_at: '2022-07-12 15:18:44.069870'
|
created_at: '2022-07-12 15:18:44.069870'
|
||||||
updated_at: '2022-07-12 15:18:44.069870'
|
updated_at: '2022-07-12 15:18:44.069870'
|
||||||
availability_id: 11
|
availability_id: 11
|
||||||
|
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Event", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_132:
|
slot_132:
|
||||||
id: 132
|
id: 132
|
||||||
@ -566,6 +636,7 @@ slot_132:
|
|||||||
created_at: '2022-07-18 12:38:21.616510'
|
created_at: '2022-07-18 12:38:21.616510'
|
||||||
updated_at: '2022-07-18 12:38:21.616510'
|
updated_at: '2022-07-18 12:38:21.616510'
|
||||||
availability_id: 20
|
availability_id: 20
|
||||||
|
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Training", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_133:
|
slot_133:
|
||||||
id: 133
|
id: 133
|
||||||
@ -574,6 +645,7 @@ slot_133:
|
|||||||
created_at: '2022-12-14 12:01:26.165110'
|
created_at: '2022-12-14 12:01:26.165110'
|
||||||
updated_at: '2022-12-14 12:01:26.165110'
|
updated_at: '2022-12-14 12:01:26.165110'
|
||||||
availability_id: 21
|
availability_id: 21
|
||||||
|
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Space", "reserved_places": 0}]
|
||||||
|
|
||||||
slot_134:
|
slot_134:
|
||||||
id: 134
|
id: 134
|
||||||
@ -582,3 +654,4 @@ slot_134:
|
|||||||
created_at: 2023-01-24 13:34:43.841240000 Z
|
created_at: 2023-01-24 13:34:43.841240000 Z
|
||||||
updated_at: 2023-01-24 13:34:43.841240000 Z
|
updated_at: 2023-01-24 13:34:43.841240000 Z
|
||||||
availability_id: 22
|
availability_id: 22
|
||||||
|
places: [{"user_ids": [], "reservable_id": 4, "reservable_type": "Training", "reserved_places": 0}]
|
||||||
|
@ -79,9 +79,9 @@ class MachinesTest < ActionDispatch::IntegrationTest
|
|||||||
end
|
end
|
||||||
|
|
||||||
test 'soft delete a machine' do
|
test 'soft delete a machine' do
|
||||||
machine = Machine.find(4)
|
machine = Machine.find(2)
|
||||||
assert_not machine.destroyable?
|
assert_not machine.destroyable?
|
||||||
delete '/api/machines/4', headers: default_headers
|
delete "/api/machines/#{machine.id}", headers: default_headers
|
||||||
assert_response :success
|
assert_response :success
|
||||||
assert_empty response.body
|
assert_empty response.body
|
||||||
|
|
||||||
|
@ -53,13 +53,13 @@ class OpenApi::MachinesTest < ActionDispatch::IntegrationTest
|
|||||||
end
|
end
|
||||||
|
|
||||||
test 'soft delete a machine' do
|
test 'soft delete a machine' do
|
||||||
assert_not Machine.find(4).destroyable?
|
assert_not Machine.find(2).destroyable?
|
||||||
delete '/open_api/v1/machines/4', headers: open_api_headers(@token)
|
delete '/open_api/v1/machines/2', headers: open_api_headers(@token)
|
||||||
assert_response :success
|
assert_response :success
|
||||||
get '/open_api/v1/machines/4', headers: open_api_headers(@token)
|
get '/open_api/v1/machines/2', headers: open_api_headers(@token)
|
||||||
assert_response :not_found
|
assert_response :not_found
|
||||||
get '/open_api/v1/machines', headers: open_api_headers(@token)
|
get '/open_api/v1/machines', headers: open_api_headers(@token)
|
||||||
machines = json_response(response.body)
|
machines = json_response(response.body)
|
||||||
assert_not(machines[:machines].any? { |m| m[:id] == 4 })
|
assert_not(machines[:machines].any? { |m| m[:id] == 2 })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -57,13 +57,13 @@ class OpenApi::ReservationsTest < ActionDispatch::IntegrationTest
|
|||||||
assert_equal ['Machine'], reservations[:reservations].pluck(:reservable_type).uniq
|
assert_equal ['Machine'], reservations[:reservations].pluck(:reservable_type).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'list all machine 4 reservations' do
|
test 'list all machine 2 reservations' do
|
||||||
get '/open_api/v1/reservations?reservable_type=Machine&reservable_id=4', headers: open_api_headers(@token)
|
get '/open_api/v1/reservations?reservable_type=Machine&reservable_id=2', headers: open_api_headers(@token)
|
||||||
assert_response :success
|
assert_response :success
|
||||||
assert_equal Mime[:json], response.content_type
|
assert_equal Mime[:json], response.content_type
|
||||||
|
|
||||||
reservations = json_response(response.body)
|
reservations = json_response(response.body)
|
||||||
assert_not_empty reservations[:reservations]
|
assert_not_empty reservations[:reservations]
|
||||||
assert_equal [4], reservations[:reservations].pluck(:reservable_id).uniq
|
assert_equal [2], reservations[:reservations].pluck(:reservable_id).uniq
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user