initial-commit

This commit is contained in:
GotthardG
2024-10-24 10:31:09 +02:00
parent fbc9eb1873
commit b6611fdac0
55 changed files with 12587 additions and 0 deletions

View File

@ -0,0 +1,321 @@
import React, { useEffect, useState } from 'react';
import FullCalendar, { EventInput } from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction';
import '../styles/Calendar.css';
// Define colors for each beamline
const beamlineColors: { [key: string]: string } = {
PXI: '#FF5733',
PXII: '#33FF57',
PXIII: '#3357FF',
Unknown: '#CCCCCC', // Gray color for unknown beamlines
};
// Custom event interface
interface CustomEvent extends EventInput {
beamline: string;
beamtime_shift: string;
isSubmitted?: boolean; // Track if information is submitted
}
// Define experiment modes
const experimentModes = ['SDU-Scheduled', 'SDU-queued', 'Remote', 'In-person'];
// Utility function to darken a hex color
const darkenColor = (color: string, percent: number): string => {
const num = parseInt(color.slice(1), 16); // Convert hex to number
const amt = Math.round(2.55 * percent); // Calculate amount to darken
const r = (num >> 16) + amt; // Red
const g = (num >> 8 & 0x00FF) + amt; // Green
const b = (num & 0x0000FF) + amt; // Blue
// Ensure values stay within 0-255 range
const newColor = (0x1000000 + (r < 255 ? (r < 0 ? 0 : r) : 255) * 0x10000 + (g < 255 ? (g < 0 ? 0 : g) : 255) * 0x100 + (b < 255 ? (b < 0 ? 0 : b) : 255)).toString(16).slice(1);
return `#${newColor}`;
};
const Calendar: React.FC = () => {
const [events, setEvents] = useState<CustomEvent[]>([]);
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
const [eventDetails, setEventDetails] = useState<CustomEvent | null>(null);
const [userDetails, setUserDetails] = useState({
name: '',
firstName: '',
phone: '',
email: '',
extAccount: '',
experimentMode: experimentModes[0],
});
const [shipments, setShipments] = useState<any[]>([]); // State for shipments
const [selectedDewars, setSelectedDewars] = useState<string[]>([]); // Track selected dewars for the experiment
useEffect(() => {
const fetchEvents = async () => {
try {
const response = await fetch('/beamtimedb.json');
const data = await response.json();
const events: CustomEvent[] = [];
data.beamtimes.forEach((beamtime: any) => {
const date = new Date(beamtime.date);
beamtime.shifts.forEach((shift: any) => {
const beamline = shift.beamline || 'Unknown';
const beamtime_shift = shift.beamtime_shift || 'morning';
const event: CustomEvent = {
id: `${beamline}-${date.toISOString()}-${beamtime_shift}`,
start: new Date(date.setHours(0, 0, 0)),
end: new Date(date.setHours(23, 59, 59)),
title: `${beamline}: ${beamtime_shift}`,
beamline,
beamtime_shift,
isSubmitted: false,
};
events.push(event);
});
});
console.log('Fetched events array:', events);
setEvents(events);
} catch (error) {
console.error('Error fetching events:', error);
}
};
const fetchShipments = async () => {
try {
const response = await fetch('/shipmentdb.json');
// Check for HTTP errors
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Parse the JSON response
const data = await response.json();
const availableDewars: any[] = [];
data.shipments.forEach(shipment => {
if (shipment.shipment_status === "In Transit") {
shipment.dewars.forEach(dewar => {
if (dewar.shippingStatus === "shipped" && dewar.returned === "") {
availableDewars.push(dewar);
}
});
}
});
console.log('Available Dewars:', availableDewars);
setShipments(availableDewars);
} catch (error) {
console.error('Error fetching shipments:', error);
// Optionally display the error to the user in the UI
}
};
fetchEvents();
fetchShipments();
}, []);
const handleEventClick = (eventInfo: any) => {
const clickedEventId = eventInfo.event.id;
setSelectedEventId(clickedEventId);
const selectedEvent = events.find(event => event.id === clickedEventId) || null;
setEventDetails(selectedEvent);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setUserDetails(prevDetails => ({
...prevDetails,
[name]: value,
}));
};
const handleDewarSelection = (dewarId: string) => {
setSelectedDewars(prevSelectedDewars => {
if (prevSelectedDewars.includes(dewarId)) {
return prevSelectedDewars.filter(id => id !== dewarId); // Remove if already selected
} else {
return [...prevSelectedDewars, dewarId]; // Add if not selected
}
});
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (eventDetails) {
const updatedEvents = events.map(event =>
event.id === eventDetails.id
? { ...event, isSubmitted: true, selectedDewars } // Associate selected dewars
: event
);
setEvents(updatedEvents);
}
console.log('User Details:', userDetails);
console.log('Selected Dewars:', selectedDewars);
// Reset user details and selected dewars after submission
setUserDetails({
name: '',
firstName: '',
phone: '',
email: '',
extAccount: '',
experimentMode: experimentModes[0],
});
setSelectedDewars([]); // Reset selected dewars
};
const eventContent = (eventInfo: any) => {
const beamline = eventInfo.event.extendedProps.beamline || 'Unknown';
const isSelected = selectedEventId === eventInfo.event.id;
const isSubmitted = eventInfo.event.extendedProps.isSubmitted;
const backgroundColor = isSubmitted
? darkenColor(beamlineColors[beamline] || beamlineColors.Unknown, -20)
: isSelected
? '#FFD700'
: (beamlineColors[beamline] || beamlineColors.Unknown);
return (
<div
style={{
backgroundColor: backgroundColor,
color: 'white',
border: isSelected ? '2px solid black' : 'none',
borderRadius: '3px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%',
cursor: 'pointer',
overflow: 'hidden',
boxSizing: 'border-box',
}}
>
{eventInfo.event.title}
</div>
);
};
return (
<div className="calendar-container">
<h2>Beamline Calendar</h2>
<FullCalendar
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
events={events}
eventContent={eventContent}
height={700}
headerToolbar={{
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth',
}}
eventClick={handleEventClick}
/>
{eventDetails && (
<div className="event-details">
<h3>Event Details</h3>
<p><strong>Beamline:</strong> {eventDetails.beamline}</p>
<p><strong>Shift:</strong> {eventDetails.beamtime_shift}</p>
<h4>Select Dewars</h4>
<ul>
{shipments.map(dewar => (
<li key={dewar.id}>
<input
type="checkbox"
id={dewar.id}
checked={selectedDewars.includes(dewar.id)}
onChange={() => handleDewarSelection(dewar.id)}
/>
<label htmlFor={dewar.id}>{dewar.dewar_name} (Pucks: {dewar.number_of_pucks})</label>
</li>
))}
</ul>
<h4>User Information</h4>
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
name="name"
value={userDetails.name}
onChange={handleInputChange}
/>
</label>
<br />
<label>
First Name:
<input
type="text"
name="firstName"
value={userDetails.firstName}
onChange={handleInputChange}
/>
</label>
<br />
<label>
Phone:
<input
type="text"
name="phone"
value={userDetails.phone}
onChange={handleInputChange}
/>
</label>
<br />
<label>
Email:
<input
type="email"
name="email"
value={userDetails.email}
onChange={handleInputChange}
/>
</label>
<br />
<label>
External Account:
<input
type="text"
name="extAccount"
value={userDetails.extAccount}
onChange={handleInputChange}
/>
</label>
<br />
<label>
Experiment Mode:
<select
name="experimentMode"
value={userDetails.experimentMode}
onChange={handleInputChange}
>
{experimentModes.map(mode => (
<option key={mode} value={mode}>{mode}</option>
))}
</select>
</label>
<br />
<button type="submit">Submit</button>
</form>
</div>
)}
</div>
);
};
export default Calendar;

