Add edit and delete flight in the frontend

Co-authored-by: shadad00 <shadad@itba.edu.ar>
This commit is contained in:
Santiago Lo Coco 2023-12-04 10:37:30 -03:00
parent a7d0fbc091
commit a35a97bfaa
13 changed files with 314 additions and 37 deletions

View File

@ -329,6 +329,7 @@ test-screen-client:
- docker compose -f subscription-domain/docker-compose.dev.yml --env-file $ENV_DEV_FILE down
- docker compose -f subscription-domain/docker-compose.dev.yml --env-file $ENV_DEV_FILE pull
- docker compose -f subscription-domain/docker-compose.dev.yml --env-file $ENV_DEV_FILE up -d
- export API_IMAGE=$CLIENT_IMAGE
- docker compose -f ${FOLDER}/docker-compose.dev.yml --env-file $ENV_DEV_FILE down
- docker compose -f ${FOLDER}/docker-compose.dev.yml --env-file $ENV_DEV_FILE pull
- docker compose -f ${FOLDER}/docker-compose.dev.yml --env-file $ENV_DEV_FILE up --abort-on-container-exit
@ -411,7 +412,7 @@ test-e2e-interface:
- *changes-frontend
- *changes-backend
script:
- export API_IMAGE=${E2E_TEST_IMAGE_NAME}
- export CLIENT_IMAGE=${E2E_TEST_IMAGE_NAME}
- export FOLDER=testing/catcher
- *test-integration
after_script:

View File

