# Issue Report Form

> A compact issue reporting form for team members with categories, impact/urgency levels, and file attachments.

A shadcn/ui block from the [@manifest](https://ui.manifest.build) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/manifest/issue-report-form).

- Page, with a live preview: https://shadcn.exchange/r/manifest/issue-report-form
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/manifest/issue-report-form-light-db8fa9a24a1d636e.png
- Registry item: https://ui.manifest.build/r/issue-report-form.json
- Categories: form
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.manifest.build/r/issue-report-form.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `button`, `input`, `label`, `select`

## Files

### `registry/form/issue-report-form.tsx`

````tsx
'use client'

import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue
} from '@/components/ui/select'
import { cn } from '@/lib/utils'
import { demoIssueReportFormData } from './demo/form'
import { ChevronDown, ChevronUp, Paperclip, Send, X } from 'lucide-react'
import { useRef, useState } from 'react'


/**
 * ═══════════════════════════════════════════════════════════════════════════
 * IssueReportFormProps
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * Props for the IssueReportForm component for IT support, help desk, or
 * internal ticketing systems.
 */
export interface IssueReportFormProps {
  data?: {
    /** Form title displayed at the top. */
    title?: string
    /** List of team options for the dropdown. */
    teams?: string[]
    /** List of location options. */
    locations?: string[]
    /** Category to subcategory mapping. */
    categories?: Record<string, string[]>
    /** Impact level options. */
    impacts?: { value: string; label: string }[]
    /** Urgency level options. */
    urgencies?: { value: string; label: string }[]
    /** Frequency options. */
    frequencies?: { value: string; label: string }[]
    /** Pre-defined actions user may have tried. */
    attemptedActions?: string[]
  }
  actions?: {
    /** Called when the form is submitted. */
    onSubmit?: (formData: IssueFormData) => void
  }
  appearance?: {
    /**
     * Whether to display the title.
     * @default true
     */
    showTitle?: boolean
    /**
     * Use compact layout.
     * @default true
     */
    compactMode?: boolean
  }
}

/**
 * Data structure representing an issue report submission.
 * @interface IssueFormData
 * @property {string} declarantName - Name of the person reporting the issue
 * @property {string} email - Contact email for follow-up
 * @property {string} team - Department or team affected
 * @property {string} location - Office location
 * @property {string} office - Specific office or area identifier
 * @property {string} workstation - Machine or workstation identifier
 * @property {string} category - Main issue category (e.g., 'Software', 'Hardware')
 * @property {string} subcategory - Specific subcategory within the main category
 * @property {string} issueTitle - Brief summary of the issue
 * @property {string} description - Detailed description of the problem
 * @property {string} impact - Impact level (critical, high, medium, low)
 * @property {string} urgency - How urgent the fix is needed
 * @property {string} frequency - How often the issue occurs
 * @property {string} startDate - When the issue first occurred
 * @property {string[]} attemptedActions - List of troubleshooting actions already tried
 * @property {File[]} attachments - Supporting files (screenshots, logs, etc.)
 * @property {string} additionalComments - Any extra information
 */
export interface IssueFormData {
  declarantName?: string
  email?: string
  team?: string
  location?: string
  office?: string
  workstation?: string
  category?: string
  subcategory?: string
  issueTitle?: string
  description?: string
  impact?: string
  urgency?: string
  frequency?: string
  startDate?: string
  attemptedActions?: string[]
  attachments?: File[]
  additionalComments?: string
}

/**
 * A comprehensive issue reporting form for IT support, help desk, or internal ticketing systems.
 * Includes categorization, impact assessment, and file attachments.
 *
 * Features:
 * - Reporter information (name, email)
 * - Team and location selection
 * - Category with dynamic subcategories
 * - Impact, urgency, and frequency assessment
 * - Collapsible sections for detailed context
 * - Pre-defined troubleshooting actions checklist
 * - Multiple file attachment support
 * - Office/workstation identification
 *
 * @component
 * @example
 * ```tsx
 * <IssueReportForm
 *   data={{
 *     title: "Report a Problem",
 *     teams: ["Engineering", "Design", "Product"],
 *     categories: {
 *       "Software": ["Email", "Browser", "VPN"],
 *       "Hardware": ["Computer", "Monitor", "Keyboard"]
 *     }
 *   }}
 *   actions={{
 *     onSubmit: (data) => console.log("Issue reported:", data)
 *   }}
 *   appearance={{ showTitle: true, compactMode: true }}
 * />
 * ```
 */