View File

@ -0,0 +1,172 @@
import * as React from 'react';
import { Box, Typography, TextField, Button, Select, MenuItem, Snackbar } from '@mui/material';
import QRCode from 'react-qr-code';
import { Dewar, ContactPerson, Address } from '../types.ts';
interface DewarDetailsProps {
dewar: Dewar | null;
trackingNumber: string;
setTrackingNumber: React.Dispatch<React.SetStateAction<string>>;
onGenerateQRCode: () => void;
contactPersons: ContactPerson[];
returnAddresses: Address[];
addNewContactPerson: (name: string) => void;
addNewReturnAddress: (address: string) => void;
ready_date?: string;
shipping_date?: string; // Make this optional
arrival_date?: string; // Make this optional
}
const DewarDetails: React.FC<DewarDetailsProps> = ({
dewar,
trackingNumber,
setTrackingNumber,
onGenerateQRCode,
contactPersons,
returnAddresses,
addNewContactPerson,
addNewReturnAddress,
}) => {
const [selectedContactPerson, setSelectedContactPerson] = React.useState<string>('');
const [selectedReturnAddress, setSelectedReturnAddress] = React.useState<string>('');
const [newContactPerson, setNewContactPerson] = React.useState<string>('');
const [newReturnAddress, setNewReturnAddress] = React.useState<string>('');
const [feedbackMessage, setFeedbackMessage] = React.useState<string>('');
const [openSnackbar, setOpenSnackbar] = React.useState<boolean>(false);
React.useEffect(() => {
if (contactPersons.length > 0) {
setSelectedContactPerson(contactPersons[0].name); // Default to the first contact person
}
if (returnAddresses.length > 0) {
setSelectedReturnAddress(returnAddresses[0].address); // Default to the first return address
}
}, [contactPersons, returnAddresses]);
if (!dewar) {
return <Typography>No dewar selected.</Typography>;
}
const handleAddContact = () => {
if (newContactPerson.trim() === '') {
setFeedbackMessage('Please enter a valid contact person name.');
} else {
addNewContactPerson(newContactPerson);
setNewContactPerson('');
setFeedbackMessage('Contact person added successfully.');
}
setOpenSnackbar(true);
};
const handleAddAddress = () => {
if (newReturnAddress.trim() === '') {
setFeedbackMessage('Please enter a valid return address.');
} else {
addNewReturnAddress(newReturnAddress);
setNewReturnAddress('');
setFeedbackMessage('Return address added successfully.');
}
setOpenSnackbar(true);
};
return (
<Box sx={{ marginTop: 2 }}>
<Typography variant="h6">Selected Dewar: {dewar.dewar_name}</Typography>
<TextField
label="Tracking Number"
value={trackingNumber}
onChange={(e) => setTrackingNumber(e.target.value)}
variant="outlined"
sx={{ width: '300px', marginBottom: 2 }}
/>
{/* QR Code display */}
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
<Box sx={{ width: 80, height: 80, backgroundColor: '#e0e0e0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{dewar.qrcode ? (
<QRCode value={dewar.qrcode} size={70} />
) : (
<Typography>No QR code available</Typography>
)}
</Box>
<Button variant="contained" onClick={onGenerateQRCode}>
Generate QR Code
</Button>
</Box>
<Typography variant="body1">Number of Pucks: {dewar.number_of_pucks}</Typography>
<Typography variant="body1">Number of Samples: {dewar.number_of_samples}</Typography>
{/* Dropdown for Contact Person */}
<Typography variant="body1">Current Contact Person:</Typography>
<Select
value={selectedContactPerson}
onChange={(e) => setSelectedContactPerson(e.target.value)}
displayEmpty
fullWidth
sx={{ marginBottom: 2 }}
>
<MenuItem value="" disabled>Select Contact Person</MenuItem>
{contactPersons.map((person) => (
<MenuItem key={person.id} value={person.name}>{person.name}</MenuItem>
))}
<MenuItem value="add">Add New Contact Person</MenuItem>
</Select>
{selectedContactPerson === "add" && (
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
<TextField
label="New Contact Person"
value={newContactPerson}
onChange={(e) => setNewContactPerson(e.target.value)}
variant="outlined"
sx={{ marginRight: 1, flexGrow: 1 }}
/>
<Button variant="contained" onClick={handleAddContact}>
Add
</Button>
</Box>
)}
{/* Dropdown for Return Address */}
<Typography variant="body1">Current Return Address:</Typography>
<Select
value={selectedReturnAddress}
onChange={(e) => setSelectedReturnAddress(e.target.value)}
displayEmpty
fullWidth
sx={{ marginBottom: 2 }}
>
<MenuItem value="" disabled>Select Return Address</MenuItem>
{returnAddresses.map((address) => (
<MenuItem key={address.id} value={address.address}>{address.address}</MenuItem>
))}
<MenuItem value="add">Add New Return Address</MenuItem>
</Select>
{selectedReturnAddress === "add" && (
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
<TextField
label="New Return Address"
value={newReturnAddress}
onChange={(e) => setNewReturnAddress(e.target.value)}
variant="outlined"
sx={{ marginRight: 1, flexGrow: 1 }}
/>
<Button variant="contained" onClick={handleAddAddress}>
Add
</Button>
</Box>
)}
{/* Snackbar for feedback messages */}
<Snackbar
open={openSnackbar}
autoHideDuration={6000}
onClose={() => setOpenSnackbar(false)}
message={feedbackMessage}
/>
</Box>
);
};
export default DewarDetails;