@ -1,5 +1,5 @@
import { Axios, AxiosError } from "axios";
import { Credentials, Token, User, Flight, FlightCreate, SubscriptionsCreate } from "./Types";
import { Credentials, Token, User, Flight, FlightCreate, SubscriptionsCreate, FlightEdit } from "./Types";
const instance = new Axios({
baseURL: process.env.REACT_APP_ENDPOINT ? process.env.REACT_APP_ENDPOINT : "http://127.0.0.1:5000/",
@ -79,6 +79,24 @@ export const createFlight = (
});
};
export const editFlight = (
flight_id:string,
fligth_data: FlightEdit,
token: string
):Promise<Flight> => {
return instance.patch("flights/" + flight_id , fligth_data, {
headers: { Authorization: `Bearer ${token}` },
});
};
export const fetchFlight = (
flight_id:string,
):Promise<Flight> => {
return instance.get("flights/" + flight_id);
};
export const subscribeToFlight = (subscription: SubscriptionsCreate, token: string): Promise<SubscriptionsCreate> => {
return instance.post("subscriptions", subscription, {
headers: { Authorization: `Bearer ${token}` },

View File

@ -5,6 +5,7 @@ import { Home } from "./components/Home/Home";
import { CreateFlight } from "./components/CreateFlight/CreateFlight";
import { Button } from "antd";
import useAuth, { AuthProvider } from "./useAuth";
import { EditFlight } from "./components/CreateFlight/EditFlight";
function Router() {
const { user, logout, isAirline } = useAuth();
@ -16,6 +17,7 @@ function Router() {
<Route path="/signup" element={<SignUp />} />
<Route path="/home" element={!user ? <LogIn /> : <Home />} />
<Route path="/create-flight" element={!isAirline ? <LogIn /> : <CreateFlight />} />
<Route path="/edit-flight/:id" element={!isAirline ? <LogIn /> : <EditFlight />} />
<Route path="/" element={!user ? <LogIn /> : <Home />} />
</Routes>
<div className="LogoutButton">

View File

@ -35,6 +35,7 @@ export interface Flight {
departure_time: string;
arrival_time: string;
gate: string;
user_id: number;
}
export interface FlightCreate {
@ -47,6 +48,20 @@ export interface FlightCreate {
gate: string;
}
export interface FlightEditNotNull {
departure_time: string,
arrival_time: string,
status: string,
gate: string
}
export interface FlightEdit {
departure_time: string?,
arrival_time: string?,
status: string?,
gate: string?
}
export interface SubscriptionsCreate {
flight_id: number;
user_id: number;

View File

@ -0,0 +1,111 @@
import React, { useEffect, useState } from "react";
import { FlightEditNotNull, Flight, FlightEdit } from "../../Types";
import { useNavigate, useParams } from "react-router";
import "./FlightForm.css";
import { createFlight, editFlight } from "../../Api";
import { useFetchFlight } from "../../hooks/useFetchFlight";
interface Props {
flight?: Flight;
}
export const EditFlight: React.FC<Props> = (props) => {
const navigate = useNavigate();
let { id } = useParams();
const [error, setError] = useState<string | null>(null);
const [flight, setFlight] = useState<Flight>();
const { flight: initialData } = useFetchFlight(id);
const [flightData, setFlightData] = useState<FlightEditNotNull>({
status: "",
departure_time: "",
arrival_time: "",
gate: ""
});
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
setError(null);
const token = localStorage.getItem("token");
if (!token) {
setError("No token!");
return;
}
let data: any = {}
if (flightData.arrival_time != "") {
data["arrival_time"] = flightData.arrival_time
}
if (flightData.departure_time != ""){
data["departure_time"] = flightData.departure_time
}
if (flightData.status != ""){
data["status"] = flightData.status
}
if (flightData.gate != ""){
data["gate"] = flightData.gate
}
if (id == null || id == undefined)
return;
editFlight(id, data, token)
.then((data) => {
setFlight(data);
navigate("/home")
})
.catch((error) => {
setError(error as string);
});
};
return (
<form onSubmit={handleSubmit}>
<label>
Status:
<input
type="text"
value={flightData?.status}
onChange={(e) =>
setFlightData({ ...flightData, status: e.target.value })
}
/>
</label>
<label>
Departure Time:
<input
type="text"
value={flightData?.departure_time}
onChange={(e) =>
setFlightData({ ...flightData, departure_time: e.target.value })
}
/>
</label>
<label>
Arrival Time:
<input
type="text"
value={flightData?.arrival_time}
onChange={(e) =>
setFlightData({ ...flightData, arrival_time: e.target.value })
}
/>
</label>
<label>
Gate:
<input
type="text"
value={flightData?.gate}
onChange={(e) => setFlightData({ ...flightData, gate: e.target.value })}
/>
</label>
<button type="submit">Submit</button>
</form>
);
};

View File

@ -1,35 +1,38 @@
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import { Avatar, Space, Typography, Tag, Button, Modal } from "antd";
import { RightOutlined, ClockCircleOutlined, SwapOutlined, EnvironmentOutlined, CalendarOutlined } from "@ant-design/icons";
import "./Card.css";
import { getChatId, getSubscription, subscribeToFlight, unsubscribeFromFlight } from "../../../Api";
import { User } from "../../../Types";
import { getChatId, getSubscription, subscribeToFlight, unsubscribeFromFlight, editFlight } from "../../../Api";
import { Flight, FlightEdit, User } from "../../../Types";
interface FlightProps {
id: number;
flight_code: string;
status: string;
origin: string;
destination: string;
departure_time: string;
arrival_time: string;
gate: string;
}
// interface FlightProps {
// id: number;
// flight_code: string;
// status: string;
// origin: string;
// destination: string;
// departure_time: string;
// arrival_time: string;
// gate: string;
// }
interface CardProps {
flight: FlightProps;
flight: Flight;
user: User | undefined;
subscribed: boolean;
refresh: any;
isAirline: boolean;
refreshFlights: any;
}
const { Text } = Typography;
export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh }) => {
export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh, refreshFlights, isAirline }) => {
const [modalVisible, setModalVisible] = useState<boolean>(false);
const navigate = useNavigate();
const handleSubscribe = async (event: React.FormEvent) => {
event.preventDefault();
@ -59,6 +62,33 @@ export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh })
});
};
const handleEdit = async (event: React.FormEvent) => {
event.preventDefault();
navigate(`/edit-flight/${flight.id}`);
};
const handleDelete = async (event: React.FormEvent) => {
event.preventDefault();
const token = localStorage.getItem("token");
if (!token || !user) {
return;
}
let data: any = {
status: "Deleted"
}
editFlight("" + flight.id, data, token)
.then(() => {
console.log("culicagado")
refreshFlights()
})
.catch((error) => {
console.log(error)
});
};
const handleModalClose = () => {
setModalVisible(false);
};
@ -83,10 +113,12 @@ export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh })
refresh()
})
.catch((error) => {
console.log(error)
});
};
console.log(subscribed)
console.log(flight.user_id)
console.log(user?.id)
return (
<div className="flight-card">
@ -126,7 +158,9 @@ export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh })
<Text>{flight.id}</Text>
</Space>
</div>
{!(subscribed) ?
{!isAirline ?
(
!(subscribed) ?
<Button type="primary" onClick={handleSubscribe}>
Subscribe
</Button>
@ -134,6 +168,21 @@ export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh })
<Button type="primary" onClick={handleUnsubscribe}>
Unsubscribe
</Button>
)
:
(
user && flight.user_id == user.id ?
<>
<Button type="primary" onClick={handleEdit}>
Edit
</Button>
<Button type="primary" onClick={handleDelete}>
Delete
</Button>
</>
:
<></>
)
}
<Modal
title="Error"

