371 lines
12 KiB
TypeScript
371 lines
12 KiB
TypeScript
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Switch } from '@/components/ui/switch';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Textarea } from '@/components/ui/textarea';
|
||
import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader';
|
||
import { Department } from '@/api/department';
|
||
import { FacilityImage, Facility } from '@/api/facility';
|
||
|
||
interface FacilityModalProps {
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
editing: Facility | null;
|
||
form: any;
|
||
setForm: any;
|
||
departments: Department[];
|
||
onSubmit: () => void;
|
||
}
|
||
|
||
export default function FacilityModal({
|
||
open,
|
||
onOpenChange,
|
||
editing,
|
||
form,
|
||
setForm,
|
||
departments,
|
||
onSubmit,
|
||
}: FacilityModalProps) {
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||
const { name, value, type } = e.target;
|
||
let finalValue: any = type === 'number' ? Number(value) : value;
|
||
|
||
if (name === 'slug') {
|
||
finalValue = finalValue
|
||
.toLowerCase()
|
||
.replace(/\s+/g, '-')
|
||
.replace(/[^\w-]+/g, '')
|
||
.replace(/--+/g, '-');
|
||
}
|
||
|
||
setForm((prev: any) => ({ ...prev, [name]: finalValue }));
|
||
};
|
||
|
||
const handleAddImageField = () => {
|
||
setForm((prev: any) => ({
|
||
...prev,
|
||
images: [...prev.images, { url: '', altText: '', description: '' }],
|
||
}));
|
||
};
|
||
|
||
const handleRemoveImageField = (index: number) => {
|
||
setForm((prev: any) => ({
|
||
...prev,
|
||
images: prev.images.filter((_: any, i: number) => i !== index),
|
||
}));
|
||
};
|
||
|
||
const handleImageChange = (index: number, field: keyof FacilityImage, value: string) => {
|
||
setForm((prev: any) => {
|
||
const updatedImages = [...prev.images];
|
||
updatedImages[index] = { ...updatedImages[index], [field]: value };
|
||
return { ...prev, images: updatedImages };
|
||
});
|
||
};
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent className="w-full !max-w-5xl h-[90vh] flex flex-col p-0 overflow-hidden">
|
||
<DialogHeader className="p-6 border-b bg-background z-10">
|
||
<DialogTitle className="text-2xl">{editing ? 'Edit Facility Record' : 'Add New Facility Asset'}</DialogTitle>
|
||
</DialogHeader>
|
||
|
||
<div className="flex-1 overflow-y-auto p-6">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||
{/* Left Side: Profile & Structure */}
|
||
<div className="space-y-6">
|
||
<h3 className="font-bold text-base border-b pb-2">Profile & Structure</h3>
|
||
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="flex items-center justify-between p-3 border rounded-md bg-muted/30">
|
||
<Label htmlFor="isActive" className="text-base font-semibold cursor-pointer">
|
||
Active
|
||
</Label>
|
||
<Switch
|
||
id="isActive"
|
||
checked={form.isActive}
|
||
onCheckedChange={(val) => setForm({ ...form, isActive: val })}
|
||
/>
|
||
</div>
|
||
<div className="flex items-center justify-between p-3 border rounded-md bg-muted/30">
|
||
<Label htmlFor="isFeatured" className="text-base font-semibold cursor-pointer">
|
||
Featured
|
||
</Label>
|
||
<Switch
|
||
id="isFeatured"
|
||
checked={form.isFeatured}
|
||
onCheckedChange={(val) => setForm({ ...form, isFeatured: val })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<Label htmlFor="sortOrder" className="text-sm font-semibold">
|
||
Priority
|
||
</Label>
|
||
<Input
|
||
id="sortOrder"
|
||
name="sortOrder"
|
||
type="number"
|
||
value={form.sortOrder}
|
||
onChange={handleChange}
|
||
className="text-base"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<Label className="text-sm font-semibold">Facility ID</Label>
|
||
<Input
|
||
name="facilityId"
|
||
placeholder="FAC-MRI-3T"
|
||
value={form.facilityId}
|
||
onChange={handleChange}
|
||
disabled={!!editing}
|
||
className="text-base"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<Label className="text-sm font-semibold">Name</Label>
|
||
<Input
|
||
name="name"
|
||
placeholder="3T Digital MRI Center"
|
||
value={form.name}
|
||
onChange={handleChange}
|
||
className="text-base"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<Label className="text-sm font-semibold">Connected Department</Label>
|
||
<select
|
||
name="departmentId"
|
||
value={form.departmentId || ''}
|
||
onChange={handleChange}
|
||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||
>
|
||
<option value="">Select Target Department (Optional)</option>
|
||
{departments.map((d) => (
|
||
<option key={d.departmentId} value={d.departmentId}>
|
||
{d.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<Label className="text-sm font-semibold">Video URL</Label>
|
||
<Input
|
||
name="videoUrl"
|
||
placeholder="https://youtube.com/watch?v=..."
|
||
value={form.videoUrl}
|
||
onChange={handleChange}
|
||
className="text-base"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<Label className="text-sm font-semibold">Brief Summary</Label>
|
||
<Textarea
|
||
name="shortDescription"
|
||
placeholder="Provide a concise introductory snippet statement description..."
|
||
value={form.shortDescription}
|
||
onChange={handleChange}
|
||
className="min-h-[80px] text-base"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<Label className="text-sm font-semibold">Specifications</Label>
|
||
<Textarea
|
||
name="description"
|
||
placeholder="Write full specifications text information description details here..."
|
||
value={form.description}
|
||
onChange={handleChange}
|
||
className="min-h-[140px] text-base"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
<h3 className="font-bold text-base border-b pb-2">Image Gallery & Marketing SEO Engine</h3>
|
||
|
||
<div className="space-y-4 p-5 border rounded-md bg-muted/20">
|
||
<div className="flex items-center justify-between">
|
||
<p className="text-base font-bold">Gallery Media Content</p>
|
||
<Button type="button" variant="outline" size="sm" onClick={handleAddImageField}>
|
||
+ Add Photo
|
||
</Button>
|
||
</div>
|
||
|
||
{form.images?.length === 0 ? (
|
||
<p className="text-sm text-muted-foreground italic text-center py-4">No gallery items attached.</p>
|
||
) : (
|
||
<div className="space-y-4 max-h-[300px] overflow-y-auto pr-1">
|
||
{form.images.map((img: any, idx: number) => (
|
||
<div key={idx} className="border rounded-lg p-4 space-y-3 bg-background relative shadow-sm">
|
||
<div className="flex justify-between items-center">
|
||
<span className="text-xs font-bold font-mono uppercase tracking-wider text-muted-foreground">
|
||
Asset Resource #{idx + 1}
|
||
</span>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
className="text-red-500 h-7 px-2"
|
||
onClick={() => handleRemoveImageField(idx)}
|
||
>
|
||
Delete
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-xs">Facility Image</Label>
|
||
<BytescaleUploader
|
||
value={img.url}
|
||
folderPath="/facilities"
|
||
onChange={(url) => handleImageChange(idx, 'url', url)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<Input
|
||
placeholder="Accessibility Alternative Image Text String..."
|
||
value={img.altText}
|
||
onChange={(e) => handleImageChange(idx, 'altText', e.target.value)}
|
||
className="h-8 text-xs"
|
||
/>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-4 p-5 border rounded-md bg-muted/20">
|
||
<p className="text-base font-bold">SEO Settings</p>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-sm font-semibold">SEO Title</Label>
|
||
<Input
|
||
name="seoTitle"
|
||
placeholder="Advanced Facility Features Setup..."
|
||
value={form.seoTitle}
|
||
onChange={handleChange}
|
||
className="text-base"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-sm font-semibold">Meta Description</Label>
|
||
<Textarea
|
||
name="metaDescription"
|
||
placeholder="Enter target Google crawler index info context snippet description..."
|
||
value={form.metaDescription}
|
||
onChange={handleChange}
|
||
className="min-h-[80px] text-base"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-sm font-semibold">Focus Keyphrase</Label>
|
||
<Input
|
||
name="focusKeyphrase"
|
||
placeholder="best specialized diagnostic lab facility"
|
||
value={form.focusKeyphrase}
|
||
onChange={handleChange}
|
||
className="text-base"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-sm font-semibold">URL Slug</Label>
|
||
<Input
|
||
name="slug"
|
||
placeholder="advanced-mri-center"
|
||
value={form.slug}
|
||
onChange={handleChange}
|
||
className="text-base"
|
||
/>
|
||
<p className="text-xs text-muted-foreground font-mono">
|
||
Routing Path Output: /facilities/{form.slug || 'slug-placeholder'}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-sm font-semibold">Tags / Keywords</Label>
|
||
<div className="flex flex-wrap gap-2 border rounded-md p-3 min-h-[48px] bg-background">
|
||
{form.tags?.map((tag: string, i: number) => (
|
||
<div
|
||
key={i}
|
||
className="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm flex items-center gap-2"
|
||
>
|
||
{tag}
|
||
<button
|
||
type="button"
|
||
onClick={() =>
|
||
setForm({ ...form, tags: form.tags.filter((_: string, idx: number) => idx !== i) })
|
||
}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
<Input
|
||
placeholder="Type keyword entity context strings and press Enter"
|
||
className="border-0 shadow-none focus-visible:ring-0 min-w-[200px]"
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter' && e.currentTarget.value.trim()) {
|
||
e.preventDefault();
|
||
setForm({ ...form, tags: [...(form.tags || []), e.currentTarget.value.trim()] });
|
||
e.currentTarget.value = '';
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="border-t pt-4 space-y-3">
|
||
<p className="text-sm font-bold text-muted-foreground">Social Graph Share Parameters (OG Settings)</p>
|
||
<div className="space-y-1">
|
||
<Label className="text-xs">OG Share Title</Label>
|
||
<Input name="ogTitle" value={form.ogTitle} onChange={handleChange} className="h-9 text-sm" />
|
||
</div>
|
||
<div className="space-y-1">
|
||
<Label className="text-xs">OG Share Summary</Label>
|
||
<Textarea
|
||
name="ogDescription"
|
||
value={form.ogDescription}
|
||
onChange={handleChange}
|
||
className="min-h-[60px] text-sm"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<Label className="text-xs">OG Share Branding Image Media</Label>
|
||
<BytescaleUploader
|
||
value={form.ogImage}
|
||
folderPath="/seo"
|
||
onChange={(url) => setForm({ ...form, ogImage: url })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<DialogFooter className="p-6 border-t bg-background z-10 mt-0">
|
||
<Button variant="ghost" onClick={() => onOpenChange(false)} className="text-base">
|
||
Cancel
|
||
</Button>
|
||
<Button onClick={onSubmit} className="px-10 text-base">
|
||
Save Changes{' '}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|