**Commit Message:**

Enhance app with active pgroup handling and token updates

Added active pgroup state management across the app for user-specific settings. Improved token handling with decoding, saving user data, and setting OpenAPI authorization. Updated components, API calls, and forms to support dynamic pgroup selection and user-specific features.
This commit is contained in:
GotthardG
2025-01-22 13:55:26 +01:00
parent 4630bcfac5
commit 6cde57f783
23 changed files with 806 additions and 250 deletions

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { setUpToken, clearToken } from './utils/auth'; // Import the token utilities
import ResponsiveAppBar from './components/ResponsiveAppBar';
import ShipmentView from './pages/ShipmentView';
@ -16,38 +17,77 @@ const App: React.FC = () => {
const [openAddressManager, setOpenAddressManager] = useState(false);
const [openContactsManager, setOpenContactsManager] = useState(false);
const handleOpenAddressManager = () => {
setOpenAddressManager(true);
};
const handleOpenAddressManager = () => setOpenAddressManager(true);
const handleCloseAddressManager = () => setOpenAddressManager(false);
const handleOpenContactsManager = () => setOpenContactsManager(true);
const handleCloseContactsManager = () => setOpenContactsManager(false);
const handleCloseAddressManager = () => {
setOpenAddressManager(false);
};
const [pgroups, setPgroups] = useState<string[]>([]);
const [activePgroup, setActivePgroup] = useState<string>('');
const handleOpenContactsManager = () => {
setOpenContactsManager(true);
};
// On app load, configure the token
useEffect(() => {
setUpToken(); // Ensure token is loaded into OpenAPI on app initialization
}, []);
const handleCloseContactsManager = () => {
setOpenContactsManager(false);
useEffect(() => {
const updateStateFromLocalStorage = () => {
const user = localStorage.getItem('user');
console.log("[DEBUG] User data in localStorage (update):", user); // Debug
if (user) {
try {
const parsedUser = JSON.parse(user);
if (parsedUser.pgroups && Array.isArray(parsedUser.pgroups)) {
setPgroups(parsedUser.pgroups);
setActivePgroup(parsedUser.pgroups[0] || '');
console.log("[DEBUG] Pgroups updated in state:", parsedUser.pgroups);
} else {
console.warn("[DEBUG] No pgroups found in user data");
}
} catch (error) {
console.error("[DEBUG] Error parsing user data:", error);
}
} else {
console.warn("[DEBUG] No user in localStorage");
}
};
// Run on component mount
updateStateFromLocalStorage();
// Listen for localStorage changes
window.addEventListener('storage', updateStateFromLocalStorage);
// Cleanup listener on unmount
return () => {
window.removeEventListener('storage', updateStateFromLocalStorage);
};
}, []);
const handlePgroupChange = (newPgroup: string) => {
setActivePgroup(newPgroup);
console.log(`pgroup changed to: ${newPgroup}`);
};
return (
<Router>
<ResponsiveAppBar
activePgroup={activePgroup}
onOpenAddressManager={handleOpenAddressManager}
onOpenContactsManager={handleOpenContactsManager}
pgroups={pgroups || []} // Default to an empty array
currentPgroup={activePgroup}
onPgroupChange={handlePgroupChange}
/>
<Routes>
<Route path="/login" element={<LoginView />} />
<Route path="/" element={<ProtectedRoute element={<HomePage />} />} />
<Route path="/shipments" element={<ProtectedRoute element={<ShipmentView />} />} />
<Route path="/shipments" element={<ProtectedRoute element={<ShipmentView activePgroup={activePgroup} />} />} />
<Route path="/planning" element={<ProtectedRoute element={<PlanningView />} />} />
<Route path="/results" element={<ProtectedRoute element={<ResultsView />} />} />
{/* Other routes as necessary */}
</Routes>
<Modal open={openAddressManager} onClose={handleCloseAddressManager} title="Address Management">
<AddressManager />
<AddressManager pgroups={pgroups} activePgroup={activePgroup} />
</Modal>
<Modal open={openContactsManager} onClose={handleCloseContactsManager} title="Contacts Management">
<ContactsManager />

View File

@ -10,7 +10,7 @@ interface ModalProps {
const Modal: React.FC<ModalProps> = ({ open, onClose, title, children }) => {
return (
<Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
<Dialog open={open} onClose={onClose} fullWidth maxWidth="lg">
<DialogTitle>{title}</DialogTitle>
<DialogContent>
{children}

View File

@ -7,25 +7,43 @@ import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import MenuIcon from '@mui/icons-material/Menu';
import Container from '@mui/material/Container';
import Avatar from '@mui/material/Avatar';
import Tooltip from '@mui/material/Tooltip';
import { Button } from '@mui/material';
import { Button, Select, FormControl, InputLabel, } from '@mui/material';
import { Link } from 'react-router-dom';
import logo from '../assets/icons/psi_01_sn.svg';
import '../App.css';
import { clearToken } from '../utils/auth';
interface ResponsiveAppBarProps {
activePgroup: string;
onOpenAddressManager: () => void;
onOpenContactsManager: () => void;
pgroups: string[]; // Pass the pgroups from the server
currentPgroup: string; // Currently selected pgroup
onPgroupChange: (pgroup: string) => void; // Callback when selected pgroup changes
}
const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({ onOpenAddressManager, onOpenContactsManager }) => {
const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({
activePgroup,
onOpenAddressManager,
onOpenContactsManager,
pgroups,
currentPgroup,
onPgroupChange }) => {
const navigate = useNavigate();
const location = useLocation();
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null);
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null);
const [selectedPgroup, setSelectedPgroup] = useState(currentPgroup);
console.log('Active Pgroup:', activePgroup);
const handlePgroupChange = (event: React.ChangeEvent<{ value: unknown }>) => {
const newPgroup = event.target.value as string;
setSelectedPgroup(newPgroup);
onPgroupChange(newPgroup); // Inform parent about the change
};
const pages = [
{ name: 'Home', path: '/' },
@ -59,8 +77,8 @@ const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({ onOpenAddressManage
const handleLogout = () => {
console.log("Performing logout...");
localStorage.removeItem('token');
navigate('/login');
clearToken(); // Clear the token from localStorage and OpenAPI
navigate('/login'); // Redirect to login page
};
const handleMenuItemClick = (action: string) => {
@ -123,7 +141,31 @@ const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({ onOpenAddressManage
</Button>
))}
</Box>
<Box>
<FormControl variant="outlined" size="small">
<InputLabel sx={{ color: 'white' }}>pgroup</InputLabel>
<Select
value={selectedPgroup}
onChange={handlePgroupChange}
sx={{
color: 'white',
borderColor: 'white',
minWidth: 150,
'&:focus': { borderColor: '#f0db4f' },
}}
>
{pgroups && pgroups.length > 0 ? (
pgroups.map((pgroup) => (
<MenuItem key={pgroup} value={pgroup}>
{pgroup}
</MenuItem>
))
) : (
<MenuItem disabled>No pgroups available</MenuItem>
)}
</Select>
</FormControl>
</Box>
<Box sx={{ flexGrow: 0 }}>
<Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>

View File

@ -12,6 +12,7 @@ import DewarDetails from './DewarDetails';
const MAX_COMMENTS_LENGTH = 200;
interface ShipmentDetailsProps {
activePgroup: string;
isCreatingShipment: boolean;
sx?: SxProps;
selectedShipment: Shipment | null;
@ -23,6 +24,7 @@ interface ShipmentDetailsProps {
}
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
activePgroup,
sx,
selectedShipment,
setSelectedDewar,
@ -34,6 +36,8 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
const [comments, setComments] = useState<string>(selectedShipment?.comments || '');
const [initialComments, setInitialComments] = useState<string>(selectedShipment?.comments || '');
console.log('Active Pgroup:', activePgroup); // Debugging or use it where required
const initialNewDewarState: Partial<Dewar> = {
dewar_name: '',
tracking_number: '',

View File

@ -6,15 +6,18 @@ import {
import { SelectChangeEvent } from '@mui/material';
import { SxProps } from '@mui/system';
import {
ContactPersonCreate, ContactPerson, Address, Proposal, ContactsService, AddressesService, AddressCreate, ProposalsService,
ContactPersonCreate, ContactPerson, Address, AddressCreate, Proposal, ContactsService, AddressesService, ProposalsService,
OpenAPI, ShipmentCreate, ShipmentsService
} from '../../openapi';
import { useEffect } from 'react';
import { CountryList } from './CountryList'; // Import the list of countries
import { jwtDecode } from 'jwt-decode';
const MAX_COMMENTS_LENGTH = 200;
interface ShipmentFormProps {
activePgroup: string;
sx?: SxProps;
onCancel: () => void;
refreshShipments: () => void;
@ -26,7 +29,17 @@ const fuse = new Fuse(CountryList, {
includeScore: true,
});
const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshShipments }) => {
const token = localStorage.getItem('token');
if (token) {
OpenAPI.TOKEN = token; // Ensure OpenAPI client uses this token
}
const ShipmentForm: React.FC<ShipmentFormProps> = ({
activePgroup,
sx = {},
onCancel,
refreshShipments }) => {
const [countrySuggestions, setCountrySuggestions] = React.useState<string[]>([]);
const [contactPersons, setContactPersons] = React.useState<ContactPerson[]>([]);
const [returnAddresses, setReturnAddresses] = React.useState<Address[]>([]);
@ -37,7 +50,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
firstname: '', lastname: '', phone_number: '', email: ''
});
const [newReturnAddress, setNewReturnAddress] = React.useState<Omit<Address, 'id'>>({
street: '', city: '', zipcode: '', country: ''
pgroup:'', house_number: '', street: '', city: '', state: '', zipcode: '', country: ''
});
const [newShipment, setNewShipment] = React.useState<Partial<ShipmentCreate>>({
shipment_name: '', shipment_status: 'In preparation', comments: ''
@ -80,12 +93,23 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
};
const getAddresses = async () => {
if (!activePgroup) {
console.error("Active pgroup is missing.");
setErrorMessage("Active pgroup is missing. Unable to load addresses.");
return;
}
try {
// Pass activePgroup directly as a string (not as an object)
const fetchedAddresses: Address[] =
await AddressesService.getReturnAddressesAddressesGet();
await AddressesService.getReturnAddressesAddressesGet(activePgroup);
setReturnAddresses(fetchedAddresses);
} catch {
setErrorMessage('Failed to load return addresses.');
} catch (error) {
console.error("Error fetching addresses:", error);
// Extract and log meaningful information from OpenAPI errors (if available)
setErrorMessage("Failed to load return addresses due to API error.");
}
};
@ -102,7 +126,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
getContacts();
getAddresses();
getProposals();
}, []);
}, [activePgroup]);
const handleCountryInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
@ -170,13 +194,14 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
const payload: ShipmentCreate = {
shipment_name: newShipment.shipment_name || '',
shipment_date: new Date().toISOString().split('T')[0], // Remove if date is not required at all
shipment_date: new Date().toISOString().split('T')[0], // Remove if date is not required
shipment_status: newShipment.shipment_status || 'In preparation',
comments: newShipment.comments || '',
contact_person_id: selectedContactPersonId!,
return_address_id: selectedReturnAddressId!,
proposal_id: selectedProposalId!,
dewars: newShipment.dewars || []
dewars: newShipment.dewars || [],
//pgroup: activePgroup,
};
console.log('Shipment Payload being sent:', payload);
@ -249,32 +274,44 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
};
const handleSaveNewReturnAddress = async () => {
if (!validateZipCode(newReturnAddress.zipcode) || !newReturnAddress.street || !newReturnAddress.city ||
!newReturnAddress.country) {
// Validate address form data
if (!validateZipCode(newReturnAddress.zipcode) || !newReturnAddress.street || !newReturnAddress.city || !newReturnAddress.country) {
setErrorMessage('Please fill in all new return address fields correctly.');
return;
}
// Ensure activePgroup is available
if (!activePgroup) {
setErrorMessage('Active pgroup is missing. Please try again.');
return;
}
// Construct the payload
const payload: AddressCreate = {
pgroups: activePgroup, // Use the activePgroup prop directly
house_number: newReturnAddress.house_number,
street: newReturnAddress.street,
city: newReturnAddress.city,
state: newReturnAddress.state,
zipcode: newReturnAddress.zipcode,
country: newReturnAddress.country,
};
console.log('Return Address Payload being sent:', payload);
// Call the API with the completed payload
try {
const response: Address = await AddressesService.createReturnAddressAddressesPost(payload);
setReturnAddresses([...returnAddresses, response]);
setReturnAddresses([...returnAddresses, response]); // Update the address state
setErrorMessage(null);
setSelectedReturnAddressId(response.id);
setSelectedReturnAddressId(response.id); // Set the newly created address ID to the form
} catch (error) {
console.error('Failed to create a new return address:', error);
setErrorMessage('Failed to create a new return address. Please try again later.');
}
setNewReturnAddress({ street: '', city: '', zipcode: '', country: '' });
// Reset form inputs and close the "Create New Address" form
setNewReturnAddress({ pgroup: '', house_number: '', street: '', city: '', state: '', zipcode: '', country: '' });
setIsCreatingReturnAddress(false);
};
@ -390,11 +427,22 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
onChange={handleReturnAddressChange}
displayEmpty
>
{returnAddresses.map((address) => (
<MenuItem key={address.id} value={address.id.toString()}>
{`${address.street}, ${address.city}, ${address.zipcode}, ${address.country}`}
</MenuItem>
))}
{returnAddresses.map((address) => {
const addressParts = [
address.house_number,
address.street,
address.city,
address.zipcode,
address.state,
address.country
].filter(part => part); // Remove falsy (null/undefined/empty) values
return (
<MenuItem key={address.id} value={address.id.toString()}>
{addressParts.join(', ')} {/* Join the valid address parts with a comma */}
</MenuItem>
);
})}
<MenuItem value="new">
<em>Create New Return Address</em>
</MenuItem>
@ -402,6 +450,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
</FormControl>
{isCreatingReturnAddress && (
<>
<TextField
label="Street"
name="street"
@ -410,6 +459,13 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
fullWidth
required
/>
<TextField
label="Number"
name="number"
value={newReturnAddress.house_number}
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, house_number: e.target.value })}
fullWidth
/>
<TextField
label="City"
name="city"
@ -418,6 +474,13 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
fullWidth
required
/>
<TextField
label="State"
name="state"
value={newReturnAddress.state}
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, state: e.target.value })}
fullWidth
/>
<TextField
label="Zip Code"
name="zipcode"

View File

@ -11,6 +11,7 @@ import bottleRed from '/src/assets/icons/bottle-svgrepo-com-red.svg';
interface ShipmentPanelProps {
setCreatingShipment: (value: boolean) => void;
activePgroup: string;
selectShipment: (shipment: Shipment | null) => void;
selectedShipment: Shipment | null;
sx?: SxProps;
@ -27,6 +28,7 @@ const statusIconMap: Record<string, string> = {
};
const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
activePgroup,
setCreatingShipment,
selectShipment,
selectedShipment,
@ -37,6 +39,8 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
}) => {
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
console.log('Active Pgroup:', activePgroup);
const handleDeleteShipment = async () => {
if (selectedShipment) {
const confirmDelete = window.confirm(

View File

@ -2,26 +2,57 @@ import React from 'react';
import Fuse from 'fuse.js';
import { CountryList } from '../components/CountryList';
import {
Container, Typography, List, ListItem, IconButton, TextField, Box, ListItemText, ListItemSecondaryAction, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button
Container,
Typography,
List,
ListItem,
IconButton,
TextField,
Box,
ListItemText,
ListItemSecondaryAction,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Button,
Chip
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import SaveIcon from '@mui/icons-material/Save';
import AddIcon from '@mui/icons-material/Add';
import { AddressesService } from '../../openapi';
import type { Address, AddressCreate, AddressUpdate } from '../models/Address';
import {Address, AddressCreate, AddressesService, AddressUpdate} from '../../openapi';
interface AddressManagerProps {
pgroups: string[];
activePgroup: string;
}
// Extend the generated Address type
interface AddressWithPgroups extends Address {
associatedPgroups: string[]; // Dynamically added pgroups
}
const fuse = new Fuse(CountryList, {
threshold: 0.3, // Lower threshold for stricter matches
includeScore: true,
});
const AddressManager: React.FC = () => {
const AddressManager: React.FC<AddressManagerProps> = ({ pgroups, activePgroup }) => {
// Use pgroups and activePgroup directly
console.log('User pgroups:', pgroups);
console.log('Active pgroup:', activePgroup);
const [countrySuggestions, setCountrySuggestions] = React.useState<string[]>([]);
const [addresses, setAddresses] = React.useState<Address[]>([]);
const [newAddress, setNewAddress] = React.useState<Partial<Address>>({
house_number: '',
street: '',
city: '',
state: '',
zipcode: '',
country: '',
});
@ -45,18 +76,28 @@ const AddressManager: React.FC = () => {
};
React.useEffect(() => {
const fetchAddresses = async () => {
const fetchAllData = async () => {
try {
const response = await AddressesService.getReturnAddressesAddressesGet();
setAddresses(response);
const response = await AddressesService.getAllAddressesAddressesAllGet();
// Preprocess: Add associated and unassociated pgroups
const transformedAddresses = response.map((address) => {
const addressPgroups = address.pgroups?.split(',').map((p) => p.trim()) || [];
const associatedPgroups = pgroups.filter((pgroup) => addressPgroups.includes(pgroup));
return {
...address,
associatedPgroups, // pgroups linked to the address
};
});
setAddresses(transformedAddresses);
} catch (error) {
console.error('Failed to fetch addresses', error);
setErrorMessage('Failed to load addresses. Please try again later.');
setErrorMessage('Failed to load addresses. Please try again.');
}
};
fetchAddresses();
}, []);
fetchAllData();
}, [pgroups]);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
@ -66,30 +107,40 @@ const AddressManager: React.FC = () => {
const handleAddOrUpdateAddress = async () => {
try {
if (editAddressId !== null) {
// Update address
await AddressesService.updateReturnAddressAddressesAddressIdPut(editAddressId, newAddress as AddressUpdate);
setAddresses(addresses.map(address => address.id === editAddressId ? { ...address, ...newAddress } : address));
// Update address (mark old one obsolete, create a new one)
const updatedAddress = await AddressesService.updateReturnAddressAddressesAddressIdPut(
editAddressId,
newAddress as AddressUpdate
);
// Replace old address with the new one in the list
setAddresses(addresses.map(address =>
address.id === editAddressId ? updatedAddress : address
).filter(address => address.status === "active")); // Keep only active addresses
setEditAddressId(null);
} else {
// Add new address
const response = await AddressesService.createReturnAddressAddressesPost(newAddress as AddressCreate);
setAddresses([...addresses, response]);
}
setNewAddress({ street: '', city: '', zipcode: '', country: '' });
setNewAddress({ house_number:'', street: '', city: '', state: '', zipcode: '', country: '' });
setErrorMessage(null);
} catch (error) {
console.error('Failed to add/update address', error);
setErrorMessage('Failed to add/update address. Please try again later.');
setErrorMessage('Failed to add/update address. Please try again.');
}
};
const handleDeleteAddress = async (id: number) => {
try {
// Delete (inactivate) the address
await AddressesService.deleteReturnAddressAddressesAddressIdDelete(id);
setAddresses(addresses.filter(address => address.id !== id));
// Remove the obsolete address from the active list in the UI
setAddresses(addresses.filter(address => address.id !== id && address.status === "active"));
} catch (error) {
console.error('Failed to delete address', error);
setErrorMessage('Failed to delete address. Please try again later.');
setErrorMessage('Failed to delete address. Please try again.');
}
};
@ -115,52 +166,149 @@ const AddressManager: React.FC = () => {
}
};
const togglePgroupAssociation = async (addressId: number, pgroup: string) => {
try {
const address = addresses.find((addr) => addr.id === addressId);
if (!address) return;
const isAssociated = address.associatedPgroups.includes(pgroup);
// Only allow adding a pgroup
if (isAssociated) {
console.warn('Removing a pgroup is not allowed.');
return;
}
const updatedPgroups = [...address.associatedPgroups, pgroup]; // Add the pgroup
// Update the backend
await AddressesService.updateReturnAddressAddressesAddressIdPut(addressId, {
...address,
pgroups: updatedPgroups.join(','), // Sync updated pgroups
});
// Update address in local state
setAddresses((prevAddresses) =>
prevAddresses.map((addr) =>
addr.id === addressId
? { ...addr, associatedPgroups: updatedPgroups }
: addr
)
);
} catch (error) {
console.error('Failed to add pgroup association', error);
setErrorMessage('Failed to add pgroup association. Please try again.');
}
};
const renderPgroupChips = (address: AddressWithPgroups) => {
return pgroups.map((pgroup) => {
const isAssociated = address.associatedPgroups.includes(pgroup);
return (
<Chip
key={pgroup}
label={pgroup}
onClick={
!isAssociated // Only allow adding a new pgroup, no removal
? () => togglePgroupAssociation(address.id, pgroup)
: undefined
}
sx={{
backgroundColor: isAssociated ? '#19d238' : '#b0b0b0',
color: 'white',
borderRadius: '8px',
fontWeight: 'bold',
height: '20px',
fontSize: '12px',
boxShadow: '0px 1px 3px rgba(0, 0, 0, 0.2)',
cursor: isAssociated ? 'default' : 'pointer', // Disable pointer for associated chips
'&:hover': { opacity: isAssociated ? 1 : 0.8 }, // Disable hover effect for associated chips
mr: 1,
mb: 1,
}}
/>
);
});
};
return (
<Container>
<Typography variant="h4" gutterBottom>
Addresses Management
</Typography>
<Box display="flex" justifyContent="center" alignItems="center" mb={3}>
<TextField label="Street" name="street" value={newAddress.street || ''} onChange={handleInputChange} />
<TextField label="City" name="city" value={newAddress.city || ''} onChange={handleInputChange} />
<TextField label="Zipcode" name="zipcode" value={newAddress.zipcode || ''} onChange={handleInputChange} />
<Box display="flex" justifyContent="center" alignItems="center" mb={3} gap={2}>
<TextField
label="Country"
name="country"
value={newAddress.country || ''}
onChange={handleCountryInputChange}
fullWidth
required
error={
!!((editAddressId !== null || newAddress.street || newAddress.city || newAddress.zipcode) && !newAddress.country)
} // Show an error only if in add/edit mode and country is empty
helperText={
!!((editAddressId !== null || newAddress.street || newAddress.city || newAddress.zipcode) && !newAddress.country)
? 'Country is required'
: ''
}
label="pgroup"
name="pgroup"
value={newAddress.activePgroup || ''}
disabled
sx={{ width: '120px' }} // Small fixed-size for non-editable field
/>
{/* Render suggestions dynamically */}
<Box sx={{ position: 'relative' }}>
<TextField
label="Number"
name="house_number"
value={newAddress.house_number || ''}
onChange={handleInputChange}
sx={{ width: '100px' }} // Small size for Number field
/>
<TextField
label="Street"
name="street"
value={newAddress.street || ''}
onChange={handleInputChange}
sx={{ flex: 1 }} // Street field takes the most space
/>
<TextField
label="City"
name="city"
value={newAddress.city || ''}
onChange={handleInputChange}
sx={{ width: '150px' }} // Medium size for City
/>
<TextField
label="State"
name="state"
value={newAddress.state || ''}
onChange={handleInputChange}
sx={{ width: '100px' }} // Small size
/>
<TextField
label="Zipcode"
name="zipcode"
value={newAddress.zipcode || ''}
onChange={handleInputChange}
sx={{ width: '120px' }} // Medium size for Zipcode
/>
<Box sx={{ position: 'relative', flex: 1 }}> {/* Country field dynamically takes available space */}
<TextField
label="Country"
name="country"
value={newAddress.country || ''}
onChange={handleCountryInputChange}
fullWidth
required
error={
!!((editAddressId !== null || newAddress.street || newAddress.city || newAddress.zipcode) && !newAddress.country)
}
helperText={
!!((editAddressId !== null || newAddress.street || newAddress.city || newAddress.zipcode) && !newAddress.country)
? 'Country is required'
: ''
}
/>
{/* Render suggestions dynamically */}
{countrySuggestions.length > 0 && (
<Box
sx={{
border: '1px solid #ccc',
borderRadius: '4px',
background: 'white',
marginTop: '4px', /* Add space below the input */
position: 'absolute',
width: '100%', /* Match the TextField width */
zIndex: 10, /* Ensure it is above other UI elements */
top: '100%', // Place below the TextField
left: 0,
zIndex: 10,
width: '100%', // Match the width of the TextField
marginTop: '4px', // Small spacing below the TextField
}}
>
{countrySuggestions.map((suggestion, index) => (
@ -172,7 +320,6 @@ const AddressManager: React.FC = () => {
'&:hover': { background: '#f5f5f5' },
}}
onClick={() => {
// Update country field with the clicked suggestion
setNewAddress({ ...newAddress, country: suggestion });
setCountrySuggestions([]); // Clear suggestions
}}
@ -193,14 +340,18 @@ const AddressManager: React.FC = () => {
addresses.map((address) => (
<ListItem key={address.id} button>
<ListItemText
primary={`${address.street}, ${address.city}`}
secondary={`${address.zipcode} - ${address.country}`}
primary={`${address.house_number}, ${address.street}, ${address.city}`}
secondary={
<Box display="flex" flexWrap="wrap">
{renderPgroupChips(address)}
</Box>
}
/>
<ListItemSecondaryAction>
<IconButton edge="end" color="primary" onClick={() => handleEditAddress(address)}>
<EditIcon />
</IconButton>
<IconButton edge="end" color="secondary" onClick={() => openDialog(address)}>
<IconButton edge="end" color="secondary" onClick={() => handleDeleteAddress(address.id)}>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>

View File

@ -89,10 +89,25 @@ const LoginView: React.FC = () => {
password: password,
});
// Save the token
localStorage.setItem('token', response.access_token);
navigate('/'); // Redirect post-login
OpenAPI.TOKEN = response.access_token;
// Decode token to extract user data (e.g., pgroups)
const decodedToken = JSON.parse(atob(response.access_token.split('.')[1])); // Decode JWT payload
const userData = {
username: decodedToken.sub,
pgroups: decodedToken.pgroups || [], // Ensure pgroups is an array
};
localStorage.setItem('user', JSON.stringify(userData)); // Save user data in localStorage
console.log("Token updated successfully:", response.access_token);
console.log("User data saved:", userData);
navigate('/'); // Redirect after successful login
} catch (err) {
setError('Login failed. Please check your credentials.');
console.error("Error during login:", err);
}
};

View File

@ -6,14 +6,20 @@ import { Dewar, OpenAPI, Shipment } from '../../openapi';
import useShipments from '../hooks/useShipments';
import { Grid, Container } from '@mui/material';
type ShipmentViewProps = React.PropsWithChildren<Record<string, never>>;
type ShipmentViewProps = {
activePgroup: string;
};
const ShipmentView: React.FC<ShipmentViewProps> = () => {
const ShipmentView: React.FC<ShipmentViewProps> = ( { activePgroup }) => {
const { shipments, error, defaultContactPerson, fetchAndSetShipments } = useShipments();
const [selectedShipment, setSelectedShipment] = useState<Shipment | null>(null);
const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null);
const [isCreatingShipment, setIsCreatingShipment] = useState(false);
useEffect(() => {
fetchAndSetShipments();
}, [activePgroup]);
useEffect(() => {
// Detect the current environment
const mode = import.meta.env.MODE;
@ -52,6 +58,7 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
if (isCreatingShipment) {
return (
<ShipmentForm
activePgroup={activePgroup}
sx={{ flexGrow: 1 }}
onCancel={handleCancelShipmentForm}
refreshShipments={fetchAndSetShipments}
@ -61,6 +68,7 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
if (selectedShipment) {
return (
<ShipmentDetails
activePgroup={activePgroup}
isCreatingShipment={isCreatingShipment}
sx={{ flexGrow: 1 }}
selectedShipment={selectedShipment}
@ -89,6 +97,7 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
}}
>
<ShipmentPanel
activePgroup={activePgroup}
setCreatingShipment={setIsCreatingShipment}
selectShipment={handleSelectShipment}
shipments={shipments}

View File

@ -0,0 +1,16 @@
import {OpenAPI} from "../../openapi";
export const setUpToken = (): void => {
const token = localStorage.getItem('token');
if (token) {
OpenAPI.TOKEN = token; // Assign the token to OpenAPI client
} else {
console.warn("No token found in localStorage.");
}
};
export const clearToken = (): void => {
localStorage.removeItem('token');
OpenAPI.TOKEN = ''; // Clear the token from the OpenAPI client
console.log("Token cleared from OpenAPI and localStorage.");
};