Refactor contact handling across backend and frontend

Replaced usage of "ContactPerson" with "Contact" for consistency across the codebase. Updated related component props, state variables, API calls, and database queries to align with the new model. Also enhanced backend functionality with stricter validations and added support for handling active pgroups in contact management.
This commit is contained in:
GotthardG
2025-01-22 16:31:08 +01:00
parent 6cde57f783
commit 382b1eaba8
24 changed files with 627 additions and 373 deletions

View File

@ -90,7 +90,7 @@ const App: React.FC = () => {
<AddressManager pgroups={pgroups} activePgroup={activePgroup} />
</Modal>
<Modal open={openContactsManager} onClose={handleCloseContactsManager} title="Contacts Management">
<ContactsManager />
<ContactsManager pgroups={pgroups} activePgroup={activePgroup} />
</Modal>
</Router>
);

View File

@ -22,7 +22,7 @@ import {
Dewar,
DewarType,
DewarSerialNumber,
ContactPerson,
Contact,
Address,
ContactsService,
AddressesService,
@ -37,14 +37,14 @@ interface DewarDetailsProps {
dewar: Dewar;
trackingNumber: string;
setTrackingNumber: (trackingNumber: string) => void;
initialContactPersons?: ContactPerson[];
initialContacts?: Contact[];
initialReturnAddresses?: Address[];
defaultContactPerson?: ContactPerson;
defaultContact?: Contact;
defaultReturnAddress?: Address;
shipmentId: number;
}
interface NewContactPerson {
interface NewContact {
id: number;
firstName: string;
lastName: string;
@ -64,21 +64,21 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
dewar,
trackingNumber,
setTrackingNumber,
initialContactPersons = [],
initialContacts = [],
initialReturnAddresses = [],
defaultContactPerson,
defaultContact,
defaultReturnAddress,
shipmentId,
}) => {
const [localTrackingNumber, setLocalTrackingNumber] = useState(trackingNumber);
const [contactPersons, setContactPersons] = useState(initialContactPersons);
const [contacts, setContacts] = useState(initialContacts);
const [returnAddresses, setReturnAddresses] = useState(initialReturnAddresses);
const [selectedContactPerson, setSelectedContactPerson] = useState<string>('');
const [selectedContact, setSelectedContact] = useState<string>('');
const [selectedReturnAddress, setSelectedReturnAddress] = useState<string>('');
const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false);
const [isCreatingContact, setIsCreatingContact] = useState(false);
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false);
const [puckStatuses, setPuckStatuses] = useState<string[][]>([]);
const [newContactPerson, setNewContactPerson] = useState<NewContactPerson>({
const [newContact, setNewContact] = useState<NewContact>({
id: 0,
firstName: '',
lastName: '',
@ -140,9 +140,9 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
useEffect(() => {
setLocalTrackingNumber(dewar.tracking_number || '');
const setInitialContactPerson = () => {
setSelectedContactPerson(
dewar.contact_person?.id?.toString() || defaultContactPerson?.id?.toString() || ''
const setInitialContact = () => {
setSelectedContact(
dewar.contact?.id?.toString() || defaultContact?.id?.toString() || ''
);
};
@ -152,7 +152,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
);
};
setInitialContactPerson();
setInitialContact();
setInitialReturnAddress();
if (dewar.dewar_type_id) {
@ -161,7 +161,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
if (dewar.dewar_serial_number_id) {
setSelectedSerialNumber(dewar.dewar_serial_number_id.toString());
}
}, [dewar, defaultContactPerson, defaultReturnAddress]);
}, [dewar, defaultContact, defaultReturnAddress]);
useEffect(() => {
const getContacts = async () => {
@ -375,7 +375,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
arrival_date: dewar.arrival_date,
returning_date: dewar.returning_date,
return_address_id: parseInt(selectedReturnAddress ?? '', 10),
contact_person_id: parseInt(selectedContactPerson ?? '', 10),
contact_id: parseInt(selectedContactPerson ?? '', 10),
};
await DewarsService.updateDewarDewarsDewarIdPut(dewarId, payload);

View File

@ -85,7 +85,7 @@ const StepIconComponent: React.FC<StepIconComponentProps> = ({ icon, dewar, isSe
returning_date: dewar.returning_date,
qrcode: dewar.qrcode,
return_address_id: dewar.return_address_id,
contact_person_id: dewar.contact_person_id,
contact_id: dewar.contact_id,
};
await DewarsService.updateDewarDewarsDewarIdPut(dewar.id, payload);

View File

@ -4,7 +4,7 @@ import QRCode from 'react-qr-code';
import DeleteIcon from "@mui/icons-material/Delete";
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
import { Dewar, DewarsService, Shipment, ContactPerson, ApiError, ShipmentsService } from "../../openapi";
import { Dewar, DewarsService, Shipment, Contact, ApiError, ShipmentsService } from "../../openapi";
import { SxProps } from "@mui/system";
import CustomStepper from "./DewarStepper";
import DewarDetails from './DewarDetails';
@ -20,7 +20,7 @@ interface ShipmentDetailsProps {
setSelectedDewar: React.Dispatch<React.SetStateAction<Dewar | null>>;
setSelectedShipment: React.Dispatch<React.SetStateAction<Shipment | null>>;
refreshShipments: () => void;
defaultContactPerson?: ContactPerson;
defaultContact?: Contact;
}
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
@ -48,7 +48,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
shipping_date: null,
arrival_date: null,
returning_date: null,
contact_person_id: selectedShipment?.contact_person?.id,
contact_id: selectedShipment?.contact?.id,
return_address_id: selectedShipment?.return_address?.id,
};
@ -59,7 +59,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
// Ensure to update the default contact person and return address when the shipment changes
setNewDewar((prev) => ({
...prev,
contact_person_id: selectedShipment?.contact_person?.id,
contact_id: selectedShipment?.contact?.id,
return_address_id: selectedShipment?.return_address?.id
}));
}, [selectedShipment]);
@ -122,7 +122,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
...initialNewDewarState,
...newDewar,
dewar_name: newDewar.dewar_name.trim(),
contact_person_id: selectedShipment?.contact_person?.id,
contact_id: selectedShipment?.contact?.id,
return_address_id: selectedShipment?.return_address?.id
} as Dewar;
@ -179,7 +179,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
};
const isCommentsEdited = comments !== initialComments;
const contactPerson = selectedShipment?.contact_person;
const contact = selectedShipment?.contact;
return (
<Box sx={{ ...sx, padding: 2, textAlign: 'left' }}>
@ -228,7 +228,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
<Box sx={{ marginTop: 2, marginBottom: 2 }}>
<Typography variant="h5">{selectedShipment.shipment_name}</Typography>
<Typography variant="body1" color="textSecondary">
Main contact person: {contactPerson ? `${contactPerson.firstname} ${contactPerson.lastname}` : 'N/A'}
Main contact person: {contact ? `${contact.firstname} ${contact.lastname}` : 'N/A'}
</Typography>
<Typography variant="body1">Number of Pucks: {totalPucks}</Typography>
<Typography variant="body1">Number of Samples: {totalSamples}</Typography>
@ -318,7 +318,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
<Typography variant="body2">Number of Samples: {dewar.number_of_samples || 0}</Typography>
<Typography variant="body2">Tracking Number: {dewar.tracking_number}</Typography>
<Typography variant="body2">
Contact Person: {dewar.contact_person?.firstname ? `${dewar.contact_person.firstname} ${dewar.contact_person.lastname}` : 'N/A'}
Contact Person: {dewar.contact?.firstname ? `${dewar.contact.firstname} ${dewar.contact.lastname}` : 'N/A'}
</Typography>
</Box>
<Box sx={{
@ -355,9 +355,9 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
setTrackingNumber={(value) => {
setLocalSelectedDewar((prev) => (prev ? { ...prev, tracking_number: value as string } : prev));
}}
initialContactPersons={localSelectedDewar?.contact_person ? [localSelectedDewar.contact_person] : []}
initialContacts={localSelectedDewar?.contact ? [localSelectedDewar.contact] : []}
initialReturnAddresses={localSelectedDewar?.return_address ? [localSelectedDewar.return_address] : []}
defaultContactPerson={localSelectedDewar?.contact_person ?? undefined}
defaultContact={localSelectedDewar?.contact ?? undefined}
defaultReturnAddress={localSelectedDewar?.return_address ?? undefined}
shipmentId={selectedShipment?.id ?? null}
refreshShipments={refreshShipments}

View File

@ -6,7 +6,7 @@ import {
import { SelectChangeEvent } from '@mui/material';
import { SxProps } from '@mui/system';
import {
ContactPersonCreate, ContactPerson, Address, AddressCreate, Proposal, ContactsService, AddressesService, ProposalsService,
ContactCreate, Contact, Address, AddressCreate, Proposal, ContactsService, AddressesService, ProposalsService,
OpenAPI, ShipmentCreate, ShipmentsService
} from '../../openapi';
import { useEffect } from 'react';
@ -41,21 +41,21 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
onCancel,
refreshShipments }) => {
const [countrySuggestions, setCountrySuggestions] = React.useState<string[]>([]);
const [contactPersons, setContactPersons] = React.useState<ContactPerson[]>([]);
const [contacts, setContacts] = React.useState<Contact[]>([]);
const [returnAddresses, setReturnAddresses] = React.useState<Address[]>([]);
const [proposals, setProposals] = React.useState<Proposal[]>([]);
const [isCreatingContactPerson, setIsCreatingContactPerson] = React.useState(false);
const [isCreatingContact, setIsCreatingContact] = React.useState(false);
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = React.useState(false);
const [newContactPerson, setNewContactPerson] = React.useState<ContactPersonCreate>({
firstname: '', lastname: '', phone_number: '', email: ''
const [newContact, setNewContact] = React.useState<ContactCreate>({
pgroups:'', firstname: '', lastname: '', phone_number: '', email: ''
});
const [newReturnAddress, setNewReturnAddress] = React.useState<Omit<Address, 'id'>>({
pgroup:'', house_number: '', street: '', city: '', state: '', zipcode: '', country: ''
pgroups:'', house_number: '', street: '', city: '', state: '', 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 [selectedContactId, setSelectedContactId] = 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);
@ -83,12 +83,17 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
// Fetch necessary data
const getContacts = async () => {
if (!activePgroup) {
console.error("Active pgroup is missing.");
setErrorMessage("Active pgroup is missing. Unable to load contacts.");
return;
}
try {
const fetchedContacts: ContactPerson[] =
await ContactsService.getContactsContactsGet();
setContactPersons(fetchedContacts);
const fetchedContacts: Contact[] =
await ContactsService.getContactsProtectedContactsGet(activePgroup);
setContacts(fetchedContacts);
} catch {
setErrorMessage('Failed to load contact persons.');
setErrorMessage('Failed to load contact s.');
}
};
@ -102,7 +107,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
try {
// Pass activePgroup directly as a string (not as an object)
const fetchedAddresses: Address[] =
await AddressesService.getReturnAddressesAddressesGet(activePgroup);
await AddressesService.getReturnAddressesProtectedAddressesGet(activePgroup);
setReturnAddresses(fetchedAddresses);
} catch (error) {
@ -150,9 +155,9 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
};
const isContactFormValid = () => {
const { firstname, lastname, phone_number, email } = newContactPerson;
const { firstname, lastname, phone_number, email } = newContact;
if (isCreatingContactPerson) {
if (isCreatingContact) {
if (!firstname || !lastname || !validateEmail(email) || !validatePhoneNumber(phone_number)) return false;
}
@ -173,9 +178,9 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
const { shipment_name } = newShipment;
if (!shipment_name) return false;
if (!selectedContactPersonId || !selectedReturnAddressId || !selectedProposalId) return false;
if (!selectedContactId || !selectedReturnAddressId || !selectedProposalId) return false;
if (isCreatingContactPerson && !isContactFormValid()) return false;
if (isCreatingContact && !isContactFormValid()) return false;
if (isCreatingReturnAddress && !isAddressFormValid()) return false;
return true;
@ -197,7 +202,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
shipment_date: new Date().toISOString().split('T')[0], // Remove if date is not required
shipment_status: newShipment.shipment_status || 'In preparation',
comments: newShipment.comments || '',
contact_person_id: selectedContactPersonId!,
contact_id: selectedContactId!,
return_address_id: selectedReturnAddressId!,
proposal_id: selectedProposalId!,
dewars: newShipment.dewars || [],
@ -217,14 +222,14 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
}
};
const handleContactPersonChange = (event: SelectChangeEvent) => {
const handleContactChange = (event: SelectChangeEvent) => {
const value = event.target.value;
if (value === 'new') {
setIsCreatingContactPerson(true);
setSelectedContactPersonId(null);
setIsCreatingContact(true);
setSelectedContactId(null);
} else {
setIsCreatingContactPerson(false);
setSelectedContactPersonId(parseInt(value));
setIsCreatingContact(false);
setSelectedContactId(parseInt(value));
}
};
@ -244,33 +249,52 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
setSelectedProposalId(parseInt(value));
};
const handleSaveNewContactPerson = async () => {
if (!isContactFormValid()) {
const handleSaveNewContact = async () => {
// Validate contact form fields
if (!isContactFormValid(newContact)) {
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.');
// Ensure activePgroup is available
if (!activePgroup) {
setErrorMessage('Active pgroup is missing. Please try again.');
return;
}
setNewContactPerson({ firstname: '', lastname: '', phone_number: '', email: '' });
setIsCreatingContactPerson(false);
// Construct the payload
const payload: ContactCreate = {
pgroups: activePgroup, // Ensure this value is available
firstname: newContact.firstname.trim(),
lastname: newContact.lastname.trim(),
phone_number: newContact.phone_number.trim(),
email: newContact.email.trim(),
};
console.log('Payload being sent:', JSON.stringify(payload, null, 2));
try {
// Call the API with the correctly constructed payload
const newPerson: Contact = await ContactsService.createContactProtectedContactsPost(payload);
// Update state on success
setContacts([...contacts, newPerson]); // Add new contact to the list
setErrorMessage(null); // Clear error messages
setSelectedContactId(newPerson.id); // Optionally select the contact
// Reset form inputs
setNewContact({ pgroups: '', firstname: '', lastname: '', phone_number: '', email: '' });
setIsCreatingContact(false);
} catch (error) {
console.error('Failed to create a new contact person:', error);
// Handle detailed backend error messages if available
if (error.response?.data?.detail) {
setErrorMessage(`Error: ${error.response.data.detail}`);
} else {
setErrorMessage('Failed to create a new contact person. Please try again later.');
}
}
};
const handleSaveNewReturnAddress = async () => {
@ -301,7 +325,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
// Call the API with the completed payload
try {
const response: Address = await AddressesService.createReturnAddressAddressesPost(payload);
const response: Address = await AddressesService.createReturnAddressProtectedAddressesPost(payload);
setReturnAddresses([...returnAddresses, response]); // Update the address state
setErrorMessage(null);
setSelectedReturnAddressId(response.id); // Set the newly created address ID to the form
@ -311,7 +335,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
}
// Reset form inputs and close the "Create New Address" form
setNewReturnAddress({ pgroup: '', house_number: '', street: '', city: '', state: '', zipcode: '', country: '' });
setNewReturnAddress({ pgroups: '', house_number: '', street: '', city: '', state: '', zipcode: '', country: '' });
setIsCreatingReturnAddress(false);
};
@ -342,11 +366,11 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
<FormControl fullWidth required>
<InputLabel>Contact Person</InputLabel>
<Select
value={selectedContactPersonId ? selectedContactPersonId.toString() : ''}
onChange={handleContactPersonChange}
value={selectedContactId ? selectedContactId.toString() : ''}
onChange={handleContactChange}
displayEmpty
>
{contactPersons.map((person) => (
{contacts.map((person) => (
<MenuItem key={person.id} value={person.id.toString()}>
{`${person.lastname}, ${person.firstname}`}
</MenuItem>
@ -356,21 +380,21 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
</MenuItem>
</Select>
</FormControl>
{isCreatingContactPerson && (
{isCreatingContact && (
<>
<TextField
label="First Name"
name="firstname"
value={newContactPerson.firstname}
onChange={(e) => setNewContactPerson({ ...newContactPerson, firstname: e.target.value })}
value={newContact.firstname}
onChange={(e) => setNewContact({ ...newContact, firstname: e.target.value })}
fullWidth
required
/>
<TextField
label="Last Name"
name="lastname"
value={newContactPerson.lastname}
onChange={(e) => setNewContactPerson({ ...newContactPerson, lastname: e.target.value })}
value={newContact.lastname}
onChange={(e) => setNewContact({ ...newContact, lastname: e.target.value })}
fullWidth
required
/>
@ -378,28 +402,28 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
label="Phone"
name="phone_number"
type="tel"
value={newContactPerson.phone_number}
onChange={(e) => setNewContactPerson({ ...newContactPerson, phone_number: e.target.value })}
value={newContact.phone_number}
onChange={(e) => setNewContact({ ...newContact, phone_number: e.target.value })}
fullWidth
required
error={!validatePhoneNumber(newContactPerson.phone_number)}
helperText={!validatePhoneNumber(newContactPerson.phone_number) ? 'Invalid phone number' : ''}
error={!validatePhoneNumber(newContact.phone_number)}
helperText={!validatePhoneNumber(newContact.phone_number) ? 'Invalid phone number' : ''}
/>
<TextField
label="Email"
name="email"
type="email"
value={newContactPerson.email}
onChange={(e) => setNewContactPerson({ ...newContactPerson, email: e.target.value })}
value={newContact.email}
onChange={(e) => setNewContact({ ...newContact, email: e.target.value })}
fullWidth
required
error={!validateEmail(newContactPerson.email)}
helperText={!validateEmail(newContactPerson.email) ? 'Invalid email' : ''}
error={!validateEmail(newContact.email)}
helperText={!validateEmail(newContact.email) ? 'Invalid email' : ''}
/>
<Button
variant="contained"
color="primary"
onClick={handleSaveNewContactPerson}
onClick={handleSaveNewContact}
disabled={!isContactFormValid()}
>
Save New Contact Person

View File

@ -65,7 +65,7 @@ const SpreadsheetTable = ({
shipping_date: null,
arrival_date: null,
returning_date: null,
contact_person_id: selectedShipment?.contact_person?.id,
contact_id: selectedShipment?.contact?.id,
return_address_id: selectedShipment?.return_address?.id,
dewar_name: '',
tracking_number: 'UNKNOWN',
@ -78,7 +78,7 @@ const SpreadsheetTable = ({
useEffect(() => {
setNewDewar((prev) => ({
...prev,
contact_person_id: selectedShipment?.contact_person?.id,
contact_id: selectedShipment?.contact?.id,
return_address_id: selectedShipment?.return_address?.id
}));
}, [selectedShipment]);
@ -259,8 +259,8 @@ const SpreadsheetTable = ({
};
const createOrUpdateDewarsFromSheet = async (data, contactPerson, returnAddress) => {
if (!contactPerson?.id || !returnAddress?.id) {
console.error('contact_person_id or return_address_id is missing');
if (!contact?.id || !returnAddress?.id) {
console.error('contact_id or return_address_id is missing');
return null;
}
@ -288,7 +288,7 @@ const SpreadsheetTable = ({
dewar = {
...initialNewDewarState,
dewar_name: dewarName,
contact_person_id: contactPerson.id,
contact_id: contactPerson.id,
return_address_id: returnAddress.id,
pucks: [],
};
@ -498,7 +498,7 @@ const SpreadsheetTable = ({
await createOrUpdateDewarsFromSheet(
raw_data,
selectedShipment?.contact_person,
selectedShipment?.contact,
selectedShipment?.return_address
);

View File

@ -1,10 +1,10 @@
import { useState, useEffect } from 'react';
import { ShipmentsService, Shipment, ContactPerson } from '../../openapi';
import { ShipmentsService, Shipment, Contact } from '../../openapi';
const useShipments = () => {
const [shipments, setShipments] = useState<Shipment[]>([]);
const [error, setError] = useState<string | null>(null);
const [defaultContactPerson, setDefaultContactPerson] = useState<ContactPerson | undefined>();
const [defaultContact, setDefaultContact] = useState<Contact | undefined>();
const fetchAndSetShipments = async () => {
try {
@ -16,10 +16,10 @@ const useShipments = () => {
}
};
const fetchDefaultContactPerson = async () => {
const fetchDefaultContact = async () => {
try {
const contacts = await ShipmentsService.getShipmentContactPersonsShipmentsContactPersonsGet();
setDefaultContactPerson(contacts[0]);
setDefaultContact(contacts[0]);
} catch (error) {
console.error('Failed to fetch contact persons:', error);
setError('Failed to load contact persons. Please try again later.');
@ -28,10 +28,10 @@ const useShipments = () => {
useEffect(() => {
fetchAndSetShipments();
fetchDefaultContactPerson();
fetchDefaultContact();
}, []);
return { shipments, error, defaultContactPerson, fetchAndSetShipments };
return { shipments, error, defaultContact, fetchAndSetShipments };
};
export default useShipments;

View File

@ -78,7 +78,7 @@ const AddressManager: React.FC<AddressManagerProps> = ({ pgroups, activePgroup }
React.useEffect(() => {
const fetchAllData = async () => {
try {
const response = await AddressesService.getAllAddressesAddressesAllGet();
const response = await AddressesService.getAllAddressesProtectedAddressesAllGet();
// Preprocess: Add associated and unassociated pgroups
const transformedAddresses = response.map((address) => {
@ -108,7 +108,7 @@ const AddressManager: React.FC<AddressManagerProps> = ({ pgroups, activePgroup }
try {
if (editAddressId !== null) {
// Update address (mark old one obsolete, create a new one)
const updatedAddress = await AddressesService.updateReturnAddressAddressesAddressIdPut(
const updatedAddress = await AddressesService.updateReturnAddressProtectedAddressesAddressIdPut(
editAddressId,
newAddress as AddressUpdate
);
@ -120,7 +120,7 @@ const AddressManager: React.FC<AddressManagerProps> = ({ pgroups, activePgroup }
setEditAddressId(null);
} else {
// Add new address
const response = await AddressesService.createReturnAddressAddressesPost(newAddress as AddressCreate);
const response = await AddressesService.createReturnAddressProtectedAddressesPost(newAddress as AddressCreate);
setAddresses([...addresses, response]);
}
setNewAddress({ house_number:'', street: '', city: '', state: '', zipcode: '', country: '' });
@ -134,7 +134,7 @@ const AddressManager: React.FC<AddressManagerProps> = ({ pgroups, activePgroup }
const handleDeleteAddress = async (id: number) => {
try {
// Delete (inactivate) the address
await AddressesService.deleteReturnAddressAddressesAddressIdDelete(id);
await AddressesService.deleteReturnAddressProtectedAddressesAddressIdDelete(id);
// Remove the obsolete address from the active list in the UI
setAddresses(addresses.filter(address => address.id !== id && address.status === "active"));
@ -182,7 +182,7 @@ const AddressManager: React.FC<AddressManagerProps> = ({ pgroups, activePgroup }
const updatedPgroups = [...address.associatedPgroups, pgroup]; // Add the pgroup
// Update the backend
await AddressesService.updateReturnAddressAddressesAddressIdPut(addressId, {
await AddressesService.updateReturnAddressProtectedAddressesAddressIdPut(addressId, {
...address,
pgroups: updatedPgroups.join(','), // Sync updated pgroups
});

View File

@ -1,158 +1,285 @@
import * as React from 'react';
import {
Container, Typography, List, ListItem, IconButton, TextField, Box, ListItemText, ListItemSecondaryAction, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button
Container,
Typography,
List,
ListItem,
IconButton,
TextField,
Box,
ListItemText,
ListItemSecondaryAction,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Button,
Chip
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import SaveIcon from '@mui/icons-material/Save';
import AddIcon from '@mui/icons-material/Add';
import { ContactsService } from '../../openapi';
import type { ContactPerson, ContactPersonCreate, ContactPersonUpdate } from '../models/ContactPerson';
import {Contact, ContactCreate, ContactsService, ContactUpdate} from '../../openapi';
const ContactsManager: React.FC = () => {
const [contacts, setContacts] = React.useState<ContactPerson[]>([]);
const [newContact, setNewContact] = React.useState<Partial<ContactPerson>>({
firstname: '',
lastname: '',
phone_number: '',
email: '',
interface ContactsManagerProps {
pgroups: string[];
activePgroup: string;
}
// Extend the generated Contact type
interface ContactWithPgroups extends Contact {
associatedPgroups: string[]; // Dynamically added pgroups
}
const ContactsManager: React.FC<ContactsManagerProps> = ({ pgroups, activePgroup }) => {
const [contacts, setContacts] = React.useState<ContactWithPgroups[]>([]);
const [newContact, setNewContact] = React.useState<Partial<Contact>>({
firstname: '',
lastname: '',
phone_number: '',
email: '',
});
const [editContactId, setEditContactId] = React.useState<number | null>(null);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const [dialogOpen, setDialogOpen] = React.useState(false);
const [selectedContact, setSelectedContact] = React.useState<ContactWithPgroups | null>(null);
React.useEffect(() => {
const fetchContacts = async () => {
try {
const response = await ContactsService.getAllContactsProtectedContactsAllGet();
// Preprocess: Add associated and unassociated pgroups
const transformedContacts = response.map((contact) => {
const contactPgroups = contact.pgroups?.split(',').map((p) => p.trim()) || [];
const associatedPgroups = pgroups.filter((pgroup) => contactPgroups.includes(pgroup));
return {
...contact,
associatedPgroups, // pgroups linked to the contact
};
});
setContacts(transformedContacts);
} catch (error) {
console.error('Failed to fetch contacts', error);
setErrorMessage('Failed to load contacts. Please try again later.');
}
};
fetchContacts();
}, [pgroups]);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setNewContact({ ...newContact, [name]: value });
};
const handleAddOrUpdateContact = async () => {
try {
if (editContactId !== null) {
// Update contact
await ContactsService.updateContactProtectedContactsContactIdPut(editContactId, newContact as ContactUpdate);
setContacts(
contacts.map((contact) =>
contact.id === editContactId ? { ...contact, ...newContact } : contact
)
);
setEditContactId(null);
} else {
// Add new contact
const response = await ContactsService.createContactProtectedContactsPost(newContact as ContactCreate);
setContacts([...contacts, response]);
}
setNewContact({ firstname: '', lastname: '', phone_number: '', email: '' });
setErrorMessage(null);
} catch (error) {
console.error('Failed to add/update contact', error);
setErrorMessage('Failed to add/update contact. Please try again later.');
}
};
const handleDeleteContact = async (id: number) => {
try {
await ContactsService.deleteContactProtectedContactsContactIdDelete(id);
setContacts(contacts.filter((contact) => contact.id !== id));
} catch (error) {
console.error('Failed to delete contact', error);
setErrorMessage('Failed to delete contact. Please try again later.');
}
};
const handleEditContact = (contact: Contact) => {
setEditContactId(contact.id);
setNewContact(contact);
};
const openDialog = (contact: ContactWithPgroups) => {
setSelectedContact(contact);
setDialogOpen(true);
};
const closeDialog = () => {
setDialogOpen(false);
setSelectedContact(null);
};
const confirmDelete = async () => {
if (selectedContact) {
await handleDeleteContact(selectedContact.id);
closeDialog();
}
};
const togglePgroupAssociation = async (contactId: number, pgroup: string) => {
try {
const contact = contacts.find((c) => c.id === contactId);
if (!contact) return;
const isAssociated = contact.associatedPgroups.includes(pgroup);
// Only allow adding a pgroup
if (isAssociated) {
console.warn('Removing a pgroup is not allowed.');
return;
}
const updatedPgroups = [...contact.associatedPgroups, pgroup]; // Add the pgroup
// Update the backend
await ContactsService.updateContactProtectedContactsContactIdPut(contactId, {
...contact,
pgroups: updatedPgroups.join(','), // Sync updated pgroups
});
// Update contact in local state
setContacts((prevContacts) =>
prevContacts.map((c) =>
c.id === contactId ? { ...c, associatedPgroups: updatedPgroups } : c
)
);
} catch (error) {
console.error('Failed to add pgroup association', error);
setErrorMessage('Failed to add pgroup association. Please try again.');
}
};
const renderPgroupChips = (contact: ContactWithPgroups) => {
return pgroups.map((pgroup) => {
const isAssociated = contact.associatedPgroups.includes(pgroup);
return (
<Chip
key={pgroup}
label={pgroup}
onClick={
!isAssociated
? () => togglePgroupAssociation(contact.id, pgroup)
: undefined
}
sx={{
backgroundColor: isAssociated ? '#19d238' : '#b0b0b0',
color: 'white',
borderRadius: '8px',
fontWeight: 'bold',
height: '20px',
fontSize: '12px',
boxShadow: '0px 1px 3px rgba(0, 0, 0, 0.2)',
cursor: isAssociated ? 'default' : 'pointer', // Disable pointer for associated chips
'&:hover': { opacity: isAssociated ? 1 : 0.8 }, // Disable hover effect for associated chips
mr: 1,
mb: 1,
}}
/>
);
});
const [editContactId, setEditContactId] = React.useState<number | null>(null);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const [dialogOpen, setDialogOpen] = React.useState(false);
const [selectedContact, setSelectedContact] = React.useState<ContactPerson | null>(null);
};
React.useEffect(() => {
const fetchContacts = async () => {
try {
const response = await ContactsService.getContactsContactsGet();
setContacts(response);
} catch (error) {
console.error('Failed to fetch contacts', error);
setErrorMessage('Failed to load contacts. Please try again later.');
}
};
fetchContacts();
}, []);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setNewContact({ ...newContact, [name]: value });
};
const handleAddOrUpdateContact = async () => {
try {
if (editContactId !== null) {
// Update contact
await ContactsService.updateContactContactsContactIdPut(editContactId, newContact as ContactPersonUpdate);
setContacts(contacts.map(contact => contact.id === editContactId ? { ...contact, ...newContact } : contact));
setEditContactId(null);
} else {
// Add new contact
const response = await ContactsService.createContactContactsPost(newContact as ContactPersonCreate);
setContacts([...contacts, response]);
}
setNewContact({ firstname: '', lastname: '', phone_number: '', email: '' });
setErrorMessage(null);
} catch (error) {
console.error('Failed to add/update contact', error);
setErrorMessage('Failed to add/update contact. Please try again later.');
}
};
const handleDeleteContact = async (id: number) => {
try {
await ContactsService.deleteContactContactsContactIdDelete(id);
setContacts(contacts.filter(contact => contact.id !== id));
} catch (error) {
console.error('Failed to delete contact', error);
setErrorMessage('Failed to delete contact. Please try again later.');
}
};
const handleEditContact = (contact: ContactPerson) => {
setEditContactId(contact.id);
setNewContact(contact);
};
const openDialog = (contact: ContactPerson) => {
setSelectedContact(contact);
setDialogOpen(true);
};
const closeDialog = () => {
setDialogOpen(false);
setSelectedContact(null);
};
const confirmDelete = async () => {
if (selectedContact) {
await handleDeleteContact(selectedContact.id);
closeDialog();
}
};
return (
<Container>
<Typography variant="h4" gutterBottom>
Contacts Management
</Typography>
<Box mb={3} display="flex" justifyContent="center" alignItems="center">
<TextField label="First Name" name="firstname" value={newContact.firstname || ''} onChange={handleInputChange} />
<TextField label="Last Name" name="lastname" value={newContact.lastname || ''} onChange={handleInputChange} />
<TextField label="Phone Number" name="phone_number" value={newContact.phone_number || ''} onChange={handleInputChange} />
<TextField label="Email" name="email" value={newContact.email || ''} onChange={handleInputChange} />
<IconButton color="primary" onClick={handleAddOrUpdateContact}>
{editContactId !== null ? <SaveIcon /> : <AddIcon />}
return (
<Container>
<Typography variant="h4" gutterBottom>
Contacts Management
</Typography>
<Box mb={3} display="flex" justifyContent="center" alignItems="center">
<TextField
label="First Name"
name="firstname"
value={newContact.firstname || ''}
onChange={handleInputChange}
/>
<TextField
label="Last Name"
name="lastname"
value={newContact.lastname || ''}
onChange={handleInputChange}
/>
<TextField
label="Phone Number"
name="phone_number"
value={newContact.phone_number || ''}
onChange={handleInputChange}
/>
<TextField
label="Email"
name="email"
value={newContact.email || ''}
onChange={handleInputChange}
/>
<IconButton color="primary" onClick={handleAddOrUpdateContact}>
{editContactId !== null ? <SaveIcon /> : <AddIcon />}
</IconButton>
</Box>
{errorMessage && <Typography color="error">{errorMessage}</Typography>}
<List>
{contacts.length > 0 ? (
contacts.map((contact) => (
<ListItem key={contact.id} button>
<ListItemText
primary={`${contact.firstname} ${contact.lastname}`}
secondary={
<Box display="flex" flexWrap="wrap">
{renderPgroupChips(contact)}
</Box>
}
/>
<ListItemSecondaryAction>
<IconButton edge="end" color="primary" onClick={() => handleEditContact(contact)}>
<EditIcon />
</IconButton>
</Box>
{errorMessage && <Typography color="error">{errorMessage}</Typography>}
<List>
{contacts.length > 0 ? (
contacts.map((contact) => (
<ListItem key={contact.id} button>
<ListItemText
primary={`${contact.firstname} ${contact.lastname}`}
secondary={`${contact.phone_number} - ${contact.email}`}
/>
<ListItemSecondaryAction>
<IconButton edge="end" color="primary" onClick={() => handleEditContact(contact)}>
<EditIcon />
</IconButton>
<IconButton edge="end" color="secondary" onClick={() => openDialog(contact)}>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
))
) : (
<Typography>No contacts found</Typography>
)}
</List>
<Dialog
open={dialogOpen}
onClose={closeDialog}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{"Confirm Delete"}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete this contact?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} color="primary">
Cancel
</Button>
<Button onClick={confirmDelete} color="secondary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
</Container>
);
<IconButton edge="end" color="secondary" onClick={() => openDialog(contact)}>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
))
) : (
<Typography>No contacts found</Typography>
)}
</List>
<Dialog
open={dialogOpen}
onClose={closeDialog}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{"Confirm Delete"}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete this contact?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} color="primary">
Cancel
</Button>
<Button onClick={confirmDelete} color="secondary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
</Container>
);
};
export default ContactsManager;

View File

@ -11,7 +11,7 @@ type ShipmentViewProps = {
};
const ShipmentView: React.FC<ShipmentViewProps> = ( { activePgroup }) => {
const { shipments, error, defaultContactPerson, fetchAndSetShipments } = useShipments();
const { shipments, error, defaultContact, fetchAndSetShipments } = useShipments();
const [selectedShipment, setSelectedShipment] = useState<Shipment | null>(null);
const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null);
const [isCreatingShipment, setIsCreatingShipment] = useState(false);
@ -76,7 +76,7 @@ const ShipmentView: React.FC<ShipmentViewProps> = ( { activePgroup }) => {
setSelectedDewar={setSelectedDewar}
setSelectedShipment={setSelectedShipment}
refreshShipments={fetchAndSetShipments}
defaultContactPerson={defaultContactPerson}
defaultContact={defaultContact}
/>
);
}

View File

@ -1,4 +1,4 @@
export interface ContactPerson {
export interface Contact {
id: string;
lastname: string;
firstname: string;
@ -29,7 +29,7 @@ export interface Dewar {
number_of_pucks: number;
number_of_samples: number;
return_address: ReturnAddress[];
contact_person: ContactPerson[];
contact_: Contact[];
status: string;
ready_date?: string; // Make sure this is included
shipping_date?: string; // Make sure this is included
@ -45,7 +45,7 @@ export interface Shipment {
shipment_date: string;
number_of_dewars: number;
shipment_status: string;
contact_person: ContactPerson[] | null; // Change to an array to accommodate multiple contacts
contact_: Contact[] | null; // Change to an array to accommodate multiple contacts
proposal_number?: string;
return_address: Address[]; // Change to an array of Address
comments?: string;