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

267 lines
9.8 KiB
JavaScript

import React, { useState, useEffect, useRef } from 'react';
import { useConfigContext } from '../../contexts/ConfigContext';
import './UserForm.css';
const formatSuggestionAddress = (addr) => {
const street = [addr.road, addr.house_number].filter(Boolean).join(' ');
const place = addr.city || addr.town || addr.village || addr.municipality || addr.hamlet || '';
const postAndPlace = addr.postcode && place ? `${addr.postcode} ${place}` : (addr.postcode || place);
return [street, postAndPlace].filter(Boolean).join(', ');
};
const UserForm = ({ user, onSave, onCancel }) => {
const { userTypes, userTypeLabels } = useConfigContext();
const typeOptions = Object.keys(userTypes);
const [formData, setFormData] = useState({
name: '',
address: '',
phone: '',
landline: '',
email: '',
type: typeOptions[0] || 'HS',
available: true,
gps: { lat: '', lng: '' }
});
const [errors, setErrors] = useState({});
const [suggestions, setSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [gpsAutoSet, setGpsAutoSet] = useState(false);
const searchTimer = useRef(null);
useEffect(() => {
if (user) {
setFormData({
name: user.name || '',
address: user.address || '',
phone: user.phone || '',
landline: user.landline || '',
email: user.email || '',
type: user.type || (typeOptions[0] || 'HS'),
available: user.available !== undefined ? user.available : true,
gps: {
lat: user.gps?.lat?.toString() || '',
lng: user.gps?.lng?.toString() || ''
}
});
} else {
setFormData({
name: '',
address: '',
phone: '',
landline: '',
email: '',
type: typeOptions[0] || 'HS',
available: true,
gps: { lat: '', lng: '' }
});
}
setErrors({});
setGpsAutoSet(false);
}, [user]);
const validate = () => {
const newErrors = {};
if (!formData.name.trim()) newErrors.name = 'Name ist erforderlich';
if (!formData.address.trim()) newErrors.address = 'Adresse ist erforderlich';
if (!formData.phone.trim()) newErrors.phone = 'Telefonnummer ist erforderlich';
if (formData.gps.lat && (isNaN(formData.gps.lat) || formData.gps.lat < -90 || formData.gps.lat > 90))
newErrors.gpsLat = 'Latitude muss zwischen -90 und 90 liegen';
if (formData.gps.lng && (isNaN(formData.gps.lng) || formData.gps.lng < -180 || formData.gps.lng > 180))
newErrors.gpsLng = 'Longitude muss zwischen -180 und 180 liegen';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (!validate()) return;
const submitData = {
name: formData.name.trim(),
address: formData.address.trim(),
phone: formData.phone.trim(),
landline: formData.landline.trim() || null,
email: formData.email.trim().toLowerCase() || null,
type: formData.type,
available: formData.available
};
if (formData.gps.lat && formData.gps.lng) {
submitData.gps = {
lat: parseFloat(formData.gps.lat),
lng: parseFloat(formData.gps.lng)
};
}
onSave(submitData);
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
if (name === 'lat' || name === 'lng') {
setFormData(prev => ({ ...prev, gps: { ...prev.gps, [name]: value } }));
setGpsAutoSet(false);
} else {
setFormData(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value }));
}
if (errors[name]) {
setErrors(prev => { const n = { ...prev }; delete n[name]; return n; });
}
};
const handleAddressChange = (e) => {
const value = e.target.value;
setFormData(prev => ({ ...prev, address: value }));
setGpsAutoSet(false);
if (errors.address) setErrors(prev => { const n = { ...prev }; delete n.address; return n; });
clearTimeout(searchTimer.current);
if (value.trim().length < 4) {
setSuggestions([]);
setShowSuggestions(false);
return;
}
searchTimer.current = setTimeout(async () => {
try {
const res = await fetch(
`https://nominatim.openstreetmap.org/search?format=json&countrycodes=de&addressdetails=1&limit=5&q=${encodeURIComponent(value)}`,
{ headers: { 'User-Agent': 'stoeberhunde-app/1.0 (admin@kasimirat.de)' } }
);
const data = await res.json();
setSuggestions(Array.isArray(data) ? data : []);
setShowSuggestions(true);
} catch {
setSuggestions([]);
}
}, 400);
};
const selectSuggestion = (s) => {
const formatted = formatSuggestionAddress(s.address);
setFormData(prev => ({
...prev,
address: formatted || s.display_name,
gps: { lat: parseFloat(s.lat).toFixed(7), lng: parseFloat(s.lon).toFixed(7) }
}));
setGpsAutoSet(true);
setSuggestions([]);
setShowSuggestions(false);
};
return (
<form className="user-form" onSubmit={handleSubmit}>
<h3>{user ? 'Stöberhundeführer bearbeiten' : 'Neuen Stöberhundeführer erstellen'}</h3>
<div className="form-group">
<label htmlFor="name">Name <span className="required">*</span></label>
<input type="text" id="name" name="name" value={formData.name} onChange={handleChange}
className={errors.name ? 'error' : ''} required />
{errors.name && <span className="error-message">{errors.name}</span>}
</div>
<div className="form-group">
<label htmlFor="address">Adresse <span className="required">*</span></label>
<div className="address-autocomplete">
<input
type="text"
id="address"
name="address"
value={formData.address}
onChange={handleAddressChange}
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
onFocus={() => suggestions.length > 0 && setShowSuggestions(true)}
className={errors.address ? 'error' : ''}
placeholder="z.B. Hauptstraße 51, 29683 Bad Fallingbostel"
autoComplete="off"
required
/>
{showSuggestions && suggestions.length > 0 && (
<ul className="address-suggestions">
{suggestions.map((s, i) => {
const main = formatSuggestionAddress(s.address);
const detail = [s.address.county, s.address.state].filter(Boolean).join(', ');
return (
<li key={i} onMouseDown={() => selectSuggestion(s)}>
<span className="suggestion-main">{main || s.display_name}</span>
{detail && <span className="suggestion-detail">{detail}</span>}
</li>
);
})}
</ul>
)}
</div>
{errors.address && <span className="error-message">{errors.address}</span>}
</div>
<div className="form-group">
<label htmlFor="phone">Mobilnummer <span className="required">*</span></label>
<input type="tel" id="phone" name="phone" value={formData.phone} onChange={handleChange}
className={errors.phone ? 'error' : ''} required />
{errors.phone && <span className="error-message">{errors.phone}</span>}
</div>
<div className="form-group">
<label htmlFor="landline">Festnetz</label>
<input type="tel" id="landline" name="landline" value={formData.landline} onChange={handleChange} />
</div>
<div className="form-group">
<label htmlFor="email">E-Mail (für Stöberhundeführer-Login)</label>
<input type="email" id="email" name="email" value={formData.email} onChange={handleChange}
placeholder="optional" />
</div>
<div className="form-group">
<label htmlFor="type">Hunderasse <span className="required">*</span></label>
<select id="type" name="type" value={formData.type} onChange={handleChange} required>
{typeOptions.length > 0
? typeOptions.map(code => (
<option key={code} value={code}>{userTypeLabels[code] || code}</option>
))
: <option value="SH">Stöberhundleiter</option>
}
</select>
</div>
<div className="form-group">
<label>
<input type="checkbox" name="available" checked={formData.available} onChange={handleChange} />
{' '}Verfügbar
</label>
</div>
<div className="form-group">
<label>GPS-Koordinaten</label>
{gpsAutoSet ? (
<p className="gps-hint gps-hint-success"> GPS automatisch aus Adresse übernommen</p>
) : (
<p className="gps-hint">Wird automatisch gesetzt wenn Adresse aus Vorschlägen gewählt wird.</p>
)}
<div className="gps-inputs">
<div>
<label htmlFor="lat">Latitude</label>
<input type="number" id="lat" name="lat" value={formData.gps.lat} onChange={handleChange}
step="any" placeholder="-90 bis 90" className={errors.gpsLat ? 'error' : ''} />
{errors.gpsLat && <span className="error-message">{errors.gpsLat}</span>}
</div>
<div>
<label htmlFor="lng">Longitude</label>
<input type="number" id="lng" name="lng" value={formData.gps.lng} onChange={handleChange}
step="any" placeholder="-180 bis 180" className={errors.gpsLng ? 'error' : ''} />
{errors.gpsLng && <span className="error-message">{errors.gpsLng}</span>}
</div>
</div>
</div>
<div className="form-actions">
<button type="submit" className="btn-primary">{user ? 'Speichern' : 'Erstellen'}</button>
<button type="button" className="btn-secondary" onClick={onCancel}>Abbrechen</button>
</div>
</form>
);
};
export default UserForm;