Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions client/components/tickets/ticket-complete-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";

import { BookOpen, CheckCircle } from "lucide-react";
import { FormEvent, useState } from "react";

import Button from "../ui/button";
import LabeledIcon from "../ui/labeled-icon";
import Modal from "../ui/modal";

export interface TicketCompleteModalProps {
isOpen: boolean;
ticketTitle: string;
existingDocumentation?: string;
onClose: () => void;
onComplete: (documentation: string) => void;
isLoading?: boolean;
}

export default function TicketCompleteModal({
isOpen,
ticketTitle,
existingDocumentation = "",
onClose,
onComplete,
isLoading = false,
}: TicketCompleteModalProps) {
const [documentation, setDocumentation] = useState(existingDocumentation);

const handleSubmit = (e: FormEvent) => {
e.preventDefault();
onComplete(documentation);
};

const handleClose = () => {
setDocumentation(existingDocumentation); // Reset to original value
onClose();
};

return (
<Modal isOpen={isOpen} onClose={handleClose}>
<div className="max-w-2xl p-4">
<div className="mb-6">
<div className="flex items-center gap-2 mb-2">
<CheckCircle className="w-5 h-5 text-green-600" />
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200">
Complete Ticket
</h3>
</div>
<p className="text-gray-600 dark:text-gray-400 mb-2">
<strong>Ticket:</strong> {ticketTitle}
</p>
<p className="text-sm text-gray-500 dark:text-gray-500">
Before marking this ticket as complete, please document how you resolved it.
This helps other team members learn from your solution.
</p>
</div>

<form onSubmit={handleSubmit}>
<div className="mb-6">
<label className="flex items-center text-sm font-medium text-gray-800 dark:text-gray-300 mb-2">
<LabeledIcon className="mr-2" icon={<BookOpen className="w-4" />} label="Resolution Documentation" />
</label>
<textarea
value={documentation}
onChange={(e) => setDocumentation(e.target.value)}
placeholder="Describe how you resolved this ticket:&#10;• What was the root cause?&#10;• What steps did you take?&#10;• What solution worked?&#10;• Any preventive measures for the future?"
className="w-full p-3 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-300 rounded-md resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={8}
required
/>
<p className="text-xs text-gray-500 mt-1">
Documentation is required to complete the ticket
</p>
</div>

<div className="flex justify-end gap-3">
<Button
type="button"
className="bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-600 dark:text-white rounded"
onClick={handleClose}
disabled={isLoading}
>
Cancel
</Button>
<Button
type="submit"
className="bg-green-600 hover:bg-green-700 text-white rounded"
disabled={isLoading || !documentation.trim()}
>
{isLoading ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Completing...
</span>
) : (
<span className="flex items-center gap-2">
<CheckCircle className="w-4 h-4" />
Complete Ticket
</span>
)}
</Button>
</div>
</form>
</div>
</Modal>
);
}
97 changes: 87 additions & 10 deletions client/components/tickets/ticket-detail.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"use client";

import Link from "next/link";
import { Building, Calendar, PencilLine, SquareArrowUpRight, Tag, User } from "lucide-react";
import { Building, Calendar, FileText, PencilLine, SquareArrowUpRight, Tag, User, BookOpen } from "lucide-react";
import { MouseEvent, useEffect, useState } from "react";

