jagd-apps/stoeberhunde/frontend/src/components/users/UserCard.js

250 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Stöberhundefü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;