diff --git a/index.html b/index.html index 844e5941..3e5e5a07 100644 --- a/index.html +++ b/index.html @@ -13,9 +13,9 @@ - + - Material Tailwind Dashboard React | By Creative Tim + S.S. 75 No'lu Yarımca Motorlu Taşıyıcılar Kooperatifi + + devto + + + + + + \ No newline at end of file diff --git a/public/img/klogo.png b/public/img/klogo.png new file mode 100644 index 00000000..e443fd5a Binary files /dev/null and b/public/img/klogo.png differ diff --git a/public/img/pattern.png b/public/img/pattern.png deleted file mode 100644 index 2b34e46f..00000000 Binary files a/public/img/pattern.png and /dev/null differ diff --git a/src/App.jsx b/src/App.jsx index 87826600..3dbbdc1e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,12 +1,29 @@ import { Routes, Route, Navigate } from "react-router-dom"; import { Dashboard, Auth } from "@/layouts"; +import PrivateRoute from "../src/component/PrivateRoute"; +import TVQueuePage from "./pages/dashboard/TVQueuePage"; function App() { return ( + + + + - } /> + + + + } + /> + + } /> + {/* Auth sayfaları (giriş, kayıt) herkes görebilir */} } /> - } /> + {/* Varsayılan yönlendirme */} + } /> ); } diff --git a/src/api/axiosConfig.js b/src/api/axiosConfig.js new file mode 100644 index 00000000..8fabae67 --- /dev/null +++ b/src/api/axiosConfig.js @@ -0,0 +1,29 @@ +import axios from "axios"; + + + +const apiClient = axios.create({ + baseURL: "https://localhost:7093/api", +}); + + +apiClient.interceptors.request.use( + (config) => { + + const token = localStorage.getItem("authToken"); + + + if (token) { + + config.headers.Authorization = `Bearer ${token}`; + } + + return config; + }, + (error) => { + + return Promise.reject(error); + } +); + +export default apiClient; \ No newline at end of file diff --git a/src/component/PrivateRoute.jsx b/src/component/PrivateRoute.jsx new file mode 100644 index 00000000..63f8e93b --- /dev/null +++ b/src/component/PrivateRoute.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Navigate } from "react-router-dom"; + +const PrivateRoute = ({ children }) => { + const token = localStorage.getItem("authToken"); + + if (!token) { + // Token yoksa login sayfasına yönlendir + return ; + } + + return children; +}; + +export default PrivateRoute; diff --git a/src/context/index.jsx b/src/context/index.jsx index 653a362d..232589ba 100644 --- a/src/context/index.jsx +++ b/src/context/index.jsx @@ -23,6 +23,10 @@ export function reducer(state, action) { } case "OPEN_CONFIGURATOR": { return { ...state, openConfigurator: action.value }; + } + // YENİ: Kullanıcı rolünü ayarlamak için yeni case eklendi. + case "SET_USER_ROLE": { + return { ...state, userRole: action.value }; } default: { throw new Error(`Unhandled action type: ${action.type}`); @@ -38,18 +42,20 @@ export function MaterialTailwindControllerProvider({ children }) { transparentNavbar: true, fixedNavbar: false, openConfigurator: false, + // YENİ: userRole state'i eklendi. Sayfa yenilendiğinde rolün kaybolmaması için localStorage'dan okunuyor. + userRole: localStorage.getItem("userRole") || null, }; const [controller, dispatch] = React.useReducer(reducer, initialState); const value = React.useMemo( - () => [controller, dispatch], - [controller, dispatch] + () => [controller, dispatch], + [controller, dispatch] ); return ( - - {children} - + + {children} + ); } @@ -58,7 +64,7 @@ export function useMaterialTailwindController() { if (!context) { throw new Error( - "useMaterialTailwindController should be used inside the MaterialTailwindControllerProvider." + "useMaterialTailwindController should be used inside the MaterialTailwindControllerProvider." ); } @@ -72,14 +78,18 @@ MaterialTailwindControllerProvider.propTypes = { }; export const setOpenSidenav = (dispatch, value) => - dispatch({ type: "OPEN_SIDENAV", value }); + dispatch({ type: "OPEN_SIDENAV", value }); export const setSidenavType = (dispatch, value) => - dispatch({ type: "SIDENAV_TYPE", value }); + dispatch({ type: "SIDENAV_TYPE", value }); export const setSidenavColor = (dispatch, value) => - dispatch({ type: "SIDENAV_COLOR", value }); + dispatch({ type: "SIDENAV_COLOR", value }); export const setTransparentNavbar = (dispatch, value) => - dispatch({ type: "TRANSPARENT_NAVBAR", value }); + dispatch({ type: "TRANSPARENT_NAVBAR", value }); export const setFixedNavbar = (dispatch, value) => - dispatch({ type: "FIXED_NAVBAR", value }); + dispatch({ type: "FIXED_NAVBAR", value }); export const setOpenConfigurator = (dispatch, value) => - dispatch({ type: "OPEN_CONFIGURATOR", value }); + dispatch({ type: "OPEN_CONFIGURATOR", value }); + + +export const setUserRole = (dispatch, value) => + dispatch({ type: "SET_USER_ROLE", value }); \ No newline at end of file diff --git a/src/layouts/dashboard.jsx b/src/layouts/dashboard.jsx index 888a627a..22a9b1f1 100644 --- a/src/layouts/dashboard.jsx +++ b/src/layouts/dashboard.jsx @@ -1,14 +1,11 @@ -import { Routes, Route } from "react-router-dom"; -import { Cog6ToothIcon } from "@heroicons/react/24/solid"; -import { IconButton } from "@material-tailwind/react"; +import { Routes, Route, Navigate } from "react-router-dom"; // 👈 Navigate eklendi import { Sidenav, DashboardNavbar, - Configurator, Footer, } from "@/widgets/layout"; import routes from "@/routes"; -import { useMaterialTailwindController, setOpenConfigurator } from "@/context"; +import { useMaterialTailwindController } from "@/context"; export function Dashboard() { const [controller, dispatch] = useMaterialTailwindController(); @@ -24,25 +21,23 @@ export function Dashboard() { />
- - setOpenConfigurator(dispatch, true)} - > - - + {routes.map( ({ layout, pages }) => - layout === "dashboard" && + layout === "anasayfa" && pages.map(({ path, element }) => ( - + )) )} + + {/* 🚀 BURASI SİHİRLİ DOKUNUŞ: + Kullanıcı panel ana dizinine geldiğinde veya + bulunmayan bir sayfaya gittiğinde 'arac-siralar' açılsın. */} + } /> + } /> +
@@ -53,4 +48,4 @@ export function Dashboard() { Dashboard.displayName = "/src/layout/dashboard.jsx"; -export default Dashboard; +export default Dashboard; \ No newline at end of file diff --git a/src/pages/auth/index.js b/src/pages/auth/index.js index ca1bbcb6..425a5351 100644 --- a/src/pages/auth/index.js +++ b/src/pages/auth/index.js @@ -1,2 +1,2 @@ export * from "@/pages/auth/sign-in"; -export * from "@/pages/auth/sign-up"; + diff --git a/src/pages/auth/sign-in.jsx b/src/pages/auth/sign-in.jsx index 3b3da41a..01acffbf 100644 --- a/src/pages/auth/sign-in.jsx +++ b/src/pages/auth/sign-in.jsx @@ -1,126 +1,106 @@ +import React, { useState } from "react"; +import { Card, Input, Button, Typography } from "@material-tailwind/react"; +import { useNavigate } from "react-router-dom"; +import { jwtDecode } from "jwt-decode"; import { - Card, - Input, - Checkbox, - Button, - Typography, -} from "@material-tailwind/react"; -import { Link } from "react-router-dom"; - + useMaterialTailwindController, + setUserRole, +} from "@/context"; +import apiClient from "../../api/axiosConfig.js" export function SignIn() { + const [, dispatch] = useMaterialTailwindController(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const navigate = useNavigate(); + + const handleSignIn = async () => { + const loginData = { + username: username, + password: password, + }; + + try { + const response = await apiClient.post("/Auth/Login", loginData); + const token = response.data; + console.log(token); + + if (token) { + localStorage.setItem("authToken", token); + apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`; + + console.log("Giriş Başarılı!"); + const decodedToken = jwtDecode(token); + const userRoleClaim = decodedToken.role || decodedToken["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; + const userRole = userRoleClaim ? userRoleClaim.toLowerCase() : null; + console.log("Kullanıcı Rolü:", userRole); + + if (userRole) { + setUserRole(dispatch, userRole); + localStorage.setItem("userRole", userRole); + } + + navigate("/anasayfa/arac-siralari"); + } else { + alert("Giriş başarılı ancak sunucudan geçerli bir token alınamadı."); + } + } catch (error) { + console.error("Giriş sırasında hata:", error); + + if (error.response) { + alert("Kullanıcı adı veya şifre hatalı!"); + } else if (error.request) { + alert("Sunucuya bağlanılamadı. Ağ bağlantınızı kontrol edin."); + } else { + alert("Beklenmedik bir hata oluştu."); + } + } + }; + return ( -
-
-
- Sign In - Enter your email and password to Sign In. -
-
-
- - Your email - - - - Password - - +
+ +
+ Logo {/* Boyut ve boşluk ayarlandı */}
- - I agree the  - - Terms and Conditions - +
+
+ + Kullanıcı Adı - } - containerProps={{ className: "-ml-2.5" }} - /> - - -
- - Subscribe me to newsletter - - } - containerProps={{ className: "-ml-2.5" }} - /> - - - Forgot Password - - -
-
- -
+
+ + Şifre + + setPassword(e.target.value)} + /> +
+
- - Not registered? - Create account - - - -
-
- -
- -
+ +
); } -export default SignIn; +export default SignIn; \ No newline at end of file diff --git a/src/pages/dashboard/TVQueuePage.jsx b/src/pages/dashboard/TVQueuePage.jsx new file mode 100644 index 00000000..0f4e1351 --- /dev/null +++ b/src/pages/dashboard/TVQueuePage.jsx @@ -0,0 +1,167 @@ +import React, { useState, useEffect } from "react"; +import apiClient from "../../api/axiosConfig"; + +const TVQueuePage = () => { + const [data, setData] = useState([]); + const [currentIndex, setCurrentIndex] = useState(0); + const [time, setTime] = useState(new Date()); + const [loading, setLoading] = useState(true); + + const fetchData = async () => { + try { + const routesRes = await apiClient.get("/admin/routes"); + const routes = routesRes.data; + + if (!routes || routes.length === 0) { + setData([]); + setLoading(false); + return; + } + + const fullData = await Promise.all( + routes.map(async (route) => { + try { + const queueRes = await apiClient.get(`/routes/${route.id}/queue`); + return { + routeName: route.routeName, + vehicles: queueRes.data.map((v) => ({ plate: v.licensePlate })), + }; + } catch (err) { + return { routeName: route.routeName, vehicles: [] }; + } + }) + ); + setData(fullData); + } catch (err) { + console.error("Veri çekme hatası:", err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + const dataTimer = setInterval(fetchData, 30000); + const clockTimer = setInterval(() => setTime(new Date()), 1000); + return () => { clearInterval(dataTimer); clearInterval(clockTimer); }; + }, []); + + useEffect(() => { + if (data.length > 3) { + const slideTimer = setInterval(() => { + setCurrentIndex((prevIndex) => (prevIndex + 3) % data.length); + }, 60000); + return () => clearInterval(slideTimer); + } else { + setCurrentIndex(0); + } + }, [data.length]); + + const getVisibleRoutes = () => { + if (data.length === 0) return []; + if (data.length <= 3) return data; + return [ + data[currentIndex % data.length], + data[(currentIndex + 1) % data.length], + data[(currentIndex + 2) % data.length] + ]; + }; + + const currentRoutes = getVisibleRoutes(); + + if (loading && data.length === 0) { + return ( +
+ SİSTEM YÜKLENİYOR... +
+ ); + } + + return ( +
+ + {/* ÜST PANEL - KÜÇÜK VE ŞIK */} +
+
+
+ 75 +
+

ARAÇ TAKİP SİSTEMİ

+
+ +
+
+ {time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })} + + {time.toLocaleDateString('tr-TR', { day: '2-digit', month: 'long', year: 'numeric' })} + +
+
+
+ + {/* ANA GÜZERGAH ALANI */} +
+ {currentRoutes.map((route, idx) => ( +
+ + {/* Güzergah Başlığı */} +
+
+
+

+ {route.routeName} +

+
+ + {route.vehicles.length} ARAÇ + +
+ + {/* PLAKA LİSTESİ - MODERN YAZI STİLİ */} +
+ {[0, 1, 2].map((colIndex) => ( +
+ {Array.from({ length: 34 }).map((_, rowIndex) => { + const vehicleIndex = colIndex * 34 + rowIndex; + const vehicle = route.vehicles && route.vehicles[vehicleIndex]; + if (vehicleIndex >= 100) return null; + + return ( +
+ {/* Sıra Numarası - Sabit ve Net */} + + {String(vehicleIndex + 1).padStart(2, '0')} + + + {/* PLAKA - GÜÇLENDİRİLMİŞ STİL */} + + {vehicle ? vehicle.plate : ""} + +
+ ); + })} +
+ ))} +
+
+ ))} +
+ + {/* ALT BİLGİ */} +
+
+ {[...Array(Math.ceil(data.length / 3))].map((_, i) => ( +
+ ))} +
+ Canlı Sevkiyat Verisi +
+
+ ); +}; + +export default TVQueuePage; \ No newline at end of file diff --git a/src/pages/dashboard/dispatch.jsx b/src/pages/dashboard/dispatch.jsx new file mode 100644 index 00000000..2a7e0a27 --- /dev/null +++ b/src/pages/dashboard/dispatch.jsx @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from "react"; +import { + Typography, + Card, + Spinner, + Button, + CardHeader, + CardBody +} from "@material-tailwind/react"; +import { InformationCircleIcon, PaperAirplaneIcon } from "@heroicons/react/24/solid"; +import apiClient from "../../api/axiosConfig.js"; +import { toast } from 'react-toastify'; + +// Her bir araç için oluşturulacak kart bileşeni +function DispatchVehicleCard({ vehicle, index, onSendToEnd }) { + const isFirstThree = index < 3; + const cardBgColor = isFirstThree ? "bg-green-100" : "bg-blue-gray-50/70"; + const textColor = isFirstThree ? "text-green-900" : "text-blue-gray-700"; + const borderColor = isFirstThree ? "border-green-300" : "border-transparent"; + + return ( +
+
+ #{index + 1} + + {vehicle.licensePlate} + +
+ +
+ ); +} + +// Yeni Sayfa Bileşeni +export function DispatchPage() { + const [routesWithQueues, setRoutesWithQueues] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // TÜM FONKSİYONLAR BİLEŞENİN İÇİNDE + const fetchAllQueues = async () => { + try { + const response = await apiClient.get("/queues/all"); + setRoutesWithQueues(response.data); + } catch (err) { + setError("Sıra verileri yüklenirken bir hata oluştu."); + console.error(err); + } finally { + if (loading) setLoading(false); + } + }; + + useEffect(() => { + fetchAllQueues(); + }, []); + + const handleSendToEnd = async (routeId, vehicleId, licensePlate) => { + if (window.confirm(`'${licensePlate}' plakalı aracı sıranın sonuna göndermek istediğinizden emin misiniz?`)) { + try { + await apiClient.post(`/routes/${routeId}/queue/move-to-end`, { vehicleId }); + toast.info(`'${licensePlate}' plakalı araç sıranın sonuna gönderildi.`); + fetchAllQueues(); + } catch (error) { + console.error("Araç sona gönderilirken hata:", error); + toast.error("İşlem sırasında bir hata oluştu."); + } + } + }; + + if (loading) { + return
; + } + + if (error) { + return {error}; + } + + return ( +
+
+ {routesWithQueues.map(route => ( +
+ + +
+ + {route.routeName} + + + {route.queuedVehicles.length} Araç + +
+
+ + {route.queuedVehicles.length > 0 ? ( +
+ {route.queuedVehicles.map((vehicle, index) => ( + handleSendToEnd(route.routeId, vehicle.id, vehicle.licensePlate)} + /> + ))} +
+ ) : ( +
+ + + Sırada Araç Yok ! + +
+ )} +
+
+
+ ))} +
+
+ ); +} + +export default DispatchPage; \ No newline at end of file diff --git a/src/pages/dashboard/home.jsx b/src/pages/dashboard/home.jsx index 2c700669..44d418bf 100644 --- a/src/pages/dashboard/home.jsx +++ b/src/pages/dashboard/home.jsx @@ -1,258 +1,133 @@ -import React from "react"; -import { - Typography, - Card, - CardHeader, - CardBody, - IconButton, - Menu, - MenuHandler, - MenuList, - MenuItem, - Avatar, - Tooltip, - Progress, -} from "@material-tailwind/react"; -import { - EllipsisVerticalIcon, - ArrowUpIcon, -} from "@heroicons/react/24/outline"; -import { StatisticsCard } from "@/widgets/cards"; -import { StatisticsChart } from "@/widgets/charts"; -import { - statisticsCardsData, - statisticsChartsData, - projectsTableData, - ordersOverviewData, -} from "@/data"; -import { CheckCircleIcon, ClockIcon } from "@heroicons/react/24/solid"; +import React, { useState, useEffect } from "react"; +import { Typography, Card, Spinner, Button, CardHeader, CardBody } from "@material-tailwind/react"; +import { ForwardIcon, InformationCircleIcon } from "@heroicons/react/24/solid"; +import apiClient from "@/api/axiosConfig"; // Yolunu projene göre ayarla +import { useMaterialTailwindController } from "@/context"; +import { VehicleQueueCard } from "@/widgets/layout/VehicleQueueCard"; + +// 1. SignalR Kütüphanesini Çağırıyoruz +import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr"; export function Home() { - return ( -
-
- {statisticsCardsData.map(({ icon, title, footer, ...rest }) => ( - - {footer.value} -  {footer.label} - - } - /> - ))} -
-
- {statisticsChartsData.map((props) => ( - - -  {props.footer} - - } - /> - ))} -
-
- - -
- - Projects - - - - 30 done this month - -
- - - - - - - - Action - Another Action - Something else here - - -
- - - - - {["companies", "members", "budget", "completion"].map( - (el) => ( - - ) - )} - - - - {projectsTableData.map( - ({ img, name, members, budget, completion }, key) => { - const className = `py-3 px-5 ${ - key === projectsTableData.length - 1 - ? "" - : "border-b border-blue-gray-50" - }`; + const [controller] = useMaterialTailwindController(); + const { userRole } = controller; + + const [routesWithQueues, setRoutesWithQueues] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + + const HUB_URL = "https://localhost:7093/hubs/queue"; + + const fetchAllQueues = async () => { + try { + const response = await apiClient.get("/queues/all"); + setRoutesWithQueues(response.data); + } catch (err) { + console.error(err); + if (loading) setError("Veri yüklenemedi."); + } finally { + if (loading) setLoading(false); + } + }; + + useEffect(() => { + // İlk açılışta veriyi çek + fetchAllQueues(); + + // --- SIGNALR BAĞLANTISI --- + const connection = new HubConnectionBuilder() + .withUrl(HUB_URL) + .withAutomaticReconnect() // Bağlantı koparsa (internet giderse) tekrar dene + .configureLogging(LogLevel.Information) + .build(); + + connection.start() + .then(() => { + console.log("🟢 TV Ekranı: SignalR Bağlandı!"); + + // Backend'den "ReceiveQueueUpdate" mesajı gelince çalışacak + connection.on("ReceiveQueueUpdate", () => { + console.log("🔔 Güncelleme sinyali geldi! Liste yenileniyor..."); + fetchAllQueues(); // Verileri sunucudan tekrar iste + }); + }) + .catch(err => console.error("🔴 SignalR Bağlantı Hatası:", err)); + + // Sayfadan çıkınca bağlantıyı kopar + return () => { + connection.stop(); + }; + // Not: setInterval artık yok! + }, []); + + const handleNextVehicle = async (routeId) => { + try { + await apiClient.post(`/admin/vehicles/${routeId}/move-first-to-end`); + // Buradan fetchAllQueues çağırmamıza gerek yok, + // çünkü Backend işlem bitince SignalR ile "Güncelle" diyecek. + } catch (error) { + console.error("Hata:", error); + alert("İşlem başarısız."); + } + }; + + if (loading) return
; + if (error) return {error}; + + return ( +
+
+ {routesWithQueues.map(route => { + // Sadece AKTİF olanları göster + const activeVehicles = route.queuedVehicles.filter(v => v.isActive); return ( -
- - - - - +
+ + +
+ + {route.routeName} + + + {activeVehicles.length} Araç + +
+ {userRole === 'admin' && ( +
+ +
+ )} +
+ + {activeVehicles.length > 0 ? ( +
+ {activeVehicles.map((vehicle, index) => ( + + ))} +
+ ) : ( +
+ + Sırada Araç Yok +
+ )} +
+
+
); - } - )} - -
- - {el} - -
-
- - - {name} - -
-
- {members.map(({ img, name }, key) => ( - - - - ))} - - - {budget} - - -
- - {completion}% - - -
-
-
-
- - - - Orders Overview - - - - 24% this month - - - - {ordersOverviewData.map( - ({ icon, color, title, description }, key) => ( -
-
- {React.createElement(icon, { - className: `!w-5 !h-5 ${color}`, - })} -
-
- - {title} - - - {description} - -
-
- ) - )} -
-
-
-
- ); + })} +
+ + ); } -export default Home; +export default Home; \ No newline at end of file diff --git a/src/pages/dashboard/index.js b/src/pages/dashboard/index.js index 7651895e..bd3c66cb 100644 --- a/src/pages/dashboard/index.js +++ b/src/pages/dashboard/index.js @@ -2,3 +2,9 @@ export * from "@/pages/dashboard/home"; export * from "@/pages/dashboard/profile"; export * from "@/pages/dashboard/tables"; export * from "@/pages/dashboard/notifications"; +export * from "@/pages/dashboard/queuemanagementpage.jsx"; +export * from "@/pages/dashboard/dispatch"; + + + + diff --git a/src/pages/dashboard/notifications.jsx b/src/pages/dashboard/notifications.jsx index f4be88b0..d6515399 100644 --- a/src/pages/dashboard/notifications.jsx +++ b/src/pages/dashboard/notifications.jsx @@ -1,88 +1,210 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { Typography, - Alert, Card, CardHeader, CardBody, + Button, } from "@material-tailwind/react"; -import { InformationCircleIcon } from "@heroicons/react/24/outline"; +// TOAST ENTEGRASYONU +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +import apiClient from "../../api/axiosConfig.js"; +import { AddRouteModal } from "@/widgets/layout/AddRouteModal"; +import { EditRouteModal } from "@/widgets/layout/EditRouteModal"; export function Notifications() { - const [showAlerts, setShowAlerts] = React.useState({ - blue: true, - green: true, - orange: true, - red: true, - }); - const [showAlertsWithIcon, setShowAlertsWithIcon] = React.useState({ - blue: true, - green: true, - orange: true, - red: true, - }); - const alerts = ["gray", "green", "orange", "red"]; + const [routes, setRoutes] = useState([]); - return ( -
- - - - Alerts - - - - {alerts.map((color) => ( - setShowAlerts((current) => ({ ...current, [color]: false }))} - > - A simple {color} alert with an example link. Give - it a click if you like. - - ))} - - - - - - Alerts with Icon - - - - {alerts.map((color) => ( - - } - onClose={() => setShowAlertsWithIcon((current) => ({ - ...current, - [color]: false, - }))} + // Modal State'leri + const [addModalOpen, setAddModalOpen] = useState(false); + const handleOpenAddModal = () => setAddModalOpen(!addModalOpen); + + const [editModalOpen, setEditModalOpen] = useState(false); + const [currentRoute, setCurrentRoute] = useState(null); + + const handleOpenEditModal = (route) => { + setCurrentRoute(route); + setEditModalOpen(true); + }; + + const handleCloseEditModal = () => { + setEditModalOpen(false); + setCurrentRoute(null); + }; + + // Güzergah listesini API'den çeken ana fonksiyon + const fetchRoutes = async () => { + try { + const response = await apiClient.get("/admin/routes"); + setRoutes(response.data); + } catch (error) { + console.error("Güzergahları çekerken hata oluştu:", error); + // Hata durumunda Toast bildirimi + toast.error("Güzergahlar yüklenirken bir sorun oluştu."); + } + }; + + useEffect(() => { + fetchRoutes(); + }, []); + + // Ekleme, silme veya güncelleme sonrası listeyi yeniden çekmek için + const handleDataChange = (message) => { + fetchRoutes(); + // Ekleme/Düzenleme modal'ları kendi başarı toast'larını göstersin + // Silme işlemi için burası tetiklenecek + if(message) { + toast.error(message, { position: "top-right", autoClose: 3000 }); + } + }; + + // ------------------------------------ + // YENİ TOAST SİLME İŞLEMLERİ + // ------------------------------------ + + const handleDeleteToast = (routeId, routeName) => { + // window.confirm yerine Toast Onayı kullanıyoruz + toast.warn( +
+

+ **{routeName}** güzergahını silmek istediğinize emin misiniz? +

+
+ + +
+
, + { + position: "top-center", + autoClose: false, + closeOnClick: false, + draggable: false, + } + ); + }; + + const confirmDelete = async (routeId, routeName) => { + toast.dismiss(); // Onay Toast'ını kapat + + try { + await apiClient.delete(`/admin/routes/${routeId}`); + + // Başarı Toast'ı ve ardından veriyi yenile + handleDataChange(`${routeName} başarıyla silindi.`); + + } catch (error) { + console.error("Güzergah silinirken hata oluştu:", error); + toast.error("Güzergah silinirken bir hata oluştu."); + } + }; + +return ( + <> + {/* Toast Container'ı ekledik */} + + + {/* Modal bileşenleri */} + + + + {/* ANA KAPSAYICI DIV + - mx-auto ve max-w-screen-lg kaldırıldı. + - h-full (dikey) ve flex (varsayılan genişlik) ile alanı dolduracak. + */} +
+ + {/* Kartın da "h-full" olması gerekiyor */} + + + + Güzergahlar + +
+ Güzergah Ekle + + + + + + + {["Güzergah", "İşlem"].map((el) => ( + + ))} + + + + {routes.map((route) => ( + + + + + ))} + +
+ + {el} + +
+ + {route.routeName} + + + + +
+
+
+
+ ); } -export default Notifications; +export default Notifications; \ No newline at end of file diff --git a/src/pages/dashboard/profile.jsx b/src/pages/dashboard/profile.jsx index 0d9f0115..ccb3c2a2 100644 --- a/src/pages/dashboard/profile.jsx +++ b/src/pages/dashboard/profile.jsx @@ -1,221 +1,229 @@ -import { - Card, - CardBody, - CardHeader, - CardFooter, - Avatar, - Typography, - Tabs, - TabsHeader, - Tab, - Switch, - Tooltip, - Button, -} from "@material-tailwind/react"; -import { - HomeIcon, - ChatBubbleLeftEllipsisIcon, - Cog6ToothIcon, - PencilIcon, -} from "@heroicons/react/24/solid"; -import { Link } from "react-router-dom"; -import { ProfileInfoCard, MessageCard } from "@/widgets/cards"; -import { platformSettingsData, conversationsData, projectsData } from "@/data"; +import React, { useState, useEffect } from "react"; +import { Card, CardHeader, CardBody, Typography, Button } from "@material-tailwind/react"; +import {toast } from 'react-toastify'; +import apiClient from "../../api/axiosConfig.js"; +import { AddUserModal } from "@/widgets/layout/AddUserModal"; +import { EditUserModal } from "@/widgets/layout/EditUserModal"; export function Profile() { - return ( - <> -
-
-
- - -
-
- -
- - Richard Davis - - - CEO / Co-Founder - -
-
-
- - - - - App - - - - Message - - - - Settings - - - -
-
-
-
- - Platform Settings - -
- {platformSettingsData.map(({ title, options }) => ( -
- - {title} - -
- {options.map(({ checked, label }) => ( - - ))} -
-
- ))} -
-
- - - - -
- ), - }} - action={ - - - - } - /> -
- - Platform Settings - -
    - {conversationsData.map((props) => ( - - reply - - } - /> - ))} -
-
-
-
- - Projects - - - Architects design houses - -
- {projectsData.map( - ({ img, title, description, tag, route, members }) => ( - - setOpenAddModal(true); + const handleCloseAddModal = () => setOpenAddModal(false); + + const handleOpenEditModal = (user) => { + setCurrentUser(user); + setOpenEditModal(true); + }; + + const handleCloseEditModal = () => { + setCurrentUser(null); + setOpenEditModal(false); + }; + + const fetchUsers = async () => { + try { + setLoading(true); + const response = await apiClient.get("/Users"); + setUsers(response.data); + setError(null); + } catch (err) { + setError("Kullanıcı verileri yüklenirken bir hata oluştu."); + console.error(err); + // Hata mesajını sessizce konsola yazabilir veya toast ile gösterebiliriz + // toast.error("Veriler yüklenirken hata oluştu."); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchUsers(); + }, []); + + // --- MERKEZİ VERİ VE MESAJ YÖNETİMİ --- + const handleDataChange = (message) => { + // 1. Listeyi yenile + fetchUsers(); + + // 2. Mesaj varsa Toast göster + if(message) { + toast.success(message, { + position: "top-right", + autoClose: 3000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + theme: "light", + }); + } + }; + + + + // Ekleme işlemi bitince bu çalışacak + const onUserAddedCallback = () => { + handleDataChange("Kullanıcı başarıyla eklendi."); + }; + + // Güncelleme işlemi bitince bu çalışacak + const onUserUpdatedCallback = () => { + handleDataChange("Kullanıcı bilgileri başarıyla güncellendi."); + }; + + + // --- SİLME İŞLEMLERİ --- + const handleDeleteToast = (userId, userName) => { + toast.warn( +
+

+ **{userName}** adlı kullanıcıyı silmek istediğinizden emin misiniz? +

+
+ + +
+
, + { + position: "top-center", + autoClose: false, + closeOnClick: false, + draggable: false, + style: { zIndex: 999999 } + } + ); + }; + + const confirmDelete = async (userId, userName) => { + // Önce toast'ı kapatıyoruz + toast.dismiss(); + + try { + // API isteğini gönder + const response = await apiClient.delete(`/Users/${userId}`); + + // Eğer API başarılı dönerse listeyi güncelle + if (response.status === 200 || response.status === 204) { + handleDataChange(`'${userName}' adlı kullanıcı başarıyla silindi.`); + } + } catch (err) { + console.error("Kullanıcı silinirken hata:", err); + toast.error("Kullanıcı silinirken bir hata oluştu."); + } + }; + + if (loading) return
Kullanıcılar Yükleniyor...
; + if (error) return
{error}
; + + return ( + <> + + + + +
+ + + Kullanıcılar + - - - {tag} - - - {title} - - - {description} - + + + + + {["Tam Ad", "Plaka", "Telefon", "İşlem"].map((el) => ( + + ))} + + + + {users.map((user) => ( + + + + + + + ))} + +
+ {el} +
+ {user.fullName} + + {user.licensePlate || "-"} + + {user.phoneNumber || "-"} + +
+ { + e.preventDefault(); + handleOpenEditModal(user); + }} + > + Düzenle + + + { + e.preventDefault(); + handleDeleteToast(user.id, user.fullName); + }} + > + Sil + +
+
- - - - -
- {members.map(({ img, name }, key) => ( - - - - ))} -
-
-
- ) - )} +
-
- - - - ); + + ); } -export default Profile; +export default Profile; \ No newline at end of file diff --git a/src/pages/dashboard/queuemanagementpage.jsx b/src/pages/dashboard/queuemanagementpage.jsx new file mode 100644 index 00000000..031a0f02 --- /dev/null +++ b/src/pages/dashboard/queuemanagementpage.jsx @@ -0,0 +1,233 @@ +import React, { useState, useEffect, useMemo } from "react"; +import { + Typography, + Card, + CardHeader, + CardBody, + Button, + Select, + Option, + List, + ListItem, +} from "@material-tailwind/react"; +import apiClient from "@/api/axiosConfig"; +import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr"; + +export function QueueManagementPage() { + const [routes, setRoutes] = useState([]); + const [allVehicles, setAllVehicles] = useState([]); + const [selectedRoute, setSelectedRoute] = useState(null); + const [queuedVehicles, setQueuedVehicles] = useState([]); + const [vehicleToAdd, setVehicleToAdd] = useState(""); + const [loadingQueue, setLoadingQueue] = useState(false); + + // IP Adresine dikkat (https/http ve port) + const HUB_URL = "https://localhost:7093/hubs/queue"; + + const fetchBaseData = async () => { + try { + const [routesRes, vehiclesRes] = await Promise.all([ + apiClient.get("/admin/routes"), + apiClient.get("/admin/vehicles"), + ]); + setRoutes(routesRes.data); + setAllVehicles(vehiclesRes.data); + } catch (error) { + console.error("Veri hatası:", error); + } + }; + + const fetchQueueData = async () => { + if (!selectedRoute) return; + setLoadingQueue(true); + try { + const response = await apiClient.get(`/routes/${selectedRoute.id}/queue`); + setQueuedVehicles(response.data); + } catch (error) { + setQueuedVehicles([]); + } finally { + setLoadingQueue(false); + } + }; + + useEffect(() => { + fetchBaseData(); + const connection = new HubConnectionBuilder() + .withUrl(HUB_URL) + .withAutomaticReconnect() + .configureLogging(LogLevel.Information) + .build(); + + connection.start() + .then(() => { + console.log("🟢 Admin Paneli: SignalR Bağlandı!"); + connection.on("ReceiveQueueUpdate", () => { + console.log("🔔 Veri güncelleme sinyali alındı."); + fetchBaseData(); + }); + }) + .catch(err => console.error("🔴 Bağlantı Hatası:", err)); + + return () => connection.stop(); + }, []); + + useEffect(() => { + fetchQueueData(); + setVehicleToAdd(""); + }, [selectedRoute]); + + useEffect(() => { + const connection = new HubConnectionBuilder() + .withUrl(HUB_URL) + .withAutomaticReconnect() + .build(); + + connection.start().then(() => { + connection.on("ReceiveQueueUpdate", () => { + if (selectedRoute) { + apiClient.get(`/routes/${selectedRoute.id}/queue`) + .then(res => setQueuedVehicles(res.data)) + .catch(() => {}); + } + fetchBaseData(); + }); + }); + + return () => connection.stop(); + }, [selectedRoute]); + + const handleAddVehicleToQueue = async () => { + if (!vehicleToAdd || !selectedRoute) return; + try { + await apiClient.post(`/routes/${selectedRoute.id}/queue`, { vehicleId: vehicleToAdd }); + setVehicleToAdd(""); + } catch (error) { + alert(error.response?.data || "Hata."); + } + }; + + const handleRemoveVehicleFromQueue = async (vehicleId) => { + if (window.confirm("Emin misiniz?")) { + try { + await apiClient.delete(`/routes/${selectedRoute.id}/queue/${vehicleId}`); + } catch (error) { + alert("Hata."); + } + } + }; + + // --- DÜZELTME YAPILAN KISIM --- + const availableVehicles = useMemo(() => { + const queuedIds = new Set(queuedVehicles.map((v) => v.id)); + + // Sadece (Kuyrukta Olmayan) VE (Aktif Olan) araçları getir + return allVehicles.filter((v) => !queuedIds.has(v.id) && v.isActive); + + }, [allVehicles, queuedVehicles]); + // ----------------------------- + + const selectKey = selectedRoute + ? `select-${selectedRoute.id}-${availableVehicles.length}` + : "empty"; + + return ( +
+ + + Güzergah Sıra Yönetimi + + + {/* SOL PANEL */} +
+ Güzergah Seçin + + + {routes.map((route) => ( + setSelectedRoute(route)} + selected={selectedRoute?.id === route.id} + > + {route.routeName} + + ))} + + +
+ + {/* SAĞ PANEL */} +
+ {!selectedRoute ? ( +
+ Lütfen güzergah seçin +
+ ) : ( +
+ + Sıradaki Araçlar: {selectedRoute.routeName} + + +
+ + + + {["Sıra", "Plaka", "Şoför", "İşlem"].map((h) => ( + + ))} + + + + {loadingQueue ? ( + + ) : ( + queuedVehicles.map((vehicle, index) => ( + + + + + + + )) + )} + +
+ {h} +
Yükleniyor...
#{index + 1}{vehicle.licensePlate}{vehicle.userFullName} + +
+
+ +
+
+ +
+ +
+ {availableVehicles.length === 0 && ( + * Eklenecek uygun (aktif) araç bulunamadı. + )} +
+ )} +
+
+
+
+ ); +} + +export default QueueManagementPage; \ No newline at end of file diff --git a/src/pages/dashboard/tables.jsx b/src/pages/dashboard/tables.jsx index 3d453ed7..3f1fcc3c 100644 --- a/src/pages/dashboard/tables.jsx +++ b/src/pages/dashboard/tables.jsx @@ -1,221 +1,297 @@ +import React, { useState, useEffect } from "react"; import { Card, CardHeader, CardBody, Typography, - Avatar, + Button, Chip, - Tooltip, - Progress, + Tooltip } from "@material-tailwind/react"; -import { EllipsisVerticalIcon } from "@heroicons/react/24/outline"; -import { authorsTableData, projectsTableData } from "@/data"; +import { + ExclamationTriangleIcon, + ArchiveBoxXMarkIcon, + EyeSlashIcon +} from "@heroicons/react/24/solid"; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +import apiClient from "../../api/axiosConfig.js"; +import { AddVehicleModal } from "@/widgets/layout/AddVehicleModal"; +import { EditVehicleModal } from "@/widgets/layout/EditVehicleModal"; + +// --- YARDIMCI FONKSİYON --- +const toTitleCase = (str) => { + if (!str) return ""; + return str + .toLowerCase() + .split(' ') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +}; export function Tables() { + const [vehicles, setVehicles] = useState([]); + const [idleVehicles, setIdleVehicles] = useState([]); + + // Modal State'leri + const [addModalOpen, setAddModalOpen] = useState(false); + const handleOpenAddModal = () => setAddModalOpen(!addModalOpen); + + const [editModalOpen, setEditModalOpen] = useState(false); + const [currentVehicle, setCurrentVehicle] = useState(null); + + const handleOpenEditModal = (vehicle) => { + setCurrentVehicle(vehicle); + setEditModalOpen(true); + }; + + const handleCloseEditModal = () => { + setEditModalOpen(false); + setCurrentVehicle(null); + } + + // --- VERİ ÇEKME --- + const fetchVehicles = async () => { + try { + const response = await apiClient.get("/admin/vehicles"); + setVehicles(response.data); + } catch (error) { + console.error("Hata:", error); + toast.error("Araç listesi yüklenemedi."); + } + }; + + const fetchIdleVehicles = async () => { + try { + // Backend endpoint'i (Varsayılan 7 gün) + const response = await apiClient.get("/admin/vehicles/idle-warnings?days=0"); + setIdleVehicles(response.data); + } catch (error) { + console.error("Uyarı verisi çekilemedi:", error); + } + }; + + useEffect(() => { + fetchVehicles(); + fetchIdleVehicles(); + }, []); + + const handleDataChange = (message) => { + fetchVehicles(); + fetchIdleVehicles(); + if(message) toast.success(message, { position: "top-right", autoClose: 3000 }); + }; + + // --- AKSİYONLAR (UYARI TABLOSU) --- + const handleSetPassive = async (vehicle) => { + try { + await apiClient.patch(`/admin/vehicles/${vehicle.id}/set-active`, { isActive: false }); + toast.info(`${vehicle.licensePlate} pasife alındı.`); + setIdleVehicles(prev => prev.filter(v => v.id !== vehicle.id)); + fetchVehicles(); + } catch (error) { + toast.error("İşlem başarısız."); + } + }; + + const handleIgnoreWarning = (vehicleId) => { + setIdleVehicles(prev => prev.filter(v => v.id !== vehicleId)); + toast.success("Uyarı listeden kaldırıldı.", { autoClose: 1000 }); + }; + + // --- SİLME (ANA TABLO) --- + const handleDeleteToast = (id, plaka) => { + toast.warn( +
+

**{plaka}** silinsin mi?

+
+ + +
+
, + { position: "top-center", autoClose: false, closeOnClick: false } + ); + }; + + const confirmDelete = async (id, plaka) => { + toast.dismiss(); + try { + await apiClient.delete(`/admin/vehicles/${id}`); + handleDataChange(`${plaka} silindi.`); + } catch (error) { + toast.error("Silme işlemi başarısız."); + } + }; + return ( -
- - - - Authors Table - - - - - - - {["author", "function", "status", "employed", ""].map((el) => ( - - ))} - - - - {authorsTableData.map( - ({ img, name, email, job, online, date }, key) => { - const className = `py-3 px-5 ${ - key === authorsTableData.length - 1 - ? "" - : "border-b border-blue-gray-50" - }`; + <> + + + handleDataChange()} + /> + handleDataChange()} + vehicleToEdit={currentVehicle} + /> +
+ + {/* ========================== + 1. ANA ARAÇ TABLOSU + ========================== */} + + + Araç Listesi + + + +
- - {el} - -
+ + + {["Plaka", "Şoför Adı", "Telefon", "Durum", "İşlem"].map((el) => ( + + ))} + + + + {vehicles.map((vehicle, key) => { + const className = `py-3 px-5 ${key === vehicles.length - 1 ? "" : "border-b border-blue-gray-50"}`; return ( - - + + + + + - - - - - + + ); - } - )} - -
+ {el} +
-
- -
- - {name} - - - {email} - +
+ {vehicle.licensePlate} + + {toTitleCase(vehicle.driverName)} + + {vehicle.phoneNumber || "-"} + + + +
+ +
- -
- - {job[0]} - - - {job[1]} - - - - - - {date} - - - - Edit - -
-
-
- - - - Projects Table - - - - - - - {["companies", "members", "budget", "completion", ""].map( - (el) => ( - - ) - )} - - - - {projectsTableData.map( - ({ img, name, members, budget, completion }, key) => { - const className = `py-3 px-5 ${ - key === projectsTableData.length - 1 - ? "" - : "border-b border-blue-gray-50" - }`; + })} + {vehicles.length === 0 && } + +
- - {el} - -
Kayıtlı araç yok.
+
+
- return ( - - -
- - - {name} - -
- - - {members.map(({ img, name }, key) => ( - - - - ))} - - - - {budget} - - - -
- - {completion}% - - -
- - - - - - + {/* ========================== + 2. UYARI TABLOSU (ARTIK ANA TABLO İLE AYNI STİLDE) + - Header: Gray (Aynı) + - Gövde: Beyaz/Gri (Aynı) + ========================== */} + {idleVehicles.length > 0 && ( + + {/* Header Rengi 'orange' yerine 'gray' yapıldı */} + + {/* İkon hala var ama uyumlu duruyor */} + +
+ Hareketsiz Araçlar + + Son 7 gündür işlem görmeyen (sırası değişmeyen) araçlar. + +
+
+ + + + + + {["Plaka", "Şoför Adı", "Telefon", "Aksiyon"].map((el) => ( + // Border rengi ana tabloyla eşitlendi: border-blue-gray-50 + + ))} - ); - } - )} - -
+ {el} +
-
-
-
+ + + {idleVehicles.map((vehicle, key) => { + // Border rengi ana tabloyla eşitlendi + const className = `py-3 px-5 ${key === idleVehicles.length - 1 ? "" : "border-b border-blue-gray-50"}`; + return ( + // Hover efekti ana tabloyla eşitlendi + + +
+ {vehicle.licensePlate} +
+ + + {toTitleCase(vehicle.driverName)} + + + {vehicle.phoneNumber || "-"} + + + +
+ + + + + + + + + +
+ + + ); + })} + + + + + )} + +
+ ); } -export default Tables; +export default Tables; \ No newline at end of file diff --git a/src/routes.jsx b/src/routes.jsx index 3a5a8da0..dcb12dbe 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -1,13 +1,25 @@ import { HomeIcon, UserCircleIcon, - TableCellsIcon, - InformationCircleIcon, + TruckIcon, + MapIcon, + Squares2X2Icon, + QueueListIcon, + PaperAirplaneIcon, ServerStackIcon, - RectangleStackIcon, } from "@heroicons/react/24/solid"; -import { Home, Profile, Tables, Notifications } from "@/pages/dashboard"; -import { SignIn, SignUp } from "@/pages/auth"; + +import { + Home, + Profile, + Tables, + Notifications, + QueueManagementPage, + DispatchPage, +} from "@/pages/dashboard"; + +import { SignIn } from "@/pages/auth"; +import TVQueuePage from "./pages/dashboard/TVQueuePage"; const icon = { className: "w-5 h-5 text-inherit", @@ -15,52 +27,73 @@ const icon = { export const routes = [ { - layout: "dashboard", + layout: "anasayfa", pages: [ { - icon: , - name: "dashboard", - path: "/home", + icon: , + name: "Araç Sıraları", + path: "/arac-siralari", // URL artık böyle görünecek element: , }, + { + icon: , + name: "Sıra Yönetimi", + path: "/sira-yonetimi", + element: , + roles: ['admin'], + }, + { + icon: , + name: "Özel Görev", + path: "/ozel-gorev", + element: , + roles: ['admin'], + }, { icon: , - name: "profile", - path: "/profile", + name: "Kullanıcılar", + path: "/kullanicilar", element: , + roles: ['admin'], }, { - icon: , - name: "tables", - path: "/tables", + icon: , + name: "Araçlar", + path: "/araclar", element: , + roles: ['admin'], }, { - icon: , - name: "notifications", - path: "/notifications", + icon: , + name: "Güzergahlar", + path: "/guzergahlar", element: , + roles: ['admin'], }, ], }, { - title: "auth pages", + title: "Giriş İşlemleri", layout: "auth", pages: [ { icon: , - name: "sign in", - path: "/sign-in", + name: "Giriş Yap", + path: "/giris", element: , }, + ], + }, + { + layout: "tv", // Layout ismi farklı olduğu için menüde gözükmez + pages: [ { - icon: , - name: "sign up", - path: "/sign-up", - element: , + name: "TV Monitor", + path: "/monitor", + element: , }, ], }, ]; -export default routes; +export default routes; \ No newline at end of file diff --git a/src/widgets/layout/AddRouteModal.jsx b/src/widgets/layout/AddRouteModal.jsx new file mode 100644 index 00000000..6558ec01 --- /dev/null +++ b/src/widgets/layout/AddRouteModal.jsx @@ -0,0 +1,77 @@ +import React, { useState } from "react"; +import { + Button, + Dialog, + DialogHeader, + DialogBody, + DialogFooter, + Input, + Typography, +} from "@material-tailwind/react"; +// TOAST ENTEGRASYONU +import { toast } from 'react-toastify'; +import apiClient from "../../api/axiosConfig.js"; + +export function AddRouteModal({ open, handleOpen, onRouteAdded }) { + const [routeName, setRouteName] = useState(""); + const [localError, setLocalError] = useState(""); // Hata mesajını local'de tutuyoruz + + const clearForm = () => { + setRouteName(""); + setLocalError(""); + }; + + const handleClose = () => { + clearForm(); + handleOpen(); + }; + + const handleSubmit = async () => { + if (!routeName.trim()) { + setLocalError("Güzergah adı boş olamaz."); + toast.error("Güzergah adı boş olamaz."); // Toast ile de bildir + return; + } + + try { + const response = await apiClient.post("/admin/routes", { routeName }); + + // Standart alert yerine BAŞARI TOAST'ı göster + toast.success(`${routeName} başarıyla eklendi!`, { position: "top-right" }); + + onRouteAdded(response.data); // Ana listeyi yenile + handleClose(); + } catch (err) { + console.error("Güzergah eklenirken hata:", err); + + // Backend'den gelen spesifik hata mesajını çek ve TOAST ile göster + const apiError = err.response?.data?.message || err.response?.data?.error || "Bir hata oluştu."; + setLocalError(apiError); + toast.error(apiError); + } + }; + + return ( + + Yeni Güzergah Ekle + + {/* Local hata gösterimi için typography */} + {localError && {localError}} + setRouteName(e.target.value)} + error={!!localError} // Hata varsa input'u kırmızı yap + /> + + + + + + + ); +} \ No newline at end of file diff --git a/src/widgets/layout/AddUserModal.jsx b/src/widgets/layout/AddUserModal.jsx new file mode 100644 index 00000000..6e246371 --- /dev/null +++ b/src/widgets/layout/AddUserModal.jsx @@ -0,0 +1,137 @@ +import React, { useState } from "react"; +import { + Button, + Dialog, + DialogHeader, + DialogBody, + DialogFooter, + Input, + Typography, +} from "@material-tailwind/react"; +import { toast } from 'react-toastify'; // Sadece hatalar için kullanacağız +import apiClient from "../../api/axiosConfig.js"; + +export function AddUserModal({ open, handleOpen, onUserAdded }) { + const [formData, setFormData] = useState({ + fullName: "", + userName: "", + password: "", + confirmPassword: "", + }); + const [error, setError] = useState(""); + + const clearForm = () => { + setFormData({ fullName: "", userName: "", password: "", confirmPassword: "" }); + setError(""); + }; + + const handleClose = () => { + clearForm(); + handleOpen(); + }; + + const handleSubmit = async () => { + // 1. İstemci Tarafı Validasyonları + if (!formData.fullName || !formData.userName || !formData.password || !formData.confirmPassword) { + const msg = "Tüm zorunlu alanları doldurunuz."; + setError(msg); + toast.error(msg); // Hata mesajları kalabilir + return; + } + if (formData.password !== formData.confirmPassword) { + const msg = "Şifreler birbiriyle eşleşmiyor."; + setError(msg); + toast.error(msg); + return; + } + + try { + const payload = { + fullName: formData.fullName, + userName: formData.userName, + password: formData.password, + confirmPassword: formData.confirmPassword + }; + + await apiClient.post("/Users", payload); + + // --- DÜZELTME BURADA --- + // Buradaki toast.success satırını SİLDİK. + // Çünkü onUserAdded() fonksiyonu, Profile.jsx içindeki mesajı zaten tetikliyor. + + onUserAdded(); // Bu satır Profile.jsx'e "İşlem tamam, mesajı göster ve listeyi yenile" der. + handleClose(); + + } catch (err) { + console.error("Kullanıcı eklenirken hata:", err.response || err); + let errorMessage = "Kullanıcı eklenirken hata oluştu."; + + if (err.response) { + if (err.response.status === 409) { + errorMessage = err.response.data?.message || `'${formData.userName}' zaten kayıtlı.`; + } else { + errorMessage = err.response.data?.message || errorMessage; + } + } else { + errorMessage = "Sunucu ile bağlantı kurulamadı."; + } + + setError(errorMessage); + toast.error(errorMessage); + } + }; + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + setError(""); + }; + + return ( + + Yeni Kullanıcı Ekle + + {error && {error}} + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/widgets/layout/AddVehicleModal.jsx b/src/widgets/layout/AddVehicleModal.jsx new file mode 100644 index 00000000..fa4f2cc5 --- /dev/null +++ b/src/widgets/layout/AddVehicleModal.jsx @@ -0,0 +1,175 @@ +import React, { useState, useEffect } from "react"; +import { + Button, + Dialog, + DialogHeader, + DialogBody, + DialogFooter, + Input, + Select, + Option, + Typography, +} from "@material-tailwind/react"; +// TOAST ENTEGRASYONU +import { toast } from 'react-toastify'; +import apiClient from "../../api/axiosConfig.js"; + +// TÜRKİYE PLAKA FORMATI REGEX KURALI +const TURKISH_PLATE_REGEX = /^(\d{2})\s*([A-Z]{1,3})\s*(\d{1,4})$/; + +export function AddVehicleModal({ open, handleOpen, onVehicleAdded }) { + const [users, setUsers] = useState([]); + + // --- DEĞİŞİKLİK 1 --- + // 'appUserId' state'ini 'formData'dan ayırıyoruz. + // 'driverName' state'den tamamen kaldırıldı. + const [selectedUserId, setSelectedUserId] = useState(""); // Seçilen kullanıcı ID'si için ayrı state + const [formData, setFormData] = useState({ + licensePlate: "", + phoneNumber: "", + }); + + const [error, setError] = useState(""); + + useEffect(() => { + if (open) { + // Modal açıldığında kullanıcıları çek + apiClient.get("/Users/without-vehicle") + .then(response => { + setUsers(response.data); + setError(""); + }) + .catch(err => { + console.error("Kullanıcılar çekilirken hata oluştu:", err); + const msg = "Atanabilir kullanıcı listesi yüklenemedi."; + setError(msg); + toast.error(msg); + }); + } else { + // Modal kapandığında formu temizle + clearForm(); + } + }, [open]); + + // Input (Plaka, Telefon) değişiklikleri için + const handleChange = (e) => { + const { name, value } = e.target; + const newValue = name === 'licensePlate' ? value.toUpperCase() : value; + setFormData(prev => ({ ...prev, [name]: newValue })); + }; + + // --- DEĞİŞİKLİK 2 --- + // Select (Kullanıcı) değişikliği için + // Bu fonksiyon artık doğrudan 'selectedUserId' state'ini güncelliyor. + const handleSelectChange = (value) => { + setSelectedUserId(value); + }; + + const clearForm = () => { + // --- DEĞİŞİKLİK 3 --- + // Formu temizlerken her iki state'i de sıfırlıyoruz. + setFormData({ licensePlate: "", phoneNumber: "" }); + setSelectedUserId(""); + setError(""); + }; + + const handleClose = () => { + // clearForm(); // useEffect [open] içinde zaten çağrılıyor + handleOpen(); + } + + const handleSubmit = async () => { + // --- DEĞİŞİKLİK 4 --- + // Zorunlu alan kontrolü 'selectedUserId' üzerinden yapılıyor + if (!formData.licensePlate || !selectedUserId) { + const msg = "Plaka ve Kullanıcı alanları zorunludur."; + setError(msg); + toast.error(msg); + return; + } + + if (!TURKISH_PLATE_REGEX.test(formData.licensePlate.trim())) { + const msg = "Plaka formatı uygun değil."; + setError(msg); + toast.error(msg); + return; + } + + try { + // --- DEĞİŞİKLİK 5 --- + // Payload'u gönderirken 'formData' ve 'selectedUserId' state'lerini birleştiriyoruz. + const payload = { + licensePlate: formData.licensePlate.trim().replace(/\s+/g, ''), + phoneNumber: formData.phoneNumber, + appUserId: selectedUserId // Ayrı state'den gelen ID + }; + + await apiClient.post("/admin/vehicles", payload); + toast.success(`'${formData.licensePlate}' plakalı araç başarıyla eklendi!`, { position: "top-right" }); + + onVehicleAdded(); + handleClose(); + } catch (err) { + console.error("Araç eklenirken hata:", err.response || err); + const apiError = err.response?.data?.message || err.response?.data?.error || "Araç eklenirken beklenmedik bir hata oluştu."; + setError(apiError); + toast.error(apiError); + } + }; + + return ( + + Yeni Araç Ekle + + {error && {error}} + + + + {/* --- DEĞİŞİKLİK 6 --- */} + {/* Select'in 'value' prop'u artık 'selectedUserId' state'ine bağlı */} + + + {/* Şoför Adı input'u tamamen kaldırılmıştı */} + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/widgets/layout/EditRouteModal.jsx b/src/widgets/layout/EditRouteModal.jsx new file mode 100644 index 00000000..67629bb0 --- /dev/null +++ b/src/widgets/layout/EditRouteModal.jsx @@ -0,0 +1,96 @@ +import React, { useState, useEffect } from "react"; +import { + Button, + Dialog, + DialogHeader, + DialogBody, + DialogFooter, + Input, + Typography, + Checkbox, +} from "@material-tailwind/react"; +import apiClient from "../../api/axiosConfig.js"; + +export function EditRouteModal({ open, handleOpen, routeToEdit, onRouteUpdated }) { + const [formData, setFormData] = useState({ + routeName: "", + isActive: true, + }); + const [error, setError] = useState(""); + + // Modal'a düzenlenecek güzergah bilgisi geldiğinde formu doldur + useEffect(() => { + if (routeToEdit) { + setFormData({ + routeName: routeToEdit.routeName, + isActive: routeToEdit.isActive, + }); + setError(""); + } + }, [routeToEdit]); + + const handleChange = (e) => { + const { name, value, type, checked } = e.target; + setFormData(prev => ({ + ...prev, + [name]: type === "checkbox" ? checked : value, + })); + }; + + const clearForm = () => { + setFormData({ routeName: "", isActive: true }); + setError(""); + }; + + const handleClose = () => { + clearForm(); + handleOpen(); + }; + + const handleSubmit = async () => { + if (!formData.routeName.trim()) { + setError("Güzergah adı boş olamaz."); + return; + } + + try { + // PUT isteği ile güzergahı güncelle + await apiClient.put(`/admin/routes/${routeToEdit.id}`, formData); + alert("Güzergah başarıyla güncellendi!"); + onRouteUpdated(); // Ana listeyi yenilemesi için sinyal gönder + handleClose(); + } catch (err) { + console.error("Güzergah güncellenirken hata:", err); + setError(err.response?.data || "Bir hata oluştu."); + } + }; + + return ( + + Güzergahı Düzenle + + {error && {error}} + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/widgets/layout/EditUserModal.jsx b/src/widgets/layout/EditUserModal.jsx new file mode 100644 index 00000000..85c1f4b6 --- /dev/null +++ b/src/widgets/layout/EditUserModal.jsx @@ -0,0 +1,115 @@ +import React, { useState, useEffect } from "react"; +import { Button, Dialog, DialogHeader, DialogBody, DialogFooter, Input, Typography } from "@material-tailwind/react"; +import apiClient from "../../api/axiosConfig.js"; +import { toast } from 'react-toastify'; + +export function EditUserModal({ open, handleOpen, userToEdit, onUserUpdated }) { + const [formData, setFormData] = useState({ fullName: "", userName: "", password: "", confirmPassword: "" }); + + // ✨ Estetik Hata Bildirimi + const notifyError = (msg) => { + toast.error(msg, { + className: "border-l-4 border-red-500 bg-white shadow-xl rounded-lg", + bodyClassName: "text-blue-gray-800 font-medium text-sm", + icon: "❌" + }); + }; + + useEffect(() => { + if (userToEdit) { + setFormData({ + fullName: userToEdit.fullName || "", + userName: userToEdit.userName || "", + password: "", + confirmPassword: "" + }); + } + }, [userToEdit]); + + const handleSubmit = async () => { + // Form Kontrolü + if (formData.password && formData.password !== formData.confirmPassword) { + notifyError("Şifreler uyuşmuyor, lütfen kontrol edin!"); + return; + } + + const payload = { fullName: formData.fullName }; + if (formData.password) payload.password = formData.password; + + try { + await apiClient.put(`/Users/${userToEdit.id}`, payload); + + // Başarı mesajı Profile.jsx içindeki onUserUpdated propu üzerinden tetiklenir ✨ + onUserUpdated(); + handleOpen(); + } catch (err) { + const backendMsg = err.response?.data?.message || "Güncelleme sırasında bir sorun oluştu."; + notifyError(backendMsg); + } + }; + + return ( + + + Kullanıcı Düzenle + + +
+ Tam Ad + setFormData({...formData, fullName: e.target.value})} + size="lg" + /> +
+ +
+ Kullanıcı Adı (Değiştirilemez) + +
+ +
+ + 🔐 Şifre İşlemleri + + + Şifreyi değiştirmeyecekseniz alanları boş bırakınız. + + + setFormData({...formData, password: e.target.value})} + size="lg" + /> + setFormData({...formData, confirmPassword: e.target.value})} + size="lg" + /> +
+
+ + + + +
+ ); +} \ No newline at end of file diff --git a/src/widgets/layout/EditVehicleModal.jsx b/src/widgets/layout/EditVehicleModal.jsx new file mode 100644 index 00000000..51a8eed0 --- /dev/null +++ b/src/widgets/layout/EditVehicleModal.jsx @@ -0,0 +1,87 @@ +import React, { useState, useEffect } from "react"; +import { + Button, Dialog, DialogHeader, DialogBody, DialogFooter, Input, Checkbox, Typography +} from "@material-tailwind/react"; +import apiClient from "../../api/axiosConfig.js"; + +export function EditVehicleModal({ open, handleOpen, onVehicleUpdated, vehicleToEdit }) { + const [formData, setFormData] = useState({ + licensePlate: "", + driverName: "", // --- DEĞİŞİKLİK --- Bu state kalıyor, sadece disabled input'u doldurmak için. + phoneNumber: "", + isActive: true, + }); + const [error, setError] = useState(""); + + // Bu useEffect, modal'a düzenlenecek araç bilgisi geldiğinde formu doldurur. + useEffect(() => { + if (vehicleToEdit) { + setFormData({ + licensePlate: vehicleToEdit.licensePlate || "", + driverName: vehicleToEdit.driverName || "", // 'driverName'i state'e atıyoruz (API'den geliyor) + phoneNumber: vehicleToEdit.phoneNumber || "", + isActive: vehicleToEdit.isActive, + }); + } else { + setFormData({ licensePlate: "", driverName: "", phoneNumber: "", isActive: true }); + setError(""); + } + }, [vehicleToEdit]); + + const handleChange = (e) => { + const { name, value, type, checked } = e.target; + setFormData(prev => ({ + ...prev, + [name]: type === 'checkbox' ? checked : value + })); + }; + + const handleSubmit = async () => { + if (!formData.licensePlate) { + setError("Plaka alanı zorunludur."); + return; + } + if (!vehicleToEdit) return; + + try { + // --- DEĞİŞİKLİK --- + // API'ye gönderilecek payload'u manuel oluşturuyoruz. + // 'driverName' bu payload'a dahil edilmiyor. + const payload = { + licensePlate: formData.licensePlate, + phoneNumber: formData.phoneNumber, + isActive: formData.isActive + }; + + await apiClient.put(`/admin/vehicles/${vehicleToEdit.id}`, payload); + + alert("Araç başarıyla güncellendi!"); // Bunu da toast'a çevirebilirsiniz + onVehicleUpdated(); + handleOpen(); + } catch (err) { + console.error("Araç güncellenirken hata:", err); + setError(err.response?.data || "Güncelleme sırasında bir hata oluştu."); + } + }; + + return ( + + Araç Düzenle + + {error && {error}} + + + {/* --- DEĞİŞİKLİK --- */} + {/* Şoför Adı alanı artık 'disabled' (değiştirilemez) */} + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/widgets/layout/VehicleQueueCard.jsx b/src/widgets/layout/VehicleQueueCard.jsx new file mode 100644 index 00000000..056476f2 --- /dev/null +++ b/src/widgets/layout/VehicleQueueCard.jsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Typography } from "@material-tailwind/react"; + +export function VehicleQueueCard({ vehicle, index }) { +    const isFirstThree = index < 3; +    const cardBgColor = isFirstThree ? "bg-green-100" : "bg-blue-gray-50/70"; +    const textColor = isFirstThree ? "text-green-900" : "text-blue-gray-700"; +    const borderColor = isFirstThree ? "border-green-300" : "border-transparent"; + +    return ( +        // DEĞİŞİKLİK: Dikey boşluğu azaltmak için padding (p-1.5) küçültüldü. +       
+            {/* DEĞİŞİKLİK: Elemanlar arası boşluk (gap-2) azaltıldı. */} +           
+                {/* DEĞİŞİKLİK: Sıra numarasının fontu (text-md) küçültüldü. */} +                +                    #{index + 1} +                +                +                    {vehicle.licensePlate} +                + +           
+       
+    ); +} + +export default VehicleQueueCard; \ No newline at end of file diff --git a/src/widgets/layout/configurator.jsx b/src/widgets/layout/_configurator.jsx similarity index 100% rename from src/widgets/layout/configurator.jsx rename to src/widgets/layout/_configurator.jsx diff --git a/src/widgets/layout/dashboard-navbar.jsx b/src/widgets/layout/dashboard-navbar.jsx index d91e23f7..427c44ab 100644 --- a/src/widgets/layout/dashboard-navbar.jsx +++ b/src/widgets/layout/dashboard-navbar.jsx @@ -1,196 +1,100 @@ -import { useLocation, Link } from "react-router-dom"; -import { - Navbar, - Typography, - Button, - IconButton, - Breadcrumbs, - Input, - Menu, - MenuHandler, - MenuList, - MenuItem, - Avatar, -} from "@material-tailwind/react"; -import { - UserCircleIcon, - Cog6ToothIcon, - BellIcon, - ClockIcon, - CreditCardIcon, - Bars3Icon, -} from "@heroicons/react/24/solid"; -import { - useMaterialTailwindController, - setOpenConfigurator, - setOpenSidenav, -} from "@/context"; +import React from "react"; +import { useLocation, Link, useNavigate } from "react-router-dom"; +import { IconButton, Navbar, Typography, Breadcrumbs } from "@material-tailwind/react"; +import { ArrowRightOnRectangleIcon, Bars3Icon } from "@heroicons/react/24/solid"; +import { useMaterialTailwindController, setOpenSidenav } from "@/context"; + +// ✨ URL'den gelen teknik ismi şık bir başlığa çeviren harita +const pageNamesMap = { + "arac-siralari": "Araç Sıraları", + "sira-yonetimi": "Sıra Yönetimi", + "ozel-gorev": "Özel Görev", + "kullanici-ayarlari": "Kullanıcılar", + "arac-listesi": "Araçlar", + "guzergahlar": "Güzergahlar", +}; export function DashboardNavbar() { - const [controller, dispatch] = useMaterialTailwindController(); - const { fixedNavbar, openSidenav } = controller; - const { pathname } = useLocation(); - const [layout, page] = pathname.split("/").filter((el) => el !== ""); + const [controller, dispatch] = useMaterialTailwindController(); + const { fixedNavbar, openSidenav } = controller; + const { pathname } = useLocation(); + const navigate = useNavigate(); + + // URL'yi parçalayıp layout (dashboard) ve sayfa (path) kısımlarını alıyoruz + const pathParts = pathname.split("/").filter((el) => el !== ""); + const layout = pathParts[0]; + const urlPath = pathParts[1]; + + // Eşleştirme listesinden Türkçe ismi çek, yoksa ham halini göster + const currentPageName = pageNamesMap[urlPath] || urlPath; - return ( - -
-
- { + localStorage.removeItem("authToken"); + localStorage.removeItem("userRole"); + navigate("/auth/giris"); // Yeni login path'imize yönlendiriyoruz + }; + + return ( + - - - {layout} - - - - {page} - - - - {page} - -
-
-
- -
- setOpenSidenav(dispatch, !openSidenav)} - > - - - - - - - - - - - - - - - - - -
- - New message from Laur - - - 13 minutes ago - -
-
- - -
- - New album by Travis Scott - - - 1 day ago - -
-
- -
- -
-
- - Payment successfully completed - - - 2 days ago + fullWidth + blurred={fixedNavbar} + > +
+
+ {/* Sayfa Yolu (Breadcrumbs) */} + + + + Anasayfa + + + + {currentPageName} + + + + {/* Büyük Sayfa Başlığı */} + + {currentPageName} +
+ + {/* Sağ Taraf: Mobil Menü ve Çıkış Butonu */} +
+ setOpenSidenav(dispatch, !openSidenav)} + > + + + + + +
- - -
- setOpenConfigurator(dispatch, true)} - > - - -
-
-
- ); + + + ); } DashboardNavbar.displayName = "/src/widgets/layout/dashboard-navbar.jsx"; -export default DashboardNavbar; +export default DashboardNavbar; \ No newline at end of file diff --git a/src/widgets/layout/footer.jsx b/src/widgets/layout/footer.jsx index 1ea98e53..f2c59643 100644 --- a/src/widgets/layout/footer.jsx +++ b/src/widgets/layout/footer.jsx @@ -8,33 +8,7 @@ export function Footer({ brandName, brandLink, routes }) { return (
- - © {year}, made with{" "} - by{" "} - - {brandName} - {" "} - for a better web. - -
    - {routes.map(({ name, path }) => ( -
  • - - {name} - -
  • - ))} -
+
); diff --git a/src/widgets/layout/index.js b/src/widgets/layout/index.js index e4fd0383..e1bd44ba 100644 --- a/src/widgets/layout/index.js +++ b/src/widgets/layout/index.js @@ -1,5 +1,4 @@ export * from "@/widgets/layout/sidenav"; export * from "@/widgets/layout/dashboard-navbar"; -export * from "@/widgets/layout/configurator"; export * from "@/widgets/layout/footer"; export * from "@/widgets/layout/navbar"; diff --git a/src/widgets/layout/sidenav.jsx b/src/widgets/layout/sidenav.jsx index cc7e6ffe..b6e88aa6 100644 --- a/src/widgets/layout/sidenav.jsx +++ b/src/widgets/layout/sidenav.jsx @@ -1,17 +1,14 @@ +import React from "react"; import PropTypes from "prop-types"; -import { Link, NavLink } from "react-router-dom"; +import { NavLink } from "react-router-dom"; import { XMarkIcon } from "@heroicons/react/24/outline"; -import { - Avatar, - Button, - IconButton, - Typography, -} from "@material-tailwind/react"; +import { Button, IconButton, Typography } from "@material-tailwind/react"; import { useMaterialTailwindController, setOpenSidenav } from "@/context"; -export function Sidenav({ brandImg, brandName, routes }) { +export function Sidenav({ brandName, routes }) { const [controller, dispatch] = useMaterialTailwindController(); - const { sidenavColor, sidenavType, openSidenav } = controller; + const { sidenavColor, sidenavType, openSidenav, userRole } = controller; + const sidenavTypes = { dark: "bg-gradient-to-br from-gray-800 to-gray-900", white: "bg-white shadow-sm", @@ -19,93 +16,80 @@ export function Sidenav({ brandImg, brandName, routes }) { }; return ( - + + + ); } Sidenav.defaultProps = { - brandImg: "/img/logo-ct.png", - brandName: "Material Tailwind React", + brandName: "S.S. 75 NO'LU KOOP", }; Sidenav.propTypes = { - brandImg: PropTypes.string, brandName: PropTypes.string, - routes: PropTypes.arrayOf(PropTypes.object).isRequired, + routes: PropTypes.array.isRequired, }; -Sidenav.displayName = "/src/widgets/layout/sidnave.jsx"; - -export default Sidenav; +export default Sidenav; \ No newline at end of file