Преглед изворни кода

Buttons und Details angepasst

- Schadennummer zu den Formularen und SQL-Abfragen hinzugefügt,
- Buttonfarbe und -funktion angepasst
gc-server3 пре 4 дана
родитељ
комит
585f7d49ff

+ 9 - 1
app/routes.py

@@ -410,6 +410,7 @@ async def post_forderungen_details(
         "Wiedervorlage": "",
         "Vers_Adresse_ID": "",
         "Versicherung_Typ": "",
+        "Schadennummer": "",
         "Reklamation_Begruendung": "",
         "Rechtsanwalt_Begruendung": "",
         "Rechtsanwalt_Aktenzeichen": "",
@@ -420,13 +421,20 @@ async def post_forderungen_details(
 
     async with request.form() as form:
         for k, v in form.items():
+            if k == "Versicherung_Schadennummer":
+                k = "Schadennummer"
             if k in data and k not in {"Client_DB", "Beleg_Nr", "Benutzer_ID", "Timestamp"}:
                 if v == "on":
                     v = "J"
-                if re.match(r"\d+,?\d*", v):
+                if k in {"Selbstbeteiligung_Betrag", "Mwst_Betrag"}:
                     v = v.replace(",", ".")
                 data[k] = v
 
+    try:
+        data["Wiedervorlage"] = parse_filter_date(data["Wiedervorlage"]) if data["Wiedervorlage"] else None
+    except ValueError as error:
+        raise HTTPException(422, "Bitte ein gültiges Wiedervorlagedatum auswählen.") from error
+
     if source not in {"comments", "details"}:
         raise HTTPException(status_code=400, detail="Ungültige Datenquelle")
     query = templates.TemplateResponse(

+ 35 - 3
static/assets/css/details.css

@@ -5,6 +5,9 @@
   --text-light: #52616f;
   --border: #dce2e7;
   --cui-primary: #72889b;
+  --gc-button-orange: #c45500;
+  --gc-button-orange-hover: #a74700;
+  --gc-button-orange-active: #8f3e00;
   font-size: 1.0625rem;
   width: 100%;
   padding: 0 0 2rem;
@@ -31,7 +34,7 @@ body:has(.gc-detail-page) { max-width: 1840px; }
 .gc-context-heading h1 { margin: .25rem 0; font-size: clamp(1.325rem, 1.6vw, 1.75rem); line-height: 1.4; font-weight: 700; overflow-wrap: anywhere; }
 .gc-context-heading p { margin: 0; font-size: 1.0625rem; }
 .gc-context-heading p span { color: #52616f; }
-.gc-detail-page .gc-back-link { padding: 0; font-size: 1rem; color: #3c5263; text-decoration: none; }
+.gc-detail-page .gc-back-link { font-size: 1rem; text-decoration: none; }
 .gc-context-metrics { display: flex; gap: .65rem; flex-wrap: wrap; }
 .gc-metric { display: flex; flex-direction: column; gap: .15rem; padding: .6rem .85rem; border-radius: 8px; }
 .gc-metric span { font-size: 1rem; }
@@ -57,8 +60,37 @@ body:has(.gc-detail-page) { max-width: 1840px; }
 .gc-data-more summary { cursor: pointer; padding: .65rem 1rem; font-size: 1rem; font-weight: 600; color: #354f63; }
 .gc-data-more[open] summary { background: #f4f6f8; }
 .gc-detail-page .btn { white-space: normal !important; border-radius: 6px; font-size: 1.0625rem; padding: .5rem .75rem; }
-/* Dark text on the exact corporate action color meets text contrast requirements. */
-.gc-detail-page .btn-primary { --cui-btn-bg: #72889b; --cui-btn-border-color: #72889b; --cui-btn-color: #101b24; --cui-btn-hover-bg: #c3ccd4; --cui-btn-hover-color: #101b24; --cui-btn-hover-border-color: #72889b; --cui-btn-active-bg: #b1bcc7; --cui-btn-active-color: #101b24; }
+/* Orange gives the main action a clear emphasis against the blue-grey panels. */
+.gc-detail-page .btn-primary {
+  --cui-btn-bg: var(--gc-button-orange);
+  --cui-btn-border-color: var(--gc-button-orange);
+  --cui-btn-color: #fff;
+  --cui-btn-hover-bg: var(--gc-button-orange-hover);
+  --cui-btn-hover-border-color: var(--gc-button-orange-hover);
+  --cui-btn-hover-color: #fff;
+  --cui-btn-active-bg: var(--gc-button-orange-active);
+  --cui-btn-active-border-color: var(--gc-button-orange-active);
+  --cui-btn-active-color: #fff;
+  --cui-btn-disabled-bg: var(--gc-button-orange);
+  --cui-btn-disabled-border-color: var(--gc-button-orange);
+  --cui-btn-disabled-color: #fff;
+  --cui-btn-focus-shadow-rgb: 196, 85, 0;
+}
+.gc-detail-page .btn-outline-primary {
+  --cui-btn-color: var(--gc-button-orange);
+  --cui-btn-bg: #fff;
+  --cui-btn-border-color: var(--gc-button-orange);
+  --cui-btn-hover-bg: #fff1e6;
+  --cui-btn-hover-border-color: var(--gc-button-orange-hover);
+  --cui-btn-hover-color: var(--gc-button-orange-hover);
+  --cui-btn-active-bg: #ffe1c7;
+  --cui-btn-active-border-color: var(--gc-button-orange-active);
+  --cui-btn-active-color: var(--gc-button-orange-active);
+  --cui-btn-disabled-color: var(--gc-button-orange);
+  --cui-btn-disabled-border-color: var(--gc-button-orange);
+  --cui-btn-focus-shadow-rgb: 196, 85, 0;
+}
+.gc-detail-page :is(.gc-back-link, .gc-form-actions > a) { --cui-btn-bg: #fff; }
 .gc-detail-page .btn-outline-secondary { --cui-btn-color: #354f63; --cui-btn-border-color: #b1bcc7; --cui-btn-hover-bg: #cdd4da; --cui-btn-hover-color: #182630; --cui-btn-hover-border-color: #72889b; }
 .gc-detail-page .btn-link { color: #354f63; }
 .gc-detail-page .gc-workflow-form { margin: 0; padding: 1rem; --cui-gutter-x: 0; --cui-gutter-y: .75rem; }

+ 1 - 1
templates/base/base.html

@@ -23,7 +23,7 @@
   <link href="/static/assets/css/main.css" rel="stylesheet">
   <link href="/static/assets/css/app.css" rel="stylesheet">
 
-  <link href="/static/assets/css/details.css?v=3" rel="stylesheet">
+  <link href="/static/assets/css/details.css?v=4" rel="stylesheet">
   <link href="/static/assets/css/list.css?v=2" rel="stylesheet">
   <link href="/static/assets/css/dashboard.css" rel="stylesheet">
   <script src="/static/assets/js/dashboard.js" defer></script>

+ 1 - 1
templates/forderungen/details/details.html

@@ -5,7 +5,7 @@
 {% set row = forderungen_details[0] %}
 <section class="gc-case-context" aria-label="Aktueller Beleg">
   <div class="gc-context-heading">
-    <a class="btn btn-link gc-back-link" href="{{ return_url }}" hx-boost="false" data-gc-return-url="{{ return_url }}">← Zurück</a>
+    <a class="btn btn-outline-secondary gc-back-link" href="{{ return_url }}" hx-boost="false" data-gc-return-url="{{ return_url }}">← Zurück zur Liste</a>
     <h1>{{ row.Beleg_Text }}</h1>
     <p>{{ row.Kunde_Name }} <span>· {{ row.Kunde_Nr }}</span></p>
   </div>

+ 3 - 3
templates/forderungen/details/details_formular.html

@@ -208,9 +208,9 @@
   </div>
 
   <div class="col-12 gc-form-actions">
-    <button type="button" class="btn btn-primary" hx-post="" hx-trigger="click" hx-swap="outerHTML" hx-target="#formular" hx-push-url="false">Speichern</button>
-    <button type="button" class="btn btn-outline-secondary" hx-post="" hx-trigger="click" hx-swap="innerHTML" hx-target="body" hx-push-url="true" hx-vals='{"redirect": "list"}'">Speichern &amp; Zurück</button>
-    <a class="btn btn-link" href="{{ return_url }}" hx-boost="false">Zurück</a>
+    <button type="button" class="btn btn-primary" hx-post="" hx-trigger="click" hx-swap="outerHTML" hx-target="#formular" hx-push-url="false"><i class="icon cil-save me-1" aria-hidden="true"></i>Speichern</button>
+    <button type="button" class="btn btn-outline-primary" hx-post="" hx-trigger="click" hx-swap="innerHTML" hx-target="body" hx-push-url="true" hx-vals='{"redirect": "list"}'>Speichern &amp; zurück</button>
+    <a class="btn btn-outline-secondary" href="{{ return_url }}" hx-boost="false">Zurück zur Liste</a>
   </div>
 
 

+ 1 - 0
templates/forderungen/details/queries/forderungen_details.sql

@@ -28,6 +28,7 @@ SELECT [F].*
      , isnull([FD].[Mwst_Betrag], 0.0) AS [Mwst_Betrag]
      , isnull([FD].[Vers_Adresse_ID], '') AS [Vers_Adresse_ID]
      , isnull([FD].[Versicherung_Typ], '') AS [Versicherung_Typ]
+     , isnull([FD].[Schadennummer], '') AS [Versicherung_Schadennummer]
      , isnull([FD].[Reklamation_Begruendung], '') AS [Reklamation_Begruendung]
      , isnull([FD].[Rechtsanwalt_Begruendung], '') AS [Rechtsanwalt_Begruendung]
      , isnull([FD].[Rechtsanwalt_Aktenzeichen], '') AS [Rechtsanwalt_Aktenzeichen]

+ 2 - 1
templates/forderungen/details/queries/post/post_details.sql

@@ -18,9 +18,10 @@ SET [Bearbeitet] = 'J'
   , [Selbstbeteiligung_Betrag] = :Selbstbeteiligung_Betrag
   , [Mwst] = :Mwst
   , [Mwst_Betrag] = :Mwst_Betrag
-  , [Wiedervorlage] = NULLIF(:Wiedervorlage, '')
+  , [Wiedervorlage] = :Wiedervorlage
   , [Vers_Adresse_ID] = :Vers_Adresse_ID
   , [Versicherung_Typ] = :Versicherung_Typ
+  , [Schadennummer] = :Schadennummer
   , [Reklamation_Begruendung] = :Reklamation_Begruendung
   , [Rechtsanwalt_Begruendung] = :Rechtsanwalt_Begruendung
   , [Rechtsanwalt_Aktenzeichen] = :Rechtsanwalt_Aktenzeichen

+ 53 - 0
tests/test_navigation.py

@@ -238,6 +238,59 @@ class DetailReturnTests(unittest.TestCase):
         self.assertEqual(native.status_code, 303)
         self.assertEqual(native.headers["location"], self.return_url)
 
+    def test_rendered_save_and_back_supplies_all_sql_parameters(self):
+        import json
+        from html.parser import HTMLParser
+
+        class DetailForm(HTMLParser):
+            active = False
+
+            def __init__(self):
+                super().__init__()
+                self.values = {}
+                self.save_back = None
+
+            def handle_starttag(self, tag, attrs):
+                attrs = dict(attrs)
+                if tag == "form":
+                    self.active = attrs.get("id") == "formular"
+                if not self.active:
+                    return
+                if tag == "input" and attrs.get("name"):
+                    if attrs.get("type") != "checkbox" or "checked" in attrs:
+                        self.values[attrs["name"]] = attrs.get("value", "on")
+                if tag == "button" and "hx-vals" in attrs:
+                    self.save_back = attrs
+
+            def handle_endtag(self, tag):
+                if tag == "form":
+                    self.active = False
+
+        self.row['Versicherung_Schadennummer'] = 'SCH-123,45'
+        response = self.client.get('/app/forderungen/details/1_R1', params={'return_to': self.return_url})
+        self.assertEqual(response.status_code, 200)
+        form = DetailForm()
+        form.feed(response.text)
+        self.assertIsNotNone(form.save_back)
+        self.assertNotIn('"', form.save_back)
+        submitted = {**form.values, **json.loads(form.save_back['hx-vals'])}
+        submitted['Wiedervorlage'] = '2026-11-18'
+
+        def check_bindings(statement, values):
+            # Exercise SQLAlchemy's actual required-parameter validation; a plain
+            # MagicMock accepted the missing Schadennummer parameter unnoticed.
+            statement.compile().construct_params(values)
+            return MagicMock(first=lambda: (1,))
+
+        self.db.execute.side_effect = check_bindings
+        response = self.client.post('/app/forderungen/details/1_R1', data=submitted, headers={'HX-Request': 'true'})
+        self.assertEqual(response.status_code, 200)
+        self.db.commit.assert_called_once()
+        self.assertEqual(response.headers['HX-Redirect'], self.return_url)
+        update = next(call for call in self.db.execute.call_args_list if str(call.args[0]).lstrip().startswith('UPDATE'))
+        self.assertEqual(update.args[1]['Schadennummer'], 'SCH-123,45')
+        self.assertEqual(update.args[1]['Wiedervorlage'], date(2026, 11, 18))
+
     def test_failed_save_does_not_return_and_external_target_is_ignored(self):
         self.db.commit.side_effect = RuntimeError("test database unavailable")
         response = self.client.post(