added contacts and addresses manager
This commit is contained in:
parent
689145150a
commit
4e76db4c9f
@ -11,7 +11,7 @@ class Shipment(Base):
|
|||||||
shipment_name = Column(String, index=True)
|
shipment_name = Column(String, index=True)
|
||||||
shipment_date = Column(Date)
|
shipment_date = Column(Date)
|
||||||
shipment_status = Column(String)
|
shipment_status = Column(String)
|
||||||
comments = Column(String, nullable=True)
|
comments = Column(String(200), nullable=True)
|
||||||
contact_person_id = Column(Integer, ForeignKey("contact_persons.id"))
|
contact_person_id = Column(Integer, ForeignKey("contact_persons.id"))
|
||||||
return_address_id = Column(Integer, ForeignKey("addresses.id"))
|
return_address_id = Column(Integer, ForeignKey("addresses.id"))
|
||||||
proposal_id = Column(Integer, ForeignKey('proposals.id'), nullable=True)
|
proposal_id = Column(Integer, ForeignKey('proposals.id'), nullable=True)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, HTTPException, status, Depends
|
from fastapi import APIRouter, HTTPException, status, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List
|
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.models import Address as AddressModel
|
||||||
from app.dependencies import get_db
|
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.add(db_address)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_address)
|
db.refresh(db_address)
|
||||||
return db_address
|
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
|
@ -1,12 +1,13 @@
|
|||||||
from fastapi import APIRouter, HTTPException, status, Depends
|
from fastapi import APIRouter, HTTPException, status, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List
|
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.models import ContactPerson as ContactPersonModel
|
||||||
from app.dependencies import get_db
|
from app.dependencies import get_db
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# Existing routes
|
||||||
@router.get("/", response_model=List[ContactPerson])
|
@router.get("/", response_model=List[ContactPerson])
|
||||||
async def get_contacts(db: Session = Depends(get_db)):
|
async def get_contacts(db: Session = Depends(get_db)):
|
||||||
return db.query(ContactPersonModel).all()
|
return db.query(ContactPersonModel).all()
|
||||||
@ -28,4 +29,31 @@ async def create_contact(contact: ContactPersonCreate, db: Session = Depends(get
|
|||||||
db.add(db_contact)
|
db.add(db_contact)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_contact)
|
db.refresh(db_contact)
|
||||||
return db_contact
|
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
|
@ -21,6 +21,11 @@ class ContactPerson(ContactPersonBase):
|
|||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
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
|
# Address schemas
|
||||||
class AddressCreate(BaseModel):
|
class AddressCreate(BaseModel):
|
||||||
@ -36,6 +41,11 @@ class Address(AddressCreate):
|
|||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
class AddressUpdate(BaseModel):
|
||||||
|
street: str | None = None
|
||||||
|
city: str | None = None
|
||||||
|
zipcode: str | None = None
|
||||||
|
country: str | None = None
|
||||||
|
|
||||||
# Sample schemas
|
# Sample schemas
|
||||||
class Sample(BaseModel):
|
class Sample(BaseModel):
|
||||||
|
BIN
backend/test.db
BIN
backend/test.db
Binary file not shown.
100
frontend/package-lock.json
generated
100
frontend/package-lock.json
generated
@ -20,6 +20,7 @@
|
|||||||
"@fullcalendar/timegrid": "^6.1.15",
|
"@fullcalendar/timegrid": "^6.1.15",
|
||||||
"@mui/icons-material": "^6.1.5",
|
"@mui/icons-material": "^6.1.5",
|
||||||
"@mui/material": "^6.1.5",
|
"@mui/material": "^6.1.5",
|
||||||
|
"axios": "^1.7.7",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"openapi-typescript-codegen": "^0.29.0",
|
"openapi-typescript-codegen": "^0.29.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@ -2611,6 +2612,23 @@
|
|||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
"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": {
|
"node_modules/babel-plugin-macros": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
||||||
@ -2801,6 +2819,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/commander": {
|
||||||
"version": "12.1.0",
|
"version": "12.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
"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==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/dequal": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
@ -3389,6 +3428,40 @@
|
|||||||
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/fs-extra": {
|
||||||
"version": "11.2.0",
|
"version": "11.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
|
||||||
@ -3876,6 +3949,27 @@
|
|||||||
"node": ">=8.6"
|
"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": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"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": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
"lint": "node_modules/.bin/eslint .",
|
"lint": "node_modules/.bin/eslint .",
|
||||||
"preview": "node_modules/.bin/vite preview",
|
"preview": "node_modules/.bin/vite preview",
|
||||||
"fetch:types": "node fetch-openapi.js"
|
"fetch:types": "node fetch-openapi.js"
|
||||||
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aldabil/react-scheduler": "^2.9.5",
|
"@aldabil/react-scheduler": "^2.9.5",
|
||||||
@ -24,6 +23,7 @@
|
|||||||
"@fullcalendar/timegrid": "^6.1.15",
|
"@fullcalendar/timegrid": "^6.1.15",
|
||||||
"@mui/icons-material": "^6.1.5",
|
"@mui/icons-material": "^6.1.5",
|
||||||
"@mui/material": "^6.1.5",
|
"@mui/material": "^6.1.5",
|
||||||
|
"axios": "^1.7.7",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"openapi-typescript-codegen": "^0.29.0",
|
"openapi-typescript-codegen": "^0.29.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
@ -5,6 +5,8 @@ import ShipmentView from './pages/ShipmentView';
|
|||||||
import HomePage from './pages/HomeView'; // Assuming this is a default export
|
import HomePage from './pages/HomeView'; // Assuming this is a default export
|
||||||
import ResultsView from './pages/ResultsView';
|
import ResultsView from './pages/ResultsView';
|
||||||
import PlanningView from './pages/PlanningView';
|
import PlanningView from './pages/PlanningView';
|
||||||
|
import ContactsManager from './pages/ContactsManagerView';
|
||||||
|
import AddressManager from './pages/AddressManagerView';
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
@ -15,9 +17,11 @@ const App: React.FC = () => {
|
|||||||
<Route path="/shipments" element={<ShipmentView />} />
|
<Route path="/shipments" element={<ShipmentView />} />
|
||||||
<Route path="/planning" element={<PlanningView />} />
|
<Route path="/planning" element={<PlanningView />} />
|
||||||
<Route path="/results" element={<ResultsView />} />
|
<Route path="/results" element={<ResultsView />} />
|
||||||
|
<Route path="/contacts_manager" element={<ContactsManager />} />
|
||||||
|
<Route path="/addresses_manager" element={<AddressManager />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
@ -23,6 +23,14 @@ const pages = [
|
|||||||
{ name: 'Results', path: '/results' }
|
{ 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 ResponsiveAppBar: React.FC = () => {
|
||||||
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null);
|
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null);
|
||||||
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null);
|
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null);
|
||||||
@ -141,8 +149,13 @@ const ResponsiveAppBar: React.FC = () => {
|
|||||||
open={Boolean(anchorElUser)}
|
open={Boolean(anchorElUser)}
|
||||||
onClose={handleCloseUserMenu}
|
onClose={handleCloseUserMenu}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={handleCloseUserMenu}>DUO</MenuItem>
|
{userMenuItems.map((item) => (
|
||||||
<MenuItem onClick={handleCloseUserMenu}>Logout</MenuItem>
|
<MenuItem key={item.name} onClick={handleCloseUserMenu}>
|
||||||
|
<Link to={item.path} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
@ -152,4 +165,4 @@ const ResponsiveAppBar: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ResponsiveAppBar;
|
export default ResponsiveAppBar;
|
138
frontend/src/pages/AddressManagerView.tsx
Normal file
138
frontend/src/pages/AddressManagerView.tsx
Normal file
@ -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<Address[]>([]);
|
||||||
|
const [newAddress, setNewAddress] = React.useState<Partial<Address>>({
|
||||||
|
id: 0,
|
||||||
|
street: '',
|
||||||
|
city: '',
|
||||||
|
zipcode: '',
|
||||||
|
country: '',
|
||||||
|
});
|
||||||
|
const [editAddressId, setEditAddressId] = React.useState<number | null>(null);
|
||||||
|
const [errorMessage, setErrorMessage] = React.useState<string | null>(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<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<Container>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Addresses Management
|
||||||
|
</Typography>
|
||||||
|
<Box mb={3} display="flex" justifyContent="center" alignItems="center">
|
||||||
|
<TextField label="Street" name="street" value={newAddress.street || ''} onChange={handleInputChange} />
|
||||||
|
<TextField label="City" name="city" value={newAddress.city || ''} onChange={handleInputChange} />
|
||||||
|
<TextField label="Zipcode" name="zipcode" value={newAddress.zipcode || ''} onChange={handleInputChange} />
|
||||||
|
<TextField label="Country" name="country" value={newAddress.country || ''} onChange={handleInputChange} />
|
||||||
|
<IconButton color="primary" onClick={handleAddOrUpdateAddress}>
|
||||||
|
{editAddressId !== null ? <SaveIcon /> : <AddIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
{errorMessage && <Typography color="error">{errorMessage}</Typography>}
|
||||||
|
<List>
|
||||||
|
{addresses.length > 0 ? (
|
||||||
|
addresses.map((address) => (
|
||||||
|
<ListItem key={address.id} button>
|
||||||
|
<ListItemText
|
||||||
|
primary={`${address.street}, ${address.city}`}
|
||||||
|
secondary={`${address.zipcode} - ${address.country}`}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<IconButton edge="end" color="primary" onClick={() => handleEditAddress(address)}>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton edge="end" color="secondary" onClick={() => handleDeleteAddress(address.id)}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Typography>No addresses found</Typography>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddressManager;
|
138
frontend/src/pages/ContactsManagerView.tsx
Normal file
138
frontend/src/pages/ContactsManagerView.tsx
Normal file
@ -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<Contact[]>([]);
|
||||||
|
const [newContact, setNewContact] = React.useState<Partial<Contact>>({
|
||||||
|
id: 0,
|
||||||
|
firstname: '',
|
||||||
|
lastname: '',
|
||||||
|
phone_number: '',
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
const [editContactId, setEditContactId] = React.useState<number | null>(null);
|
||||||
|
const [errorMessage, setErrorMessage] = React.useState<string | null>(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<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<Container>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Contacts Management
|
||||||
|
</Typography>
|
||||||
|
<Box mb={3} display="flex" justifyContent="center" alignItems="center">
|
||||||
|
<TextField label="First Name" name="firstname" value={newContact.firstname || ''} onChange={handleInputChange} />
|
||||||
|
<TextField label="Last Name" name="lastname" value={newContact.lastname || ''} onChange={handleInputChange} />
|
||||||
|
<TextField label="Phone Number" name="phone_number" value={newContact.phone_number || ''} onChange={handleInputChange} />
|
||||||
|
<TextField label="Email" name="email" value={newContact.email || ''} onChange={handleInputChange} />
|
||||||
|
<IconButton color="primary" onClick={handleAddOrUpdateContact}>
|
||||||
|
{editContactId !== null ? <SaveIcon /> : <AddIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
{errorMessage && <Typography color="error">{errorMessage}</Typography>}
|
||||||
|
<List>
|
||||||
|
{contacts.length > 0 ? (
|
||||||
|
contacts.map((contact) => (
|
||||||
|
<ListItem key={contact.id} button>
|
||||||
|
<ListItemText
|
||||||
|
primary={`${contact.firstname} ${contact.lastname}`}
|
||||||
|
secondary={`${contact.phone_number} - ${contact.email}`}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<IconButton edge="end" color="primary" onClick={() => handleEditContact(contact)}>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton edge="end" color="secondary" onClick={() => handleDeleteContact(contact.id)}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Typography>No contacts found</Typography>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactsManager;
|
Loading…
x
Reference in New Issue
Block a user