Latest Version
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { X, User, Mail, Phone, Shield, Check, Save, Camera, Upload, Trash2 } from 'lucide-react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { SupabaseService } from '../services/supabaseService';
|
||||
|
||||
interface UserProfileModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const UserProfileModal: React.FC<UserProfileModalProps> = ({ isOpen, onClose }) => {
|
||||
const { profile, updateProfile } = useAuth();
|
||||
|
||||
const [fullName, setFullName] = useState(profile?.fullName || '');
|
||||
const [phone, setPhone] = useState(profile?.phone || '');
|
||||
const [avatarUrl, setAvatarUrl] = useState(profile?.avatarUrl || '');
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isUploadingPhoto, setIsUploadingPhoto] = useState(false);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
if (!isOpen || !profile) return null;
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setIsUploadingPhoto(true);
|
||||
|
||||
try {
|
||||
// 1. Try uploading to Supabase Storage
|
||||
const path = `avatars/${profile.id}_${Date.now()}.${file.name.split('.').pop() || 'jpg'}`;
|
||||
const storageUrl = await SupabaseService.uploadFileToStorage('equipment-files', path, file);
|
||||
|
||||
if (storageUrl) {
|
||||
setAvatarUrl(storageUrl);
|
||||
} else {
|
||||
// Fallback: Read as Data URL for immediate local preview & storage
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
if (event.target?.result) {
|
||||
setAvatarUrl(event.target.result as string);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Could not upload to storage bucket, using data URL fallback:', err);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
if (event.target?.result) {
|
||||
setAvatarUrl(event.target.result as string);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} finally {
|
||||
setIsUploadingPhoto(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSaving(true);
|
||||
await updateProfile({
|
||||
fullName,
|
||||
phone,
|
||||
avatarUrl
|
||||
});
|
||||
setIsSaving(false);
|
||||
setIsSaved(true);
|
||||
setTimeout(() => setIsSaved(false), 2500);
|
||||
};
|
||||
|
||||
const getRoleBadgeStyle = (role: string) => {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
case 'disponent':
|
||||
return { bg: 'rgba(56, 189, 248, 0.15)', border: '#38bdf8', color: '#38bdf8', label: 'Admin / Disponent' };
|
||||
case 'monteur':
|
||||
return { bg: 'rgba(74, 222, 128, 0.15)', border: '#4ade80', color: '#4ade80', label: 'Monteur / Außendienst' };
|
||||
case 'kunde':
|
||||
return { bg: 'rgba(250, 204, 21, 0.15)', border: '#facc15', color: '#facc15', label: 'Kunde' };
|
||||
default:
|
||||
return { bg: '#1e293b', border: '#64748b', color: '#94a3b8', label: role };
|
||||
}
|
||||
};
|
||||
|
||||
const badge = getRoleBadgeStyle(profile.role);
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(15, 23, 42, 0.8)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
padding: '16px'
|
||||
}}>
|
||||
<div className="modal-container" style={{
|
||||
backgroundColor: '#1e293b',
|
||||
border: '1px solid #334155',
|
||||
borderRadius: '16px',
|
||||
width: '100%',
|
||||
maxWidth: '480px',
|
||||
color: '#f8fafc',
|
||||
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.5)',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
padding: '20px 24px',
|
||||
borderBottom: '1px solid #334155',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
background: 'linear-gradient(to right, #1e293b, #0f172a)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={fullName}
|
||||
style={{
|
||||
width: '44px',
|
||||
height: '44px',
|
||||
borderRadius: '50%',
|
||||
objectFit: 'cover',
|
||||
border: '2px solid #0284c7'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div style={{
|
||||
width: '44px',
|
||||
height: '44px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#0284c7',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 700,
|
||||
fontSize: '1.2rem',
|
||||
color: '#ffffff'
|
||||
}}>
|
||||
{profile.fullName.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ margin: 0, fontSize: '1.1rem', fontWeight: 700 }}>Mein Profil</h3>
|
||||
<p style={{ margin: 0, fontSize: '0.8rem', color: '#94a3b8' }}>Profilbild & Benutzerkontodaten</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#94a3b8',
|
||||
cursor: 'pointer',
|
||||
padding: '4px',
|
||||
borderRadius: '6px'
|
||||
}}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content Form */}
|
||||
<form onSubmit={handleSave} style={{ padding: '24px', display: 'flex', flexDirection: 'column', gap: '18px' }}>
|
||||
{/* Avatar Upload Section */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
padding: '16px',
|
||||
backgroundColor: '#0f172a',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #334155'
|
||||
}}>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
<div style={{ position: 'relative', cursor: 'pointer' }} onClick={() => fileInputRef.current?.click()}>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt="Profilbild"
|
||||
style={{
|
||||
width: '64px',
|
||||
height: '64px',
|
||||
borderRadius: '50%',
|
||||
objectFit: 'cover',
|
||||
border: '2px solid #38bdf8'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div style={{
|
||||
width: '64px',
|
||||
height: '64px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#0284c7',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 700,
|
||||
color: '#ffffff'
|
||||
}}>
|
||||
{fullName.charAt(0).toUpperCase() || 'U'}
|
||||
</div>
|
||||
)}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#ea580c',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#ffffff',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.3)'
|
||||
}}>
|
||||
<Camera size={13} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '0.9rem', fontWeight: 600, color: '#f8fafc', marginBottom: '4px' }}>
|
||||
Profilfoto
|
||||
</div>
|
||||
<div style={{ fontSize: '0.78rem', color: '#94a3b8', marginBottom: '10px' }}>
|
||||
JPG, PNG oder WebP Format
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploadingPhoto}
|
||||
className="btn-secondary"
|
||||
style={{ padding: '6px 12px', fontSize: '0.78rem' }}
|
||||
>
|
||||
<Upload size={14} />
|
||||
<span>{isUploadingPhoto ? 'Lädt...' : 'Foto hochladen'}</span>
|
||||
</button>
|
||||
|
||||
{avatarUrl && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAvatarUrl('')}
|
||||
style={{
|
||||
padding: '6px 10px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid #334155',
|
||||
color: '#fca5a5',
|
||||
fontSize: '0.78rem',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>Entfernen</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Role Pill */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '10px 14px',
|
||||
backgroundColor: badge.bg,
|
||||
border: `1px solid ${badge.border}`,
|
||||
borderRadius: '10px'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: badge.color, fontSize: '0.85rem', fontWeight: 600 }}>
|
||||
<Shield size={16} />
|
||||
<span>Zugriffsrolle</span>
|
||||
</div>
|
||||
<span style={{
|
||||
backgroundColor: '#0f172a',
|
||||
color: badge.color,
|
||||
padding: '3px 8px',
|
||||
borderRadius: '20px',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 700
|
||||
}}>
|
||||
{badge.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Full Name */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, color: '#cbd5e1', marginBottom: '6px' }}>
|
||||
Vollständiger Name
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<User size={18} color="#64748b" style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)' }} />
|
||||
<input
|
||||
type="text"
|
||||
value={fullName}
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px 10px 42px',
|
||||
backgroundColor: '#0f172a',
|
||||
border: '1px solid #334155',
|
||||
borderRadius: '8px',
|
||||
color: '#f8fafc',
|
||||
fontSize: '0.9rem',
|
||||
boxSizing: 'border-box',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email (Readonly) */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, color: '#cbd5e1', marginBottom: '6px' }}>
|
||||
E-Mail Adresse (Schreibgeschützt)
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Mail size={18} color="#64748b" style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)' }} />
|
||||
<input
|
||||
type="email"
|
||||
disabled
|
||||
value={profile.email}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px 10px 42px',
|
||||
backgroundColor: '#0f172a',
|
||||
border: '1px solid #1e293b',
|
||||
borderRadius: '8px',
|
||||
color: '#64748b',
|
||||
fontSize: '0.9rem',
|
||||
boxSizing: 'border-box',
|
||||
cursor: 'not-allowed'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone */}
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, color: '#cbd5e1', marginBottom: '6px' }}>
|
||||
Telefonnummer
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Phone size={18} color="#64748b" style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)' }} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="0170 1234567"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px 10px 42px',
|
||||
backgroundColor: '#0f172a',
|
||||
border: '1px solid #334155',
|
||||
borderRadius: '8px',
|
||||
color: '#f8fafc',
|
||||
fontSize: '0.9rem',
|
||||
boxSizing: 'border-box',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '12px', marginTop: '10px' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="btn-ghost"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
className="btn-primary-cta"
|
||||
>
|
||||
{isSaved ? (
|
||||
<>
|
||||
<Check size={18} />
|
||||
<span>Gespeichert!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save size={18} />
|
||||
<span>{isSaving ? 'Speichert...' : 'Änderungen Speichern'}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user