From 235e1d9b52268ae5f9dcd4f86b0110859fd5ab02 Mon Sep 17 00:00:00 2001 From: htom Date: Fri, 26 Dec 2025 14:03:18 +0100 Subject: [PATCH] stripe --- components/FeedbackModal.tsx | 383 ++-- components/OrderForm.tsx | 1099 +++++------- components/ProfileCompleter.tsx | 117 +- index.html | 2 +- lib/defaultPlans.ts | 11 +- pages/Admin.tsx | 1561 +++++++++++++---- pages/Dashboard.tsx | 260 ++- pages/Home.tsx | 326 ++-- pages/Privacy.tsx | 22 +- pages/Products.tsx | 136 +- pages/Terms.tsx | 45 +- pages/auth/Login.tsx | 3 +- pages/demos/BlueWave.tsx | 564 +++--- pages/demos/Steelguard.tsx | 664 ++++--- pages/demos/SweetCraving.tsx | 218 +-- .../functions/check-subscriptions/index.ts | 104 ++ .../create-checkout-session/index.ts | 90 + types.ts | 19 +- 18 files changed, 3334 insertions(+), 2290 deletions(-) create mode 100644 supabase/functions/check-subscriptions/index.ts create mode 100644 supabase/functions/create-checkout-session/index.ts diff --git a/components/FeedbackModal.tsx b/components/FeedbackModal.tsx index f2f9df0..74ef1ec 100644 --- a/components/FeedbackModal.tsx +++ b/components/FeedbackModal.tsx @@ -1,5 +1,6 @@ -import React, { useState } from 'react'; -import { X, Info, CheckCircle, AlertTriangle, MessageSquare, Calendar, ChevronDown, ChevronUp } from 'lucide-react'; + +import React, { useState, useEffect } from 'react'; +import { X, Info, CheckCircle, AlertTriangle, MessageSquare, Calendar, ChevronDown, ChevronUp, Wallet, ArrowLeft, RefreshCw, CreditCard } from 'lucide-react'; import { Button } from './Button'; interface FeedbackModalProps { @@ -7,11 +8,13 @@ interface FeedbackModalProps { onClose: () => void; onSubmit: (feedbackData: any) => Promise; loading: boolean; + order?: any; } -export const FeedbackModal: React.FC = ({ isOpen, onClose, onSubmit, loading }) => { +export const FeedbackModal: React.FC = ({ isOpen, onClose, onSubmit, loading, order }) => { // --- STATE --- const [mainDecision, setMainDecision] = useState<'approved' | 'minor' | 'major' | null>(null); + const [showPayment, setShowPayment] = useState(false); // Approval Flow const [approvalConfirmed, setApprovalConfirmed] = useState(false); @@ -38,8 +41,28 @@ export const FeedbackModal: React.FC = ({ isOpen, onClose, o const [revisionConfirmed, setRevisionConfirmed] = useState(false); + useEffect(() => { + if (isOpen) { + setMainDecision(null); + setShowPayment(false); + setApprovalConfirmed(false); + setRevisionConfirmed(false); + // Reset other fields if needed + } + }, [isOpen]); + if (!isOpen) return null; + // --- LOGIC --- + const paymentDetails = order?.details?.payment_summary; + const totalAmount = paymentDetails?.total || 0; + const advanceAmount = paymentDetails?.advance || 0; + const remainingAmount = paymentDetails?.remaining || 0; + const isCustomPrice = paymentDetails?.is_custom; + + // We show payment step ONLY if Approved AND there is a remaining amount AND it's not custom price (unless handled) + const shouldShowPaymentStep = mainDecision === 'approved' && remainingAmount > 0 && !isCustomPrice; + // --- HANDLERS --- const handleCheckbox = ( currentList: string[], @@ -71,7 +94,8 @@ export const FeedbackModal: React.FC = ({ isOpen, onClose, o decision: mainDecision, submittedAt: new Date().toISOString(), approval: mainDecision === 'approved' ? { - confirmed: approvalConfirmed + confirmed: approvalConfirmed, + paymentComplete: showPayment // Flag that payment step was shown } : null, revision: isRevision ? { design: { selected: designCheckboxes, comment: designText }, @@ -88,7 +112,12 @@ export const FeedbackModal: React.FC = ({ isOpen, onClose, o onSubmit(feedbackData); }; + const handleProceedToPayment = () => { + setShowPayment(true); + }; + const commonInputStyles = "w-full p-4 border border-gray-300 rounded-xl text-sm font-medium text-black placeholder-gray-400 focus:ring-4 focus:ring-primary/10 focus:border-primary outline-none bg-white transition-all shadow-sm"; + const formatPrice = (num: number) => num.toLocaleString('hu-HU') + ' Ft'; const renderFeedbackCategory = ( title: string, @@ -133,157 +162,233 @@ export const FeedbackModal: React.FC = ({ isOpen, onClose, o return (
-
+
-
-
-
- -
-
-

Visszajelzés a Weboldalról

-

A bemutató verzió alapján kérjük jelezd, ha valamit módosítanál.

-
+ {showPayment ? ( + /* PAYMENT SUMMARY VIEW */ +
+
+
+ +
+

Véglegesítés & Fizetés

+

A fennmaradó összeg rendezése a projekt lezárásához.

+
+
+
+ +
+
+
+ +
+

Projekt Elszámolás

+

A demó oldal jóváhagyásával a projekt a befejező szakaszba lép. Kérjük, egyenlítse ki a fennmaradó összeget.

+
+ +
+
+

Projekt Teljes Ára

+

{formatPrice(totalAmount)}

+
+
+

Már Befizetve (Előleg)

+

{formatPrice(advanceAmount)}

+
+
+ +
+
+

Most Fizetendő (Fennmaradó)

+
+ {formatPrice(remainingAmount)} +
+
+ +
+
+ +
+
+

+ Fontos: A befizetés után a rendszer automatikusan kiállítja a végszámlát és elküldi e-mailben. A fejlesztő csapat értesítést kap a jóváhagyásról és megkezdi a végleges élesítést. +

+
+
-
- -
-
-

- Döntés a továbblépésről * -

-
- {[ - { id: 'approved', label: 'Igen, jóváhagyom a terveket', color: 'green', icon: '✅' }, - { id: 'minor', label: 'Alapvetően jó, de kisebb javításokat kérek', color: 'yellow', icon: '🔧' }, - { id: 'major', label: 'Nem megfelelő, jelentős módosításra van szükség', color: 'red', icon: '❌' } - ].map(opt => ( - - ))} + ) : ( + /* STANDARD FEEDBACK FORM */ + <> +
+
+
+ +
+
+

Visszajelzés a Weboldalról

+

A bemutató verzió alapján kérjük jelezd, ha valamit módosítanál.

+
+
-
- {mainDecision === 'approved' && ( -
-

- Megerősítés -

- +
+
+

+ Döntés a továbblépésről * +

+
+ {[ + { id: 'approved', label: 'Igen, jóváhagyom a terveket', color: 'green', icon: '✅' }, + { id: 'minor', label: 'Alapvetően jó, de kisebb javításokat kérek', color: 'yellow', icon: '🔧' }, + { id: 'major', label: 'Nem megfelelő, jelentős módosításra van szükség', color: 'red', icon: '❌' } + ].map(opt => ( + + ))} +
+
+ + {mainDecision === 'approved' && ( +
+

+ Megerősítés +

+ +
+ )} + + {isRevision && ( +
+ + {renderFeedbackCategory( + "Dizájn módosítások", + ["Színek", "Betűtípusok", "Elrendezés", "Képek / Illusztrációk", "Nem szeretnék dizájn módosítást"], + designCheckboxes, setDesignCheckboxes, + designText, setDesignText, + "Írd le pontosan, mit változtatnál a megjelenésen..." + )} + + {renderFeedbackCategory( + "Tartalmi változtatások", + ["Szövegek stílusa", "Adatok pontosítása", "Hiányzó tartalom", "Nem szeretnék tartalmi módosítást"], + contentCheckboxes, setContentCheckboxes, + contentText, setContentText, + "Írd le a szöveges módosításokat..." + )} + +
+

Mi a legfontosabb kérésed? *

+ +
+ +
+

Egyéb észrevételek

+ +
+ +
+ +
+ +
+ )}
- )} - {isRevision && ( -
+
+ - {renderFeedbackCategory( - "Dizájn módosítások", - ["Színek", "Betűtípusok", "Elrendezés", "Képek / Illusztrációk", "Nem szeretnék dizájn módosítást"], - designCheckboxes, setDesignCheckboxes, - designText, setDesignText, - "Írd le pontosan, mit változtatnál a megjelenésen..." + {mainDecision === 'approved' && ( + shouldShowPaymentStep ? ( + + ) : ( + + ) )} - {renderFeedbackCategory( - "Tartalmi változtatások", - ["Szövegek stílusa", "Adatok pontosítása", "Hiányzó tartalom", "Nem szeretnék tartalmi módosítást"], - contentCheckboxes, setContentCheckboxes, - contentText, setContentText, - "Írd le a szöveges módosításokat..." + {isRevision && ( + )} -
-

Mi a legfontosabb kérésed? *

- -
- -
-

Egyéb észrevételek

- -
- -
- -
- + {!mainDecision && ( + + )}
- )} -
- -
- - - {mainDecision === 'approved' && ( - - )} - - {isRevision && ( - - )} - - {!mainDecision && ( - - )} -
+ + )}
); -}; \ No newline at end of file +}; diff --git a/components/OrderForm.tsx b/components/OrderForm.tsx index 34593a5..98a8581 100644 --- a/components/OrderForm.tsx +++ b/components/OrderForm.tsx @@ -1,14 +1,18 @@ -import React, { useState } from 'react'; + +import React, { useState, useEffect } from 'react'; import { Send, CheckCircle, AlertCircle, Globe, Server, Check, ArrowRight, ArrowLeft, User, FileText, Target, Layout, Palette, Zap, Lightbulb, Settings, ClipboardCheck, Lock, - Cloud, Upload + Cloud, Upload, Receipt, Building2, Link as LinkIcon, Info, + CreditCard, Wallet, Calculator, RefreshCw } from 'lucide-react'; import { Button } from './Button'; import { useAuth } from '../context/AuthContext'; import { Link } from 'react-router-dom'; import { supabase, isSupabaseConfigured } from '../lib/supabaseClient'; +import { defaultPlans } from '../lib/defaultPlans'; +import { ProductPackage } from '../types'; interface Inspiration { url: string; @@ -18,60 +22,53 @@ interface Inspiration { export const OrderForm: React.FC = () => { const { user, loading } = useAuth(); const [currentStep, setCurrentStep] = useState(1); + const [showPaymentSummary, setShowPaymentSummary] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); const [errors, setErrors] = useState([]); const [privacyAccepted, setPrivacyAccepted] = useState(false); const [aszfAccepted, setAszfAccepted] = useState(false); - const totalSteps = 9; + const [availablePackages, setAvailablePackages] = useState(defaultPlans); + const totalSteps = 10; const [formData, setFormData] = useState({ - // 1. Kapcsolattartás name: '', company: '', email: user?.email || '', phone: '', - package: 'Pro Web', // Default selection updated - - // 2. Bemutatkozás + package: 'Pro Web', description: '', - - // 3. Célok goals: [] as string[], goalOther: '', successCriteria: '', - - // 4. Tartalom content: [] as string[], contentOther: '', - existingAssets: 'Nincs' as 'Igen' | 'Nem' | 'Részben', - contentLink: '', // New field for drive link - - // 5. Design + existingAssets: 'Nem' as 'Igen' | 'Nem', + contentLink: '', primaryColor: '', secondaryColor: '', balanceColor: '', style: [] as string[], targetAudience: '', - - // 6. Funkciók features: [] as string[], - - // 7. Inspirációk inspirations: [ { url: '', comment: '' }, { url: '', comment: '' }, { url: '', comment: '' } ] as Inspiration[], - - // 8. Extrák & Megjegyzések extras: [] as string[], domainName: '', - notes: '' + notes: '', + // Számlázási Mezők + billingType: 'individual' as 'individual' | 'company', + billingName: '', + billingZip: '', + billingCity: '', + billingAddress: '', + taxNumber: '' }); - // Helper arrays const colorOptions = [ { name: 'Piros', value: '#ef4444' }, { name: 'Kék', value: '#3b82f6' }, @@ -100,94 +97,46 @@ export const OrderForm: React.FC = () => { { id: 6, title: 'Funkciók', icon: Zap }, { id: 7, title: 'Inspirációk', icon: Lightbulb }, { id: 8, title: 'Extrák', icon: Settings }, - { id: 9, title: 'Összegzés', icon: ClipboardCheck }, + { id: 9, title: 'Számlázás', icon: Receipt }, + { id: 10, title: 'Összegzés', icon: ClipboardCheck }, ]; - // Auto-fill user data (Name and Email) when user loads - React.useEffect(() => { - const prefillUserData = async () => { - if (!user) return; - - let emailToSet = user.email || ''; - let nameToSet = ''; - - // 1. Try metadata (from session - fastest) - const meta = user.user_metadata; - if (meta?.last_name && meta?.first_name) { - // Hungarian order: Lastname Firstname - nameToSet = `${meta.last_name} ${meta.first_name}`; - } - - // 2. Fallback to DB if Supabase is configured and metadata is missing name - if (!nameToSet && isSupabaseConfigured) { - try { - const { data } = await supabase - .from('profiles') - .select('first_name, last_name') - .eq('id', user.id) - .maybeSingle(); - - if (data?.first_name && data?.last_name) { - nameToSet = `${data.last_name} ${data.first_name}`; - } - } catch (error) { - console.error('Error fetching user profile for order form:', error); + useEffect(() => { + const fetchPlans = async () => { + if (!isSupabaseConfigured) return; + try { + const { data } = await supabase.from('plans').select('*'); + if (data && data.length > 0) { + const sorted = [...data].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; + }); + setAvailablePackages(sorted); } + } catch (e) { + console.error("Error fetching packages:", e); } - - setFormData(prev => { - // Only update if fields are empty to avoid overwriting user input - if (prev.name && prev.email) return prev; - - return { - ...prev, - email: prev.email || emailToSet, - name: prev.name || nameToSet - }; - }); }; + fetchPlans(); + }, []); - prefillUserData(); + useEffect(() => { + if (user) { + setFormData(prev => ({ + ...prev, + email: user.email || '', + name: prev.name || `${user.user_metadata?.last_name || ''} ${user.user_metadata?.first_name || ''}`.trim(), + billingName: prev.billingName || `${user.user_metadata?.last_name || ''} ${user.user_metadata?.first_name || ''}`.trim() + })); + } }, [user]); - // If loading auth state, show a skeleton or loader - if (loading) { - return ( -
-
Betöltés...
-
- ); - } - - // Auth Protection - Lock Screen - if (!user) { - return ( -
-
-
- -
-

Jelentkezzen be a rendeléshez

-

- Weboldal rendelés leadásához kérjük, jelentkezzen be fiókjába, vagy regisztráljon egyet ingyenesen. -

-
-
-
- - - - - - -
-

- A regisztráció mindössze 1 percet vesz igénybe, és segít a projekt későbbi nyomon követésében. -

-
-
- ); - } + const selectedPkg = availablePackages.find(p => p.name === formData.package); + const totalAmount = selectedPkg?.total_price || 0; + const advanceAmount = selectedPkg?.advance_price || 0; + const remainingAmount = totalAmount - advanceAmount; + const isCustomPrice = selectedPkg?.is_custom_price; const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; @@ -200,33 +149,32 @@ export const OrderForm: React.FC = () => { if (list.includes(value)) { return { ...prev, [category]: list.filter(item => item !== value) }; } else { + if (category === 'style' && list.length >= 2) return prev; return { ...prev, [category]: [...list, value] }; } }); }; - const handleInspirationChange = (index: number, field: 'url' | 'comment', value: string) => { - const newInspirations = [...formData.inspirations]; - newInspirations[index] = { ...newInspirations[index], [field]: value }; - setFormData(prev => ({ ...prev, inspirations: newInspirations })); - }; - const validateStep = (step: number): boolean => { const newErrors: string[] = []; - if (step === 1) { if (!formData.name) newErrors.push('A név megadása kötelező.'); - if (!formData.email) newErrors.push('Az e-mail cím megadása kötelező.'); if (!formData.phone) newErrors.push('A telefonszám megadása kötelező.'); } - - if (newErrors.length > 0) { - setErrors(newErrors); - return false; + if (step === 9) { + if (!formData.billingName) newErrors.push('A számlázási név megadása kötelező.'); + if (!formData.billingZip) newErrors.push('Az irányítószám megadása kötelező.'); + if (!formData.billingCity) newErrors.push('A város megadása kötelező.'); + if (!formData.billingAddress) newErrors.push('A cím megadása kötelező.'); + if (formData.billingType === 'company' && !formData.taxNumber) newErrors.push('Cég esetén az adószám kötelező.'); } - - setErrors([]); - return true; + if (step === 10) { + if (!privacyAccepted) newErrors.push('Az adatkezelési tájékoztató elfogadása kötelező.'); + if (!aszfAccepted) newErrors.push('Az ÁSZF elfogadása kötelező.'); + } + + setErrors(newErrors); + return newErrors.length === 0; }; const nextStep = () => { @@ -235,12 +183,17 @@ export const OrderForm: React.FC = () => { setCurrentStep(prev => prev + 1); window.scrollTo({ top: document.getElementById('order-form-container')?.offsetTop || 0, behavior: 'smooth' }); } else { - handleSubmit(); + setShowPaymentSummary(true); + window.scrollTo({ top: document.getElementById('order-form-container')?.offsetTop || 0, behavior: 'smooth' }); } } }; const prevStep = () => { + if (showPaymentSummary) { + setShowPaymentSummary(false); + return; + } if (currentStep > 1) { setCurrentStep(prev => prev - 1); window.scrollTo({ top: document.getElementById('order-form-container')?.offsetTop || 0, behavior: 'smooth' }); @@ -251,626 +204,430 @@ export const OrderForm: React.FC = () => { setIsSubmitting(true); setSubmitError(null); - if (!privacyAccepted) { - setSubmitError('A rendelés leadásához el kell fogadnia az Adatkezelési tájékoztatót.'); - setIsSubmitting(false); - return; - } - - if (!aszfAccepted) { - setSubmitError('A rendelés leadásához el kell fogadnia az Általános Szerződési Feltételeket (ÁSZF).'); - setIsSubmitting(false); - return; - } - - // Determine amount text - const amount = - formData.package === 'Landing Page' ? '190.000 Ft' : - formData.package === 'Pro Web' ? '350.000 Ft' : - 'Egyedi Árazás'; + const amountStr = isCustomPrice ? 'Egyedi árazás' : `${totalAmount.toLocaleString('hu-HU')} Ft`; if (isSupabaseConfigured && user) { try { - const { error } = await supabase.from('orders').insert({ + // 1. Create order + const { data: orderData, error } = await supabase.from('orders').insert({ user_id: user.id, customer_name: formData.name, customer_email: formData.email, package: formData.package, status: 'new', - amount: amount, - details: formData - }); + amount: amountStr, + details: { + ...formData, + payment_summary: { + total: totalAmount, + advance: advanceAmount, + remaining: remainingAmount, + currency: 'HUF', + is_custom: isCustomPrice + } + } + }).select().single(); + + if (error) throw error; - if (error) { - throw error; + // 2. Initiate Stripe Payment if not custom price + if (!isCustomPrice && (formData.package === 'Landing Page' || formData.package === 'Pro Web')) { + try { + const { data: checkoutData, error: checkoutError } = await supabase.functions.invoke('create-checkout-session', { + body: { + order_id: orderData.id, + package_name: formData.package, + payment_type: 'deposit', + customer_email: formData.email + } + }); + + if (checkoutError) throw checkoutError; + + if (checkoutData?.url) { + window.location.href = checkoutData.url; + return; // Stop execution to redirect + } + } catch (stripeErr: any) { + console.error("Stripe error:", stripeErr); + alert("Rendelés rögzítve, de a fizetési rendszer átmenetileg nem elérhető. Kérjük vegye fel velünk a kapcsolatot."); + setIsSubmitted(true); + } + } else { + // Custom price or demo mode + setIsSubmitted(true); } - setIsSubmitted(true); - window.scrollTo({ top: document.getElementById('order-form-container')?.offsetTop || 0, behavior: 'smooth' }); } catch (err: any) { - console.error('Error submitting order:', err); - setSubmitError('Hiba történt a rendelés elküldésekor: ' + err.message); + setSubmitError('Hiba a rendelés mentésekor: ' + err.message); } finally { setIsSubmitting(false); } } else { - // Fallback for Demo Mode - console.log('Demo Mode: Order Data:', formData); - await new Promise(resolve => setTimeout(resolve, 1500)); // Simulate delay + // Demo mode fallback + await new Promise(r => setTimeout(r, 1000)); setIsSubmitted(true); setIsSubmitting(false); - window.scrollTo({ top: document.getElementById('order-form-container')?.offsetTop || 0, behavior: 'smooth' }); } }; - // Modern Input Style Class - MotionWeb Style - const inputClass = "w-full px-4 py-3 rounded-lg border border-gray-200 bg-white text-[#111111] placeholder-gray-400 focus:ring-4 focus:ring-[#4e6bff]/10 focus:border-[#4e6bff] outline-none transition-all shadow-sm"; - const labelClass = "text-sm font-semibold text-gray-800 block mb-2"; - const checkboxClass = "w-5 h-5 text-[#4e6bff] rounded focus:ring-[#4e6bff] border-gray-300 flex-shrink-0 transition-colors cursor-pointer"; + const inputClass = "w-full px-4 py-3 rounded-xl border border-gray-200 bg-white focus:ring-4 focus:ring-primary/10 focus:border-primary outline-none transition-all shadow-sm font-medium text-gray-900"; + const labelClass = "text-xs font-black text-gray-400 uppercase tracking-widest mb-2 block ml-1"; - const ColorPickerSection = ({ label, selected, onChange }: { label: string, selected: string, onChange: (color: string) => void }) => ( -
- -
- {colorOptions.map((color) => ( - - ))} -
-

{selected ? `Választott: ${selected}` : ''}

-
- ); + const formatPrice = (num: number) => num.toLocaleString('hu-HU') + ' Ft'; + + if (loading) { + return
Betöltés...
; + } + + // Ha nincs bejelentkezve, mutassuk a bejelentkezési felhívást + if (!user) { + return ( +
+
+ +
+

Jelentkezz be a folytatáshoz!

+

+ A projekt indításához és a rendelés leadásához kérjük, lépj be fiókodba, vagy regisztrálj egyet pár perc alatt. +

+
+ + + + + + +
+
+ ); + } if (isSubmitted) { return ( -
-
- -
-

Rendelését sikeresen rögzítettük!

-

- Átirányítjuk az előlegfizetési oldalra. Amennyiben ez nem történik meg, kollégáink hamarosan felveszik Önnel a kapcsolatot. -

- +
+
+

Rendelésedet fogadtuk!

+

Köszönjük a bizalmat! A megadott e-mail címre elküldtük a visszaigazolást. Kollégánk hamarosan felveszi veled a kapcsolatot.

+ + +
); } return ( -
- {/* Light Gradient Header & Step Navigation */} -
+
+ {/* Header with Progress */} +
-

Rendelés Leadása

-

Töltse ki az űrlapot a pontos ajánlatadáshoz, és segítünk megvalósítani elképzeléseit.

+

Projekt Indítása

+

+ {showPaymentSummary + ? (isCustomPrice ? 'Utolsó lépés: Véglegesítés' : 'Utolsó lépés: Fizetés') + : `Lépés ${currentStep} / ${totalSteps}`} +

- {/* Desktop Stepper Navigation */} -
- {/* Background Line */} -
- {/* Active Progress Line */} -
- - {steps.map((step) => { - const isActive = step.id === currentStep; - const isCompleted = step.id < currentStep; - - return ( -
-
- {isCompleted ? : } + {!showPaymentSummary && ( +
+
+
+
+
+ {steps.map((s) => ( + -
- {step.title} -
-
- ); - })} -
- - {/* Mobile/Tablet Progress Bar */} -
-
- Lépés {currentStep} / {totalSteps} - {steps[currentStep - 1].title} -
-
-
-
-
+ ))} +
+
+ )}
-
+ {/* Main Content */} +
{errors.length > 0 && ( -
- -
-

Kérjük, javítsa a következő hibákat:

-
    - {errors.map((err, idx) =>
  • {err}
  • )} -
+
+ +
+ {errors.map((err, idx) =>

{err}

)}
)} - {submitError && ( -
- -
{submitError}
-
- )} - - {/* Step 1: Kapcsolattartási adatok */} - {currentStep === 1 && ( -
-
-
- + {!showPaymentSummary ? ( +
+ {currentStep === 1 && ( +
+
+
+
+
+
-

Kapcsolattartási adatok

-
- -
-
- - -
-
- - -
-
- - -

A bejelentkezett e-mail címed.

-
-
- - -
-
- -
- -
- {['Landing Page', 'Pro Web', 'Enterprise'].map((pkg) => ( -
-
- )} - - {currentStep === 2 && ( -
-
-
- -
-

A vállalkozás rövid bemutatása

-
- -
- - -

💡 Tipp: Írjon le mindent, amit fontosnak tart a tevékenységével kapcsolatban.

-
-
- )} - - {currentStep === 3 && ( -
-
-
- -
-

Mi a weboldal fő célja?

-
- -
- {['Cég bemutatása', 'Termék / szolgáltatás értékesítés', 'Időpontfoglalás vagy ügyfélszerzés', 'Hírlevél / e-mail lista építése', 'Közösségépítés', 'Egyéb'].map((goal) => ( -
- - {goal === 'Egyéb' && formData.goals.includes('Egyéb') && ( -
- -
- )} -
- ))} -
-
- - -
-
- )} - - {currentStep === 4 && ( -
-
-
- -
-

Tartalom és szerkezet

-
- -
- -
- {['Rólunk', 'Szolgáltatások', 'Termékek', 'Referenciák / Vélemények', 'Blog / Hírek', 'Kapcsolat', 'Egyéb'].map((item) => ( -
- - {item === 'Egyéb' && formData.content.includes('Egyéb') && ( - - )} +
+ +
+ {availablePackages.map((pkg) => ( + + ))}
- ))} +
-
+ )} -
- -
- {['Igen', 'Nem', 'Részben'].map((opt) => ( - - ))} + {currentStep === 2 && ( +
+ +
- - {/* Content Upload / Link Section */} - {(formData.existingAssets === 'Igen' || formData.existingAssets === 'Részben') && ( -
-

- Tartalom megosztása -

-

- Kérjük, töltse fel a meglévő tartalmakat (képek, szövegek, logó) egy felhő tárhelyre (pl. Google Drive) és ossza meg a linket, vagy töltsön fel fájlt (max 1GB). -

-
-
- - -
-
- -
- -

Kattintson a feltöltéshez vagy húzza ide a fájlt

-

(Max 1GB)

- + )} + + {currentStep === 3 && ( +
+ +
+ {['Cég bemutatása', 'Online értékesítés', 'Időpontfoglalás', 'Ügyfélszerzés', 'Információközlés'].map(goal => ( + + ))} +
+
+ )} + + {currentStep === 4 && ( +
+ +
+ {['Rólunk', 'Szolgáltatások', 'Referenciák', 'Gyakori Kérdések', 'Blog', 'Kapcsolat'].map(item => ( + + ))} +
+
+ + +
+
+ )} + + {currentStep === 5 && ( +
+ +
+

FŐSZÍN

setFormData({...formData, primaryColor: e.target.value})} className="w-full h-12 rounded-xl cursor-pointer bg-white" />
+

MELLÉKSZÍN

setFormData({...formData, secondaryColor: e.target.value})} className="w-full h-12 rounded-xl cursor-pointer bg-white" />
+

HÁTTÉRSZÍN

setFormData({...formData, balanceColor: e.target.value})} className="w-full h-12 rounded-xl cursor-pointer bg-white" />
+
+
+ +
+ {styleOptions.map(style => ( + + ))} +
+
+
+ )} + + {currentStep === 6 && ( +
+ +
+ {['Kapcsolatfelvételi űrlap', 'Hírlevél feliratkozás', 'Kereső funkció', 'Többnyelvűség', 'Admin felület', 'Bankkártyás fizetés'].map(feat => ( + + ))} +
+
+ )} + + {currentStep === 7 && ( +
+ +
+ {[0, 1, 2].map(i => ( +
+
+

Példa oldal {i+1} linkje

+ { + const ins = [...formData.inspirations]; + ins[i].url = e.target.value; + setFormData({...formData, inspirations: ins}); + }} + placeholder="https://..." + className={inputClass} + />
-
-
-
- )} -
-
- )} - - {currentStep === 5 && ( -
-
-
- -
-

Design és stílus

-
- -
- setFormData(prev => ({ ...prev, primaryColor: c }))} /> - setFormData(prev => ({ ...prev, secondaryColor: c }))} /> - setFormData(prev => ({ ...prev, balanceColor: c }))} /> -
- -
- -
- {styleOptions.map((style) => ( - - ))} +
+

Mi tetszik rajta?

+ { + const ins = [...formData.inspirations]; + ins[i].comment = e.target.value; + setFormData({...formData, inspirations: ins}); + }} + placeholder="Színek, elrendezés, stílus..." + className={inputClass} + /> +
+
+ ))} +
-
+ )} -
- - -
-
- )} - - {currentStep === 6 && ( -
-
-
- -
-

Funkciók és technikai igények

-
- -
- {['Kapcsolatfelvételi űrlap', 'Időpontfoglalás rendszer', 'Hírlevél-feliratkozás', 'Webshop / fizetési lehetőség', 'Blog / hírek', 'Többnyelvűség', 'Képgaléria / videógaléria', 'Google Térkép integráció', 'Chat gomb (Messenger / WhatsApp)'].map((feat) => ( - - ))} -
-
- )} - - {currentStep === 7 && ( -
-
-
- -
-

Inspirációk

-

Segítsen megérteni az ízlését!

-
- -
-
-

Osszon meg velünk 3 weboldalt, ami tetszik, és írja le röviden, miért (pl. színek, elrendezés, hangulat).

-
- -
- {[0, 1, 2].map((i) => ( -
-
- -
- - handleInspirationChange(i, 'url', e.target.value)} - placeholder="https://pelda.hu" - className={`${inputClass} pl-9`} - /> -
-
-
- - handleInspirationChange(i, 'comment', e.target.value)} - placeholder="Színek, elrendezés, animációk..." - className={inputClass} - /> -
-
- ))} -
-
- )} - - {currentStep === 8 && ( -
-
-
- -
-

Extra szolgáltatások és Megjegyzések

-
- -
- -
- {['SEO optimalizálás', 'Szövegírás', 'Domain ügyintézés', 'Tárhely ügyintézés'].map((extra) => ( - - ))} + {currentStep === 8 && ( +
+ +
+ )} - {/* Dynamic Content */} -
- {formData.extras.includes('Domain ügyintézés') && ( -
- -
- - + {currentStep === 9 && ( +
+
+ + +
+
+
+ {formData.billingType === 'company' &&
} +
+
+
+
+
+ )} + + {currentStep === 10 && ( +
+
+
+

Csomag

{formData.package}

+

Ügyfél

{formData.name}

+

Számlázási Cím

{formData.billingZip} {formData.billingCity}, {formData.billingAddress}

+
+
+
+ + +
+
+ )} +
+ ) : ( + /* PAYMENT SUMMARY / CUSTOM ORDER VIEW */ +
+ {isCustomPrice ? ( + /* CUSTOM PRICE VIEW */ + <> +
+
+
-
- )} - - {/* Domain Warning */} - {!formData.extras.includes('Domain ügyintézés') && ( -
-
-
- Figyelem: Nem jelölte be a domain ügyintézést. Kérjük győződjön meg róla, hogy rendelkezik saját domain névvel, vagy a későbbiekben tudja biztosítani a DNS beállításokat. -
-
- )} +

Rendelés Véglegesítése

+

+ Egyedi csomagot választottál. A rendelés leadása után munkatársunk hamarosan felveszi veled a kapcsolatot a pontos igények és az árazás egyeztetése miatt. +

+
- {!formData.extras.includes('Tárhely ügyintézés') && ( -
-
-
- Figyelem: Mivel nem jelölte be a tárhely ügyintézést, a weboldalt átadjuk (forráskód), de az üzemeltetést és a szerver beállítását nem a MotionWeb végzi. +
+
+

Választott Konstrukció

+
+ Egyedi Árazás
-
- )} -
-
- -
- - -
-
- )} - - {/* Step 9: Összegzés */} - {currentStep === 9 && ( -
-
-
- -
-

Rendelés összesítése

-

Ellenőrizze az adatokat a véglegesítés előtt.

-
- -
-
-
-

Kapcsolattartó

-

{formData.name}

-

{formData.email}

-

{formData.phone}

- {formData.company &&

{formData.company}

} -
-
-

Választott Csomag

- {formData.package} -
-
- -
-
-

Célok

-
- {formData.goals.length > 0 ? formData.goals.map(g => {g}) : Nincs megadva} +

+ A "Rendelés Leadása" gombra kattintva rögzítjük igényedet. +

+
+ + ) : ( + /* STANDARD PAYMENT VIEW */ + <> +
+
+
-
-
-

Színvilág

-
- {formData.primaryColor &&
c.name === formData.primaryColor)?.value}} title="Főszín">
} - {formData.secondaryColor &&
c.name === formData.secondaryColor)?.value}} title="Mellékszín">
} - {formData.balanceColor &&
c.name === formData.balanceColor)?.value}} title="Kiegyensúlyozó">
} - {!formData.primaryColor && !formData.secondaryColor && !formData.balanceColor && Nincs színválasztás} -
-
-
-
+

