summaries.go
⎇
Raw
1package web
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "strings"
8 "time"
9
10 "forge.gpl-nas.de/physiobase/internal/befund"
11 "forge.gpl-nas.de/physiobase/internal/store"
12)
13
14// Zeilen, die mit diesem Zeichen beginnen, sind Hinweise für die Therapeutin
15// und erscheinen nicht im Ausdruck.
16const noteMark = "✎"
17
18// Printable entfernt Hinweiszeilen und daraus entstandene leere Abschnitte.
19func Printable(body string) string {
20 var out []string
21 for _, line := range strings.Split(body, "\n") {
22 if strings.HasPrefix(strings.TrimSpace(line), noteMark) {
23 continue
24 }
25 out = append(out, line)
26 }
27 // Überschriften ohne Inhalt bis zur nächsten Überschrift entfernen
28 var cleaned []string
29 for i := 0; i < len(out); i++ {
30 if strings.HasPrefix(out[i], "## ") {
31 j := i + 1
32 for j < len(out) && strings.TrimSpace(out[j]) == "" {
33 j++
34 }
35 if j == len(out) || strings.HasPrefix(out[j], "## ") {
36 i = j - 1
37 continue
38 }
39 }
40 cleaned = append(cleaned, out[i])
41 }
42 return strings.TrimSpace(strings.Join(cleaned, "\n"))
43}
44
45type draft struct{ b strings.Builder }
46
47func (d *draft) section(title string) { fmt.Fprintf(&d.b, "\n## %s\n\n", title) }
48func (d *draft) line(format string, a ...any) {
49 fmt.Fprintf(&d.b, format+"\n", a...)
50}
51func (d *draft) note(text string) { d.line("%s %s", noteMark, text) }
52func (d *draft) String() string { return strings.TrimSpace(d.b.String()) + "\n" }
53
54// latestHandoutItems: Übungen des zuletzt bearbeiteten Heimprogramms des Patienten.
55func (s *Server) latestHandoutItems(ctx context.Context, pid int64) (store.Handout, []store.HandoutItem, error) {
56 hs, err := s.st.HandoutsForPatient(ctx, pid)
57 if err != nil || len(hs) == 0 {
58 return store.Handout{}, nil, err
59 }
60 items, err := s.st.HandoutItems(ctx, hs[0].ID)
61 return hs[0], items, err
62}
63
64func (d *draft) exercises(h store.Handout, items []store.HandoutItem) {
65 if len(items) == 0 {
66 d.line("- ")
67 d.note("Noch kein Heimprogramm – auf der Patientenseite unter „Heimprogramme“ anlegen.")
68 return
69 }
70 for _, it := range items {
71 if it.Dosage != "" {
72 d.line("- **%s** – %s", it.Entry.Title, it.Dosage)
73 } else {
74 d.line("- **%s**", it.Entry.Title)
75 }
76 }
77 d.line("")
78 d.line("Die genaue Anleitung mit Bildern finden Sie im Übungsblatt „%s“.", h.Title)
79}
80
81func safetyNet(d *draft) {
82 d.section("Wann Sie sich melden sollten")
83 d.line("- wenn die Beschwerden deutlich stärker werden oder nach Übungen länger als einen Tag schlechter bleiben")
84 d.line("- bei neuem Taubheitsgefühl, Kribbeln oder Kraftverlust")
85 d.line("- bei Schwellung, Rötung, Überwärmung oder Fieber")
86 d.note("Bei Rückenbeschwerden ggf. ergänzen: Taubheit im Gesäß-/Genitalbereich oder Probleme beim Wasserlassen → sofort ärztlich abklären.")
87}
88
89// generalDraft baut die Gesamtzusammenfassung aus dem neuesten Befund, dem
90// Heimprogramm und dem Verlauf seit dem ersten Befund.
91func (s *Server) generalDraft(ctx context.Context, p store.Patient) (string, error) {
92 bs, err := s.st.Befunde(ctx, p.ID)
93 if err != nil {
94 return "", err
95 }
96 h, items, err := s.latestHandoutItems(ctx, p.ID)
97 if err != nil {
98 return "", err
99 }
100 d := &draft{}
101 d.note("Entwurf aus den Befunddaten. In einfache Worte bringen; ✎-Zeilen werden nicht gedruckt.")
102 var last, first befund.Data
103 if len(bs) > 0 {
104 last, _ = befund.Parse(bs[0].Data)
105 first, _ = befund.Parse(bs[len(bs)-1].Data)
106 }
107
108 d.section("Ihre Beschwerden")
109 if diag := last.Get("diag"); diag != "" {
110 d.line("Ärztliche Diagnose: %s", diag)
111 d.line("")
112 }
113 d.line("")
114 d.note("In 2–3 Sätzen erklären, was los ist und warum. Fachbegriffe übersetzen.")
115 if h := last.Get("hypAfterExam"); h != "" {
116 d.note("Ihre Arbeitshypothese: " + oneLine(h))
117 }
118
119 if now, worst := last.Get("pain_now"), last.Get("pain_worst"); now != "" || worst != "" {
120 d.section("Was wir festgestellt haben")
121 if now != "" {
122 d.line("- Schmerz heute: %s von 10", now)
123 }
124 if worst != "" {
125 d.line("- Stärkster Schmerz in den letzten 24 Stunden: %s von 10", worst)
126 }
127 d.note("Wichtigste Befunde in Alltagssprache ergänzen (Beweglichkeit, Kraft …).")
128 }
129
130 d.section("Ihre Ziele")
131 for _, k := range []string{"goal_patient", "goal_agreement"} {
132 if v := last.Get(k); v != "" {
133 for _, l := range strings.Split(v, "\n") {
134 if l = strings.TrimSpace(strings.TrimLeft(l, "-•* ")); l != "" {
135 d.line("- %s", l)
136 }
137 }
138 }
139 }
140 for _, r := range last.Tables["psfs"] {
141 if r["activity"] != "" {
142 if r["score"] != "" {
143 d.line("- %s (heute %s von 10 möglich)", r["activity"], r["score"])
144 } else {
145 d.line("- %s", r["activity"])
146 }
147 }
148 }
149 d.note("Ziele so formulieren, wie der Patient sie selbst sagen würde.")
150
151 d.section("So gehen wir vor")
152 if plan := last.Get("plan"); plan != "" {
153 d.note("Therapieplan aus dem Befund: " + oneLine(plan))
154 }
155 d.line("- ")
156 d.note("Behandlungsschwerpunkte, Anzahl/Häufigkeit der Termine, wann wir neu messen.")
157
158 d.section("Ihr Übungsprogramm für zu Hause")
159 d.exercises(h, items)
160
161 if len(bs) > 1 {
162 d.section("Ihr Fortschritt")
163 if a, b := first.Get("pain_now"), last.Get("pain_now"); a != "" && b != "" {
164 d.line("- Schmerz: von %s auf %s (von 10)", a, b)
165 }
166 if a, b := befund.PSFSMean(first), befund.PSFSMean(last); a != "" && b != "" {
167 d.line("- Ihre Alltagsaktivitäten: von durchschnittlich %s auf %s (von 10)", a, b)
168 }
169 d.note("Was hat sich im Alltag verbessert? Konkrete Beispiele des Patienten aufgreifen.")
170 }
171
172 safetyNet(d)
173 return d.String(), nil
174}
175
176// sessionDraft baut die Zusammenfassung einer Behandlung.
177func (s *Server) sessionDraft(ctx context.Context, x store.Session) (string, error) {
178 h, items, err := s.latestHandoutItems(ctx, x.PatientID)
179 if err != nil {
180 return "", err
181 }
182 d := &draft{}
183 d.note("Entwurf aus dem Verlaufseintrag. ✎-Zeilen werden nicht gedruckt.")
184 d.section("Das haben wir heute gemacht")
185 if x.Goal != "" {
186 d.line("- %s", x.Goal)
187 } else {
188 d.line("- ")
189 }
190 if x.Note != "" {
191 d.note("Ihre Notiz: " + oneLine(x.Note))
192 }
193 d.section("Ihre Übungen bis zum nächsten Termin")
194 d.exercises(h, items)
195 d.section("Darauf sollten Sie achten")
196 d.line("- ")
197 d.note("z. B. Belastung dosieren, Schmerz bei Übungen bis 3–4 von 10 ist in Ordnung, am Folgetag nicht schlechter.")
198 d.section("Nächster Termin")
199 d.line("- ")
200 return d.String(), nil
201}
202
203func oneLine(s string) string { return strings.Join(strings.Fields(s), " ") }
204
205// ---------- Handler ----------
206
207func (s *Server) createGeneralSummary(w http.ResponseWriter, r *http.Request) {
208 ctx := r.Context()
209 pid, ok := pathID(r, "id")
210 if !ok {
211 http.NotFound(w, r)
212 return
213 }
214 p, err := s.st.Patient(ctx, pid)
215 if err != nil {
216 s.fail(w, r, err)
217 return
218 }
219 body, err := s.generalDraft(ctx, p)
220 if err != nil {
221 s.fail(w, r, err)
222 return
223 }
224 id, err := s.st.CreateSummary(ctx, store.Summary{
225 PatientID: pid, Kind: "gesamt",
226 Date: time.Now().Format("2006-01-02"), Title: "Ihre Physiotherapie – Zusammenfassung", Body: body,
227 })
228 if err != nil {
229 s.fail(w, r, err)
230 return
231 }
232 redirect(w, r, "/zusammenfassung/%d", id)
233}
234
235func (s *Server) createSessionSummary(w http.ResponseWriter, r *http.Request) {
236 ctx := r.Context()
237 sid, ok := pathID(r, "id")
238 if !ok {
239 http.NotFound(w, r)
240 return
241 }
242 x, err := s.st.Session(ctx, sid)
243 if err != nil {
244 s.fail(w, r, err)
245 return
246 }
247 body, err := s.sessionDraft(ctx, x)
248 if err != nil {
249 s.fail(w, r, err)
250 return
251 }
252 id, err := s.st.CreateSummary(ctx, store.Summary{
253 PatientID: x.PatientID, SessionID: x.ID, Kind: "sitzung",
254 Date: x.Date, Title: "Ihre Behandlung am " + deDate(x.Date), Body: body,
255 })
256 if err != nil {
257 s.fail(w, r, err)
258 return
259 }
260 redirect(w, r, "/zusammenfassung/%d", id)
261}
262
263func (s *Server) loadSummary(w http.ResponseWriter, r *http.Request) (store.Summary, store.Patient, bool) {
264 id, ok := pathID(r, "id")
265 if !ok {
266 http.NotFound(w, r)
267 return store.Summary{}, store.Patient{}, false
268 }
269 x, err := s.st.Summary(r.Context(), id)
270 if err != nil {
271 s.fail(w, r, err)
272 return x, store.Patient{}, false
273 }
274 p, err := s.st.Patient(r.Context(), x.PatientID)
275 if err != nil {
276 s.fail(w, r, err)
277 return x, p, false
278 }
279 return x, p, true
280}
281
282func (s *Server) showSummary(w http.ResponseWriter, r *http.Request) {
283 x, p, ok := s.loadSummary(w, r)
284 if !ok {
285 return
286 }
287 log, err := s.st.MailLogFor(r.Context(), x.ID)
288 if err != nil {
289 s.fail(w, r, err)
290 return
291 }
292 q := r.URL.Query()
293 s.render(w, r, "summary", http.StatusOK, map[string]any{
294 "S": x, "P": p, "Preview": Printable(x.Body), "Saved": q.Get("gespeichert") == "1",
295 "MailBlock": s.mailBlockReason(p), "MailLog": log, "Sent": q.Get("gesendet") == "1", "MailError": q.Get("fehler"),
296 })
297}
298
299func (s *Server) saveSummary(w http.ResponseWriter, r *http.Request) {
300 if !limitForm(w, r) {
301 return
302 }
303 x, _, ok := s.loadSummary(w, r)
304 if !ok {
305 return
306 }
307 x.Title, x.Date, x.Body = formText(r, "title"), validDate(formText(r, "date")), formText(r, "body")
308 if x.Title == "" {
309 x.Title = "Zusammenfassung"
310 }
311 if err := s.st.UpdateSummary(r.Context(), x); err != nil {
312 s.fail(w, r, err)
313 return
314 }
315 if r.FormValue("next") == "druck" {
316 redirect(w, r, "/zusammenfassung/%d/druck", x.ID)
317 return
318 }
319 redirect(w, r, "/zusammenfassung/%d?gespeichert=1", x.ID)
320}
321
322func (s *Server) deleteSummary(w http.ResponseWriter, r *http.Request) {
323 id, ok := pathID(r, "id")
324 if !ok {
325 http.NotFound(w, r)
326 return
327 }
328 pid, err := s.st.DeleteSummary(r.Context(), id)
329 if err != nil {
330 s.fail(w, r, err)
331 return
332 }
333 redirect(w, r, "/patient/%d#zusammenfassungen", pid)
334}
335
336func (s *Server) printSummary(w http.ResponseWriter, r *http.Request) {
337 x, p, ok := s.loadSummary(w, r)
338 if !ok {
339 return
340 }
341 s.render(w, r, "summary_print", http.StatusOK, map[string]any{"S": x, "P": p, "Body": Printable(x.Body)})
342}
343
344func deDate(iso string) string {
345 if t, err := time.Parse("2006-01-02", iso); err == nil {
346 return t.Format("02.01.2006")
347 }
348 return iso
349}
350