fixed bug with spreadsheet import
This commit is contained in:
parent
996fc66d76
commit
e28c8b05d4
@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from app.models import Shipment
|
||||
from .models import Shipment
|
||||
|
||||
def get_shipments(db: Session):
|
||||
logging.info("Fetching all shipments from the database.")
|
||||
|
@ -3,7 +3,7 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app import models
|
||||
from . import models
|
||||
import os
|
||||
|
||||
# Get username and password from environment variables
|
||||
@ -31,12 +31,12 @@ def get_db():
|
||||
|
||||
def init_db():
|
||||
# Import models inside function to avoid circular dependency
|
||||
from app import models
|
||||
from . import models
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
def load_sample_data(session: Session):
|
||||
# Import models inside function to avoid circular dependency
|
||||
from app.data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples, dewar_types, serial_numbers, slots, sample_events
|
||||
from .data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples, dewar_types, serial_numbers, slots, sample_events
|
||||
|
||||
# If any data already exists, skip seeding
|
||||
if session.query(models.ContactPerson).first():
|
||||
|
@ -1,5 +1,5 @@
|
||||
# app/dependencies.py
|
||||
from app.database import SessionLocal # Import SessionLocal from database.py
|
||||
from .database import SessionLocal # Import SessionLocal from database.py
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
|
@ -1,4 +1,4 @@
|
||||
from app.database import init_db
|
||||
from .database import init_db
|
||||
|
||||
|
||||
def initialize_database():
|
||||
|
@ -1,6 +1,6 @@
|
||||
from sqlalchemy import Column, Integer, String, Date, ForeignKey, JSON, Interval, DateTime, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.database import Base
|
||||
from .database import Base
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
|
||||
|
@ -4,16 +4,31 @@ from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app import ssl_heidi
|
||||
from pathlib import Path
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
from app.routers import address, contact, proposal, dewar, shipment, puck, spreadsheet, logistics, auth, sample
|
||||
from app.database import Base, engine, SessionLocal, load_sample_data
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Generate SSL Key and Certificate if not exist
|
||||
Path("ssl").mkdir(parents=True, exist_ok=True)
|
||||
if not Path("ssl/cert.pem").exists() or not Path("ssl/key.pem").exists():
|
||||
ssl_heidi.generate_self_signed_cert("ssl/cert.pem", "ssl/key.pem")
|
||||
# Determine environment and configuration file path
|
||||
environment = os.getenv('ENVIRONMENT', 'dev')
|
||||
config_file = Path(__file__).resolve().parent.parent / f'config_{environment}.json'
|
||||
|
||||
# Load configuration
|
||||
with open(config_file) as f:
|
||||
config = json.load(f)
|
||||
|
||||
cert_path = config['ssl_cert_path']
|
||||
key_path = config['ssl_key_path']
|
||||
|
||||
# Generate SSL Key and Certificate if not exist (only for development)
|
||||
if environment == 'development':
|
||||
Path("ssl").mkdir(parents=True, exist_ok=True)
|
||||
if not Path(cert_path).exists() or not Path(key_path).exists():
|
||||
ssl_heidi.generate_self_signed_cert(cert_path, key_path)
|
||||
|
||||
# Apply CORS middleware
|
||||
app.add_middleware(
|
||||
@ -52,7 +67,20 @@ app.include_router(sample.router, prefix="/samples", tags=["samples"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ssl_context.load_cert_chain(certfile="ssl/cert.pem", keyfile="ssl/key.pem")
|
||||
import uvicorn
|
||||
import os
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8000, log_level="debug", ssl_context=ssl_context)
|
||||
# Get environment from an environment variable
|
||||
environment = os.getenv('ENVIRONMENT', 'dev')
|
||||
|
||||
# Paths for SSL certificates
|
||||
cert_path = "ssl/cert.pem"
|
||||
key_path = "ssl/key.pem"
|
||||
|
||||
if environment == 'testing':
|
||||
cert_path = "ssl/mx-aare-test.psi.ch.pem"
|
||||
key_path = "ssl/mx-aare-test.psi.ch.key"
|
||||
|
||||
# Run the application with appropriate SSL setup
|
||||
uvicorn.run(app, host="127.0.0.1", port=8000, log_level="debug",
|
||||
ssl_keyfile=key_path, ssl_certfile=cert_path)
|
9
config_dev.json
Normal file
9
config_dev.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"ssl_cert_path": "ssl/cert.pem",
|
||||
"ssl_key_path": "ssl/key.pem",
|
||||
"OPENAPI_URL": "https://127.0.0.1:8000/openapi.json",
|
||||
"SCHEMA_PATH": "./src/openapi.json",
|
||||
"OUTPUT_DIRECTORY": "./openapi",
|
||||
"SSL_KEY_PATH": "../backend/ssl/key.pem",
|
||||
"SSL_CERT_PATH": "../backend/ssl/cert.pem"
|
||||
}
|
9
config_test.json
Normal file
9
config_test.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"ssl_cert_path": "ssl/mx-aare-test.psi.ch.pem",
|
||||
"ssl_key_path": "ss/mx-aare-test.psi.ch.key",
|
||||
"OPENAPI_URL": "https://mx-aare-test.psi.ch:8000/openapi.json",
|
||||
"SCHEMA_PATH": "./src/openapi.json",
|
||||
"OUTPUT_DIRECTORY": "./openapi",
|
||||
"SSL_KEY_PATH": "/home/jungfrau/heidi-v2/backend/app/ssl/mx-aare-test.psi.ch.key",
|
||||
"SSL_CERT_PATH": "/home/jungfrau/heidi-v2/backend/app/ssl/mx-aare-test.psi.ch.pem"
|
||||
}
|
2
frontend/.env
Normal file
2
frontend/.env
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_OPENAPI_BASE_DEV=https://127.0.0.1:8000
|
||||
VITE_OPENAPI_BASE_TEST=https://mx-aare-test.psi.ch:8000
|
@ -1,16 +1,28 @@
|
||||
// fetch-and-generate-openapi.js
|
||||
import fs from 'fs';
|
||||
import https from 'https'; // Use https instead of http
|
||||
import https from 'https';
|
||||
import { exec } from 'child_process';
|
||||
import chokidar from 'chokidar';
|
||||
import path from 'path';
|
||||
import util from 'util';
|
||||
|
||||
const OPENAPI_URL = 'https://127.0.0.1:8000/openapi.json';
|
||||
const SCHEMA_PATH = path.resolve('./src/openapi.json');
|
||||
const OUTPUT_DIRECTORY = path.resolve('./openapi');
|
||||
const SSL_KEY_PATH = path.resolve('../backend/ssl/key.pem'); // Path to SSL key
|
||||
const SSL_CERT_PATH = path.resolve('../backend/ssl/cert.pem'); // Path to SSL certificate
|
||||
// Determine the environment
|
||||
const environment = process.env.NODE_ENV || 'development';
|
||||
const configFile = `config_${environment}.json`;
|
||||
|
||||
// Load the appropriate configuration
|
||||
let config;
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(path.resolve('../', configFile), 'utf8'));
|
||||
} catch (error) {
|
||||
console.error(`❌ Error reading configuration file: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const OPENAPI_URL = config.OPENAPI_URL;
|
||||
const SCHEMA_PATH = path.resolve(config.SCHEMA_PATH);
|
||||
const OUTPUT_DIRECTORY = path.resolve(config.OUTPUT_DIRECTORY);
|
||||
const SSL_KEY_PATH = path.resolve(config.SSL_KEY_PATH);
|
||||
const SSL_CERT_PATH = path.resolve(config.SSL_CERT_PATH);
|
||||
|
||||
console.log(`Using SCHEMA_PATH: ${SCHEMA_PATH}`);
|
||||
console.log(`Using OUTPUT_DIRECTORY: ${OUTPUT_DIRECTORY}`);
|
||||
@ -70,7 +82,6 @@ async function fetchAndGenerate() {
|
||||
await fs.promises.rm(OUTPUT_DIRECTORY, { recursive: true, force: true });
|
||||
console.log(`✅ Output directory cleaned at ${OUTPUT_DIRECTORY}`);
|
||||
|
||||
// Verify directory removal
|
||||
if (fs.existsSync(OUTPUT_DIRECTORY)) {
|
||||
console.error(`❌ Output directory still exists: ${OUTPUT_DIRECTORY}`);
|
||||
} else {
|
||||
@ -81,7 +92,6 @@ async function fetchAndGenerate() {
|
||||
console.log(`Executing debug command: ${command}`);
|
||||
|
||||
const { stdout, stderr } = await execPromisified(command);
|
||||
console.log("🔍 Inside exec callback");
|
||||
if (stderr) {
|
||||
console.error(`⚠️ stderr while generating services: ${stderr}`);
|
||||
} else {
|
||||
@ -99,13 +109,13 @@ async function fetchAndGenerate() {
|
||||
}
|
||||
}
|
||||
|
||||
const backendDirectory = '/Users/gotthardg/PycharmProjects/heidi-v2/backend/app';
|
||||
const backendDirectory = path.resolve('/Users/gotthardg/PycharmProjects/heidi-v2/backend/app');
|
||||
console.log(`👀 Watching for changes in ${backendDirectory}`);
|
||||
const watcher = chokidar.watch(backendDirectory, { persistent: true, ignored: [SCHEMA_PATH, OUTPUT_DIRECTORY] });
|
||||
|
||||
watcher
|
||||
.on('add', debounce(fetchAndGenerate, debounceDelay))
|
||||
.on('change', debounce(fetchAndGenerate, debounceDelay)) // Corrected typo here
|
||||
.on('change', debounce(fetchAndGenerate, debounceDelay))
|
||||
.on('unlink', debounce(fetchAndGenerate, debounceDelay));
|
||||
|
||||
console.log(`👀 Watching for changes in ${backendDirectory}`);
|
33
frontend/package-lock.json
generated
33
frontend/package-lock.json
generated
@ -24,6 +24,7 @@
|
||||
"axios": "^1.7.7",
|
||||
"chokidar": "^4.0.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"exceljs": "^4.4.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"openapi-typescript-codegen": "^0.29.0",
|
||||
@ -41,6 +42,7 @@
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.12",
|
||||
@ -3434,6 +3436,25 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "src/bin/cross-env.js",
|
||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
|
||||
@ -3560,6 +3581,18 @@
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer2": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
||||
|
@ -4,12 +4,14 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node_modules/.bin/vite",
|
||||
"build": "node_modules/.bin/tsc -b && vite build",
|
||||
"lint": "node_modules/.bin/eslint .",
|
||||
"preview": "node_modules/.bin/vite preview",
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"start-dev": "cross-env NODE_ENV=dev vite",
|
||||
"start-test": "cross-env NODE_ENV=test vite",
|
||||
"watch:openapi": "node fetch-openapi.js"
|
||||
},
|
||||
},
|
||||
"dependencies": {
|
||||
"@aldabil/react-scheduler": "^2.9.5",
|
||||
"@bitnoi.se/react-scheduler": "^0.3.1",
|
||||
@ -27,6 +29,7 @@
|
||||
"axios": "^1.7.7",
|
||||
"chokidar": "^4.0.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"exceljs": "^4.4.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"openapi-typescript-codegen": "^0.29.0",
|
||||
@ -44,6 +47,7 @@
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.12",
|
||||
|
@ -39,7 +39,10 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
|
||||
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
OpenAPI.BASE = 'https://127.0.0.1:8000';
|
||||
const isTestEnv = import.meta.env.MODE === 'test';
|
||||
OpenAPI.BASE = isTestEnv
|
||||
? import.meta.env.VITE_OPENAPI_BASE_TEST
|
||||
: import.meta.env.VITE_OPENAPI_BASE_DEV;
|
||||
|
||||
const getContacts = async () => {
|
||||
try {
|
||||
|
@ -1,14 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
IconButton,
|
||||
Box,
|
||||
CircularProgress
|
||||
Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, IconButton, Box, CircularProgress
|
||||
} from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
@ -17,45 +9,39 @@ import logo from '../assets/Heidi-logo.png';
|
||||
import { OpenAPI, SpreadsheetService } from '../../openapi';
|
||||
import type { Body_upload_file_upload_post } from '../../openapi/models/Body_upload_file_upload_post';
|
||||
import SpreadsheetTable from './SpreadsheetTable';
|
||||
import Modal from './Modal'; // Ensure correct import paths
|
||||
import Modal from './Modal';
|
||||
import * as ExcelJS from 'exceljs';
|
||||
|
||||
interface UploadDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
selectedShipment: any; // Adjust the type based on your implementation
|
||||
selectedShipment: any;
|
||||
}
|
||||
|
||||
const UploadDialog: React.FC<UploadDialogProps> = ({ open, onClose, selectedShipment }) => {
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
const [fileSummary, setFileSummary] = useState<{
|
||||
data: any[];
|
||||
errors: { row: number, cell: number, value: any, message: string }[];
|
||||
raw_data: { row_num: number, data: any[] }[];
|
||||
dewars_count: number;
|
||||
dewars: string[];
|
||||
pucks_count: number;
|
||||
pucks: string[];
|
||||
samples_count: number;
|
||||
samples: string[];
|
||||
headers: string[];
|
||||
} | null>(null);
|
||||
const [fileBlob, setFileBlob] = useState<Blob | null>(null); // New state to store the file blob
|
||||
const [fileSummary, setFileSummary] = useState<any>(null);
|
||||
const [fileBlob, setFileBlob] = useState<Blob | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
OpenAPI.BASE = 'https://127.0.0.1:8000';
|
||||
const isTestEnv = import.meta.env.MODE === 'test';
|
||||
OpenAPI.BASE = isTestEnv
|
||||
? import.meta.env.VITE_OPENAPI_BASE_TEST
|
||||
: import.meta.env.VITE_OPENAPI_BASE_DEV;
|
||||
}, []);
|
||||
|
||||
const downloadUrl = `${OpenAPI.BASE}/download-template`;
|
||||
|
||||
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setUploadError(null);
|
||||
setFileSummary(null);
|
||||
setFileBlob(file); // Store the file blob
|
||||
setFileBlob(file);
|
||||
setIsLoading(true);
|
||||
|
||||
if (!file.name.endsWith('.xlsx')) {
|
||||
@ -68,8 +54,8 @@ const UploadDialog: React.FC<UploadDialogProps> = ({ open, onClose, selectedShip
|
||||
|
||||
try {
|
||||
const response = await SpreadsheetService.uploadFileUploadPost(formData);
|
||||
|
||||
const { headers, raw_data, errors } = response;
|
||||
|
||||
setFileSummary({
|
||||
data: raw_data,
|
||||
errors: errors,
|
||||
@ -82,6 +68,7 @@ const UploadDialog: React.FC<UploadDialogProps> = ({ open, onClose, selectedShip
|
||||
samples_count: 23,
|
||||
samples: ['Sample1', 'Sample2']
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
setIsModalOpen(true);
|
||||
} catch (error) {
|
||||
@ -115,7 +102,7 @@ const UploadDialog: React.FC<UploadDialogProps> = ({ open, onClose, selectedShip
|
||||
<img src={logo} alt="Logo" style={{ width: 200, marginBottom: 16 }} />
|
||||
<Typography variant="subtitle1">Latest Spreadsheet Template Version 7</Typography>
|
||||
<Typography variant="body2" color="textSecondary">Last update: November 7, 2024</Typography>
|
||||
<Button variant="outlined" startIcon={<DownloadIcon />} href="http://127.0.0.1:8000/download-template" download sx={{ mt: 1 }}>
|
||||
<Button variant="outlined" startIcon={<DownloadIcon />} href={downloadUrl} download sx={{ mt: 1 }}>
|
||||
Download XLSX
|
||||
</Button>
|
||||
<Typography variant="subtitle1" sx={{ mt: 3 }}>Latest Spreadsheet Instructions Version 2.3</Typography>
|
||||
@ -165,8 +152,8 @@ const UploadDialog: React.FC<UploadDialogProps> = ({ open, onClose, selectedShip
|
||||
headers={fileSummary.headers}
|
||||
setRawData={(newRawData) => setFileSummary((prevSummary) => ({ ...prevSummary, raw_data: newRawData }))}
|
||||
onCancel={handleCancel}
|
||||
fileBlob={fileBlob} // Pass the original file blob
|
||||
selectedShipment={selectedShipment} // Pass the selected shipment ID
|
||||
fileBlob={fileBlob}
|
||||
selectedShipment={selectedShipment}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
|
@ -6,12 +6,8 @@ import { Dewar, OpenAPI, Shipment } from '../../openapi';
|
||||
import useShipments from '../hooks/useShipments';
|
||||
import { Grid, Container } from '@mui/material';
|
||||
|
||||
// Define props for Shipments View
|
||||
type ShipmentViewProps = React.PropsWithChildren<Record<string, never>>;
|
||||
|
||||
const API_BASE_URL = 'https://127.0.0.1:8000';
|
||||
OpenAPI.BASE = API_BASE_URL;
|
||||
|
||||
const ShipmentView: React.FC<ShipmentViewProps> = () => {
|
||||
const { shipments, error, defaultContactPerson, fetchAndSetShipments } = useShipments();
|
||||
const [selectedShipment, setSelectedShipment] = useState<Shipment | null>(null);
|
||||
@ -19,10 +15,13 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
|
||||
const [isCreatingShipment, setIsCreatingShipment] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Updated shipments:', shipments);
|
||||
}, [shipments]);
|
||||
const isTestEnv = import.meta.env.MODE === 'test';
|
||||
OpenAPI.BASE = isTestEnv
|
||||
? import.meta.env.VITE_OPENAPI_BASE_TEST
|
||||
: import.meta.env.VITE_OPENAPI_BASE_DEV;
|
||||
fetchAndSetShipments();
|
||||
}, []);
|
||||
|
||||
// Handlers for selecting shipment and canceling form
|
||||
const handleSelectShipment = (shipment: Shipment | null) => {
|
||||
setSelectedShipment(shipment);
|
||||
setIsCreatingShipment(false);
|
||||
@ -32,7 +31,6 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
|
||||
setIsCreatingShipment(false);
|
||||
};
|
||||
|
||||
// Render the shipment content based on state
|
||||
const renderShipmentContent = () => {
|
||||
if (isCreatingShipment) {
|
||||
return (
|
||||
@ -60,7 +58,6 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
|
||||
return <div>No shipment details available.</div>;
|
||||
};
|
||||
|
||||
// Render the main layout using Grid for layout
|
||||
return (
|
||||
<Container maxWidth={false} disableGutters sx={{ display: 'flex', height: '100vh' }}>
|
||||
<Grid container spacing={2} sx={{ height: '100vh' }}>
|
||||
|
12
logistics/index.html
Normal file
12
logistics/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
1
logistics/public/vite.svg
Normal file
1
logistics/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
42
logistics/src/App.css
Normal file
42
logistics/src/App.css
Normal file
@ -0,0 +1,42 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
12
logistics/src/App.tsx
Normal file
12
logistics/src/App.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import LogisticsView from './pages/LogisticsView';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<LogisticsView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
1
logistics/src/assets/react.svg
Normal file
1
logistics/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
49
logistics/src/components/Modal.tsx
Normal file
49
logistics/src/components/Modal.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { Modal as MuiModal, Box, Typography, IconButton } from '@mui/material';
|
||||
import { Close } from '@mui/icons-material';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ModalContent = styled(Box)`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
`;
|
||||
|
||||
const ModalHeader = styled(Box)`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({ open, onClose, title, children }) => {
|
||||
return (
|
||||
<MuiModal open={open} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</ModalHeader>
|
||||
{children}
|
||||
</ModalContent>
|
||||
</MuiModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
6
logistics/src/index.css
Normal file
6
logistics/src/index.css
Normal file
@ -0,0 +1,6 @@
|
||||
/* index.css */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
10
logistics/src/main.tsx
Normal file
10
logistics/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
533
logistics/src/pages/LogisticsView.tsx
Normal file
533
logistics/src/pages/LogisticsView.tsx
Normal file
@ -0,0 +1,533 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Box, Button, TextField, Typography, Grid, IconButton, Snackbar, Alert } from '@mui/material';
|
||||
import { CameraAlt } from '@mui/icons-material';
|
||||
import ScannerModal from '../components/ScannerModal';
|
||||
import Storage from '../components/Storage';
|
||||
import { LogisticsService, OpenAPI } from '../../../frontend/openapi';
|
||||
import type { Dewar, Slot as SlotSchema } from '../../../frontend/openapi/models';
|
||||
import styled from 'styled-components';
|
||||
import moment from 'moment';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const slotQRCodes = [
|
||||
"A1-X06SA", "A2-X06SA", "A3-X06SA", "A4-X06SA", "A5-X06SA",
|
||||
"B1-X06SA", "B2-X06SA", "B3-X06SA", "B4-X06SA", "B5-X06SA",
|
||||
"C1-X06SA", "C2-X06SA", "C3-X06SA", "C4-X06SA", "C5-X06SA",
|
||||
"D1-X06SA", "D2-X06SA", "D3-X06SA", "D4-X06SA", "D5-X06SA",
|
||||
"A1-X10SA", "A2-X10SA", "A3-X10SA", "A4-X10SA", "A5-X10SA",
|
||||
"B1-X10SA", "B2-X10SA", "B3-X10SA", "B4-X10SA", "B5-X10SA",
|
||||
"C1-X10SA", "C2-X10SA", "C3-X10SA", "C4-X10SA", "C5-X10SA",
|
||||
"D1-X10SA", "D2-X10SA", "D3-X10SA", "D4-X10SA", "D5-X10SA",
|
||||
"NB1", "NB2", "NB3", "NB4", "NB5", "NB6"
|
||||
];
|
||||
|
||||
const beamlineQRCodes = [
|
||||
"X10SA-Beamline", "X06SA-Beamline", "X06DA-Beamline"
|
||||
];
|
||||
|
||||
const outgoingQRCodes = [
|
||||
"Outgoing X10SA", "Outgoing X06SA"
|
||||
];
|
||||
|
||||
const storageToSlotsMapping = {
|
||||
"X06SA-storage": [
|
||||
"A1-X06SA", "A2-X06SA", "A3-X06SA", "A4-X06SA", "A5-X06SA",
|
||||
"B1-X06SA", "B2-X06SA", "B3-X06SA", "B4-X06SA", "B5-X06SA",
|
||||
"C1-X06SA", "C2-X06SA", "C3-X06SA", "C4-X06SA", "C5-X06SA",
|
||||
"D1-X06SA", "D2-X06SA", "D3-X06SA", "D4-X06SA", "D5-X06SA"
|
||||
],
|
||||
"X10SA-storage": [
|
||||
"A1-X10SA", "A2-X10SA", "A3-X10SA", "A4-X10SA", "A5-X10SA",
|
||||
"B1-X10SA", "B2-X10SA", "B3-X10SA", "B4-X10SA", "B5-X10SA",
|
||||
"C1-X10SA", "C2-X10SA", "C3-X10SA", "C4-X10SA", "C5-X10SA",
|
||||
"D1-X10SA", "D2-X10SA", "D3-X10SA", "D4-X10SA", "D5-X10SA"
|
||||
],
|
||||
"Novartis-Box": ["NB1", "NB2", "NB3", "NB4", "NB5", "NB6"]
|
||||
};
|
||||
|
||||
interface SlotData extends SlotSchema {
|
||||
dewar: Dewar | null;
|
||||
qr_code: string;
|
||||
dewar_name?: string;
|
||||
needsRefillWarning?: boolean;
|
||||
retrievedTimestamp?: string;
|
||||
beamlineLocation?: string;
|
||||
shipment_name?: string;
|
||||
contact_person?: string;
|
||||
local_contact?: string;
|
||||
}
|
||||
|
||||
const DetailPanel = styled(Box)`
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
margin-top: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const LogisticsView: React.FC = () => {
|
||||
const [dewarQr, setDewarQr] = useState<string | null>(null);
|
||||
const [locationQr, setLocationQr] = useState<string | null>(null);
|
||||
const [transactionType, setTransactionType] = useState<string>("");
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
|
||||
const [sslError, setSslError] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [slotsData, setSlotsData] = useState<SlotData[]>([]);
|
||||
const [warningMessage, setWarningMessage] = useState<string | null>(null);
|
||||
const [selectedSlotData, setSelectedSlotData] = useState<SlotData | null>(null);
|
||||
const [retrievedDewar, setRetrievedDewar] = useState<string | null>(null);
|
||||
|
||||
const formatTimestamp = (timestamp: string | undefined) => {
|
||||
if (!timestamp) return 'N/A';
|
||||
const date = new Date(timestamp);
|
||||
return format(date, 'PPpp', { addSuffix: true });
|
||||
};
|
||||
|
||||
// Reference to the audio element
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
OpenAPI.BASE = 'https://localhost:8000';
|
||||
|
||||
const fetchDewarsAndSlots = async () => {
|
||||
try {
|
||||
console.log("Fetching dewars and slots...");
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setWarningMessage(null);
|
||||
|
||||
const [dewars, slots] = await Promise.all([
|
||||
LogisticsService.getAllDewarsLogisticsDewarsGet(),
|
||||
LogisticsService.getAllSlotsLogisticsSlotsGet(),
|
||||
]);
|
||||
|
||||
console.log("Dewars fetched:", dewars);
|
||||
console.log("Slots fetched:", slots);
|
||||
|
||||
const dewarMap: { [key: string]: Dewar } = {};
|
||||
const usedDewarUniqueIds = new Map<string, string>();
|
||||
|
||||
dewars.forEach((dewar) => {
|
||||
if (dewar.unique_id) {
|
||||
dewarMap[dewar.unique_id] = dewar;
|
||||
console.log(`Dewar ID: ${dewar.unique_id}`);
|
||||
}
|
||||
});
|
||||
|
||||
const newSlotsData = slots.map((slot) => {
|
||||
let associatedDewar: Dewar | undefined;
|
||||
|
||||
if (slot.dewar_unique_id) {
|
||||
if (usedDewarUniqueIds.has(slot.dewar_unique_id)) {
|
||||
const existingSlotId = usedDewarUniqueIds.get(slot.dewar_unique_id);
|
||||
console.warn(`Dewar with unique ID ${slot.dewar_unique_id} is already assigned to slot ${existingSlotId}`);
|
||||
setWarningMessage(`Dewar ${slot.dewar_unique_id} is already assigned to slot ${existingSlotId}`);
|
||||
return {
|
||||
...slot,
|
||||
occupied: false,
|
||||
dewar: null,
|
||||
time_until_refill: undefined,
|
||||
needsRefillWarning: true,
|
||||
beamlineLocation: undefined,
|
||||
shipmnet_name: undefined,
|
||||
contact_person: undefined,
|
||||
local_contact: undefined,
|
||||
};
|
||||
} else {
|
||||
associatedDewar = dewarMap[slot.dewar_unique_id];
|
||||
if (associatedDewar) {
|
||||
usedDewarUniqueIds.set(slot.dewar_unique_id, slot.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...slot,
|
||||
occupied: !!associatedDewar,
|
||||
dewar: associatedDewar || null,
|
||||
dewar_name: associatedDewar ? associatedDewar.dewar_name : undefined,
|
||||
needsRefillWarning: !associatedDewar || slot.time_until_refill === undefined,
|
||||
beamlineLocation: slot.beamlineLocation,
|
||||
shipment_name: slot.shipment_name,
|
||||
contact_person: slot.contact_person,
|
||||
local_contact: slot.local_contact,
|
||||
};
|
||||
});
|
||||
|
||||
setSlotsData(newSlotsData);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError('Failed to fetch data.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDewarsAndSlots();
|
||||
}, []);
|
||||
|
||||
const handleSlotSelection = async (data: { text: string }) => {
|
||||
const scannedText = data.text.trim();
|
||||
|
||||
if (beamlineQRCodes.includes(scannedText)) {
|
||||
if (dewarQr || selectedSlot) {
|
||||
const dewarId = dewarQr || slotsData.find(slot => slot.qr_code === selectedSlot)?.dewar?.unique_id;
|
||||
if (dewarId) {
|
||||
try {
|
||||
const timestamp = moment().toISOString();
|
||||
await LogisticsService.scanDewarLogisticsDewarScanPost({
|
||||
dewar_qr_code: dewarId,
|
||||
location_qr_code: scannedText,
|
||||
transaction_type: 'beamline',
|
||||
timestamp: timestamp,
|
||||
});
|
||||
|
||||
fetchDewarsAndSlots();
|
||||
setWarningMessage(`Dewar ${dewarId} assigned to beamline.`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError('Error updating dewar status at beamline.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fetchDewarAndAssociate(scannedText);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (outgoingQRCodes.includes(scannedText)) {
|
||||
setDewarQr(scannedText);
|
||||
handleOutgoing();
|
||||
return;
|
||||
}
|
||||
|
||||
if (slotQRCodes.includes(scannedText)) {
|
||||
const slot = slotsData.find(slot => slot.qr_code === scannedText);
|
||||
|
||||
setLocationQr(scannedText);
|
||||
setSelectedSlot(scannedText);
|
||||
|
||||
if (slot?.dewar?.unique_id) {
|
||||
console.log(`Returning dewar ${slot.dewar.unique_id} to slot ${scannedText}`);
|
||||
await returnDewarToStorage(slot.dewar.unique_id, scannedText);
|
||||
} else {
|
||||
fetchDewarAndAssociate(scannedText);
|
||||
}
|
||||
} else {
|
||||
fetchDewarAndAssociate(scannedText);
|
||||
}
|
||||
};
|
||||
|
||||
const returnDewarToStorage = async (dewarId: string, slotQrCode: string) => {
|
||||
const payload = {
|
||||
dewar_qr_code: dewarId,
|
||||
location_qr_code: slotQrCode,
|
||||
transaction_type: "returned",
|
||||
};
|
||||
|
||||
console.log("Sending payload:", payload);
|
||||
console.log(`Dewar ID: ${dewarId}, Slot QR Code: ${slotQrCode}`);
|
||||
|
||||
try {
|
||||
await LogisticsService.returnToStorageLogisticsDewarsReturnPost(payload);
|
||||
|
||||
fetchDewarsAndSlots();
|
||||
alert(`Dewar ${dewarId} successfully returned to storage.`);
|
||||
} catch (error) {
|
||||
console.error('Failed to return dewar to storage:', error);
|
||||
if (error.status === 400 && error.response?.data?.detail === "Selected slot is already occupied") {
|
||||
alert('Selected slot is already occupied. Please choose a different slot.');
|
||||
} else {
|
||||
console.error('Unexpected error occurred:', error);
|
||||
alert('Failed to return dewar to storage.');
|
||||
}
|
||||
|
||||
setError('Failed to return dewar to storage.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSlotSelect = (slot: SlotData) => {
|
||||
if (selectedSlot === slot.qr_code) {
|
||||
setSelectedSlot(null);
|
||||
setLocationQr(null);
|
||||
setSelectedSlotData(null);
|
||||
} else {
|
||||
setSelectedSlot(slot.qr_code);
|
||||
setLocationQr(slot.qr_code);
|
||||
setSelectedSlotData(slot);
|
||||
setRetrievedDewar(slot.dewar?.unique_id || null);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDewarAndAssociate = async (scannedText: string) => {
|
||||
try {
|
||||
const dewar = await LogisticsService.getDewarByUniqueIdLogisticsDewarUniqueIdGet(scannedText);
|
||||
setDewarQr(dewar.unique_id);
|
||||
if (audioRef.current) {
|
||||
audioRef.current.play();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e.message.includes('SSL')) {
|
||||
setSslError(true);
|
||||
} else {
|
||||
alert('No dewar found with this QR code.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefillDewar = async (qrCode?: string) => {
|
||||
const dewarUniqueId = qrCode || slotsData.find(slot => slot.qr_code === selectedSlot)?.dewar?.unique_id;
|
||||
if (!dewarUniqueId) {
|
||||
alert('No dewar associated with the selected slot.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const trimmedUniqueId = dewarUniqueId.trim();
|
||||
const response = await LogisticsService.refillDewarLogisticsDewarRefillPost(trimmedUniqueId);
|
||||
|
||||
if (response && response.time_until_refill) {
|
||||
alert(`Dewar refilled successfully. Time until next refill: ${response.time_until_refill}`);
|
||||
} else {
|
||||
alert('Dewar refilled successfully.');
|
||||
}
|
||||
|
||||
fetchDewarsAndSlots();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Error in refilling dewar');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!dewarQr || !locationQr || !transactionType) {
|
||||
alert('All fields are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dewarQr.trim()) {
|
||||
alert('Dewar QR code should not be empty.');
|
||||
return;
|
||||
}
|
||||
|
||||
const conflictingSlots = slotsData.filter(
|
||||
slot => slot.dewar?.unique_id === dewarQr && slot.qr_code !== locationQr
|
||||
);
|
||||
|
||||
if (conflictingSlots.length > 0) {
|
||||
alert(`Dewar ${dewarQr} is already assigned to slot ${conflictingSlots[0].qr_code}. Please resolve the conflict first.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const timestamp = moment().toISOString();
|
||||
await LogisticsService.scanDewarLogisticsDewarScanPost({
|
||||
dewar_qr_code: dewarQr.trim(),
|
||||
location_qr_code: locationQr.trim(),
|
||||
transaction_type: transactionType,
|
||||
timestamp: timestamp,
|
||||
});
|
||||
|
||||
alert('Dewar status updated successfully');
|
||||
if (audioRef.current) {
|
||||
audioRef.current.play();
|
||||
}
|
||||
|
||||
fetchDewarsAndSlots();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Error updating dewar status');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetrieve = async () => {
|
||||
if (!retrievedDewar) {
|
||||
alert('No dewar selected for retrieval.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await LogisticsService.retrieveDewarLogisticsDewarsRetrievePost({
|
||||
dewar_qr_code: retrievedDewar,
|
||||
location_qr_code: '',
|
||||
transaction_type: 'retrieved',
|
||||
});
|
||||
|
||||
alert(`Dewar ${retrievedDewar} retrieved successfully.`);
|
||||
setRetrievedDewar(null);
|
||||
fetchDewarsAndSlots();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Error retrieving dewar');
|
||||
}
|
||||
};
|
||||
|
||||
const handleOutgoing = async () => {
|
||||
if (!dewarQr) {
|
||||
alert('Scan a dewar QR code first.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await LogisticsService.scanDewarLogisticsDewarScanPost({
|
||||
dewar_qr_code: dewarQr,
|
||||
location_qr_code: dewarQr, // Using dewar QR code as location for outgoing
|
||||
transaction_type: 'outgoing',
|
||||
timestamp: moment().toISOString(),
|
||||
});
|
||||
|
||||
alert(`Dewar ${dewarQr} is now marked as outgoing.`);
|
||||
fetchDewarsAndSlots();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Error marking dewar as outgoing');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Logistics Tracking
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box display="flex" flexDirection="column" alignItems="flex-start">
|
||||
<Typography variant="h6">Dewar QR Code</Typography>
|
||||
<TextField
|
||||
label="Enter Dewar QR Code"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={dewarQr ?? ''}
|
||||
onChange={(e) => setDewarQr(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<IconButton onClick={() => setIsModalOpen(true)} sx={{ fontSize: 40, mb: 2 }}>
|
||||
<CameraAlt fontSize="inherit" />
|
||||
</IconButton>
|
||||
|
||||
<Typography variant="h6">Selected Slot</Typography>
|
||||
<TextField
|
||||
label="Slot QR Code"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={locationQr ?? ''}
|
||||
onChange={(e) => setLocationQr(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setTransactionType('incoming')}
|
||||
color={transactionType === 'incoming' ? 'primary' : 'default'}
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
Incoming
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setTransactionType('outgoing')}
|
||||
color={transactionType === 'outgoing' ? 'primary' : 'default'}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
Outgoing
|
||||
</Button>
|
||||
|
||||
<Button variant="contained" color="primary" onClick={handleSubmit} sx={{ mb: 2 }}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="contained" color="secondary" onClick={() => handleRefillDewar()}>
|
||||
Refill Dewar
|
||||
</Button>
|
||||
<Button variant="outlined" color="warning" onClick={handleRetrieve} sx={{ mb: 2 }}>
|
||||
Retrieved
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
{selectedSlotData ? (
|
||||
<Grid item xs={12} sm={6}>
|
||||
{selectedSlotData ? (
|
||||
<DetailPanel>
|
||||
<Typography variant="h6">{selectedSlotData.label}</Typography>
|
||||
<Typography variant="body2">{`Shipment: ${selectedSlotData.shipment_name}`}</Typography>
|
||||
<Typography variant="body2">{`Dewar: ${selectedSlotData.dewar_name || 'N/A'}`}</Typography>
|
||||
<Typography variant="body2">{`Contact Person: ${selectedSlotData.contact_person}`}</Typography>
|
||||
<Typography variant="body2">{`QR Code: ${selectedSlotData.qr_code}`}</Typography>
|
||||
<Typography variant="body2">{`Occupied: ${selectedSlotData.occupied ? 'Yes' : 'No'}`}</Typography>
|
||||
<Typography variant="body2">{`Needs Refill: ${selectedSlotData.needsRefillWarning ? 'Yes' : 'No'}`}</Typography>
|
||||
<Typography variant="body2">{`Time Until Refill: ${selectedSlotData.time_until_refill ?? 'N/A'}`}</Typography>
|
||||
<Typography variant="body2">{`Retrieved Timestamp: ${formatTimestamp(selectedSlotData.retrievedTimestamp)}`}</Typography>
|
||||
<Typography variant="body2">{`Local Contact: ${selectedSlotData.local_contact}`}</Typography>
|
||||
<Typography variant="body2">{`Beamline Location: ${selectedSlotData.beamlineLocation || 'N/A'}`}</Typography>
|
||||
</DetailPanel>
|
||||
) : (
|
||||
<Typography variant="h6">Select a slot to see more details.</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography>No slot selected</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{loading ? (
|
||||
<Typography mt={2}>Loading...</Typography>
|
||||
) : error ? (
|
||||
<Typography mt={2} color="error">{error}</Typography>
|
||||
) : (
|
||||
<Box>
|
||||
{['X06SA-storage', 'X10SA-storage', 'Novartis-Box'].map((storageKey) => {
|
||||
const filteredSlots = slotsData.filter((slot) => storageToSlotsMapping[storageKey].includes(slot.qr_code));
|
||||
return (
|
||||
<Storage
|
||||
key={storageKey}
|
||||
name={storageKey}
|
||||
selectedSlot={selectedSlot}
|
||||
slotsData={filteredSlots}
|
||||
onSelectSlot={handleSlotSelect}
|
||||
onRefillDewar={handleRefillDewar}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<ScannerModal open={isModalOpen} onClose={() => setIsModalOpen(false)} onScan={handleSlotSelection} />
|
||||
|
||||
<Snackbar open={sslError} autoHideDuration={6000} onClose={() => setSslError(false)}>
|
||||
<Alert onClose={() => setSslError(false)} severity="error">
|
||||
SSL Error: Unable to establish a secure connection with the server.
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
{warningMessage && (
|
||||
<Snackbar open autoHideDuration={6000} onClose={() => setWarningMessage(null)}>
|
||||
<Alert severity="warning" onClose={() => setWarningMessage(null)}>
|
||||
{warningMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Snackbar open autoHideDuration={6000} onClose={() => setError(null)}>
|
||||
<Alert severity="error" onClose={() => setError(null)}>
|
||||
{error}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
)}
|
||||
|
||||
<audio ref={audioRef} src="src/assets/50565__broumbroum__sf3-sfx-menu-validate.wav" preload="auto" />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogisticsView;
|
1
logistics/src/vite-env.d.ts
vendored
Normal file
1
logistics/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
Loading…
x
Reference in New Issue
Block a user