Rendelés Összesítése

+

+ A projekt elindításához kérjük, fizesse be az előleget. A rendszerünk automatikusan értesít minket a befizetésről, és azonnal megkezdjük a munkát. +

+
-
-

Mi történik a rendelés leadása után?

-

- A "Rendelés leadása" gombra kattintva átirányítjuk az előlegfizetési oldalra. A sikeres tranzakciót követően kollégáink 48 órán belül felveszik Önnel a kapcsolatot a megadott elérhetőségeken a projekt indításához. -

-
+
+
+

Projekt Teljes Ára

+

{formatPrice(totalAmount)}

+
+
+

Fennmaradó (Demó után)

+

{formatPrice(remainingAmount)}

+
+
-
-
-
- setPrivacyAccepted(e.target.checked)} - className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded cursor-pointer" - /> -
-
- -
-
+
+
+

Jelenleg Fizetendő Előleg

+
+ {formatPrice(advanceAmount)} +
+

+ A gombra kattintva átirányítjuk a biztonságos Stripe fizetési oldalra. +

+
-
-
- setAszfAccepted(e.target.checked)} - className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded cursor-pointer" - /> -
-
- -
-
-
+
+
+

+ Fontos: A rendszer a rendelés leadása után elküldi Önnek az előlegszámlát is. A bankkártyás fizetés azonnali feldolgozást tesz lehetővé. +

