Added basic equipment booking functionality

This commit is contained in:
Arnaud Vergnet 2020-07-09 14:40:01 +02:00
parent 63b02cd83c
commit 5067fd47d6
8 changed files with 853 additions and 5 deletions

View file

@ -21,6 +21,7 @@
"dependencies": {
"@nartc/react-native-barcode-mask": "^1.2.0",
"@react-native-community/async-storage": "^1.11.0",
"@react-native-community/datetimepicker": "^2.6.0",
"@react-native-community/masked-view": "^0.1.10",
"@react-native-community/push-notification-ios": "^1.2.2",
"@react-native-community/slider": "^3.0.0",
@ -34,7 +35,7 @@
"react-native-app-intro-slider": "^4.0.0",
"react-native-appearance": "^0.3.3",
"react-native-autolink": "^3.0.0",
"react-native-calendars": "^1.299.0",
"react-native-calendars": "^1.300.0",
"react-native-camera": "^3.30.0",
"react-native-collapsible": "^1.5.2",
"react-native-gesture-handler": "^1.6.1",

View file

@ -0,0 +1,86 @@
// @flow
import * as React from 'react';
import {Avatar, List, withTheme} from 'react-native-paper';
import type {CustomTheme} from "../../../managers/ThemeManager";
import type {Device} from "../../../screens/Amicale/Equipment/EquipmentListScreen";
import i18n from "i18n-js";
import {getTimeOnlyString, stringToDate} from "../../../utils/Planning";
type Props = {
onPress: () => void,
item: Device,
height: number,
theme: CustomTheme,
}
class EquipmentListItem extends React.Component<Props> {
shouldComponentUpdate() {
return false;
}
isAvailable() {
const availableDate = stringToDate(this.props.item.available_at);
return availableDate != null && availableDate < new Date();
}
/**
* Gets the string representation of the given date.
*
* If the given date is the same day as today, only return the tile.
* Otherwise, return the full date.
*
* @param dateString The string representation of the wanted date
* @returns {string}
*/
getDateString(dateString: string): string {
const today = new Date();
const date = stringToDate(dateString);
if (date != null && today.getDate() === date.getDate()) {
const str = getTimeOnlyString(dateString);
return str != null ? str : "";
} else
return dateString;
}
render() {
const colors = this.props.theme.colors;
const item = this.props.item;
const isAvailable = this.isAvailable();
return (
<List.Item
title={item.name}
description={isAvailable
? i18n.t('equipmentScreen.bail', {cost: item.caution})
: i18n.t('equipmentScreen.availableAt', {date: this.getDateString(item.available_at)})}
onPress={this.props.onPress}
left={(props) => <Avatar.Icon
{...props}
style={{
backgroundColor: 'transparent',
}}
icon={isAvailable ? "check-circle-outline" : "update"}
color={isAvailable ? colors.success : colors.primary}
/>}
right={(props) => <Avatar.Icon
{...props}
style={{
marginTop: 'auto',
marginBottom: 'auto',
backgroundColor: 'transparent',
}}
size={48}
icon={"chevron-right"}
/>}
style={{
height: this.props.height,
justifyContent: 'center',
}}
/>
);
}
}
export default withTheme(EquipmentListItem);

View file

@ -23,6 +23,8 @@ import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
import {createScreenCollapsibleStack, getWebsiteStack} from "../utils/CollapsibleUtils";
import BugReportScreen from "../screens/Other/FeedbackScreen";
import WebsiteScreen from "../screens/Services/WebsiteScreen";
import EquipmentScreen from "../screens/Amicale/Equipment/EquipmentListScreen";
import EquipmentLendScreen from "../screens/Amicale/Equipment/EquipmentRentScreen";
const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS;
@ -119,6 +121,8 @@ function MainStackComponent(props: { createTabNavigator: () => React.Node }) {
{createScreenCollapsibleStack("profile", MainStack, ProfileScreen, i18n.t('screens.profile'))}
{createScreenCollapsibleStack("club-list", MainStack, ClubListScreen, i18n.t('clubs.clubList'))}
{createScreenCollapsibleStack("equipment-list", MainStack, EquipmentScreen, i18n.t('screens.equipmentList'))}
{createScreenCollapsibleStack("equipment-lend", MainStack, EquipmentLendScreen, i18n.t('screens.equipmentLend'))}
<MainStack.Screen
name="club-information"
component={ClubDisplayScreen}

View file

@ -0,0 +1,145 @@
// @flow
import * as React from 'react';
import {Animated} from "react-native";
import {Avatar, Card, Paragraph, withTheme} from 'react-native-paper';
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
import {Collapsible} from "react-navigation-collapsible";
import {withCollapsible} from "../../../utils/withCollapsible";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../../managers/ThemeManager";
import i18n from "i18n-js";
import type {club} from "../Clubs/ClubListScreen";
import EquipmentListItem from "../../../components/Lists/Equipment/EquipmentListItem";
type Props = {
navigation: StackNavigationProp,
theme: CustomTheme,
collapsibleStack: Collapsible,
}
export type Device = {
id: number,
name: string,
caution: number,
available_at: string,
};
const TEST_DATASET = [
{
id: 1,
name: "Petit barbecue",
caution: 100,
available_at: "2020-07-07 21:12"
},
{
id: 2,
name: "Grand barbecue",
caution: 100,
available_at: "2020-07-08 21:12"
},
{
id: 3,
name: "Appareil à fondue",
caution: 100,
available_at: "2020-07-09 14:12"
},
{
id: 4,
name: "Appareil à croque-monsieur",
caution: 100,
available_at: "2020-07-10 12:12"
}
]
const ICON_AMICALE = require('../../../../assets/amicale.png');
const LIST_ITEM_HEIGHT = 64;
class EquipmentListScreen extends React.Component<Props> {
data: Array<Device>;
getRenderItem = ({item}: { item: Device }) => {
return (
<EquipmentListItem
onPress={() => this.props.navigation.navigate('equipment-lend', {item: item})}
item={item}
height={LIST_ITEM_HEIGHT}/>
);
};
/**
* Gets the list header, with explains this screen's purpose
*
* @returns {*}
*/
getListHeader() {
return <Card style={{margin: 5}}>
<Card.Title
title={i18n.t('equipmentScreen.title')}
left={(props) => <Avatar.Image
{...props}
source={ICON_AMICALE}
style={{backgroundColor: 'transparent'}}
/>}
/>
<Card.Content>
<Paragraph>
{i18n.t('equipmentScreen.message')}
</Paragraph>
</Card.Content>
</Card>;
}
keyExtractor = (item: club) => item.id.toString();
/**
* Gets the main screen component with the fetched data
*
* @param data The data fetched from the server
* @returns {*}
*/
getScreen = (data: Array<{ [key: string]: any } | null>) => {
if (data[0] != null) {
const fetchedData = data[0];
if (fetchedData != null)
this.data = fetchedData["devices"];
this.data = TEST_DATASET; // TODO remove in prod
}
const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
return (
<Animated.FlatList
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()}
data={this.data}
// Animations
onScroll={onScroll}
contentContainerStyle={{
paddingTop: containerPaddingTop,
minHeight: '100%'
}}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}
/>
)
};
render() {
return (
<AuthenticatedScreen
{...this.props}
requests={[
{
link: 'user/profile',
params: {},
mandatory: false,
}
]}
renderFunction={this.getScreen}
/>
);
}
}
export default withCollapsible(withTheme(EquipmentListScreen));

