aaredb/frontend/src/components/ShipmentForm.tsx
2024-12-20 14:14:53 +01:00

519 lines
22 KiB
TypeScript

import * as React from 'react';
import Fuse from 'fuse.js';
import {
Box, Button, TextField, Typography, Select, MenuItem, Stack, FormControl, InputLabel
} from '@mui/material';
import { SelectChangeEvent } from '@mui/material';
import { SxProps } from '@mui/system';
import {
ContactPersonCreate, ContactPerson, Address, Proposal, ContactsService, AddressesService, AddressCreate, ProposalsService,
OpenAPI, ShipmentCreate, ShipmentsService
} from '../../openapi';
import { useEffect } from 'react';
import { CountryList } from './CountryList'; // Import the list of countries
const MAX_COMMENTS_LENGTH = 200;
interface ShipmentFormProps {
sx?: SxProps;
onCancel: () => void;
refreshShipments: () => void;
}
// Set up Fuse.js for fuzzy searching
const fuse = new Fuse(CountryList, {
threshold: 0.3,
includeScore: true,
});
const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshShipments }) => {
const [countrySuggestions, setCountrySuggestions] = React.useState<string[]>([]);
const [contactPersons, setContactPersons] = React.useState<ContactPerson[]>([]);
const [returnAddresses, setReturnAddresses] = React.useState<Address[]>([]);
const [proposals, setProposals] = React.useState<Proposal[]>([]);
const [isCreatingContactPerson, setIsCreatingContactPerson] = React.useState(false);
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = React.useState(false);
const [newContactPerson, setNewContactPerson] = React.useState<ContactPersonCreate>({
firstname: '', lastname: '', phone_number: '', email: ''
});
const [newReturnAddress, setNewReturnAddress] = React.useState<Omit<Address, 'id'>>({
street: '', city: '', zipcode: '', country: ''
});
const [newShipment, setNewShipment] = React.useState<Partial<ShipmentCreate>>({
shipment_name: '', shipment_status: 'In preparation', comments: ''
});
const [selectedContactPersonId, setSelectedContactPersonId] = React.useState<number | null>(null);
const [selectedReturnAddressId, setSelectedReturnAddressId] = React.useState<number | null>(null);
const [selectedProposalId, setSelectedProposalId] = React.useState<number | null>(null);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
useEffect(() => {
// Detect the current environment
const mode = import.meta.env.MODE;
// Consistent dynamic `OpenAPI.BASE` resolution
OpenAPI.BASE =
mode === 'test'
? import.meta.env.VITE_OPENAPI_BASE_TEST
: mode === 'prod'
? import.meta.env.VITE_OPENAPI_BASE_PROD
: import.meta.env.VITE_OPENAPI_BASE_DEV;
// Log warning if `OpenAPI.BASE` is unresolved
if (!OpenAPI.BASE) {
console.error('OpenAPI.BASE is not set. Falling back to a default value.');
OpenAPI.BASE = 'https://default-url.com'; // Define fallback
}
console.log('Environment Mode:', mode);
console.log('Resolved OpenAPI.BASE:', OpenAPI.BASE);
// Fetch necessary data
const getContacts = async () => {
try {
const fetchedContacts: ContactPerson[] =
await ContactsService.getContactsContactsGet();
setContactPersons(fetchedContacts);
} catch {
setErrorMessage('Failed to load contact persons.');
}
};
const getAddresses = async () => {
try {
const fetchedAddresses: Address[] =
await AddressesService.getReturnAddressesAddressesGet();
setReturnAddresses(fetchedAddresses);
} catch {
setErrorMessage('Failed to load return addresses.');
}
};
const getProposals = async () => {
try {
const fetchedProposals: Proposal[] =
await ProposalsService.getProposalsProposalsGet();
setProposals(fetchedProposals);
} catch {
setErrorMessage('Failed to load proposals.');
}
};
getContacts();
getAddresses();
getProposals();
}, []);
const handleCountryInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setNewReturnAddress({ ...newReturnAddress, country: value });
if (value) {
const suggestions = fuse.search(value).map((result) => result.item);
setCountrySuggestions(suggestions);
} else {
setCountrySuggestions([]);
}
};
const validateEmail = (email: string) => /\S+@\S+\.\S+/.test(email);
const validatePhoneNumber = (phone: string) => /^\+?[1-9]\d{1,14}$/.test(phone);
const validateZipCode = (zipcode: string) => {
const usZipCodeRegex = /^\d{4,5}(?:[-\s]\d{4})?$/;
const ukPostCodeRegex = /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i;
return usZipCodeRegex.test(zipcode) || ukPostCodeRegex.test(zipcode);
};
const isContactFormValid = () => {
const { firstname, lastname, phone_number, email } = newContactPerson;
if (isCreatingContactPerson) {
if (!firstname || !lastname || !validateEmail(email) || !validatePhoneNumber(phone_number)) return false;
}
return true;
};
const isAddressFormValid = () => {
const { street, city, zipcode, country } = newReturnAddress;
if (isCreatingReturnAddress) {
if (!street || !city || !validateZipCode(zipcode) || !country) return false;
}
return true;
};
const isFormValid = () => {
const { shipment_name } = newShipment;
if (!shipment_name) return false;
if (!selectedContactPersonId || !selectedReturnAddressId || !selectedProposalId) return false;
if (isCreatingContactPerson && !isContactFormValid()) return false;
if (isCreatingReturnAddress && !isAddressFormValid()) return false;
return true;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setNewShipment((prev) => ({ ...prev, [name]: value }));
};
const handleSaveShipment = async () => {
if (!isFormValid()) {
setErrorMessage('Please fill in all mandatory fields correctly.');
return;
}
const payload: ShipmentCreate = {
shipment_name: newShipment.shipment_name || '',
shipment_date: new Date().toISOString().split('T')[0], // Remove if date is not required at all
shipment_status: newShipment.shipment_status || 'In preparation',
comments: newShipment.comments || '',
contact_person_id: selectedContactPersonId!,
return_address_id: selectedReturnAddressId!,
proposal_id: selectedProposalId!,
dewars: newShipment.dewars || []
};
console.log('Shipment Payload being sent:', payload);
try {
await ShipmentsService.createShipmentShipmentsPost(payload);
setErrorMessage(null);
refreshShipments();
onCancel();
} catch (error) {
console.error('Failed to save shipment:', error);
setErrorMessage('Failed to save shipment. Please try again.');
}
};
const handleContactPersonChange = (event: SelectChangeEvent) => {
const value = event.target.value;
if (value === 'new') {
setIsCreatingContactPerson(true);
setSelectedContactPersonId(null);
} else {
setIsCreatingContactPerson(false);
setSelectedContactPersonId(parseInt(value));
}
};
const handleReturnAddressChange = (event: SelectChangeEvent) => {
const value = event.target.value;
if (value === 'new') {
setIsCreatingReturnAddress(true);
setSelectedReturnAddressId(null);
} else {
setIsCreatingReturnAddress(false);
setSelectedReturnAddressId(parseInt(value));
}
};
const handleProposalChange = (event: SelectChangeEvent) => {
const value = event.target.value;
setSelectedProposalId(parseInt(value));
};
const handleSaveNewContactPerson = async () => {
if (!isContactFormValid()) {
setErrorMessage('Please fill in all new contact person fields correctly.');
return;
}
const payload: ContactPersonCreate = {
firstname: newContactPerson.firstname,
lastname: newContactPerson.lastname,
phone_number: newContactPerson.phone_number,
email: newContactPerson.email,
};
console.log('Contact Person Payload being sent:', payload);
try {
const newPerson: ContactPerson = await ContactsService.createContactContactsPost(payload);
setContactPersons([...contactPersons, newPerson]);
setErrorMessage(null);
setSelectedContactPersonId(newPerson.id);
} catch (error) {
console.error('Failed to create a new contact person:', error);
setErrorMessage('Failed to create a new contact person. Please try again later.');
}
setNewContactPerson({ firstname: '', lastname: '', phone_number: '', email: '' });
setIsCreatingContactPerson(false);
};
const handleSaveNewReturnAddress = async () => {
if (!validateZipCode(newReturnAddress.zipcode) || !newReturnAddress.street || !newReturnAddress.city ||
!newReturnAddress.country) {
setErrorMessage('Please fill in all new return address fields correctly.');
return;
}
const payload: AddressCreate = {
street: newReturnAddress.street,
city: newReturnAddress.city,
zipcode: newReturnAddress.zipcode,
country: newReturnAddress.country,
};
console.log('Return Address Payload being sent:', payload);
try {
const response: Address = await AddressesService.createReturnAddressAddressesPost(payload);
setReturnAddresses([...returnAddresses, response]);
setErrorMessage(null);
setSelectedReturnAddressId(response.id);
} catch (error) {
console.error('Failed to create a new return address:', error);
setErrorMessage('Failed to create a new return address. Please try again later.');
}
setNewReturnAddress({ street: '', city: '', zipcode: '', country: '' });
setIsCreatingReturnAddress(false);
};
return (
<Box
sx={{
padding: 4,
border: '1px solid #ccc',
borderRadius: '4px',
marginBottom: 2,
maxWidth: '600px',
...sx,
}}
>
<Typography variant="h6" sx={{ marginBottom: 2 }}>
Create Shipment
</Typography>
{errorMessage && <Typography color="error">{errorMessage}</Typography>}
<Stack spacing={2}>
<TextField
label="Shipment Name"
name="shipment_name"
value={newShipment.shipment_name || ''}
onChange={handleChange}
fullWidth
required
/>
<FormControl fullWidth required>
<InputLabel>Contact Person</InputLabel>
<Select
value={selectedContactPersonId ? selectedContactPersonId.toString() : ''}
onChange={handleContactPersonChange}
displayEmpty
>
{contactPersons.map((person) => (
<MenuItem key={person.id} value={person.id.toString()}>
{`${person.lastname}, ${person.firstname}`}
</MenuItem>
))}
<MenuItem value="new">
<em>Create New Contact Person</em>
</MenuItem>
</Select>
</FormControl>
{isCreatingContactPerson && (
<>
<TextField
label="First Name"
name="firstname"
value={newContactPerson.firstname}
onChange={(e) => setNewContactPerson({ ...newContactPerson, firstname: e.target.value })}
fullWidth
required
/>
<TextField
label="Last Name"
name="lastname"
value={newContactPerson.lastname}
onChange={(e) => setNewContactPerson({ ...newContactPerson, lastname: e.target.value })}
fullWidth
required
/>
<TextField
label="Phone"
name="phone_number"
type="tel"
value={newContactPerson.phone_number}
onChange={(e) => setNewContactPerson({ ...newContactPerson, phone_number: e.target.value })}
fullWidth
required
error={!validatePhoneNumber(newContactPerson.phone_number)}
helperText={!validatePhoneNumber(newContactPerson.phone_number) ? 'Invalid phone number' : ''}
/>
<TextField
label="Email"
name="email"
type="email"
value={newContactPerson.email}
onChange={(e) => setNewContactPerson({ ...newContactPerson, email: e.target.value })}
fullWidth
required
error={!validateEmail(newContactPerson.email)}
helperText={!validateEmail(newContactPerson.email) ? 'Invalid email' : ''}
/>
<Button
variant="contained"
color="primary"
onClick={handleSaveNewContactPerson}
disabled={!isContactFormValid()}
>
Save New Contact Person
</Button>
</>
)}
<FormControl fullWidth required>
<InputLabel>Proposal Number</InputLabel>
<Select
value={selectedProposalId ? selectedProposalId.toString() : ''}
onChange={handleProposalChange}
displayEmpty
>
{proposals.map((proposal) => (
<MenuItem key={proposal.id} value={proposal.id.toString()}>
{proposal.number}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth required>
<InputLabel>Return Address</InputLabel>
<Select
value={selectedReturnAddressId ? selectedReturnAddressId.toString() : ''}
onChange={handleReturnAddressChange}
displayEmpty
>
{returnAddresses.map((address) => (
<MenuItem key={address.id} value={address.id.toString()}>
{`${address.street}, ${address.city}, ${address.zipcode}, ${address.country}`}
</MenuItem>
))}
<MenuItem value="new">
<em>Create New Return Address</em>
</MenuItem>
</Select>
</FormControl>
{isCreatingReturnAddress && (
<>
<TextField
label="Street"
name="street"
value={newReturnAddress.street}
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, street: e.target.value })}
fullWidth
required
/>
<TextField
label="City"
name="city"
value={newReturnAddress.city}
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, city: e.target.value })}
fullWidth
required
/>
<TextField
label="Zip Code"
name="zipcode"
value={newReturnAddress.zipcode}
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, zipcode: e.target.value })}
fullWidth
required
error={!validateZipCode(newReturnAddress.zipcode)}
helperText={!validateZipCode(newReturnAddress.zipcode) ? 'Invalid zip code' : ''}
/>
<TextField
label="Country"
name="country"
value={newReturnAddress.country}
onChange={handleCountryInputChange} // Ensure this matches the function name exactly
fullWidth
required
error={!newReturnAddress.country} // Optional: Add an error indicator if the input is empty
helperText={!newReturnAddress.country ? 'Country is required' : ''}
/>
{/* Render country suggestions below the input field */}
{countrySuggestions.length > 0 && (
<Box sx={{ marginTop: '0.5rem', border: '1px solid #ccc', borderRadius: '4px', padding: '0.5rem' }}>
{countrySuggestions.map((suggestion, index) => (
<Typography
key={index}
sx={{
cursor: 'pointer',
padding: '0.2rem 0',
'&:hover': {
backgroundColor: '#f0f0f0',
},
}}
onClick={() => {
// Update country field with selected suggestion
setNewReturnAddress({ ...newReturnAddress, country: suggestion });
setCountrySuggestions([]); // Clear suggestions
}}
>
{suggestion}
</Typography>
))}
</Box>
)}
<Button
variant="contained"
color="primary"
onClick={handleSaveNewReturnAddress}
disabled={!isAddressFormValid()}
>
Save New Return Address
</Button>
</>
)}
<Box
sx={{
position: 'relative',
}}
>
<TextField
label="Comments"
name="comments"
fullWidth
multiline
rows={4}
value={newShipment.comments || ''}
onChange={handleChange}
inputProps={{ maxLength: MAX_COMMENTS_LENGTH }}
/>
<Typography
variant="caption"
color={newShipment.comments && newShipment.comments.length > MAX_COMMENTS_LENGTH ? 'error' : 'textSecondary'}
sx={{
position: 'absolute',
bottom: 8,
right: 8,
}}
>
{MAX_COMMENTS_LENGTH - (newShipment.comments?.length || 0)} characters remaining
</Typography>
</Box>
<Stack direction="row" spacing={2} justifyContent="flex-end">
<Button variant="outlined" color="error" onClick={onCancel}>Cancel</Button>
<Button
variant="contained"
color="primary"
onClick={handleSaveShipment}
disabled={(newShipment.comments?.length ?? 0) > MAX_COMMENTS_LENGTH}
>
Save Shipment
</Button>
</Stack>
</Stack>
</Box>
);
};
export default ShipmentForm;