From 7f46006435341002c1415cff8f9a0f7bc98e74f5 Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:11:53 +0100 Subject: [PATCH] Connected frontend new contact, new address and shipments to backend --- backend/main.py | 18 +- .../src/{keep => components}/DewarDetails.tsx | 13 +- .../{keep => components}/ShipmentDetails.tsx | 126 +++++---- frontend/src/components/ShipmentForm.tsx | 254 +++++++++++------- frontend/src/components/ShipmentPanel.tsx | 125 +++++---- frontend/src/keep/ParentComponent.tsx | 61 ----- frontend/src/keep/ResponsiveAppBar.tsx | 157 ----------- frontend/src/pages/ShipmentView.tsx | 146 +++------- 8 files changed, 362 insertions(+), 538 deletions(-) rename frontend/src/{keep => components}/DewarDetails.tsx (93%) rename frontend/src/{keep => components}/ShipmentDetails.tsx (79%) delete mode 100644 frontend/src/keep/ParentComponent.tsx delete mode 100644 frontend/src/keep/ResponsiveAppBar.tsx diff --git a/backend/main.py b/backend/main.py index b6db486..a75bcba 100644 --- a/backend/main.py +++ b/backend/main.py @@ -38,9 +38,9 @@ class Proposal(BaseModel): number: str class Dewar(BaseModel): - id: str + id: Optional[str] = None dewar_name: str - tracking_number: str + tracking_number: Optional[str] = None number_of_pucks: int number_of_samples: int return_address: List[Address] @@ -218,11 +218,25 @@ async def get_proposals(): async def get_shipments(): return shipments +@app.delete("/shipments/{shipment_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_shipment(shipment_id: str): + global shipments # Use global variable to access the shipments list + shipments = [shipment for shipment in shipments if shipment.shipment_id != shipment_id] + @app.get("/dewars", response_model=List[Dewar]) async def get_dewars(): return dewars +@app.post("/dewars", response_model=List[Dewar], status_code=status.HTTP_201_CREATED) +async def create_dewar(shipment: Dewar): + dewar_id = f'SHIP-{uuid.uuid4().hex[:8].upper()}' # Generates a unique shipment ID + shipment.id = dewar_id # Set the generated ID on the shipment object + + dewars.append(shipment) # Add the modified shipment object to the list + + return dewars # Return the list of all dewars + # Endpoint to get the number of dewars in each shipment @app.get("/shipment_dewars") diff --git a/frontend/src/keep/DewarDetails.tsx b/frontend/src/components/DewarDetails.tsx similarity index 93% rename from frontend/src/keep/DewarDetails.tsx rename to frontend/src/components/DewarDetails.tsx index a5471c1..f28bcfd 100644 --- a/frontend/src/keep/DewarDetails.tsx +++ b/frontend/src/components/DewarDetails.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { Box, Typography, TextField, Button, Select, MenuItem, Snackbar } from '@mui/material'; import QRCode from 'react-qr-code'; -import { Dewar, ContactPerson, Address } from '../types.ts'; +import {ContactPerson, Address, Dewar} from "../../openapi"; + interface DewarDetailsProps { dewar: Dewar | null; @@ -13,8 +14,8 @@ interface DewarDetailsProps { addNewContactPerson: (name: string) => void; addNewReturnAddress: (address: string) => void; ready_date?: string; - shipping_date?: string; // Make this optional - arrival_date?: string; // Make this optional + shipping_date?: string; + arrival_date?: string; } const DewarDetails: React.FC = ({ @@ -36,10 +37,10 @@ const DewarDetails: React.FC = ({ React.useEffect(() => { if (contactPersons.length > 0) { - setSelectedContactPerson(contactPersons[0].name); // Default to the first contact person + setSelectedContactPerson(contactPersons[0].firstname); // Default to the first contact person } if (returnAddresses.length > 0) { - setSelectedReturnAddress(returnAddresses[0].address); // Default to the first return address + setSelectedReturnAddress(returnAddresses[0].return_address); // Default to the first return address } }, [contactPersons, returnAddresses]); @@ -109,7 +110,7 @@ const DewarDetails: React.FC = ({ > Select Contact Person {contactPersons.map((person) => ( - {person.name} + {person.lastname} ))} Add New Contact Person diff --git a/frontend/src/keep/ShipmentDetails.tsx b/frontend/src/components/ShipmentDetails.tsx similarity index 79% rename from frontend/src/keep/ShipmentDetails.tsx rename to frontend/src/components/ShipmentDetails.tsx index a7da54d..64e5707 100644 --- a/frontend/src/keep/ShipmentDetails.tsx +++ b/frontend/src/components/ShipmentDetails.tsx @@ -1,33 +1,22 @@ import React from 'react'; -import { - Box, - Typography, - Button, - Stack, - TextField, - Stepper, - Step, - StepLabel, -} from '@mui/material'; -import ShipmentForm from './ShipmentForm.tsx'; -import DewarDetails from './DewarDetails.tsx'; -import { Shipment, Dewar, ContactPerson, Proposal, Address } from '../types.ts'; +import {Box, Typography, Button, Stack, TextField, Stepper, Step, StepLabel} from '@mui/material'; +import DewarDetails from '../components/DewarDetails.tsx'; import { SxProps } from '@mui/system'; import QRCode from 'react-qr-code'; import bottleIcon from '../assets/icons/bottle-svgrepo-com-grey.svg'; import AirplanemodeActiveIcon from "@mui/icons-material/AirplanemodeActive"; import StoreIcon from "@mui/icons-material/Store"; import DeleteIcon from "@mui/icons-material/Delete"; -import {Contact} from "../../openapi"; // Import delete icon +import {ContactPerson, Dewar, Proposal, Address, Shipment_Input, DefaultService} from "../../openapi"; // Import delete icon interface ShipmentDetailsProps { - selectedShipment: Shipment | null; + selectedShipment: Shipment_Input | null; setSelectedDewar: React.Dispatch>; isCreatingShipment: boolean; - newShipment: Shipment; - setNewShipment: React.Dispatch>; + newShipment: Shipment_Input; + setNewShipment: React.Dispatch>; handleSaveShipment: () => void; - contactPersons: Contact[]; + contactPersons: ContactPerson[]; proposals: Proposal[]; returnAddresses: Address[]; sx?: SxProps; @@ -36,12 +25,8 @@ interface ShipmentDetailsProps { const ShipmentDetails: React.FC = ({ selectedShipment, setSelectedDewar, - isCreatingShipment, - newShipment, setNewShipment, - handleSaveShipment, contactPersons, - proposals, returnAddresses, sx = {}, }) => { @@ -55,29 +40,6 @@ const ShipmentDetails: React.FC = ({ // Step titles based on your status const steps = ['Ready for Shipping', 'Shipped', 'Arrived']; - - React.useEffect(() => { - if (localSelectedDewar) { - setTrackingNumber(localSelectedDewar.tracking_number); - } - }, [localSelectedDewar]); - - if (!selectedShipment) { - return isCreatingShipment ? ( - - ) : ( - No shipment selected. - ); - } - - // Calculate total pucks and samples const totalPucks = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_pucks || 0), 0); const totalSamples = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_samples || 0), 0); @@ -108,19 +70,59 @@ const ShipmentDetails: React.FC = ({ })); }; + const createDewar = async (newDewar: Partial, shipmentId: string) => { + console.log("Payload being sent to the API:", newDewar); + try { + const response = await DefaultService.createDewarDewarsPost(shipmentId, newDewar); + console.log("Response from API:", response); + return response; + } catch (error) { + console.error("Error creating dewar:", error); + if (error.response) { + console.error("Validation error details:", error.response.data); + } + throw error; + } + }; + // Handle adding a new dewar - const handleAddDewar = () => { + const handleAddDewar = async () => { if (selectedShipment && newDewar.dewar_name) { - const updatedDewars = [ - ...selectedShipment.dewars, - { ...newDewar, tracking_number: newDewar.tracking_number || `TN-${Date.now()}` } as Dewar, - ]; - setNewShipment({ - ...selectedShipment, - dewars: updatedDewars, - }); - setIsAddingDewar(false); - setNewDewar({ dewar_name: '', number_of_pucks: 0, number_of_samples: 0, tracking_number: '' }); + try { + const newDewarToPost: Dewar = { + //id: `DEWAR${Date.now()}`, + dewar_name: newDewar.dewar_name.trim() || 'Unnamed Dewar', + number_of_pucks: newDewar.number_of_pucks ?? 0, + number_of_samples: newDewar.number_of_samples ?? 0, + return_address: selectedShipment.return_address, + contact_person: selectedShipment.contact_person, + status: 'In preparation', + shippingStatus: 'not shipped', + arrivalStatus: 'not arrived', + qrcode: newDewar.qrcode || 'N/A', + //tracking_number: newDewar.tracking_number?.trim() || `TN-${Date.now()}`, + //ready_date: newDewar.ready_date || 'N/A', + //shipping_date: newDewar.shipping_date || 'N/A', + //arrival_date: newDewar.arrival_date || 'N/A', + }; + + + // Post to backend + const createdDewar = await createDewar(newDewarToPost, selectedShipment.id); + + // Update state with the response from backend + setNewShipment(prev => ({ + ...prev, + dewars: [...prev.dewars, createdDewar], + })); + + // Reset form fields + setIsAddingDewar(false); + //setNewDewar({ dewar_name: '', number_of_pucks: 0, number_of_samples: 0, tracking_number: '' }); + } catch (error) { + alert("Failed to add dewar. Please try again."); + console.error("Error adding dewar:", error); + } } else { alert('Please fill in the Dewar Name'); } @@ -188,6 +190,18 @@ const ShipmentDetails: React.FC = ({ )} {selectedShipment.shipment_name} + + {/* Iterate over contact persons if it's an array */} + {selectedShipment.contact_person && selectedShipment.contact_person.length > 0 ? ( + selectedShipment.contact_person.map((person, index) => ( + + Contact Person: {person.firstname} {person.lastname} + + )) + ) : ( + No contact person assigned. + )} + Number of Pucks: {totalPucks} Number of Samples: {totalSamples} Shipment Date: {selectedShipment.shipment_date} diff --git a/frontend/src/components/ShipmentForm.tsx b/frontend/src/components/ShipmentForm.tsx index f8e334c..bd10f94 100644 --- a/frontend/src/components/ShipmentForm.tsx +++ b/frontend/src/components/ShipmentForm.tsx @@ -1,52 +1,61 @@ import * as React from 'react'; import { Box, Button, TextField, Typography, Select, MenuItem, Stack, FormControl, InputLabel } from '@mui/material'; import { SelectChangeEvent } from '@mui/material'; -import { SxProps } from '@mui/material'; -import {Dispatch, SetStateAction, useEffect, useState} from "react"; -import {ContactPerson, Address, Proposal, DefaultService, OpenAPI, Shipment_Input, type Dewar} from "../../openapi"; +import { SxProps } from '@mui/system'; +import { ContactPerson, Address, Proposal, DefaultService, OpenAPI, Shipment_Input } from "../../openapi"; +import { useEffect } from "react"; interface ShipmentFormProps { - newShipment: Shipment; - setNewShipment: Dispatch>; - onShipmentCreated: () => void; sx?: SxProps; + onCancel: () => void; } const ShipmentForm: React.FC = ({ - newShipment, - setNewShipment, - onShipmentCreated, sx = {}, + onCancel, }) => { const [contactPersons, setContactPersons] = React.useState([]); const [returnAddresses, setReturnAddresses] = React.useState([]); const [proposals, setProposals] = React.useState([]); - const [shipments, setShipments] = useState([]); const [isCreatingContactPerson, setIsCreatingContactPerson] = React.useState(false); const [isCreatingReturnAddress, setIsCreatingReturnAddress] = React.useState(false); + const [newContactPerson, setNewContactPerson] = React.useState({ firstName: '', lastName: '', phone_number: '', email: '', }); + const [newReturnAddress, setNewReturnAddress] = React.useState
({ street: '', city: '', zipcode: '', country: '', }); - const [errorMessage, setErrorMessage] = React.useState(null); // For error handling + + const [newShipment, setNewShipment] = React.useState({ + comments: undefined, + contact_person: [], + dewars: [], + proposal_number: [], + return_address: [], + shipment_date: "", + shipment_id: undefined, + shipment_name: "", + shipment_status: "" + }); + + const [errorMessage, setErrorMessage] = React.useState(null); useEffect(() => { - OpenAPI.BASE = 'http://127.0.0.1:8000'; + OpenAPI.BASE = 'http://127.0.0.1:8000'; // Define Base URL const getContacts = async () => { try { const c: ContactPerson[] = await DefaultService.getContactsContactsGet(); setContactPersons(c); } catch (err) { - console.error('Failed to fetch contact persons:', err); setErrorMessage('Failed to load contact persons. Please try again later.'); } }; @@ -56,7 +65,6 @@ const ShipmentForm: React.FC = ({ const a: Address[] = await DefaultService.getReturnAddressesReturnAddressesGet(); setReturnAddresses(a); } catch (err) { - console.error('Failed to fetch return addresses:', err); setErrorMessage('Failed to load return addresses. Please try again later.'); } }; @@ -66,7 +74,6 @@ const ShipmentForm: React.FC = ({ const p: Proposal[] = await DefaultService.getProposalsProposalsGet(); setProposals(p); } catch (err) { - console.error('Error fetching proposals:', err); setErrorMessage('Failed to load proposals. Please try again later.'); } }; @@ -76,65 +83,95 @@ const ShipmentForm: React.FC = ({ getProposals(); }, []); + const validateEmail = (email: string) => /\S+@\S+\.\S+/.test(email); + const validatePhoneNumber = (phone: string) => /^\+?[1-9]\d{1,14}$/.test(phone); + const validateZipCode = (zipcode: string) => /^\d{5}(?:[-\s]\d{4})?$/.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, proposal_number, contact_person, return_address } = newShipment; + + if (!shipment_name || !proposal_number.length) return false; + if (!contact_person.length || !return_address.length) return false; + + if (isCreatingContactPerson && !isContactFormValid()) return false; + if (isCreatingReturnAddress && !isAddressFormValid()) return false; + + return true; + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setNewShipment((prev) => ({ ...prev, [name]: value })); + if (name === 'email') { + setNewContactPerson((prev) => ({ ...prev, email: value })); + } + if (name === 'phone_number') { + setNewContactPerson((prev) => ({ ...prev, phone_number: value })); + } + if (name === 'zipcode') { + setNewReturnAddress((prev) => ({ ...prev, zipcode: value })); + } + }; + const handleSaveShipment = async () => { - // Set the base URL for the OpenAPI requests + if (!isFormValid()) { + setErrorMessage('Please fill in all mandatory fields correctly.'); + return; + } + OpenAPI.BASE = 'http://127.0.0.1:8000'; - // Create the payload for the shipment const payload: Shipment_Input = { - shipment_name: newShipment.shipment_name, - shipment_date: new Date().toISOString().split('T')[0], // YYYY-MM-DD + shipment_name: newShipment.shipment_name || '', + shipment_date: new Date().toISOString().split('T')[0], shipment_status: 'In preparation', - contact_person: newShipment.contact_person.map(person => ({ + contact_person: newShipment.contact_person ? newShipment.contact_person.map(person => ({ firstname: person.firstname, lastname: person.lastname, phone_number: person.phone_number, email: person.email - })), - proposal_number: [ - { - id: 1, // Use the correct ID from your context - number: newShipment.proposal_number || "Default Proposal Number" // Make sure it's a valid string - } - ], - return_address: newShipment.return_address.map(address => ({ + })) : [], + proposal_number: newShipment.proposal_number ? [{ + id: 1, + number: newShipment.proposal_number + }] : [], + return_address: newShipment.return_address ? newShipment.return_address.map(address => ({ street: address.street, city: address.city, zipcode: address.zipcode, country: address.country - })), - comments: newShipment.comments || '', // Set to an empty string if null - dewars: [] // Assuming you want this to be an empty array + })) : [], + comments: newShipment.comments || '', + dewars: [] }; - // Print the payload to the console for debugging - console.log('Request Payload:', JSON.stringify(payload, null, 2)); - try { - // Create a shipment using the API - const s: Shipment_Input[] = await DefaultService.createShipmentShipmentsPost(payload); - setShipments(s); - - if (onShipmentCreated) { - onShipmentCreated(s[0]); // Call the function if it is defined - } else { - console.error('onShipmentCreated is not defined'); - } - // Pass the created shipment to the callback - console.log('Shipment created successfully:', s); - // Optionally reset the form or handle the response - setNewShipment({ - shipment_name: '', - contact_person: [], // Reset to an empty array - proposal_number: '', - return_address: [], // Reset to an empty array - comments: '', - dewars: [], - }); - setErrorMessage(null); // Clear any previous error message - } catch (err) { - console.error('Failed to create shipment:', err.response?.data || err.message); - setErrorMessage('Failed to create shipment. Please try again later.'); + await DefaultService.createShipmentShipmentsPost(payload); + setErrorMessage(null); + // Handle successful save action + onCancel(); // close the form after saving + } catch (error) { + setErrorMessage('Failed to save shipment. Please try again.'); } }; @@ -145,9 +182,9 @@ const ShipmentForm: React.FC = ({ setNewShipment({ ...newShipment, contact_person: [] }); } else { setIsCreatingContactPerson(false); - const selectedPerson = contactPersons.find((person) => person.lastname === value) || null; + const selectedPerson = contactPersons.find((person) => person.lastname === value); if (selectedPerson) { - setNewShipment({ ...newShipment, contact_person: [selectedPerson] }); + setNewShipment({ ...newShipment, contact_person: [{ ...selectedPerson }] }); } } }; @@ -161,7 +198,7 @@ const ShipmentForm: React.FC = ({ setIsCreatingReturnAddress(false); const selectedAddress = returnAddresses.find((address) => address.city === value); if (selectedAddress) { - setNewShipment({ ...newShipment, return_address: [selectedAddress] }); + setNewShipment({ ...newShipment, return_address: [{ ...selectedAddress }] }); } } }; @@ -171,12 +208,13 @@ const ShipmentForm: React.FC = ({ setNewShipment({ ...newShipment, proposal_number: selectedProposal }); }; - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setNewShipment((prev) => ({ ...prev, [name]: value })); - }; - const handleSaveNewContactPerson = async () => { + if (!validateEmail(newContactPerson.email) || !validatePhoneNumber(newContactPerson.phone_number) || + !newContactPerson.firstName || !newContactPerson.lastName) { + setErrorMessage('Please fill in all new contact person fields correctly.'); + return; + } + const payload = { firstname: newContactPerson.firstName, lastname: newContactPerson.lastName, @@ -189,7 +227,6 @@ const ShipmentForm: React.FC = ({ setContactPersons([...contactPersons, c]); setErrorMessage(null); } catch (err) { - console.error('Failed to create a new contact person:', err); setErrorMessage('Failed to create a new contact person. Please try again later.'); } @@ -198,6 +235,12 @@ const ShipmentForm: React.FC = ({ }; 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 = { street: newReturnAddress.street.trim(), city: newReturnAddress.city.trim(), @@ -205,17 +248,11 @@ const ShipmentForm: React.FC = ({ country: newReturnAddress.country.trim(), }; - if (!payload.street || !payload.city || !payload.zipcode || !payload.country) { - setErrorMessage('All fields are required.'); - return; - } - try { const a: Address = await DefaultService.createReturnAddressReturnAddressesPost(payload); setReturnAddresses([...returnAddresses, a]); setErrorMessage(null); } catch (err) { - console.error('Failed to create a new return address:', err); setErrorMessage('Failed to create a new return address. Please try again later.'); } @@ -245,8 +282,9 @@ const ShipmentForm: React.FC = ({ value={newShipment.shipment_name || ''} onChange={handleChange} fullWidth + required /> - + Contact Person = ({ ))} - + Return Address