Connected frontend new contact, new address and shipments to backend
This commit is contained in:
@ -4,6 +4,8 @@ from fastapi import HTTPException, status
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import logging
|
import logging
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
@ -53,7 +55,7 @@ class Dewar(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Shipment(BaseModel):
|
class Shipment(BaseModel):
|
||||||
shipment_id: str
|
shipment_id: Optional[str] = None
|
||||||
shipment_name: str
|
shipment_name: str
|
||||||
shipment_date: str
|
shipment_date: str
|
||||||
shipment_status: str
|
shipment_status: str
|
||||||
@ -233,16 +235,14 @@ async def get_shipment_contact_persons():
|
|||||||
return [{"shipment_id": shipment.shipment_id, "contact_person": shipment.get_shipment_contact_persons()} for shipment in
|
return [{"shipment_id": shipment.shipment_id, "contact_person": shipment.get_shipment_contact_persons()} for shipment in
|
||||||
shipments]
|
shipments]
|
||||||
|
|
||||||
# Creation of a new shipment
|
|
||||||
@app.post("/shipments", response_model=Shipment, status_code=status.HTTP_201_CREATED)
|
@app.post("/shipments", response_model=Shipment, status_code=status.HTTP_201_CREATED)
|
||||||
async def create_shipment(shipment: Shipment):
|
async def create_shipment(shipment: Shipment):
|
||||||
# Check for duplicate shipment_id
|
# Automatically generate a shipment ID
|
||||||
if any(s.shipment_id == shipment.shipment_id for s in shipments):
|
shipment_id = f'SHIP-{uuid.uuid4().hex[:8].upper()}' # Generates a unique shipment ID
|
||||||
raise HTTPException(
|
shipment.shipment_id = shipment_id # Set the generated ID
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Shipment with this ID already exists."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Append the shipment to the list
|
||||||
shipments.append(shipment)
|
shipments.append(shipment)
|
||||||
return shipment
|
return shipment
|
||||||
|
|
||||||
|
@ -2,43 +2,32 @@ import * as React from 'react';
|
|||||||
import { Box, Button, TextField, Typography, Select, MenuItem, Stack, FormControl, InputLabel } from '@mui/material';
|
import { Box, Button, TextField, Typography, Select, MenuItem, Stack, FormControl, InputLabel } from '@mui/material';
|
||||||
import { SelectChangeEvent } from '@mui/material';
|
import { SelectChangeEvent } from '@mui/material';
|
||||||
import { SxProps } from '@mui/material';
|
import { SxProps } from '@mui/material';
|
||||||
import { useEffect } from "react";
|
import {Dispatch, SetStateAction, useEffect, useState} from "react";
|
||||||
import { ContactPerson, Address, Proposal, DefaultService, OpenAPI } from "../../openapi";
|
import {ContactPerson, Address, Proposal, DefaultService, OpenAPI, Shipment_Input, type Dewar} from "../../openapi";
|
||||||
|
|
||||||
// Define the Shipment interface (example; adjust according to your types)
|
|
||||||
interface Shipment {
|
|
||||||
shipment_name: string;
|
|
||||||
contact_person: ContactPerson[];
|
|
||||||
proposal_number: string;
|
|
||||||
return_address: Address[];
|
|
||||||
comments: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the ShipmentFormProps interface
|
|
||||||
interface ShipmentFormProps {
|
interface ShipmentFormProps {
|
||||||
newShipment: Shipment;
|
newShipment: Shipment;
|
||||||
setNewShipment: React.Dispatch<React.SetStateAction<Shipment>>;
|
setNewShipment: Dispatch<SetStateAction<Shipment>>;
|
||||||
handleSaveShipment: () => void;
|
onShipmentCreated: () => void;
|
||||||
proposals: Proposal[]; // Define proposals type
|
|
||||||
returnAddresses: Address[];
|
|
||||||
sx?: SxProps;
|
sx?: SxProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShipmentForm: React.FC<ShipmentFormProps> = ({
|
const ShipmentForm: React.FC<ShipmentFormProps> = ({
|
||||||
newShipment,
|
newShipment,
|
||||||
setNewShipment,
|
setNewShipment,
|
||||||
handleSaveShipment,
|
onShipmentCreated,
|
||||||
sx = {},
|
sx = {},
|
||||||
}) => {
|
}) => {
|
||||||
const [contactPersons, setContactPersons] = React.useState<ContactPerson[]>([]); // State to hold contact persons
|
const [contactPersons, setContactPersons] = React.useState<ContactPerson[]>([]);
|
||||||
const [returnAddresses, setReturnAddresses] = React.useState<Address[]>([]); // Renamed for consistency
|
const [returnAddresses, setReturnAddresses] = React.useState<Address[]>([]);
|
||||||
const [proposals, setProposals] = React.useState<Proposal[]>([]);
|
const [proposals, setProposals] = React.useState<Proposal[]>([]);
|
||||||
|
const [shipments, setShipments] = useState([]);
|
||||||
const [isCreatingContactPerson, setIsCreatingContactPerson] = React.useState(false);
|
const [isCreatingContactPerson, setIsCreatingContactPerson] = React.useState(false);
|
||||||
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = React.useState(false);
|
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = React.useState(false);
|
||||||
const [newContactPerson, setNewContactPerson] = React.useState({
|
const [newContactPerson, setNewContactPerson] = React.useState({
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
phone: '',
|
phone_number: '',
|
||||||
email: '',
|
email: '',
|
||||||
});
|
});
|
||||||
const [newReturnAddress, setNewReturnAddress] = React.useState<Address>({
|
const [newReturnAddress, setNewReturnAddress] = React.useState<Address>({
|
||||||
@ -50,12 +39,11 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
|
|||||||
const [errorMessage, setErrorMessage] = React.useState<string | null>(null); // For error handling
|
const [errorMessage, setErrorMessage] = React.useState<string | null>(null); // For error handling
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
OpenAPI.BASE = 'http://127.0.0.1:8000'; // Set the base URL for OpenAPI
|
OpenAPI.BASE = 'http://127.0.0.1:8000';
|
||||||
|
|
||||||
const getContacts = async () => {
|
const getContacts = async () => {
|
||||||
console.log('Trying to fetch some contacts');
|
|
||||||
try {
|
try {
|
||||||
const c: ContactPerson[] = await DefaultService.getContactsContactsGet(); // Fetch contacts
|
const c: ContactPerson[] = await DefaultService.getContactsContactsGet();
|
||||||
setContactPersons(c);
|
setContactPersons(c);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch contact persons:', err);
|
console.error('Failed to fetch contact persons:', err);
|
||||||
@ -64,9 +52,8 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getAddresses = async () => {
|
const getAddresses = async () => {
|
||||||
console.log('Trying to fetch some return addresses');
|
|
||||||
try {
|
try {
|
||||||
const a: Address[] = await DefaultService.getReturnAddressesReturnAddressesGet(); // Fetch addresses
|
const a: Address[] = await DefaultService.getReturnAddressesReturnAddressesGet();
|
||||||
setReturnAddresses(a);
|
setReturnAddresses(a);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch return addresses:', err);
|
console.error('Failed to fetch return addresses:', err);
|
||||||
@ -77,10 +64,9 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
|
|||||||
const getProposals = async () => {
|
const getProposals = async () => {
|
||||||
try {
|
try {
|
||||||
const p: Proposal[] = await DefaultService.getProposalsProposalsGet();
|
const p: Proposal[] = await DefaultService.getProposalsProposalsGet();
|
||||||
console.log('Fetched Proposals:', p); // Debug log to check if proposals are being fetched
|
|
||||||
setProposals(p);
|
setProposals(p);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching proposals:', err); // Log the error
|
console.error('Error fetching proposals:', err);
|
||||||
setErrorMessage('Failed to load proposals. Please try again later.');
|
setErrorMessage('Failed to load proposals. Please try again later.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -90,16 +76,78 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
|
|||||||
getProposals();
|
getProposals();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleSaveShipment = async () => {
|
||||||
|
// Set the base URL for the OpenAPI requests
|
||||||
|
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_status: 'In preparation',
|
||||||
|
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 => ({
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleContactPersonChange = (event: SelectChangeEvent) => {
|
const handleContactPersonChange = (event: SelectChangeEvent) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
if (value === 'new') {
|
if (value === 'new') {
|
||||||
setIsCreatingContactPerson(true);
|
setIsCreatingContactPerson(true);
|
||||||
setNewShipment({ ...newShipment, contact_person: [] }); // Reset for new person
|
setNewShipment({ ...newShipment, contact_person: [] });
|
||||||
} else {
|
} else {
|
||||||
setIsCreatingContactPerson(false);
|
setIsCreatingContactPerson(false);
|
||||||
const selectedPerson = contactPersons.find((person) => person.lastname === value) || null;
|
const selectedPerson = contactPersons.find((person) => person.lastname === value) || null;
|
||||||
if (selectedPerson) {
|
if (selectedPerson) {
|
||||||
setNewShipment({ ...newShipment, contact_person: [selectedPerson] }); // Set selected person
|
setNewShipment({ ...newShipment, contact_person: [selectedPerson] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -108,43 +156,48 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
|
|||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
if (value === 'new') {
|
if (value === 'new') {
|
||||||
setIsCreatingReturnAddress(true);
|
setIsCreatingReturnAddress(true);
|
||||||
setNewShipment({ ...newShipment, return_address: [] }); // Reset for new address
|
setNewShipment({ ...newShipment, return_address: [] });
|
||||||
} else {
|
} else {
|
||||||
setIsCreatingReturnAddress(false);
|
setIsCreatingReturnAddress(false);
|
||||||
const selectedAddress = returnAddresses.find((address) => address.city === value); // Match by a unique ID
|
const selectedAddress = returnAddresses.find((address) => address.city === value);
|
||||||
if (selectedAddress) {
|
if (selectedAddress) {
|
||||||
setNewShipment({ ...newShipment, return_address: [selectedAddress] }); // Set selected address
|
setNewShipment({ ...newShipment, return_address: [selectedAddress] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProposalChange = (event: SelectChangeEvent) => {
|
const handleProposalChange = (event: SelectChangeEvent) => {
|
||||||
const selectedProposal = event.target.value;
|
const selectedProposal = event.target.value;
|
||||||
console.log('Selected Proposal:', selectedProposal); // Corrected log statement
|
|
||||||
setNewShipment({ ...newShipment, proposal_number: selectedProposal });
|
setNewShipment({ ...newShipment, proposal_number: selectedProposal });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNewReturnAddressChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setNewReturnAddress((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[name]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setNewShipment((prev) => ({ ...prev, [name]: value }));
|
setNewShipment((prev) => ({ ...prev, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveNewReturnAddress = async () => {
|
const handleSaveNewContactPerson = async () => {
|
||||||
OpenAPI.BASE = 'http://127.0.0.1:8000';
|
const payload = {
|
||||||
|
firstname: newContactPerson.firstName,
|
||||||
|
lastname: newContactPerson.lastName,
|
||||||
|
phone_number: newContactPerson.phone_number,
|
||||||
|
email: newContactPerson.email,
|
||||||
|
};
|
||||||
|
|
||||||
if (!newReturnAddress.street || !newReturnAddress.city || !newReturnAddress.zipcode || !newReturnAddress.country) {
|
try {
|
||||||
setErrorMessage('All fields are required.');
|
const c: ContactPerson = await DefaultService.createContactContactsPost(payload);
|
||||||
return;
|
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.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setNewContactPerson({ firstName: '', lastName: '', phone_number: '', email: '' });
|
||||||
|
setIsCreatingContactPerson(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveNewReturnAddress = async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
street: newReturnAddress.street.trim(),
|
street: newReturnAddress.street.trim(),
|
||||||
city: newReturnAddress.city.trim(),
|
city: newReturnAddress.city.trim(),
|
||||||
@ -152,6 +205,11 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
|
|||||||
country: newReturnAddress.country.trim(),
|
country: newReturnAddress.country.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!payload.street || !payload.city || !payload.zipcode || !payload.country) {
|
||||||
|
setErrorMessage('All fields are required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const a: Address = await DefaultService.createReturnAddressReturnAddressesPost(payload);
|
const a: Address = await DefaultService.createReturnAddressReturnAddressesPost(payload);
|
||||||
setReturnAddresses([...returnAddresses, a]);
|
setReturnAddresses([...returnAddresses, a]);
|
||||||
@ -205,6 +263,41 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
{isCreatingContactPerson && (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
label="First Name"
|
||||||
|
name="firstName"
|
||||||
|
value={newContactPerson.firstName}
|
||||||
|
onChange={(e) => setNewContactPerson({ ...newContactPerson, firstName: e.target.value })}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Last Name"
|
||||||
|
name="lastName"
|
||||||
|
value={newContactPerson.lastName}
|
||||||
|
onChange={(e) => setNewContactPerson({ ...newContactPerson, lastName: e.target.value })}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Phone"
|
||||||
|
name="phone"
|
||||||
|
value={newContactPerson.phone_number}
|
||||||
|
onChange={(e) => setNewContactPerson({ ...newContactPerson, phone_number: e.target.value })}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Email"
|
||||||
|
name="email"
|
||||||
|
value={newContactPerson.email}
|
||||||
|
onChange={(e) => setNewContactPerson({ ...newContactPerson, email: e.target.value })}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<Button variant="contained" color="primary" onClick={handleSaveNewContactPerson}>
|
||||||
|
Save New Contact Person
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<InputLabel>Proposal Number</InputLabel>
|
<InputLabel>Proposal Number</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@ -242,28 +335,28 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
|
|||||||
label="Street"
|
label="Street"
|
||||||
name="street"
|
name="street"
|
||||||
value={newReturnAddress.street}
|
value={newReturnAddress.street}
|
||||||
onChange={handleNewReturnAddressChange}
|
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, street: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="City"
|
label="City"
|
||||||
name="city"
|
name="city"
|
||||||
value={newReturnAddress.city}
|
value={newReturnAddress.city}
|
||||||
onChange={handleNewReturnAddressChange}
|
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, city: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Zip Code"
|
label="Zip Code"
|
||||||
name="zipcode"
|
name="zipcode"
|
||||||
value={newReturnAddress.zipcode}
|
value={newReturnAddress.zipcode}
|
||||||
onChange={handleNewReturnAddressChange}
|
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, zipcode: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Country"
|
label="Country"
|
||||||
name="country"
|
name="country"
|
||||||
value={newReturnAddress.country}
|
value={newReturnAddress.country}
|
||||||
onChange={handleNewReturnAddressChange}
|
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, country: e.target.value })}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Button variant="contained" color="primary" onClick={handleSaveNewReturnAddress}>
|
<Button variant="contained" color="primary" onClick={handleSaveNewReturnAddress}>
|
||||||
|
@ -2,49 +2,49 @@ import * as React from 'react';
|
|||||||
import { useEffect, useState, Dispatch, SetStateAction } from 'react';
|
import { useEffect, useState, Dispatch, SetStateAction } from 'react';
|
||||||
import { Button, Box, Typography, IconButton } from '@mui/material';
|
import { Button, Box, Typography, IconButton } from '@mui/material';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'; // Import delete icon
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import UploadFileIcon from '@mui/icons-material/UploadFile'; // Import the upload icon
|
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||||
import UploadDialog from './UploadDialog.tsx'; // Import the UploadDialog component
|
import UploadDialog from './UploadDialog'; // Ensure the file extension is correct
|
||||||
//import {Shipment} from '../types.ts'; // Ensure Shipment type is correctly imported
|
|
||||||
import { SxProps } from '@mui/material';
|
import { SxProps } from '@mui/material';
|
||||||
import bottleGrey from '../assets/icons/bottle-svgrepo-com-grey.svg';
|
import bottleGrey from '../assets/icons/bottle-svgrepo-com-grey.svg';
|
||||||
import bottleYellow from '../assets/icons/bottle-svgrepo-com-yellow.svg';
|
import bottleYellow from '../assets/icons/bottle-svgrepo-com-yellow.svg';
|
||||||
import bottleGreen from '../assets/icons/bottle-svgrepo-com-green.svg';
|
import bottleGreen from '../assets/icons/bottle-svgrepo-com-green.svg';
|
||||||
import bottleRed from '../assets/icons/bottle-svgrepo-com-red.svg';
|
import bottleRed from '../assets/icons/bottle-svgrepo-com-red.svg';
|
||||||
import {Shipment_Input, DefaultService, OpenAPI} from "../../openapi";
|
import { Shipment_Input, DefaultService, OpenAPI } from "../../openapi";
|
||||||
|
|
||||||
//interface ShipmentPanelProps {
|
interface ShipmentPanelProps {
|
||||||
// selectedPage: string;
|
selectedPage?: string;
|
||||||
// setIsCreatingShipment: Dispatch<SetStateAction<boolean>>;
|
setIsCreatingShipment: Dispatch<SetStateAction<boolean>>;
|
||||||
// newShipment: Shipment; // Ensure this aligns with the Shipment type
|
selectShipment: (shipment: Shipment_Input | null) => void;
|
||||||
// setNewShipment: Dispatch<SetStateAction<Shipment>>;
|
sx?: SxProps;
|
||||||
// selectShipment: (shipment: Shipment | null) => void; // Allow null for deselection
|
}
|
||||||
// sx?: SxProps; // Optional sx prop for styling
|
|
||||||
//}
|
|
||||||
|
|
||||||
const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
|
const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
|
||||||
setIsCreatingShipment,
|
setIsCreatingShipment,
|
||||||
//newShipment,
|
|
||||||
//setNewShipment,
|
|
||||||
//selectedPage,
|
|
||||||
selectShipment,
|
selectShipment,
|
||||||
sx,
|
sx,
|
||||||
}) => {
|
}) => {
|
||||||
const [shipments, setShipments] = useState<Shipment[]>([]);
|
const [shipments, setShipments] = useState<Shipment_Input[]>([]);
|
||||||
const [selectedShipment, setSelectedShipment] = useState<Shipment | null>(null);
|
const [selectedShipment, setSelectedShipment] = useState<Shipment_Input | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
||||||
|
|
||||||
const handleOpenUploadDialog = () => {
|
const fetchShipments = async () => {
|
||||||
setUploadDialogOpen(true);
|
console.log('trying to fetch some shipments');
|
||||||
|
try {
|
||||||
|
const s: Shipment_Input[] = await DefaultService.getShipmentsShipmentsGet();
|
||||||
|
setShipments(s);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch shipments:', error);
|
||||||
|
setError("Failed to fetch shipments. Please try again later.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseUploadDialog = () => {
|
useEffect(() => {
|
||||||
setUploadDialogOpen(false);
|
OpenAPI.BASE = 'http://127.0.0.1:8000';
|
||||||
};
|
fetchShipments();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// Status icon mapping
|
|
||||||
const statusIconMap: Record<string, string> = {
|
const statusIconMap: Record<string, string> = {
|
||||||
"In Transit": bottleYellow,
|
"In Transit": bottleYellow,
|
||||||
"Delivered": bottleGreen,
|
"Delivered": bottleGreen,
|
||||||
@ -52,26 +52,7 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
|
|||||||
"Unknown": bottleRed,
|
"Unknown": bottleRed,
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleShipmentSelection = (shipment: Shipment_Input) => {
|
||||||
OpenAPI.BASE = 'http://127.0.0.1:8000';
|
|
||||||
const fetchShipments = async () => {
|
|
||||||
console.log('trying to fetch some shipments');
|
|
||||||
try {
|
|
||||||
// Await the response from the service call
|
|
||||||
const s: Shipment_Input[] = await DefaultService.getShipmentsShipmentsGet();
|
|
||||||
// Set the shipments state with the fetched data
|
|
||||||
setShipments(s);
|
|
||||||
} catch (error) {
|
|
||||||
// If an error occurs, set the error message
|
|
||||||
console.error('Failed to fetch shipments:', error);
|
|
||||||
setError("Failed to fetch shipments. Please try again later.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchShipments();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleShipmentSelection = (shipment: Shipment) => {
|
|
||||||
const isCurrentlySelected = selectedShipment?.shipment_id === shipment.shipment_id;
|
const isCurrentlySelected = selectedShipment?.shipment_id === shipment.shipment_id;
|
||||||
setSelectedShipment(isCurrentlySelected ? null : shipment);
|
setSelectedShipment(isCurrentlySelected ? null : shipment);
|
||||||
selectShipment(isCurrentlySelected ? null : shipment);
|
selectShipment(isCurrentlySelected ? null : shipment);
|
||||||
@ -83,11 +64,19 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
|
|||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
const updatedShipments = shipments.filter(shipment => shipment.shipment_id !== selectedShipment.shipment_id);
|
const updatedShipments = shipments.filter(shipment => shipment.shipment_id !== selectedShipment.shipment_id);
|
||||||
setShipments(updatedShipments);
|
setShipments(updatedShipments);
|
||||||
setSelectedShipment(null); // Optionally clear the selected shipment
|
setSelectedShipment(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenUploadDialog = () => {
|
||||||
|
setUploadDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseUploadDialog = () => {
|
||||||
|
setUploadDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ width: '90%', borderRight: '1px solid #ccc', padding: 2, ...sx }}>
|
<Box sx={{ width: '90%', borderRight: '1px solid #ccc', padding: 2, ...sx }}>
|
||||||
{error && <Typography color="error">{error}</Typography>}
|
{error && <Typography color="error">{error}</Typography>}
|
||||||
@ -98,21 +87,7 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<AddIcon />}
|
startIcon={<AddIcon />}
|
||||||
onClick={() => {
|
onClick={() => setIsCreatingShipment(true)}
|
||||||
// setNewShipment({
|
|
||||||
// shipment_id: '', // Ensure this matches the Shipment type
|
|
||||||
// shipment_name: '',
|
|
||||||
// shipment_status: '',
|
|
||||||
// number_of_dewars: 0,
|
|
||||||
// shipment_date: '',
|
|
||||||
// contact_person: null, // Keep this as null to match Shipment type
|
|
||||||
// dewars: [],
|
|
||||||
// return_address: [], // Make sure return_address is initialized as an array
|
|
||||||
// proposal_number: undefined, // Optional property
|
|
||||||
// comments: '', // Optional property
|
|
||||||
// });
|
|
||||||
setIsCreatingShipment(true);
|
|
||||||
}}
|
|
||||||
sx={{ marginBottom: 2, padding: '10px 16px' }}
|
sx={{ marginBottom: 2, padding: '10px 16px' }}
|
||||||
>
|
>
|
||||||
Create Shipment
|
Create Shipment
|
||||||
@ -144,8 +119,8 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{display: 'flex', alignItems: 'center'}}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<div style={{position: 'relative', marginRight: '8px'}}>
|
<div style={{ position: 'relative', marginRight: '8px' }}>
|
||||||
<img
|
<img
|
||||||
src={statusIconMap[shipment.shipment_status] || bottleGrey}
|
src={statusIconMap[shipment.shipment_status] || bottleGrey}
|
||||||
alt={`Status: ${shipment.shipment_status}`}
|
alt={`Status: ${shipment.shipment_status}`}
|
||||||
@ -168,36 +143,34 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div>{shipment.shipment_name}</div>
|
<div>{shipment.shipment_name}</div>
|
||||||
<div style={{fontSize: '0.6rem', color: '#ccc'}}>{shipment.shipment_date}</div>
|
<div style={{ fontSize: '0.6rem', color: '#ccc' }}>{shipment.shipment_date}</div>
|
||||||
<div style={{fontSize: '0.6rem', color: '#ccc'}}>
|
<div style={{ fontSize: '0.6rem', color: '#ccc' }}>
|
||||||
Total
|
Total Pucks: {shipment.dewars.reduce((total, dewar) => total + dewar.number_of_pucks, 0)}
|
||||||
Pucks: {shipment.dewars.reduce((total, dewar) => total + dewar.number_of_pucks, 0)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{display: 'flex', alignItems: 'center'}}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleOpenUploadDialog}
|
onClick={handleOpenUploadDialog}
|
||||||
color="primary"
|
color="primary"
|
||||||
title="Upload Sample Data Sheet"
|
title="Upload Sample Data Sheet"
|
||||||
sx={{marginLeft: 1}}
|
sx={{ marginLeft: 1 }}
|
||||||
>
|
>
|
||||||
<UploadFileIcon/>
|
<UploadFileIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{selectedShipment?.shipment_id === shipment.shipment_id && (
|
{selectedShipment?.shipment_id === shipment.shipment_id && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleDeleteShipment}
|
onClick={handleDeleteShipment}
|
||||||
color="error"
|
color="error"
|
||||||
title="Delete Shipment"
|
title="Delete Shipment"
|
||||||
sx={{marginLeft: 1}}
|
sx={{ marginLeft: 1 }}
|
||||||
>
|
>
|
||||||
<DeleteIcon/>
|
<DeleteIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
{/* UploadDialog component */}
|
|
||||||
<UploadDialog
|
<UploadDialog
|
||||||
open={uploadDialogOpen}
|
open={uploadDialogOpen}
|
||||||
onClose={handleCloseUploadDialog}
|
onClose={handleCloseUploadDialog}
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Grid } from '@mui/material';
|
|
||||||
import ShipmentPanel from './ShipmentPanel.tsx';
|
|
||||||
import ShipmentDetails from './ShipmentDetails.tsx';
|
|
||||||
import ShipmentForm from './ShipmentForm.tsx';
|
|
||||||
import ParentComponent from "./ParentComponent.tsx";
|
|
||||||
import { Shipment, Dewar } from '../types.ts';
|
|
||||||
import { ContactPerson, Address, Proposal } from '../types.ts';
|
|
||||||
|
|
||||||
|
|
||||||
const ShipmentView: React.FC<ParentComponent> = ({
|
|
||||||
newShipment,
|
|
||||||
setNewShipment,
|
|
||||||
isCreatingShipment,
|
|
||||||
setIsCreatingShipment,
|
|
||||||
selectedShipment,
|
|
||||||
selectShipment,
|
|
||||||
selectedDewar,
|
|
||||||
setSelectedDewar,
|
|
||||||
handleSaveShipment,
|
|
||||||
contactPersons,
|
|
||||||
returnAddresses,
|
|
||||||
proposals,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Grid container spacing={2} sx={{ height: '100vh' }}>
|
|
||||||
{/* Left column: ShipmentPanel */}
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
md={3}
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
flexGrow: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ShipmentPanel
|
|
||||||
selectedPage="Shipments"
|
|
||||||
setIsCreatingShipment={setIsCreatingShipment}
|
|
||||||
newShipment={newShipment}
|
|
||||||
setNewShipment={setNewShipment}
|
|
||||||
selectShipment={selectShipment}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Right column: ShipmentForm or ShipmentDetails */}
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
md={9}
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
flexGrow: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isCreatingShipment ? (
|
|
||||||
<ShipmentForm
|
|
||||||
newShipment={newShipment}
|
|
||||||
setNewShipment={setNewShipment}
|
|
||||||
handleSaveShipment={handleSaveShipment}
|
|
||||||
contactPersons={contactPersons}
|
|
||||||
proposals={proposals}
|
|
||||||
returnAddresses={returnAddresses}
|
|
||||||
sx={{ flexGrow: 1 }} // Allow form to grow and take available space
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ShipmentDetails
|
|
||||||
selectedShipment={selectedShipment}
|
|
||||||
setSelectedDewar={setSelectedDewar}
|
|
||||||
isCreatingShipment={isCreatingShipment}
|
|
||||||
newShipment={newShipment}
|
|
||||||
setNewShipment={setNewShipment}
|
|
||||||
handleSaveShipment={handleSaveShipment}
|
|
||||||
contactPersons={contactPersons}
|
|
||||||
proposals={proposals}
|
|
||||||
returnAddresses={returnAddresses}
|
|
||||||
sx={{ flexGrow: 1 }} // Allow details to grow and take available space
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShipmentView;
|
|
@ -1,70 +1,138 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import ShipmentPanel from '../components/ShipmentPanel';
|
import { Grid } from '@mui/material';
|
||||||
import ShipmentForm from '../components/ShipmentForm'; // Import the ShipmentForm component
|
import ShipmentPanel from '../components/ShipmentPanel.tsx';
|
||||||
import { Shipment, ContactPerson, Proposal, Address } from '../types'; // Ensure these types are defined
|
// import ShipmentDetails from '../components/ShipmentDetails.tsx'; // Commented out for now
|
||||||
|
import ShipmentForm from '../components/ShipmentForm.tsx';
|
||||||
|
import { ContactPerson, Address, Proposal, Dewar, DefaultService, Shipment_Input } from '../../openapi'; // Import your API service
|
||||||
|
|
||||||
const ShipmentView: React.FC = () => {
|
const ShipmentView: React.FC = () => {
|
||||||
const [isCreatingShipment, setIsCreatingShipment] = useState(false); // State to track if the form is open
|
// State for managing shipments
|
||||||
const [newShipment, setNewShipment] = useState<Shipment>({
|
const [shipments, setShipments] = useState<Shipment_Input[]>([]);
|
||||||
shipment_id: '',
|
const [newShipment, setNewShipment] = useState<Shipment_Input>({
|
||||||
shipment_name: '',
|
shipment_name: '',
|
||||||
shipment_status: '',
|
contact_person: [],
|
||||||
number_of_dewars: 0,
|
proposal_number: '',
|
||||||
shipment_date: '',
|
|
||||||
contact_person: null,
|
|
||||||
dewars: [],
|
|
||||||
return_address: [],
|
return_address: [],
|
||||||
proposal_number: undefined,
|
|
||||||
comments: '',
|
comments: '',
|
||||||
|
dewars: [],
|
||||||
});
|
});
|
||||||
|
const [isCreatingShipment, setIsCreatingShipment] = useState(false);
|
||||||
|
const [selectedShipment, setSelectedShipment] = useState<Shipment_Input | null>(null);
|
||||||
|
const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null);
|
||||||
|
const [contactPersons, setContactPersons] = useState<ContactPerson[]>([]);
|
||||||
|
const [returnAddresses, setReturnAddresses] = useState<Address[]>([]);
|
||||||
|
const [proposals, setProposals] = useState<Proposal[]>([]);
|
||||||
|
|
||||||
// Dummy data for contact persons, proposals, and addresses
|
// Fetch initial shipments data
|
||||||
const contactPersons: ContactPerson[] = [
|
const fetchShipments = async () => {
|
||||||
// Populate with contact person objects
|
try {
|
||||||
];
|
const fetchedShipments = await DefaultService.getShipmentsShipmentsGet();
|
||||||
const proposals: Proposal[] = [
|
setShipments(fetchedShipments);
|
||||||
// Populate with proposal objects
|
} catch (error) {
|
||||||
];
|
console.error("Failed to fetch shipments:", error);
|
||||||
const returnAddresses: Address[] = [
|
}
|
||||||
// Populate with return address objects
|
|
||||||
];
|
|
||||||
|
|
||||||
const selectShipment = (shipment: Shipment | null) => {
|
|
||||||
console.log('Selected Shipment:', shipment);
|
|
||||||
// Additional logic for selected shipment
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveShipment = () => {
|
useEffect(() => {
|
||||||
console.log('Saving shipment:', newShipment);
|
fetchShipments();
|
||||||
setIsCreatingShipment(false); // Close the form after saving
|
}, []);
|
||||||
// Add logic to save the shipment
|
|
||||||
|
// Handle saving a new shipment
|
||||||
|
const handleSaveShipment = async (shipment: Shipment_Input) => {
|
||||||
|
try {
|
||||||
|
const savedShipment = await DefaultService.createShipmentShipmentsPost(shipment);
|
||||||
|
// Update the shipments list with the newly created shipment
|
||||||
|
setShipments(prev => [...prev, savedShipment]); // Add the new shipment to the state
|
||||||
|
onShipmentCreated(savedShipment); // Trigger the callback after saving
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save shipment:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shipment creation callback
|
||||||
|
const onShipmentCreated = async (newShipment: Shipment_Input) => {
|
||||||
|
setIsCreatingShipment(false); // Close the form
|
||||||
|
console.log("Shipment created:", newShipment);
|
||||||
|
|
||||||
|
// Reset the form
|
||||||
|
setNewShipment({
|
||||||
|
shipment_name: '',
|
||||||
|
contact_person: [],
|
||||||
|
proposal_number: [],
|
||||||
|
return_address: [],
|
||||||
|
comments: '',
|
||||||
|
dewars: [],
|
||||||
|
});
|
||||||
|
await fetchShipments(); // Re-fetch the shipments after creation
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to select a shipment
|
||||||
|
const selectShipment = (shipment: Shipment_Input | null) => {
|
||||||
|
setSelectedShipment(shipment);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', padding: '16px' }}>
|
<Grid container spacing={2} sx={{ height: '100vh' }}>
|
||||||
{/* Shipment Panel on the left side */}
|
{/* Left column: ShipmentPanel */}
|
||||||
<ShipmentPanel
|
<Grid
|
||||||
selectedPage="shipments"
|
item
|
||||||
setIsCreatingShipment={setIsCreatingShipment} // Pass the state setter
|
xs={12}
|
||||||
selectShipment={selectShipment}
|
md={3}
|
||||||
sx={{ width: '300px' }} // Adjust width as needed
|
sx={{
|
||||||
/>
|
display: 'flex',
|
||||||
{/* Conditional rendering of the ShipmentForm */}
|
flexDirection: 'column',
|
||||||
<div style={{ marginLeft: '16px', flexGrow: 1 }}>
|
flexGrow: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShipmentPanel
|
||||||
|
selectedPage="Shipments"
|
||||||
|
setIsCreatingShipment={setIsCreatingShipment}
|
||||||
|
fetchShipments={fetchShipments}
|
||||||
|
shipments={shipments} // Pass down the updated shipments
|
||||||
|
selectShipment={selectShipment}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Right column: ShipmentForm or ShipmentDetails */}
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={12}
|
||||||
|
md={9}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flexGrow: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{isCreatingShipment ? (
|
{isCreatingShipment ? (
|
||||||
<ShipmentForm
|
<ShipmentForm
|
||||||
newShipment={newShipment}
|
newShipment={newShipment}
|
||||||
setNewShipment={setNewShipment}
|
setNewShipment={setNewShipment}
|
||||||
handleSaveShipment={handleSaveShipment}
|
handleSaveShipment={handleSaveShipment}
|
||||||
|
onShipmentCreated={onShipmentCreated} // Pass this prop
|
||||||
contactPersons={contactPersons}
|
contactPersons={contactPersons}
|
||||||
proposals={proposals}
|
proposals={proposals}
|
||||||
returnAddresses={returnAddresses}
|
returnAddresses={returnAddresses}
|
||||||
|
sx={{ flexGrow: 1 }} // Allow form to grow and take available space
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div> {/* Content when form is not open */} </div>
|
// Commented out ShipmentDetails for now
|
||||||
|
// <ShipmentDetails
|
||||||
|
// selectedShipment={selectedShipment}
|
||||||
|
// setSelectedDewar={setSelectedDewar}
|
||||||
|
// isCreatingShipment={isCreatingShipment}
|
||||||
|
// newShipment={newShipment}
|
||||||
|
// setNewShipment={setNewShipment}
|
||||||
|
// handleSaveShipment={handleSaveShipment}
|
||||||
|
// contactPersons={contactPersons}
|
||||||
|
// proposals={proposals}
|
||||||
|
// returnAddresses={returnAddresses}
|
||||||
|
// sx={{ flexGrow: 1 }} // Allow details to grow and take available space
|
||||||
|
// />
|
||||||
|
<div>No shipment details available.</div> // Placeholder while ShipmentDetails is commented out
|
||||||
)}
|
)}
|
||||||
</div>
|
</Grid>
|
||||||
</div>
|
</Grid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user