260 lines
8.5 KiB
TypeScript
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;
|