Initial commit
CI / test (push) Canceled after 0s

This commit is contained in:
2026-09-12 22:22:17 +02:00
commit 904d14b64c
314 changed files with 31884 additions and 0 deletions
@@ -0,0 +1,8 @@
{{define "title"}}Benutzerkonto{{end}}
{{define "main"}}
<header class='page-heading'><div><p class='eyebrow'>Persönlich</p><h2>Benutzerkonto</h2></div></header>{{$form:=.Form}}{{with $form.Message}}<p class='form-message'>{{.}}</p>{{end}}
<div class='account-grid'><section class='panel'><h3>Profil</h3><form method='POST' action='{{with .Garden}}{{pathWithQuery (webPath "account.profile") "garden" .ID}}{{else}}{{webPath "account.profile"}}{{end}}'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><label for='name'>Name</label><input id='name' name='name' value='{{$form.Name}}' required>{{with index $form.Errors "name"}}<p class='field-error'>{{.}}</p>{{end}}<label for='color'>Farbe für deine Einträge</label><input id='color' name='color' type='color' value='{{$form.Color}}' required><p class='muted'>Diese Farbe wird verwendet, um deine Tagebucheinträge schnell zu erkennen.</p>{{with index $form.Errors "color"}}<p class='field-error'>{{.}}</p>{{end}}<button>Profil speichern</button></form></section>
<section class='panel'><h3>E-Mail-Adresse</h3><p>Aktuell: {{.CurrentUser.Email}}</p><form method='POST' action='{{with .Garden}}{{pathWithQuery (webPath "account.email") "garden" .ID}}{{else}}{{webPath "account.email"}}{{end}}'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><label for='email'>Neue E-Mail-Adresse</label><input id='email' type='email' name='email' value='{{$form.Email}}' required><label for='email-password'>Aktuelles Passwort</label><input id='email-password' type='password' name='current_password' required>{{with index $form.Errors "email"}}<p class='field-error'>{{.}}</p>{{end}}{{with index $form.Errors "current_password"}}<p class='field-error'>{{.}}</p>{{end}}<button>Bestätigung senden</button></form></section>
<section class='panel'><h3>Passwort</h3><form method='POST' action='{{with .Garden}}{{pathWithQuery (webPath "account.password") "garden" .ID}}{{else}}{{webPath "account.password"}}{{end}}'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><label for='current-password'>Aktuelles Passwort</label><input id='current-password' type='password' name='current_password' required><label for='new-password'>Neues Passwort</label><input id='new-password' type='password' name='new_password' minlength='8' required>{{with index $form.Errors "new_password"}}<p class='field-error'>{{.}}</p>{{end}}<label for='confirm-password'>Neues Passwort bestätigen</label><input id='confirm-password' type='password' name='new_password_confirmation' required>{{with index $form.Errors "new_password_confirmation"}}<p class='field-error'>{{.}}</p>{{end}}{{with index $form.Errors "current_password"}}<p class='field-error'>{{.}}</p>{{end}}<button>Passwort ändern</button></form></section>
<section class='panel'><h3>Aktive Sitzungen</h3>{{if .AccountSessions}}{{range .AccountSessions}}{{$session := .}}<div class='member-row'><div><strong>{{if .Current}}Dieses Gerät{{else}}Weitere Sitzung{{end}}</strong><br><span class='muted'>Angemeldet: {{humanDate .CreatedAt}} · gültig bis {{humanDate .ExpiresAt}}</span></div><form action='{{with $.Garden}}{{pathWithQuery (webPath "account.session.delete" $session.ID) "garden" .ID}}{{else}}{{webPath "account.session.delete" .ID}}{{end}}' method='POST'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><button class='danger'>Widerrufen</button></form></div>{{end}}{{else}}<p>Keine aktiven Sitzungen gefunden.</p>{{end}}</section></div>
{{end}}
@@ -0,0 +1,28 @@
{{define "title"}}Account aktivieren{{end}}
{{define "main"}}
<section class='panel narrow'>
<h2>Account aktivieren</h2>
<p>Gib den Aktivierungstoken aus deiner E-Mail ein, um deinen Account freizuschalten.</p>
{{$form := .Form}}
{{with $form.Message}}<p class='form-message error'>{{.}}</p>{{end}}
<form action='{{webPath "activate"}}' method='POST'>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<label for='token'>Aktivierungstoken</label>
<input id='token' name='token' type='text' value='{{$form.Token}}' autocomplete='one-time-code' autocapitalize='characters' spellcheck='false' required>
{{with index $form.Errors "token"}}<p class='field-error'>{{.}}</p>{{end}}
{{if $form.SetPassword}}
<input type='hidden' name='set_password' value='true'>
<label for='password'>Passwort festlegen</label>
<input id='password' name='password' type='password' minlength='8' maxlength='72' autocomplete='new-password' required>
{{with index $form.Errors "password"}}<p class='field-error'>{{.}}</p>{{end}}
<label for='password-confirm'>Passwort wiederholen</label>
<input id='password-confirm' name='password_confirm' type='password' minlength='8' maxlength='72' autocomplete='new-password' required>
{{with index $form.Errors "password_confirm"}}<p class='field-error'>{{.}}</p>{{end}}
{{end}}
<button type='submit'>Account aktivieren</button>
</form>
</section>
{{end}}
+120
View File
@@ -0,0 +1,120 @@
{{define "title"}}Administration{{end}}
{{define "main"}}
<header class='page-heading'><div><p class='eyebrow'>Administration</p><h2>Anwendung verwalten</h2></div></header>
<div class='settings-layout'>
{{template "settings_nav" (dict "Kind" "admin")}}
<div class='settings-content'>
{{with .ApplicationSettings}}
<section class='panel' id='lifecycle-settings'>
<h3>Pflanzenlebensdauer</h3>
<p>Ein- und zweijährige Pflanzen können am globalen Stichtag automatisch als entfernt markiert werden. Mehrjährige Pflanzen sowie Pflanzen ohne Pflanzdatum bleiben unverändert.</p>
<form method='POST' action='{{gardenAwarePath (webPath "admin.application-settings") $.Garden}}'>
<input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'>
<label class='checkbox'><input type='checkbox' name='lifecycle_status_enabled' value='true' {{if .LifecycleStatusEnabled}}checked{{end}}> Automatische Statusänderung aktivieren</label>
<div class='form-grid'>
<label for='lifecycle-removal-day'>Tag<input id='lifecycle-removal-day' name='lifecycle_removal_day' type='number' min='1' max='31' value='{{.LifecycleRemovalDay}}' required></label>
<label for='lifecycle-removal-month'>Monat<select id='lifecycle-removal-month' name='lifecycle_removal_month'>{{range months}}<option value='{{.Value}}' {{if eqInt $.ApplicationSettings.LifecycleRemovalMonth .Value}}selected{{end}}>{{.Label}}</option>{{end}}</select></label>
</div>
<label for='lifecycle-timezone'>Zeitzone</label><input id='lifecycle-timezone' name='timezone' value='{{.Timezone}}' required>
<p class='muted'>Als Ausgangspunkt dient „Erworben oder gepflanzt am“. Offene Aufgaben und aktive Standortzuordnungen werden beim Entfernen ebenfalls beendet.</p>
<button type='submit'>Einstellungen speichern</button>
</form>
</section>
{{end}}
<section class='panel' id='mail-settings'>
<h3>Mailversand testen</h3>
<p>Sendet über die aktuell konfigurierte Versandart eine Testmail an eine beliebige gültige E-Mail-Adresse.</p>
<form method='POST' action='{{gardenAwarePath (webPath "admin.test-mail") .Garden}}'>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<label for='test-mail-address'>Empfängeradresse</label>
<input id='test-mail-address' name='email' type='email' autocomplete='email' required>
<button type='submit'>Testmail senden</button>
</form>
</section>
<section class='panel' id='environment-settings'>
<h3>Umgebungsvariablen</h3>
<p>Wirksame Konfigurationswerte der API und Webanwendung. Sensible Werte werden ausschließlich maskiert angezeigt.</p>
<div class='environment-list'>
{{range .EnvironmentVariables}}
<div class='environment-row'>
<span class='status'>{{.Component}}</span>
<code>{{.Name}}</code>
<code>{{if .Value}}{{.Value}}{{else}}(leer){{end}}</code>
</div>
{{else}}<p>Keine Umgebungsvariablen verfügbar.</p>{{end}}
</div>
</section>
<section class='panel' id='users-settings'>
<h3>Nutzer und Rechte</h3>
<p>Lade neue Nutzer per E-Mail ein und verwalte anschließend ihre anwendungsweiten Rollen.</p>
<form class='admin-user-invite' method='POST' action='{{gardenAwarePath (webPath "admin.user.invite") .Garden}}'>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<label for='invite-user-name'>Name<input id='invite-user-name' name='name' autocomplete='name' required></label>
<label for='invite-user-email'>E-Mail-Adresse<input id='invite-user-email' name='email' type='email' autocomplete='email' required></label>
<button type='submit'>Nutzer einladen</button>
</form>
{{range .AdminUsers}}{{$user := .}}<article class='member-row'><div><strong>{{.Name}}</strong><br><span>{{.Email}}</span></div>
{{if neInt .ID $.CurrentUser.ID}}<div class='admin-user-actions'><form method='POST' action='{{gardenAwarePath (webPath "admin.user.role" .ID) $.Garden}}'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><label for='role-{{.ID}}'>Rolle</label><select id='role-{{.ID}}' name='role'>{{range $.ApplicationRoles}}<option value='{{.Name}}' {{if eqString .Name $user.Role}}selected{{end}}>{{.Label}}</option>{{end}}</select><button>Speichern</button></form><details class='delete-user'><summary>Nutzer löschen</summary><form method='POST' action='{{gardenAwarePath (webPath "admin.user.delete" .ID) $.Garden}}'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><p>Kontodaten anonymisieren und alle Zugriffe entfernen?</p><button type='submit' class='danger'>Endgültig löschen</button></form></details></div>{{else}}<span class='status'>Eigenes Konto · {{.Role}}</span>{{end}}</article>{{else}}<p>Keine Nutzer vorhanden.</p>{{end}}
</section>
{{range .RoleEditors}}{{template "role_editor" .}}{{end}}
<section class='panel admin-categories' id='species-categories'>
<h3>Artenkategorien</h3>
<p>Kategorien stehen anschließend in allen Artenformularen zur Auswahl.</p>
{{range .SpeciesCategories}}
<article class='admin-category-row'>
<form method='POST' action='{{gardenAwarePath (webPath "admin.species-category.edit" .ID) $.Garden}}'>
<input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'>
<label for='category-name-{{.ID}}'>Name</label><input id='category-name-{{.ID}}' name='name' value='{{.Name}}' required>
<label for='category-order-{{.ID}}'>Reihenfolge</label><input id='category-order-{{.ID}}' name='sort_order' type='number' value='{{.SortOrder}}'>
<label for='category-lifecycle-{{.ID}}'>Lebensdauer</label><select id='category-lifecycle-{{.ID}}' name='lifecycle'><option value=''>Nicht festgelegt</option><option value='annual' {{if eqString (stringValue .Lifecycle) "annual"}}selected{{end}}>Einjährig</option><option value='biennial' {{if eqString (stringValue .Lifecycle) "biennial"}}selected{{end}}>Zweijährig</option><option value='perennial' {{if eqString (stringValue .Lifecycle) "perennial"}}selected{{end}}>Mehrjährig</option></select>
<label class='checkbox'><input type='checkbox' name='active' value='true' {{if .Active}}checked{{end}}> Aktiv</label>
<button type='submit'>Speichern</button>
</form>
{{if .Active}}<form method='POST' action='{{gardenAwarePath (webPath "admin.species-category.delete" .ID) $.Garden}}'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><button type='submit' class='link-button danger-text'>Deaktivieren</button></form>{{end}}
</article>
{{else}}<p>Noch keine Kategorien vorhanden.</p>{{end}}
<form class='admin-category-create' method='POST' action='{{gardenAwarePath (webPath "admin.species-category.new") .Garden}}'>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<h4>Neue Kategorie</h4>
<label for='new-category-name'>Name</label><input id='new-category-name' name='name' required>
<label for='new-category-order'>Reihenfolge</label><input id='new-category-order' name='sort_order' type='number' value='0'>
<label for='new-category-lifecycle'>Lebensdauer</label><select id='new-category-lifecycle' name='lifecycle'><option value=''>Nicht festgelegt</option><option value='annual'>Einjährig</option><option value='biennial'>Zweijährig</option><option value='perennial'>Mehrjährig</option></select>
<button type='submit'>Kategorie anlegen</button>
</form>
</section>
<section class='panel admin-categories' id='task-priorities'>
<h3>Aufgabenprioritäten</h3>
<p>Diese Prioritäten werden einheitlich für Aufgaben und Aufgabenvorlagen verwendet.</p>
{{range .TaskPriorities}}
<article class='admin-category-row'>
<form method='POST' action='{{gardenAwarePath (webPath "admin.task-priority.edit" .ID) $.Garden}}'>
<input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'>
<label for='priority-name-{{.ID}}'>Name</label><input id='priority-name-{{.ID}}' name='name' value='{{.Name}}' required>
<label for='priority-value-{{.ID}}'>Wert</label><input id='priority-value-{{.ID}}' name='value' type='number' min='-100' max='100' value='{{.Value}}'>
<label for='priority-order-{{.ID}}'>Reihenfolge</label><input id='priority-order-{{.ID}}' name='sort_order' type='number' value='{{.SortOrder}}'>
<label class='checkbox'><input type='checkbox' name='active' value='true' {{if .Active}}checked{{end}}> Aktiv</label>
<button type='submit'>Speichern</button>
</form>
{{if .Active}}<form method='POST' action='{{gardenAwarePath (webPath "admin.task-priority.delete" .ID) $.Garden}}'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><button type='submit' class='link-button danger-text'>Deaktivieren</button></form>{{end}}
</article>
{{else}}<p>Noch keine Prioritäten vorhanden.</p>{{end}}
<form class='admin-category-create' method='POST' action='{{gardenAwarePath (webPath "admin.task-priority.new") .Garden}}'>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<h4>Neue Priorität</h4>
<label for='new-priority-name'>Name</label><input id='new-priority-name' name='name' required>
<label for='new-priority-value'>Wert</label><input id='new-priority-value' name='value' type='number' min='-100' max='100' value='0'>
<label for='new-priority-order'>Reihenfolge</label><input id='new-priority-order' name='sort_order' type='number' value='0'>
<button type='submit'>Priorität anlegen</button>
</form>
</section>
</div>
</div>
{{end}}
@@ -0,0 +1,7 @@
{{define "title"}}Übersicht{{end}}
{{define "main"}}
<header class='page-heading'><div><h2>{{.Garden.Name}}</h2></div><div class='actions'><a class='button secondary' href='{{webPath "tasks.calendar" .Garden.ID}}'>Kalender</a>{{if or (canGarden .Garden "garden:update") (canGarden .Garden "members:write") (canGarden .Garden "garden:delete")}}<a class='button' href='{{webPath "garden.edit" .Garden.ID}}'>Bearbeiten</a>{{end}}{{template "view_toggle" "dashboard"}}</div></header>
{{if or .Garden.ImageData .Garden.Description}}<section class='card garden-summary {{if .Garden.ImageData}}card--image{{end}}' {{if .Garden.ImageData}}data-background-image='{{.Garden.ImageData}}'{{end}}>{{with .Garden.Description}}<div class='card-content'><p>{{.}}</p></div>{{end}}</section>{{end}}
<section class='summary-grid collection' data-view-key='dashboard'><a class='card' href='{{webPath "plants" .Garden.ID}}'><span class='summary-number'>{{len .Plants}}</span><h3>Im Garten</h3></a><a class='card' href='{{webPath "locations" .Garden.ID}}'><span class='summary-number'>{{len .Locations}}</span><h3>Orte</h3></a><a class='card' href='{{webPath "tasks" .Garden.ID}}'><span class='summary-number'>{{len .Tasks}}</span><h3>Aufgaben</h3></a><a class='card' href='{{webPath "garden.journal" .Garden.ID}}'><span class='summary-number'>{{len .JournalEntries}}</span><h3>Tagebuch</h3></a><a class='card' href='{{webPath "garden.pinboard" .Garden.ID}}'><span class='summary-number'>{{len .PinboardEntries}}</span><h3>Pinnwand</h3></a><a class='card' href='{{webPath "garden.images" .Garden.ID}}'><span class='summary-number'>{{.PhotoCount}}</span><h3>Fotos</h3></a></section>
<section><div class='page-heading'><h3>Jetzt und demnächst</h3><a href='{{webPath "tasks.calendar" .Garden.ID}}'>Wochen- und Monatsansicht</a></div>{{range .Tasks}}{{$editable := canGardenResource $.Garden $.CurrentUser .CreatedBy "tasks:update:own" "tasks:update:other"}}<article class='panel task-card{{if $editable}} card--interactive{{end}}'{{if $editable}} role='link' tabindex='0' data-card-href='{{webPath "task.edit" $.Garden.ID .ID}}' aria-label='{{.Title}} öffnen'{{end}}><div class='task-heading'><h3>{{.Title}}</h3><strong>{{taskDueDate .}}</strong></div>{{with .Description}}<p>{{.}}</p>{{end}}</article>{{else}}<p>In den nächsten 30 Tagen stehen keine Aufgaben an.</p>{{end}}</section>
{{end}}
@@ -0,0 +1 @@
{{define "title"}}E-Mail bestätigen{{end}}{{define "main"}}<section class='panel narrow'><p class='eyebrow'>Benutzerkonto</p><h2>Neue E-Mail-Adresse bestätigen</h2><form method='POST'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><input type='hidden' name='token' value='{{.Form.Token}}'><button>E-Mail-Adresse übernehmen</button></form></section>{{end}}
+9
View File
@@ -0,0 +1,9 @@
{{define "title"}}{{.ErrorTitle}}{{end}}
{{define "main"}}
<section class='panel narrow error-page'>
<p class='eyebrow'>Fehler {{.ErrorStatus}}</p>
<h2>{{.ErrorTitle}}</h2>
<p>{{.ErrorMessage}}</p>
<div class='actions'><button type='button' class='secondary' data-history-back>Zurück</button><a href='/'>Zur Startseite</a></div>
</section>
{{end}}
@@ -0,0 +1,56 @@
{{define "title"}}Garten bearbeiten{{end}}
{{define "main"}}
{{$garden := .Garden}}{{$current := .CurrentUser}}{{$forms := .Form}}
<header class='page-heading'><div><p class='eyebrow'>Garten</p><h2>Garten bearbeiten</h2></div></header>
<div class='settings-layout'>
<aside class='settings-sidebar panel'><nav aria-label='Bearbeitungsbereiche'>
{{if canGarden $garden "garden:update"}}<a href='#garden-general'>Allgemein</a>{{end}}
<a href='#garden-members'>Mitglieder</a>
{{if canGarden $garden "members:write"}}<a href='#garden-invites'>Einladungen</a>{{end}}
{{if canGarden $garden "garden:delete"}}<a href='#garden-role-editor'>Rollen</a><a href='#garden-delete'>Garten löschen</a>{{end}}
</nav></aside>
<div class='settings-content'>
{{if canGarden $garden "garden:update"}}
<section class='panel' id='garden-general'>
<h3>Allgemein</h3>
{{$form := $forms.Garden}}
<form method='POST' action='{{webPath "garden.edit" $garden.ID}}'>{{template "garden_fields" (dict "Form" $form "CSRFToken" .CSRFToken "Images" $.Images "GardenID" $garden.ID)}}<div class='actions'><button type='submit'>Speichern</button><a href='{{webPath "plants" $garden.ID}}'>Abbrechen</a></div></form>
</section>
{{end}}
<section class='panel member-list' id='garden-members'>
<h3>Mitglieder</h3>
{{range .Members}}{{$member := .}}
<article class='member-row'><div><strong>{{.Name}}</strong><br><span>{{.Email}} · {{.Role}}</span></div>
{{if and (canGarden $garden "members:write") (neInt .UserID $current.ID) (neString .Role "owner")}}
<div class='actions'>
<form method='POST' action='{{webPath "garden.member.role" $garden.ID .UserID}}'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><select name='role'>{{range $.GardenRoles}}{{if neString .Name "owner"}}<option value='{{.Name}}' {{if eqString .Name $member.Role}}selected{{end}}>{{.Label}}</option>{{end}}{{end}}</select><button>Rolle speichern</button></form>
<form method='POST' action='{{webPath "garden.member.delete" $garden.ID .UserID}}'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><button class='danger'>Entfernen</button></form>
{{if and (canGarden $garden "garden:delete") (neInt .UserID $current.ID)}}<form method='POST' action='{{webPath "garden.member.transfer" $garden.ID .UserID}}'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><button class='secondary'>Eigentum übertragen</button></form>{{end}}
</div>{{end}}
</article>{{else}}<p>Keine Mitglieder vorhanden.</p>{{end}}
</section>
{{if canGarden $garden "members:write"}}
<section class='panel' id='garden-invites'><h3>Mitglied einladen</h3>{{$invite := $forms.Invite}}<form method='POST' action='{{webPath "garden.invite.new" $garden.ID}}'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><label for='email'>E-Mail-Adresse</label><input id='email' name='email' type='email' value='{{$invite.Email}}' required>{{with index $invite.Errors "email"}}<p class='field-error'>{{.}}</p>{{end}}<label for='role'>Rolle</label><select id='role' name='role'>{{range .GardenRoles}}{{if neString .Name "owner"}}<option value='{{.Name}}' {{if eqString .Name $invite.Role}}selected{{end}}>{{.Label}}</option>{{end}}{{end}}</select>{{with index $invite.Errors "role"}}<p class='field-error'>{{.}}</p>{{end}}<button>Einladung senden</button></form></section>
<section class='panel'><h3>Offene Einladungen</h3>{{range .Invites}}<div class='member-row'><span>{{.Email}} · {{.Role}} · bis {{humanDate .ExpiresAt}}</span><form method='POST' action='{{webPath "garden.invite.delete" $garden.ID .ID}}'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><button class='link-button danger-text'>Widerrufen</button></form></div>{{else}}<p>Keine offenen Einladungen.</p>{{end}}</section>
{{end}}
{{if canGarden $garden "garden:delete"}}{{range .RoleEditors}}{{template "role_editor" .}}{{end}}{{end}}
{{if canGarden $garden "garden:delete"}}
<section class='panel danger-panel' id='garden-delete'>
<div><h3>Garten löschen</h3><p>Der Garten und alle zugehörigen Inhalte werden dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.</p></div>
<button type='button' class='danger' aria-haspopup='dialog' aria-controls='delete-garden-dialog' data-open-dialog='delete-garden-dialog'>Garten löschen</button>
</section>
<dialog id='delete-garden-dialog' class='confirm-dialog' aria-labelledby='delete-garden-title'>
<form method='POST' action='{{webPath "garden.delete" $garden.ID}}'>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<h3 id='delete-garden-title'>„{{$garden.Name}}“ wirklich löschen?</h3>
<p>Alle Pflanzen, Orte, Aufgaben, Tagebucheinträge und Mitgliedschaften dieses Gartens werden unwiderruflich gelöscht.</p>
<div class='actions'><button type='button' class='secondary' data-cancel-dialog>Abbrechen</button><button type='submit' class='danger'>Endgültig löschen</button></div>
</form>
</dialog>
{{end}}
</div></div>
{{end}}
@@ -0,0 +1,16 @@
{{define "title"}}Garten anlegen{{end}}
{{define "main"}}
<section class='panel narrow'>
<p class='eyebrow'>Garten</p>
<h2>Garten anlegen</h2>
{{$form := .Form}}
<form method='POST'>
{{template "garden_fields" (dict "Form" $form "CSRFToken" .CSRFToken "Images" .Images "GardenID" 0 "Autofocus" true)}}
<div class='actions'>
<button type='submit'>Garten anlegen</button>
<a href='{{webPath "gardens"}}'>Abbrechen</a>
</div>
</form>
</section>
{{end}}
+22
View File
@@ -0,0 +1,22 @@
{{define "title"}}Gärten{{end}}
{{define "main"}}
<header class='page-heading'>
<div>
<p class='eyebrow'>Übersicht</p>
<h2>Deine Gärten</h2>
</div>
<div class='actions'>{{if canUser .CurrentUser "gardens:create"}}<a class='button' href='{{webPath "garden.new"}}'>Garten anlegen</a>{{end}}{{template "view_toggle" "gardens"}}</div>
</header>
<form class='panel filter-bar' method='GET'><div class='filter-fields'><div><label for='q'>Suche</label><input id='q' name='q' type='search' value='{{index .Filters "q"}}' placeholder='Name oder Beschreibung'></div><div><label for='role'>Rolle</label><select id='role' name='role'><option value=''>Alle</option><option value='owner' {{if eqString (index .Filters "role") "owner"}}selected{{end}}>Eigentümer</option><option value='admin' {{if eqString (index .Filters "role") "admin"}}selected{{end}}>Administration</option><option value='member' {{if eqString (index .Filters "role") "member"}}selected{{end}}>Mitglied</option><option value='viewer' {{if eqString (index .Filters "role") "viewer"}}selected{{end}}>Nur Lesen</option></select></div><div><label for='sort'>Sortierung</label><select id='sort' name='sort'><option value='name_asc'>Name AZ</option><option value='name_desc' {{if eqString (index .Filters "sort") "name_desc"}}selected{{end}}>Name ZA</option><option value='newest' {{if eqString (index .Filters "sort") "newest"}}selected{{end}}>Neueste zuerst</option><option value='oldest' {{if eqString (index .Filters "sort") "oldest"}}selected{{end}}>Älteste zuerst</option></select></div><button class='filter-submit'>Filtern</button></div></form>
<section class='card-grid collection' data-view-key='gardens'>
{{range .Gardens}}
<article class='card card--clickable card--interactive {{if .ImageData}}card--image{{end}}' {{if .ImageData}}data-background-image='{{.ImageData}}'{{end}} role='link' tabindex='0' data-card-href='{{webPath "garden.dashboard" .ID}}'><div class='card-content'><h3><a class='card-link' href='{{webPath "garden.dashboard" .ID}}'>{{.Name}}</a></h3>{{with .Description}}<p>{{.}}</p>{{end}}</div></article>
{{else}}
<p>Noch kein Garten vorhanden.</p>
{{end}}
</section>
{{template "pagination" .}}
{{end}}
@@ -0,0 +1,19 @@
{{define "title"}}Systemstatus{{end}}
{{define "main"}}
<header class='page-heading'><div><p class='eyebrow'>System</p><h2>Status</h2></div></header>
<section class='panel narrow health-status'>
{{with .Health}}
<dl>
<div><dt>Status</dt><dd><span class='status'>{{.Status}}</span></dd></div>
<div><dt>Umgebung</dt><dd>{{.SystemInfo.Environment}}</dd></div>
<div><dt>API-Version</dt><dd><code>{{.SystemInfo.Version}}</code></dd></div>
<div><dt>Serverzeit</dt><dd><time datetime='{{.ServerTime.Format "2006-01-02T15:04:05Z07:00"}}'>{{.ServerTime.Local.Format "02.01.2006 15:04:05 MST"}}</time></dd></div>
<div><dt>Systemzeit</dt><dd><time datetime='{{$.SystemTime.Format "2006-01-02T15:04:05Z07:00"}}'>{{$.SystemTime.Format "02.01.2006 15:04:05 MST"}}</time></dd></div>
<div><dt>Web-Version</dt><dd><code>{{$.WebVersion}}</code></dd></div>
</dl>
{{else}}
<p class='field-error'>{{.HealthError}}</p>
<dl><div><dt>Systemzeit</dt><dd><time datetime='{{.SystemTime.Format "2006-01-02T15:04:05Z07:00"}}'>{{.SystemTime.Format "02.01.2006 15:04:05 MST"}}</time></dd></div><div><dt>Web-Version</dt><dd><code>{{.WebVersion}}</code></dd></div></dl>
{{end}}
</section>
{{end}}
+10
View File
@@ -0,0 +1,10 @@
{{define "title"}}Home{{end}}
{{define "main"}}
<section class='hero'>
<p class='eyebrow'>Dein Garten. Klar organisiert.</p>
<h2>Pflanzenwissen und Gartenarbeit an einem Ort.</h2>
<p>Verwalte Gärten, Pflanzen, Orte und Aufgaben gemeinsam mit anderen.</p>
<a class='button' href='{{webPath "login"}}'>Jetzt anmelden</a>
</section>
{{end}}
+7
View File
@@ -0,0 +1,7 @@
{{define "title"}}Bilder{{end}}
{{define "main"}}
<header class='page-heading'><div><h2>Bilder</h2><p class='muted'>Alle Fotos dieses Gartens an einem Ort.</p></div></header>
<form class='panel filter-bar' method='GET'><div class='filter-fields'><div><label for='q'>Suche</label><input id='q' name='q' type='search' value='{{index .Filters "q"}}' placeholder='Dateiname'></div><div><label for='source'>Herkunft</label><select id='source' name='source'><option value=''>Alle</option><option value='plant' {{if eqString (index .Filters "source") "plant"}}selected{{end}}>Pflanzen</option><option value='species' {{if eqString (index .Filters "source") "species"}}selected{{end}}>Arten</option><option value='location' {{if eqString (index .Filters "source") "location"}}selected{{end}}>Orte</option><option value='garden' {{if eqString (index .Filters "source") "garden"}}selected{{end}}>Garten</option><option value='journal' {{if eqString (index .Filters "source") "journal"}}selected{{end}}>Tagebuch</option><option value='pinboard' {{if eqString (index .Filters "source") "pinboard"}}selected{{end}}>Pinnwand</option><option value='migration' {{if eqString (index .Filters "source") "migration"}}selected{{end}}>Übernommen</option></select></div><button class='filter-submit'>Filtern</button></div></form>
<section class='image-library-grid'>{{range .Images}}<figure class='panel image-library-item'><img src='/g/{{$.Garden.ID}}/images/{{.ID}}/data' alt='{{if .FileName}}{{.FileName}}{{else}}Gartenfoto{{end}}' loading='lazy'><figcaption>{{if .FileName}}{{.FileName}} · {{end}}{{fileSize .Size}} · {{.CreatedAt.Format "02.01.2006"}}</figcaption></figure>{{else}}<p>Noch keine Bilder vorhanden.</p>{{end}}</section>
{{template "pagination" .}}
{{end}}
+2
View File
@@ -0,0 +1,2 @@
{{define "title"}}Einladung{{end}}
{{define "main"}}<section class='panel narrow'><p class='eyebrow'>Zusammenarbeit</p><h2>Garteneinladung annehmen</h2><p>Die Einladung wird mit deiner angemeldeten E-Mail-Adresse abgeglichen.</p><form method='POST'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><input type='hidden' name='token' value='{{.Form.Token}}'><button>Einladung annehmen</button></form></section>{{end}}
+17
View File
@@ -0,0 +1,17 @@
{{define "title"}}{{if eqString .ContentKind "pinboard"}}Pinnwand{{else}}Tagebuch{{end}}{{end}}
{{define "main"}}
{{$pinboard := eqString .ContentKind "pinboard"}}{{$path := "journal"}}{{if $pinboard}}{{$path = "pinboard"}}{{end}}
<header class='page-heading'><div><span class='eyebrow'>{{if $pinboard}}Ideen und Notizen{{else}}Gartentagebuch{{end}}</span><h2>{{if $pinboard}}Pinnwand{{else}}Tagebuch{{end}}</h2></div>{{if canGarden .Garden "content:write"}}<div class='actions'><a class='button' href='{{webPath (printf "%s.new" $path) .Garden.ID}}'>{{if $pinboard}}Notiz anlegen{{else}}Eintrag anlegen{{end}}</a></div>{{end}}</header>
{{$editable := canGarden .Garden "content:write"}}
<div class='journal-list{{if $pinboard}} pinboard-list{{end}}'>
{{range .JournalEntries}}
<article class='panel journal-entry{{if $editable}} card--clickable card--interactive{{end}}' id='entry-{{.ID}}' style='--author-color: {{.AuthorColor}}'{{if $editable}} role='link' tabindex='0' data-card-href='{{webPath (printf "%s.edit" $path) $.Garden.ID .ID}}' aria-label='{{if .Title}}{{.Title}}{{else}}Eintrag{{end}} bearbeiten'{{end}}>
<header><div>{{with .Title}}<h3>{{.}}</h3>{{end}}<p class='journal-meta'><time datetime='{{.CreatedAt.Format "2006-01-02T15:04:05Z07:00"}}'>{{journalDate .CreatedAt}}</time> · {{.AuthorName}}</p></div></header>
{{with .Tags}}<ul class='tag-list' aria-label='Schlagwörter'>{{range .}}<li>{{.}}</li>{{end}}</ul>{{end}}
<div class='markdown-body'>{{markdown .Body}}</div>
{{with .Attachments}}<div class='journal-media'>{{range .}}{{if hasPrefix .MediaType "image/"}}<figure><img src='/g/{{$.Garden.ID}}/{{$path}}/attachments/{{.EntryID}}/{{.ID}}' alt='{{.FileName}}' loading='lazy'><figcaption>{{.FileName}} · {{fileSize .Size}}</figcaption></figure>{{else if hasPrefix .MediaType "video/"}}<figure><video controls preload='metadata' src='/g/{{$.Garden.ID}}/{{$path}}/attachments/{{.EntryID}}/{{.ID}}'></video><figcaption>{{.FileName}} · {{fileSize .Size}}</figcaption></figure>{{else if hasPrefix .MediaType "audio/"}}<figure><audio controls preload='metadata' src='/g/{{$.Garden.ID}}/{{$path}}/attachments/{{.EntryID}}/{{.ID}}'></audio><figcaption>{{.FileName}} · {{fileSize .Size}}</figcaption></figure>{{end}}{{end}}</div>{{end}}
</article>
{{else}}<div class='panel'><p>{{if $pinboard}}Noch keine Notizen. Sammle hier spontane Ideen für deinen Garten.{{else}}Noch keine Tagebucheinträge. Halte fest, was in deinem Garten passiert.{{end}}</p></div>{{end}}
</div>
{{template "pagination" .}}
{{end}}
@@ -0,0 +1,32 @@
{{define "title"}}{{if eqString .ContentKind "pinboard"}}{{if .EntryID}}Notiz bearbeiten{{else}}Neue Notiz{{end}}{{else}}{{if .EntryID}}Tagebucheintrag bearbeiten{{else}}Neuer Tagebucheintrag{{end}}{{end}}{{end}}
{{define "main"}}
{{$pinboard := eqString .ContentKind "pinboard"}}{{$path := "journal"}}{{if $pinboard}}{{$path = "pinboard"}}{{end}}{{$editable := canGarden .Garden "content:write"}}
<header class='page-heading'><div><span class='eyebrow'>{{if $pinboard}}Pinnwand{{else}}Gartentagebuch{{end}}</span><h2>{{if $pinboard}}{{if .EntryID}}Notiz bearbeiten{{else}}Neue Notiz{{end}}{{else}}{{if .EntryID}}Eintrag bearbeiten{{else}}Neuer Eintrag{{end}}{{end}}</h2></div></header>
{{$form := .Form}}
<form class='panel journal-form' method='POST' enctype='multipart/form-data' data-journal-editor data-entry-kind='{{.ContentKind}}'>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<label for='title'>Titel{{if $pinboard}} <span class='muted'>(optional)</span>{{end}}</label><input id='title' name='title' value='{{$form.Title}}' maxlength='500' {{if not $pinboard}}required{{end}}>{{with index $form.Errors "title"}}<p class='field-error'>{{.}}</p>{{end}}
{{if $pinboard}}<input type='hidden' name='date_time' value='{{$form.DateTime}}'>{{else}}<label for='date-time'>Datum und Uhrzeit</label><input id='date-time' type='datetime-local' name='date_time' value='{{$form.DateTime}}' required>{{with index $form.Errors "date_time"}}<p class='field-error'>{{.}}</p>{{end}}{{end}}
<label for='journal-body'>Text</label>
<div id='journal-editor'></div><textarea id='journal-body' name='body' rows='14' maxlength='100000' data-journal-body>{{$form.Body}}</textarea>{{with index $form.Errors "body"}}<p class='field-error'>{{.}}</p>{{end}}
<label for='keywords'>Tags</label><div class='tag-editor' data-tag-editor><div class='tag-bubbles' data-tag-bubbles></div><input id='keywords' name='keywords' value='{{$form.Keywords}}' placeholder='Tag eingeben …' autocomplete='off' data-tag-input><div class='tag-suggestions' data-tag-suggestions role='listbox' hidden>{{range $form.TagSuggestions}}<button type='button' data-tag-suggestion='{{.}}'>{{.}}</button>{{end}}</div></div><p class='muted'>Mit Enter oder Komma übernehmen. Bereits verwendete Tags werden vorgeschlagen.</p>
{{with $form.Attachments}}<fieldset><legend>Vorhandene Anhänge</legend><div class='attachment-checklist'>{{range .}}<div class='attachment-row'><a href='{{webPath (printf "%s.attachment" $path) $.Garden.ID $.EntryID .ID}}' download='{{.FileName}}'>{{.FileName}} ({{fileSize .Size}})</a><label class='attachment-remove' title='Anhang löschen'><input type='checkbox' name='remove_attachment' value='{{.ID}}'><span aria-hidden='true'>×</span><span class='sr-only'>{{.FileName}} löschen</span></label></div>{{end}}</div></fieldset>{{end}}
<fieldset class='journal-attachments'><legend>{{if $pinboard}}Fotos{{else}}Fotos, Videos und Audio{{end}}</legend>
<label class='button secondary file-picker'>{{if $pinboard}}Fotos auswählen{{else}}Mediendateien auswählen{{end}}<input type='file' name='attachments' accept='{{if $pinboard}}image/*{{else}}image/*,video/*,audio/*{{end}}' multiple data-journal-files></label>
{{template "journal_photo_editor"}}
{{if .Images}}<button type='button' class='secondary' data-open-dialog='journal-image-library'>Aus Bilderdatenbank</button>{{end}}
{{if not $pinboard}}<button type='button' class='secondary' data-video-record>Video aufnehmen</button>
<button type='button' class='secondary' data-audio-record>Audio aufnehmen</button>{{end}}
<input type='file' name='attachments' multiple hidden data-journal-generated-files>
<input type='file' name='embedded_images' accept='image/*' multiple hidden data-embedded-images>
<ul class='attachment-preview' data-attachment-preview></ul>
</fieldset>
{{if .Images}}<dialog id='journal-image-library' class='image-library-dialog'><h3>Bilder aus der Bilderdatenbank auswählen</h3><p class='muted'>Es können mehrere Bilder ausgewählt werden.</p><div class='image-picker-grid'>{{range .Images}}<label class='image-picker-item'><input type='checkbox' name='library_image_id' value='{{.ID}}' data-journal-library-image data-image-name='{{if .FileName}}{{.FileName}}{{else}}Bild vom {{.CreatedAt.Format "02.01.2006"}}{{end}}' data-image-size='{{.Size}}' {{if containsInt $form.LibraryImageIDs .ID}}checked{{end}}><img src='/g/{{$.Garden.ID}}/images/{{.ID}}/data' alt='{{if .FileName}}{{.FileName}}{{else}}Bild auswählen{{end}}' loading='lazy'><span>{{if .FileName}}{{.FileName}}{{else}}{{.CreatedAt.Format "02.01.2006"}}{{end}}</span></label>{{end}}</div><div class='image-actions'><button type='button' data-cancel-dialog>Auswahl übernehmen</button></div></dialog>{{end}}
<div class='actions'>{{if $editable}}<button>Speichern</button>{{end}}<a href='{{webPath (printf "garden.%s" $path) .Garden.ID}}'>{{if $editable}}Abbrechen{{else}}Zurück{{end}}</a></div>
</form>
{{if not $pinboard}}
<dialog class='camera-dialog' data-video-dialog><h3>Video aufnehmen</h3><video muted playsinline data-video-preview></video><p class='field-error' data-video-error aria-live='polite'></p><div class='image-actions'><button type='button' class='secondary' data-video-switch-camera>Kamera wechseln</button><button type='button' data-video-toggle>Aufnahme starten</button><button type='button' class='secondary' data-video-pause hidden>Pause</button><button type='button' data-video-apply hidden>Übernehmen</button><button type='button' class='secondary' data-video-cancel>Abbrechen</button></div></dialog>
<dialog class='camera-dialog' data-audio-dialog><h3>Audio aufnehmen</h3><div class='recording-indicator' data-audio-indicator data-recording-indicator hidden aria-label='Aufnahme läuft'><i></i><i></i><i></i><i></i><i></i></div><audio controls data-audio-preview hidden></audio><p class='muted' data-audio-status>Bereit zur Aufnahme.</p><p class='field-error' data-audio-error aria-live='polite'></p><div class='image-actions'><button type='button' data-audio-toggle>Aufnahme starten</button><button type='button' class='secondary' data-audio-pause hidden>Pause</button><button type='button' data-audio-apply hidden>Übernehmen</button><button type='button' class='secondary' data-audio-cancel>Abbrechen</button></div></dialog>
{{end}}
{{if and .EntryID $editable}}<form class='delete-form' method='POST' action='{{webPath (printf "%s.delete" $path) .Garden.ID .EntryID}}'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><button class='danger'>{{if $pinboard}}Notiz löschen{{else}}Eintrag löschen{{end}}</button></form>{{end}}
{{end}}
@@ -0,0 +1,42 @@
{{define "title"}}{{if .LocationID}}Ort bearbeiten{{else}}Ort anlegen{{end}}{{end}}
{{define "main"}}
{{$form := .Form}}
{{$canSave := canGarden .Garden "locations:create"}}{{if .LocationID}}{{$canSave = canGardenResource .Garden .CurrentUser .LocationCreatedBy "locations:update:own" "locations:update:other"}}{{end}}
<div class='{{if .LocationID}}settings-layout{{end}}'>
{{if .LocationID}}<aside class='settings-sidebar panel'><nav aria-label='Bearbeitungsbereiche'><a href='#location-general'>Allgemein</a><a href='#location-plants'>Pflanzen</a><a href='#location-history'>Historie</a></nav></aside>{{end}}
<div class='settings-content'><section class='panel location-detail' id='location-general'>
<h2>{{if .LocationID}}Ort bearbeiten{{else}}Ort anlegen{{end}}</h2>
<form id='location-form' action='{{if .LocationID}}{{webPath "location.edit" .Garden.ID .LocationID}}{{else}}{{webPath "location.new" .Garden.ID}}{{end}}' method='POST'>
{{template "location_fields" .}}
</form>
<div class='actions location-form-actions'>
{{if $canSave}}<button type='submit' form='location-form'>Speichern</button>{{end}}
<a href='{{$form.ReturnTo}}'>Abbrechen</a>
{{if and .LocationID (canGardenResource .Garden .CurrentUser .LocationCreatedBy "locations:delete:own" "locations:delete:other")}}<form class='inline-form' method='POST' action='{{webPath "location.delete" .Garden.ID .LocationID}}'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><button type='submit' class='danger'>Ort löschen</button></form>{{end}}
</div>
</section>
{{if .LocationID}}
<section class='panel location-detail-plants' id='location-plants'>
<div class='page-heading'><h3>Pflanzen an diesem Ort</h3><div class='actions'>{{template "view_toggle" "location-plants"}}</div></div>
<div class='location-plant-list' data-view-key='location-plants'>
{{range .LocationPlants}}
{{$canEditPlant := canGardenResource $.Garden $.CurrentUser .Plant.CreatedBy "plants:update:own" "plants:update:other"}}<article class='card location-plant-item {{if $canEditPlant}}card--interactive{{end}}' {{if $canEditPlant}}role='link' tabindex='0' data-card-href='{{webPath "plant.edit" $.Garden.ID .Plant.ID}}'{{end}}><div class='card-content'><h3>{{if $canEditPlant}}<a href='{{webPath "plant.edit" $.Garden.ID .Plant.ID}}'>{{.Plant.Name}}</a>{{else}}{{.Plant.Name}}{{end}}</h3><p>Anzahl: {{.Assignment.Quantity}}{{with .Assignment.PlantedAt}} · gepflanzt am {{humanDate .}}{{end}}</p>{{with .Assignment.Notes}}<p>{{.}}</p>{{end}}</div></article>
{{else}}
<p>Diesem Ort sind noch keine Pflanzen zugeordnet.</p>
{{end}}
</div>
</section>
<section class='panel location-history' id='location-history'>
<h3>Historie der letzten 3 Jahre</h3>
{{range .LocationHistory}}
<section class='location-history-year'><h4>{{.Year}}</h4><div class='card-grid collection'>
{{range .Plants}}
{{$canEditPlant := canGardenResource $.Garden $.CurrentUser .Plant.CreatedBy "plants:update:own" "plants:update:other"}}<article class='card {{if $canEditPlant}}card--interactive{{end}} {{if .Plant.ImageData}}card--image{{end}}' {{if .Plant.ImageData}}data-background-image='{{.Plant.ImageData}}'{{end}} {{if $canEditPlant}}role='link' tabindex='0' data-card-href='{{webPath "plant.edit" $.Garden.ID .Plant.ID}}'{{end}}><div class='card-content'><h3>{{if $canEditPlant}}<a href='{{webPath "plant.edit" $.Garden.ID .Plant.ID}}'>{{.Plant.Name}}</a>{{else}}{{.Plant.Name}}{{end}}</h3><p>{{plantStatusName .Plant.Status}}{{with .Assignment.RemovedAt}} · entfernt am {{humanDate .}}{{end}}</p>{{with .Assignment.Notes}}<p>{{.}}</p>{{end}}</div></article>
{{else}}<p class='muted'>Keine Pflanzen in diesem Jahr.</p>{{end}}
</div></section>
{{end}}
</section>
{{end}}
</div></div>
{{end}}
@@ -0,0 +1,39 @@
{{define "title"}}Orte{{end}}
{{define "location_nodes"}}
<ul class='location-tree'>
{{range .}}
<li>
<a class='location-row' href='{{webPath "location.view" .Location.GardenID .Location.ID}}'>
<div><h3>{{.Location.Name}}</h3>{{with .Location.Kind}}<span class='status'>{{.}}</span>{{end}}</div>
{{with .Location.Description}}<p>{{.}}</p>{{end}}
</a>
{{with .Children}}{{template "location_nodes" .}}{{end}}
</li>
{{end}}
</ul>
{{end}}
{{define "main"}}
<header class='page-heading'>
<div><h2>Orte</h2></div>
<div class='actions'>{{if canGarden .Garden "locations:create"}}<a class='button' href='{{webPath "location.new" .Garden.ID}}'>Ort anlegen</a>{{end}}{{template "view_toggle" "locations"}}</div>
</header>
<form class='panel filter-bar' method='GET'><div class='filter-fields'><div><label for='q'>Suche</label><input id='q' name='q' type='search' value='{{index .Filters "q"}}' placeholder='Name oder Beschreibung'></div><div><label for='kind'>Art des Orts</label><input id='kind' name='kind' value='{{index .Filters "kind"}}' placeholder='z. B. Beet'></div><div><label for='sort'>Sortierung</label><select id='sort' name='sort'><option value='name_asc'>Name AZ</option><option value='name_desc' {{if eqString (index .Filters "sort") "name_desc"}}selected{{end}}>Name ZA</option><option value='newest' {{if eqString (index .Filters "sort") "newest"}}selected{{end}}>Neueste zuerst</option><option value='oldest' {{if eqString (index .Filters "sort") "oldest"}}selected{{end}}>Älteste zuerst</option></select></div><button class='filter-submit'>Filtern</button></div></form>
{{if .LocationTree}}
<section class='locations-views' data-view-variants data-view-key='locations'>
<div class='panel location-panel' data-view-variant='list' hidden>{{template "location_nodes" .LocationTree}}</div>
<div class='card-grid' data-view-variant='grid'>
{{range .Locations}}
<article class='card card--clickable card--interactive {{if .ImageData}}card--image{{end}}' {{if .ImageData}}data-background-image='{{.ImageData}}'{{end}} role='link' tabindex='0' data-card-href='{{webPath "location.view" .GardenID .ID}}'><div class='card-content'>
<h3><a class='card-link' href='{{webPath "location.view" .GardenID .ID}}'>{{.Name}}</a></h3>
{{with .Kind}}<span class='status'>{{.}}</span>{{end}}{{with .Description}}<p>{{.}}</p>{{end}}
</div></article>
{{end}}
</div>
</section>
{{else}}
<p>In diesem Garten wurden noch keine Orte angelegt.</p>
{{end}}
{{template "pagination" .}}
{{end}}
+42
View File
@@ -0,0 +1,42 @@
{{define "title"}}Im Garten{{end}}
{{define "main"}}
<header class='page-heading'>
<div><h2>Im Garten</h2></div>
<div class='actions'>{{if canGarden .Garden "plants:create"}}<a class='button' href='{{webPath "plant.new" .Garden.ID}}'>Pflanze erfassen</a>{{end}}{{template "view_toggle" "plants"}}</div>
</header>
<form class='panel filter-bar' method='GET'><div class='filter-fields filter-fields--plants'><div><label for='q'>Suche</label><input id='q' name='q' type='search' value='{{index .Filters "q"}}' placeholder='Name oder Notiz'></div><div><label for='status'>Status</label><select id='status' name='status'><option value=''>Alle</option>{{range plantStatuses}}<option value='{{.Value}}' {{if eqString (index $.Filters "status") .Value}}selected{{end}}>{{.Label}}</option>{{end}}</select></div><div><label for='species'>Art oder Sorte</label><select id='species' name='species'><option value=''>Alle</option>{{range .Species}}<option value='{{.ID}}' {{if eqString (index $.Filters "species") (printf "%d" .ID)}}selected{{end}}>{{.CommonName}}{{with .Cultivar}} · {{.}}{{end}}</option>{{end}}</select></div><div><label for='location'>Ort</label><select id='location' name='location'><option value=''>Alle</option>{{range .Locations}}<option value='{{.ID}}' {{if eqString (index $.Filters "location") (printf "%d" .ID)}}selected{{end}}>{{.Name}}</option>{{end}}</select></div><div><label for='sort'>Sortierung</label><select id='sort' name='sort'><option value='name_asc'>Name AZ</option><option value='name_desc' {{if eqString (index .Filters "sort") "name_desc"}}selected{{end}}>Name ZA</option><option value='newest' {{if eqString (index .Filters "sort") "newest"}}selected{{end}}>Neueste zuerst</option><option value='oldest' {{if eqString (index .Filters "sort") "oldest"}}selected{{end}}>Älteste zuerst</option></select></div><button class='filter-submit'>Filtern</button></div></form>
<section class='card-grid collection' data-view-key='plants'>
{{range .Plants}}
{{$plant := .}}
<article class='card card--clickable plant-card card--interactive {{if .ImageData}}card--image{{end}}' {{if .ImageData}}data-background-image='{{.ImageData}}'{{end}} role='link' tabindex='0' data-card-href='{{webPath "plant.edit" $.Garden.ID .ID}}'><div class='card-content'>
<p class='status'>{{plantStatusName .Status}}</p>
<h3><a class='card-link' href='{{webPath "plant.edit" $.Garden.ID .ID}}'>{{.Name}}</a></h3>
<p>{{speciesName $.Species .SpeciesID}}</p>
{{with .PlantedByName}}<p class='muted'>Gepflanzt von {{.}}</p>{{end}}
<div class='plant-location-summary'>{{range index $.PlantLocations .ID}}<p><span>{{.Quantity}}</span><span>x</span><span>{{index $.LocationNames .LocationID}}</span></p>{{end}}</div>
{{with .Notes}}<p>{{.}}</p>{{end}}</div>
{{if canGardenResource $.Garden $.CurrentUser .CreatedBy "plants:update:own" "plants:update:other"}}
<details class='user-menu plant-card-menu card-action'>
<summary aria-label='Status von {{.Name}} ändern' title='Status ändern'><span class='burger-icon' aria-hidden='true'></span></summary>
<div class='user-menu-content'>
<span class='menu-heading'>Status ändern</span>
{{range plantStatuses}}
<form method='POST' action='{{webPath "plant.status" $.Garden.ID $plant.ID}}'>
<input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'>
<input type='hidden' name='status' value='{{.Value}}'>
<button type='submit' class='link-button' {{if eqString $plant.Status .Value}}disabled{{end}}>{{.Label}}</button>
</form>
{{end}}
</div>
</details>
{{end}}
</article>
{{else}}
<p>In diesem Garten wurden noch keine Pflanzen erfasst.</p>
{{end}}
</section>
{{template "pagination" .}}
{{end}}
@@ -0,0 +1,89 @@
{{define "title"}}{{if .PlantID}}Pflanze bearbeiten{{else}}Pflanze erfassen{{end}}{{end}}
{{define "main"}}
{{$canSave := canGarden .Garden "plants:create"}}{{if .PlantID}}{{$canSave = canGardenResource .Garden .CurrentUser .PlantCreatedBy "plants:update:own" "plants:update:other"}}{{end}}
<div class='{{if .PlantID}}settings-layout{{end}}'>
{{if .PlantID}}<aside class='settings-sidebar panel'><nav aria-label='Bearbeitungsbereiche'><a href='#plant-general'>Allgemein</a><a href='#plant-tasks'>Aufgaben</a></nav></aside>{{end}}
<div class='settings-content'><section class='panel plant-detail' id='plant-general'>
<h2>{{if .PlantID}}Pflanze bearbeiten{{else}}Pflanze erfassen{{end}}</h2>
{{$form := .Form}}
<form action='{{if .PlantID}}{{webPath "plant.edit" .Garden.ID .PlantID}}{{else}}{{webPath "plant.new" .Garden.ID}}{{end}}' method='POST'>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<label for='name'>Name</label>
<input id='name' name='name' value='{{$form.Name}}' required data-plant-name>
{{with index $form.Errors "name"}}<p class='field-error'>{{.}}</p>{{end}}
{{template "image_editor" (dict "ImageData" $form.ImageData "ImageID" $form.ImageID "Images" .Images "GardenID" .Garden.ID)}}
<label for='species_id'>Art oder Sorte</label>
<select id='species_id' name='species_id' data-plant-species hx-get='{{if .PlantID}}{{pathWithQuery (webPath "plant.template-tasks" .Garden.ID) "plant_id" .PlantID}}{{else}}{{webPath "plant.template-tasks" .Garden.ID}}{{end}}' hx-trigger='change' hx-target='#plant-template-tasks' hx-swap='outerHTML' hx-include='this'>
<option value='0' {{if eqInt $form.SpeciesID 0}}selected{{end}}>Ohne Artzuordnung</option>
{{range .Species}}<option value='{{.ID}}' data-plant-name-value='{{.CommonName}}{{with .Cultivar}} · {{.}}{{end}}' data-sun='{{stringValue .SunExposure}}' data-soil='{{stringValue .SoilCondition}}' data-reaction='{{stringValue .SoilReaction}}' {{if eqInt $form.SpeciesID .ID}}selected{{end}}>{{.CommonName}}{{with .Cultivar}} · {{.}}{{end}}</option>{{end}}
</select>
{{with index $form.Errors "species_id"}}<p class='field-error'>{{.}}</p>{{end}}
<fieldset class='assignment-fieldset'>
<legend>Orte und Anzahl</legend>
{{if not .PlantID}}{{$locationURL := pathWithQuery (webPath "location.new" .Garden.ID) "return_to" (webPath "plant.new" .Garden.ID)}}<a class='create-location-link' href='{{$locationURL}}' hx-get='{{$locationURL}}' hx-target='#location-dialog-host' hx-swap='innerHTML'>Ort direkt anlegen</a>{{end}}
<div class='assignment-list' data-assignment-list>
{{range $index, $assignment := $form.Assignments}}
<div class='assignment-row' data-assignment-row>
<input type='hidden' name='assignment_id' value='{{$assignment.AssignmentID}}'>
<div>
<label for='{{if eqInt $index 0}}location_id{{else}}location_id_{{$index}}{{end}}'>Ort</label>
<select id='{{if eqInt $index 0}}location_id{{else}}location_id_{{$index}}{{end}}' name='location_id' class='assignment-location'>
<option value='0' {{if eqInt $assignment.LocationID 0}}selected{{end}}>Noch keinem Ort zuordnen</option>
{{range $.Locations}}<option value='{{.ID}}' {{if eqInt $assignment.LocationID .ID}}selected{{end}}>{{.Name}}</option>{{end}}
</select>
</div>
<div class='quantity-field'>
<label for='quantity_{{$index}}'>Anzahl</label>
<input id='quantity_{{$index}}' name='quantity' type='number' min='1' value='{{$assignment.Quantity}}'>
{{with index $form.Errors (printf "quantity_%d" $index)}}<p class='field-error'>{{.}}</p>{{end}}
</div>
<button type='button' class='assignment-remove secondary' data-remove-assignment aria-label='Ort entfernen' title='Ort entfernen'>×</button>
</div>
{{end}}
</div>
<button type='button' class='icon-button assignment-add' data-add-assignment aria-label='Weiteren Ort hinzufügen' title='Weiteren Ort hinzufügen'></button>
</fieldset>
<template id='assignment-row-template'>
<div class='assignment-row' data-assignment-row>
<input type='hidden' name='assignment_id' value='0'>
<div><label>Ort</label><select name='location_id'><option value='0' selected>Noch keinem Ort zuordnen</option>{{range .Locations}}<option value='{{.ID}}'>{{.Name}}</option>{{end}}</select></div>
<div class='quantity-field'><label>Anzahl</label><input name='quantity' type='number' min='1' value='1'></div>
<button type='button' class='assignment-remove secondary' data-remove-assignment aria-label='Ort entfernen' title='Ort entfernen'>×</button>
</div>
</template>
<div hidden data-location-compatibilities>{{range .Locations}}<span data-location-id='{{.ID}}' data-sun='{{stringValue .SunExposure}}' data-soil='{{stringValue .SoilCondition}}' data-reaction='{{stringValue .SoilReaction}}'></span>{{end}}</div><div class='compatibility-warning' data-compatibility-warning role='status' hidden></div>
<label for='acquired_at'>Erworben oder gepflanzt am</label>
<input id='acquired_at' name='acquired_at' type='date' value='{{$form.AcquiredAt}}'>
{{with index $form.Errors "acquired_at"}}<p class='field-error'>{{.}}</p>{{end}}
<label for='status'>Status</label>
<select id='status' name='status'>
{{range plantStatuses}}<option value='{{.Value}}' {{if eqString $form.Status .Value}}selected{{end}}>{{.Label}}</option>{{end}}
</select>
{{template "tag_editor" .}}
<label for='notes'>Notizen</label>
<textarea id='notes' name='notes' rows='5'>{{$form.Notes}}</textarea>
{{with index $form.Errors "notes"}}<p class='field-error'>{{.}}</p>{{end}}
<fieldset class='plant-task-fieldset' id='plant-tasks'>
<legend>Aufgaben</legend>
{{template "plant_template_tasks" .}}
<div id='plant-manual-tasks' class='plant-task-list'>
{{range $form.Tasks}}{{template "plant_task_row_fragment" (plantTaskData $.Garden .)}}{{end}}
</div>
<button type='button' class='icon-button plant-task-add' hx-get='{{pathWithQuery (webPath "plant.task-row" .Garden.ID) "new" true "plant_id" .PlantID}}' hx-include='#name, #location_id' hx-target='#plant-task-dialog-host' hx-swap='innerHTML' aria-label='Weitere Aufgabe hinzufügen' title='Weitere Aufgabe hinzufügen'></button>
</fieldset>
<div class='actions'>{{if $canSave}}<button type='submit'>Speichern</button>{{end}}<a href='{{webPath "plants" .Garden.ID}}'>{{if $canSave}}Abbrechen{{else}}Zurück{{end}}</a></div>
</form>
<div id='location-dialog-host'></div>
<div id='plant-task-dialog-host'></div>
{{if and .PlantID (canGardenResource .Garden .CurrentUser .PlantCreatedBy "plants:delete:own" "plants:delete:other")}}<form class='delete-form' method='POST' action='{{webPath "plant.delete" .Garden.ID .PlantID}}'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><button type='submit' class='danger'>Pflanze löschen</button></form>{{end}}
</section>
</div></div>
{{end}}
@@ -0,0 +1,2 @@
{{define "title"}}{{if .LocationID}}Zuordnung bearbeiten{{else}}Ort zuordnen{{end}}{{end}}
{{define "main"}}{{$editable := canGarden .Garden "content:write"}}<section class='panel narrow'><h2>{{(index .Plants 0).Name}}: Ort {{if .LocationID}}bearbeiten{{else}}zuordnen{{end}}</h2>{{$form:=.Form}}<form method='POST'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><label for='location_id'>Ort</label><select id='location_id' name='location_id' required><option value='0'>Bitte wählen</option>{{range .Locations}}<option value='{{.ID}}' {{if eqInt $form.LocationID .ID}}selected{{end}}>{{.Name}}</option>{{end}}</select>{{with index $form.Errors "location_id"}}<p class='field-error'>{{.}}</p>{{end}}<label for='quantity'>Anzahl</label><input id='quantity' type='number' min='1' name='quantity' value='{{$form.Quantity}}' required><label for='planted_at'>Gepflanzt am</label><input id='planted_at' type='date' name='planted_at' value='{{$form.PlantedAt}}'><label for='notes'>Notizen</label><textarea id='notes' name='notes'>{{$form.Notes}}</textarea><div class='actions'>{{if $editable}}<button>Speichern</button>{{end}}<a href='{{webPath "plants" .Garden.ID}}'>{{if $editable}}Abbrechen{{else}}Zurück{{end}}</a></div></form></section>{{end}}
+15
View File
@@ -0,0 +1,15 @@
{{define "title"}}Datenschutz{{end}}
{{define "main"}}
<header class='page-heading'><div><p class='eyebrow'>Rechtliches</p><h2>Datenschutz</h2></div></header>
<article class='panel legal-content'>
<p>Diese Gardomatic-Instanz verarbeitet nur Daten, die für Benutzerkonten und die gemeinsame Gartenverwaltung benötigt werden.</p>
<h3>Verarbeitete Daten</h3>
<p>Dazu gehören Kontaktdaten des Benutzerkontos, Sitzungsdaten sowie die von Nutzern eingetragenen Garten-, Pflanzen-, Aufgaben-, Tagebuch- und Mediendaten. Serverprotokolle können außerdem technische Verbindungsdaten enthalten.</p>
<h3>Zweck und Weitergabe</h3>
<p>Die Daten werden zur Bereitstellung und Absicherung der Anwendung verarbeitet. Eine Weitergabe erfolgt nur an technisch notwendige Dienstleister des Instanzbetreibers oder wenn eine gesetzliche Verpflichtung besteht.</p>
<h3>Speicherdauer und Rechte</h3>
<p>Daten werden gelöscht oder anonymisiert, sobald sie für den Betrieb nicht mehr erforderlich sind. Betroffene Personen können beim Betreiber dieser Instanz Auskunft, Berichtigung, Löschung oder Einschränkung der Verarbeitung anfragen.</p>
<h3>Verantwortlicher</h3>
<p>Verantwortlich ist der Betreiber dieser Gardomatic-Instanz. Die konkreten Kontakt- und Hostingangaben sind vom Betreiber vor dem öffentlichen Einsatz zu ergänzen.</p>
</article>
{{end}}
+16
View File
@@ -0,0 +1,16 @@
{{define "title"}}Suche{{end}}
{{define "main"}}
<header class='page-heading'><div><h2>Suche</h2></div><div class='actions'>{{template "view_toggle" "search"}}</div></header>
<form class='search-form' method='GET'><label for='q'>Freitextsuche</label><div class='search-fields'><input id='q' name='q' type='search' value='{{.Search.Query}}' placeholder='z. B. Tomate, Rückschnitt oder #ernte'><button>Suchen</button></div><div class='search-fields'><label for='month'>Monat</label><select id='month' name='month'><option value=''>Alle Monate</option>{{range months}}<option value='{{.Value}}'{{if eq $.Search.Month .Value}} selected{{end}}>{{.Label}}</option>{{end}}</select><label for='year'>Jahr</label><select id='year' name='year'><option value=''>Alle Jahre</option>{{range .Search.Years}}<option value='{{.}}'{{if eq $.Search.Year .}} selected{{end}}>{{.}}</option>{{end}}</select><button name='mode' value='month'>Suchen</button></div><p class='muted'>Monat und Jahr beziehen sich bei Aufgaben auf das Start- oder Enddatum.</p></form>
{{if or .Search.Query .Search.Month .Search.Year}}
<nav class='search-tabs' aria-label='Suchkategorien'><a href='#search-all' data-search-tab='search-all' aria-current='page'>Alle <span>{{len .Search.All}}</span></a><a href='#search-journal' data-search-tab='search-journal'>Tagebuch <span>{{len .Search.Journal}}</span></a><a href='#search-pinboard' data-search-tab='search-pinboard'>Pinnwand <span>{{len .Search.Pinboard}}</span></a><a href='#search-plants' data-search-tab='search-plants'>Im Garten <span>{{len .Search.Plants}}</span></a><a href='#search-tasks' data-search-tab='search-tasks'>Aufgaben <span>{{len .Search.Tasks}}</span></a><a href='#search-tags' data-search-tab='search-tags'>Tags <span>{{len .Search.Tags}}</span></a><a href='#search-species' data-search-tab='search-species'>Pflanzen <span>{{len .Search.Species}}</span></a></nav>
<section id='search-all' class='search-results'><h3>Alle Treffer</h3>{{template "search_cards" .Search.All}}</section>
<section id='search-journal' class='search-results'><h3>Tagebuch</h3>{{template "search_cards" .Search.Journal}}</section>
<section id='search-pinboard' class='search-results'><h3>Pinnwand</h3>{{template "search_cards" .Search.Pinboard}}</section>
<section id='search-plants' class='search-results'><h3>Im Garten</h3>{{template "search_cards" .Search.Plants}}</section>
<section id='search-tasks' class='search-results'><h3>Aufgaben</h3>{{template "search_cards" .Search.Tasks}}</section>
<section id='search-tags' class='search-results'><h3>Tags</h3>{{template "search_cards" .Search.Tags}}</section>
<section id='search-species' class='search-results'><h3>Pflanzen und Pflanzzeiten</h3>{{template "search_cards" .Search.Species}}</section>
{{end}}
{{end}}
{{define "search_cards"}}<div class='search-card-list'>{{range .}}<a class='search-card' href='{{.URL}}'><span><strong>{{.Title}}</strong>{{with .Detail}}<span>{{.}}</span>{{end}}{{with .Meta}}<small>{{.}}</small>{{end}}</span><em>{{.Type}}</em></a>{{else}}<p>Keine Treffer.</p>{{end}}</div>{{end}}
@@ -0,0 +1,37 @@
{{define "title"}}Einstellungen{{end}}
{{define "main"}}
<header class='page-heading'><div><p class='eyebrow'>Nutzerkonto</p><h2>Einstellungen</h2></div></header>
<div class='settings-layout'>
{{template "settings_nav" (dict "Kind" "user")}}
<form class='settings-content' data-view-settings>
<section class='panel' id='list-settings'>
<h3>Seitengröße</h3>
<p>Lege fest, wie viele Einträge auf Listen- und Übersichtsseiten erscheinen.</p>
<label for='entries-per-page'>Einträge pro Seite</label>
<select id='entries-per-page' name='entriesPerPage'><option value='10'>10</option><option value='20'>20</option><option value='50'>50</option><option value='100'>100</option></select>
</section>
<section class='panel' id='view-settings'>
<h3>Ansichten</h3>
<label for='view-default'>Standard für alle Seiten</label>
<select id='view-default' name='default'><option value='grid'>Kacheln</option><option value='list'>Liste</option></select>
<fieldset><legend>Abweichungen je Seite</legend>
<label for='view-dashboard'>Gartenübersicht</label><select id='view-dashboard' name='dashboard'><option value='inherit'>Standard verwenden</option><option value='grid'>Kacheln</option><option value='list'>Liste</option></select>
<label for='view-gardens'>Gärten</label><select id='view-gardens' name='gardens'><option value='inherit'>Standard verwenden</option><option value='grid'>Kacheln</option><option value='list'>Liste</option></select>
<label for='view-plants'>Im Garten</label><select id='view-plants' name='plants'><option value='inherit'>Standard verwenden</option><option value='grid'>Kacheln</option><option value='list'>Liste</option></select>
<label for='view-species'>Pflanzen</label><select id='view-species' name='species'><option value='inherit'>Standard verwenden</option><option value='grid'>Kacheln</option><option value='list'>Liste</option></select>
<label for='view-locations'>Orte</label><select id='view-locations' name='locations'><option value='inherit'>Standard verwenden</option><option value='grid'>Kacheln</option><option value='list'>Liste</option></select>
<label for='view-tasks'>Aufgaben</label><select id='view-tasks' name='tasks'><option value='inherit'>Standard verwenden</option><option value='grid'>Kacheln</option><option value='list'>Liste</option></select>
<label for='view-location-plants'>Pflanzen an einem Ort</label><select id='view-location-plants' name='location-plants'><option value='inherit'>Standard verwenden</option><option value='grid'>Kacheln</option><option value='list'>Liste</option></select>
<label for='view-search'>Suchergebnisse</label><select id='view-search' name='search'><option value='inherit'>Standard verwenden</option><option value='grid'>Kacheln</option><option value='list'>Liste</option></select>
</fieldset>
<p class='form-message' data-settings-message aria-live='polite'></p>
<button type='submit'>Einstellungen speichern</button>
</section>
<section class='panel' id='editor-settings'>
<h3>Texteingabe</h3>
<p>Wähle, wie Texte im Tagebuch und an der Pinnwand bearbeitet werden.</p>
<label for='journal-editor-mode'>Editor</label>
<select id='journal-editor-mode' name='journalEditor'><option value='rich'>Formatierter Editor</option><option value='plain'>Einfaches Textfeld</option></select>
</section>
</form>
</div>{{end}}
+23
View File
@@ -0,0 +1,23 @@
{{define "title"}}Anmelden{{end}}
{{define "main"}}
<section class='panel narrow'>
<h2>Anmelden</h2>
{{$form := .Form}}
{{with $form.Message}}<p class='form-message error'>{{.}}</p>{{end}}
<form action='{{webPath "login"}}' method='POST'>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<label for='email'>E-Mail-Adresse</label>
<input id='email' name='email' type='email' value='{{$form.Email}}' autocomplete='email' required>
{{with index $form.Errors "email"}}<p class='field-error'>{{.}}</p>{{end}}
<label for='password'>Passwort</label>
<input id='password' name='password' type='password' autocomplete='current-password' required>
{{with index $form.Errors "password"}}<p class='field-error'>{{.}}</p>{{end}}
<label class='checkbox'><input name='remember_email' type='checkbox' value='true' {{if $form.RememberEmail}}checked{{end}}> E-Mail merken</label>
<button type='submit'>Anmelden</button>
</form>
</section>
{{end}}
+18
View File
@@ -0,0 +1,18 @@
{{define "title"}}Pflanzen{{end}}
{{define "main"}}
<header class='page-heading'>
<div><h2>Pflanzen</h2></div>
<div class='actions'>{{if or (canGarden .Garden "species:write") (canGlobalSpecies .CurrentUser)}}<a class='button' href='{{webPath "species.new" .Garden.ID}}'>Art anlegen</a>{{end}}{{template "view_toggle" "species"}}</div>
</header>
<form class='panel filter-bar' method='GET'><div class='filter-fields'><div><label for='q'>Suche</label><input id='q' name='q' type='search' value='{{index .Filters "q"}}' placeholder='Name, Sorte oder Kategorie'></div><div><label for='origin'>Herkunft</label><select id='origin' name='origin'><option value=''>Alle</option><option value='garden' {{if eqString (index .Filters "origin") "garden"}}selected{{end}}>Garteneigen</option><option value='global' {{if eqString (index .Filters "origin") "global"}}selected{{end}}>Global</option></select></div><div><label for='sort'>Sortierung</label><select id='sort' name='sort'><option value='name_asc'>Name AZ</option><option value='name_desc' {{if eqString (index .Filters "sort") "name_desc"}}selected{{end}}>Name ZA</option><option value='newest' {{if eqString (index .Filters "sort") "newest"}}selected{{end}}>Neueste zuerst</option><option value='oldest' {{if eqString (index .Filters "sort") "oldest"}}selected{{end}}>Älteste zuerst</option></select></div><button class='filter-submit'>Filtern</button></div></form>
<section class='card-grid collection' data-view-key='species'>
{{range .Species}}
<article class='card card--interactive {{if .ImageData}}card--image{{end}}' {{if .ImageData}}data-background-image='{{.ImageData}}'{{end}} role='link' tabindex='0' data-card-href='{{webPath "species.edit" $.Garden.ID .ID}}' aria-label='{{.CommonName}}{{with .Cultivar}} · {{.}}{{end}} öffnen'><div class='card-content'>
<span class='status'>{{if .GardenID}}Garteneigen{{else}}Global{{end}}</span>
<h3>{{.CommonName}}{{with .Cultivar}} · {{.}}{{end}}</h3>
{{with .BotanicalName}}<p>{{.}}</p>{{end}}</div>
</article>
{{else}}<p>Keine Arten verfügbar.</p>{{end}}
</section>
{{template "pagination" .}}
{{end}}
@@ -0,0 +1,72 @@
{{define "title"}}{{if .SpeciesID}}Art ansehen{{else}}Art anlegen{{end}}{{end}}
{{define "main"}}
{{$form := .Form}}
{{$editable := canEditSpecies .Garden .CurrentUser $form.Global .SpeciesID}}
{{$step := .WizardStep}}
{{$all := eqString $step "all"}}
{{$general := or $all (eqString $step "") (eqString $step "general")}}
<div class='{{if .SpeciesID}}settings-layout{{end}}'>
{{if .SpeciesID}}<aside class='settings-sidebar panel'><nav aria-label='Bearbeitungsbereiche'><a href='{{if $all}}#species-general{{else}}{{pathWithQuery (webPath "species.edit" .Garden.ID .SpeciesID) "step" "general"}}{{end}}' {{if and (not $all) $general}}aria-current='page'{{end}}>Allgemein</a><a href='{{if $all}}#species-care{{else}}{{pathWithQuery (webPath "species.edit" .Garden.ID .SpeciesID) "step" "care"}}{{end}}' {{if eqString $step "care"}}aria-current='page'{{end}}>Pflegeanweisungen</a><a href='{{if $all}}#species-tasks{{else}}{{pathWithQuery (webPath "species.edit" .Garden.ID .SpeciesID) "step" "tasks"}}{{end}}' {{if eqString $step "tasks"}}aria-current='page'{{end}}>Aufgabenvorlagen</a></nav></aside>{{end}}
<div class='settings-content'>
{{if $general}}
<section class='panel species-detail' id='species-general'>
<h2>{{if .SpeciesID}}{{if $editable}}{{if $form.Global}}Globale Art bearbeiten{{else}}Art bearbeiten{{end}}{{else}}Art ansehen{{end}}{{else}}Art anlegen{{end}}</h2>
{{if and .SpeciesID (not $editable)}}<p class='muted'>Diese Art ist nur lesbar.</p>{{end}}
<form method='POST'>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
{{if and .SpeciesID $form.Global}}<input type='hidden' name='global' value='true'>{{end}}
{{if and (not .SpeciesID) (canGlobalSpecies .CurrentUser)}}<label class='checkbox'><input type='checkbox' name='global' value='true' {{if or $form.Global (not (canGarden .Garden "species:write"))}}checked{{end}} {{if not (canGarden .Garden "species:write")}}disabled{{end}}> Global anlegen <span class='muted'>für alle Gärten verfügbar</span></label>{{if not (canGarden .Garden "species:write")}}<input type='hidden' name='global' value='true'>{{end}}{{end}}
{{if not .SpeciesID}}<label for='template_species_id'>Vorlage</label><select id='template_species_id' name='template_species_id' data-species-template><option value='0'>Ohne Vorlage</option>{{range .Species}}<option value='{{.ID}}' {{if eqInt $form.TemplateSpeciesID .ID}}selected{{end}}>{{.CommonName}}{{with .Cultivar}} · {{.}}{{end}}</option>{{end}}</select><p class='muted'>Die Felder und Aufgabenvorlagen werden übernommen; der Name bleibt leer.</p>{{end}}
<fieldset class='plain-fieldset' {{if not $editable}}disabled{{end}}>
<label>Name</label><input name='common_name' value='{{$form.CommonName}}' required>{{with index $form.Errors "common_name"}}<p class='field-error'>{{.}}</p>{{end}}
<label>Sorte</label><input name='cultivar' value='{{$form.Cultivar}}'>
<label>Botanischer Name</label><input name='botanical_name' value='{{$form.BotanicalName}}'>
{{template "image_editor" (dict "ImageData" $form.ImageData "ImageID" $form.ImageID "Images" .Images "GardenID" .Garden.ID)}}
<input type='hidden' name='attributes' value='{{$form.Attributes}}'>
<label for='category_id'>Kategorie</label>
<select id='category_id' name='category_id'>
<option value='0' {{if eqInt $form.CategoryID 0}}selected{{end}}>Keine Kategorie</option>
{{range .SpeciesCategories}}{{if or .Active (eqInt $form.CategoryID .ID)}}<option value='{{.ID}}' {{if eqInt $form.CategoryID .ID}}selected{{end}}>{{.Name}}{{with lifecycleName .Lifecycle}} ({{.}}){{end}}{{if not .Active}} (inaktiv){{end}}</option>{{end}}{{end}}
</select>
{{with index $form.Errors "category_id"}}<p class='field-error'>{{.}}</p>{{end}}
<div class='form-grid'><label>Licht<select name='sun_exposure'><option value=''>Nicht angegeben</option><option value='sunny' {{if eqString $form.SunExposure "sunny"}}selected{{end}}>Sonnig</option><option value='partial_shade' {{if eqString $form.SunExposure "partial_shade"}}selected{{end}}>Halbschattig</option><option value='shade' {{if eqString $form.SunExposure "shade"}}selected{{end}}>Schattig</option></select></label><label>Bodenbeschaffenheit<select name='soil_condition'><option value=''>Nicht angegeben</option><option value='dry' {{if eqString $form.SoilCondition "dry"}}selected{{end}}>Trocken</option><option value='moist' {{if eqString $form.SoilCondition "moist"}}selected{{end}}>Feucht</option><option value='boggy' {{if eqString $form.SoilCondition "boggy"}}selected{{end}}>Sumpfig</option></select></label><label>Bodenreaktion<select name='soil_reaction'><option value=''>Nicht angegeben</option><option value='alkaline' {{if eqString $form.SoilReaction "alkaline"}}selected{{end}}>Basisch</option><option value='acidic' {{if eqString $form.SoilReaction "acidic"}}selected{{end}}>Sauer</option><option value='neutral' {{if eqString $form.SoilReaction "neutral"}}selected{{end}}>Neutral</option></select></label><label>Winterschutz<input name='winter_protection' value='{{$form.WinterProtection}}'></label><label>Pflanzabstand in cm<input type='number' min='1' name='spacing_cm' value='{{if $form.SpacingCM}}{{$form.SpacingCM}}{{end}}'></label><label>Höhe in cm<input type='number' min='1' name='height_cm' value='{{if $form.HeightCM}}{{$form.HeightCM}}{{end}}'></label></div>
{{template "season_range" (dict "Legend" "Aussaat" "Prefix" "sow" "Month" $form.SowMonthFrom "Day" $form.SowDayFrom "Duration" $form.SowDuration "Unit" $form.SowDurationUnit)}}
{{template "season_range" (dict "Legend" "Pflanzzeit" "Prefix" "planting" "Month" $form.PlantingMonthFrom "Day" $form.PlantingDayFrom "Duration" $form.PlantingDuration "Unit" $form.PlantingDurationUnit)}}
{{template "season_range" (dict "Legend" "Ernte" "Prefix" "harvest" "Month" $form.HarvestMonthFrom "Day" $form.HarvestDayFrom "Duration" $form.HarvestDuration "Unit" $form.HarvestDurationUnit)}}
{{template "tag_editor" .}}
<label>Notizen</label><textarea name='notes'>{{$form.Notes}}</textarea>
</fieldset>
<div class='actions'>{{if $editable}}{{if $all}}<button type='submit'>Speichern</button>{{else}}<button type='submit' name='continue' value='care'>Speichern und weiter</button><button type='submit' class='secondary'>Speichern und beenden</button>{{end}}{{end}}<a href='{{webPath "species" .Garden.ID}}'>{{if $editable}}Abbrechen{{else}}Zurück{{end}}</a>{{if and .SpeciesID $editable}}<button class='danger' type='submit' formaction='{{webPath "species.delete" .Garden.ID .SpeciesID}}' formmethod='post' formnovalidate>Art löschen</button>{{end}}</div>
</form>
</section>
{{end}}
{{if and .SpeciesID (or $all (eqString $step "care"))}}<div class='species-support-grid'>
<section class='panel care-instructions' id='species-care'>
<h3>Pflegeanweisungen</h3>
{{template "care_instruction_list" .}}
{{if $editable}}<details class='care-instruction-add'><summary class='icon-button' aria-label='Pflegeanweisung hinzufügen' title='Pflegeanweisung hinzufügen'></summary><form method='POST' action='{{webPath "species.care.new" .Garden.ID .SpeciesID}}' hx-post='{{webPath "species.care.new" .Garden.ID .SpeciesID}}' hx-target='#care-instruction-list' hx-swap='innerHTML'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><label>Text<textarea name='text' required></textarea></label><label>Status<select name='status'>{{range careStatuses}}<option value='{{.Value}}'>{{.Label}}</option>{{end}}</select></label><button>Hinzufügen</button></form></details>{{end}}
{{if not $all}}<div class='actions'><a class='button' href='{{pathWithQuery (webPath "species.edit" .Garden.ID .SpeciesID) "step" "tasks"}}'>Weiter zu Aufgabenvorlagen</a><a href='{{webPath "species" .Garden.ID}}'>Überspringen und beenden</a></div>{{end}}
</section>
</div>{{end}}
{{if and .SpeciesID (or $all (eqString $step "tasks"))}}<div class='species-support-grid'>
<section class='panel species-task-templates' id='species-tasks'>
<h3>Aufgabenvorlagen</h3>
<div class='species-template-list'>
{{range index .TaskTemplates .SpeciesID}}
<div class='species-template-row'>
{{if $editable}}
<button type='button' class='species-template-open' hx-get='{{pathWithQuery (webPath "task-template.edit" $.Garden.ID .ID) "species_id" $.SpeciesID}}' hx-target='#task-template-dialog-host' hx-swap='innerHTML'>{{.Title}}{{if and .Origin (not (eqString .Origin "manual"))}} · automatisch{{end}}{{if not .Active}} · inaktiv{{end}}</button>
{{if or (not .Origin) (eqString .Origin "manual")}}<form method='POST' action='{{pathWithQuery (webPath "task-template.delete" $.Garden.ID .ID) "species_id" $.SpeciesID}}'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><button type='submit' class='template-remove' aria-label='{{.Title}} entfernen' title='Vorlage entfernen'>×</button></form>{{end}}
{{else}}<span>{{.Title}}</span>{{end}}
</div>
{{else}}<p class='muted'>Noch keine Vorlagen.</p>{{end}}
</div>
{{if $editable}}<button type='button' class='icon-button species-template-add' hx-get='{{pathWithQuery (webPath "task-template.new" .Garden.ID) "species_id" .SpeciesID}}' hx-target='#task-template-dialog-host' hx-swap='innerHTML' aria-label='Aufgabenvorlage hinzufügen' title='Aufgabenvorlage hinzufügen'></button>{{end}}
<div id='task-template-dialog-host'></div>
{{if not $all}}<div class='actions'><a class='button' href='{{webPath "species" .Garden.ID}}'>Fertig</a></div>{{end}}
</section>
</div>{{end}}
</div></div>
{{end}}
@@ -0,0 +1,6 @@
{{define "title"}}Aufgabenkalender{{end}}
{{define "main"}}
<header class='page-heading'><div><h2>Anstehende Aufgaben</h2><p>{{calendarDate .CalendarStart}} bis {{calendarDate .CalendarEnd}}</p></div><div class='actions'><a class='button secondary' href='{{pathWithQuery (webPath "tasks.calendar" .Garden.ID) "view" .CalendarView "date" .PreviousDate}}'>Zurück</a><a class='button secondary' href='{{pathWithQuery (webPath "tasks.calendar" .Garden.ID) "view" .CalendarView "date" .NextDate}}'>Weiter</a></div></header>
<nav class='view-switch' aria-label='Kalenderansicht'><a href='{{pathWithQuery (webPath "tasks.calendar" .Garden.ID) "view" "week" "date" (dateValue .CalendarStart)}}' {{if eqString .CalendarView "week"}}aria-current='page'{{end}}>Woche</a><a href='{{pathWithQuery (webPath "tasks.calendar" .Garden.ID) "view" "month" "date" (dateValue .CalendarStart)}}' {{if eqString .CalendarView "month"}}aria-current='page'{{end}}>Monat</a></nav>
<section class='calendar-grid {{.CalendarView}}'>{{range .CalendarDays}}<article class='calendar-day'><h3><time datetime='{{dateValue .Date}}'>{{calendarDate .Date}}</time></h3>{{range .Tasks}}{{$editable := canGardenResource $.Garden $.CurrentUser .CreatedBy "tasks:update:own" "tasks:update:other"}}{{if $editable}}<a class='calendar-task' href='{{webPath "task.edit" $.Garden.ID .ID}}'><strong>{{.Title}}</strong><span>{{taskDue .}}</span></a>{{else}}<div class='calendar-task'><strong>{{.Title}}</strong><span>{{taskDue .}}</span></div>{{end}}{{else}}<span class='muted'>Keine Aufgaben</span>{{end}}</article>{{end}}</section>
{{end}}
@@ -0,0 +1,13 @@
{{define "title"}}{{if .TaskID}}Aufgabe bearbeiten{{else}}Aufgabe anlegen{{end}}{{end}}
{{define "main"}}
{{$canSave := canGarden .Garden "tasks:create"}}{{if .TaskID}}{{$canSave = canGardenResource .Garden .CurrentUser .TaskCreatedBy "tasks:update:own" "tasks:update:other"}}{{end}}
<section class='panel narrow'>
<h2>{{if .TaskID}}Aufgabe bearbeiten{{else}}Neue Aufgabe{{end}}</h2>
<form method='POST'>
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
{{template "task_fields" (dict "Form" .Form "Prefix" "" "ShowPlant" true "Plants" .Plants "Locations" .Locations "TaskPriorities" .TaskPriorities "TagSuggestions" .TagSuggestions)}}
<div class='actions'>{{if $canSave}}<button type='submit'>Speichern</button>{{end}}<a href='{{webPath "tasks" .Garden.ID}}'>{{if $canSave}}Abbrechen{{else}}Zurück{{end}}</a></div>
</form>
{{if and .TaskID (canGardenResource .Garden .CurrentUser .TaskCreatedBy "tasks:delete:own" "tasks:delete:other")}}<form class='delete-form' method='POST' action='{{webPath "task.delete" .Garden.ID .TaskID}}'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><button class='danger' type='submit'>Aufgabe löschen</button></form>{{end}}
</section>
{{end}}
@@ -0,0 +1,11 @@
{{define "title"}}Aufgabenvorlage{{end}}
{{define "main"}}
<section class='panel'>{{$species := index .Species 0}}
<p class='eyebrow'>{{$species.CommonName}}</p><h2>{{if .TemplateID}}Vorlage bearbeiten{{else}}Vorlage anlegen{{end}}</h2>
<form method='POST'>
{{template "task_template_fields" .}}
<div class='actions'><button type='submit'>Speichern</button><a href='{{webPath "species.edit" .Garden.ID .SpeciesID}}'>Abbrechen</a></div>
</form>
</section>
{{end}}
+32
View File
@@ -0,0 +1,32 @@
{{define "title"}}Aufgaben{{end}}
{{define "main"}}
<header class='page-heading'>
<div><h2>Aufgaben</h2></div>
<div class='actions'><a class='button secondary' href='{{webPath "tasks.calendar" .Garden.ID}}'>Kalender</a>{{if canGarden .Garden "tasks:create"}}<a class='button' href='{{webPath "task.new" .Garden.ID}}'>Aufgabe anlegen</a>{{end}}{{template "view_toggle" "tasks"}}</div>
</header>
<form class='panel filter-bar' method='GET'><div class='filter-fields filter-fields--tasks'><div><label for='status'>Status</label><select id='status' name='status'><option value=''>Alle</option><option value='open' {{if eqString (index .Filters "status") "open"}}selected{{end}}>Offen</option><option value='done' {{if eqString (index .Filters "status") "done"}}selected{{end}}>Erledigt</option></select></div><div><label for='plant'>Pflanze</label><select id='plant' name='plant'><option value=''>Alle</option>{{range .Plants}}<option value='{{.ID}}' {{if eqString (index $.Filters "plant") (printf "%d" .ID)}}selected{{end}}>{{.Name}}</option>{{end}}</select></div><div><label for='location'>Ort</label><select id='location' name='location'><option value=''>Alle</option>{{range .Locations}}<option value='{{.ID}}' {{if eqString (index $.Filters "location") (printf "%d" .ID)}}selected{{end}}>{{.Name}}</option>{{end}}</select></div><div><label for='priority'>Priorität</label><select id='priority' name='priority'><option value=''>Alle</option>{{range .TaskPriorities}}<option value='{{.Value}}' {{if eqString (index $.Filters "priority") (printf "%d" .Value)}}selected{{end}}>{{.Name}}</option>{{end}}</select></div><div><label for='month'>Monat</label><select id='month' name='month'><option value=''>Alle</option>{{range months}}<option value='{{.Value}}' {{if eqString (index $.Filters "month") (printf "%d" .Value)}}selected{{end}}>{{.Label}}</option>{{end}}</select></div><div><label for='year'>Jahr</label><select id='year' name='year'><option value=''>Alle</option>{{range .TaskYears}}<option value='{{.}}' {{if eqString (index $.Filters "year") (printf "%d" .)}}selected{{end}}>{{.}}</option>{{end}}</select></div><div><label for='sort'>Sortierung</label><select id='sort' name='sort'><option value='due_asc'>Fälligkeit aufsteigend</option><option value='due_desc' {{if eqString (index .Filters "sort") "due_desc"}}selected{{end}}>Fälligkeit absteigend</option><option value='month_asc' {{if eqString (index .Filters "sort") "month_asc"}}selected{{end}}>Monat aufsteigend</option><option value='month_desc' {{if eqString (index .Filters "sort") "month_desc"}}selected{{end}}>Monat absteigend</option><option value='period_asc' {{if eqString (index .Filters "sort") "period_asc"}}selected{{end}}>Zeitraum kurzlang</option><option value='period_desc' {{if eqString (index .Filters "sort") "period_desc"}}selected{{end}}>Zeitraum langkurz</option><option value='name_asc' {{if eqString (index .Filters "sort") "name_asc"}}selected{{end}}>Name AZ</option><option value='name_desc' {{if eqString (index .Filters "sort") "name_desc"}}selected{{end}}>Name ZA</option><option value='priority_desc' {{if eqString (index .Filters "sort") "priority_desc"}}selected{{end}}>Priorität</option></select></div><button class='filter-submit'>Filtern</button></div></form>
{{if .Tasks}}
<section class='task-list collection' data-view-key='tasks'>
{{range .Tasks}}
{{$editable := canGardenResource $.Garden $.CurrentUser .CreatedBy "tasks:update:own" "tasks:update:other"}}
{{$canComplete := canGardenResource $.Garden $.CurrentUser .CreatedBy "tasks:complete:own" "tasks:complete:other"}}
<article class='panel task-card{{if $editable}} card--interactive{{end}}{{if .CompletedAt}} completed{{end}}'{{if $editable}} role='link' tabindex='0' data-card-href='{{webPath "task.edit" $.Garden.ID .ID}}' aria-label='{{.Title}} bearbeiten'{{end}}>
<div class='task-heading'><div><span class='status'>{{configuredPriorityName $.TaskPriorities .Priority}}</span><h3>{{.Title}}</h3></div><strong>{{taskDueDate .}}</strong></div>
{{with .Description}}<p>{{.}}</p>{{end}}
{{with recurrenceDescription .Recurrence .RecurrenceInterval}}<p class='muted'>Wiederholung: {{.}}</p>{{end}}
<div class='task-card-footer'>
<p class='task-links'>{{with plantName $.Plants .PlantID}}Pflanze: {{.}}{{end}}{{with locationName $.Locations .LocationID}} · Ort: {{.}}{{end}}</p>
{{if $canComplete}}<div class='actions task-card-actions'>
{{if .CompletedAt}}
<form method='POST' action='{{webPath "task.reopen" $.Garden.ID .ID}}' class='inline-form'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><button type='submit'>Wieder öffnen</button></form>
{{else}}
<form method='POST' action='{{webPath "task.complete" $.Garden.ID .ID}}' class='inline-form'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><button type='submit'>Erledigt</button></form>
{{end}}
</div>{{end}}
</div>
</article>
{{end}}
</section>
{{else}}<p>In diesem Garten gibt es noch keine Aufgaben.</p>{{end}}
{{template "pagination" .}}
{{end}}