fix(feedback): inline edit + live status change without reload
- Replaced broken status_aendern (missing status_choices in response)
with a single eintrag_bearbeiten view that always returns the full
partial context
- eintrag_zeile.html is now a <tbody x-data="{ editing: false }"> with
two rows: display row + collapsible edit form
- Click anywhere on a row to expand the edit form; @click.stop on the
status cell prevents accidental toggles
- Status dropdown in the display row posts via HTMX and swaps the whole
<tbody> — no page reload needed
- Edit form covers all fields: titel, beschreibung, kategorie,
dringlichkeit, status, bewertung, entscheidung
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
40739c1bfd
commit
40e70e64f0
4 changed files with 145 additions and 56 deletions
|
|
@ -8,5 +8,5 @@ urlpatterns = [
|
||||||
path('modal/', views.modal, name='modal'),
|
path('modal/', views.modal, name='modal'),
|
||||||
path('', views.submit, name='submit'),
|
path('', views.submit, name='submit'),
|
||||||
path('backlog/', views.backlog, name='backlog'),
|
path('backlog/', views.backlog, name='backlog'),
|
||||||
path('backlog/<int:pk>/status/', views.status_aendern, name='status_aendern'),
|
path('backlog/<int:pk>/bearbeiten/', views.eintrag_bearbeiten, name='eintrag_bearbeiten'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -65,17 +65,32 @@ def backlog(request):
|
||||||
return render(request, 'feedback/backlog.html', ctx)
|
return render(request, 'feedback/backlog.html', ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def _eintrag_ctx(eintrag):
|
||||||
|
return {
|
||||||
|
'eintrag': eintrag,
|
||||||
|
'status_choices': Feedbackeintrag.STATUS_CHOICES,
|
||||||
|
'kategorie_choices': Feedbackeintrag.KATEGORIE_CHOICES,
|
||||||
|
'dringlichkeit_choices': Feedbackeintrag.DRINGLICHKEIT_CHOICES,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def status_aendern(request, pk):
|
def eintrag_bearbeiten(request, pk):
|
||||||
eintrag = get_object_or_404(Feedbackeintrag, pk=pk)
|
eintrag = get_object_or_404(Feedbackeintrag, pk=pk)
|
||||||
neuer_status = request.POST.get('status')
|
for field, choices in [
|
||||||
if neuer_status in dict(Feedbackeintrag.STATUS_CHOICES):
|
('status', Feedbackeintrag.STATUS_CHOICES),
|
||||||
eintrag.status = neuer_status
|
('kategorie', Feedbackeintrag.KATEGORIE_CHOICES),
|
||||||
bewertung = request.POST.get('bewertung')
|
('dringlichkeit', Feedbackeintrag.DRINGLICHKEIT_CHOICES),
|
||||||
if bewertung is not None:
|
]:
|
||||||
eintrag.bewertung = bewertung
|
val = request.POST.get(field)
|
||||||
entscheidung = request.POST.get('entscheidung')
|
if val and val in dict(choices):
|
||||||
if entscheidung is not None:
|
setattr(eintrag, field, val)
|
||||||
eintrag.entscheidung = entscheidung
|
for field in ('beschreibung', 'bewertung', 'entscheidung'):
|
||||||
|
val = request.POST.get(field)
|
||||||
|
if val is not None:
|
||||||
|
setattr(eintrag, field, val)
|
||||||
|
titel = request.POST.get('titel', '').strip()
|
||||||
|
if titel:
|
||||||
|
eintrag.titel = titel
|
||||||
eintrag.save()
|
eintrag.save()
|
||||||
return render(request, 'feedback/partials/eintrag_zeile.html', {'eintrag': eintrag})
|
return render(request, 'feedback/partials/eintrag_zeile.html', _eintrag_ctx(eintrag))
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,9 @@
|
||||||
<th class="pb-2 font-medium text-slate-600">Datum</th>
|
<th class="pb-2 font-medium text-slate-600">Datum</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
{% for eintrag in eintraege %}
|
||||||
{% for eintrag in eintraege %}
|
{% include "feedback/partials/eintrag_zeile.html" %}
|
||||||
{% include "feedback/partials/eintrag_zeile.html" with status_choices=status_choices %}
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-slate-500 text-sm">Keine Einträge gefunden.</p>
|
<p class="text-slate-500 text-sm">Keine Einträge gefunden.</p>
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,115 @@
|
||||||
<tr id="eintrag-{{ eintrag.pk }}" class="border-b border-slate-100 hover:bg-slate-50 text-sm">
|
<tbody id="eintrag-wrapper-{{ eintrag.pk }}" x-data="{ editing: false }">
|
||||||
<td class="py-2 font-medium text-slate-800 max-w-xs">
|
|
||||||
{{ eintrag.titel|truncatechars:60 }}
|
{# display row #}
|
||||||
{% if eintrag.ausschreibung %}
|
<tr class="border-b border-slate-100 hover:bg-slate-50 text-sm cursor-pointer select-none"
|
||||||
<span class="text-xs text-slate-400 block">{{ eintrag.ausschreibung.titel|truncatechars:40 }}</span>
|
@click="editing = !editing">
|
||||||
{% endif %}
|
<td class="py-2 pr-3 font-medium text-slate-800 max-w-xs">
|
||||||
</td>
|
<span class="flex items-start gap-1.5">
|
||||||
<td class="py-2 text-slate-500 max-w-xs text-xs">{{ eintrag.beschreibung|truncatechars:100 }}</td>
|
<span class="mt-0.5 text-slate-300 text-xs shrink-0" x-text="editing ? '▾' : '▸'"></span>
|
||||||
<td class="py-2">
|
<span>
|
||||||
<span class="inline-block rounded-full px-2 py-0.5 text-xs
|
{{ eintrag.titel }}
|
||||||
{% if eintrag.kategorie == 'fehler' %}bg-red-100 text-red-700
|
{% if eintrag.ausschreibung %}
|
||||||
{% elif eintrag.kategorie == 'verbesserung' %}bg-blue-100 text-blue-700
|
<span class="text-xs text-slate-400 block font-normal">{{ eintrag.ausschreibung.titel|truncatechars:40 }}</span>
|
||||||
{% else %}bg-slate-100 text-slate-700{% endif %}">
|
{% endif %}
|
||||||
{{ eintrag.get_kategorie_display }}
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2">
|
<td class="py-2 pr-3 text-slate-500 text-xs max-w-xs">{{ eintrag.beschreibung|truncatechars:120 }}</td>
|
||||||
<span class="inline-block rounded-full px-2 py-0.5 text-xs
|
<td class="py-2 pr-3">
|
||||||
{% if eintrag.dringlichkeit == 'kritisch' %}bg-red-100 text-red-700
|
<span class="inline-block rounded-full px-2 py-0.5 text-xs
|
||||||
{% elif eintrag.dringlichkeit == 'hoch' %}bg-orange-100 text-orange-700
|
{% if eintrag.kategorie == 'fehler' %}bg-red-100 text-red-700
|
||||||
{% elif eintrag.dringlichkeit == 'mittel' %}bg-amber-100 text-amber-700
|
{% elif eintrag.kategorie == 'verbesserung' %}bg-blue-100 text-blue-700
|
||||||
{% else %}bg-slate-100 text-slate-600{% endif %}">
|
{% else %}bg-slate-100 text-slate-700{% endif %}">
|
||||||
{{ eintrag.get_dringlichkeit_display }}
|
{{ eintrag.get_kategorie_display }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2">
|
<td class="py-2 pr-3">
|
||||||
<form hx-post="/feedback/backlog/{{ eintrag.pk }}/status/"
|
<span class="inline-block rounded-full px-2 py-0.5 text-xs
|
||||||
hx-target="#eintrag-{{ eintrag.pk }}"
|
{% if eintrag.dringlichkeit == 'kritisch' %}bg-red-100 text-red-700
|
||||||
hx-swap="outerHTML">
|
{% elif eintrag.dringlichkeit == 'hoch' %}bg-orange-100 text-orange-700
|
||||||
{% csrf_token %}
|
{% elif eintrag.dringlichkeit == 'mittel' %}bg-amber-100 text-amber-700
|
||||||
<select name="status" onchange="this.form.requestSubmit()" class="text-xs border-slate-200 rounded p-1">
|
{% else %}bg-slate-100 text-slate-600{% endif %}">
|
||||||
{% for val, label in status_choices %}
|
{{ eintrag.get_dringlichkeit_display }}
|
||||||
<option value="{{ val }}" {% if val == eintrag.status %}selected{% endif %}>{{ label }}</option>
|
</span>
|
||||||
{% endfor %}
|
</td>
|
||||||
</select>
|
<td class="py-2 pr-3" @click.stop>
|
||||||
</form>
|
<form hx-post="/feedback/backlog/{{ eintrag.pk }}/bearbeiten/"
|
||||||
</td>
|
hx-target="#eintrag-wrapper-{{ eintrag.pk }}"
|
||||||
<td class="py-2 text-xs text-slate-500">{{ eintrag.datum|date:"d.m.Y" }}</td>
|
hx-swap="outerHTML">
|
||||||
</tr>
|
{% csrf_token %}
|
||||||
|
<select name="status" onchange="this.form.requestSubmit()"
|
||||||
|
class="text-xs border border-slate-200 rounded p-1 bg-white">
|
||||||
|
{% for val, label in status_choices %}
|
||||||
|
<option value="{{ val }}" {% if val == eintrag.status %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 text-xs text-slate-500 whitespace-nowrap">{{ eintrag.datum|date:"d.m.Y" }}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{# edit row #}
|
||||||
|
<tr x-show="editing" x-cloak class="bg-slate-50 border-b border-slate-200">
|
||||||
|
<td colspan="6" class="px-6 pt-3 pb-4">
|
||||||
|
<form hx-post="/feedback/backlog/{{ eintrag.pk }}/bearbeiten/"
|
||||||
|
hx-target="#eintrag-wrapper-{{ eintrag.pk }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="grid grid-cols-3 gap-3 mb-3">
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label class="form-label">Titel</label>
|
||||||
|
<input type="text" name="titel" value="{{ eintrag.titel }}"
|
||||||
|
class="form-input w-full text-sm" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label class="form-label">Beschreibung</label>
|
||||||
|
<textarea name="beschreibung" rows="3"
|
||||||
|
class="form-input w-full text-sm">{{ eintrag.beschreibung }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Kategorie</label>
|
||||||
|
<select name="kategorie" class="form-input text-sm">
|
||||||
|
{% for val, label in kategorie_choices %}
|
||||||
|
<option value="{{ val }}" {% if val == eintrag.kategorie %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Dringlichkeit</label>
|
||||||
|
<select name="dringlichkeit" class="form-input text-sm">
|
||||||
|
{% for val, label in dringlichkeit_choices %}
|
||||||
|
<option value="{{ val }}" {% if val == eintrag.dringlichkeit %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Status</label>
|
||||||
|
<select name="status" class="form-input text-sm">
|
||||||
|
{% for val, label in status_choices %}
|
||||||
|
<option value="{{ val }}" {% if val == eintrag.status %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label class="form-label">Bewertung / Notizen</label>
|
||||||
|
<textarea name="bewertung" rows="2"
|
||||||
|
class="form-input w-full text-sm"
|
||||||
|
placeholder="Interne Einschätzung...">{{ eintrag.bewertung }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label class="form-label">Entscheidung</label>
|
||||||
|
<textarea name="entscheidung" rows="2"
|
||||||
|
class="form-input w-full text-sm"
|
||||||
|
placeholder="Wie wird damit umgegangen?">{{ eintrag.entscheidung }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button type="submit" class="btn-primary text-sm py-1.5 px-4">Speichern</button>
|
||||||
|
<button type="button" @click="editing = false"
|
||||||
|
class="btn-secondary text-sm py-1.5 px-4">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue