diff --git a/backend/app/models.py b/backend/app/models.py
index 2a210cb..114cc13 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -11,7 +11,7 @@ class Shipment(Base):
shipment_name = Column(String, index=True)
shipment_date = Column(Date)
shipment_status = Column(String)
- comments = Column(String, nullable=True)
+ comments = Column(String(200), nullable=True)
contact_person_id = Column(Integer, ForeignKey("contact_persons.id"))
return_address_id = Column(Integer, ForeignKey("addresses.id"))
proposal_id = Column(Integer, ForeignKey('proposals.id'), nullable=True)
diff --git a/backend/app/routers/address.py b/backend/app/routers/address.py
index c765d0c..c20e40c 100644
--- a/backend/app/routers/address.py
+++ b/backend/app/routers/address.py
@@ -1,7 +1,7 @@
from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session
from typing import List
-from app.schemas import Address as AddressSchema, AddressCreate
+from app.schemas import Address as AddressSchema, AddressCreate, AddressUpdate
from app.models import Address as AddressModel
from app.dependencies import get_db
@@ -29,4 +29,30 @@ async def create_return_address(address: AddressCreate, db: Session = Depends(ge
db.add(db_address)
db.commit()
db.refresh(db_address)
- return db_address
\ No newline at end of file
+ return db_address
+
+@router.put("/{address_id}", response_model=AddressSchema)
+async def update_return_address(address_id: int, address: AddressUpdate, db: Session = Depends(get_db)):
+ db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first()
+ if not db_address:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Address not found."
+ )
+ for key, value in address.dict(exclude_unset=True).items():
+ setattr(db_address, key, value)
+ db.commit()
+ db.refresh(db_address)
+ return db_address
+
+@router.delete("/{address_id}", status_code=status.HTTP_204_NO_CONTENT)
+async def delete_return_address(address_id: int, db: Session = Depends(get_db)):
+ db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first()
+ if not db_address:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Address not found."
+ )
+ db.delete(db_address)
+ db.commit()
+ return
\ No newline at end of file
diff --git a/backend/app/routers/contact.py b/backend/app/routers/contact.py
index 1296c99..f5cad9f 100644
--- a/backend/app/routers/contact.py
+++ b/backend/app/routers/contact.py
@@ -1,12 +1,13 @@
from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session
from typing import List
-from app.schemas import ContactPerson, ContactPersonCreate
+from app.schemas import ContactPerson, ContactPersonCreate, ContactPersonUpdate
from app.models import ContactPerson as ContactPersonModel
from app.dependencies import get_db
router = APIRouter()
+# Existing routes
@router.get("/", response_model=List[ContactPerson])
async def get_contacts(db: Session = Depends(get_db)):
return db.query(ContactPersonModel).all()
@@ -28,4 +29,31 @@ async def create_contact(contact: ContactPersonCreate, db: Session = Depends(get
db.add(db_contact)
db.commit()
db.refresh(db_contact)
- return db_contact
\ No newline at end of file
+ return db_contact
+
+# New routes
+@router.put("/{contact_id}", response_model=ContactPerson)
+async def update_contact(contact_id: int, contact: ContactPersonUpdate, db: Session = Depends(get_db)):
+ db_contact = db.query(ContactPersonModel).filter(ContactPersonModel.id == contact_id).first()
+ if not db_contact:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Contact not found."
+ )
+ for key, value in contact.dict(exclude_unset=True).items():
+ setattr(db_contact, key, value)
+ db.commit()
+ db.refresh(db_contact)
+ return db_contact
+
+@router.delete("/{contact_id}", status_code=status.HTTP_204_NO_CONTENT)
+async def delete_contact(contact_id: int, db: Session = Depends(get_db)):
+ db_contact = db.query(ContactPersonModel).filter(ContactPersonModel.id == contact_id).first()
+ if not db_contact:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Contact not found."
+ )
+ db.delete(db_contact)
+ db.commit()
+ return
\ No newline at end of file
diff --git a/backend/app/schemas.py b/backend/app/schemas.py
index 3b27393..9d0170d 100644
--- a/backend/app/schemas.py
+++ b/backend/app/schemas.py
@@ -21,6 +21,11 @@ class ContactPerson(ContactPersonBase):
class Config:
from_attributes = True
+class ContactPersonUpdate(BaseModel):
+ firstname: str | None = None
+ lastname: str | None = None
+ phone_number: str | None = None
+ email: EmailStr | None = None
# Address schemas
class AddressCreate(BaseModel):
@@ -36,6 +41,11 @@ class Address(AddressCreate):
class Config:
from_attributes = True
+class AddressUpdate(BaseModel):
+ street: str | None = None
+ city: str | None = None
+ zipcode: str | None = None
+ country: str | None = None
# Sample schemas
class Sample(BaseModel):
diff --git a/backend/test.db b/backend/test.db
index 8cb10b9..c44651d 100644
Binary files a/backend/test.db and b/backend/test.db differ
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 63f1a72..c98898b 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -20,6 +20,7 @@
"@fullcalendar/timegrid": "^6.1.15",
"@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.5",
+ "axios": "^1.7.7",
"dayjs": "^1.11.13",
"openapi-typescript-codegen": "^0.29.0",
"react": "^18.3.1",
@@ -2611,6 +2612,23 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
+ "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@@ -2801,6 +2819,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -2914,6 +2944,15 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -3389,6 +3428,40 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+ "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
@@ -3876,6 +3949,27 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -4277,6 +4371,12 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index dc013c1..54c73d2 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,7 +9,6 @@
"lint": "node_modules/.bin/eslint .",
"preview": "node_modules/.bin/vite preview",
"fetch:types": "node fetch-openapi.js"
-
},
"dependencies": {
"@aldabil/react-scheduler": "^2.9.5",
@@ -24,6 +23,7 @@
"@fullcalendar/timegrid": "^6.1.15",
"@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.5",
+ "axios": "^1.7.7",
"dayjs": "^1.11.13",
"openapi-typescript-codegen": "^0.29.0",
"react": "^18.3.1",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index d3a9afc..9ca043e 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -5,6 +5,8 @@ import ShipmentView from './pages/ShipmentView';
import HomePage from './pages/HomeView'; // Assuming this is a default export
import ResultsView from './pages/ResultsView';
import PlanningView from './pages/PlanningView';
+import ContactsManager from './pages/ContactsManagerView';
+import AddressManager from './pages/AddressManagerView';
const App: React.FC = () => {
return (
@@ -15,9 +17,11 @@ const App: React.FC = () => {
} />
} />
} />
+ } />
+ } />
);
};
-export default App;
+export default App;
\ No newline at end of file
diff --git a/frontend/src/components/ResponsiveAppBar.tsx b/frontend/src/components/ResponsiveAppBar.tsx
index c6480fe..6f4787d 100644
--- a/frontend/src/components/ResponsiveAppBar.tsx
+++ b/frontend/src/components/ResponsiveAppBar.tsx
@@ -23,6 +23,14 @@ const pages = [
{ name: 'Results', path: '/results' }
];
+// User menu items
+const userMenuItems = [
+ { name: 'My Contacts', path: '/contacts_manager' },
+ { name: 'My Addresses', path: '/addresses_manager' },
+ { name: 'DUO', path: '/duo' },
+ { name: 'Logout', path: '/logout' }
+];
+
const ResponsiveAppBar: React.FC = () => {
const [anchorElNav, setAnchorElNav] = useState(null);
const [anchorElUser, setAnchorElUser] = useState(null);
@@ -141,8 +149,13 @@ const ResponsiveAppBar: React.FC = () => {
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
-
-
+ {userMenuItems.map((item) => (
+
+ ))}
@@ -152,4 +165,4 @@ const ResponsiveAppBar: React.FC = () => {
);
};
-export default ResponsiveAppBar;
+export default ResponsiveAppBar;
\ No newline at end of file
diff --git a/frontend/src/pages/AddressManagerView.tsx b/frontend/src/pages/AddressManagerView.tsx
new file mode 100644
index 0000000..2bb5f1d
--- /dev/null
+++ b/frontend/src/pages/AddressManagerView.tsx
@@ -0,0 +1,138 @@
+import * as React from 'react';
+import axios from 'axios';
+import {
+ Container, Typography, List, ListItem, IconButton, TextField, Box, ListItemText, ListItemSecondaryAction
+} from '@mui/material';
+import DeleteIcon from '@mui/icons-material/Delete';
+import EditIcon from '@mui/icons-material/Edit';
+import SaveIcon from '@mui/icons-material/Save';
+import AddIcon from '@mui/icons-material/Add';
+
+interface Address {
+ id: number;
+ street: string;
+ city: string;
+ zipcode: string;
+ country: string;
+}
+
+const AddressManager: React.FC = () => {
+ const [addresses, setAddresses] = React.useState([]);
+ const [newAddress, setNewAddress] = React.useState>({
+ id: 0,
+ street: '',
+ city: '',
+ zipcode: '',
+ country: '',
+ });
+ const [editAddressId, setEditAddressId] = React.useState(null);
+ const [errorMessage, setErrorMessage] = React.useState(null);
+
+ React.useEffect(() => {
+ const fetchAddresses = async () => {
+ try {
+ const response = await axios.get('http://127.0.0.1:8000/addresses');
+ if (Array.isArray(response.data)) {
+ setAddresses(response.data);
+ } else {
+ setErrorMessage('Failed to load addresses. Expected an array of addresses.');
+ }
+ } catch (error) {
+ console.error('Failed to fetch addresses', error);
+ setErrorMessage('Failed to load addresses. Please try again later.');
+ }
+ };
+
+ fetchAddresses();
+ }, []);
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ const { name, value } = event.target;
+ setNewAddress({ ...newAddress, [name]: value });
+ };
+
+ const handleAddOrUpdateAddress = async () => {
+ if (editAddressId !== null) {
+ // Update address
+ try {
+ await axios.put(`http://127.0.0.1:8000/addresses/${editAddressId}`, newAddress);
+ setAddresses(addresses.map(address => address.id === editAddressId ? { ...address, ...newAddress } : address));
+ setEditAddressId(null);
+ setNewAddress({ street: '', city: '', zipcode: '', country: '' });
+ setErrorMessage(null);
+ } catch (error) {
+ console.error('Failed to update address', error);
+ setErrorMessage('Failed to update address. Please try again later.');
+ }
+ } else {
+ // Add new address
+ try {
+ const response = await axios.post('http://127.0.0.1:8000/addresses', newAddress);
+ setAddresses([...addresses, response.data]);
+ setNewAddress({ street: '', city: '', zipcode: '', country: '' });
+ setErrorMessage(null);
+ } catch (error) {
+ console.error('Failed to add address', error);
+ setErrorMessage('Failed to add address. Please try again later.');
+ }
+ }
+ };
+
+ const handleDeleteAddress = async (id: number) => {
+ try {
+ await axios.delete(`http://127.0.0.1:8000/addresses/${id}`);
+ setAddresses(addresses.filter(address => address.id !== id));
+ setErrorMessage(null);
+ } catch (error) {
+ console.error('Failed to delete address', error);
+ setErrorMessage('Failed to delete address. Please try again later.');
+ }
+ };
+
+ const handleEditAddress = (address: Address) => {
+ setEditAddressId(address.id);
+ setNewAddress(address);
+ };
+
+ return (
+
+
+ Addresses Management
+
+
+
+
+
+
+
+ {editAddressId !== null ? : }
+
+
+ {errorMessage && {errorMessage}}
+
+ {addresses.length > 0 ? (
+ addresses.map((address) => (
+
+
+
+ handleEditAddress(address)}>
+
+
+ handleDeleteAddress(address.id)}>
+
+
+
+
+ ))
+ ) : (
+ No addresses found
+ )}
+
+
+ );
+};
+
+export default AddressManager;
\ No newline at end of file
diff --git a/frontend/src/pages/ContactsManagerView.tsx b/frontend/src/pages/ContactsManagerView.tsx
new file mode 100644
index 0000000..00f621d
--- /dev/null
+++ b/frontend/src/pages/ContactsManagerView.tsx
@@ -0,0 +1,138 @@
+import * as React from 'react';
+import axios from 'axios';
+import {
+ Container, Typography, List, ListItem, IconButton, TextField, Box, ListItemText, ListItemSecondaryAction
+} from '@mui/material';
+import DeleteIcon from '@mui/icons-material/Delete';
+import EditIcon from '@mui/icons-material/Edit';
+import SaveIcon from '@mui/icons-material/Save';
+import AddIcon from '@mui/icons-material/Add';
+
+interface Contact {
+ id: number;
+ firstname: string;
+ lastname: string;
+ phone_number: string;
+ email: string;
+}
+
+const ContactsManager: React.FC = () => {
+ const [contacts, setContacts] = React.useState([]);
+ const [newContact, setNewContact] = React.useState>({
+ id: 0,
+ firstname: '',
+ lastname: '',
+ phone_number: '',
+ email: '',
+ });
+ const [editContactId, setEditContactId] = React.useState(null);
+ const [errorMessage, setErrorMessage] = React.useState(null);
+
+ React.useEffect(() => {
+ const fetchContacts = async () => {
+ try {
+ const response = await axios.get('http://127.0.0.1:8000/contacts');
+ if (Array.isArray(response.data)) {
+ setContacts(response.data);
+ } else {
+ setErrorMessage('Failed to load contacts. Expected an array of contacts.');
+ }
+ } catch (error) {
+ console.error('Failed to fetch contacts', error);
+ setErrorMessage('Failed to load contacts. Please try again later.');
+ }
+ };
+
+ fetchContacts();
+ }, []);
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ const { name, value } = event.target;
+ setNewContact({ ...newContact, [name]: value });
+ };
+
+ const handleAddOrUpdateContact = async () => {
+ if (editContactId !== null) {
+ // Update contact
+ try {
+ await axios.put(`http://127.0.0.1:8000/contacts/${editContactId}`, newContact);
+ setContacts(contacts.map(contact => contact.id === editContactId ? { ...contact, ...newContact } : contact));
+ setEditContactId(null);
+ setNewContact({ firstname: '', lastname: '', phone_number: '', email: '' });
+ setErrorMessage(null);
+ } catch (error) {
+ console.error('Failed to update contact', error);
+ setErrorMessage('Failed to update contact. Please try again later.');
+ }
+ } else {
+ // Add new contact
+ try {
+ const response = await axios.post('http://127.0.0.1:8000/contacts', newContact);
+ setContacts([...contacts, response.data]);
+ setNewContact({ firstname: '', lastname: '', phone_number: '', email: '' });
+ setErrorMessage(null);
+ } catch (error) {
+ console.error('Failed to add contact', error);
+ setErrorMessage('Failed to add contact. Please try again later.');
+ }
+ }
+ };
+
+ const handleDeleteContact = async (id: number) => {
+ try {
+ await axios.delete(`http://127.0.0.1:8000/contacts/${id}`);
+ setContacts(contacts.filter(contact => contact.id !== id));
+ setErrorMessage(null);
+ } catch (error) {
+ console.error('Failed to delete contact', error);
+ setErrorMessage('Failed to delete contact. Please try again later.');
+ }
+ };
+
+ const handleEditContact = (contact: Contact) => {
+ setEditContactId(contact.id);
+ setNewContact(contact);
+ };
+
+ return (
+
+
+ Contacts Management
+
+
+
+
+
+
+
+ {editContactId !== null ? : }
+
+
+ {errorMessage && {errorMessage}}
+
+ {contacts.length > 0 ? (
+ contacts.map((contact) => (
+
+
+
+ handleEditContact(contact)}>
+
+
+ handleDeleteContact(contact.id)}>
+
+
+
+
+ ))
+ ) : (
+ No contacts found
+ )}
+
+
+ );
+};
+
+export default ContactsManager;
\ No newline at end of file