changing contact person and address of a specific dewar is now possible
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
from fastapi import FastAPI, HTTPException, status
|
from fastapi import FastAPI, HTTPException, status, Query
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.logger import logger
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import logging
|
import logging
|
||||||
@@ -265,7 +266,12 @@ async def get_proposals():
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/shipments", response_model=List[Shipment])
|
@app.get("/shipments", response_model=List[Shipment])
|
||||||
async def get_shipments():
|
async def get_shipments(shipment_id: Optional[str] = Query(None, description="ID of the specific shipment to retrieve")):
|
||||||
|
if shipment_id:
|
||||||
|
shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None)
|
||||||
|
if not shipment:
|
||||||
|
raise HTTPException(status_code=404, detail="Shipment not found")
|
||||||
|
return [shipment]
|
||||||
return shipments
|
return shipments
|
||||||
|
|
||||||
|
|
||||||
@@ -274,21 +280,17 @@ async def delete_shipment(shipment_id: str):
|
|||||||
global shipments # Use global variable to access the shipments list
|
global shipments # Use global variable to access the shipments list
|
||||||
shipments = [shipment for shipment in shipments if shipment.shipment_id != shipment_id]
|
shipments = [shipment for shipment in shipments if shipment.shipment_id != shipment_id]
|
||||||
|
|
||||||
|
|
||||||
@app.post("/shipments/{shipment_id}/add_dewar", response_model=Shipment)
|
@app.post("/shipments/{shipment_id}/add_dewar", response_model=Shipment)
|
||||||
async def add_dewar_to_shipment(shipment_id: str, dewar_id: str):
|
async def add_dewar_to_shipment(shipment_id: str, dewar_id: str):
|
||||||
# Log received parameters for debugging
|
|
||||||
logging.info(f"Received request to add dewar {dewar_id} to shipment {shipment_id}")
|
|
||||||
|
|
||||||
# Find the shipment by id
|
# Find the shipment by id
|
||||||
shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None)
|
shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None)
|
||||||
if not shipment:
|
if not shipment:
|
||||||
logging.error("Shipment not found")
|
|
||||||
raise HTTPException(status_code=404, detail="Shipment not found")
|
raise HTTPException(status_code=404, detail="Shipment not found")
|
||||||
|
|
||||||
# Find the dewar by id
|
# Find the dewar by id
|
||||||
dewar = next((dw for dw in dewars if dw.id == dewar_id), None)
|
dewar = next((dw for dw in dewars if dw.id == dewar_id), None)
|
||||||
if not dewar:
|
if not dewar:
|
||||||
logging.error("Dewar not found")
|
|
||||||
raise HTTPException(status_code=404, detail="Dewar not found")
|
raise HTTPException(status_code=404, detail="Dewar not found")
|
||||||
|
|
||||||
# Add the dewar to the shipment
|
# Add the dewar to the shipment
|
||||||
@@ -298,6 +300,54 @@ async def add_dewar_to_shipment(shipment_id: str, dewar_id: str):
|
|||||||
return shipment
|
return shipment
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/shipments/{shipment_id}", response_model=Shipment)
|
||||||
|
async def update_shipment(shipment_id: str, updated_shipment: Shipment):
|
||||||
|
global shipments
|
||||||
|
shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None)
|
||||||
|
|
||||||
|
if not shipment:
|
||||||
|
raise HTTPException(status_code=404, detail="Shipment not found")
|
||||||
|
|
||||||
|
logger.info(f"Updating shipment: {shipment_id}")
|
||||||
|
logger.info(f"Updated shipment data: {updated_shipment}")
|
||||||
|
|
||||||
|
# Create a dictionary of existing dewars for fast lookup
|
||||||
|
existing_dewar_dict = {dewar.id: dewar for dewar in shipment.dewars}
|
||||||
|
|
||||||
|
# Update or add dewars from the updated shipment data
|
||||||
|
for updated_dewar in updated_shipment.dewars:
|
||||||
|
if updated_dewar.id in existing_dewar_dict:
|
||||||
|
# Update existing dewar
|
||||||
|
existing_dewar_dict[updated_dewar.id].dewar_name = updated_dewar.dewar_name
|
||||||
|
existing_dewar_dict[updated_dewar.id].tracking_number = updated_dewar.tracking_number
|
||||||
|
existing_dewar_dict[updated_dewar.id].number_of_pucks = updated_dewar.number_of_pucks
|
||||||
|
existing_dewar_dict[updated_dewar.id].number_of_samples = updated_dewar.number_of_samples
|
||||||
|
existing_dewar_dict[updated_dewar.id].return_address = updated_dewar.return_address
|
||||||
|
existing_dewar_dict[updated_dewar.id].contact_person = updated_dewar.contact_person
|
||||||
|
existing_dewar_dict[updated_dewar.id].status = updated_dewar.status
|
||||||
|
existing_dewar_dict[updated_dewar.id].ready_date = updated_dewar.ready_date
|
||||||
|
existing_dewar_dict[updated_dewar.id].shipping_date = updated_dewar.shipping_date
|
||||||
|
existing_dewar_dict[updated_dewar.id].arrival_date = updated_dewar.arrival_date
|
||||||
|
existing_dewar_dict[updated_dewar.id].returning_date = updated_dewar.returning_date
|
||||||
|
existing_dewar_dict[updated_dewar.id].qrcode = updated_dewar.qrcode
|
||||||
|
else:
|
||||||
|
# Add new dewar
|
||||||
|
shipment.dewars.append(updated_dewar)
|
||||||
|
|
||||||
|
# Update the shipment's fields
|
||||||
|
shipment.shipment_name = updated_shipment.shipment_name
|
||||||
|
shipment.shipment_date = updated_shipment.shipment_date
|
||||||
|
shipment.shipment_status = updated_shipment.shipment_status
|
||||||
|
shipment.contact_person = updated_shipment.contact_person
|
||||||
|
shipment.proposal_number = updated_shipment.proposal_number
|
||||||
|
shipment.return_address = updated_shipment.return_address
|
||||||
|
shipment.comments = updated_shipment.comments
|
||||||
|
|
||||||
|
logger.info(f"Shipment after update: {shipment}")
|
||||||
|
|
||||||
|
return shipment
|
||||||
|
|
||||||
|
|
||||||
@app.get("/dewars", response_model=List[Dewar])
|
@app.get("/dewars", response_model=List[Dewar])
|
||||||
async def get_dewars():
|
async def get_dewars():
|
||||||
return dewars
|
return dewars
|
||||||
@@ -312,16 +362,13 @@ async def create_dewar(dewar: Dewar) -> Dewar:
|
|||||||
|
|
||||||
return dewar # Return the newly created dewar
|
return dewar # Return the newly created dewar
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/shipments/{shipment_id}/remove_dewar/{dewar_id}", response_model=Shipment)
|
@app.delete("/shipments/{shipment_id}/remove_dewar/{dewar_id}", response_model=Shipment)
|
||||||
async def remove_dewar_from_shipment(shipment_id: str, dewar_id: str):
|
async def remove_dewar_from_shipment(shipment_id: str, dewar_id: str):
|
||||||
"""Remove a dewar from a shipment."""
|
"""Remove a dewar from a shipment."""
|
||||||
# Log parameters
|
|
||||||
logging.info(f"Received request to remove dewar {dewar_id} from shipment {shipment_id}")
|
|
||||||
|
|
||||||
# Find the shipment by ID
|
# Find the shipment by ID
|
||||||
shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None)
|
shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None)
|
||||||
if not shipment:
|
if not shipment:
|
||||||
logging.error(f"Shipment with ID {shipment_id} not found")
|
|
||||||
raise HTTPException(status_code=404, detail="Shipment not found")
|
raise HTTPException(status_code=404, detail="Shipment not found")
|
||||||
|
|
||||||
# Remove the dewar from the shipment
|
# Remove the dewar from the shipment
|
||||||
@@ -350,7 +397,6 @@ async def create_shipment(shipment: Shipment):
|
|||||||
# Creation of a new contact
|
# Creation of a new contact
|
||||||
@app.post("/contacts", response_model=ContactPerson, status_code=status.HTTP_201_CREATED)
|
@app.post("/contacts", response_model=ContactPerson, status_code=status.HTTP_201_CREATED)
|
||||||
async def create_contact(contact: ContactPerson):
|
async def create_contact(contact: ContactPerson):
|
||||||
logging.info(f"Received contact creation request: {contact}")
|
|
||||||
# Check for duplicate contact by email (or other unique fields)
|
# Check for duplicate contact by email (or other unique fields)
|
||||||
if any(c.email == contact.email for c in contacts):
|
if any(c.email == contact.email for c in contacts):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -372,7 +418,6 @@ async def create_contact(contact: ContactPerson):
|
|||||||
# Creation of a return address
|
# Creation of a return address
|
||||||
@app.post("/return_addresses", response_model=Address, status_code=status.HTTP_201_CREATED)
|
@app.post("/return_addresses", response_model=Address, status_code=status.HTTP_201_CREATED)
|
||||||
async def create_return_address(address: Address):
|
async def create_return_address(address: Address):
|
||||||
logging.info(f"Received address creation request: {address}")
|
|
||||||
# Check for duplicate address by city
|
# Check for duplicate address by city
|
||||||
if any(a.city == address.city for a in return_addresses):
|
if any(a.city == address.city for a in return_addresses):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -388,4 +433,4 @@ async def create_return_address(address: Address):
|
|||||||
address.id = 1 if address.id is None else address.id
|
address.id = 1 if address.id is None else address.id
|
||||||
|
|
||||||
return_addresses.append(address)
|
return_addresses.append(address)
|
||||||
return address
|
return address
|
||||||
@@ -1,85 +1,271 @@
|
|||||||
import * as React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Box, Typography, TextField, Button, Select, MenuItem, Snackbar } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Snackbar
|
||||||
|
} from '@mui/material';
|
||||||
import QRCode from 'react-qr-code';
|
import QRCode from 'react-qr-code';
|
||||||
import { ContactPerson, Address, Dewar } from "../../openapi";
|
import {
|
||||||
|
ContactPerson,
|
||||||
|
Address,
|
||||||
|
Dewar,
|
||||||
|
DefaultService
|
||||||
|
} from '../../openapi';
|
||||||
|
|
||||||
interface DewarDetailsProps {
|
interface DewarDetailsProps {
|
||||||
dewar: Dewar;
|
dewar: Dewar;
|
||||||
trackingNumber: string;
|
trackingNumber: string;
|
||||||
setTrackingNumber: React.Dispatch<React.SetStateAction<string>>;
|
setTrackingNumber: React.Dispatch<React.SetStateAction<string>>;
|
||||||
contactPersons: ContactPerson[];
|
initialContactPersons: ContactPerson[];
|
||||||
returnAddresses: Address[];
|
initialReturnAddresses: Address[];
|
||||||
|
defaultContactPerson?: ContactPerson;
|
||||||
|
defaultReturnAddress?: Address;
|
||||||
|
shipmentId: string;
|
||||||
|
refreshShipments: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const DewarDetails: React.FC<DewarDetailsProps> = ({
|
const DewarDetails: React.FC<DewarDetailsProps> = ({
|
||||||
dewar,
|
dewar,
|
||||||
trackingNumber,
|
trackingNumber,
|
||||||
setTrackingNumber,
|
//setTrackingNumber,
|
||||||
contactPersons,
|
initialContactPersons = [],
|
||||||
returnAddresses
|
initialReturnAddresses = [],
|
||||||
|
defaultContactPerson,
|
||||||
|
defaultReturnAddress,
|
||||||
|
shipmentId,
|
||||||
|
refreshShipments,
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedContactPerson, setSelectedContactPerson] = React.useState<string>(contactPersons[0]?.firstname || '');
|
const [localTrackingNumber, setLocalTrackingNumber] = useState(trackingNumber);
|
||||||
const [selectedReturnAddress, setSelectedReturnAddress] = React.useState<string>(returnAddresses[0]?.id?.toString() || '');
|
const [contactPersons, setContactPersons] = useState<ContactPerson[]>(initialContactPersons);
|
||||||
|
const [returnAddresses, setReturnAddresses] = useState<Address[]>(initialReturnAddresses);
|
||||||
|
const [selectedContactPerson, setSelectedContactPerson] = useState<string>('');
|
||||||
|
const [selectedReturnAddress, setSelectedReturnAddress] = useState<string>('');
|
||||||
|
const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false);
|
||||||
|
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false);
|
||||||
|
const [newContactPerson, setNewContactPerson] = useState({
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
phone_number: '',
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
const [newReturnAddress, setNewReturnAddress] = useState<Address>({
|
||||||
|
street: '',
|
||||||
|
city: '',
|
||||||
|
zipcode: '',
|
||||||
|
country: '',
|
||||||
|
});
|
||||||
|
const [changesMade, setChangesMade] = useState<boolean>(false);
|
||||||
|
const [feedbackMessage, setFeedbackMessage] = useState<string>('');
|
||||||
|
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
|
||||||
|
const [updatedDewar, setUpdatedDewar] = useState<Dewar>(dewar);
|
||||||
|
|
||||||
const updateSelectedDetails = (contactPerson?: { firstname: string }, returnAddress?: Address) => {
|
useEffect(() => {
|
||||||
if (contactPerson) setSelectedContactPerson(contactPerson.firstname);
|
setSelectedContactPerson(
|
||||||
if (returnAddress?.id != null) {
|
(dewar.contact_person?.[0]?.id?.toString() || defaultContactPerson?.id?.toString() || '')
|
||||||
setSelectedReturnAddress(returnAddress.id.toString());
|
);
|
||||||
|
setSelectedReturnAddress(
|
||||||
|
(dewar.return_address?.[0]?.id?.toString() || defaultReturnAddress?.id?.toString() || '')
|
||||||
|
);
|
||||||
|
setLocalTrackingNumber(dewar.tracking_number || '');
|
||||||
|
}, [dewar, defaultContactPerson, defaultReturnAddress]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('DewarDetails - dewar updated:', dewar);
|
||||||
|
}, [dewar]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getContacts = async () => {
|
||||||
|
try {
|
||||||
|
const c: ContactPerson[] = await DefaultService.getContactsContactsGet();
|
||||||
|
setContactPersons(c);
|
||||||
|
} catch {
|
||||||
|
setFeedbackMessage('Failed to load contact persons. Please try again later.');
|
||||||
|
setOpenSnackbar(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
const getReturnAddresses = async () => {
|
||||||
updateSelectedDetails(contactPersons[0], returnAddresses[0]);
|
try {
|
||||||
}, [contactPersons, returnAddresses]);
|
const a: Address[] = await DefaultService.getReturnAddressesReturnAddressesGet();
|
||||||
|
setReturnAddresses(a);
|
||||||
|
} catch {
|
||||||
|
setFeedbackMessage('Failed to load return addresses. Please try again later.');
|
||||||
|
setOpenSnackbar(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const [newContactPerson, setNewContactPerson] = React.useState<string>('');
|
getContacts();
|
||||||
const [newReturnAddress, setNewReturnAddress] = React.useState<string>('');
|
getReturnAddresses();
|
||||||
const [feedbackMessage, setFeedbackMessage] = React.useState<string>('');
|
}, []);
|
||||||
const [openSnackbar, setOpenSnackbar] = React.useState<boolean>(false);
|
|
||||||
|
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);
|
||||||
|
|
||||||
// Ensure dewar is defined before attempting to render the dewar details
|
|
||||||
if (!dewar) {
|
if (!dewar) {
|
||||||
return <Typography>No dewar selected.</Typography>;
|
return <Typography>No dewar selected.</Typography>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddContact = () => {
|
const handleAddContact = async () => {
|
||||||
if (newContactPerson.trim() === '') {
|
if (!validateEmail(newContactPerson.email) || !validatePhoneNumber(newContactPerson.phone_number) ||
|
||||||
setFeedbackMessage('Please enter a valid contact person name.');
|
!newContactPerson.firstName || !newContactPerson.lastName) {
|
||||||
} else {
|
setFeedbackMessage('Please fill in all new contact person fields correctly.');
|
||||||
setNewContactPerson(''); // Add logic to save the new contact person
|
setOpenSnackbar(true);
|
||||||
setFeedbackMessage('Contact person added successfully.');
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
firstname: newContactPerson.firstName,
|
||||||
|
lastname: newContactPerson.lastName,
|
||||||
|
phone_number: newContactPerson.phone_number,
|
||||||
|
email: newContactPerson.email,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const c: ContactPerson = await DefaultService.createContactContactsPost(payload);
|
||||||
|
setContactPersons([...contactPersons, c]);
|
||||||
|
setFeedbackMessage('Contact person added successfully.');
|
||||||
|
setNewContactPerson({ firstName: '', lastName: '', phone_number: '', email: '' });
|
||||||
|
setSelectedContactPerson(c.id?.toString() || '');
|
||||||
|
} catch {
|
||||||
|
setFeedbackMessage('Failed to create a new contact person. Please try again later.');
|
||||||
|
}
|
||||||
|
|
||||||
setOpenSnackbar(true);
|
setOpenSnackbar(true);
|
||||||
|
setIsCreatingContactPerson(false);
|
||||||
|
setChangesMade(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddAddress = () => {
|
const handleAddAddress = async () => {
|
||||||
if (newReturnAddress.trim() === '') {
|
if (!validateZipCode(newReturnAddress.zipcode) || !newReturnAddress.street || !newReturnAddress.city ||
|
||||||
setFeedbackMessage('Please enter a valid return address.');
|
!newReturnAddress.country) {
|
||||||
} else {
|
setFeedbackMessage('Please fill in all new return address fields correctly.');
|
||||||
setNewReturnAddress(''); // Add logic to save the new return address
|
setOpenSnackbar(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
street: newReturnAddress.street.trim(),
|
||||||
|
city: newReturnAddress.city.trim(),
|
||||||
|
zipcode: newReturnAddress.zipcode.trim(),
|
||||||
|
country: newReturnAddress.country.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const a: Address = await DefaultService.createReturnAddressReturnAddressesPost(payload);
|
||||||
|
setReturnAddresses([...returnAddresses, a]);
|
||||||
setFeedbackMessage('Return address added successfully.');
|
setFeedbackMessage('Return address added successfully.');
|
||||||
|
setNewReturnAddress({ street: '', city: '', zipcode: '', country: '' });
|
||||||
|
setSelectedReturnAddress(a.id?.toString() || '');
|
||||||
|
} catch {
|
||||||
|
setFeedbackMessage('Failed to create a new return address. Please try again later.');
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpenSnackbar(true);
|
||||||
|
setIsCreatingReturnAddress(false);
|
||||||
|
setChangesMade(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getShipmentById = async (shipmentId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await DefaultService.getShipmentsShipmentsGet(shipmentId);
|
||||||
|
if (response && response.length > 0) {
|
||||||
|
return response[0]; // Since the result is an array, we take the first element
|
||||||
|
}
|
||||||
|
throw new Error('Shipment not found');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching shipment:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveChanges = async () => {
|
||||||
|
const formatDate = (dateString: string | undefined): string => {
|
||||||
|
if (!dateString) return '2024-01-01'; // Default date if undefined
|
||||||
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date.getTime())) return '2024-01-01'; // Default date if invalid
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!dewar.dewar_name || !selectedContactPerson || !selectedReturnAddress || !trackingNumber) {
|
||||||
|
setFeedbackMessage('Please ensure all required fields are filled.');
|
||||||
|
setOpenSnackbar(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let existingShipment;
|
||||||
|
try {
|
||||||
|
existingShipment = await getShipmentById(shipmentId);
|
||||||
|
} catch {
|
||||||
|
setFeedbackMessage('Failed to fetch existing shipment data. Please try again later.');
|
||||||
|
setOpenSnackbar(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedDewar = {
|
||||||
|
id: dewar.id, // Ensure dewar ID is included
|
||||||
|
dewar_name: dewar.dewar_name,
|
||||||
|
return_address: returnAddresses.find((a) => a.id?.toString() === selectedReturnAddress)
|
||||||
|
? [returnAddresses.find((a) => a.id?.toString() === selectedReturnAddress)]
|
||||||
|
: [],
|
||||||
|
contact_person: contactPersons.find((c) => c.id?.toString() === selectedContactPerson)
|
||||||
|
? [contactPersons.find((c) => c.id?.toString() === selectedContactPerson)]
|
||||||
|
: [],
|
||||||
|
number_of_pucks: dewar.number_of_pucks,
|
||||||
|
number_of_samples: dewar.number_of_samples,
|
||||||
|
qrcode: dewar.qrcode,
|
||||||
|
ready_date: formatDate(dewar.ready_date),
|
||||||
|
shipping_date: formatDate(dewar.shipping_date),
|
||||||
|
status: dewar.status,
|
||||||
|
tracking_number: trackingNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...existingShipment,
|
||||||
|
dewars: existingShipment.dewars?.map(d => d.id === dewar.id ? updatedDewar : d) || [], // Update specific dewar in the dewars array
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await DefaultService.updateShipmentShipmentsShipmentIdPut(shipmentId, payload);
|
||||||
|
setFeedbackMessage('Changes saved successfully.');
|
||||||
|
setChangesMade(false);
|
||||||
|
setUpdatedDewar(updatedDewar);
|
||||||
|
console.log('Calling refreshShipments');
|
||||||
|
refreshShipments(); // Trigger refresh shipments after saving changes
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
console.error('Server Response:', error.response.data);
|
||||||
|
} else {
|
||||||
|
console.error('Update Shipment Error:', error);
|
||||||
|
setFeedbackMessage('Failed to save changes. Please try again later.');
|
||||||
|
}
|
||||||
|
setOpenSnackbar(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
setOpenSnackbar(true);
|
setOpenSnackbar(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ marginTop: 2 }}>
|
<Box sx={{ marginTop: 2 }}>
|
||||||
<Typography variant="h6">Selected Dewar: {dewar.dewar_name}</Typography>
|
<Typography variant="h6">Selected Dewar: {updatedDewar.dewar_name}</Typography>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Tracking Number"
|
label="Tracking Number"
|
||||||
value={trackingNumber}
|
value={localTrackingNumber}
|
||||||
onChange={(e) => setTrackingNumber(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setLocalTrackingNumber(e.target.value);
|
||||||
|
setChangesMade(true);
|
||||||
|
}}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{ width: '300px', marginBottom: 2 }}
|
sx={{ width: '300px', marginBottom: 2 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* QR Code display */}
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
|
||||||
<Box sx={{ width: 80, height: 80, backgroundColor: '#e0e0e0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<Box sx={{ width: 80, height: 80, backgroundColor: '#e0e0e0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
{dewar.qrcode ? (
|
{updatedDewar.qrcode ? (
|
||||||
<QRCode value={dewar.qrcode} size={70} />
|
<QRCode value={updatedDewar.qrcode} size={70} />
|
||||||
) : (
|
) : (
|
||||||
<Typography>No QR code available</Typography>
|
<Typography>No QR code available</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -88,74 +274,137 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
|
|||||||
Generate QR Code
|
Generate QR Code
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Typography variant="body1">Number of Pucks: {updatedDewar.number_of_pucks}</Typography>
|
||||||
<Typography variant="body1">Number of Pucks: {dewar.number_of_pucks}</Typography>
|
<Typography variant="body1">Number of Samples: {updatedDewar.number_of_samples}</Typography>
|
||||||
<Typography variant="body1">Number of Samples: {dewar.number_of_samples}</Typography>
|
|
||||||
|
|
||||||
{/* Dropdown for Contact Person */}
|
|
||||||
<Typography variant="body1">Current Contact Person:</Typography>
|
<Typography variant="body1">Current Contact Person:</Typography>
|
||||||
<Select
|
<Select
|
||||||
value={selectedContactPerson}
|
value={selectedContactPerson}
|
||||||
onChange={(e) => setSelectedContactPerson(e.target.value)}
|
onChange={(e) => {
|
||||||
displayEmpty
|
setSelectedContactPerson(e.target.value);
|
||||||
|
setIsCreatingContactPerson(e.target.value === 'add');
|
||||||
|
setChangesMade(true);
|
||||||
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ marginBottom: 2 }}
|
sx={{ marginBottom: 2 }}
|
||||||
variant={'outlined'}
|
variant="outlined"
|
||||||
|
displayEmpty
|
||||||
>
|
>
|
||||||
<MenuItem value="" disabled>Select Contact Person</MenuItem>
|
{contactPersons?.map((person) => (
|
||||||
{contactPersons.map((person) => (
|
<MenuItem key={person.id?.toString()} value={person.id?.toString() || ''}>
|
||||||
<MenuItem key={person.id} value={person.firstname}>{person.firstname + " " + person.lastname}</MenuItem>
|
{person.firstname} {person.lastname}
|
||||||
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
<MenuItem value="add">Add New Contact Person</MenuItem>
|
<MenuItem value="add">Add New Contact Person</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
{selectedContactPerson === "add" && (
|
{isCreatingContactPerson && (
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
|
<Box sx={{ marginBottom: 2 }}>
|
||||||
<TextField
|
<TextField
|
||||||
label="New Contact Person"
|
label="First Name"
|
||||||
value={newContactPerson}
|
value={newContactPerson.firstName}
|
||||||
onChange={(e) => setNewContactPerson(e.target.value)}
|
onChange={(e) => setNewContactPerson({ ...newContactPerson, firstName: e.target.value })}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{ marginRight: 1, flexGrow: 1 }}
|
fullWidth
|
||||||
|
sx={{ marginBottom: 1 }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Last Name"
|
||||||
|
value={newContactPerson.lastName}
|
||||||
|
onChange={(e) => setNewContactPerson({ ...newContactPerson, lastName: e.target.value })}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
sx={{ marginBottom: 1 }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Phone"
|
||||||
|
value={newContactPerson.phone_number}
|
||||||
|
onChange={(e) => setNewContactPerson({ ...newContactPerson, phone_number: e.target.value })}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
sx={{ marginBottom: 1 }}
|
||||||
|
error={!validatePhoneNumber(newContactPerson.phone_number)}
|
||||||
|
helperText={!validatePhoneNumber(newContactPerson.phone_number) ? "Invalid phone number" : ""}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Email"
|
||||||
|
value={newContactPerson.email}
|
||||||
|
onChange={(e) => setNewContactPerson({ ...newContactPerson, email: e.target.value })}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
sx={{ marginBottom: 1 }}
|
||||||
|
error={!validateEmail(newContactPerson.email)}
|
||||||
|
helperText={!validateEmail(newContactPerson.email) ? "Invalid email" : ""}
|
||||||
/>
|
/>
|
||||||
<Button variant="contained" onClick={handleAddContact}>
|
<Button variant="contained" onClick={handleAddContact}>
|
||||||
Add
|
Save Contact Person
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Dropdown for Return Address */}
|
|
||||||
<Typography variant="body1">Current Return Address:</Typography>
|
<Typography variant="body1">Current Return Address:</Typography>
|
||||||
<Select
|
<Select
|
||||||
value={selectedReturnAddress}
|
value={selectedReturnAddress}
|
||||||
onChange={(e) => setSelectedReturnAddress(e.target.value)}
|
onChange={(e) => {
|
||||||
displayEmpty
|
setSelectedReturnAddress(e.target.value);
|
||||||
|
setIsCreatingReturnAddress(e.target.value === 'add');
|
||||||
|
setChangesMade(true);
|
||||||
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ marginBottom: 2 }}
|
sx={{ marginBottom: 2 }}
|
||||||
variant={'outlined'}
|
variant="outlined"
|
||||||
|
displayEmpty
|
||||||
>
|
>
|
||||||
<MenuItem value="" disabled>Select Return Address</MenuItem>
|
{returnAddresses?.map((address) => (
|
||||||
{returnAddresses.map((address) => (
|
<MenuItem key={address.id?.toString()} value={address.id?.toString() || ''}>
|
||||||
<MenuItem key={address.id ?? 'unknown'} value={address.id?.toString() ?? 'unknown'}>
|
{address.street}, {address.city}
|
||||||
{address.street} </MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
<MenuItem value="add">Add New Return Address</MenuItem>
|
<MenuItem value="add">Add New Return Address</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
{selectedReturnAddress === "add" && (
|
{isCreatingReturnAddress && (
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
|
<Box sx={{ marginBottom: 2 }}>
|
||||||
<TextField
|
<TextField
|
||||||
label="New Return Address"
|
label="Street"
|
||||||
value={newReturnAddress}
|
value={newReturnAddress.street}
|
||||||
onChange={(e) => setNewReturnAddress(e.target.value)}
|
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, street: e.target.value })}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{ marginRight: 1, flexGrow: 1 }}
|
fullWidth
|
||||||
|
sx={{ marginBottom: 1 }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="City"
|
||||||
|
value={newReturnAddress.city}
|
||||||
|
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, city: e.target.value })}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
sx={{ marginBottom: 1 }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Zip Code"
|
||||||
|
value={newReturnAddress.zipcode}
|
||||||
|
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, zipcode: e.target.value })}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
sx={{ marginBottom: 1 }}
|
||||||
|
error={!validateZipCode(newReturnAddress.zipcode)}
|
||||||
|
helperText={!validateZipCode(newReturnAddress.zipcode) ? "Invalid zip code" : ""}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Country"
|
||||||
|
value={newReturnAddress.country}
|
||||||
|
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, country: e.target.value })}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
sx={{ marginBottom: 1 }}
|
||||||
/>
|
/>
|
||||||
<Button variant="contained" onClick={handleAddAddress}>
|
<Button variant="contained" onClick={handleAddAddress}>
|
||||||
Add
|
Save Return Address
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
{changesMade && (
|
||||||
{/* Snackbar for feedback messages */}
|
<Button variant="contained" color="primary" onClick={handleSaveChanges}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Snackbar
|
<Snackbar
|
||||||
open={openSnackbar}
|
open={openSnackbar}
|
||||||
autoHideDuration={6000}
|
autoHideDuration={6000}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface ShipmentDetailsProps {
|
|||||||
setSelectedDewar: React.Dispatch<React.SetStateAction<Dewar | null>>;
|
setSelectedDewar: React.Dispatch<React.SetStateAction<Dewar | null>>;
|
||||||
setSelectedShipment: React.Dispatch<React.SetStateAction<Shipment_Input>>;
|
setSelectedShipment: React.Dispatch<React.SetStateAction<Shipment_Input>>;
|
||||||
sx?: SxProps;
|
sx?: SxProps;
|
||||||
|
refreshShipments: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
||||||
@@ -21,12 +22,12 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
|||||||
setSelectedDewar,
|
setSelectedDewar,
|
||||||
setSelectedShipment,
|
setSelectedShipment,
|
||||||
sx = {},
|
sx = {},
|
||||||
|
refreshShipments,
|
||||||
}) => {
|
}) => {
|
||||||
const [localSelectedDewar, setLocalSelectedDewar] = React.useState<Dewar | null>(null);
|
const [localSelectedDewar, setLocalSelectedDewar] = React.useState<Dewar | null>(null);
|
||||||
const [isAddingDewar, setIsAddingDewar] = React.useState<boolean>(false);
|
const [isAddingDewar, setIsAddingDewar] = React.useState<boolean>(false);
|
||||||
const [newDewar, setNewDewar] = React.useState<Partial<Dewar>>({
|
const [newDewar, setNewDewar] = React.useState<Partial<Dewar>>({
|
||||||
dewar_name: '',
|
dewar_name: '',
|
||||||
tracking_number: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// To reset localSelectedDewar when selectedShipment changes
|
// To reset localSelectedDewar when selectedShipment changes
|
||||||
@@ -34,6 +35,10 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
|||||||
setLocalSelectedDewar(null);
|
setLocalSelectedDewar(null);
|
||||||
}, [selectedShipment]);
|
}, [selectedShipment]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
console.log('ShipmentDetails - selectedShipment updated:', selectedShipment);
|
||||||
|
}, [selectedShipment]);
|
||||||
|
|
||||||
const totalPucks = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_pucks || 0), 0);
|
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);
|
const totalSamples = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_samples || 0), 0);
|
||||||
|
|
||||||
@@ -111,6 +116,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
|||||||
|
|
||||||
setIsAddingDewar(false);
|
setIsAddingDewar(false);
|
||||||
setNewDewar({ dewar_name: '', tracking_number: '' });
|
setNewDewar({ dewar_name: '', tracking_number: '' });
|
||||||
|
refreshShipments()
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Failed to add dewar or update shipment. Please try again.');
|
alert('Failed to add dewar or update shipment. Please try again.');
|
||||||
@@ -164,10 +170,14 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
|||||||
dewar={localSelectedDewar}
|
dewar={localSelectedDewar}
|
||||||
trackingNumber={localSelectedDewar.tracking_number || ''}
|
trackingNumber={localSelectedDewar.tracking_number || ''}
|
||||||
setTrackingNumber={(value) => {
|
setTrackingNumber={(value) => {
|
||||||
setLocalSelectedDewar((prev) => prev ? { ...prev, tracking_number: value as string } : prev);
|
setLocalSelectedDewar((prev) => (prev ? { ...prev, tracking_number: value as string } : prev));
|
||||||
}}
|
}}
|
||||||
contactPersons={selectedShipment.contact_person}
|
initialContactPersons={selectedShipment.contact_person}
|
||||||
returnAddresses={selectedShipment.return_address}
|
initialReturnAddresses={selectedShipment.return_address}
|
||||||
|
defaultContactPerson={selectedShipment.contact_person[0]}
|
||||||
|
defaultReturnAddress={selectedShipment.return_address[0]}
|
||||||
|
shipmentId={selectedShipment.shipment_id}
|
||||||
|
refreshShipments={refreshShipments}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -220,6 +230,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
|||||||
<Typography variant="body2">Number of Pucks: {dewar.number_of_pucks || 0}</Typography>
|
<Typography variant="body2">Number of Pucks: {dewar.number_of_pucks || 0}</Typography>
|
||||||
<Typography variant="body2">Number of Samples: {dewar.number_of_samples || 0}</Typography>
|
<Typography variant="body2">Number of Samples: {dewar.number_of_samples || 0}</Typography>
|
||||||
<Typography variant="body2">Tracking Number: {dewar.tracking_number}</Typography>
|
<Typography variant="body2">Tracking Number: {dewar.tracking_number}</Typography>
|
||||||
|
<Typography variant="body2">Contact Person: {`${dewar.contact_person[0].firstname} ${dewar.contact_person[0].lastname}`}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Grid from '@mui/material/Grid'; // Using Grid (deprecated but configurable)
|
import Grid from '@mui/material/Grid';
|
||||||
import ShipmentPanel from '../components/ShipmentPanel';
|
import ShipmentPanel from '../components/ShipmentPanel';
|
||||||
import ShipmentDetails from '../components/ShipmentDetails';
|
import ShipmentDetails from '../components/ShipmentDetails';
|
||||||
import ShipmentForm from '../components/ShipmentForm';
|
import ShipmentForm from '../components/ShipmentForm';
|
||||||
import { Dewar, Shipment_Input, DefaultService, OpenAPI } from '../../openapi';
|
import { Dewar, Shipment_Input, DefaultService, OpenAPI, ContactPerson } from '../../openapi';
|
||||||
|
|
||||||
type ShipmentViewProps = React.PropsWithChildren<Record<string, never>>;
|
type ShipmentViewProps = React.PropsWithChildren<Record<string, never>>;
|
||||||
|
|
||||||
@@ -16,21 +16,38 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
|
|||||||
const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null);
|
const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null);
|
||||||
const [shipments, setShipments] = useState<Shipment_Input[]>([]);
|
const [shipments, setShipments] = useState<Shipment_Input[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [defaultContactPerson, setDefaultContactPerson] = useState<ContactPerson | undefined>();
|
||||||
|
|
||||||
const fetchAndSetShipments = async () => {
|
const fetchAndSetShipments = async () => {
|
||||||
try {
|
try {
|
||||||
const shipmentsData: Shipment_Input[] = await DefaultService.getShipmentsShipmentsGet();
|
const shipmentsData: Shipment_Input[] = await DefaultService.getShipmentsShipmentsGet();
|
||||||
shipmentsData.sort((a, b) => new Date(b.shipment_date).getTime() - new Date(a.shipment_date).getTime());
|
shipmentsData.sort((a, b) => new Date(b.shipment_date).getTime() - new Date(a.shipment_date).getTime());
|
||||||
setShipments(shipmentsData);
|
setShipments(shipmentsData);
|
||||||
|
console.log('Fetched and set shipments:', shipmentsData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch shipments:', error);
|
||||||
setError('Failed to fetch shipments. Please try again later.');
|
setError('Failed to fetch shipments. Please try again later.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchDefaultContactPerson = async () => {
|
||||||
|
try {
|
||||||
|
const c: ContactPerson[] = await DefaultService.getContactsContactsGet();
|
||||||
|
setDefaultContactPerson(c[0]);
|
||||||
|
} catch {
|
||||||
|
setError('Failed to load contact persons. Please try again later.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAndSetShipments();
|
fetchAndSetShipments();
|
||||||
|
fetchDefaultContactPerson();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Updated shipments:', shipments);
|
||||||
|
}, [shipments]);
|
||||||
|
|
||||||
const handleSelectShipment = (shipment: Shipment_Input | null) => {
|
const handleSelectShipment = (shipment: Shipment_Input | null) => {
|
||||||
setSelectedShipment(shipment);
|
setSelectedShipment(shipment);
|
||||||
setIsCreatingShipment(false);
|
setIsCreatingShipment(false);
|
||||||
@@ -42,11 +59,13 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
|
|||||||
|
|
||||||
const renderShipmentContent = () => {
|
const renderShipmentContent = () => {
|
||||||
if (isCreatingShipment) {
|
if (isCreatingShipment) {
|
||||||
return <ShipmentForm
|
return (
|
||||||
sx={{ flexGrow: 1 }}
|
<ShipmentForm
|
||||||
onCancel={handleCancelShipmentForm}
|
sx={{ flexGrow: 1 }}
|
||||||
refreshShipments={fetchAndSetShipments} // Pass the fetch function to refresh shipments
|
onCancel={handleCancelShipmentForm}
|
||||||
/>;
|
refreshShipments={fetchAndSetShipments}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (selectedShipment) {
|
if (selectedShipment) {
|
||||||
return (
|
return (
|
||||||
@@ -57,6 +76,8 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
|
|||||||
selectedDewar={selectedDewar}
|
selectedDewar={selectedDewar}
|
||||||
setSelectedDewar={setSelectedDewar}
|
setSelectedDewar={setSelectedDewar}
|
||||||
setSelectedShipment={setSelectedShipment}
|
setSelectedShipment={setSelectedShipment}
|
||||||
|
defaultContactPerson={defaultContactPerson}
|
||||||
|
refreshShipments={fetchAndSetShipments} // Ensure refreshShipments is passed here
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user