View File

@ -14,7 +14,7 @@ export const Home: React.FC<Props> = (props) => {
const urlParams = new URLSearchParams(window.location.search);
const origin = urlParams.get('origin');
const initialPage = parseInt(urlParams.get('page') || '1', 10);
const { flights, count, error } = useFetchFlights(origin, initialPage);
const { flights, count, error, fetchData: refreshFlights } = useFetchFlights(origin, initialPage);
const navigate = useNavigate()
const [currentPage, setCurrentPage] = useState(initialPage);
@ -56,7 +56,9 @@ export const Home: React.FC<Props> = (props) => {
<h2>Flights</h2>
<div className="Items">
{(props.flights ? props.flights : flights).map((f) => {
return <Card key={f.id} flight={f} user={user} subscribed={subscriptions.some((i => i.flight_id === f.id))} refresh={fetchData} />;
return <Card key={f.id} flight={f} user={user}
subscribed={subscriptions.some((i => i.flight_id === f.id))}
refresh={fetchData} refreshFlights={refreshFlights} isAirline={isAirline} />;
})}
{error ? <div className="Disconnected">{error}</div> : <></>}
</div>

View File

@ -0,0 +1,25 @@
import { useEffect } from "react";
import { useState } from "react";
import { Flight } from "../Types";
import { fetchFlight } from "../Api";
export const useFetchFlight = (id: string | undefined) => {
const [error, setError] = useState<string | null>(null);
const [flight, setFlight] = useState<Flight>();
const [count, setCount] = useState<number>(0);
useEffect(() => {
setError(null);
if (id == null || id == undefined)
return;
fetchFlight(id)
.then((data) => {
setFlight(data);
})
.catch((error) => { });
}, [id]);
return { flight, count, error };
};

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useCallback, useEffect } from "react";
import { useState } from "react";
import { User, Flight } from "../Types";
import { fetchFlights } from "../Api";
@ -8,16 +8,31 @@ export const useFetchFlights = (origin: string | null, page: number | null) =>
const [flights, setFlights] = useState<Flight[]>([]);
const [count, setCount] = useState<number>(0);
useEffect(() => {
// useEffect(() => {
// setError(null);
// fetchFlights(origin, page)
// .then((data) => {
// setCount(data.count)
// setFlights(data.flights.filter((e) => e.status != "Deleted" ));
// })
// .catch((error) => { });
// }, [page]);
const fetchData = useCallback(async () => {
setError(null);
fetchFlights(origin, page)
.then((data) => {
setCount(data.count)
setFlights(data.flights);
setFlights(data.flights.filter((e) => e.status != "Deleted" ));
})
.catch((error) => { });
}, [page]);
}, [origin, page]);
return { flights, count, error };
useEffect(() => {
fetchData()
}, [fetchData]);
return { flights, count, error, fetchData };
};

