From 4e76db4c9f1303bd2a0c856fbb0ff04b4fe019ea Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:31:01 +0100 Subject: [PATCH] added contacts and addresses manager --- backend/app/models.py | 2 +- backend/app/routers/address.py | 30 +++- backend/app/routers/contact.py | 32 ++++- backend/app/schemas.py | 10 ++ backend/test.db | Bin 77824 -> 77824 bytes frontend/package-lock.json | 100 ++++++++++++++ frontend/package.json | 2 +- frontend/src/App.tsx | 6 +- frontend/src/components/ResponsiveAppBar.tsx | 19 ++- frontend/src/pages/AddressManagerView.tsx | 138 +++++++++++++++++++ frontend/src/pages/ContactsManagerView.tsx | 138 +++++++++++++++++++ 11 files changed, 467 insertions(+), 10 deletions(-) create mode 100644 frontend/src/pages/AddressManagerView.tsx create mode 100644 frontend/src/pages/ContactsManagerView.tsx 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 8cb10b994ed08ded4e34e94a1a061586f055d64c..c44651d45462d3d085e800ed09a3f99c01160b5e 100644 GIT binary patch literal 77824 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 blNhK>k7A%QEn(oo$=X+8-~gDwz+UtJLtILo delta 5260 zcmajjYj9k38OQP6bIzXoc}{6ldV$ge!ljhLbN=TJD8;6gQf{TGm8%vvp_g=9;sVO; zur=c=70PCY7mfm&8J!ARNnS&d@Irl|NYokM2pDySadgxuj>C*To22b;d}C)a)8~_a zHcxhT_k8C({jTx!yT-HYSL%PN)oQEcYx$pk;>1Ns|DN`q0|&=P#FF&ZgU#mHNdNZz0~P zpT76RiQV0kRZ|P!n;zKuYFJ;{ee;=Z?%TES?eMSRd#Jm#@VoM>z8==8`|=CJeS!SA zNo#B2R5(l@(RXM!t(EWpJJ`14%#&Kn&0y2UU{h~iRjb#Bw(q;~&Mi^2V|%(SVcD=b zdiasC*3MS{vGI{(`^WdU4<2pZcFm23+ui-z$-3M9=(T-yyF2{)DboyJsfDjh+o!)B zu3D*Y`k#j~yQhCP^#9IGpZ@I*uHLH2ANGFuPBTIkV1;Gv!P=6HeP{I>XM8lQ@G; zpJUhy_Pjl3&)PHgls#!r*loLM58Fd_Vh>h!pRHSev);3QZoO`O-};9273*>9kad@J zla*VSS*tC>{JZ(S`L=o5{DJvR^Q-0)=3(=0bEjFDmz(FCjmARbca2{(-e{a^ywLbs z9+Qv_6O~k+N?IMeM>u`J*^$p?$>VBuGcne>ouqT zulgVBzpB4gqt5+CZDW1U{v*f6MwC+PsB<4eBycYR3*55|Rqtn_Qsizp5)}e>A>;y| zM#u#AA*2GILP!MmBE$m22x=`=ojoYgTI$@1zyiAgxD>bpp%9Sw?%5}R^EK4D4Iw+9 zI=3RE0=FzdwfLlQBC-pQ!~!=XD1n^_k-$v|Ebz&55S8U>>g+%$1hymOt1IdZp=5Go z8$v4Z34}!8Mub>kD}oZZ0U_$6&c_j0;A4wWExu}^q7=CvjueH!bqKk@wFsF&iI55u z2#G+B5DR1osuD?2db=hFy%2F6atqcRJq7yD4D>e2&upzLT}Ye5PGX#jL=(k03kY$ zI{gSNuu-5|{@jStQsg2yk`^ndvjHI&xDX)|xBwv)SdWkhtV4(e)&f+i5?KS1Mgr#} zu)u17q!j2wC_?J2LdXSHB4h&RA*2E;j9PW(b0bO;kr0o>0)(Ih0)$AwM_>UDAT9-5 zgx;#oIjEwzx2t_lqBtY!SO}?riI5015MluXK?&#xk${H40(GN0`@tEhQlth)R6$hX zoD)5Tvn=`~&a&qdILn%k<1AY~hO;bLeGx&H`~uD#(4#O%Wk8SMTmoU?#F zk8>K(IL=8>(L)GvK*w-a0UgCT3aEv%EO-QFmIcRfmIV*vEDIjOIjej+h>-ep0O!P~ z2XT&l8pT=pG=g*FQxj+Q>2v31E=!*tz_}`X+K-U?^jVxUpFV@LEC{*5r7Q@w!KEw+ zvB9PC=uB%!YH%5O1f{`M$sR#za8Y^$p}|Gr5p)I@xkr#0Tx1?WWpI&t1d+i-;t?bU z7qLfB7!?f7o3+aL0)iPxCC{27;H0z(0)msmCg=xFa+?*zmivd}#3CpLj$?};7&ulI^?G4B7dVbAf?D91ErM8JB@03; zu#yEK6Q)(Nii94VdPBF|Bz6I|mtVvXPu&s=H*7kK7EBe=dZ zUz}?M*LUVjBe=XXr`q$g8|z#3D}(HLtG~Wi?zH^Iujn{Ds>+g!=r}yA%3=X{Sd}RO zcwCi50+03t{P{tI}KqvO%TU2I@dJs5BLTa8PL?0Og?4 zSOC&NrAh$WL8Z}!)PZPk_tdVs3Z}9 zhEPc?uow}`Zyl8=5vT~2L;{czDq#WW2o?8Mg^*BjZ&fG>73b@z11X{6OaNL!#Z@W- zH6i!L1fV8V919#c_pVj35_k|H5*S5bfe`>z3P4gQRS5K&;`Xz*9C|kgpemH=-5h|b VP^x!x0J1`<-pv8%3Z>$8{{>>5;!6Mk 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