Files
MotionWebStudio/pages/Dashboard.tsx

509 lines
26 KiB
TypeScript
Raw Normal View History

2025-12-26 14:03:18 +01:00
2025-12-21 20:40:32 +01:00
import React, { useEffect, useState } from 'react';
import { useAuth } from '../context/AuthContext';
import { Button } from '../components/Button';
2025-12-26 14:03:18 +01:00
import { LogOut, User, Settings as SettingsIcon, CreditCard, Layout, Cake, ShieldAlert, Clock, Activity, CheckCircle, XCircle, MessageSquare, ArrowRight, Edit2, Download, FileText, ExternalLink, History, RefreshCw, FileDown, ShieldCheck, Calendar } from 'lucide-react';
2025-12-29 16:07:33 +01:00
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
2025-12-21 20:40:32 +01:00
import { supabase, isSupabaseConfigured } from '../lib/supabaseClient';
import { SettingsModal } from '../components/SettingsModal';
import { FeedbackModal } from '../components/FeedbackModal';
2025-12-26 14:03:18 +01:00
import { Invoice, MaintenanceSubscription } from '../types';
2025-12-21 20:40:32 +01:00
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;
}
2025-12-26 14:03:18 +01:00
interface Bill {
id: string;
order_id: string;
type: 'advance' | 'final';
file_url: string;
created_at: string;
}
2025-12-21 20:40:32 +01:00
interface UserOrder {
id: string;
created_at: string;
package: string;
status: string;
amount: string;
displayId?: string;
details?: any;
history?: OrderHistoryEntry[];
2025-12-26 14:03:18 +01:00
bills?: Bill[];
subscription?: MaintenanceSubscription;
2025-12-21 20:40:32 +01:00
}
export const Dashboard: React.FC = () => {
const { user, signOut, isAdmin } = useAuth();
const navigate = useNavigate();
2025-12-29 16:07:33 +01:00
const [searchParams] = useSearchParams();
2025-12-21 20:40:32 +01:00
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);
2025-12-29 16:07:33 +01:00
// Check for payment success/cancel query params
useEffect(() => {
const paymentSuccess = searchParams.get('payment_success');
const orderIdParam = searchParams.get('order_id');
if (paymentSuccess === 'true' && orderIdParam) {
// Opcionális: Itt lehetne egy sikeres fizetés modált megjeleníteni vagy toast üzenetet
// A státusz frissítést (pl. 'pending_feedback' -> 'in_progress') a webhook végezné ideális esetben,
// de kliens oldalon is jelezhetjük a sikert.
// Mivel a redirect után újratölt az oldal, a fetchData friss adatokat hoz.
// Töröljük a query paramokat, hogy ne maradjanak ott
window.history.replaceState({}, '', '#/dashboard');
alert("Sikeres fizetés! Köszönjük.");
}
}, [searchParams]);
2025-12-21 20:40:32 +01:00
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 [];
}
};
2025-12-26 14:03:18 +01:00
const fetchOrderBills = async (orderId: string) => {
if (!isSupabaseConfigured) return [];
try {
const { data } = await supabase
.from('bills')
.select('*')
.eq('order_id', orderId)
.order('created_at', { ascending: false });
return data || [];
} catch (e) {
return [];
}
};
const fetchSubscription = async (orderId: string) => {
if (!isSupabaseConfigured) return undefined;
try {
const { data } = await supabase
.from('maintenance_subscriptions')
.select('*')
.eq('order_id', orderId)
.maybeSingle();
return data as MaintenanceSubscription | undefined;
} catch (e) {
return undefined;
}
}
2025-12-21 20:40:32 +01:00
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' });
2025-12-26 14:03:18 +01:00
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',
payment_summary: {
total: 350000,
advance: 80000,
remaining: 270000,
currency: 'HUF'
}
},
history: [{ id: 'h1', status: 'new', changed_at: new Date().toISOString() }],
bills: []
}]);
2025-12-21 20:40:32 +01:00
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);
2025-12-26 14:03:18 +01:00
const bills = await fetchOrderBills(o.id);
const sub = await fetchSubscription(o.id);
2025-12-21 20:40:32 +01:00
return {
...o,
displayId: o.id.substring(0, 8).toUpperCase(),
2025-12-26 14:03:18 +01:00
history: history,
bills: bills,
subscription: sub
2025-12-21 20:40:32 +01:00
};
}));
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;
}
2025-12-29 16:07:33 +01:00
// 1. Fetch fresh order data to get payment details
const { data: currentOrder } = await supabase.from('orders').select('*').eq('id', selectedOrderId).single();
if (!currentOrder) throw new Error("Rendelés nem található");
const isApproved = feedbackData.decision === 'approved';
const paymentSummary = currentOrder.details?.payment_summary;
const isStandardPackage = ['Landing Page', 'Pro Web'].includes(currentOrder.package);
// Check if payment is needed: Approved + Standard Package + Not Custom Price + Has Remaining Balance
// Note: paymentSummary.remaining > 0 check is implicitly handled by Logic in FeedbackModal (it shows payment button only if true)
// But we double check here for security.
const needsPayment = isApproved && isStandardPackage && !paymentSummary?.is_custom && (paymentSummary?.remaining > 0);
if (needsPayment) {
// Initiate Stripe Checkout for Final Payment
const { data: checkoutData, error: checkoutError } = await supabase.functions.invoke('create-checkout-session', {
body: {
order_id: selectedOrderId,
package_name: currentOrder.package,
payment_type: 'final',
customer_email: user?.email
}
});
if (checkoutError) {
console.error("Supabase Invoke Error:", checkoutError);
let msg = "Ismeretlen hiba";
if (checkoutError && typeof checkoutError === 'object' && 'message' in checkoutError) {
msg = checkoutError.message;
}
throw new Error(`Fizetési rendszer hiba: ${msg}`);
}
if (checkoutData?.url) {
// Update details with feedback content BEFORE redirecting
// This ensures we save the "Approval" state even if they drop off payment (status remains pending_feedback though)
const updatedDetails = {
...(currentOrder.details || {}),
latestFeedback: feedbackData,
feedbackDate: new Date().toISOString()
};
await supabase.from('orders').update({ details: updatedDetails }).eq('id', selectedOrderId);
// Redirect to Stripe
window.location.href = checkoutData.url;
return; // Stop execution to allow redirect
} else {
throw new Error("Nem sikerült létrehozni a fizetési linket.");
}
}
// If no payment needed (e.g. revision request, or custom price, or enterprise), update status directly
const updatedDetails = {
...(currentOrder.details || {}),
latestFeedback: feedbackData,
feedbackDate: new Date().toISOString()
};
const newStatus = 'in_progress'; // Send back to dev in both cases (to finalize or revise)
2025-12-21 20:40:32 +01:00
const { error: updateError } = await supabase.from('orders').update({
2025-12-29 16:07:33 +01:00
status: newStatus,
2025-12-21 20:40:32 +01:00
details: updatedDetails
}).eq('id', selectedOrderId);
if (updateError) throw updateError;
2025-12-29 16:07:33 +01:00
await supabase.from('order_status_history').insert({ order_id: selectedOrderId, status: newStatus });
2025-12-21 20:40:32 +01:00
await fetchData();
setFeedbackModalOpen(false);
2025-12-29 16:07:33 +01:00
alert("Visszajelzés sikeresen elküldve!");
2025-12-21 20:40:32 +01:00
} catch (err: any) {
2025-12-29 16:07:33 +01:00
console.error("Feedback submit error:", err);
alert("Hiba: " + (err.message || "Ismeretlen hiba történt."));
2025-12-21 20:40:32 +01:00
} 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 };
}
};
2025-12-26 14:03:18 +01:00
const selectedOrder = orders.find(o => o.id === selectedOrderId);
2025-12-21 20:40:32 +01:00
return (
2025-12-26 14:03:18 +01:00
<div className="pt-24 bg-gray-50 min-h-screen pb-12">
2025-12-21 20:40:32 +01:00
<SettingsModal isOpen={showSettings} onClose={() => setShowSettings(false)} userProfile={profile} onUpdate={fetchData} />
2025-12-26 14:03:18 +01:00
<FeedbackModal
isOpen={feedbackModalOpen}
onClose={() => setFeedbackModalOpen(false)}
onSubmit={handleSubmitFeedback}
loading={feedbackLoading}
order={selectedOrder}
/>
2025-12-21 20:40:32 +01:00
<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">
2025-12-26 14:03:18 +01:00
<div className="bg-gradient-to-r from-gray-900 to-gray-800 p-6 md:p-8 text-white flex flex-col md:flex-row justify-between items-center gap-4 text-center md:text-left">
2025-12-21 20:40:32 +01:00
<div>
2025-12-26 14:03:18 +01:00
<h1 className="text-2xl md:text-3xl font-bold mb-2">Fiók Áttekintése</h1>
<p className="text-gray-300 text-sm md:text-base">Üdvözöljük, {getFullName()}!</p>
2025-12-21 20:40:32 +01:00
</div>
2025-12-26 14:03:18 +01:00
<div className="h-12 w-12 bg-white/10 rounded-full flex items-center justify-center border border-white/20 shrink-0">
2025-12-21 20:40:32 +01:00
<User className="h-6 w-6 text-white" />
</div>
</div>
2025-12-26 14:03:18 +01:00
<div className="p-6 md:p-8">
2025-12-21 20:40:32 +01:00
<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">
2025-12-26 14:03:18 +01:00
<h3 className="font-bold text-gray-900 flex items-center gap-2 text-lg md:text-xl">
2025-12-21 20:40:32 +01:00
<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">
2025-12-26 14:03:18 +01:00
<div className="p-5 md:p-6 flex flex-col sm:flex-row justify-between gap-4 border-b border-gray-50 bg-gray-50/30">
2025-12-21 20:40:32 +01:00
<div>
2025-12-26 14:03:18 +01:00
<p className="text-[10px] font-black text-primary uppercase tracking-widest mb-1">{order.package}</p>
<h4 className="text-lg md:text-xl font-bold text-gray-900">ID: {order.displayId}</h4>
2025-12-21 20:40:32 +01:00
</div>
2025-12-26 14:03:18 +01:00
<div className="flex flex-wrap items-center gap-3">
2025-12-21 20:40:32 +01:00
{needsFeedback && (
2025-12-26 14:03:18 +01:00
<>
<a href={order.details?.demoUrl || '#'} target="_blank" rel="noreferrer" className="flex-grow sm:flex-grow-0 text-[10px] px-4 py-2.5 rounded-xl font-black bg-blue-600 text-white flex items-center justify-center gap-2 shadow-lg shadow-blue-200">
<ExternalLink className="w-3 h-3" /> DEMÓ MEGNYITÁSA
2025-12-21 20:40:32 +01:00
</a>
2025-12-26 14:03:18 +01:00
<button onClick={() => openFeedbackModal(order.id)} className="flex-grow sm:flex-grow-0 text-[10px] px-4 py-2.5 rounded-xl font-black bg-purple-100 text-purple-700 hover:bg-purple-200 uppercase tracking-wider">
Visszajelzés
2025-12-21 20:40:32 +01:00
</button>
2025-12-26 14:03:18 +01:00
</>
2025-12-21 20:40:32 +01:00
)}
{!needsFeedback && (
2025-12-26 14:03:18 +01:00
<span className={`text-[10px] px-4 py-2.5 rounded-xl font-black flex items-center justify-center gap-2 ${statusConfig.color} uppercase tracking-wider`}>
<statusConfig.icon className="w-3.5 h-3.5" /> {statusConfig.label}
2025-12-21 20:40:32 +01:00
</span>
)}
</div>
</div>
2025-12-26 14:03:18 +01:00
<div className="grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-gray-100">
{/* PROJECT TIMELINE */}
<div className="p-5 md:p-6 bg-white">
<div className="flex items-center gap-2 mb-4 text-[10px] 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>
</div>
))
) : (
<p className="text-xs text-gray-400 italic pl-6">Nincs korábbi bejegyzés.</p>
)}
</div>
</div>
{/* BILLS & DOCUMENTS / MAINTENANCE */}
<div className="p-5 md:p-6 bg-white flex flex-col justify-between">
<div>
<div className="flex items-center gap-2 mb-4 text-[10px] font-black text-gray-400 uppercase tracking-[0.2em]">
<FileText className="w-3 h-3" /> Dokumentumok
</div>
<div className="space-y-3">
{order.bills && order.bills.length > 0 ? (
order.bills.map((bill) => (
<a
key={bill.id}
href={bill.file_url}
target="_blank"
rel="noreferrer"
className="flex items-center justify-between p-3 rounded-xl border border-gray-100 hover:border-primary/30 hover:bg-purple-50/30 transition-all group"
>
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${bill.type === 'advance' ? 'bg-blue-50 text-blue-600' : 'bg-green-50 text-green-600'}`}>
<CreditCard className="w-4 h-4" />
</div>
<div>
<p className="text-xs font-bold text-gray-900">{bill.type === 'advance' ? 'Előleg számla' : 'Végszámla'}</p>
<p className="text-[9px] text-gray-400 font-black uppercase tracking-widest">{new Date(bill.created_at).toLocaleDateString('hu-HU')}</p>
</div>
</div>
<FileDown className="w-4 h-4 text-gray-300 group-hover:text-primary group-hover:translate-y-0.5 transition-all flex-shrink-0" />
</a>
))
) : (
<div className="p-4 text-center bg-gray-50/50 rounded-2xl border border-dashed border-gray-100">
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest leading-relaxed">Még nincs számla.</p>
</div>
)}
</div>
</div>
{/* Maintenance Subscription Status (If Completed) */}
{order.subscription && (
<div className="mt-6 pt-4 border-t border-gray-50">
<div className="flex items-center gap-2 mb-3 text-[10px] font-black text-gray-400 uppercase tracking-[0.2em]">
<ShieldCheck className="w-3 h-3 text-primary" /> Karbantartás
</div>
<div className={`p-3 rounded-xl border flex items-center justify-between ${order.subscription.status === 'overdue' ? 'bg-red-50 border-red-100' : 'bg-green-50 border-green-100'}`}>
<div className="flex items-center gap-2">
<Calendar className={`w-4 h-4 ${order.subscription.status === 'overdue' ? 'text-red-500' : 'text-green-500'}`} />
<div>
<p className={`text-[10px] font-black uppercase tracking-wider ${order.subscription.status === 'overdue' ? 'text-red-700' : 'text-green-700'}`}>
{order.subscription.status === 'active' ? 'Aktív szolgáltatás' : 'Esedékes díj'}
</p>
<p className="text-xs font-bold text-gray-700">Fordulónap: {new Date(order.subscription.next_billing_date).toLocaleDateString('hu-HU')}</p>
</div>
</div>
2025-12-21 20:40:32 +01:00
</div>
</div>
2025-12-26 14:03:18 +01:00
)}
2025-12-21 20:40:32 +01:00
</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>
2025-12-26 14:03:18 +01:00
{/* SETTINGS COLUMN */}
<div className="space-y-6 md:space-y-8">
<div className="bg-white p-6 md:p-8 rounded-[32px] border border-gray-100 shadow-sm">
2025-12-21 20:40:32 +01:00
<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">
2025-12-26 14:03:18 +01:00
<div className="bg-gray-50 p-4 rounded-2xl overflow-hidden">
2025-12-21 20:40:32 +01:00
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">E-mail</p>
2025-12-26 14:03:18 +01:00
<p className="text-sm font-bold text-gray-900 truncate">{user?.email}</p>
2025-12-21 20:40:32 +01:00
</div>
<Button fullWidth variant="outline" size="sm" onClick={() => setShowSettings(true)}>Profil szerkesztése</Button>
</div>
</div>
2025-12-26 14:03:18 +01:00
<div className="bg-gradient-to-br from-primary/10 to-secondary/10 p-6 md:p-8 rounded-[32px] border border-primary/10 shadow-sm">
<h3 className="font-bold text-gray-900 mb-4 flex items-center gap-2 text-lg"><CheckCircle className="w-5 h-5 text-primary" /> Ügyfélközpont</h3>
<p className="text-xs text-gray-600 leading-relaxed mb-6">
Bármilyen kérdése van a számlázással vagy a fejlesztéssel kapcsolatban, írjon nekünk közvetlenül.
</p>
<a href="mailto:motionstudiohq@gmail.com" className="block text-center py-3 bg-white border border-gray-200 rounded-xl text-xs font-black text-primary uppercase tracking-widest hover:shadow-md transition-all shadow-sm">
Kapcsolatfelvétel
</a>
2025-12-21 20:40:32 +01:00
</div>
</div>
</div>
2025-12-26 14:03:18 +01:00
<div className="border-t border-gray-100 pt-6 flex justify-center md:justify-end">
<Button variant="outline" onClick={handleLogout} className="w-full md:w-auto border-red-200 text-red-600 hover:bg-red-50 hover:text-red-700 hover:border-red-300">
2025-12-21 20:40:32 +01:00
<LogOut className="w-4 h-4 mr-2" /> Kijelentkezés
</Button>
</div>
</div>
</div>
</div>
</div>
);
2025-12-26 14:03:18 +01:00
};