frontend_nyla/src/components/UiComponents/Button/CreateButton/CreateButton.tsx

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;