import React, { useState, useEffect, useRef } from 'react'; import { useAuth } from '../context/AuthContext'; import { useNavigate } from 'react-router-dom'; import { ChevronLeft, RefreshCw, BarChart2, Users, ShoppingCart, Package, FileText, Globe, Sparkles, MessageSquare, AlertCircle, CheckCircle, XCircle, Clock, Activity, Save, Mail, Rocket, Edit3, Bell, AlertTriangle, Archive, Send, Layout, History, MessageCircle, Info, ChevronDown, ChevronUp, Upload, FileDown, Receipt, CreditCard, DollarSign, Plus, Trash2, Building2, User as UserIcon, Palette, Zap, Lightbulb, Link as LinkIcon, ExternalLink, Target, FileStack, Star, Check, Copy, Cpu, Wand2, Eye, EyeOff, ShieldCheck, Calendar, Search, ArrowUpDown, Filter } from 'lucide-react'; import { Button } from '../components/Button'; import { supabase, isSupabaseConfigured } from '../lib/supabaseClient'; import { ProductPackage, MaintenanceSubscription } from '../types'; import { defaultPlans } from '../lib/defaultPlans'; import { GoogleGenAI } from "@google/genai"; interface AdminUser { id: string; email: string; first_name?: string; last_name?: string; created_at: string; role: string; } interface OrderHistoryEntry { id: string; status: string; changed_at: string; } interface EmailLogEntry { id: string; email_type: string; sent_at: string; body?: string; } interface Bill { id: string; order_id: string; type: 'advance' | 'final'; file_url: string; created_at: string; } interface AdminOrder { id: string; user_id: string; displayId: string; customer: string; email: string; package: string; status: string; date: string; amount: string; details?: any; history?: OrderHistoryEntry[]; emailLogs?: EmailLogEntry[]; bills?: Bill[]; } // Extended interface for Subscription to include joined order data interface ExtendedSubscription extends MaintenanceSubscription { order?: { id: string; amount: string; customer_name?: string; customer_email: string; details?: { domainName?: string; company?: string; }; }; } interface AIPrompt { title: string; content: string; } const getEmailTemplate = (type: string, data: { customer: string, package: string, demoUrl?: string, billUrl?: string, billType?: string, domain?: string, daysLeft?: number, paymentLink?: string, amount?: string }) => { const baseStyle = "font-family: 'Inter', Helvetica, Arial, sans-serif; line-height: 1.6; color: #1a1a1a; max-width: 600px; margin: 0 auto; padding: 40px 20px; border: 1px solid #f0f0f0; border-radius: 24px; background-color: #ffffff;"; const headerStyle = "color: #7c3aed; font-size: 28px; font-weight: 800; margin-bottom: 24px; letter-spacing: -0.02em; border-bottom: 1px solid #f0f0f0; padding-bottom: 20px;"; const footerStyle = "margin-top: 40px; padding-top: 24px; border-top: 1px solid #f0f0f0; font-size: 13px; color: #94a3b8;"; const buttonStyle = "display: inline-block; background-color: #7c3aed; color: #ffffff !important; padding: 14px 28px; text-decoration: none; border-radius: 12px; font-weight: 700; margin: 20px 0; font-size: 15px;"; const templates: Record = { 'Fejlesztés megkezdése': { subject: `Projekt Indítása: Megkezdtem a fejlesztést - ${data.package}`, body: `
MotionWeb

Kedves ${data.customer}!

Örömmel értesítem, hogy a ${data.package} csomagjához tartozó fejlesztési folyamatot a mai napon hivatalosan is elindítottam.