View File

@ -0,0 +1,54 @@
import React, { useEffect, useState } from 'react';
import DewarDetails from './DewarDetails.tsx';
import { Shipment, ContactPerson, Address, Dewar } from '../types.ts';
import shipmentData from '../../public/shipmentsdb.json'; // Adjust the path to where your JSON file is stored
const ParentComponent = () => {
const [dewars, setDewars] = useState<Dewar[]>([]);
const [contactPersons, setContactPersons] = useState<ContactPerson[]>([]);
const [returnAddresses, setReturnAddresses] = useState<Address[]>([]);
const [selectedShipment, setSelectedShipment] = useState<Shipment | null>(null);
useEffect(() => {
const firstShipment = shipmentData.shipments[0] as Shipment; // Ensure proper typing
setSelectedShipment(firstShipment);
setContactPersons(firstShipment.contact_person || []); // Set to array directly
if (firstShipment.return_address) {
setReturnAddresses(firstShipment.return_address); // Set to array directly
}
const dewarsWithId = firstShipment.dewars.map((dewar, index) => ({
...dewar,
id: `${firstShipment.shipment_id}-Dewar-${index + 1}`,
}));
setDewars(dewarsWithId);
}, []);
const addNewContactPerson = (name: string) => {
const newContact: ContactPerson = { id: `${contactPersons.length + 1}`, name };
setContactPersons((prev) => [...prev, newContact]);
};
const addNewReturnAddress = (address: string) => {
const newAddress: Address = { id: `${returnAddresses.length + 1}`, address };
setReturnAddresses((prev) => [...prev, newAddress]);
};
const selectedDewar = dewars[0]; // Just picking the first dewar for demonstration
return (
<DewarDetails
dewar={selectedDewar}
trackingNumber={''}
setTrackingNumber={() => {}}
onGenerateQRCode={() => {}}
contactPersons={contactPersons}
returnAddresses={returnAddresses}
addNewContactPerson={addNewContactPerson}
addNewReturnAddress={addNewReturnAddress}
/>
);
};
export default ParentComponent;

View File

@ -0,0 +1,9 @@
// Planning.tsx
import React from 'react';
import CustomCalendar from './Calendar.tsx';
const PlanningView: React.FC = () => {
return <CustomCalendar />;
};
export default PlanningView;

View File

@ -0,0 +1,206 @@
import React, { useState } from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
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 logo from '../assets/icons/psi_01_sn.svg';
import '../App.css';
import { Shipment, Dewar, ContactPerson, Address, Proposal } from '../types.ts'; // Import types from a single statement
import ShipmentView from './ShipmentView.tsx';
import PlanningView from './PlanningView.tsx';
const pages = ['Validator', 'Shipments', 'Samples', 'Planning', 'Experiments', 'Results', 'Docs'];
const ResponsiveAppBar: React.FC = () => {
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null);
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null);
const [selectedPage, setSelectedPage] = useState<string>('Shipments');
const [newShipment, setNewShipment] = useState<Shipment>({
shipment_id: '',
shipment_name: '',
shipment_status: '',
number_of_dewars: 0,
shipment_date: '',
return_address: [], // Correctly initialize return_address
contact_person: null, // Use null
dewars: [],
});
const [isCreatingShipment, setIsCreatingShipment] = useState(false);
const [selectedShipment, setSelectedShipment] = useState<Shipment | null>(null);
const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null);
// Define missing state variables for contacts, addresses, and proposals
const [contactPersons, setContactPersons] = useState<ContactPerson[]>([]);
const [returnAddresses, setReturnAddresses] = useState<Address[]>([]);
const [proposals, setProposals] = useState<Proposal[]>([]);
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElNav(event.currentTarget);
};
const handleCloseNavMenu = () => {
setAnchorElNav(null);
};
const handlePageClick = (page: string) => {
setSelectedPage(page);
handleCloseNavMenu();
};
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUser(event.currentTarget);
};
const handleCloseUserMenu = () => {
setAnchorElUser(null);
};
// Updated selectShipment to accept Shipment | null
const selectShipment = (shipment: Shipment | null) => {
setSelectedShipment(shipment);
setIsCreatingShipment(false);
setSelectedDewar(null);
};
const handleSaveShipment = () => {
console.log('Saving shipment:', newShipment);
setIsCreatingShipment(false);
setNewShipment({
shipment_id: '',
shipment_name: '',
shipment_status: '',
number_of_dewars: 0,
shipment_date: '',
return_address: [], // Add return_address to the reset state
contact_person: null, // Use null
dewars: [],
});
};
return (
<div>
<AppBar position="static" sx={{ backgroundColor: '#2F4858' }}>
<Container maxWidth="xl">
<Toolbar disableGutters>
<a className="nav" href="">
<img src={logo} height="50px" alt="PSI logo" />
</a>
<Typography
variant="h6"
noWrap
component="a"
href="#app-bar-with-responsive-menu"
sx={{
mr: 2,
display: { xs: 'none', md: 'flex' },
fontFamily: 'monospace',
fontWeight: 300,
color: 'inherit',
textDecoration: 'none',
}}
>
Heidi v2
</Typography>
<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none' } }}>
<IconButton
size="large"
aria-label="menu"
onClick={handleOpenNavMenu}
color="inherit"
>
<MenuIcon />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorElNav}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
open={Boolean(anchorElNav)}
onClose={handleCloseNavMenu}
>
{pages.map((page) => (
<MenuItem key={page} onClick={() => handlePageClick(page)}>
{page}
</MenuItem>
))}
</Menu>
</Box>
<Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
{pages.map((page) => (
<Button
key={page}
onClick={() => handlePageClick(page)}
sx={{ my: 2, color: 'white', display: 'block', fontSize: '1rem', padding: '12px 24px' }}
>
{page}
</Button>
))}
</Box>
<Box sx={{ flexGrow: 0 }}>
<Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar />
</IconButton>
</Tooltip>
<Menu
sx={{ mt: '45px' }}
id="menu-appbar"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
<MenuItem onClick={handleCloseUserMenu}>DUO</MenuItem>
<MenuItem onClick={handleCloseUserMenu}>Logout</MenuItem>
</Menu>
</Box>
</Toolbar>
</Container>
</AppBar>
<Box sx={{ width: '100%', borderRight: '1px solid #ccc', padding: 2 }}>
{selectedPage === 'Shipments' && (
<ShipmentView
newShipment={newShipment}
setNewShipment={setNewShipment}
isCreatingShipment={isCreatingShipment}
setIsCreatingShipment={setIsCreatingShipment}
selectedShipment={selectedShipment}
selectShipment={selectShipment} // Now accepts Shipment | null
selectedDewar={selectedDewar}
setSelectedDewar={setSelectedDewar}
handleSaveShipment={handleSaveShipment}
contactPersons={contactPersons}
returnAddresses={returnAddresses}
proposals={proposals}
/>
)}
{selectedPage === 'Planning' && <PlanningView />}
</Box>
</div>
);
};
export default ResponsiveAppBar;