View file

@ -0,0 +1,569 @@
// @flow
import * as React from 'react';
import {Button, Caption, Card, Headline, Subheading, Text, withTheme} from 'react-native-paper';
import {Collapsible} from "react-navigation-collapsible";
import {withCollapsible} from "../../../utils/withCollapsible";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../../managers/ThemeManager";
import type {Device} from "./EquipmentListScreen";
import {Animated, BackHandler} from "react-native";
import * as Animatable from "react-native-animatable";
import {View} from "react-native-animatable";
import i18n from "i18n-js";
import {dateToString, getTimeOnlyString, stringToDate} from "../../../utils/Planning";
import {CalendarList} from "react-native-calendars";
import DateTimePicker from '@react-native-community/datetimepicker';
import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog";
import ConnectionManager from "../../../managers/ConnectionManager";
import ErrorDialog from "../../../components/Dialogs/ErrorDialog";
type Props = {
navigation: StackNavigationProp,
route: {
params?: {
item?: Device,
},
},
theme: CustomTheme,
collapsibleStack: Collapsible,
}
type State = {
dialogVisible: boolean,
errorDialogVisible: boolean,
markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } },
timePickerVisible: boolean,
currentError: number,
}
class EquipmentRentScreen extends React.Component<Props, State> {
state = {
dialogVisible: false,
errorDialogVisible: false,
markedDates: {},
timePickerVisible: false,
currentError: 0,
}
item: Device | null;
selectedDates: {
start: Date | null,
end: Date | null,
};
currentlySelectedDate: Date | null;
bookRef: { current: null | Animatable.View }
canBookEquipment: boolean;
constructor(props: Props) {
super(props);
this.resetSelection();
this.bookRef = React.createRef();
this.canBookEquipment = false;
if (this.props.route.params != null) {
if (this.props.route.params.item != null)
this.item = this.props.route.params.item;
else
this.item = null;
}
}
/**
* Captures focus and blur events to hook on android back button
*/
componentDidMount() {
this.props.navigation.addListener(
'focus',
() =>
BackHandler.addEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid
)
);
this.props.navigation.addListener(
'blur',
() =>
BackHandler.removeEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid
)
);
}
/**
* Overrides default android back button behaviour to deselect date if any is selected.
*
* @return {boolean}
*/
onBackButtonPressAndroid = () => {
if (this.currentlySelectedDate != null) {
this.resetSelection();
this.setState({
markedDates: this.generateMarkedDates(),
});
return true;
} else
return false;
};
isAvailable(item: Device) {
const availableDate = stringToDate(item.available_at);
return availableDate != null && availableDate < new Date();
}
/**
* Gets the string representation of the given date.
*
* If the given date is the same day as today, only return the tile.
* Otherwise, return the full date.
*
* @param dateString The string representation of the wanted date
* @returns {string}
*/
getDateString(dateString: string): string {
const today = new Date();
const date = stringToDate(dateString);
if (date != null && today.getDate() === date.getDate()) {
const str = getTimeOnlyString(dateString);
return str != null ? str : "";
} else
return dateString;
}
/**
* Gets the minimum date for renting equipment
*
* @param item The item to rent
* @param isAvailable True is it is available right now
* @returns {Date}
*/
getMinDate(item: Device, isAvailable: boolean) {
let date = new Date();
if (isAvailable)
return date;
else {
const limit = stringToDate(item.available_at)
return limit != null ? limit : date;
}
}
/**
* Selects a new date on the calendar.
* If both start and end dates are already selected, unselect all.
*
* @param day The day selected
*/
selectNewDate = (day: { dateString: string, day: number, month: number, timestamp: number, year: number }) => {
this.currentlySelectedDate = new Date(day.dateString);
if (!this.canBookEquipment) {
const start = this.selectedDates.start;
if (start == null)
this.selectedDates.start = this.currentlySelectedDate;
else if (this.currentlySelectedDate < start) {
this.selectedDates.end = start;
this.selectedDates.start = this.currentlySelectedDate;
} else
this.selectedDates.end = this.currentlySelectedDate;
} else
this.resetSelection();
if (this.selectedDates.start != null) {
this.setState({
markedDates: this.generateMarkedDates(),
timePickerVisible: true,
});
} else {
this.setState({
markedDates: this.generateMarkedDates(),
});
}
}
resetSelection() {
if (this.canBookEquipment)
this.hideBookButton();
this.canBookEquipment = false;
this.selectedDates = {start: null, end: null};
this.currentlySelectedDate = null;
}
/**
* Deselect the currently selected date
*/
deselectCurrentDate() {
let currentlySelectedDate = this.currentlySelectedDate;
const start = this.selectedDates.start;
const end = this.selectedDates.end;
if (currentlySelectedDate != null && start != null) {
if (currentlySelectedDate === start && end === null)
this.resetSelection();
else if (end != null && currentlySelectedDate === end) {
this.currentlySelectedDate = start;
this.selectedDates.end = null;
} else if (currentlySelectedDate === start) {
this.currentlySelectedDate = end;
this.selectedDates.start = this.selectedDates.end;
this.selectedDates.end = null;
}
}
}
/**
* Saves the selected time to the currently selected date.
* If no the time selection was canceled, cancels the current selecction
*
* @param event The click event
* @param date The date selected
*/
onTimeChange = (event: { nativeEvent: { timestamp: number }, type: string }, date: Date) => {
let currentDate = this.currentlySelectedDate;
const item = this.item;
if (item != null && event.type === "set" && currentDate != null) {
currentDate.setHours(date.getHours());
currentDate.setMinutes(date.getMinutes());
const isAvailable = this.isAvailable(item);
let limit = this.getMinDate(item, isAvailable);
// Prevent selecting a date before now
if (this.getISODate(currentDate) === this.getISODate(limit) && currentDate < limit) {
currentDate.setHours(limit.getHours());
currentDate.setMinutes(limit.getMinutes());
}
if (this.selectedDates.start != null && this.selectedDates.end != null) {
if (this.selectedDates.start > this.selectedDates.end) {
const temp = this.selectedDates.start;
this.selectedDates.start = this.selectedDates.end;
this.selectedDates.end = temp;
}
this.canBookEquipment = true;
this.showBookButton();
}
} else
this.deselectCurrentDate();
this.setState({
timePickerVisible: false,
markedDates: this.generateMarkedDates(),
});
}
/**
* Returns the ISO date format (without the time)
*
* @param date The date to recover the ISO format from
* @returns {*}
*/
getISODate(date: Date) {
return date.toISOString().split("T")[0];
}
/**
* Generates the object containing all marked dates between the start and end dates selected
*
* @returns {{}}
*/
generateMarkedDates() {
let markedDates = {}
const start = this.selectedDates.start;
const end = this.selectedDates.end;
if (start != null) {
const startISODate = this.getISODate(start);
if (end != null && this.getISODate(end) !== startISODate) {
markedDates[startISODate] = {
startingDay: true,
endingDay: false,
color: this.props.theme.colors.primary
};
markedDates[this.getISODate(end)] = {
startingDay: false,
endingDay: true,
color: this.props.theme.colors.primary
};
let date = new Date(start);
date.setDate(date.getDate() + 1);
while (date < end && this.getISODate(date) !== this.getISODate(end)) {
markedDates[this.getISODate(date)] =
{startingDay: false, endingDay: false, color: this.props.theme.colors.danger};
date.setDate(date.getDate() + 1);
}
} else {
markedDates[startISODate] = {
startingDay: true,
endingDay: true,
color: this.props.theme.colors.primary
};
}
}
return markedDates;
}
/**
* Shows the book button by plying a fade animation
*/
showBookButton() {
if (this.bookRef.current != null) {
this.bookRef.current.fadeInUp(500);
}
}
/**
* Hides the book button by plying a fade animation
*/
hideBookButton() {
if (this.bookRef.current != null) {
this.bookRef.current.fadeOutDown(500);
}
}
showDialog = () => {
this.setState({dialogVisible: true});
}
showErrorDialog = (error: number) => {
this.setState({
errorDialogVisible: true,
currentError: error,
});
}
onDialogDismiss = () => {
this.setState({dialogVisible: false});
}
onErrorDialogDismiss = () => {
this.setState({errorDialogVisible: false});
}
/**
* Sends the selected data to the server and waits for a response.
* If the request is a success, navigate to the recap screen.
* If it is an error, display the error to the user.
*
* @returns {Promise<R>}
*/
onDialogAccept = () => {
return new Promise((resolve) => {
const item = this.item;
const start = this.selectedDates.start;
const end = this.selectedDates.end;
if (item != null && start != null && end != null) {
ConnectionManager.getInstance().authenticatedRequest(
"", // TODO set path
{
"id": item.id,
"start": dateToString(start, false),
"end": dateToString(end, false),
})
.then(() => {
console.log("Success, replace screen");
resolve();
})
.catch((error: number) => {
this.onDialogDismiss();
this.showErrorDialog(error);
resolve();
});
} else
resolve();
});
}
render() {
const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
let startString = <Caption>{i18n.t('equipmentScreen.notSet')}</Caption>;
let endString = <Caption>{i18n.t('equipmentScreen.notSet')}</Caption>;
const start = this.selectedDates.start;
const end = this.selectedDates.end;
if (start != null)
startString = dateToString(start, false);
if (end != null)
endString = dateToString(end, false);
const item = this.item;
if (item != null) {
const isAvailable = this.isAvailable(item);
return (
<View style={{flex: 1}}>
<Animated.ScrollView
// Animations
onScroll={onScroll}
contentContainerStyle={{
paddingTop: containerPaddingTop,
minHeight: '100%'
}}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
<Card style={{margin: 5}}>
<Card.Content>
<View style={{flex: 1}}>
<View style={{
marginLeft: "auto",
marginRight: "auto",
flexDirection: "row",
flexWrap: "wrap",
}}>
<Headline style={{textAlign: "center"}}>
{item.name}
</Headline>
<Caption style={{
textAlign: "center",
lineHeight: 35,
marginLeft: 10,
}}>
({i18n.t('equipmentScreen.bail', {cost: item.caution})})
</Caption>
</View>
</View>
<Button
icon={isAvailable ? "check-circle-outline" : "update"}
color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary}
mode="text"
>
{
isAvailable
? i18n.t('equipmentScreen.available')
: i18n.t('equipmentScreen.availableAt', {date: this.getDateString(item.available_at)})
}
</Button>
<Text style={{
textAlign: "center",
marginBottom: 10
}}>
{i18n.t('equipmentScreen.booking')}
</Text>
<Subheading style={{textAlign: "center"}}>
{i18n.t('equipmentScreen.startDate')}
{startString}
</Subheading>
<Subheading style={{textAlign: "center"}}>
{i18n.t('equipmentScreen.endDate')}
{endString}
</Subheading>
</Card.Content>
</Card>
{this.state.timePickerVisible
? <DateTimePicker
value={new Date()}
mode={"time"}
display={"clock"}
is24Hour={true}
onChange={this.onTimeChange}
/>
: null}
<CalendarList
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
minDate={this.getMinDate(item, isAvailable)}
// Max amount of months allowed to scroll to the past. Default = 50
pastScrollRange={0}
// Max amount of months allowed to scroll to the future. Default = 50
futureScrollRange={3}
// Enable horizontal scrolling, default = false
horizontal={true}
// Enable paging on horizontal, default = false
pagingEnabled={true}
// Handler which gets executed on day press. Default = undefined
onDayPress={this.selectNewDate}
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
firstDay={1}
// Disable all touch events for disabled days. can be override with disableTouchEvent in markedDates
disableAllTouchEventsForDisabledDays={true}
// Hide month navigation arrows.
hideArrows={false}
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
markingType={'period'}
markedDates={this.state.markedDates}
theme={{
backgroundColor: this.props.theme.colors.agendaBackgroundColor,
calendarBackground: this.props.theme.colors.background,
textSectionTitleColor: this.props.theme.colors.agendaDayTextColor,
selectedDayBackgroundColor: this.props.theme.colors.primary,
selectedDayTextColor: '#ffffff',
todayTextColor: this.props.theme.colors.text,
dayTextColor: this.props.theme.colors.text,
textDisabledColor: this.props.theme.colors.agendaDayTextColor,
dotColor: this.props.theme.colors.primary,
selectedDotColor: '#ffffff',
arrowColor: this.props.theme.colors.primary,
monthTextColor: this.props.theme.colors.text,
indicatorColor: this.props.theme.colors.primary,
textDayFontFamily: 'monospace',
textMonthFontFamily: 'monospace',
textDayHeaderFontFamily: 'monospace',
textDayFontWeight: '300',
textMonthFontWeight: 'bold',
textDayHeaderFontWeight: '300',
textDayFontSize: 16,
textMonthFontSize: 16,
textDayHeaderFontSize: 16,
'stylesheet.day.period': {
base: {
overflow: 'hidden',
height: 34,
width: 34,
alignItems: 'center',
}
}
}}
style={{marginBottom: 50}}
/>
</Animated.ScrollView>
<LoadingConfirmDialog
visible={this.state.dialogVisible}
onDismiss={this.onDialogDismiss}
onAccept={this.onDialogAccept}
title={i18n.t('equipmentScreen.dialogTitle')}
titleLoading={i18n.t('equipmentScreen.dialogTitleLoading')}
message={i18n.t('equipmentScreen.dialogMessage')}
/>
<ErrorDialog
visible={this.state.errorDialogVisible}
onDismiss={this.onErrorDialogDismiss}
errorCode={this.state.currentError}
/>
<Animatable.View
ref={this.bookRef}
style={{
position: "absolute",
bottom: 0,
left: 0,
width: "100%",
flex: 1,
transform: [
{translateY: 100},
]
}}>
<Button
icon="bookmark-check"
mode="contained"
onPress={this.showDialog}
style={{
width: "80%",
flex: 1,
marginLeft: "auto",
marginRight: "auto",
marginBottom: 20,
borderRadius: 10
}}
>
{i18n.t('equipmentScreen.bookButton')}
</Button>
</Animatable.View>
</View>
)
} else
return <View/>;
}
}
export default withCollapsible(withTheme(EquipmentRentScreen));

