Files
PromptTech/frontend/src/pages/AdminDashboard.js

4369 lines
170 KiB
JavaScript
Raw Normal View History

import React, { useState, useEffect, useRef } from "react";
2026-01-27 18:07:00 -06:00
import { useNavigate } from "react-router-dom";
import axios from "axios";
import {
LayoutDashboard,
Package,
Wrench,
ShoppingCart,
Users,
BarChart3,
Settings,
AlertTriangle,
TrendingUp,
DollarSign,
Calendar,
Download,
Plus,
Edit,
Trash2,
Eye,
ChevronRight,
ChevronLeft,
Search,
Filter,
RefreshCw,
FileText,
FileSpreadsheet,
Info,
Image,
Printer,
CheckCircle,
Clock,
2026-01-27 18:07:00 -06:00
} from "lucide-react";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Badge } from "../components/ui/badge";
import { Separator } from "../components/ui/separator";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "../components/ui/tabs";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "../components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../components/ui/alert-dialog";
import { Label } from "../components/ui/label";
import { Textarea } from "../components/ui/textarea";
import { useAuth } from "../context/AuthContext";
import { toast } from "sonner";
import RichTextEditor from "../components/RichTextEditor";
import ImageUploadManager from "../components/ImageUploadManager";
import MediaManager from "../components/MediaManager";
2026-01-27 18:07:00 -06:00
import { clearCache } from "../utils/apiCache";
const API = `${process.env.REACT_APP_BACKEND_URL}/api`;
const statusColors = {
pending: "bg-yellow-500",
processing: "bg-blue-500",
layaway: "bg-purple-500",
shipped: "bg-cyan-500",
delivered: "bg-green-500",
cancelled: "bg-red-500",
refunded: "bg-orange-500",
on_hold: "bg-gray-500",
};
const AdminDashboard = () => {
const { user, token, isAuthenticated } = useAuth();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("dashboard");
const [loading, setLoading] = useState(true);
// Dashboard data
const [dashboardData, setDashboardData] = useState(null);
// Products state
const [products, setProducts] = useState([]);
const [productDialog, setProductDialog] = useState(false);
const [editingProduct, setEditingProduct] = useState(null);
const [productForm, setProductForm] = useState({
name: "",
description: "",
price: "",
category: "",
image_url: "",
stock: "",
brand: "",
images: [],
});
// Services state
const [services, setServices] = useState([]);
const [serviceDialog, setServiceDialog] = useState(false);
const [editingService, setEditingService] = useState(null);
const [serviceForm, setServiceForm] = useState({
name: "",
description: "",
price: "",
duration: "",
image_url: "",
category: "",
images: [],
});
// Orders state
const [orders, setOrders] = useState([]);
const [orderStatusFilter, setOrderStatusFilter] = useState("all");
const [selectedOrder, setSelectedOrder] = useState(null);
const [newStatus, setNewStatus] = useState("");
const [statusNotes, setStatusNotes] = useState("");
const [trackingNumber, setTrackingNumber] = useState("");
// Inventory state
const [inventory, setInventory] = useState([]);
const [adjustDialog, setAdjustDialog] = useState(false);
const [adjustProduct, setAdjustProduct] = useState(null);
const [adjustQty, setAdjustQty] = useState(0);
const [adjustNotes, setAdjustNotes] = useState("");
const [inventorySearch, setInventorySearch] = useState("");
const [inventoryCategory, setInventoryCategory] = useState("all");
const [inventoryItemsPerPage, setInventoryItemsPerPage] = useState(20);
const [inventoryCurrentPage, setInventoryCurrentPage] = useState(1);
// Reports state
const [reportPeriod, setReportPeriod] = useState("monthly");
const [salesReport, setSalesReport] = useState(null);
// Bookings state
const [bookings, setBookings] = useState([]);
const [bookingCompleteDialog, setBookingCompleteDialog] = useState(false);
const [selectedBooking, setSelectedBooking] = useState(null);
const [bookingCompleteForm, setBookingCompleteForm] = useState({
diagnosis: "",
work_performed: "",
technician_notes: "",
service_cost: "",
paid: true,
device_model: "",
serial_number: "",
product_number: "",
screen_size: "",
});
const [receiptData, setReceiptData] = useState(null);
const [showReceiptDialog, setShowReceiptDialog] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
const receiptRef = useRef(null);
2026-01-27 18:07:00 -06:00
// Categories state
const [categories, setCategories] = useState([]);
const [categoryDialog, setCategoryDialog] = useState(false);
const [editingCategory, setEditingCategory] = useState(null);
const [categoryForm, setCategoryForm] = useState({
name: "",
description: "",
});
// Users state
const [users, setUsers] = useState([]);
const [usersTotal, setUsersTotal] = useState(0);
const [userDialog, setUserDialog] = useState(false);
const [editingUser, setEditingUser] = useState(null);
const [userForm, setUserForm] = useState({
name: "",
email: "",
password: "",
role: "user",
is_active: true,
});
const [userSearch, setUserSearch] = useState("");
const [userRoleFilter, setUserRoleFilter] = useState("");
const [userStatusFilter, setUserStatusFilter] = useState("");
const [usersPerPage, setUsersPerPage] = useState(20);
const [currentUsersPage, setCurrentUsersPage] = useState(1);
// Delete confirmation state (generic for all delete operations)
2026-01-27 18:07:00 -06:00
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [productToDelete, setProductToDelete] = useState(null);
const [confirmDialog, setConfirmDialog] = useState({
open: false,
title: "Confirm Action",
message: "Are you sure you want to proceed?",
onConfirm: null,
});
2026-01-27 18:07:00 -06:00
// About Page state
const [aboutContent, setAboutContent] = useState([]);
const [teamMembers, setTeamMembers] = useState([]);
const [companyValues, setCompanyValues] = useState([]);
const [aboutDialog, setAboutDialog] = useState(false);
const [aboutEditType, setAboutEditType] = useState(""); // "content", "team", "value"
const [editingAboutItem, setEditingAboutItem] = useState(null);
const [aboutForm, setAboutForm] = useState({
section: "",
title: "",
subtitle: "",
content: "",
image_url: "",
data: null,
display_order: 0,
is_active: true,
name: "",
role: "",
bio: "",
email: "",
linkedin: "",
description: "",
icon: "",
});
// Helper function to show confirmation dialog
const showConfirmDialog = (title, message, onConfirm) => {
setConfirmDialog({
open: true,
title,
message,
onConfirm,
});
};
const handleConfirmAction = () => {
if (confirmDialog.onConfirm) {
confirmDialog.onConfirm();
}
setConfirmDialog({ ...confirmDialog, open: false, onConfirm: null });
};
const handleCancelConfirm = () => {
setConfirmDialog({ ...confirmDialog, open: false, onConfirm: null });
};
2026-01-27 18:07:00 -06:00
useEffect(() => {
// Only fetch if properly authenticated as admin
if (isAuthenticated && user?.role === "admin" && token) {
fetchDashboardData();
fetchCategories(); // Fetch categories on mount for product form dropdown
} else if (isAuthenticated && user && user.role !== "admin") {
// User is authenticated but not admin
toast.error("Admin access required");
navigate("/");
}
}, [isAuthenticated, user, token, navigate]);
useEffect(() => {
if (activeTab === "products") fetchProducts();
if (activeTab === "services") fetchServices();
if (activeTab === "orders") fetchOrders();
if (activeTab === "inventory") fetchInventory();
if (activeTab === "reports") fetchReports();
if (activeTab === "bookings") fetchBookings();
if (activeTab === "categories") fetchCategories();
if (activeTab === "users") fetchUsers();
if (activeTab === "about") fetchAboutData();
}, [activeTab]);
// Fetch users when filters or pagination change
useEffect(() => {
if (activeTab === "users") {
fetchUsers();
}
}, [
currentUsersPage,
usersPerPage,
userSearch,
userRoleFilter,
userStatusFilter,
]);
const fetchDashboardData = async () => {
try {
// Validate token exists
if (!token) {
console.warn("No authentication token available");
toast.error("Authentication required");
navigate("/login");
return;
}
const response = await axios.get(`${API}/admin/dashboard`, {
headers: { Authorization: `Bearer ${token}` },
timeout: 10000, // 10 second timeout
});
// Validate response structure
if (response.data && response.data.stats) {
setDashboardData(response.data);
// Show warning if partial data
if (response.data.error) {
toast.warning(response.data.error);
}
} else {
console.error("Invalid dashboard response structure:", response.data);
toast.error("Received invalid dashboard data");
}
} catch (error) {
console.error("Failed to fetch dashboard:", error);
// Detailed error handling
if (error.response) {
// Server responded with error
if (error.response.status === 401) {
toast.error("Session expired. Please login again.");
navigate("/login");
} else if (error.response.status === 403) {
toast.error("Admin access required");
navigate("/");
} else if (error.response.status === 500) {
toast.error("Server error. Please try again later.");
} else {
toast.error(
`Failed to load dashboard data (Error ${error.response.status})`,
2026-01-27 18:07:00 -06:00
);
}
} else if (error.request) {
// Request made but no response
toast.error("Cannot connect to server. Please check your connection.");
} else {
// Other errors
toast.error("Failed to load dashboard data");
}
} finally {
setLoading(false);
}
};
const fetchProducts = async () => {
try {
const response = await axios.get(
`${API}/admin/products?include_inactive=true`,
{
headers: { Authorization: `Bearer ${token}` },
},
2026-01-27 18:07:00 -06:00
);
setProducts(response.data);
} catch (error) {
toast.error("Failed to load products");
}
};
const fetchServices = async () => {
try {
const response = await axios.get(
`${API}/admin/services?include_inactive=true`,
{
headers: { Authorization: `Bearer ${token}` },
},
2026-01-27 18:07:00 -06:00
);
setServices(response.data);
} catch (error) {
toast.error("Failed to load services");
}
};
const fetchOrders = async () => {
try {
const params =
orderStatusFilter !== "all" ? `?status=${orderStatusFilter}` : "";
const response = await axios.get(`${API}/admin/orders${params}`, {
headers: { Authorization: `Bearer ${token}` },
});
setOrders(response.data);
} catch (error) {
toast.error("Failed to load orders");
}
};
const fetchInventory = async () => {
try {
const response = await axios.get(`${API}/admin/inventory`, {
headers: { Authorization: `Bearer ${token}` },
});
setInventory(response.data);
} catch (error) {
toast.error("Failed to load inventory");
}
};
const fetchReports = async () => {
try {
const response = await axios.get(
`${API}/admin/reports/sales?period=${reportPeriod}`,
{
headers: { Authorization: `Bearer ${token}` },
},
2026-01-27 18:07:00 -06:00
);
setSalesReport(response.data);
} catch (error) {
toast.error("Failed to load reports");
}
};
const fetchBookings = async () => {
try {
const response = await axios.get(`${API}/admin/bookings`, {
headers: { Authorization: `Bearer ${token}` },
});
setBookings(response.data);
} catch (error) {
toast.error("Failed to load bookings");
}
};
const fetchCategories = async () => {
try {
const response = await axios.get(`${API}/admin/categories`, {
headers: { Authorization: `Bearer ${token}` },
});
setCategories(response.data);
} catch (error) {
toast.error("Failed to load categories");
}
};
const fetchUsers = async () => {
try {
const skip = (currentUsersPage - 1) * usersPerPage;
const response = await axios.get(`${API}/admin/users`, {
headers: { Authorization: `Bearer ${token}` },
params: {
skip,
limit: usersPerPage,
search: userSearch,
role: userRoleFilter,
status: userStatusFilter,
},
});
setUsers(response.data.users);
setUsersTotal(response.data.total);
} catch (error) {
toast.error("Failed to load users");
}
};
const handleUserSubmit = async () => {
try {
const data = {
...userForm,
role: userForm.role.toLowerCase(),
};
if (editingUser) {
// Update user - only include password if it's not empty
if (!data.password || data.password.trim() === "") {
delete data.password; // Don't send empty password
}
await axios.put(`${API}/admin/users/${editingUser.id}`, data, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("User updated successfully");
} else {
// Create new user
if (!data.password) {
toast.error("Password is required for new users");
return;
}
await axios.post(`${API}/admin/users`, data, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("User created");
}
setUserDialog(false);
setEditingUser(null);
setUserForm({
name: "",
email: "",
password: "",
role: "user",
is_active: true,
});
fetchUsers();
} catch (error) {
toast.error(error.response?.data?.detail || "Failed to save user");
}
};
const handleToggleUserActive = async (userId) => {
try {
await axios.put(
`${API}/admin/users/${userId}/toggle-active`,
{},
{
headers: { Authorization: `Bearer ${token}` },
},
2026-01-27 18:07:00 -06:00
);
toast.success("User status updated");
fetchUsers();
} catch (error) {
toast.error(
error.response?.data?.detail || "Failed to update user status",
2026-01-27 18:07:00 -06:00
);
}
};
const handleDeleteUser = async (userId) => {
try {
await axios.delete(`${API}/admin/users/${userId}`, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("User deleted");
fetchUsers();
} catch (error) {
toast.error(error.response?.data?.detail || "Failed to delete user");
}
};
// About Page Functions
const fetchAboutData = async () => {
try {
const [contentRes, teamRes, valuesRes] = await Promise.all([
axios.get(`${API}/admin/about/content`, {
headers: { Authorization: `Bearer ${token}` },
}),
axios.get(`${API}/admin/about/team`, {
headers: { Authorization: `Bearer ${token}` },
}),
axios.get(`${API}/admin/about/values`, {
headers: { Authorization: `Bearer ${token}` },
}),
]);
setAboutContent(contentRes.data);
setTeamMembers(teamRes.data);
setCompanyValues(valuesRes.data);
} catch (error) {
toast.error("Failed to fetch about page data");
}
};
const handleSaveAboutItem = async () => {
try {
if (aboutEditType === "content") {
const data = {
section: aboutForm.section,
title: aboutForm.title || null,
subtitle: aboutForm.subtitle || null,
content: aboutForm.content || null,
image_url: aboutForm.image_url || null,
data: aboutForm.data,
display_order: aboutForm.display_order,
is_active: aboutForm.is_active,
};
if (editingAboutItem) {
await axios.put(
`${API}/admin/about/content/${editingAboutItem.id}`,
data,
{ headers: { Authorization: `Bearer ${token}` } },
2026-01-27 18:07:00 -06:00
);
toast.success("Content updated");
} else {
await axios.post(`${API}/admin/about/content`, data, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Content created");
}
} else if (aboutEditType === "team") {
const data = {
name: aboutForm.name,
role: aboutForm.role,
bio: aboutForm.bio || null,
image_url: aboutForm.image_url || null,
email: aboutForm.email || null,
linkedin: aboutForm.linkedin || null,
display_order: aboutForm.display_order,
is_active: aboutForm.is_active,
};
if (editingAboutItem) {
await axios.put(
`${API}/admin/about/team/${editingAboutItem.id}`,
data,
{ headers: { Authorization: `Bearer ${token}` } },
2026-01-27 18:07:00 -06:00
);
toast.success("Team member updated");
} else {
await axios.post(`${API}/admin/about/team`, data, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Team member created");
}
} else if (aboutEditType === "value") {
const data = {
title: aboutForm.title,
description: aboutForm.description,
icon: aboutForm.icon,
display_order: aboutForm.display_order,
is_active: aboutForm.is_active,
};
if (editingAboutItem) {
await axios.put(
`${API}/admin/about/values/${editingAboutItem.id}`,
data,
{ headers: { Authorization: `Bearer ${token}` } },
2026-01-27 18:07:00 -06:00
);
toast.success("Company value updated");
} else {
await axios.post(`${API}/admin/about/values`, data, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Company value created");
}
}
setAboutDialog(false);
setEditingAboutItem(null);
fetchAboutData();
} catch (error) {
toast.error(
error.response?.data?.detail || "Failed to save about page item",
2026-01-27 18:07:00 -06:00
);
}
};
const handleDeleteAboutItem = (id, type) => {
const itemName =
type === "content"
? "content section"
: type === "team"
? "team member"
: "company value";
showConfirmDialog(
"Delete Item",
`Are you sure you want to delete this ${itemName}? This action cannot be undone.`,
async () => {
try {
if (type === "content") {
await axios.delete(`${API}/admin/about/content/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Content deleted");
} else if (type === "team") {
await axios.delete(`${API}/admin/about/team/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Team member deleted");
} else if (type === "value") {
await axios.delete(`${API}/admin/about/values/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Company value deleted");
}
fetchAboutData();
} catch (error) {
toast.error(
error.response?.data?.detail || "Failed to delete about page item",
);
}
},
);
2026-01-27 18:07:00 -06:00
};
// Product CRUD
const handleProductSubmit = async () => {
try {
const price = parseFloat(productForm.price);
if (isNaN(price) || price <= 0) {
toast.error("Price must be greater than 0");
return;
}
2026-01-27 18:07:00 -06:00
const data = {
...productForm,
price: price,
2026-01-27 18:07:00 -06:00
stock: parseInt(productForm.stock) || 10,
};
if (editingProduct) {
await axios.put(`${API}/admin/products/${editingProduct.id}`, data, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Product updated");
} else {
await axios.post(`${API}/admin/products`, data, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Product created");
}
clearCache("products-");
setProductDialog(false);
setEditingProduct(null);
setProductForm({
name: "",
description: "",
price: "",
category: "",
image_url: "",
stock: "",
brand: "",
images: [],
});
fetchProducts();
} catch (error) {
toast.error("Failed to save product");
}
};
const handleDeleteProduct = async () => {
if (!productToDelete) return;
try {
await axios.delete(`${API}/admin/products/${productToDelete.id}`, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success(`"${productToDelete.name}" has been deleted`);
clearCache("products-");
setDeleteConfirmOpen(false);
setProductToDelete(null);
fetchProducts();
} catch (error) {
toast.error("Failed to delete product");
setDeleteConfirmOpen(false);
setProductToDelete(null);
}
};
// Service CRUD
const handleServiceSubmit = async () => {
try {
const price = parseFloat(serviceForm.price) || 0;
if (isNaN(price) || price < 0) {
toast.error("Price cannot be negative");
return;
}
2026-01-27 18:07:00 -06:00
const data = {
...serviceForm,
price: price,
2026-01-27 18:07:00 -06:00
};
if (editingService) {
await axios.put(`${API}/admin/services/${editingService.id}`, data, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Service updated");
} else {
await axios.post(`${API}/admin/services`, data, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Service created");
}
clearCache("services-");
setServiceDialog(false);
setEditingService(null);
setServiceForm({
name: "",
description: "",
price: "",
duration: "",
image_url: "",
category: "",
images: [],
});
fetchServices();
} catch (error) {
toast.error("Failed to save service");
}
};
const handleDeleteService = (id) => {
showConfirmDialog(
"Delete Service",
"Are you sure you want to delete this service? This action cannot be undone.",
async () => {
try {
await axios.delete(`${API}/admin/services/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Service deleted");
clearCache("services-");
fetchServices();
} catch (error) {
toast.error("Failed to delete service");
}
},
);
};
// Booking completion
const handleOpenCompleteDialog = (booking) => {
setSelectedBooking(booking);
setBookingCompleteForm({
diagnosis: booking.diagnosis || "",
work_performed: booking.work_performed || "",
technician_notes: booking.technician_notes || "",
service_cost: booking.service_cost || "",
paid: booking.paid !== undefined ? booking.paid : true,
device_model: booking.device_model || "",
serial_number: booking.serial_number || "",
product_number: booking.product_number || "",
screen_size: booking.screen_size || "",
});
setIsEditMode(false);
setBookingCompleteDialog(true);
};
const handleDeleteBooking = (booking) => {
showConfirmDialog(
"Delete Booking",
`Are you sure you want to delete the booking for "${booking.name}"? This action cannot be undone.`,
async () => {
try {
await axios.delete(`${API}/admin/bookings/${booking.id}`, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Booking deleted successfully");
fetchBookings();
} catch (error) {
toast.error("Failed to delete booking");
}
},
);
};
const handleCompleteBooking = async () => {
if (!selectedBooking) return;
2026-01-27 18:07:00 -06:00
try {
const data = {
...bookingCompleteForm,
service_cost: bookingCompleteForm.service_cost
? parseFloat(bookingCompleteForm.service_cost)
: null,
};
await axios.put(
`${API}/admin/bookings/${selectedBooking.id}/complete`,
data,
{ headers: { Authorization: `Bearer ${token}` } },
);
toast.success("Service marked as completed!");
setBookingCompleteDialog(false);
setSelectedBooking(null);
fetchBookings();
} catch (error) {
toast.error("Failed to complete booking");
}
};
const handleUpdateBookingStatus = async (bookingId, status) => {
try {
await axios.put(
`${API}/admin/bookings/${bookingId}/status?status=${status}`,
{},
{ headers: { Authorization: `Bearer ${token}` } },
);
toast.success(`Booking status updated to ${status}`);
fetchBookings();
2026-01-27 18:07:00 -06:00
} catch (error) {
toast.error("Failed to update status");
2026-01-27 18:07:00 -06:00
}
};
const handlePrintReceipt = async (booking) => {
try {
const response = await axios.get(
`${API}/admin/bookings/${booking.id}/receipt`,
{ headers: { Authorization: `Bearer ${token}` } },
);
setReceiptData(response.data);
setShowReceiptDialog(true);
} catch (error) {
toast.error("Failed to load receipt data");
}
};
const printReceipt = () => {
if (!receiptData) return;
const printWindow = window.open("", "_blank");
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Service Receipt - PromptTech Solutions</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
padding: 15px;
color: #333;
font-size: 13px;
line-height: 1.4;
}
.receipt {
max-width: 700px;
margin: 0 auto;
border: 1px solid #ddd;
padding: 15px 20px;
}
.header {
text-align: center;
margin-bottom: 12px;
border-bottom: 2px solid #2563eb;
padding-bottom: 10px;
}
.logo {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 5px;
}
.logo img {
height: 40px;
width: auto;
max-width: 100px;
margin-bottom: 5px;
}
.logo-text {
font-size: 20px;
font-weight: bold;
color: #2563eb;
}
.company-info { font-size: 12px; color: #666; }
.receipt-title {
font-size: 16px;
font-weight: bold;
margin: 10px 0;
text-align: center;
}
.section { margin-bottom: 10px; }
.section-title {
font-weight: bold;
font-size: 13px;
border-bottom: 1px solid #eee;
padding-bottom: 3px;
margin-bottom: 6px;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3px 15px;
}
.grid-item { font-size: 13px; }
.grid-item .label { font-weight: 500; color: #666; }
.notes-box {
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
margin-top: 5px;
font-size: 13px;
}
.notes-label { font-weight: 500; color: #666; margin-bottom: 3px; }
.total {
font-size: 16px;
font-weight: bold;
text-align: right;
padding-top: 10px;
margin-top: 10px;
border-top: 2px solid #333;
}
.footer {
text-align: center;
margin-top: 15px;
padding-top: 10px;
border-top: 1px solid #eee;
font-size: 12px;
color: #888;
}
@media print {
@page {
margin: 0.5in;
size: auto;
}
body {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
padding: 0;
margin: 0;
}
.receipt { border: none; padding: 10px; }
}
</style>
</head>
<body>
<div class="receipt">
<div class="header">
<div class="logo">
<img src="/logo.png" alt="PromptTech Solutions" onerror="this.style.display='none'" />
<span class="logo-text">PromptTech Solutions</span>
</div>
<div class="company-info">
<p>${receiptData.company?.address || "Belmopan City, Cayo District, Belize"}</p>
<p>Phone: ${receiptData.company?.phone || "+501 638-6318"} | Email: ${receiptData.company?.email || "prompttechbz@gmail.com"}</p>
</div>
</div>
<h2 class="receipt-title">SERVICE RECEIPT</h2>
<div class="section">
<h3 class="section-title">Customer Information</h3>
<div class="grid">
<div class="grid-item"><span class="label">Name:</span> ${receiptData.name || ""}</div>
<div class="grid-item"><span class="label">Phone:</span> ${receiptData.phone || ""}</div>
<div class="grid-item"><span class="label">Email:</span> ${receiptData.email || ""}</div>
<div class="grid-item"><span class="label">Date Booked:</span> ${receiptData.created_at ? new Date(receiptData.created_at).toLocaleDateString() : ""}</div>
</div>
</div>
<div class="section">
<h3 class="section-title">Service Details</h3>
<div class="grid">
<div class="grid-item"><span class="label">Service:</span> ${receiptData.service_name || ""}</div>
<div class="grid-item"><span class="label">Preferred Date:</span> ${receiptData.preferred_date || ""}</div>
<div class="grid-item"><span class="label">Status:</span> ${receiptData.status || ""}</div>
${receiptData.completed_at ? `<div class="grid-item"><span class="label">Completed:</span> ${new Date(receiptData.completed_at).toLocaleDateString()}</div>` : ""}
</div>
</div>
${
receiptData.device_model ||
receiptData.serial_number ||
receiptData.product_number ||
receiptData.screen_size
? `
<div class="section">
<h3 class="section-title">Device Information</h3>
<div class="grid">
${receiptData.device_model ? `<div class="grid-item"><span class="label">Model:</span> ${receiptData.device_model}</div>` : ""}
${receiptData.screen_size ? `<div class="grid-item"><span class="label">Screen Size:</span> ${receiptData.screen_size}</div>` : ""}
${receiptData.serial_number ? `<div class="grid-item"><span class="label">Serial #:</span> ${receiptData.serial_number}</div>` : ""}
${receiptData.product_number ? `<div class="grid-item"><span class="label">Product #:</span> ${receiptData.product_number}</div>` : ""}
</div>
</div>`
: ""
}
${
receiptData.diagnosis || receiptData.work_performed
? `
<div class="section">
<h3 class="section-title">Service Report</h3>
${
receiptData.diagnosis
? `
<div class="notes-box">
<p class="notes-label">Diagnosis:</p>
<p>${receiptData.diagnosis}</p>
</div>`
: ""
}
${
receiptData.work_performed
? `
<div class="notes-box" style="margin-top: 5px;">
<p class="notes-label">Work Performed:</p>
<p>${receiptData.work_performed}</p>
</div>`
: ""
}
</div>`
: ""
}
${
receiptData.notes
? `
<div class="section">
<h3 class="section-title">Customer Notes</h3>
<div class="notes-box">
<p>${receiptData.notes}</p>
</div>
</div>`
: ""
}
<div class="total">
Total: $${receiptData.final_cost?.toFixed(2) || "0.00"}
</div>
<div class="footer">
<p>Thank you for choosing PromptTech Solutions!</p>
<p>Questions? Contact us at ${receiptData.company?.phone || "+501 638-6318"}</p>
</div>
</div>
</body>
</html>
`);
printWindow.document.close();
// Small delay to ensure styles are loaded before printing
setTimeout(() => {
printWindow.print();
}, 250);
};
2026-01-27 18:07:00 -06:00
// Order status update
const handleUpdateOrderStatus = async () => {
if (!selectedOrder || !newStatus) return;
try {
await axios.put(
`${API}/admin/orders/${selectedOrder}/status`,
{
status: newStatus,
notes: statusNotes,
tracking_number: trackingNumber || null,
},
{
headers: { Authorization: `Bearer ${token}` },
},
2026-01-27 18:07:00 -06:00
);
toast.success("Order status updated");
setSelectedOrder(null);
setNewStatus("");
setStatusNotes("");
setTrackingNumber("");
fetchOrders();
} catch (error) {
toast.error("Failed to update order");
}
};
// Inventory adjustment
const handleAdjustInventory = async () => {
if (!adjustProduct) return;
try {
await axios.post(
`${API}/admin/inventory/${adjustProduct.id}/adjust`,
{
quantity_change: adjustQty,
notes: adjustNotes,
},
{
headers: { Authorization: `Bearer ${token}` },
},
2026-01-27 18:07:00 -06:00
);
toast.success("Inventory adjusted");
setAdjustDialog(false);
setAdjustProduct(null);
setAdjustQty(0);
setAdjustNotes("");
fetchInventory();
} catch (error) {
toast.error("Failed to adjust inventory");
}
};
// Toggle product active status
const handleToggleActive = async (product) => {
try {
await axios.put(
`${API}/admin/products/${product.id}`,
{ is_active: !product.is_active },
{ headers: { Authorization: `Bearer ${token}` } },
2026-01-27 18:07:00 -06:00
);
toast.success(
`Product ${!product.is_active ? "activated" : "deactivated"}`,
2026-01-27 18:07:00 -06:00
);
fetchInventory();
} catch (error) {
toast.error("Failed to update product status");
}
};
// Export reports
const handleExportCSV = async (type) => {
try {
const response = await axios.get(
`${API}/admin/reports/export/csv?report_type=${type}&period=${reportPeriod}`,
{
headers: { Authorization: `Bearer ${token}` },
responseType: "blob",
},
2026-01-27 18:07:00 -06:00
);
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", `${type}_report.csv`);
document.body.appendChild(link);
link.click();
link.remove();
toast.success("CSV exported");
} catch (error) {
toast.error("Failed to export CSV");
}
};
const handleExportPDF = async (type) => {
try {
const response = await axios.get(
`${API}/admin/reports/export/pdf?report_type=${type}&period=${reportPeriod}`,
{
headers: { Authorization: `Bearer ${token}` },
responseType: "blob",
},
2026-01-27 18:07:00 -06:00
);
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", `${type}_report.pdf`);
document.body.appendChild(link);
link.click();
link.remove();
toast.success("PDF exported");
} catch (error) {
toast.error("Failed to export PDF");
}
};
if (!isAuthenticated || user?.role !== "admin") {
return (
<div className="min-h-screen py-12 md:py-16">
<div className="max-w-4xl mx-auto px-4 md:px-8 text-center py-16">
<h2 className="text-2xl font-bold mb-4 font-['Outfit']">
Access Denied
</h2>
<p className="text-muted-foreground mb-8">
You need admin privileges to access this page.
</p>
<Button onClick={() => navigate("/login")} className="rounded-full">
Sign In as Admin
</Button>
</div>
</div>
);
}
// Category CRUD
const handleCategorySubmit = async () => {
try {
if (editingCategory) {
await axios.put(
`${API}/admin/categories/${editingCategory.id}`,
categoryForm,
{
headers: { Authorization: `Bearer ${token}` },
},
2026-01-27 18:07:00 -06:00
);
toast.success("Category updated");
} else {
await axios.post(`${API}/admin/categories`, categoryForm, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Category created");
}
setCategoryDialog(false);
setEditingCategory(null);
setCategoryForm({ name: "", description: "" });
fetchCategories();
} catch (error) {
toast.error("Failed to save category");
}
};
const handleDeleteCategory = (id) => {
showConfirmDialog(
"Delete Category",
"Are you sure you want to delete this category? This action cannot be undone.",
async () => {
try {
await axios.delete(`${API}/admin/categories/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Category deleted");
fetchCategories();
} catch (error) {
toast.error("Failed to delete category");
}
},
);
2026-01-27 18:07:00 -06:00
};
if (loading) {
return (
<div className="min-h-screen py-12 flex items-center justify-center">
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full" />
</div>
);
}
return (
<div className="min-h-screen bg-muted/30">
<div className="max-w-7xl mx-auto px-4 md:px-8 py-8">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold font-['Outfit']">
Admin Dashboard
</h1>
<p className="text-muted-foreground">Manage your store</p>
</div>
<Button
onClick={fetchDashboardData}
variant="outline"
className="gap-2"
>
<RefreshCw className="h-4 w-4" />
Refresh
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-4 md:grid-cols-8 gap-2 mb-8 h-auto p-1">
<TabsTrigger value="dashboard" className="gap-2">
<LayoutDashboard className="h-4 w-4" />
<span className="hidden md:inline">Dashboard</span>
</TabsTrigger>
<TabsTrigger value="products" className="gap-2">
<Package className="h-4 w-4" />
<span className="hidden md:inline">Products</span>
</TabsTrigger>
<TabsTrigger value="services" className="gap-2">
<Wrench className="h-4 w-4" />
<span className="hidden md:inline">Services</span>
</TabsTrigger>
<TabsTrigger value="orders" className="gap-2">
<ShoppingCart className="h-4 w-4" />
<span className="hidden md:inline">Orders</span>
</TabsTrigger>
<TabsTrigger value="inventory" className="gap-2">
<Package className="h-4 w-4" />
<span className="hidden md:inline">Inventory</span>
</TabsTrigger>
<TabsTrigger value="bookings" className="gap-2">
<Calendar className="h-4 w-4" />
<span className="hidden md:inline">Bookings</span>
</TabsTrigger>
<TabsTrigger value="categories" className="gap-2">
<Settings className="h-4 w-4" />
<span className="hidden md:inline">Categories</span>
</TabsTrigger>
<TabsTrigger value="users" className="gap-2">
<Users className="h-4 w-4" />
<span className="hidden md:inline">Users</span>
</TabsTrigger>
<TabsTrigger value="reports" className="gap-2">
<BarChart3 className="h-4 w-4" />
<span className="hidden md:inline">Reports</span>
</TabsTrigger>
<TabsTrigger value="media" className="gap-2">
<Image className="h-4 w-4" />
<span className="hidden md:inline">Media</span>
</TabsTrigger>
2026-01-27 18:07:00 -06:00
<TabsTrigger value="about" className="gap-2">
<Info className="h-4 w-4" />
<span className="hidden md:inline">About Page</span>
</TabsTrigger>
</TabsList>
{/* Dashboard Tab */}
<TabsContent value="dashboard">
{dashboardData && (
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center">
<DollarSign className="h-5 w-5 text-blue-500" />
</div>
<span className="text-sm text-muted-foreground">
Total Revenue
</span>
</div>
<p className="text-2xl font-bold font-['Outfit']">
${dashboardData.stats.total_revenue.toFixed(2)}
</p>
</div>
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-full bg-green-500/10 flex items-center justify-center">
<ShoppingCart className="h-5 w-5 text-green-500" />
</div>
<span className="text-sm text-muted-foreground">
Total Orders
</span>
</div>
<p className="text-2xl font-bold font-['Outfit']">
{dashboardData.stats.total_orders}
</p>
</div>
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-full bg-purple-500/10 flex items-center justify-center">
<Package className="h-5 w-5 text-purple-500" />
</div>
<span className="text-sm text-muted-foreground">
Products
</span>
</div>
<p className="text-2xl font-bold font-['Outfit']">
{dashboardData.stats.total_products}
</p>
</div>
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-full bg-orange-500/10 flex items-center justify-center">
<Users className="h-5 w-5 text-orange-500" />
</div>
<span className="text-sm text-muted-foreground">
Users
</span>
</div>
<p className="text-2xl font-bold font-['Outfit']">
{dashboardData.stats.total_users}
</p>
</div>
</div>
{/* Today's Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-card border border-border rounded-xl p-6">
<h3 className="font-semibold mb-4 font-['Outfit']">
Today's Performance
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Orders</p>
<p className="text-xl font-bold">
{dashboardData.stats.today_orders}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Revenue</p>
<p className="text-xl font-bold">
${(dashboardData.stats.today_revenue || 0).toFixed(2)}
</p>
</div>
</div>
</div>
<div className="bg-card border border-border rounded-xl p-6">
<h3 className="font-semibold mb-4 font-['Outfit']">
Monthly Stats
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Revenue</p>
<p className="text-xl font-bold">
$
{(dashboardData.stats.monthly_revenue || 0).toFixed(
2,
2026-01-27 18:07:00 -06:00
)}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">
Pending Bookings
</p>
<p className="text-xl font-bold">
{dashboardData.stats.pending_bookings}
</p>
</div>
</div>
</div>
</div>
{/* Low Stock Alert */}
{dashboardData.low_stock_products.length > 0 && (
<div className="bg-card border border-orange-500/50 rounded-xl p-6">
<div className="flex items-center gap-2 mb-4">
<AlertTriangle className="h-5 w-5 text-orange-500" />
<h3 className="font-semibold font-['Outfit']">
Low Stock Alert
</h3>
</div>
<div className="space-y-2">
{dashboardData.low_stock_products.map((product) => (
<div
key={product.id}
className="flex items-center justify-between p-3 bg-muted rounded-lg"
>
<span>{product.name}</span>
<Badge variant="destructive">
{product.stock} left
</Badge>
</div>
))}
</div>
</div>
)}
{/* Recent Orders */}
<div className="bg-card border border-border rounded-xl p-6">
<h3 className="font-semibold mb-4 font-['Outfit']">
Recent Orders
</h3>
<div className="space-y-3">
{dashboardData.recent_orders.slice(0, 5).map((order) => (
<div
key={order.id}
className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"
>
<div>
<p className="font-medium font-mono text-sm">
{order.id.slice(0, 8)}...
</p>
<p className="text-xs text-muted-foreground">
{new Date(order.created_at).toLocaleString()}
</p>
</div>
<div className="flex items-center gap-3">
<span className="font-semibold">
${order.total.toFixed(2)}
</span>
<Badge className={statusColors[order.status]}>
{order.status}
</Badge>
</div>
</div>
))}
</div>
</div>
</div>
)}
</TabsContent>
{/* Products Tab */}
<TabsContent value="products">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold font-['Outfit']">
Products Management
</h3>
<Dialog open={productDialog} onOpenChange={setProductDialog}>
<DialogTrigger asChild>
<Button
className="gap-2"
onClick={() => {
setEditingProduct(null);
setProductForm({
name: "",
description: "",
price: "",
category: "",
image_url: "",
stock: "",
brand: "",
images: [],
});
}}
>
<Plus className="h-4 w-4" />
Add Product
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto custom-scrollbar">
<DialogHeader>
<DialogTitle>
{editingProduct ? "Edit Product" : "Add Product"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Name</Label>
<Input
value={productForm.name}
onChange={(e) =>
setProductForm({
...productForm,
name: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label>Description</Label>
<RichTextEditor
content={productForm.description}
onChange={(html) =>
setProductForm({
...productForm,
description: html,
})
}
placeholder="Enter product description with rich formatting..."
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Price</Label>
<Input
type="number"
value={productForm.price}
onChange={(e) =>
setProductForm({
...productForm,
price: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label>Stock</Label>
<Input
type="number"
value={productForm.stock}
onChange={(e) =>
setProductForm({
...productForm,
stock: e.target.value,
})
}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Category</Label>
<Select
value={productForm.category}
onValueChange={(v) =>
setProductForm({ ...productForm, category: v })
}
>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
{categories.map((cat) => (
<SelectItem
key={cat.id}
value={cat.name.toLowerCase()}
>
{cat.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Brand</Label>
<Input
value={productForm.brand}
onChange={(e) =>
setProductForm({
...productForm,
brand: e.target.value,
})
}
/>
</div>
</div>
<Separator className="my-4" />
<ImageUploadManager
images={productForm.images}
onChange={(images) =>
setProductForm({ ...productForm, images })
}
token={token}
/>
<div className="space-y-2">
<Label>Legacy Image URL (Optional)</Label>
<Input
placeholder="Or enter an image URL..."
value={productForm.image_url}
onChange={(e) =>
setProductForm({
...productForm,
image_url: e.target.value,
})
}
/>
<p className="text-xs text-muted-foreground">
Use the image uploader above for best results. This
field is for backwards compatibility.
</p>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setProductDialog(false)}
>
Cancel
</Button>
<Button onClick={handleProductSubmit}>
{editingProduct ? "Update" : "Create"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-2">Product</th>
<th className="text-left py-3 px-2">Category</th>
<th className="text-right py-3 px-2">Price</th>
<th className="text-right py-3 px-2">Stock</th>
<th className="text-center py-3 px-2">Status</th>
<th className="text-right py-3 px-2">Actions</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr
key={product.id}
className="border-b border-border/50"
>
<td className="py-3 px-2">
<div className="flex items-center gap-3">
<img
src={
product.images && product.images.length > 0
? (
product.images[0].url ||
product.images[0].image_url
).startsWith("/uploads")
? `${process.env.REACT_APP_BACKEND_URL}${
product.images[0].url ||
product.images[0].image_url
}`
: product.images[0].url ||
product.images[0].image_url
: product.image_url
}
alt={product.name}
className="w-10 h-10 rounded object-cover bg-muted"
onError={(e) => {
e.target.src =
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"%3E%3Crect x="3" y="3" width="18" height="18" rx="2" /%3E%3Ccircle cx="8.5" cy="8.5" r="1.5" /%3E%3Cpath d="M21 15l-5-5L5 21" /%3E%3C/svg%3E';
}}
/>
<span className="font-medium">{product.name}</span>
</div>
</td>
<td className="py-3 px-2 capitalize">
{product.category}
</td>
<td className="py-3 px-2 text-right">
${product.price.toFixed(2)}
</td>
<td className="py-3 px-2 text-right">
{product.stock}
</td>
<td className="py-3 px-2 text-center">
<Badge
variant={
product.is_active ? "default" : "secondary"
}
>
{product.is_active ? "Active" : "Inactive"}
</Badge>
</td>
<td className="py-3 px-2 text-right">
<div className="flex justify-end gap-2">
<Button
size="icon"
variant="ghost"
onClick={() => {
setEditingProduct(product);
setProductForm({
name: product.name,
description: product.description,
price: product.price.toString(),
category: product.category,
image_url: product.image_url,
stock: product.stock.toString(),
brand: product.brand,
images:
product.images?.map(
(img) => img.url || img.image_url,
2026-01-27 18:07:00 -06:00
) || [],
});
setProductDialog(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="text-destructive"
onClick={() => {
setProductToDelete(product);
setDeleteConfirmOpen(true);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</TabsContent>
{/* Services Tab */}
<TabsContent value="services">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold font-['Outfit']">
Services Management
</h3>
<Dialog open={serviceDialog} onOpenChange={setServiceDialog}>
<DialogTrigger asChild>
<Button
className="gap-2"
onClick={() => {
setEditingService(null);
setServiceForm({
name: "",
description: "",
price: "",
duration: "",
image_url: "",
category: "",
images: [],
});
}}
>
<Plus className="h-4 w-4" />
Add Service
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto custom-scrollbar">
<DialogHeader>
<DialogTitle>
{editingService ? "Edit Service" : "Add Service"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Name</Label>
<Input
value={serviceForm.name}
onChange={(e) =>
setServiceForm({
...serviceForm,
name: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label>Category</Label>
<Select
value={serviceForm.category}
onValueChange={(v) =>
setServiceForm({ ...serviceForm, category: v })
}
>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="repair">Repair</SelectItem>
<SelectItem value="data">
Data Recovery
</SelectItem>
<SelectItem value="software">Software</SelectItem>
<SelectItem value="upgrade">Upgrade</SelectItem>
<SelectItem value="setup">Setup</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Price</Label>
<Input
type="number"
step="0.01"
value={serviceForm.price}
onChange={(e) =>
setServiceForm({
...serviceForm,
price: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label>Duration</Label>
<Input
value={serviceForm.duration}
onChange={(e) =>
setServiceForm({
...serviceForm,
duration: e.target.value,
})
}
placeholder="e.g. 1-2 hours"
/>
</div>
</div>
<div className="space-y-2">
<Label>Description</Label>
<div className="border rounded-lg">
<RichTextEditor
content={serviceForm.description}
onChange={(html) =>
setServiceForm({
...serviceForm,
description: html,
})
}
/>
</div>
</div>
<Separator className="my-4" />
<ImageUploadManager
images={serviceForm.images}
onChange={(images) =>
setServiceForm({ ...serviceForm, images })
}
token={token}
/>
<div className="space-y-2">
<Label>Legacy Image URL (Optional)</Label>
<Input
placeholder="Or enter an image URL..."
value={serviceForm.image_url}
onChange={(e) =>
setServiceForm({
...serviceForm,
image_url: e.target.value,
})
}
/>
<p className="text-xs text-muted-foreground">
Use the image uploader above for best results. This
field is for backwards compatibility.
</p>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setServiceDialog(false)}
>
Cancel
</Button>
<Button onClick={handleServiceSubmit}>
{editingService ? "Update" : "Create"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-2">Service</th>
<th className="text-left py-3 px-2">Category</th>
<th className="text-left py-3 px-2">Duration</th>
<th className="text-right py-3 px-2">Price</th>
<th className="text-center py-3 px-2">Status</th>
<th className="text-right py-3 px-2">Actions</th>
</tr>
</thead>
<tbody>
{services.map((service) => (
<tr
key={service.id}
className="border-b border-border/50"
>
<td className="py-3 px-2">
<div className="flex items-center gap-3">
<img
src={
service.images && service.images.length > 0
? (
service.images[0].url ||
service.images[0].image_url
).startsWith("/uploads")
? `${process.env.REACT_APP_BACKEND_URL}${
service.images[0].url ||
service.images[0].image_url
}`
: service.images[0].url ||
service.images[0].image_url
: service.image_url
}
alt={service.name}
className="w-10 h-10 rounded object-cover bg-muted"
onError={(e) => {
e.target.src =
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"%3E%3Crect x="3" y="3" width="18" height="18" rx="2" /%3E%3Ccircle cx="8.5" cy="8.5" r="1.5" /%3E%3Cpath d="M21 15l-5-5L5 21" /%3E%3C/svg%3E';
}}
/>
<span className="font-medium">{service.name}</span>
</div>
</td>
<td className="py-3 px-2 capitalize">
{service.category}
</td>
<td className="py-3 px-2">{service.duration}</td>
<td className="py-3 px-2 text-right">
${service.price.toFixed(2)}
</td>
<td className="py-3 px-2 text-center">
<Badge
variant={
service.is_active ? "default" : "secondary"
}
>
{service.is_active ? "Active" : "Inactive"}
</Badge>
</td>
<td className="py-3 px-2 text-right">
<div className="flex justify-end gap-2">
<Button
size="icon"
variant="ghost"
onClick={() => {
setEditingService(service);
setServiceForm({
name: service.name,
description: service.description,
price: service.price.toString(),
duration: service.duration,
image_url: service.image_url || "",
2026-01-27 18:07:00 -06:00
category: service.category,
images:
service.images?.map(
(img) => img.url || img.image_url,
2026-01-27 18:07:00 -06:00
) || [],
});
setServiceDialog(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="text-destructive"
onClick={() => handleDeleteService(service.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</TabsContent>
{/* Orders Tab */}
<TabsContent value="orders">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold font-['Outfit']">
Orders Management
</h3>
<Select
value={orderStatusFilter}
onValueChange={(v) => {
setOrderStatusFilter(v);
setTimeout(fetchOrders, 100);
}}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Orders</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="processing">Processing</SelectItem>
<SelectItem value="layaway">Layaway</SelectItem>
<SelectItem value="shipped">Shipped</SelectItem>
<SelectItem value="delivered">Delivered</SelectItem>
<SelectItem value="cancelled">Cancelled</SelectItem>
<SelectItem value="refunded">Refunded</SelectItem>
<SelectItem value="on_hold">On Hold</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-4">
{orders.map((order) => (
<div
key={order.id}
className="border border-border rounded-lg p-4"
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-4">
<div className="flex flex-wrap gap-4">
<div>
<p className="text-xs text-muted-foreground">
Order ID
</p>
<p className="font-mono text-sm">
{order.id.slice(0, 8)}...
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">
Customer
</p>
<p className="text-sm">{order.user_name}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Date</p>
<p className="text-sm">
{new Date(order.created_at).toLocaleDateString()}
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Total</p>
<p className="font-semibold">
${order.total.toFixed(2)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge className={statusColors[order.status]}>
{order.status}
</Badge>
<Dialog>
<DialogTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={() => {
setSelectedOrder(order.id);
setNewStatus(order.status);
setTrackingNumber(order.tracking_number || "");
}}
>
Update Status
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Update Order Status</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>New Status</Label>
<Select
value={newStatus}
onValueChange={setNewStatus}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="pending">
Pending
</SelectItem>
<SelectItem value="processing">
Processing
</SelectItem>
<SelectItem value="layaway">
Layaway
</SelectItem>
<SelectItem value="shipped">
Shipped
</SelectItem>
<SelectItem value="delivered">
Delivered
</SelectItem>
<SelectItem value="cancelled">
Cancelled
</SelectItem>
<SelectItem value="refunded">
Refunded
</SelectItem>
<SelectItem value="on_hold">
On Hold
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Tracking Number (optional)</Label>
<Input
value={trackingNumber}
onChange={(e) =>
setTrackingNumber(e.target.value)
}
/>
</div>
<div className="space-y-2">
<Label>Notes</Label>
<Textarea
value={statusNotes}
onChange={(e) =>
setStatusNotes(e.target.value)
}
/>
</div>
</div>
<DialogFooter>
<Button onClick={handleUpdateOrderStatus}>
Update
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
{/* Order Items */}
<div className="text-sm text-muted-foreground">
{order.items.map((item) => (
<span key={item.id} className="mr-4">
{item.product_name} x{item.quantity}
</span>
))}
</div>
</div>
))}
</div>
</div>
</TabsContent>
{/* Inventory Tab */}
<TabsContent value="inventory">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold font-['Outfit']">
Inventory Management
</h3>
<div className="flex gap-2">
<Button
variant="outline"
className="gap-2"
onClick={() => handleExportCSV("inventory")}
>
<FileSpreadsheet className="h-4 w-4" />
Export CSV
</Button>
<Button
variant="outline"
className="gap-2"
onClick={() => handleExportPDF("inventory")}
>
<FileText className="h-4 w-4" />
Export PDF
</Button>
</div>
</div>
{/* Filters */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search products..."
value={inventorySearch}
onChange={(e) => {
setInventorySearch(e.target.value);
setInventoryCurrentPage(1);
}}
className="pl-9"
/>
</div>
<Select
value={inventoryCategory}
onValueChange={(v) => {
setInventoryCategory(v);
setInventoryCurrentPage(1);
}}
>
<SelectTrigger>
<SelectValue placeholder="Filter by category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
{categories.map((cat) => (
<SelectItem key={cat.id} value={cat.name.toLowerCase()}>
{cat.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={inventoryItemsPerPage.toString()}
onValueChange={(v) => {
setInventoryItemsPerPage(parseInt(v));
setInventoryCurrentPage(1);
}}
>
<SelectTrigger>
<SelectValue placeholder="Items per page" />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10 per page</SelectItem>
<SelectItem value="20">20 per page</SelectItem>
<SelectItem value="50">50 per page</SelectItem>
<SelectItem value="100">100 per page</SelectItem>
</SelectContent>
</Select>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-2">Product</th>
<th className="text-left py-3 px-2">Category</th>
<th className="text-right py-3 px-2">Price</th>
<th className="text-right py-3 px-2">Stock</th>
<th className="text-right py-3 px-2">Threshold</th>
<th className="text-center py-3 px-2">Active</th>
<th className="text-right py-3 px-2">Actions</th>
</tr>
</thead>
<tbody>
{(() => {
// Filter inventory
let filteredInventory = inventory.filter((product) => {
const matchesSearch =
inventorySearch === "" ||
product.name
.toLowerCase()
.includes(inventorySearch.toLowerCase()) ||
product.brand
?.toLowerCase()
.includes(inventorySearch.toLowerCase());
const matchesCategory =
inventoryCategory === "all" ||
product.category === inventoryCategory;
return matchesSearch && matchesCategory;
});
// Pagination
const totalPages = Math.ceil(
filteredInventory.length / inventoryItemsPerPage,
2026-01-27 18:07:00 -06:00
);
const startIndex =
(inventoryCurrentPage - 1) * inventoryItemsPerPage;
const endIndex = startIndex + inventoryItemsPerPage;
const paginatedInventory = filteredInventory.slice(
startIndex,
endIndex,
2026-01-27 18:07:00 -06:00
);
return paginatedInventory.map((product) => (
<tr
key={product.id}
className="border-b border-border/50"
>
<td className="py-3 px-2 font-medium">
{product.name}
</td>
<td className="py-3 px-2 capitalize">
{product.category}
</td>
<td className="py-3 px-2 text-right">
${product.price?.toFixed(2) || "0.00"}
</td>
<td className="py-3 px-2 text-right">
<span
className={
product.is_low_stock
? "text-destructive font-semibold"
: ""
}
>
{product.stock}
</span>
</td>
<td className="py-3 px-2 text-right">
{product.low_stock_threshold}
</td>
<td className="py-3 px-2 text-center">
<Button
size="sm"
variant={
product.is_active ? "default" : "secondary"
}
onClick={() => handleToggleActive(product)}
className="gap-2"
>
{product.is_active ? "Active" : "Inactive"}
</Button>
</td>
<td className="py-3 px-2 text-right">
<Dialog
open={
adjustDialog && adjustProduct?.id === product.id
}
onOpenChange={(open) => {
setAdjustDialog(open);
if (!open) setAdjustProduct(null);
}}
>
<DialogTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={() => {
setAdjustProduct(product);
setAdjustDialog(true);
}}
>
Adjust
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Adjust Inventory</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<p className="text-sm">
Current stock:{" "}
<strong>{adjustProduct?.stock}</strong>
</p>
<div className="space-y-2">
<Label>Quantity Change (+/-)</Label>
<Input
type="number"
value={adjustQty}
onChange={(e) =>
setAdjustQty(
parseInt(e.target.value) || 0,
2026-01-27 18:07:00 -06:00
)
}
/>
</div>
<div className="space-y-2">
<Label>Notes</Label>
<Textarea
value={adjustNotes}
onChange={(e) =>
setAdjustNotes(e.target.value)
}
placeholder="Reason for adjustment..."
/>
</div>
</div>
<DialogFooter>
<Button onClick={handleAdjustInventory}>
Apply
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</td>
</tr>
));
})()}
</tbody>
</table>
</div>
{/* Pagination Controls */}
<div className="flex items-center justify-between mt-6 pt-6 border-t border-border">
<div className="text-sm text-muted-foreground">
{(() => {
const filtered = inventory.filter((p) => {
const matchesSearch =
inventorySearch === "" ||
p.name
.toLowerCase()
.includes(inventorySearch.toLowerCase()) ||
p.brand
?.toLowerCase()
.includes(inventorySearch.toLowerCase());
const matchesCategory =
inventoryCategory === "all" ||
p.category === inventoryCategory;
return matchesSearch && matchesCategory;
});
const start = Math.min(
(inventoryCurrentPage - 1) * inventoryItemsPerPage + 1,
filtered.length,
2026-01-27 18:07:00 -06:00
);
const end = Math.min(
inventoryCurrentPage * inventoryItemsPerPage,
filtered.length,
2026-01-27 18:07:00 -06:00
);
return `Showing ${
filtered.length === 0 ? 0 : start
} - ${end} of ${filtered.length} products`;
})()}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() =>
setInventoryCurrentPage((prev) => Math.max(1, prev - 1))
}
disabled={inventoryCurrentPage === 1}
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<div className="text-sm">
Page {inventoryCurrentPage} of{" "}
{Math.max(
1,
Math.ceil(
inventory.filter((p) => {
const matchesSearch =
inventorySearch === "" ||
p.name
.toLowerCase()
.includes(inventorySearch.toLowerCase()) ||
p.brand
?.toLowerCase()
.includes(inventorySearch.toLowerCase());
const matchesCategory =
inventoryCategory === "all" ||
p.category === inventoryCategory;
return matchesSearch && matchesCategory;
}).length / inventoryItemsPerPage,
),
2026-01-27 18:07:00 -06:00
)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setInventoryCurrentPage((prev) => prev + 1)}
disabled={
inventoryCurrentPage >=
Math.ceil(
inventory.filter((p) => {
const matchesSearch =
inventorySearch === "" ||
p.name
.toLowerCase()
.includes(inventorySearch.toLowerCase()) ||
p.brand
?.toLowerCase()
.includes(inventorySearch.toLowerCase());
const matchesCategory =
inventoryCategory === "all" ||
p.category === inventoryCategory;
return matchesSearch && matchesCategory;
}).length / inventoryItemsPerPage,
2026-01-27 18:07:00 -06:00
)
}
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</TabsContent>
{/* Bookings Tab */}
<TabsContent value="bookings">
<div className="bg-card border border-border rounded-xl p-6">
<h3 className="font-semibold font-['Outfit'] mb-6">
Service Bookings
</h3>
<div className="space-y-4">
{bookings.length === 0 ? (
<p className="text-muted-foreground text-center py-8">
No bookings yet
</p>
) : (
bookings.map((booking) => (
<div
key={booking.id}
className="border border-border rounded-lg p-4"
>
<div className="flex flex-col md:flex-row md:items-start justify-between gap-4">
<div className="flex-1">
<div className="flex flex-wrap gap-4 mb-3">
<div>
<p className="text-xs text-muted-foreground">
Service
</p>
<p className="font-medium">
{booking.service_name}
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">
Customer
</p>
<p className="text-sm">{booking.name}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">
Email
</p>
<p className="text-sm">{booking.email}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">
Phone
</p>
<p className="text-sm">{booking.phone}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">
Preferred Date
</p>
<p className="text-sm">
{booking.preferred_date}
</p>
</div>
{booking.completed_at && (
<div>
<p className="text-xs text-muted-foreground">
Completed
</p>
<p className="text-sm text-green-600">
{new Date(
booking.completed_at,
).toLocaleDateString()}
</p>
</div>
)}
</div>
{booking.notes && (
<p className="text-sm text-muted-foreground mb-2">
<span className="font-medium">Notes:</span>{" "}
{booking.notes}
</p>
)}
{booking.diagnosis && (
<p className="text-sm text-muted-foreground mb-1">
<span className="font-medium">Diagnosis:</span>{" "}
{booking.diagnosis}
</p>
)}
{booking.work_performed && (
<p className="text-sm text-muted-foreground">
<span className="font-medium">Work Done:</span>{" "}
{booking.work_performed}
</p>
)}
2026-01-27 18:07:00 -06:00
</div>
<div className="flex flex-col items-end gap-2">
<Badge
variant={
booking.status === "completed"
? "default"
: booking.status === "in-progress"
? "secondary"
: "outline"
}
className={
booking.status === "completed"
? "bg-green-500"
: ""
}
>
{booking.status === "completed" && (
<CheckCircle className="h-3 w-3 mr-1" />
)}
{booking.status === "in-progress" && (
<Clock className="h-3 w-3 mr-1" />
)}
{booking.status}
</Badge>
{booking.paid && (
<Badge className="bg-blue-500">
<DollarSign className="h-3 w-3 mr-1" />
Paid
</Badge>
)}
<div className="flex gap-2 mt-2">
{booking.status !== "completed" && (
<>
{booking.status === "pending" && (
<Button
size="sm"
variant="outline"
onClick={() =>
handleUpdateBookingStatus(
booking.id,
"in-progress",
)
}
>
<Clock className="h-4 w-4 mr-1" />
Start
</Button>
)}
<Button
size="sm"
variant="default"
className="bg-green-600 hover:bg-green-700"
onClick={() =>
handleOpenCompleteDialog(booking)
}
>
<CheckCircle className="h-4 w-4 mr-1" />
Complete
</Button>
</>
)}
<Button
size="sm"
variant="outline"
onClick={() => handlePrintReceipt(booking)}
>
<Printer className="h-4 w-4 mr-1" />
Receipt
</Button>
<Button
size="sm"
variant="outline"
className="text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => handleDeleteBooking(booking)}
>
<Trash2 className="h-4 w-4 mr-1" />
Delete
</Button>
</div>
2026-01-27 18:07:00 -06:00
</div>
</div>
</div>
))
)}
2026-01-27 18:07:00 -06:00
</div>
</div>
</TabsContent>
{/* Categories Tab */}
<TabsContent value="categories">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold font-['Outfit']">
Categories Management
</h3>
<Dialog open={categoryDialog} onOpenChange={setCategoryDialog}>
<DialogTrigger asChild>
<Button
className="gap-2"
onClick={() => {
setEditingCategory(null);
setCategoryForm({ name: "", description: "" });
}}
>
<Plus className="h-4 w-4" />
Add Category
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>
{editingCategory ? "Edit Category" : "Add Category"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Name</Label>
<Input
value={categoryForm.name}
onChange={(e) =>
setCategoryForm({
...categoryForm,
name: e.target.value,
})
}
placeholder="e.g., Phones, Laptops"
/>
</div>
<div className="space-y-2">
<Label>Description (Optional)</Label>
<Textarea
value={categoryForm.description}
onChange={(e) =>
setCategoryForm({
...categoryForm,
description: e.target.value,
})
}
placeholder="Brief description of the category"
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setCategoryDialog(false)}
>
Cancel
</Button>
<Button onClick={handleCategorySubmit}>
{editingCategory ? "Update" : "Create"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-2">Name</th>
<th className="text-left py-3 px-2">Description</th>
<th className="text-right py-3 px-2">Actions</th>
</tr>
</thead>
<tbody>
{categories.map((category) => (
<tr
key={category.id}
className="border-b border-border/50"
>
<td className="py-3 px-2">
<span className="font-medium">{category.name}</span>
</td>
<td className="py-3 px-2">
<span className="text-muted-foreground text-sm">
{category.description || "No description"}
</span>
</td>
<td className="py-3 px-2 text-right">
<div className="flex justify-end gap-2">
<Button
size="icon"
variant="ghost"
onClick={() => {
setEditingCategory(category);
setCategoryForm({
name: category.name,
description: category.description || "",
});
setCategoryDialog(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="text-destructive"
onClick={() => handleDeleteCategory(category.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{categories.length === 0 && (
<div className="text-center py-12 text-muted-foreground">
No categories yet. Create one to get started.
</div>
)}
</div>
</div>
</TabsContent>
{/* Reports Tab */}
{/* Users Tab */}
<TabsContent value="users">
<div className="space-y-6">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
<h3 className="font-semibold font-['Outfit']">
User Management
</h3>
<Dialog open={userDialog} onOpenChange={setUserDialog}>
<DialogTrigger asChild>
<Button
onClick={() => {
setEditingUser(null);
setUserForm({
name: "",
email: "",
password: "",
role: "user",
is_active: true,
});
}}
className="gap-2"
>
<Plus className="h-4 w-4" />
Add User
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>
{editingUser ? "Edit User" : "Add New User"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Name</Label>
<Input
value={userForm.name}
onChange={(e) =>
setUserForm({ ...userForm, name: e.target.value })
}
placeholder="Enter user name"
/>
</div>
<div>
<Label>Email</Label>
<Input
type="email"
value={userForm.email}
onChange={(e) =>
setUserForm({
...userForm,
email: e.target.value,
})
}
placeholder="user@example.com"
/>
</div>
<div>
<Label>
Password{" "}
{editingUser && "(leave blank to keep current)"}
</Label>
<Input
type="password"
value={userForm.password}
onChange={(e) =>
setUserForm({
...userForm,
password: e.target.value,
})
}
placeholder={
editingUser
? "Leave blank to keep current password"
: "Enter password"
}
/>
{editingUser && (
<p className="text-xs text-muted-foreground mt-1">
Only enter a new password if you want to change it
</p>
)}
</div>
<div>
<Label>Role</Label>
<Select
value={userForm.role}
onValueChange={(value) =>
setUserForm({ ...userForm, role: value })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">User</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="employee">Employee</SelectItem>
<SelectItem value="accountant">
Accountant
</SelectItem>
<SelectItem value="sales_manager">
Sales Manager
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="is_active"
checked={userForm.is_active}
onChange={(e) =>
setUserForm({
...userForm,
is_active: e.target.checked,
})
}
className="h-4 w-4 rounded border-gray-300"
/>
<Label htmlFor="is_active">Active Account</Label>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setUserDialog(false);
setEditingUser(null);
}}
>
Cancel
</Button>
<Button onClick={handleUserSubmit}>
{editingUser ? "Update" : "Create"} User
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Filters */}
<div className="flex flex-col md:flex-row gap-4 mb-6">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by name or email..."
value={userSearch}
onChange={(e) => setUserSearch(e.target.value)}
className="pl-10"
/>
</div>
</div>
<Select
value={userRoleFilter || undefined}
onValueChange={(value) =>
setUserRoleFilter(value === "all" ? "" : value)
}
>
<SelectTrigger className="w-full md:w-[180px]">
<SelectValue placeholder="All Roles" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Roles</SelectItem>
<SelectItem value="user">User</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="employee">Employee</SelectItem>
<SelectItem value="accountant">Accountant</SelectItem>
<SelectItem value="sales_manager">
Sales Manager
</SelectItem>
</SelectContent>
</Select>
<Select
value={userStatusFilter || undefined}
onValueChange={(value) =>
setUserStatusFilter(value === "all" ? "" : value)
}
>
<SelectTrigger className="w-full md:w-[180px]">
<SelectValue placeholder="All Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
onClick={fetchUsers}
className="gap-2"
>
<RefreshCw className="h-4 w-4" />
Apply
</Button>
</div>
{/* Users Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b">
<tr>
<th className="text-left p-3 font-medium">Name</th>
<th className="text-left p-3 font-medium">Email</th>
<th className="text-left p-3 font-medium">Role</th>
<th className="text-left p-3 font-medium">Status</th>
<th className="text-left p-3 font-medium">Created</th>
<th className="text-right p-3 font-medium">Actions</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr
key={user.id}
className="border-b hover:bg-muted/50"
>
<td className="p-3">{user.name}</td>
<td className="p-3">{user.email}</td>
<td className="p-3">
<Badge variant="outline" className="capitalize">
{user.role.replace("_", " ")}
</Badge>
</td>
<td className="p-3">
<Badge
variant={user.is_active ? "default" : "secondary"}
>
{user.is_active ? "Active" : "Inactive"}
</Badge>
</td>
<td className="p-3">
{new Date(user.created_at).toLocaleDateString()}
</td>
<td className="p-3">
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="outline"
onClick={() => handleToggleUserActive(user.id)}
title={
user.is_active ? "Deactivate" : "Activate"
}
>
{user.is_active ? "Deactivate" : "Activate"}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => {
setEditingUser(user);
setUserForm({
name: user.name,
email: user.email,
password: "",
role: user.role,
is_active: user.is_active,
});
setUserDialog(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
className="text-destructive hover:text-destructive"
onClick={() => {
showConfirmDialog(
"Delete User",
`Are you sure you want to delete ${user.name}? This action cannot be undone.`,
() => handleDeleteUser(user.id),
);
2026-01-27 18:07:00 -06:00
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="flex flex-col md:flex-row items-center justify-between gap-4 mt-6 pt-6 border-t">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
Items per page:
</span>
<Select
value={usersPerPage.toString()}
onValueChange={(value) => {
setUsersPerPage(parseInt(value));
setCurrentUsersPage(1);
}}
>
<SelectTrigger className="w-[80px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() =>
setCurrentUsersPage((p) => Math.max(1, p - 1))
}
disabled={currentUsersPage === 1}
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<span className="text-sm text-muted-foreground px-4">
Page {currentUsersPage} of{" "}
{Math.ceil(usersTotal / usersPerPage) || 1}
</span>
<Button
variant="outline"
size="sm"
onClick={() =>
setCurrentUsersPage((p) =>
Math.min(Math.ceil(usersTotal / usersPerPage), p + 1),
2026-01-27 18:07:00 -06:00
)
}
disabled={
currentUsersPage >= Math.ceil(usersTotal / usersPerPage)
}
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="reports">
<div className="space-y-6">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
<h3 className="font-semibold font-['Outfit']">
Sales Reports
</h3>
<div className="flex gap-2">
<Select
value={reportPeriod}
onValueChange={(v) => {
setReportPeriod(v);
setTimeout(fetchReports, 100);
}}
>
<SelectTrigger className="w-[150px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="daily">Daily</SelectItem>
<SelectItem value="weekly">Weekly</SelectItem>
<SelectItem value="monthly">Monthly</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
className="gap-2"
onClick={() => handleExportCSV("sales")}
>
<FileSpreadsheet className="h-4 w-4" />
CSV
</Button>
<Button
variant="outline"
className="gap-2"
onClick={() => handleExportPDF("sales")}
>
<FileText className="h-4 w-4" />
PDF
</Button>
</div>
</div>
{salesReport && (
<>
{/* Summary */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-muted rounded-lg p-4">
<p className="text-sm text-muted-foreground">
Total Orders
</p>
<p className="text-2xl font-bold">
{salesReport.summary.total_orders}
</p>
</div>
<div className="bg-muted rounded-lg p-4">
<p className="text-sm text-muted-foreground">
Product Revenue
2026-01-27 18:07:00 -06:00
</p>
<p className="text-2xl font-bold">
${salesReport.summary.total_revenue.toFixed(2)}
</p>
</div>
<div className="bg-muted rounded-lg p-4">
<p className="text-sm text-muted-foreground">
Products Sold
</p>
<p className="text-2xl font-bold">
{salesReport.summary.total_products_sold}
</p>
</div>
<div className="bg-muted rounded-lg p-4">
<p className="text-sm text-muted-foreground">
Avg Order Value
</p>
<p className="text-2xl font-bold">
${salesReport.summary.average_order_value.toFixed(2)}
</p>
</div>
</div>
{/* Service Revenue Summary */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-4 border border-green-200 dark:border-green-800">
<p className="text-sm text-green-700 dark:text-green-400">
Services Booked
</p>
<p className="text-2xl font-bold text-green-800 dark:text-green-300">
{salesReport.summary.total_services_booked || 0}
</p>
</div>
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-4 border border-green-200 dark:border-green-800">
<p className="text-sm text-green-700 dark:text-green-400">
Services Paid
</p>
<p className="text-2xl font-bold text-green-800 dark:text-green-300">
{salesReport.summary.total_services_paid || 0}
</p>
</div>
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-4 border border-green-200 dark:border-green-800">
<p className="text-sm text-green-700 dark:text-green-400">
Service Revenue
</p>
<p className="text-2xl font-bold text-green-800 dark:text-green-300">
$
{(
salesReport.summary.total_service_revenue || 0
).toFixed(2)}
</p>
</div>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
<p className="text-sm text-blue-700 dark:text-blue-400">
Combined Revenue
</p>
<p className="text-2xl font-bold text-blue-800 dark:text-blue-300">
$
{(salesReport.summary.combined_revenue || 0).toFixed(
2,
)}
</p>
</div>
</div>
2026-01-27 18:07:00 -06:00
{/* Data Table */}
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-2">Period</th>
<th className="text-right py-3 px-2">Orders</th>
<th className="text-right py-3 px-2">
Product Rev
2026-01-27 18:07:00 -06:00
</th>
<th className="text-right py-3 px-2">Services</th>
<th className="text-right py-3 px-2">Paid</th>
2026-01-27 18:07:00 -06:00
<th className="text-right py-3 px-2">
Service Rev
2026-01-27 18:07:00 -06:00
</th>
</tr>
</thead>
<tbody>
{salesReport.data.map((row, idx) => (
<tr key={idx} className="border-b border-border/50">
<td className="py-3 px-2">{row.period}</td>
<td className="py-3 px-2 text-right">
{row.orders}
</td>
<td className="py-3 px-2 text-right">
${row.revenue.toFixed(2)}
</td>
<td className="py-3 px-2 text-right">
{row.services_booked || 0}
2026-01-27 18:07:00 -06:00
</td>
<td className="py-3 px-2 text-right">
{row.services_paid || 0}
</td>
<td className="py-3 px-2 text-right text-green-600">
${(row.service_revenue || 0).toFixed(2)}
2026-01-27 18:07:00 -06:00
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</div>
</div>
</TabsContent>
{/* Media Tab */}
<TabsContent value="media">
<div className="bg-card border border-border rounded-xl p-6">
<MediaManager />
</div>
</TabsContent>
2026-01-27 18:07:00 -06:00
{/* About Page Tab */}
<TabsContent value="about">
<div className="space-y-6">
{/* Hero/Story Content Section */}
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold font-['Outfit']">
Page Content
</h3>
<Button
onClick={() => {
setAboutEditType("content");
setEditingAboutItem(null);
setAboutForm({
section: "hero",
title: "",
subtitle: "",
content: "",
image_url: "",
data: null,
display_order: 0,
is_active: true,
});
setAboutDialog(true);
}}
className="gap-2"
>
<Plus className="h-4 w-4" />
Add Content Section
</Button>
</div>
<div className="space-y-3">
{aboutContent.map((item) => (
<div
key={item.id}
className="flex items-center justify-between p-4 bg-muted rounded-lg"
>
<div className="flex-1">
<div className="flex items-center gap-2">
<Badge variant="outline">{item.section}</Badge>
<h4 className="font-medium">
{item.title || "No title"}
</h4>
{!item.is_active && (
<Badge variant="secondary">Inactive</Badge>
)}
</div>
{item.subtitle && (
<p className="text-sm text-muted-foreground mt-1">
{item.subtitle}
</p>
)}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setAboutEditType("content");
setEditingAboutItem(item);
setAboutForm({
section: item.section,
title: item.title || "",
subtitle: item.subtitle || "",
content: item.content || "",
image_url: item.image_url || "",
data: item.data,
display_order: item.display_order,
is_active: item.is_active,
});
setAboutDialog(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
handleDeleteAboutItem(item.id, "content")
}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
{aboutContent.length === 0 && (
<p className="text-center text-muted-foreground py-8">
No content sections yet. Add your first section!
</p>
)}
</div>
</div>
{/* Team Members Section */}
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold font-['Outfit']">
Team Members
</h3>
<Button
onClick={() => {
setAboutEditType("team");
setEditingAboutItem(null);
setAboutForm({
name: "",
role: "",
bio: "",
image_url: "",
email: "",
linkedin: "",
display_order: 0,
is_active: true,
});
setAboutDialog(true);
}}
className="gap-2"
>
<Plus className="h-4 w-4" />
Add Team Member
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{teamMembers.map((member) => (
<div
key={member.id}
className="border border-border rounded-lg p-4 space-y-3"
>
{member.image_url && (
<img
src={member.image_url}
alt={member.name}
className="w-full h-48 object-cover rounded-lg"
/>
)}
<div>
<div className="flex items-center gap-2">
<h4 className="font-medium">{member.name}</h4>
{!member.is_active && (
<Badge variant="secondary" className="text-xs">
Inactive
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground">
{member.role}
</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
setAboutEditType("team");
setEditingAboutItem(member);
setAboutForm({
name: member.name,
role: member.role,
bio: member.bio || "",
image_url: member.image_url || "",
email: member.email || "",
linkedin: member.linkedin || "",
display_order: member.display_order,
is_active: member.is_active,
});
setAboutDialog(true);
}}
>
<Edit className="h-4 w-4 mr-1" />
Edit
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
handleDeleteAboutItem(member.id, "team")
}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
{teamMembers.length === 0 && (
<p className="text-center text-muted-foreground py-8">
No team members yet. Add your first team member!
</p>
)}
</div>
{/* Company Values Section */}
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold font-['Outfit']">
Company Values
</h3>
<Button
onClick={() => {
setAboutEditType("value");
setEditingAboutItem(null);
setAboutForm({
title: "",
description: "",
icon: "",
display_order: 0,
is_active: true,
});
setAboutDialog(true);
}}
className="gap-2"
>
<Plus className="h-4 w-4" />
Add Value
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{companyValues.map((value) => (
<div
key={value.id}
className="border border-border rounded-lg p-4 space-y-2"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
{value.icon && (
<span className="text-2xl">{value.icon}</span>
)}
<h4 className="font-medium">{value.title}</h4>
{!value.is_active && (
<Badge variant="secondary" className="text-xs">
Inactive
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground mt-2">
{value.description}
</p>
</div>
</div>
<div className="flex gap-2 pt-2">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
setAboutEditType("value");
setEditingAboutItem(value);
setAboutForm({
title: value.title,
description: value.description,
icon: value.icon,
display_order: value.display_order,
is_active: value.is_active,
});
setAboutDialog(true);
}}
>
<Edit className="h-4 w-4 mr-1" />
Edit
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
handleDeleteAboutItem(value.id, "value")
}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
{companyValues.length === 0 && (
<p className="text-center text-muted-foreground py-8">
No company values yet. Add your first value!
</p>
)}
</div>
</div>
</TabsContent>
</Tabs>
</div>
{/* About Page Dialog */}
<Dialog open={aboutDialog} onOpenChange={setAboutDialog}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{editingAboutItem ? "Edit" : "Add"}{" "}
{aboutEditType === "content" && "Content Section"}
{aboutEditType === "team" && "Team Member"}
{aboutEditType === "value" && "Company Value"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* Content Section Form */}
{aboutEditType === "content" && (
<>
<div>
<Label>Section</Label>
<Select
value={aboutForm.section}
onValueChange={(v) =>
setAboutForm({ ...aboutForm, section: v })
}
>
<SelectTrigger>
<SelectValue placeholder="Select section" />
</SelectTrigger>
<SelectContent>
<SelectItem value="hero">Hero</SelectItem>
<SelectItem value="story">Our Story</SelectItem>
<SelectItem value="stats">Statistics</SelectItem>
<SelectItem value="mission">Mission</SelectItem>
<SelectItem value="vision">Vision</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Title</Label>
<Input
value={aboutForm.title}
onChange={(e) =>
setAboutForm({ ...aboutForm, title: e.target.value })
}
/>
</div>
<div>
<Label>Subtitle</Label>
<Input
value={aboutForm.subtitle}
onChange={(e) =>
setAboutForm({ ...aboutForm, subtitle: e.target.value })
}
/>
</div>
<div>
<Label>Content (HTML)</Label>
<RichTextEditor
value={aboutForm.content}
onChange={(html) =>
setAboutForm({ ...aboutForm, content: html })
}
/>
</div>
<div>
<Label>Image</Label>
<ImageUploadManager
images={aboutForm.image_url ? [aboutForm.image_url] : []}
onChange={(imgs) =>
setAboutForm({ ...aboutForm, image_url: imgs[0] || "" })
}
token={token}
/>
</div>
</>
)}
{/* Team Member Form */}
{aboutEditType === "team" && (
<>
<div>
<Label>Name*</Label>
<Input
value={aboutForm.name}
onChange={(e) =>
setAboutForm({ ...aboutForm, name: e.target.value })
}
required
/>
</div>
<div>
<Label>Role*</Label>
<Input
value={aboutForm.role}
onChange={(e) =>
setAboutForm({ ...aboutForm, role: e.target.value })
}
required
/>
</div>
<div>
<Label>Bio (HTML)</Label>
<RichTextEditor
value={aboutForm.bio}
onChange={(html) =>
setAboutForm({ ...aboutForm, bio: html })
}
/>
</div>
<div>
<Label>Email</Label>
<Input
type="email"
value={aboutForm.email}
onChange={(e) =>
setAboutForm({ ...aboutForm, email: e.target.value })
}
/>
</div>
<div>
<Label>LinkedIn URL</Label>
<Input
value={aboutForm.linkedin}
onChange={(e) =>
setAboutForm({ ...aboutForm, linkedin: e.target.value })
}
placeholder="https://linkedin.com/in/..."
/>
</div>
<div>
<Label>Profile Image</Label>
<ImageUploadManager
images={aboutForm.image_url ? [aboutForm.image_url] : []}
onChange={(imgs) =>
setAboutForm({ ...aboutForm, image_url: imgs[0] || "" })
}
token={token}
/>
</div>
</>
)}
{/* Company Value Form */}
{aboutEditType === "value" && (
<>
<div>
<Label>Title*</Label>
<Input
value={aboutForm.title}
onChange={(e) =>
setAboutForm({ ...aboutForm, title: e.target.value })
}
required
/>
</div>
<div>
<Label>Description*</Label>
<Textarea
value={aboutForm.description}
onChange={(e) =>
setAboutForm({
...aboutForm,
description: e.target.value,
})
}
rows={3}
required
/>
</div>
<div>
<Label>Icon (emoji or icon name)</Label>
<Input
value={aboutForm.icon}
onChange={(e) =>
setAboutForm({ ...aboutForm, icon: e.target.value })
}
placeholder="💡 or icon-name"
/>
</div>
</>
)}
{/* Common fields */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Display Order</Label>
<Input
type="number"
value={aboutForm.display_order}
onChange={(e) =>
setAboutForm({
...aboutForm,
display_order: parseInt(e.target.value) || 0,
})
}
/>
</div>
<div className="flex items-center gap-2 pt-6">
<input
type="checkbox"
checked={aboutForm.is_active}
onChange={(e) =>
setAboutForm({ ...aboutForm, is_active: e.target.checked })
}
className="h-4 w-4"
/>
<Label>Active</Label>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setAboutDialog(false)}>
Cancel
</Button>
<Button onClick={handleSaveAboutItem}>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Product</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete{" "}
<span className="font-semibold text-foreground">
"{productToDelete?.name}"
</span>
? This action cannot be undone. The product will be removed from
the database and will no longer be available to customers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setProductToDelete(null)}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteProduct}
className="bg-destructive hover:bg-destructive/90"
>
Delete Product
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Generic Confirmation Dialog */}
<AlertDialog
open={confirmDialog.open}
onOpenChange={(open) => !open && handleCancelConfirm()}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{confirmDialog.title}</AlertDialogTitle>
<AlertDialogDescription>
{confirmDialog.message}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={handleCancelConfirm}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmAction}
className="bg-destructive hover:bg-destructive/90"
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Booking Complete Dialog */}
<Dialog
open={bookingCompleteDialog}
onOpenChange={setBookingCompleteDialog}
>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>
{isEditMode ? "Edit Booking Details" : "Complete Service"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4 max-h-[60vh] overflow-y-auto">
<div className="bg-muted p-3 rounded-lg">
<p className="font-medium">{selectedBooking?.service_name}</p>
<p className="text-sm text-muted-foreground">
Customer: {selectedBooking?.name}
</p>
</div>
{/* Device Information Section */}
<div className="border border-border rounded-lg p-3 space-y-3">
<h4 className="font-medium text-sm">Device Information</h4>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-xs">Device Model</Label>
<Input
value={bookingCompleteForm.device_model}
onChange={(e) =>
setBookingCompleteForm({
...bookingCompleteForm,
device_model: e.target.value,
})
}
placeholder="e.g., Dell Latitude 5520"
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Screen Size</Label>
<Input
value={bookingCompleteForm.screen_size}
onChange={(e) =>
setBookingCompleteForm({
...bookingCompleteForm,
screen_size: e.target.value,
})
}
placeholder="e.g., 15-inch"
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Serial Number</Label>
<Input
value={bookingCompleteForm.serial_number}
onChange={(e) =>
setBookingCompleteForm({
...bookingCompleteForm,
serial_number: e.target.value,
})
}
placeholder="Enter serial number"
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Product Number</Label>
<Input
value={bookingCompleteForm.product_number}
onChange={(e) =>
setBookingCompleteForm({
...bookingCompleteForm,
product_number: e.target.value,
})
}
placeholder="Enter product number"
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label>Diagnosis / Issue Found</Label>
<Textarea
value={bookingCompleteForm.diagnosis}
onChange={(e) =>
setBookingCompleteForm({
...bookingCompleteForm,
diagnosis: e.target.value,
})
}
placeholder="Describe what was wrong with the device..."
rows={3}
/>
</div>
<div className="space-y-2">
<Label>Work Performed</Label>
<Textarea
value={bookingCompleteForm.work_performed}
onChange={(e) =>
setBookingCompleteForm({
...bookingCompleteForm,
work_performed: e.target.value,
})
}
placeholder="Describe what was done to fix it..."
rows={3}
/>
</div>
<div className="space-y-2">
<Label>Technician Notes (Internal)</Label>
<Textarea
value={bookingCompleteForm.technician_notes}
onChange={(e) =>
setBookingCompleteForm({
...bookingCompleteForm,
technician_notes: e.target.value,
})
}
placeholder="Any internal notes (not shown on receipt)..."
rows={2}
/>
</div>
<div className="space-y-2">
<Label>Final Service Cost ($)</Label>
<Input
type="number"
step="0.01"
value={bookingCompleteForm.service_cost}
onChange={(e) =>
setBookingCompleteForm({
...bookingCompleteForm,
service_cost: e.target.value,
})
}
placeholder="Leave blank to use base service price"
/>
</div>
{!isEditMode && (
<div className="flex items-center space-x-2 pt-2">
<input
type="checkbox"
id="paid-checkbox"
checked={bookingCompleteForm.paid}
onChange={(e) =>
setBookingCompleteForm({
...bookingCompleteForm,
paid: e.target.checked,
})
}
className="h-4 w-4 rounded border-gray-300"
/>
<Label
htmlFor="paid-checkbox"
className="text-sm font-medium cursor-pointer"
>
Payment Received
</Label>
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setBookingCompleteDialog(false)}
>
Cancel
</Button>
{isEditMode ? (
<Button onClick={handleSaveBooking}>Save Changes</Button>
) : (
<Button
onClick={handleCompleteBooking}
className="bg-green-600 hover:bg-green-700"
>
<CheckCircle className="h-4 w-4 mr-2" />
Mark as Completed
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
{/* Receipt Dialog */}
<Dialog open={showReceiptDialog} onOpenChange={setShowReceiptDialog}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Service Receipt</DialogTitle>
</DialogHeader>
{receiptData && (
<>
<div
ref={receiptRef}
className="bg-white text-black p-6 rounded-lg print-receipt"
>
<style>{`
@media print {
.print-receipt { padding: 10px !important; }
.print-receipt .logo img { max-height: 50px !important; width: auto !important; }
.print-receipt .section { margin-bottom: 12px !important; }
.print-receipt h2 { font-size: 16px !important; margin-bottom: 12px !important; }
.print-receipt h3 { font-size: 12px !important; }
.print-receipt .text-sm { font-size: 11px !important; }
.print-receipt .text-xs { font-size: 10px !important; }
}
`}</style>
<div className="receipt">
{/* Header */}
<div className="header text-center border-b-2 border-blue-600 pb-3 mb-4">
<div className="logo flex flex-col items-center mb-2">
<img
src="/logo.png"
alt="PromptTech Solutions"
className="h-12 max-h-12 w-auto mb-1"
style={{ maxWidth: "120px" }}
onError={(e) => {
e.target.style.display = "none";
}}
/>
<span className="text-xl font-bold text-blue-600">
PromptTech Solutions
</span>
</div>
<div className="company-info text-xs text-gray-600">
<p>{receiptData.company?.address}</p>
<p>
Phone: {receiptData.company?.phone} | Email:{" "}
{receiptData.company?.email}
</p>
</div>
</div>
{/* Receipt Title */}
<h2 className="text-lg font-bold text-center mb-4">
SERVICE RECEIPT
</h2>
{/* Customer Info */}
<div className="section mb-4">
<h3 className="font-bold border-b pb-1 mb-2 text-sm">
Customer Information
</h3>
<div className="grid grid-cols-2 gap-1 text-sm">
<div>
<span className="font-medium text-gray-600">Name:</span>{" "}
{receiptData.name}
</div>
<div>
<span className="font-medium text-gray-600">
Phone:
</span>{" "}
{receiptData.phone}
</div>
<div>
<span className="font-medium text-gray-600">
Email:
</span>{" "}
{receiptData.email}
</div>
<div>
<span className="font-medium text-gray-600">
Date Booked:
</span>{" "}
{new Date(receiptData.created_at).toLocaleDateString()}
</div>
</div>
</div>
{/* Service Info */}
<div className="section mb-4">
<h3 className="font-bold border-b pb-1 mb-2 text-sm">
Service Details
</h3>
<div className="grid grid-cols-2 gap-1 text-sm">
<div>
<span className="font-medium text-gray-600">
Service:
</span>{" "}
{receiptData.service_name}
</div>
<div>
<span className="font-medium text-gray-600">
Preferred Date:
</span>{" "}
{receiptData.preferred_date}
</div>
<div>
<span className="font-medium text-gray-600">
Status:
</span>{" "}
{receiptData.status}
</div>
{receiptData.completed_at && (
<div>
<span className="font-medium text-gray-600">
Completed:
</span>{" "}
{new Date(
receiptData.completed_at,
).toLocaleDateString()}
</div>
)}
</div>
</div>
{/* Device Information */}
{(receiptData.device_model ||
receiptData.serial_number ||
receiptData.product_number ||
receiptData.screen_size) && (
<div className="section mb-4">
<h3 className="font-bold border-b pb-1 mb-2 text-sm">
Device Information
</h3>
<div className="grid grid-cols-2 gap-1 text-sm">
{receiptData.device_model && (
<div>
<span className="font-medium text-gray-600">
Model:
</span>{" "}
{receiptData.device_model}
</div>
)}
{receiptData.screen_size && (
<div>
<span className="font-medium text-gray-600">
Screen Size:
</span>{" "}
{receiptData.screen_size}
</div>
)}
{receiptData.serial_number && (
<div>
<span className="font-medium text-gray-600">
Serial #:
</span>{" "}
{receiptData.serial_number}
</div>
)}
{receiptData.product_number && (
<div>
<span className="font-medium text-gray-600">
Product #:
</span>{" "}
{receiptData.product_number}
</div>
)}
</div>
</div>
)}
{/* Diagnosis & Work */}
{(receiptData.diagnosis || receiptData.work_performed) && (
<div className="section mb-4">
<h3 className="font-bold border-b pb-1 mb-2 text-sm">
Service Report
</h3>
{receiptData.diagnosis && (
<div className="bg-gray-50 p-2 rounded mb-2">
<p className="font-medium text-gray-600 mb-1">
Diagnosis:
</p>
<p className="text-sm">{receiptData.diagnosis}</p>
</div>
)}
{receiptData.work_performed && (
<div className="bg-gray-50 p-2 rounded">
<p className="font-medium text-gray-600 mb-1">
Work Performed:
</p>
<p className="text-sm">
{receiptData.work_performed}
</p>
</div>
)}
</div>
)}
{/* Notes */}
{receiptData.notes && (
<div className="section mb-4">
<h3 className="font-bold border-b pb-1 mb-2">
Customer Notes
</h3>
<p className="text-sm bg-gray-50 p-2 rounded">
{receiptData.notes}
</p>
</div>
)}
{/* Total */}
<div className="total text-lg font-bold text-right border-t-2 border-gray-800 pt-3 mt-4">
Total: ${receiptData.final_cost?.toFixed(2) || "0.00"}
</div>
{/* Footer */}
<div className="footer text-center mt-4 pt-3 border-t text-xs text-gray-500">
<p>Thank you for choosing PromptTech Solutions!</p>
<p>Questions? Contact us at {receiptData.company?.phone}</p>
</div>
</div>
</div>
<DialogFooter className="mt-4">
<Button
variant="outline"
onClick={() => setShowReceiptDialog(false)}
>
Close
</Button>
<Button onClick={printReceipt}>
<Printer className="h-4 w-4 mr-2" />
Print / Save as PDF
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
2026-01-27 18:07:00 -06:00
</div>
);
};
export default AdminDashboard;