View File

@ -0,0 +1,308 @@
import React from 'react';
import { Box, Typography, Button, Stack, TextField } from '@mui/material';
import ShipmentForm from './ShipmentForm.tsx';
import DewarDetails from './DewarDetails.tsx';
import { Shipment, Dewar, ContactPerson, Proposal, Address } from '../types.ts';
import { SxProps } from '@mui/system';
import QRCode from 'react-qr-code';
import bottleGrey from '../assets/icons/bottle-svgrepo-com-grey.svg';
import bottleYellow from '../assets/icons/bottle-svgrepo-com-yellow.svg';
import bottleGreen from '../assets/icons/bottle-svgrepo-com-green.svg';
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import AirplanemodeActiveIcon from "@mui/icons-material/AirplanemodeActive";
import StoreIcon from "@mui/icons-material/Store";
import DeleteIcon from "@mui/icons-material/Delete"; // Import delete icon
interface ShipmentDetailsProps {
selectedShipment: Shipment | null;
setSelectedDewar: React.Dispatch<React.SetStateAction<Dewar | null>>;
isCreatingShipment: boolean;
newShipment: Shipment;
setNewShipment: React.Dispatch<React.SetStateAction<Shipment>>;
handleSaveShipment: () => void;
contactPersons: ContactPerson[];
proposals: Proposal[];
returnAddresses: Address[];
sx?: SxProps;
}
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
selectedShipment,
setSelectedDewar,
isCreatingShipment,
newShipment,
setNewShipment,
handleSaveShipment,
contactPersons,
proposals,
returnAddresses,
sx = {},
}) => {
const [localSelectedDewar, setLocalSelectedDewar] = React.useState<Dewar | null>(null);
const [trackingNumber, setTrackingNumber] = React.useState<string>('');
const [isAddingDewar, setIsAddingDewar] = React.useState<boolean>(false);
const [newDewar, setNewDewar] = React.useState<Partial<Dewar>>({
dewar_name: '',
tracking_number: '',
});
const shippingStatusMap: { [key: string]: string } = {
"not shipped": "grey",
"shipped": "yellow",
"arrived": "green",
};
const arrivalStatusMap: { [key: string]: string } = {
"not arrived": "grey",
"arrived": "green",
};
React.useEffect(() => {
if (localSelectedDewar) {
setTrackingNumber(localSelectedDewar.tracking_number);
}
}, [localSelectedDewar]);
if (!selectedShipment) {
return isCreatingShipment ? (
<ShipmentForm
newShipment={newShipment}
setNewShipment={setNewShipment}
handleSaveShipment={handleSaveShipment}
contactPersons={contactPersons}
proposals={proposals}
returnAddresses={returnAddresses}
/>
) : (
<Typography>No shipment selected.</Typography>
);
}
// Calculate total pucks and samples
const totalPucks = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_pucks || 0), 0);
const totalSamples = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_samples || 0), 0);
// Handle dewar selection
const handleDewarSelection = (dewar: Dewar) => {
setLocalSelectedDewar(prevDewar => (prevDewar?.tracking_number === dewar.tracking_number ? null : dewar));
setSelectedDewar(prevDewar => (prevDewar?.tracking_number === dewar.tracking_number ? null : dewar));
};
// Handle dewar deletion
const handleDeleteDewar = () => {
if (localSelectedDewar) {
const confirmed = window.confirm('Are you sure you want to delete this dewar?');
if (confirmed) {
const updatedDewars = selectedShipment.dewars.filter(dewar => dewar.tracking_number !== localSelectedDewar.tracking_number);
console.log('Updated Dewars:', updatedDewars); // Log or update state as needed
setLocalSelectedDewar(null); // Reset selection after deletion
}
}
};
// Handle form input changes for the new dewar
const handleNewDewarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setNewDewar((prev) => ({
...prev,
[name]: value,
}));
};
// Handle adding a new dewar
const handleAddDewar = () => {
if (selectedShipment && newDewar.dewar_name) {
const updatedDewars = [
...selectedShipment.dewars,
{ ...newDewar, tracking_number: newDewar.tracking_number || `TN-${Date.now()}` } as Dewar,
];
setNewShipment({
...selectedShipment,
dewars: updatedDewars,
});
setIsAddingDewar(false);
setNewDewar({ dewar_name: '', number_of_pucks: 0, number_of_samples: 0, tracking_number: '' });
} else {
alert('Please fill in the Dewar Name');
}
};
// Function to generate QR Code (Placeholder)
const generateQRCode = () => {
console.log('Generate QR Code');
};
// Handle adding new contact person and return address
const addNewContactPerson = (name: string) => {
// Implementation to add a new contact person
console.log('Add new contact person:', name);
};
const addNewReturnAddress = (address: string) => {
// Implementation to add a new return address
console.log('Add new return address:', address);
};
return (
<Box sx={{ ...sx, padding: 2, textAlign: 'left' }}>
{/* Add Dewar Button - only visible if no dewar is selected */}
{!localSelectedDewar && !isAddingDewar && (
<Button
variant="contained"
onClick={() => setIsAddingDewar(true)}
sx={{ marginBottom: 2 }}
>
Add Dewar
</Button>
)}
{/* Add Dewar Form */}
{isAddingDewar && (
<Box sx={{ marginBottom: 2, width: '20%' }}>
<Typography variant="h6">Add New Dewar</Typography>
<TextField
label="Dewar Name"
name="dewar_name"
value={newDewar.dewar_name}
onChange={handleNewDewarChange}
fullWidth
sx={{ marginBottom: 2 }}
/>
<Button variant="contained" color="primary" onClick={handleAddDewar} sx={{ marginRight: 2 }}>
Save Dewar
</Button>
<Button variant="outlined" color="secondary" onClick={() => setIsAddingDewar(false)}>
Cancel
</Button>
</Box>
)}
<Typography variant="h5">{selectedShipment.shipment_name}</Typography>
<Typography variant="body1">Number of Pucks: {totalPucks}</Typography>
<Typography variant="body1">Number of Samples: {totalSamples}</Typography>
<Typography variant="body1">Shipment Date: {selectedShipment.shipment_date}</Typography>
<Stack spacing={1}>
{/* Render the DewarDetails component only if a dewar is selected */}
{localSelectedDewar && (
<DewarDetails
dewar={localSelectedDewar}
trackingNumber={trackingNumber}
setTrackingNumber={setTrackingNumber}
onGenerateQRCode={generateQRCode}
contactPersons={contactPersons} // Pass contact persons
returnAddresses={returnAddresses} // Pass return addresses
addNewContactPerson={addNewContactPerson} // Pass function to add a new contact person
addNewReturnAddress={addNewReturnAddress} // Pass function to add a new return address
shipping_date={localSelectedDewar?.shipping_date} // Ensure these are passed
arrival_date={localSelectedDewar?.arrival_date}
/>
)}
{selectedShipment.dewars.map((dewar: Dewar) => (
<Button
key={dewar.tracking_number}
onClick={() => handleDewarSelection(dewar)}
sx={{
width: '100%',
textAlign: 'left',
backgroundColor: localSelectedDewar?.tracking_number === dewar.tracking_number ? '#d0f0c0' : '#f0f0f0', // Highlight if selected
padding: 2,
display: 'flex',
alignItems: 'center',
}}
>
<Box
sx={{
width: 80,
height: 80,
backgroundColor: '#e0e0e0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: 2,
}}
>
{dewar.qrcode ? (
<QRCode value={dewar.qrcode} size={70} />
) : (
<Typography variant="body2" color="textSecondary">No QR code available</Typography>
)}
</Box>
<Box sx={{ flexGrow: 1, marginRight: 0 }}>
<Typography variant="body1">{dewar.dewar_name}</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">Tracking Number: {dewar.tracking_number}</Typography>
</Box>
<Box sx={{ flexGrow: 1, display: 'flex', alignItems: 'center', flexDirection: 'row', justifyContent: 'space-evenly' }}>
{/* Status icons and date information */}
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<img
src={dewar.status === "in preparation" ? bottleYellow : (dewar.status === "ready for shipping" ? bottleGreen : bottleGrey)}
alt={`Status: ${dewar.status}`}
style={{ width: '40px', height: '40px', marginBottom: '4px' }}
/>
<Typography variant="caption" sx={{ fontSize: '12px' }} color="textSecondary">
{dewar.ready_date ? `Ready: ${new Date(dewar.ready_date).toLocaleDateString()}` : 'N/A'}
</Typography>
</Box>
<ArrowForwardIcon sx={{ margin: '0 8px', fontSize: '40px', alignSelf: 'center' }} />
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<AirplanemodeActiveIcon
sx={{
color: shippingStatusMap[dewar.shippingStatus || ""] || "grey",
fontSize: '40px',
marginBottom: '4px'
}}
/>
<Typography variant="caption" sx={{ fontSize: '12px' }} color="textSecondary">
{dewar.shipping_date ? `Shipped: ${new Date(dewar.shipping_date).toLocaleDateString()}` : 'N/A'}
</Typography>
</Box>
<ArrowForwardIcon sx={{ margin: '0 8px', fontSize: '40px', alignSelf: 'center' }} />
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<StoreIcon
sx={{
color: arrivalStatusMap[dewar.arrivalStatus || ""] || "grey",
fontSize: '40px',
marginBottom: '4px'
}}
/>
<Typography variant="caption" sx={{ fontSize: '12px' }} color="textSecondary">
{dewar.arrival_date ? `Arrived: ${new Date(dewar.arrival_date).toLocaleDateString()}` : 'N/A'}
</Typography>
</Box>
{/* Delete button if the dewar is selected */}
{localSelectedDewar?.tracking_number === dewar.tracking_number && (
<Button
onClick={handleDeleteDewar}
color="error"
sx={{
minWidth: '40px',
height: '40px',
marginLeft: 2,
padding: 0,
alignSelf: 'center'
}}
title="Delete Dewar"
>
<DeleteIcon />
</Button>
)}
</Box>
</Button>
))}
</Stack>
</Box>
);
};
export default ShipmentDetails;

