From 81ed4e0f510d77bbf94cbf03f5a05b66ed80c338 Mon Sep 17 00:00:00 2001
From: PianoNic <79938743+Pianonic@users.noreply.github.com>
Date: Tue, 10 Nov 2026 14:37:12 +0100
Subject: [PATCH 1/7] Serve a made-up student ID card from the fake API
---
tools/store-screenshots/fake-api/server.ts | 34 ++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/tools/store-screenshots/fake-api/server.ts b/tools/store-screenshots/fake-api/server.ts
index bea5ebd..9b40359 100644
--- a/tools/store-screenshots/fake-api/server.ts
+++ b/tools/store-screenshots/fake-api/server.ts
@@ -203,6 +203,35 @@ const outlookEvents = [
{ id: "ev4", title: "Training", start: at("2026-11-13", "18:00"), end: at("2026-11-13", "19:30"), isAllDay: false, location: "Sporthalle", organizer: null, preview: null, webLink: "https://outlook.office.com/calendar/item/ev4", joinUrl: null },
].map((e) => ({ ...e, accountId: MS_ACC, showAs: "busy", isCancelled: false }));
+// A made-up Lernendenausweis shaped like the Schulnetz one: elements placed absolutely with inline CSS, `data:` images,
+// and taller than a phone's card area, so the page has to show all of it.
+const qr = (() => {
+ let cells = "";
+ for (let y = 0; y < 25; y++) for (let x = 0; x < 25; x++) {
+ const finder = (a: number, b: number) => a >= 0 && a < 7 && b >= 0 && b < 7 && (a === 0 || a === 6 || b === 0 || b === 6 || (a > 1 && a < 5 && b > 1 && b < 5));
+ if (finder(x, y) || finder(x - 18, y) || finder(x, y - 18) || (!(x < 8 && y < 8) && !(x > 16 && y < 8) && !(x < 8 && y > 16) && (x * 7 + y * 13 + x * y) % 3 === 0)) cells += ``;
+ }
+ return "data:image/svg+xml;base64," + Buffer.from(``).toString("base64");
+})();
+const studentIdCardHtml = async () => {
+ const photo = "data:image/jpeg;base64," + Buffer.from(await Bun.file(STATIC + "/mia.jpg").arrayBuffer()).toString("base64");
+ const t = "position:absolute;font-family:Arial,sans-serif;color:#111;";
+ return `
`
+ + `Lernendenausweis 2026/27
`
+ + `Berufsfachschule Beispielstadt
`
+ + `
`
+ + `Name
Muster
`
+ + `Vorname
Mia
`
+ + `Geburtsdatum
14.03.2009
`
+ + `Ausbildung
Informatikerin EFZ
`
+ + `
`
+ + `Gültig bis 31.07.2027
`
+ + ``;
+};
+
+// SLOW= holds back the student ID card and the picture upload, to see their spinners.
+const slow = () => Bun.sleep(Number(process.env.SLOW ?? 0));
+
const tokens = () => ({ access_token: "fake-access-" + Date.now(), refresh_token: "fake-refresh", expires_in: 86400, token_type: "Bearer" });
const routes: Record any> = {
@@ -234,6 +263,7 @@ const routes: Record any> = {
return { accounts: [{ id: MS_ACC, email: "mia.muster@bfs-beispielstadt.ch", displayName: "Mia Muster", status: "ok", message: null }], events };
},
"PUT /api/me/avatar": async (req) => {
+ await slow();
const form = await req.formData();
const file = form.get("file") as File | null;
if (!file) return new Response(JSON.stringify({ message: "no file" }), { status: 400 });
@@ -251,6 +281,10 @@ const routes: Record any> = {
[`GET ${BASE}/accounts/${ACC}/sync`]: () => ({ lastSync: minutesAgo(12), syncStatus: "Up to date", syncError: null }),
[`POST ${BASE}/accounts/${ACC}/sync`]: () => null,
[`GET ${BASE}/accounts/${ACC}/absences/reported`]: () => reported,
+ [`GET ${BASE}/accounts/${ACC}/student-id-card`]: async () => {
+ await slow();
+ return { html: await studentIdCardHtml(), fetchedAt: nowIso(), source: "mobile" };
+ },
[`GET ${BASE}/accounts/${ACC}/absences/reasons`]: () => ["Krankheit", "Arzttermin", "Unfall", "Familiäre Gründe", "Militär / Zivilschutz"],
};
From b21ff7164d177f98de49d2b7dfffe9cba9b188a1 Mon Sep 17 00:00:00 2001
From: PianoNic <79938743+Pianonic@users.noreply.github.com>
Date: Tue, 10 Nov 2026 14:37:13 +0100
Subject: [PATCH 2/7] Show the whole student ID card
---
lib/ui/account/student_id_page.dart | 28 ++++++++++++++++++++++------
1 file changed, 22 insertions(+), 6 deletions(-)
diff --git a/lib/ui/account/student_id_page.dart b/lib/ui/account/student_id_page.dart
index 7aa5ed7..eaa66ab 100644
--- a/lib/ui/account/student_id_page.dart
+++ b/lib/ui/account/student_id_page.dart
@@ -1,6 +1,8 @@
import '../core/ui/auth_image.dart';
import 'dart:math' as math;
+import 'package:flutter/foundation.dart' show Factory;
+import 'package:flutter/gestures.dart' show EagerGestureRecognizer, OneSequenceGestureRecognizer;
import 'package:flutter/material.dart' show MaterialPageRoute;
import 'package:flutter/widgets.dart';
import 'package:forui/forui.dart';
@@ -197,7 +199,12 @@ class _StudentIdPageState extends State {
final typography = context.theme.typography;
final dates = Dates(context);
final card = _card;
- final height = math.max(420.0, MediaQuery.sizeOf(context).height * 0.62);
+ // The card gets all the height the page has left below the title and above the "As of" row, so the page itself
+ // does not scroll; a card taller than that scrolls inside the WebView.
+ final media = MediaQuery.of(context);
+ double line(TextStyle style) => media.textScaler.scale(style.fontSize ?? 14) * (style.height ?? 1.2);
+ final chrome = 48 + 6 + 16 + line(typography.xl2) + 10 + 12 + line(typography.sm) + 24;
+ final height = math.max(320.0, media.size.height - media.viewPadding.top - media.viewPadding.bottom - chrome);
final List body;
if (card != null) {
@@ -227,10 +234,13 @@ class _StudentIdPageState extends State {
),
),
if (_loading)
- const SizedBox(
- width: 14,
- height: 14,
- child: FCircularProgress(),
+ SizedBox.square(
+ dimension: 14,
+ child: FCircularProgress(
+ style: (s) => s.copyWith(
+ iconStyle: s.iconStyle.copyWith(size: 14),
+ ),
+ ),
),
],
),
@@ -316,7 +326,13 @@ class _OfficialCardState extends State<_OfficialCard> {
}
@override
- Widget build(BuildContext context) => WebViewWidget(controller: _controller);
+ Widget build(BuildContext context) => WebViewWidget(
+ controller: _controller,
+ // Drags on the card scroll the card, not the page, so a card taller than its box can be scrolled to its end.
+ gestureRecognizers: {
+ Factory(EagerGestureRecognizer.new),
+ },
+ );
}
/// The card Schuly draws itself from the school profile; tap or "Turn over" flips it.
From 67369512243f29341a150b4b742934d207dca59d Mon Sep 17 00:00:00 2001
From: PianoNic <79938743+Pianonic@users.noreply.github.com>
Date: Tue, 10 Nov 2026 14:37:13 +0100
Subject: [PATCH 3/7] Size the small loading spinners to their box
---
lib/ui/account/profile_picture.dart | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/ui/account/profile_picture.dart b/lib/ui/account/profile_picture.dart
index 830549d..4aa4dd8 100644
--- a/lib/ui/account/profile_picture.dart
+++ b/lib/ui/account/profile_picture.dart
@@ -119,7 +119,7 @@ class _ProfilePictureButtonState extends State with Single
width: 24,
height: 24,
decoration: BoxDecoration(shape: BoxShape.circle, color: colors.background, border: Border.all(color: colors.border)),
- child: _busy ? const Padding(padding: EdgeInsets.all(5), child: FCircularProgress()) : Icon(FIcons.pencil, size: 12, color: colors.foreground),
+ child: _busy ? Padding(padding: const EdgeInsets.all(5), child: FCircularProgress(style: (s) => s.copyWith(iconStyle: s.iconStyle.copyWith(size: 12)))) : Icon(FIcons.pencil, size: 12, color: colors.foreground),
),
),
],
From e46f4504c472e7841536647b6bf7a64f791ee2d1 Mon Sep 17 00:00:00 2001
From: PianoNic <79938743+Pianonic@users.noreply.github.com>
Date: Tue, 10 Nov 2026 14:37:13 +0100
Subject: [PATCH 4/7] Split account pictures diagonally with the provider logo
---
lib/ui/linking/schools_accounts_screen.dart | 76 ++++++++++++++++++++-
1 file changed, 74 insertions(+), 2 deletions(-)
diff --git a/lib/ui/linking/schools_accounts_screen.dart b/lib/ui/linking/schools_accounts_screen.dart
index 44a1fa8..f7c227c 100644
--- a/lib/ui/linking/schools_accounts_screen.dart
+++ b/lib/ui/linking/schools_accounts_screen.dart
@@ -9,6 +9,7 @@ import '../../services/linking_service.dart';
import '../../services/school_data_service.dart';
import '../../services/toast_service.dart';
import '../core/theme/schuly_theme.dart';
+import '../core/ui/auth_image.dart';
import '../core/ui/kit.dart';
import 'add_account_flow.dart';
import 'provider_icon.dart';
@@ -172,8 +173,8 @@ class _SchoolsAccountsScreenState extends State {
children: [
for (final a in entry.value)
FTile(
- // The account's own picture, as on the web's account cards; initials when it has none.
- prefix: a.isProviderError ? null : Avatar(url: a.pictureUrl, name: a.title, size: 36),
+ // The provider's logo and the account's own picture, split diagonally; only the logo when it has no picture.
+ prefix: a.isProviderError ? null : _AccountAvatar(account: a, provider: _c.provider(a.providerId), size: 36),
title: Text(a.isProviderError ? t.accountsProviderFailed(a.providerName) : a.title),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -198,6 +199,77 @@ class _SchoolsAccountsScreenState extends State {
}
/// A provider's logo, name and what it gives Schuly.
+/// An account's round picture, cut diagonally from top right to bottom left: the provider's logo in the top-left half,
+/// the account's own picture in the bottom-right half. Only the logo when the account has no picture, and the initial
+/// when the provider is unknown.
+class _AccountAvatar extends StatelessWidget {
+ const _AccountAvatar({required this.account, required this.provider, required this.size});
+
+ final LinkedAccount account;
+ final LinkProvider? provider;
+ final double size;
+
+ @override
+ Widget build(BuildContext context) {
+ final provider = this.provider;
+ if (provider == null) return Avatar(url: account.pictureUrl, name: account.title, size: size);
+ Widget logo(double s) => ProviderIcon(id: provider.id, lucide: provider.lucide, svgUrl: provider.svgUrl, size: s);
+ final url = account.pictureUrl;
+ if (url == null || url.isEmpty) return FAvatar.raw(size: size, child: logo(size * 0.5));
+ final colors = context.theme.colors;
+ return ClipOval(
+ child: SizedBox.square(
+ dimension: size,
+ child: Stack(
+ children: [
+ Positioned.fill(
+ child: ClipPath(
+ clipper: const _DiagonalHalf(topLeft: true),
+ child: ColoredBox(color: colors.muted),
+ ),
+ ),
+ // Centred in the top-left half disc, clear of the cut.
+ Positioned(left: size * 0.16, top: size * 0.16, child: logo(size * 0.32)),
+ Positioned.fill(
+ child: ClipPath(
+ clipper: const _DiagonalHalf(topLeft: false),
+ child: Image(image: AuthImage(url), fit: BoxFit.cover, errorBuilder: (_, _, _) => ColoredBox(color: colors.muted)),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// One side of the cut from the top-right to the bottom-left corner, leaving a thin gap along it.
+class _DiagonalHalf extends CustomClipper {
+ const _DiagonalHalf({required this.topLeft});
+
+ final bool topLeft;
+
+ @override
+ Path getClip(Size size) {
+ const gap = 1.0;
+ final w = size.width, h = size.height;
+ return topLeft
+ ? (Path()
+ ..moveTo(0, 0)
+ ..lineTo(w - gap, 0)
+ ..lineTo(0, h - gap)
+ ..close())
+ : (Path()
+ ..moveTo(w, gap)
+ ..lineTo(w, h)
+ ..lineTo(gap, h)
+ ..close());
+ }
+
+ @override
+ bool shouldReclip(_DiagonalHalf old) => old.topLeft != topLeft;
+}
+
class _ProviderHead extends StatelessWidget {
const _ProviderHead({required this.name, required this.provider});
From 739d370d5ed17eca1fa517e121cdcec786ae72a6 Mon Sep 17 00:00:00 2001
From: PianoNic <79938743+Pianonic@users.noreply.github.com>
Date: Tue, 10 Nov 2026 14:49:14 +0100
Subject: [PATCH 5/7] Show account pictures on Schools & accounts as before
---
lib/ui/linking/schools_accounts_screen.dart | 76 +--------------------
1 file changed, 2 insertions(+), 74 deletions(-)
diff --git a/lib/ui/linking/schools_accounts_screen.dart b/lib/ui/linking/schools_accounts_screen.dart
index f7c227c..44a1fa8 100644
--- a/lib/ui/linking/schools_accounts_screen.dart
+++ b/lib/ui/linking/schools_accounts_screen.dart
@@ -9,7 +9,6 @@ import '../../services/linking_service.dart';
import '../../services/school_data_service.dart';
import '../../services/toast_service.dart';
import '../core/theme/schuly_theme.dart';
-import '../core/ui/auth_image.dart';
import '../core/ui/kit.dart';
import 'add_account_flow.dart';
import 'provider_icon.dart';
@@ -173,8 +172,8 @@ class _SchoolsAccountsScreenState extends State {
children: [
for (final a in entry.value)
FTile(
- // The provider's logo and the account's own picture, split diagonally; only the logo when it has no picture.
- prefix: a.isProviderError ? null : _AccountAvatar(account: a, provider: _c.provider(a.providerId), size: 36),
+ // The account's own picture, as on the web's account cards; initials when it has none.
+ prefix: a.isProviderError ? null : Avatar(url: a.pictureUrl, name: a.title, size: 36),
title: Text(a.isProviderError ? t.accountsProviderFailed(a.providerName) : a.title),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -199,77 +198,6 @@ class _SchoolsAccountsScreenState extends State {
}
/// A provider's logo, name and what it gives Schuly.
-/// An account's round picture, cut diagonally from top right to bottom left: the provider's logo in the top-left half,
-/// the account's own picture in the bottom-right half. Only the logo when the account has no picture, and the initial
-/// when the provider is unknown.
-class _AccountAvatar extends StatelessWidget {
- const _AccountAvatar({required this.account, required this.provider, required this.size});
-
- final LinkedAccount account;
- final LinkProvider? provider;
- final double size;
-
- @override
- Widget build(BuildContext context) {
- final provider = this.provider;
- if (provider == null) return Avatar(url: account.pictureUrl, name: account.title, size: size);
- Widget logo(double s) => ProviderIcon(id: provider.id, lucide: provider.lucide, svgUrl: provider.svgUrl, size: s);
- final url = account.pictureUrl;
- if (url == null || url.isEmpty) return FAvatar.raw(size: size, child: logo(size * 0.5));
- final colors = context.theme.colors;
- return ClipOval(
- child: SizedBox.square(
- dimension: size,
- child: Stack(
- children: [
- Positioned.fill(
- child: ClipPath(
- clipper: const _DiagonalHalf(topLeft: true),
- child: ColoredBox(color: colors.muted),
- ),
- ),
- // Centred in the top-left half disc, clear of the cut.
- Positioned(left: size * 0.16, top: size * 0.16, child: logo(size * 0.32)),
- Positioned.fill(
- child: ClipPath(
- clipper: const _DiagonalHalf(topLeft: false),
- child: Image(image: AuthImage(url), fit: BoxFit.cover, errorBuilder: (_, _, _) => ColoredBox(color: colors.muted)),
- ),
- ),
- ],
- ),
- ),
- );
- }
-}
-
-/// One side of the cut from the top-right to the bottom-left corner, leaving a thin gap along it.
-class _DiagonalHalf extends CustomClipper {
- const _DiagonalHalf({required this.topLeft});
-
- final bool topLeft;
-
- @override
- Path getClip(Size size) {
- const gap = 1.0;
- final w = size.width, h = size.height;
- return topLeft
- ? (Path()
- ..moveTo(0, 0)
- ..lineTo(w - gap, 0)
- ..lineTo(0, h - gap)
- ..close())
- : (Path()
- ..moveTo(w, gap)
- ..lineTo(w, h)
- ..lineTo(gap, h)
- ..close());
- }
-
- @override
- bool shouldReclip(_DiagonalHalf old) => old.topLeft != topLeft;
-}
-
class _ProviderHead extends StatelessWidget {
const _ProviderHead({required this.name, required this.provider});
From 7f2b8b5db442ede32691c274594edb2fe80e9c95 Mon Sep 17 00:00:00 2001
From: PianoNic <79938743+Pianonic@users.noreply.github.com>
Date: Tue, 10 Nov 2026 14:49:14 +0100
Subject: [PATCH 6/7] Split school pictures in the Schools sheet diagonally
with the logo
---
lib/ui/dashboard/schools_sheet.dart | 82 ++++++++++++++++++++++++++---
1 file changed, 74 insertions(+), 8 deletions(-)
diff --git a/lib/ui/dashboard/schools_sheet.dart b/lib/ui/dashboard/schools_sheet.dart
index c788dd6..9b3ed64 100644
--- a/lib/ui/dashboard/schools_sheet.dart
+++ b/lib/ui/dashboard/schools_sheet.dart
@@ -123,27 +123,93 @@ class _SchoolsSheet extends StatelessWidget {
}
}
-/// The school's logo, or the profile picture the school holds, in a rounded square.
+/// The school's logo in a rounded square. With a profile picture the circle is cut diagonally from top right to bottom
+/// left: the logo in the top-left half, the picture in the bottom-right half.
class _SchoolLogo extends StatelessWidget {
const _SchoolLogo({required this.school});
final MySchool school;
+ static const _size = 40.0;
+
@override
Widget build(BuildContext context) {
final colors = context.theme.colors;
final initials = school.name.trim().isEmpty ? '?' : school.name.trim().characters.take(2).toString().toUpperCase();
final fallback = Center(child: Text(initials, style: context.theme.typography.base.copyWith(fontWeight: FontWeight.w600)));
- Widget network(String? url) => url == null || url.isEmpty ? fallback : Image(image: AuthImage(url), fit: BoxFit.cover, errorBuilder: (_, _, _) => fallback);
- if (school.profilePictureUrl?.isNotEmpty == true) return Avatar(url: school.profilePictureUrl, name: school.name, size: 40);
+ Widget logo(Widget fallback) {
+ Widget network(String? url) => url == null || url.isEmpty ? fallback : Image(image: AuthImage(url), fit: BoxFit.cover, errorBuilder: (_, _, _) => fallback);
+ return school.schoolSystemKey != null
+ ? Image.asset('assets/schoolsystems/${school.schoolSystemKey}.webp', fit: BoxFit.contain, errorBuilder: (_, _, _) => network(school.logoUrl))
+ : network(school.logoUrl);
+ }
+
+ final picture = school.profilePictureUrl;
+ if (picture != null && picture.isNotEmpty) {
+ const logoSize = _size * 0.32;
+ return ClipOval(
+ child: SizedBox.square(
+ dimension: _size,
+ child: Stack(
+ children: [
+ Positioned.fill(
+ child: ClipPath(
+ clipper: const _DiagonalHalf(topLeft: true),
+ child: ColoredBox(color: colors.muted),
+ ),
+ ),
+ // Centred in the top-left half disc, clear of the cut.
+ Positioned(
+ left: _size * 0.16,
+ top: _size * 0.16,
+ width: logoSize,
+ height: logoSize,
+ child: logo(FittedBox(child: fallback)),
+ ),
+ Positioned.fill(
+ child: ClipPath(
+ clipper: const _DiagonalHalf(topLeft: false),
+ child: Image(image: AuthImage(picture), fit: BoxFit.cover, errorBuilder: (_, _, _) => ColoredBox(color: colors.muted)),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
return Container(
- width: 40,
- height: 40,
+ width: _size,
+ height: _size,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(color: colors.muted, borderRadius: BorderRadius.circular(SchulyRadii.base)),
- child: school.schoolSystemKey != null
- ? Padding(padding: const EdgeInsets.all(7), child: Image.asset('assets/schoolsystems/${school.schoolSystemKey}.webp', fit: BoxFit.contain, errorBuilder: (_, _, _) => network(school.logoUrl)))
- : network(school.logoUrl),
+ child: school.schoolSystemKey != null ? Padding(padding: const EdgeInsets.all(7), child: logo(fallback)) : logo(fallback),
);
}
}
+
+/// One side of the cut from the top-right to the bottom-left corner, leaving a thin gap along it.
+class _DiagonalHalf extends CustomClipper {
+ const _DiagonalHalf({required this.topLeft});
+
+ final bool topLeft;
+
+ @override
+ Path getClip(Size size) {
+ const gap = 1.0;
+ final w = size.width, h = size.height;
+ return topLeft
+ ? (Path()
+ ..moveTo(0, 0)
+ ..lineTo(w - gap, 0)
+ ..lineTo(0, h - gap)
+ ..close())
+ : (Path()
+ ..moveTo(w, gap)
+ ..lineTo(w, h)
+ ..lineTo(gap, h)
+ ..close());
+ }
+
+ @override
+ bool shouldReclip(_DiagonalHalf old) => old.topLeft != topLeft;
+}
From c27e8269f708f31914c42f27ff9fd95312d03a32 Mon Sep 17 00:00:00 2001
From: PianoNic <79938743+Pianonic@users.noreply.github.com>
Date: Tue, 10 Nov 2026 14:49:15 +0100
Subject: [PATCH 7/7] Add two more schools to the fake API for the Schools
sheet
---
tools/store-screenshots/fake-api/server.ts | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/tools/store-screenshots/fake-api/server.ts b/tools/store-screenshots/fake-api/server.ts
index 9b40359..1ba26c7 100644
--- a/tools/store-screenshots/fake-api/server.ts
+++ b/tools/store-screenshots/fake-api/server.ts
@@ -243,7 +243,14 @@ const routes: Record any> = {
"POST /auth/logout": () => null,
"GET /api/Auth/me": () => ({ id: APP_USER, externalId: "mia.muster", email: "mia.muster@bfs-beispielstadt.ch", displayName: "Mia Muster", profilePictureUrl: picture, createdAt: "2025-08-20T08:00:00Z", schoolUsers: [{ id: ME, firstName: "Mia", lastName: "Muster", email: "mia.muster@bfs-beispielstadt.ch", role: "Student" }] }),
"GET /api/Auth/me/privacy-notice": () => ({ policyVersion: "2026-05", policyUrl: "https://schuly.dev/en/privacy", acceptanceRequired: false, swissSchoolConfirmationRequired: false, acceptedVersion: "2026-05", acceptedAt: "2026-05-12T07:00:00Z", swissSchoolConfirmedAt: "2026-05-12T07:00:00Z" }),
- "GET /api/Schools/my-schools": () => [{ id: SCHOOL, name: "BFS Beispielstadt", fullName: "Berufsfachschule Beispielstadt", email: "mia.muster@bfs-beispielstadt.ch", logoUrl: null, schoolSystemKey: "schulportal", profilePictureUrl: "/static/mia.jpg" }],
+ "GET /api/Schools/my-schools": () => [
+ { id: SCHOOL, name: "BFS Beispielstadt", fullName: "Berufsfachschule Beispielstadt", email: "mia.muster@bfs-beispielstadt.ch", logoUrl: null, schoolSystemKey: "schulportal", profilePictureUrl: "/static/mia.jpg" },
+ // Two more schools for the Schools sheet: one with a bundled logo and a picture, one with the logo only.
+ ...(EXTRAS ? [
+ { id: "sch-bms", name: "BMS Beispielstadt", fullName: "Berufsmaturitätsschule Beispielstadt", email: "mia.muster@bms-beispielstadt.ch", logoUrl: null, schoolSystemKey: "schulnetz", profilePictureUrl: "/static/mia.jpg" },
+ { id: "sch-uek", name: "üK Beispielstadt", fullName: "Überbetriebliche Kurse Beispielstadt", email: "mia.muster@uek-beispielstadt.ch", logoUrl: null, schoolSystemKey: "odaorg", profilePictureUrl: null },
+ ] : []),
+ ],
"GET /api/SchoolUsers": () => [schoolUser],
"GET /api/Exams": () => exams,
"GET /api/Agendas": () => agenda,