feat: Collections für Trails, Versionen, Flags, Marker und Kommentare
This commit is contained in:
parent
ced89ed9d2
commit
c41a6cb056
3 changed files with 1153 additions and 15 deletions
File diff suppressed because it is too large
Load diff
267
backend/pb_migrations/1754500000_created_trails.js
Normal file
267
backend/pb_migrations/1754500000_created_trails.js
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// Trails: GPX-Tracks mit Versionen, verorteten Markern und Kommentaren.
|
||||
//
|
||||
// Reihenfolge ist wichtig: trails.current zeigt auf trail_versions,
|
||||
// trail_versions.trail zeigt zurück auf trails. Deshalb wird trails zuerst
|
||||
// OHNE current angelegt, dann trail_versions, und current danach ergänzt.
|
||||
|
||||
migrate((app) => {
|
||||
const teams = app.findCollectionByNameOrId('teams')
|
||||
const users = app.findCollectionByNameOrId('users')
|
||||
|
||||
// Für jede Collection gleich: Standardfelder, die PocketBase nicht
|
||||
// selbst anlegt.
|
||||
const timestamps = [
|
||||
{ name: 'created', type: 'autodate', onCreate: true, onUpdate: false },
|
||||
{ name: 'updated', type: 'autodate', onCreate: true, onUpdate: true },
|
||||
]
|
||||
|
||||
const teamScoped = (name) => ({
|
||||
name,
|
||||
type: 'relation',
|
||||
required: true,
|
||||
maxSelect: 1,
|
||||
collectionId: teams.id,
|
||||
cascadeDelete: true,
|
||||
})
|
||||
|
||||
// --- trails (noch ohne current) --------------------------------------
|
||||
const trails = new Collection({
|
||||
type: 'base',
|
||||
name: 'trails',
|
||||
listRule: 'team.users.id ?= @request.auth.id',
|
||||
viewRule: 'team.users.id ?= @request.auth.id',
|
||||
createRule: 'team.users.id ?= @request.auth.id',
|
||||
// Bearbeiten dürfen Paten und Team-Admins
|
||||
updateRule:
|
||||
'stewards.id ?= @request.auth.id' +
|
||||
' || team.owner.id ?= @request.auth.id' +
|
||||
' || team.admins.id ?= @request.auth.id',
|
||||
// Löschen nur Admins — am Trail hängen fremde Marker und Kommentare
|
||||
deleteRule:
|
||||
'team.owner.id ?= @request.auth.id' +
|
||||
' || team.admins.id ?= @request.auth.id',
|
||||
fields: [
|
||||
teamScoped('team'),
|
||||
{ name: 'name', type: 'text', required: true, max: 200 },
|
||||
{ name: 'description', type: 'editor' },
|
||||
{
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
maxSelect: 1,
|
||||
values: ['offen', 'eingeschraenkt', 'gesperrt'],
|
||||
},
|
||||
{
|
||||
name: 'stewards',
|
||||
type: 'relation',
|
||||
maxSelect: 0,
|
||||
collectionId: users.id,
|
||||
cascadeDelete: false,
|
||||
},
|
||||
{
|
||||
name: 'event',
|
||||
type: 'relation',
|
||||
maxSelect: 1,
|
||||
collectionId: app.findCollectionByNameOrId('events').id,
|
||||
cascadeDelete: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'relation',
|
||||
maxSelect: 1,
|
||||
collectionId: users.id,
|
||||
cascadeDelete: false,
|
||||
},
|
||||
...timestamps,
|
||||
],
|
||||
})
|
||||
app.save(trails)
|
||||
|
||||
// --- trail_versions ---------------------------------------------------
|
||||
const versions = new Collection({
|
||||
type: 'base',
|
||||
name: 'trail_versions',
|
||||
listRule: 'trail.team.users.id ?= @request.auth.id',
|
||||
viewRule: 'trail.team.users.id ?= @request.auth.id',
|
||||
createRule:
|
||||
'trail.stewards.id ?= @request.auth.id' +
|
||||
' || trail.team.owner.id ?= @request.auth.id' +
|
||||
' || trail.team.admins.id ?= @request.auth.id',
|
||||
// Versionen sind unveränderlich — eine Korrektur ist eine neue Version
|
||||
updateRule: null,
|
||||
deleteRule:
|
||||
'trail.team.owner.id ?= @request.auth.id' +
|
||||
' || trail.team.admins.id ?= @request.auth.id',
|
||||
fields: [
|
||||
{
|
||||
name: 'trail',
|
||||
type: 'relation',
|
||||
required: true,
|
||||
maxSelect: 1,
|
||||
collectionId: trails.id,
|
||||
cascadeDelete: true,
|
||||
},
|
||||
{
|
||||
name: 'gpx',
|
||||
type: 'file',
|
||||
maxSelect: 1,
|
||||
maxSize: 10485760,
|
||||
mimeTypes: ['application/gpx+xml', 'application/xml', 'text/xml'],
|
||||
},
|
||||
{ name: 'geojson', type: 'json', maxSize: 5000000 },
|
||||
{ name: 'elevation', type: 'json', maxSize: 2000000 },
|
||||
{ name: 'bounds', type: 'json', maxSize: 1000 },
|
||||
{ name: 'distance_m', type: 'number' },
|
||||
{ name: 'ascent_m', type: 'number' },
|
||||
{ name: 'note', type: 'text', max: 500 },
|
||||
{
|
||||
name: 'uploaded_by',
|
||||
type: 'relation',
|
||||
maxSelect: 1,
|
||||
collectionId: users.id,
|
||||
cascadeDelete: false,
|
||||
},
|
||||
...timestamps,
|
||||
],
|
||||
})
|
||||
app.save(versions)
|
||||
|
||||
// --- trails.current nachtragen ---------------------------------------
|
||||
trails.fields.add(new Field({
|
||||
name: 'current',
|
||||
type: 'relation',
|
||||
maxSelect: 1,
|
||||
collectionId: versions.id,
|
||||
cascadeDelete: false,
|
||||
}))
|
||||
app.save(trails)
|
||||
|
||||
// --- trail_flags ------------------------------------------------------
|
||||
const adminOnly =
|
||||
'team.owner.id ?= @request.auth.id' +
|
||||
' || team.admins.id ?= @request.auth.id'
|
||||
|
||||
const flags = new Collection({
|
||||
type: 'base',
|
||||
name: 'trail_flags',
|
||||
listRule: 'team.users.id ?= @request.auth.id',
|
||||
viewRule: 'team.users.id ?= @request.auth.id',
|
||||
createRule: adminOnly,
|
||||
updateRule: adminOnly,
|
||||
deleteRule: adminOnly,
|
||||
fields: [
|
||||
teamScoped('team'),
|
||||
{ name: 'label', type: 'text', required: true, max: 60 },
|
||||
{ name: 'icon', type: 'text', max: 40 },
|
||||
{ name: 'color', type: 'text', max: 9 },
|
||||
{
|
||||
name: 'severity',
|
||||
type: 'select',
|
||||
maxSelect: 1,
|
||||
values: ['info', 'warnung', 'kritisch'],
|
||||
},
|
||||
...timestamps,
|
||||
],
|
||||
})
|
||||
app.save(flags)
|
||||
|
||||
// --- trail_markers ----------------------------------------------------
|
||||
// Anlegen darf jedes Team-Mitglied; ändern nur der Ersteller, Paten
|
||||
// oder Admins.
|
||||
const ownerOrSteward =
|
||||
'created_by.id ?= @request.auth.id' +
|
||||
' || trail.stewards.id ?= @request.auth.id' +
|
||||
' || team.owner.id ?= @request.auth.id' +
|
||||
' || team.admins.id ?= @request.auth.id'
|
||||
|
||||
const markers = new Collection({
|
||||
type: 'base',
|
||||
name: 'trail_markers',
|
||||
listRule: 'team.users.id ?= @request.auth.id',
|
||||
viewRule: 'team.users.id ?= @request.auth.id',
|
||||
createRule: 'team.users.id ?= @request.auth.id',
|
||||
updateRule: ownerOrSteward,
|
||||
deleteRule: ownerOrSteward,
|
||||
fields: [
|
||||
{
|
||||
name: 'trail',
|
||||
type: 'relation',
|
||||
required: true,
|
||||
maxSelect: 1,
|
||||
collectionId: trails.id,
|
||||
cascadeDelete: true,
|
||||
},
|
||||
teamScoped('team'),
|
||||
{
|
||||
name: 'flag',
|
||||
type: 'relation',
|
||||
required: true,
|
||||
maxSelect: 1,
|
||||
collectionId: flags.id,
|
||||
cascadeDelete: false,
|
||||
},
|
||||
{ name: 'lat', type: 'number', required: true },
|
||||
{ name: 'lng', type: 'number', required: true },
|
||||
{ name: 'note', type: 'text', max: 500 },
|
||||
{ name: 'resolved', type: 'bool' },
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'relation',
|
||||
maxSelect: 1,
|
||||
collectionId: users.id,
|
||||
cascadeDelete: false,
|
||||
},
|
||||
...timestamps,
|
||||
],
|
||||
})
|
||||
app.save(markers)
|
||||
|
||||
// --- trail_comments ---------------------------------------------------
|
||||
const comments = new Collection({
|
||||
type: 'base',
|
||||
name: 'trail_comments',
|
||||
listRule: 'team.users.id ?= @request.auth.id',
|
||||
viewRule: 'team.users.id ?= @request.auth.id',
|
||||
createRule: 'team.users.id ?= @request.auth.id',
|
||||
updateRule: ownerOrSteward,
|
||||
deleteRule: ownerOrSteward,
|
||||
fields: [
|
||||
{
|
||||
name: 'trail',
|
||||
type: 'relation',
|
||||
required: true,
|
||||
maxSelect: 1,
|
||||
collectionId: trails.id,
|
||||
cascadeDelete: true,
|
||||
},
|
||||
teamScoped('team'),
|
||||
{ name: 'text', type: 'text', required: true, max: 2000 },
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'relation',
|
||||
maxSelect: 1,
|
||||
collectionId: users.id,
|
||||
cascadeDelete: false,
|
||||
},
|
||||
...timestamps,
|
||||
],
|
||||
})
|
||||
app.save(comments)
|
||||
}, (app) => {
|
||||
// Rückwärts in umgekehrter Reihenfolge, damit keine Relation ins Leere
|
||||
// zeigt. trails zuletzt, weil alle anderen darauf verweisen.
|
||||
for (const name of [
|
||||
'trail_comments',
|
||||
'trail_markers',
|
||||
'trail_flags',
|
||||
'trail_versions',
|
||||
'trails',
|
||||
]) {
|
||||
try {
|
||||
app.delete(app.findCollectionByNameOrId(name))
|
||||
} catch {
|
||||
// Bereits entfernt — nichts zu tun.
|
||||
}
|
||||
}
|
||||
})
|
||||
99
frontend/src/lib/types.d.ts
vendored
99
frontend/src/lib/types.d.ts
vendored
|
|
@ -11,6 +11,11 @@ export enum Collections {
|
|||
Runs = "runs",
|
||||
Teams = "teams",
|
||||
Times = "times",
|
||||
TrailComments = "trail_comments",
|
||||
TrailFlags = "trail_flags",
|
||||
TrailMarkers = "trail_markers",
|
||||
TrailVersions = "trail_versions",
|
||||
Trails = "trails",
|
||||
Users = "users",
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +118,80 @@ export type TimesRecord = {
|
|||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export type TrailCommentsRecord = {
|
||||
created?: IsoDateString
|
||||
created_by?: RecordIdString
|
||||
id: string
|
||||
team: RecordIdString
|
||||
text: string
|
||||
trail: RecordIdString
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export enum TrailFlagsSeverityOptions {
|
||||
"info" = "info",
|
||||
"warnung" = "warnung",
|
||||
"kritisch" = "kritisch",
|
||||
}
|
||||
export type TrailFlagsRecord = {
|
||||
color?: string
|
||||
created?: IsoDateString
|
||||
icon?: string
|
||||
id: string
|
||||
label: string
|
||||
severity?: TrailFlagsSeverityOptions
|
||||
team: RecordIdString
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export type TrailMarkersRecord = {
|
||||
created?: IsoDateString
|
||||
created_by?: RecordIdString
|
||||
flag: RecordIdString
|
||||
id: string
|
||||
lat: number
|
||||
lng: number
|
||||
note?: string
|
||||
resolved?: boolean
|
||||
team: RecordIdString
|
||||
trail: RecordIdString
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export type TrailVersionsRecord<Tbounds = unknown, Televation = unknown, Tgeojson = unknown> = {
|
||||
ascent_m?: number
|
||||
bounds?: null | Tbounds
|
||||
created?: IsoDateString
|
||||
distance_m?: number
|
||||
elevation?: null | Televation
|
||||
geojson?: null | Tgeojson
|
||||
gpx?: string
|
||||
id: string
|
||||
note?: string
|
||||
trail: RecordIdString
|
||||
updated?: IsoDateString
|
||||
uploaded_by?: RecordIdString
|
||||
}
|
||||
|
||||
export enum TrailsStatusOptions {
|
||||
"offen" = "offen",
|
||||
"eingeschraenkt" = "eingeschraenkt",
|
||||
"gesperrt" = "gesperrt",
|
||||
}
|
||||
export type TrailsRecord = {
|
||||
created?: IsoDateString
|
||||
created_by?: RecordIdString
|
||||
current?: RecordIdString
|
||||
description?: HTMLString
|
||||
event?: RecordIdString
|
||||
id: string
|
||||
name: string
|
||||
status?: TrailsStatusOptions
|
||||
stewards?: RecordIdString[]
|
||||
team: RecordIdString
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export type UsersRecord = {
|
||||
avatar?: string
|
||||
created?: IsoDateString
|
||||
|
|
@ -133,6 +212,11 @@ export type RidersResponse<Texpand = unknown> = Required<RidersRecord> & BaseSys
|
|||
export type RunsResponse<Texpand = unknown> = Required<RunsRecord> & BaseSystemFields<Texpand>
|
||||
export type TeamsResponse<Texpand = unknown> = Required<TeamsRecord> & BaseSystemFields<Texpand>
|
||||
export type TimesResponse<Texpand = unknown> = Required<TimesRecord> & BaseSystemFields<Texpand>
|
||||
export type TrailCommentsResponse<Texpand = unknown> = Required<TrailCommentsRecord> & BaseSystemFields<Texpand>
|
||||
export type TrailFlagsResponse<Texpand = unknown> = Required<TrailFlagsRecord> & BaseSystemFields<Texpand>
|
||||
export type TrailMarkersResponse<Texpand = unknown> = Required<TrailMarkersRecord> & BaseSystemFields<Texpand>
|
||||
export type TrailVersionsResponse<Tbounds = unknown, Televation = unknown, Tgeojson = unknown, Texpand = unknown> = Required<TrailVersionsRecord<Tbounds, Televation, Tgeojson>> & BaseSystemFields<Texpand>
|
||||
export type TrailsResponse<Texpand = unknown> = Required<TrailsRecord> & BaseSystemFields<Texpand>
|
||||
export type UsersResponse<Texpand = unknown> = Required<UsersRecord> & AuthSystemFields<Texpand>
|
||||
|
||||
// Types containing all Records and Responses, useful for creating typing helper functions
|
||||
|
|
@ -143,6 +227,11 @@ export type CollectionRecords = {
|
|||
runs: RunsRecord
|
||||
teams: TeamsRecord
|
||||
times: TimesRecord
|
||||
trail_comments: TrailCommentsRecord
|
||||
trail_flags: TrailFlagsRecord
|
||||
trail_markers: TrailMarkersRecord
|
||||
trail_versions: TrailVersionsRecord
|
||||
trails: TrailsRecord
|
||||
users: UsersRecord
|
||||
}
|
||||
|
||||
|
|
@ -152,6 +241,11 @@ export type CollectionResponses = {
|
|||
runs: RunsResponse
|
||||
teams: TeamsResponse
|
||||
times: TimesResponse
|
||||
trail_comments: TrailCommentsResponse
|
||||
trail_flags: TrailFlagsResponse
|
||||
trail_markers: TrailMarkersResponse
|
||||
trail_versions: TrailVersionsResponse
|
||||
trails: TrailsResponse
|
||||
users: UsersResponse
|
||||
}
|
||||
|
||||
|
|
@ -164,5 +258,10 @@ export type TypedPocketBase = PocketBase & {
|
|||
collection(idOrName: 'runs'): RecordService<RunsResponse>
|
||||
collection(idOrName: 'teams'): RecordService<TeamsResponse>
|
||||
collection(idOrName: 'times'): RecordService<TimesResponse>
|
||||
collection(idOrName: 'trail_comments'): RecordService<TrailCommentsResponse>
|
||||
collection(idOrName: 'trail_flags'): RecordService<TrailFlagsResponse>
|
||||
collection(idOrName: 'trail_markers'): RecordService<TrailMarkersResponse>
|
||||
collection(idOrName: 'trail_versions'): RecordService<TrailVersionsResponse>
|
||||
collection(idOrName: 'trails'): RecordService<TrailsResponse>
|
||||
collection(idOrName: 'users'): RecordService<UsersResponse>
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue