diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9cb2d82..1effdd3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,6 +22,7 @@ model Ticket { updatedAt DateTime @updatedAt updates Update[] comments Comment[] + favorites Favorite[] } @@ -45,6 +46,13 @@ model Comment { ticket Ticket @relation(fields: [ticket_id], references: [ticket_id]) } +model Favorite { + favorite_id Int @id @default(autoincrement()) + userId String + ticketId Int + ticket Ticket @relation(fields: [ticketId], references: [ticket_id], onDelete: Cascade) + @@unique([userId, ticketId]) // Ensure each user can only favorite a ticket once +} // model PublicMessages { // message_id Int @id @default(autoincrement()) diff --git a/src/app/_components/createupdate.tsx b/src/app/_components/createupdate.tsx index 108ad23..9ca858f 100644 --- a/src/app/_components/createupdate.tsx +++ b/src/app/_components/createupdate.tsx @@ -6,9 +6,11 @@ import { api } from "~/trpc/react"; interface CreateUpdateProps { - ticketId: number, - updates: any, - setUpdates: any + + ticketId: number; + + + } @@ -19,6 +21,8 @@ const CreateUpdate: React.FC = ({ ticketId, updates, setUpdat const [status, setStatus] = useState(''); const createUpdateMutation = api.update.create.useMutation(); + const updateTicketStatusMutation = api.ticket.updateStatus.useMutation(); + if (!user?.publicMetadata?.isAdmin) { return null; } @@ -35,15 +39,26 @@ const CreateUpdate: React.FC = ({ ticketId, updates, setUpdat if (!description || !status) return; try { - const newupdate = await createUpdateMutation.mutateAsync({ - ticket_id: ticketId, - content: description, - status, - }); - setUpdates([...updates, newupdate]); - setDescription(''); - setStatus(''); - // Optionally, trigger a refetch of updates or update the state + + await createUpdateMutation.mutateAsync({ + ticket_id: ticketId, + content: description, + status, + }); + + + + await updateTicketStatusMutation.mutateAsync({ + ticket_id: ticketId, + newStatus: status + }) + + setDescription(''); + setStatus(''); + + // Optionally, trigger a refetch of updates or update the state + + } catch (error) { console.error('Error creating update:', error); } diff --git a/src/app/_components/ticketlist.tsx b/src/app/_components/ticketlist.tsx index 4e79d8c..8925411 100644 --- a/src/app/_components/ticketlist.tsx +++ b/src/app/_components/ticketlist.tsx @@ -2,6 +2,7 @@ "use client"; import { Ticket, Comment } from '@prisma/client'; +import { useQueryClient } from "@tanstack/react-query"; import Link from 'next/link'; import { useEffect, useState } from 'react'; import { api } from '~/trpc/react'; // Import trpc hook @@ -26,19 +27,69 @@ interface IUpdate { } +interface TicketWithFavorite extends Ticket { + isFavorited: boolean; +} + + const TicketList = () => { - const {user} = useUser(); // Use Clerk's hook to get user data + const { user, isLoaded, isSignedIn } = useUser(); + + + + // const [tickets, setTickets] = useState([]); + const [tickets, setTickets] = useState([]); + const [selectedTicket, setSelectedTicket] = useState(null); + // const { data, isLoading, isError, error } = api.ticket.getTickets.useQuery(); + const queryClient = useQueryClient(); - const [tickets, setTickets] = useState([]); - const { data, isLoading, isError, error } = api.ticket.getLatest.useQuery(); - - const [selectedTicket, setSelectedTicket] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [comments, setComments] = useState([]); const [updates, setUpdates] = useState([]); const [newComment, setNewComment] = useState(''); + + // const [data, setData] = useState(null); + // const [data, setData] = useState(null); + // const [isLoading, setIsLoading] = useState(false); + // const [error, setError] = useState(null); + + // const { data, isLoading, isError, error } = api.ticket.getLatestWithFavorites.useQuery({userId: user?.id.toString()}); + + const { data, isLoading, isError, error } = api.ticket.getLatestWithFavorites.useQuery( + { userId: user?.id ?? "" }, + { + enabled: isLoaded && !!user?.id, // Only run when `user.id` is defined and `useUser` is fully loaded + } + ); + + // const { data, isLoading, isError, error } = api.ticket.getLatestWithFavorites.useQuery( + // { userId: user?.id || "" }, + // { enabled: !!user } // Only run query if user is available + // ); + + // const fetchData = async () => { + // setIsLoading(true); + // setError(null); + + // try { + // // Create a caller instance to fetch data imperatively + // const caller = api.ticket.getLatestWithFavorites.createCaller(); + // const response = await caller({ userId: user?.id || "" }); + // setData(response || []); // Update data state with the fetched response or an empty array + // } catch (err) { + // setError("Failed to fetch data"); + // console.error("Error fetching data:", err); + // } finally { + // setIsLoading(false); + // } + // }; + + + + const toggleFavoriteMutation = api.ticket.toggleFavorite.useMutation(); + const handleCommentChange = (e: React.ChangeEvent) => { setNewComment(e.target.value); }; @@ -58,14 +109,16 @@ const TicketList = () => { // fullname: z.string().min(1), // imageUrl: z.string().min(1) - console.log({ - ticket_id: selectedTicket.ticket_id, - content: newComment, - user_id: user!.id, - fullname: (user?.firstName ?? '') + " " + (user?.lastName ?? ''), - imageUrl: user!.imageUrl - }) - const newcommentfromdb = await createCommentMutation.mutateAsync({ + + // console.log({ + // ticket_id: selectedTicket.ticket_id, + // content: newComment, + // user_id: user!.id, + // fullname: (user?.firstName ?? '') + " " + (user?.lastName ?? ''), + // imageUrl: user!.imageUrl + // }) + await createCommentMutation.mutateAsync({ + ticket_id: selectedTicket.ticket_id, content: newComment, user_id: user!.id, @@ -98,8 +151,18 @@ const TicketList = () => { } }, [data]); + const toggleFavorite = async (ticketId: number) => { + if (!user) return; + await toggleFavoriteMutation.mutateAsync({ + ticketId, + userId: user.id, + }); - const openModal = (ticket: Ticket) => { + + }; + + + const openModal = (ticket: TicketWithFavorite) => { setSelectedTicket(ticket); // @ts-expect-error: just forget about it setComments(ticket.comments as IComment[] | null); @@ -133,6 +196,28 @@ const TicketList = () => { className="bg-white p-6 rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-300" onClick={() => openModal(ticket)} > +
+

{ticket.title}

+ +

{ticket.title}

{ticket.content}

@@ -175,7 +260,7 @@ const TicketList = () => {
- {updates?.sort((a,b) => new Date(b.createdAt).getTime()- new Date(a.createdAt).getTime()).map((update: IUpdate) => ( + {updates?.sort((a,b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).map((update: IUpdate) => (
diff --git a/src/app/create-ticket/page.tsx b/src/app/create-ticket/page.tsx index 4353fcb..802655a 100644 --- a/src/app/create-ticket/page.tsx +++ b/src/app/create-ticket/page.tsx @@ -5,14 +5,29 @@ import { auth } from '@clerk/nextjs/server'; import { useUser } from '@clerk/nextjs'; // Import the Clerk hook import { useState } from 'react'; import { api } from '~/trpc/react'; // Import the trpc hook +import { useRouter } from 'next/navigation'; +import { useQueryClient } from '@tanstack/react-query'; const CreateTicketPage = () => { const { user, isLoaded, isSignedIn } = useUser(); // Clerk hook to get user data const [title, setTitle] = useState(''); const [content, setContent] = useState(''); + const router = useRouter(); // Initialize the router + const queryClient = useQueryClient(); // Initialize query client + // Get the mutate function for creating a ticket - const createTicketMutation = api.ticket.create.useMutation(); + const createTicketMutation = api.ticket.create.useMutation({ + onSuccess: () => { + // queryClient.invalidateQueries(api.ticket.getTickets.getQueryKey()); + router.push('/'); + }, + onError: (error) => { + // Optionally handle errors here + console.error('Error creating ticket:', error); + alert('Failed to create ticket'); + }, + }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -32,7 +47,6 @@ const CreateTicketPage = () => { fullname: (user?.firstName ?? '') + " " + (user?.lastName ?? ''), imageUrl: user?.imageUrl }); - alert('Ticket created successfully!'); } catch (error) { console.error('Error creating ticket:', error); alert('Failed to create ticket'); diff --git a/src/server/api/routers/ticket.ts b/src/server/api/routers/ticket.ts index eefeb8a..0129183 100644 --- a/src/server/api/routers/ticket.ts +++ b/src/server/api/routers/ticket.ts @@ -23,6 +23,24 @@ export const ticketRouter = createTRPCRouter({ }, }); }), + updateStatus: publicProcedure + .input( + z.object({ + ticket_id: z.number().int().positive(), + newStatus: z.string().min(1), + }) + ) + .mutation(async ({ ctx, input }) => { + const { ticket_id, newStatus } = input; + + // Update the ticket's status + const updatedTicket = await ctx.db.ticket.update({ + where: { ticket_id }, + data: { status: newStatus }, + }); + + return updatedTicket; + }), getLatest: publicProcedure.query(async ({ ctx }) => { const tickets = await ctx.db.ticket.findMany({ @@ -41,6 +59,69 @@ export const ticketRouter = createTRPCRouter({ }); return ticket ?? null; }), + getLatestWithFavorites: publicProcedure + .input(z.object({ userId: z.string().nonempty("User ID is required") })) + .query(async ({ ctx, input }) => { + const { userId } = input; + + // Fetch the latest tickets with their favorite status for the user + const tickets = await ctx.db.ticket.findMany({ + orderBy: { createdAt: "desc" }, + include: { + favorites: { + where: { userId }, // Check if the user has favorited this ticket + select: { favorite_id: true }, // Only select the favorite ID to determine if it's favorited + }, + comments: true, + updates: true + }, + }); + + // Map each ticket to include an `isFavorited` boolean based on the favorites array + return tickets.map(ticket => ({ + ...ticket, + isFavorited: ticket.favorites.length > 0, // If there are any favorites, the user has favorited this ticket + })); + }), + toggleFavorite: publicProcedure + .input( + z.object({ + ticketId: z.number().int(), + userId: z.string().nonempty("User ID is required"), + }) + ) + .mutation(async ({ ctx, input }) => { + const { ticketId, userId } = input; + + // Check if a favorite already exists for this user and ticket + const existingFavorite = await ctx.db.favorite.findUnique({ + where: { + userId_ticketId: { + userId, + ticketId, + }, + }, + }); + + if (existingFavorite) { + // If it exists, delete the favorite (unfavorite the ticket) + await ctx.db.favorite.delete({ + where: { + favorite_id: existingFavorite.favorite_id, + }, + }); + return { isFavorited: false }; // Return `isFavorited` status as false + } else { + // If it doesn't exist, create a new favorite + await ctx.db.favorite.create({ + data: { + userId, + ticketId, + }, + }); + return { isFavorited: true }; // Return `isFavorited` status as true + } + }), // getById: publicProcedure // .input(z.object({ ticketId: z.string() })) // Validate ticketId parameter // .query(async ({ ctx, input }) => {