feat: 🎸 Skip to the next day in calendar (#425)
* Add meaningful starting date to components * Add warn on console use * Add better error message if response from skola24 is wrong * Change heading to only show name of day * Remove extra menu Co-authored-by: Andreas Eriksson <addeman@gmail.com>
This commit is contained in:
parent
7bee08a550
commit
fce1d98847
|
@ -9,7 +9,7 @@
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
"rules": {}
|
"rules": {"no-console":"warn"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx"],
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
Text,
|
Text,
|
||||||
useStyleSheet,
|
useStyleSheet,
|
||||||
} from '@ui-kitten/components'
|
} from '@ui-kitten/components'
|
||||||
import moment from 'moment'
|
import moment, { Moment } from 'moment'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { TouchableOpacity, useColorScheme, View } from 'react-native'
|
import { TouchableOpacity, useColorScheme, View } from 'react-native'
|
||||||
import { useTranslation } from '../hooks/useTranslation'
|
import { useTranslation } from '../hooks/useTranslation'
|
||||||
|
@ -31,6 +31,7 @@ interface ChildListItemProps {
|
||||||
child: Child
|
child: Child
|
||||||
color: string
|
color: string
|
||||||
updated: string
|
updated: string
|
||||||
|
currentDate?: Moment
|
||||||
}
|
}
|
||||||
type ChildListItemNavigationProp = StackNavigationProp<
|
type ChildListItemNavigationProp = StackNavigationProp<
|
||||||
RootStackParamList,
|
RootStackParamList,
|
||||||
|
@ -41,6 +42,7 @@ export const ChildListItem = ({
|
||||||
child,
|
child,
|
||||||
color,
|
color,
|
||||||
updated,
|
updated,
|
||||||
|
currentDate = moment(),
|
||||||
}: ChildListItemProps) => {
|
}: ChildListItemProps) => {
|
||||||
// Forces rerender when child.id changes
|
// Forces rerender when child.id changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -57,16 +59,14 @@ export const ChildListItem = ({
|
||||||
const { data: menu, reload: menuReload } = useMenu(child)
|
const { data: menu, reload: menuReload } = useMenu(child)
|
||||||
const { data: schedule, reload: scheduleReload } = useSchedule(
|
const { data: schedule, reload: scheduleReload } = useSchedule(
|
||||||
child,
|
child,
|
||||||
moment().toISOString(),
|
moment(currentDate).toISOString(),
|
||||||
moment().add(7, 'days').toISOString()
|
moment(currentDate).add(7, 'days').toISOString()
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Do not refresh if updated is empty (first render of component)
|
// Do not refresh if updated is empty (first render of component)
|
||||||
if (updated === '') return
|
if (updated === '') return
|
||||||
|
|
||||||
console.log('Reload', child.name, updated)
|
|
||||||
|
|
||||||
newsReload()
|
newsReload()
|
||||||
classmatesReload()
|
classmatesReload()
|
||||||
notificationsReload()
|
notificationsReload()
|
||||||
|
@ -89,8 +89,8 @@ export const ChildListItem = ({
|
||||||
)
|
)
|
||||||
|
|
||||||
const newsThisWeek = news.filter(({ modified, published }) => {
|
const newsThisWeek = news.filter(({ modified, published }) => {
|
||||||
const date = modified || published
|
const newsDate = modified || published
|
||||||
return date ? moment(date).isSame(moment(), 'week') : false
|
return newsDate ? moment(newsDate).isSame(currentDate, 'week') : false
|
||||||
})
|
})
|
||||||
|
|
||||||
const scheduleAndCalendarThisWeek = [
|
const scheduleAndCalendarThisWeek = [
|
||||||
|
@ -99,14 +99,18 @@ export const ChildListItem = ({
|
||||||
].filter(({ startDate }) =>
|
].filter(({ startDate }) =>
|
||||||
startDate
|
startDate
|
||||||
? moment(startDate).isBetween(
|
? moment(startDate).isBetween(
|
||||||
moment().startOf('day'),
|
moment(currentDate).startOf('day'),
|
||||||
moment().add(7, 'days')
|
moment(currentDate).add(7, 'days')
|
||||||
)
|
)
|
||||||
: false
|
: false
|
||||||
)
|
)
|
||||||
|
|
||||||
const displayDate = (date: moment.MomentInput) => {
|
const displayDate = (inputDate: moment.MomentInput) => {
|
||||||
return moment(date).fromNow()
|
return moment(inputDate).fromNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
const capitalizeFirstLetter = (string) => {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getClassName = () => {
|
const getClassName = () => {
|
||||||
|
@ -168,12 +172,19 @@ export const ChildListItem = ({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<DaySummary child={child} />
|
{currentDate.hour() > 17 && currentDate.hour() <= 23 ? (
|
||||||
|
<Text category="c2" style={styles.weekday}>
|
||||||
|
{capitalizeFirstLetter(currentDate.format('dddd'))}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<DaySummary child={child} date={currentDate} />
|
||||||
|
|
||||||
{scheduleAndCalendarThisWeek.slice(0, 3).map((calendarItem, i) => (
|
{scheduleAndCalendarThisWeek.slice(0, 3).map((calendarItem, i) => (
|
||||||
<Text category="p1" key={i}>
|
<Text category="p1" key={i}>
|
||||||
{`${calendarItem.title} (${displayDate(calendarItem.startDate)})`}
|
{`${calendarItem.title} (${displayDate(calendarItem.startDate)})`}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Text category="c2" style={styles.label}>
|
<Text category="c2" style={styles.label}>
|
||||||
{t('navigation.news')}
|
{t('navigation.news')}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -182,11 +193,13 @@ export const ChildListItem = ({
|
||||||
{notification.message}
|
{notification.message}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{newsThisWeek.slice(0, 3).map((newsItem, i) => (
|
{newsThisWeek.slice(0, 3).map((newsItem, i) => (
|
||||||
<Text category="p1" key={i}>
|
<Text category="p1" key={i}>
|
||||||
{newsItem.header ?? ''}
|
{newsItem.header ?? ''}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{scheduleAndCalendarThisWeek.length ||
|
{scheduleAndCalendarThisWeek.length ||
|
||||||
notificationsThisWeek.length ||
|
notificationsThisWeek.length ||
|
||||||
newsThisWeek.length ? null : (
|
newsThisWeek.length ? null : (
|
||||||
|
@ -194,14 +207,15 @@ export const ChildListItem = ({
|
||||||
{t('news.noNewNewsItemsThisWeek')}
|
{t('news.noNewNewsItemsThisWeek')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{!menu[moment().isoWeekday() - 1] ? null : (
|
{!menu[currentDate.isoWeekday() - 1] ? null : (
|
||||||
<>
|
<>
|
||||||
<Text category="c2" style={styles.label}>
|
<Text category="c2" style={styles.label}>
|
||||||
{t('schedule.lunch')}
|
{t('schedule.lunch')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>{menu[moment().isoWeekday() - 1]?.description}</Text>
|
<Text>{menu[currentDate.isoWeekday() - 1]?.description}</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.itemFooter}>
|
<View style={styles.itemFooter}>
|
||||||
<Button
|
<Button
|
||||||
accessible
|
accessible
|
||||||
|
@ -274,4 +288,8 @@ const themeStyles = StyleService.create({
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
},
|
},
|
||||||
noNewNewsItemsText: {},
|
noNewNewsItemsText: {},
|
||||||
|
weekday: {
|
||||||
|
marginBottom: -5,
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { Colors, Layout as LayoutStyle, Sizing, Typography } from '../styles'
|
||||||
import { translate } from '../utils/translation'
|
import { translate } from '../utils/translation'
|
||||||
import { ChildListItem } from './childListItem.component'
|
import { ChildListItem } from './childListItem.component'
|
||||||
import { SettingsIcon, RefreshIcon } from './icon.component'
|
import { SettingsIcon, RefreshIcon } from './icon.component'
|
||||||
|
import { getMeaningfulStartingDate } from '../utils/calendarHelpers'
|
||||||
|
|
||||||
const colors = ['primary', 'success', 'info', 'warning', 'danger']
|
const colors = ['primary', 'success', 'info', 'warning', 'danger']
|
||||||
|
|
||||||
|
@ -80,80 +81,77 @@ export const Children = () => {
|
||||||
})
|
})
|
||||||
}, [navigation, reloadChildren])
|
}, [navigation, reloadChildren])
|
||||||
|
|
||||||
|
const currentDate = getMeaningfulStartingDate()
|
||||||
|
|
||||||
// We need to skip safe area view here, due to the reason that it's adding a white border
|
// We need to skip safe area view here, due to the reason that it's adding a white border
|
||||||
// when this view is actually lightgrey. Taking the padding top value from the use inset hook.
|
// when this view is actually lightgrey. Taking the padding top value from the use inset hook.
|
||||||
return (
|
return status === 'loaded' ? (
|
||||||
<>
|
<List
|
||||||
{status === 'loaded' ? (
|
contentContainerStyle={styles.childListContainer}
|
||||||
<List
|
data={childList}
|
||||||
contentContainerStyle={styles.childListContainer}
|
style={styles.childList}
|
||||||
data={childList}
|
ListEmptyComponent={
|
||||||
style={styles.childList}
|
<View style={styles.emptyState}>
|
||||||
ListEmptyComponent={
|
<Text category="h2">{translate('children.noKids_title')}</Text>
|
||||||
<View style={styles.emptyState}>
|
<Text style={styles.emptyStateDescription}>
|
||||||
<Text category="h2">{translate('children.noKids_title')}</Text>
|
{translate('children.noKids_description')}
|
||||||
<Text style={styles.emptyStateDescription}>
|
</Text>
|
||||||
{translate('children.noKids_description')}
|
|
||||||
</Text>
|
|
||||||
<Image
|
|
||||||
accessibilityIgnoresInvertColors={false}
|
|
||||||
source={require('../assets/children.png')}
|
|
||||||
style={styles.emptyStateImage as ImageStyle}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
renderItem={({ item: child, index }: ListRenderItemInfo<Child>) => (
|
|
||||||
<ChildListItem
|
|
||||||
child={child}
|
|
||||||
color={colors[index % colors.length]}
|
|
||||||
key={child.id}
|
|
||||||
updated={updatedAt}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<View style={styles.loading}>
|
|
||||||
<Image
|
<Image
|
||||||
accessibilityIgnoresInvertColors={false}
|
accessibilityIgnoresInvertColors={false}
|
||||||
source={require('../assets/girls.png')}
|
source={require('../assets/children.png')}
|
||||||
style={styles.loadingImage as ImageStyle}
|
style={styles.emptyStateImage as ImageStyle}
|
||||||
/>
|
/>
|
||||||
{status === 'error' ? (
|
</View>
|
||||||
<View style={styles.errorMessage}>
|
}
|
||||||
<Text category="h5">
|
renderItem={({ item: child, index }: ListRenderItemInfo<Child>) => (
|
||||||
{translate('children.loadingErrorHeading')}
|
<ChildListItem
|
||||||
</Text>
|
child={child}
|
||||||
<Text style={{ fontSize: Sizing.t4 }}>
|
color={colors[index % colors.length]}
|
||||||
{translate('children.loadingErrorInformationText')}
|
currentDate={currentDate}
|
||||||
</Text>
|
updated={updatedAt}
|
||||||
<View style={styles.errorButtons}>
|
key={child.id}
|
||||||
<Button status="success" onPress={() => reloadChildren()}>
|
/>
|
||||||
{translate('children.tryAgain')}
|
)}
|
||||||
</Button>
|
/>
|
||||||
<Button
|
) : (
|
||||||
status="basic"
|
<View style={styles.loading}>
|
||||||
onPress={() =>
|
<Image
|
||||||
Linking.openURL('https://skolplattformen.org/status')
|
accessibilityIgnoresInvertColors={false}
|
||||||
}
|
source={require('../assets/girls.png')}
|
||||||
>
|
style={styles.loadingImage as ImageStyle}
|
||||||
{translate('children.viewStatus')}
|
/>
|
||||||
</Button>
|
{status === 'error' ? (
|
||||||
<Button onPress={() => logout()}>
|
<View style={styles.errorMessage}>
|
||||||
{translate('general.logout')}
|
<Text category="h5">{translate('children.loadingErrorHeading')}</Text>
|
||||||
</Button>
|
<Text style={{ fontSize: Sizing.t4 }}>
|
||||||
</View>
|
{translate('children.loadingErrorInformationText')}
|
||||||
</View>
|
</Text>
|
||||||
) : (
|
<View style={styles.errorButtons}>
|
||||||
<View style={styles.loadingMessage}>
|
<Button status="success" onPress={() => reloadChildren()}>
|
||||||
<Spinner size="large" status="primary" />
|
{translate('children.tryAgain')}
|
||||||
<Text category="h1" style={styles.loadingText}>
|
</Button>
|
||||||
{translate('general.loading')}
|
<Button
|
||||||
</Text>
|
status="basic"
|
||||||
</View>
|
onPress={() =>
|
||||||
)}
|
Linking.openURL('https://skolplattformen.org/status')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{translate('children.viewStatus')}
|
||||||
|
</Button>
|
||||||
|
<Button onPress={() => logout()}>
|
||||||
|
{translate('general.logout')}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.loadingMessage}>
|
||||||
|
<Spinner size="large" status="primary" />
|
||||||
|
<Text category="h1" style={styles.loadingText}>
|
||||||
|
{translate('general.loading')}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Child } from '@skolplattformen/api'
|
||||||
import { useTimetable } from '@skolplattformen/hooks'
|
import { useTimetable } from '@skolplattformen/hooks'
|
||||||
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
|
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
|
||||||
import moment, { Moment } from 'moment'
|
import moment, { Moment } from 'moment'
|
||||||
import React from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { LanguageService } from '../services/languageService'
|
import { LanguageService } from '../services/languageService'
|
||||||
import { translate } from '../utils/translation'
|
import { translate } from '../utils/translation'
|
||||||
|
@ -12,9 +12,13 @@ interface DaySummaryProps {
|
||||||
date?: Moment
|
date?: Moment
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DaySummary = ({ child, date = moment() }: DaySummaryProps) => {
|
export const DaySummary = ({
|
||||||
|
child,
|
||||||
|
date: currentDate = moment(),
|
||||||
|
}: DaySummaryProps) => {
|
||||||
const styles = useStyleSheet(themedStyles)
|
const styles = useStyleSheet(themedStyles)
|
||||||
const [year, week] = [moment().isoWeekYear(), moment().isoWeek()]
|
const [week, year] = [currentDate.isoWeek(), currentDate.isoWeekYear()]
|
||||||
|
|
||||||
const { data: weekLessons } = useTimetable(
|
const { data: weekLessons } = useTimetable(
|
||||||
child,
|
child,
|
||||||
week,
|
week,
|
||||||
|
@ -23,7 +27,7 @@ export const DaySummary = ({ child, date = moment() }: DaySummaryProps) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const lessons = weekLessons
|
const lessons = weekLessons
|
||||||
.filter((lesson) => lesson.dayOfWeek === date.isoWeekday())
|
.filter((lesson) => lesson.dayOfWeek === currentDate.isoWeekday())
|
||||||
.sort((a, b) => a.dateStart.localeCompare(b.dateStart))
|
.sort((a, b) => a.dateStart.localeCompare(b.dateStart))
|
||||||
|
|
||||||
if (lessons.length <= 0) {
|
if (lessons.length <= 0) {
|
||||||
|
@ -53,15 +57,21 @@ export const DaySummary = ({ child, date = moment() }: DaySummaryProps) => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
<View style={styles.part}>
|
||||||
|
<View>
|
||||||
|
<Text category="c2" style={styles.label}>
|
||||||
|
|
||||||
|
</Text>
|
||||||
|
<Text category="s2">
|
||||||
|
{gymBag
|
||||||
|
? ` 🤼♀️ ${translate('schedule.gymBag', {
|
||||||
|
defaultValue: 'Gympapåse',
|
||||||
|
})}`
|
||||||
|
: ''}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text category="s2">
|
|
||||||
{gymBag
|
|
||||||
? ` 🤼♀️ ${translate('schedule.gymBag', {
|
|
||||||
defaultValue: 'Gympapåse',
|
|
||||||
})}`
|
|
||||||
: ''}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -76,4 +86,7 @@ const themedStyles = StyleService.create({
|
||||||
label: {
|
label: {
|
||||||
marginTop: 10,
|
marginTop: 10,
|
||||||
},
|
},
|
||||||
|
heading: {
|
||||||
|
marginBottom: -10,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { View } from 'react-native'
|
||||||
import { LanguageService } from '../services/languageService'
|
import { LanguageService } from '../services/languageService'
|
||||||
import { Sizing, Typography } from '../styles'
|
import { Sizing, Typography } from '../styles'
|
||||||
import { TransitionView } from './transitionView.component'
|
import { TransitionView } from './transitionView.component'
|
||||||
|
import { getMeaningfulStartingDate } from '../utils/calendarHelpers'
|
||||||
|
|
||||||
interface WeekProps {
|
interface WeekProps {
|
||||||
child: Child
|
child: Child
|
||||||
|
@ -107,10 +108,12 @@ export const Day = ({ weekDay, lunch, lessons }: DayProps) => {
|
||||||
export const Week = ({ child }: WeekProps) => {
|
export const Week = ({ child }: WeekProps) => {
|
||||||
moment.locale(LanguageService.getLocale())
|
moment.locale(LanguageService.getLocale())
|
||||||
const days = moment.weekdaysShort().slice(1, 6)
|
const days = moment.weekdaysShort().slice(1, 6)
|
||||||
const currentDayIndex = Math.min(moment().isoWeekday() - 1, 5)
|
const displayDate = getMeaningfulStartingDate()
|
||||||
|
|
||||||
|
const currentDayIndex = Math.min(moment(displayDate).isoWeekday() - 1, 5)
|
||||||
const [selectedIndex, setSelectedIndex] = useState(currentDayIndex)
|
const [selectedIndex, setSelectedIndex] = useState(currentDayIndex)
|
||||||
const [showSchema, setShowSchema] = useState(false)
|
const [showSchema, setShowSchema] = useState(false)
|
||||||
const [year, week] = [moment().isoWeekYear(), moment().isoWeek()]
|
const [year, week] = [displayDate.isoWeekYear(), displayDate.isoWeek()]
|
||||||
const { data: lessons } = useTimetable(
|
const { data: lessons } = useTimetable(
|
||||||
child,
|
child,
|
||||||
week,
|
week,
|
||||||
|
|
|
@ -80,7 +80,8 @@
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"socialSecurityNumber": "Personal identity number",
|
"socialSecurityNumber": "Personal identity number",
|
||||||
"title": "Öppna skolplattformen"
|
"title": "Öppna skolplattformen",
|
||||||
|
"tomorrow": "Tomorrow"
|
||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"changeLanguage": "Change language",
|
"changeLanguage": "Change language",
|
||||||
|
|
|
@ -82,7 +82,8 @@
|
||||||
"title": "Öppna skolplattformen",
|
"title": "Öppna skolplattformen",
|
||||||
"cancel": "Avbryt",
|
"cancel": "Avbryt",
|
||||||
"logoutAndClearAllDataInclSettings": "Logga ut och rensa all sparad data inkl inställningar",
|
"logoutAndClearAllDataInclSettings": "Logga ut och rensa all sparad data inkl inställningar",
|
||||||
"logoutAndClearPersonalData": "Logga ut och rensa all personlig data"
|
"logoutAndClearPersonalData": "Logga ut och rensa all personlig data",
|
||||||
|
"tomorrow": "Imorgon"
|
||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"changeLanguage": "Byt språk",
|
"changeLanguage": "Byt språk",
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
export const getMeaningfulStartingDate = (date = moment()) => {
|
||||||
|
// are we on the evening?
|
||||||
|
if (date.hour() > 17) date = date.add('1', 'day')
|
||||||
|
// are we on the weekend
|
||||||
|
if (date.isoWeekday() > 5) date = date.add(5, 'days').startOf('isoWeek')
|
||||||
|
return date
|
||||||
|
}
|
|
@ -84,6 +84,11 @@ export const timetable = (
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
throw new Error(response.error)
|
throw new Error(response.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!response.data.lessonInfo){
|
||||||
|
throw new Error("Empty lessonInfo received")
|
||||||
|
}
|
||||||
|
|
||||||
return response.data.lessonInfo.map((entry) =>
|
return response.data.lessonInfo.map((entry) =>
|
||||||
timetableEntry(entry, year, week, lang)
|
timetableEntry(entry, year, week, lang)
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue