-
- Projects
-
-
- Architects design houses
-
-
- {projectsData.map(
- ({ img, title, description, tag, route, members }) => (
-
-
-
+
+
+
+
+
+
+
+
+
+ Kullanıcılar
+
+
-
-
- {tag}
-
-
- {title}
-
-
- {description}
-
+
+
+
+
+ {["Tam Ad", "Plaka", "Telefon", "İşlem"].map((el) => (
+ |
+
+ {el}
+
+ |
+ ))}
+
+
+
+ {users.map((user) => (
+
+ |
+ {user.fullName}
+ |
+
+ {user.licensePlate || "-"}
+ |
+
+ {user.phoneNumber || "-"}
+ |
+
+
+ {
+ e.preventDefault();
+ handleOpenEditModal(user);
+ }}
+ >
+ Düzenle
+
+
+ {
+ e.preventDefault();
+ handleOpenDeleteModal(user);
+ }}
+ >
+ 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..19fd5a40
--- /dev/null
+++ b/src/pages/dashboard/queuemanagementpage.jsx
@@ -0,0 +1,346 @@
+// src/pages/dashboard/queuemanagementpage.jsx
+import React, { useState, useEffect, useMemo } from "react";
+import {
+ Typography, Card, CardHeader, CardBody,
+ Button, Select, Option, List, ListItem,
+} from "@material-tailwind/react";
+import {
+ DndContext,
+ closestCenter,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from "@dnd-kit/core";
+import {
+ arrayMove,
+ SortableContext,
+ useSortable,
+ verticalListSortingStrategy,
+} from "@dnd-kit/sortable";
+// ❌ CSS import YOK - uyumsuz olduğu için kaldırıldı
+import apiClient from "@/api/axiosConfig";
+import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr";
+import { toast } from "react-toastify";
+
+const HUB_URL = "https://75ymkt.com/hubs/queue";
+
+// --- Sürüklenebilir Satır ---
+function SortableRow({ vehicle, index, onRemove }) {
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id: vehicle.id });
+
+ // CSS import yerine manuel transform string
+ const transformStr = transform
+ ? `translate3d(${transform.x}px, ${transform.y}px, 0) scaleX(${transform.scaleX ?? 1}) scaleY(${transform.scaleY ?? 1})`
+ : undefined;
+
+ const style = {
+ transform: transformStr,
+ transition: transition ?? undefined,
+ opacity: isDragging ? 0.5 : 1,
+ backgroundColor: isDragging ? "#eff6ff" : "white",
+ position: "relative",
+ };
+
+ return (
+
+
+
+ {/* Hamburger ikonu - Heroicons olmadan */}
+
+
+ |
+
+ #{index + 1}
+ |
+ {vehicle.licensePlate} |
+ {vehicle.userFullName || vehicle.driverName || "-"} |
+
+
+ |
+
+ );
+}
+
+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);
+ const [isSaving, setIsSaving] = useState(false);
+
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ // Yanlışlıkla sürüklemeyi önlemek için 8px eşik
+ activationConstraint: { distance: 8 },
+ })
+ );
+
+ 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 (err) {
+ console.error("Veri hatası:", err);
+ }
+ };
+
+ const fetchQueueData = async () => {
+ if (!selectedRoute) return;
+ setLoadingQueue(true);
+ try {
+ const res = await apiClient.get(`/routes/${selectedRoute.id}/queue`);
+ setQueuedVehicles(res.data);
+ } catch {
+ setQueuedVehicles([]);
+ } finally {
+ setLoadingQueue(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchBaseData();
+ const connection = new HubConnectionBuilder()
+ .withUrl(HUB_URL)
+ .withAutomaticReconnect()
+ .configureLogging(LogLevel.Information)
+ .build();
+ connection.start().then(() => {
+ connection.on("ReceiveQueueUpdate", fetchBaseData);
+ }).catch(console.error);
+ return () => connection.stop();
+ }, []);
+
+ useEffect(() => {
+ fetchQueueData();
+ setVehicleToAdd("");
+ }, [selectedRoute]);
+
+ const handleDragEnd = async (event) => {
+ const { active, over } = event;
+ if (!over || active.id === over.id) return;
+
+ const oldIndex = queuedVehicles.findIndex((v) => v.id === active.id);
+ const newIndex = queuedVehicles.findIndex((v) => v.id === over.id);
+ const reordered = arrayMove(queuedVehicles, oldIndex, newIndex);
+
+ setQueuedVehicles(reordered); // Optimistic update
+
+ setIsSaving(true);
+ try {
+ await apiClient.post(`/routes/${selectedRoute.id}/queue/reorder`, {
+ orderedVehicleIds: reordered.map((v) => v.id),
+ });
+ toast.success("Sıra güncellendi!", { autoClose: 1500 });
+ } catch {
+ toast.error("Sıra kaydedilemedi.");
+ fetchQueueData(); // Hata varsa geri al
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleAddVehicleToQueue = async () => {
+ if (!vehicleToAdd || !selectedRoute) return;
+ try {
+ await apiClient.post(`/routes/${selectedRoute.id}/queue`, {
+ vehicleId: Number(vehicleToAdd),
+ });
+ setVehicleToAdd("");
+ await fetchQueueData();
+ } catch (err) {
+ toast.error(err.response?.data || "Araç eklenemedi.");
+ }
+ };
+
+ const handleRemoveVehicleFromQueue = async (vehicleId) => {
+ if (!window.confirm("Bu aracı sıradan çıkarmak istediğinize emin misiniz?")) return;
+ try {
+ await apiClient.delete(`/routes/${selectedRoute.id}/queue/${vehicleId}`);
+ await fetchQueueData();
+ } catch {
+ toast.error("Araç çıkarılamadı.");
+ }
+ };
+
+ const availableVehicles = useMemo(() => {
+ const queuedIds = new Set(queuedVehicles.map((v) => v.id));
+ 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
+
+ ☰ ikonuna basılı tutup sürükleyerek sıra değiştirebilirsiniz.
+
+
+
+
+ {/* SOL - Güzergah seçimi */}
+
+ Güzergah Seçin
+
+
+ {routes.map((route) => (
+ setSelectedRoute(route)}
+ selected={selectedRoute?.id === route.id}
+ className="cursor-pointer"
+ >
+ {route.routeName}
+
+ ))}
+
+
+
+
+ {/* SAĞ - Sıra tablosu */}
+
+ {!selectedRoute ? (
+
+ Lütfen soldan bir güzergah seçin
+
+ ) : (
+
+
+
+ {selectedRoute.routeName} — {queuedVehicles.length} Araç
+
+ {isSaving && (
+
+ ⏳ Kaydediliyor...
+
+ )}
+
+
+
+
+
+
+
+ {["", "Sıra", "Plaka", "Şoför", "İşlem"].map((h) => (
+ |
+
+ {h}
+
+ |
+ ))}
+
+
+ v.id)}
+ strategy={verticalListSortingStrategy}
+ >
+
+ {loadingQueue ? (
+
+ |
+ Yükleniyor...
+ |
+
+ ) : queuedVehicles.length === 0 ? (
+
+ |
+ Bu güzergahta sırada araç yok.
+ |
+
+ ) : (
+ queuedVehicles.map((vehicle, index) => (
+
+ ))
+ )}
+
+
+
+
+
+
+ {/* Araç ekleme */}
+
+
+
+
+
+
+ {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..2cb6775d 100644
--- a/src/pages/dashboard/tables.jsx
+++ b/src/pages/dashboard/tables.jsx
@@ -1,221 +1,325 @@
+import React, { useState, useEffect } from "react";
import {
- Card,
- CardHeader,
- CardBody,
- Typography,
- Avatar,
- Chip,
- Tooltip,
- Progress,
+ Card, CardHeader, CardBody, Typography, Button, Chip, 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";
+import { AssignUserModal } from "@/widgets/layout/AssignUserModal";
+
+const toTitleCase = (str) =>
+ !str ? "-" : str.toLowerCase().split(" ").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
export function Tables() {
+ const [vehicles, setVehicles] = useState([]);
+ const [idleVehicles, setIdleVehicles] = useState([]);
+
+ 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); };
+
+ const [assignModalOpen, setAssignModalOpen] = useState(false);
+ const [vehicleToAssign, setVehicleToAssign] = useState(null);
+ const handleOpenAssignModal = (vehicle) => { setVehicleToAssign(vehicle); setAssignModalOpen(true); };
+ const handleCloseAssignModal = () => { setAssignModalOpen(false); setVehicleToAssign(null); };
+
+ const fetchVehicles = async () => {
+ try {
+ const response = await apiClient.get("/admin/vehicles");
+ setVehicles(response.data || []);
+ } catch (error) {
+ console.error(error);
+ toast.error("Araç listesi yüklenemedi.");
+ }
+ };
+
+ const fetchIdleVehicles = async () => {
+ try {
+ const response = await apiClient.get("/admin/vehicles/idle-warnings?days=7");
+ 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 });
+ };
+
+ 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 {
+ 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 });
+ };
+
+ const unassignUser = async (vehicle) => {
+ if (!vehicle.appUserId) {
+ toast.info("Bu araçta zaten atanmış kullanıcı yok.");
+ return;
+ }
+ try {
+ await apiClient.patch(`/admin/vehicles/${vehicle.id}/assign-user`, { appUserId: null });
+ handleDataChange(`${vehicle.licensePlate} aracından kullanıcı ataması kaldırıldı.`);
+ } catch (err) {
+ toast.error(err?.response?.data?.message || "Atama kaldırılamadı.");
+ }
+ };
+
+ 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 {
+ toast.error("Silme işlemi başarısız.");
+ }
+ };
+
return (
-
-
-
-
- Authors Table
-
-
-
-
-
-
- {["author", "function", "status", "employed", ""].map((el) => (
- |
-
- {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}
+ />
+ handleDataChange()}
+ />
+
+
+
+
+ Araç Listesi
+
+
+
+
+
+
+ {["Plaka", "Kullanıcı Adı", "Telefon", "Durum", "İşlem"].map((el) => (
+ |
+
+ {el}
+
+ |
+ ))}
+
+
+
+ {vehicles.map((vehicle, key) => {
+ const className = `py-3 px-5 ${key === vehicles.length - 1 ? "" : "border-b border-blue-gray-50"}`;
+ const displayName = vehicle.userFullName && vehicle.userFullName !== "Atanmadı"
+ ? toTitleCase(vehicle.userFullName)
+ : vehicle.driverName && vehicle.driverName !== "Atanmadı"
+ ? toTitleCase(vehicle.driverName)
+ : null;
return (
-
-
-
-
-
-
+
+
+ {vehicle.licensePlate}
+
+ |
+
+ {displayName ? (
+
+ {displayName}
+
+ ) : (
+
+ Atanmadı
+
+ )}
+ |
+
+
+ {vehicle.phoneNumber || "-"}
+
+ |
+
+
+ |
+
+
+
+
+
+
-
- |
-
-
- {job[0]}
-
-
- {job[1]}
-
- |
-
-
- |
-
-
- {date}
-
- |
-
-
- Edit
-
+ |
+ |
+ );
+ })}
+ {vehicles.length === 0 && (
+
+ |
+ Kayıtlı araç bulunamadı.
|
- );
- }
- )}
-
-
-
-
-
-
-
- Projects Table
-
-
-
-
-
-
- {["companies", "members", "budget", "completion", ""].map(
- (el) => (
- |
-
- {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"
- }`;
+
+
+
+
- return (
-
- |
-
- |
-
- {members.map(({ img, name }, key) => (
-
-
-
- ))}
- |
-
-
- {budget}
-
- |
-
-
- 0 && (
+
+
+
+
+ Hareketsiz Araçlar
+
+ Son 7 gündür işlem görmeyen araçlar.
+
+
+
+
+
+
+
+ {["Plaka", "Kullanıcı Adı", "Telefon", "Aksiyon"].map((el) => (
+ |
- {completion}%
-
-
-
-
- |
-
-
-
- |
+
+ {el}
+
+
+ ))}
- );
- }
- )}
-
-
-
-
-
+
+ |
+ {idleVehicles.map((vehicle, key) => {
+ const className = `py-3 px-5 ${key === idleVehicles.length - 1 ? "" : "border-b border-blue-gray-50"}`;
+ const displayName = vehicle.userFullName && vehicle.userFullName !== "Atanmadı"
+ ? toTitleCase(vehicle.userFullName)
+ : vehicle.driverName && vehicle.driverName !== "Atanmadı"
+ ? toTitleCase(vehicle.driverName)
+ : null;
+
+ return (
+
+ |
+
+ {vehicle.licensePlate}
+
+ |
+
+ {displayName ? (
+ {displayName}
+ ) : (
+ Atanmadı
+ )}
+ |
+
+
+ {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..110e25ce 100644
--- a/src/routes.jsx
+++ b/src/routes.jsx
@@ -1,13 +1,26 @@
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";
+import DispatchDetailPage from "./pages/dashboard/DispatchDetailPage";
const icon = {
className: "w-5 h-5 text-inherit",
@@ -15,52 +28,79 @@ const icon = {
export const routes = [
{
- layout: "dashboard",
+ layout: "anasayfa",
pages: [
{
- icon:
,
- name: "dashboard",
- path: "/home",
+ icon:
,
+ name: "Araç Sıraları",
+ path: "/arac-siralari",
element:
,
},
+ {
+ icon:
,
+ name: "Sıra Yönetimi",
+ path: "/sira-yonetimi",
+ element:
,
+ roles: ['admin'],
+ },
+ {
+ icon:
,
+ name: "Özel Görev",
+ path: "/ozel-gorev",
+ element:
,
+ roles: ['admin'],
+ },
+ {
+ hidden: true,
+ path: "/ozel-gorev/:routeId",
+ 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",
+ 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 (
+
+ );
+}
\ 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..e409689d
--- /dev/null
+++ b/src/widgets/layout/AddUserModal.jsx
@@ -0,0 +1,71 @@
+import React, { useState } from "react";
+import {
+ Button, Dialog, DialogHeader, DialogBody, DialogFooter, Input, Typography,
+} from "@material-tailwind/react";
+import { toast } from "react-toastify";
+import apiClient from "../../api/axiosConfig.js";
+
+export function AddUserModal({ open, handleOpen, onUserAdded }) {
+ const [formData, setFormData] = useState({
+ fullName: "", userName: "", password: "", confirmPassword: "", phoneNumber: "",
+ });
+ const [error, setError] = useState("");
+
+ const clearForm = () => {
+ setFormData({ fullName: "", userName: "", password: "", confirmPassword: "", phoneNumber: "" });
+ setError("");
+ };
+
+ const handleClose = () => { clearForm(); handleOpen(); };
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ setError("");
+ };
+
+ const handleSubmit = async () => {
+ if (!formData.fullName || !formData.userName || !formData.password || !formData.confirmPassword) {
+ const msg = "Zorunlu alanları doldurunuz.";
+ setError(msg); toast.error(msg); return;
+ }
+ if (formData.password !== formData.confirmPassword) {
+ const msg = "Şifreler eşleşmiyor.";
+ setError(msg); toast.error(msg); return;
+ }
+ try {
+ await apiClient.post("/Users", {
+ fullName: formData.fullName,
+ userName: formData.userName,
+ password: formData.password,
+ confirmPassword: formData.confirmPassword,
+ phoneNumber: formData.phoneNumber?.trim() || null,
+ });
+ onUserAdded();
+ handleClose();
+ } catch (err) {
+ const msg = err.response?.status === 409
+ ? `'${formData.userName}' zaten kayıtlı.`
+ : (err.response?.data?.message || "Kullanıcı eklenirken hata oluştu.");
+ setError(msg); toast.error(msg);
+ }
+ };
+
+ return (
+
+ );
+}
\ 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..b32751f5
--- /dev/null
+++ b/src/widgets/layout/AddVehicleModal.jsx
@@ -0,0 +1,80 @@
+import React, { useState, useEffect } from "react";
+import {
+ Button, Dialog, DialogHeader, DialogBody, DialogFooter,
+ Input, Select, Option, Typography,
+} from "@material-tailwind/react";
+import { toast } from "react-toastify";
+import apiClient from "../../api/axiosConfig.js";
+
+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([]);
+ const [selectedUserId, setSelectedUserId] = useState("");
+ const [licensePlate, setLicensePlate] = useState("");
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ if (open) {
+ apiClient.get("/Users")
+ .then(r => { setUsers(r.data || []); setError(""); })
+ .catch(() => toast.error("Kullanıcı listesi yüklenemedi."));
+ } else {
+ setLicensePlate("");
+ setSelectedUserId("");
+ setError("");
+ }
+ }, [open]);
+
+ const handleSubmit = async () => {
+ const plate = licensePlate.trim().toUpperCase();
+ if (!plate) { toast.error("Plaka zorunludur."); return; }
+ if (!TURKISH_PLATE_REGEX.test(plate)) {
+ toast.error("Plaka formatı uygun değil. Örn: 34 ABC 123"); return;
+ }
+ try {
+ await apiClient.post("/admin/vehicles", {
+ licensePlate: plate.replace(/\s+/g, ""),
+ appUserId: selectedUserId || null,
+ });
+ toast.success(`'${plate}' plakalı araç eklendi!`);
+ onVehicleAdded?.();
+ handleOpen();
+ } catch (err) {
+ const msg = err?.response?.data?.message || "Araç eklenirken hata oluştu.";
+ toast.error(msg);
+ setError(msg);
+ }
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/widgets/layout/AssignUserModal.jsx b/src/widgets/layout/AssignUserModal.jsx
new file mode 100644
index 00000000..fc0f2018
--- /dev/null
+++ b/src/widgets/layout/AssignUserModal.jsx
@@ -0,0 +1,115 @@
+import React, { useState, useEffect } from "react";
+import {
+ Button,
+ Dialog,
+ DialogHeader,
+ DialogBody,
+ DialogFooter,
+ Typography,
+} from "@material-tailwind/react";
+import { toast } from "react-toastify";
+import apiClient from "../../api/axiosConfig.js";
+
+export function AssignUserModal({ open, handleOpen, vehicle, onAssigned }) {
+ const [users, setUsers] = useState([]);
+ const [selectedUserId, setSelectedUserId] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ if (open) {
+ setSelectedUserId(vehicle?.appUserId || "");
+ apiClient
+ .get("/Users")
+ .then((r) => setUsers(r.data || []))
+ .catch(() => toast.error("Kullanıcı listesi yüklenemedi."));
+ }
+ }, [open, vehicle]);
+
+ const handleSubmit = async () => {
+ if (!selectedUserId) {
+ toast.error("Lütfen bir kullanıcı seçin.");
+ return;
+ }
+ setLoading(true);
+ try {
+ await apiClient.patch(`/admin/vehicles/${vehicle.id}/assign-user`, {
+ appUserId: selectedUserId,
+ });
+ toast.success("Kullanıcı ataması güncellendi.");
+ onAssigned();
+ handleOpen();
+ } catch (err) {
+ toast.error(err?.response?.data?.message || "Atama yapılamadı.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
\ 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 (
+
+ );
+}
\ 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..2613f211
--- /dev/null
+++ b/src/widgets/layout/EditUserModal.jsx
@@ -0,0 +1,146 @@
+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 }) {
+ // Sadece Swagger'da istenen ve formda olan alanlar
+ const [formData, setFormData] = useState({
+ fullName: "",
+ userName: "", // Sadece ekranda göstermek için, göndermeyeceğiz
+ password: "",
+ confirmPassword: ""
+ });
+
+ 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 () => {
+ // Şifrelerden biri girilmişse Frontend kontrolü
+ if (formData.password || formData.confirmPassword) {
+ if (formData.password !== formData.confirmPassword) {
+ notifyError("Şifreler uyuşmuyor!");
+ return;
+ }
+ if (formData.password.length < 6) {
+ notifyError("Şifre en az 6 karakter olmalıdır.");
+ return;
+ }
+ }
+
+ // --- PAYLOAD HAZIRLIĞI (SWAGGER'A GÖRE) ---
+ // ID zaten URL'de (/Users/{id}) gidiyor, body'ye koymaya gerek yok.
+ // UserName Swagger'da yok, o yüzden onu da çıkardık.
+ const payload = {
+ fullName: formData.fullName
+ };
+
+ // Eğer şifre kutusu doluysa, password VE confirmPassword alanlarını ekle
+ // Swagger modelinde confirmPassword olduğu için onu da göndermeliyiz!
+ if (formData.password && formData.password.trim() !== "") {
+ payload.password = formData.password;
+ payload.confirmPassword = formData.confirmPassword;
+ }
+
+ try {
+ await apiClient.put(`/Users/${userToEdit.id}`, payload);
+
+ toast.success("Kullanıcı güncellendi!");
+ onUserUpdated();
+ handleOpen();
+ } catch (err) {
+ console.error(err);
+ // Hata mesajını yakalama
+ const backendMsg = err.response?.data?.errors
+ ? JSON.stringify(err.response.data.errors) // Validation hatası dönerse
+ : (err.response?.data?.message || "Güncelleme başarısız.");
+
+ notifyError(backendMsg);
+ }
+ };
+
+ return (
+
+ );
+}
\ 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..0c4a9967
--- /dev/null
+++ b/src/widgets/layout/EditVehicleModal.jsx
@@ -0,0 +1,67 @@
+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: "",
+ isActive: true,
+ });
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ if (vehicleToEdit) {
+ setFormData({
+ licensePlate: vehicleToEdit.licensePlate || "",
+ isActive: vehicleToEdit.isActive,
+ });
+ } else {
+ setFormData({ licensePlate: "", 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 {
+ await apiClient.put(`/admin/vehicles/${vehicleToEdit.id}`, {
+ licensePlate: formData.licensePlate,
+ isActive: formData.isActive,
+ appUserId: vehicleToEdit.appUserId ?? null,
+ });
+ alert("Araç başarıyla güncellendi!");
+ onVehicleUpdated();
+ handleOpen();
+ } catch (err) {
+ setError(err.response?.data?.message || "Güncelleme sırasında bir hata oluştu.");
+ }
+ };
+
+ return (
+
+ );
+}
\ 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)}
- >
-
-
-
-
-
-
-
-
-
+
+ );
}
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 (
);
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..1208f18e 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,79 @@ export function Sidenav({ brandImg, brandName, routes }) {
};
return (
-