Merge branch 'main' into feat/abscense-registration-hjarntorget

This commit is contained in:
Emil Hellman 2021-12-15 09:48:00 +01:00 committed by GitHub
commit 957650699c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 2851 additions and 1886 deletions

View File

@ -1,3 +1,107 @@
## [2.10.2](https://github.com/kolplattformen/skolplattformen/compare/v2.10.1...v2.10.2) (2021-12-11)
### Bug Fixes
* 🐛 adjust fakeData lessons offset in ht ([#598](https://github.com/kolplattformen/skolplattformen/issues/598)) ([3f69bc8](https://github.com/kolplattformen/skolplattformen/commit/3f69bc89aaa7eb1cf4ade789551667b960ff8825))
## [2.10.1](https://github.com/kolplattformen/skolplattformen/compare/v2.10.0...v2.10.1) (2021-12-10)
### Bug Fixes
* 🐛 scroll in timetable ([#597](https://github.com/kolplattformen/skolplattformen/issues/597)) ([f844a3c](https://github.com/kolplattformen/skolplattformen/commit/f844a3cdd4ea26b6a4b76e4236debec7e6e83986))
# [2.10.0](https://github.com/kolplattformen/skolplattformen/compare/v2.9.1...v2.10.0) (2021-12-10)
### Features
* 🎸 Ämnesnamn syns nu i schemat i Hjärntorget. ([#596](https://github.com/kolplattformen/skolplattformen/issues/596)) ([14ff985](https://github.com/kolplattformen/skolplattformen/commit/14ff985b0c7a0c9ab9c8547679caff66c7b5aa1f))
## [2.9.1](https://github.com/kolplattformen/skolplattformen/compare/v2.9.0...v2.9.1) (2021-12-10)
### Bug Fixes
* 🐛 Felaktig sortering av lektioner ([#595](https://github.com/kolplattformen/skolplattformen/issues/595)) ([ed3a27b](https://github.com/kolplattformen/skolplattformen/commit/ed3a27bba185b39fd8581b639ac7efc03b050d6e)), closes [#591](https://github.com/kolplattformen/skolplattformen/issues/591)
# [2.9.0](https://github.com/kolplattformen/skolplattformen/compare/v2.8.0...v2.9.0) (2021-12-10)
### Features
* 🎸 Hämta lärare och skolkontakter från api-skolplattfomen och visa lärarens namn i schemat ([#589](https://github.com/kolplattformen/skolplattformen/issues/589)) ([b7dbd35](https://github.com/kolplattformen/skolplattformen/commit/b7dbd356c652bf53a9d73dd38f11744ff364470b))
# [2.8.0](https://github.com/kolplattformen/skolplattformen/compare/v2.7.3...v2.8.0) (2021-12-08)
### Bug Fixes
* 🐛 Ingen text syns i dark mode när inga barn hittas ([bce1165](https://github.com/kolplattformen/skolplattformen/commit/bce1165f300749502351fe28ee8f0c5f3ce7ee3c)), closes [#571](https://github.com/kolplattformen/skolplattformen/issues/571)
### Features
* 🎸 bumped to 3.0.2 for new version releasea ([2af189e](https://github.com/kolplattformen/skolplattformen/commit/2af189ef0fb23723dae982382bee169e2c7ed8f6))
## [2.7.3](https://github.com/kolplattformen/skolplattformen/compare/v2.7.2...v2.7.3) (2021-12-07)
### Bug Fixes
* 🐛 Store childs personalIdNumbers in settings ([cc3fd86](https://github.com/kolplattformen/skolplattformen/commit/cc3fd8670c37be2b5f3e9f642bb3b1882350e2f6))
## [2.7.2](https://github.com/kolplattformen/skolplattformen/compare/v2.7.1...v2.7.2) (2021-12-06)
### Bug Fixes
* 🐛 Add missing translations for calendar ([7ada994](https://github.com/kolplattformen/skolplattformen/commit/7ada9945dfc7dd5766cd7a3208d62cf4c1a2659f))
## [2.7.1](https://github.com/kolplattformen/skolplattformen/compare/v2.7.0...v2.7.1) (2021-12-06)
### Bug Fixes
* 🐛 Add translation to week ([cfa39de](https://github.com/kolplattformen/skolplattformen/commit/cfa39de3934e2386d90f37ad2c3dc830106e175a))
# [2.7.0](https://github.com/kolplattformen/skolplattformen/compare/v2.6.0...v2.7.0) (2021-12-06)
### Features
* 🎸 Add week number and calendar dates to timetable ([6fbbcc8](https://github.com/kolplattformen/skolplattformen/commit/6fbbcc803e92e288904be1ffba5e380bd9523100))
# [2.6.0](https://github.com/kolplattformen/skolplattformen/compare/v2.5.1...v2.6.0) (2021-12-04)
### Features
* Frontpage date tweaks ([#582](https://github.com/kolplattformen/skolplattformen/issues/582)) ([66e7811](https://github.com/kolplattformen/skolplattformen/commit/66e7811b83e96f9fb83aa82cb34736f38a3bf16a))
## [2.5.1](https://github.com/kolplattformen/skolplattformen/compare/v2.5.0...v2.5.1) (2021-12-03)
### Bug Fixes
* 🐛 Show the weekday on startpage if not today ([df28066](https://github.com/kolplattformen/skolplattformen/commit/df2806648ab6b573d277a338f76fb199cdd307a2))
# [2.5.0](https://github.com/kolplattformen/skolplattformen/compare/v2.4.0...v2.5.0) (2021-12-03)
### Features
* 🎸 Skip to the next day in calendar ([#425](https://github.com/kolplattformen/skolplattformen/issues/425)) ([fce1d98](https://github.com/kolplattformen/skolplattformen/commit/fce1d98847f4cc7c27bfa359b1d2b1bdc86e12ea))
# [2.4.0](https://github.com/kolplattformen/skolplattformen/compare/v2.3.2...v2.4.0) (2021-12-02)
### Features
* 🎸 Clear personal cache on login and logout ([#572](https://github.com/kolplattformen/skolplattformen/issues/572)) ([bf29ab5](https://github.com/kolplattformen/skolplattformen/commit/bf29ab58edd13db62b34f873a3429d319c0e7297))
* 🎸 Fix image load and typescript errors ([#570](https://github.com/kolplattformen/skolplattformen/issues/570)) ([933a884](https://github.com/kolplattformen/skolplattformen/commit/933a8840a3ebd049711a000ca6faf3e534f77ace))
## [2.3.2](https://github.com/kolplattformen/skolplattformen/compare/v2.3.1...v2.3.2) (2021-12-01) ## [2.3.2](https://github.com/kolplattformen/skolplattformen/compare/v2.3.1...v2.3.2) (2021-12-01)

View File

@ -14,9 +14,13 @@ const path = require('path')
const fs = require('fs') const fs = require('fs')
const HttpProxyAgent = require('https-proxy-agent') const HttpProxyAgent = require('https-proxy-agent')
const agentWrapper = require('./app/agentFetchWrapper') const agentWrapper = require('./app/agentFetchWrapper')
const init = require('@skolplattformen/api-skolplattformen').default const initSkolplattformen = require('@skolplattformen/api-skolplattformen').default
const initHjarntorget = require('@skolplattformen/api-hjarntorget').default
const [, , personalNumber, platform] = process.argv
const isHjarntorget = platform && platform.startsWith('hj')
const init = isHjarntorget ? initHjarntorget : initSkolplattformen;
const [, , personalNumber] = process.argv
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
const cookieJar = new CookieJar() const cookieJar = new CookieJar()
let bankIdUsed = false let bankIdUsed = false
@ -136,10 +140,9 @@ async function Login(api) {
try { try {
console.log('Attempt to use saved session cookie to login') console.log('Attempt to use saved session cookie to login')
const rawContent = await readFile(`${recordFolder}/latestSessionCookie.txt`) const rawContent = await readFile(`${recordFolder}/latestSessionCookie.txt`)
const sessionCookie = JSON.parse(rawContent) const sessionCookies = JSON.parse(rawContent)
await api.setSessionCookie(`${sessionCookies[0].key}=${sessionCookies[0].value}`)
await api.setSessionCookie(`${sessionCookie.key}=${sessionCookie.value}`)
useBankId = false useBankId = false
console.log('Login with old cookie succeeded') console.log('Login with old cookie succeeded')
} catch (error) { } catch (error) {
@ -177,10 +180,12 @@ function ensureDirectoryExistence(filePath) {
fs.mkdirSync(dirname) fs.mkdirSync(dirname)
} }
function getSessionCookieFromCookieJar() { function getSessionCookieFromCookieJar() {
const cookies = cookieJar.getCookiesSync('https://etjanst.stockholm.se') const cookieUrl = isHjarntorget ? 'https://hjarntorget.goteborg.se' : 'https://etjanst.stockholm.se'
const sessionCookie = cookies.find((c) => c.key === 'SMSESSION') const cookies = cookieJar.getCookiesSync(cookieUrl)
return sessionCookie const sessionCookieKey = isHjarntorget ? 'JSESSIONID' : 'SMSESSION'
return cookies.find(c => c.key === sessionCookieKey)
} }
const record = async (info, data) => { const record = async (info, data) => {

View File

@ -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"],

View File

@ -19,13 +19,16 @@ import { translations } from './utils/translation'
const reporter: Reporter | undefined = __DEV__ const reporter: Reporter | undefined = __DEV__
? { ? {
log: (message: string) => console.log(message), log: (message: string) => console.log(message),
error: (error: Error, label?: string) => console.error(label, error), error: (error: Error, label?: string) => console.log(label, error),
} }
: undefined : undefined
if (__DEV__) { if (__DEV__) {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const DevMenu = require('react-native-dev-menu') const DevMenu = require('react-native-dev-menu')
DevMenu.addItem('Clear AsyncStorage from all contents', () =>
AsyncStorage.clear().then(() => logAsyncStorage())
)
DevMenu.addItem('Log AsyncStorage contents', () => logAsyncStorage()) DevMenu.addItem('Log AsyncStorage contents', () => logAsyncStorage())
} }

View File

@ -134,7 +134,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 20000 versionCode 20000
versionName "3.0.1" versionName "3.0.3"
} }
splits { splits {
abi { abi {

View File

@ -18,6 +18,7 @@ import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
import * as Yup from 'yup' import * as Yup from 'yup'
import { defaultStackStyling } from '../design/navigationThemes' import { defaultStackStyling } from '../design/navigationThemes'
import usePersonalStorage from '../hooks/usePersonalStorage' import usePersonalStorage from '../hooks/usePersonalStorage'
import useSettingsStorage from '../hooks/useSettingsStorage'
import { Layout as LayoutStyle, Sizing, Typography } from '../styles' import { Layout as LayoutStyle, Sizing, Typography } from '../styles'
import { studentName } from '../utils/peopleHelpers' import { studentName } from '../utils/peopleHelpers'
import { useSMS } from '../utils/SMS' import { useSMS } from '../utils/SMS'
@ -70,12 +71,11 @@ const Absence = () => {
const route = useRoute<AbsenceRouteProps>() const route = useRoute<AbsenceRouteProps>()
const { sendSMS } = useSMS() const { sendSMS } = useSMS()
const { child } = route.params const { child } = route.params
const [personalIdFromStorage, setPersonalIdInStorage] = usePersonalStorage(
user,
`@childssn.${child.id}`,
''
)
const [personalIdentityNumber, setPersonalIdentityNumber] = React.useState('') const [personalIdentityNumber, setPersonalIdentityNumber] = React.useState('')
const [personalIdsFromStorage, setPersonalIdInStorage] = useSettingsStorage(
'childPersonalIdentityNumber'
)
const personalIdKey = `@childPersonalIdNumber.${child.id}`
const minumumDate = moment().hours(8).minute(0) const minumumDate = moment().hours(8).minute(0)
const maximumDate = moment().hours(17).minute(0) const maximumDate = moment().hours(17).minute(0)
const styles = useStyleSheet(themedStyles) const styles = useStyleSheet(themedStyles)
@ -96,15 +96,19 @@ const Absence = () => {
) )
} }
setPersonalIdInStorage(values.personalIdentityNumber) const toStore = {
setPersonalIdentityNumber(values.personalIdentityNumber) ...personalIdsFromStorage,
...{ [personalIdKey]: personalIdNumber },
}
setPersonalIdInStorage(toStore)
}, },
[sendSMS, setPersonalIdInStorage] [personalIdKey, personalIdsFromStorage, sendSMS, setPersonalIdInStorage]
) )
React.useEffect(() => { React.useEffect(() => {
const personalIdFromStorage = personalIdsFromStorage[personalIdKey] || ''
setPersonalIdentityNumber(personalIdFromStorage || '') setPersonalIdentityNumber(personalIdFromStorage || '')
}, [child, personalIdFromStorage, user]) }, [child, personalIdKey, personalIdsFromStorage, user])
const initialValues: AbsenceFormValues = { const initialValues: AbsenceFormValues = {
displayStartTimePicker: false, displayStartTimePicker: false,

View File

@ -1,5 +1,5 @@
import { CalendarItem } from '@skolplattformen/api-skolplattformen'
import { useCalendar } from '@skolplattformen/hooks' import { useCalendar } from '@skolplattformen/hooks'
import { CalendarItem } from '@skolplattformen/api'
import { import {
Divider, Divider,
List, List,
@ -10,8 +10,9 @@ import {
} from '@ui-kitten/components' } from '@ui-kitten/components'
import moment from 'moment' import moment from 'moment'
import React from 'react' import React from 'react'
import { ListRenderItemInfo, View } from 'react-native' import { ListRenderItemInfo, RefreshControl, View } from 'react-native'
import { Typography } from '../styles' import { Layout as LayoutStyle, Sizing, Typography } from '../styles'
import { translate } from '../utils/translation'
import { useChild } from './childContext.component' import { useChild } from './childContext.component'
import { CalendarOutlineIcon } from './icon.component' import { CalendarOutlineIcon } from './icon.component'
import { SaveToCalendar } from './saveToCalendar.component' import { SaveToCalendar } from './saveToCalendar.component'
@ -19,7 +20,7 @@ import { Week } from './week.component'
export const Calendar = () => { export const Calendar = () => {
const child = useChild() const child = useChild()
const { data } = useCalendar(child) const { data, status, reload } = useCalendar(child)
const styles = useStyleSheet(themedStyles) const styles = useStyleSheet(themedStyles)
const formatStartDate = (startDate: moment.MomentInput) => { const formatStartDate = (startDate: moment.MomentInput) => {
@ -28,37 +29,55 @@ export const Calendar = () => {
'll' 'll'
)} ${date.fromNow()}` )} ${date.fromNow()}`
// Hack to remove yarn if it is this year // Hack to remove year if it is this year
const currentYear = moment().year().toString(10) const currentYear = moment().year().toString(10)
return output.replace(currentYear, '') return output.replace(currentYear, '')
} }
const sortedData = () => {
if (!data) return []
return data.sort((a, b) =>
a.startDate && b.startDate ? a.startDate.localeCompare(b.startDate) : 0
)
}
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Week child={child} /> <Week child={child} />
{data && data.length > 0 && ( <List
<List data={sortedData()}
data={data.sort((a, b) => ItemSeparatorComponent={Divider}
a.startDate && b.startDate ListEmptyComponent={
? a.startDate.localeCompare(b.startDate) <View style={styles.emptyState}>
: 0 <Text style={styles.emptyStateHeadline} category="h6">
)} {translate('calender.emptyHeadline')}
ItemSeparatorComponent={Divider} </Text>
renderItem={({ item }: ListRenderItemInfo<CalendarItem>) => ( <Text style={styles.emptyStateDescription}>
<ListItem {translate('calender.emptyText')}
disabled={true} </Text>
title={`${item.title}`} </View>
description={(props) => ( }
<Text style={[props?.style, styles.description]}> renderItem={({ item }: ListRenderItemInfo<CalendarItem>) => (
{formatStartDate(item.startDate)} <ListItem
</Text> disabled={true}
)} title={`${item.title}`}
accessoryLeft={CalendarOutlineIcon} description={(props) => (
accessoryRight={() => <SaveToCalendar event={item} />} <Text style={[props?.style, styles.description]}>
/> {formatStartDate(item.startDate)}
)} </Text>
/> )}
)} accessoryLeft={CalendarOutlineIcon}
accessoryRight={() => <SaveToCalendar event={item} />}
/>
)}
refreshControl={
<RefreshControl
refreshing={status === 'loading'}
onRefresh={reload}
/>
}
/>
</View> </View>
) )
} }
@ -73,4 +92,18 @@ const themedStyles = StyleService.create({
...Typography.fontSize.xs, ...Typography.fontSize.xs,
color: 'text-hint-color', color: 'text-hint-color',
}, },
emptyState: {
...LayoutStyle.center,
...LayoutStyle.flex.full,
},
emptyStateHeadline: {
...Typography.align.center,
margin: Sizing.t4,
},
emptyStateDescription: {
...Typography.align.center,
lineHeight: 21,
paddingHorizontal: Sizing.t3,
margin: Sizing.t4,
},
}) })

View File

@ -1,4 +1,4 @@
import { Child } from '@skolplattformen/api-skolplattformen' import { Child } from '@skolplattformen/api'
import React, { createContext, useContext } from 'react' import React, { createContext, useContext } from 'react'
interface ChildProviderProps { interface ChildProviderProps {

View File

@ -1,7 +1,7 @@
/* eslint-disable react-native-a11y/has-accessibility-hint */ /* eslint-disable react-native-a11y/has-accessibility-hint */
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack' import { StackNavigationProp } from '@react-navigation/stack'
import { Child } from '@skolplattformen/api-skolplattformen' import { Child } from '@skolplattformen/api'
import { import {
useCalendar, useCalendar,
useClassmates, useClassmates,
@ -16,11 +16,12 @@ import {
Text, Text,
useStyleSheet, useStyleSheet,
} from '@ui-kitten/components' } from '@ui-kitten/components'
import moment from 'moment' import moment, { Moment } from 'moment'
import React 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'
import { Colors, Layout, Sizing } from '../styles' import { Colors, Layout, Sizing } from '../styles'
import { getMeaningfulStartingDate } from '../utils/calendarHelpers'
import { studentName } from '../utils/peopleHelpers' import { studentName } from '../utils/peopleHelpers'
import { DaySummary } from './daySummary.component' import { DaySummary } from './daySummary.component'
import { AlertIcon, RightArrowIcon } from './icon.component' import { AlertIcon, RightArrowIcon } from './icon.component'
@ -30,13 +31,20 @@ import { StudentAvatar } from './studentAvatar.component'
interface ChildListItemProps { interface ChildListItemProps {
child: Child child: Child
color: string color: string
updated: string
currentDate?: Moment
} }
type ChildListItemNavigationProp = StackNavigationProp< type ChildListItemNavigationProp = StackNavigationProp<
RootStackParamList, RootStackParamList,
'Children' 'Children'
> >
export const ChildListItem = ({ child, color }: ChildListItemProps) => { export const ChildListItem = ({
child,
color,
updated,
currentDate = moment(),
}: ChildListItemProps) => {
// Forces rerender when child.id changes // Forces rerender when child.id changes
React.useEffect(() => { React.useEffect(() => {
// noop // noop
@ -44,17 +52,36 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
const navigation = useNavigation<ChildListItemNavigationProp>() const navigation = useNavigation<ChildListItemNavigationProp>()
const { t } = useTranslation() const { t } = useTranslation()
const { data: notifications } = useNotifications(child) const { data: notifications, reload: notificationsReload } =
const { data: news } = useNews(child) useNotifications(child)
const { data: classmates } = useClassmates(child) const { data: news, status: newsStatus, reload: newsReload } = useNews(child)
const { data: calendar } = useCalendar(child) const { data: classmates, reload: classmatesReload } = useClassmates(child)
const { data: menu } = useMenu(child) const { data: calendar, reload: calendarReload } = useCalendar(child)
const { data: schedule } = useSchedule( const { data: menu, reload: menuReload } = useMenu(child)
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(() => {
// Do not refresh if updated is empty (first render of component)
if (updated === '') return
newsReload()
classmatesReload()
notificationsReload()
calendarReload()
menuReload()
scheduleReload()
// Without eslint-disable below we get into a forever loop
// because the function pointers to reload functions change on every reload.
// I do not know a workaround for this.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updated])
const notificationsThisWeek = notifications.filter( const notificationsThisWeek = notifications.filter(
({ dateCreated, dateModified }) => { ({ dateCreated, dateModified }) => {
const date = dateModified || dateCreated const date = dateModified || dateCreated
@ -63,8 +90,8 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
) )
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 = [
@ -73,14 +100,14 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
].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 getClassName = () => { const getClassName = () => {
@ -118,6 +145,16 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
const className = getClassName() const className = getClassName()
const styles = useStyleSheet(themeStyles) const styles = useStyleSheet(themeStyles)
const isDarkMode = useColorScheme() === 'dark' const isDarkMode = useColorScheme() === 'dark'
const meaningfulStartingDate = getMeaningfulStartingDate(currentDate)
// Hide menu if we want to show monday but it is not monday yet.
// The menu for next week is not available until monday
const shouldShowLunchMenu =
menu[meaningfulStartingDate.isoWeekday() - 1] &&
!(
meaningfulStartingDate.isoWeekday() === 1 &&
currentDate.isoWeekday() !== 1
)
return ( return (
<TouchableOpacity <TouchableOpacity
@ -142,12 +179,15 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
/> />
</View> </View>
</View> </View>
<DaySummary child={child} />
<DaySummary child={child} date={meaningfulStartingDate} />
{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>
@ -156,11 +196,13 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
{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 : (
@ -168,16 +210,20 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
{t('news.noNewNewsItemsThisWeek')} {t('news.noNewNewsItemsThisWeek')}
</Text> </Text>
)} )}
{shouldShowLunchMenu ? (
{!menu[moment().isoWeekday() - 1] ? null : (
<> <>
<Text category="c2" style={styles.label}> <Text category="c2" style={styles.label}>
{t('schedule.lunch')} {meaningfulStartingDate.format(
'[' + t('schedule.lunch') + '] dddd'
)}
</Text>
<Text>
{menu[meaningfulStartingDate.isoWeekday() - 1]?.description}
</Text> </Text>
<Text>{menu[moment().isoWeekday() - 1]?.description}</Text>
</> </>
)} ) : null}
<View style={styles.itemFooterAbsence}>
<View style={styles.itemFooter}>
<Button <Button
accessible accessible
accessibilityRole="button" accessibilityRole="button"
@ -232,15 +278,16 @@ const themeStyles = StyleService.create({
}, },
itemFooter: { itemFooter: {
...Layout.flex.row, ...Layout.flex.row,
marginTop: Sizing.t4, justifyContent: 'space-between',
}, alignItems: 'flex-end',
itemFooterAbsence: {
...Layout.mainAxis.flexStart,
marginTop: Sizing.t4, marginTop: Sizing.t4,
}, },
absenceButton: { absenceButton: {
marginLeft: -20, marginLeft: -20,
}, },
itemFooterSpinner: {
alignSelf: 'flex-end',
},
item: { item: {
marginRight: 12, marginRight: 12,
paddingHorizontal: 2, paddingHorizontal: 2,

View File

@ -1,5 +1,5 @@
import { useNavigation } from '@react-navigation/core' import { useNavigation } from '@react-navigation/core'
import { Child } from '@skolplattformen/api-skolplattformen' import { Child } from '@skolplattformen/api'
import { useApi, useChildList } from '@skolplattformen/hooks' import { useApi, useChildList } from '@skolplattformen/hooks'
import { import {
Button, Button,
@ -10,7 +10,8 @@ import {
TopNavigationAction, TopNavigationAction,
useStyleSheet, useStyleSheet,
} from '@ui-kitten/components' } from '@ui-kitten/components'
import React, { useCallback, useEffect } from 'react' import moment from 'moment'
import React, { useCallback, useEffect, useState } from 'react'
import { import {
Image, Image,
ImageStyle, ImageStyle,
@ -24,7 +25,7 @@ import AppStorage from '../services/appStorage'
import { Colors, Layout as LayoutStyle, Sizing, Typography } from '../styles' 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 } from './icon.component' import { SettingsIcon, RefreshIcon } from './icon.component'
const colors = ['primary', 'success', 'info', 'warning', 'danger'] const colors = ['primary', 'success', 'info', 'warning', 'danger']
@ -45,9 +46,12 @@ export const Children = () => {
const { api } = useApi() const { api } = useApi()
const { data: childList, status, reload } = useChildList() const { data: childList, status, reload } = useChildList()
const reloadChildren = () => { const reloadChildren = useCallback(() => {
reload() reload()
} setUpdated(moment().toISOString())
}, [reload])
const [updatedAt, setUpdated] = useState('')
const logout = useCallback(() => { const logout = useCallback(() => {
AppStorage.clearTemporaryItems().then(() => api.logout()) AppStorage.clearTemporaryItems().then(() => api.logout())
@ -63,82 +67,87 @@ export const Children = () => {
/> />
) )
}, },
headerRight: () => {
return (
<TopNavigationAction
icon={RefreshIcon}
onPress={() => reloadChildren()}
accessibilityHint="Reload"
accessibilityLabel="Reload"
/>
)
},
}) })
}, [navigation]) }, [navigation, reloadChildren])
// 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}
/>
)}
/>
) : (
<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')} updated={updatedAt}
</Text> key={child.id}
<View style={styles.errorButtons}> />
<Button status="success" onPress={() => reloadChildren()}> )}
{translate('children.tryAgain')} />
</Button> ) : (
<Button <View style={styles.loading}>
status="basic" <Image
onPress={() => accessibilityIgnoresInvertColors={false}
Linking.openURL('https://skolplattformen.org/status') source={require('../assets/girls.png')}
} style={styles.loadingImage as ImageStyle}
> />
{translate('children.viewStatus')} {status === 'error' ? (
</Button> <View style={styles.errorMessage}>
<Button onPress={() => logout()}> <Text category="h5">{translate('children.loadingErrorHeading')}</Text>
{translate('general.logout')} <Text style={{ fontSize: Sizing.t4 }}>
</Button> {translate('children.loadingErrorInformationText')}
</View> </Text>
</View> <View style={styles.errorButtons}>
) : ( <Button status="success" onPress={() => reloadChildren()}>
<View style={styles.loadingMessage}> {translate('children.tryAgain')}
<Spinner size="large" status="primary" /> </Button>
<Text category="h1" style={styles.loadingText}> <Button
{translate('general.loading')} status="basic"
</Text> onPress={() =>
</View> 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>
) )
} }
@ -187,7 +196,6 @@ const themedStyles = StyleService.create({
emptyState: { emptyState: {
...LayoutStyle.center, ...LayoutStyle.center,
...LayoutStyle.flex.full, ...LayoutStyle.flex.full,
backgroundColor: Colors.neutral.white,
paddingHorizontal: Sizing.t5, paddingHorizontal: Sizing.t5,
}, },
emptyStateDescription: { emptyStateDescription: {

View File

@ -1,4 +1,4 @@
import { Classmate } from '@skolplattformen/api-skolplattformen' import { Classmate } from '@skolplattformen/api'
import { useClassmates } from '@skolplattformen/hooks' import { useClassmates } from '@skolplattformen/hooks'
import { import {
Divider, Divider,
@ -9,7 +9,7 @@ import {
Text, Text,
} from '@ui-kitten/components' } from '@ui-kitten/components'
import React from 'react' import React from 'react'
import { ListRenderItemInfo, StyleSheet } from 'react-native' import { ListRenderItemInfo, RefreshControl, StyleSheet } from 'react-native'
import { fullName, guardians, sortByFirstName } from '../utils/peopleHelpers' import { fullName, guardians, sortByFirstName } from '../utils/peopleHelpers'
import { translate } from '../utils/translation' import { translate } from '../utils/translation'
import { useChild } from './childContext.component' import { useChild } from './childContext.component'
@ -22,7 +22,7 @@ interface ClassmatesProps {
export const Classmates = () => { export const Classmates = () => {
const child = useChild() const child = useChild()
const { data } = useClassmates(child) const { data, status, reload } = useClassmates(child)
const renderItemIcon = (props: IconProps) => ( const renderItemIcon = (props: IconProps) => (
<Icon {...props} name="people-outline" /> <Icon {...props} name="people-outline" />
) )
@ -60,6 +60,9 @@ export const Classmates = () => {
} }
renderItem={renderItem} renderItem={renderItem}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
refreshControl={
<RefreshControl refreshing={status === 'loading'} onRefresh={reload} />
}
/> />
) )
} }

View File

@ -1,5 +1,5 @@
/* eslint-disable react-native-a11y/has-accessibility-hint */ /* eslint-disable react-native-a11y/has-accessibility-hint */
import { Classmate } from '@skolplattformen/api-skolplattformen' import { Classmate } from '@skolplattformen/api'
import { import {
Button, Button,
MenuGroup, MenuGroup,

View File

@ -1,8 +1,8 @@
import { Child } from '@skolplattformen/api-skolplattformen' 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,17 @@ interface DaySummaryProps {
date?: Moment date?: Moment
} }
export const DaySummary = ({ child, date = moment() }: DaySummaryProps) => { const capitalizeFirstLetter = (string) => {
return string.charAt(0).toUpperCase() + string.slice(1)
}
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,8 +31,8 @@ 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.timeStart.localeCompare(b.timeStart))
if (lessons.length <= 0) { if (lessons.length <= 0) {
return null return null
@ -34,6 +42,11 @@ export const DaySummary = ({ child, date = moment() }: DaySummaryProps) => {
return ( return (
<View> <View>
{moment().weekday() !== currentDate.weekday() ? (
<Text category="c2" style={styles.weekday}>
{capitalizeFirstLetter(currentDate.format('dddd'))}
</Text>
) : null}
<View style={styles.summary}> <View style={styles.summary}>
<View style={styles.part}> <View style={styles.part}>
<View> <View>
@ -49,19 +62,27 @@ export const DaySummary = ({ child, date = moment() }: DaySummaryProps) => {
{translate('schedule.end')} {translate('schedule.end')}
</Text> </Text>
<Text category="h5"> <Text category="h5">
{lessons[lessons.length - 1].timeEnd.slice(0, 5)} {lessons
.sort((a, b) => a.timeEnd.localeCompare(b.timeEnd))
[lessons.length - 1].timeEnd.slice(0, 5)}
</Text>
</View>
</View>
<View style={styles.part}>
<View>
<Text category="c2" style={styles.label}>
&nbsp;
</Text>
<Text category="s2">
{gymBag
? ` 🤼‍♀️ ${translate('schedule.gymBag', {
defaultValue: 'Gympapåse',
})}`
: ''}
</Text> </Text>
</View> </View>
</View> </View>
</View> </View>
<Text category="s2">
{gymBag
? ` 🤼‍♀️ ${translate('schedule.gymBag', {
defaultValue: 'Gympapåse',
})}`
: ''}
</Text>
</View> </View>
) )
} }
@ -76,4 +97,11 @@ const themedStyles = StyleService.create({
label: { label: {
marginTop: 10, marginTop: 10,
}, },
heading: {
marginBottom: -10,
},
weekday: {
marginBottom: -10,
padding: 0,
},
}) })

View File

@ -32,3 +32,4 @@ export const ClipboardIcon = uiIcon('clipboard-outline')
export const RightArrowIcon = uiIcon('arrow-ios-forward-outline') export const RightArrowIcon = uiIcon('arrow-ios-forward-outline')
export const QuestionMarkIcon = uiIcon('question-mark') export const QuestionMarkIcon = uiIcon('question-mark')
export const AwardIcon = uiIcon('award') export const AwardIcon = uiIcon('award')
export const RefreshIcon = uiIcon('refresh')

View File

@ -31,7 +31,7 @@ export const Image = ({
resizeMode = 'contain', resizeMode = 'contain',
}: ImageProps) => { }: ImageProps) => {
const { api } = useApi() const { api } = useApi()
const [headers, setHeaders] = useState() const [headers, setHeaders] = useState<{ [index: string]: string }>()
const { width: windowWidth } = useWindowDimensions() const { width: windowWidth } = useWindowDimensions()
const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
@ -40,7 +40,7 @@ export const Image = ({
const prefetchImageInformation = useCallback( const prefetchImageInformation = useCallback(
async (url: string) => { async (url: string) => {
if (!url) return if (!url) return
const { headers: newHeaders } = await api.getSession(url) const newHeaders = await api.getSessionHeaders(url)
console.log('[IMAGE] Getting image dimensions with headers', { console.log('[IMAGE] Getting image dimensions with headers', {
debugImageName, debugImageName,

View File

@ -34,6 +34,7 @@ import {
PersonIcon, PersonIcon,
SelectIcon, SelectIcon,
} from './icon.component' } from './icon.component'
import AppStorage from '../services/appStorage'
const BankId = () => ( const BankId = () => (
<Image <Image
@ -88,6 +89,8 @@ export const Login = () => {
] as const ] as const
const loginHandler = async () => { const loginHandler = async () => {
const user = await api.getUser()
await AppStorage.clearPersonalData(user)
showModal(false) showModal(false)
} }

View File

@ -1,4 +1,4 @@
import { MenuItem } from '@skolplattformen/api-skolplattformen' import { MenuItem } from '@skolplattformen/api'
import { useMenu } from '@skolplattformen/hooks' import { useMenu } from '@skolplattformen/hooks'
import { import {
Divider, Divider,
@ -9,7 +9,13 @@ import {
} from '@ui-kitten/components' } from '@ui-kitten/components'
import 'moment/locale/sv' import 'moment/locale/sv'
import React from 'react' import React from 'react'
import { Image, ImageStyle, ListRenderItemInfo, View } from 'react-native' import {
Image,
ImageStyle,
ListRenderItemInfo,
RefreshControl,
View,
} from 'react-native'
import { Layout as LayoutStyle, Sizing, Typography } from '../styles' import { Layout as LayoutStyle, Sizing, Typography } from '../styles'
import { translate } from '../utils/translation' import { translate } from '../utils/translation'
import { useChild } from './childContext.component' import { useChild } from './childContext.component'
@ -18,7 +24,7 @@ import { MenuListItem } from './menuListItem.component'
export const Menu = () => { export const Menu = () => {
const styles = useStyleSheet(themedStyles) const styles = useStyleSheet(themedStyles)
const child = useChild() const child = useChild()
const { data } = useMenu(child) const { data, status, reload } = useMenu(child)
return ( return (
<List <List
@ -42,6 +48,9 @@ export const Menu = () => {
<MenuListItem key={item.title} item={item} /> <MenuListItem key={item.title} item={item} />
)} )}
style={styles.container} style={styles.container}
refreshControl={
<RefreshControl refreshing={status === 'loading'} onRefresh={reload} />
}
/> />
) )
} }

View File

@ -1,4 +1,4 @@
import { MenuItem } from '@skolplattformen/api-skolplattformen' import { MenuItem } from '@skolplattformen/api'
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components' import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
import React from 'react' import React from 'react'
import { View } from 'react-native' import { View } from 'react-native'

View File

@ -20,12 +20,12 @@ export const ModalWebView = ({
const [modalVisible, setModalVisible] = React.useState(true) const [modalVisible, setModalVisible] = React.useState(true)
const { api } = useApi() const { api } = useApi()
const [title, setTitle] = React.useState('...') const [title, setTitle] = React.useState('...')
const [headers, setHeaders] = useState() const [headers, setHeaders] = useState<{ [index: string]: string }>()
useEffect(() => { useEffect(() => {
const getHeaders = async (urlToGetSessionFor: string) => { const getHeaders = async (urlToGetSessionFor: string) => {
if (sharedCookiesEnabled) return if (sharedCookiesEnabled) return
const { headers: newHeaders } = await api.getSession(urlToGetSessionFor) const newHeaders = await api.getSessionHeaders(urlToGetSessionFor)
setHeaders(newHeaders) setHeaders(newHeaders)
} }

View File

@ -2,7 +2,7 @@ import { NavigationContainer } from '@react-navigation/native'
import { import {
Child as ChildType, Child as ChildType,
NewsItem as NewsItemType, NewsItem as NewsItemType,
} from '@skolplattformen/api-skolplattformen' } from '@skolplattformen/api'
import { useApi } from '@skolplattformen/hooks' import { useApi } from '@skolplattformen/hooks'
import { useTheme } from '@ui-kitten/components' import { useTheme } from '@ui-kitten/components'
import { Library } from 'libraries.json' import { Library } from 'libraries.json'

View File

@ -5,7 +5,13 @@ import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
import moment from 'moment' import moment from 'moment'
import 'moment/locale/sv' import 'moment/locale/sv'
import React from 'react' import React from 'react'
import { Dimensions, ImageStyle, ScrollView, View } from 'react-native' import {
Dimensions,
ImageStyle,
RefreshControl,
ScrollView,
View,
} from 'react-native'
import { NativeStackNavigationOptions } from 'react-native-screens/native-stack' import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
import { defaultStackStyling } from '../design/navigationThemes' import { defaultStackStyling } from '../design/navigationThemes'
import { Layout, Sizing, Typography } from '../styles' import { Layout, Sizing, Typography } from '../styles'
@ -46,7 +52,7 @@ export const newsItemRouteOptions =
export const NewsItem = ({ route }: NewsItemProps) => { export const NewsItem = ({ route }: NewsItemProps) => {
const { newsItem, child } = route.params const { newsItem, child } = route.params
const { data } = useNewsDetails(child, newsItem) const { data, status, reload } = useNewsDetails(child, newsItem)
const styles = useStyleSheet(themedStyles) const styles = useStyleSheet(themedStyles)
const stylesMarkdown = useStyleSheet(themedStylesMarkdown) const stylesMarkdown = useStyleSheet(themedStylesMarkdown)
@ -54,6 +60,9 @@ export const NewsItem = ({ route }: NewsItemProps) => {
<ScrollView <ScrollView
contentContainerStyle={styles.article} contentContainerStyle={styles.article}
style={styles.scrollView} style={styles.scrollView}
refreshControl={
<RefreshControl refreshing={status === 'loading'} onRefresh={reload} />
}
> >
<Text maxFontSizeMultiplier={2} style={styles.title}> <Text maxFontSizeMultiplier={2} style={styles.title}>
{newsItem.header} {newsItem.header}

View File

@ -1,7 +1,7 @@
import { useNews } from '@skolplattformen/hooks' import { useNews } from '@skolplattformen/hooks'
import { Input, List, StyleService, useStyleSheet } from '@ui-kitten/components' import { Input, List, StyleService, useStyleSheet } from '@ui-kitten/components'
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { TouchableOpacity, View } from 'react-native' import { TouchableOpacity, View, RefreshControl } from 'react-native'
import { Sizing } from '../styles' import { Sizing } from '../styles'
import { import {
renderSearchResultPreview, renderSearchResultPreview,
@ -15,7 +15,7 @@ import { NewsListItem } from './newsListItem.component'
export const NewsList = () => { export const NewsList = () => {
const styles = useStyleSheet(themedStyles) const styles = useStyleSheet(themedStyles)
const child = useChild() const child = useChild()
const { data } = useNews(child) const { data, status, reload } = useNews(child)
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const searchResults = useNewsListSearchResults(searchQuery) const searchResults = useNewsListSearchResults(searchQuery)
@ -62,6 +62,13 @@ export const NewsList = () => {
{renderSearchResultPreview(searchResult)} {renderSearchResultPreview(searchResult)}
</NewsListItem> </NewsListItem>
)} )}
refreshControl={
<RefreshControl
refreshing={status === 'loading'}
onRefresh={reload}
tintColor={'color-basic-100'}
/>
}
/> />
) )
} }
@ -74,6 +81,13 @@ export const NewsList = () => {
data={data} data={data}
ListHeaderComponent={header} ListHeaderComponent={header}
renderItem={({ item }) => <NewsListItem key={item.id} item={item} />} renderItem={({ item }) => <NewsListItem key={item.id} item={item} />}
refreshControl={
<RefreshControl
refreshing={status === 'loading'}
onRefresh={reload}
tintColor={'color-basic-100'}
/>
}
/> />
) )
} }

View File

@ -1,6 +1,6 @@
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack' import { StackNavigationProp } from '@react-navigation/stack'
import { NewsItem } from '@skolplattformen/api-skolplattformen' import { NewsItem } from '@skolplattformen/api'
import { StyleService, useStyleSheet } from '@ui-kitten/components' import { StyleService, useStyleSheet } from '@ui-kitten/components'
import moment from 'moment' import moment from 'moment'
import React, { ReactNode } from 'react' import React, { ReactNode } from 'react'

View File

@ -1,4 +1,4 @@
import { Notification as NotificationType } from '@skolplattformen/api-skolplattformen' import { Notification as NotificationType } from '@skolplattformen/api'
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components' import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
import moment from 'moment' import moment from 'moment'
import React from 'react' import React from 'react'

View File

@ -1,6 +1,7 @@
import { useNotifications } from '@skolplattformen/hooks' import { useNotifications } from '@skolplattformen/hooks'
import { List, StyleService, useStyleSheet } from '@ui-kitten/components' import { List, StyleService, useStyleSheet } from '@ui-kitten/components'
import React from 'react' import React from 'react'
import { RefreshControl } from 'react-native'
import { Sizing } from '../styles' import { Sizing } from '../styles'
import { useChild } from './childContext.component' import { useChild } from './childContext.component'
import { Notification } from './notification.component' import { Notification } from './notification.component'
@ -8,7 +9,7 @@ import { Notification } from './notification.component'
export const NotificationsList = () => { export const NotificationsList = () => {
const styles = useStyleSheet(themedStyles) const styles = useStyleSheet(themedStyles)
const child = useChild() const child = useChild()
const { data } = useNotifications(child) const { data, status, reload } = useNotifications(child)
return ( return (
<List <List
@ -18,6 +19,9 @@ export const NotificationsList = () => {
renderItem={(info) => ( renderItem={(info) => (
<Notification key={info.item.id} item={info.item} /> <Notification key={info.item.id} item={info.item} />
)} )}
refreshControl={
<RefreshControl refreshing={status === 'loading'} onRefresh={reload} />
}
/> />
) )
} }

View File

@ -1,4 +1,4 @@
import { CalendarItem } from '@skolplattformen/api-skolplattformen' import { CalendarItem } from '@skolplattformen/api'
import { Button, MenuItem, OverflowMenu, Text } from '@ui-kitten/components' import { Button, MenuItem, OverflowMenu, Text } from '@ui-kitten/components'
import React from 'react' import React from 'react'
import RNCalendarEvents from 'react-native-calendar-events' import RNCalendarEvents from 'react-native-calendar-events'

View File

@ -1,5 +1,5 @@
import { NavigationProp, useNavigation } from '@react-navigation/core' import { NavigationProp, useNavigation } from '@react-navigation/core'
import { useApi } from '@skolplattformen/hooks' import { useApi, useUser } from '@skolplattformen/hooks'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { ScrollView } from 'react-native' import { ScrollView } from 'react-native'
import { NativeStackNavigationOptions } from 'react-native-screens/native-stack' import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
@ -28,14 +28,16 @@ export const SettingsScreen = () => {
const langCode = LanguageService.getLanguageCode() const langCode = LanguageService.getLanguageCode()
const language = languages.find((l) => l.langCode === langCode) const language = languages.find((l) => l.langCode === langCode)
const { api } = useApi() const { api } = useApi()
const { data: user } = useUser()
const logout = useCallback(async () => { const logout = useCallback(async () => {
await AppStorage.clearTemporaryItems() await AppStorage.clearTemporaryItems()
await AppStorage.clearPersonalData(user)
await api.logout() await api.logout()
navigation.reset({ navigation.reset({
routes: [{ name: 'Login' }], routes: [{ name: 'Login' }],
}) })
}, [api, navigation]) }, [api, navigation, user])
return ( return (
<ScrollView <ScrollView

View File

@ -1,8 +1,4 @@
import { import { Child, MenuItem, TimetableEntry } from '@skolplattformen/api'
Child,
MenuItem,
TimetableEntry,
} from '@skolplattformen/api-skolplattformen'
import { useMenu, useTimetable } from '@skolplattformen/hooks' import { useMenu, useTimetable } from '@skolplattformen/hooks'
import { import {
List, List,
@ -20,6 +16,8 @@ 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'
import { translate } from '../utils/translation'
interface WeekProps { interface WeekProps {
child: Child child: Child
@ -111,10 +109,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(moment())
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,
@ -130,15 +130,30 @@ export const Week = ({ child }: WeekProps) => {
setShowSchema(shouldShowSchema) setShowSchema(shouldShowSchema)
}, [lessons]) }, [lessons])
const getWeekText = (date = moment()) => {
return `${translate('schedule.week')} ${date.isoWeek()}`
}
return showSchema ? ( return showSchema ? (
<TransitionView style={styles.view} animation={'fadeInDown'}> <TransitionView style={styles.view} animation={'fadeInDown'}>
<TransitionView style={styles.innerView} animation={'fadeIn'}> <TransitionView style={styles.innerView} animation={'fadeIn'}>
<Text style={styles.weekNumber}>{getWeekText(displayDate)}</Text>
<TabBar <TabBar
selectedIndex={selectedIndex} selectedIndex={selectedIndex}
onSelect={(index) => setSelectedIndex(index)} onSelect={(index) => setSelectedIndex(index)}
> >
{days.map((weekDay) => ( {days.map((weekDay, index) => (
<Tab key={weekDay} title={weekDay} /> <Tab
key={weekDay}
title={(_) => (
<>
<Text style={styles.tabTitle}>{weekDay}</Text>
<Text style={styles.tabTitleDate}>
{displayDate.startOf('week').add(index, 'day').format('D')}
</Text>
</>
)}
/>
))} ))}
</TabBar> </TabBar>
@ -154,7 +169,7 @@ export const Week = ({ child }: WeekProps) => {
lunch={menu[index] || {}} lunch={menu[index] || {}}
lessons={lessons lessons={lessons
.filter((lesson) => days[lesson.dayOfWeek - 1] === weekDay) .filter((lesson) => days[lesson.dayOfWeek - 1] === weekDay)
.sort((a, b) => a.dateStart.localeCompare(b.dateStart))} .sort((a, b) => a.timeStart.localeCompare(b.timeStart))}
/> />
))} ))}
</ViewPager> </ViewPager>
@ -166,12 +181,12 @@ export const Week = ({ child }: WeekProps) => {
const themedStyles = StyleService.create({ const themedStyles = StyleService.create({
view: { view: {
backgroundColor: 'background-basic-color-1', backgroundColor: 'background-basic-color-1',
maxHeight: '60%', maxHeight: '65%',
paddingBottom: 0, paddingBottom: 0,
margin: 0, margin: 0,
}, },
innerView: { innerView: {
paddingBottom: 60, paddingBottom: 170,
margin: 0, margin: 0,
}, },
part: { part: {
@ -233,4 +248,15 @@ const themedStyles = StyleService.create({
lesson: { lesson: {
flexDirection: 'column', flexDirection: 'column',
}, },
weekNumber: {
marginLeft: 10,
marginTop: 10,
...Typography.fontWeight.bold,
},
tabTitle: {
textAlign: 'center',
},
tabTitleDate: {
textAlign: 'center',
},
}) })

View File

@ -2,8 +2,9 @@ import { Features, FeatureType } from '@skolplattformen/api'
import React from 'react' import React from 'react'
export const FeatureFlagsContext = React.createContext<Features>({ export const FeatureFlagsContext = React.createContext<Features>({
LOGIN_BANK_ID_SAME_DEVICE: false, LOGIN_BANK_ID_SAME_DEVICE_WITHOUT_ID: true,
FOOD_MENU: false, FOOD_MENU: false,
CLASS_LIST: true,
}) })
interface Props { interface Props {

View File

@ -16,7 +16,7 @@ export const SchoolPlatformProvider: React.FC = ({ children }) => {
'currentSchoolPlatform' 'currentSchoolPlatform'
) )
const changeSchoolPlatform = (platform: string) => { const changeSchoolPlatform = (platform) => {
setCurrentSchoolPlatform(platform) setCurrentSchoolPlatform(platform)
} }

View File

@ -10,13 +10,13 @@ export const schoolPlatforms = [
{ {
id: 'stockholm-skolplattformen', id: 'stockholm-skolplattformen',
displayName: 'Stockholm stad (Skolplattformen)', displayName: 'Stockholm stad (Skolplattformen)',
api: initSkolplattformen(fetch, CookieManager), api: initSkolplattformen(fetch as any, CookieManager),
features: featuresSkolPlattformen, features: featuresSkolPlattformen,
}, },
{ {
id: 'goteborg-hjarntorget', id: 'goteborg-hjarntorget',
displayName: 'Göteborg stad (Hjärntorget)', displayName: 'Göteborg stad (Hjärntorget)',
api: initHjarntorget(fetch, CookieManager), api: initHjarntorget(fetch as any, CookieManager),
features: featuresHjarntorget, features: featuresHjarntorget,
}, },
] ]

View File

@ -1,5 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import { User } from '@skolplattformen/api-skolplattformen' import { User } from '@skolplattformen/api'
import { act, renderHook } from '@testing-library/react-hooks' import { act, renderHook } from '@testing-library/react-hooks'
import usePersonalStorage from '../usePersonalStorage' import usePersonalStorage from '../usePersonalStorage'

View File

@ -2,6 +2,8 @@ import { useCallback } from 'react'
import { proxy, subscribe, useSnapshot } from 'valtio' import { proxy, subscribe, useSnapshot } from 'valtio'
import AppStorage from '../services/appStorage' import AppStorage from '../services/appStorage'
export type ChildPersonalNumbers = Record<string, string>
export const settingsState = proxy({ export const settingsState = proxy({
hydrated: false, hydrated: false,
settings: { settings: {
@ -12,6 +14,7 @@ export const settingsState = proxy({
currentSchoolPlatform: 'stockholm-skolplattformen' as currentSchoolPlatform: 'stockholm-skolplattformen' as
| 'stockholm-skolplattformen' | 'stockholm-skolplattformen'
| 'goteborg-hjarntorget', | 'goteborg-hjarntorget',
childPersonalIdentityNumber: {} as ChildPersonalNumbers,
}, },
}) })

View File

@ -669,4 +669,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 85f5a2dfa1de342b427eecb6e9652410ad153247 PODFILE CHECKSUM: 85f5a2dfa1de342b427eecb6e9652410ad153247
COCOAPODS: 1.10.1 COCOAPODS: 1.11.2

View File

@ -573,9 +573,9 @@
); );
inputPaths = ( inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks.sh", "${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/double-conversion/double-conversion.framework/double-conversion", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL/OpenSSL.framework/OpenSSL", "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes/hermes.framework/hermes", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework/hermes",
); );
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputPaths = ( outputPaths = (
@ -595,9 +595,9 @@
); );
inputPaths = ( inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-frameworks.sh", "${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/double-conversion/double-conversion.framework/double-conversion", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL/OpenSSL.framework/OpenSSL", "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes/hermes.framework/hermes", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework/hermes",
); );
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputPaths = ( outputPaths = (
@ -793,7 +793,7 @@
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = app/Info.plist; INFOPLIST_FILE = app/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 3.0.1; MARKETING_VERSION = 3.0.3;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
@ -822,7 +822,7 @@
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = app/Info.plist; INFOPLIST_FILE = app/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 3.0.1; MARKETING_VERSION = 3.0.3;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",

View File

@ -1,5 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import { User } from '@skolplattformen/api-skolplattformen' import { User } from '@skolplattformen/api'
import AppStorage from '../appStorage' import AppStorage from '../appStorage'
beforeEach(() => { beforeEach(() => {

View File

@ -1,5 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import { User } from '@skolplattformen/api-skolplattformen' import { User } from '@skolplattformen/api'
export default class AppStorage { export default class AppStorage {
static settingsStorageKeyPrefix = 'appsetting_' static settingsStorageKeyPrefix = 'appsetting_'

View File

@ -58,7 +58,9 @@
"saveToCalender": "Save in calendar", "saveToCalender": "Save in calendar",
"saveToCalenderError": "Something went wrong", "saveToCalenderError": "Something went wrong",
"saveToCalenderSuccess": "✔️ Saved in calendar", "saveToCalenderSuccess": "✔️ Saved in calendar",
"showCalenderActions": "Show calendar actions" "showCalenderActions": "Show calendar actions",
"emptyHeadline": "The calendar looks kinda empty",
"emptyText": "Couldn't find anything to show"
}, },
"children": { "children": {
"loadingErrorHeading": "Oops!", "loadingErrorHeading": "Oops!",
@ -80,7 +82,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",
@ -128,7 +131,8 @@
"start": "Start", "start": "Start",
"end": "End", "end": "End",
"lunch": "Lunch", "lunch": "Lunch",
"gymBag": "Gym bag" "gymBag": "Gym bag",
"week": "Week"
}, },
"classmates": { "classmates": {
"class": "Class", "class": "Class",

View File

@ -10,7 +10,8 @@
"changeLanguage": "Changer de langue", "changeLanguage": "Changer de langue",
"cancel": "Annuler", "cancel": "Annuler",
"logoutAndClearPersonalData": "Se déconnecter et effacer les données personnelles", "logoutAndClearPersonalData": "Se déconnecter et effacer les données personnelles",
"logoutAndClearAllDataInclSettings": "Se déconnecter et effacer toutes les données, y compris les paramètres" "logoutAndClearAllDataInclSettings": "Se déconnecter et effacer toutes les données, y compris les paramètres",
"tomorrow": "Demain"
}, },
"calender": { "calender": {
"saveToCalenderError": "Un problème est survenu", "saveToCalenderError": "Un problème est survenu",
@ -53,7 +54,8 @@
"a11y_select_login_method": "Sélectionnez la méthode de connexion", "a11y_select_login_method": "Sélectionnez la méthode de connexion",
"a11y_clear_social_security_input_field": "Effacer le champ du numéro national didentité", "a11y_clear_social_security_input_field": "Effacer le champ du numéro national didentité",
"a11y_image_two_boys": "Photo de deux personnes consultant leur téléphone portable", "a11y_image_two_boys": "Photo de deux personnes consultant leur téléphone portable",
"a11y_change_language": "Sélectionnez votre langue" "a11y_change_language": "Sélectionnez votre langue",
"chooseSchoolPlatform": "Choisir la plateforme"
}, },
"abscense": { "abscense": {
"startTime": "Heure de début", "startTime": "Heure de début",

View File

@ -50,7 +50,9 @@
"saveToCalenderSuccess": "✔️ Lagret i kalender", "saveToCalenderSuccess": "✔️ Lagret i kalender",
"showCalenderActions": "Vis kalenderhandlinger", "showCalenderActions": "Vis kalenderhandlinger",
"saveToCalenderError": "Noe gikk galt", "saveToCalenderError": "Noe gikk galt",
"approveAccessToCalender": "Du må innvilge tilgang til kalenderen din" "approveAccessToCalender": "Du må innvilge tilgang til kalenderen din",
"emptyHeadline": "Kalenderen ser tom ut",
"emptyText": "Ingenting å vise"
}, },
"general": { "general": {
"loading": "Laster inn …", "loading": "Laster inn …",
@ -63,7 +65,8 @@
"send": "Send", "send": "Send",
"cancel": "Avbryt", "cancel": "Avbryt",
"logoutAndClearPersonalData": "Logg ut og tøm personlig data", "logoutAndClearPersonalData": "Logg ut og tøm personlig data",
"logoutAndClearAllDataInclSettings": "Logg ut og tøm all data, inkludert innstillinger" "logoutAndClearAllDataInclSettings": "Logg ut og tøm all data, inkludert innstillinger",
"tomorrow": "I morgen"
}, },
"news": { "news": {
"updated": "Oppdatert", "updated": "Oppdatert",
@ -115,7 +118,8 @@
"gymBag": "Gym-bag", "gymBag": "Gym-bag",
"lunch": "Lunsj", "lunch": "Lunsj",
"end": "Slutt", "end": "Slutt",
"start": "Start" "start": "Start",
"week": "Uke"
}, },
"contact": { "contact": {
"a11y_show_contact_info_button_hint": "Vis kontaktinfo", "a11y_show_contact_info_button_hint": "Vis kontaktinfo",

View File

@ -50,14 +50,17 @@
"a11y_select_login_method": "Wybierz metodę logowania", "a11y_select_login_method": "Wybierz metodę logowania",
"a11y_clear_social_security_input_field": "Wyczyść pole z personnumerem", "a11y_clear_social_security_input_field": "Wyczyść pole z personnumerem",
"a11y_image_two_boys": "Ilustracja: dwie osoby patrzą w telefony komórkowe", "a11y_image_two_boys": "Ilustracja: dwie osoby patrzą w telefony komórkowe",
"a11y_change_language": "Wybierz język" "a11y_change_language": "Wybierz język",
"chooseSchoolPlatform": "Wybierz platformę"
}, },
"calender": { "calender": {
"approveAccessToCalender": "Musisz zatwierdzić dostęp do kalendarza", "approveAccessToCalender": "Musisz zatwierdzić dostęp do kalendarza",
"saveToCalender": "Zapisz w kalendarzu", "saveToCalender": "Zapisz w kalendarzu",
"saveToCalenderError": "Coś poszło nie tak", "saveToCalenderError": "Coś poszło nie tak",
"saveToCalenderSuccess": "✔️ Zapisano w kalendarzu", "saveToCalenderSuccess": "✔️ Zapisano w kalendarzu",
"showCalenderActions": "Pokaż opcje kalendarza" "showCalenderActions": "Pokaż opcje kalendarza",
"emptyHeadline": "Kalendarz jest pusty",
"emptyText": "Nie znaleziono nic do pokazania"
}, },
"children": { "children": {
"loadingErrorHeading": "Oj!", "loadingErrorHeading": "Oj!",
@ -79,7 +82,8 @@
"title": "Öppna skolplattformen", "title": "Öppna skolplattformen",
"cancel": "Anuluj", "cancel": "Anuluj",
"logoutAndClearPersonalData": "Wyloguj i skasuj dane osobowe", "logoutAndClearPersonalData": "Wyloguj i skasuj dane osobowe",
"logoutAndClearAllDataInclSettings": "Wyloguj i skasuj wszystkie dane łącznie z ustawieniami" "logoutAndClearAllDataInclSettings": "Wyloguj i skasuj wszystkie dane łącznie z ustawieniami",
"tomorrow": "Jutro"
}, },
"language": { "language": {
"changeLanguage": "Zmień język", "changeLanguage": "Zmień język",
@ -114,11 +118,12 @@
"gymBag": "Ubrania do WF", "gymBag": "Ubrania do WF",
"lunch": "Lunch", "lunch": "Lunch",
"end": "Kończy", "end": "Kończy",
"start": "Zaczyna" "start": "Zaczyna",
"week": "Tydzień"
}, },
"contact": { "contact": {
"a11y_show_contact_info_button_hint": "Pokazuje dane kontaktowe", "a11y_show_contact_info_button_hint": "Pokazuje dane kontaktowe",
"home": "Dom", "home": "Adres",
"email": "E-mail", "email": "E-mail",
"sms": "SMS", "sms": "SMS",
"call": "Zadzwoń", "call": "Zadzwoń",

View File

@ -7,7 +7,8 @@
"selectAbscenseEndTime": "Indique a hora de fim", "selectAbscenseEndTime": "Indique a hora de fim",
"endTime": "Hora de fim", "endTime": "Hora de fim",
"invalidPersonalNumber": "Número de identidade pessoal inválido", "invalidPersonalNumber": "Número de identidade pessoal inválido",
"personalNumberMissing": "Falta o número de identidade pessoal" "personalNumberMissing": "Falta o número de identidade pessoal",
"childsPersonalNumber": "Número de identidade pessoal da criança"
}, },
"auth": { "auth": {
"bankid": { "bankid": {

View File

@ -58,7 +58,9 @@
"saveToCalender": "Spara till kalender", "saveToCalender": "Spara till kalender",
"saveToCalenderError": "Något gick fel", "saveToCalenderError": "Något gick fel",
"saveToCalenderSuccess": "✔️ Sparad till kalender", "saveToCalenderSuccess": "✔️ Sparad till kalender",
"showCalenderActions": "Visa kalenderfunktioner" "showCalenderActions": "Visa kalenderfunktioner",
"emptyText": "Hittade ingenting att visa",
"emptyHeadline": "Det ser lite tomt ut i kalendern"
}, },
"children": { "children": {
"loadingErrorHeading": "Hoppsan!", "loadingErrorHeading": "Hoppsan!",
@ -80,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",
@ -128,7 +131,8 @@
"start": "Börjar", "start": "Börjar",
"end": "Slutar", "end": "Slutar",
"lunch": "Lunch", "lunch": "Lunch",
"gymBag": "Gympapåse" "gymBag": "Gympapåse",
"week": "Vecka"
}, },
"classmates": { "classmates": {
"class": "Klass", "class": "Klass",

View File

@ -0,0 +1,47 @@
import moment from 'moment'
import { getMeaningfulStartingDate } from '../calendarHelpers'
const tuesdayMorning = moment('2021-11-30T08:20:00+0100')
const tuesdayEvening = moment('2021-11-30T19:20:26+0100')
const wednesdayEvening = moment('2021-12-01T19:20:26+0100')
const fridayEvening = moment('2021-12-03T19:20:26+0100')
const saturdayEvening = moment('2021-12-04T19:20:26+0100')
const sundayEvening = moment('2021-12-05T19:20:26+0100')
const mondayEvening = moment('2021-12-06T19:20:26+0100')
describe('getMeaningfulStartingDate should not touch inputdate', () => {
const origDate = moment()
const origDateClone = origDate.clone()
getMeaningfulStartingDate(origDate)
expect(origDate).toEqual(origDateClone)
})
describe('getMeaningfulStartingDate on weekends', () => {
it('should give next monday if on friday evening', () => {
const startDate = getMeaningfulStartingDate(fridayEvening)
expect(startDate.toISOString()).toEqual(mondayEvening.toISOString())
})
it('should give next monday if on saturday', () => {
const startDate = getMeaningfulStartingDate(saturdayEvening)
expect(startDate.toISOString()).toEqual(mondayEvening.toISOString())
})
it('should give next monday if on sunday', () => {
const startDate = getMeaningfulStartingDate(sundayEvening)
expect(startDate.toISOString()).toEqual(mondayEvening.toISOString())
})
})
describe('getMeaningfulStartingDate on weekdays', () => {
it('should give next day if on tuesday evening', () => {
const startDate = getMeaningfulStartingDate(tuesdayEvening)
expect(startDate.toISOString()).toEqual(wednesdayEvening.toISOString())
})
it('should give same day if on tuesday morning', () => {
const startDate = getMeaningfulStartingDate(tuesdayMorning)
expect(startDate.toISOString()).toEqual(tuesdayMorning.toISOString())
})
})

View File

@ -0,0 +1,18 @@
import moment from 'moment'
export const getMeaningfulStartingDate = (date = moment()) => {
const originalDate = date.clone()
let returnDate = date.clone()
// are we on the evening?
if (date.hour() > 17) returnDate.add('1', 'day')
// are we on the weekend
if (returnDate.isoWeekday() > 5) {
returnDate = returnDate.add(5, 'days').startOf('isoWeek')
returnDate
.hour(originalDate.hour())
.minute(originalDate.minute())
.second(originalDate.second())
}
return returnDate
}

View File

@ -1,4 +1,4 @@
import { Guardian } from '@skolplattformen/api-skolplattformen' import { Guardian } from '@skolplattformen/api'
export const studentName = (name?: string) => name?.replace(/\s?\(\w+\)$/, '') export const studentName = (name?: string) => name?.replace(/\s?\(\w+\)$/, '')

View File

@ -1,4 +1,4 @@
import { NewsItem } from '@skolplattformen/api-skolplattformen' import { NewsItem } from '@skolplattformen/api'
import { useNews } from '@skolplattformen/hooks' import { useNews } from '@skolplattformen/hooks'
import { MatchData, Searcher } from 'fast-fuzzy' import { MatchData, Searcher } from 'fast-fuzzy'
import React, { ReactNode, useMemo } from 'react' import React, { ReactNode, useMemo } from 'react'

View File

@ -5,6 +5,7 @@ import { EvaIconsPack } from '@ui-kitten/eva-icons'
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { LanguageProvider } from '../context/language/languageContext' import { LanguageProvider } from '../context/language/languageContext'
import { translations } from './translation' import { translations } from './translation'
import { lightTheme } from '../design/themes'
export const render = ( export const render = (
ui: ReactElement<any, string>, ui: ReactElement<any, string>,
@ -14,7 +15,7 @@ export const render = (
return ( return (
<> <>
<IconRegistry icons={EvaIconsPack} /> <IconRegistry icons={EvaIconsPack} />
<ApplicationProvider {...eva} theme={eva.light}> <ApplicationProvider {...eva} theme={lightTheme}>
<LanguageProvider <LanguageProvider
cache={false} cache={false}
data={translations} data={translations}

View File

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import CountUp from 'react-countup' import CountUp from 'react-countup'
import VisibilitySensor from 'react-visibility-sensor' import VisibilitySensor from 'react-visibility-sensor'
import { price } from './Pricing'
const FUNFACTS_DATA = [ const FUNFACTS_DATA = [
{ {
@ -12,7 +13,7 @@ const FUNFACTS_DATA = [
title: 'år att utveckla', title: 'år att utveckla',
}, },
{ {
count: 11, count: price,
title: 'kronor kostar vår app :)', title: 'kronor kostar vår app :)',
}, },
{ {

View File

@ -3,7 +3,7 @@ import DownloadButtons from './DownloadButtons'
import Icon from './Icon' import Icon from './Icon'
import SectionTitle from './SectionTitle' import SectionTitle from './SectionTitle'
const price = 12 export const price = 11
const baseFeatures = [ const baseFeatures = [
{ {

View File

@ -2,8 +2,7 @@ import { formatPrice } from '../utils/intl'
import DownloadButtons from './DownloadButtons' import DownloadButtons from './DownloadButtons'
import Icon from './Icon' import Icon from './Icon'
import SectionTitle from './SectionTitle' import SectionTitle from './SectionTitle'
import { price } from './Pricing'
const price = 12
const baseFeatures = [ const baseFeatures = [
{ {

View File

@ -9,7 +9,7 @@ const Privacy = () => {
<p> <p>
"Öppna Skolplattformen", hädanefter "appen", byggs av "Not free beer "Öppna Skolplattformen", hädanefter "appen", byggs av "Not free beer
AB" som en kommersiell app. Appen hämtar all information från AB" som en kommersiell app. Appen hämtar all information från
Stockholms stads skolplattform, hädanefter Skolplattformen, efter respektive skolplattform, hädanefter Skolplattformen, efter
inloggning via BankID. Appens funktion är därmed direkt knuten till inloggning via BankID. Appens funktion är därmed direkt knuten till
att Skolplattformen fungerar. Vi kan endast ta ansvar för att vår kod att Skolplattformen fungerar. Vi kan endast ta ansvar för att vår kod
fungerar inte deras. fungerar inte deras.

View File

@ -1,5 +1,7 @@
import Link from './Link' import Link from './Link'
import { price } from './Pricing'
const QA = () => { const QA = () => {
return ( return (
<div className="header"> <div className="header">
@ -249,7 +251,7 @@ const QA = () => {
de? de?
</h3> </h3>
<p> <p>
Appen kostar 12 kronor. Intäkten registreras i aktiebolaget Not Free Appen kostar {price} kronor. Intäkten registreras i aktiebolaget Not Free
Beer som ägs av tre av utvecklarna och går till att täcka kostnader Beer som ägs av tre av utvecklarna och går till att täcka kostnader
för inköp. Det täcker inte långa vägar den tid vi lagt ner. Med en för inköp. Det täcker inte långa vägar den tid vi lagt ner. Med en
låg engångskostnad ökar vi chansen att vi orkar syssla med underhåll låg engångskostnad ökar vi chansen att vi orkar syssla med underhåll
@ -315,7 +317,7 @@ const QA = () => {
<h3>Kontakta oss</h3> <h3>Kontakta oss</h3>
<p> <p>
Tveka inte att kontakta oss. Skicka ett mail till{' '} Tveka inte att kontakta oss. Skicka ett mail till{' '}
<a href="mailto:info@skolplattformen.org">dev@skolplattformen.org</a>. <a href="mailto:info@skolplattformen.org">info@skolplattformen.org</a>.
</p> </p>
</div> </div>
</div> </div>

View File

@ -66,7 +66,7 @@ export const FEATURES_DATA = [
{ {
title: 'Kan byggas ut till fler skolsystem', title: 'Kan byggas ut till fler skolsystem',
text: text:
'Just nu stöds bara Stockholm Stads skolplattform men med din hjälp kan fler skolplattformar integreras så att du slipper logga in i flera appar om du har barn i olika skolor.', 'Just nu stöds Stockholms och Göteborgs stads skolplattformar. Med din hjälp kan fler integreras så att du slipper använda flera appar om du har barn i olika skolor.',
image: ( image: (
<svg <svg
className="fill-current" className="fill-current"

View File

@ -35,6 +35,9 @@ module.exports = {
h3: { h3: {
color: theme('colors.white'), color: theme('colors.white'),
}, },
h4: {
color: theme('colors.white'),
},
a: { a: {
color: theme('colors.indigo.500'), color: theme('colors.indigo.500'),
} }

View File

@ -13,7 +13,9 @@ import {
NewsItem, NewsItem,
Notification, Notification,
ScheduleItem, ScheduleItem,
SchoolContact,
Skola24Child, Skola24Child,
Teacher,
TimetableEntry, TimetableEntry,
toMarkdown, toMarkdown,
URLSearchParams, URLSearchParams,
@ -25,7 +27,7 @@ import { decode } from 'he'
import { DateTime, FixedOffsetZone } from 'luxon' import { DateTime, FixedOffsetZone } from 'luxon'
import * as html from 'node-html-parser' import * as html from 'node-html-parser'
import { fakeFetcher } from './fake/fakeFetcher' import { fakeFetcher } from './fake/fakeFetcher'
import { checkStatus } from './loginStatus' import { checkStatus, DummyStatusChecker } from './loginStatus'
import { extractMvghostRequestBody, parseCalendarItem } from './parse/parsers' import { extractMvghostRequestBody, parseCalendarItem } from './parse/parsers'
import { import {
beginBankIdUrl, beginBankIdUrl,
@ -50,6 +52,7 @@ import {
wallMessagesUrl, wallMessagesUrl,
abscenseRegistrationUrl abscenseRegistrationUrl
} from './routes' } from './routes'
import parse from '@skolplattformen/curriculum'
function getDateOfISOWeek(week: number, year: number) { function getDateOfISOWeek(week: number, year: number) {
const simple = new Date(year, 0, 1 + (week - 1) * 7) const simple = new Date(year, 0, 1 + (week - 1) * 7)
@ -140,14 +143,16 @@ export class ApiHjarntorget extends EventEmitter implements Api {
return this.personalNumber return this.personalNumber
} }
async setSessionCookie(sessionCookie: string): Promise<void> { public async getSessionHeaders(url: string): Promise<{ [index: string]: string }> {
await this.fetch('login-cookie', hjarntorgetUrl, { const cookie = await this.cookieManager.getCookieString(url)
headers: { return {
cookie: sessionCookie, cookie,
}, }
redirect: 'manual', }
})
async setSessionCookie(sessionCookie: string): Promise<void> {
this.cookieManager.setCookieString(sessionCookie, hjarntorgetUrl)
const user = await this.getUser() const user = await this.getUser()
if (!user.isAuthenticated) { if (!user.isAuthenticated) {
throw new Error('Session cookie is expired') throw new Error('Session cookie is expired')
@ -247,6 +252,21 @@ export class ApiHjarntorget extends EventEmitter implements Api {
return Promise.resolve([]) return Promise.resolve([])
} }
public async getTeachers(child: EtjanstChild): Promise<Teacher[]> {
if (!this.isLoggedIn) {
throw new Error('Not logged in...')
}
return Promise.resolve([])
}
public async getSchoolContacts(child: EtjanstChild): Promise<SchoolContact[]> {
if (!this.isLoggedIn) {
throw new Error('Not logged in...')
}
return Promise.resolve([])
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
async getNews(_child: EtjanstChild): Promise<NewsItem[]> { async getNews(_child: EtjanstChild): Promise<NewsItem[]> {
if (!this.isLoggedIn) { if (!this.isLoggedIn) {
@ -452,9 +472,8 @@ export class ApiHjarntorget extends EventEmitter implements Api {
zone: FixedOffsetZone.instance(l.endDate.timezoneOffsetMinutes), zone: FixedOffsetZone.instance(l.endDate.timezoneOffsetMinutes),
}) })
return { return {
...parse(l.title, _lang),
id: l.id, id: l.id,
code: l.title,
name: l.title,
teacher: l.bookedTeacherNames && l.bookedTeacherNames[0], teacher: l.bookedTeacherNames && l.bookedTeacherNames[0],
location: l.location, location: l.location,
timeStart: start.toISOTime().substring(0, 5), timeStart: start.toISOTime().substring(0, 5),
@ -462,7 +481,7 @@ export class ApiHjarntorget extends EventEmitter implements Api {
dayOfWeek: start.toJSDate().getDay(), dayOfWeek: start.toJSDate().getDay(),
blockName: l.title, blockName: l.title,
dateStart: start.toISODate(), dateStart: start.toISODate(),
dateEnd: start.toISODate(), dateEnd: end.toISODate(),
} as TimetableEntry } as TimetableEntry
}) })
} }
@ -492,13 +511,13 @@ export class ApiHjarntorget extends EventEmitter implements Api {
if((beginLoginRedirectResponse as any).url.endsWith("startPage.do")) { if((beginLoginRedirectResponse as any).url.endsWith("startPage.do")) {
// already logged in! // already logged in!
const emitter = new EventEmitter() const emitter = new DummyStatusChecker()
setTimeout(() => { setTimeout(() => {
this.isLoggedIn = true this.isLoggedIn = true
emitter.emit('OK') emitter.emit('OK')
this.emit('login') this.emit('login')
}, 50) }, 50)
return emitter as unknown as LoginStatusChecker; return emitter as LoginStatusChecker;
} }
console.log('prepping??? shibboleth') console.log('prepping??? shibboleth')

File diff suppressed because one or more lines are too long

View File

@ -28,10 +28,9 @@ const fetchMappings: { [name:string]: () => Response} = {
'event-role-members-24-821': eventRoleMembers24, 'event-role-members-24-821': eventRoleMembers24,
'calendars': calendars, 'calendars': calendars,
'calendar-14241345': calendar_14241345, 'calendar-14241345': calendar_14241345,
} }
export const fakeFetcher: Fetcher = (name: string, url: string, init?: any): Promise<Response> => { export const fakeFetcher: Fetcher = (name: string, url: string, init?: any): Promise<Response> => {
const responder = fetchMappings[name] ?? (() => {throw new Error("Request not faked for name: " + name)}) const responder = fetchMappings[name] ?? (() => {throw new Error("Request not faked for name: " + name)})
return Promise.resolve(responder()); return Promise.resolve(responder());
} }

View File

@ -1,13 +1,22 @@
import { toNamespacedPath } from "path";
// TODO: fix the startDate/endDate of all lessons // TODO: fix the startDate/endDate of all lessons
export const lessons_133700_goteborgsstad = () => { export const lessons_133700_goteborgsstad = () => {
const baseTime = 1636357800000; const baseTime = 1636357800000;
const baseDate = new Date(baseTime) const baseDate = new Date(baseTime)
const today = new Date() const today = new Date()
const currentHour = today.getHours()
today.setHours(baseDate.getHours()) today.setHours(baseDate.getHours())
today.setMinutes(baseDate.getMinutes()) today.setMinutes(baseDate.getMinutes())
today.setSeconds(0) today.setSeconds(0)
const offset = Math.abs(baseTime - today.getTime()) let offset = Math.abs(baseTime - today.getTime())
const weekDay = today.getDay()
if(weekDay == 6 || (weekDay == 5 && currentHour >= 18)) offset = offset + 2 * 86400000
if(weekDay == 0) offset = offset + 86400000
if(weekDay > 0 && weekDay < 6 && currentHour >= 18) offset = offset + 86400000
return { return {
"url": "https://hjarntorget.goteborg.se/api/schema/lessons?forUser=133700_goteborgsstad&startDateIso=2021-11-01&endDateIso=2021-11-08", "url": "https://hjarntorget.goteborg.se/api/schema/lessons?forUser=133700_goteborgsstad&startDateIso=2021-11-01&endDateIso=2021-11-08",
"headers": { "headers": {
@ -206,14 +215,21 @@ export const lessons_133700_goteborgsstad = () => {
} }
export const lessons_123456_goteborgsstad = () => { export const lessons_123456_goteborgsstad = () => {
const baseTime = 1636355400000; const baseTime = 1636357800000;
const baseDate = new Date(baseTime) const baseDate = new Date(baseTime)
const today = new Date() const today = new Date()
const currentHour = today.getHours()
today.setHours(baseDate.getHours()) today.setHours(baseDate.getHours())
today.setMinutes(baseDate.getMinutes()) today.setMinutes(baseDate.getMinutes())
today.setSeconds(0) today.setSeconds(0)
const offset = Math.abs(baseTime - today.getTime()) let offset = Math.abs(baseTime - today.getTime())
const weekDay = today.getDay()
if(weekDay == 6 || (weekDay == 5 && currentHour >= 18)) offset = offset + 2 * 86400000
if(weekDay == 0) offset = offset + 86400000
if(weekDay > 0 && weekDay < 6 && currentHour >= 18) offset = offset + 86400000
return { return {
"url": "https://hjarntorget.goteborg.se/api/schema/lessons?forUser=123456_goteborgsstad&startDateIso=2021-11-01&endDateIso=2021-11-08", "url": "https://hjarntorget.goteborg.se/api/schema/lessons?forUser=123456_goteborgsstad&startDateIso=2021-11-01&endDateIso=2021-11-08",
"headers": { "headers": {

View File

@ -10,7 +10,7 @@ import {
pollStatusUrl, pollStatusUrl,
} from './routes' } from './routes'
export class HjarntorgetChecker extends EventEmitter { export class HjarntorgetChecker extends EventEmitter implements LoginStatusChecker {
private fetcher: Fetcher private fetcher: Fetcher
private basePollingUrl: string private basePollingUrl: string
@ -120,3 +120,10 @@ export const checkStatus = (
fetch: Fetcher, fetch: Fetcher,
basePollingUrl: string basePollingUrl: string
): LoginStatusChecker => new HjarntorgetChecker(fetch, basePollingUrl) ): LoginStatusChecker => new HjarntorgetChecker(fetch, basePollingUrl)
export class DummyStatusChecker extends EventEmitter implements LoginStatusChecker {
token = ""
async cancel(): Promise<void> {
// do nothing
}
}

View File

@ -16,7 +16,7 @@ the concrete implementation of fetch and cookie handler must be injected.
#### react-native #### react-native
```javascript ```javascript
import init from '@skolplattformen/api-skolplattformen' import init from '@skolplattformen/api'
import CookieManager from '@react-native-cookies/cookies' import CookieManager from '@react-native-cookies/cookies'
const api = init(fetch, () => CookieManager.clearAll()) const api = init(fetch, () => CookieManager.clearAll())
@ -25,7 +25,7 @@ const api = init(fetch, () => CookieManager.clearAll())
#### node #### node
```javascript ```javascript
import init from '@skolplattformen/api-skolplattformen' import init from '@skolplattformen/api'
import nodeFetch from 'node-fetch' import nodeFetch from 'node-fetch'
import fetchCookie from 'fetch-cookie/node-fetch' import fetchCookie from 'fetch-cookie/node-fetch'
import { CookieJar } from 'tough-cookie' import { CookieJar } from 'tough-cookie'

View File

@ -17,7 +17,9 @@ import {
ScheduleItem, ScheduleItem,
Skola24Child, Skola24Child,
SSOSystem, SSOSystem,
Teacher,
TimetableEntry, TimetableEntry,
SchoolContact,
URLSearchParams, URLSearchParams,
User, User,
wrap, wrap,
@ -28,7 +30,7 @@ import { decode } from 'he'
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import * as html from 'node-html-parser' import * as html from 'node-html-parser'
import * as fake from './fakeData' import * as fake from './fakeData'
import { checkStatus } from './loginStatusChecker' import { checkStatus, DummyStatusChecker } from './loginStatusChecker'
import * as parse from './parse/index' import * as parse from './parse/index'
import * as routes from './routes' import * as routes from './routes'
@ -97,6 +99,16 @@ export class ApiSkolplattformen extends EventEmitter implements Api {
} }
} }
public async getSessionHeaders(url: string): Promise<{ [index: string]: string }> {
const init = this.getRequestInit()
const cookie = await this.cookieManager.getCookieString(url)
return {
...init.headers,
cookie,
}
}
public async getSession( public async getSession(
url: string, url: string,
options?: RequestInit options?: RequestInit
@ -206,8 +218,7 @@ export class ApiSkolplattformen extends EventEmitter implements Api {
this.emit('login') this.emit('login')
}, 50) }, 50)
// eslint-disable-next-line @typescript-eslint/no-explicit-any const emitter = new DummyStatusChecker()
const emitter: any = new EventEmitter()
emitter.token = 'fake' emitter.token = 'fake'
return emitter return emitter
} }
@ -265,6 +276,39 @@ export class ApiSkolplattformen extends EventEmitter implements Api {
return parse.classmates(data) return parse.classmates(data)
} }
public async getTeachers(child: EtjanstChild): Promise<Teacher[]> {
if (this.isFake) return fakeResponse(fake.teachers(child))
const session = this.getRequestInit()
const schoolForms = (child.status || '').split(';')
let teachers: Teacher[] = []
for(let i = 0; i< schoolForms.length; i+=1){
const url = routes.teachers(child.sdsId, schoolForms[i])
// eslint-disable-next-line no-await-in-loop
const response = await this.fetch(`teachers_${schoolForms[i]}`, url, session)
// eslint-disable-next-line no-await-in-loop
const data = await response.json()
teachers = [
...teachers,
...parse.teachers(data)
]
}
return teachers
}
public async getSchoolContacts(child: EtjanstChild): Promise<SchoolContact[]> {
if(this.isFake) return fakeResponse(fake.schoolContacts(child))
const url = routes.schoolContacts(child.sdsId, child.schoolId || '')
const session = this.getRequestInit()
const response = await this.fetch('schoolContacts', url, session)
const data = await response.json()
return parse.schoolContacts(data)
}
public async getSchedule( public async getSchedule(
child: EtjanstChild, child: EtjanstChild,
from: DateTime, from: DateTime,
@ -518,6 +562,8 @@ export class ApiSkolplattformen extends EventEmitter implements Api {
return parse.timetable(json, year, week, lang) return parse.timetable(json, year, week, lang)
} }
public async logout() { public async logout() {
this.isFake = false this.isFake = false
this.personalNumber = undefined this.personalNumber = undefined

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
import { EtjanstChild, Skola24Child } from "@skolplattformen/api"
export const children = (): EtjanstChild[] => [
{
name: 'Shanel Nilsson (elev)',
id: '39b59e-bf4b9f-f68ac25321-977218-bf0',
sdsId: '8e81a06-53f55fb-d1b93-f0e5b357ad0b7caaf1d36',
status: 'F;GR',
schoolId: '9e58434-8800-da59547-614bf0e-e09c015',
},
{
name: 'Alan Nilsson (elev)',
id: 'eea96a-a3e045-caab589391-ed7d17-029',
sdsId: 'bc2d341-8d970cc-69526-43501c082aaa870d9fe99',
status: 'GR',
schoolId: '8e6b13b-3116-e66c39b-a4c3fa5-a1d72d9',
},
]
export const skola24Children = (): Skola24Child[] => [
{
firstName: 'Shanel',
lastName: 'Jonsson Nilsson',
personGuid: 'abc123',
schoolGuid: 'def456',
schoolID: 'Superskolan',
timetableID: 'jkl012',
unitGuid: 'mno345'
},
]

View File

@ -0,0 +1,448 @@
import { Child, Classmate } from '@skolplattformen/api';
import { children } from './children'
export const classmates = (child: Child): Classmate[] => classmatesData.get(child.id) ?? []
const [child1, child2] = children()
const classmatesData = new Map<string, Classmate[]>([
[
child1.id, [
{
sisId: 'd004a-98d965a-45174-d2894ca2-f74ebcb',
firstname: 'Darion',
lastname: 'Gustafsson',
guardians: [
{
email: 'Mike_Svensson@example.net',
firstname: 'Tad',
lastname: 'Eriksson',
mobile: '07074791613',
address: 'Martinvägen 50',
},
],
className: '2B',
},
{
sisId: '54075-284de06-5664c-750b7b13-520fb61',
firstname: 'Brock',
lastname: 'Andersson',
guardians: [
{
email: 'Brad56@example.org',
firstname: 'Camren',
lastname: 'Eriksson',
mobile: '07075129297',
address: undefined,
},
],
className: '2B',
},
{
sisId: 'c1fc7-285f95d-c0f37-ea48a297-281e985',
firstname: 'Eloy',
lastname: 'Karlsson',
guardians: [
{
email: 'Samara.Larsson@example.net',
firstname: 'Ike',
lastname: 'Gustafsson',
mobile: '07077667407',
address: undefined,
},
],
className: '2B',
},
{
sisId: '212e9-8a2609c-b29c1-97a32bd8-5f84645',
firstname: 'Kristina',
lastname: 'Eriksson',
guardians: [
{
email: 'Doug57@example.com',
firstname: 'Rollin',
lastname: 'Olsson',
mobile: '07071720107',
address: 'Höckertsvägen 2',
},
],
className: '2B',
},
{
sisId: '01d21-ebc6f8b-526f8-7cfba0ab-26b9956',
firstname: 'Cydney',
lastname: 'Larsson',
guardians: [
{
email: 'Davon6@example.org',
firstname: 'Oleta',
lastname: 'Svensson',
mobile: '07079762186',
address: undefined,
},
],
className: '2B',
},
{
sisId: 'a45bb-8a481af-0ad12-7bd1fa4c-1eed4b1',
firstname: 'Berneice',
lastname: 'Persson',
guardians: [
{
email: 'Milford_Johansson72@example.com',
firstname: 'Arely',
lastname: 'Johansson',
mobile: '07071926019',
address: 'Roslinvägen 36',
},
],
className: '2B',
},
{
sisId: '32f31-039fbed-9060b-2d857c46-e47177d',
firstname: 'Emory',
lastname: 'Svensson',
guardians: [
{
email: 'Alfredo_Nilsson96@example.org',
firstname: 'Dolores',
lastname: 'Andersson',
mobile: '070752561937',
address: 'Börjesonsvägen 6',
},
],
className: '2B',
},
{
sisId: 'c9d0a-28c371d-e7be2-9781386b-6841eb0',
firstname: 'Maryjane',
lastname: 'Eriksson',
guardians: [
{
email: 'Eula_Olsson@example.net',
firstname: 'Wendy',
lastname: 'Andersson',
mobile: '07078513037',
address: undefined,
},
{
email: 'Lesley_Persson45@example.org',
firstname: 'Erich',
lastname: 'Persson',
mobile: '070788191316',
address: undefined,
},
],
className: '2B',
},
{
sisId: 'e0f51-3fbd0be-5a8c3-ded7bbed-1d655d5',
firstname: 'Rosendo',
lastname: 'Eriksson',
guardians: [
{
email: 'Mitchell.Gustafsson84@example.org',
firstname: 'Mariam',
lastname: 'Johansson',
mobile: '07074537423',
address: 'Molinvägen 29',
},
{
email: 'Rachelle_Olsson@example.net',
firstname: 'Shaniya',
lastname: 'Persson',
mobile: '070765878480',
address: 'Molinvägen 29',
},
],
className: '2B',
},
{
sisId: '298c2-46a24d4-548b9-3d1f90ee-4fae0ab',
firstname: 'Sammy',
lastname: 'Persson',
guardians: [
{
email: 'Gloria_Svensson@example.com',
firstname: 'Simeon',
lastname: 'Olsson',
mobile: '070753525610',
address: 'Börjesonsvägen 43',
},
],
className: '2B',
},
{
sisId: 'e7628-09352ea-b5d19-1af845b7-63b3e08',
firstname: 'Abraham',
lastname: 'Svensson',
guardians: [
{
email: 'Erica_Johansson40@example.net',
firstname: 'Carlotta',
lastname: 'Nilsson',
mobile: '070737951712',
address: 'Aroseniusvägen 27',
},
{
email: 'Malcolm_Gustafsson55@example.org',
firstname: 'Ramon',
lastname: 'Persson',
mobile: '07070395626',
address: 'Aroseniusvägen 27',
},
],
className: '2B',
},
{
sisId: 'ae315-4696438-b3db6-8f0a5b39-74e34bd',
firstname: 'Devante',
lastname: 'Olsson',
guardians: [
{
email: 'Alf.Johansson39@example.com',
firstname: 'Schuyler',
lastname: 'Gustafsson',
mobile: '07070724289',
address: undefined,
},
],
className: '2B',
},
{
sisId: '0d812-350f1d5-323aa-d5d93cdd-406e337',
firstname: 'Tyrell',
lastname: 'Eriksson',
guardians: [
{
email: 'Brennon.Svensson@example.com',
firstname: 'Belle',
lastname: 'Nilsson',
mobile: '07070137347',
address: undefined,
},
],
className: '2B',
},
]],
[
child2.id, [
{
sisId: '9ee9e-312233c-0df98-05fa5a65-a3787ec',
firstname: 'Raphael',
lastname: 'Olsson',
guardians: [
{
email: 'Johan99@example.com',
firstname: 'Alessandra',
lastname: 'Svensson',
mobile: '070767120463',
address: 'Franklandsvägen 34',
},
],
className: '8C',
},
{
sisId: 'd3a4b-16b53de-63c22-56d1ad24-4a64a2d',
firstname: 'Fanny',
lastname: 'Karlsson',
guardians: [
{
email: 'Bernadette.Eriksson@example.org',
firstname: 'Bernadette',
lastname: 'Karlsson',
mobile: '070759877956',
address: undefined,
},
{
email: 'Candice29@example.net',
firstname: 'Kelley',
lastname: 'Gustafsson',
mobile: '070748592035',
address: undefined,
},
],
className: '8C',
},
{
sisId: '42bde-8fabd1c-7a00e-28aea88a-8481bac',
firstname: 'Jamie',
lastname: 'Persson',
guardians: [
{
email: 'Louisa82@example.net',
firstname: 'Mose',
lastname: 'Larsson',
mobile: '07076548362',
address: undefined,
},
],
className: '8C',
},
{
sisId: 'dad49-74308c8-83612-5eb7f3a5-e1c4047',
firstname: 'Iris',
lastname: 'Eriksson',
guardians: [
{
email: 'Vaughn90@example.net',
firstname: 'Ezra',
lastname: 'Andersson',
mobile: '07078700165',
address: 'Björnsonsgatan 251 D Lgh 1503',
},
{
email: 'Stephany_Svensson22@example.net',
firstname: 'Mia',
lastname: 'Larsson',
mobile: '070761752378',
address: 'Björnsonsgatan 251 D Lgh 1503',
},
],
className: '8C',
},
{
sisId: 'b3425-ada6d70-d3acc-a49a12a6-8b3afdc',
firstname: 'Evans',
lastname: 'Nilsson',
guardians: [
{
email: 'Terry_Svensson@example.com',
firstname: 'Christop',
lastname: 'Olsson',
mobile: '070767660094',
address: undefined,
},
{
email: 'Johanna_Svensson30@example.org',
firstname: 'Madisen',
lastname: 'Johansson',
mobile: '07072269029',
address: undefined,
},
],
className: '8C',
},
{
sisId: '67471-6c03979-9ef6e-bb2827c4-96d00d5',
firstname: 'Evy',
lastname: 'Larsson',
guardians: [
{
email: 'Serenity.Gustafsson@example.net',
firstname: 'Toni',
lastname: 'Larsson',
mobile: '07075211567',
address: 'Roslinvägen 48',
},
],
className: '8C',
},
{
sisId: 'f4040-516c4ed-34555-fd525183-6a2f666',
firstname: 'Maximillia',
lastname: 'Karlsson',
guardians: [
{
email: 'Faustino.Andersson@example.com',
firstname: 'Eriberto',
lastname: 'Nilsson',
mobile: '07076024039',
address: 'Beckombergavägen 213 Lgh 1304',
},
],
className: '8C',
},
{
sisId: 'a9494-75d8ca7-a5fd4-977eca3c-40edbc1',
firstname: 'Pia',
lastname: 'Karlsson',
guardians: [
{
email: 'Arthur.Karlsson4@example.org',
firstname: 'Eldred',
lastname: 'Svensson',
mobile: '07077609534',
address: 'Börjesonsvägen 6',
},
],
className: '8C',
},
{
sisId: '42a6d-3eaf407-fed01-4a9538de-b822503',
firstname: 'Logan',
lastname: 'Larsson',
guardians: [
{
email: 'Blake4@example.org',
firstname: 'Jan',
lastname: 'Karlsson',
mobile: '070728715653',
address: 'Bällstavägen 162',
},
],
className: '8C',
},
{
sisId: '9077d-c323c8d-d0d29-5690abfb-d348317',
firstname: 'Torun',
lastname: 'Eriksson',
guardians: [
{
email: 'Blanca98@example.net',
firstname: 'Dallin',
lastname: 'Eriksson',
mobile: '070766214425',
address: 'Molinvägen 1',
},
],
className: '8C',
},
{
sisId: '31c68-5b86667-0701d-6b7e2471-89e6df9',
firstname: 'Izabella',
lastname: 'Johansson',
guardians: [
{
email: 'Elouise_Johansson25@example.org',
firstname: 'Jerrold',
lastname: 'Nilsson',
mobile: '07073789274',
address: 'Stobaeusvägen 11',
},
],
className: '8C',
},
{
sisId: '1bb69-5f1c3a6-f0ea8-e1dbb608-2756a52',
firstname: 'Ella',
lastname: 'Persson',
guardians: [
{
email: 'Shayna.Olsson54@example.net',
firstname: 'Onie',
lastname: 'Nilsson',
mobile: '07076957797',
address: undefined,
},
],
className: '8C',
},
{
sisId: '348a7-2d0eccc-02981-a02ccb03-cb2a8f2',
firstname: 'Jaylen',
lastname: 'Larsson',
guardians: [
{
email: 'Aileen_Andersson@example.net',
firstname: 'Tess',
lastname: 'Karlsson',
mobile: '070715315590',
address: 'Peringskiöldsvägen 64',
},
],
className: '8C',
}
],
]
])

View File

@ -0,0 +1,509 @@
import { fourDaysAgo, oneDayAgo, oneWeekAgo } from './dates';
/* eslint-disable max-len */
import {
CalendarItem,
Child,
Notification,
ScheduleItem,
User,
} from '@skolplattformen/api';
import { oneDayForward, oneWeekForward, twoDaysForward } from './dates';
const data: any = {
'39b59e-bf4b9f-f68ac25321-977218-bf0': {
calendar: [
{
title: 'Terminslut',
id: 73,
description: null,
location: null,
startDate: '2020-12-18',
endDate: '2020-12-18',
allDay: true,
},
{
title: 'Terminen börjar',
id: 74,
description: null,
location: null,
startDate: '2021-01-12',
endDate: '2021-01-12',
allDay: true,
},
{
title: 'APT - fritids stänger 15:45',
id: 75,
description: null,
location: null,
startDate: '2021-01-21',
endDate: '2021-01-21',
allDay: true,
},
{
title: 'Utvecklingsamtal',
id: 76,
description: null,
location: null,
startDate: '2021-02-04',
endDate: '2021-02-04',
allDay: true,
},
{
title: 'Vänliga veckan',
id: 77,
description: null,
location: null,
startDate: '2021-02-08',
endDate: '2021-02-12',
allDay: true,
},
{
title: 'Utvecklingsamtal',
id: 79,
description: null,
location: null,
startDate: '2021-02-09',
endDate: '2021-02-09',
allDay: true,
},
{
title: 'Trygghetsdag',
id: 78,
description: null,
location: null,
startDate: '2021-02-12',
endDate: '2021-02-12',
allDay: true,
},
{
title: 'APT fritids stänger 15:45',
id: 80,
description: null,
location: null,
startDate: '2021-02-25',
endDate: '2021-02-25',
allDay: true,
},
{
title: 'Sportlov',
id: 81,
description: null,
location: null,
startDate: '2021-03-01',
endDate: '2021-03-05',
allDay: true,
},
{
title: 'Studiedag',
id: 82,
description: null,
location: null,
startDate: oneWeekForward.startOf('day').toISODate(),
endDate: oneWeekForward.endOf('day').toISODate(),
allDay: true,
},
{
title: 'APT - fritids stänger 15:45',
id: 83,
description: null,
location: null,
startDate: '2021-04-01',
endDate: '2021-04-01',
allDay: true,
},
{
title: 'Långfredag',
id: 84,
description: null,
location: null,
startDate: '2021-04-02',
endDate: '2021-04-02',
allDay: true,
},
{
title: 'Påsklov',
id: 85,
description: null,
location: null,
startDate: '2021-04-05',
endDate: '2021-04-09',
allDay: true,
},
{
title: 'Föräldraråd',
id: 86,
description: null,
location: null,
startDate: '2021-04-20',
endDate: '2021-04-20',
allDay: true,
},
{
title: 'Prao åk 8',
id: 97,
description: null,
location: null,
startDate: '2021-04-26',
endDate: '2021-05-12',
allDay: true,
},
{
title: 'Kristi Himmelfärd',
id: 87,
description: null,
location: null,
startDate: '2021-05-13',
endDate: '2021-05-13',
allDay: true,
},
{
title: 'Lov',
id: 88,
description: null,
location: null,
startDate: '2021-05-14',
endDate: '2021-05-14',
allDay: true,
},
{
title: 'APT Fritids stänger 15:45',
id: 90,
description: null,
location: null,
startDate: '2021-05-20',
endDate: '2021-05-20',
allDay: true,
},
{
title: 'Läsårsslut',
id: 91,
description:
"<html><head><style>\r\np.MsoNormal, li.MsoNormal, div.MsoNormal {\nmargin:0cm;\nmargin-bottom:.0001pt;\nfont-size:11.0pt;\nfont-family:'Calibri',sans-serif;\n}\n\na:link, span.MsoHyperlink {\ncolor:#0563C1;\ntext-decoration:underline;\n}\n\nspan.MsoHyperlinkFollowed {\ncolor:#954F72;\ntext-decoration:underline;\n}\n\nspan.E-postmall17 {\nfont-family:'Calibri',sans-serif;\ncolor:windowtext;\n}\n\n.MsoChpDefault {\nfont-family:'Calibri',sans-serif;\n}\n\ndiv.WordSection1 {\n}\n\r\n</style></head><body lang='SV' link='#0563C1' vlink='#954F72' style=''><div class='WordSection1'><p class='MsoNormal'>&#160;</p></div></body></html>",
location: null,
startDate: '2021-06-11',
endDate: '2021-06-11',
allDay: true,
},
{
title: 'Fritids stängt',
id: 92,
description:
"<html><head><style>\r\np.MsoNormal, li.MsoNormal, div.MsoNormal {\nmargin:0cm;\nmargin-bottom:.0001pt;\nfont-size:11.0pt;\nfont-family:'Calibri',sans-serif;\n}\n\na:link, span.MsoHyperlink {\ncolor:#0563C1;\ntext-decoration:underline;\n}\n\nspan.MsoHyperlinkFollowed {\ncolor:#954F72;\ntext-decoration:underline;\n}\n\nspan.E-postmall17 {\nfont-family:'Calibri',sans-serif;\ncolor:windowtext;\n}\n\n.MsoChpDefault {\nfont-family:'Calibri',sans-serif;\n}\n\ndiv.WordSection1 {\n}\n\r\n</style></head><body lang='SV' link='#0563C1' vlink='#954F72' style=''><div class='WordSection1'><p class='MsoNormal'>&#160;</p></div></body></html>",
location: null,
startDate: '2021-06-14',
endDate: '2021-06-14',
allDay: true,
},
],
schedule: [
{
title: 'Läsläxan tillbaka',
description: 'Ta med boken tillbaka till skolan',
location: '',
allDayEvent: false,
startDate: oneDayForward.startOf('day').toISO(),
endDate: oneDayForward.endOf('day').toISO(),
oneDayEvent: true
} as ScheduleItem
],
notifications: [
{
id: 'bfe19b-766db3-b38d99d321-bbed3d-506',
sender: 'Planering och Bedömning',
dateCreated: oneDayAgo.minus({months: 6}).toISO(),
dateModified: fourDaysAgo.toISO(),
message: 'Ett nytt inlägg i en lärlogg har skapats.',
url:
'https://www.breakit.se/artikel/21423/har-ar-it-bolaget-bakom-haveriet-pa-skolplattformen',
category: 'Lärlogg',
type: 'avisering',
},
{
id: '9025f9-a1e685-d7c4668f09-e14bc5-0ab',
sender: 'Elevdokumentation',
dateCreated: '2020-12-10T14:31:29.966Z',
message:
'Nu kan du ta del av ditt barns dokumentation av utvecklingssamtal',
url:
'https://www.breakit.se/artikel/21404/kodaren-slog-larm-nu-akutstoppas-skolplattformen-i-stockholm',
category: null,
type: 'webnotify',
},
{
id: 'a24061-1c9a4e-83dc479d7c-f44fe9-376',
sender: 'Planering och Bedömning',
dateCreated: '2020-06-10T12:18:00.000Z',
message: 'Nu finns det en bedömning att titta på.',
url:
'https://www.svt.se/nyheter/lokalt/stockholm/skolplattformen-i-stockholm-beratta-om-era-erfarenheter',
category: 'Bedömning',
type: 'avisering',
},
{
id: '79d65c-1f8240-35c94296ec-9f4bdc-cea',
sender: 'Planering och Bedömning',
dateCreated: '2020-03-24T14:28:00.000Z',
message: 'Nu finns det en bedömning att titta på.',
url:
'https://www.breakit.se/artikel/18120/skolplattformen-kostade-700-miljoner-strid-med-entreprenor-om-varumarket',
category: 'Bedömning',
type: 'avisering',
},
{
id: '9c5b7b-52c16d-b9fc2e8248-e4de76-279',
sender: 'Planering och Bedömning',
dateCreated: '2020-03-24T13:48:00.000Z',
message: 'Nu finns det en bedömning att titta på.',
url:
'https://www.mitti.se/nyheter/forskolans-tur-att-fa-kritiserade-skolplattformen-app/lmsau!5338007/',
category: 'Bedömning',
type: 'avisering',
},
],
},
'eea96a-a3e045-caab589391-ed7d17-029': {
calendar: [
{
title: 'Terminslut',
id: 73,
description: null,
location: null,
startDate: '2020-12-18',
endDate: '2020-12-18',
allDay: true,
},
{
title: 'Terminen börjar',
id: 74,
description: null,
location: null,
startDate: '2021-01-12',
endDate: '2021-01-12',
allDay: true,
},
{
title: 'APT - fritids stänger 15:45',
id: 75,
description: null,
location: null,
startDate: oneWeekForward.startOf('day').toISODate(),
endDate: oneWeekForward.endOf('day').toISODate(),
allDay: true,
},
{
title: 'Utvecklingsamtal',
id: 76,
description: null,
location: null,
startDate: '2021-02-04',
endDate: '2021-02-04',
allDay: true,
},
{
title: 'Vänliga veckan',
id: 77,
description: null,
location: null,
startDate: '2021-02-08',
endDate: '2021-02-12',
allDay: true,
},
{
title: 'Utvecklingsamtal',
id: 79,
description: null,
location: null,
startDate: '2021-02-09',
endDate: '2021-02-09',
allDay: true,
},
{
title: 'Trygghetsdag',
id: 78,
description: null,
location: null,
startDate: '2021-02-12',
endDate: '2021-02-12',
allDay: true,
},
{
title: 'APT fritids stänger 15:45',
id: 80,
description: null,
location: null,
startDate: '2021-02-25',
endDate: '2021-02-25',
allDay: true,
},
{
title: 'Sportlov',
id: 81,
description: null,
location: null,
startDate: '2021-03-01',
endDate: '2021-03-05',
allDay: true,
},
{
title: 'Studiedag',
id: 82,
description: null,
location: null,
startDate: '2021-03-22',
endDate: '2021-03-22',
allDay: true,
},
{
title: 'APT - fritids stänger 15:45',
id: 83,
description: null,
location: null,
startDate: '2021-04-01',
endDate: '2021-04-01',
allDay: true,
},
{
title: 'Långfredag',
id: 84,
description: null,
location: null,
startDate: '2021-04-02',
endDate: '2021-04-02',
allDay: true,
},
{
title: 'Påsklov',
id: 85,
description: null,
location: null,
startDate: '2021-04-05',
endDate: '2021-04-09',
allDay: true,
},
{
title: 'Föräldraråd',
id: 86,
description: null,
location: null,
startDate: '2021-04-20',
endDate: '2021-04-20',
allDay: true,
},
{
title: 'Prao åk 8',
id: 97,
description: null,
location: null,
startDate: '2021-04-26',
endDate: '2021-05-12',
allDay: true,
},
{
title: 'Kristi Himmelfärd',
id: 87,
description: null,
location: null,
startDate: '2021-05-13',
endDate: '2021-05-13',
allDay: true,
},
{
title: 'Lov',
id: 88,
description: null,
location: null,
startDate: '2021-05-14',
endDate: '2021-05-14',
allDay: true,
},
{
title: 'APT Fritids stänger 15:45',
id: 90,
description: null,
location: null,
startDate: '2021-05-20',
endDate: '2021-05-20',
allDay: true,
},
],
schedule: [
{
title: 'Läxförhör franska',
description: 'Läxförhör, glosor samt verben!',
location: 'Klassrummet',
allDayEvent: false,
startDate: twoDaysForward.startOf('day').toISO(),
endDate: twoDaysForward.endOf('day').toISO(),
oneDayEvent: false
} as ScheduleItem
],
notifications: [
{
id: 'e1b5bc-597fa8-5511794939-3614e1-615',
sender: 'Planering och Bedömning',
dateCreated: fourDaysAgo.toISO(),
dateModified: fourDaysAgo.toISO(),
message: 'Ett nytt inlägg i en lärlogg har skapats.',
url:
'https://www.mitti.se/nyheter/rekorddyr-skolplattform-kostar-258-miljoner-till/lmsao!5381301/',
category: 'Lärlogg',
messageType: 'avisering',
},
{
id: '7dbc20-bfa1ac-e20171b865-82c1f7-f3c',
sender: 'Planering och Bedömning',
dateCreated: '2020-12-01T12:43:00.000Z',
message: 'Ett nytt inlägg i en lärlogg har skapats.',
url:
'https://computersweden.idg.se/2.2683/1.722561/lacka-skolplattformen-datainspektionen',
category: 'Lärlogg',
messageType: 'avisering',
},
{
id: 'a6829b-ecf912-b71582e8fb-b6dc14-f60',
sender: 'Planering och Bedömning',
dateCreated: '2020-11-24T13:34:00.000Z',
message: 'Ett nytt inlägg i en lärlogg har skapats.',
url: 'https://www.dagensarena.se/redaktionen/en-systemkramare-ger-upp/',
category: 'Lärlogg',
messageType: 'avisering',
},
{
id: '3cedb4-767d24-8ccd6ac3ac-c05cb7-a3a',
sender: 'Planering och Bedömning',
dateCreated: '2020-11-16T13:24:00.000Z',
message: 'Ett nytt inlägg i en lärlogg har skapats.',
url:
'https://www.breakit.se/artikel/27075/skolplattformen-kostade-1-miljard-att-bygga-nu-tvingas-stockholm-bota',
category: 'Lärlogg',
messageType: 'avisering',
},
{
id: '6ace13-5f99da-d1d50ac7a6-4a6108-d8e',
sender: 'Planering och Bedömning',
dateCreated: '2020-11-12T13:27:00.000Z',
message: 'Ett nytt inlägg i en lärlogg har skapats.',
url:
'https://www.nyteknik.se/sakerhet/ygeman-om-datalackan-i-skolplattformen-det-ar-upprorande-6968853',
category: 'Lärlogg',
messageType: 'avisering',
},
],
},
}
export const user = (): User => ({
personalNumber: '195001182046', // Test personal number from Skatteverket
firstName: 'Namn',
lastName: 'Namnsson',
isAuthenticated: true
})
export const calendar = (child: Child): CalendarItem[] =>
data[child.id].calendar
export const schedule = (child: Child): ScheduleItem[] =>
data[child.id].schedule
export const notifications = (child: Child): Notification[] =>
data[child.id].notifications

View File

@ -0,0 +1,14 @@
import { DateTime } from "luxon"
export const getDate = () => DateTime.now()
export const oneDayAgo = getDate().minus({days: 1})
export const twoDaysAgo = getDate().minus({days: 2})
export const fourDaysAgo = getDate().minus({days: 4})
export const oneWeekAgo = getDate().minus({weeks: 1})
export const oneDayForward = getDate().plus({days: 1})
export const twoDaysForward = getDate().plus({days: 2})
export const fourDaysForward = getDate().plus({days: 4})
export const oneWeekForward = getDate().plus({weeks: 1})
export const week = getDate().weekNumber.toString()

View File

@ -0,0 +1,8 @@
export * from './data'
export * from './children'
export * from './menu'
export * from './classmates'
export * from './teachers'
export * from './timetable'
export * from './schoolContacts'
export * from './news'

View File

@ -0,0 +1,64 @@
import { Child, MenuItem } from '@skolplattformen/api'
import { DateTime } from 'luxon'
import { children } from './children'
export const menu = (child: Child): MenuItem[] => menuData.get(child.id) ?? []
const getDate = () => DateTime.now()
const week = getDate().weekNumber.toString()
const [child1, child2] = children()
const menuData = new Map<string, MenuItem[]>([
[
child1.id,
[
{
title: 'Måndag - Vecka ' + week,
description: 'Kebabgryta ris<br/>Ratatouille med kikärter',
},
{
title: 'Tisdag - Vecka ' + week,
description: 'Ost-broccolisås pasta Fusilli',
},
{
title: 'Onsdag - Vecka ' + week,
description: 'Köttbullar potatis gräddsås lingon<br/>Falafel',
},
{
title: 'Torsdag - Vecka ' + week,
description:
'Prinskorv potatis rödbetssallad +<br/>Inlagd och senapssill',
},
{
title: 'Fredag - Vecka ' + week,
description:
'Avslutning Varmkorv bröd ketchup senap<br/>( F-3 i matsalen från 10:30 )',
},
],
],
[child2.id,
[
{
title: "Måndag - Vecka " + week,
description: "Thailändsk kycklinggryta med kokosmjölk, rödcurry och jasminris<br/>Thailänsk grönsaksgryta med kokosmjölk, rödcurry och jasminris"
},
{
title: "Tisdag - Vecka " + week,
description: "Örtomlett med potatis , medelhavsost och olivtapenad"
},
{
title: "Onsdag - Vecka " + week,
description: "Spagetti med rökt kalkon , grädde, dijon och persilja<br/>Spagetti med rostade bönor , grädde , dijon och persilja"
},
{
title: "Torsdag - Vecka " + week,
description: "Panerad flundra med dansk remoulad och koktåotatis<br/>morot och linsbiff med danska remoulad och koktpotatis"
},
{
title: "Fredag - Vecka " + week,
description: "Texaschili på högrev med picklad rödlök och bulgur<br/>Texaschili på svartabönor picklad rödlök och bulgur"
}
],
]
])

View File

@ -0,0 +1,143 @@
import { children } from './children'
import { Child, NewsItem } from '@skolplattformen/api'
import * as dates from './dates'
export const news = (child: Child): NewsItem[] => newsData.get(child.id) ?? []
const [child1, child2] = children()
const newsData = new Map<string, NewsItem[]>([
[child1.id, [
{
id: 'asdfasdfasdfw',
author: 'Vaktmästare Persson',
header: 'Brandsläckare!',
intro: 'Idag hade vi en incident med en brandsläckare.',
body:
'## Information om brandsläckarincidenten\n\nHej, idag vid lunchtid utlöste en elev av misstag en pulverbrandsläckare i kapprummet. En del pulver yrde runt i rummet och under saneringen fick eleverna i angränsande klassrum vara i aulan istället för klassrummet.\n\nFlera elever var på plats i hallen när detta inträffade men utrymdes kort därefter. Pulvret är INTE hälsovådligt men kan ge upphov till halsirritation vid inandning.\n\nJag har pratat med berörda elever om det inträffade och uppmanat dem att ta hem kläder och tillhörigheter som fanns i kapprummet eftersom de troligen blivit dammiga. Vi rekommenderar att ni tvättar eller vädrar dessa.',
imageUrl: '6607f9b923edb6f85aa4417bab43c0f8.jpg',
fullImageUrl:
'https://cdn.breakit.se/assets/article/6607f9b923edb6f85aa4417bab43c0f8.jpg?d=980x500',
imageAltText: 'Nyhetsbild. Bildtext ej tillgänglig.',
published: dates.twoDaysAgo.toISO(),
modified: dates.twoDaysAgo.plus({ hours: 1 }).toISO(),
},
{
id: 'asdfabbuasdfs',
author: 'Ada L.',
header: 'Bygg din egen app',
intro: 'Denna vecka bygger vi appar!',
body:
'## Appar med öppen data \n\nDenna vecka har vi förmånen att få besök av några föräldrar som visar hur vi enkelt kan skapa appar som visar information ifrån öppna datakällor.\n\nEn fantastisk möjlighet att lära oss hur digitalisering skapar nya möjligheter i såväl skolan som arbetslivet.',
imageUrl: '6607f9b923edb6f85aa4417bab43c0f8.jpg',
fullImageUrl:
'https://live.staticflickr.com/4063/4369776892_5cd42d27ba.jpg',
imageAltText: 'Nyhetsbild. Bildtext ej tillgänglig.',
published: dates.oneWeekAgo.toISO(),
modified: dates.oneWeekAgo.toISO(),
},
{
id: 'asdfasdfasdfs',
author: 'Magister Svensson',
header: 'Läxor vecka 6.',
intro: 'Alla elever måste göra sina läxor!',
body:
'## Läxor vecka 6 \n\nFöljande läxor är obligatoriska:\n\n- Antikens historia\n- Svenska stormaktstiden\n- Statistik A\n- Flerdimensionell analys, del 1',
imageUrl: '6607f9b923edb6f85aa4417bab43c0f8.jpg',
fullImageUrl:
'https://www.mitti.se/_internal/cimg!0/ejf8efxee735ymm8tm40q3hhkl36sdt.jpeg',
imageAltText: 'Nyhetsbild. Bildtext ej tillgänglig.',
published: dates.oneWeekAgo.toISO(),
modified: dates.oneWeekAgo.minus({ hours: 3 }).toISO(),
},
]
],
[child2.id, [
{
id: 'asdfasdfasdfa',
author: 'Rektor Gustavsson',
header: 'Välkommen till skolan!',
intro:
'Hej alla barn och föräldrar och välkomna till Storskolan! Här kommer en del information som kan vara bra att känna till inför första dagen.',
body:
'## Information till föräldrar \n\nSkolan börjar kl 08.00 och slutar 18.00. Kommer man sent eller blir sjuk så ska det anmälas via Skolplattformen. Se till så att dina barn har ätit frukost. Frukt är nyttigt! \n\n## Information till barn\n\nLek är tillåtet på rasterna men enbart på skolgården. Medtag ej egna leksaker. Tvätta händerna.',
imageUrl: '6607f9b923edb6f85aa4417bab43c0f8.jpg',
fullImageUrl:
'https://timbro.se/app/uploads/2020/10/broman-skolplattformen-1280x752.jpg',
imageAltText: 'Nyhetsbild. Bildtext ej tillgänglig.',
published: dates.oneWeekAgo.toISO(),
modified: dates.oneWeekAgo.toISO(),
},
{
id: 'asdfabbuasdfs',
author: 'Ada L.',
header: 'App, App, App',
intro: 'Denna vecka bygger vi appar!',
body:
'## Appar med öppen data \n\nDenna vecka har vi förmånen att få besök av några föräldrar som visar hur vi enkelt kan skapa appar som visar information ifrån öppna datakällor.\n\nEn fantastisk möjlighet att lära oss hur digitalisering skapar nya möjligheter i såväl skolan som arbetslivet.',
imageUrl: '6607f9b923edb6f85aa4417bab43c0f8.jpg',
fullImageUrl:
'https://live.staticflickr.com/4063/4369776892_5cd42d27ba.jpg',
imageAltText: 'Nyhetsbild. Bildtext ej tillgänglig.',
published: dates.fourDaysAgo.toISO(),
modified: dates.fourDaysAgo.plus({minutes: 45}).toISO(),
},
{
id: 'asdfasdfasdfs',
author: 'Magister Svensson',
header: 'Läxor i veckan',
intro: 'Alla elever måste göra sina läxor!',
body:
'## Läxor vecka 6 \n\nFöljande läxor är obligatoriska:\n\n- Antikens historia\n- Svenska stormaktstiden\n- Statistik A\n- Flerdimensionell analys, del 1',
imageUrl: '6607f9b923edb6f85aa4417bab43c0f8.jpg',
fullImageUrl:
'https://www.mitti.se/_internal/cimg!0/ejf8efxee735ymm8tm40q3hhkl36sdt.jpeg',
imageAltText: 'Nyhetsbild. Bildtext ej tillgänglig.',
published: dates.oneWeekAgo.toISO(),
modified: dates.oneWeekAgo.toISO(),
},
{
id: 'asdfasdfasdfd',
author: 'Information från Förskoleklass',
header: 'Vinteraktiviteter',
intro:
'Vi kommer efter att förskoleklassen är slut arrangera olika vinteraktiviteter genom fridtidsverksamheten.',
body:
'## Vänligen ta med hjälm, skridskor eller stjärtlapp.\n\n ![Bild](https://images.unsplash.com/photo-1495377701095-00261b767581?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=988&q=80)\n\n Alla barn måste ha hjälm på sig samt varma kläder. Vi kommer åka i backen bakom skolbyggnaden samt använda isen som spolats vid Mullsjöskolan. Personal kommer finnas på plats samt att vi erbjuda varm dryck, frukt och lek för de barn som ej har hjälm eller lämpligt åkdon.',
imageUrl: '6607f9b923edb6f85aa4417bab43c0f8.jpg',
fullImageUrl: 'https://unsplash.com/photos/yB_aiAWkm40',
imageAltText: 'Nyhetsbild. Bildtext ej tillgänglig.',
published: dates.oneWeekAgo.minus({weeks: 2}).toISO(),
modified: dates.oneWeekAgo.minus({weeks: 1}).toISO(),
},
{
id: 'asdfasdfasdfdsa',
author: 'Köket',
header: 'Ekologisk vecka i matsalen',
intro: 'Ekologiska veckan i matsalen vecka 11',
body:
'## Vi kommer ha tema jorden i matsalen och servera ekologisk mat från hela världen med tema jorden. Detta för att belysa att man kan använda alla delar av råvaorna. Det kommer erbjudas rätter från alla världsdelar som är producerat för jordens bästa. Smaklig spis hälsar Gunnel i köket med personal.',
imageUrl: '6607f9b923edb6f85aa4417bab43c0f8.jpg',
fullImageUrl: 'https://unsplash.com/photos/7K17MvT8qBg',
imageAltText: 'Nyhetsbild. Bildtext ej tillgänglig.',
published: dates.oneWeekAgo.minus({weeks: 3}).toISO(),
modified: dates.oneWeekAgo.minus({days: 2}).toISO(),
},
{
id: 'asdfasdfasdfbvdsa',
author: 'Vaktmästaren',
header: 'Klotter i korridoren (igen)',
intro:
'Ännu en gång har vi råka ut för skadegörelse i korridorerna vid åk 5',
body:
'## Tyvärr har flera elever klottat på skåp och väggar vid åk5 skåpen. Detta är helt oacceptablet beteende och kostar skolan stora belopp att åtgärda. Vi ber alla föräldrar prata med sina barn om klotter samt att det var väldigt grovt spårkbruk. Personalen på skolan kommer att hålla extra uppsikt och vi har även pratat med en del av de inblandade eleverna i denna skadegörelse.\n\nPersonalen har även börjat forska på vad vissa av de skrivna orden betyder och Eva-Britt är förfasad över språkbruket samt vad de innebär. Bernt kommer att påbörja saneringen och återställningen av skadegörelsen samt vakta korridorerna nogrannare för att säkerställa att detta ej kommer ske igen.\n\n Klotter\n\nUPPDATERING: Det som är skrivet om Sara är inte sant! ',
imageUrl: '6607f9b923edb6f85aa4417bab43c0f8.jpg',
fullImageUrl: 'https://unsplash.com/photos/SkbEZ16VywM',
imageAltText: 'Nyhetsbild. Bildtext ej tillgänglig.',
published: dates.oneWeekAgo.minus({weeks: 4}).toISO(),
modified: dates.oneWeekAgo.minus({weeks: 2}).toISO(),
},
]
]
])

View File

@ -0,0 +1,47 @@
import { SchoolContact, Child } from '@skolplattformen/api';
import { children } from './children'
export const schoolContacts = (child: Child): SchoolContact[] => schoolContactData.get(child.id) ?? []
const [child1,child2] = children()
const schoolContactData = new Map<string, SchoolContact[]>([
[
child1.id, [
{
title: "Expedition",
name: null,
phone: "508 000 00",
email: "",
schoolName: "Vallaskolan",
className: null,
},
{
title: "Rektor",
name: "Alvar Sträng",
phone: "08-50800001",
email: "alvar.strang@edu.stockholm.se",
schoolName: null,
className: null,
}
]],
[
child2.id, [
{
title: "Expedition",
name: null,
phone: "508 000 00",
email: "",
schoolName: "Vallaskolan",
className: null,
},
{
title: "Rektor",
name: "Alvar Sträng",
phone: "08-50800001",
email: "alvar.strang@edu.stockholm.se",
schoolName: null,
className: null,
}
]]
])

View File

@ -0,0 +1,81 @@
import { Teacher, Child } from '@skolplattformen/api';
import { children } from './children'
export const teachers = (child: Child): Teacher[] => teacherData.get(child.id) ?? []
const [child1,child2] = children()
const teacherData = new Map<string, Teacher[]>([
[
child1.id, [
{
id: 15662220,
firstname: "Cecilia",
sisId: null,
lastname: "Test",
email: "cecilia.test@edu.stockholm.se",
phoneWork: null,
active: true,
status: " S",
timeTableAbbreviation: 'CTE',
},
{
id: 15662221,
firstname: "Anna",
lastname: "Test",
sisId: null,
email: "anna.test@edu.stockholm.se",
phoneWork: '08000000',
active: true,
status: " GR",
timeTableAbbreviation: 'ATE',
},
{
id: 15662221,
firstname: "Greta",
lastname: "Test",
sisId: null,
email: null,
phoneWork: '08000001',
active: true,
status: " F",
timeTableAbbreviation: 'GTE',
},
]],
[
child2.id, [
{
id: 15662220,
firstname: "Cecilia",
sisId: null,
lastname: "Test",
email: "cecilia.test@edu.stockholm.se",
phoneWork: null,
active: true,
status: " S",
timeTableAbbreviation: 'CTE',
},
{
id: 15662221,
firstname: "Anna",
lastname: "Test",
sisId: null,
email: "anna.test@edu.stockholm.se",
phoneWork: '08000000',
active: true,
status: " GR",
timeTableAbbreviation: 'ATE',
},
{
id: 15662221,
firstname: "Greta",
lastname: "Test",
sisId: null,
email: null,
phoneWork: '08000001',
active: true,
status: " F",
timeTableAbbreviation: 'GTE',
},
]],
])

View File

@ -0,0 +1,465 @@
import { Skola24Child, TimetableEntry } from "@skolplattformen/api"
export const timetable = (child: Skola24Child): TimetableEntry[] => {
if (!child.personGuid || !child.unitGuid) return []
return [
{
id: 'N2FjMDc1NjYtZmM2Yy0wZDQyLTY3M2YtZWI5NGNiZDA3ZGU4',
code: 'Lunch',
name: 'Lunch',
category: '',
blockName: '',
dayOfWeek: 1,
location: 'Ö5',
teacher: '',
timeEnd: '12:05:00',
timeStart: '11:40:00',
dateStart: '2021-04-12T11:40:00.000+02:00',
dateEnd: '2021-04-12T12:05:00.000+02:00',
},
{
id: 'ZTQ1NWE0N2EtNzAwOS0wZTAzLTQ1ZDYtNTA1NWI4Y2JhNDYw',
code: 'BL',
name: 'Bild',
category: '',
blockName: '',
dayOfWeek: 1,
location: '221',
teacher: 'CTe',
timeEnd: '11:35:00',
timeStart: '09:40:00',
dateStart: '2021-04-12T09:40:00.000+02:00',
dateEnd: '2021-04-12T11:35:00.000+02:00',
},
{
id: 'YjAxODRmY2QtNTJjZS0wMDJlLTYxOGItYmFlNTVlNDgzZmVk',
code: 'NO',
name: 'Naturorienterande ämnen',
category: '',
comment: 'a)',
blockName: '',
dayOfWeek: 1,
location: '307',
teacher: 'TBo',
timeEnd: '13:30:00',
timeStart: '12:30:00',
dateStart: '2021-04-12T12:30:00.000+02:00',
dateEnd: '2021-04-12T13:30:00.000+02:00',
},
{
id: 'MWRiZGI1NzgtYWIzNy0wYzMwLTVkMmEtMWFjNWRkMTRmOTdh',
code: 'IDH',
name: 'Idrott & hälsa',
category: '',
blockName: '',
dayOfWeek: 1,
location: '215',
teacher: 'ATe, GTe',
timeEnd: '15:45:00',
timeStart: '14:40:00',
dateStart: '2021-04-12T14:40:00.000+02:00',
dateEnd: '2021-04-12T15:45:00.000+02:00',
},
{
id: 'MmZkZTZiMzMtMjdjMS0wZGIzLTUzYWYtZTg0Zjc1NDRlNzQw',
code: 'M2FR',
name: 'Franska',
category: 'Moderna språk, språkval',
blockName: '',
dayOfWeek: 1,
location: '304',
teacher: 'CTe,ATe',
timeEnd: '14:25:00',
timeStart: '13:40:00',
dateStart: '2021-04-12T13:40:00.000+02:00',
dateEnd: '2021-04-12T14:25:00.000+02:00',
},
{
id: 'MzAxMzU3MWItZGM1Ny0wOGVhLTVkZjUtOGFkMGIyYTY2OTAx',
code: 'SO',
name: 'Samhällsorienterande ämnen',
category: '',
blockName: '',
dayOfWeek: 1,
location: '303',
teacher: 'HRr',
timeEnd: '09:25:00',
timeStart: '08:15:00',
dateStart: '2021-04-12T08:15:00.000+02:00',
dateEnd: '2021-04-12T09:25:00.000+02:00',
},
{
id: 'NDY3MDY1MmYtOTIzYi0wZmQ0LTVlZGEtNGVhZDRkOTExNTgz',
code: 'M2FR',
name: 'Franska',
category: 'Moderna språk, språkval',
blockName: '',
dayOfWeek: 2,
location: '302,Fjärr asd asdasd asdad aasdds',
teacher: 'DNi',
timeEnd: '09:50:00',
timeStart: '09:05:00',
dateStart: '2021-04-13T09:05:00.000+02:00',
dateEnd: '2021-04-13T09:50:00.000+02:00',
},
{
id: 'NmE4OTU1NmItYzM0ZS0wYTI1LTYzM2QtYzBiN2M4OTVmYTQ3',
code: 'EN',
name: 'Engelska',
category: '',
blockName: '',
dayOfWeek: 2,
location: 'Fjärr',
teacher: 'TPe',
timeEnd: '13:15:00',
timeStart: '12:30:00',
dateStart: '2021-04-13T12:30:00.000+02:00',
dateEnd: '2021-04-13T13:15:00.000+02:00',
},
{
id: 'NDAxODRjOTctMmE5ZC0wMzdjLTY2NDMtODhlODEzOTQ3YTJh',
code: 'Lunch',
name: 'Lunch',
category: '',
blockName: '',
dayOfWeek: 2,
location: 'Fjärr',
teacher: '',
timeEnd: '12:05:00',
timeStart: '11:40:00',
dateStart: '2021-04-13T11:40:00.000+02:00',
dateEnd: '2021-04-13T12:05:00.000+02:00',
},
{
id: 'ZTc4YTcyZTUtMDc0NS0wNDE0LTVjODctYjY0MzQ2MGM3MDll',
code: 'MA',
name: 'Matematik',
category: '',
blockName: '',
dayOfWeek: 2,
location: 'Fjärr',
teacher: 'CBr',
timeEnd: '11:20:00',
timeStart: '10:00:00',
dateStart: '2021-04-13T10:00:00.000+02:00',
dateEnd: '2021-04-13T11:20:00.000+02:00',
},
{
id: 'MjRkMWE4YTItYTk5ZC0wYTFmLTVhMDgtMThiMmNhZDc1ZDUz',
code: 'MU',
name: 'Musik',
category: '',
blockName: '',
dayOfWeek: 2,
location: 'Fjärr',
teacher: 'KBj',
timeEnd: '14:15:00',
timeStart: '13:30:00',
dateStart: '2021-04-13T13:30:00.000+02:00',
dateEnd: '2021-04-13T14:15:00.000+02:00',
},
{
id: 'NTU4ZTc4ZTctNDQyMy0wMjVkLTRiYzktZGUwYmFmYzk2YTlj',
code: 'EN',
name: 'Engelska',
category: '',
blockName: '',
dayOfWeek: 3,
location: '303',
teacher: 'TPe',
timeEnd: '09:55:00',
timeStart: '09:10:00',
dateStart: '2021-04-14T09:10:00.000+02:00',
dateEnd: '2021-04-14T09:55:00.000+02:00',
},
{
id: 'NDUyNjIxODItYzFiOC0wOTFjLTYwODYtZDllZjZjN2QyYzA3',
code: 'SV',
name: 'Svenska',
category: '',
comment: 'a)',
blockName: '',
dayOfWeek: 3,
location: '303',
teacher: 'JCa',
timeEnd: '14:45:00',
timeStart: '14:00:00',
dateStart: '2021-04-14T14:00:00.000+02:00',
dateEnd: '2021-04-14T14:45:00.000+02:00',
},
{
id: 'NDdkMGI0ZjItMjkxMC0wYWI1LTQ0YWMtNDY3NTdkZTE2Njg3',
code: 'SO',
name: 'Engelska',
category: 'Samhällsorienterande ämnen',
blockName: '',
dayOfWeek: 3,
location: '303',
teacher: 'HRr',
timeEnd: '11:00:00',
timeStart: '10:05:00',
dateStart: '2021-04-14T10:05:00.000+02:00',
dateEnd: '2021-04-14T11:00:00.000+02:00',
},
{
id: 'ZTI2ZDgyNWUtM2ZlOS0wZDVmLTY5NTctNGYzZThjMTMxOTdh',
code: 'NO',
name: 'Naturorienterande ämnen',
category: '',
comment: 'a)',
blockName: '',
dayOfWeek: 3,
location: '307',
teacher: 'TBo',
timeEnd: '13:50:00',
timeStart: '12:50:00',
dateStart: '2021-04-14T12:50:00.000+02:00',
dateEnd: '2021-04-14T13:50:00.000+02:00',
},
{
id: 'NzMxNjczNGMtMmZmZi0wM2YzLTU0ZjMtODdjOTAwYzIwNTUw',
code: 'Lunch',
name: 'Lunch',
category: '',
blockName: '',
dayOfWeek: 3,
location: 'Ö5',
teacher: '',
timeEnd: '12:40:00',
timeStart: '12:15:00',
dateStart: '2021-04-14T12:15:00.000+02:00',
dateEnd: '2021-04-14T12:40:00.000+02:00',
},
{
id: 'MWRkZjhlZTktNTBmMC0wZjNhLTQ1OTgtMWJkOWM3MjI2NWQ4',
code: 'SV',
name: 'Svenska',
category: '',
blockName: '',
dayOfWeek: 3,
location: '303',
teacher: 'JCa',
timeEnd: '12:05:00',
timeStart: '11:20:00',
dateStart: '2021-04-14T11:20:00.000+02:00',
dateEnd: '2021-04-14T12:05:00.000+02:00',
},
{
id: 'NzM2Mjc2ZTYtY2JlYy0wOTc1LTU1ZGYtNjMwZjhjZWVjNjgy',
code: 'MA',
name: 'Matematik',
category: '',
comment: 'a)',
blockName: '',
dayOfWeek: 3,
location: '307',
teacher: 'CBr',
timeEnd: '15:45:00',
timeStart: '15:00:00',
dateStart: '2021-04-14T15:00:00.000+02:00',
dateEnd: '2021-04-14T15:45:00.000+02:00',
},
{
id: 'YWNlZmEzZjYtM2EwNC0wYWY3LTU1N2MtMDBlMTA4MDQzMzRl',
code: 'MU',
name: 'Musik',
category: '',
blockName: '',
dayOfWeek: 3,
location: '504',
teacher: 'KBj',
timeEnd: '09:00:00',
timeStart: '08:15:00',
dateStart: '2021-04-14T08:15:00.000+02:00',
dateEnd: '2021-04-14T09:00:00.000+02:00',
},
{
id: 'NDc4MThmMDYtYmYxYi0wZDBkLTdhNmItZGVjMjY3OWY3MmYz',
code: 'IDH',
name: 'Idrott & Hälsa',
category: '',
blockName: '',
dayOfWeek: 4,
location: 'Fjärr',
teacher: 'AKö,CSv,HAl',
timeEnd: '15:45:00',
timeStart: '14:35:00',
dateStart: '2021-04-15T14:35:00.000+02:00',
dateEnd: '2021-04-15T15:45:00.000+02:00',
},
{
id: 'ZjQyZjNkOWItYWMzZi0wYWRhLTQ3YzItNTZiNTJkOTRmY2Iy',
code: 'M2FR',
name: 'Franska',
category: 'Moderna språk, språkval',
blockName: '',
dayOfWeek: 4,
location: 'Fjärr',
teacher: 'DNi',
timeEnd: '11:55:00',
timeStart: '11:10:00',
dateStart: '2021-04-15T11:10:00.000+02:00',
dateEnd: '2021-04-15T11:55:00.000+02:00',
},
{
id: 'YzQ2NWZlOWMtYzM3ZC0wYzBlLTQzNTQtODMyYmU3ODcxMDQ3',
code: 'MTID',
name: 'Mentorstid',
category: 'Diverse',
comment: 'Arbetslagsråd 6C',
blockName: '',
dayOfWeek: 4,
location: 'Fjärr',
teacher: 'JCa,CBr',
timeEnd: '10:00:00',
timeStart: '09:15:00',
dateStart: '2021-04-15T09:15:00.000+02:00',
dateEnd: '2021-04-15T10:00:00.000+02:00',
},
{
id: 'YzMwMGY0YzAtNjhjNi0wYzY0LTU1MjctODg2MWQ4ZTRmZTI2',
code: 'MU',
name: 'Musik',
category: '',
blockName: '',
dayOfWeek: 4,
location: 'Fjärr',
teacher: 'KBj',
timeEnd: '10:55:00',
timeStart: '10:10:00',
dateStart: '2021-04-15T10:10:00.000+02:00',
dateEnd: '2021-04-15T10:55:00.000+02:00',
},
{
id: 'ZDNlNTFhMGUtYWFlYy0wOGI0LTVlMGItOTc0MzFiZmIwODcx',
code: 'Lunch',
name: 'Lunch',
category: 'Diverse',
blockName: '',
dayOfWeek: 4,
location: 'Fjärr',
teacher: '',
timeEnd: '12:25:00',
timeStart: '12:00:00',
dateStart: '2021-04-15T12:00:00.000+02:00',
dateEnd: '2021-04-15T12:25:00.000+02:00',
},
{
id: 'MDRiZWMyODMtNjEwZC0wZDYwLTRlOWItYTY1MjAwZTc0YTZm',
code: 'SO',
name: 'Samhällsorienterande ämnen',
category: '',
blockName: '',
dayOfWeek: 4,
location: 'Fjärr',
teacher: 'HRr',
timeEnd: '13:10:00',
timeStart: '12:35:00',
dateStart: '2021-04-15T12:35:00.000+02:00',
dateEnd: '2021-04-15T13:10:00.000+02:00',
},
{
id: 'YTA0ZTA2NTktYTU5MS0wMTFmLTVlYWYtNWM1MTgxNDJlMDcy',
code: 'EN',
name: 'Engelska',
category: '',
comment: 'a)',
blockName: '',
dayOfWeek: 4,
location: 'Fjärr',
teacher: 'TPe',
timeEnd: '14:20:00',
timeStart: '13:35:00',
dateStart: '2021-04-15T13:35:00.000+02:00',
dateEnd: '2021-04-15T14:20:00.000+02:00',
},
{
id: 'OGJhN2MxYTYtMDQ4NS0wNWNhLTUwZWEtZDQ5YzQyMzFhYzc5',
code: 'Lunch',
name: 'Lunch',
category: 'Diverse',
blockName: '',
dayOfWeek: 5,
location: 'Ö5',
teacher: '',
timeEnd: '12:05:00',
timeStart: '11:40:00',
dateStart: '2021-04-16T11:40:00.000+02:00',
dateEnd: '2021-04-16T12:05:00.000+02:00',
},
{
id: 'ZmUwMGEwM2QtNTExMy0wODliLTY1ZGEtODM0YmRjNjc1NDIw',
code: 'MA',
name: 'Matematik',
category: '',
comment: 'a)',
blockName: '',
dayOfWeek: 5,
location: '303',
teacher: 'CBr',
timeEnd: '14:00:00',
timeStart: '13:15:00',
dateStart: '2021-04-16T13:15:00.000+02:00',
dateEnd: '2021-04-16T14:00:00.000+02:00',
},
{
id: 'Y2IwYjYzZDEtODAxYi0wMTNjLTRjNDMtMDFlODgzMmY4MWEy',
code: 'MU',
name: 'Musik',
category: '',
comment: 'a)',
blockName: '',
dayOfWeek: 5,
location: '510',
teacher: 'KBj',
timeEnd: '13:05:00',
timeStart: '12:20:00',
dateStart: '2021-04-16T12:20:00.000+02:00',
dateEnd: '2021-04-16T13:05:00.000+02:00',
},
{
id: 'N2JkMGFiOTYtMjI5OC0wMjZiLTc3OGEtN2JkN2Q4MDZkNTEy',
code: 'SL',
name: 'Slöjd',
category: '',
comment: 'tmtx)',
blockName: '',
dayOfWeek: 5,
location: '860',
teacher: 'EAl',
timeEnd: '15:10:00',
timeStart: '14:10:00',
dateStart: '2021-04-16T14:10:00.000+02:00',
dateEnd: '2021-04-16T15:10:00.000+02:00',
},
{
id: 'NzkxMjE3MDctMWExNS0wN2RmLTQwMzQtNTEyZTczZjQyZTUw',
code: 'SV',
name: 'Svenska',
category: '',
blockName: '',
dayOfWeek: 5,
location: '303',
teacher: 'JCa',
timeEnd: '10:35:00',
timeStart: '09:20:00',
dateStart: '2021-04-16T09:20:00.000+02:00',
dateEnd: '2021-04-16T10:35:00.000+02:00',
},
{
id: 'ZTU1ZDQxNzQtN2Q3Yy0wMDMxLTY2ZmYtZmIyNGM5MjM3ZTRj',
code: 'MA',
name: 'Matematik',
category: '',
blockName: '',
dayOfWeek: 5,
location: '303',
teacher: 'CBr',
timeEnd: '11:35:00',
timeStart: '10:40:00',
dateStart: '2021-04-16T10:40:00.000+02:00',
dateEnd: '2021-04-16T11:35:00.000+02:00',
}
]
}

View File

@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
import { loginStatus } from './routes'; import { loginStatus } from './routes';
import { AuthTicket, Fetcher, LoginStatusChecker } from '@skolplattformen/api'; import { AuthTicket, Fetcher, LoginStatusChecker } from '@skolplattformen/api';
export class Checker extends EventEmitter { export class Checker extends EventEmitter implements LoginStatusChecker {
public token: string; public token: string;
private fetcher: Fetcher; private fetcher: Fetcher;
@ -41,3 +41,10 @@ export const checkStatus = (
fetch: Fetcher, fetch: Fetcher,
ticket: AuthTicket ticket: AuthTicket
): LoginStatusChecker => new Checker(fetch, ticket) ): LoginStatusChecker => new Checker(fetch, ticket)
export class DummyStatusChecker extends EventEmitter implements LoginStatusChecker {
token = ""
async cancel(): Promise<void> {
// do nothing
}
}

View File

@ -0,0 +1,50 @@
import { EtjanstResponse } from '../'
import { schoolContacts } from '../schoolContacts'
let response: EtjanstResponse
beforeEach(() => {
response = {
"Success": true,
"Error": null,
"Data": [
{
"Title": "Expedition",
"Name": null,
"Phone": "508 000 00",
"Email": "",
"SchoolName": "Påhittade skolan",
"ClassName": null
},
{
"Title": "Rektor",
"Name": "Andersson, Anna Bella Cecilia",
"Phone": "08-508 000 00",
"Email": "anna.anderssonn@edu.stockholm.se",
"SchoolName": null,
"ClassName": null
}
]
}
})
it('parses teachers correctly', () => {
expect(schoolContacts(response)).toEqual([
{
title: 'Expedition',
name: null,
phone: '508 000 00',
email: '',
schoolName: 'Påhittade skolan',
className: null
},
{
title: 'Rektor',
name: 'Andersson, Anna Bella Cecilia',
phone: '08-508 000 00',
email: 'anna.anderssonn@edu.stockholm.se',
schoolName: null,
className: null
}
])
})

View File

@ -0,0 +1,68 @@
import { EtjanstResponse } from '../'
import { teachers } from '../teachers'
let response: EtjanstResponse
beforeEach(() => {
response = {
"Success": true,
"Error": null,
"Data": [
{
"ID": 156735,
"BATCH": "GR",
"SIS_ID": "F154239A-EA4A-4C6C-A112-0B9581132E3D",
"USERNAME": "anna.andersson",
"SCHOOL_SIS_ID": "DE2E1293-0F40-4B91-9D91-1E99355DC257",
"EMAILADDRESS": null,
"STATUS": " GR",
"ERRORCODE": 0,
"FIRSTNAME": "Anna",
"LASTNAME": "Andersson",
"ACTIVE": true,
"TELWORK": "08 508 0000000"
},
{
"ID": 156690,
"BATCH": "GR",
"SIS_ID": "9EC59FCA-80AD-4774-AABD-427040207E33",
"USERNAME": "gunnar.grymm",
"SCHOOL_SIS_ID": "DE2E1293-0F40-4B91-9D91-1E99355DC257",
"EMAILADDRESS": "gunnar.grymm@edu.stockholm.se",
"STATUS": " F",
"ERRORCODE": 0,
"FIRSTNAME": "Gunnar",
"LASTNAME": "Grymm",
"ACTIVE": true,
"TELWORK": null
}
]
}
})
it('parses teachers correctly', () => {
expect(teachers(response)).toEqual([
{
id: 156735,
sisId: 'F154239A-EA4A-4C6C-A112-0B9581132E3D',
firstname: 'Anna',
lastname: 'Andersson',
email: null,
phoneWork: '08 508 0000000',
active: true,
status: ' GR',
timeTableAbbreviation: 'AAN'
},
{
id: 156690,
sisId: '9EC59FCA-80AD-4774-AABD-427040207E33',
firstname: 'Gunnar',
lastname: 'Grymm',
email: 'gunnar.grymm@edu.stockholm.se',
phoneWork: null,
active: true,
status: ' F',
timeTableAbbreviation: 'GGR'
},
])
})

View File

@ -6,5 +6,7 @@ export * from './menu'
export * from './news' export * from './news'
export * from './notifications' export * from './notifications'
export * from './schedule' export * from './schedule'
export * from './schoolContacts'
export * from './teachers'
export * from './timetable' export * from './timetable'
export * from './user' export * from './user'

View File

@ -0,0 +1,22 @@
import { etjanst } from './etjanst'
import { SchoolContact } from '@skolplattformen/api'
export const schoolContact = ({
title,
name,
phone,
email,
schoolName,
className,
}: any): SchoolContact => ({
title,
name,
phone,
email,
schoolName,
className,
})
export const schoolContacts = (data: any): SchoolContact[] =>
etjanst(data).map(schoolContact)

View File

@ -0,0 +1,29 @@
import { etjanst } from './etjanst'
import { Teacher } from '@skolplattformen/api'
const abbreviate = (firstname?: string, lastname?: string): string =>
`${firstname?.substr(0,1)}${lastname?.substr(0,2)}`.toUpperCase()
export const teacher = ({
id,
sisId,
firstname,
lastname,
emailaddress,
telwork,
active,
status,
}: any): Teacher => ({
id,
sisId,
firstname,
lastname,
email: emailaddress,
phoneWork: telwork,
active,
status,
timeTableAbbreviation: abbreviate(firstname, lastname)
})
export const teachers = (data: any): Teacher[] =>
etjanst(data).map(teacher)

View File

@ -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)
) )

View File

@ -20,6 +20,12 @@ export const calendar = (childId: string) =>
export const classmates = (childId: string) => export const classmates = (childId: string) =>
`${urlLoggedIn}/contacts/GetStudentsByClass?studentId=${childId}` `${urlLoggedIn}/contacts/GetStudentsByClass?studentId=${childId}`
export const teachers = (childId: string, schoolForm: string) =>
`${urlLoggedIn}/contacts/GetTeachersByStudent?studentId=${childId}&schoolForm=${schoolForm}`
export const schoolContacts = (childId: string, schoolId: string) =>
`${urlLoggedIn}/contacts/GetSchoolContacts?schoolId=${schoolId}&studentId=${childId}&schoolForm=Klasslista`
export const user = export const user =
'https://etjanst.stockholm.se/vardnadshavare/base/getuserdata' 'https://etjanst.stockholm.se/vardnadshavare/base/getuserdata'

View File

@ -13,6 +13,8 @@ import {
EtjanstChild, EtjanstChild,
TimetableEntry, TimetableEntry,
ScheduleItem, ScheduleItem,
SchoolContact,
Teacher
} from './types' } from './types'
export interface Api extends EventEmitter { export interface Api extends EventEmitter {
@ -21,6 +23,7 @@ export interface Api extends EventEmitter {
getPersonalNumber(): string | undefined getPersonalNumber(): string | undefined
login(personalNumber?: string): Promise<LoginStatusChecker> login(personalNumber?: string): Promise<LoginStatusChecker>
setSessionCookie(sessionCookie: string): Promise<void> setSessionCookie(sessionCookie: string): Promise<void>
getSessionHeaders(url: string): Promise<{ [index: string]: string }>
getUser(): Promise<User> getUser(): Promise<User>
getChildren(): Promise<EtjanstChild[]> getChildren(): Promise<EtjanstChild[]>
getCalendar(child: EtjanstChild): Promise<CalendarItem[]> getCalendar(child: EtjanstChild): Promise<CalendarItem[]>
@ -29,7 +32,9 @@ export interface Api extends EventEmitter {
getNewsDetails(child: EtjanstChild, item: NewsItem): Promise<any> getNewsDetails(child: EtjanstChild, item: NewsItem): Promise<any>
getMenu(child: EtjanstChild): Promise<MenuItem[]> getMenu(child: EtjanstChild): Promise<MenuItem[]>
getNotifications(child: EtjanstChild): Promise<Notification[]> getNotifications(child: EtjanstChild): Promise<Notification[]>
getTeachers(child: EtjanstChild): Promise<Teacher[]>
getSchedule(child: EtjanstChild, from: DateTime, to: DateTime): Promise<ScheduleItem[]> getSchedule(child: EtjanstChild, from: DateTime, to: DateTime): Promise<ScheduleItem[]>
getSchoolContacts(child: EtjanstChild): Promise<SchoolContact[]>
getSkola24Children(): Promise<Skola24Child[]> getSkola24Children(): Promise<Skola24Child[]>
getTimetable(child: Skola24Child, week: number, year: number, lang: Language): Promise<TimetableEntry[]> getTimetable(child: Skola24Child, week: number, year: number, lang: Language): Promise<TimetableEntry[]>
registerAbscense(child: EtjanstChild, startDate: DateTime, endDate: DateTime): Promise<void> registerAbscense(child: EtjanstChild, startDate: DateTime, endDate: DateTime): Promise<void>

View File

@ -214,3 +214,24 @@ export interface TimetableEntry extends Subject {
dateStart: string dateStart: string
dateEnd: string dateEnd: string
} }
export interface Teacher {
id: number
sisId: string
firstname: string
lastname: string
email?: string
phoneWork?: string
active: boolean
status: string
timeTableAbbreviation: string
}
export interface SchoolContact {
title?: string
name?: string
phone?: string
email?: string
schoolName: string
className: string
}

View File

@ -9,6 +9,7 @@ test.each([
['15 oktober 2020 11:34', '2020-10-15T09:34:00.000Z'], ['15 oktober 2020 11:34', '2020-10-15T09:34:00.000Z'],
['2020-12-18T15:59:46.34', '2020-12-18T14:59:46.340Z'], ['2020-12-18T15:59:46.34', '2020-12-18T14:59:46.340Z'],
['2020-12-18T15:59:46.340Z', '2020-12-18T15:59:46.340Z'], ['2020-12-18T15:59:46.340Z', '2020-12-18T15:59:46.340Z'],
['/Date(1637935089877)/', '2021-11-26T13:58:09.877Z'],
['This is an invalid date', undefined], ['This is an invalid date', undefined],
])('handles date parsing of %s', (input, expected) => { ])('handles date parsing of %s', (input, expected) => {
expect(parseDate(input)).toEqual(expected) expect(parseDate(input)).toEqual(expected)

View File

@ -18,7 +18,7 @@ In order to use api hooks, you must wrap your app in an ApiProvider
```javascript ```javascript
import React from 'react' import React from 'react'
import { ApiProvider } from '@skolplattformen/hooks' import { ApiProvider } from '@skolplattformen/hooks'
import init from '@skolplattformen/api-skolplattformen' import init from '@skolplattformen/api-skolplattformet'
import { CookieManager } from '@react-native-cookies/cookies' import { CookieManager } from '@react-native-cookies/cookies'
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import { RootComponent } from './components/root' import { RootComponent } from './components/root'

View File

@ -22,6 +22,8 @@ const createApi = () => ({
getNewsDetails: jest.fn(), getNewsDetails: jest.fn(),
getNotifications: jest.fn(), getNotifications: jest.fn(),
getSchedule: jest.fn(), getSchedule: jest.fn(),
getSchoolContacts: jest.fn(),
getTeachers: jest.fn(),
getTimetable: jest.fn(), getTimetable: jest.fn(),
getUser: jest.fn(), getUser: jest.fn(),
}) })

View File

@ -9,7 +9,9 @@ import {
NewsItem, NewsItem,
Notification, Notification,
ScheduleItem, ScheduleItem,
SchoolContact,
Skola24Child, Skola24Child,
Teacher,
TimetableEntry, TimetableEntry,
User, User,
} from '@skolplattformen/api' } from '@skolplattformen/api'
@ -107,7 +109,7 @@ const hook = <T>(
if (newState.error) { if (newState.error) {
const description = `Error getting ${entityName} from API` const description = `Error getting ${entityName} from API`
reporter.error(newState.error, description) reporter.error && reporter.error(newState.error, description)
} }
} }
} }
@ -201,8 +203,26 @@ export const useSchedule = (child: Child, from: string, to: string) =>
api.getSchedule(child, DateTime.fromISO(from), DateTime.fromISO(to)) api.getSchedule(child, DateTime.fromISO(from), DateTime.fromISO(to))
) )
export const useSchoolContacts = (child: Child) =>
hook<SchoolContact[]>(
'SCHOOL_CONTACTS',
`schoolContacts_${child.id}`,
[],
(s) => s.schoolContacts,
(api) => () => api.getSchoolContacts(child)
)
export const useTeachers = (child: Child) =>
hook<Teacher[]>(
'TEACHERS',
`teachers_${child.id}`,
[],
(s) => s.teachers,
(api) => () => api.getTeachers(child)
)
export const useTimetable = ( export const useTimetable = (
child: Skola24Child, child: Child,
week: number, week: number,
year: number, year: number,
lang: Language lang: Language
@ -212,9 +232,31 @@ export const useTimetable = (
`timetable_${child.personGuid}_${week}_${year}_${lang}`, `timetable_${child.personGuid}_${week}_${year}_${lang}`,
[], [],
(s) => s.timetable, (s) => s.timetable,
(api) => () => api.getTimetable(child, week, year, lang) (api) => async () => {
const tt = await api.getTimetable(child, week, year, lang)
const ts = await api.getTeachers(child)
tt.forEach((element) => {
element.teacher = replaceTeacherInitials(element.teacher, ts)
})
return tt
}
) )
const replaceTeacherInitials = (
initials: string,
teachers: Teacher[]
): string => {
if (!initials || teachers?.length == 0) return initials
const arr = initials.split(',') || [initials]
const arr2 = arr.map((element) => {
const t = teachers.find(
(t) => t.timeTableAbbreviation === element.trim().toUpperCase()
)
return t ? `${t.firstname} ${t.lastname}` : element
})
return arr2.join(', ')
}
export const useUser = () => export const useUser = () =>
hook<User>( hook<User>(
'USER', 'USER',

View File

@ -6,7 +6,9 @@ import {
NewsItem, NewsItem,
Notification, Notification,
ScheduleItem, ScheduleItem,
SchoolContact,
Skola24Child, Skola24Child,
Teacher,
TimetableEntry, TimetableEntry,
User, User,
} from '@skolplattformen/api' } from '@skolplattformen/api'
@ -77,3 +79,5 @@ export const newsDetails = createReducer<NewsItem[]>('NEWS_DETAILS')
export const notifications = createReducer<Notification[]>('NOTIFICATIONS') export const notifications = createReducer<Notification[]>('NOTIFICATIONS')
export const schedule = createReducer<ScheduleItem[]>('SCHEDULE') export const schedule = createReducer<ScheduleItem[]>('SCHEDULE')
export const timetable = createReducer<TimetableEntry[]>('TIMETABLE') export const timetable = createReducer<TimetableEntry[]>('TIMETABLE')
export const teachers = createReducer<Teacher[]>('TEACHERS')
export const schoolContacts = createReducer<SchoolContact[]>('SCHOOL_CONTACTS')

View File

@ -9,7 +9,9 @@ import {
newsDetails, newsDetails,
notifications, notifications,
schedule, schedule,
schoolContacts,
skola24Children, skola24Children,
teachers,
timetable, timetable,
user, user,
} from './reducers' } from './reducers'
@ -23,7 +25,9 @@ const appReducer = combineReducers({
newsDetails, newsDetails,
notifications, notifications,
schedule, schedule,
schoolContacts,
skola24Children, skola24Children,
teachers,
timetable, timetable,
user, user,
}) })

View File

@ -7,7 +7,9 @@ import {
NewsItem, NewsItem,
Notification, Notification,
ScheduleItem, ScheduleItem,
SchoolContact,
Skola24Child, Skola24Child,
Teacher,
TimetableEntry, TimetableEntry,
User, User,
} from '@skolplattformen/api' } from '@skolplattformen/api'
@ -64,6 +66,8 @@ export type EntityName =
| 'NEWS_DETAILS' | 'NEWS_DETAILS'
| 'NOTIFICATIONS' | 'NOTIFICATIONS'
| 'SCHEDULE' | 'SCHEDULE'
| 'SCHOOL_CONTACTS'
| 'TEACHERS'
| 'TIMETABLE' | 'TIMETABLE'
| 'ALL' | 'ALL'
export interface EntityAction<T> extends Action<EntityActionType> { export interface EntityAction<T> extends Action<EntityActionType> {
@ -88,6 +92,8 @@ export interface EntityStoreRootState {
newsDetails: EntityMap<NewsItem> newsDetails: EntityMap<NewsItem>
notifications: EntityMap<Notification[]> notifications: EntityMap<Notification[]>
schedule: EntityMap<ScheduleItem[]> schedule: EntityMap<ScheduleItem[]>
schoolContacts: EntityMap<SchoolContact[]>
teachers: EntityMap<Teacher[]>
timetable: EntityMap<TimetableEntry[]> timetable: EntityMap<TimetableEntry[]>
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "skolplattformen", "name": "skolplattformen",
"version": "2.3.2", "version": "2.10.2",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"start": "nx start", "start": "nx start",