267 lines
9.8 KiB
JavaScript
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;
|