diff --git a/backend/pb_migrations/1754400000_init_schema.js b/backend/pb_migrations/1754400000_init_schema.js
index 23aed54..a8ad793 100644
--- a/backend/pb_migrations/1754400000_init_schema.js
+++ b/backend/pb_migrations/1754400000_init_schema.js
@@ -12,8 +12,8 @@ migrate((app) => {
{
"id": "_pb_users_auth_",
"listRule": "id = @request.auth.id",
- "viewRule": "id = @request.auth.id",
- "createRule": null,
+ "viewRule": "@request.auth.id != \"\"",
+ "createRule": "@request.auth.id != \"\"",
"updateRule": "id = @request.auth.id",
"deleteRule": "id = @request.auth.id",
"name": "users",
@@ -457,20 +457,26 @@ migrate((app) => {
"type": "text"
},
{
- "help": "",
"hidden": false,
- "id": "select2063623452",
- "maxSelect": 1,
- "name": "status",
+ "id": "date_event_starts",
+ "max": "",
+ "min": "",
+ "name": "starts",
"presentable": false,
"required": false,
"system": false,
- "type": "select",
- "values": [
- "draft",
- "active",
- "finished"
- ]
+ "type": "date"
+ },
+ {
+ "hidden": false,
+ "id": "date_event_ends",
+ "max": "",
+ "min": "",
+ "name": "ends",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "date"
},
{
"cascadeDelete": false,
@@ -590,9 +596,9 @@ migrate((app) => {
"id": "spgs7e4anufen9u",
"listRule": "team.users.id ?= @request.auth.id",
"viewRule": "team.users.id ?= @request.auth.id",
- "createRule": "team.users.id ?= @request.auth.id",
- "updateRule": "team.users.id ?= @request.auth.id",
- "deleteRule": "team.users.id ?= @request.auth.id",
+ "createRule": "team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id",
+ "updateRule": "team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id",
+ "deleteRule": "team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id",
"name": "riders",
"type": "base",
"fields": [
@@ -655,6 +661,20 @@ migrate((app) => {
"system": false,
"type": "relation"
},
+ {
+ "cascadeDelete": false,
+ "collectionId": "_pb_users_auth_",
+ "help": "",
+ "hidden": false,
+ "id": "relation_rider_user",
+ "maxSelect": 1,
+ "minSelect": 0,
+ "name": "user",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "relation"
+ },
{
"hidden": false,
"id": "autodate2990389176",
@@ -1013,6 +1033,31 @@ migrate((app) => {
"gesperrt"
]
},
+ {
+ "hidden": false,
+ "id": "date_trail_status_changed",
+ "max": "",
+ "min": "",
+ "name": "status_changed",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "date"
+ },
+ {
+ "cascadeDelete": false,
+ "collectionId": "_pb_users_auth_",
+ "help": "",
+ "hidden": false,
+ "id": "relation_trail_status_by",
+ "maxSelect": 1,
+ "minSelect": 0,
+ "name": "status_by",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "relation"
+ },
{
"cascadeDelete": false,
"collectionId": "_pb_users_auth_",
diff --git a/backend/pb_migrations/1754500500_riders_user_and_admin_rules.js b/backend/pb_migrations/1754500500_riders_user_and_admin_rules.js
new file mode 100644
index 0000000..38fd881
--- /dev/null
+++ b/backend/pb_migrations/1754500500_riders_user_and_admin_rules.js
@@ -0,0 +1,73 @@
+///
+
+// Fahrer bekommen ein optionales Benutzerkonto, und die Fahrerverwaltung wird
+// zur Sache der Teamleitung.
+//
+// 1) riders.user — Verknüpfung eines Fahrers mit einem Login. Optional, weil
+// die meisten Fahrer nur in Ergebnislisten auftauchen und sich nie
+// anmelden. cascadeDelete ist bewusst aus: Wird ein Konto gelöscht, soll
+// der Fahrer samt seiner Zeiten erhalten bleiben.
+//
+// 2) riders create/update/delete nur noch für Owner und Admins. Bisher durfte
+// jedes Teammitglied Fahrer anlegen, ändern und löschen — mit den Zeiten
+// daran ist das zu viel Vertrauen in jeden Mitfahrer. Lesen bleibt für das
+// ganze Team offen.
+//
+// 3) users.viewRule von "nur ich selbst" auf "eingeloggt" gelockert. Ohne das
+// lässt sich zu einer bekannten User-ID kein Name anzeigen — betrifft die
+// Mitgliederliste eines Teams, den Melder eines Trail-Markers und die
+// Kontoverknüpfung eines Fahrers. Die listRule bleibt eng: Wer keine ID
+// kennt, kann auch keine Konten durchblättern. E-Mail-Adressen bleiben
+// über emailVisibility verborgen.
+//
+// 4) users.createRule von "niemand" auf "eingeloggt". Damit kann ein Admin
+// einem Fahrer direkt ein Login anlegen. Bewusst nicht offen (""), sonst
+// wäre es eine öffentliche Registrierung.
+
+const RIDERS_ADMIN_RULE =
+ 'team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id'
+const RIDERS_MEMBER_RULE = 'team.users.id ?= @request.auth.id'
+const LOGGED_IN = '@request.auth.id != ""'
+
+migrate((app) => {
+ const users = app.findCollectionByNameOrId('users')
+ const riders = app.findCollectionByNameOrId('riders')
+
+ // Der Snapshot in 1754400000_init_schema.js führt das Feld inzwischen
+ // mit. Auf einer frischen Datenbank ist es also schon da, auf einer
+ // bestehenden noch nicht — dieselbe Lage wie bei den trail-Collections.
+ if (!riders.fields.getByName('user')) {
+ riders.fields.add(new Field({
+ id: 'relation_rider_user',
+ name: 'user',
+ type: 'relation',
+ collectionId: users.id,
+ cascadeDelete: false,
+ maxSelect: 1,
+ minSelect: 0,
+ required: false,
+ }))
+ }
+
+ riders.createRule = RIDERS_ADMIN_RULE
+ riders.updateRule = RIDERS_ADMIN_RULE
+ riders.deleteRule = RIDERS_ADMIN_RULE
+ app.save(riders)
+
+ users.viewRule = LOGGED_IN
+ users.createRule = LOGGED_IN
+ app.save(users)
+}, (app) => {
+ const users = app.findCollectionByNameOrId('users')
+ const riders = app.findCollectionByNameOrId('riders')
+
+ riders.fields.removeByName('user')
+ riders.createRule = RIDERS_MEMBER_RULE
+ riders.updateRule = RIDERS_MEMBER_RULE
+ riders.deleteRule = RIDERS_MEMBER_RULE
+ app.save(riders)
+
+ users.viewRule = 'id = @request.auth.id'
+ users.createRule = null
+ app.save(users)
+})
diff --git a/backend/pb_migrations/1754500600_events_dates_instead_of_status.js b/backend/pb_migrations/1754500600_events_dates_instead_of_status.js
new file mode 100644
index 0000000..0d0cb8b
--- /dev/null
+++ b/backend/pb_migrations/1754500600_events_dates_instead_of_status.js
@@ -0,0 +1,52 @@
+///
+
+// Events werden datiert statt bewertet.
+//
+// Der Status (draft/active/finished) war Handarbeit: Jemand musste ein Event
+// aktiv schalten und hinterher wieder abschließen, sonst stand die Übersicht
+// falsch da. Ein Datum weiß von selbst, was ansteht, was läuft und was vorbei
+// ist.
+//
+// starts trägt den Tag; ends bleibt leer, wenn das Event nur einen Tag dauert,
+// und trägt sonst den letzten Tag. Beide sind optional, damit bestehende
+// Events ohne Datum weiterleben, bis jemand eines nachträgt.
+
+migrate((app) => {
+ const events = app.findCollectionByNameOrId('events')
+
+ // Auf einer frischen Datenbank hat der Snapshot die Felder bereits.
+ if (!events.fields.getByName('starts')) {
+ events.fields.add(new Field({
+ id: 'date_event_starts',
+ name: 'starts',
+ type: 'date',
+ }))
+ }
+ if (!events.fields.getByName('ends')) {
+ events.fields.add(new Field({
+ id: 'date_event_ends',
+ name: 'ends',
+ type: 'date',
+ }))
+ }
+
+ if (events.fields.getByName('status')) {
+ events.fields.removeByName('status')
+ }
+
+ app.save(events)
+}, (app) => {
+ const events = app.findCollectionByNameOrId('events')
+
+ events.fields.removeByName('starts')
+ events.fields.removeByName('ends')
+ events.fields.add(new Field({
+ id: 'select_event_status',
+ name: 'status',
+ type: 'select',
+ maxSelect: 1,
+ values: ['draft', 'active', 'finished'],
+ }))
+
+ app.save(events)
+})
diff --git a/backend/pb_migrations/1754500700_trail_status_meta.js b/backend/pb_migrations/1754500700_trail_status_meta.js
new file mode 100644
index 0000000..705ce8a
--- /dev/null
+++ b/backend/pb_migrations/1754500700_trail_status_meta.js
@@ -0,0 +1,45 @@
+///
+
+// Wer hat den Trail gesperrt, und wann?
+//
+// Der Status allein sagt nur, wie es gerade steht. Für eine Meldung, der man
+// trauen soll, gehört dazu, wann sie kam und von wem — eine Sperrung von
+// heute Morgen wiegt anders als eine vom letzten Herbst.
+//
+// Beide Felder schreibt die Anwendung beim Setzen des Status mit; bestehende
+// Trails bleiben ohne Angabe, bis der Status das nächste Mal geändert wird.
+
+migrate((app) => {
+ const trails = app.findCollectionByNameOrId('trails')
+ const users = app.findCollectionByNameOrId('users')
+
+ if (!trails.fields.getByName('status_changed')) {
+ trails.fields.add(new Field({
+ id: 'date_trail_status_changed',
+ name: 'status_changed',
+ type: 'date',
+ }))
+ }
+
+ if (!trails.fields.getByName('status_by')) {
+ trails.fields.add(new Field({
+ id: 'relation_trail_status_by',
+ name: 'status_by',
+ type: 'relation',
+ collectionId: users.id,
+ cascadeDelete: false,
+ maxSelect: 1,
+ minSelect: 0,
+ required: false,
+ }))
+ }
+
+ app.save(trails)
+}, (app) => {
+ const trails = app.findCollectionByNameOrId('trails')
+
+ trails.fields.removeByName('status_changed')
+ trails.fields.removeByName('status_by')
+
+ app.save(trails)
+})
diff --git a/frontend/src/lib/types.d.ts b/frontend/src/lib/types.d.ts
index b0f45cd..df80871 100644
--- a/frontend/src/lib/types.d.ts
+++ b/frontend/src/lib/types.d.ts
@@ -46,19 +46,15 @@ export type AuthSystemFields = {
// Record types for each collection
-export enum EventsStatusOptions {
- "draft" = "draft",
- "active" = "active",
- "finished" = "finished",
-}
export type EventsRecord = {
created?: IsoDateString
description?: string
+ ends?: IsoDateString
gps?: string
id: string
location?: string
name?: string
- status?: EventsStatusOptions
+ starts?: IsoDateString
team?: RecordIdString
updated?: IsoDateString
}
@@ -74,6 +70,7 @@ export type RidersRecord = {
number?: string
team?: RecordIdString
updated?: IsoDateString
+ user?: RecordIdString
}
export type RunsRecord = {
@@ -188,6 +185,8 @@ export type TrailsRecord = {
id: string
name: string
status?: TrailsStatusOptions
+ status_by?: RecordIdString
+ status_changed?: IsoDateString
stewards?: RecordIdString[]
team: RecordIdString
updated?: IsoDateString