Initial commit

This commit is contained in:
2026-01-12 08:17:17 -07:00
commit de5b6feef4
104 changed files with 12925 additions and 0 deletions

View File

@@ -0,0 +1,272 @@
import { writable } from 'svelte/store';
import { auth } from './auth.js';
import { get } from 'svelte/store';
import type { Writable } from 'svelte/store';
// Types
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;
user_id: string;
user_name: string;
user_role: string;
content: string;
is_internal: boolean;
created_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);
const response = await fetch(`/api/v1${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authState.token}`,
...options.headers
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Request failed');
}
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 => ({ ...state, isLoading: true, error: null }));
try {
const tickets = await apiCall('/tickets/');
update(state => ({ ...state, tickets, isLoading: false }));
} catch (error) {
update(state => ({
...state,
isLoading: false,
error: error instanceof Error ? error.message : 'Failed to load tickets'
}));
}
},
// Load specific ticket with details
loadTicket: async (ticketId: string) => {
update(state => ({ ...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 => ({
...state,
currentTicket: ticket,
comments,
attachments,
isLoading: false
}));
} catch (error) {
update(state => ({
...state,
isLoading: false,
error: error instanceof Error ? error.message : 'Failed to load ticket'
}));
}
},
// Create new ticket
createTicket: async (ticket: CreateTicketRequest) => {
update(state => ({ ...state, isLoading: true, error: null }));
try {
const newTicket = await apiCall('/tickets/', {
method: 'POST',
body: JSON.stringify(ticket)
});
update(state => ({
...state,
tickets: [newTicket, ...state.tickets],
isLoading: false
}));
return newTicket;
} catch (error) {
update(state => ({
...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 => ({
...state,
comments: [...state.comments, comment]
}));
return comment;
} catch (error) {
update(state => ({
...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);
const response = await fetch(`/api/v1/tickets/${ticketId}/attachments`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authState.token}`
},
body: formData
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Upload failed');
}
const attachment = await response.json();
update(state => ({
...state,
attachments: [...state.attachments, attachment]
}));
return attachment;
} catch (error) {
update(state => ({
...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 => ({
...state,
currentTicket: state.currentTicket?.id === ticketId ? updatedTicket : state.currentTicket,
tickets: state.tickets.map(t => t.id === ticketId ? updatedTicket : t)
}));
return updatedTicket;
} catch (error) {
update(state => ({
...state,
error: error instanceof Error ? error.message : 'Failed to close ticket'
}));
throw error;
}
},
// Clear error
clearError: () => {
update(state => ({ ...state, error: null }));
},
// Clear current ticket
clearCurrentTicket: () => {
update(state => ({
...state,
currentTicket: null,
comments: [],
attachments: []
}));
}
};
}
export const tickets = createTicketsStore();