View file

@ -33,6 +33,7 @@ const AMICALE_LOGO = require("../../../assets/amicale.png");
const CLUBS_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png";
const PROFILE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png";
const EQUIPMENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Materiel.png";
const VOTE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png";
const AMICALE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/WebsiteAmicale.png";
@ -72,6 +73,12 @@ class ServicesScreen extends React.Component<Props> {
image: PROFILE_IMAGE,
onPress: () => this.onAmicaleServicePress("profile"),
},
{
title: i18n.t('screens.equipmentList'),
subtitle: i18n.t('servicesScreen.descriptions.equipment'),
image: EQUIPMENT_IMAGE,
onPress: () => this.onAmicaleServicePress("equipment-list"),
},
{
title: i18n.t('screens.amicaleWebsite'),
subtitle: i18n.t('servicesScreen.descriptions.amicaleWebsite'),

View file

@ -26,7 +26,9 @@
"vote": "Elections",
"scanner": "Scanotron 3000",
"feedback": "Feedback",
"insaAccount": "INSA Account"
"insaAccount": "INSA Account",
"equipmentList": "Equipment Booking",
"equipmentLend": "Book"
},
"intro": {
"slideMain": {
@ -428,7 +430,8 @@
"bib": "Book a Bib'Box for project work",
"mails": "Check your INSA mails",
"ent": "See your grades",
"insaAccount": "See your information and change your password"
"insaAccount": "See your information and change your password",
"equipment": "Book a BBQ or other equipment"
}
},
"planningScreen": {
@ -444,5 +447,20 @@
"contactMeans": "Using Gitea is recommended, to use it simply login with your INSA account.",
"homeButtonTitle": "Feedback/Bug report",
"homeButtonSubtitle": "Contact the devs"
},
"equipmentScreen": {
"title": "Equipment booking",
"message": "Thanks to the Amicale, students have access to some equipment like BBQs and others. To book one of those items, click the equipment of your choice in the list bellow, enter your lend dates, then come around the Amicale to claim it and give your bail.",
"bail": "Bail: %{cost}€",
"availableAt": "Available at: %{date}",
"available": "Available!",
"booking": "Click on the calendar to set the start and end dates",
"startDate": "Start: ",
"endDate": "End: ",
"notSet": "Not set",
"bookButton": "Book selected dates",
"dialogTitle": "Confirm booking?",
"dialogTitleLoading": "Sending your booking...",
"dialogMessage": "Are you sure you want to confirm your booking?\n\nYou will then be able to claim the selected equipment at the Amicale for the duration of your booking in exchange of a bail."
}
}

