From ccda1f18b70062acf06fee7b47143c9f7965733c Mon Sep 17 00:00:00 2001 From: Irfan Emre Utkan <127414322+emreutkan@users.noreply.github.com> Date: Sat, 17 May 2025 13:42:34 +0300 Subject: [PATCH 1/3] feat: add tickets management and punishment functionality for support role --- src/AppWithMaps.tsx | 4 + src/feature/Login/Login.tsx | 13 +- .../screens/PunishRestaurant.module.css | 252 ++++++++++ .../Tickets/screens/PunishRestaurant.tsx | 263 +++++++++++ .../Tickets/screens/Tickets.module.css | 444 ++++++++++++++++++ src/feature/Tickets/screens/Tickets.tsx | 418 +++++++++++++++++ src/redux/Api/apiService.ts | 3 +- src/redux/slices/userSlice.ts | 2 +- 8 files changed, 1393 insertions(+), 6 deletions(-) create mode 100644 src/feature/Tickets/screens/PunishRestaurant.module.css create mode 100644 src/feature/Tickets/screens/PunishRestaurant.tsx create mode 100644 src/feature/Tickets/screens/Tickets.module.css create mode 100644 src/feature/Tickets/screens/Tickets.tsx diff --git a/src/AppWithMaps.tsx b/src/AppWithMaps.tsx index 03aa036..2264bf3 100644 --- a/src/AppWithMaps.tsx +++ b/src/AppWithMaps.tsx @@ -7,6 +7,8 @@ import PartnershipPage from "./feature/Partnership/screens/partnershipPage"; import Login from "./feature/Login/Login"; import Register from "./feature/Register/Register"; import Dashboard from "./feature/Dashboard/Dashboard"; +import TicketsPage from "./feature/Tickets/screens/Tickets"; +import PunishRestaurantPage from "./feature/Tickets/screens/PunishRestaurant"; import type { Libraries } from '@react-google-maps/api'; const GOOGLE_MAPS_LIBRARIES: Libraries = ['places']; @@ -33,6 +35,8 @@ const AppWithMaps = () => { } /> } /> } /> + } /> + } /> } /> diff --git a/src/feature/Login/Login.tsx b/src/feature/Login/Login.tsx index 832e2fd..1cde94a 100644 --- a/src/feature/Login/Login.tsx +++ b/src/feature/Login/Login.tsx @@ -10,13 +10,18 @@ import { IoArrowBack } from 'react-icons/io5'; const Login: React.FC = () => { const dispatch = useDispatch(); const navigate = useNavigate(); - const { email, password, loading, error, token } = useSelector((state: RootState) => state.user); + const { email, password, loading, error, token, role } = useSelector((state: RootState) => state.user); useEffect(() => { if (token) { - navigate('/dashboard'); + // Redirect based on user role + if (role === 'support') { + navigate('/tickets'); + } else { + navigate('/dashboard'); + } } - }, [token, navigate]); + }, [token, role, navigate]); const handleLogin = async () => { try { @@ -31,7 +36,7 @@ const Login: React.FC = () => { if (result && result.token) { dispatch(setToken(result.token)); - navigate('/dashboard'); + // The useEffect above will handle redirection after the role is set } } catch (err) { console.error("Failed to login:", err); diff --git a/src/feature/Tickets/screens/PunishRestaurant.module.css b/src/feature/Tickets/screens/PunishRestaurant.module.css new file mode 100644 index 0000000..d2301e7 --- /dev/null +++ b/src/feature/Tickets/screens/PunishRestaurant.module.css @@ -0,0 +1,252 @@ +.punishPage { + min-height: 100vh; + background-color: #f5f7fa; +} + +.contentContainer { + max-width: 900px; + margin: 0 auto; + padding: 20px; +} + +.navigationHeader { + margin-bottom: 20px; +} + +.backButton { + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + color: #1890ff; + cursor: pointer; + font-size: 16px; + padding: 0; +} + +.backButton:hover { + text-decoration: underline; +} + +.pageHeader { + display: flex; + justify-content: flex-end; + align-items: flex-start; + margin-bottom: 20px; +} + +.userInfo { + text-align: right; +} + +.currentUser { + font-weight: 500; + color: #333; + display: block; + margin-bottom: 4px; +} + +.currentDate { + display: block; + color: #666; + font-size: 14px; +} + +h1 { + font-size: 28px; + font-weight: 600; + color: #333; + margin-bottom: 24px; +} + +.error { + background-color: #fff1f0; + border: 1px solid #ffccc7; + color: #f5222d; + padding: 12px 16px; + margin-bottom: 20px; + border-radius: 4px; +} + +.success { + background-color: #f6ffed; + border: 1px solid #b7eb8f; + color: #52c41a; + padding: 12px 16px; + margin-bottom: 20px; + border-radius: 4px; +} + +.loadingContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 0; +} + +.loadingSpinner { + border: 4px solid rgba(0, 0, 0, 0.1); + border-radius: 50%; + border-top: 4px solid #4caf50; + width: 30px; + height: 30px; + animation: spin 1s linear infinite; + margin-bottom: 15px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.notFound { + text-align: center; + color: #999; + padding: 40px 0; + font-size: 18px; +} + +.punishmentContainer { + background-color: white; + border-radius: 8px; + padding: 24px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.restaurantCard { + display: flex; + gap: 20px; + padding-bottom: 24px; + margin-bottom: 24px; + border-bottom: 1px solid #f0f0f0; +} + +.restaurantImageContainer { + flex: 0 0 120px; + height: 120px; + overflow: hidden; + border-radius: 8px; + background-color: #f0f0f0; +} + +.restaurantImage { + width: 100%; + height: 100%; + object-fit: cover; +} + +.restaurantDetails { + flex: 1; +} + +.restaurantDetails h2 { + font-size: 22px; + margin-bottom: 16px; + color: #333; +} + +.restaurantInfo { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px 16px; +} + +.restaurantInfo p { + margin: 0; + font-size: 14px; + color: #666; +} + +.punishmentForm { + margin-top: 24px; +} + +.formGroup { + margin-bottom: 20px; +} + +.formGroup label { + display: block; + font-weight: 600; + margin-bottom: 8px; + color: #333; +} + +.formGroup select, +.formGroup textarea { + width: 100%; + padding: 12px; + border: 1px solid #d9d9d9; + border-radius: 4px; + font-size: 16px; +} + +.formGroup select { + background-color: white; + height: 48px; +} + +.formGroup textarea { + min-height: 120px; + resize: vertical; +} + +.formActions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 24px; +} + +.cancelButton, +.submitButton { + padding: 10px 20px; + font-size: 16px; + font-weight: 500; + border-radius: 4px; + cursor: pointer; +} + +.cancelButton { + background-color: white; + border: 1px solid #d9d9d9; + color: #666; +} + +.cancelButton:hover { + background-color: #fafafa; + border-color: #999; +} + +.submitButton { + background-color: #f5222d; + border: none; + color: white; +} + +.submitButton:hover { + background-color: #cf1322; +} + +.submitButton:disabled, +.cancelButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +@media (max-width: 768px) { + .restaurantCard { + flex-direction: column; + } + + .restaurantImageContainer { + width: 100%; + height: 180px; + } + + .restaurantInfo { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/src/feature/Tickets/screens/PunishRestaurant.tsx b/src/feature/Tickets/screens/PunishRestaurant.tsx new file mode 100644 index 0000000..e904d34 --- /dev/null +++ b/src/feature/Tickets/screens/PunishRestaurant.tsx @@ -0,0 +1,263 @@ +import React, { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import styles from './PunishRestaurant.module.css'; +import { RootState } from '../../../redux/store'; +import Header from '../../Header/Header'; +import { IoArrowBack } from 'react-icons/io5'; +import { API_BASE_URL } from "../../../redux/Api/apiService.ts"; + +interface Restaurant { + id: number; + restaurantName: string; + restaurantDescription: string; + longitude: number; + latitude: number; + category: string; + image_url: string; + rating: number | null; + workingDays: string[]; + restaurantEmail: string | null; + restaurantPhone: string | null; +} + +const PunishRestaurantPage: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { token, role } = useSelector((state: RootState) => state.user); + const [restaurant, setRestaurant] = useState(null); + const [durationType, setDurationType] = useState('ONE_WEEK'); + const [reason, setReason] = useState(''); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const [currentDate] = useState('2025-05-16 22:50:40'); + const [currentUser] = useState('emreutkan'); + + // Check if user is support + useEffect(() => { + if (!token) { + navigate('/login'); + return; + } + + if (role !== 'support') { + navigate('/dashboard'); + } + }, [token, role, navigate]); + + // Fetch restaurant details + useEffect(() => { + const fetchRestaurant = async () => { + if (!id || !token) return; + + try { + setLoading(true); + console.log('Fetching restaurant details for ID:', id, 'with token:', token); + + // Use the direct restaurant endpoint for detailed info + const response = await fetch(`${API_BASE_URL}/restaurants/${id}`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); + + if (response.status === 404) { + throw new Error('Restaurant not found'); + } + + if (!response.ok) { + const errorText = await response.text(); + console.error('API error response:', errorText); + throw new Error(`Failed to fetch restaurant: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + console.log('Restaurant data:', data); + setRestaurant(data); + setError(null); + } catch (err) { + console.error('Error fetching restaurant:', err); + setError('Failed to load restaurant details. Please try again later.'); + } finally { + setLoading(false); + } + }; + + fetchRestaurant(); + }, [token, id]); + + const handleReasonChange = (e: React.ChangeEvent) => { + setReason(e.target.value); + }; + + const handleDurationChange = (e: React.ChangeEvent) => { + setDurationType(e.target.value); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!reason.trim() || !token) { + setError('Please provide a reason for punishment'); + return; + } + + try { + setSubmitting(true); + setError(null); + + console.log('Submitting punishment for restaurant:', id); + + const response = await fetch(`${API_BASE_URL}/restaurants/${id}/punish`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + duration_type: durationType, + reason + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Failed to punish restaurant'); + } + + // Success + setSuccess(true); + + // Redirect back to tickets page after delay + setTimeout(() => { + navigate('/tickets'); + }, 2000); + + } catch (err) { + console.error('Error punishing restaurant:', err); + setError(err instanceof Error ? err.message : 'Failed to punish restaurant'); + } finally { + setSubmitting(false); + } + }; + + const handleGoBack = () => { + navigate('/tickets'); + }; + + return ( +
+
+ +
+
+ +
+ +

Issue Restaurant Punishment

+ +
+
+ Logged in as: {currentUser} + {currentDate} +
+
+ + {error &&
{error}
} + {success &&
Restaurant punished successfully! Redirecting...
} + + {loading ? ( +
+
+

Loading restaurant details...

+
+ ) : restaurant ? ( +
+
+ {restaurant.image_url && ( +
+ {restaurant.restaurantName} +
+ )} + +
+

{restaurant.restaurantName}

+
+

ID: {restaurant.id}

+

Category: {restaurant.category}

+

Working Days: {restaurant.workingDays?.join(', ') || 'N/A'}

+ {restaurant.rating !== null &&

Rating: {restaurant.rating.toFixed(1)}

} + {restaurant.restaurantEmail &&

Email: {restaurant.restaurantEmail}

} + {restaurant.restaurantPhone &&

Phone: {restaurant.restaurantPhone}

} +

Location: {restaurant.latitude}, {restaurant.longitude}

+
+
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ ) : ( +
Restaurant not found
+ )} +
+
+ ); +}; + +export default PunishRestaurantPage; \ No newline at end of file diff --git a/src/feature/Tickets/screens/Tickets.module.css b/src/feature/Tickets/screens/Tickets.module.css new file mode 100644 index 0000000..af3b0b3 --- /dev/null +++ b/src/feature/Tickets/screens/Tickets.module.css @@ -0,0 +1,444 @@ +.ticketsPage { + min-height: 100vh; + background-color: #f5f7fa; +} + +.contentContainer { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +.pageHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.userInfo { + text-align: right; +} + +.currentUser { + font-weight: 500; + color: #333; + display: block; + margin-bottom: 4px; +} + +.currentDate { + display: block; + color: #666; + font-size: 14px; +} + +h1 { + font-size: 28px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.error { + background-color: #fff1f0; + border: 1px solid #ffccc7; + color: #f5222d; + padding: 12px 16px; + margin-bottom: 20px; + border-radius: 4px; +} + +/* Restaurant section */ +.restaurantsSection { + margin-bottom: 30px; + background-color: white; + border-radius: 8px; + padding: 20px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.sectionHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.sectionHeader h2 { + font-size: 20px; + margin-bottom: 0; + color: #333; + font-weight: 600; +} + +.radiusSelector { + display: flex; + align-items: center; + gap: 8px; +} + +.locationIcon { + color: #1890ff; + font-size: 18px; +} + +.radiusSelector label { + color: #666; + font-size: 14px; +} + +.radiusSelect { + padding: 6px 8px; + border: 1px solid #d9d9d9; + border-radius: 4px; + font-size: 14px; +} + +.restaurantList { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); + gap: 16px; +} + +.restaurantCard { + background-color: #f9f9f9; + border-radius: 8px; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} + +.restaurantImageWrapper { + height: 140px; + overflow: hidden; + background-color: #e8e8e8; +} + +.restaurantImage { + width: 100%; + height: 100%; + object-fit: cover; +} + +.restaurantInfo { + padding: 16px; + flex: 1; +} + +.restaurantInfo h3 { + font-size: 18px; + margin-bottom: 8px; + color: #333; +} + +.restaurantDescription { + font-size: 14px; + color: #666; + margin-bottom: 8px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.restaurantCategory { + display: inline-block; + background-color: #e6f7ff; + color: #1890ff; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + margin-bottom: 8px; +} + +.restaurantMeta { + display: flex; + justify-content: space-between; + margin-top: 8px; +} + +.rating { + font-weight: 600; + color: #faad14; +} + +.distance { + color: #52c41a; + font-weight: 500; +} + +.restaurantActions { + padding: 12px 16px; + background-color: rgba(0,0,0,0.03); + border-top: 1px solid #eee; +} + +.noResults { + text-align: center; + color: #999; + padding: 40px 0; + font-size: 16px; +} + +/* Tickets section */ +.ticketsSection { + background-color: white; + border-radius: 8px; + padding: 20px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.ticketsHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.ticketsHeader h2 { + font-size: 20px; + color: #333; + font-weight: 600; + margin-bottom: 0; +} + +.statusFilters { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.filterLabel { + display: flex; + align-items: center; + gap: 5px; + font-size: 14px; + color: #666; +} + +.filterOptions { + display: flex; + gap: 8px; +} + +.filterButton { + background-color: #f5f5f5; + border: 1px solid #d9d9d9; + border-radius: 4px; + padding: 6px 12px; + font-size: 14px; + cursor: pointer; +} + +.filterButton.active { + background-color: #e6f7ff; + border-color: #91d5ff; + color: #1890ff; +} + +.loadingContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 0; +} + +.loadingSpinner { + border: 4px solid rgba(0, 0, 0, 0.1); + border-radius: 50%; + border-top: 4px solid #4caf50; + width: 30px; + height: 30px; + animation: spin 1s linear infinite; + margin-bottom: 15px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.noTickets { + text-align: center; + color: #999; + padding: 40px 0; + font-size: 16px; +} + +/* Ticket cards */ +.ticketsList { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 16px; +} + +.ticketCard { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.ticketCard.pending { + border-left: 4px solid #faad14; +} + +.ticketCard.accepted { + border-left: 4px solid #52c41a; +} + +.ticketCard.completed { + border-left: 4px solid #1890ff; +} + +.ticketCard.rejected { + border-left: 4px solid #f5222d; +} + +.ticketHeader { + display: flex; + justify-content: space-between; + padding: 12px 16px; + background-color: #fafafa; + border-bottom: 1px solid #f0f0f0; +} + +.ticketId { + font-weight: 600; + color: #333; +} + +.ticketStatus span { + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.pendingStatus { + background-color: #fff7e6; + color: #faad14; +} + +.acceptedStatus { + background-color: #f6ffed; + color: #52c41a; +} + +.completedStatus { + background-color: #e6f7ff; + color: #1890ff; +} + +.rejectedStatus { + background-color: #fff1f0; + color: #f5222d; +} + +.ticketBody { + padding: 16px; + background-color: white; +} + +.ticketDetail { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + font-size: 14px; +} + +.detailLabel { + color: #8c8c8c; + font-weight: 500; +} + +.detailValue { + color: #333; + font-weight: 600; +} + +.ticketActions { + display: flex; + gap: 8px; + padding: 12px 16px; + background-color: #fafafa; + border-top: 1px solid #f0f0f0; +} + +.disregardButton, +.punishButton { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + border-radius: 4px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + border: none; +} + +.disregardButton { + background-color: #ff4d4f; + color: white; +} + +.disregardButton:hover { + background-color: #cf1322; +} + +.punishButton { + background-color: #faad14; + color: white; +} + +.punishButton:hover { + background-color: #d48806; +} + +@media (max-width: 768px) { + .pageHeader { + flex-direction: column; + } + + .userInfo { + text-align: left; + margin-top: 8px; + } + + .sectionHeader { + flex-direction: column; + align-items: flex-start; + } + + .radiusSelector { + margin-top: 12px; + } + + .ticketsList { + grid-template-columns: 1fr; + } + + .ticketsHeader { + flex-direction: column; + align-items: flex-start; + } + + .statusFilters { + margin-top: 12px; + } + + .filterOptions { + margin-top: 8px; + } + + .restaurantList { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/src/feature/Tickets/screens/Tickets.tsx b/src/feature/Tickets/screens/Tickets.tsx new file mode 100644 index 0000000..f2b0438 --- /dev/null +++ b/src/feature/Tickets/screens/Tickets.tsx @@ -0,0 +1,418 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import styles from './Tickets.module.css'; +import { RootState } from '../../../redux/store'; +import Header from '../../Header/Header'; +import { IoFilter, IoCloseCircleOutline, IoBan, IoLocationOutline } from 'react-icons/io5'; +import { API_BASE_URL } from "../../../redux/Api/apiService.ts"; + +interface Ticket { + id: number; + user_id: number; + restaurant_id: number; + listing_title: string; + quantity: number; + total_price: string; + purchase_date: string; + status: string; + is_delivery: boolean; +} + +interface Restaurant { + id: number; + restaurantName: string; + restaurantDescription: string; + image_url: string; + rating: number | null; + category: string; + distance_km: number; +} + +// Default coordinates for Turkey (Ankara) +const TURKEY_COORDS = { + latitude: 39.9334, + longitude: 32.8597 +}; + +const TicketsPage: React.FC = () => { + const navigate = useNavigate(); + const { token, role } = useSelector((state: RootState) => state.user); + const [tickets, setTickets] = useState([]); + const [restaurants, setRestaurants] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [statusFilter, setStatusFilter] = useState('all'); + const [searchRadius, setSearchRadius] = useState(50); // Default 50km radius + const [currentDate] = useState('2025-05-16 22:50:40'); // Updated with your timestamp + const [currentUser] = useState('emreutkan'); // Updated with your username + + // Check if user is support role + useEffect(() => { + if (!token) { + navigate('/login'); + return; + } + + if (role !== 'support') { + navigate('/dashboard'); + } + }, [token, role, navigate]); + + // Fetch tickets data + useEffect(() => { + const fetchTickets = async () => { + if (!token) return; // Ensure token exists before making request + + try { + setLoading(true); + const response = await fetch(`${API_BASE_URL}/tickets`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('Failed to fetch tickets'); + } + + const data = await response.json(); + setTickets(data.tickets || []); + setError(null); + } catch (err) { + console.error('Error fetching tickets:', err); + setError('Failed to load tickets. Please try again later.'); + } finally { + setLoading(false); + } + }; + + if (token && role === 'support') { + fetchTickets(); + } + }, [token, role]); + + // Fetch initial restaurants in Turkey with token + useEffect(() => { + const fetchRestaurantsInTurkey = async () => { + if (!token) return; // Don't proceed without token + + try { + setLoading(true); + + console.log('Fetching restaurants with token:', token); + + const response = await fetch(`${API_BASE_URL}/restaurants/proximity`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, // Important: Include token in Authorization header + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + latitude: TURKEY_COORDS.latitude, + longitude: TURKEY_COORDS.longitude, + radius: searchRadius + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('API error response:', errorText); + throw new Error(`Failed to fetch restaurants: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + console.log('Restaurants data:', data); + + if (data.restaurants && Array.isArray(data.restaurants)) { + setRestaurants(data.restaurants); + } else { + setRestaurants([]); + } + + setError(null); + } catch (err) { + console.error('Error fetching restaurants:', err); + setError('Failed to load restaurants. Please try again later.'); + } finally { + setLoading(false); + } + }; + + fetchRestaurantsInTurkey(); + }, [token, searchRadius]); + + const handleSearchRadiusChange = (e: React.ChangeEvent) => { + setSearchRadius(Number(e.target.value)); + }; + + const handleStatusFilterChange = (status: string) => { + setStatusFilter(status); + }; + + const handleDisregardTicket = async (ticketId: number) => { + if (!token || !window.confirm('Are you sure you want to disregard this ticket?')) return; + + try { + setLoading(true); + const response = await fetch(`${API_BASE_URL}/tickets/${ticketId}/disregard`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, // Pass token in the header + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('Failed to disregard ticket'); + } + + // Update ticket in the list + setTickets(tickets.map(ticket => + ticket.id === ticketId ? { ...ticket, status: 'REJECTED' } : ticket + )); + + setError(null); + } catch (err) { + console.error('Error disregarding ticket:', err); + setError('Failed to disregard ticket. Please try again.'); + } finally { + setLoading(false); + } + }; + + const getStatusClass = (status: string | undefined | null) => { + if (!status) return ''; + try { + const statusLower = status.toLowerCase(); + return styles[statusLower] || ''; + } catch (e) { + console.error("Error getting status class:", e); + return ''; + } + }; + + const handlePunishRestaurant = (restaurantId: number) => { + navigate(`/punish-restaurant/${restaurantId}`); + }; + + // Filter tickets based on selected status + const filteredTickets = statusFilter === 'all' + ? tickets + : tickets.filter(ticket => ticket.status === statusFilter); + + return ( +
+
+ +
+
+

Support Tickets Dashboard

+
+ Logged in as: {currentUser} + {currentDate} +
+
+ + {error &&
{error}
} + +
+
+

Restaurants in Turkey

+
+ + + +
+
+ + {loading && restaurants.length === 0 ? ( +
+
+

Loading restaurants...

+
+ ) : restaurants.length === 0 ? ( +
+

No restaurants found within {searchRadius} km radius. Try increasing the search radius.

+
+ ) : ( +
+ {restaurants.map(restaurant => ( +
+
+ {restaurant.image_url && ( + {restaurant.restaurantName} + )} +
+
+

{restaurant.restaurantName}

+

{restaurant.restaurantDescription}

+

{restaurant.category}

+
+ + Rating: {restaurant.rating ? restaurant.rating.toFixed(1) : "No ratings"} + + + {restaurant.distance_km.toFixed(1)} km away + +
+
+
+ +
+
+ ))} +
+ )} +
+ +
+
+

Tickets

+
+
+ Filter by status: +
+
+ + + + + +
+
+
+ + {loading && tickets.length === 0 ? ( +
+
+

Loading tickets...

+
+ ) : filteredTickets.length === 0 ? ( +
+

No tickets found

+
+ ) : ( +
+ {filteredTickets.map(ticket => ( +
+
+
Ticket #{ticket.id}
+
+ {ticket.status === 'PENDING' && Pending} + {ticket.status === 'ACCEPTED' && Accepted} + {ticket.status === 'COMPLETED' && Completed} + {ticket.status === 'REJECTED' && Rejected} +
+
+
+
+ Order + {ticket.listing_title} +
+
+ Quantity + {ticket.quantity} +
+
+ Total + ${ticket.total_price} +
+
+ Date + + {new Date(ticket.purchase_date).toLocaleDateString()} + +
+
+ User ID + {ticket.user_id} +
+
+ Restaurant ID + {ticket.restaurant_id} +
+
+ Delivery + + {ticket.is_delivery ? 'Yes' : 'No'} + +
+
+
+ {ticket.status === 'PENDING' && ( + + )} + +
+
+ ))} +
+ )} +
+
+
+ ); +}; + +export default TicketsPage; \ No newline at end of file diff --git a/src/redux/Api/apiService.ts b/src/redux/Api/apiService.ts index eda9e9e..8a1aaf8 100644 --- a/src/redux/Api/apiService.ts +++ b/src/redux/Api/apiService.ts @@ -1,6 +1,7 @@ import {logout} from "../slices/userSlice.ts"; -export const API_BASE_URL = 'https://freshdealbackend.azurewebsites.net/v1'; +// export const API_BASE_URL = 'https://freshdealbackend.azurewebsites.net/v1'; +export const API_BASE_URL = 'http://192.168.1.6:8000/v1'; export const TOKEN_KEY = 'userToken'; diff --git a/src/redux/slices/userSlice.ts b/src/redux/slices/userSlice.ts index 4852275..9c92541 100644 --- a/src/redux/slices/userSlice.ts +++ b/src/redux/slices/userSlice.ts @@ -22,7 +22,7 @@ interface UserState { token: string | null; loading: boolean; error: string | null; - role: "owner" | "customer" | ""; + role: "owner" | "customer" | "support" | ""; // Added "support" role } const storedToken = getStoredToken(); From a6db7479fcd74f7c1d6afb3063748fc20efb0dd6 Mon Sep 17 00:00:00 2001 From: Irfan Emre Utkan <127414322+emreutkan@users.noreply.github.com> Date: Sat, 17 May 2025 13:45:40 +0300 Subject: [PATCH 2/3] refactor: remove unused user info and date state from Tickets component --- src/feature/Tickets/screens/Tickets.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/feature/Tickets/screens/Tickets.tsx b/src/feature/Tickets/screens/Tickets.tsx index f2b0438..0ea557f 100644 --- a/src/feature/Tickets/screens/Tickets.tsx +++ b/src/feature/Tickets/screens/Tickets.tsx @@ -44,8 +44,6 @@ const TicketsPage: React.FC = () => { const [error, setError] = useState(null); const [statusFilter, setStatusFilter] = useState('all'); const [searchRadius, setSearchRadius] = useState(50); // Default 50km radius - const [currentDate] = useState('2025-05-16 22:50:40'); // Updated with your timestamp - const [currentUser] = useState('emreutkan'); // Updated with your username // Check if user is support role useEffect(() => { @@ -210,10 +208,7 @@ const TicketsPage: React.FC = () => {

Support Tickets Dashboard

-
- Logged in as: {currentUser} - {currentDate} -
+
{error &&
{error}
} From d84c51923fa5a90d20e30a9e12e575dfd33cde5f Mon Sep 17 00:00:00 2001 From: Irfan Emre Utkan <127414322+emreutkan@users.noreply.github.com> Date: Sat, 17 May 2025 15:26:18 +0300 Subject: [PATCH 3/3] refactor: remove unused current user and date state from PunishRestaurant component --- src/feature/Tickets/screens/PunishRestaurant.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/feature/Tickets/screens/PunishRestaurant.tsx b/src/feature/Tickets/screens/PunishRestaurant.tsx index e904d34..29d0f9f 100644 --- a/src/feature/Tickets/screens/PunishRestaurant.tsx +++ b/src/feature/Tickets/screens/PunishRestaurant.tsx @@ -32,8 +32,7 @@ const PunishRestaurantPage: React.FC = () => { const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); - const [currentDate] = useState('2025-05-16 22:50:40'); - const [currentUser] = useState('emreutkan'); + // Check if user is support useEffect(() => { @@ -162,12 +161,7 @@ const PunishRestaurantPage: React.FC = () => {

Issue Restaurant Punishment

-
-
- Logged in as: {currentUser} - {currentDate} -
-
+ {error &&
{error}
} {success &&
Restaurant punished successfully! Redirecting...
}