+
+ + )}
)}
{/* Footer Navigation */} -
- - - {currentStep < totalSteps ? ( - + {!showPaymentSummary ? ( + ) : ( - )}
); -}; \ No newline at end of file +}; diff --git a/components/ProfileCompleter.tsx b/components/ProfileCompleter.tsx index 18b8847..fac641c 100644 --- a/components/ProfileCompleter.tsx +++ b/components/ProfileCompleter.tsx @@ -23,48 +23,39 @@ export const ProfileCompleter: React.FC = () => { const checkProfile = async () => { setChecking(true); - // --- DEMO MODE --- - if (!isSupabaseConfigured) { + const checkMetadataFallback = () => { const meta = user.user_metadata || {}; - if (!meta.first_name || !meta.last_name || !meta.date_of_birth) { + const hasRequiredData = meta.first_name && meta.last_name && meta.date_of_birth; + + if (!hasRequiredData) { setIsOpen(true); - if (meta.first_name) setFirstName(meta.first_name); - if (meta.last_name) setLastName(meta.last_name); - if (meta.date_of_birth) setDateOfBirth(meta.date_of_birth); + if (meta.first_name) setFirstName(meta.first_name); + if (meta.last_name) setLastName(meta.last_name); + if (meta.date_of_birth) setDateOfBirth(meta.date_of_birth); } + }; + + if (!isSupabaseConfigured) { + checkMetadataFallback(); setChecking(false); return; } - // ----------------- try { - const { data, error } = await supabase + const { data, error: fetchError } = await supabase .from('profiles') .select('first_name, last_name, date_of_birth') .eq('id', user.id) .maybeSingle(); - if (error) { - console.error('Error checking profile:', error.message || error); - // If DB check fails, fallback to metadata to see if we should block the user. - // This prevents locking the user out if the DB is temporarily unavailable or misconfigured, - // provided they have the data in their auth metadata. - const meta = user.user_metadata || {}; - const hasMetadata = meta.first_name && meta.last_name && meta.date_of_birth; - - if (!hasMetadata) { - setIsOpen(true); - if (meta.first_name) setFirstName(meta.first_name); - if (meta.last_name) setLastName(meta.last_name); - if (meta.date_of_birth) setDateOfBirth(meta.date_of_birth); - } + if (fetchError) { + // Ha hálózati hiba van (pl. adblocker vagy CORS), a metadata-ra hagyatkozunk csendben + checkMetadataFallback(); return; } - // If no profile exists, or names/dob are missing if (!data || !data.first_name || !data.last_name || !data.date_of_birth) { setIsOpen(true); - // Pre-fill if partial data exists if (data?.first_name) setFirstName(data.first_name); else if (user.user_metadata?.first_name) setFirstName(user.user_metadata.first_name); @@ -75,7 +66,13 @@ export const ProfileCompleter: React.FC = () => { else if (user.user_metadata?.date_of_birth) setDateOfBirth(user.user_metadata.date_of_birth); } } catch (err: any) { - console.error('Unexpected error in ProfileCompleter:', err); + // Hálózat-specifikus hibák esetén nem logolunk noisyt + if (err instanceof TypeError && err.message.includes('fetch')) { + checkMetadataFallback(); + } else { + console.debug('Supabase connection issue, using metadata fallback.'); + checkMetadataFallback(); + } } finally { setChecking(false); } @@ -96,11 +93,8 @@ export const ProfileCompleter: React.FC = () => { } try { - // --- DEMO MODE UPDATE --- if (!isSupabaseConfigured && user) { - await new Promise(resolve => setTimeout(resolve, 800)); // Fake delay - - // Update local storage session + await new Promise(resolve => setTimeout(resolve, 800)); const storedSession = localStorage.getItem('demo_user_session'); if (storedSession) { const parsed = JSON.parse(storedSession); @@ -111,15 +105,13 @@ export const ProfileCompleter: React.FC = () => { date_of_birth: dateOfBirth }; localStorage.setItem('demo_user_session', JSON.stringify(parsed)); - refreshDemoUser(); // Refresh context + refreshDemoUser(); } setIsOpen(false); return; } - // ------------------------ if (user) { - // 1. Update Profile Table const { error: dbError } = await supabase .from('profiles') .upsert({ @@ -133,7 +125,6 @@ export const ProfileCompleter: React.FC = () => { if (dbError) throw dbError; - // 2. Update Auth Metadata (optional, but good for consistency) await supabase.auth.updateUser({ data: { first_name: firstName, @@ -143,8 +134,6 @@ export const ProfileCompleter: React.FC = () => { }); setIsOpen(false); - // We do not reload here to avoid infinite loops if checks fail on reload. - // The state close is enough. } } catch (err: any) { console.error('Error updating profile:', err); @@ -164,9 +153,7 @@ export const ProfileCompleter: React.FC = () => {

Hiányzó Adatok

-

- Kérjük, a folytatáshoz adja meg a hiányzó adatait. -

+

Kérjük, a folytatáshoz adja meg a hiányzó adatait.

@@ -177,71 +164,33 @@ export const ProfileCompleter: React.FC = () => { {error}
)} -
- - setLastName(e.target.value)} - className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all bg-white text-gray-900" - placeholder="Kovács" - required - /> + + setLastName(e.target.value)} className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all bg-white text-gray-900" placeholder="Kovács" required />
-
- - setFirstName(e.target.value)} - className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all bg-white text-gray-900" - placeholder="János" - required - /> + + setFirstName(e.target.value)} className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all bg-white text-gray-900" placeholder="János" required />
-
- +
- setDateOfBirth(e.target.value)} - className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all bg-white text-gray-900" - required - /> + setDateOfBirth(e.target.value)} className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all bg-white text-gray-900" required />
-
- -

