Files
schichtplanung/frontend/src/components/ProfileModal.jsx
T

246 lines
9.9 KiB
React

import React, { useState, useEffect } from 'react';
import { X, User, Award, Check, AlertCircle, CheckCircle2 } from 'lucide-react';
import { apiFetch } from '../api/client';
import { getRandomSkillColor } from '../utils/colors';
export default function ProfileModal({ user, skills = [], onClose, onUserUpdated, onRefreshSkills }) {
const safeSkills = Array.isArray(skills) ? skills : [];
const safeUserSkills = user?.skills ? (Array.isArray(user.skills) ? user.skills : []) : [];
const [displayName, setDisplayName] = useState(
user?.display_name || user?.username || localStorage.getItem('guest_display_name') || ''
);
const [selectedSkillIds, setSelectedSkillIds] = useState(
safeUserSkills.map(s => (typeof s === 'object' ? s.id : s))
);
useEffect(() => {
if (user) {
setDisplayName(user.display_name || user.username || '');
const uSkills = user.skills ? (Array.isArray(user.skills) ? user.skills : []) : [];
setSelectedSkillIds(uSkills.map(s => (typeof s === 'object' ? s.id : s)));
} else {
setDisplayName(localStorage.getItem('guest_display_name') || '');
setSelectedSkillIds([]);
}
}, [user]);
// New skill fields
const [newSkillName, setNewSkillName] = useState('');
const [newSkillDesc, setNewSkillDesc] = useState('');
const [newSkillColor, setNewSkillColor] = useState(getRandomSkillColor());
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 {
if (user) {
const updatedUser = await apiFetch('/users/me/', {
method: 'PATCH',
body: JSON.stringify({
display_name: displayName.trim(),
skill_ids: selectedSkillIds
})
});
setSuccess('Profil und Anzeigename erfolgreich aktualisiert!');
if (onUserUpdated) onUserUpdated(updatedUser);
setTimeout(() => onClose(), 800);
} else {
localStorage.setItem('guest_display_name', displayName.trim());
window.dispatchEvent(new Event('guest_name_updated'));
setSuccess('Gast-Name erfolgreich gespeichert!');
setTimeout(() => onClose(), 600);
}
} 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('');
setNewSkillColor(getRandomSkillColor());
setSuccess('Neue Fähigkeit erfolgreich angelegt!');
if (onRefreshSkills) 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-black/70 backdrop-blur-sm overflow-y-auto animate-in fade-in duration-200">
<div className="bg-surface w-full max-w-lg rounded-sm border border-grid p-6 shadow-2xl relative my-8 text-main font-sans max-h-[90vh] overflow-y-auto">
{/* Close Button */}
<button
onClick={onClose}
className="absolute top-4 right-4 p-2 text-muted hover:text-main rounded-sm transition"
title="Schließen"
>
<X className="w-5 h-5" />
</button>
{/* Modal Header */}
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-grid font-mono">
<div className="w-10 h-10 rounded-sm bg-subtle text-amber-500 border border-grid flex items-center justify-center font-bold">
<User className="w-5 h-5" />
</div>
<div>
<h3 className="font-serif font-bold text-xl uppercase tracking-tight text-main">
{user ? 'Profil & Qualifikationen' : 'Gast-Name anpassen'}
</h3>
<p className="text-xs text-muted">
{user ? 'Anzeigename & persönliche Qualifikationen verwalten' : 'Passe deinen Namen für Schichteintragungen an'}
</p>
</div>
</div>
{error && (
<div className="mb-4 p-3 rounded-sm bg-red-500/10 border border-red-500/20 text-red-400 text-xs font-mono 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-sm bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs font-mono flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 shrink-0" />
<span>{success}</span>
</div>
)}
<form onSubmit={handleSaveProfile} className="space-y-5 font-mono text-xs">
{/* Display Name Edit Field */}
<div>
<label className="block text-xs font-bold text-main mb-1 uppercase">
Dein Anzeigename
</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-sm bg-subtle border border-grid text-main text-sm focus:outline-none focus:border-muted font-sans"
/>
<p className="text-[11px] text-muted font-sans mt-1">
Wird in allen Schichtplänen bei deinen Eintragungen angezeigt.
</p>
</div>
{/* User Skills Selection (Only for logged-in users) */}
{user && (
<div className="space-y-2 pt-2 border-t border-grid">
<label className="block text-xs font-bold text-main uppercase">
Deine Qualifikationen & Fähigkeiten
</label>
{safeSkills.length === 0 ? (
<p className="text-xs text-muted italic font-sans">[ Noch keine Qualifikationen im System angelegt ]</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{safeSkills.map((s) => {
const isSelected = selectedSkillIds.includes(s.id);
return (
<div
key={s.id}
onClick={() => toggleSkill(s.id)}
className={`p-2.5 rounded-sm border cursor-pointer transition flex items-center justify-between font-mono ${
isSelected
? 'bg-surface-hover border-grid text-main font-bold shadow-sm'
: 'bg-subtle border-grid/60 text-muted hover:text-main hover:border-grid'
}`}
>
<div className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: s.color }}></span>
<span className="text-xs truncate">{s.name}</span>
</div>
{isSelected && <Check className="w-3.5 h-3.5 text-emerald-400 shrink-0" />}
</div>
);
})}
</div>
)}
</div>
)}
<div className="pt-3 flex justify-end">
<button
type="submit"
disabled={savingProfile}
className="px-5 py-2.5 rounded-sm text-xs font-bold bg-indigo-600 hover:bg-indigo-500 text-white transition shadow-sm disabled:opacity-50"
>
{savingProfile ? '[ SPEICHERN... ]' : '[ PROFIL SPEICHERN ]'}
</button>
</div>
</form>
{/* Add New Skill Section (For Admins / Staff) */}
{user && (user.is_admin_user || user.is_staff || user.is_superuser) && (
<form onSubmit={handleCreateSkill} className="border-t border-grid mt-6 pt-5 space-y-3 font-mono text-xs">
<h4 className="font-bold text-xs text-muted uppercase tracking-wider flex items-center gap-1.5">
<Award className="w-3.5 h-3.5 text-amber-500" /> Neue Qualifikation im System anlegen
</h4>
<div className="grid grid-cols-1 sm:grid-cols-12 gap-2">
<input
type="text"
required
placeholder="Fähigkeit (z. B. Ersthelfer)"
value={newSkillName}
onChange={(e) => setNewSkillName(e.target.value)}
className="sm:col-span-7 px-3 py-2 rounded-sm bg-subtle border border-grid text-main text-xs font-sans"
/>
<input
type="color"
value={newSkillColor}
onChange={(e) => setNewSkillColor(e.target.value)}
className="sm:col-span-2 h-8 w-full rounded-sm border border-grid cursor-pointer bg-subtle p-0.5"
/>
<button
type="submit"
disabled={creatingSkill}
className="sm:col-span-3 px-3 py-1.5 rounded-sm text-xs font-bold bg-surface hover:bg-surface-hover text-main border border-grid transition"
>
{creatingSkill ? '[ ANLEGEN... ]' : '[ ANLEGEN ]'}
</button>
</div>
</form>
)}
</div>
</div>
);
}