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)

View File

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

View File

@ -9,7 +9,7 @@
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
"rules": {"no-console":"warn"}
},
{
"files": ["*.ts", "*.tsx"],

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { CalendarItem } from '@skolplattformen/api-skolplattformen'
import { useCalendar } from '@skolplattformen/hooks'
import { CalendarItem } from '@skolplattformen/api'
import {
Divider,
List,
@ -10,8 +10,9 @@ import {
} from '@ui-kitten/components'
import moment from 'moment'
import React from 'react'
import { ListRenderItemInfo, View } from 'react-native'
import { Typography } from '../styles'
import { ListRenderItemInfo, RefreshControl, View } from 'react-native'
import { Layout as LayoutStyle, Sizing, Typography } from '../styles'
import { translate } from '../utils/translation'
import { useChild } from './childContext.component'
import { CalendarOutlineIcon } from './icon.component'
import { SaveToCalendar } from './saveToCalendar.component'
@ -19,7 +20,7 @@ import { Week } from './week.component'
export const Calendar = () => {
const child = useChild()
const { data } = useCalendar(child)
const { data, status, reload } = useCalendar(child)
const styles = useStyleSheet(themedStyles)
const formatStartDate = (startDate: moment.MomentInput) => {
@ -28,37 +29,55 @@ export const Calendar = () => {
'll'
)} ${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)
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 (
<View style={styles.container}>
<Week child={child} />
{data && data.length > 0 && (
<List
data={data.sort((a, b) =>
a.startDate && b.startDate
? a.startDate.localeCompare(b.startDate)
: 0
)}
ItemSeparatorComponent={Divider}
renderItem={({ item }: ListRenderItemInfo<CalendarItem>) => (
<ListItem
disabled={true}
title={`${item.title}`}
description={(props) => (
<Text style={[props?.style, styles.description]}>
{formatStartDate(item.startDate)}
</Text>
)}
accessoryLeft={CalendarOutlineIcon}
accessoryRight={() => <SaveToCalendar event={item} />}
/>
)}
/>
)}
<List
data={sortedData()}
ItemSeparatorComponent={Divider}
ListEmptyComponent={
<View style={styles.emptyState}>
<Text style={styles.emptyStateHeadline} category="h6">
{translate('calender.emptyHeadline')}
</Text>
<Text style={styles.emptyStateDescription}>
{translate('calender.emptyText')}
</Text>
</View>
}
renderItem={({ item }: ListRenderItemInfo<CalendarItem>) => (
<ListItem
disabled={true}
title={`${item.title}`}
description={(props) => (
<Text style={[props?.style, styles.description]}>
{formatStartDate(item.startDate)}
</Text>
)}
accessoryLeft={CalendarOutlineIcon}
accessoryRight={() => <SaveToCalendar event={item} />}
/>
)}
refreshControl={
<RefreshControl
refreshing={status === 'loading'}
onRefresh={reload}
/>
}
/>
</View>
)
}
@ -73,4 +92,18 @@ const themedStyles = StyleService.create({
...Typography.fontSize.xs,
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'
interface ChildProviderProps {

View File

@ -1,7 +1,7 @@
/* eslint-disable react-native-a11y/has-accessibility-hint */
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { Child } from '@skolplattformen/api-skolplattformen'
import { Child } from '@skolplattformen/api'
import {
useCalendar,
useClassmates,
@ -16,11 +16,12 @@ import {
Text,
useStyleSheet,
} from '@ui-kitten/components'
import moment from 'moment'
import React from 'react'
import moment, { Moment } from 'moment'
import React, { useEffect } from 'react'
import { TouchableOpacity, useColorScheme, View } from 'react-native'
import { useTranslation } from '../hooks/useTranslation'
import { Colors, Layout, Sizing } from '../styles'
import { getMeaningfulStartingDate } from '../utils/calendarHelpers'
import { studentName } from '../utils/peopleHelpers'
import { DaySummary } from './daySummary.component'
import { AlertIcon, RightArrowIcon } from './icon.component'
@ -30,13 +31,20 @@ import { StudentAvatar } from './studentAvatar.component'
interface ChildListItemProps {
child: Child
color: string
updated: string
currentDate?: Moment
}
type ChildListItemNavigationProp = StackNavigationProp<
RootStackParamList,
'Children'
>
export const ChildListItem = ({ child, color }: ChildListItemProps) => {
export const ChildListItem = ({
child,
color,
updated,
currentDate = moment(),
}: ChildListItemProps) => {
// Forces rerender when child.id changes
React.useEffect(() => {
// noop
@ -44,17 +52,36 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
const navigation = useNavigation<ChildListItemNavigationProp>()
const { t } = useTranslation()
const { data: notifications } = useNotifications(child)
const { data: news } = useNews(child)
const { data: classmates } = useClassmates(child)
const { data: calendar } = useCalendar(child)
const { data: menu } = useMenu(child)
const { data: schedule } = useSchedule(
const { data: notifications, reload: notificationsReload } =
useNotifications(child)
const { data: news, status: newsStatus, reload: newsReload } = useNews(child)
const { data: classmates, reload: classmatesReload } = useClassmates(child)
const { data: calendar, reload: calendarReload } = useCalendar(child)
const { data: menu, reload: menuReload } = useMenu(child)
const { data: schedule, reload: scheduleReload } = useSchedule(
child,
moment().toISOString(),
moment().add(7, 'days').toISOString()
moment(currentDate).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(
({ dateCreated, dateModified }) => {
const date = dateModified || dateCreated
@ -63,8 +90,8 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
)
const newsThisWeek = news.filter(({ modified, published }) => {
const date = modified || published
return date ? moment(date).isSame(moment(), 'week') : false
const newsDate = modified || published
return newsDate ? moment(newsDate).isSame(currentDate, 'week') : false
})
const scheduleAndCalendarThisWeek = [
@ -73,14 +100,14 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
].filter(({ startDate }) =>
startDate
? moment(startDate).isBetween(
moment().startOf('day'),
moment().add(7, 'days')
moment(currentDate).startOf('day'),
moment(currentDate).add(7, 'days')
)
: false
)
const displayDate = (date: moment.MomentInput) => {
return moment(date).fromNow()
const displayDate = (inputDate: moment.MomentInput) => {
return moment(inputDate).fromNow()
}
const getClassName = () => {
@ -118,6 +145,16 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
const className = getClassName()
const styles = useStyleSheet(themeStyles)
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 (
<TouchableOpacity
@ -142,12 +179,15 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
/>
</View>
</View>
<DaySummary child={child} />
<DaySummary child={child} date={meaningfulStartingDate} />
{scheduleAndCalendarThisWeek.slice(0, 3).map((calendarItem, i) => (
<Text category="p1" key={i}>
{`${calendarItem.title} (${displayDate(calendarItem.startDate)})`}
</Text>
))}
<Text category="c2" style={styles.label}>
{t('navigation.news')}
</Text>
@ -156,11 +196,13 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
{notification.message}
</Text>
))}
{newsThisWeek.slice(0, 3).map((newsItem, i) => (
<Text category="p1" key={i}>
{newsItem.header ?? ''}
</Text>
))}
{scheduleAndCalendarThisWeek.length ||
notificationsThisWeek.length ||
newsThisWeek.length ? null : (
@ -168,16 +210,20 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
{t('news.noNewNewsItemsThisWeek')}
</Text>
)}
{!menu[moment().isoWeekday() - 1] ? null : (
{shouldShowLunchMenu ? (
<>
<Text category="c2" style={styles.label}>
{t('schedule.lunch')}
{meaningfulStartingDate.format(
'[' + t('schedule.lunch') + '] dddd'
)}
</Text>
<Text>
{menu[meaningfulStartingDate.isoWeekday() - 1]?.description}
</Text>
<Text>{menu[moment().isoWeekday() - 1]?.description}</Text>
</>
)}
<View style={styles.itemFooterAbsence}>
) : null}
<View style={styles.itemFooter}>
<Button
accessible
accessibilityRole="button"
@ -232,15 +278,16 @@ const themeStyles = StyleService.create({
},
itemFooter: {
...Layout.flex.row,
marginTop: Sizing.t4,
},
itemFooterAbsence: {
...Layout.mainAxis.flexStart,
justifyContent: 'space-between',
alignItems: 'flex-end',
marginTop: Sizing.t4,
},
absenceButton: {
marginLeft: -20,
},
itemFooterSpinner: {
alignSelf: 'flex-end',
},
item: {
marginRight: 12,
paddingHorizontal: 2,

View File

@ -1,5 +1,5 @@
import { useNavigation } from '@react-navigation/core'
import { Child } from '@skolplattformen/api-skolplattformen'
import { Child } from '@skolplattformen/api'
import { useApi, useChildList } from '@skolplattformen/hooks'
import {
Button,
@ -10,7 +10,8 @@ import {
TopNavigationAction,
useStyleSheet,
} from '@ui-kitten/components'
import React, { useCallback, useEffect } from 'react'
import moment from 'moment'
import React, { useCallback, useEffect, useState } from 'react'
import {
Image,
ImageStyle,
@ -24,7 +25,7 @@ import AppStorage from '../services/appStorage'
import { Colors, Layout as LayoutStyle, Sizing, Typography } from '../styles'
import { translate } from '../utils/translation'
import { ChildListItem } from './childListItem.component'
import { SettingsIcon } from './icon.component'
import { SettingsIcon, RefreshIcon } from './icon.component'
const colors = ['primary', 'success', 'info', 'warning', 'danger']
@ -45,9 +46,12 @@ export const Children = () => {
const { api } = useApi()
const { data: childList, status, reload } = useChildList()
const reloadChildren = () => {
const reloadChildren = useCallback(() => {
reload()
}
setUpdated(moment().toISOString())
}, [reload])
const [updatedAt, setUpdated] = useState('')
const logout = useCallback(() => {
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
// when this view is actually lightgrey. Taking the padding top value from the use inset hook.
return (
<>
{status === 'loaded' ? (
<List
contentContainerStyle={styles.childListContainer}
data={childList}
style={styles.childList}
ListEmptyComponent={
<View style={styles.emptyState}>
<Text category="h2">{translate('children.noKids_title')}</Text>
<Text style={styles.emptyStateDescription}>
{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}>
return status === 'loaded' ? (
<List
contentContainerStyle={styles.childListContainer}
data={childList}
style={styles.childList}
ListEmptyComponent={
<View style={styles.emptyState}>
<Text category="h2">{translate('children.noKids_title')}</Text>
<Text style={styles.emptyStateDescription}>
{translate('children.noKids_description')}
</Text>
<Image
accessibilityIgnoresInvertColors={false}
source={require('../assets/girls.png')}
style={styles.loadingImage as ImageStyle}
source={require('../assets/children.png')}
style={styles.emptyStateImage as ImageStyle}
/>
{status === 'error' ? (
<View style={styles.errorMessage}>
<Text category="h5">
{translate('children.loadingErrorHeading')}
</Text>
<Text style={{ fontSize: Sizing.t4 }}>
{translate('children.loadingErrorInformationText')}
</Text>
<View style={styles.errorButtons}>
<Button status="success" onPress={() => reloadChildren()}>
{translate('children.tryAgain')}
</Button>
<Button
status="basic"
onPress={() =>
Linking.openURL('https://skolplattformen.org/status')
}
>
{translate('children.viewStatus')}
</Button>
<Button onPress={() => logout()}>
{translate('general.logout')}
</Button>
</View>
</View>
) : (
<View style={styles.loadingMessage}>
<Spinner size="large" status="primary" />
<Text category="h1" style={styles.loadingText}>
{translate('general.loading')}
</Text>
</View>
)}
</View>
}
renderItem={({ item: child, index }: ListRenderItemInfo<Child>) => (
<ChildListItem
child={child}
color={colors[index % colors.length]}
updated={updatedAt}
key={child.id}
/>
)}
/>
) : (
<View style={styles.loading}>
<Image
accessibilityIgnoresInvertColors={false}
source={require('../assets/girls.png')}
style={styles.loadingImage as ImageStyle}
/>
{status === 'error' ? (
<View style={styles.errorMessage}>
<Text category="h5">{translate('children.loadingErrorHeading')}</Text>
<Text style={{ fontSize: Sizing.t4 }}>
{translate('children.loadingErrorInformationText')}
</Text>
<View style={styles.errorButtons}>
<Button status="success" onPress={() => reloadChildren()}>
{translate('children.tryAgain')}
</Button>
<Button
status="basic"
onPress={() =>
Linking.openURL('https://skolplattformen.org/status')
}
>
{translate('children.viewStatus')}
</Button>
<Button onPress={() => logout()}>
{translate('general.logout')}
</Button>
</View>
</View>
) : (
<View style={styles.loadingMessage}>
<Spinner size="large" status="primary" />
<Text category="h1" style={styles.loadingText}>
{translate('general.loading')}
</Text>
</View>
)}
</>
</View>
)
}
@ -187,7 +196,6 @@ const themedStyles = StyleService.create({
emptyState: {
...LayoutStyle.center,
...LayoutStyle.flex.full,
backgroundColor: Colors.neutral.white,
paddingHorizontal: Sizing.t5,
},
emptyStateDescription: {

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { Child } from '@skolplattformen/api-skolplattformen'
import { Child } from '@skolplattformen/api'
import { useTimetable } from '@skolplattformen/hooks'
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
import moment, { Moment } from 'moment'
import React from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { View } from 'react-native'
import { LanguageService } from '../services/languageService'
import { translate } from '../utils/translation'
@ -12,9 +12,17 @@ interface DaySummaryProps {
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 [year, week] = [moment().isoWeekYear(), moment().isoWeek()]
const [week, year] = [currentDate.isoWeek(), currentDate.isoWeekYear()]
const { data: weekLessons } = useTimetable(
child,
week,
@ -23,8 +31,8 @@ export const DaySummary = ({ child, date = moment() }: DaySummaryProps) => {
)
const lessons = weekLessons
.filter((lesson) => lesson.dayOfWeek === date.isoWeekday())
.sort((a, b) => a.dateStart.localeCompare(b.dateStart))
.filter((lesson) => lesson.dayOfWeek === currentDate.isoWeekday())
.sort((a, b) => a.timeStart.localeCompare(b.timeStart))
if (lessons.length <= 0) {
return null
@ -34,6 +42,11 @@ export const DaySummary = ({ child, date = moment() }: DaySummaryProps) => {
return (
<View>
{moment().weekday() !== currentDate.weekday() ? (
<Text category="c2" style={styles.weekday}>
{capitalizeFirstLetter(currentDate.format('dddd'))}
</Text>
) : null}
<View style={styles.summary}>
<View style={styles.part}>
<View>
@ -49,19 +62,27 @@ export const DaySummary = ({ child, date = moment() }: DaySummaryProps) => {
{translate('schedule.end')}
</Text>
<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>
</View>
</View>
</View>
<Text category="s2">
{gymBag
? ` 🤼‍♀️ ${translate('schedule.gymBag', {
defaultValue: 'Gympapåse',
})}`
: ''}
</Text>
</View>
)
}
@ -76,4 +97,11 @@ const themedStyles = StyleService.create({
label: {
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 QuestionMarkIcon = uiIcon('question-mark')
export const AwardIcon = uiIcon('award')
export const RefreshIcon = uiIcon('refresh')

View File

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

View File

@ -34,6 +34,7 @@ import {
PersonIcon,
SelectIcon,
} from './icon.component'
import AppStorage from '../services/appStorage'
const BankId = () => (
<Image
@ -88,6 +89,8 @@ export const Login = () => {
] as const
const loginHandler = async () => {
const user = await api.getUser()
await AppStorage.clearPersonalData(user)
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 {
Divider,
@ -9,7 +9,13 @@ import {
} from '@ui-kitten/components'
import 'moment/locale/sv'
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 { translate } from '../utils/translation'
import { useChild } from './childContext.component'
@ -18,7 +24,7 @@ import { MenuListItem } from './menuListItem.component'
export const Menu = () => {
const styles = useStyleSheet(themedStyles)
const child = useChild()
const { data } = useMenu(child)
const { data, status, reload } = useMenu(child)
return (
<List
@ -42,6 +48,9 @@ export const Menu = () => {
<MenuListItem key={item.title} item={item} />
)}
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 React from 'react'
import { View } from 'react-native'

View File

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

View File

@ -2,7 +2,7 @@ import { NavigationContainer } from '@react-navigation/native'
import {
Child as ChildType,
NewsItem as NewsItemType,
} from '@skolplattformen/api-skolplattformen'
} from '@skolplattformen/api'
import { useApi } from '@skolplattformen/hooks'
import { useTheme } from '@ui-kitten/components'
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/locale/sv'
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 { defaultStackStyling } from '../design/navigationThemes'
import { Layout, Sizing, Typography } from '../styles'
@ -46,7 +52,7 @@ export const newsItemRouteOptions =
export const NewsItem = ({ route }: NewsItemProps) => {
const { newsItem, child } = route.params
const { data } = useNewsDetails(child, newsItem)
const { data, status, reload } = useNewsDetails(child, newsItem)
const styles = useStyleSheet(themedStyles)
const stylesMarkdown = useStyleSheet(themedStylesMarkdown)
@ -54,6 +60,9 @@ export const NewsItem = ({ route }: NewsItemProps) => {
<ScrollView
contentContainerStyle={styles.article}
style={styles.scrollView}
refreshControl={
<RefreshControl refreshing={status === 'loading'} onRefresh={reload} />
}
>
<Text maxFontSizeMultiplier={2} style={styles.title}>
{newsItem.header}

View File

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

View File

@ -1,6 +1,7 @@
import { useNotifications } from '@skolplattformen/hooks'
import { List, StyleService, useStyleSheet } from '@ui-kitten/components'
import React from 'react'
import { RefreshControl } from 'react-native'
import { Sizing } from '../styles'
import { useChild } from './childContext.component'
import { Notification } from './notification.component'
@ -8,7 +9,7 @@ import { Notification } from './notification.component'
export const NotificationsList = () => {
const styles = useStyleSheet(themedStyles)
const child = useChild()
const { data } = useNotifications(child)
const { data, status, reload } = useNotifications(child)
return (
<List
@ -18,6 +19,9 @@ export const NotificationsList = () => {
renderItem={(info) => (
<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 React from 'react'
import RNCalendarEvents from 'react-native-calendar-events'

View File

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

View File

@ -1,8 +1,4 @@
import {
Child,
MenuItem,
TimetableEntry,
} from '@skolplattformen/api-skolplattformen'
import { Child, MenuItem, TimetableEntry } from '@skolplattformen/api'
import { useMenu, useTimetable } from '@skolplattformen/hooks'
import {
List,
@ -20,6 +16,8 @@ import { View } from 'react-native'
import { LanguageService } from '../services/languageService'
import { Sizing, Typography } from '../styles'
import { TransitionView } from './transitionView.component'
import { getMeaningfulStartingDate } from '../utils/calendarHelpers'
import { translate } from '../utils/translation'
interface WeekProps {
child: Child
@ -111,10 +109,12 @@ export const Day = ({ weekDay, lunch, lessons }: DayProps) => {
export const Week = ({ child }: WeekProps) => {
moment.locale(LanguageService.getLocale())
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 [showSchema, setShowSchema] = useState(false)
const [year, week] = [moment().isoWeekYear(), moment().isoWeek()]
const [year, week] = [displayDate.isoWeekYear(), displayDate.isoWeek()]
const { data: lessons } = useTimetable(
child,
week,
@ -130,15 +130,30 @@ export const Week = ({ child }: WeekProps) => {
setShowSchema(shouldShowSchema)
}, [lessons])
const getWeekText = (date = moment()) => {
return `${translate('schedule.week')} ${date.isoWeek()}`
}
return showSchema ? (
<TransitionView style={styles.view} animation={'fadeInDown'}>
<TransitionView style={styles.innerView} animation={'fadeIn'}>
<Text style={styles.weekNumber}>{getWeekText(displayDate)}</Text>
<TabBar
selectedIndex={selectedIndex}
onSelect={(index) => setSelectedIndex(index)}
>
{days.map((weekDay) => (
<Tab key={weekDay} title={weekDay} />
{days.map((weekDay, index) => (
<Tab
key={weekDay}
title={(_) => (
<>
<Text style={styles.tabTitle}>{weekDay}</Text>
<Text style={styles.tabTitleDate}>
{displayDate.startOf('week').add(index, 'day').format('D')}
</Text>
</>
)}
/>
))}
</TabBar>
@ -154,7 +169,7 @@ export const Week = ({ child }: WeekProps) => {
lunch={menu[index] || {}}
lessons={lessons
.filter((lesson) => days[lesson.dayOfWeek - 1] === weekDay)
.sort((a, b) => a.dateStart.localeCompare(b.dateStart))}
.sort((a, b) => a.timeStart.localeCompare(b.timeStart))}
/>
))}
</ViewPager>
@ -166,12 +181,12 @@ export const Week = ({ child }: WeekProps) => {
const themedStyles = StyleService.create({
view: {
backgroundColor: 'background-basic-color-1',
maxHeight: '60%',
maxHeight: '65%',
paddingBottom: 0,
margin: 0,
},
innerView: {
paddingBottom: 60,
paddingBottom: 170,
margin: 0,
},
part: {
@ -233,4 +248,15 @@ const themedStyles = StyleService.create({
lesson: {
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'
export const FeatureFlagsContext = React.createContext<Features>({
LOGIN_BANK_ID_SAME_DEVICE: false,
LOGIN_BANK_ID_SAME_DEVICE_WITHOUT_ID: true,
FOOD_MENU: false,
CLASS_LIST: true,
})
interface Props {

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 usePersonalStorage from '../usePersonalStorage'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,9 @@
"saveToCalender": "Save in calendar",
"saveToCalenderError": "Something went wrong",
"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": {
"loadingErrorHeading": "Oops!",
@ -80,7 +82,8 @@
"send": "Send",
"settings": "Settings",
"socialSecurityNumber": "Personal identity number",
"title": "Öppna skolplattformen"
"title": "Öppna skolplattformen",
"tomorrow": "Tomorrow"
},
"language": {
"changeLanguage": "Change language",
@ -128,7 +131,8 @@
"start": "Start",
"end": "End",
"lunch": "Lunch",
"gymBag": "Gym bag"
"gymBag": "Gym bag",
"week": "Week"
},
"classmates": {
"class": "Class",

View File

@ -10,7 +10,8 @@
"changeLanguage": "Changer de langue",
"cancel": "Annuler",
"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": {
"saveToCalenderError": "Un problème est survenu",
@ -53,7 +54,8 @@
"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_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": {
"startTime": "Heure de début",

View File

@ -50,7 +50,9 @@
"saveToCalenderSuccess": "✔️ Lagret i kalender",
"showCalenderActions": "Vis kalenderhandlinger",
"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": {
"loading": "Laster inn …",
@ -63,7 +65,8 @@
"send": "Send",
"cancel": "Avbryt",
"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": {
"updated": "Oppdatert",
@ -115,7 +118,8 @@
"gymBag": "Gym-bag",
"lunch": "Lunsj",
"end": "Slutt",
"start": "Start"
"start": "Start",
"week": "Uke"
},
"contact": {
"a11y_show_contact_info_button_hint": "Vis kontaktinfo",

View File

@ -50,14 +50,17 @@
"a11y_select_login_method": "Wybierz metodę logowania",
"a11y_clear_social_security_input_field": "Wyczyść pole z personnumerem",
"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": {
"approveAccessToCalender": "Musisz zatwierdzić dostęp do kalendarza",
"saveToCalender": "Zapisz w kalendarzu",
"saveToCalenderError": "Coś poszło nie tak",
"saveToCalenderSuccess": "✔️ Zapisano w kalendarzu",
"showCalenderActions": "Pokaż opcje kalendarza"
"showCalenderActions": "Pokaż opcje kalendarza",
"emptyHeadline": "Kalendarz jest pusty",
"emptyText": "Nie znaleziono nic do pokazania"
},
"children": {
"loadingErrorHeading": "Oj!",
@ -79,7 +82,8 @@
"title": "Öppna skolplattformen",
"cancel": "Anuluj",
"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": {
"changeLanguage": "Zmień język",
@ -114,11 +118,12 @@
"gymBag": "Ubrania do WF",
"lunch": "Lunch",
"end": "Kończy",
"start": "Zaczyna"
"start": "Zaczyna",
"week": "Tydzień"
},
"contact": {
"a11y_show_contact_info_button_hint": "Pokazuje dane kontaktowe",
"home": "Dom",
"home": "Adres",
"email": "E-mail",
"sms": "SMS",
"call": "Zadzwoń",

View File

@ -7,7 +7,8 @@
"selectAbscenseEndTime": "Indique a hora de fim",
"endTime": "Hora de fim",
"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": {
"bankid": {

View File

@ -58,7 +58,9 @@
"saveToCalender": "Spara till kalender",
"saveToCalenderError": "Något gick fel",
"saveToCalenderSuccess": "✔️ Sparad till kalender",
"showCalenderActions": "Visa kalenderfunktioner"
"showCalenderActions": "Visa kalenderfunktioner",
"emptyText": "Hittade ingenting att visa",
"emptyHeadline": "Det ser lite tomt ut i kalendern"
},
"children": {
"loadingErrorHeading": "Hoppsan!",
@ -80,7 +82,8 @@
"title": "Öppna skolplattformen",
"cancel": "Avbryt",
"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": {
"changeLanguage": "Byt språk",
@ -128,7 +131,8 @@
"start": "Börjar",
"end": "Slutar",
"lunch": "Lunch",
"gymBag": "Gympapåse"
"gymBag": "Gympapåse",
"week": "Vecka"
},
"classmates": {
"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+\)$/, '')

View File

@ -1,4 +1,4 @@
import { NewsItem } from '@skolplattformen/api-skolplattformen'
import { NewsItem } from '@skolplattformen/api'
import { useNews } from '@skolplattformen/hooks'
import { MatchData, Searcher } from 'fast-fuzzy'
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 { LanguageProvider } from '../context/language/languageContext'
import { translations } from './translation'
import { lightTheme } from '../design/themes'
export const render = (
ui: ReactElement<any, string>,
@ -14,7 +15,7 @@ export const render = (
return (
<>
<IconRegistry icons={EvaIconsPack} />
<ApplicationProvider {...eva} theme={eva.light}>
<ApplicationProvider {...eva} theme={lightTheme}>
<LanguageProvider
cache={false}
data={translations}

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ const Privacy = () => {
<p>
"Öppna Skolplattformen", hädanefter "appen", byggs av "Not free beer
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
att Skolplattformen fungerar. Vi kan endast ta ansvar för att vår kod
fungerar inte deras.

View File

@ -1,5 +1,7 @@
import Link from './Link'
import { price } from './Pricing'
const QA = () => {
return (
<div className="header">
@ -249,7 +251,7 @@ const QA = () => {
de?
</h3>
<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
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
@ -315,7 +317,7 @@ const QA = () => {
<h3>Kontakta oss</h3>
<p>
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>
</div>
</div>

View File

@ -66,7 +66,7 @@ export const FEATURES_DATA = [
{
title: 'Kan byggas ut till fler skolsystem',
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: (
<svg
className="fill-current"

View File

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

View File

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

View File

@ -1,13 +1,22 @@
import { toNamespacedPath } from "path";
// TODO: fix the startDate/endDate of all lessons
export const lessons_133700_goteborgsstad = () => {
const baseTime = 1636357800000;
const baseDate = new Date(baseTime)
const today = new Date()
const currentHour = today.getHours()
today.setHours(baseDate.getHours())
today.setMinutes(baseDate.getMinutes())
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 {
"url": "https://hjarntorget.goteborg.se/api/schema/lessons?forUser=133700_goteborgsstad&startDateIso=2021-11-01&endDateIso=2021-11-08",
"headers": {
@ -206,14 +215,21 @@ export const lessons_133700_goteborgsstad = () => {
}
export const lessons_123456_goteborgsstad = () => {
const baseTime = 1636355400000;
const baseTime = 1636357800000;
const baseDate = new Date(baseTime)
const today = new Date()
const currentHour = today.getHours()
today.setHours(baseDate.getHours())
today.setMinutes(baseDate.getMinutes())
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 {
"url": "https://hjarntorget.goteborg.se/api/schema/lessons?forUser=123456_goteborgsstad&startDateIso=2021-11-01&endDateIso=2021-11-08",
"headers": {

View File

@ -10,7 +10,7 @@ import {
pollStatusUrl,
} from './routes'
export class HjarntorgetChecker extends EventEmitter {
export class HjarntorgetChecker extends EventEmitter implements LoginStatusChecker {
private fetcher: Fetcher
private basePollingUrl: string
@ -120,3 +120,10 @@ export const checkStatus = (
fetch: Fetcher,
basePollingUrl: string
): 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
```javascript
import init from '@skolplattformen/api-skolplattformen'
import init from '@skolplattformen/api'
import CookieManager from '@react-native-cookies/cookies'
const api = init(fetch, () => CookieManager.clearAll())
@ -25,7 +25,7 @@ const api = init(fetch, () => CookieManager.clearAll())
#### node
```javascript
import init from '@skolplattformen/api-skolplattformen'
import init from '@skolplattformen/api'
import nodeFetch from 'node-fetch'
import fetchCookie from 'fetch-cookie/node-fetch'
import { CookieJar } from 'tough-cookie'

View File

@ -17,7 +17,9 @@ import {
ScheduleItem,
Skola24Child,
SSOSystem,
Teacher,
TimetableEntry,
SchoolContact,
URLSearchParams,
User,
wrap,
@ -28,7 +30,7 @@ import { decode } from 'he'
import { DateTime } from 'luxon'
import * as html from 'node-html-parser'
import * as fake from './fakeData'
import { checkStatus } from './loginStatusChecker'
import { checkStatus, DummyStatusChecker } from './loginStatusChecker'
import * as parse from './parse/index'
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(
url: string,
options?: RequestInit
@ -206,8 +218,7 @@ export class ApiSkolplattformen extends EventEmitter implements Api {
this.emit('login')
}, 50)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const emitter: any = new EventEmitter()
const emitter = new DummyStatusChecker()
emitter.token = 'fake'
return emitter
}
@ -265,6 +276,39 @@ export class ApiSkolplattformen extends EventEmitter implements Api {
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(
child: EtjanstChild,
from: DateTime,
@ -518,6 +562,8 @@ export class ApiSkolplattformen extends EventEmitter implements Api {
return parse.timetable(json, year, week, lang)
}
public async logout() {
this.isFake = false
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 { AuthTicket, Fetcher, LoginStatusChecker } from '@skolplattformen/api';
export class Checker extends EventEmitter {
export class Checker extends EventEmitter implements LoginStatusChecker {
public token: string;
private fetcher: Fetcher;
@ -41,3 +41,10 @@ export const checkStatus = (
fetch: Fetcher,
ticket: AuthTicket
): 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 './notifications'
export * from './schedule'
export * from './schoolContacts'
export * from './teachers'
export * from './timetable'
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) {
throw new Error(response.error)
}
if(!response.data.lessonInfo){
throw new Error("Empty lessonInfo received")
}
return response.data.lessonInfo.map((entry) =>
timetableEntry(entry, year, week, lang)
)

View File

@ -20,6 +20,12 @@ export const calendar = (childId: string) =>
export const classmates = (childId: string) =>
`${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 =
'https://etjanst.stockholm.se/vardnadshavare/base/getuserdata'

View File

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

View File

@ -214,3 +214,24 @@ export interface TimetableEntry extends Subject {
dateStart: 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'],
['2020-12-18T15:59:46.34', '2020-12-18T14: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],
])('handles date parsing of %s', (input, 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
import React from 'react'
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 AsyncStorage from '@react-native-async-storage/async-storage'
import { RootComponent } from './components/root'

View File

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

View File

@ -9,7 +9,9 @@ import {
NewsItem,
Notification,
ScheduleItem,
SchoolContact,
Skola24Child,
Teacher,
TimetableEntry,
User,
} from '@skolplattformen/api'
@ -107,7 +109,7 @@ const hook = <T>(
if (newState.error) {
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))
)
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 = (
child: Skola24Child,
child: Child,
week: number,
year: number,
lang: Language
@ -212,9 +232,31 @@ export const useTimetable = (
`timetable_${child.personGuid}_${week}_${year}_${lang}`,
[],
(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 = () =>
hook<User>(
'USER',

View File

@ -6,7 +6,9 @@ import {
NewsItem,
Notification,
ScheduleItem,
SchoolContact,
Skola24Child,
Teacher,
TimetableEntry,
User,
} from '@skolplattformen/api'
@ -77,3 +79,5 @@ export const newsDetails = createReducer<NewsItem[]>('NEWS_DETAILS')
export const notifications = createReducer<Notification[]>('NOTIFICATIONS')
export const schedule = createReducer<ScheduleItem[]>('SCHEDULE')
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,
notifications,
schedule,
schoolContacts,
skola24Children,
teachers,
timetable,
user,
} from './reducers'
@ -23,7 +25,9 @@ const appReducer = combineReducers({
newsDetails,
notifications,
schedule,
schoolContacts,
skola24Children,
teachers,
timetable,
user,
})

View File

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

View File

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