Initial commit
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, User, Award, Check, AlertCircle } from 'lucide-react';
|
||||
import { apiFetch } from '../api/client';
|
||||
|
||||
export default function ProfileModal({ user, skills, onClose, onUserUpdated, onRefreshSkills }) {
|
||||
const [displayName, setDisplayName] = useState(user?.display_name || '');
|
||||
const [selectedSkillIds, setSelectedSkillIds] = useState(
|
||||
user?.skills ? user.skills.map(s => s.id) : []
|
||||
);
|
||||
|
||||
// New skill fields
|
||||
const [newSkillName, setNewSkillName] = useState('');
|
||||
const [newSkillDesc, setNewSkillDesc] = useState('');
|
||||
const [newSkillColor, setNewSkillColor] = useState('#2563eb');
|
||||
|
||||
const [savingProfile, setSavingProfile] = useState(false);
|
||||
const [creatingSkill, setCreatingSkill] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
|
||||
const handleSaveProfile = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setSuccess('');
|
||||
setSavingProfile(true);
|
||||
|
||||
try {
|
||||
const updatedUser = await apiFetch('/users/me/', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
display_name: displayName.trim(),
|
||||
skill_ids: selectedSkillIds
|
||||
})
|
||||
});
|
||||
setSuccess('Profil und Anzeigename erfolgreich aktualisiert!');
|
||||
onUserUpdated(updatedUser);
|
||||
} catch (err) {
|
||||
setError(err.message || 'Speichern des Profils fehlgeschlagen.');
|
||||
} finally {
|
||||
setSavingProfile(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSkill = (skillId) => {
|
||||
if (selectedSkillIds.includes(skillId)) {
|
||||
setSelectedSkillIds(selectedSkillIds.filter(id => id !== skillId));
|
||||
} else {
|
||||
setSelectedSkillIds([...selectedSkillIds, skillId]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateSkill = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!newSkillName.trim()) return;
|
||||
|
||||
setCreatingSkill(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
try {
|
||||
await apiFetch('/skills/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: newSkillName.trim(),
|
||||
description: newSkillDesc.trim(),
|
||||
color: newSkillColor
|
||||
})
|
||||
});
|
||||
setNewSkillName('');
|
||||
setNewSkillDesc('');
|
||||
setSuccess('Neue Fähigkeit erfolgreich angelegt!');
|
||||
onRefreshSkills();
|
||||
} catch (err) {
|
||||
setError(err.message || 'Erstellen der Fähigkeit fehlgeschlagen.');
|
||||
} finally {
|
||||
setCreatingSkill(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/70 backdrop-blur-sm overflow-y-auto">
|
||||
<div className="bg-slate-900 w-full max-w-lg rounded-2xl border border-slate-800 p-6 shadow-xl relative my-8">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 p-2 text-slate-400 hover:text-white rounded-lg transition"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-xl bg-slate-800 text-slate-200 border border-slate-700 flex items-center justify-center">
|
||||
<User className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-lg text-white">Profil & Einstellungen</h3>
|
||||
<p className="text-xs text-slate-400">Anzeigenamen und Qualifikationen verwalten</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-xs flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="mb-4 p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs flex items-center gap-2">
|
||||
<Check className="w-4 h-4 shrink-0" />
|
||||
<span>{success}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSaveProfile} className="space-y-5">
|
||||
{/* Display Name Edit Field */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||||
Dein Anzeigename (wird in allen Schichtlisten angezeigt)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="z. B. Alex Muster"
|
||||
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-950 border border-slate-800 text-white text-sm focus:outline-none focus:border-slate-600"
|
||||
/>
|
||||
<p className="text-[11px] text-slate-500 mt-1">
|
||||
Änderungen werden sofort in allen von dir gebuchten Schichten wirksam.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* User Skills Selection */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-semibold text-slate-300">
|
||||
Deine Qualifikationen & Fähigkeiten
|
||||
</label>
|
||||
{skills.length === 0 ? (
|
||||
<p className="text-xs text-slate-500 italic">Noch keine Fähigkeiten definiert.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{skills.map((s) => {
|
||||
const isSelected = selectedSkillIds.includes(s.id);
|
||||
return (
|
||||
<div
|
||||
key={s.id}
|
||||
onClick={() => toggleSkill(s.id)}
|
||||
className={`p-2.5 rounded-xl border cursor-pointer transition flex items-center justify-between ${
|
||||
isSelected
|
||||
? 'bg-slate-800 border-slate-700 text-white'
|
||||
: 'bg-slate-950/60 border-slate-800/80 text-slate-400 hover:border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: s.color }}></span>
|
||||
<span className="text-xs font-medium">{s.name}</span>
|
||||
</div>
|
||||
{isSelected && <Check className="w-3.5 h-3.5 text-emerald-400" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-2 flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={savingProfile}
|
||||
className="px-5 py-2.5 rounded-xl text-xs font-bold bg-slate-100 hover:bg-white text-slate-950 transition shadow-sm disabled:opacity-50"
|
||||
>
|
||||
{savingProfile ? 'Speichern...' : 'Profil speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Add New Skill Section */}
|
||||
<form onSubmit={handleCreateSkill} className="border-t border-slate-800 mt-6 pt-5 space-y-3">
|
||||
<h4 className="font-bold text-xs text-slate-400 uppercase tracking-wider flex items-center gap-1.5">
|
||||
<Award className="w-3.5 h-3.5" /> Neue Qualifikation im System anlegen
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Fähigkeit (z. B. Ersthelfer)"
|
||||
value={newSkillName}
|
||||
onChange={(e) => setNewSkillName(e.target.value)}
|
||||
className="sm:col-span-2 px-3 py-2 rounded-xl bg-slate-950 border border-slate-800 text-white text-xs"
|
||||
/>
|
||||
<input
|
||||
type="color"
|
||||
value={newSkillColor}
|
||||
onChange={(e) => setNewSkillColor(e.target.value)}
|
||||
className="w-full h-8 rounded-xl border-0 cursor-pointer bg-slate-950 p-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={creatingSkill}
|
||||
className="px-3.5 py-1.5 rounded-xl text-xs font-semibold bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 transition"
|
||||
>
|
||||
{creatingSkill ? 'Anlegen...' : 'Fähigkeit hinzufügen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user