mirror of
https://github.com/Motion-Games/MotionWebStudio.git
synced 2026-04-21 09:00:53 +02:00
init
This commit is contained in:
313
pages/Dashboard.tsx
Normal file
313
pages/Dashboard.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { Button } from '../components/Button';
|
||||
import { LogOut, User, Settings as SettingsIcon, CreditCard, Layout, Cake, ShieldAlert, Clock, Activity, CheckCircle, XCircle, MessageSquare, ArrowRight, Edit2, Download, FileText, ExternalLink, History, RefreshCw } from 'lucide-react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { supabase, isSupabaseConfigured } from '../lib/supabaseClient';
|
||||
import { SettingsModal } from '../components/SettingsModal';
|
||||
import { FeedbackModal } from '../components/FeedbackModal';
|
||||
import { Invoice } from '../types';
|
||||
|
||||
interface UserProfile {
|
||||
id: string;
|
||||
email: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
date_of_birth: string;
|
||||
}
|
||||
|
||||
interface OrderHistoryEntry {
|
||||
id: string;
|
||||
status: string;
|
||||
changed_at: string;
|
||||
}
|
||||
|
||||
interface UserOrder {
|
||||
id: string;
|
||||
created_at: string;
|
||||
package: string;
|
||||
status: string;
|
||||
amount: string;
|
||||
displayId?: string;
|
||||
details?: any;
|
||||
history?: OrderHistoryEntry[];
|
||||
}
|
||||
|
||||
export const Dashboard: React.FC = () => {
|
||||
const { user, signOut, isAdmin } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [orders, setOrders] = useState<UserOrder[]>([]);
|
||||
const [invoices, setInvoices] = useState<Invoice[]>([]);
|
||||
const [loadingOrders, setLoadingOrders] = useState(true);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
const [feedbackModalOpen, setFeedbackModalOpen] = useState(false);
|
||||
const [selectedOrderId, setSelectedOrderId] = useState<string | null>(null);
|
||||
const [feedbackLoading, setFeedbackLoading] = useState(false);
|
||||
|
||||
const fetchOrderHistory = async (orderId: string) => {
|
||||
if (!isSupabaseConfigured) return [];
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('order_status_history')
|
||||
.select('*')
|
||||
.eq('order_id', orderId)
|
||||
.order('changed_at', { ascending: false });
|
||||
return data || [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!user) return;
|
||||
setLoadingOrders(true);
|
||||
|
||||
if (!isSupabaseConfigured) {
|
||||
const meta = user.user_metadata || {};
|
||||
setProfile({ id: user.id, email: user.email || '', first_name: meta.first_name || '', last_name: meta.last_name || '', date_of_birth: meta.date_of_birth || '1990-01-01' });
|
||||
setOrders([{ id: 'demo-order-1', created_at: new Date().toISOString(), package: 'Pro Web', status: 'pending_feedback', amount: '350.000 Ft', displayId: 'DEMO-123', details: { demoUrl: 'https://example.com' }, history: [{ id: 'h1', status: 'new', changed_at: new Date().toISOString() }] }]);
|
||||
setLoadingOrders(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: profileData } = await supabase.from('profiles').select('*').eq('id', user.id).maybeSingle();
|
||||
if (profileData) setProfile(profileData);
|
||||
|
||||
const { data: orderData } = await supabase.from('orders').select('*').eq('user_id', user.id).order('created_at', { ascending: false });
|
||||
|
||||
if (orderData) {
|
||||
const enrichedOrders = await Promise.all(orderData.map(async (o) => {
|
||||
const history = await fetchOrderHistory(o.id);
|
||||
return {
|
||||
...o,
|
||||
displayId: o.id.substring(0, 8).toUpperCase(),
|
||||
history: history
|
||||
};
|
||||
}));
|
||||
setOrders(enrichedOrders as UserOrder[]);
|
||||
}
|
||||
|
||||
const { data: invoiceData } = await supabase.from('invoices').select('*').eq('user_id', user.id).order('created_at', { ascending: false });
|
||||
if (invoiceData) setInvoices(invoiceData as Invoice[]);
|
||||
|
||||
} catch (err) {
|
||||
console.error("Unexpected error fetching data:", err);
|
||||
} finally {
|
||||
setLoadingOrders(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [user]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
await signOut();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const openFeedbackModal = (orderId: string) => {
|
||||
setSelectedOrderId(orderId);
|
||||
setFeedbackModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmitFeedback = async (feedbackData: any) => {
|
||||
if (!selectedOrderId) return;
|
||||
setFeedbackLoading(true);
|
||||
|
||||
try {
|
||||
if (!isSupabaseConfigured) {
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
setOrders(prev => prev.map(o => o.id === selectedOrderId ? { ...o, status: 'in_progress' } : o));
|
||||
setFeedbackModalOpen(false);
|
||||
setFeedbackLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: currentOrder } = await supabase.from('orders').select('details').eq('id', selectedOrderId).single();
|
||||
const updatedDetails = { ...(currentOrder?.details || {}), latestFeedback: feedbackData, feedbackDate: new Date().toISOString() };
|
||||
|
||||
// Approved status also goes to in_progress now, admin will set to completed later
|
||||
const { error: updateError } = await supabase.from('orders').update({
|
||||
status: 'in_progress',
|
||||
details: updatedDetails
|
||||
}).eq('id', selectedOrderId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
|
||||
// Log history
|
||||
await supabase.from('order_status_history').insert({ order_id: selectedOrderId, status: 'in_progress' });
|
||||
|
||||
await fetchData();
|
||||
setFeedbackModalOpen(false);
|
||||
} catch (err: any) {
|
||||
alert("Hiba: " + err.message);
|
||||
} finally {
|
||||
setFeedbackLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getFullName = () => profile?.last_name && profile?.first_name ? `${profile.last_name} ${profile.first_name}` : user?.email?.split('@')[0] || 'Felhasználó';
|
||||
|
||||
const getStatusConfig = (status: string) => {
|
||||
switch (status) {
|
||||
case 'new': return { label: 'Beérkezett', color: 'bg-blue-100 text-blue-800', icon: Clock };
|
||||
case 'in_progress':
|
||||
case 'progress': return { label: 'Fejlesztés', color: 'bg-yellow-100 text-yellow-800', icon: Activity };
|
||||
case 'pending_feedback':
|
||||
case 'waiting_feedback':
|
||||
case 'feedback': return { label: 'Visszajelzés', color: 'bg-purple-100 text-purple-800', icon: MessageSquare };
|
||||
case 'completed': return { label: 'Kész', color: 'bg-green-100 text-green-800', icon: CheckCircle };
|
||||
case 'cancelled': return { label: 'Törölve', color: 'bg-gray-100 text-gray-500', icon: XCircle };
|
||||
default: return { label: 'Beérkezett', color: 'bg-gray-100 text-gray-800', icon: Clock };
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pt-24 bg-gray-50 min-h-screen">
|
||||
<SettingsModal isOpen={showSettings} onClose={() => setShowSettings(false)} userProfile={profile} onUpdate={fetchData} />
|
||||
<FeedbackModal isOpen={feedbackModalOpen} onClose={() => setFeedbackModalOpen(false)} onSubmit={handleSubmitFeedback} loading={feedbackLoading} />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="bg-white rounded-[24px] shadow-lg border border-gray-100 overflow-hidden mb-8">
|
||||
<div className="bg-gradient-to-r from-gray-900 to-gray-800 p-8 text-white flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2">Fiók Áttekintése</h1>
|
||||
<p className="text-gray-300">Üdvözöljük, {getFullName()}!</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-white/10 rounded-full flex items-center justify-center border border-white/20">
|
||||
<User className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
|
||||
{/* ORDERS COLUMN */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="font-bold text-gray-900 flex items-center gap-2 text-xl">
|
||||
<Layout className="w-5 h-5 text-primary" /> Aktív Projektjeim
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{loadingOrders ? (
|
||||
<div className="flex justify-center py-10"><RefreshCw className="animate-spin text-primary" /></div>
|
||||
) : orders.length > 0 ? (
|
||||
<div className="space-y-6">
|
||||
{orders.map(order => {
|
||||
const statusConfig = getStatusConfig(order.status);
|
||||
const needsFeedback = ['pending_feedback', 'waiting_feedback', 'feedback'].includes(order.status);
|
||||
|
||||
return (
|
||||
<div key={order.id} className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div className="p-6 flex flex-col sm:flex-row justify-between gap-6 border-b border-gray-50 bg-gray-50/30">
|
||||
<div>
|
||||
<p className="text-xs font-black text-primary uppercase tracking-widest mb-1">{order.package}</p>
|
||||
<h4 className="text-xl font-bold text-gray-900">ID: {order.displayId}</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{needsFeedback && (
|
||||
<div className="flex gap-2">
|
||||
<a href={order.details?.demoUrl || '#'} target="_blank" rel="noreferrer" className="text-xs px-4 py-2 rounded-xl font-black bg-blue-600 text-white flex items-center gap-2 shadow-lg shadow-blue-200">
|
||||
<ExternalLink className="w-3 h-3" /> Demó oldal megnyitása
|
||||
</a>
|
||||
<button onClick={() => openFeedbackModal(order.id)} className="text-xs px-4 py-2 rounded-xl font-black bg-purple-100 text-purple-700 hover:bg-purple-200">
|
||||
VISSZAJELZÉS
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!needsFeedback && (
|
||||
<span className={`text-xs px-4 py-2 rounded-xl font-black flex items-center gap-2 ${statusConfig.color}`}>
|
||||
<statusConfig.icon className="w-4 h-4" /> {statusConfig.label.toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PROJECT TIMELINE */}
|
||||
<div className="p-6 bg-white">
|
||||
<div className="flex items-center gap-2 mb-4 text-xs font-black text-gray-400 uppercase tracking-[0.2em]">
|
||||
<History className="w-3 h-3" /> Projekt Történet
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 relative before:absolute before:left-[7px] before:top-2 before:bottom-2 before:w-0.5 before:bg-gray-50">
|
||||
{order.history && order.history.length > 0 ? (
|
||||
order.history.map((h, idx) => (
|
||||
<div key={h.id} className="relative pl-6 flex justify-between items-center group">
|
||||
<div className={`absolute left-0 top-1.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm z-10 ${idx === 0 ? 'bg-primary' : 'bg-gray-200'}`}></div>
|
||||
<div className="flex flex-col">
|
||||
<span className={`text-xs font-bold ${idx === 0 ? 'text-gray-900' : 'text-gray-500'}`}>
|
||||
{getStatusConfig(h.status).label}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400 font-medium">
|
||||
{new Date(h.changed_at).toLocaleString('hu-HU')}
|
||||
</span>
|
||||
</div>
|
||||
{idx === 0 && <span className="text-[9px] font-black text-primary bg-primary/5 px-2 py-0.5 rounded uppercase tracking-tighter">Aktuális</span>}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-xs text-gray-400 italic pl-6">Nincs korábbi bejegyzés.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white p-10 rounded-3xl border-2 border-dashed border-gray-100 text-center">
|
||||
<p className="text-gray-400 mb-6 italic">Még nincsenek leadott rendelései.</p>
|
||||
<Link to="/#rendeles"><Button>Új projekt indítása <ArrowRight className="w-4 h-4 ml-2" /></Button></Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SETTINGS & INVOICES COLUMN */}
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="font-bold text-gray-900">Beállítások</h3>
|
||||
<button onClick={() => setShowSettings(true)} className="p-2 hover:bg-gray-50 rounded-xl transition-colors"><SettingsIcon className="w-5 h-5 text-gray-400" /></button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 p-4 rounded-2xl">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">E-mail</p>
|
||||
<p className="text-sm font-bold text-gray-900">{user?.email}</p>
|
||||
</div>
|
||||
<Button fullWidth variant="outline" size="sm" onClick={() => setShowSettings(true)}>Profil szerkesztése</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm">
|
||||
<h3 className="font-bold text-gray-900 mb-6 flex items-center gap-2"><CreditCard className="w-5 h-5 text-primary" /> Számlák</h3>
|
||||
{invoices.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{invoices.map(inv => (
|
||||
<div key={inv.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-xl group hover:bg-gray-100 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-5 h-5 text-gray-300" />
|
||||
<span className="text-xs font-bold text-gray-700">{inv.invoice_number}</span>
|
||||
</div>
|
||||
<button className="p-2 text-gray-400 hover:text-primary"><Download className="w-4 h-4" /></button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : <p className="text-xs text-gray-400 italic text-center">Nincs számlázott tétel.</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-100 pt-6 flex justify-end">
|
||||
<Button variant="outline" onClick={handleLogout} className="border-red-200 text-red-600 hover:bg-red-50 hover:text-red-700 hover:border-red-300">
|
||||
<LogOut className="w-4 h-4 mr-2" /> Kijelentkezés
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user