KFH_ASM/asm_app/src/components/WoFeedbackForm.tsx

260 lines
8.5 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { FaCheckCircle, FaSpinner } from 'react-icons/fa';
import woFeedbackWebFormService, {
type WoFeedbackRecord,
type WoFeedbackWebFormConfig,
} from '../services/woFeedbackWebFormService';
export interface WoFeedbackFormProps {
workOrder: string;
defaultRequesterName?: string;
compact?: boolean;
onSuccess?: (record: WoFeedbackRecord) => void;
}
type FormPhase = 'loading' | 'form' | 'existing' | 'success' | 'error';
const inputCls =
'w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-400';
const WoFeedbackForm: React.FC<WoFeedbackFormProps> = ({
workOrder,
defaultRequesterName = '',
compact = false,
onSuccess,
}) => {
const [phase, setPhase] = useState<FormPhase>('loading');
const [config, setConfig] = useState<WoFeedbackWebFormConfig | null>(null);
const [existing, setExisting] = useState<WoFeedbackRecord | null>(null);
const [requesterName, setRequesterName] = useState(defaultRequesterName);
const [rating, setRating] = useState('');
const [comments, setComments] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
setRequesterName(defaultRequesterName);
}, [defaultRequesterName]);
useEffect(() => {
if (!workOrder) {
setPhase('error');
setError('Work Order is required.');
return;
}
let cancelled = false;
(async () => {
setPhase('loading');
setError('');
try {
const [cfg, fb] = await Promise.all([
woFeedbackWebFormService.getWebFormConfig(),
woFeedbackWebFormService.getFeedbackForWorkOrder(workOrder),
]);
if (cancelled) return;
setConfig(cfg);
if (fb) {
setExisting(fb);
setPhase('existing');
} else {
setPhase('form');
}
} catch (e) {
if (cancelled) return;
setError(e instanceof Error ? e.message : 'Failed to load feedback form');
setPhase('error');
}
})();
return () => { cancelled = true; };
}, [workOrder]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!requesterName.trim()) {
setError('Requester Name is required.');
return;
}
if (!rating) {
setError('Please select a rating.');
return;
}
setSubmitting(true);
setError('');
try {
const saved = await woFeedbackWebFormService.submitFeedback({
work_order: workOrder,
requester_name: requesterName.trim(),
rating,
comments: comments.trim(),
});
setExisting(saved);
setPhase('success');
onSuccess?.(saved);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to submit feedback');
} finally {
setSubmitting(false);
}
};
if (phase === 'loading') {
return (
<div className={`flex items-center justify-center gap-2 text-gray-500 ${compact ? 'py-4' : 'py-10'}`}>
<FaSpinner className="animate-spin text-indigo-500" />
<span className="text-sm">Loading feedback form</span>
</div>
);
}
if (phase === 'error' && !config) {
return (
<div className="rounded-lg border border-red-200 bg-red-50 dark:bg-red-900/20 dark:border-red-800 px-4 py-3 text-sm text-red-700 dark:text-red-300">
{error || 'Unable to load feedback form.'}
</div>
);
}
const title = config?.title || 'WO Feedback';
if (phase === 'existing' && existing) {
return (
<div className={compact ? 'space-y-3' : 'space-y-4'}>
{!compact && config?.introductionText && (
<p className="text-sm text-gray-600 dark:text-gray-400">{config.introductionText}</p>
)}
<div className="rounded-lg border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20 p-4 text-center">
<FaCheckCircle className="mx-auto text-green-600 mb-2" />
<p className="text-sm font-semibold text-green-800 dark:text-green-300">Feedback already submitted</p>
<p className="text-xs text-green-700 dark:text-green-400 mt-2">
<span className="font-medium">Requester:</span> {existing.requester_name || '—'}
</p>
<p className="text-sm font-bold text-green-800 dark:text-green-200 mt-1">
{existing.rating || '—'}
</p>
{existing.comments && (
<p className="text-xs text-green-700 dark:text-green-400 mt-2 text-left">
<span className="font-medium">Comments:</span> {existing.comments}
</p>
)}
</div>
</div>
);
}
if (phase === 'success' && existing) {
return (
<div className={`rounded-lg border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20 p-4 text-center ${compact ? '' : 'py-8'}`}>
<FaCheckCircle className="mx-auto text-green-600 text-2xl mb-3" />
<h3 className="text-base font-bold text-green-800 dark:text-green-200">
{config?.successTitle || 'Feedback Submitted'}
</h3>
<p className="text-sm text-green-700 dark:text-green-300 mt-1">
{config?.successMessage || 'Your feedback was submitted successfully.'}
</p>
<p className="text-xs text-green-600 dark:text-green-400 mt-3">
{existing.rating}
</p>
{existing.comments && (
<p className="text-xs text-green-600 dark:text-green-400 mt-2">
{existing.comments}
</p>
)}
</div>
);
}
return (
<form onSubmit={handleSubmit} className={compact ? 'space-y-3' : 'space-y-4'}>
{!compact && (
<div>
<h3 className="text-base font-semibold text-gray-900 dark:text-white">{title}</h3>
{config?.introductionText && (
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{config.introductionText}</p>
)}
</div>
)}
{compact && config?.introductionText && (
<p className="text-xs text-gray-500 dark:text-gray-400">{config.introductionText}</p>
)}
<div>
<label className="block text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-1">
Work Order
</label>
<div className="px-3 py-2 text-sm bg-gray-100 dark:bg-gray-900/40 border border-gray-200 dark:border-gray-700 rounded-lg text-gray-800 dark:text-gray-200">
{workOrder}
</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-1">
Requester Name <span className="text-red-500">*</span>
</label>
<input
type="text"
value={requesterName}
onChange={e => setRequesterName(e.target.value)}
className={inputCls}
placeholder="Your name"
required
/>
</div>
<div>
<label className="block text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-1">
Rating <span className="text-red-500">*</span>
</label>
<select
value={rating}
onChange={e => setRating(e.target.value)}
className={inputCls}
required
>
<option value="">Select rating</option>
{(config?.ratingOptions || []).map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-1">
Comments
</label>
<textarea
value={comments}
onChange={e => setComments(e.target.value)}
className={`${inputCls} min-h-[88px] resize-y`}
placeholder="Optional comments…"
rows={3}
/>
</div>
{error && (
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
)}
<button
type="submit"
disabled={submitting}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium disabled:opacity-50"
>
{submitting ? (
<>
<FaSpinner className="animate-spin" size={14} />
Submitting
</>
) : (
config?.buttonLabel || 'Save'
)}
</button>
</form>
);
};
export default WoFeedbackForm;