Maintenance Request Ticket
blockreal-estateResident repair and facility work order submission portal with urgency priority and photo dropzone.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/maintenance-request-ticket.json$npx shadcn@latest add https://uipkge.dev/r/react/maintenance-request-ticket.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/maintenance-request-ticket.json$bunx shadcn@latest add https://uipkge.dev/r/react/maintenance-request-ticket.jsonNamed registry:
npx shadcn@latest add @uipkge-react/maintenance-request-ticketInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
className | string | — | optional |
initialCategory | string | — | optional |
initialUrgency | 'routine' | 'standard' | 'emergency' | — | optional |
initialLocation | string | — | optional |
initialDescription | string | — | optional |
initialPermission | 'granted' | 'call_first' | — | optional |
initialFiles | AttachedFile[] | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
AttachedFileinterface AttachedFile {
id: string
name: string
size: string
type: 'image' | 'video'
uploadedAt: string
}npm dependencies
Files installed (1)
components/blocks/MaintenanceRequestTicket.tsx42.3 kB
'use client' import * as React from 'react' import { useState } from 'react' import { AlertCircle, Bug, Building2, Check, CheckCircle2, ChevronDown, Clock, Droplets, FileImage, FileVideo, Flame, KeyRound, PhoneCall, Plus, RefreshCw, ShieldAlert, ShieldCheck, Trash2, UploadCloud, UserCheck, Wrench, Zap, } from 'lucide-react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' import { Textarea } from '@/components/ui/textarea' export interface AttachedFile { id: string name: string size: string type: 'image' | 'video' uploadedAt: string } export interface MaintenanceRequestTicketProps { className?: string initialCategory?: string initialUrgency?: 'routine' | 'standard' | 'emergency' initialLocation?: string initialDescription?: string initialPermission?: 'granted' | 'call_first' initialFiles?: AttachedFile[] } const defaultFiles: AttachedFile[] = [ { id: 'f-1', name: 'kitchen-sink-leak-pipe.jpg', size: '2.4 MB', type: 'image', uploadedAt: 'Today, 10:14 AM', }, { id: 'f-2', name: 'disposal-motor-hum.mp4', size: '8.1 MB', type: 'video', uploadedAt: 'Today, 10:16 AM', }, ] const categories = [ { id: 'plumbing', label: 'Plumbing & Leaks', description: 'Faucets, pipes, toilets, drains', icon: Droplets, badgeColor: 'text-info bg-info/10 text-info', }, { id: 'electrical', label: 'Electrical & Lighting', description: 'Outlets, switches, fixtures, breakers', icon: Zap, badgeColor: 'text-warning bg-warning/10 text-warning', }, { id: 'hvac', label: 'HVAC & AC Heating', description: 'AC, thermostat, heater, airflow', icon: Flame, badgeColor: 'text-chart-2 bg-chart-2/10', }, { id: 'appliances', label: 'Appliances', description: 'Dishwasher, oven, disposal, fridge', icon: Wrench, badgeColor: 'text-chart-1 bg-chart-1/10', }, { id: 'locks', label: 'Doors & Locks', description: 'Keys, latches, deadbolts, windows', icon: KeyRound, badgeColor: 'text-success bg-success/10 text-success', }, { id: 'pest', label: 'Pest Control', description: 'Insects, rodents, preventative traps', icon: Bug, badgeColor: 'text-destructive bg-destructive/10 text-destructive', }, ] const urgencyOptions = [ { value: 'routine', label: 'Low / Routine', badge: '3–5 Business Days', badgeVariant: 'outline' as const, description: 'Non-urgent preventative maintenance or routine hardware adjustments.', }, { value: 'standard', label: 'Medium / Standard 48h', badge: '24–48h SLA', badgeVariant: 'secondary' as const, description: 'Standard repair with moderate impact on daily convenience or appliance usage.', }, { value: 'emergency', label: 'High / Emergency 2h SLA', badge: 'Emergency < 2h', badgeVariant: 'destructive' as const, description: 'Immediate hazard to property or habitability. Triggers 24/7 on-call dispatch.', }, ] const locationOptions = [ { value: 'kitchen', label: 'Kitchen' }, { value: 'master_bathroom', label: 'Master Bathroom' }, { value: 'guest_bathroom', label: 'Guest Bathroom' }, { value: 'living_room', label: 'Living Room' }, { value: 'balcony', label: 'Balcony / Patio' }, { value: 'master_bedroom', label: 'Master Bedroom' }, { value: 'guest_bedroom', label: 'Guest Bedroom' }, { value: 'hallway', label: 'Hallway / Entryway' }, { value: 'laundry', label: 'Laundry & Utility Closet' }, ] const quickSnippets = [ 'Motor humming but not spinning', 'Water leaking under sink', 'Reset button tripped', 'Slow draining', ] const maxChars = 500 export function MaintenanceRequestTicket({ className, initialCategory = 'appliances', initialUrgency = 'standard', initialLocation = 'kitchen', initialDescription = 'Kitchen sink garbage disposal is jammed and leaking slightly under the cabinet when water runs. Motor makes a low humming sound.', initialPermission = 'granted', initialFiles = defaultFiles, }: MaintenanceRequestTicketProps) { const [category, setCategory] = useState<string>(initialCategory) const [urgency, setUrgency] = useState<'routine' | 'standard' | 'emergency'>(initialUrgency) const [location, setLocation] = useState<string>(initialLocation) const [description, setDescription] = useState<string>(initialDescription) const [permission, setPermission] = useState<'granted' | 'call_first'>(initialPermission) const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([...initialFiles]) const [isDragging, setIsDragging] = useState(false) const [showOpenRequests, setShowOpenRequests] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitted, setIsSubmitted] = useState(false) const [draftSaved, setDraftSaved] = useState(false) const [generatedTicketId, setGeneratedTicketId] = useState('MNT-9042') const descriptionLength = description.length const appendSnippet = (snippet: string) => { if (!description) { setDescription(snippet) } else if (!description.includes(snippet)) { setDescription(`${description.trim()} ${snippet}.`) } } const removeFile = (id: string) => { setAttachedFiles((prev) => prev.filter((f) => f.id !== id)) } const handleDrop = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault() setIsDragging(false) if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { const newFiles: AttachedFile[] = [] for (let i = 0; i < e.dataTransfer.files.length; i++) { const file = e.dataTransfer.files[i] newFiles.push({ id: `f-${Date.now()}-${i}`, name: file.name, size: `${(file.size / (1024 * 1024)).toFixed(1)} MB`, type: file.type.startsWith('video') ? 'video' : 'image', uploadedAt: 'Just now', }) } setAttachedFiles((prev) => [...prev, ...newFiles]) } } const simulateAddFile = () => { const sampleNames = ['cabinet-water-mark.jpg', 'under-sink-plumbing.jpg', 'noise-recording.mp4'] const randomName = sampleNames[attachedFiles.length % sampleNames.length] setAttachedFiles((prev) => [ ...prev, { id: `f-${Date.now()}`, name: randomName, size: '3.1 MB', type: randomName.endsWith('.mp4') ? 'video' : 'image', uploadedAt: 'Just now', }, ]) } const handleSaveDraft = () => { setDraftSaved(true) setTimeout(() => { setDraftSaved(false) }, 3000) } const handleSubmit = () => { setIsSubmitting(true) setTimeout(() => { setIsSubmitting(false) setIsSubmitted(true) setGeneratedTicketId(`MNT-${Math.floor(1000 + Math.random() * 9000)}`) }, 700) } const handleReset = () => { setIsSubmitted(false) setCategory('appliances') setUrgency('standard') setLocation('kitchen') setDescription( 'Kitchen sink garbage disposal is jammed and leaking slightly under the cabinet when water runs. Motor makes a low humming sound.', ) setPermission('granted') setAttachedFiles([ { id: 'f-1', name: 'kitchen-sink-leak-pipe.jpg', size: '2.4 MB', type: 'image', uploadedAt: 'Today, 10:14 AM', }, { id: 'f-2', name: 'disposal-motor-hum.mp4', size: '8.1 MB', type: 'video', uploadedAt: 'Today, 10:16 AM', }, ]) } return ( <div data-slot="maintenance-request-ticket" className={cn('w-full space-y-6', className)}> {/* Header Banner */} <Card className="border-border shadow-xs"> <CardContent className="p-4 sm:p-6"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="space-y-1.5"> <div className="flex flex-wrap items-center gap-2.5"> <h1 className="text-foreground text-xl font-bold tracking-tight sm:text-2xl"> Submit Maintenance Request </h1> <Badge variant="outline" className="gap-1.5 font-mono text-xs"> <span className="bg-success size-1.5 rounded-full" /> Unit 4B · Pacific Heights </Badge> </div> <p className="text-muted-foreground text-sm"> Resident: <span className="text-foreground font-medium">Elena Rostova</span> · Facility maintenance & repair portal </p> </div> <div className="flex flex-wrap items-center gap-2.5"> <div className="bg-muted/60 border-border inline-flex items-center gap-2 rounded-lg border px-3 py-1.5 text-xs"> <span className="relative flex size-2"> <span className="bg-warning absolute inline-flex size-full rounded-full opacity-75" /> <span className="bg-warning relative inline-flex size-2 rounded-full" /> </span> <span className="text-foreground font-medium">1 In Progress</span> <span className="text-muted-foreground hidden sm:inline">· #MNT-8821</span> </div> <Button variant="outline" size="sm" className="gap-1.5 shadow-xs" onClick={() => setShowOpenRequests(!showOpenRequests)} > <span>{showOpenRequests ? 'Hide Open Requests' : 'View Open Requests'}</span> <ChevronDown className={cn('size-3.5 transition-transform duration-200', showOpenRequests && 'rotate-180')} /> </Button> </div> </div> {/* Collapsible Active Work Orders Panel */} {showOpenRequests && ( <div className="border-border bg-muted/30 mt-4 space-y-3 rounded-lg border p-4 transition-colors"> <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> <Clock className="text-muted-foreground size-4" /> <span className="text-foreground text-xs font-semibold tracking-wider uppercase"> Currently Active Work Orders (1) </span> </div> <Badge variant="secondary" className="text-xs"> Technician Assigned </Badge> </div> <div className="bg-card border-border flex flex-col justify-between gap-3 rounded-md border p-3 sm:flex-row sm:items-center"> <div className="space-y-1"> <div className="flex items-center gap-2"> <span className="text-foreground text-sm font-semibold"> #MNT-8821 · HVAC Filter & Thermostat Inspection </span> <Badge variant="outline" className="text-xs"> Living Room </Badge> </div> <p className="text-muted-foreground text-xs"> Assigned to: <span className="text-foreground font-medium">Dave Miller (Lead HVAC Specialist)</span>{' '} · Scheduled window: Today 2:00 PM – 4:00 PM </p> </div> <div className="flex shrink-0 items-center gap-2"> <Badge variant="info" className="text-xs"> In Progress </Badge> </div> </div> </div> )} </CardContent> </Card> {/* Main 2-Column Layout */} <div className="grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-8"> {/* Left Column: Work Order Form (8 cols) */} <div className="space-y-6 lg:col-span-8"> {/* Submission Success State */} {isSubmitted ? ( <Card className="border-success/30 bg-success/5 dark:bg-success/10 shadow-xs"> <CardContent className="space-y-4 p-6 text-center sm:p-8"> <div className="bg-success/10 text-success mx-auto flex size-14 items-center justify-center rounded-full"> <CheckCircle2 className="size-8" /> </div> <div className="space-y-2"> <h2 className="text-foreground text-xl font-bold">Maintenance Request Submitted!</h2> <p className="text-muted-foreground mx-auto max-w-md text-sm"> Your request <span className="text-foreground font-mono font-semibold">#{generatedTicketId}</span>{' '} has been logged and dispatched to the Pacific Heights Facility Operations team. </p> </div> <div className="border-border bg-card mx-auto max-w-lg space-y-2 rounded-lg border p-4 text-left text-xs"> <div className="flex justify-between"> <span className="text-muted-foreground">Ticket ID:</span> <span className="text-foreground font-mono font-medium">#{generatedTicketId}</span> </div> <div className="flex justify-between"> <span className="text-muted-foreground">Unit:</span> <span className="text-foreground font-medium">Unit 4B (Pacific Heights)</span> </div> <div className="flex justify-between"> <span className="text-muted-foreground">Category:</span> <span className="text-foreground font-medium capitalize">{category}</span> </div> <div className="flex justify-between"> <span className="text-muted-foreground">Urgency Tier:</span> <span className="text-foreground font-medium capitalize">{urgency} SLA</span> </div> <div className="flex justify-between"> <span className="text-muted-foreground">Entry Authorization:</span> <span className="text-foreground font-medium"> {permission === 'granted' ? 'Permission Granted (Master Key)' : 'Call Resident First'} </span> </div> </div> <div className="flex flex-wrap justify-center gap-3 pt-2"> <Button className="gap-1.5 shadow-xs" onClick={handleReset}> <RefreshCw className="size-4" /> <span>Submit Another Request</span> </Button> </div> </CardContent> </Card> ) : ( <Card className="border-border shadow-xs"> <CardHeader className="pb-4"> <CardTitle className="text-lg">Work Order Details</CardTitle> <CardDescription>Complete the fields below to schedule a maintenance technician visit.</CardDescription> </CardHeader> <CardContent className="space-y-6"> {/* 1. Category Selection Grid */} <div className="space-y-3"> <div className="flex items-center justify-between"> <label className="text-foreground text-sm font-semibold"> 1. Issue Category <span className="text-destructive">*</span> </label> <span className="text-muted-foreground text-xs">Select one primary discipline</span> </div> <div className="grid grid-cols-2 gap-3 sm:grid-cols-3"> {categories.map((cat) => { const Icon = cat.icon const isSelected = category === cat.id return ( <button key={cat.id} type="button" aria-pressed={isSelected} className={cn( 'group focus-visible:ring-ring relative flex cursor-pointer flex-col items-start gap-2.5 rounded-lg border p-3.5 text-left transition-colors outline-none select-none focus-visible:ring-2', isSelected ? 'border-primary bg-primary/5 ring-primary/30 shadow-xs ring-1' : 'border-border bg-card hover:bg-muted/40 hover:border-muted-foreground/30', )} onClick={() => setCategory(cat.id)} > <div className="flex w-full items-center justify-between"> <div className={cn('flex size-8 items-center justify-center rounded-md', cat.badgeColor)}> <Icon className="size-4.5" /> </div> <div className={cn( 'flex size-4 items-center justify-center rounded-full border transition-colors', isSelected ? 'border-primary bg-primary text-primary-foreground' : 'border-muted-foreground/30 opacity-0 group-hover:opacity-60', )} > {isSelected && <Check className="size-2.5 stroke-[3]" />} </div> </div> <div> <p className="text-foreground text-xs font-semibold">{cat.label}</p> <p className="text-muted-foreground mt-0.5 line-clamp-1 text-xs">{cat.description}</p> </div> </button> ) })} </div> </div> <Separator /> {/* 2. Urgency Level Radios */} <div className="space-y-3"> <div className="flex items-center justify-between"> <label className="text-foreground text-sm font-semibold"> 2. Urgency Priority <span className="text-destructive">*</span> </label> <span className="text-muted-foreground text-xs">Determines facility response window</span> </div> {/* Emergency Banner Alert if High Selected */} {urgency === 'emergency' && ( <div className="border-destructive/30 bg-destructive/10 text-destructive flex items-start gap-3 rounded-lg border p-3.5 text-xs"> <AlertCircle className="mt-0.5 size-4 shrink-0" /> <div className="space-y-1"> <p className="font-semibold">High / Emergency SLA Activated</p> <p className="text-destructive/90 leading-relaxed"> Emergency requests notify on-call building engineers immediately. For active gas leaks or major structural flooding, call the 24/7 hotline directly at <strong>(415) 555-0192</strong>. </p> </div> </div> )} <RadioGroup value={urgency} onValueChange={(val) => setUrgency(val as 'routine' | 'standard' | 'emergency')} className="grid gap-3 sm:grid-cols-3" > {urgencyOptions.map((opt) => ( <div key={opt.value} className={cn( 'border-border relative flex cursor-pointer flex-col justify-between gap-3 rounded-lg border p-3.5 transition-colors', urgency === opt.value ? 'border-primary bg-primary/5 ring-primary/30 shadow-xs ring-1' : 'bg-card hover:bg-muted/40 hover:border-muted-foreground/30', )} onClick={() => setUrgency(opt.value as any)} > <div className="flex items-start justify-between gap-2"> <div className="flex items-center gap-2"> <RadioGroupItem id={`urgency-${opt.value}`} value={opt.value} /> <label htmlFor={`urgency-${opt.value}`} className="text-foreground cursor-pointer text-xs font-semibold select-none" > {opt.label} </label> </div> </div> <div> <Badge variant={opt.badgeVariant} className="mb-1 text-xs"> {opt.badge} </Badge> <p className="text-muted-foreground text-xs leading-relaxed">{opt.description}</p> </div> </div> ))} </RadioGroup> </div> <Separator /> {/* 3. Location in Unit */} <div className="space-y-3"> <div className="flex items-center justify-between"> <label className="text-foreground text-sm font-semibold"> 3. Location in Unit <span className="text-destructive">*</span> </label> <span className="text-muted-foreground text-xs">Specific area or room</span> </div> <Select value={location} onValueChange={setLocation}> <SelectTrigger className="w-full"> <SelectValue placeholder="Select location in unit..." /> </SelectTrigger> <SelectContent> <SelectGroup> <SelectLabel>Unit 4B Interior Areas</SelectLabel> {locationOptions.map((loc) => ( <SelectItem key={loc.value} value={loc.value}> {loc.label} </SelectItem> ))} </SelectGroup> </SelectContent> </Select> </div> <Separator /> {/* 4. Issue Description */} <div className="space-y-3"> <div className="flex items-center justify-between"> <label htmlFor="mnt-description" className="text-foreground text-sm font-semibold"> 4. Issue Description <span className="text-destructive">*</span> </label> <span className={cn( 'text-xs', descriptionLength > maxChars ? 'text-destructive font-medium' : 'text-muted-foreground', )} > {descriptionLength} / {maxChars} </span> </div> <Textarea id="mnt-description" value={description} onValueChange={setDescription} rows={4} placeholder="Please describe the maintenance issue with as much detail as possible..." className="w-full text-sm" /> {/* Quick Snippet Helper Chips */} <div className="space-y-1.5"> <p className="text-muted-foreground text-xs">Quick details:</p> <div className="flex flex-wrap gap-1.5"> {quickSnippets.map((chip) => ( <button key={chip} type="button" className="bg-muted hover:bg-muted/80 text-muted-foreground hover:text-foreground inline-flex cursor-pointer items-center gap-1 rounded-md px-2.5 py-1 text-xs transition-colors" onClick={() => appendSnippet(chip)} > <Plus className="size-3" /> <span>{chip}</span> </button> ))} </div> </div> </div> <Separator /> {/* 5. Permission to Enter Unit */} <div className="space-y-3"> <div className="flex items-center justify-between"> <label className="text-foreground text-sm font-semibold"> 5. Permission to Enter Unit <span className="text-destructive">*</span> </label> <span className="text-muted-foreground text-xs">Access protocol</span> </div> <RadioGroup value={permission} onValueChange={(val) => setPermission(val as 'granted' | 'call_first')} className="grid gap-3 sm:grid-cols-2" > <div className={cn( 'border-border relative flex cursor-pointer flex-col justify-between gap-2.5 rounded-lg border p-4 transition-colors', permission === 'granted' ? 'border-primary bg-primary/5 ring-primary/30 shadow-xs ring-1' : 'bg-card hover:bg-muted/40 hover:border-muted-foreground/30', )} onClick={() => setPermission('granted')} > <div className="flex items-start gap-2.5"> <RadioGroupItem id="perm-granted" value="granted" className="mt-0.5" /> <div className="space-y-1"> <label htmlFor="perm-granted" className="text-foreground cursor-pointer text-xs font-semibold select-none" > Permission Granted to Enter </label> <p className="text-muted-foreground text-xs leading-relaxed"> Authorized staff may enter with master key if resident is not home. Work order sign-off notice will be left in unit. </p> </div> </div> <div className="text-success flex items-center gap-1.5 pl-6 text-xs font-medium"> <UserCheck className="size-3.5" /> <span>Faster dispatch window</span> </div> </div> <div className={cn( 'border-border relative flex cursor-pointer flex-col justify-between gap-2.5 rounded-lg border p-4 transition-colors', permission === 'call_first' ? 'border-primary bg-primary/5 ring-primary/30 shadow-xs ring-1' : 'bg-card hover:bg-muted/40 hover:border-muted-foreground/30', )} onClick={() => setPermission('call_first')} > <div className="flex items-start gap-2.5"> <RadioGroupItem id="perm-call" value="call_first" className="mt-0.5" /> <div className="space-y-1"> <label htmlFor="perm-call" className="text-foreground cursor-pointer text-xs font-semibold select-none" > Call Resident Before Entering </label> <p className="text-muted-foreground text-xs leading-relaxed"> Technician must call <span className="text-foreground font-medium">(415) 890-4412</span> 30 mins prior to arrival. Adult must be present. </p> </div> </div> <div className="text-muted-foreground flex items-center gap-1.5 pl-6 text-xs"> <Clock className="size-3.5" /> <span>Requires resident appointment</span> </div> </div> </RadioGroup> </div> <Separator /> {/* 6. Photo & Video Dropzone */} <div className="space-y-3"> <div className="flex items-center justify-between"> <label className="text-foreground text-sm font-semibold"> 6. Photo & Video Dropzone{' '} <span className="text-muted-foreground text-xs font-normal">(Optional)</span> </label> <span className="text-muted-foreground text-xs">{attachedFiles.length} Attached</span> </div> {/* Interactive Drop Area */} <div className={cn( 'cursor-pointer rounded-lg border-2 border-dashed p-6 text-center transition-colors', isDragging ? 'border-primary bg-primary/10' : 'border-border bg-muted/20 hover:border-muted-foreground/40 hover:bg-muted/40', )} onDragOver={(e) => { e.preventDefault() setIsDragging(true) }} onDragLeave={(e) => { e.preventDefault() setIsDragging(false) }} onDrop={handleDrop} onClick={simulateAddFile} > <div className="bg-muted text-muted-foreground mx-auto flex size-10 items-center justify-center rounded-full"> <UploadCloud className="size-5" /> </div> <div className="mt-2.5 space-y-1"> <p className="text-foreground text-xs font-medium"> <span className="text-primary font-semibold hover:underline">Click to upload</span> or drag and drop media files </p> <p className="text-muted-foreground text-xs"> PNG, JPG, HEIC, MP4 up to 25MB (helps technician arrive with correct parts) </p> </div> </div> {/* Attached Files Previews */} {attachedFiles.length > 0 && ( <div className="space-y-2 pt-1"> {attachedFiles.map((f) => ( <div key={f.id} className="bg-card border-border flex items-center justify-between gap-3 rounded-lg border p-2.5 text-xs transition-colors" > <div className="flex min-w-0 items-center gap-2.5"> <div className="bg-primary/10 text-primary flex size-8 shrink-0 items-center justify-center rounded-md"> {f.type === 'video' ? <FileVideo className="size-4" /> : <FileImage className="size-4" />} </div> <div className="min-w-0"> <p className="text-foreground truncate font-medium">{f.name}</p> <p className="text-muted-foreground text-xs"> {f.size} · {f.uploadedAt} </p> </div> </div> <div className="flex shrink-0 items-center gap-2"> <Badge variant="outline" className="border-success/30 text-success text-xs"> Attached </Badge> <Button variant="ghost" size="icon-sm" className="text-muted-foreground hover:text-destructive size-7" onClick={() => removeFile(f.id)} > <Trash2 className="size-3.5" /> <span className="sr-only">Remove file</span> </Button> </div> </div> ))} </div> )} </div> </CardContent> <CardFooter className="border-border bg-muted/20 flex flex-col-reverse justify-between gap-3 border-t p-4 sm:flex-row sm:items-center"> <div className="flex items-center gap-2"> <Button variant="outline" size="sm" className="shadow-xs" disabled={isSubmitting} onClick={handleSaveDraft} > <span>{draftSaved ? 'Draft Saved!' : 'Save as Draft'}</span> </Button> {draftSaved && ( <span className="text-success flex items-center gap-1 text-xs"> <Check className="size-3" /> Saved locally </span> )} </div> <Button size="default" className="w-full gap-2 shadow-xs sm:w-auto" disabled={isSubmitting || !description.trim()} onClick={handleSubmit} > {!isSubmitting ? <Wrench className="size-4" /> : <RefreshCw className="size-4 animate-spin" />} <span>{isSubmitting ? 'Dispatching Work Order...' : 'Submit Maintenance Request'}</span> </Button> </CardFooter> </Card> )} </div> {/* Right Column: Emergency & SLA Sidebar (4 cols) */} <div className="space-y-6 lg:sticky lg:top-6 lg:col-span-4"> {/* 24/7 Emergency Maintenance Hotline Card */} <Card className="border-destructive/40 bg-destructive/5 dark:bg-destructive/10 shadow-xs"> <CardHeader className="pb-3"> <div className="text-destructive flex items-center gap-2"> <ShieldAlert className="size-5 shrink-0" /> <CardTitle className="text-destructive text-base">24/7 Emergency Hotline</CardTitle> </div> <CardDescription className="text-foreground/80 text-xs"> For urgent situations threatening life safety, gas leaks, or active flooding. </CardDescription> </CardHeader> <CardContent className="space-y-4"> <div className="bg-card border-destructive/30 space-y-1 rounded-lg border p-3 text-center"> <p className="text-muted-foreground text-xs font-medium">Immediate Dispatch Phone</p> <p className="text-foreground font-mono text-xl font-semibold tracking-tight">(415) 555-0192</p> <p className="text-muted-foreground text-xs">Facility Operations On-Call Desk</p> </div> <div className="space-y-2"> <p className="text-foreground text-xs font-semibold">What qualifies as an emergency:</p> <ul className="text-muted-foreground space-y-1.5 text-xs"> <li className="flex items-start gap-2"> <span className="text-destructive font-bold">•</span> <span>Active uncontrolled water leaks or burst pipes</span> </li> <li className="flex items-start gap-2"> <span className="text-destructive font-bold">•</span> <span>Smell of natural gas or carbon monoxide alert</span> </li> <li className="flex items-start gap-2"> <span className="text-destructive font-bold">•</span> <span>Complete electrical power loss in unit</span> </li> <li className="flex items-start gap-2"> <span className="text-destructive font-bold">•</span> <span>Inoperable exterior lock or door security issue</span> </li> <li className="flex items-start gap-2"> <span className="text-destructive font-bold">•</span> <span>Total loss of heating when temp is below 55°F</span> </li> </ul> </div> <Button asChild variant="destructive" className="w-full gap-2 shadow-xs"> <a href="tel:4155550192"> <PhoneCall className="size-4" /> <span>Call Emergency Dispatch</span> </a> </Button> </CardContent> </Card> {/* Service Level Agreement (SLA) Card */} <Card className="border-border shadow-xs"> <CardHeader className="pb-3"> <div className="flex items-center gap-2"> <Clock className="text-primary size-4.5" /> <CardTitle className="text-base">Service Level Agreement (SLA)</CardTitle> </div> <CardDescription className="text-xs"> Response benchmarks for Pacific Heights property maintenance. </CardDescription> </CardHeader> <CardContent className="space-y-3.5"> <div className="bg-muted/40 border-border space-y-2 rounded-lg border p-3"> <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> <span className="bg-destructive size-2 rounded-full" /> <span className="text-foreground text-xs font-semibold">Emergency Priority</span> </div> <Badge variant="destructive" className="text-xs"> < 2 Hours </Badge> </div> <p className="text-muted-foreground text-xs"> 24/7/365 immediate dispatch with on-call technician response. </p> </div> <div className="bg-muted/40 border-border space-y-2 rounded-lg border p-3"> <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> <span className="bg-secondary-foreground size-2 rounded-full" /> <span className="text-foreground text-xs font-semibold">Standard Priority</span> </div> <Badge variant="secondary" className="text-xs"> 24–48 Hours </Badge> </div> <p className="text-muted-foreground text-xs"> Standard repairs scheduled Mon–Fri during normal operational hours. </p> </div> <div className="bg-muted/40 border-border space-y-2 rounded-lg border p-3"> <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> <span className="bg-muted-foreground size-2 rounded-full" /> <span className="text-foreground text-xs font-semibold">Routine / Preventative</span> </div> <Badge variant="outline" className="text-xs"> 3–5 Days </Badge> </div> <p className="text-muted-foreground text-xs"> Filter changes, caulking touch-ups, and scheduled inspections. </p> </div> <div className="border-border bg-card flex items-start gap-2.5 rounded-lg border p-3 text-xs"> <ShieldCheck className="text-primary mt-0.5 size-4 shrink-0" /> <p className="text-muted-foreground leading-relaxed"> Status notifications are automatically sent via SMS and resident email as work orders progress. </p> </div> </CardContent> </Card> {/* Building Operations Info Card */} <Card className="border-border shadow-xs"> <CardHeader className="pb-3"> <div className="flex items-center gap-2"> <Building2 className="text-muted-foreground size-4.5" /> <CardTitle className="text-base">Building Operations</CardTitle> </div> </CardHeader> <CardContent className="space-y-2.5 text-xs"> <div className="border-border/60 flex justify-between border-b py-1"> <span className="text-muted-foreground">Building Super:</span> <span className="text-foreground font-medium">Marcus Vance (Office #102)</span> </div> <div className="border-border/60 flex justify-between border-b py-1"> <span className="text-muted-foreground">Service Window:</span> <span className="text-foreground font-medium">Mon – Sat · 8:00 AM – 6:00 PM</span> </div> <div className="border-border/60 flex justify-between border-b py-1"> <span className="text-muted-foreground">Quiet Hours:</span> <span className="text-foreground font-medium">10:00 PM – 8:00 AM</span> </div> <div className="flex justify-between py-1"> <span className="text-muted-foreground">Resident Portal ID:</span> <span className="text-foreground font-mono font-medium">PH-RES-4B</span> </div> </CardContent> </Card> </div> </div> </div> ) } export default MaintenanceRequestTicket
Raw manifest:https://uipkge.dev/r/react/maintenance-request-ticket.json