View File

@ -0,0 +1,232 @@
import * as React from 'react';
import { Box, Button, TextField, Typography, Select, MenuItem, Stack, FormControl, InputLabel } from '@mui/material';
import { SelectChangeEvent } from '@mui/material';
import { Shipment, ContactPerson, Proposal, Address } from '../types.ts'; // Adjust import paths as necessary
import { SxProps } from '@mui/material';
interface ShipmentFormProps {
newShipment: Shipment;
setNewShipment: React.Dispatch<React.SetStateAction<Shipment>>;
handleSaveShipment: () => void;
contactPersons: ContactPerson[];
proposals: Proposal[];
returnAddresses: Address[];
sx?: SxProps;
}
const ShipmentForm: React.FC<ShipmentFormProps> = ({
newShipment,
setNewShipment,
handleSaveShipment,
contactPersons,
proposals,
returnAddresses,
sx = {},
}) => {
const [isCreatingContactPerson, setIsCreatingContactPerson] = React.useState(false);
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = React.useState(false);
const [newContactPerson, setNewContactPerson] = React.useState({
firstName: '',
lastName: '',
phone: '',
email: '',
});
const [newReturnAddress, setNewReturnAddress] = React.useState('');
const handleContactPersonChange = (event: SelectChangeEvent<string>) => {
const value = event.target.value;
if (value === 'new') {
setIsCreatingContactPerson(true);
setNewShipment({ ...newShipment, contact_person: [] }); // Set to empty array for new person
} else {
setIsCreatingContactPerson(false);
const selectedPerson = contactPersons.find((person) => person.name === value) || null;
if (selectedPerson) {
setNewShipment({ ...newShipment, contact_person: [selectedPerson] }); // Wrap in array
}
}
};
const handleReturnAddressChange = (event: SelectChangeEvent<string>) => {
const value = event.target.value;
if (value === 'new') {
setIsCreatingReturnAddress(true);
setNewShipment({ ...newShipment, return_address: [] }); // Set to empty array for new address
} else {
setIsCreatingReturnAddress(false);
const selectedAddress = returnAddresses.find((address) => address.address === value);
if (selectedAddress) {
setNewShipment({ ...newShipment, return_address: [selectedAddress] }); // Wrap in array of Address
}
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setNewShipment((prev) => ({ ...prev, [name]: value }));
};
const handleSaveNewContactPerson = () => {
// Add logic to save the new contact person
console.log('Saving new contact person:', newContactPerson);
setIsCreatingContactPerson(false);
setNewContactPerson({ firstName: '', lastName: '', phone: '', email: '' }); // Reset fields
};
const handleSaveNewReturnAddress = () => {
// Add logic to save the new return address
console.log('Saving new return address:', newReturnAddress);
setIsCreatingReturnAddress(false);
setNewReturnAddress(''); // Reset field
};
return (
<Box
sx={{
padding: 4,
border: '1px solid #ccc',
borderRadius: '4px',
marginBottom: 2,
maxWidth: '600px',
...sx,
}}
>
<Typography variant="h6" sx={{ marginBottom: 2 }}>
Create Shipment
</Typography>
<Stack spacing={2}>
<TextField
label="Shipment Name"
name="shipment_name"
value={newShipment.shipment_name || ''}
onChange={handleChange}
fullWidth
/>
<FormControl fullWidth>
<InputLabel>Contact Person</InputLabel>
<Select
value={newShipment.contact_person?.[0]?.name || ''} // Access first contact person
onChange={handleContactPersonChange}
displayEmpty
>
<MenuItem value="">
<em>Select a Contact Person</em>
</MenuItem>
{contactPersons.map((person) => (
<MenuItem key={person.id} value={person.name}>
{person.name}
</MenuItem>
))}
<MenuItem value="new">
<em>Create New Contact Person</em>
</MenuItem>
</Select>
</FormControl>
{isCreatingContactPerson && (
<>
<TextField
label="First Name"
name="firstName"
value={newContactPerson.firstName}
onChange={(e) => setNewContactPerson({ ...newContactPerson, firstName: e.target.value })}
fullWidth
/>
<TextField
label="Last Name"
name="lastName"
value={newContactPerson.lastName}
onChange={(e) => setNewContactPerson({ ...newContactPerson, lastName: e.target.value })}
fullWidth
/>
<TextField
label="Phone"
name="phone"
value={newContactPerson.phone}
onChange={(e) => setNewContactPerson({ ...newContactPerson, phone: e.target.value })}
fullWidth
/>
<TextField
label="Email"
name="email"
value={newContactPerson.email}
onChange={(e) => setNewContactPerson({ ...newContactPerson, email: e.target.value })}
fullWidth
/>
<Button variant="contained" color="primary" onClick={handleSaveNewContactPerson}>
Save New Contact Person
</Button>
</>
)}
<FormControl fullWidth>
<InputLabel>Proposal Number</InputLabel>
<Select
value={newShipment.proposal_number || ''}
onChange={(e) => setNewShipment({ ...newShipment, proposal_number: e.target.value })}
displayEmpty
>
<MenuItem value="">
<em>Select a Proposal Number</em>
</MenuItem>
{proposals.map((proposal) => (
<MenuItem key={proposal.id} value={proposal.number}>
{proposal.number}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Return Address</InputLabel>
<Select
value={newShipment.return_address?.[0]?.address || ''} // Access first return address's address
onChange={handleReturnAddressChange}
displayEmpty
>
<MenuItem value="">
<em>Select a Return Address</em>
</MenuItem>
{returnAddresses.map((address) => (
<MenuItem key={address.id} value={address.address}>
{address.address}
</MenuItem>
))}
<MenuItem value="new">
<em>Create New Return Address</em>
</MenuItem>
</Select>
</FormControl>
{isCreatingReturnAddress && (
<>
<TextField
label="New Return Address"
value={newReturnAddress}
onChange={(e) => setNewReturnAddress(e.target.value)}
fullWidth
/>
<Button variant="contained" color="primary" onClick={handleSaveNewReturnAddress}>
Save New Return Address
</Button>
</>
)}
<TextField
label="Comments"
name="comments"
fullWidth
multiline
rows={4}
value={newShipment.comments || ''}
onChange={handleChange}
/>
<Button
variant="contained"
color="primary"
onClick={handleSaveShipment}
sx={{ alignSelf: 'flex-end' }}
>
Save Shipment
</Button>
</Stack>
</Box>
);
};
export default ShipmentForm;

View File

@ -0,0 +1,206 @@
import * as React from 'react';
import { useEffect, useState, Dispatch, SetStateAction } from 'react';
import { Button, Box, Typography, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete'; // Import delete icon
import UploadFileIcon from '@mui/icons-material/UploadFile'; // Import the upload icon
import UploadDialog from './UploadDialog.tsx'; // Import the UploadDialog component
import { Shipment } from '../types.ts'; // Ensure Shipment type is correctly imported
import { SxProps } from '@mui/material';
import bottleGrey from '../assets/icons/bottle-svgrepo-com-grey.svg';
import bottleYellow from '../assets/icons/bottle-svgrepo-com-yellow.svg';
import bottleGreen from '../assets/icons/bottle-svgrepo-com-green.svg';
import bottleRed from '../assets/icons/bottle-svgrepo-com-red.svg';
interface ShipmentPanelProps {
selectedPage: string;
setIsCreatingShipment: Dispatch<SetStateAction<boolean>>;
newShipment: Shipment; // Ensure this aligns with the Shipment type
setNewShipment: Dispatch<SetStateAction<Shipment>>;
selectShipment: (shipment: Shipment | null) => void; // Allow null for deselection
sx?: SxProps; // Optional sx prop for styling
}
const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
setIsCreatingShipment,
newShipment,
setNewShipment,
selectedPage,
selectShipment,
sx,
}) => {
const [shipments, setShipments] = useState<Shipment[]>([]);
const [selectedShipment, setSelectedShipment] = useState<Shipment | null>(null);
const [error, setError] = useState<string | null>(null);
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
const handleOpenUploadDialog = () => {
setUploadDialogOpen(true);
};
const handleCloseUploadDialog = () => {
setUploadDialogOpen(false);
};
// Status icon mapping
const statusIconMap: Record<string, string> = {
"In Transit": bottleYellow,
"Delivered": bottleGreen,
"Pending": bottleGrey,
"Unknown": bottleRed,
};
useEffect(() => {
const fetchShipments = async () => {
try {
const response = await fetch('/shipmentsdb.json');
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
setShipments(data.shipments);
} catch (error) {
console.error('Failed to fetch shipments:', error);
setError("Failed to fetch shipments. Please try again later.");
}
};
fetchShipments();
}, []);
const handleShipmentSelection = (shipment: Shipment) => {
const isCurrentlySelected = selectedShipment?.shipment_id === shipment.shipment_id;
setSelectedShipment(isCurrentlySelected ? null : shipment);
selectShipment(isCurrentlySelected ? null : shipment);
};
const handleDeleteShipment = () => {
if (selectedShipment) {
const confirmed = window.confirm(`Are you sure you want to delete the shipment: ${selectedShipment.shipment_name}?`);
if (confirmed) {
const updatedShipments = shipments.filter(shipment => shipment.shipment_id !== selectedShipment.shipment_id);
setShipments(updatedShipments);
setSelectedShipment(null); // Optionally clear the selected shipment
}
}
};
return (
<Box sx={{ width: '90%', borderRight: '1px solid #ccc', padding: 2, ...sx }}>
{error && <Typography color="error">{error}</Typography>}
<Typography variant="h6" sx={{ marginBottom: 2, fontWeight: 'bold' }}>
Shipments
</Typography>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
onClick={() => {
setNewShipment({
shipment_id: '', // Ensure this matches the Shipment type
shipment_name: '',
shipment_status: '',
number_of_dewars: 0,
shipment_date: '',
contact_person: null, // Keep this as null to match Shipment type
dewars: [],
return_address: [], // Make sure return_address is initialized as an array
proposal_number: undefined, // Optional property
comments: '', // Optional property
});
setIsCreatingShipment(true);
}}
sx={{ marginBottom: 2, padding: '10px 16px' }}
>
Create Shipment
</Button>
{shipments.map((shipment) => (
<Button
key={shipment.shipment_id}
onClick={() => handleShipmentSelection(shipment)}
sx={{
width: '100%',
textAlign: 'left',
marginBottom: 1,
color: 'white',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
padding: '10px 16px',
fontSize: '0.7rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: selectedShipment?.shipment_id === shipment.shipment_id ? '#52893e' : '#424242',
'&:hover': {
backgroundColor: selectedShipment?.shipment_id === shipment.shipment_id ? '#9aca8c' : '#616161',
},
'&:active': {
backgroundColor: selectedShipment?.shipment_id === shipment.shipment_id ? '#915151' : '#212121',
},
}}
>
<div style={{display: 'flex', alignItems: 'center'}}>
<div style={{position: 'relative', marginRight: '8px'}}>
<img
src={statusIconMap[shipment.shipment_status] || bottleGrey}
alt={`Status: ${shipment.shipment_status}`}
width="24"
/>
<span style={{
position: 'absolute',
top: '0%',
right: '0%',
transform: 'translate(50%, -50%)',
color: 'white',
fontWeight: 'bold',
fontSize: '0.6rem',
backgroundColor: 'transparent',
borderRadius: '50%',
padding: '0 2px',
}}>
{shipment.number_of_dewars}
</span>
</div>
<div>
<div>{shipment.shipment_name}</div>
<div style={{fontSize: '0.6rem', color: '#ccc'}}>{shipment.shipment_date}</div>
<div style={{fontSize: '0.6rem', color: '#ccc'}}>
Total
Pucks: {shipment.dewars.reduce((total, dewar) => total + dewar.number_of_pucks, 0)}
</div>
</div>
</div>
<div style={{display: 'flex', alignItems: 'center'}}>
<IconButton
onClick={handleOpenUploadDialog}
color="primary"
title="Upload Sample Data Sheet"
sx={{marginLeft: 1}}
>
<UploadFileIcon/>
</IconButton>
{selectedShipment?.shipment_id === shipment.shipment_id && (
<IconButton
onClick={handleDeleteShipment}
color="error"
title="Delete Shipment"
sx={{marginLeft: 1}}
>
<DeleteIcon/>
</IconButton>
)}
</div>
</Button>
))}
{/* UploadDialog component */}
<UploadDialog
open={uploadDialogOpen}
onClose={handleCloseUploadDialog}
/>
</Box>
);
};
export default ShipmentPanel;

View File

@ -0,0 +1,100 @@
import React from 'react';
import { Grid } from '@mui/material';
import ShipmentPanel from './ShipmentPanel.tsx';
import ShipmentDetails from './ShipmentDetails.tsx';
import ShipmentForm from './ShipmentForm.tsx';
import { Shipment, Dewar } from '../types.ts';
import { ContactPerson, Address, Proposal } from '../types.ts';
interface ShipmentProps {
newShipment: Shipment;
setNewShipment: React.Dispatch<React.SetStateAction<Shipment>>;
isCreatingShipment: boolean;
setIsCreatingShipment: React.Dispatch<React.SetStateAction<boolean>>;
selectedShipment: Shipment | null;
selectShipment: (shipment: Shipment | null) => void; // Allow null for deselection
selectedDewar: Dewar | null;
setSelectedDewar: React.Dispatch<React.SetStateAction<Dewar | null>>;
handleSaveShipment: () => void;
contactPersons: ContactPerson[];
returnAddresses: Address[];
proposals: Proposal[];
}
const ShipmentView: React.FC<ShipmentProps> = ({
newShipment,
setNewShipment,
isCreatingShipment,
setIsCreatingShipment,
selectedShipment,
selectShipment,
selectedDewar,
setSelectedDewar,
handleSaveShipment,
contactPersons,
returnAddresses,
proposals,
}) => {
return (
<Grid container spacing={2} sx={{ height: '100vh' }}>
{/* Left column: ShipmentPanel */}
<Grid
item
xs={12}
md={3} // Adjust width for left column
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 0, // Do not allow the panel to grow
}}
>
<ShipmentPanel
selectedPage="Shipments"
setIsCreatingShipment={setIsCreatingShipment}
newShipment={newShipment}
setNewShipment={setNewShipment}
selectShipment={selectShipment} // This now accepts Shipment | null
/>
</Grid>
{/* Right column: ShipmentForm or ShipmentDetails */}
<Grid
item
xs={12}
md={9}
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
}}
>
{isCreatingShipment ? (
<ShipmentForm
newShipment={newShipment}
setNewShipment={setNewShipment}
handleSaveShipment={handleSaveShipment}
contactPersons={contactPersons}
proposals={proposals}
returnAddresses={returnAddresses}
sx={{ flexGrow: 1 }} // Allow form to grow and take available space
/>
) : (
<ShipmentDetails
selectedShipment={selectedShipment}
setSelectedDewar={setSelectedDewar}
isCreatingShipment={isCreatingShipment}
newShipment={newShipment}
setNewShipment={setNewShipment}
handleSaveShipment={handleSaveShipment}
contactPersons={contactPersons}
proposals={proposals}
returnAddresses={returnAddresses}
sx={{ flexGrow: 1 }} // Allow details to grow and take available space
/>
)}
</Grid>
</Grid>
);
};
export default ShipmentView;