- Ezekre az adatokra a számlázáshoz és a kapcsolattartáshoz van szükségünk. -

+

Ezekre az adatokra a számlázáshoz és a kapcsolattartáshoz van szükségünk.

diff --git a/index.html b/index.html index 9e43ed4..dff3402 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Motion Web Stúdió - Professzionális Webfejlesztés diff --git a/lib/defaultPlans.ts b/lib/defaultPlans.ts index 95a0d44..7dee9f4 100644 --- a/lib/defaultPlans.ts +++ b/lib/defaultPlans.ts @@ -4,7 +4,9 @@ export const defaultPlans: ProductPackage[] = [ { id: 'start', name: "Landing Page", - price: "190.000 Ft-tól", + price: "190.000 Ft", + total_price: 190000, + advance_price: 40000, desc: "Ideális induló vállalkozásoknak és személyes brandeknek.", features: [ "Egyoldalas Landing Page", @@ -20,7 +22,9 @@ export const defaultPlans: ProductPackage[] = [ { id: 'pro', name: "Pro Web", - price: "350.000 Ft-tól", + price: "350.000 Ft", + total_price: 350000, + advance_price: 80000, desc: "A legnépszerűbb választás kis- és középvállalkozások számára.", features: [ "Max 5 aloldal (Rólunk, Szolgáltatások...)", @@ -38,6 +42,9 @@ export const defaultPlans: ProductPackage[] = [ id: 'enterprise', name: "Enterprise", price: "Egyedi árazás", + total_price: 0, + advance_price: 0, + is_custom_price: true, desc: "Komplex rendszerek és webáruházak nagyvállalati igényekre.", features: [ "Korlátlan aloldalak", diff --git a/pages/Admin.tsx b/pages/Admin.tsx index 1e43abe..abf21f7 100644 --- a/pages/Admin.tsx +++ b/pages/Admin.tsx @@ -1,17 +1,21 @@ -import React, { useState, useEffect } from 'react'; + +import React, { useState, useEffect, useRef } from 'react'; import { useAuth } from '../context/AuthContext'; import { useNavigate } from 'react-router-dom'; import { ChevronLeft, RefreshCw, BarChart2, Users, ShoppingCart, Package, FileText, Globe, Sparkles, MessageSquare, AlertCircle, CheckCircle, XCircle, Clock, Activity, Save, Mail, Rocket, Edit3, Bell, - AlertTriangle, Archive, Send, Layout, History + AlertTriangle, Archive, Send, Layout, History, MessageCircle, Info, ChevronDown, ChevronUp, + Upload, FileDown, Receipt, CreditCard, DollarSign, Plus, Trash2, Building2, User as UserIcon, + Palette, Zap, Lightbulb, Link as LinkIcon, ExternalLink, Target, FileStack, Star, Check, + Copy, Cpu, Wand2, Eye, EyeOff, ShieldCheck, Calendar, Search, ArrowUpDown, Filter } from 'lucide-react'; import { Button } from '../components/Button'; import { supabase, isSupabaseConfigured } from '../lib/supabaseClient'; -import { ProductPackage } from '../types'; +import { ProductPackage, MaintenanceSubscription } from '../types'; import { defaultPlans } from '../lib/defaultPlans'; -import { generateOrderPrompts, PromptResult } from '../lib/promptGenerator'; +import { GoogleGenAI } from "@google/genai"; interface AdminUser { id: string; @@ -35,6 +39,14 @@ interface EmailLogEntry { body?: string; } +interface Bill { + id: string; + order_id: string; + type: 'advance' | 'final'; + file_url: string; + created_at: string; +} + interface AdminOrder { id: string; user_id: string; @@ -48,9 +60,39 @@ interface AdminOrder { details?: any; history?: OrderHistoryEntry[]; emailLogs?: EmailLogEntry[]; + bills?: Bill[]; } -const getEmailTemplate = (type: string, data: { customer: string, package: string, demoUrl?: string }) => { +// Extended interface for Subscription to include joined order data +interface ExtendedSubscription extends MaintenanceSubscription { + order?: { + id: string; + amount: string; + customer_name?: string; + customer_email: string; + details?: { + domainName?: string; + company?: string; + }; + }; +} + +interface AIPrompt { + title: string; + content: string; +} + +const getEmailTemplate = (type: string, data: { + customer: string, + package: string, + demoUrl?: string, + billUrl?: string, + billType?: string, + domain?: string, + daysLeft?: number, + paymentLink?: string, + amount?: string +}) => { const baseStyle = "font-family: 'Inter', Helvetica, Arial, sans-serif; line-height: 1.6; color: #1a1a1a; max-width: 600px; margin: 0 auto; padding: 40px 20px; border: 1px solid #f0f0f0; border-radius: 24px; background-color: #ffffff;"; const headerStyle = "color: #7c3aed; font-size: 28px; font-weight: 800; margin-bottom: 24px; letter-spacing: -0.02em; border-bottom: 1px solid #f0f0f0; padding-bottom: 20px;"; const footerStyle = "margin-top: 40px; padding-top: 24px; border-top: 1px solid #f0f0f0; font-size: 13px; color: #94a3b8;"; @@ -58,107 +100,124 @@ const getEmailTemplate = (type: string, data: { customer: string, package: strin const templates: Record = { 'Fejlesztés megkezdése': { - subject: `Projekt Indítása: Megkezdtük a fejlesztést - ${data.package}`, + subject: `Projekt Indítása: Megkezdtem a fejlesztést - ${data.package}`, body: `
MotionWeb

Kedves ${data.customer}!

-

Örömmel értesítjük, hogy a ${data.package} csomagjához tartozó fejlesztési folyamat a mai napon hivatalosan is elindult.

-

Szakembereink elkezdték a weboldal vázlatának és design elemeinek kidolgozását. Amint elkészülünk az első megtekinthető demó verzióval, azonnal jelentkezünk a hozzáférési linkkel.

-

Köszönjük a bizalmát!

-
- Üdvözlettel,
- Balogh Bence
- MotionWeb Stúdió | motionweb.hu -
+

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

+

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

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

Kedves ${data.customer}!

-

Jó hírünk van: elkészültünk weboldala első, interaktív bemutató verziójával!

-

A demó oldalt az alábbi biztonságos linken tudja megtekinteni:

- -

Kérjük, nézze át az oldalt, és a MotionWeb ügyfélkapujában a Rendeléseim menüpont alatt küldje el visszajelzését, hogy rögzíthessük az esetleges módosítási kéréseit.

-
- Üdvözlettel,
- Balogh Bence
- MotionWeb Stúdió | motionweb.hu -
+

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

+ +

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

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

Kedves ${data.customer}!

+

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

+

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

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

Kedves ${data.customer}!

+

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

+

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

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

Kedves ${data.customer}!

+

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

+

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

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

Kedves ${data.customer}!

-

Köszönjük részletes visszajelzését! A kért módosításokat rögzítettük és szakembereink már dolgoznak azok átvezetésén.

-

Amint a frissített verzió megtekinthető lesz, e-mailben ismét értesíteni fogjuk.

-
- Üdvözlettel,
- Balogh Bence
- MotionWeb Stúdió | motionweb.hu -
+

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

+

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

+
Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, - '1 hete nincs visszajelzés': { - subject: `Emlékeztető: Visszajelzés várható a demó oldallal kapcsolatban`, + 'Weboldal élesítése megkezdődött': { + subject: `Folyamatban: Megkezdődött a weboldal élesítése`, body: `
MotionWeb

Kedves ${data.customer}!

-

Szeretnénk emlékeztetni, hogy egy héttel ezelőtt küldtük el Önnek a weboldal demó verzióját.

-

Annak érdekében, hogy tartsuk a fejlesztési ütemtervet, kérjük, amint ideje engedi, nézze át a tervet és küldje el véleményét az ügyfélkapun keresztül.

-
- Üdvözlettel,
- Balogh Bence
- MotionWeb Stúdió | motionweb.hu -
+

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

+

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

+
Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, - '2 hete nincs visszajelzés': { - subject: `Fontos: Továbblépéshez szükséges visszajelzés hiánya`, + 'Élesített weboldal elkészült': { + subject: `Gratulálunk! Weboldala sikeresen élesítve lett`, body: `
MotionWeb

Kedves ${data.customer}!

-

Szeretnénk felhívni figyelmét, hogy weboldala demó verziója már két hete várakozik az Ön jóváhagyására.

-

Amennyiben elégedett a látottakkal, kérjük, jelezze azt a felületen a véglegesítés megkezdéséhez. Bármilyen kérdés vagy észrevétel esetén állunk rendelkezésére.

-
- Üdvözlettel,
- Balogh Bence
- MotionWeb Stúdió | motionweb.hu -
+

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

+

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

+ +
Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, - 'Projekt lezárva (Nincs válasz)': { - subject: `Értesítés: Projekt adminisztratív lezárása - ${data.package}`, + 'Előlegszámla elkészült': { + subject: `Előlegszámla a ${data.package} projekthez`, body: `
MotionWeb

Kedves ${data.customer}!

-

Sajnálattal értesítjük, hogy mivel az elmúlt időszakban többszöri megkeresésünkre sem érkezett válasz, a ${data.package} projektet a mai napon adminisztratív okokból lezártnak tekintjük.

-

Amennyiben a jövőben folytatni szeretné a fejlesztést, kérjük, vegye fel velünk a kapcsolatot egy új ütemterv egyeztetése érdekében.

-
- Üdvözlettel,
- Balogh Bence
- MotionWeb Stúdió | motionweb.hu -
+

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

+

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

+ +

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

+
Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` }, - 'Végleges oldal elérhető': { - subject: `Gratulálunk! Weboldala elkészült és élesítésre került`, + 'Végszámla elkészült': { + subject: `Végszámla a ${data.package} projekthez`, body: `
MotionWeb

Kedves ${data.customer}!

-

Örömmel jelentjük, hogy a ${data.package} projektünk sikeresen befejeződött!

-

A weboldalt élesítettük, így az mostantól minden látogató számára elérhető a végleges domain címen.

-

Köszönjük, hogy minket választott digitális partnerének. Kérjük, amennyiben elégedett volt a munkánkkal, ajánljon minket ismerőseinek is!

-
- Üdvözlettel,
- Balogh Bence
- MotionWeb Stúdió | motionweb.hu -
+

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

+

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

+ +

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

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

Kedves ${data.customer}!

+

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

+

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

+ +

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

+

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

+
Üdvözlettel,
Balogh Bence Benedek
MotionWeb | motionweb.hu
` } }; @@ -169,10 +228,11 @@ const getEmailTemplate = (type: string, data: { customer: string, package: strin export const Admin: React.FC = () => { const { user, isAdmin, loading } = useAuth(); const navigate = useNavigate(); - const [activeTab, setActiveTab] = useState<'overview' | 'users' | 'orders' | 'plans'>('overview'); + const [activeTab, setActiveTab] = useState<'overview' | 'users' | 'orders' | 'plans' | 'subscriptions'>('overview'); const [users, setUsers] = useState([]); const [orders, setOrders] = useState([]); const [plans, setPlans] = useState([]); + const [subscriptions, setSubscriptions] = useState([]); const [loadingData, setLoadingData] = useState(false); const [visitorStats, setVisitorStats] = useState({ month: 0, week: 0 }); const [statusUpdating, setStatusUpdating] = useState(null); @@ -180,11 +240,22 @@ export const Admin: React.FC = () => { const [errorMsg, setErrorMsg] = useState(null); const [viewOrder, setViewOrder] = useState(null); - const [generatedPrompts, setGeneratedPrompts] = useState([]); - const [copiedIndex, setCopiedIndex] = useState(null); + const [generatedPrompts, setGeneratedPrompts] = useState([]); + const [generatingPrompts, setGeneratingPrompts] = useState(false); + const [expandedLogId, setExpandedLogId] = useState(null); const [demoUrlInput, setDemoUrlInput] = useState(''); const [savingDemoUrl, setSavingDemoUrl] = useState(false); const [emailSending, setEmailSending] = useState(null); + const [checkingSubs, setCheckingSubs] = useState(false); + const [manualNotifying, setManualNotifying] = useState(null); + + // Filter & Sort State for Subscriptions + const [subSearch, setSubSearch] = useState(''); + const [subSort, setSubSort] = useState<'asc' | 'desc'>('asc'); + + const advanceBillInput = useRef(null); + const finalBillInput = useRef(null); + const [billUploading, setBillUploading] = useState(null); useEffect(() => { if (!loading && !isAdmin) { @@ -236,8 +307,36 @@ export const Admin: React.FC = () => { email: o.customer_email }))); - const { data: plData } = await supabase.from('plans').select('*').order('price', { ascending: true }); - setPlans(plData && plData.length > 0 ? plData : defaultPlans); + const { data: plData } = await supabase.from('plans').select('*'); + if (plData && plData.length > 0) { + const sortedPlans = [...plData].sort((a, b) => { + const priceA = a.is_custom_price ? Infinity : (a.total_price || 0); + const priceB = b.is_custom_price ? Infinity : (b.total_price || 0); + return priceA - priceB; + }); + setPlans(sortedPlans); + } else { + setPlans(defaultPlans); + } + + // Updated fetch for subscriptions to include Order details + const { data: subsData } = await supabase + .from('maintenance_subscriptions') + .select(` + *, + order:orders ( + id, + amount, + customer_name, + customer_email, + details + ) + `) + .order('next_billing_date', { ascending: true }); + + if (subsData) { + setSubscriptions(subsData as ExtendedSubscription[]); + } const { count: mCount } = await supabase.from('page_visits').select('*', { count: 'exact', head: true }); setVisitorStats({ month: mCount || 0, week: Math.floor((mCount || 0) / 4) }); @@ -266,20 +365,28 @@ export const Admin: React.FC = () => { const fetchEmailLogs = async (orderId: string) => { if (!isSupabaseConfigured) return []; try { - // Megpróbáljuk lekérni, de ha hiányzik oszlop vagy tábla, hibatűrően kezeljük const { data, error } = await supabase .from('email_log') .select('*') .eq('order_id', orderId) .order('sent_at', { ascending: false }); - - if (error) { - console.error("Email log fetch error:", error.message); - return []; - } return data || []; } catch (e) { - console.warn("Could not fetch email_log table."); + return []; + } + }; + + const fetchBills = async (orderId: string) => { + if (!isSupabaseConfigured) return []; + try { + const { data, error } = await supabase + .from('bills') + .select('*') + .eq('order_id', orderId) + .order('created_at', { ascending: false }); + if (error) throw error; + return data || []; + } catch (e) { return []; } }; @@ -288,26 +395,113 @@ export const Admin: React.FC = () => { if (statusUpdating) return; setStatusUpdating(orderId); + // Find order in current state + const currentOrder = orders.find(o => o.id === orderId); + if (!currentOrder) { + setStatusUpdating(null); + return; + } + try { if (isSupabaseConfigured) { - const { error } = await supabase.from('orders').update({ status: newStatus }).eq('id', orderId); - if (error) throw error; + // 1. Database Update (Status) + const { error: updateError } = await supabase + .from('orders') + .update({ status: newStatus }) + .eq('id', orderId); + + if (updateError) throw updateError; - await supabase.from('order_status_history').insert({ order_id: orderId, status: newStatus }); - const newHist = await fetchOrderHistory(orderId); + // 2. Log History + const { error: historyError } = await supabase.from('order_status_history').insert({ + order_id: orderId, + status: newStatus + }); - setOrders(prev => prev.map(o => o.id === orderId ? { ...o, status: newStatus } : o)); - if (viewOrder && viewOrder.id === orderId) { - setViewOrder({ ...viewOrder, status: newStatus, history: newHist }); + if (historyError) console.warn("Failed to log status history:", historyError); + + // 3. Subscription Logic (Only for 'completed') + if (newStatus === 'completed') { + try { + // Wait a bit to ensure trigger consistency if any + await new Promise(r => setTimeout(r, 500)); + + // Check existing + const { data: existing } = await supabase + .from('maintenance_subscriptions') + .select('id') + .eq('order_id', orderId) + .maybeSingle(); + + if (!existing) { + const startDate = new Date(); + const nextBilling = new Date(); + nextBilling.setFullYear(nextBilling.getFullYear() + 1); + + // Valid email check + const clientEmail = currentOrder.email || (currentOrder as any).customer_email || 'nincs_email@megadva.hu'; + + const subData: any = { + order_id: orderId, + client_email: clientEmail, + start_date: startDate.toISOString(), + next_billing_date: nextBilling.toISOString(), + status: 'active' + }; + + // Try adding user_id if present + if (currentOrder.user_id) { + subData.user_id = currentOrder.user_id; + } + + const { error: subError } = await supabase + .from('maintenance_subscriptions') + .insert(subData); + + if (subError) { + console.error('Subscription creation failed:', subError); + const subMsg = subError?.message || JSON.stringify(subError); + alert(`Figyelem: Státusz frissítve, de az előfizetés létrehozása sikertelen: ${subMsg}`); + } else { + alert('Sikeres átadás! Az 1 éves karbantartási időszak elindult, az előfizetés létrejött.'); + fetchAdminData(); + } + } + } catch (err: any) { + console.error('Subscription logic error:', err); + } } + + // 4. Update UI State + const updatedOrder = { ...currentOrder, status: newStatus }; + setOrders(prev => prev.map(o => o.id === orderId ? updatedOrder : o)); + + if (viewOrder && viewOrder.id === orderId) { + const newHist = await fetchOrderHistory(orderId); + setViewOrder(prev => prev ? { ...prev, status: newStatus, history: newHist } : null); + } + } else { + // Demo Mode setOrders(prev => prev.map(o => o.id === orderId ? { ...o, status: newStatus } : o)); if (viewOrder && viewOrder.id === orderId) { - setViewOrder({ ...viewOrder, status: newStatus }); + setViewOrder(prev => prev ? { ...prev, status: newStatus } : null); } + if (newStatus === 'completed') alert('Demo: Státusz kész, előfizetés szimulálva.'); } } catch (e: any) { - alert("Hiba: " + e.message); + console.error("Status update failed:", e); + let errorMessage = "Ismeretlen hiba"; + try { + if (typeof e === 'string') errorMessage = e; + else if (e instanceof Error) errorMessage = e.message; + else if (typeof e === 'object' && e !== null) { + errorMessage = e.message || e.error_description || JSON.stringify(e); + } + } catch (jsonErr) { + errorMessage = "Nem szerializálható hiba objektum"; + } + alert("Hiba történt a státusz frissítésekor: " + errorMessage); } finally { setStatusUpdating(null); } @@ -337,13 +531,10 @@ export const Admin: React.FC = () => { setViewOrder({ ...viewOrder, status: targetStatus, details: updatedDetails, history: newHist }); if (demoUrlInput.startsWith('http')) { - handleSendEmail('Demó oldal elkészült'); + handleSendEmail('Demo elkészült'); } alert(`Sikeres mentés és visszajelzés kérése!`); - } else { - setViewOrder({ ...viewOrder, details: updatedDetails }); - alert("Demo URL mentve."); } } catch (e: any) { alert("Hiba: " + e.message); @@ -356,73 +547,268 @@ export const Admin: React.FC = () => { setLoadingData(true); const history = await fetchOrderHistory(order.id); const emailLogs = await fetchEmailLogs(order.id); - setViewOrder({ ...order, history, emailLogs }); + const bills = await fetchBills(order.id); + setViewOrder({ ...order, history, emailLogs, bills }); setDemoUrlInput(order.details?.demoUrl || ''); setGeneratedPrompts([]); setLoadingData(false); }; - const handleSendEmail = async (emailType: string) => { + const handleCheckSubscriptions = async () => { + if (checkingSubs) return; + setCheckingSubs(true); + try { + const { data, error } = await supabase.functions.invoke('check-subscriptions'); + if (error) throw error; + + const count = data?.notifications?.length || 0; + alert(`Ellenőrzés sikeres! ${count} db értesítés kiküldve (mock).`); + await fetchAdminData(); // Refresh list just in case + } catch (e: any) { + alert("Hiba a futtatáskor: " + e.message); + } finally { + setCheckingSubs(false); + } + }; + + const handleManualNotification = async (sub: ExtendedSubscription) => { + if (manualNotifying) return; + setManualNotifying(sub.id); + + try { + if (!isSupabaseConfigured) { + await new Promise(r => setTimeout(r, 1000)); + alert("Demo: E-mail elküldve."); + setManualNotifying(null); + return; + } + + const daysLeft = getDaysRemaining(sub.next_billing_date); + const emailType = 'Előfizetés emlékeztető'; + const nowIso = new Date().toISOString(); + + // Placeholder payment link + const paymentLink = "https://stripe.com/payment_placeholder"; + + const orderData = sub.order as any; // Cast to avoid TS issues if types aren't perfect + const customerName = orderData?.customer_name || 'Ügyfél'; + const customerEmail = sub.client_email || orderData?.customer_email; + const domain = orderData?.details?.domainName; + const amount = "59 990 Ft"; // Or use orderData.amount if applicable + + const template = getEmailTemplate(emailType, { + customer: customerName, + package: 'Fenntartás', // Dummy package name for template type signature + domain: domain, + daysLeft: daysLeft, + paymentLink: paymentLink, + amount: amount + } as any); + + // 1. Send Email + const { error: sendError } = await supabase.functions.invoke('resend', { + body: { + to: customerEmail, + subject: template.subject, + html: template.body, + } + }); + + if (sendError) throw sendError; + + // 2. Log to Database + const orderId = sub.order_id || sub.order?.id; + if (orderId) { + const { error: logError } = await supabase.from('email_log').insert({ + order_id: orderId, + email_type: emailType, + body: template.body, + sent_at: nowIso + }); + if (logError) console.error("Error logging email:", logError); + } else { + console.warn("Manual notification sent, but could not log to history: Missing Order ID."); + } + + // 3. Update last_notified_at in Database + const { error: updateError } = await supabase.from('maintenance_subscriptions') + .update({ last_notified_at: nowIso }) + .eq('id', sub.id); + + if (updateError) console.error("Error updating last_notified_at:", updateError); + + // 4. Update local state immediately + setSubscriptions(prev => prev.map(s => { + if (s.id === sub.id) { + return { ...s, last_notified_at: nowIso }; + } + return s; + })); + + // Update local state if we are currently viewing this order (for log view) + if (viewOrder && orderId && viewOrder.id === orderId) { + // Small delay to ensure DB write consistency before fetch + await new Promise(r => setTimeout(r, 500)); + const newLogs = await fetchEmailLogs(orderId); + setViewOrder(prev => prev ? { ...prev, emailLogs: newLogs } : null); + } + + alert(`Sikeresen elküldve és naplózva: ${emailType}`); + + } catch (e: any) { + console.error("Manual notification error:", e); + let msg = "Ismeretlen hiba"; + if (typeof e === 'string') msg = e; + else if (e instanceof Error) msg = e.message; + else if (typeof e === 'object') msg = JSON.stringify(e); + + alert("Hiba a küldéskor: " + msg); + } finally { + setManualNotifying(null); + } + }; + + const getDaysRemaining = (targetDate: string) => { + const target = new Date(targetDate); + const now = new Date(); + const diffTime = target.getTime() - now.getTime(); + return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + }; + + // Filter and Sort Subscriptions + const filteredSubscriptions = subscriptions + .filter(sub => { + const domain = sub.order?.details?.domainName?.toLowerCase() || ''; + const email = sub.client_email?.toLowerCase() || ''; + const search = subSearch.toLowerCase(); + return domain.includes(search) || email.includes(search); + }) + .sort((a, b) => { + const dateA = new Date(a.next_billing_date).getTime(); + const dateB = new Date(b.next_billing_date).getTime(); + return subSort === 'asc' ? dateA - dateB : dateB - dateA; + }); + + const handleGenerateAIPrompts = async () => { + if (!viewOrder || generatingPrompts) return; + setGeneratingPrompts(true); + try { + const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); + const details = viewOrder.details || {}; + + const prompt = ` + Készíts részletes technikai promptokat egy webfejlesztő AI számára az alábbi rendelési adatok alapján. + A cél egy professzionális React weboldal fejlesztése. + + ADATOK: + - Ügyfél: ${viewOrder.customer} + - Csomag: ${viewOrder.package} + - Bemutatkozás: ${details.description} + - Színek: ${details.primaryColor}, ${details.secondaryColor}, ${details.balanceColor} + - Stílus: ${details.style?.join(', ')} + - Funkciók: ${details.features?.join(', ')} + - Aloldalak: ${details.content?.join(', ')} + + Kérlek adj 3-4 különálló promptot (pl. Főoldal, Strukturális felépítés, Specifikus funkciók). + A válasz JSON formátumban legyen: [{"title": "Prompt címe", "content": "Részletes prompt szövege"}] + `; + + const response = await ai.models.generateContent({ + model: 'gemini-3-pro-preview', + contents: prompt, + config: { + responseMimeType: "application/json", + thinkingConfig: { thinkingBudget: 10000 } + } + }); + + const result = JSON.parse(response.text || "[]"); + setGeneratedPrompts(result); + } catch (e: any) { + alert("AI hiba: " + e.message); + } finally { + setGeneratingPrompts(false); + } + }; + + const handleBillUpload = async (type: 'advance' | 'final', e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file || !viewOrder) return; + + setBillUploading(type); + try { + const fileExt = file.name.split('.').pop(); + const fileName = `${viewOrder.id}_${type}_${Date.now()}.${fileExt}`; + const filePath = `invoices/${fileName}`; + + const { error: uploadError } = await supabase.storage + .from('invoices') + .upload(filePath, file); + + if (uploadError) throw uploadError; + + const { data: { publicUrl } } = supabase.storage + .from('invoices') + .getPublicUrl(filePath); + + const { error: dbError } = await supabase + .from('bills') + .insert({ + order_id: viewOrder.id, + type: type, + file_url: publicUrl + }); + + if (dbError) throw dbError; + + const newBills = await fetchBills(viewOrder.id); + setViewOrder(prev => prev ? { ...prev, bills: newBills } : null); + alert('Számla sikeresen feltöltve!'); + + } catch (err: any) { + alert('Hiba a feltöltéskor: ' + err.message); + } finally { + setBillUploading(null); + } + }; + + const handleSendEmail = async (emailType: string, extraData: any = {}) => { if (!viewOrder || emailSending) return; + setEmailSending(emailType); try { const template = getEmailTemplate(emailType, { customer: viewOrder.customer, package: viewOrder.package, - demoUrl: demoUrlInput || viewOrder.details?.demoUrl + demoUrl: demoUrlInput || viewOrder.details?.demoUrl, + ...extraData }); if (isSupabaseConfigured) { - // MEGHÍVJUK A FÜGGVÉNYT - const response = await supabase.functions.invoke('resend', { + const { error: sendError } = await supabase.functions.invoke('resend', { body: { to: viewOrder.email, subject: template.subject, - html: template.body + html: template.body, } }); - // Supabase hiba kezelése (az invoke hiba objektumot ad vissza, ha a hívás sikertelen) - if (response.error) { - let errorDetail = response.error.message; - if (errorDetail.includes('API kulcs hiányzik')) { - throw new Error("HIÁNYZIK A BEÁLLÍTÁS: A Supabase Dashboardon az Edge Functions -> Secrets menüpontban fel kell venni a RESEND_API_KEY kulcsot!"); - } - throw new Error(errorDetail); - } + if (sendError) throw sendError; - // A függvény által visszaadott egyedi hiba (ha van) - if (response.data && response.data.error) { - throw new Error(response.data.error); - } - - // SIKERES KÜLDÉS NAPLÓZÁSA (Hibatűrő módon) - try { - const { error: logError } = await supabase.from('email_log').insert({ - order_id: viewOrder.id, - email_type: emailType, - body: template.body - }); - - if (logError) { - console.error("Adatbázis naplózási hiba (lehet, hogy hiányzik a 'body' oszlop):", logError.message); - // Itt nem dobunk hibát, mert az email már kiment! Csak logolunk a konzolra. - } - } catch (dbErr) { - console.error("Váratlan hiba az adatbázisba íráskor:", dbErr); - } + await supabase.from('email_log').insert({ + order_id: viewOrder.id, + email_type: emailType, + body: template.body + }); const newLogs = await fetchEmailLogs(viewOrder.id); - setViewOrder({ ...viewOrder, emailLogs: newLogs }); + setViewOrder(prev => prev ? { ...prev, emailLogs: newLogs } : null); - alert(`Sikeres küldés! Az értesítőt kiküldtük ${viewOrder.email} címre.`); - } else { - alert(`DEMO MÓD: Tárgy: ${template.subject}`); + alert(`Sikeres küldés: ${emailType}`); } } catch (e: any) { - console.error("Hiba az e-mail folyamatban:", e); - alert("Hiba az e-mail küldésekor:\n\n" + e.message); + alert("Hiba: " + e.message); } finally { setEmailSending(null); } @@ -440,8 +826,75 @@ export const Admin: React.FC = () => { return {c.label}; }; - if (loading) return
Admin felület...
; - if (!isAdmin) return null; + const BillSection = ({ type, label }: { type: 'advance' | 'final', label: string }) => { + const bill = viewOrder?.bills?.find(b => b.type === type); + const isUploading = billUploading === type; + const templateName = type === 'advance' ? 'Előlegszámla elkészült' : 'Végszámla elkészült'; + const isThisEmailSending = emailSending === templateName; + + return ( +
+
+
+ {type === 'advance' ? : } +
+

{label}

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

Kattints a feltöltéshez

+
+ ) : ( +
+
+
+ +

Számla file

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

Vezérlőpult

+

Vezérlőpult

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

Látogatók

-

{visitorStats.month}

+

{visitorStats.month}

-
+

Rendelések

-

{orders.length}

+

{orders.length}

-
-

Regisztrációk

-

{users.length}

+
+

Aktív Előfizetések

+

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

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

{o.customer}

-

{o.email}

-
{o.package} - -
+
+
+ + + + + + {orders.map(o => ( + + + + + + + ))} + +
ÜgyfélCsomagStátuszMűvelet
+

{o.customer}

+

{o.email}

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

+ Karbantartás Monitor +

+

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

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