added contacts and addresses manager

This commit is contained in:
GotthardG 2024-11-04 21:31:01 +01:00
parent 689145150a
commit 4e76db4c9f
11 changed files with 467 additions and 10 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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):

Binary file not shown.

View File

@ -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",

View File

@ -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",

View File

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

View File

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

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

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