**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:
@ -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 />
|
||||
|
@ -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}
|
||||
|
@ -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 }}>
|
||||
|
@ -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: '',
|
||||
|
@ -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"
|
||||
|
@ -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(
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
|
16
frontend/src/utils/auth.ts
Normal file
16
frontend/src/utils/auth.ts
Normal 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.");
|
||||
};
|
Reference in New Issue
Block a user