Files
MotionWebStudio/pages/Admin.tsx

1577 lines
86 KiB
TypeScript
Raw Normal View History

2025-12-26 14:03:18 +01:00
import React, { useState, useEffect, useRef } from 'react';
2025-12-21 20:40:32 +01:00
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,
2025-12-26 14:03:18 +01:00
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
2025-12-21 20:40:32 +01:00
} from 'lucide-react';
import { Button } from '../components/Button';
import { supabase, isSupabaseConfigured } from '../lib/supabaseClient';
2025-12-26 14:03:18 +01:00
import { ProductPackage, MaintenanceSubscription } from '../types';
2025-12-21 20:40:32 +01:00
import { defaultPlans } from '../lib/defaultPlans';
2025-12-26 14:03:18 +01:00
import { GoogleGenAI } from "@google/genai";
2025-12-21 20:40:32 +01:00
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;
2025-12-22 17:59:43 +01:00
body?: string;
2025-12-21 20:40:32 +01:00
}
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 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[];
2025-12-26 14:03:18 +01:00
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;
};
};
2025-12-21 20:40:32 +01:00
}
2025-12-26 14:03:18 +01:00
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
}) => {
2025-12-22 17:59:43 +01:00
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<string, { subject: string, body: string }> = {
'Fejlesztés megkezdése': {
2025-12-26 14:03:18 +01:00
subject: `Projekt Indítása: Megkezdtem a fejlesztést - ${data.package}`,
2025-12-22 17:59:43 +01:00
body: `<div style="${baseStyle}">
<div style="${headerStyle}">MotionWeb</div>
<p>Kedves <strong>${data.customer}</strong>!</p>
2025-12-26 14:03:18 +01:00
<p>Örömmel értesítem, hogy a <strong>${data.package}</strong> csomagjához tartozó fejlesztési folyamatot a mai napon hivatalosan is elindítottam.</p>
<p>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.</p>
<div style="${footerStyle}">Üdvözlettel,<br><strong>Balogh Bence Benedek</strong><br>MotionWeb | motionweb.hu</div>
2025-12-22 17:59:43 +01:00
</div>`
},
2025-12-26 14:03:18 +01:00
'Demo elkészült': {
2025-12-22 17:59:43 +01:00
subject: `Elkészült a weboldal demó verziója - ${data.package}`,
body: `<div style="${baseStyle}">
<div style="${headerStyle}">MotionWeb</div>
<p>Kedves <strong>${data.customer}</strong>!</p>
2025-12-26 14:03:18 +01:00
<p>Örömmel értesítem, hogy elkészültem weboldala első, interaktív bemutató verziójával!</p>
<div style="text-align: center;"><a href="${data.demoUrl || '#'}" style="${buttonStyle}">DEMÓ MEGTEKINTÉSE</a></div>
<p>Kérjük, nézze át az oldalt az ügyfélkapun keresztül, és küldje el visszajelzését a gombra kattintva.</p>
<div style="${footerStyle}">Üdvözlettel,<br><strong>Balogh Bence Benedek</strong><br>MotionWeb | motionweb.hu</div>
</div>`
},
'1 hete nincs válasz': {
subject: `Emlékeztető: Visszajelzés kérése a demó oldalhoz`,
body: `<div style="${baseStyle}">
<div style="${headerStyle}">MotionWeb</div>
<p>Kedves <strong>${data.customer}</strong>!</p>
<p>Szeretnék érdeklődni, hogy volt-e alkalma megtekinteni a <strong>${data.package}</strong> demó verzióját? Immár egy hete elküldtem Önnek a terveket.</p>
<p>Kérem, amennyiben ideje engedi, nézze át az oldalt és küldje el visszajelzését, hogy haladhassunk a véglegesítéssel.</p>
<div style="text-align: center;"><a href="https://motionweb.hu/#/dashboard" style="${buttonStyle}">ÜGYFÉLKAPU MEGNYITÁSA</a></div>
<div style="${footerStyle}">Üdvözlettel,<br><strong>Balogh Bence Benedek</strong><br>MotionWeb | motionweb.hu</div>
</div>`
},
'2 hete nincs válasz': {
subject: `Második emlékeztető: Várakozás a visszajelzésre`,
body: `<div style="${baseStyle}">
<div style="${headerStyle}">MotionWeb</div>
<p>Kedves <strong>${data.customer}</strong>!</p>
<p>Szeretném ismételten jelezni, hogy a <strong>${data.package}</strong> demó verziója immár két hete várakozik az Ön jóváhagyására.</p>
<p>Fontos számunkra a folyamatos haladás, ezért kérem, szíveskedjen visszajelzést adni a projekt állapotáról.</p>
<div style="text-align: center;"><a href="https://motionweb.hu/#/dashboard" style="${buttonStyle}">ÜGYFÉLKAPU MEGNYITÁSA</a></div>
<div style="${footerStyle}">Üdvözlettel,<br><strong>Balogh Bence Benedek</strong><br>MotionWeb | motionweb.hu</div>
</div>`
},
'Projekt lezárva (1 hónap)': {
subject: `Értesítés: Projekt lezárása kommunikáció hiánya miatt`,
body: `<div style="${baseStyle}">
<div style="${headerStyle}">MotionWeb</div>
<p>Kedves <strong>${data.customer}</strong>!</p>
<p>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 <strong>kommunikáció hiányában lezártnak tekintjük</strong>.</p>
<p>A projekt fájljait archiváltuk. Amennyiben később folytatni szeretné a munkát, kérjük vegye fel velünk a kapcsolatot.</p>
<div style="${footerStyle}">Üdvözlettel,<br><strong>Balogh Bence Benedek</strong><br>MotionWeb | motionweb.hu</div>
2025-12-22 17:59:43 +01:00
</div>`
},
'Módosítások fejlesztése': {
2025-12-26 14:03:18 +01:00
subject: `Fejlesztés: Megkezdtem a kért módosítások átvezetését`,
2025-12-22 17:59:43 +01:00
body: `<div style="${baseStyle}">
<div style="${headerStyle}">MotionWeb</div>
<p>Kedves <strong>${data.customer}</strong>!</p>
2025-12-26 14:03:18 +01:00
<p>Köszönöm a visszajelzését a <strong>${data.package}</strong> demó verziójával kapcsolatban.</p>
<p>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.</p>
<div style="${footerStyle}">Üdvözlettel,<br><strong>Balogh Bence Benedek</strong><br>MotionWeb | motionweb.hu</div>
2025-12-22 17:59:43 +01:00
</div>`
},
2025-12-26 14:03:18 +01:00
'Weboldal élesítése megkezdődött': {
subject: `Folyamatban: Megkezdődött a weboldal élesítése`,
2025-12-22 17:59:43 +01:00
body: `<div style="${baseStyle}">
<div style="${headerStyle}">MotionWeb</div>
<p>Kedves <strong>${data.customer}</strong>!</p>
2025-12-26 14:03:18 +01:00
<p>A fejlesztési szakasz lezárult. Örömmel értesítem, hogy a <strong>${data.package}</strong> projekt keretében megkezdtem a weboldal végleges élesítését a választott domain címen.</p>
<p>Ez a folyamat a DNS beállításoktól függően 24-48 órát vehet igénybe.</p>
<div style="${footerStyle}">Üdvözlettel,<br><strong>Balogh Bence Benedek</strong><br>MotionWeb | motionweb.hu</div>
2025-12-22 17:59:43 +01:00
</div>`
},
2025-12-26 14:03:18 +01:00
'Élesített weboldal elkészült': {
subject: `Gratulálunk! Weboldala sikeresen élesítve lett`,
2025-12-22 17:59:43 +01:00
body: `<div style="${baseStyle}">
<div style="${headerStyle}">MotionWeb</div>
<p>Kedves <strong>${data.customer}</strong>!</p>
2025-12-26 14:03:18 +01:00
<p>Örömmel jelentem, hogy a <strong>${data.package}</strong> projektet sikeresen befejeztem!</p>
<p>A weboldalt élesítettem, így az mostantól minden látogató számára elérhető. Köszönöm a megtisztelő bizalmat!</p>
<div style="text-align: center;"><a href="https://motionweb.hu/#/dashboard" style="${buttonStyle}">ÜGYFÉLKAPU MEGNYITÁSA</a></div>
<div style="${footerStyle}">Üdvözlettel,<br><strong>Balogh Bence Benedek</strong><br>MotionWeb | motionweb.hu</div>
2025-12-22 17:59:43 +01:00
</div>`
},
2025-12-26 14:03:18 +01:00
'Előlegszámla elkészült': {
subject: `Előlegszámla a ${data.package} projekthez`,
2025-12-22 17:59:43 +01:00
body: `<div style="${baseStyle}">
<div style="${headerStyle}">MotionWeb</div>
<p>Kedves <strong>${data.customer}</strong>!</p>
2025-12-26 14:03:18 +01:00
<p>Értesítem, hogy a <strong>${data.package}</strong> csomagjához tartozó <strong>előlegszámla</strong> elkészült.</p>
<p>A számlát megtekintheti és letöltheti az alábbi gombra kattintva:</p>
<div style="text-align: center;"><a href="${data.billUrl || '#'}" style="${buttonStyle}">SZÁMLA MEGTEKINTÉSE</a></div>
<p>A befizetés beérkezése után tudom folytatni a munkálatokat. Amennyiben bármilyen kérdése van, kérem jelezze.</p>
<div style="${footerStyle}">Üdvözlettel,<br><strong>Balogh Bence Benedek</strong><br>MotionWeb | motionweb.hu</div>
2025-12-22 17:59:43 +01:00
</div>`
},
2025-12-26 14:03:18 +01:00
'Végszámla elkészült': {
subject: `Végszámla a ${data.package} projekthez`,
2025-12-22 17:59:43 +01:00
body: `<div style="${baseStyle}">
<div style="${headerStyle}">MotionWeb</div>
<p>Kedves <strong>${data.customer}</strong>!</p>
2025-12-26 14:03:18 +01:00
<p>A <strong>${data.package}</strong> projekt a befejezéséhez közeledik. Értesítem, hogy a projekt végleges elszámolásához tartozó <strong>végszámla</strong> elkészült.</p>
<p>A számlát az alábbi gombra kattintva töltheti le:</p>
<div style="text-align: center;"><a href="${data.billUrl || '#'}" style="${buttonStyle}">SZÁMLA MEGTEKINTÉSE</a></div>
<p>A teljes összeg kiegyenlítése után történik meg a weboldal végleges élesítése.</p>
<div style="${footerStyle}">Üdvözlettel,<br><strong>Balogh Bence Benedek</strong><br>MotionWeb | motionweb.hu</div>
</div>`
},
'Előfizetés emlékeztető': {
subject: `Emlékeztető: Éves karbantartási díj esedékes - ${data.domain || 'Weboldal'}`,
body: `<div style="${baseStyle}">
<div style="${headerStyle}">MotionWeb</div>
<p>Kedves <strong>${data.customer}</strong>!</p>
<p>Ezúton emlékeztetjük, hogy a(z) <strong>${data.domain || 'weboldal'}</strong> éves karbantartási és üzemeltetési díja hamarosan (${data.daysLeft} nap múlva) esedékessé válik.</p>
<p>A szolgáltatás folyamatos biztosítása érdekében kérjük, rendezze a díjat az alábbi linken keresztül:</p>
<div style="text-align: center;"><a href="${data.paymentLink || '#'}" style="${buttonStyle}">BEFIZETÉS INDÍTÁSA</a></div>
<p>Összeg: <strong>${data.amount || '59 990 Ft'}</strong></p>
<p>Amennyiben már rendezte a díjat, kérjük tekintse levelünket tárgytalannak.</p>
<div style="${footerStyle}">Üdvözlettel,<br><strong>Balogh Bence Benedek</strong><br>MotionWeb | motionweb.hu</div>
2025-12-22 17:59:43 +01:00
</div>`
}
};
return templates[type] || { subject: 'Értesítés a MotionWeb-től', body: 'Üzenete érkezett a MotionWeb Stúdiótól.' };
};
2025-12-21 20:40:32 +01:00
export const Admin: React.FC = () => {
const { user, isAdmin, loading } = useAuth();
const navigate = useNavigate();
2025-12-26 14:03:18 +01:00
const [activeTab, setActiveTab] = useState<'overview' | 'users' | 'orders' | 'plans' | 'subscriptions'>('overview');
2025-12-21 20:40:32 +01:00
const [users, setUsers] = useState<AdminUser[]>([]);
const [orders, setOrders] = useState<AdminOrder[]>([]);
const [plans, setPlans] = useState<ProductPackage[]>([]);
2025-12-26 14:03:18 +01:00
const [subscriptions, setSubscriptions] = useState<ExtendedSubscription[]>([]);
2025-12-21 20:40:32 +01:00
const [loadingData, setLoadingData] = useState(false);
const [visitorStats, setVisitorStats] = useState({ month: 0, week: 0 });
const [statusUpdating, setStatusUpdating] = useState<string | null>(null);
const [planSaving, setPlanSaving] = useState<string | null>(null);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [viewOrder, setViewOrder] = useState<AdminOrder | null>(null);
2025-12-26 14:03:18 +01:00
const [generatedPrompts, setGeneratedPrompts] = useState<AIPrompt[]>([]);
const [generatingPrompts, setGeneratingPrompts] = useState(false);
const [expandedLogId, setExpandedLogId] = useState<string | null>(null);
2025-12-21 20:40:32 +01:00
const [demoUrlInput, setDemoUrlInput] = useState('');
const [savingDemoUrl, setSavingDemoUrl] = useState(false);
const [emailSending, setEmailSending] = useState<string | null>(null);
2025-12-26 14:03:18 +01:00
const [checkingSubs, setCheckingSubs] = useState(false);
const [manualNotifying, setManualNotifying] = useState<string | null>(null);
// Filter & Sort State for Subscriptions
const [subSearch, setSubSearch] = useState('');
const [subSort, setSubSort] = useState<'asc' | 'desc'>('asc');
const advanceBillInput = useRef<HTMLInputElement>(null);
const finalBillInput = useRef<HTMLInputElement>(null);
const [billUploading, setBillUploading] = useState<string | null>(null);
2025-12-21 20:40:32 +01:00
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
})));
2025-12-26 14:03:18 +01:00
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[]);
}
2025-12-21 20:40:32 +01:00
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 {
2025-12-22 17:59:43 +01:00
const { data, error } = await supabase
2025-12-21 20:40:32 +01:00
.from('email_log')
.select('*')
.eq('order_id', orderId)
.order('sent_at', { ascending: false });
return data || [];
} catch (e) {
2025-12-26 14:03:18 +01:00
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) {
2025-12-21 20:40:32 +01:00
return [];
}
};
const handleStatusChange = async (orderId: string, newStatus: string) => {
if (statusUpdating) return;
setStatusUpdating(orderId);
2025-12-26 14:03:18 +01:00
// Find order in current state
const currentOrder = orders.find(o => o.id === orderId);
if (!currentOrder) {
setStatusUpdating(null);
return;
}
2025-12-21 20:40:32 +01:00
try {
if (isSupabaseConfigured) {
2025-12-26 14:03:18 +01:00
// 1. Database Update (Status)
const { error: updateError } = await supabase
.from('orders')
.update({ status: newStatus })
.eq('id', orderId);
if (updateError) throw updateError;
2025-12-21 20:40:32 +01:00
2025-12-26 14:03:18 +01:00
// 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));
2025-12-21 20:40:32 +01:00
if (viewOrder && viewOrder.id === orderId) {
2025-12-26 14:03:18 +01:00
const newHist = await fetchOrderHistory(orderId);
setViewOrder(prev => prev ? { ...prev, status: newStatus, history: newHist } : null);
2025-12-21 20:40:32 +01:00
}
2025-12-26 14:03:18 +01:00
2025-12-21 20:40:32 +01:00
} else {
2025-12-26 14:03:18 +01:00
// Demo Mode
2025-12-21 20:40:32 +01:00
setOrders(prev => prev.map(o => o.id === orderId ? { ...o, status: newStatus } : o));
if (viewOrder && viewOrder.id === orderId) {
2025-12-26 14:03:18 +01:00
setViewOrder(prev => prev ? { ...prev, status: newStatus } : null);
2025-12-21 20:40:32 +01:00
}
2025-12-26 14:03:18 +01:00
if (newStatus === 'completed') alert('Demo: Státusz kész, előfizetés szimulálva.');
2025-12-21 20:40:32 +01:00
}
} catch (e: any) {
2025-12-26 14:03:18 +01:00
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);
2025-12-21 20:40:32 +01:00
} 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 });
2025-12-22 17:59:43 +01:00
if (demoUrlInput.startsWith('http')) {
2025-12-26 14:03:18 +01:00
handleSendEmail('Demo elkészült');
2025-12-22 17:59:43 +01:00
}
2025-12-21 20:40:32 +01:00
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);
2025-12-26 14:03:18 +01:00
const bills = await fetchBills(order.id);
setViewOrder({ ...order, history, emailLogs, bills });
2025-12-21 20:40:32 +01:00
setDemoUrlInput(order.details?.demoUrl || '');
setGeneratedPrompts([]);
setLoadingData(false);
};
2025-12-26 14:03:18 +01:00
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<HTMLInputElement>) => {
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 = {}) => {
2025-12-21 20:40:32 +01:00
if (!viewOrder || emailSending) return;
2025-12-26 14:03:18 +01:00
2025-12-21 20:40:32 +01:00
setEmailSending(emailType);
try {
2025-12-22 17:59:43 +01:00
const template = getEmailTemplate(emailType, {
customer: viewOrder.customer,
package: viewOrder.package,
2025-12-26 14:03:18 +01:00
demoUrl: demoUrlInput || viewOrder.details?.demoUrl,
...extraData
2025-12-22 17:59:43 +01:00
});
2025-12-21 20:40:32 +01:00
if (isSupabaseConfigured) {
2025-12-26 14:03:18 +01:00
const { error: sendError } = await supabase.functions.invoke('resend', {
2025-12-22 17:59:43 +01:00
body: {
to: viewOrder.email,
subject: template.subject,
2025-12-26 14:03:18 +01:00
html: template.body,
2025-12-22 17:59:43 +01:00
}
2025-12-21 20:40:32 +01:00
});
2025-12-22 17:59:43 +01:00
2025-12-26 14:03:18 +01:00
if (sendError) throw sendError;
2025-12-22 17:59:43 +01:00
2025-12-26 14:03:18 +01:00
await supabase.from('email_log').insert({
order_id: viewOrder.id,
email_type: emailType,
body: template.body
});
2025-12-21 20:40:32 +01:00
const newLogs = await fetchEmailLogs(viewOrder.id);
2025-12-26 14:03:18 +01:00
setViewOrder(prev => prev ? { ...prev, emailLogs: newLogs } : null);
2025-12-21 20:40:32 +01:00
2025-12-26 14:03:18 +01:00
alert(`Sikeres küldés: ${emailType}`);
2025-12-21 20:40:32 +01:00
}
} catch (e: any) {
2025-12-26 14:03:18 +01:00
alert("Hiba: " + e.message);
2025-12-21 20:40:32 +01:00
} 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 <span className={`text-[10px] uppercase font-black px-2 py-0.5 rounded-full ${c.color}`}>{c.label}</span>;
};
2025-12-26 14:03:18 +01:00
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 (
<div className="bg-white p-6 rounded-[24px] border border-gray-100 shadow-sm flex flex-col h-full">
<div className="flex items-center gap-3 mb-6">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${type === 'advance' ? 'bg-blue-50 text-blue-600' : 'bg-green-50 text-green-600'}`}>
{type === 'advance' ? <CreditCard className="w-5 h-5" /> : <DollarSign className="w-5 h-5" />}
</div>
<h4 className="text-sm font-black text-gray-900 uppercase tracking-widest">{label}</h4>
</div>
{!bill ? (
<div
onClick={() => (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"
>
<input type="file" ref={type === 'advance' ? advanceBillInput : finalBillInput} className="hidden" onChange={(e) => handleBillUpload(type, e)} />
{isUploading ? <RefreshCw className="w-8 h-8 text-primary animate-spin" /> : <Upload className="w-8 h-8 text-gray-300 group-hover:text-primary transition-colors mb-2" />}
<p className="text-xs font-bold text-gray-500">Kattints a feltöltéshez</p>
</div>
) : (
<div className="flex-grow space-y-4">
<div className="p-4 bg-gray-50 rounded-xl border border-gray-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-gray-400" />
<p className="text-xs font-bold text-gray-900 truncate max-w-[120px]">Számla file</p>
</div>
<a href={bill.file_url} target="_blank" rel="noreferrer" className="text-primary hover:text-primary-dark transition-colors"><FileDown className="w-4 h-4" /></a>
</div>
<button
onClick={() => handleSendEmail(templateName, { billUrl: bill.file_url, billType: type })}
disabled={!!emailSending}
className="w-full py-3 bg-gray-900 text-white rounded-xl text-[10px] font-black uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-primary transition-all disabled:opacity-50"
>
{isThisEmailSending ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Send className="w-3 h-3" />}
Küldés Ügyfélnek
</button>
</div>
)}
</div>
);
};
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);
}
};
2025-12-21 20:40:32 +01:00
return (
<div className="pt-24 bg-gray-50 min-h-screen pb-20 font-sans text-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-8 gap-4">
<div>
<div className="flex items-center gap-2 mb-2">
2025-12-26 14:03:18 +01:00
<button onClick={() => navigate('/dashboard')} className="p-2 hover:bg-gray-200 rounded-full transition-colors bg-white shadow-sm border border-gray-100"><ChevronLeft className="w-5 h-5 text-gray-500" /></button>
2025-12-21 20:40:32 +01:00
<span className="text-[10px] font-black text-red-600 bg-red-50 px-3 py-1 rounded-full border border-red-100 uppercase tracking-widest">Admin Access</span>
</div>
2025-12-26 14:03:18 +01:00
<h1 className="text-3xl md:text-4xl font-black tracking-tighter text-gray-900">Vezérlőpult</h1>
2025-12-21 20:40:32 +01:00
</div>
2025-12-26 14:03:18 +01:00
<Button variant="white" size="sm" onClick={fetchAdminData} disabled={loadingData} className="border-gray-200 shadow-sm font-bold uppercase text-[10px] tracking-widest w-full md:w-auto">
<RefreshCw className={`w-4 h-4 mr-2 ${loadingData ? 'animate-spin' : ''}`} /> Frissítés
2025-12-21 20:40:32 +01:00
</Button>
</div>
2025-12-26 14:03:18 +01:00
<div className="flex gap-2 mb-10 bg-white p-2 rounded-[24px] border border-gray-100 shadow-sm w-full md:w-fit overflow-x-auto scrollbar-hide">
2025-12-21 20:40:32 +01:00
{[
{ id: 'overview', label: 'Statisztika', icon: BarChart2 },
{ id: 'users', label: 'Felhasználók', icon: Users },
{ id: 'orders', label: 'Rendelések', icon: ShoppingCart },
2025-12-26 14:03:18 +01:00
{ id: 'plans', label: 'Csomagok', icon: Package },
{ id: 'subscriptions', label: 'Előfizetések', icon: ShieldCheck }
2025-12-21 20:40:32 +01:00
].map((tab) => (
2025-12-26 14:03:18 +01:00
<button key={tab.id} onClick={() => setActiveTab(tab.id as any)} className={`flex items-center gap-2 px-6 py-3 rounded-2xl text-sm font-bold transition-all whitespace-nowrap ${activeTab === tab.id ? 'bg-primary text-white shadow-lg' : 'text-gray-500 hover:bg-gray-50'}`}>
<tab.icon className="w-4 h-4 flex-shrink-0" /> {tab.label}
2025-12-21 20:40:32 +01:00
</button>
))}
</div>
<div className="animate-fade-in">
{activeTab === 'overview' && (
2025-12-26 14:03:18 +01:00
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8">
<div className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm">
2025-12-21 20:40:32 +01:00
<p className="text-xs font-bold text-gray-400 uppercase mb-3 tracking-widest">Látogatók</p>
2025-12-26 14:03:18 +01:00
<h3 className="text-4xl md:text-5xl font-black text-gray-900">{visitorStats.month}</h3>
2025-12-21 20:40:32 +01:00
</div>
2025-12-26 14:03:18 +01:00
<div className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm">
2025-12-21 20:40:32 +01:00
<p className="text-xs font-bold text-gray-400 uppercase mb-3 tracking-widest">Rendelések</p>
2025-12-26 14:03:18 +01:00
<h3 className="text-4xl md:text-5xl font-black text-gray-900">{orders.length}</h3>
2025-12-21 20:40:32 +01:00
</div>
2025-12-26 14:03:18 +01:00
<div className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm">
<p className="text-xs font-bold text-gray-400 uppercase mb-3 tracking-widest">Aktív Előfizetések</p>
<h3 className="text-4xl md:text-5xl font-black text-gray-900">{subscriptions.filter(s => s.status === 'active').length}</h3>
2025-12-21 20:40:32 +01:00
</div>
</div>
)}
{activeTab === 'users' && (
2025-12-26 14:03:18 +01:00
<div className="bg-white rounded-[32px] border border-gray-100 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left min-w-[600px]">
<thead className="bg-gray-50/50 border-b border-gray-100 text-xs font-bold text-gray-400 uppercase tracking-widest">
<tr><th className="px-10 py-6">Felhasználó</th><th className="px-10 py-6">Szint</th><th className="px-10 py-6 text-right">Regisztráció</th></tr>
</thead>
<tbody className="divide-y divide-gray-50 font-medium">
{users.map(u => (
<tr key={u.id} className="hover:bg-gray-50 transition-colors">
<td className="px-10 py-6">
<span className="font-black text-gray-900 block">{u.last_name} {u.first_name}</span>
<span className="text-xs text-gray-500">{u.email}</span>
</td>
<td className="px-10 py-6"><span className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${u.role === 'admin' ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-600'}`}>{u.role}</span></td>
<td className="px-10 py-6 text-right text-sm text-gray-500">{new Date(u.created_at).toLocaleDateString('hu-HU')}</td>
</tr>
))}
</tbody>
</table>
</div>
2025-12-21 20:40:32 +01:00
</div>
)}
{activeTab === 'orders' && (
2025-12-26 14:03:18 +01:00
<div className="bg-white rounded-[32px] border border-gray-100 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left min-w-[700px]">
<thead className="bg-gray-50/50 border-b border-gray-100 text-xs font-bold text-gray-400 uppercase tracking-widest">
<tr><th className="px-10 py-6">Ügyfél</th><th className="px-10 py-6">Csomag</th><th className="px-10 py-6">Státusz</th><th className="px-10 py-6 text-right">Művelet</th></tr>
</thead>
<tbody className="divide-y divide-gray-50">
{orders.map(o => (
<tr key={o.id} className="hover:bg-gray-50 transition-colors">
<td className="px-10 py-6">
<p className="font-black leading-tight text-gray-900">{o.customer}</p>
<p className="text-[11px] text-gray-500 font-bold">{o.email}</p>
</td>
<td className="px-10 py-6 text-sm font-bold text-primary">{o.package}</td>
<td className="px-10 py-6"><StatusBadge status={o.status} /></td>
<td className="px-10 py-6 text-right"><Button size="sm" variant="outline" onClick={() => handleViewDetails(o)} className="rounded-2xl font-black uppercase text-[10px] tracking-widest">Kezelés</Button></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'subscriptions' && (
<div className="bg-[#0f172a] rounded-[32px] border border-gray-800 shadow-2xl overflow-hidden relative">
{/* Background Glow */}
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-primary/5 rounded-full blur-[100px] pointer-events-none"></div>
<div className="p-8 border-b border-gray-800 flex flex-col md:flex-row justify-between items-start md:items-center gap-6 relative z-10 bg-[#0f172a]/80 backdrop-blur-md">
<div>
<h3 className="font-black text-white flex items-center gap-3 text-xl tracking-tight">
<ShieldCheck className="w-6 h-6 text-primary" /> Karbantartás Monitor
</h3>
<p className="text-xs text-gray-400 mt-1 font-medium">Aktív előfizetések és esedékes díjak kezelése.</p>
</div>
<div className="flex gap-4">
<Button
size="sm"
onClick={handleCheckSubscriptions}
disabled={checkingSubs}
className="uppercase text-[10px] tracking-widest font-black bg-primary/20 hover:bg-primary/30 text-primary border border-primary/30 backdrop-blur-sm"
>
{checkingSubs ? <RefreshCw className="w-4 h-4 animate-spin mr-2" /> : <Zap className="w-4 h-4 mr-2" />}
Tömeges Ellenőrzés
</Button>
</div>
</div>
{/* Filter Bar */}
<div className="p-6 bg-gray-800/30 border-b border-gray-800 flex flex-col sm:flex-row gap-4">
<div className="relative flex-grow">
<Search className="absolute left-4 top-3 w-4 h-4 text-gray-500" />
<input
type="text"
placeholder="Keresés domain vagy email alapján..."
value={subSearch}
onChange={(e) => 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"
/>
</div>
<button
onClick={() => setSubSort(subSort === 'asc' ? 'desc' : 'asc')}
className="flex items-center gap-2 px-4 py-2.5 bg-[#1e293b] border border-gray-700 rounded-xl text-gray-300 text-xs font-bold hover:bg-gray-700 transition-colors uppercase tracking-wider"
>
<ArrowUpDown className="w-3 h-3" /> {subSort === 'asc' ? 'Határidő: Növekvő' : 'Határidő: Csökkenő'}
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left min-w-[900px] border-collapse">
<thead className="bg-[#1e293b]/50 text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] border-b border-gray-800">
<tr>
<th className="px-8 py-5">Projekt / Domain</th>
<th className="px-8 py-5">Ügyfél</th>
<th className="px-8 py-5">Éves Díj</th>
<th className="px-8 py-5">Státusz</th>
<th className="px-8 py-5">Következő Fizetés</th>
<th className="px-8 py-5">Küldött emlékeztetők</th>
<th className="px-8 py-5 text-right">Művelet</th>
2025-12-21 20:40:32 +01:00
</tr>
2025-12-26 14:03:18 +01:00
</thead>
<tbody className="divide-y divide-gray-800/50 text-sm">
{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 (
<tr key={sub.id} className="hover:bg-white/5 transition-colors group">
<td className="px-8 py-5">
<a href={`https://${domainName}`} target="_blank" rel="noreferrer" className="flex items-center gap-2 text-white font-bold hover:text-primary transition-colors">
{domainName} <ExternalLink className="w-3 h-3 opacity-50 group-hover:opacity-100" />
</a>
</td>
<td className="px-8 py-5 text-gray-400 font-medium">
{sub.client_email}
</td>
<td className="px-8 py-5 text-gray-300 font-mono">
{annualFee}
</td>
<td className="px-8 py-5">
<span className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest border ${statusColor}`}>
{statusLabel}
</span>
</td>
<td className="px-8 py-5">
<div className="flex flex-col">
<span className="text-white font-bold">{new Date(sub.next_billing_date).toLocaleDateString('hu-HU')}</span>
<span className={`text-[10px] font-bold mt-0.5 ${daysLeft < 0 ? 'text-red-400' : daysLeft <= 30 ? 'text-yellow-400' : 'text-gray-500'}`}>
{daysLeft < 0 ? `${Math.abs(daysLeft)} napja lejárt` : `${daysLeft} nap van hátra`}
</span>
</div>
</td>
<td className="px-8 py-5">
<span className={sentReminderClass}>
{sentReminderText}
</span>
</td>
<td className="px-8 py-5 text-right">
<Button
size="sm"
variant="outline"
onClick={() => handleManualNotification(sub)}
disabled={manualNotifying === sub.id}
className="border-gray-700 text-gray-300 hover:text-white hover:bg-primary hover:border-primary uppercase text-[9px] tracking-widest rounded-lg h-8 px-3"
>
{manualNotifying === sub.id ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Bell className="w-3 h-3" />}
</Button>
</td>
</tr>
);
}) : (
<tr>
<td colSpan={7} className="px-8 py-16 text-center text-gray-500 italic bg-gray-900/50">
<Filter className="w-8 h-8 mx-auto mb-3 opacity-30" />
Nincs a keresésnek megfelelő előfizetés.
</td>
</tr>
)}
</tbody>
</table>
</div>
2025-12-21 20:40:32 +01:00
</div>
)}
2025-12-26 14:03:18 +01:00
{/* ... PLANS TAB ... */}
2025-12-21 20:40:32 +01:00
{activeTab === 'plans' && (
2025-12-26 14:03:18 +01:00
<div className="space-y-8 animate-fade-in">
{plans.map((plan, index) => {
const advance = plan.advance_price || 0;
const total = plan.total_price || 0;
const remaining = total - advance;
return (
<div key={plan.id} className="bg-white p-8 md:p-12 rounded-[32px] border border-gray-100 shadow-sm space-y-10 relative overflow-hidden group">
<div className="absolute top-0 left-0 w-2 h-full bg-primary opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div className="flex flex-col md:flex-row justify-between items-start gap-6 border-b border-gray-50 pb-8">
<div className="flex-grow space-y-2">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Csomag Megnevezése</label>
<input
type="text"
value={plan.name}
onChange={(e) => 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"
/>
</div>
<div className="flex flex-wrap gap-4 items-center">
<label className="flex items-center gap-3 bg-gray-50 px-5 py-3 rounded-2xl cursor-pointer hover:bg-purple-50 transition-colors border border-gray-100">
<input
type="checkbox"
checked={plan.isPopular}
onChange={(e) => handlePlanChange(index, 'isPopular', e.target.checked)}
className="w-5 h-5 text-primary rounded border-gray-300 focus:ring-primary"
/>
<div className="flex items-center gap-2">
<Star className={`w-4 h-4 ${plan.isPopular ? 'text-yellow-400 fill-current' : 'text-gray-300'}`} />
<span className="text-[10px] font-black uppercase tracking-widest">Népszerű</span>
</div>
</label>
<label className="flex items-center gap-3 bg-gray-50 px-5 py-3 rounded-2xl cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100">
<input
type="checkbox"
checked={plan.is_custom_price}
onChange={(e) => handlePlanChange(index, 'is_custom_price', e.target.checked)}
className="w-5 h-5 text-primary rounded border-gray-300 focus:ring-primary"
/>
<span className="text-[10px] font-black uppercase tracking-widest text-blue-600">Egyedi Árazás</span>
</label>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 pt-4">
<div className="space-y-4">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] flex items-center gap-2"><DollarSign className="w-3 h-3" /> Árazási Adatok</label>
<div className="space-y-4">
<div>
<p className="text-[9px] font-bold text-gray-400 mb-1 uppercase">Teljes ár (Szöveges)</p>
<input
type="text"
value={plan.price}
onChange={(e) => 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"
/>
</div>
<div className="grid grid-cols-1 gap-4">
<div className="grid grid-cols-2 gap-3">
<div>
<p className="text-[9px] font-bold text-gray-400 mb-1 uppercase">Előleg (HUF)</p>
<input
type="number"
value={advance}
onChange={(e) => {
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"
/>
</div>
<div>
<p className="text-[9px] font-bold text-gray-400 mb-1 uppercase">Fennmaradó (HUF)</p>
<input
type="number"
value={remaining}
onChange={(e) => {
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"
/>
</div>
</div>
<div className="p-4 bg-gray-900 rounded-xl border border-gray-800">
<p className="text-[9px] font-bold text-gray-500 uppercase mb-1 tracking-widest">Összesen (Kalkulált)</p>
<p className="text-xl font-black text-primary">{total.toLocaleString('hu-HU')} Ft</p>
</div>
</div>
</div>
</div>
<div className="md:col-span-2 space-y-4">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] flex items-center gap-2"><FileText className="w-3 h-3" /> Leírás és Gomb</label>
<div className="space-y-4">
<textarea
value={plan.desc}
onChange={(e) => handlePlanChange(index, 'desc', e.target.value)}
rows={2}
className="w-full px-4 py-3 rounded-xl border border-gray-100 text-sm font-medium bg-gray-50/50 outline-none focus:border-primary resize-none"
placeholder="Csomag rövid leírása..."
/>
<div>
<p className="text-[9px] font-bold text-gray-400 mb-1">CTA Gomb Szövege</p>
<input
type="text"
value={plan.cta}
onChange={(e) => handlePlanChange(index, 'cta', 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"
/>
</div>
</div>
</div>
</div>
<div className="space-y-4 pt-4">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] flex items-center gap-2"><Zap className="w-3 h-3" /> Tartalmazott Funkciók (Egy sorba egyet)</label>
<textarea
value={plan.features.join('\n')}
onChange={(e) => handlePlanChange(index, 'features', e.target.value.split('\n'))}
rows={6}
className="w-full px-6 py-5 rounded-[24px] border border-gray-100 text-sm font-mono leading-relaxed bg-gray-900 text-primary outline-none focus:ring-2 focus:ring-primary/30"
placeholder="Funkció 1&#10;Funkció 2..."
/>
</div>
<div className="pt-8 border-t border-gray-50 flex justify-end">
<button
onClick={() => handleSavePlan(index)}
disabled={planSaving === plan.id}
className="bg-primary hover:bg-primary-dark text-white px-10 py-4 rounded-2xl font-black uppercase text-[10px] tracking-[0.2em] shadow-xl shadow-primary/20 transition-all flex items-center gap-3 disabled:opacity-50"
>
{planSaving === plan.id ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
Módosítások Mentése
</button>
</div>
</div>
);
})}
<div className="bg-gray-50 border-2 border-dashed border-gray-200 rounded-[32px] p-12 text-center group hover:bg-white hover:border-primary/30 transition-all cursor-pointer">
<div className="w-16 h-16 bg-white rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-sm group-hover:scale-110 transition-transform">
<Plus className="w-8 h-8 text-gray-300 group-hover:text-primary" />
</div>
<h3 className="text-xl font-bold text-gray-400 group-hover:text-gray-900 mb-2">Új Csomag Hozzáadása</h3>
<p className="text-sm text-gray-400">Kattintson ide egy új szolgáltatási csomag létrehozásához.</p>
</div>
2025-12-21 20:40:32 +01:00
</div>
)}
</div>
{viewOrder && (
2025-12-26 14:03:18 +01:00
<div className="fixed inset-0 z-50 flex items-center justify-center p-2 sm:p-4 bg-gray-900/80 backdrop-blur-sm overflow-y-auto">
<div className="bg-white rounded-[32px] shadow-2xl w-full max-w-6xl max-h-[95vh] overflow-hidden flex flex-col animate-fade-in-up border border-white/10">
<div className="px-6 md:px-10 py-6 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<div>
<h2 className="text-xl md:text-3xl font-black tracking-tighter text-gray-900">{viewOrder.customer}</h2>
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest">{viewOrder.email} ID: {viewOrder.displayId}</p>
2025-12-21 20:40:32 +01:00
</div>
2025-12-26 14:03:18 +01:00
<button onClick={() => setViewOrder(null)} className="p-2 hover:bg-gray-200 rounded-full transition-all bg-white border border-gray-100"><XCircle className="w-6 h-6 text-gray-400" /></button>
2025-12-21 20:40:32 +01:00
</div>
2025-12-26 14:03:18 +01:00
<div className="flex-grow overflow-y-auto p-6 md:p-10 space-y-12 bg-gray-50/30">
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 lg:gap-12">
<div className="md:col-span-8 space-y-10">
{/* FORM VÁLASZOK SZEKCIÓ */}
2025-12-21 20:40:32 +01:00
<section>
2025-12-26 14:03:18 +01:00
<h3 className="text-[11px] font-black text-gray-400 uppercase tracking-[0.3em] mb-6 flex items-center gap-2"><FileStack className="w-4 h-4" /> Rendelési ŰRLap Válaszai</h3>
<div className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm space-y-10">
{/* Kapcsolati Infók */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
<div><p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest mb-1">Cégnév</p><p className="font-bold text-sm">{viewOrder.details?.company || 'Nincs megadva'}</p></div>
<div><p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest mb-1">Telefonszám</p><p className="font-bold text-sm">{viewOrder.details?.phone}</p></div>
<div><p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest mb-1">Csomag</p><p className="font-black text-sm text-primary">{viewOrder.package}</p></div>
2025-12-21 20:40:32 +01:00
</div>
2025-12-26 14:03:18 +01:00
<div className="pt-6 border-t border-gray-50">
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest mb-3 flex items-center gap-2"><FileText className="w-3 h-3" /> Bemutatkozás</p>
<p className="text-sm font-medium leading-relaxed italic text-gray-700">"{viewOrder.details?.description || 'Nincs kitöltve'}"</p>
</div>
2025-12-21 20:40:32 +01:00
2025-12-26 14:03:18 +01:00
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-6 border-t border-gray-50">
<div>
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest mb-3 flex items-center gap-2"><Target className="w-3 h-3" /> Főbb Célok</p>
<div className="flex flex-wrap gap-2">{viewOrder.details?.goals?.map((g: string) => <span key={g} className="bg-blue-50 text-blue-700 px-2.5 py-1 rounded-lg text-[10px] font-black uppercase tracking-wider">{g}</span>)}</div>
</div>
<div>
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest mb-3 flex items-center gap-2"><Layout className="w-3 h-3" /> Tervezett Aloldalak</p>
<div className="flex flex-wrap gap-2">{viewOrder.details?.content?.map((c: string) => <span key={c} className="bg-purple-50 text-purple-700 px-2.5 py-1 rounded-lg text-[10px] font-black uppercase tracking-wider">{c}</span>)}</div>
</div>
</div>
<div className="pt-6 border-t border-gray-50">
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest mb-4 flex items-center gap-2"><Palette className="w-3 h-3" /> Design & Arculat</p>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div><p className="text-[8px] font-bold text-gray-400 uppercase mb-2">Főszín</p><div className="flex items-center gap-2"><div className="w-4 h-4 rounded-full border border-gray-200" style={{backgroundColor: viewOrder.details?.primaryColor}} /><span className="text-xs font-bold">{viewOrder.details?.primaryColor}</span></div></div>
<div><p className="text-[8px] font-bold text-gray-400 uppercase mb-2">Mellékszín</p><div className="flex items-center gap-2"><div className="w-4 h-4 rounded-full border border-gray-200" style={{backgroundColor: viewOrder.details?.secondaryColor}} /><span className="text-xs font-bold">{viewOrder.details?.secondaryColor}</span></div></div>
<div><p className="text-[8px] font-bold text-gray-400 uppercase mb-2">Kiegyensúlyozó</p><div className="flex items-center gap-2"><div className="w-4 h-4 rounded-full border border-gray-200" style={{backgroundColor: viewOrder.details?.balanceColor}} /><span className="text-xs font-bold">{viewOrder.details?.balanceColor}</span></div></div>
<div><p className="text-[8px] font-bold text-gray-400 uppercase mb-2">Stílus</p><p className="text-[10px] font-black text-gray-900">{viewOrder.details?.style?.join(', ')}</p></div>
</div>
</div>
<div className="pt-6 border-t border-gray-50">
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest mb-4 flex items-center gap-2"><Zap className="w-3 h-3" /> Funkcionális Igények</p>
<div className="flex flex-wrap gap-2">{viewOrder.details?.features?.map((f: string) => <span key={f} className="bg-gray-900 text-white px-3 py-1 rounded-lg text-[9px] font-black uppercase tracking-widest">{f}</span>)}</div>
2025-12-21 20:40:32 +01:00
</div>
2025-12-26 14:03:18 +01:00
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-6 border-t border-gray-50">
{/* Anyagok Linkje */}
<div>
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest mb-3 flex items-center gap-2"><LinkIcon className="w-3 h-3" /> Meglévő Anyagok (Anyagok megosztása)</p>
{viewOrder.details?.contentLink ? (
<a href={viewOrder.details.contentLink} target="_blank" rel="noreferrer" className="flex items-center gap-2 bg-blue-50 text-blue-600 p-3 rounded-xl hover:bg-blue-100 transition-all font-bold text-xs">
<ExternalLink className="w-3.5 h-3.5" /> Megnyitás <span className="opacity-50 font-medium truncate ml-2">{viewOrder.details.contentLink}</span>
</a>
) : <p className="text-xs text-gray-400 italic">Nincs megadva link.</p>}
</div>
{/* Inspirációk */}
<div>
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-widest mb-3 flex items-center gap-2"><Lightbulb className="w-3 h-3" /> Inspirációk / Referenciák</p>
<div className="space-y-2">
{viewOrder.details?.inspirations?.filter((ins: any) => ins.url)?.map((ins: any, idx: number) => (
<div key={idx} className="bg-gray-50 p-2.5 rounded-lg border border-gray-100 flex items-center justify-between">
<a href={ins.url} target="_blank" rel="noreferrer" className="text-[10px] font-bold text-primary hover:underline truncate mr-4">{ins.url}</a>
{ins.comment && <span title={ins.comment}><Info className="w-3 h-3 text-gray-300" /></span>}
2025-12-21 20:40:32 +01:00
</div>
2025-12-26 14:03:18 +01:00
))}
{(!viewOrder.details?.inspirations || viewOrder.details.inspirations.filter((ins: any) => ins.url).length === 0) && <p className="text-xs text-gray-400 italic">Nincs megadva.</p>}
</div>
</div>
</div>
{/* Számlázási Adatok */}
<div className="pt-8 border-t border-gray-100">
<div className="flex items-center gap-2 mb-6">
<Receipt className="w-4 h-4 text-primary" />
<h4 className="text-[11px] font-black text-gray-900 uppercase tracking-widest">Számlázási Adatok</h4>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-y-6 gap-x-10 bg-gray-50 p-6 rounded-3xl border border-gray-100">
<div><p className="text-[8px] font-bold text-gray-400 uppercase tracking-widest mb-1">Név</p><p className="font-bold text-gray-900">{viewOrder.details?.billingName}</p></div>
<div><p className="text-[8px] font-bold text-gray-400 uppercase tracking-widest mb-1">Típus</p><p className="font-bold uppercase text-[10px]">{viewOrder.details?.billingType === 'company' ? 'Cég' : 'Magánszemély'}</p></div>
{viewOrder.details?.billingType === 'company' && <div><p className="text-[8px] font-bold text-gray-400 uppercase tracking-widest mb-1 text-red-500">Adószám</p><p className="font-black text-red-600">{viewOrder.details?.taxNumber}</p></div>}
<div className="sm:col-span-2"><p className="text-[8px] font-bold text-gray-400 uppercase tracking-widest mb-1">Cím</p><p className="font-bold">{viewOrder.details?.billingZip} {viewOrder.details?.billingCity}, {viewOrder.details?.billingAddress}</p></div>
</div>
2025-12-21 20:40:32 +01:00
</div>
</div>
</section>
2025-12-26 14:03:18 +01:00
{/* AI PROMPT PANEL */}
<section className="bg-gray-900 p-8 rounded-[40px] shadow-2xl border border-primary/20">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 mb-8">
<div>
<h3 className="text-xs font-black uppercase tracking-[0.3em] flex items-center gap-2 text-primary"><Cpu className="w-5 h-5" /> AI Prompt Generátor</h3>
<p className="text-[10px] text-gray-400 mt-1 uppercase font-bold tracking-widest">Műszaki specifikáció készítése AI-val</p>
</div>
<button
onClick={handleGenerateAIPrompts}
disabled={generatingPrompts}
className="bg-primary hover:bg-primary-dark text-white px-6 py-3 rounded-2xl font-black text-[10px] uppercase tracking-widest transition-all flex items-center gap-2 shadow-lg shadow-primary/40 disabled:opacity-50"
>
{generatingPrompts ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Wand2 className="w-4 h-4" />}
PROMPTOK GENERÁLÁSA
</button>
2025-12-21 20:40:32 +01:00
</div>
2025-12-26 14:03:18 +01:00
{generatedPrompts.length > 0 ? (
<div className="space-y-6 animate-fade-in">
{generatedPrompts.map((p, idx) => (
<div key={idx} className="bg-white/5 border border-white/10 rounded-[24px] p-6 group">
<div className="flex justify-between items-center mb-4">
<h4 className="text-primary font-black text-xs uppercase tracking-wider">{p.title}</h4>
<button
onClick={() => {
navigator.clipboard.writeText(p.content);
alert("Másolva!");
}}
className="p-2 text-gray-500 hover:text-white hover:bg-white/10 rounded-xl transition-all"
>
<Copy className="w-4 h-4" />
</button>
</div>
<pre className="text-[11px] text-gray-300 font-mono leading-relaxed whitespace-pre-wrap bg-black/40 p-4 rounded-xl border border-white/5 max-h-[200px] overflow-y-auto scrollbar-hide">
{p.content}
</pre>
</div>
))}
</div>
) : (
<div className="text-center py-12 border-2 border-dashed border-white/10 rounded-[32px]">
<p className="text-xs text-gray-500 italic">Kattints a gombra a technikai promptok legenerálásához a megbeszéltek alapján.</p>
</div>
)}
</section>
{/* PÉNZÜGYEK SZEKCIÓ */}
<section>
<h3 className="text-[11px] font-black text-gray-400 uppercase tracking-[0.3em] mb-6 flex items-center gap-2"><DollarSign className="w-4 h-4" /> Pénzügyek & Számlázás</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 lg:gap-10">
<BillSection type="advance" label="Előlegszámla" />
<BillSection type="final" label="Végszámla" />
</div>
</section>
{/* ÉRTESÍTŐK SZEKCIÓ (MIND A 8 DB) */}
<section>
<h3 className="text-[11px] font-black text-gray-400 uppercase tracking-[0.3em] mb-6 flex items-center gap-2"><Mail className="w-4 h-4" /> Rendszer Értesítők</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{[
{ label: 'Fejlesztés megkezdése', icon: Rocket },
{ label: 'Demo elkészült', icon: Globe },
{ label: '1 hete nincs válasz', icon: Clock },
{ label: '2 hete nincs válasz', icon: AlertTriangle },
{ label: 'Projekt lezárva (1 hónap)', icon: Archive },
{ label: 'Módosítások fejlesztése', icon: Edit3 },
{ label: 'Weboldal élesítése megkezdődött', icon: Zap },
{ label: 'Élesített weboldal elkészült', icon: CheckCircle }
].map((tmpl) => (
<button
key={tmpl.label}
onClick={() => handleSendEmail(tmpl.label)}
disabled={!!emailSending}
className="bg-white p-4 rounded-2xl border border-gray-100 hover:border-primary hover:shadow-md transition-all flex items-center gap-4 text-left group disabled:opacity-50"
>
<div className="w-10 h-10 rounded-xl bg-gray-50 flex items-center justify-center text-gray-400 group-hover:bg-primary/10 group-hover:text-primary transition-colors flex-shrink-0">
{emailSending === tmpl.label ? <RefreshCw className="w-5 h-5 animate-spin" /> : <tmpl.icon className="w-5 h-5" />}
</div>
<p className="text-[10px] font-black text-gray-900 uppercase tracking-widest leading-tight">{tmpl.label}</p>
</button>
))}
</div>
</section>
{/* E-MAIL ELŐZMÉNYEK SZEKCIÓ */}
<section className="bg-purple-50/50 p-8 rounded-[32px] border border-purple-100">
<h3 className="text-[11px] font-black text-purple-600 uppercase tracking-[0.3em] mb-6 flex items-center gap-2"><History className="w-4 h-4" /> E-mail Előzmények</h3>
<div className="space-y-4 max-h-[500px] overflow-y-auto pr-4 scrollbar-hide">
{viewOrder.emailLogs?.map(log => (
<div key={log.id} className="bg-white rounded-2xl border border-purple-100 overflow-hidden shadow-sm transition-all">
<div className="p-4 flex justify-between items-center bg-white">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-purple-50 flex items-center justify-center text-purple-400">
<Mail className="w-4 h-4" />
</div>
<div>
<p className="text-[11px] font-black text-gray-900 uppercase tracking-wider">{log.email_type}</p>
<p className="text-[9px] text-gray-400 font-bold">{new Date(log.sent_at).toLocaleString('hu-HU')}</p>
</div>
</div>
<button
onClick={() => setExpandedLogId(expandedLogId === log.id ? null : log.id)}
className="p-2 text-purple-400 hover:text-purple-600 hover:bg-purple-50 rounded-xl transition-all flex items-center gap-2 text-[10px] font-black uppercase tracking-widest"
>
{expandedLogId === log.id ? <><EyeOff className="w-3.5 h-3.5" /> Elrejtés</> : <><Eye className="w-3.5 h-3.5" /> Megtekintés</>}
</button>
</div>
{expandedLogId === log.id && (
<div className="p-6 bg-gray-50 border-t border-purple-50 animate-fade-in overflow-x-auto">
<div className="bg-white p-6 rounded-xl border border-purple-50 shadow-inner max-w-full">
<div dangerouslySetInnerHTML={{ __html: log.body || '' }} className="email-preview-content" />
</div>
</div>
)}
2025-12-21 20:40:32 +01:00
</div>
))}
2025-12-26 14:03:18 +01:00
{(!viewOrder.emailLogs || viewOrder.emailLogs.length === 0) && <p className="text-xs text-gray-400 italic">Még nem küldtél e-mailt ebben a projektben.</p>}
2025-12-21 20:40:32 +01:00
</div>
</section>
</div>
2025-12-26 14:03:18 +01:00
{/* OLDALSÁV */}
<div className="md:col-span-4 space-y-10">
<section className="bg-gray-900 p-8 rounded-[40px] shadow-2xl text-white">
<h3 className="text-xs font-black uppercase tracking-[0.3em] mb-6 flex items-center gap-2 text-primary"><Globe className="w-5 h-5" /> Projekt Demo Link</h3>
<div className="space-y-4">
<input type="text" value={demoUrlInput} onChange={e => setDemoUrlInput(e.target.value)} placeholder="https://demo.motionweb.hu/..." className="w-full bg-white/5 border border-white/10 px-5 py-4 rounded-2xl text-sm font-bold text-white outline-none focus:border-primary transition-all" />
<button onClick={handleUpdateDemoUrl} disabled={savingDemoUrl} className="w-full py-4 bg-primary hover:bg-primary-dark text-white font-black text-[10px] uppercase tracking-widest rounded-2xl transition-all shadow-lg shadow-primary/40">
{savingDemoUrl ? 'Mentés...' : 'PUBLIKÁLÁS & ÉRTESÍTÉS'}
2025-12-21 20:40:32 +01:00
</button>
</div>
</section>
2025-12-26 14:03:18 +01:00
<section className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm">
<h3 className="text-xs font-black text-gray-900 uppercase tracking-[0.3em] mb-6 flex items-center gap-2"><Activity className="w-5 h-5 text-primary" /> Státusz Kezelés</h3>
<div className="flex flex-col gap-2">
{[
{ id: 'new', label: 'Beérkezett (Új)' },
{ id: 'in_progress', label: 'Fejlesztés alatt' },
{ id: 'pending_feedback', label: 'Visszajelzésre vár' },
{ id: 'completed', label: 'Kész / Átadva' },
{ id: 'cancelled', label: 'Törölve' }
].map(s => (
<button key={s.id} onClick={() => handleStatusChange(viewOrder.id, s.id)} disabled={statusUpdating === viewOrder.id} className={`w-full p-4 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all border-2 flex items-center justify-between ${viewOrder.status === s.id ? 'bg-primary/5 border-primary text-primary' : 'bg-white border-gray-50 text-gray-400 hover:border-gray-200'}`}>
<span>{s.label}</span>
{viewOrder.status === s.id && <CheckCircle className="w-3 h-3" />}
{statusUpdating === viewOrder.id && viewOrder.status !== s.id && s.id === statusUpdating && <RefreshCw className="w-3 h-3 animate-spin" />}
</button>
))}
</div>
</section>
2025-12-21 20:40:32 +01:00
<section>
2025-12-26 14:03:18 +01:00
<h3 className="text-xs font-black text-gray-900 uppercase tracking-[0.3em] mb-6 flex items-center gap-2"><History className="w-5 h-5 text-gray-400" /> Státusztörténet</h3>
<div className="space-y-6 relative before:absolute before:left-[11px] before:top-2 before:bottom-2 before:w-0.5 before:bg-gray-100 ml-2">
2025-12-21 20:40:32 +01:00
{viewOrder.history?.map((h, i) => (
<div key={h.id} className="relative pl-8">
2025-12-26 14:03:18 +01:00
<div className={`absolute left-0 top-1.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm z-10 ${i === 0 ? 'bg-primary' : 'bg-gray-300'}`} />
<StatusBadge status={h.status} />
<p className="text-[9px] font-bold text-gray-400 mt-1 uppercase">{new Date(h.changed_at).toLocaleString('hu-HU')}</p>
2025-12-21 20:40:32 +01:00
</div>
))}
</div>
</section>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
);
2025-12-26 14:03:18 +01:00
};