Skip to content

Commit b838b42

Browse files
PetruccioUb0nsun9
andauthored
Feature/#55 (#68)
* SignIn (with Google) is implemented. * corrected index * DashBoard updated to display all surcharges data * DashBoard updated to show surcharges information, status filtering, confirmSurcharge introduced but not tested * Search.tsx - surcharges search component created and imported into DashBoard.tsx * Confirmation modal created, DashBoard updated * DashBoard and ConfirmationModal updated * added renderContent() for conditional buttons rendering * DashBoard and ConfirmationModal fixed * displayName and addressComponents introduced to DashBoard, api URLs updated * Surcharges from Places formatting updated * DashBoard updated * DashBoard updated and divided by components * Handle conflict * Handle Conflict * All conflicts are handled. --------- Co-authored-by: Bonsung Koo <developer@bonsung.me>
1 parent 6e5b0db commit b838b42

File tree

29 files changed

+732
-57
lines changed

29 files changed

+732
-57
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
"@mui/material": "^6.3.0",
1818
"@vis.gl/react-google-maps": "^1.4.2",
1919
"firebase": "^11.1.0",
20-
"firebase-tools": "^13.29.1",
2120
"react": "^18.3.1",
2221
"react-confetti": "^6.2.2",
2322
"react-dom": "^18.3.1",
@@ -36,6 +35,7 @@
3635
"eslint": "^9.15.0",
3736
"eslint-plugin-react-hooks": "^5.0.0",
3837
"eslint-plugin-react-refresh": "^0.4.14",
38+
"firebase-tools": "^13.29.1",
3939
"globals": "^15.12.0",
4040
"postcss": "^8.4.49",
4141
"prettier": "^3.4.2",

src/1_app/ui/app/App.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { Main } from '@widgets/main'
55
import { Results } from '@widgets/results'
66
import { Detail } from '@widgets/detail'
77
import { Report } from '@widgets/report'
8+
import { Login } from '@widgets/login'
9+
import { DashBoard } from '@widgets/dashboard'
10+
import { Protected } from '@features/protected'
11+
import { AuthContextProvider } from '@shared/model'
812
import { PrivacyPolicy } from '@widgets/privacyPolicy'
913

