250 lines
7.9 KiB
JavaScript
250 lines
7.9 KiB
JavaScript
import React, { useState, useRef } from 'react';
|
||
import { useConfigContext } from '../../contexts/ConfigContext';
|
||
import './UserCard.css';
|
||
|
||
const UserCard = ({ user, onAvailabilityToggle, onGPSUpdate, onEdit, onDelete, onPhotoUpload, onPhotoDelete, onGenerateInvite }) => {
|
||
const { userTypeLabels } = useConfigContext();
|
||
const [lat, setLat] = useState(user.gps?.lat?.toString() || '');
|
||
const [lng, setLng] = useState(user.gps?.lng?.toString() || '');
|
||
const [isEditingGPS, setIsEditingGPS] = useState(false);
|
||
const [photoUploading, setPhotoUploading] = useState(false);
|
||
const [invite, setInvite] = useState(null);
|
||
const [inviteLoading, setInviteLoading] = useState(false);
|
||
const photoInputRef = useRef(null);
|
||
|
||
const handleGPSUpdate = () => {
|
||
if (lat && lng) {
|
||
onGPSUpdate(user._id, parseFloat(lat), parseFloat(lng));
|
||
setIsEditingGPS(false);
|
||
}
|
||
};
|
||
|
||
const handleDelete = () => {
|
||
if (window.confirm(`Möchten Sie "${user.name}" wirklich löschen?`)) {
|
||
onDelete(user._id);
|
||
}
|
||
};
|
||
|
||
const handlePhotoUpload = (e) => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
if (!file.type.startsWith('image/')) {
|
||
alert('Nur Bilddateien erlaubt (JPG, PNG, ...)');
|
||
return;
|
||
}
|
||
if (file.size > 500 * 1024) {
|
||
alert('Foto darf max. 500 KB groß sein');
|
||
return;
|
||
}
|
||
setPhotoUploading(true);
|
||
const reader = new FileReader();
|
||
reader.onload = async (ev) => {
|
||
await onPhotoUpload(user._id, ev.target.result);
|
||
setPhotoUploading(false);
|
||
};
|
||
reader.readAsDataURL(file);
|
||
e.target.value = '';
|
||
};
|
||
|
||
const handlePhotoDelete = () => {
|
||
if (window.confirm('Foto wirklich entfernen?')) {
|
||
onPhotoDelete(user._id);
|
||
}
|
||
};
|
||
|
||
const handleGenerateInvite = async () => {
|
||
if (!onGenerateInvite) return;
|
||
setInviteLoading(true);
|
||
const result = await onGenerateInvite(user._id);
|
||
if (result?.success) {
|
||
setInvite(result.data);
|
||
}
|
||
setInviteLoading(false);
|
||
};
|
||
|
||
return (
|
||
<div className={`user-card-admin ${user.available ? 'card-available' : 'card-unavailable'}`}>
|
||
<div className="user-header">
|
||
<div className="user-name-row">
|
||
<span className={`status-dot ${user.available ? 'dot-green' : 'dot-red'}`} title={user.available ? 'Verfügbar' : 'Nicht verfügbar'} />
|
||
<h3 className="user-name">{user.name}</h3>
|
||
</div>
|
||
<label className="availability-toggle">
|
||
<input
|
||
type="checkbox"
|
||
checked={user.available}
|
||
onChange={(e) => onAvailabilityToggle(user._id, e.target.checked)}
|
||
/>
|
||
<span className={`toggle-label ${user.available ? 'label-green' : 'label-red'}`}>
|
||
{user.available ? '✅ Verfügbar' : '🔴 Nicht verfügbar'}
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div className="user-info">
|
||
<p className="user-address">📍 {user.address}</p>
|
||
<p className="user-type">
|
||
<span className="type-badge">{userTypeLabels[user.type] || user.type}</span>
|
||
</p>
|
||
<a href={`tel:${user.phone}`} className="user-phone">
|
||
📱 {user.phone}
|
||
</a>
|
||
{user.landline && (
|
||
<a href={`tel:${user.landline}`} className="user-phone">
|
||
📞 {user.landline}
|
||
</a>
|
||
)}
|
||
</div>
|
||
|
||
<div className="photo-section">
|
||
<h4>Foto</h4>
|
||
{user.photo ? (
|
||
<div className="photo-preview">
|
||
<img src={user.photo} alt={user.name} className="user-photo-thumb" />
|
||
</div>
|
||
) : (
|
||
<div className="user-photo-placeholder">Kein Foto</div>
|
||
)}
|
||
<div className="photo-actions">
|
||
<input
|
||
ref={photoInputRef}
|
||
type="file"
|
||
accept="image/*"
|
||
style={{ display: 'none' }}
|
||
onChange={handlePhotoUpload}
|
||
/>
|
||
<button
|
||
type="button"
|
||
className="btn btn-secondary btn-sm"
|
||
onClick={() => photoInputRef.current.click()}
|
||
disabled={photoUploading}
|
||
>
|
||
{photoUploading ? 'Lädt…' : user.photo ? 'Ersetzen' : 'Hochladen'}
|
||
</button>
|
||
{user.photo && (
|
||
<button type="button" className="btn btn-danger btn-sm" onClick={handlePhotoDelete}>
|
||
Entfernen
|
||
</button>
|
||
)}
|
||
</div>
|
||
<p className="settings-hint">Max. 500 KB, JPG oder PNG</p>
|
||
</div>
|
||
|
||
<div className="gps-section">
|
||
<h4>GPS-Koordinaten</h4>
|
||
{!isEditingGPS ? (
|
||
<div className="gps-display">
|
||
{user.gps && user.gps.lat && user.gps.lng ? (
|
||
<span>
|
||
{user.gps.lat.toFixed(6)}, {user.gps.lng.toFixed(6)}
|
||
</span>
|
||
) : (
|
||
<span className="no-gps">Keine GPS-Daten</span>
|
||
)}
|
||
<button
|
||
className="edit-gps-button"
|
||
onClick={() => setIsEditingGPS(true)}
|
||
>
|
||
GPS bearbeiten
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="gps-edit">
|
||
<div className="gps-inputs">
|
||
<input
|
||
type="number"
|
||
placeholder="Latitude"
|
||
value={lat}
|
||
onChange={(e) => setLat(e.target.value)}
|
||
step="any"
|
||
className="gps-input"
|
||
/>
|
||
<input
|
||
type="number"
|
||
placeholder="Longitude"
|
||
value={lng}
|
||
onChange={(e) => setLng(e.target.value)}
|
||
step="any"
|
||
className="gps-input"
|
||
/>
|
||
</div>
|
||
<div className="gps-actions">
|
||
<button
|
||
className="save-gps-button"
|
||
onClick={handleGPSUpdate}
|
||
disabled={!lat || !lng}
|
||
>
|
||
Speichern
|
||
</button>
|
||
<button
|
||
className="cancel-gps-button"
|
||
onClick={() => {
|
||
setIsEditingGPS(false);
|
||
setLat(user.gps?.lat?.toString() || '');
|
||
setLng(user.gps?.lng?.toString() || '');
|
||
}}
|
||
>
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Einladungs-Token: einmalig gültiger Code, mit dem der Hundeführer sein
|
||
eigenes Passwort setzt. Ohne hinterlegte E-Mail nicht möglich. */}
|
||
<div className="invite-section">
|
||
<h4>Zugang für den Drohnenführer</h4>
|
||
{!user.email ? (
|
||
<p className="settings-hint">
|
||
Keine E-Mail hinterlegt – ohne E-Mail ist kein Login möglich.
|
||
</p>
|
||
) : (
|
||
<>
|
||
<button
|
||
type="button"
|
||
className="btn btn-secondary btn-sm"
|
||
onClick={handleGenerateInvite}
|
||
disabled={inviteLoading}
|
||
>
|
||
{inviteLoading ? 'Erzeuge…' : '🔑 Einladungs-Token erzeugen'}
|
||
</button>
|
||
{invite && (
|
||
<div className="invite-result">
|
||
<p className="settings-hint">
|
||
Diesen Token an <strong>{invite.email}</strong> weitergeben. Gültig bis{' '}
|
||
{new Date(invite.expiresAt).toLocaleDateString('de-DE')}.
|
||
</p>
|
||
<code className="invite-token">{invite.inviteToken}</code>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="card-actions">
|
||
<button
|
||
className="edit-button"
|
||
onClick={() => {
|
||
if (onEdit) {
|
||
onEdit(user);
|
||
} else {
|
||
console.error('onEdit function is not defined');
|
||
}
|
||
}}
|
||
>
|
||
✏️ Bearbeiten
|
||
</button>
|
||
<button
|
||
className="delete-button"
|
||
onClick={handleDelete}
|
||
>
|
||
🗑️ Löschen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default UserCard;
|