feat: Add ticket permission UI and fix assignee dropdown
Assignee Dropdown: - Fix useUsers hook to fetch from /api/staff/ endpoint - Add useStaffForAssignment hook for formatted dropdown data - Update TicketModal to use new hook for assignee selection Staff Permissions UI: - Add "Can access support tickets" permission to invite modal for both managers and staff - Add permission to edit modal for both managers and staff - Managers default to having ticket access enabled - Staff default to having ticket access disabled (must be explicitly granted) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { X, User, Send, MessageSquare, Clock, AlertCircle } from 'lucide-react';
|
import { X, User, Send, MessageSquare, Clock, AlertCircle } from 'lucide-react';
|
||||||
import { Ticket, TicketComment, TicketStatus, TicketPriority, TicketCategory, TicketType } from '../types';
|
import { Ticket, TicketComment, TicketStatus, TicketPriority, TicketCategory, TicketType } from '../types';
|
||||||
import { useCreateTicket, useUpdateTicket, useTicketComments, useCreateTicketComment } from '../hooks/useTickets';
|
import { useCreateTicket, useUpdateTicket, useTicketComments, useCreateTicketComment } from '../hooks/useTickets';
|
||||||
import { useUsers } from '../hooks/useUsers'; // Assuming a useUsers hook exists to fetch users for assignee dropdown
|
import { useStaffForAssignment } from '../hooks/useUsers';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
interface TicketModalProps {
|
interface TicketModalProps {
|
||||||
@@ -34,7 +34,7 @@ const TicketModal: React.FC<TicketModalProps> = ({ ticket, onClose, defaultTicke
|
|||||||
const [isInternalComment, setIsInternalComment] = useState(false);
|
const [isInternalComment, setIsInternalComment] = useState(false);
|
||||||
|
|
||||||
// Fetch users for assignee dropdown
|
// Fetch users for assignee dropdown
|
||||||
const { data: users = [] } = useUsers(); // Assuming useUsers fetches all relevant users (staff/platform admins)
|
const { data: users = [] } = useStaffForAssignment();
|
||||||
|
|
||||||
// Fetch comments for the ticket if in detail/edit mode
|
// Fetch comments for the ticket if in detail/edit mode
|
||||||
const { data: comments, isLoading: isLoadingComments } = useTicketComments(ticket?.id);
|
const { data: comments, isLoading: isLoadingComments } = useTicketComments(ticket?.id);
|
||||||
|
|||||||
@@ -1,17 +1,68 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import apiClient from '../api/client';
|
import apiClient from '../api/client';
|
||||||
import { User } from '../types';
|
import { User } from '../types';
|
||||||
|
|
||||||
|
interface StaffUser {
|
||||||
|
id: number | string;
|
||||||
|
email: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
full_name: string;
|
||||||
|
role: string;
|
||||||
|
role_display: string;
|
||||||
|
is_active: boolean;
|
||||||
|
permissions: Record<string, boolean>;
|
||||||
|
has_resource: boolean;
|
||||||
|
resource_id?: string;
|
||||||
|
resource_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to fetch all users (staff, owners, customers) for the current business.
|
* Hook to fetch all staff members (owners, managers, staff) for the current business.
|
||||||
* This can be filtered/refined later based on specific needs (e.g., only staff).
|
* Used for assignee dropdowns in tickets and other features.
|
||||||
*/
|
*/
|
||||||
export const useUsers = () => {
|
export const useUsers = () => {
|
||||||
return useQuery<User[]>({
|
return useQuery<StaffUser[]>({
|
||||||
queryKey: ['businessUsers'],
|
queryKey: ['staff'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await apiClient.get('/api/business/users/');
|
const response = await apiClient.get('/api/staff/');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch staff members for assignee selection.
|
||||||
|
* Returns users formatted for dropdown use.
|
||||||
|
*/
|
||||||
|
export const useStaffForAssignment = () => {
|
||||||
|
return useQuery<{ id: string; name: string; email: string; role: string }[]>({
|
||||||
|
queryKey: ['staffForAssignment'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/staff/');
|
||||||
|
return response.data.map((user: StaffUser) => ({
|
||||||
|
id: String(user.id),
|
||||||
|
name: user.full_name || `${user.first_name} ${user.last_name}`.trim() || user.email,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role_display || user.role,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to update a staff member's permissions
|
||||||
|
*/
|
||||||
|
export const useUpdateStaffPermissions = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ userId, permissions }: { userId: string | number; permissions: Record<string, boolean> }) => {
|
||||||
|
const response = await apiClient.patch(`/api/staff/${userId}/`, { permissions });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['staff'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -661,6 +661,26 @@ const Staff: React.FC<StaffProps> = ({ onMasquerade, effectiveUser }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{/* Can Access Tickets */}
|
||||||
|
<label className="flex items-start gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={invitePermissions.can_access_tickets ?? true}
|
||||||
|
onChange={(e) =>
|
||||||
|
setInvitePermissions({ ...invitePermissions, can_access_tickets: e.target.checked })
|
||||||
|
}
|
||||||
|
className="w-4 h-4 mt-0.5 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{t('staff.canAccessTickets', 'Can access support tickets')}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
{t('staff.canAccessTicketsHint', 'View and manage customer support tickets')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -710,6 +730,26 @@ const Staff: React.FC<StaffProps> = ({ onMasquerade, effectiveUser }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{/* Can Access Tickets */}
|
||||||
|
<label className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={invitePermissions.can_access_tickets ?? false}
|
||||||
|
onChange={(e) =>
|
||||||
|
setInvitePermissions({ ...invitePermissions, can_access_tickets: e.target.checked })
|
||||||
|
}
|
||||||
|
className="w-4 h-4 mt-0.5 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{t('staff.canAccessTickets', 'Can access support tickets')}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
{t('staff.canAccessTicketsHint', 'View and manage customer support tickets')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -959,6 +999,26 @@ const Staff: React.FC<StaffProps> = ({ onMasquerade, effectiveUser }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{/* Can Access Tickets */}
|
||||||
|
<label className="flex items-start gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editPermissions.can_access_tickets ?? true}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditPermissions({ ...editPermissions, can_access_tickets: e.target.checked })
|
||||||
|
}
|
||||||
|
className="w-4 h-4 mt-0.5 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{t('staff.canAccessTickets', 'Can access support tickets')}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
{t('staff.canAccessTicketsHint', 'View and manage customer support tickets')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1008,6 +1068,26 @@ const Staff: React.FC<StaffProps> = ({ onMasquerade, effectiveUser }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{/* Can Access Tickets */}
|
||||||
|
<label className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editPermissions.can_access_tickets ?? false}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditPermissions({ ...editPermissions, can_access_tickets: e.target.checked })
|
||||||
|
}
|
||||||
|
className="w-4 h-4 mt-0.5 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{t('staff.canAccessTickets', 'Can access support tickets')}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
{t('staff.canAccessTicketsHint', 'View and manage customer support tickets')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user