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} > - DUO - Logout + {userMenuItems.map((item) => ( + + + {item.name} + + + ))} @@ -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