View File

@ -28,7 +28,7 @@ app.add_middleware(
"http://localhost:3000",
],
allow_credentials=True,
allow_methods=["POST", "GET", "PUT", "DELETE", "OPTIONS"],
allow_methods=["POST", "GET", "PUT", "DELETE", "OPTIONS", "PATCH"],
allow_headers=["*"],
expose_headers=["x-count"],
)

View File

@ -5,12 +5,12 @@ from fastapi import APIRouter, Header, HTTPException, Request, Response
from src.api.config import API_FLIGHTS
from src.api.routes.auth import checkAuth
from src.api.schemas.flight import Flight, FlightCreate, FlightUpdate
from src.api.schemas.flight import Flight, FlightCreate, FlightFull, FlightUpdate
router = APIRouter()
@router.get("/{id}", response_model=Flight)
@router.get("/{id}", response_model=FlightFull)
async def get_flight_by_id(
id: int,
req: Request,
@ -42,6 +42,23 @@ async def create_flight(
return response
# @router.delete("/{id}")
# async def delete_flight(
# id: int,
# req: Request,
# authorization: Annotated[str | None, Header()] = None,
# ):
# id = await checkAuth(req, authorization, isAirline=True)
# request_id = req.state.request_id
# header = {"x-api-request-id": request_id}
# (response, status, _) = await request(
# f"{API_FLIGHTS}/{id}", "DELETE", headers=header
# )
# if status < 200 or status > 204:
# raise HTTPException(status_code=status, detail=response)
# return response
@router.patch("/{id}", response_model=Flight)
async def update_flight(
id: int,
@ -62,7 +79,7 @@ async def update_flight(
return response
@router.get("", response_model=list[Flight])
@router.get("", response_model=list[FlightFull])
async def get_flights(
req: Request,
res: Response,

View File

@ -21,6 +21,24 @@ class Flight(BaseModel):
return value
class FlightFull(BaseModel):
id: int
flight_code: str
status: str
origin: str
destination: str
departure_time: str
arrival_time: str
gate: str = None
user_id: int
@validator("departure_time", "arrival_time", pre=True, always=True)
def parse_datetime(cls, value):
if isinstance(value, datetime):
return value.strftime("%Y-%m-%d %I:%M %p")
return value
class FlightCreate(BaseModel):
flight_code: str
status: str

4
run.sh
View File

@ -221,12 +221,16 @@ elif [ -n "$domain" ] && [ -z "$down" ]; then
elif [ -n "$down" ]; then
export API_IMAGE=$USER/flights-information:prod
docker compose -f flights-domain/docker-compose.yml --env-file flights-domain/.env.prod down
docker compose -f flights-domain/docker-compose.dev.yml --env-file flights-domain/.env.dev down
export API_IMAGE=$USER/user-manager:prod
docker compose -f auth-domain/docker-compose.yml --env-file auth-domain/.env.prod down
docker compose -f auth-domain/docker-compose.dev.yml --env-file auth-domain/.env.dev down
export API_IMAGE=$USER/subs-manager:prod
docker compose -f subscription-domain/docker-compose.yml --env-file subscription-domain/.env.prod down
docker compose -f subscription-domain/docker-compose.dev.yml --env-file subscription-domain/.env.dev down
export API_IMAGE=$USER/gateway:prod
docker compose -f gateway/docker-compose.yml --env-file gateway/.env.prod down
docker compose -f gateway/docker-compose.dev.yml --env-file gateway/.env.dev down
export CLIENT_IMAGE=$USER/screen-client:prod
docker compose -f screen-domain/docker-compose.yml down