519 lines
22 KiB
TypeScript
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; |