mirror of
https://github.com/Motion-Games/MotionWebStudio.git
synced 2026-04-21 17:10:54 +02:00
395 lines
19 KiB
TypeScript
395 lines
19 KiB
TypeScript
|
|
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 {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (feedbackData: any) => Promise<void>;
|
|
loading: boolean;
|
|
order?: any;
|
|
}
|
|
|
|
export const FeedbackModal: React.FC<FeedbackModalProps> = ({ 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);
|
|
|
|
// Revision Flow
|
|
const [designCheckboxes, setDesignCheckboxes] = useState<string[]>([]);
|
|
const [designText, setDesignText] = useState('');
|
|
|
|
const [contentCheckboxes, setContentCheckboxes] = useState<string[]>([]);
|
|
const [contentText, setContentText] = useState('');
|
|
|
|
const [structureCheckboxes, setStructureCheckboxes] = useState<string[]>([]);
|
|
const [structureText, setStructureText] = useState('');
|
|
|
|
const [funcCheckboxes, setFuncCheckboxes] = useState<string[]>([]);
|
|
const [funcText, setFuncText] = useState('');
|
|
|
|
const [priorityText, setPriorityText] = useState('');
|
|
|
|
const [deadlineType, setDeadlineType] = useState<'none' | 'date' | 'discuss'>('none');
|
|
const [deadlineDate, setDeadlineDate] = useState('');
|
|
|
|
const [extraNotes, setExtraNotes] = useState('');
|
|
|
|
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[],
|
|
setList: React.Dispatch<React.SetStateAction<string[]>>,
|
|
value: string,
|
|
noneValue: string
|
|
) => {
|
|
if (value === noneValue) {
|
|
if (currentList.includes(noneValue)) {
|
|
setList([]);
|
|
} else {
|
|
setList([noneValue]);
|
|
}
|
|
} else {
|
|
let newList = currentList.filter(item => item !== noneValue);
|
|
if (newList.includes(value)) {
|
|
newList = newList.filter(item => item !== value);
|
|
} else {
|
|
newList = [...newList, value];
|
|
}
|
|
setList(newList);
|
|
}
|
|
};
|
|
|
|
const isRevision = mainDecision === 'minor' || mainDecision === 'major';
|
|
|
|
const handleSubmit = () => {
|
|
const feedbackData = {
|
|
decision: mainDecision,
|
|
submittedAt: new Date().toISOString(),
|
|
approval: mainDecision === 'approved' ? {
|
|
confirmed: approvalConfirmed,
|
|
paymentComplete: showPayment // Flag that payment step was shown
|
|
} : null,
|
|
revision: isRevision ? {
|
|
design: { selected: designCheckboxes, comment: designText },
|
|
content: { selected: contentCheckboxes, comment: contentText },
|
|
structure: { selected: structureCheckboxes, comment: structureText },
|
|
functionality: { selected: funcCheckboxes, comment: funcText },
|
|
priority: priorityText,
|
|
deadline: { type: deadlineType, date: deadlineDate },
|
|
extraNotes: extraNotes,
|
|
confirmed: revisionConfirmed
|
|
} : null
|
|
};
|
|
|
|
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,
|
|
options: string[],
|
|
selected: string[],
|
|
setSelected: React.Dispatch<React.SetStateAction<string[]>>,
|
|
textValue: string,
|
|
setTextValue: React.Dispatch<React.SetStateAction<string>>,
|
|
placeholder: string
|
|
) => {
|
|
const noneOption = options[options.length - 1];
|
|
const showTextarea = selected.length > 0 && !selected.includes(noneOption);
|
|
|
|
return (
|
|
<div className="mb-6 bg-gray-50/50 p-6 rounded-2xl border border-gray-100 shadow-sm">
|
|
<h4 className="font-bold text-gray-800 mb-4 text-sm uppercase tracking-widest">{title}</h4>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
|
|
{options.map(opt => (
|
|
<label key={opt} className={`flex items-center space-x-3 p-3 rounded-xl cursor-pointer border-2 transition-all ${selected.includes(opt) ? 'bg-white border-primary shadow-md' : 'bg-white/50 border-transparent hover:border-gray-200'}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={selected.includes(opt)}
|
|
onChange={() => handleCheckbox(selected, setSelected, opt, noneOption)}
|
|
className="rounded text-primary focus:ring-primary w-5 h-5 border-gray-300"
|
|
/>
|
|
<span className={`text-sm font-semibold ${selected.includes(opt) ? 'text-primary' : 'text-gray-600'}`}>{opt}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
{showTextarea && (
|
|
<textarea
|
|
value={textValue}
|
|
onChange={(e) => setTextValue(e.target.value)}
|
|
className={`${commonInputStyles} animate-fade-in mt-2`}
|
|
rows={3}
|
|
placeholder={placeholder}
|
|
></textarea>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-gray-900/75 backdrop-blur-sm overflow-y-auto">
|
|
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-3xl flex flex-col my-8 relative animate-fade-in-up border border-white/20 min-h-[600px]">
|
|
|
|
<button onClick={onClose} className="absolute top-6 right-6 p-2 hover:bg-gray-100 rounded-full transition-colors z-10 bg-white shadow-sm border border-gray-100">
|
|
<X className="w-5 h-5 text-gray-500" />
|
|
</button>
|
|
|
|
{showPayment ? (
|
|
/* PAYMENT SUMMARY VIEW */
|
|
<div className="flex flex-col h-full animate-fade-in">
|
|
<div className="p-8 border-b border-gray-100 bg-gradient-to-r from-blue-50/50 to-purple-50/50 rounded-t-3xl">
|
|
<div className="flex items-center gap-4">
|
|
<button onClick={() => setShowPayment(false)} className="p-2 bg-white rounded-xl shadow-sm border border-gray-100 hover:bg-gray-50 transition-colors text-gray-600">
|
|
<ArrowLeft className="w-5 h-5" />
|
|
</button>
|
|
<div>
|
|
<h2 className="text-2xl font-black text-gray-900 tracking-tighter">Véglegesítés & Fizetés</h2>
|
|
<p className="text-sm text-gray-500 font-medium mt-1">A fennmaradó összeg rendezése a projekt lezárásához.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-8 md:p-12 flex-grow space-y-10">
|
|
<div className="text-center">
|
|
<div className="w-20 h-20 bg-green-50 rounded-3xl flex items-center justify-center mx-auto mb-6 text-green-600 shadow-sm border border-green-100">
|
|
<Wallet className="w-10 h-10" />
|
|
</div>
|
|
<h3 className="text-xl font-bold text-gray-900 mb-2">Projekt Elszámolás</h3>
|
|
<p className="text-gray-500 text-sm max-w-md mx-auto">A demó oldal jóváhagyásával a projekt a befejező szakaszba lép. Kérjük, egyenlítse ki a fennmaradó összeget.</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="bg-gray-50 p-6 rounded-[24px] border border-gray-100 flex flex-col items-center text-center">
|
|
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2">Projekt Teljes Ára</p>
|
|
<p className="text-lg font-bold text-gray-900">{formatPrice(totalAmount)}</p>
|
|
</div>
|
|
<div className="bg-green-50/50 p-6 rounded-[24px] border border-green-100 flex flex-col items-center text-center">
|
|
<p className="text-[10px] font-black text-green-600 uppercase tracking-widest mb-2">Már Befizetve (Előleg)</p>
|
|
<p className="text-lg font-bold text-green-700">{formatPrice(advanceAmount)}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-primary/5 p-10 rounded-[32px] border-2 border-primary/20 text-center shadow-xl shadow-primary/5 relative overflow-hidden">
|
|
<div className="absolute -top-10 -right-10 w-40 h-40 bg-primary/10 rounded-full blur-3xl"></div>
|
|
<p className="text-[11px] font-black text-primary uppercase tracking-[0.3em] mb-4">Most Fizetendő (Fennmaradó)</p>
|
|
<div className="text-4xl md:text-5xl font-black text-primary mb-6 tracking-tighter">
|
|
{formatPrice(remainingAmount)}
|
|
</div>
|
|
<div className="flex justify-center">
|
|
<Button
|
|
onClick={handleSubmit}
|
|
disabled={loading}
|
|
className="font-black uppercase text-[10px] tracking-widest px-12 py-4 h-auto shadow-2xl shadow-primary/40"
|
|
>
|
|
{loading ? <RefreshCw className="w-4 h-4 animate-spin mr-2" /> : <CreditCard className="w-4 h-4 mr-2" />}
|
|
FIZETÉS & JÓVÁHAGYOM
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-amber-50 border border-amber-200 p-6 rounded-2xl flex gap-4">
|
|
<div className="shrink-0"><Info className="w-5 h-5 text-amber-500" /></div>
|
|
<p className="text-xs text-amber-800 font-medium leading-relaxed">
|
|
<strong>Fontos:</strong> 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.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* STANDARD FEEDBACK FORM */
|
|
<>
|
|
<div className="p-8 border-b border-gray-100 bg-gradient-to-r from-blue-50/50 to-purple-50/50 rounded-t-3xl">
|
|
<div className="flex items-start gap-4">
|
|
<div className="bg-white p-3 rounded-2xl shadow-md text-primary border border-gray-100">
|
|
<MessageSquare className="w-6 h-6" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-2xl font-black text-gray-900 tracking-tighter">Visszajelzés a Weboldalról</h2>
|
|
<p className="text-sm text-gray-500 font-medium mt-1">A bemutató verzió alapján kérjük jelezd, ha valamit módosítanál.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-8 md:p-10 space-y-10 flex-grow overflow-y-auto">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-900 mb-5 flex items-center gap-2">
|
|
Döntés a továbblépésről <span className="text-red-500">*</span>
|
|
</h3>
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{[
|
|
{ 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 => (
|
|
<label key={opt.id} className={`flex items-center p-5 border-2 rounded-2xl cursor-pointer transition-all ${mainDecision === opt.id ? `border-${opt.color}-500 bg-${opt.color}-50 shadow-lg ring-1 ring-${opt.color}-500` : 'border-gray-100 hover:border-gray-200'}`}>
|
|
<input
|
|
type="radio"
|
|
name="decision"
|
|
checked={mainDecision === opt.id}
|
|
onChange={() => setMainDecision(opt.id as any)}
|
|
className={`w-6 h-6 text-${opt.color}-600 focus:ring-${opt.color}-500 border-gray-300`}
|
|
/>
|
|
<span className="ml-4 font-bold text-gray-900">{opt.icon} {opt.label}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{mainDecision === 'approved' && (
|
|
<div className="animate-fade-in bg-green-50 p-6 rounded-2xl border-2 border-green-200 shadow-inner">
|
|
<h3 className="text-green-900 font-black mb-4 flex items-center gap-2 uppercase text-sm tracking-widest">
|
|
<CheckCircle className="w-5 h-5" /> Megerősítés
|
|
</h3>
|
|
<label className="flex items-start cursor-pointer group">
|
|
<input
|
|
type="checkbox"
|
|
checked={approvalConfirmed}
|
|
onChange={(e) => setApprovalConfirmed(e.target.checked)}
|
|
className="mt-1 w-6 h-6 text-green-600 rounded-lg focus:ring-green-500 border-green-300"
|
|
/>
|
|
<span className="ml-4 text-sm text-green-800 font-bold leading-relaxed">
|
|
Tudomásul veszem, hogy a jóváhagyással elfogadom a jelenlegi állapotot, és a folyamat a végleges elszámolással folytatódik.
|
|
</span>
|
|
</label>
|
|
</div>
|
|
)}
|
|
|
|
{isRevision && (
|
|
<div className="animate-fade-in space-y-8 border-t border-gray-100 pt-10">
|
|
|
|
{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..."
|
|
)}
|
|
|
|
<div>
|
|
<h3 className="text-sm font-black text-gray-900 mb-3 uppercase tracking-widest">Mi a legfontosabb kérésed? <span className="text-red-500">*</span></h3>
|
|
<textarea
|
|
value={priorityText}
|
|
onChange={(e) => setPriorityText(e.target.value)}
|
|
className={commonInputStyles}
|
|
rows={2}
|
|
placeholder="A legkritikusabb pont, amin változtatni kell..."
|
|
></textarea>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="text-sm font-black text-gray-900 mb-3 uppercase tracking-widest">Egyéb észrevételek</h3>
|
|
<textarea
|
|
value={extraNotes}
|
|
onChange={(e) => setExtraNotes(e.target.value)}
|
|
className={commonInputStyles}
|
|
rows={4}
|
|
placeholder="Bármi egyéb megjegyzés..."
|
|
></textarea>
|
|
</div>
|
|
|
|
<div className="bg-yellow-50 p-6 rounded-2xl border-2 border-yellow-200 shadow-inner">
|
|
<label className="flex items-start cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={revisionConfirmed}
|
|
onChange={(e) => setRevisionConfirmed(e.target.checked)}
|
|
className="mt-1 w-6 h-6 text-yellow-600 rounded-lg focus:ring-yellow-500 border-yellow-300"
|
|
/>
|
|
<span className="ml-4 text-sm text-yellow-900 font-bold leading-relaxed">
|
|
Tudomásul veszem, hogy a kért módosítások feldolgozása után kollégáik keresni fognak az újabb verzióval.
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="p-8 border-t border-gray-100 bg-gray-50/50 rounded-b-3xl flex flex-col sm:flex-row justify-end gap-4">
|
|
<Button variant="white" onClick={onClose} disabled={loading} className="px-10 border-gray-200">Mégse</Button>
|
|
|
|
{mainDecision === 'approved' && (
|
|
shouldShowPaymentStep ? (
|
|
<Button
|
|
onClick={handleProceedToPayment}
|
|
disabled={loading || !approvalConfirmed}
|
|
className="bg-primary hover:bg-primary-dark text-white font-black uppercase tracking-widest px-10 shadow-lg shadow-primary/20"
|
|
>
|
|
Tovább a fizetéshez
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
onClick={handleSubmit}
|
|
disabled={loading || !approvalConfirmed}
|
|
className="bg-green-600 hover:bg-green-700 text-white font-black uppercase tracking-widest px-10 shadow-lg shadow-green-200"
|
|
>
|
|
{loading ? 'Küldés...' : 'Végleges Jóváhagyás'}
|
|
</Button>
|
|
)
|
|
)}
|
|
|
|
{isRevision && (
|
|
<Button
|
|
onClick={handleSubmit}
|
|
disabled={loading || !revisionConfirmed || !priorityText.trim()}
|
|
className="font-black uppercase tracking-widest px-10 shadow-lg shadow-primary/20"
|
|
>
|
|
{loading ? 'Küldés...' : 'Visszajelzés Elküldése'}
|
|
</Button>
|
|
)}
|
|
|
|
{!mainDecision && (
|
|
<Button disabled className="opacity-50 cursor-not-allowed font-black uppercase tracking-widest px-10">
|
|
Válasszon opciót
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|