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 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
|
|
|
};
|