A következő lépésben elkészül a weboldal első, bemutató (demó) verziója. Amint a demó megtekinthetővé válik, e-mailben értesítem.

Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, 'Demo elkészült': { subject: `Elkészült a weboldal demó verziója - ${data.package}`, body: `
MotionWeb

Kedves ${data.customer}!

Örömmel értesítem, hogy elkészültem weboldala első, interaktív bemutató verziójával!

DEMÓ MEGTEKINTÉSE

Kérjük, nézze át az oldalt az ügyfélkapun keresztül, és küldje el visszajelzését a gombra kattintva.

Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, '1 hete nincs válasz': { subject: `Emlékeztető: Visszajelzés kérése a demó oldalhoz`, body: `
MotionWeb

Kedves ${data.customer}!

Szeretnék érdeklődni, hogy volt-e alkalma megtekinteni a ${data.package} demó verzióját? Immár egy hete elküldtem Önnek a terveket.

Kérem, amennyiben ideje engedi, nézze át az oldalt és küldje el visszajelzését, hogy haladhassunk a véglegesítéssel.

ÜGYFÉLKAPU MEGNYITÁSA
Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, '2 hete nincs válasz': { subject: `Második emlékeztető: Várakozás a visszajelzésre`, body: `
MotionWeb

Kedves ${data.customer}!

Szeretném ismételten jelezni, hogy a ${data.package} demó verziója immár két hete várakozik az Ön jóváhagyására.

Fontos számunkra a folyamatos haladás, ezért kérem, szíveskedjen visszajelzést adni a projekt állapotáról.

ÜGYFÉLKAPU MEGNYITÁSA
Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, 'Projekt lezárva (1 hónap)': { subject: `Értesítés: Projekt lezárása kommunikáció hiánya miatt`, body: `
MotionWeb

Kedves ${data.customer}!

Sajnálattal értesítem, hogy mivel a demó oldal elkészülte óta egy hónap telt el visszajelzés nélkül, az ÁSZF-ben foglaltaknak megfelelően a projektet kommunikáció hiányában lezártnak tekintjük.

A projekt fájljait archiváltuk. Amennyiben később folytatni szeretné a munkát, kérjük vegye fel velünk a kapcsolatot.

Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, 'Módosítások fejlesztése': { subject: `Fejlesztés: Megkezdtem a kért módosítások átvezetését`, body: `
MotionWeb

Kedves ${data.customer}!

Köszönöm a visszajelzését a ${data.package} demó verziójával kapcsolatban.

A kért módosításokat feldolgoztam, és megkezdtem azok átvezetését a weboldalon. Amint elkészültem a frissített verzióval, e-mailben értesítem.

Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, 'Weboldal élesítése megkezdődött': { subject: `Folyamatban: Megkezdődött a weboldal élesítése`, body: `
MotionWeb

Kedves ${data.customer}!

A fejlesztési szakasz lezárult. Örömmel értesítem, hogy a ${data.package} projekt keretében megkezdtem a weboldal végleges élesítését a választott domain címen.

Ez a folyamat a DNS beállításoktól függően 24-48 órát vehet igénybe.

Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, 'Élesített weboldal elkészült': { subject: `Gratulálunk! Weboldala sikeresen élesítve lett`, body: `
MotionWeb

Kedves ${data.customer}!

Örömmel jelentem, hogy a ${data.package} projektet sikeresen befejeztem!

A weboldalt élesítettem, így az mostantól minden látogató számára elérhető. Köszönöm a megtisztelő bizalmat!

ÜGYFÉLKAPU MEGNYITÁSA
Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, 'Előlegszámla elkészült': { subject: `Előlegszámla a ${data.package} projekthez`, body: `
MotionWeb

Kedves ${data.customer}!

Értesítem, hogy a ${data.package} csomagjához tartozó előlegszámla elkészült.

A számlát megtekintheti és letöltheti az alábbi gombra kattintva:

SZÁMLA MEGTEKINTÉSE

A befizetés beérkezése után tudom folytatni a munkálatokat. Amennyiben bármilyen kérdése van, kérem jelezze.

Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, 'Végszámla elkészült': { subject: `Végszámla a ${data.package} projekthez`, body: `
MotionWeb

Kedves ${data.customer}!

A ${data.package} projekt a befejezéséhez közeledik. Értesítem, hogy a projekt végleges elszámolásához tartozó végszámla elkészült.

A számlát az alábbi gombra kattintva töltheti le:

SZÁMLA MEGTEKINTÉSE

A teljes összeg kiegyenlítése után történik meg a weboldal végleges élesítése.

Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, 'Előfizetés emlékeztető': { subject: `Emlékeztető: Éves karbantartási díj esedékes - ${data.domain || 'Weboldal'}`, body: `
MotionWeb

Kedves ${data.customer}!

Ezúton emlékeztetjük, hogy a(z) ${data.domain || 'weboldal'} éves karbantartási és üzemeltetési díja hamarosan (${data.daysLeft} nap múlva) esedékessé válik.

A szolgáltatás folyamatos biztosítása érdekében kérjük, rendezze a díjat az alábbi linken keresztül:

BEFIZETÉS INDÍTÁSA

Összeg: ${data.amount || '59 990 Ft'}

Amennyiben már rendezte a díjat, kérjük tekintse levelünket tárgytalannak.

Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` } }; return templates[type] || { subject: 'Értesítés a MotionWeb-től', body: 'Üzenete érkezett a MotionWeb Stúdiótól.' }; }; export const Admin: React.FC = () => { const { user, isAdmin, loading } = useAuth(); const navigate = useNavigate(); const [activeTab, setActiveTab] = useState<'overview' | 'users' | 'orders' | 'plans' | 'subscriptions'>('overview'); const [users, setUsers] = useState([]); const [orders, setOrders] = useState([]); const [plans, setPlans] = useState([]); const [subscriptions, setSubscriptions] = useState([]); const [loadingData, setLoadingData] = useState(false); const [visitorStats, setVisitorStats] = useState({ month: 0, week: 0 }); const [statusUpdating, setStatusUpdating] = useState(null); const [planSaving, setPlanSaving] = useState(null); const [errorMsg, setErrorMsg] = useState(null); const [viewOrder, setViewOrder] = useState(null); const [generatedPrompts, setGeneratedPrompts] = useState([]); const [generatingPrompts, setGeneratingPrompts] = useState(false); const [expandedLogId, setExpandedLogId] = useState(null); const [demoUrlInput, setDemoUrlInput] = useState(''); const [savingDemoUrl, setSavingDemoUrl] = useState(false); const [emailSending, setEmailSending] = useState(null); const [checkingSubs, setCheckingSubs] = useState(false); const [manualNotifying, setManualNotifying] = useState(null); // Filter & Sort State for Subscriptions const [subSearch, setSubSearch] = useState(''); const [subSort, setSubSort] = useState<'asc' | 'desc'>('asc'); const advanceBillInput = useRef(null); const finalBillInput = useRef(null); const [billUploading, setBillUploading] = useState(null); useEffect(() => { if (!loading && !isAdmin) { navigate('/dashboard'); } }, [isAdmin, loading, navigate]); useEffect(() => { if (isAdmin) fetchAdminData(); }, [isAdmin]); const fetchAdminData = async () => { setLoadingData(true); setErrorMsg(null); if (!isSupabaseConfigured) { setPlans(defaultPlans); setLoadingData(false); return; } try { const { data: profiles, error: pErr } = await supabase.from('profiles').select('*'); if (pErr) throw pErr; let rolesData: any[] = []; try { const { data: roles } = await supabase.from('roles').select('*'); rolesData = roles || []; } catch (e) { console.warn("Could not fetch roles table."); } setUsers(profiles.map(p => ({ id: p.id, email: p.email || 'N/A', first_name: p.first_name, last_name: p.last_name, created_at: p.created_at, role: rolesData.find(r => r.id === p.id)?.role || (p.email === 'motionstudiohq@gmail.com' ? 'admin' : 'user') }))); const { data: oData, error: oErr } = await supabase.from('orders').select('*').order('created_at', { ascending: false }); if (oErr) throw oErr; setOrders(oData.map(o => ({ ...o, displayId: o.id.substring(0, 8).toUpperCase(), customer: o.customer_name, email: o.customer_email }))); const { data: plData } = await supabase.from('plans').select('*'); if (plData && plData.length > 0) { const sortedPlans = [...plData].sort((a, b) => { const priceA = a.is_custom_price ? Infinity : (a.total_price || 0); const priceB = b.is_custom_price ? Infinity : (b.total_price || 0); return priceA - priceB; }); setPlans(sortedPlans); } else { setPlans(defaultPlans); } // Updated fetch for subscriptions to include Order details const { data: subsData } = await supabase .from('maintenance_subscriptions') .select(` *, order:orders ( id, amount, customer_name, customer_email, details ) `) .order('next_billing_date', { ascending: true }); if (subsData) { setSubscriptions(subsData as ExtendedSubscription[]); } const { count: mCount } = await supabase.from('page_visits').select('*', { count: 'exact', head: true }); setVisitorStats({ month: mCount || 0, week: Math.floor((mCount || 0) / 4) }); } catch (err: any) { setErrorMsg(err.message || "Hiba történt az adatok lekérésekor."); } finally { setLoadingData(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 fetchEmailLogs = async (orderId: string) => { if (!isSupabaseConfigured) return []; try { const { data, error } = await supabase .from('email_log') .select('*') .eq('order_id', orderId) .order('sent_at', { ascending: false }); return data || []; } catch (e) { return []; } }; const fetchBills = async (orderId: string) => { if (!isSupabaseConfigured) return []; try { const { data, error } = await supabase .from('bills') .select('*') .eq('order_id', orderId) .order('created_at', { ascending: false }); if (error) throw error; return data || []; } catch (e) { return []; } }; const handleStatusChange = async (orderId: string, newStatus: string) => { if (statusUpdating) return; setStatusUpdating(orderId); // Find order in current state const currentOrder = orders.find(o => o.id === orderId); if (!currentOrder) { setStatusUpdating(null); return; } try { if (isSupabaseConfigured) { // 1. Database Update (Status) const { error: updateError } = await supabase .from('orders') .update({ status: newStatus }) .eq('id', orderId); if (updateError) throw updateError; // 2. Log History const { error: historyError } = await supabase.from('order_status_history').insert({ order_id: orderId, status: newStatus }); if (historyError) console.warn("Failed to log status history:", historyError); // 3. Subscription Logic (Only for 'completed') if (newStatus === 'completed') { try { // Wait a bit to ensure trigger consistency if any await new Promise(r => setTimeout(r, 500)); // Check existing const { data: existing } = await supabase .from('maintenance_subscriptions') .select('id') .eq('order_id', orderId) .maybeSingle(); if (!existing) { const startDate = new Date(); const nextBilling = new Date(); nextBilling.setFullYear(nextBilling.getFullYear() + 1); // Valid email check const clientEmail = currentOrder.email || (currentOrder as any).customer_email || 'nincs_email@megadva.hu'; const subData: any = { order_id: orderId, client_email: clientEmail, start_date: startDate.toISOString(), next_billing_date: nextBilling.toISOString(), status: 'active' }; // Try adding user_id if present if (currentOrder.user_id) { subData.user_id = currentOrder.user_id; } const { error: subError } = await supabase .from('maintenance_subscriptions') .insert(subData); if (subError) { console.error('Subscription creation failed:', subError); const subMsg = subError?.message || JSON.stringify(subError); alert(`Figyelem: Státusz frissítve, de az előfizetés létrehozása sikertelen: ${subMsg}`); } else { alert('Sikeres átadás! Az 1 éves karbantartási időszak elindult, az előfizetés létrejött.'); fetchAdminData(); } } } catch (err: any) { console.error('Subscription logic error:', err); } } // 4. Update UI State const updatedOrder = { ...currentOrder, status: newStatus }; setOrders(prev => prev.map(o => o.id === orderId ? updatedOrder : o)); if (viewOrder && viewOrder.id === orderId) { const newHist = await fetchOrderHistory(orderId); setViewOrder(prev => prev ? { ...prev, status: newStatus, history: newHist } : null); } } else { // Demo Mode setOrders(prev => prev.map(o => o.id === orderId ? { ...o, status: newStatus } : o)); if (viewOrder && viewOrder.id === orderId) { setViewOrder(prev => prev ? { ...prev, status: newStatus } : null); } if (newStatus === 'completed') alert('Demo: Státusz kész, előfizetés szimulálva.'); } } catch (e: any) { console.error("Status update failed:", e); let errorMessage = "Ismeretlen hiba"; try { if (typeof e === 'string') errorMessage = e; else if (e instanceof Error) errorMessage = e.message; else if (typeof e === 'object' && e !== null) { errorMessage = e.message || e.error_description || JSON.stringify(e); } } catch (jsonErr) { errorMessage = "Nem szerializálható hiba objektum"; } alert("Hiba történt a státusz frissítésekor: " + errorMessage); } finally { setStatusUpdating(null); } }; const handleUpdateDemoUrl = async () => { if (!viewOrder || savingDemoUrl) return; setSavingDemoUrl(true); const orderId = viewOrder.id; const targetStatus = 'pending_feedback'; const updatedDetails = { ...(viewOrder.details || {}), demoUrl: demoUrlInput }; try { if (isSupabaseConfigured) { const { error } = await supabase.from('orders').update({ details: updatedDetails, status: targetStatus }).eq('id', orderId); if (error) throw error; await supabase.from('order_status_history').insert({ order_id: orderId, status: targetStatus }); const newHist = await fetchOrderHistory(orderId); setOrders(prev => prev.map(o => o.id === orderId ? { ...o, status: targetStatus, details: updatedDetails } : o)); setViewOrder({ ...viewOrder, status: targetStatus, details: updatedDetails, history: newHist }); if (demoUrlInput.startsWith('http')) { handleSendEmail('Demo elkészült'); } alert(`Sikeres mentés és visszajelzés kérése!`); } } catch (e: any) { alert("Hiba: " + e.message); } finally { setSavingDemoUrl(false); } }; const handleViewDetails = async (order: AdminOrder) => { setLoadingData(true); const history = await fetchOrderHistory(order.id); const emailLogs = await fetchEmailLogs(order.id); const bills = await fetchBills(order.id); setViewOrder({ ...order, history, emailLogs, bills }); setDemoUrlInput(order.details?.demoUrl || ''); setGeneratedPrompts([]); setLoadingData(false); }; const handleCheckSubscriptions = async () => { if (checkingSubs) return; setCheckingSubs(true); try { const { data, error } = await supabase.functions.invoke('check-subscriptions'); if (error) throw error; const count = data?.notifications?.length || 0; alert(`Ellenőrzés sikeres! ${count} db értesítés kiküldve (mock).`); await fetchAdminData(); // Refresh list just in case } catch (e: any) { alert("Hiba a futtatáskor: " + e.message); } finally { setCheckingSubs(false); } }; const handleManualNotification = async (sub: ExtendedSubscription) => { if (manualNotifying) return; setManualNotifying(sub.id); try { if (!isSupabaseConfigured) { await new Promise(r => setTimeout(r, 1000)); alert("Demo: E-mail elküldve."); setManualNotifying(null); return; } const daysLeft = getDaysRemaining(sub.next_billing_date); const emailType = 'Előfizetés emlékeztető'; const nowIso = new Date().toISOString(); // Placeholder payment link const paymentLink = "https://stripe.com/payment_placeholder"; const orderData = sub.order as any; // Cast to avoid TS issues if types aren't perfect const customerName = orderData?.customer_name || 'Ügyfél'; const customerEmail = sub.client_email || orderData?.customer_email; const domain = orderData?.details?.domainName; const amount = "59 990 Ft"; // Or use orderData.amount if applicable const template = getEmailTemplate(emailType, { customer: customerName, package: 'Fenntartás', // Dummy package name for template type signature domain: domain, daysLeft: daysLeft, paymentLink: paymentLink, amount: amount } as any); // 1. Send Email const { error: sendError } = await supabase.functions.invoke('resend', { body: { to: customerEmail, subject: template.subject, html: template.body, } }); if (sendError) throw sendError; // 2. Log to Database const orderId = sub.order_id || sub.order?.id; if (orderId) { const { error: logError } = await supabase.from('email_log').insert({ order_id: orderId, email_type: emailType, body: template.body, sent_at: nowIso }); if (logError) console.error("Error logging email:", logError); } else { console.warn("Manual notification sent, but could not log to history: Missing Order ID."); } // 3. Update last_notified_at in Database const { error: updateError } = await supabase.from('maintenance_subscriptions') .update({ last_notified_at: nowIso }) .eq('id', sub.id); if (updateError) console.error("Error updating last_notified_at:", updateError); // 4. Update local state immediately setSubscriptions(prev => prev.map(s => { if (s.id === sub.id) { return { ...s, last_notified_at: nowIso }; } return s; })); // Update local state if we are currently viewing this order (for log view) if (viewOrder && orderId && viewOrder.id === orderId) { // Small delay to ensure DB write consistency before fetch await new Promise(r => setTimeout(r, 500)); const newLogs = await fetchEmailLogs(orderId); setViewOrder(prev => prev ? { ...prev, emailLogs: newLogs } : null); } alert(`Sikeresen elküldve és naplózva: ${emailType}`); } catch (e: any) { console.error("Manual notification error:", e); let msg = "Ismeretlen hiba"; if (typeof e === 'string') msg = e; else if (e instanceof Error) msg = e.message; else if (typeof e === 'object') msg = JSON.stringify(e); alert("Hiba a küldéskor: " + msg); } finally { setManualNotifying(null); } }; const getDaysRemaining = (targetDate: string) => { const target = new Date(targetDate); const now = new Date(); const diffTime = target.getTime() - now.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); }; // Filter and Sort Subscriptions const filteredSubscriptions = subscriptions .filter(sub => { const domain = sub.order?.details?.domainName?.toLowerCase() || ''; const email = sub.client_email?.toLowerCase() || ''; const search = subSearch.toLowerCase(); return domain.includes(search) || email.includes(search); }) .sort((a, b) => { const dateA = new Date(a.next_billing_date).getTime(); const dateB = new Date(b.next_billing_date).getTime(); return subSort === 'asc' ? dateA - dateB : dateB - dateA; }); const handleGenerateAIPrompts = async () => { if (!viewOrder || generatingPrompts) return; setGeneratingPrompts(true); try { const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); const details = viewOrder.details || {}; const prompt = ` Készíts részletes technikai promptokat egy webfejlesztő AI számára az alábbi rendelési adatok alapján. A cél egy professzionális React weboldal fejlesztése. ADATOK: - Ügyfél: ${viewOrder.customer} - Csomag: ${viewOrder.package} - Bemutatkozás: ${details.description} - Színek: ${details.primaryColor}, ${details.secondaryColor}, ${details.balanceColor} - Stílus: ${details.style?.join(', ')} - Funkciók: ${details.features?.join(', ')} - Aloldalak: ${details.content?.join(', ')} Kérlek adj 3-4 különálló promptot (pl. Főoldal, Strukturális felépítés, Specifikus funkciók). A válasz JSON formátumban legyen: [{"title": "Prompt címe", "content": "Részletes prompt szövege"}] `; const response = await ai.models.generateContent({ model: 'gemini-3-pro-preview', contents: prompt, config: { responseMimeType: "application/json", thinkingConfig: { thinkingBudget: 10000 } } }); const result = JSON.parse(response.text || "[]"); setGeneratedPrompts(result); } catch (e: any) { alert("AI hiba: " + e.message); } finally { setGeneratingPrompts(false); } }; const handleBillUpload = async (type: 'advance' | 'final', e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !viewOrder) return; setBillUploading(type); try { const fileExt = file.name.split('.').pop(); const fileName = `${viewOrder.id}_${type}_${Date.now()}.${fileExt}`; const filePath = `invoices/${fileName}`; const { error: uploadError } = await supabase.storage .from('invoices') .upload(filePath, file); if (uploadError) throw uploadError; const { data: { publicUrl } } = supabase.storage .from('invoices') .getPublicUrl(filePath); const { error: dbError } = await supabase .from('bills') .insert({ order_id: viewOrder.id, type: type, file_url: publicUrl }); if (dbError) throw dbError; const newBills = await fetchBills(viewOrder.id); setViewOrder(prev => prev ? { ...prev, bills: newBills } : null); alert('Számla sikeresen feltöltve!'); } catch (err: any) { alert('Hiba a feltöltéskor: ' + err.message); } finally { setBillUploading(null); } }; const handleSendEmail = async (emailType: string, extraData: any = {}) => { if (!viewOrder || emailSending) return; setEmailSending(emailType); try { const template = getEmailTemplate(emailType, { customer: viewOrder.customer, package: viewOrder.package, demoUrl: demoUrlInput || viewOrder.details?.demoUrl, ...extraData }); if (isSupabaseConfigured) { const { error: sendError } = await supabase.functions.invoke('resend', { body: { to: viewOrder.email, subject: template.subject, html: template.body, } }); if (sendError) throw sendError; await supabase.from('email_log').insert({ order_id: viewOrder.id, email_type: emailType, body: template.body }); const newLogs = await fetchEmailLogs(viewOrder.id); setViewOrder(prev => prev ? { ...prev, emailLogs: newLogs } : null); alert(`Sikeres küldés: ${emailType}`); } } catch (e: any) { alert("Hiba: " + e.message); } finally { setEmailSending(null); } }; const StatusBadge = ({ status }: { status: string }) => { const configs: any = { new: { label: 'Új', color: 'bg-blue-100 text-blue-800' }, in_progress: { label: 'Folyamatban', color: 'bg-yellow-100 text-yellow-800' }, pending_feedback: { label: 'Visszajelzés', color: 'bg-purple-100 text-purple-800' }, completed: { label: 'Kész', color: 'bg-green-100 text-green-800' }, cancelled: { label: 'Törölve', color: 'bg-gray-100 text-gray-800' } }; const c = configs[status] || configs.new; return {c.label}; }; const BillSection = ({ type, label }: { type: 'advance' | 'final', label: string }) => { const bill = viewOrder?.bills?.find(b => b.type === type); const isUploading = billUploading === type; const templateName = type === 'advance' ? 'Előlegszámla elkészült' : 'Végszámla elkészült'; const isThisEmailSending = emailSending === templateName; return (
{type === 'advance' ? : }

{label}

{!bill ? (
(type === 'advance' ? advanceBillInput : finalBillInput).current?.click()} className="flex-grow border-2 border-dashed border-gray-100 rounded-xl flex flex-col items-center justify-center p-6 cursor-pointer hover:bg-gray-50 transition-all text-center group" > handleBillUpload(type, e)} /> {isUploading ? : }

Kattints a feltöltéshez

) : (

Számla file

)}
); }; const handlePlanChange = (index: number, field: keyof ProductPackage, value: any) => { const updatedPlans = [...plans]; const plan = { ...updatedPlans[index], [field]: value }; updatedPlans[index] = plan; setPlans(updatedPlans); }; const handleSavePlan = async (index: number) => { const plan = plans[index]; setPlanSaving(plan.id); try { if (isSupabaseConfigured) { const { error } = await supabase.from('plans').upsert(plan); if (error) throw error; alert('Csomag sikeresen mentve!'); } } catch (e: any) { alert("Hiba a mentés során: " + e.message); } finally { setPlanSaving(null); } }; return (
Admin Access

Vezérlőpult

{[ { id: 'overview', label: 'Statisztika', icon: BarChart2 }, { id: 'users', label: 'Felhasználók', icon: Users }, { id: 'orders', label: 'Rendelések', icon: ShoppingCart }, { id: 'plans', label: 'Csomagok', icon: Package }, { id: 'subscriptions', label: 'Előfizetések', icon: ShieldCheck } ].map((tab) => ( ))}
{activeTab === 'overview' && (

Látogatók

{visitorStats.month}

Rendelések

{orders.length}

Aktív Előfizetések

{subscriptions.filter(s => s.status === 'active').length}

)} {activeTab === 'users' && (
{users.map(u => ( ))}
FelhasználóSzintRegisztráció
{u.last_name} {u.first_name} {u.email} {u.role} {new Date(u.created_at).toLocaleDateString('hu-HU')}
)} {activeTab === 'orders' && (
{orders.map(o => ( ))}
ÜgyfélCsomagStátuszMűvelet

{o.customer}

{o.email}

{o.package}
)} {activeTab === 'subscriptions' && (
{/* Background Glow */}

Karbantartás Monitor

Aktív előfizetések és esedékes díjak kezelése.

{/* Filter Bar */}
setSubSearch(e.target.value)} className="w-full bg-[#1e293b] border border-gray-700 text-white rounded-xl py-2.5 pl-10 pr-4 text-sm focus:ring-2 focus:ring-primary/50 outline-none placeholder:text-gray-600" />
{filteredSubscriptions.length > 0 ? filteredSubscriptions.map(sub => { const daysLeft = getDaysRemaining(sub.next_billing_date); let statusColor = 'bg-green-500/10 text-green-400 border-green-500/20'; let statusLabel = 'Aktív'; if (sub.status === 'overdue' || daysLeft < 0) { statusColor = 'bg-red-500/10 text-red-400 border-red-500/20'; statusLabel = 'Lejárt'; } else if (daysLeft <= 30) { statusColor = 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'; statusLabel = 'Hamarosan'; } const domainName = sub.order?.details?.domainName || 'Nincs domain'; const annualFee = "59 990 Ft"; // Calculate logic for "Sent Reminders" column let sentReminderText = "Még nem kapott emlékeztető e-mailt az éves díj fizetéséről."; let sentReminderClass = "text-gray-500 italic text-[10px]"; if (sub.last_notified_at) { const billingDate = new Date(sub.next_billing_date); const notifiedDate = new Date(sub.last_notified_at); // Calculate days remaining at the time of notification const diffTime = billingDate.getTime() - notifiedDate.getTime(); const daysDiff = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); sentReminderText = `${daysDiff} nap volt hátra`; sentReminderClass = "text-primary font-bold text-xs"; } return ( ); }) : ( )}
Projekt / Domain Ügyfél Éves Díj Státusz Következő Fizetés Küldött emlékeztetők Művelet
{domainName} {sub.client_email} {annualFee} {statusLabel}
{new Date(sub.next_billing_date).toLocaleDateString('hu-HU')} {daysLeft < 0 ? `${Math.abs(daysLeft)} napja lejárt` : `${daysLeft} nap van hátra`}
{sentReminderText}
Nincs a keresésnek megfelelő előfizetés.
)} {/* ... PLANS TAB ... */} {activeTab === 'plans' && (
{plans.map((plan, index) => { const advance = plan.advance_price || 0; const total = plan.total_price || 0; const remaining = total - advance; return (
handlePlanChange(index, 'name', e.target.value)} className="text-2xl font-black text-gray-900 bg-transparent border-none p-0 outline-none focus:text-primary w-full" placeholder="Csomag neve" />

Teljes ár (Szöveges)

handlePlanChange(index, 'price', e.target.value)} className="w-full px-4 py-3 rounded-xl border border-gray-100 font-bold text-sm bg-gray-50/50 outline-none focus:border-primary" placeholder="Pl: 350.000 Ft" />

Előleg (HUF)

{ const v = parseInt(e.target.value) || 0; const currentRemaining = (plan.total_price || 0) - (plan.advance_price || 0); const updatedPlans = [...plans]; updatedPlans[index] = { ...updatedPlans[index], advance_price: v, total_price: v + currentRemaining }; setPlans(updatedPlans); }} className="w-full px-4 py-3 rounded-xl border border-gray-100 font-bold text-sm bg-gray-50/50 outline-none focus:border-primary" />

Fennmaradó (HUF)

{ const v = parseInt(e.target.value) || 0; const updatedPlans = [...plans]; updatedPlans[index] = { ...updatedPlans[index], total_price: (updatedPlans[index].advance_price || 0) + v }; setPlans(updatedPlans); }} className="w-full px-4 py-3 rounded-xl border border-gray-100 font-bold text-sm bg-gray-50/50 outline-none focus:border-primary" />

Összesen (Kalkulált)

{total.toLocaleString('hu-HU')} Ft