import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { Pencil, Trash2, Plus, Save, X, ArrowLeft, TrendingUp } from 'lucide-react'
import { api } from '../utils/api'
/**
* AdminActivityMappingsPage - Manage activity_type → training_type mappings
* v9d Phase 1b - Learnable system (replaces hardcoded mappings)
*/
export default function AdminActivityMappingsPage() {
const nav = useNavigate()
const [mappings, setMappings] = useState([])
const [trainingTypes, setTrainingTypes] = useState([])
const [coverage, setCoverage] = useState(null)
const [loading, setLoading] = useState(true)
const [editingId, setEditingId] = useState(null)
const [formData, setFormData] = useState(null)
const [error, setError] = useState(null)
const [saving, setSaving] = useState(false)
const [filter, setFilter] = useState('all') // 'all', 'global', 'user'
useEffect(() => {
load()
}, [filter])
const load = () => {
setLoading(true)
Promise.all([
api.adminListActivityMappings(null, filter === 'global'),
api.listTrainingTypesFlat(),
api.adminGetMappingCoverage()
]).then(([mappingsData, typesData, coverageData]) => {
setMappings(mappingsData)
setTrainingTypes(typesData)
setCoverage(coverageData)
setLoading(false)
}).catch(err => {
console.error('Failed to load mappings:', err)
setError(err.message)
setLoading(false)
})
}
const startCreate = () => {
setFormData({
activity_type: '',
training_type_id: trainingTypes[0]?.id || null,
profile_id: '',
source: 'admin'
})
setEditingId('new')
}
const startEdit = (mapping) => {
setFormData({
activity_type: mapping.activity_type,
training_type_id: mapping.training_type_id,
profile_id: mapping.profile_id || '',
source: mapping.source
})
setEditingId(mapping.id)
}
const cancelEdit = () => {
setEditingId(null)
setFormData(null)
setError(null)
}
const handleSave = async () => {
if (!formData.activity_type || !formData.training_type_id) {
setError('Activity Type und Training Type sind Pflichtfelder')
return
}
setSaving(true)
setError(null)
try {
const payload = {
...formData,
profile_id: formData.profile_id || null
}
if (editingId === 'new') {
await api.adminCreateActivityMapping(payload)
} else {
await api.adminUpdateActivityMapping(editingId, {
training_type_id: payload.training_type_id,
profile_id: payload.profile_id,
source: payload.source
})
}
await load()
cancelEdit()
} catch (err) {
console.error('Save failed:', err)
setError(err.message)
} finally {
setSaving(false)
}
}
const handleDelete = async (id, activityType) => {
if (!confirm(`Mapping für "${activityType}" wirklich löschen?\n\nZukünftige Imports werden diesen Typ nicht mehr automatisch zuordnen.`)) {
return
}
try {
await api.adminDeleteActivityMapping(id)
await load()
} catch (err) {
alert('Löschen fehlgeschlagen: ' + err.message)
}
}
if (loading) {
return (
)
}
const coveragePercent = coverage ? Math.round((coverage.mapped_activities / coverage.total_activities) * 100) : 0
return (
nav('/settings')}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 6,
display: 'flex',
color: 'var(--text2)'
}}
>
Activity-Mappings
{error && (
{error}
)}
{/* Coverage Stats */}
{coverage && (
{coveragePercent}%
Zugeordnet
{coverage.mapped_activities}
Mit Typ
{coverage.unmapped_activities}
Ohne Typ
{coverage.unmapped_types} verschiedene Activity-Types noch nicht gemappt
)}
{/* Filter */}
setFilter('all')}
className={filter === 'all' ? 'btn btn-primary' : 'btn btn-secondary'}
style={{ flex: 1, fontSize: 12 }}
>
Alle ({mappings.length})
setFilter('global')}
className={filter === 'global' ? 'btn btn-primary' : 'btn btn-secondary'}
style={{ flex: 1, fontSize: 12 }}
>
Global
{/* Create new button */}
Neues Mapping anlegen
{/* New mapping form (only shown when creating) */}
{editingId === 'new' && formData && (
➕ Neues Mapping
Activity Type * (exakt wie in CSV)
setFormData({ ...formData, activity_type: e.target.value })}
placeholder="z.B. Traditionelles Krafttraining"
style={{ width: '100%' }}
autoFocus
/>
Groß-/Kleinschreibung beachten! Muss exakt mit CSV übereinstimmen.
Training Type *
setFormData({ ...formData, training_type_id: parseInt(e.target.value) })}
style={{ width: '100%' }}
>
{trainingTypes.map(type => (
{type.icon} {type.name_de} ({type.category})
))}
Profil-ID (leer = global)
setFormData({ ...formData, profile_id: e.target.value })}
placeholder="Leer lassen für globales Mapping"
style={{ width: '100%' }}
/>
Global = für alle User, sonst user-spezifisch
{saving ? (
<>
Speichere...
>
) : (
<>
Speichern
>
)}
Abbrechen
)}
{/* List with inline editing */}
{mappings.length === 0 ? (
Keine Mappings gefunden
) : (
{mappings.map(mapping => {
const isEditing = editingId === mapping.id
return (
{isEditing && formData ? (
/* Inline edit form */
✏️ Mapping bearbeiten
Training Type *
setFormData({ ...formData, training_type_id: parseInt(e.target.value) })}
style={{ width: '100%' }}
>
{trainingTypes.map(type => (
{type.icon} {type.name_de} ({type.category})
))}
{saving ? (
) : (
<>
Speichern
>
)}
Abbrechen
) : (
/* Normal view */
{mapping.icon}
{mapping.activity_type}
→ {mapping.training_type_name_de}
{mapping.profile_id && <> · User-spezifisch>}
{!mapping.profile_id && <> · Global>}
{mapping.source && <> · {mapping.source}>}
startEdit(mapping)}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 6,
color: 'var(--accent)'
}}
title="Bearbeiten"
>
handleDelete(mapping.id, mapping.activity_type)}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 6,
color: '#D85A30'
}}
title="Löschen"
>
)}
)
})}
)}
💡 Tipp: Das System lernt automatisch! Wenn du im Tab "Kategorisieren" Aktivitäten zuordnest, wird das Mapping gespeichert und beim nächsten Import automatisch angewendet.
)
}