View file

@ -26,7 +26,9 @@
"vote": "Élections",
"scanner": "Scanotron 3000",
"feedback": "Votre avis",
"insaAccount": "Compte INSA"
"insaAccount": "Compte INSA",
"equipmentList": "Réservation Matériel",
"equipmentLend": "Réserver"
},
"intro": {
"slideMain": {
@ -430,7 +432,8 @@
"bib": "Réservez une Bib'Box pour les travaux de groupe",
"mails": "Vérifiez vos mails INSA",
"ent": "Retrouvez vos notes",
"insaAccount": "Accédez à vos informations et modifiez votre mot de passe"
"insaAccount": "Accédez à vos informations et modifiez votre mot de passe",
"equipment": "Réservez un BBQ ou d'autre matériel"
}
},
"planningScreen": {
@ -446,5 +449,20 @@
"contactMeans": "L'utilisation de Gitea est recommandée, pour l'utiliser, connectez vous avec vos identifiants INSA.",
"homeButtonTitle": "Feedback/Bugs",
"homeButtonSubtitle": "Contacter le développeur"
},
"equipmentScreen": {
"title": "Réservation de Matériel",
"message": "L'Amicale mets à disposition des étudiants du matériel comme des BBQ, des appareils à raclette et autres. Pour réserver l'un de ces formidables appareils, cliquez sur celui de votre choix dans la liste, indiquez les dates du prêt, puis passez à l'Amicale pour le récupérer et donner votre caution.",
"bail": "Caution : %{cost}€",
"availableAt": "Disponible à : %{date}",
"available": "Disponible !",
"booking": "Cliquez sur le calendrier pour choisir les dates de début et de fin de la réservation",
"startDate": "Début: ",
"endDate": "Fin: ",
"notSet": "Non défini",
"bookButton": "Choisir ces dates",
"dialogTitle": "Confirmer la réservation ?",
"dialogTitleLoading": "Envoi de la réservation...",
"dialogMessage": "Êtes vous sûr de vouloir confirmer cette réservation?\n\nVous pourrez ensuite récupérer le matériel à l'Amicale pour la durée de votre reservation en échange d'une caution."
}
}