View File

@ -0,0 +1,152 @@
import * as React from 'react';
import { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
IconButton,
Box,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import DownloadIcon from '@mui/icons-material/Download';
import UploadFileIcon from '@mui/icons-material/UploadFile';
import logo from '../assets/Heidi-logo.png';
interface UploadDialogProps {
open: boolean;
onClose: () => void;
}
const UploadDialog: React.FC<UploadDialogProps> = ({ open, onClose }) => {
const [uploadError, setUploadError] = useState<string | null>(null);
const [fileSummary, setFileSummary] = useState<{
dewars: number;
pucks: number;
samples: number;
} | null>(null);
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
// Reset the previous state
setUploadError(null);
setFileSummary(null);
// Example file type check: only allow .xlsx files
if (!file.name.endsWith('.xlsx')) {
setUploadError('Invalid file format. Please upload an .xlsx file.');
return;
}
// Simulate file reading and validation
const reader = new FileReader();
reader.onload = () => {
// Here, parse the file content and validate
// For the demo, we'll mock the summary
const mockSummary = {
dewars: 5,
pucks: 10,
samples: 100,
};
setFileSummary(mockSummary);
};
reader.onerror = () => {
setUploadError('Failed to read the file. Please try again.');
};
reader.readAsArrayBuffer(file);
};
return (
<Dialog open={open} onClose={onClose} fullWidth maxWidth="sm">
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Upload Sample Data Sheet</Typography>
<IconButton onClick={onClose}>
<CloseIcon />
</IconButton>
</Box>
</DialogTitle>
<DialogContent dividers>
<Box display="flex" flexDirection="column" alignItems="center" mb={2}>
<img
src={logo}
alt="Logo"
style={{ width: 200, marginBottom: 16 }}
/>
<Typography variant="subtitle1">
Latest Spreadsheet Template Version 6
</Typography>
<Typography variant="body2" color="textSecondary">
Last update: October 18, 2024
</Typography>
<Button
variant="outlined"
startIcon={<DownloadIcon />}
href="/path/to/template.xlsx"
download
sx={{ mt: 1 }}
>
Download XLSX
</Button>
<Typography variant="subtitle1" sx={{ mt: 3 }}>
Latest Spreadsheet Instructions Version 2.3
</Typography>
<Typography variant="body2" color="textSecondary">
Last updated: October 18, 2024
</Typography>
<Button
variant="outlined"
startIcon={<DownloadIcon />}
href="/path/to/instructions.pdf"
download
sx={{ mt: 1 }}
>
Download PDF
</Button>
</Box>
<Box mt={3}>
<Button
variant="contained"
component="label"
startIcon={<UploadFileIcon />}
>
Choose a File
<input
type="file"
hidden
onChange={handleFileUpload}
/>
</Button>
{uploadError && (
<Typography color="error" sx={{ mt: 2 }}>
{uploadError}
</Typography>
)}
{fileSummary && (
<Box mt={2}>
<Typography variant="body1">
<strong>File Summary:</strong>
</Typography>
<Typography>Dewars: {fileSummary.dewars}</Typography>
<Typography>Pucks: {fileSummary.pucks}</Typography>
<Typography>Samples: {fileSummary.samples}</Typography>
</Box>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
);
};
export default UploadDialog;