pmtool / src /components /projects /ReleaseDetailsSection.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
18.2 kB
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" />
Print
</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>
);
}