- Replace router.push with window.location.href for reliable redirects - Fix auth check to wait for component mount before accessing localStorage - Ensure proper redirect after successful login - Fix redirect behavior on all admin pages (dashboard, colors, requests)
360 lines
15 KiB
TypeScript
360 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { colorRequestService } from '@/src/services/api';
|
|
import Link from 'next/link';
|
|
|
|
interface ColorRequest {
|
|
id: string;
|
|
color_name: string;
|
|
material_type: string;
|
|
finish_type: string;
|
|
user_email: string;
|
|
user_phone: string;
|
|
user_name: string;
|
|
description: string;
|
|
reference_url: string;
|
|
status: 'pending' | 'approved' | 'rejected' | 'completed';
|
|
admin_notes: string;
|
|
request_count: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
processed_at: string;
|
|
processed_by: string;
|
|
}
|
|
|
|
export default function ColorRequestsAdmin() {
|
|
const [requests, setRequests] = useState<ColorRequest[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [editingId, setEditingId] = useState<string | null>(null);
|
|
const [editForm, setEditForm] = useState({ status: '', admin_notes: '' });
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
checkAuth();
|
|
fetchRequests();
|
|
}, []);
|
|
|
|
const checkAuth = () => {
|
|
const token = localStorage.getItem('authToken');
|
|
const expiry = localStorage.getItem('tokenExpiry');
|
|
|
|
if (!token || !expiry || new Date().getTime() > parseInt(expiry)) {
|
|
window.location.href = '/upadaj';
|
|
}
|
|
};
|
|
|
|
const fetchRequests = async () => {
|
|
try {
|
|
const data = await colorRequestService.getAll();
|
|
setRequests(data);
|
|
} catch (error) {
|
|
setError('Failed to fetch color requests');
|
|
console.error('Error:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleStatusUpdate = async (id: string) => {
|
|
try {
|
|
await colorRequestService.updateStatus(id, editForm.status, editForm.admin_notes);
|
|
await fetchRequests();
|
|
setEditingId(null);
|
|
setEditForm({ status: '', admin_notes: '' });
|
|
} catch (error) {
|
|
setError('Failed to update request');
|
|
console.error('Error:', error);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (!confirm('Are you sure you want to delete this request?')) return;
|
|
|
|
try {
|
|
await colorRequestService.delete(id);
|
|
await fetchRequests();
|
|
} catch (error) {
|
|
setError('Failed to delete request');
|
|
console.error('Error:', error);
|
|
}
|
|
};
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
const colors = {
|
|
pending: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300',
|
|
approved: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300',
|
|
rejected: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300',
|
|
completed: 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300'
|
|
};
|
|
return colors[status as keyof typeof colors] || 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300';
|
|
};
|
|
|
|
const getStatusLabel = (status: string) => {
|
|
const labels = {
|
|
pending: 'Na čekanju',
|
|
approved: 'Odobreno',
|
|
rejected: 'Odbijeno',
|
|
completed: 'Završeno'
|
|
};
|
|
return labels[status as keyof typeof labels] || status;
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
if (!dateString) return '-';
|
|
const date = new Date(dateString);
|
|
const month = date.toLocaleDateString('sr-RS', { month: 'short' });
|
|
const capitalizedMonth = month.charAt(0).toUpperCase() + month.slice(1);
|
|
return `${capitalizedMonth} ${date.getDate()}, ${date.getFullYear()}`;
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
|
|
<div className="text-gray-500 dark:text-gray-400">Učitavanje zahteva za boje...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h1 className="text-3xl font-bold text-gray-800 dark:text-gray-100">Zahtevi za Boje</h1>
|
|
<div className="space-x-4">
|
|
<Link
|
|
href="/upadaj/dashboard"
|
|
className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
|
|
>
|
|
Inventar
|
|
</Link>
|
|
<Link
|
|
href="/upadaj/colors"
|
|
className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
|
|
>
|
|
Boje
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mb-4 p-4 bg-red-100 text-red-700 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-gray-100 dark:bg-gray-700">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Boja
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Materijal/Finiš
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Broj Zahteva
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Korisnik
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Datum
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Akcije
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
{requests.map((request) => (
|
|
<tr key={request.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
<td className="px-4 py-3">
|
|
<div>
|
|
<div className="font-medium text-gray-900 dark:text-gray-100">{request.color_name}</div>
|
|
{request.description && (
|
|
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">{request.description}</div>
|
|
)}
|
|
{request.reference_url && (
|
|
<a
|
|
href={request.reference_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
|
>
|
|
Pogledaj referencu
|
|
</a>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="text-sm">
|
|
<div className="text-gray-900 dark:text-gray-100">{request.material_type}</div>
|
|
{request.finish_type && (
|
|
<div className="text-gray-500 dark:text-gray-400">{request.finish_type}</div>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300">
|
|
{request.request_count || 1} {(request.request_count || 1) === 1 ? 'zahtev' : 'zahteva'}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="text-sm">
|
|
{request.user_email ? (
|
|
<a href={`mailto:${request.user_email}`} className="text-blue-600 dark:text-blue-400 hover:underline">
|
|
{request.user_email}
|
|
</a>
|
|
) : (
|
|
<span className="text-gray-400 dark:text-gray-500">Anonimno</span>
|
|
)}
|
|
{request.user_phone && (
|
|
<div className="mt-1">
|
|
<a href={`tel:${request.user_phone}`} className="text-blue-600 dark:text-blue-400 hover:underline">
|
|
{request.user_phone}
|
|
</a>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
{editingId === request.id ? (
|
|
<div className="space-y-2">
|
|
<select
|
|
value={editForm.status}
|
|
onChange={(e) => setEditForm({ ...editForm, status: e.target.value })}
|
|
className="text-sm border rounded px-2 py-1"
|
|
>
|
|
<option value="">Izaberi status</option>
|
|
<option value="pending">Na čekanju</option>
|
|
<option value="approved">Odobreno</option>
|
|
<option value="rejected">Odbijeno</option>
|
|
<option value="completed">Završeno</option>
|
|
</select>
|
|
<textarea
|
|
placeholder="Napomene..."
|
|
value={editForm.admin_notes}
|
|
onChange={(e) => setEditForm({ ...editForm, admin_notes: e.target.value })}
|
|
className="text-sm border rounded px-2 py-1 w-full"
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadge(request.status)}`}>
|
|
{getStatusLabel(request.status)}
|
|
</span>
|
|
{request.admin_notes && (
|
|
<div className="text-xs text-gray-500 mt-1">{request.admin_notes}</div>
|
|
)}
|
|
{request.processed_by && (
|
|
<div className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
|
od {request.processed_by}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
{formatDate(request.created_at)}
|
|
</div>
|
|
{request.processed_at && (
|
|
<div className="text-xs text-gray-500 dark:text-gray-500">
|
|
Obrađeno: {formatDate(request.processed_at)}
|
|
</div>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
{editingId === request.id ? (
|
|
<div className="space-x-2">
|
|
<button
|
|
onClick={() => handleStatusUpdate(request.id)}
|
|
className="text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300 text-sm"
|
|
>
|
|
Sačuvaj
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setEditingId(null);
|
|
setEditForm({ status: '', admin_notes: '' });
|
|
}}
|
|
className="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-300 text-sm"
|
|
>
|
|
Otkaži
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-x-2">
|
|
<button
|
|
onClick={() => {
|
|
setEditingId(request.id);
|
|
setEditForm({
|
|
status: request.status,
|
|
admin_notes: request.admin_notes || ''
|
|
});
|
|
}}
|
|
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 text-sm"
|
|
>
|
|
Izmeni
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(request.id)}
|
|
className="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 text-sm"
|
|
>
|
|
Obriši
|
|
</button>
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{requests.length === 0 && (
|
|
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
Nema zahteva za boje
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-6 grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Ukupno Zahteva</div>
|
|
<div className="text-2xl font-bold text-gray-800 dark:text-gray-100">
|
|
{requests.length}
|
|
</div>
|
|
</div>
|
|
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Na Čekanju</div>
|
|
<div className="text-2xl font-bold text-yellow-600 dark:text-yellow-400">
|
|
{requests.filter(r => r.status === 'pending').length}
|
|
</div>
|
|
</div>
|
|
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Odobreno</div>
|
|
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
|
|
{requests.filter(r => r.status === 'approved').length}
|
|
</div>
|
|
</div>
|
|
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Završeno</div>
|
|
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
|
{requests.filter(r => r.status === 'completed').length}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |