initial-commit

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

BIN
.DS_Store vendored Normal file

Binary file not shown.

24
frontend/.gitignore vendored Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

45
frontend/package.json Normal file
View 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"
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
frontend/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View 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"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View 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
View 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
View 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);
}
}

View 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
View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

View 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

View 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

View 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

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 516 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 516 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 95 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 95 KiB

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

14
frontend/src/index.css Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View 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;

View 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';

View 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;
}

View 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
View 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
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View 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
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json"},
{ "path": "./tsconfig.node.json"}
]
}

View 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
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})