1014
const queryClient = new QueryClient()
@@ -13,17 +17,25 @@ export function App() {
1317
return (
1418
<div>
1519
<main>
16-
<QueryClientProvider client={queryClient}>
17-
<BrowserRouter>
18-
<Routes>
19-
<Route path="/" element={<Main />} />
20-
<Route path="/search" element={<Results />} />
21-
<Route path="/place" element={<Detail />} />
22-
<Route path="/report" element={<Report />} />
23-
<Route path="/privacy" element={<PrivacyPolicy />} />
24-
</Routes>
25-
</BrowserRouter>
26-
</QueryClientProvider>
20+
<AuthContextProvider>
21+
<QueryClientProvider client={queryClient}>
22+
<BrowserRouter>
23+
<Routes>
24+
<Route path="/" element={<Main />} />
25+
<Route path="/search" element={<Results />} />
26+
<Route path="/place" element={<Detail />} />
27+
<Route path="/report" element={<Report />} />
28+
<Route path="/login" element={<Login />} />
29+
<Route path='/admin' element={
30+
<Protected>
31+
<DashBoard />
32+
</Protected>
33+
} />
34+
<Route path="/privacy" element={<PrivacyPolicy />} />
35+
</Routes>
36+
</BrowserRouter>
37+
</QueryClientProvider>
38+
</AuthContextProvider>
2739
</main>
2840
</div>
2941
)

src/3_widgets/dashboard/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { DashBoard } from "./ui/DashBoard"
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { useEffect, useState } from "react";
2+
import { NavigationBar } from "./components/NavigationBar";
3+
import { useAuth } from "@shared/model";
4+
import Search from "./components/Search";
5+
import SurhcargesList from "./components/SurchargesList"
6+
import { Surcharge } from "./model/surcharge/Surcharge"
7+
import { Box } from '@mui/material';
8+
9+
export function DashBoard() {
10+
const { user } = useAuth();
11+
const [surcharges, setSurcharges] = useState<Surcharge[]>([]);
12+
const [searchedSurcharges, setSearchedSurcharges] = useState<Surcharge[]>([]);
13+
const [loading, setLoading] = useState(true);
14+
const [errorProp, setError] = useState<string | null>(null);
15+
16+
useEffect(() => {
17+
const fetchSurcharges = async () => {
18+
try {
19+
const baseURL = import.meta.env.VITE_BASE_URL;
20+
const token = user ? await user.getIdToken() : "";
21+
22+
const response = await fetch(`${baseURL}/admin/places`, {
23+
method: "GET",
24+
headers: {
25+
Accept: "application/json",
26+
"Content-Type": "application/json",
27+
Authorization: `Bearer ${token}`,
28+
},
29+
});
30+
31+
if (!response.ok) {
32+
throw new Error(`Error fetching surcharges: ${response.statusText}`);
33+
}
34+
35+
const allPlaces = await response.json();
36+
const formattedSurcharges: Surcharge[] = allPlaces.places.map(
37+
(surcharge: any) => ({
38+
id: surcharge.id,
39+
image: surcharge.image,
40+
rate: surcharge.rate,
41+
reportedDate: surcharge.reportedDate._seconds * 1000,
42+
totalAmount: surcharge.totalAmount,
43+
surchargeAmount: surcharge.surchargeAmount,
44+
surchargeStatus: surcharge.surchargeStatus,
45+
displayName: surcharge.displayName.text,
46+
addressComponents: `${surcharge.addressComponents[0].shortText}/${surcharge.addressComponents[1].shortText} ${surcharge.addressComponents[2].shortText}`
47+
})
48+
);
49+
50+
setSurcharges(formattedSurcharges);
51+
setSearchedSurcharges(formattedSurcharges);
52+
} catch (err) {
53+
setError(
54+
err instanceof Error
55+
? err.message
56+
: "An unknown error occurred while fetching surcharges"
57+
);
58+
} finally {
59+
setLoading(false);
60+
}
61+
};
62+
63+
fetchSurcharges();
64+
}, [user]);
65+
66+
const handleSearchChange = (newFilter: string) => {
67+
const filterValue = newFilter.toLowerCase();
68+
const filteredSurcharges = surcharges.filter(
69+
(surcharge) =>
70+
surcharge.displayName.toLowerCase().includes(filterValue) ||
71+
surcharge.addressComponents.toLowerCase().includes(filterValue) ||
72+
surcharge.surchargeStatus.toLowerCase().includes(filterValue) ||
73+
surcharge.rate.toString() === filterValue ||
74+
surcharge.totalAmount.toString() === filterValue ||
75+
surcharge.surchargeAmount.toString() === filterValue
76+
);
77+
setSearchedSurcharges(filteredSurcharges);
78+
};
79+
80+
return (
81+
<div className="h-screen flex flex-col">
82+
<nav className="fixed top-0 left-0 w-full bg-white shadow-md z-10">
83+
<NavigationBar />
84+
<Search onSearch={handleSearchChange} />
85+
<Box sx={{ display: "flex", justifyContent: "center", flexGrow: 1 }}>
86+
<div className="m-4 text-center">
87+
<h2 className="text-lg font-bold mb-4">Reported Surcharges:</h2>
88+
</div>
89+
</Box>
90+
</nav>
91+
<div className="flex flex-col flex-grow mt-[65px] overflow-y-auto">
92+
<SurhcargesList searchedSurcharges={searchedSurcharges} loading={loading} errorProp={errorProp}/>
93+
</div>
94+
</div>
95+
);
96+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Button, TextField, Dialog, DialogTitle, DialogContent, DialogActions, CircularProgress, Box } from '@mui/material';
3+
4+
interface Props {
5+
status: string
6+
surchargeId: string;
7+
imageName: string | undefined;
8+
isOpen: boolean;
9+
onClose: () => void;
10+
onConfirm: (surchargeId: string, action: string, newSurchargeAmount: number | undefined, newTotalAmount: number | undefined) => Promise<void>;
11+
}
12+
13+
const ConfirmationModal: React.FC<Props> = ({status, surchargeId, imageName, isOpen, onClose, onConfirm }) => {
14+
const [newSurchargeAmount, setNewSurchargeAmount] = useState('');
15+
const [newTotalAmount, setNewTotalAmount] = useState('');
16+
const [imageBase64, setImageBase64] = useState<string | null>(null);
17+
const [loadingImage, setLoadingImage] = useState(false);
18+
19+
useEffect(() => {
20+
if (isOpen && imageName) {
21+
const fetchImage = async () => {
22+
setLoadingImage(true);
23+
try {
24+
const baseURL = import.meta.env.VITE_BASE_URL;
25+
const response = await fetch(`${baseURL}/admin/image?image=${imageName}`, { // TODO: api -> admin
26+
method: 'GET',
27+
headers: {
28+
Accept: 'application/json',
29+
'Content-Type': 'application/json',
30+
},
31+
});
32+
33+
if (response.ok) {
34+
const data = await response.json();
35+
setImageBase64(data.image); // Assuming `data.image` contains the Base64 string
36+
} else {
37+
console.error('Error fetching image:', response.statusText);
38+
setImageBase64(null);
39+
}
40+
} catch (error) {
41+
console.error('Error fetching image:', error);
42+
setImageBase64(null);
43+
} finally {
44+
setLoadingImage(false);
45+
}
46+
};
47+
48+
fetchImage();
49+
} else {
50+
setImageBase64(null);
51+
}
52+
}, [isOpen, imageName]);
53+
54+
const handleInputChange = (
55+
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
56+
setFunction: (value: React.SetStateAction<string>) => void
57+
) => {
58+
setFunction(e.target.value);
59+
};
60+
61+
function renderContent() {
62+
if (status === "REPORTED") {
63+
return (
64+
<>
65+
<Button
66+
onClick={() =>
67+
onConfirm(surchargeId, "CONFIRM", Number(newSurchargeAmount), Number(newTotalAmount))
68+
}
69+
color="primary"
70+
variant="contained"
71+
>
72+
Confirm Surcharge
73+
</Button>
74+
<Button
75+
onClick={() =>
76+
onConfirm(surchargeId, "REJECT", Number(newSurchargeAmount), Number(newTotalAmount))
77+
}
78+
color="secondary"
79+
variant="contained"
80+
>
81+
Reject Surcharge
82+
</Button>
83+
</>
84+
);
85+
} else if (status === "CONFIRMED") {
86+
return (
87+
<Button
88+
onClick={() =>
89+
onConfirm(surchargeId, "REJECT", Number(newSurchargeAmount), Number(newTotalAmount))
90+
}
91+
color="secondary"
92+
variant="contained"
93+
>
94+
Reject Surcharge
95+
</Button>
96+
);
97+
} else if (status === "REJECTED") {
98+
return (
99+
<Button
100+
onClick={() =>
101+
onConfirm(surchargeId, "CONFIRM", Number(newSurchargeAmount), Number(newTotalAmount))
102+
}
103+
color="primary"
104+
variant="contained"
105+
>
106+
Confirm Surcharge
107+
</Button>
108+
);
109+
}
110+
}
111+
112+
113+
return (
114+
<Dialog open={isOpen} onClose={onClose} maxWidth="sm" fullWidth>
115+
<DialogTitle>Confirm Surcharge</DialogTitle>
116+
<DialogContent dividers>
117+
<Box display="flex" justifyContent="center" mb={2}>
118+
{loadingImage ? (
119+
<CircularProgress />
120+
) : imageBase64 ? (
121+
<img
122+
src={`data:image/jpeg;base64,${imageBase64}`}
123+
alt="Surcharge Evidence"
124+
style={{ maxWidth: '100%', maxHeight: '300px', borderRadius: '8px' }}
125+
/>
126+
) : (
127+
<p>No image available</p>
128+
)}
129+
</Box>
130+
<TextField
131+
label="New Surcharge Amount"
132+
type="number"
133+
value={newSurchargeAmount}
134+
onChange={(e) => handleInputChange(e, setNewSurchargeAmount)}
135+
fullWidth
136+
variant="outlined"
137+
margin="normal"
138+
/>
139+
<TextField
140+
label="New Total Amount"
141+
type="number"
142+
value={newTotalAmount}
143+
onChange={(e) => handleInputChange(e, setNewTotalAmount)}
144+
fullWidth
145+
variant="outlined"
146+
margin="normal"
147+
/>
148+
</DialogContent>
149+
<DialogActions>
150+
<Button onClick={onClose} color="secondary" variant="outlined">
151+
Close
152+
</Button>
153+
{renderContent()}
154+
</DialogActions>
155+
</Dialog>
156+
);
157+
};
158+
159+
export default ConfirmationModal;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useAuth } from '@shared/model'
2+
import { Button } from '@mui/material'
3+
4+
export function NavigationBar() {
5+
6+
const { user, logout } = useAuth()
7+
8+
const handleLogout = async () => {
9+
try {
10+
await logout()
11+
} catch {
12+
console.error('Sign out error')
13+
}
14+
}
15+
16+
return (
17+
<div className='fixed top-0 right-0 m-4'>
18+
<div className='flex items-center'>
19+
<p>Hello, {user?.displayName}</p>
20+
<Button onClick={handleLogout}>Sign out</Button>
21+
</div>
22+
</div>
23+
)
24+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from "react";
2+
3+
interface SearchProps {
4+
onSearch: (filter: string) => void;
5+
}
6+
7+
const Search: React.FC<SearchProps> = ({ onSearch }) => {
8+
const [searchTerm, setSearchTerm] = React.useState<string>("");
9+
10+
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
11+
setSearchTerm(event.target.value);
12+
};
13+
14+
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
15+
event.preventDefault();
16+
onSearch(searchTerm);
17+
};
18+
19+
return (
20+
<div className='fixed top-0 left-0 m-4'>
21+
<div className='flex items-center'>
22+
<form onSubmit={handleSubmit}>
23+
<input
24+
type="text"
25+
placeholder="Enter to search surcharges"
26+
value={searchTerm}
27+
onChange={handleSearchChange}
28+
className="px-4 py-2 mb-2 border rounded"
29+
/>
30+
</form>
31+
</div>
32+
</div>
33+
);
34+
};
35+
36+
export default Search;

0 commit comments

Comments
 (0)