- Fixed proxy configuration in vite.config.js (servicemanager-backend -> backend) - Added X-Tenant-ID header to client-profile API calls - Improved error handling with proper authentication checks - Updated profile page UI to white theme (removed icons and colors) - Changed all btn-primary buttons to white theme styling - Enhanced API call error handling in tickets store
335 lines
9.0 KiB
TypeScript
335 lines
9.0 KiB
TypeScript
import type { Writable } from 'svelte/store';
|
|
import { get, writable } from 'svelte/store';
|
|
import { auth } from './auth';
|
|
|
|
// Types
|
|
interface FastAPIValidationError {
|
|
loc: (string | number)[];
|
|
msg: string;
|
|
type: string;
|
|
}
|
|
|
|
export interface Ticket {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
status: 'NEW' | 'IN_PROGRESS' | 'WAITING_FOR_CLIENT' | 'RESOLVED' | 'CLOSED' | 'REOPENED';
|
|
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
|
category_id: string;
|
|
category_name?: string;
|
|
client_id: string;
|
|
assigned_to_id: string | null;
|
|
assigned_to_name?: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
due_date: string | null;
|
|
resolution: string | null;
|
|
}
|
|
|
|
export interface TicketComment {
|
|
id: string;
|
|
ticket_id: string;
|
|
author_id: string;
|
|
author_name: string;
|
|
author_role: string;
|
|
content: string;
|
|
is_internal: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface TicketAttachment {
|
|
id: string;
|
|
ticket_id: string;
|
|
filename: string;
|
|
original_filename: string;
|
|
mime_type: string;
|
|
size_bytes: number;
|
|
uploaded_by_id: string;
|
|
uploaded_by_name: string;
|
|
uploaded_at: string;
|
|
}
|
|
|
|
export interface CreateTicketRequest {
|
|
title: string;
|
|
description: string;
|
|
category_id: string;
|
|
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
|
}
|
|
|
|
export interface TicketsState {
|
|
tickets: Ticket[];
|
|
currentTicket: Ticket | null;
|
|
comments: TicketComment[];
|
|
attachments: TicketAttachment[];
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
// Initial state
|
|
const initialState: TicketsState = {
|
|
tickets: [],
|
|
currentTicket: null,
|
|
comments: [],
|
|
attachments: [],
|
|
isLoading: false,
|
|
error: null
|
|
};
|
|
|
|
// API helper function
|
|
async function apiCall(endpoint: string, options: RequestInit = {}) {
|
|
const authState = get(auth);
|
|
|
|
if (!authState.token || !authState.user) {
|
|
throw new Error('Not authenticated');
|
|
}
|
|
|
|
const response = await fetch(`/api/v1${endpoint}`, {
|
|
...options,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${authState.token}`,
|
|
'X-Tenant-ID': authState.user.tenant_id,
|
|
...options.headers
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let errorMessage = 'Request failed';
|
|
try {
|
|
const error = await response.json();
|
|
console.error('❌ API Error Response:', error);
|
|
|
|
// Manejar diferentes formatos de error de FastAPI
|
|
if (error.detail) {
|
|
if (Array.isArray(error.detail)) {
|
|
// Errores de validación de FastAPI
|
|
errorMessage = error.detail.map((e: FastAPIValidationError) => `${e.loc.join('.')}: ${e.msg}`).join(', ');
|
|
} else if (typeof error.detail === 'string') {
|
|
errorMessage = error.detail;
|
|
} else {
|
|
errorMessage = JSON.stringify(error.detail);
|
|
}
|
|
} else {
|
|
errorMessage = JSON.stringify(error);
|
|
}
|
|
} catch (e) {
|
|
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
}
|
|
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
// Create tickets store
|
|
function createTicketsStore() {
|
|
const { subscribe, set, update }: Writable<TicketsState> = writable(initialState);
|
|
|
|
return {
|
|
subscribe,
|
|
|
|
// Load user's tickets
|
|
loadTickets: async () => {
|
|
update((state: TicketsState) => ({ ...state, isLoading: true, error: null }));
|
|
|
|
try {
|
|
const tickets = await apiCall('/tickets/');
|
|
update((state: TicketsState) => ({ ...state, tickets, isLoading: false }));
|
|
} catch (error) {
|
|
update((state: TicketsState) => ({
|
|
...state,
|
|
isLoading: false,
|
|
error: error instanceof Error ? error.message : 'Failed to load tickets'
|
|
}));
|
|
}
|
|
},
|
|
|
|
// Load specific ticket with details
|
|
loadTicket: async (ticketId: string) => {
|
|
update((state: TicketsState) => ({ ...state, isLoading: true, error: null }));
|
|
|
|
try {
|
|
const [ticket, comments, attachments] = await Promise.all([
|
|
apiCall(`/tickets/${ticketId}`),
|
|
apiCall(`/tickets/${ticketId}/comments`),
|
|
apiCall(`/tickets/${ticketId}/attachments`)
|
|
]);
|
|
|
|
update((state: TicketsState) => ({
|
|
...state,
|
|
currentTicket: ticket,
|
|
comments,
|
|
attachments,
|
|
isLoading: false
|
|
}));
|
|
} catch (error) {
|
|
update((state: TicketsState) => ({
|
|
...state,
|
|
isLoading: false,
|
|
error: error instanceof Error ? error.message : 'Failed to load ticket'
|
|
}));
|
|
}
|
|
},
|
|
// Reload only comments silently (for polling)
|
|
reloadComments: async (ticketId: string) => {
|
|
try {
|
|
const comments = await apiCall(`/tickets/${ticketId}/comments`);
|
|
update((state: TicketsState) => ({ ...state, comments }));
|
|
} catch (error) {
|
|
// Silent fail - no mostrar error en polling
|
|
console.error('Error reloading comments:', error);
|
|
}
|
|
},
|
|
|
|
|
|
// Create new ticket
|
|
createTicket: async (ticket: CreateTicketRequest) => {
|
|
update((state: TicketsState) => ({ ...state, isLoading: true, error: null }));
|
|
|
|
try {
|
|
// Mapear campos del frontend al formato del backend
|
|
const ticketData = {
|
|
subject: ticket.title, // ← Backend espera "subject" no "title"
|
|
description: ticket.description,
|
|
category_id: ticket.category_id,
|
|
priority: ticket.priority,
|
|
system_id: null // ← Opcional
|
|
};
|
|
|
|
|
|
console.log('Sending ticket data:', ticketData);
|
|
|
|
const newTicket = await apiCall('/tickets/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(ticketData)
|
|
});
|
|
|
|
update((state: TicketsState) => ({
|
|
...state,
|
|
tickets: [newTicket, ...state.tickets],
|
|
isLoading: false
|
|
}));
|
|
|
|
return newTicket;
|
|
} catch (error) {
|
|
console.error('Create ticket error:', error);
|
|
update((state: TicketsState) => ({
|
|
...state,
|
|
isLoading: false,
|
|
error: error instanceof Error ? error.message : 'Failed to create ticket'
|
|
}));
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Add comment to ticket
|
|
addComment: async (ticketId: string, content: string) => {
|
|
try {
|
|
const comment = await apiCall(`/tickets/${ticketId}/comments`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ content })
|
|
});
|
|
|
|
update((state: TicketsState) => ({
|
|
...state,
|
|
comments: [...state.comments, comment]
|
|
}));
|
|
|
|
return comment;
|
|
} catch (error) {
|
|
update((state: TicketsState) => ({
|
|
...state,
|
|
error: error instanceof Error ? error.message : 'Failed to add comment'
|
|
}));
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Upload attachment
|
|
uploadAttachment: async (ticketId: string, file: File) => {
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const authState = get(auth);
|
|
|
|
if (!authState.token || !authState.user) {
|
|
throw new Error('Not authenticated');
|
|
}
|
|
|
|
const response = await fetch(`/api/v1/tickets/${ticketId}/attachments`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${authState.token}`,
|
|
'X-Tenant-ID': authState.user.tenant_id
|
|
},
|
|
body: formData
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Upload failed');
|
|
}
|
|
|
|
const result = await response.json();
|
|
const attachment = result.data || result;
|
|
|
|
update((state: TicketsState) => ({
|
|
...state,
|
|
attachments: [...state.attachments, attachment]
|
|
}));
|
|
|
|
return attachment;
|
|
} catch (error) {
|
|
update((state: TicketsState) => ({
|
|
...state,
|
|
error: error instanceof Error ? error.message : 'Failed to upload attachment'
|
|
}));
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Close ticket (client can close their own tickets)
|
|
closeTicket: async (ticketId: string, resolution?: string) => {
|
|
try {
|
|
const updatedTicket = await apiCall(`/tickets/${ticketId}/close`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ resolution })
|
|
});
|
|
|
|
update((state: TicketsState) => ({
|
|
...state,
|
|
currentTicket: state.currentTicket?.id === ticketId ? updatedTicket : state.currentTicket,
|
|
tickets: state.tickets.map((t: Ticket) => t.id === ticketId ? updatedTicket : t)
|
|
}));
|
|
|
|
return updatedTicket;
|
|
} catch (error) {
|
|
update((state: TicketsState) => ({
|
|
...state,
|
|
error: error instanceof Error ? error.message : 'Failed to close ticket'
|
|
}));
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Clear error
|
|
clearError: () => {
|
|
update((state: TicketsState) => ({ ...state, error: null }));
|
|
},
|
|
|
|
// Clear current ticket
|
|
clearCurrentTicket: () => {
|
|
update((state: TicketsState) => ({
|
|
...state,
|
|
currentTicket: null,
|
|
comments: [],
|
|
attachments: []
|
|
}));
|
|
}
|
|
};
|
|
}
|
|
|
|
export const tickets = createTicketsStore(); |