feat: inline editing for activity mappings (improved UX)
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s

- Edit form now appears at the position of the item being edited
- No scrolling needed - stays at same location
- Matches ActivityPage inline editing behavior
- Visual indicator: Accent border when editing
- Create form still appears at top (separate from list)

Benefits:
- Better UX - no need to scroll to top
- Easier to find edited item after saving
- Consistent with rest of app

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-21 19:46:11 +01:00
parent 829edecbdc
commit 3be82dc8c2

View File

@ -195,7 +195,6 @@ export default function AdminActivityMappingsPage() {
</div> </div>
{/* Create new button */} {/* Create new button */}
{!editingId && (
<button <button
onClick={startCreate} onClick={startCreate}
className="btn btn-primary btn-full" className="btn btn-primary btn-full"
@ -203,14 +202,11 @@ export default function AdminActivityMappingsPage() {
> >
<Plus size={16} /> Neues Mapping anlegen <Plus size={16} /> Neues Mapping anlegen
</button> </button>
)}
{/* Edit form */} {/* New mapping form (only shown when creating) */}
{editingId && formData && ( {editingId === 'new' && formData && (
<div className="card" style={{ padding: 16, marginBottom: 16 }}> <div className="card" style={{ padding: 16, marginBottom: 16, border: '2px solid var(--accent)' }}>
<div style={{ fontWeight: 600, marginBottom: 12 }}> <div style={{ fontWeight: 600, marginBottom: 12 }}> Neues Mapping</div>
{editingId === 'new' ? ' Neues Mapping' : '✏️ Mapping bearbeiten'}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div> <div>
@ -220,8 +216,8 @@ export default function AdminActivityMappingsPage() {
value={formData.activity_type} value={formData.activity_type}
onChange={e => setFormData({ ...formData, activity_type: e.target.value })} onChange={e => setFormData({ ...formData, activity_type: e.target.value })}
placeholder="z.B. Traditionelles Krafttraining" placeholder="z.B. Traditionelles Krafttraining"
disabled={editingId !== 'new'}
style={{ width: '100%' }} style={{ width: '100%' }}
autoFocus
/> />
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 4 }}> <div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 4 }}>
Groß-/Kleinschreibung beachten! Muss exakt mit CSV übereinstimmen. Groß-/Kleinschreibung beachten! Muss exakt mit CSV übereinstimmen.
@ -289,19 +285,101 @@ export default function AdminActivityMappingsPage() {
</div> </div>
)} )}
{/* List */} {/* List with inline editing */}
{mappings.length === 0 ? ( {mappings.length === 0 ? (
<div className="card" style={{ padding: 40, textAlign: 'center', color: 'var(--text3)' }}> <div className="card" style={{ padding: 40, textAlign: 'center', color: 'var(--text3)' }}>
Keine Mappings gefunden Keine Mappings gefunden
</div> </div>
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{mappings.map(mapping => ( {mappings.map(mapping => {
const isEditing = editingId === mapping.id
return (
<div <div
key={mapping.id} key={mapping.id}
className="card" className="card"
style={{ padding: 12 }} style={{
padding: 12,
border: isEditing ? '2px solid var(--accent)' : undefined
}}
> >
{isEditing && formData ? (
/* Inline edit form */
<div>
<div style={{ fontWeight: 600, marginBottom: 12, color: 'var(--accent)' }}>
Mapping bearbeiten
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div>
<div className="form-label">Activity Type (nicht änderbar)</div>
<input
className="form-input"
value={formData.activity_type}
disabled
style={{ width: '100%', background: 'var(--surface2)' }}
/>
</div>
<div>
<div className="form-label">Training Type *</div>
<select
className="form-input"
value={formData.training_type_id}
onChange={e => setFormData({ ...formData, training_type_id: parseInt(e.target.value) })}
style={{ width: '100%' }}
>
{trainingTypes.map(type => (
<option key={type.id} value={type.id}>
{type.icon} {type.name_de} ({type.category})
</option>
))}
</select>
</div>
<div>
<div className="form-label">Profil-ID (leer = global)</div>
<input
className="form-input"
value={formData.profile_id}
onChange={e => setFormData({ ...formData, profile_id: e.target.value })}
placeholder="Leer lassen für globales Mapping"
style={{ width: '100%' }}
/>
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<button
onClick={handleSave}
disabled={saving}
className="btn btn-primary"
style={{ flex: 1 }}
>
{saving ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'center' }}>
<div className="spinner" style={{ width: 14, height: 14 }} />
Speichere...
</div>
) : (
<>
<Save size={16} /> Speichern
</>
)}
</button>
<button
onClick={cancelEdit}
disabled={saving}
className="btn btn-secondary"
style={{ flex: 1 }}
>
<X size={16} /> Abbrechen
</button>
</div>
</div>
</div>
) : (
/* Normal view */
<div style={{ <div style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -346,8 +424,10 @@ export default function AdminActivityMappingsPage() {
<Trash2 size={16} /> <Trash2 size={16} />
</button> </button>
</div> </div>
)}
</div> </div>
))} )
})}
</div> </div>
)} )}