feat: Enhance ticketing system with categories, templates, SLA tracking, and fix frontend integration
- Add ticket categories (billing, technical, feature_request, etc.) with type-specific options - Add TicketTemplate and CannedResponse models for quick ticket creation - Implement SLA tracking with due_at and first_response_at fields - Add is_platform_admin and is_customer helper functions to fix permission checks - Register models in Django admin with filters and fieldsets - Enhance signals with error handling for WebSocket notifications - Fix frontend API URLs for templates and canned responses - Update PlatformSupport page to use real ticketing API - Add comprehensive i18n translations for all ticket fields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,7 @@ export const useCurrentBusiness = () => {
|
||||
initialSetupComplete: data.initial_setup_complete,
|
||||
websitePages: data.website_pages || {},
|
||||
customerDashboardContent: data.customer_dashboard_content || [],
|
||||
paymentsEnabled: data.payments_enabled ?? false,
|
||||
// Platform-controlled permissions
|
||||
canManageOAuthCredentials: data.can_manage_oauth_credentials || false,
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import * as ticketsApi from '../api/tickets'; // Import all functions from the API service
|
||||
import { Ticket, TicketComment, User } from '../types'; // Assuming User type is also needed
|
||||
import { Ticket, TicketComment, TicketTemplate, CannedResponse, TicketStatus, TicketPriority, TicketCategory, TicketType } from '../types';
|
||||
|
||||
// Define interfaces for filters and mutation payloads if necessary
|
||||
interface TicketFilters {
|
||||
status?: Ticket['status'];
|
||||
type?: Ticket['ticketType'];
|
||||
assignee?: User['id'];
|
||||
creator?: User['id'];
|
||||
// Add other filters as needed
|
||||
status?: TicketStatus;
|
||||
priority?: TicketPriority;
|
||||
category?: TicketCategory;
|
||||
ticketType?: TicketType;
|
||||
assignee?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,26 +18,39 @@ export const useTickets = (filters?: TicketFilters) => {
|
||||
return useQuery<Ticket[]>({
|
||||
queryKey: ['tickets', filters],
|
||||
queryFn: async () => {
|
||||
// Construct query parameters from filters object
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.type) params.append('ticket_type', filters.type); // Backend expects 'ticket_type'
|
||||
if (filters?.assignee) params.append('assignee', String(filters.assignee));
|
||||
if (filters?.creator) params.append('creator', String(filters.creator));
|
||||
// Use the API filters
|
||||
const apiFilters: ticketsApi.TicketFilters = {};
|
||||
if (filters?.status) apiFilters.status = filters.status;
|
||||
if (filters?.priority) apiFilters.priority = filters.priority;
|
||||
if (filters?.category) apiFilters.category = filters.category;
|
||||
if (filters?.ticketType) apiFilters.ticketType = filters.ticketType;
|
||||
if (filters?.assignee) apiFilters.assignee = filters.assignee;
|
||||
|
||||
const { data } = await ticketsApi.getTickets(); // Pass params if API supported
|
||||
// Transform data to match frontend types if necessary (e.g., date strings to Date objects)
|
||||
const data = await ticketsApi.getTickets(apiFilters);
|
||||
// Transform data to match frontend types if necessary (e.g., snake_case to camelCase)
|
||||
return data.map((ticket: any) => ({
|
||||
...ticket,
|
||||
id: String(ticket.id),
|
||||
tenant: ticket.tenant ? String(ticket.tenant) : undefined,
|
||||
creator: String(ticket.creator),
|
||||
creatorEmail: ticket.creator_email,
|
||||
creatorFullName: ticket.creator_full_name,
|
||||
assignee: ticket.assignee ? String(ticket.assignee) : undefined,
|
||||
createdAt: new Date(ticket.created_at).toISOString(),
|
||||
updatedAt: new Date(ticket.updated_at).toISOString(),
|
||||
resolvedAt: ticket.resolved_at ? new Date(ticket.resolved_at).toISOString() : undefined,
|
||||
ticketType: ticket.ticket_type, // Map backend 'ticket_type' to frontend 'ticketType'
|
||||
commentText: ticket.comment_text, // Assuming this is from comments, if not, remove
|
||||
assigneeEmail: ticket.assignee_email,
|
||||
assigneeFullName: ticket.assignee_full_name,
|
||||
ticketType: ticket.ticket_type,
|
||||
status: ticket.status,
|
||||
priority: ticket.priority,
|
||||
subject: ticket.subject,
|
||||
description: ticket.description,
|
||||
category: ticket.category,
|
||||
relatedAppointmentId: ticket.related_appointment_id || undefined,
|
||||
dueAt: ticket.due_at,
|
||||
firstResponseAt: ticket.first_response_at,
|
||||
isOverdue: ticket.is_overdue,
|
||||
createdAt: ticket.created_at,
|
||||
updatedAt: ticket.updated_at,
|
||||
resolvedAt: ticket.resolved_at,
|
||||
comments: ticket.comments,
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -50,17 +63,30 @@ export const useTicket = (id: string | undefined) => {
|
||||
return useQuery<Ticket>({
|
||||
queryKey: ['tickets', id],
|
||||
queryFn: async () => {
|
||||
const { data } = await ticketsApi.getTicket(id as string);
|
||||
const ticket: any = await ticketsApi.getTicket(id as string);
|
||||
return {
|
||||
...data,
|
||||
id: String(data.id),
|
||||
tenant: data.tenant ? String(data.tenant) : undefined,
|
||||
creator: String(data.creator),
|
||||
assignee: data.assignee ? String(data.assignee) : undefined,
|
||||
createdAt: new Date(data.created_at).toISOString(),
|
||||
updatedAt: new Date(data.updated_at).toISOString(),
|
||||
resolvedAt: data.resolved_at ? new Date(data.resolved_at).toISOString() : undefined,
|
||||
ticketType: data.ticket_type,
|
||||
id: String(ticket.id),
|
||||
tenant: ticket.tenant ? String(ticket.tenant) : undefined,
|
||||
creator: String(ticket.creator),
|
||||
creatorEmail: ticket.creator_email,
|
||||
creatorFullName: ticket.creator_full_name,
|
||||
assignee: ticket.assignee ? String(ticket.assignee) : undefined,
|
||||
assigneeEmail: ticket.assignee_email,
|
||||
assigneeFullName: ticket.assignee_full_name,
|
||||
ticketType: ticket.ticket_type,
|
||||
status: ticket.status,
|
||||
priority: ticket.priority,
|
||||
subject: ticket.subject,
|
||||
description: ticket.description,
|
||||
category: ticket.category,
|
||||
relatedAppointmentId: ticket.related_appointment_id || undefined,
|
||||
dueAt: ticket.due_at,
|
||||
firstResponseAt: ticket.first_response_at,
|
||||
isOverdue: ticket.is_overdue,
|
||||
createdAt: ticket.created_at,
|
||||
updatedAt: ticket.updated_at,
|
||||
resolvedAt: ticket.resolved_at,
|
||||
comments: ticket.comments,
|
||||
};
|
||||
},
|
||||
enabled: !!id, // Only run query if ID is provided
|
||||
@@ -82,7 +108,7 @@ export const useCreateTicket = () => {
|
||||
// No need to send creator or tenant, backend serializer handles it
|
||||
};
|
||||
const response = await ticketsApi.createTicket(dataToSend);
|
||||
return response.data;
|
||||
return response;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tickets'] }); // Invalidate tickets list to refetch
|
||||
@@ -104,7 +130,7 @@ export const useUpdateTicket = () => {
|
||||
// creator, tenant, comments are read-only on update
|
||||
};
|
||||
const response = await ticketsApi.updateTicket(id, dataToSend);
|
||||
return response.data;
|
||||
return response;
|
||||
},
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tickets'] });
|
||||
@@ -137,8 +163,8 @@ export const useTicketComments = (ticketId: string | undefined) => {
|
||||
queryKey: ['ticketComments', ticketId],
|
||||
queryFn: async () => {
|
||||
if (!ticketId) return [];
|
||||
const { data } = await ticketsApi.getTicketComments(ticketId);
|
||||
return data.map((comment: any) => ({
|
||||
const comments = await ticketsApi.getTicketComments(ticketId);
|
||||
return comments.map((comment: any) => ({
|
||||
...comment,
|
||||
id: String(comment.id),
|
||||
ticket: String(comment.ticket),
|
||||
@@ -166,7 +192,7 @@ export const useCreateTicketComment = () => {
|
||||
// ticket and author are handled by backend serializer
|
||||
};
|
||||
const response = await ticketsApi.createTicketComment(ticketId, dataToSend);
|
||||
return response.data;
|
||||
return response;
|
||||
},
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ticketComments', variables.ticketId] }); // Invalidate comments for this ticket
|
||||
@@ -174,3 +200,77 @@ export const useCreateTicketComment = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch ticket templates
|
||||
*/
|
||||
export const useTicketTemplates = () => {
|
||||
return useQuery<TicketTemplate[]>({
|
||||
queryKey: ['ticketTemplates'],
|
||||
queryFn: async () => {
|
||||
const data = await ticketsApi.getTicketTemplates();
|
||||
return data.map((template: any) => ({
|
||||
id: String(template.id),
|
||||
tenant: template.tenant ? String(template.tenant) : undefined,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
ticketType: template.ticket_type,
|
||||
category: template.category,
|
||||
defaultPriority: template.default_priority,
|
||||
subjectTemplate: template.subject_template,
|
||||
descriptionTemplate: template.description_template,
|
||||
isActive: template.is_active,
|
||||
createdAt: template.created_at,
|
||||
}));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch a single ticket template by ID
|
||||
*/
|
||||
export const useTicketTemplate = (id: string | undefined) => {
|
||||
return useQuery<TicketTemplate>({
|
||||
queryKey: ['ticketTemplates', id],
|
||||
queryFn: async () => {
|
||||
const template: any = await ticketsApi.getTicketTemplate(id as string);
|
||||
return {
|
||||
id: String(template.id),
|
||||
tenant: template.tenant ? String(template.tenant) : undefined,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
ticketType: template.ticket_type,
|
||||
category: template.category,
|
||||
defaultPriority: template.default_priority,
|
||||
subjectTemplate: template.subject_template,
|
||||
descriptionTemplate: template.description_template,
|
||||
isActive: template.is_active,
|
||||
createdAt: template.created_at,
|
||||
};
|
||||
},
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch canned responses
|
||||
*/
|
||||
export const useCannedResponses = () => {
|
||||
return useQuery<CannedResponse[]>({
|
||||
queryKey: ['cannedResponses'],
|
||||
queryFn: async () => {
|
||||
const data = await ticketsApi.getCannedResponses();
|
||||
return data.map((response: any) => ({
|
||||
id: String(response.id),
|
||||
tenant: response.tenant ? String(response.tenant) : undefined,
|
||||
title: response.title,
|
||||
content: response.content,
|
||||
category: response.category,
|
||||
isActive: response.is_active,
|
||||
useCount: response.use_count,
|
||||
createdBy: response.created_by ? String(response.created_by) : undefined,
|
||||
createdAt: response.created_at,
|
||||
}));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiClient } from '../api/client';
|
||||
import apiClient from '../api/client';
|
||||
import { User } from '../types';
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user