Spaces:
Sleeping
Sleeping
| import React, { useState, useRef } from "react"; | |
| import { Plus, Edit, Trash, CalendarDays, Eye, Printer } from "lucide-react"; | |
| import { format, parseISO } from "date-fns"; | |
| import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Label } from "@/components/ui/label"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Textarea } from "@/components/ui/textarea"; | |
| import { Calendar } from "@/components/ui/calendar"; | |
| import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; | |
| import { | |
| Table, | |
| TableBody, | |
| TableCell, | |
| TableHead, | |
| TableHeader, | |
| TableRow | |
| } from "@/components/ui/table"; | |
| import { toast } from "@/lib/custom-toast"; | |
| import { cn } from "@/lib/utils"; | |
| import { | |
| ReleaseDate, | |
| ReleaseDateCreateRequest, | |
| ReleaseDateUpdateRequest, | |
| ProjectApi | |
| } from "@/types"; | |
| import { releaseDateService, projectService } from "@/lib/api"; | |
| import CustomDialog from "@/components/CustomDialog"; | |
| import SlateRichTextEditor from "@/components/custom/SlateRichTextEditor"; | |
| interface ReleaseDetailsSectionProps { | |
| projectId: number; | |
| } | |
| export default function ReleaseDetailsSection({ projectId }: ReleaseDetailsSectionProps) { | |
| const queryClient = useQueryClient(); | |
| const [open, setOpen] = useState(false); | |
| const [viewMode, setViewMode] = useState(false); | |
| const [editMode, setEditMode] = useState(false); | |
| const [printMode, setPrintMode] = useState(false); | |
| const printRef = useRef<HTMLDivElement>(null); | |
| const [formData, setFormData] = useState<ReleaseDateCreateRequest>({ | |
| projectId: projectId, | |
| releaseName: '', | |
| releaseDateValue: new Date().toISOString(), | |
| description: '' | |
| }); | |
| const [selectedId, setSelectedId] = useState<number | null>(null); | |
| const [releaseDateOpen, setReleaseDateOpen] = useState(false); | |
| // Fetch project details to get the project name | |
| const { data: project } = useQuery({ | |
| queryKey: ['project', projectId], | |
| queryFn: () => projectService.getById(projectId), | |
| enabled: !!projectId | |
| }); | |
| // Get project name | |
| const projectName = project?.projectName || `Project #${projectId}`; | |
| // Fetch release dates for the project | |
| const { data: releaseDates = [], isLoading } = useQuery({ | |
| queryKey: ['releaseDates', projectId], | |
| queryFn: () => releaseDateService.getByProjectId(projectId), | |
| enabled: !!projectId | |
| }); | |
| // Create mutation | |
| const createMutation = useMutation({ | |
| mutationFn: (data: ReleaseDateCreateRequest) => releaseDateService.create(data), | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: ['releaseDates', projectId] }); | |
| toast.success('Release date added successfully'); | |
| resetForm(); | |
| setOpen(false); | |
| }, | |
| onError: (error) => { | |
| console.error('Error creating release date:', error); | |
| toast.error('Failed to add release date'); | |
| } | |
| }); | |
| // Update mutation | |
| const updateMutation = useMutation({ | |
| mutationFn: (data: ReleaseDateUpdateRequest) => releaseDateService.update(data), | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: ['releaseDates', projectId] }); | |
| toast.success('Release date updated successfully'); | |
| resetForm(); | |
| setOpen(false); | |
| }, | |
| onError: (error) => { | |
| console.error('Error updating release date:', error); | |
| toast.error('Failed to update release date'); | |
| } | |
| }); | |
| // Delete mutation | |
| const deleteMutation = useMutation({ | |
| mutationFn: (id: number) => releaseDateService.delete(id), | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: ['releaseDates', projectId] }); | |
| toast.success('Release date deleted successfully'); | |
| }, | |
| onError: (error) => { | |
| console.error('Error deleting release date:', error); | |
| toast.error('Failed to delete release date'); | |
| } | |
| }); | |
| // Handle view button click | |
| const handleView = (item: ReleaseDate) => { | |
| setViewMode(true); | |
| setEditMode(false); | |
| setSelectedId(item.releaseId); | |
| setFormData({ | |
| projectId: item.projectId, | |
| releaseName: item.releaseName, | |
| releaseDateValue: item.releaseDateValue, | |
| description: item.description | |
| }); | |
| setOpen(true); | |
| }; | |
| // Reset form data | |
| const resetForm = () => { | |
| setFormData({ | |
| projectId: projectId, | |
| releaseName: '', | |
| releaseDateValue: new Date().toISOString(), | |
| description: '' | |
| }); | |
| setEditMode(false); | |
| setViewMode(false); | |
| setSelectedId(null); | |
| }; | |
| // Handle submit | |
| const handleSubmit = (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!formData.releaseName.trim()) { | |
| toast.error('Release Name is required'); | |
| return; | |
| } | |
| if (editMode && selectedId) { | |
| updateMutation.mutate({ | |
| ...formData, | |
| releaseId: selectedId | |
| } as ReleaseDateUpdateRequest); | |
| } else { | |
| createMutation.mutate(formData); | |
| } | |
| }; | |
| // Handle edit button click | |
| const handleEdit = (item: ReleaseDate) => { | |
| setEditMode(true); | |
| setSelectedId(item.releaseId); | |
| setFormData({ | |
| projectId: item.projectId, | |
| releaseName: item.releaseName, | |
| releaseDateValue: item.releaseDateValue, | |
| description: item.description | |
| }); | |
| setOpen(true); | |
| }; | |
| // Handle delete button click | |
| const handleDelete = (id: number) => { | |
| if (confirm('Are you sure you want to delete this release date?')) { | |
| deleteMutation.mutate(id); | |
| } | |
| }; | |
| // Handle form input changes | |
| const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { | |
| const { name, value } = e.target; | |
| setFormData(prev => ({ ...prev, [name]: value })); | |
| }; | |
| // Handle date changes | |
| const handleDateChange = (date: Date | undefined) => { | |
| if (date) { | |
| setFormData(prev => ({ ...prev, releaseDateValue: date.toISOString() })); | |
| } | |
| }; | |
| // Format date for display | |
| const formatDate = (dateString: string) => { | |
| try { | |
| return format(parseISO(dateString), 'PPP'); | |
| } catch (error) { | |
| console.error('Error formatting date:', error); | |
| return 'Invalid date'; | |
| } | |
| }; | |
| // Handle print button click | |
| const handlePrint = () => { | |
| setPrintMode(true); | |
| // Create a new window for printing instead of modifying the current document | |
| const printWindow = window.open('', '_blank'); | |
| if (printWindow && printRef.current) { | |
| // Get the HTML content directly to ensure all elements are included | |
| const printContents = printRef.current.innerHTML; | |
| const totalItems = releaseDates.length; | |
| printWindow.document.write(` | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>${projectName} - Release Dates</title> | |
| <style> | |
| @page { margin: 1cm; } | |
| body { | |
| font-family: Arial, sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| .print-container { | |
| width: 100%; | |
| padding: 20px; | |
| } | |
| .header { | |
| display: flex; | |
| flex-direction: column; | |
| margin-bottom: 20px; | |
| border-bottom: 1px solid #ccc; | |
| padding-bottom: 10px; | |
| } | |
| .header h1 { | |
| margin: 0 0 10px 0; | |
| } | |
| .header .project-name { | |
| font-size: 16px; | |
| font-weight: 500; | |
| color: #555; | |
| margin-bottom: 5px; | |
| word-break: break-word; | |
| } | |
| .content-info { | |
| margin-bottom: 20px; | |
| font-style: italic; | |
| color: #666; | |
| } | |
| .release-item { | |
| margin-bottom: 25px; | |
| page-break-inside: avoid; | |
| border-bottom: 1px solid #eee; | |
| padding-bottom: 20px; | |
| } | |
| .release-item h3 { | |
| border-bottom: 1px solid #ccc; | |
| padding-bottom: 5px; | |
| margin-bottom: 15px; | |
| } | |
| @media print { | |
| .no-print { display: none; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="print-container"> | |
| <div class="header"> | |
| <h1>Release Dates</h1> | |
| <div class="project-name">${projectName}</div> | |
| </div> | |
| <div class="content-info"> | |
| Total release dates: ${totalItems} | |
| </div> | |
| <div class="print-content"> | |
| ${printContents} | |
| </div> | |
| <div class="no-print" style="margin-top: 30px; text-align: center;"> | |
| <button onclick="window.print()" style="padding: 8px 16px; margin-right: 10px;">Print</button> | |
| <button onclick="window.close()" style="padding: 8px 16px;">Close</button> | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |
| `); | |
| printWindow.document.close(); | |
| // Add event listeners | |
| printWindow.onafterprint = () => { | |
| setPrintMode(false); | |
| }; | |
| printWindow.onunload = () => { | |
| setPrintMode(false); | |
| }; | |
| // Use a longer delay to ensure content is properly rendered | |
| setTimeout(() => { | |
| // Log to debug | |
| console.log(`Preparing to print ${totalItems} release dates`); | |
| try { | |
| printWindow.focus(); | |
| printWindow.print(); | |
| } catch (error) { | |
| console.error("Print error:", error); | |
| setPrintMode(false); | |
| } | |
| }, 1200); | |
| } else { | |
| // If window couldn't be opened, reset the state | |
| setPrintMode(false); | |
| toast.error("Could not open print window. Please check your popup blocker settings."); | |
| } | |
| }; | |
| return ( | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <h3 className="text-lg font-medium">Release Dates</h3> | |
| <div className="flex gap-2"> | |
| <Button | |
| size="sm" | |
| variant="outline" | |
| onClick={handlePrint} | |
| className="flex items-center gap-1" | |
| > | |
| <Printer className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| size="sm" | |
| onClick={() => { | |
| resetForm(); | |
| setOpen(true); | |
| }} | |
| > | |
| <Plus className="h-4 w-4 mr-2" /> | |
| Add New | |
| </Button> | |
| </div> | |
| </div> | |
| <CustomDialog | |
| isOpen={open} | |
| onClose={() => setOpen(false)} | |
| title={viewMode ? "View Release Date" : `${editMode ? 'Edit' : 'Add'} Release Date`} | |
| > | |
| <form onSubmit={handleSubmit} className="space-y-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="releaseName">Release Name</Label> | |
| <Input | |
| id="releaseName" | |
| name="releaseName" | |
| value={formData.releaseName} | |
| onChange={handleInputChange} | |
| placeholder="e.g. Version 1.0, Milestone 2" | |
| required | |
| readOnly={viewMode} | |
| disabled={viewMode} | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="releaseDateValue">Release Date</Label> | |
| <Popover open={releaseDateOpen} onOpenChange={viewMode ? () => {} : setReleaseDateOpen}> | |
| <PopoverTrigger asChild> | |
| <Button | |
| variant="outline" | |
| className={cn( | |
| "w-full justify-start text-left font-normal", | |
| !formData.releaseDateValue && "text-muted-foreground" | |
| )} | |
| disabled={viewMode} | |
| > | |
| <CalendarDays className="mr-2 h-4 w-4" /> | |
| {formData.releaseDateValue ? formatDate(formData.releaseDateValue) : <span>Select date</span>} | |
| </Button> | |
| </PopoverTrigger> | |
| {!viewMode && ( | |
| <PopoverContent className="w-auto p-0"> | |
| <Calendar | |
| mode="single" | |
| selected={formData.releaseDateValue ? parseISO(formData.releaseDateValue) : undefined} | |
| onSelect={(date) => { | |
| handleDateChange(date); | |
| setReleaseDateOpen(false); | |
| }} | |
| initialFocus | |
| /> | |
| </PopoverContent> | |
| )} | |
| </Popover> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="description">Description</Label> | |
| <SlateRichTextEditor | |
| initialValue={formData.description} | |
| onChange={(value) => setFormData(prev => ({ ...prev, description: value }))} | |
| placeholder="Additional details or description" | |
| readOnly={viewMode} | |
| disabled={viewMode} | |
| minHeight={viewMode ? "150px" : "120px"} | |
| className={viewMode ? "min-h-[150px]" : ""} | |
| /> | |
| </div> | |
| <div className="flex justify-end gap-2 pt-2"> | |
| <Button type="button" variant="outline" onClick={() => setOpen(false)}> | |
| {viewMode ? 'Close' : 'Cancel'} | |
| </Button> | |
| {!viewMode && ( | |
| <Button type="submit"> | |
| {editMode ? 'Update' : 'Add'} | |
| </Button> | |
| )} | |
| </div> | |
| </form> | |
| </CustomDialog> | |
| {isLoading ? ( | |
| <div className="text-center py-4">Loading...</div> | |
| ) : releaseDates.length === 0 ? ( | |
| <div className="text-center py-8 border rounded-md bg-muted/20"> | |
| <CalendarDays className="h-8 w-8 mx-auto mb-2 text-muted-foreground" /> | |
| <p className="text-muted-foreground">No release dates have been added yet.</p> | |
| </div> | |
| ) : ( | |
| <> | |
| <div className={printMode ? "hidden" : ""}> | |
| <Table> | |
| <TableHeader> | |
| <TableRow> | |
| <TableHead>Release Name</TableHead> | |
| <TableHead>Release Date</TableHead> | |
| <TableHead className="max-w-[300px]">Description</TableHead> | |
| <TableHead className="text-right">Actions</TableHead> | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {releaseDates.map((item) => ( | |
| <TableRow key={item.releaseId}> | |
| <TableCell className="font-medium">{item.releaseName}</TableCell> | |
| <TableCell>{formatDate(item.releaseDateValue)}</TableCell> | |
| <TableCell className="max-w-[300px] overflow-hidden"> | |
| <div className="line-clamp-2" dangerouslySetInnerHTML={{ __html: item.description || '' }} /> | |
| </TableCell> | |
| <TableCell className="text-right"> | |
| <div className="flex justify-end gap-2"> | |
| <Button | |
| size="icon" | |
| variant="ghost" | |
| onClick={() => handleView(item)} | |
| > | |
| <Eye className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| size="icon" | |
| variant="ghost" | |
| onClick={() => handleEdit(item)} | |
| > | |
| <Edit className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| size="icon" | |
| variant="ghost" | |
| className="text-destructive" | |
| onClick={() => handleDelete(item.releaseId)} | |
| > | |
| <Trash className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </TableCell> | |
| </TableRow> | |
| ))} | |
| </TableBody> | |
| </Table> | |
| </div> | |
| {/* Printable version - hidden from normal view */} | |
| <div className="hidden"> | |
| <div ref={printRef} className="print-content"> | |
| {releaseDates.map((item, index) => ( | |
| <div key={item.releaseId} className="release-item" data-index={index}> | |
| <h3>{item.releaseName}</h3> | |
| <div style={{ marginBottom: "10px" }}> | |
| <div style={{ fontWeight: "bold" }}>Release Name:</div> | |
| <div style={{ marginLeft: "15px" }}>{item.releaseName}</div> | |
| </div> | |
| <div style={{ marginBottom: "10px" }}> | |
| <div style={{ fontWeight: "bold" }}>Release Date:</div> | |
| <div style={{ marginLeft: "15px" }}>{formatDate(item.releaseDateValue)}</div> | |
| </div> | |
| <div style={{ marginBottom: "10px" }}> | |
| <div style={{ fontWeight: "bold" }}>Description:</div> | |
| <div | |
| style={{ marginLeft: "15px" }} | |
| dangerouslySetInnerHTML={{ | |
| __html: item.description || "No description provided." | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| ); | |
| } |