initial-commit
24
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
50
frontend/README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import react from 'eslint-plugin-react'
|
||||
|
||||
export default tseslint.config({
|
||||
// Set the react version
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
// Add the react plugin
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended rules
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
},
|
||||
})
|
||||
```
|
28
frontend/eslint.config.js
Normal file
@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
4232
frontend/package-lock.json
generated
Normal file
45
frontend/package.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "mheidi-frontend-v2",
|
||||
"private": true,
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aldabil/react-scheduler": "^2.9.5",
|
||||
"@bitnoi.se/react-scheduler": "^0.3.1",
|
||||
"@devexpress/dx-react-core": "^4.0.9",
|
||||
"@devexpress/dx-react-scheduler": "^4.0.9",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fullcalendar/daygrid": "^6.1.15",
|
||||
"@fullcalendar/interaction": "^6.1.15",
|
||||
"@fullcalendar/react": "^6.1.15",
|
||||
"@fullcalendar/timegrid": "^6.1.15",
|
||||
"@mui/icons-material": "^6.1.5",
|
||||
"@mui/material": "^6.1.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.15.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-qr-code": "^2.0.15",
|
||||
"react-scheduler": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.11.1",
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.12",
|
||||
"globals": "^15.9.0",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.7.0",
|
||||
"vite": "^5.4.8"
|
||||
}
|
||||
}
|
322
frontend/public/beamtimedb.json
Normal file
@ -0,0 +1,322 @@
|
||||
{
|
||||
"beamtimes": [
|
||||
{
|
||||
"date": "2024-10-01",
|
||||
"shifts": [
|
||||
{
|
||||
"beamtime_shift": "morning",
|
||||
"beamline": "PXI",
|
||||
"local_contact": "Dr. Alice Johnson",
|
||||
"experimental_mode": "SDU-scheduled",
|
||||
"pucks": [
|
||||
{
|
||||
"puck_id": "PSIMX-001",
|
||||
"shipment_id": "SH001",
|
||||
"dewar_id": "DW001"
|
||||
},
|
||||
{
|
||||
"puck_id": "PSIMX-002",
|
||||
"shipment_id": "SH001",
|
||||
"dewar_id": "DW001"
|
||||
},
|
||||
{
|
||||
"puck_id": "CPS-027",
|
||||
"shipment_id": "SH002",
|
||||
"dewar_id": "DW002"
|
||||
},
|
||||
{
|
||||
"puck_id": "CPS-041",
|
||||
"shipment_id": "SH002",
|
||||
"dewar_id": "DW002"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"beamtime_shift": "evening",
|
||||
"beamline": "PXII",
|
||||
"local_contact": "Dr. Bob Smith",
|
||||
"experimental_mode": "SDU-scheduled",
|
||||
"pucks": [
|
||||
{
|
||||
"puck_id": "XQT-032",
|
||||
"shipment_id": "SH003",
|
||||
"dewar_id": "DW003"
|
||||
},
|
||||
{
|
||||
"puck_id": "XQT-033",
|
||||
"shipment_id": "SH003",
|
||||
"dewar_id": "DW003"
|
||||
},
|
||||
{
|
||||
"puck_id": "XQT-034",
|
||||
"shipment_id": "SH003",
|
||||
"dewar_id": "DW003"
|
||||
},
|
||||
{
|
||||
"puck_id": "XQT-035",
|
||||
"shipment_id": "SH003",
|
||||
"dewar_id": "DW003"
|
||||
},
|
||||
{
|
||||
"puck_id": "TLS-024",
|
||||
"shipment_id": "SH004",
|
||||
"dewar_id": "DW004"
|
||||
},
|
||||
{
|
||||
"puck_id": "TLS-025",
|
||||
"shipment_id": "SH004",
|
||||
"dewar_id": "DW004"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"beamtime_shift": "night",
|
||||
"beamline": "PXII",
|
||||
"local_contact": "Dr. Bob Smith",
|
||||
"experimental_mode": "SDU-scheduled",
|
||||
"pucks": [
|
||||
{
|
||||
"puck_id": "TLS-026",
|
||||
"shipment_id": "SH005",
|
||||
"dewar_id": "DW005"
|
||||
},
|
||||
{
|
||||
"puck_id": "TLS-027",
|
||||
"shipment_id": "SH005",
|
||||
"dewar_id": "DW005"
|
||||
},
|
||||
{
|
||||
"puck_id": "XQT-036",
|
||||
"shipment_id": "SH005",
|
||||
"dewar_id": "DW005"
|
||||
},
|
||||
{
|
||||
"puck_id": "XQT-037",
|
||||
"shipment_id": "SH005",
|
||||
"dewar_id": "DW005"
|
||||
},
|
||||
{
|
||||
"puck_id": "XQT-038",
|
||||
"shipment_id": "SH005",
|
||||
"dewar_id": "DW005"
|
||||
},
|
||||
{
|
||||
"puck_id": "XQT-039",
|
||||
"shipment_id": "SH005",
|
||||
"dewar_id": "DW005"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2024-10-17",
|
||||
"shifts": [
|
||||
{
|
||||
"beamtime_shift": "morning",
|
||||
"beamline": "PXI",
|
||||
"local_contact": "Dr. Charlie Brown",
|
||||
"experimental_mode": "Remote",
|
||||
"pucks": [
|
||||
{
|
||||
"puck_id": "CTB-001",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-002",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-003",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-004",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-005",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-006",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-007",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-008",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-009",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-010",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-011",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-012",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-013",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
},
|
||||
{
|
||||
"puck_id": "CTB-014",
|
||||
"shipment_id": "SH006",
|
||||
"dewar_id": "DW006"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"beamtime_shift": "morning",
|
||||
"beamline": "PXII",
|
||||
"local_contact": "Dr. Dana White",
|
||||
"experimental_mode": "SDU-Scheduled",
|
||||
"pucks": [
|
||||
{
|
||||
"puck_id": "PSIMX-005",
|
||||
"shipment_id": "SH007",
|
||||
"dewar_id": "DW007"
|
||||
},
|
||||
{
|
||||
"puck_id": "PSIMX-006",
|
||||
"shipment_id": "SH007",
|
||||
"dewar_id": "DW007"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"beamtime_shift": "night",
|
||||
"beamline": "PXIII",
|
||||
"local_contact": "Dr. Dana White",
|
||||
"experimental_mode": "",
|
||||
"pucks": []
|
||||
},
|
||||
{
|
||||
"beamtime_shift": "evening",
|
||||
"beamline": "PXIII",
|
||||
"local_contact": "Dr. Dana White",
|
||||
"experimental_mode": "Remote",
|
||||
"pucks": [
|
||||
{
|
||||
"puck_id": "PSIMX-005",
|
||||
"shipment_id": "SH007",
|
||||
"dewar_id": "DW007"
|
||||
},
|
||||
{
|
||||
"puck_id": "PSIMX-006",
|
||||
"shipment_id": "SH007",
|
||||
"dewar_id": "DW007"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"beamtime_shift": "night",
|
||||
"beamline": "PXII",
|
||||
"local_contact": "Dr. Dana White",
|
||||
"experimental_mode": "",
|
||||
"pucks": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2024-10-13",
|
||||
"shifts": [
|
||||
{
|
||||
"beamtime_shift": "morning",
|
||||
"beamline": "PXI",
|
||||
"local_contact": "Dr. Evan Green",
|
||||
"experimental_mode": "In person",
|
||||
"pucks": [
|
||||
{
|
||||
"puck_id": "PSIMX-007",
|
||||
"shipment_id": "SH008",
|
||||
"dewar_id": "DW008"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"beamtime_shift": "evening",
|
||||
"beamline": "PXI",
|
||||
"local_contact": "Dr. Evan Green",
|
||||
"experimental_mode": "In person",
|
||||
"pucks": []
|
||||
},
|
||||
{
|
||||
"beamtime_shift": "night",
|
||||
"beamline": "PXI",
|
||||
"local_contact": "Dr. Fiona Blue",
|
||||
"experimental_mode": "In person",
|
||||
"pucks": [
|
||||
{
|
||||
"puck_id": "PSIMX-008",
|
||||
"shipment_id": "SH009",
|
||||
"dewar_id": "DW009"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2024-10-04",
|
||||
"shifts": [
|
||||
{
|
||||
"beamtime_shift": "",
|
||||
"beamline": "",
|
||||
"local_contact": "Dr. Heidi Brown",
|
||||
"experimental_mode": "SDU-queued",
|
||||
"pucks": [
|
||||
{
|
||||
"puck_id": "PSIMX-009",
|
||||
"shipment_id": "SH010",
|
||||
"dewar_id": "DW010"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"beamtime_shift": "evening",
|
||||
"beamline": "PXII",
|
||||
"local_contact": "Dr. Ian White",
|
||||
"experimental_mode": "SDU-Scheduled",
|
||||
"pucks": [
|
||||
{
|
||||
"puck_id": "PSIMX-010",
|
||||
"shipment_id": "SH011",
|
||||
"dewar_id": "DW011"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"beamtime_shift": "night",
|
||||
"beamline": "PXI",
|
||||
"local_contact": "Dr. Jack Black",
|
||||
"experimental_mode": "SDU-Scheduled",
|
||||
"pucks": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
BIN
frontend/public/favicon.ico
Normal file
After Width: | Height: | Size: 3.8 KiB |
43
frontend/public/index.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
BIN
frontend/public/logo192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/public/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
frontend/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
frontend/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
250
frontend/public/shipmentsdb.json
Normal file
@ -0,0 +1,250 @@
|
||||
{
|
||||
"shipments": [
|
||||
{
|
||||
"shipment_id": "SHP001",
|
||||
"shipment_name": "BioLab_Shipment_001",
|
||||
"number_of_dewars": 2,
|
||||
"shipment_status": "In Transit",
|
||||
"shipment_date": "2024-01-15",
|
||||
"contact_person": [
|
||||
{ "name": "Alice Johnson", "id": "alice" }
|
||||
],
|
||||
"dewars": [
|
||||
{
|
||||
"id": "SHP001-Dewar-1",
|
||||
"dewar_name": "Dewar_001",
|
||||
"tracking_number": "TRACK123456",
|
||||
"number_of_pucks": 4,
|
||||
"number_of_samples": 24,
|
||||
"return_address": [
|
||||
{ "address": "123 Main St, Anytown, USA", "id": "address1" }
|
||||
],
|
||||
"contact_person": [
|
||||
{ "name": "Alice Johnson", "id": "alice" }
|
||||
],
|
||||
"status": "in preparation",
|
||||
"ready_date": "",
|
||||
"shipping_date": "",
|
||||
"arrival_date": "",
|
||||
"shippingStatus": "not shipped",
|
||||
"arrivalStatus": "not arrived",
|
||||
"returned": "",
|
||||
"qrcode": "https://example.com/qrcode/dewar_001"
|
||||
},
|
||||
{
|
||||
"id": "SHP001-Dewar-2",
|
||||
"dewar_name": "Dewar_002",
|
||||
"tracking_number": "TRACK654321",
|
||||
"number_of_pucks": 3,
|
||||
"number_of_samples": 18,
|
||||
"return_address": [
|
||||
{ "address": "123 Main St, Anytown, USA", "id": "address1" }
|
||||
],
|
||||
"contact_person": [
|
||||
{ "name": "Alice Johnson", "id": "alice" }
|
||||
],
|
||||
"status": "in preparation",
|
||||
"ready_date": "",
|
||||
"shipping_date": "",
|
||||
"arrival_date": "",
|
||||
"shippingStatus": "not shipped",
|
||||
"arrivalStatus": "not arrived",
|
||||
"returned": "",
|
||||
"qrcode": "https://example.com/qrcode/dewar_002"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"shipment_id": "SHP002",
|
||||
"shipment_name": "BioLab_Shipment_002",
|
||||
"number_of_dewars": 3,
|
||||
"shipment_status": "In Transit",
|
||||
"shipment_date": "2024-02-20",
|
||||
"contact_person": [
|
||||
{ "name": "Bob Smith", "id": "bob" }
|
||||
],
|
||||
"dewars": [
|
||||
{
|
||||
"id": "SHP002-Dewar-3",
|
||||
"dewar_name": "Dewar_003",
|
||||
"tracking_number": "TRACK987654",
|
||||
"number_of_pucks": 5,
|
||||
"number_of_samples": 30,
|
||||
"contact_person": [
|
||||
{ "name": "Bob Smith", "id": "bob" }
|
||||
],
|
||||
"return_address": [
|
||||
{ "address": "123 Main St, Anytown, USA", "id": "address1" }
|
||||
],
|
||||
"status": "ready for shipping",
|
||||
"ready_date": "2024-02-20",
|
||||
"shipping_date": "",
|
||||
"arrival_date": "",
|
||||
"shippingStatus": "shipped",
|
||||
"arrivalStatus": "arrived",
|
||||
"returned": "",
|
||||
"qrcode": ""
|
||||
},
|
||||
{
|
||||
"id": "SHP002-Dewar-4",
|
||||
"dewar_name": "Dewar_004",
|
||||
"tracking_number": "TRACK876543",
|
||||
"number_of_pucks": 6,
|
||||
"number_of_samples": 36,
|
||||
"contact_person": [
|
||||
{ "name": "Bob Smith", "id": "bob" }
|
||||
],
|
||||
"return_address": [
|
||||
{ "address": "123 Main St, Anytown, USA", "id": "address1" }
|
||||
],
|
||||
"status": "ready for shipping",
|
||||
"ready_date": "2024-02-20",
|
||||
"shipping_date": "2024-02-21",
|
||||
"arrival_date": "",
|
||||
"shippingStatus": "shipped",
|
||||
"arrivalStatus": "arrived",
|
||||
"returned": "",
|
||||
"qrcode": "https://example.com/qrcode/dewar_004"
|
||||
},
|
||||
{
|
||||
"id": "SHP002-Dewar-5",
|
||||
"dewar_name": "Dewar_005",
|
||||
"tracking_number": "TRACK765432",
|
||||
"number_of_pucks": 4,
|
||||
"number_of_samples": 24,
|
||||
"contact_person": [
|
||||
{ "name": "Bob Smith", "id": "bob" }
|
||||
],
|
||||
"return_address": [
|
||||
{ "address": "123 Main St, Anytown, USA", "id": "address1" }
|
||||
],
|
||||
"status": "ready for shipping",
|
||||
"ready_date": "2024-02-21",
|
||||
"shipping_date": "2024-02-22",
|
||||
"arrival_date": "",
|
||||
"shippingStatus": "shipped",
|
||||
"arrivalStatus": "not arrived",
|
||||
"returned": "",
|
||||
"qrcode": "https://example.com/qrcode/dewar_005"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"shipment_id": "SHP003",
|
||||
"shipment_name": "BioLab_Shipment_003",
|
||||
"number_of_dewars": 5,
|
||||
"shipment_status": "Pending",
|
||||
"shipment_date": "2024-03-10",
|
||||
"contact_person": [
|
||||
{ "name": "Charlie Brown", "id": "charlie" }
|
||||
],
|
||||
"dewars": [
|
||||
{
|
||||
"id": "SHP003-Dewar-6",
|
||||
"dewar_name": "Dewar_006",
|
||||
"tracking_number": "TRACK112233",
|
||||
"number_of_pucks": 7,
|
||||
"number_of_samples": 42,
|
||||
"contact_person": [
|
||||
{ "name": "Charlie Brown", "id": "charlie" }
|
||||
],
|
||||
"return_address": [
|
||||
{ "address": "789 Maple St, Thistown, USA", "id": "address3" }
|
||||
],
|
||||
"status": "in preparation",
|
||||
"ready_date": "",
|
||||
"shipping_date": "",
|
||||
"arrival_date": "",
|
||||
"shippingStatus": "not shipped",
|
||||
"arrivalStatus": "not arrived",
|
||||
"returned": "",
|
||||
"qrcode": ""
|
||||
},
|
||||
{
|
||||
"id": "SHP003-Dewar-7",
|
||||
"dewar_name": "Dewar_007",
|
||||
"tracking_number": "TRACK223344",
|
||||
"number_of_pucks": 5,
|
||||
"number_of_samples": 30,
|
||||
"contact_person": [
|
||||
{ "name": "Charlie Brown", "id": "charlie" }
|
||||
],
|
||||
"return_address": [
|
||||
{ "address": "789 Maple St, Thistown, USA", "id": "address3" }
|
||||
],
|
||||
"status": "ready for shipping",
|
||||
"ready_date": "2024-03-12",
|
||||
"shipping_date": "",
|
||||
"arrival_date": "",
|
||||
"shippingStatus": "not shipped",
|
||||
"arrivalStatus": "not arrived",
|
||||
"returned": "",
|
||||
"qrcode": "https://example.com/qrcode/dewar_007"
|
||||
},
|
||||
{
|
||||
"id": "SHP003-Dewar-8",
|
||||
"dewar_name": "Dewar_008",
|
||||
"tracking_number": "TRACK334455",
|
||||
"number_of_pucks": 8,
|
||||
"number_of_samples": 48,
|
||||
"contact_person": [
|
||||
{ "name": "Charlie Brown", "id": "charlie" }
|
||||
],
|
||||
"return_address": [
|
||||
{ "address": "789 Maple St, Thistown, USA", "id": "address3" }
|
||||
],
|
||||
"status": "in preparation",
|
||||
"ready_date": "",
|
||||
"shipping_date": "",
|
||||
"arrival_date": "",
|
||||
"shippingStatus": "not shipped",
|
||||
"arrivalStatus": "not arrived",
|
||||
"returned": "",
|
||||
"qrcode": ""
|
||||
},
|
||||
{
|
||||
"id": "SHP003-Dewar-9",
|
||||
"dewar_name": "Dewar_009",
|
||||
"tracking_number": "TRACK445566",
|
||||
"number_of_pucks": 6,
|
||||
"number_of_samples": 36,
|
||||
"contact_person": [
|
||||
{ "name": "Charlie Brown", "id": "charlie" }
|
||||
],
|
||||
"return_address": [
|
||||
{ "address": "789 Maple St, Thistown, USA", "id": "address3" }
|
||||
],
|
||||
"status": "ready for shipping",
|
||||
"ready_date": "2024-03-12",
|
||||
"shipping_date": "2024-03-13",
|
||||
"arrival_date": "2024-03-17",
|
||||
"shippingStatus": "shipped",
|
||||
"arrivalStatus": "arrived",
|
||||
"returned": "returned",
|
||||
"qrcode": "https://example.com/qrcode/dewar_009"
|
||||
},
|
||||
{
|
||||
"id": "SHP003-Dewar-10",
|
||||
"dewar_name": "Dewar_010",
|
||||
"tracking_number": "TRACK556677",
|
||||
"number_of_pucks": 4,
|
||||
"number_of_samples": 24,
|
||||
"contact_person": [
|
||||
{ "name": "Charlie Brown", "id": "charlie" }
|
||||
],
|
||||
"return_address": [
|
||||
{ "address": "456 Elm St, Othertown, USA", "id": "address2" }
|
||||
],
|
||||
"status": "ready for shipping",
|
||||
"ready_date": "2024-03-12",
|
||||
"shipping_date": "2024-03-13",
|
||||
"arrival_date": "2024-03-17",
|
||||
"shippingStatus": "shipped",
|
||||
"arrivalStatus": "arrived",
|
||||
"returned": "returned",
|
||||
"qrcode": "https://example.com/qrcode/dewar_010"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
1
frontend/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 |
38
frontend/src/App.css
Normal file
@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #2F4858;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
9
frontend/src/App.test.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App.tsx';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
12
frontend/src/App.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import ResponsiveAppBar from './components/ResponsiveAppBar.tsx';
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<ResponsiveAppBar />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
BIN
frontend/src/assets/Heidi-logo.png
Normal file
After Width: | Height: | Size: 579 KiB |
BIN
frontend/src/assets/avatar.jpg
Normal file
After Width: | Height: | Size: 778 KiB |
5
frontend/src/assets/icons/bottle-svgrepo-com-green.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version='1.0' encoding='iso-8859-1'?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#008000" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 276.777 276.777" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 276.777 276.777">
|
||||
<path d="M190.886,82.273c-3.23-2.586-7.525-7.643-7.525-21.639V43h8.027V0h-106v43h8.027v17.635c0,11.66-1.891,17.93-6.524,21.639 c-21.813,17.459-31.121,36.748-31.121,64.5v100.088c0,16.496,13.42,29.916,29.916,29.916h105.405 c16.496,0,29.916-13.42,29.916-29.916V146.773C221.007,121.103,210.029,97.594,190.886,82.273z M191.007,246.777H85.77V146.773 c0-18.589,5.199-29.339,19.867-41.078c15.758-12.612,17.778-30.706,17.778-45.061V43h29.945v17.635 c0,19.927,6.318,35.087,18.779,45.061c11.99,9.597,18.867,24.568,18.867,41.078V246.777z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 882 B |
5
frontend/src/assets/icons/bottle-svgrepo-com-grey.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version='1.0' encoding='iso-8859-1'?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#808080" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 276.777 276.777" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 276.777 276.777">
|
||||
<path d="M190.886,82.273c-3.23-2.586-7.525-7.643-7.525-21.639V43h8.027V0h-106v43h8.027v17.635c0,11.66-1.891,17.93-6.524,21.639 c-21.813,17.459-31.121,36.748-31.121,64.5v100.088c0,16.496,13.42,29.916,29.916,29.916h105.405 c16.496,0,29.916-13.42,29.916-29.916V146.773C221.007,121.103,210.029,97.594,190.886,82.273z M191.007,246.777H85.77V146.773 c0-18.589,5.199-29.339,19.867-41.078c15.758-12.612,17.778-30.706,17.778-45.061V43h29.945v17.635 c0,19.927,6.318,35.087,18.779,45.061c11.99,9.597,18.867,24.568,18.867,41.078V246.777z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 882 B |
5
frontend/src/assets/icons/bottle-svgrepo-com-red.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version='1.0' encoding='iso-8859-1'?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#FF0000" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 276.777 276.777" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 276.777 276.777">
|
||||
<path d="M190.886,82.273c-3.23-2.586-7.525-7.643-7.525-21.639V43h8.027V0h-106v43h8.027v17.635c0,11.66-1.891,17.93-6.524,21.639 c-21.813,17.459-31.121,36.748-31.121,64.5v100.088c0,16.496,13.42,29.916,29.916,29.916h105.405 c16.496,0,29.916-13.42,29.916-29.916V146.773C221.007,121.103,210.029,97.594,190.886,82.273z M191.007,246.777H85.77V146.773 c0-18.589,5.199-29.339,19.867-41.078c15.758-12.612,17.778-30.706,17.778-45.061V43h29.945v17.635 c0,19.927,6.318,35.087,18.779,45.061c11.99,9.597,18.867,24.568,18.867,41.078V246.777z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 882 B |
5
frontend/src/assets/icons/bottle-svgrepo-com-yellow.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version='1.0' encoding='iso-8859-1'?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#FFA500" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 276.777 276.777" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 276.777 276.777">
|
||||
<path d="M190.886,82.273c-3.23-2.586-7.525-7.643-7.525-21.639V43h8.027V0h-106v43h8.027v17.635c0,11.66-1.891,17.93-6.524,21.639 c-21.813,17.459-31.121,36.748-31.121,64.5v100.088c0,16.496,13.42,29.916,29.916,29.916h105.405 c16.496,0,29.916-13.42,29.916-29.916V146.773C221.007,121.103,210.029,97.594,190.886,82.273z M191.007,246.777H85.77V146.773 c0-18.589,5.199-29.339,19.867-41.078c15.758-12.612,17.778-30.706,17.778-45.061V43h29.945v17.635 c0,19.927,6.318,35.087,18.779,45.061c11.99,9.597,18.867,24.568,18.867,41.078V246.777z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 882 B |
2085
frontend/src/assets/icons/psi_01_ln.svg
Normal file
After Width: | Height: | Size: 516 KiB |
2084
frontend/src/assets/icons/psi_01_lp.svg
Normal file
After Width: | Height: | Size: 516 KiB |
636
frontend/src/assets/icons/psi_01_sn.svg
Normal file
After Width: | Height: | Size: 95 KiB |
635
frontend/src/assets/icons/psi_01_sp.svg
Normal file
After Width: | Height: | Size: 95 KiB |
1
frontend/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 |
321
frontend/src/components/Calendar.tsx
Normal 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;
|
172
frontend/src/components/DewarDetails.tsx
Normal 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;
|
54
frontend/src/components/ParentComponent.tsx
Normal 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;
|
9
frontend/src/components/PlanningView.tsx
Normal 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;
|
206
frontend/src/components/ResponsiveAppBar.tsx
Normal 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;
|
308
frontend/src/components/ShipmentDetails.tsx
Normal 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;
|
232
frontend/src/components/ShipmentForm.tsx
Normal 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;
|
206
frontend/src/components/ShipmentPanel.tsx
Normal 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;
|
100
frontend/src/components/ShipmentView.tsx
Normal 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;
|
152
frontend/src/components/UploadDialog.tsx
Normal 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;
|
14
frontend/src/index.css
Normal file
@ -0,0 +1,14 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
20
frontend/src/index.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App.tsx';
|
||||
import reportWebVitals from './reportWebVitals.ts';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
1
frontend/src/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
11
frontend/src/main.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
// src/main.tsx
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css'; // Optional: If you have global styles
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
1
frontend/src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
15
frontend/src/reportWebVitals.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
5
frontend/src/setupTests.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
46
frontend/src/styles/Calendar.css
Normal file
@ -0,0 +1,46 @@
|
||||
.calendar-container {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Styling each day cell */
|
||||
.fc-daygrid-day-frame {
|
||||
position: relative; /* Ensure positioning for child elements */
|
||||
border: 1px solid #e0e0e0; /* Grid cell border for better visibility */
|
||||
}
|
||||
|
||||
/* Event styling */
|
||||
.fc-event {
|
||||
border-radius: 3px; /* Rounded corners for events */
|
||||
padding: 4px; /* Padding for events */
|
||||
font-size: 12px; /* Font size for event text */
|
||||
cursor: pointer; /* Pointer cursor for events */
|
||||
box-sizing: border-box; /* Include padding in the width/height */
|
||||
}
|
||||
|
||||
/* Selected event styling */
|
||||
.fc-event-selected {
|
||||
border: 2px solid black; /* Border for selected events */
|
||||
}
|
||||
|
||||
/* Optional: Add hover effect for events */
|
||||
.fc-event:hover {
|
||||
background-color: #FF7043; /* Change color on hover */
|
||||
}
|
||||
|
||||
.event-details {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.event-details h3 {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.event-details label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
14
frontend/src/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
79
frontend/src/types.ts
Normal file
@ -0,0 +1,79 @@
|
||||
export interface ContactPerson {
|
||||
id: string; // ID is a string
|
||||
name: string; // Name of the contact person
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
id: string; // ID of the address
|
||||
address: string; // Full address
|
||||
}
|
||||
|
||||
export interface ReturnAddress {
|
||||
id: string; // ID of the return address
|
||||
address: string; // Full address
|
||||
// Add other fields as necessary
|
||||
}
|
||||
|
||||
export interface Proposal {
|
||||
id: number; // ID of the proposal
|
||||
number: string; // Proposal number
|
||||
}
|
||||
|
||||
export interface NewShipment {
|
||||
shipment_name: string;
|
||||
contact_person: string; // Could also be a ContactPerson type if you want to relate it
|
||||
proposal_number: string;
|
||||
return_address: string; // Could be changed to Address
|
||||
comments: string;
|
||||
}
|
||||
|
||||
export interface Dewar {
|
||||
id: string; // Add this line
|
||||
dewar_name: string;
|
||||
tracking_number: string;
|
||||
number_of_pucks: number;
|
||||
number_of_samples: number;
|
||||
return_address: ReturnAddress[];
|
||||
contact_person: ContactPerson[];
|
||||
status: string;
|
||||
ready_date?: string; // Make sure this is included
|
||||
shipping_date?: string; // Make sure this is included
|
||||
arrival_date?: string; // Make sure this is included
|
||||
shippingStatus: string;
|
||||
arrivalStatus: string;
|
||||
qrcode: string;
|
||||
}
|
||||
|
||||
export interface Shipment {
|
||||
shipment_id: string;
|
||||
shipment_date: string;
|
||||
shipment_name: string;
|
||||
number_of_dewars: number;
|
||||
shipment_status: string;
|
||||
contact_person: ContactPerson[] | null; // Change to an array to accommodate multiple contacts
|
||||
proposal_number?: string;
|
||||
return_address: Address[]; // Change to an array of Address
|
||||
comments?: string;
|
||||
dewars: Dewar[];
|
||||
}
|
||||
|
||||
import { DateLocalizer, Event as BigCalendarEvent } from 'react-big-calendar'; // Adjust according to your import structure
|
||||
|
||||
export interface CustomEvent extends BigCalendarEvent {
|
||||
id: number; // Custom property
|
||||
title: string; // Ensure title is included
|
||||
start: Date;
|
||||
end: Date;
|
||||
beamline?: string; // Optional property for beamline
|
||||
}
|
||||
|
||||
export interface Shift {
|
||||
beamline: string;
|
||||
local_contact: string;
|
||||
beamtime_shift: 'morning' | 'evening' | 'night';
|
||||
}
|
||||
|
||||
export interface Beamtime {
|
||||
date: string;
|
||||
shifts: Shift[];
|
||||
}
|
1
frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
26
frontend/tsconfig.app.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
7
frontend/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json"},
|
||||
{ "path": "./tsconfig.node.json"}
|
||||
]
|
||||
}
|
24
frontend/tsconfig.node.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
7
frontend/vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|