640 lines
21 KiB
TypeScript
640 lines
21 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import { CreateButtonProps } from '../ButtonTypes';
|
|
import Button from '../Button';
|
|
import { Popup } from '../../Popup';
|
|
import { FormGeneratorForm, AttributeDefinition } from '../../../FormGenerator/FormGeneratorForm';
|
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
|
import { TextField } from '../../TextField';
|
|
import { PekProvider } from '../../../../contexts/PekContext';
|
|
import { MapView } from '../../MapView';
|
|
import { usePekContext } from '../../../../contexts/PekContext';
|
|
import { FaLocationArrow, FaTimes } from 'react-icons/fa';
|
|
import { IoMdSend } from 'react-icons/io';
|
|
import styles from './CreateButton.module.css';
|
|
|
|
// Step 2 component for parcel selection (must be inside PekProvider)
|
|
const Step2Content: React.FC<{
|
|
onNext: (data: any) => void;
|
|
onBack: () => void;
|
|
addressData: {
|
|
street: string;
|
|
postalCode: string;
|
|
city: string;
|
|
};
|
|
onAddressChange: (field: string, value: string) => void;
|
|
}> = ({ onNext, onBack, addressData, onAddressChange }) => {
|
|
const { t } = useLanguage();
|
|
const {
|
|
selectedParcels,
|
|
searchParcel,
|
|
useCurrentLocation,
|
|
isGettingLocation,
|
|
isSearchingParcel,
|
|
setAdresse,
|
|
mapCenter,
|
|
mapZoomBounds,
|
|
parcelGeometries,
|
|
handleMapClick,
|
|
handleParcelClick,
|
|
removeParcel,
|
|
setIsPanelOpen
|
|
} = usePekContext();
|
|
const [step2Errors, setStep2Errors] = useState<Record<string, string>>({});
|
|
|
|
// Prevent panel from opening when parcel is clicked
|
|
React.useEffect(() => {
|
|
setIsPanelOpen(false);
|
|
}, [selectedParcels, setIsPanelOpen]);
|
|
|
|
// Build location string from address fields
|
|
const buildLocationString = () => {
|
|
const parts = [];
|
|
if (addressData.street.trim()) parts.push(addressData.street.trim());
|
|
if (addressData.postalCode.trim()) parts.push(addressData.postalCode.trim());
|
|
if (addressData.city.trim()) parts.push(addressData.city.trim());
|
|
return parts.join(', ');
|
|
};
|
|
|
|
const handleSearch = async () => {
|
|
const locationString = buildLocationString();
|
|
if (locationString.trim()) {
|
|
// Update the adresse field in context for consistency
|
|
setAdresse(locationString);
|
|
await searchParcel(locationString.trim(), true);
|
|
}
|
|
};
|
|
|
|
const handleUseCurrentLocation = async () => {
|
|
await useCurrentLocation();
|
|
};
|
|
|
|
const handleNext = () => {
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
if (!addressData.street.trim()) {
|
|
newErrors.street = t('formgen.form.required', 'Strasse und Hausnummer ist erforderlich');
|
|
}
|
|
if (!addressData.postalCode.trim()) {
|
|
newErrors.postalCode = t('formgen.form.required', 'Postleitzahl ist erforderlich');
|
|
}
|
|
if (!addressData.city.trim()) {
|
|
newErrors.city = t('formgen.form.required', 'Stadt ist erforderlich');
|
|
}
|
|
if (!selectedParcels || selectedParcels.length === 0) {
|
|
newErrors.parcel = t('formgen.form.required', 'Bitte wählen Sie mindestens eine Parzelle aus');
|
|
}
|
|
|
|
setStep2Errors(newErrors);
|
|
|
|
if (Object.keys(newErrors).length === 0) {
|
|
onNext({
|
|
address: addressData,
|
|
parzellen: selectedParcels
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={styles.step2Container}>
|
|
<div className={styles.stepIndicator}>
|
|
<span className={styles.stepNumber}>2</span>
|
|
<span className={styles.stepLabel}>Parzelle hinzufügen</span>
|
|
</div>
|
|
|
|
<div className={styles.addressFields}>
|
|
<TextField
|
|
value={addressData.street}
|
|
onChange={(value) => onAddressChange('street', value)}
|
|
label="Strasse und Hausnummer"
|
|
placeholder="z.B. Bundesplatz 3"
|
|
required
|
|
error={step2Errors.street}
|
|
size="md"
|
|
type="text"
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
handleSearch();
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<div className={styles.addressRow}>
|
|
<TextField
|
|
value={addressData.postalCode}
|
|
onChange={(value) => onAddressChange('postalCode', value)}
|
|
label="Postleitzahl"
|
|
placeholder="z.B. 3000"
|
|
required
|
|
error={step2Errors.postalCode}
|
|
size="md"
|
|
type="text"
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
handleSearch();
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<TextField
|
|
value={addressData.city}
|
|
onChange={(value) => onAddressChange('city', value)}
|
|
label="Stadt"
|
|
placeholder="z.B. Bern"
|
|
required
|
|
error={step2Errors.city}
|
|
size="md"
|
|
type="text"
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
handleSearch();
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search buttons */}
|
|
<div className={styles.searchButtons}>
|
|
<Button
|
|
variant="primary"
|
|
size="md"
|
|
icon={IoMdSend}
|
|
onClick={handleSearch}
|
|
disabled={!buildLocationString().trim() || isGettingLocation || isSearchingParcel}
|
|
loading={isSearchingParcel}
|
|
>
|
|
Suchen
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="md"
|
|
icon={FaLocationArrow}
|
|
onClick={handleUseCurrentLocation}
|
|
disabled={isGettingLocation || isSearchingParcel}
|
|
loading={isGettingLocation}
|
|
>
|
|
Meine Position
|
|
</Button>
|
|
</div>
|
|
|
|
<div className={styles.mapSection}>
|
|
<div className={styles.mapContainer}>
|
|
<MapView
|
|
parcels={parcelGeometries}
|
|
center={mapCenter || undefined}
|
|
zoomBounds={mapZoomBounds || undefined}
|
|
onMapClick={handleMapClick}
|
|
onParcelClick={handleParcelClick}
|
|
height="400px"
|
|
emptyMessage="Klicken Sie auf die Karte, um einen Standort auszuwählen, oder suchen Sie nach einer Adresse oben."
|
|
/>
|
|
</div>
|
|
|
|
{/* Selected parcels list displayed below map */}
|
|
{selectedParcels && selectedParcels.length > 0 && (
|
|
<div className={styles.parcelInfo}>
|
|
<h3 className={styles.parcelInfoTitle}>Ausgewählte Parzellen ({selectedParcels.length})</h3>
|
|
<div className={styles.selectedParcelsList}>
|
|
{selectedParcels.map((selectedParcel, index) => (
|
|
<div key={selectedParcel.parcel.id || index} className={styles.selectedParcelCard}>
|
|
<div className={styles.selectedParcelHeader}>
|
|
<h4 className={styles.selectedParcelTitle}>
|
|
Parzelle {index + 1}: {selectedParcel.parcel.number || selectedParcel.parcel.id || 'Unbekannt'}
|
|
</h4>
|
|
<button
|
|
className={styles.removeParcelButton}
|
|
onClick={() => removeParcel(selectedParcel.parcel.id)}
|
|
title="Parzelle entfernen"
|
|
>
|
|
<FaTimes />
|
|
</button>
|
|
</div>
|
|
<div className={styles.parcelInfoGrid}>
|
|
{selectedParcel.parcel.id && (
|
|
<div className={styles.parcelInfoItem}>
|
|
<span className={styles.parcelInfoLabel}>ID:</span>
|
|
<span className={styles.parcelInfoValue}>{selectedParcel.parcel.id}</span>
|
|
</div>
|
|
)}
|
|
{selectedParcel.parcel.number && (
|
|
<div className={styles.parcelInfoItem}>
|
|
<span className={styles.parcelInfoLabel}>Nummer:</span>
|
|
<span className={styles.parcelInfoValue}>{selectedParcel.parcel.number}</span>
|
|
</div>
|
|
)}
|
|
{selectedParcel.parcel.egrid && (
|
|
<div className={styles.parcelInfoItem}>
|
|
<span className={styles.parcelInfoLabel}>EGRID:</span>
|
|
<span className={styles.parcelInfoValue}>{selectedParcel.parcel.egrid}</span>
|
|
</div>
|
|
)}
|
|
{selectedParcel.parcel.address && (
|
|
<div className={styles.parcelInfoItem}>
|
|
<span className={styles.parcelInfoLabel}>Adresse:</span>
|
|
<span className={styles.parcelInfoValue}>{selectedParcel.parcel.address}</span>
|
|
</div>
|
|
)}
|
|
{selectedParcel.parcel.area_m2 && (
|
|
<div className={styles.parcelInfoItem}>
|
|
<span className={styles.parcelInfoLabel}>Fläche (m²):</span>
|
|
<span className={styles.parcelInfoValue}>{selectedParcel.parcel.area_m2}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step2Errors.parcel && (
|
|
<span className={styles.errorText}>{step2Errors.parcel}</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className={styles.buttonGroup}>
|
|
<Button
|
|
variant="secondary"
|
|
size="md"
|
|
onClick={onBack}
|
|
>
|
|
{t('common.back', 'Zurück')}
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
size="md"
|
|
onClick={handleNext}
|
|
>
|
|
{t('common.finish', 'Abschließen')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const CreateButton: React.FC<CreateButtonProps> = ({
|
|
onCreate,
|
|
fields,
|
|
popupTitle = 'Create New Item',
|
|
popupSize = 'medium',
|
|
disabled = false,
|
|
loading = false,
|
|
className = '',
|
|
children,
|
|
icon,
|
|
iconPosition = 'left',
|
|
variant = 'primary',
|
|
size = 'md',
|
|
onSuccess,
|
|
onError,
|
|
multiStep = false,
|
|
...props
|
|
}) => {
|
|
const { t } = useLanguage();
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
|
const [currentStep, setCurrentStep] = useState<1 | 2>(1);
|
|
const [step1Data, setStep1Data] = useState<any>({});
|
|
const [addressData, setAddressData] = useState({
|
|
street: '',
|
|
postalCode: '',
|
|
city: ''
|
|
});
|
|
|
|
// Filter fields for multi-step: Step 1 only shows "label" field
|
|
const step1Fields = useMemo(() => {
|
|
if (multiStep) {
|
|
return fields.filter(field => field.key === 'label');
|
|
}
|
|
return fields;
|
|
}, [fields, multiStep]);
|
|
|
|
// Convert CreateButtonFieldConfig to AttributeDefinition format
|
|
const attributes: AttributeDefinition[] = useMemo(() => {
|
|
const fieldsToUse = multiStep ? step1Fields : fields;
|
|
return fieldsToUse.map(field => {
|
|
// Convert options to AttributeOption[] format
|
|
let options: AttributeDefinition['options'] = undefined;
|
|
|
|
if (field.options) {
|
|
// If options is an array of strings, convert to AttributeOption format
|
|
if (Array.isArray(field.options)) {
|
|
options = field.options.map(opt => {
|
|
if (typeof opt === 'string') {
|
|
return { value: opt, label: opt };
|
|
}
|
|
// Already in {value, label} format
|
|
return opt;
|
|
});
|
|
}
|
|
} else if (field.optionsReference) {
|
|
// Use optionsReference as string (will be fetched from API)
|
|
options = field.optionsReference;
|
|
}
|
|
|
|
// Map field types to FormGeneratorForm attribute types
|
|
let attributeType: AttributeDefinition['type'] = 'text';
|
|
if (field.type === 'boolean') {
|
|
attributeType = 'checkbox';
|
|
} else if (field.type === 'enum') {
|
|
attributeType = 'select';
|
|
} else if (field.type === 'multiselect') {
|
|
attributeType = 'multiselect';
|
|
} else if (field.type === 'email') {
|
|
attributeType = 'email';
|
|
} else if (field.type === 'date') {
|
|
attributeType = 'date';
|
|
} else if (field.type === 'textarea') {
|
|
attributeType = 'textarea';
|
|
} else if (field.type === 'readonly') {
|
|
attributeType = 'readonly';
|
|
} else if (field.type === 'string') {
|
|
// Check if it's a password field by key name
|
|
attributeType = field.key.toLowerCase().includes('password') ? 'password' : 'text';
|
|
}
|
|
|
|
return {
|
|
name: field.key,
|
|
label: typeof field.label === 'string' ? field.label : String(field.label),
|
|
type: attributeType,
|
|
required: field.required || false,
|
|
placeholder: field.placeholder,
|
|
default: field.defaultValue,
|
|
editable: true,
|
|
visible: true,
|
|
minRows: field.minRows,
|
|
maxRows: field.maxRows,
|
|
validation: field.validator,
|
|
options: options
|
|
};
|
|
});
|
|
}, [fields, multiStep, step1Fields]);
|
|
|
|
// Initialize form data with default values
|
|
const initialFormData = useMemo(() => {
|
|
const data: any = {};
|
|
const fieldsToUse = multiStep ? step1Fields : fields;
|
|
fieldsToUse.forEach(field => {
|
|
if (field.type === 'multiselect') {
|
|
// Multiselect fields should default to empty array
|
|
data[field.key] = field.defaultValue || [];
|
|
} else if (field.type === 'boolean') {
|
|
// Boolean fields should default to false
|
|
data[field.key] = field.defaultValue !== undefined ? field.defaultValue : false;
|
|
} else {
|
|
// Other fields default to empty string or provided default
|
|
data[field.key] = field.defaultValue !== undefined ? field.defaultValue : '';
|
|
}
|
|
});
|
|
return data;
|
|
}, [fields, multiStep, step1Fields]);
|
|
|
|
const handleButtonClick = () => {
|
|
if (!disabled && !loading && !isCreating) {
|
|
setIsPopupOpen(true);
|
|
// Reset to step 1 when opening popup
|
|
if (multiStep) {
|
|
setCurrentStep(1);
|
|
setStep1Data({});
|
|
setAddressData({ street: '', postalCode: '', city: '' });
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleStep1Next = (formData: any) => {
|
|
// Validate label is present
|
|
if (!formData.label || !formData.label.trim()) {
|
|
return; // FormGeneratorForm will show validation error
|
|
}
|
|
setStep1Data(formData);
|
|
setCurrentStep(2);
|
|
};
|
|
|
|
const handleStep2Back = () => {
|
|
setCurrentStep(1);
|
|
};
|
|
|
|
const handleStep2Finish = async (step2FormData: any) => {
|
|
// Combine step 1 and step 2 data
|
|
const selectedParcels = step2FormData.parzellen || [];
|
|
const completeData: any = {
|
|
label: step1Data.label,
|
|
// mandateId is NOT included - will be set by backend
|
|
};
|
|
|
|
// Add parzellen array if parcels were selected - include ALL parcel information for each
|
|
if (selectedParcels && selectedParcels.length > 0) {
|
|
completeData.parzellen = selectedParcels.map((selectedParcel: any) => ({
|
|
// Basic parcel info
|
|
id: selectedParcel.parcel.id,
|
|
egrid: selectedParcel.parcel.egrid,
|
|
number: selectedParcel.parcel.number,
|
|
name: selectedParcel.parcel.name,
|
|
identnd: selectedParcel.parcel.identnd,
|
|
canton: selectedParcel.parcel.canton,
|
|
municipality_code: selectedParcel.parcel.municipality_code,
|
|
municipality_name: selectedParcel.parcel.municipality_name,
|
|
address: selectedParcel.parcel.address,
|
|
area_m2: selectedParcel.parcel.area_m2,
|
|
centroid: selectedParcel.parcel.centroid,
|
|
geoportal_url: selectedParcel.parcel.geoportal_url,
|
|
realestate_type: selectedParcel.parcel.realestate_type,
|
|
|
|
// User-entered address fields (from step 2)
|
|
userAddress: {
|
|
street: step2FormData.address.street,
|
|
postalCode: step2FormData.address.postalCode,
|
|
city: step2FormData.address.city
|
|
},
|
|
|
|
// Geometry and map data
|
|
geometry: selectedParcel.map_view?.geometry_geojson,
|
|
perimeter: selectedParcel.parcel.perimeter,
|
|
map_view: selectedParcel.map_view,
|
|
|
|
// Adjacent parcels
|
|
adjacent_parcels: selectedParcel.adjacent_parcels || [],
|
|
|
|
// Include any other parcel properties that might exist
|
|
...selectedParcel.parcel
|
|
}));
|
|
}
|
|
|
|
// Send request to backend via onCreate handler
|
|
setIsCreating(true);
|
|
try {
|
|
const result = await onCreate(completeData);
|
|
|
|
if (result?.success !== false) {
|
|
// Success - close popup
|
|
setIsPopupOpen(false);
|
|
setCurrentStep(1);
|
|
setStep1Data({});
|
|
setAddressData({ street: '', postalCode: '', city: '' });
|
|
|
|
if (onSuccess) {
|
|
onSuccess(result);
|
|
}
|
|
} else {
|
|
// Handle error
|
|
if (onError) {
|
|
onError(result?.error || 'Projekt konnte nicht erstellt werden');
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Project creation failed:', error);
|
|
if (onError) {
|
|
onError(error.message || 'Projekt konnte nicht erstellt werden');
|
|
}
|
|
} finally {
|
|
setIsCreating(false);
|
|
}
|
|
};
|
|
|
|
const handleSave = async (updatedData: any) => {
|
|
if (multiStep && currentStep === 1) {
|
|
handleStep1Next(updatedData);
|
|
return;
|
|
}
|
|
|
|
setIsCreating(true);
|
|
|
|
try {
|
|
const result = await onCreate(updatedData);
|
|
|
|
if (result?.success !== false) {
|
|
// Success
|
|
setIsPopupOpen(false);
|
|
if (multiStep) {
|
|
setCurrentStep(1);
|
|
setStep1Data({});
|
|
setAddressData({ street: '', postalCode: '', city: '' });
|
|
}
|
|
if (onSuccess) {
|
|
onSuccess(result);
|
|
}
|
|
} else {
|
|
// Handle error
|
|
if (onError) {
|
|
onError(result?.error || 'Creation failed');
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Creation failed:', error);
|
|
if (onError) {
|
|
onError(error.message || 'Creation failed');
|
|
}
|
|
} finally {
|
|
setIsCreating(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setIsPopupOpen(false);
|
|
if (multiStep) {
|
|
setCurrentStep(1);
|
|
setStep1Data({});
|
|
setAddressData({ street: '', postalCode: '', city: '' });
|
|
}
|
|
};
|
|
|
|
const handleAddressChange = (field: string, value: string) => {
|
|
setAddressData(prev => ({
|
|
...prev,
|
|
[field]: value
|
|
}));
|
|
};
|
|
|
|
const isDisabled = disabled || loading || isCreating;
|
|
|
|
// Resolve language text for popup title
|
|
const resolvedPopupTitle = typeof popupTitle === 'string'
|
|
? t(popupTitle, popupTitle)
|
|
: popupTitle;
|
|
|
|
// Resolve language text for attributes
|
|
const resolvedAttributes: AttributeDefinition[] = useMemo(() => {
|
|
return attributes.map(attr => ({
|
|
...attr,
|
|
label: typeof attr.label === 'string' ? t(attr.label, attr.label) : attr.label,
|
|
placeholder: attr.placeholder
|
|
? (typeof attr.placeholder === 'string' ? t(attr.placeholder, attr.placeholder) : attr.placeholder)
|
|
: undefined
|
|
}));
|
|
}, [attributes, t]);
|
|
|
|
return (
|
|
<>
|
|
<Button
|
|
{...props}
|
|
variant={variant}
|
|
size={size}
|
|
disabled={isDisabled}
|
|
loading={false}
|
|
className={`createButton ${className}`}
|
|
onClick={handleButtonClick}
|
|
icon={isCreating ? undefined : icon}
|
|
iconPosition={iconPosition}
|
|
>
|
|
{isCreating && (
|
|
<div className="spinnerIcon" style={{ marginRight: '8px' }} />
|
|
)}
|
|
{children || (isCreating ? t('common.creating', 'Creating...') : t('common.create', 'Create'))}
|
|
</Button>
|
|
|
|
{/* Create Popup */}
|
|
<Popup
|
|
isOpen={isPopupOpen}
|
|
title={resolvedPopupTitle}
|
|
onClose={handleCancel}
|
|
size={multiStep ? 'large' : popupSize}
|
|
closable={!isCreating}
|
|
>
|
|
{multiStep ? (
|
|
currentStep === 1 ? (
|
|
<div className={styles.step1Container}>
|
|
<div className={styles.stepIndicator}>
|
|
<span className={styles.stepNumber}>1</span>
|
|
<span className={styles.stepLabel}>Titel festlegen</span>
|
|
</div>
|
|
<FormGeneratorForm
|
|
attributes={resolvedAttributes}
|
|
data={initialFormData}
|
|
mode="create"
|
|
onSubmit={handleSave}
|
|
onCancel={handleCancel}
|
|
submitButtonText={t('common.next', 'Weiter')}
|
|
cancelButtonText={t('common.cancel', 'Abbrechen')}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<PekProvider>
|
|
<Step2Content
|
|
onNext={handleStep2Finish}
|
|
onBack={handleStep2Back}
|
|
addressData={addressData}
|
|
onAddressChange={handleAddressChange}
|
|
/>
|
|
</PekProvider>
|
|
)
|
|
) : (
|
|
<FormGeneratorForm
|
|
attributes={resolvedAttributes}
|
|
data={initialFormData}
|
|
mode="create"
|
|
onSubmit={handleSave}
|
|
onCancel={handleCancel}
|
|
submitButtonText={t('common.create', 'Create')}
|
|
cancelButtonText={t('common.cancel', 'Cancel')}
|
|
/>
|
|
)}
|
|
</Popup>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default CreateButton;
|
|
|