import TicketAssignedTo from "./ticket-assigned-to";
import TicketEdit from "./ticket-edit";
import TicketCompleteModal from "./ticket-complete-modal";
import { useTicket, useUpdateTicket } from "@/lib/hooks/queries/use-tickets";
import { Status, Statuses } from "@/lib/types/ticket";
import Button from "../ui/button";
Expand All @@ -23,6 +24,8 @@ export default function TicketDetail({ ticketId, onDismiss, onUpdate }: TicketDe
const { mutate: updateTicket } = useUpdateTicket();
const [status, setStatus] = useState<Status>("Active");
const [isEditing, setIsEditing] = useState(false);
const [isCompletionModalOpen, setIsCompletionModalOpen] = useState(false);
const [isCompleting, setIsCompleting] = useState(false);

useEffect(() => {
if (ticket) {
Expand Down Expand Up @@ -58,7 +61,7 @@ export default function TicketDetail({ ticketId, onDismiss, onUpdate }: TicketDe
onSave={(updates) => {
setIsEditing(false);
updateTicket({ id: ticketId, updates });
setTimeout(onUpdate, 300);
onUpdate(); // Call immediately since cache is updated
}}
/>
);
Expand Down Expand Up @@ -91,7 +94,35 @@ export default function TicketDetail({ ticketId, onDismiss, onUpdate }: TicketDe

setStatus(updatedStatus);
updateTicket({ id: ticketId, updates: { status: updatedStatus } });
setTimeout(onUpdate, 300);
onUpdate(); // Call immediately since cache is updated
};

const handleCompleteTicket = () => {
setIsCompletionModalOpen(true);
};

const handleCompleteWithDocumentation = (documentation: string) => {
setIsCompleting(true);

// Use updateTicket instead of completeTicket to set both status and documentation
updateTicket({
id: ticketId,
updates: {
status: "Closed",
documentation: documentation
}
}, {
onSuccess: (updatedTicket) => {
setStatus("Closed");
setIsCompletionModalOpen(false);
setIsCompleting(false);
onUpdate(); // Call immediately since cache is updated
},
onError: (error) => {
console.error('Error updating ticket:', error);
setIsCompleting(false);
},
});
Comment on lines +108 to +125
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The completion flow bypasses the dedicated completion endpoint (/api/v1/tickets/{id}/complete) and uses the general update endpoint instead. This creates inconsistency - there's a specific completion API that's not being used. Consider using the completeTicket function or removing the unused completion endpoint.

Copilot uses AI. Check for mistakes.
};

return (
Expand All @@ -103,12 +134,22 @@ export default function TicketDetail({ ticketId, onDismiss, onUpdate }: TicketDe
>
&lt;-
</Button>
<Button
className="bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-600 dark:text-white rounded"
onClick={() => setIsEditing(true)}
>
<LabeledIcon icon={<PencilLine className="w-4" />} label="Edit" />
</Button>
<div className="flex gap-2">
{ticket.status !== "Closed" && (
<Button
className="bg-green-600 hover:bg-green-700 text-white rounded px-4 py-2 font-medium"
onClick={handleCompleteTicket}
>
Complete Ticket
</Button>
)}
<Button
className="bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-600 dark:text-white rounded"
onClick={() => setIsEditing(true)}
>
<LabeledIcon icon={<PencilLine className="w-4" />} label="Edit" />
</Button>
</div>
</div>
<p className="mt-3 mb-1 text-sm text-gray-600 dark:text-gray-500">TK {ticket.id}</p>
<div className="flex gap-5 items-center">
Expand All @@ -122,7 +163,33 @@ export default function TicketDetail({ ticketId, onDismiss, onUpdate }: TicketDe
</Dropdown>
<p className="text-xl font-bold text-gray-800 dark:text-gray-300">{ticket.title}</p>
</div>
<p className="my-4 text-gray-800 dark:text-gray-300">{ticket.description}</p>

{/* Description Section */}
<div className="my-6">
<div className="mb-2">
<LabeledIcon icon={<FileText className="w-5" />} label="Description" />
</div>
<div className="bg-white dark:bg-gray-900 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<p className="text-gray-800 dark:text-gray-300 whitespace-pre-wrap">
{ticket.description || "No description provided."}
</p>
</div>
</div>

{/* Documentation Section */}
<div className="my-6">
<div className="mb-2">
<LabeledIcon icon={<BookOpen className="w-5" />} label="Resolution Documentation" />
</div>
<div className="bg-white dark:bg-gray-900 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<p className="text-gray-800 dark:text-gray-300 whitespace-pre-wrap">
{ticket.documentation && ticket.documentation.trim()
? ticket.documentation
: "No resolution documentation provided yet."}
</p>
</div>
</div>

<table className="table-auto min-w-full">
<tbody>
<tr>
Expand Down Expand Up @@ -167,6 +234,16 @@ export default function TicketDetail({ ticketId, onDismiss, onUpdate }: TicketDe
</tr>
</tbody>
</table>

{/* Completion Modal */}
<TicketCompleteModal
isOpen={isCompletionModalOpen}
ticketTitle={ticket.title}
existingDocumentation={ticket.documentation || ""}
onClose={() => setIsCompletionModalOpen(false)}
onComplete={handleCompleteWithDocumentation}
isLoading={isCompleting}
/>
</div>
);
}
40 changes: 30 additions & 10 deletions client/components/tickets/ticket-edit.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { Building, Tag, User } from "lucide-react";
import { FormEvent, useState } from "react";
import { BookOpen, Building, Tag, User } from "lucide-react";
import { FormEvent, useState, useEffect } from "react";

import { useTicket, useUpdateTicket } from "@/lib/hooks/queries/use-tickets";
import { useToast } from "@/lib/hooks/use-toast";
Expand All @@ -20,9 +20,16 @@ export default function TicketEdit({ ticketId, onCancel, onSave }: TicketEditPro
const { data: ticket } = useTicket(ticketId);
const { mutate: updateTicket } = useUpdateTicket();

const [formData, setFormData] = useState<Partial<Ticket>>(ticket || {});
const [formData, setFormData] = useState<Partial<Ticket>>({});
const [isSaving, setIsSaving] = useState(false);

// Update form data when ticket data loads/changes
useEffect(() => {
if (ticket) {
setFormData(ticket);
}
}, [ticket]);

if (!ticket) {
let layout = <></>;

Expand Down Expand Up @@ -95,7 +102,7 @@ export default function TicketEdit({ ticketId, onCancel, onSave }: TicketEditPro
<p className="mb-2 text-sm text-gray-600 dark:text-gray-500">TK {ticket.id}</p>
<div className="mb-4">
<label className={`block ${labelStyle}`}>Status</label>
<select name="status" value={formData.status || ticket.status} onChange={handleChange} className={inputStyles}>
<select name="status" value={formData.status || ""} onChange={handleChange} className={inputStyles}>
{Statuses.map((status) => (
<option key={status} value={status}>
{status}
Expand All @@ -107,7 +114,7 @@ export default function TicketEdit({ ticketId, onCancel, onSave }: TicketEditPro
<label className={`block ${labelStyle}`}>Priority</label>
<select
name="priority"
value={formData.priority || ticket.priority}
value={formData.priority || ""}
onChange={handleChange}
className={inputStyles}
required
Expand All @@ -127,7 +134,7 @@ export default function TicketEdit({ ticketId, onCancel, onSave }: TicketEditPro
<input
type="text"
name="title"
value={formData.title}
value={formData.title || ""}
placeholder="Enter a title"
onChange={handleChange}
className={inputStyles}
Expand All @@ -138,13 +145,26 @@ export default function TicketEdit({ ticketId, onCancel, onSave }: TicketEditPro
<label className={`block ${labelStyle}`}>Description</label>
<textarea
name="description"
value={formData.description}
value={formData.description || ""}
placeholder="A descriptive ticket makes a good ticket."
onChange={handleChange}
className={inputStyles}
rows={4}
/>
</div>
<div className="mb-4">
<label className={`flex items-center ${labelStyle}`}>
<LabeledIcon className="mr-1" icon={<BookOpen className="w-4" />} label="Resolution Documentation" />
</label>
<textarea
name="documentation"
value={formData.documentation || ""}
placeholder="Document how this ticket was resolved, steps taken, solutions applied, etc."
onChange={handleChange}
className={inputStyles}
rows={4}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className={`flex items-center ${labelStyle}`}>
Expand All @@ -153,7 +173,7 @@ export default function TicketEdit({ ticketId, onCancel, onSave }: TicketEditPro
<input
type="text"
name="assignedTo"
value={formData.assignedTo}
value={formData.assignedTo || ""}
placeholder="Unassigned"
onChange={handleChange}
className={inputStyles}
Expand All @@ -163,7 +183,7 @@ export default function TicketEdit({ ticketId, onCancel, onSave }: TicketEditPro
<label className={`flex items-center ${labelStyle}`}>
<LabeledIcon className="mr-1" icon={<Building className="w-4" />} label="Site" />
</label>
<select name="site" value={formData.site || ticket.site} onChange={handleChange} className={inputStyles}>
<select name="site" value={formData.site || ""} onChange={handleChange} className={inputStyles}>
{Sites.map((site) => (
<option key={site} value={site}>
{site}
Expand All @@ -175,7 +195,7 @@ export default function TicketEdit({ ticketId, onCancel, onSave }: TicketEditPro
<label className={`flex items-center ${labelStyle}`}>
<LabeledIcon className="mr-1" icon={<Tag className="w-4" />} label="Category" />
</label>
<select name="category" value={formData.category || ticket.category} onChange={handleChange} className={inputStyles}>
<select name="category" value={formData.category || ""} onChange={handleChange} className={inputStyles}>
{Categories.map((category) => (
<option key={category} value={category}>
{category}
Expand Down
5 changes: 5 additions & 0 deletions client/lib/api/tickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ export async function updateTicket(id: string, updates: Partial<Ticket>) {
const { data: updatedTicket } = await server.put<Ticket>(`/tickets/${id}`, updates);
return updatedTicket;
}

export async function completeTicket(id: string) {
const { data: completedTicket } = await server.post<Ticket>(`/tickets/${id}/complete`);
return completedTicket;
}
Loading