Spaces:
Build error
Build error
| 'use client' | |
| import { useState } from 'react' | |
| interface ChecklistItemProps { | |
| id: string | |
| itemName: string | |
| status: string | |
| owner: { id: string; name: string } | null | |
| phase: string | |
| onUpdate: () => void | |
| } | |
| export function InteractiveChecklistItem({ | |
| id, | |
| itemName, | |
| status, | |
| owner, | |
| phase, | |
| onUpdate, | |
| }: ChecklistItemProps) { | |
| const [isEditing, setIsEditing] = useState(false) | |
| const [editName, setEditName] = useState(itemName) | |
| const [isUpdating, setIsUpdating] = useState(false) | |
| const handleStatusChange = async (newStatus: string) => { | |
| setIsUpdating(true) | |
| try { | |
| const response = await fetch(`/api/checklist/${id}`, { | |
| method: 'PATCH', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ status: newStatus }), | |
| }) | |
| if (response.ok) { | |
| onUpdate() | |
| } | |
| } catch (error) { | |
| console.error('Failed to update status:', error) | |
| } finally { | |
| setIsUpdating(false) | |
| } | |
| } | |
| const handleNameUpdate = async () => { | |
| if (editName.trim() === itemName) { | |
| setIsEditing(false) | |
| return | |
| } | |
| setIsUpdating(true) | |
| try { | |
| const response = await fetch(`/api/checklist/${id}`, { | |
| method: 'PATCH', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ itemName: editName.trim() }), | |
| }) | |
| if (response.ok) { | |
| setIsEditing(false) | |
| onUpdate() | |
| } | |
| } catch (error) { | |
| console.error('Failed to update name:', error) | |
| } finally { | |
| setIsUpdating(false) | |
| } | |
| } | |
| const handleDelete = async () => { | |
| if (!confirm('Delete this checklist item?')) return | |
| setIsUpdating(true) | |
| try { | |
| const response = await fetch(`/api/checklist/${id}`, { | |
| method: 'DELETE', | |
| }) | |
| if (response.ok) { | |
| onUpdate() | |
| } | |
| } catch (error) { | |
| console.error('Failed to delete item:', error) | |
| } finally { | |
| setIsUpdating(false) | |
| } | |
| } | |
| const statusOptions = ['Not Started', 'In Progress', 'Blocked', 'Complete'] | |
| return ( | |
| <div | |
| className={`flex items-center gap-4 p-4 rounded-xl transition-all ${ | |
| status === 'Complete' | |
| ? 'bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200' | |
| : status === 'In Progress' | |
| ? 'bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200' | |
| : status === 'Blocked' | |
| ? 'bg-gradient-to-r from-red-50 to-pink-50 border border-red-200' | |
| : 'bg-white border border-gray-200' | |
| } ${isUpdating ? 'opacity-50' : ''}`} | |
| > | |
| {/* Status Dropdown */} | |
| <select | |
| value={status} | |
| onChange={(e) => handleStatusChange(e.target.value)} | |
| disabled={isUpdating} | |
| className="px-3 py-2 rounded-lg border-2 border-gray-300 focus:border-blue-500 focus:outline-none bg-white text-sm font-medium cursor-pointer hover:border-blue-400 transition-all" | |
| > | |
| {statusOptions.map((s) => ( | |
| <option key={s} value={s}> | |
| {s === 'Not Started' ? '⚪' : s === 'In Progress' ? '🔵' : s === 'Blocked' ? '🔴' : '✅'} {s} | |
| </option> | |
| ))} | |
| </select> | |
| {/* Item Name - Editable */} | |
| <div className="flex-1"> | |
| {isEditing ? ( | |
| <div className="flex items-center gap-2"> | |
| <input | |
| type="text" | |
| value={editName} | |
| onChange={(e) => setEditName(e.target.value)} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter') handleNameUpdate() | |
| if (e.key === 'Escape') { | |
| setEditName(itemName) | |
| setIsEditing(false) | |
| } | |
| }} | |
| className="flex-1 px-3 py-2 rounded-lg border-2 border-blue-500 focus:outline-none" | |
| autoFocus | |
| /> | |
| <button | |
| onClick={handleNameUpdate} | |
| className="px-3 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 text-sm" | |
| > | |
| Save | |
| </button> | |
| <button | |
| onClick={() => { | |
| setEditName(itemName) | |
| setIsEditing(false) | |
| }} | |
| className="px-3 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 text-sm" | |
| > | |
| Cancel | |
| </button> | |
| </div> | |
| ) : ( | |
| <div | |
| className="flex items-center gap-2 cursor-pointer group" | |
| onClick={() => setIsEditing(true)} | |
| > | |
| <span className={`${status === 'Complete' ? 'line-through text-gray-500' : 'text-gray-900'}`}> | |
| {itemName} | |
| </span> | |
| <span className="opacity-0 group-hover:opacity-100 text-xs text-gray-400">✏️ Click to edit</span> | |
| </div> | |
| )} | |
| </div> | |
| {/* Owner */} | |
| <div className="text-sm text-gray-600 min-w-[120px]"> | |
| <span className="text-xs text-gray-500">Owner:</span> {owner?.name || 'Unassigned'} | |
| </div> | |
| {/* Delete Button */} | |
| <button | |
| onClick={handleDelete} | |
| disabled={isUpdating} | |
| className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-all" | |
| title="Delete item" | |
| > | |
| 🗑️ | |
| </button> | |
| </div> | |
| ) | |
| } | |
| interface AddChecklistItemProps { | |
| rfpId: string | |
| phase: string | |
| onAdd: () => void | |
| } | |
| export function AddChecklistItem({ rfpId, phase, onAdd }: AddChecklistItemProps) { | |
| const [isAdding, setIsAdding] = useState(false) | |
| const [newItemName, setNewItemName] = useState('') | |
| const [isSubmitting, setIsSubmitting] = useState(false) | |
| const handleAdd = async () => { | |
| if (!newItemName.trim()) return | |
| setIsSubmitting(true) | |
| try { | |
| const response = await fetch('/api/checklist', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| rfpId, | |
| itemName: newItemName.trim(), | |
| phase, | |
| status: 'Not Started', | |
| }), | |
| }) | |
| if (response.ok) { | |
| setNewItemName('') | |
| setIsAdding(false) | |
| onAdd() | |
| } | |
| } catch (error) { | |
| console.error('Failed to add item:', error) | |
| } finally { | |
| setIsSubmitting(false) | |
| } | |
| } | |
| if (!isAdding) { | |
| return ( | |
| <button | |
| onClick={() => setIsAdding(true)} | |
| className="w-full p-3 border-2 border-dashed border-gray-300 rounded-xl text-gray-600 hover:border-blue-400 hover:text-blue-600 hover:bg-blue-50/50 transition-all text-sm font-medium" | |
| > | |
| + Add Custom Item to {phase} | |
| </button> | |
| ) | |
| } | |
| return ( | |
| <div className="flex items-center gap-2 p-3 border-2 border-blue-300 rounded-xl bg-blue-50/50"> | |
| <input | |
| type="text" | |
| value={newItemName} | |
| onChange={(e) => setNewItemName(e.target.value)} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter') handleAdd() | |
| if (e.key === 'Escape') { | |
| setNewItemName('') | |
| setIsAdding(false) | |
| } | |
| }} | |
| placeholder="Enter checklist item name..." | |
| className="flex-1 px-3 py-2 rounded-lg border-2 border-gray-300 focus:border-blue-500 focus:outline-none" | |
| autoFocus | |
| /> | |
| <button | |
| onClick={handleAdd} | |
| disabled={isSubmitting || !newItemName.trim()} | |
| className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 text-sm font-medium" | |
| > | |
| Add | |
| </button> | |
| <button | |
| onClick={() => { | |
| setNewItemName('') | |
| setIsAdding(false) | |
| }} | |
| className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 text-sm" | |
| > | |
| Cancel | |
| </button> | |
| </div> | |
| ) | |
| } | |