export function IssueReportForm({
  data,
  actions,
  appearance
}: IssueReportFormProps) {
  const resolved: NonNullable<IssueReportFormProps['data']> = data ?? demoIssueReportFormData
  const title = resolved.title
  const teams = resolved.teams ?? []
  const locations = resolved.locations ?? []
  const categories = resolved.categories ?? {}
  const impacts = resolved.impacts ?? []
  const urgencies = resolved.urgencies ?? []
  const frequencies = resolved.frequencies ?? []
  const attemptedActions = resolved.attemptedActions ?? []
  const { onSubmit } = actions ?? {}
  const { showTitle = true } = appearance ?? {}

  const [formData, setFormData] = useState<IssueFormData>({
    declarantName: '',
    email: '',
    team: '',
    location: '',
    office: '',
    workstation: '',
    category: '',
    subcategory: '',
    issueTitle: '',
    description: '',
    impact: '',
    urgency: '',
    frequency: '',
    startDate: '',
    attemptedActions: [],
    attachments: [],
    additionalComments: ''
  })

  const [expandedSection, setExpandedSection] = useState<
    'details' | 'context' | null
  >('details')
  const fileInputRef = useRef<HTMLInputElement>(null)

  const subcategories = formData.category
    ? categories[formData.category] || []
    : []

  const updateField = <K extends keyof IssueFormData>(
    field: K,
    value: IssueFormData[K]
  ) => {
    setFormData((prev) => ({ ...prev, [field]: value }))
  }

  const handleCategoryChange = (value: string) => {
    setFormData((prev) => ({ ...prev, category: value, subcategory: '' }))
  }

  const toggleAttemptedAction = (action: string) => {
    setFormData((prev) => {
      const currentActions = prev.attemptedActions ?? []
      return {
        ...prev,
        attemptedActions: currentActions.includes(action)
          ? currentActions.filter((a) => a !== action)
          : [...currentActions, action]
      }
    })
  }

  const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = Array.from(e.target.files || [])
    setFormData((prev) => ({
      ...prev,
      attachments: [...(prev.attachments ?? []), ...files]
    }))
    if (fileInputRef.current) fileInputRef.current.value = ''
  }

  const removeFile = (index: number) => {
    setFormData((prev) => ({
      ...prev,
      attachments: (prev.attachments ?? []).filter((_, i) => i !== index)
    }))
  }

  const handleSubmit = () => {
    onSubmit?.(formData)
  }

  const toggleSection = (section: 'details' | 'context') => {
    setExpandedSection((prev) => (prev === section ? null : section))
  }

  return (
    <div className="w-full bg-card rounded-xl p-4">
      {showTitle && title && (
        <div className="flex items-center gap-2 mb-4">
          <h2 className="text-lg font-semibold text-foreground">{title}</h2>
        </div>
      )}

      <div className="space-y-3">
        {/* Declarant Info - Always visible */}
        <div className="grid grid-cols-2 gap-2">
          <div>
            <Label className="text-xs text-muted-foreground mb-1 block">
              Name
            </Label>
            <Input
              placeholder="Your name"
              value={formData.declarantName}
              onChange={(e) => updateField('declarantName', e.target.value)}
              className="h-9 text-sm"
            />
          </div>
          <div>
            <Label className="text-xs text-muted-foreground mb-1 block">
              Email
            </Label>
            <Input
              type="email"
              placeholder="your@email.com"
              value={formData.email}
              onChange={(e) => updateField('email', e.target.value)}
              className="h-9 text-sm"
            />
          </div>
        </div>

        {/* Team, Location, Category, Subcategory - 2 cols on mobile, 4 on desktop */}
        <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
          <div className="min-w-0">
            <Label className="text-xs text-muted-foreground mb-1 block">
              Team
            </Label>
            <Select
              value={formData.team}
              onValueChange={(v) => updateField('team', v)}
            >
              <SelectTrigger className="h-9 text-sm w-full">
                <SelectValue placeholder="Select" />
              </SelectTrigger>
              <SelectContent>
                {teams.map((team) => (
                  <SelectItem key={team} value={team}>
                    {team}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>
          <div className="min-w-0">
            <Label className="text-xs text-muted-foreground mb-1 block">
              Location
            </Label>
            <Select
              value={formData.location}
              onValueChange={(v) => updateField('location', v)}
            >
              <SelectTrigger className="h-9 text-sm w-full">
                <SelectValue placeholder="Select" />
              </SelectTrigger>
              <SelectContent>
                {locations.map((loc) => (
                  <SelectItem key={loc} value={loc}>
                    {loc}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>
          <div className="min-w-0">
            <Label className="text-xs text-muted-foreground mb-1 block">
              Category
            </Label>
            <Select
              value={formData.category}
              onValueChange={handleCategoryChange}
            >
              <SelectTrigger className="h-9 text-sm w-full">
                <SelectValue placeholder="Select" />
              </SelectTrigger>
              <SelectContent>
                {Object.keys(categories).map((cat) => (
                  <SelectItem key={cat} value={cat}>
                    {cat}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>
          <div className="min-w-0">
            <Label className="text-xs text-muted-foreground mb-1 block">
              Subcategory
            </Label>
            <Select
              value={formData.subcategory}
              onValueChange={(v) => updateField('subcategory', v)}
              disabled={!formData.category}
            >
              <SelectTrigger className="h-9 text-sm w-full">
                <SelectValue
                  placeholder={formData.category ? 'Select' : 'Pick category'}
                />
              </SelectTrigger>
              <SelectContent>
                {subcategories.map((sub) => (
                  <SelectItem key={sub} value={sub}>
                    {sub}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>
        </div>

        {/* Issue Title */}
        <div>
          <Label className="text-xs text-muted-foreground mb-1 block">
            Issue Title
          </Label>
          <Input
            placeholder="Summarize your issue in a few words"
            value={formData.issueTitle}
            onChange={(e) => updateField('issueTitle', e.target.value)}
            className="h-9 text-sm"
          />
        </div>

        {/* Description */}
        <div>
          <Label className="text-xs text-muted-foreground mb-1 block">
            Description
          </Label>
          <textarea
            placeholder="Describe the issue in detail..."
            value={formData.description}
            onChange={(e) => updateField('description', e.target.value)}
            className="w-full px-3 py-2 text-sm border rounded-lg resize-none focus:outline-none focus:border-primary bg-background min-h-[80px]"
          />
        </div>

        {/* Collapsible: Details Section */}
        <div className="border rounded-lg overflow-hidden">
          <button
            onClick={() => toggleSection('details')}
            className="w-full px-3 py-2 flex items-center justify-between text-sm font-medium text-foreground bg-muted/50 hover:bg-muted transition-colors"
          >
            <span>Impact & Urgency</span>
            {expandedSection === 'details' ? (
              <ChevronUp className="h-4 w-4 text-muted-foreground" />
            ) : (
              <ChevronDown className="h-4 w-4 text-muted-foreground" />
            )}
          </button>
          <div
            className={cn(
              'overflow-hidden transition-all duration-200',
              expandedSection === 'details' ? 'max-h-[500px] p-3' : 'max-h-0'
            )}
          >
            <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
              <div className="min-w-0">
                <Label className="text-xs text-muted-foreground mb-1 block">
                  Impact
                </Label>
                <Select
                  value={formData.impact}
                  onValueChange={(v) => updateField('impact', v)}
                >
                  <SelectTrigger className="h-9 text-sm w-full">
                    <SelectValue placeholder="Impact" />
                  </SelectTrigger>
                  <SelectContent>
                    {impacts.map((imp) => (
                      <SelectItem key={imp.value} value={imp.value}>
                        {imp.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>
              <div className="min-w-0">
                <Label className="text-xs text-muted-foreground mb-1 block">
                  Urgency
                </Label>
                <Select
                  value={formData.urgency}
                  onValueChange={(v) => updateField('urgency', v)}
                >
                  <SelectTrigger className="h-9 text-sm w-full">
                    <SelectValue placeholder="Urgency" />
                  </SelectTrigger>
                  <SelectContent>
                    {urgencies.map((urg) => (
                      <SelectItem key={urg.value} value={urg.value}>
                        {urg.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>
              <div className="min-w-0">
                <Label className="text-xs text-muted-foreground mb-1 block">
                  Frequency
                </Label>
                <Select
                  value={formData.frequency}
                  onValueChange={(v) => updateField('frequency', v)}
                >
                  <SelectTrigger className="h-9 text-sm w-full">
                    <SelectValue placeholder="Frequency" />
                  </SelectTrigger>
                  <SelectContent>
                    {frequencies.map((freq) => (
                      <SelectItem key={freq.value} value={freq.value}>
                        {freq.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>
              <div className="min-w-0">
                <Label className="text-xs text-muted-foreground mb-1 block">
                  Start Date
                </Label>
                <Input
                  type="date"
                  value={formData.startDate}
                  onChange={(e) => updateField('startDate', e.target.value)}
                  className="h-9 text-sm w-full"
                />
              </div>
            </div>
          </div>
        </div>

        {/* Collapsible: Context Section */}
        <div className="border rounded-lg overflow-hidden">
          <button
            onClick={() => toggleSection('context')}
            className="w-full px-3 py-2 flex items-center justify-between text-sm font-medium text-foreground bg-muted/50 hover:bg-muted transition-colors"
          >
            <span>Additional Context</span>
            {expandedSection === 'context' ? (
              <ChevronUp className="h-4 w-4 text-muted-foreground" />
            ) : (
              <ChevronDown className="h-4 w-4 text-muted-foreground" />
            )}
          </button>
          <div
            className={cn(
              'overflow-hidden transition-all duration-200',
              expandedSection === 'context' ? 'max-h-[500px] p-3' : 'max-h-0'
            )}
          >
            <div className="space-y-3">
              <div>
                <Label className="text-xs text-muted-foreground mb-2 block">
                  Actions Already Tried
                </Label>
                <div className="flex flex-wrap gap-1.5">
                  {attemptedActions.map((action) => (
                    <button
                      key={action}
                      onClick={() => toggleAttemptedAction(action)}
                      className={cn(
                        'px-2 py-1 text-xs rounded-md border transition-colors',
                        (formData.attemptedActions ?? []).includes(action)
                          ? 'bg-primary text-primary-foreground border-primary'
                          : 'bg-background text-foreground border-border hover:border-primary'
                      )}
                    >
                      {action}
                    </button>
                  ))}
                </div>
              </div>

              <div className="grid grid-cols-2 gap-2">
                {/* Left column: Office and Workstation stacked */}
                <div className="space-y-2">
                  <div>
                    <Label className="text-xs text-muted-foreground mb-1 block">
                      Office / Area
                    </Label>
                    <Input
                      placeholder="E.g.: Office 3B"
                      value={formData.office}
                      onChange={(e) => updateField('office', e.target.value)}
                      className="h-9 text-sm"
                    />
                  </div>
                  <div>
                    <Label className="text-xs text-muted-foreground mb-1 block">
                      Workstation / Machine
                    </Label>
                    <Input
                      placeholder="E.g.: PC-DEV-042"
                      value={formData.workstation}
                      onChange={(e) =>
                        updateField('workstation', e.target.value)
                      }
                      className="h-9 text-sm"
                    />
                  </div>
                </div>
                {/* Right column: Comments matching height of left column */}
                <div className="flex flex-col">
                  <Label className="text-xs text-muted-foreground mb-1 block">
                    Comments
                  </Label>
                  <textarea
                    placeholder="Additional information..."
                    value={formData.additionalComments}
                    onChange={(e) =>
                      updateField('additionalComments', e.target.value)
                    }
                    className="flex-1 w-full px-3 py-2 text-sm border rounded-lg resize-none focus:outline-none focus:border-primary bg-background"
                  />
                </div>
              </div>
            </div>
          </div>
        </div>

        {/* Attachments */}
        <div>
          <input
            ref={fileInputRef}
            type="file"
            multiple
            onChange={handleFileSelect}
            className="hidden"
          />
          {(formData.attachments ?? []).length > 0 && (
            <div className="flex flex-wrap gap-1.5 mb-2">
              {(formData.attachments ?? []).map((file, index) => (
                <div
                  key={index}
                  className="flex items-center gap-1 px-2 py-1 bg-muted rounded text-xs"
                >
                  <Paperclip className="h-3 w-3" />
                  <span className="max-w-[100px] truncate">{file.name}</span>
                  <button
                    onClick={() => removeFile(index)}
                    aria-label="Remove file"
                    className="text-muted-foreground hover:text-foreground"
                  >
                    <X className="h-3 w-3" />
                  </button>
                </div>
              ))}
            </div>
          )}
        </div>

        {/* Actions */}
        <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 pt-2">
          <Button
            variant="outline"
            size="sm"
            onClick={() => fileInputRef.current?.click()}
            className="h-9 w-full sm:w-auto"
          >
            <Paperclip className="h-4 w-4 mr-1.5" />
            Attach a file
          </Button>
          <Button
            onClick={handleSubmit}
            size="sm"
            className="h-9 w-full sm:w-auto"
          >
            <Send className="h-4 w-4 mr-1.5" />
            Submit
          </Button>
        </div>
      </div>
    </div>
  )
}
````

### `registry/form/demo/form.ts`

```ts
// Demo data for Form category components
// This file contains sample data used for component previews and documentation

/**
 * Generate available dates dynamically so the DateTimePicker preview
 * always has clickable dates regardless of when it is viewed.
 */
function generateAvailableDates(): Date[] {
  const dates: Date[] = [];
  const now = new Date();
  // Walk through current and next month, only keeping weekdays (Mon-Fri)
  for (let m = 0; m <= 1; m++) {
    for (let d = 1; d <= 28; d += 2) {
      const date = new Date(now.getFullYear(), now.getMonth() + m, d);
      const day = date.getDay();
      if (day !== 0 && day !== 6) {
        dates.push(date);
      }
    }
  }
  return dates;
}

export const demoContactFormData = {
  title: 'Get in Touch',
  subtitle: "We'd love to hear from you. Fill out the form below.",
  submitLabel: 'Send Message',
}

export const demoIssueReportFormData = {
  title: 'Report an Issue',
  teams: ['Engineering', 'Product', 'Design', 'Marketing', 'Operations'],
  locations: ['New York - HQ', 'San Francisco', 'London', 'Remote'],
  categories: {
    Software: ['Business App', 'Email', 'VPN', 'Browser', 'OS'],
    Hardware: ['Computer', 'Monitor', 'Keyboard', 'Mouse', 'Printer'],
    Network: ['Wi-Fi', 'Ethernet', 'VPN Access'],
    Access: ['Account', 'Permissions', 'Password Reset'],
  } as Record<string, string[]>,
  impacts: [
    { value: 'critical', label: 'Critical - Work stopped' },
    { value: 'high', label: 'High - Major feature broken' },
    { value: 'medium', label: 'Medium - Workaround available' },
    { value: 'low', label: 'Low - Minor inconvenience' },
  ],
  urgencies: [
    { value: 'immediate', label: 'Immediate' },
    { value: 'today', label: 'Today' },
    { value: 'this-week', label: 'This week' },
    { value: 'no-rush', label: 'No rush' },
  ],
  frequencies: [
    { value: 'constant', label: 'Constant' },
    { value: 'frequent', label: 'Frequent' },
    { value: 'occasional', label: 'Occasional' },
    { value: 'once', label: 'Happened once' },
  ],
  attemptedActions: [
    'Restarted the application',
    'Cleared browser cache',
    'Restarted computer',
    'Checked internet connection',
    'Contacted a colleague',
  ],
}

export const demoDateTimePickerData = {
  title: 'Select a Date & Time',
  availableDates: generateAvailableDates(),
  availableTimeSlots: [
    '9:00am',
    '10:00am',
    '11:30am',
    '1:00pm',
    '2:30pm',
    '4:00pm',
  ],
  timezone: 'Eastern Time - US & Canada',
};
```
