485 lines
12 KiB
TypeScript
485 lines
12 KiB
TypeScript
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000";
|
|
|
|
async function fetchApi<T>(
|
|
path: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
const token =
|
|
typeof window !== "undefined" ? localStorage.getItem("helpdesk_token") : null;
|
|
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
...((options.headers as Record<string, string>) || {}),
|
|
};
|
|
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
|
|
|
if (!res.ok) {
|
|
const errorData = await res.json().catch(() => ({}));
|
|
throw new Error(errorData.message || `API Error: ${res.status}`);
|
|
}
|
|
|
|
if (res.status === 204) return undefined as T;
|
|
return res.json();
|
|
}
|
|
|
|
// ==================== Auth ====================
|
|
export async function login(idUsuario: string, password: string) {
|
|
return fetchApi<{ success: boolean; message?: string; token?: string; user?: HelpDeskUser }>(
|
|
"/api/helpdesk/login",
|
|
{ method: "POST", body: JSON.stringify({ idUsuario, password }) }
|
|
);
|
|
}
|
|
|
|
export async function getMe() {
|
|
return fetchApi<HelpDeskUser>("/api/helpdesk/me");
|
|
}
|
|
|
|
// ==================== Tickets ====================
|
|
export async function getTickets(params?: {
|
|
status?: string;
|
|
priority?: string;
|
|
assigneeId?: number;
|
|
requesterId?: number;
|
|
groupId?: number;
|
|
search?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}) {
|
|
const query = new URLSearchParams();
|
|
if (params) {
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== "" && value !== null)
|
|
query.set(key, String(value));
|
|
});
|
|
}
|
|
return fetchApi<TicketListResult>(`/api/helpdesk/tickets?${query.toString()}`);
|
|
}
|
|
|
|
export async function getTicket(id: number) {
|
|
return fetchApi<Ticket>(`/api/helpdesk/ticket/${id}`);
|
|
}
|
|
|
|
export async function createTicket(data: {
|
|
title: string;
|
|
description: string;
|
|
priority?: string;
|
|
assigneeId?: number;
|
|
groupId?: number;
|
|
slaPolicyId?: number;
|
|
}) {
|
|
return fetchApi<{ id: number }>("/api/helpdesk/tickets", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function updateTicket(id: number, data: {
|
|
title?: string;
|
|
description?: string;
|
|
priority?: string;
|
|
assigneeId?: number;
|
|
groupId?: number;
|
|
}) {
|
|
return fetchApi<void>(`/api/helpdesk/ticket/${id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function deleteTicket(id: number) {
|
|
return fetchApi<void>(`/api/helpdesk/ticket/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
export async function updateTicketStatus(id: number, status: string) {
|
|
return fetchApi<void>(`/api/helpdesk/ticket/${id}/status`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ status }),
|
|
});
|
|
}
|
|
|
|
export async function assignTicket(id: number, assigneeId: number, groupId?: number) {
|
|
return fetchApi<void>(`/api/helpdesk/ticket/${id}/assign`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ assigneeId, groupId }),
|
|
});
|
|
}
|
|
|
|
export async function getTicketStats() {
|
|
return fetchApi<DashboardStats>("/api/helpdesk/stats");
|
|
}
|
|
|
|
// ==================== Comments ====================
|
|
export async function getComments(ticketId: number) {
|
|
return fetchApi<Comment[]>(`/api/helpdesk/comments/${ticketId}`);
|
|
}
|
|
|
|
export async function addComment(
|
|
ticketId: number,
|
|
content: string,
|
|
isInternal: boolean = false
|
|
) {
|
|
return fetchApi<{ id: number }>(`/api/helpdesk/comments/${ticketId}`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ content, isInternal }),
|
|
});
|
|
}
|
|
|
|
// ==================== Knowledge Base ====================
|
|
export async function getKBArticles() {
|
|
return fetchApi<KBArticle[]>("/api/helpdesk/kb/published");
|
|
}
|
|
|
|
export async function searchKB(query: string) {
|
|
return fetchApi<KBArticle[]>(`/api/helpdesk/kb/search?q=${encodeURIComponent(query)}`);
|
|
}
|
|
|
|
export async function getKBCategories() {
|
|
return fetchApi<KBCategory[]>("/api/helpdesk/kb/categories");
|
|
}
|
|
|
|
// ==================== Groups & SLA ====================
|
|
export async function getGroups() {
|
|
return fetchApi<Group[]>("/api/helpdesk/groups");
|
|
}
|
|
|
|
export async function getSLAPolicies() {
|
|
return fetchApi<SLAPolicy[]>("/api/helpdesk/sla");
|
|
}
|
|
|
|
// ==================== Types ====================
|
|
export interface HelpDeskUser {
|
|
idUsuario: string;
|
|
nombres: string;
|
|
apPaterno: string;
|
|
apMaterno?: string;
|
|
email: string;
|
|
tipo?: string;
|
|
activo: number;
|
|
fono?: string;
|
|
idSede?: number;
|
|
role?: string;
|
|
departmentId?: number;
|
|
departmentName?: string;
|
|
}
|
|
|
|
export interface Ticket {
|
|
id: number;
|
|
title: string;
|
|
description: string;
|
|
status: string;
|
|
priority: string;
|
|
ticketNumber: string;
|
|
requesterId: number;
|
|
requesterName?: string;
|
|
requesterEmail?: string;
|
|
assigneeId?: number;
|
|
assigneeName?: string;
|
|
groupId?: number;
|
|
groupName?: string;
|
|
slaPolicyId?: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
firstResponseAt?: string;
|
|
resolvedAt?: string;
|
|
closedAt?: string;
|
|
}
|
|
|
|
export interface TicketListResult {
|
|
items: Ticket[];
|
|
total: number;
|
|
page: number;
|
|
pageSize: number;
|
|
totalPages: number;
|
|
}
|
|
|
|
export interface Comment {
|
|
id: number;
|
|
ticketId: number;
|
|
userId: string;
|
|
userName?: string;
|
|
content: string;
|
|
isInternal: boolean;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface Group {
|
|
id: number;
|
|
name: string;
|
|
description?: string;
|
|
isActive: boolean;
|
|
}
|
|
|
|
export interface KBArticle {
|
|
id: number;
|
|
title: string;
|
|
content: string;
|
|
categoryId: number;
|
|
categoryName?: string;
|
|
authorId: string;
|
|
authorName?: string;
|
|
isPublished: boolean;
|
|
viewCount: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface KBCategory {
|
|
id: number;
|
|
name: string;
|
|
description?: string;
|
|
sortOrder: number;
|
|
}
|
|
|
|
export interface SLAPolicy {
|
|
id: number;
|
|
name: string;
|
|
description?: string;
|
|
responseTimeHours: number;
|
|
resolutionTimeHours: number;
|
|
isActive: boolean;
|
|
}
|
|
|
|
export interface DashboardStats {
|
|
totalTickets: number;
|
|
openTickets: number;
|
|
inProgressTickets: number;
|
|
resolvedTickets: number;
|
|
closedTickets: number;
|
|
unassignedTickets: number;
|
|
criticalTickets: number;
|
|
dailyCounts: { date: string; count: number }[];
|
|
}
|
|
|
|
// ==================== Departamentos ====================
|
|
export async function createGroup(data: { name: string; description?: string }) {
|
|
return fetchApi<Group>("/api/helpdesk/groups", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function updateGroup(
|
|
id: number,
|
|
data: { name?: string; description?: string; isActive?: boolean }
|
|
) {
|
|
return fetchApi<void>(`/api/helpdesk/groups/${id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function deleteGroup(id: number) {
|
|
return fetchApi<void>(`/api/helpdesk/groups/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
// ==================== Equipo ====================
|
|
export interface TeamMember extends HelpDeskUser {
|
|
role: string;
|
|
departmentId?: number;
|
|
departmentName?: string;
|
|
isActive?: boolean;
|
|
}
|
|
|
|
export async function getTeam() {
|
|
return fetchApi<TeamMember[]>("/api/helpdesk/users");
|
|
}
|
|
|
|
export async function addMember(data: {
|
|
idUsuario: string;
|
|
role?: string;
|
|
departmentId?: number;
|
|
}) {
|
|
return fetchApi<TeamMember>("/api/helpdesk/users", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function updateMember(
|
|
idUsuario: string,
|
|
data: { role?: string; departmentId?: number; isActive?: boolean }
|
|
) {
|
|
return fetchApi<TeamMember>(`/api/helpdesk/users/${encodeURIComponent(idUsuario)}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
// ==================== SLA ====================
|
|
export async function createSla(data: {
|
|
name: string;
|
|
description?: string;
|
|
responseTimeHours?: number;
|
|
resolutionTimeHours?: number;
|
|
}) {
|
|
return fetchApi<SLAPolicy>("/api/helpdesk/sla", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function updateSla(
|
|
id: number,
|
|
data: {
|
|
name?: string;
|
|
description?: string;
|
|
responseTimeHours?: number;
|
|
resolutionTimeHours?: number;
|
|
isActive?: boolean;
|
|
}
|
|
) {
|
|
return fetchApi<void>(`/api/helpdesk/sla/${id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
// ==================== Correo / plantillas (banco único mail.* vía /api/email/*) ====================
|
|
export interface EmailTemplate {
|
|
key: string;
|
|
name: string;
|
|
subject: string;
|
|
bodyHtml: string;
|
|
module?: string;
|
|
isActive: boolean;
|
|
}
|
|
|
|
export interface EmailLogEntry {
|
|
id: number;
|
|
template: string;
|
|
module?: string;
|
|
ticketId?: number;
|
|
recipient: string;
|
|
subject: string;
|
|
status: string;
|
|
error?: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export async function getEmailTemplates() {
|
|
return fetchApi<EmailTemplate[]>("/api/email/templates");
|
|
}
|
|
|
|
export async function createEmailTemplate(data: {
|
|
key: string;
|
|
name: string;
|
|
subject: string;
|
|
bodyHtml: string;
|
|
module?: string;
|
|
}) {
|
|
return fetchApi<{ key: string }>("/api/email/templates", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function updateEmailTemplate(
|
|
key: string,
|
|
data: { subject?: string; bodyHtml?: string; isActive?: boolean }
|
|
) {
|
|
return fetchApi<void>(`/api/email/templates/${encodeURIComponent(key)}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function getEmailLog(limit = 50, module = "helpdesk") {
|
|
const query = new URLSearchParams({ limit: String(limit) });
|
|
if (module) query.set("module", module);
|
|
return fetchApi<EmailLogEntry[]>(`/api/email/log?${query.toString()}`);
|
|
}
|
|
|
|
// Envío vía route handler propio (/api/correos) → POST /api/email/send con
|
|
// X-Module + X-Api-Key en el servidor (la clave nunca va en el bundle).
|
|
// Reenvía el Bearer de admin para que el route verifique el rol.
|
|
export async function sendModuleEmail(data: {
|
|
to: string | string[];
|
|
templateKey?: string;
|
|
subject?: string;
|
|
htmlBody?: string;
|
|
vars?: Record<string, string | null>;
|
|
}) {
|
|
const token =
|
|
typeof window !== "undefined" ? localStorage.getItem("helpdesk_token") : null;
|
|
const res = await fetch("/api/correos", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
body: JSON.stringify(data),
|
|
});
|
|
const payload = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
throw new Error(
|
|
(payload as { message?: string }).message || `Error de envío: ${res.status}`
|
|
);
|
|
}
|
|
return payload as { success: boolean; sent?: number; failed?: number; message?: string };
|
|
}
|
|
|
|
export async function sendTestEmail(to: string) {
|
|
return sendModuleEmail({
|
|
to,
|
|
subject: "[HelpDesk] Prueba de correo",
|
|
htmlBody: `<p>Prueba de envío desde la Mesa de Ayuda ICHN (${new Date().toISOString().slice(0, 16).replace("T", " ")} UTC).</p><p>Vía banco <code>mail.templates</code> + <code>POST /api/email/send</code> (módulo helpdesk).</p>`,
|
|
});
|
|
}
|
|
|
|
export async function syncInboundEmail() {
|
|
return fetchApi<{
|
|
success: boolean;
|
|
checkedCount: number;
|
|
inserted: number;
|
|
skipped: number;
|
|
error?: string;
|
|
}>("/api/helpdesk/email/inbound/sync", { method: "POST" });
|
|
}
|
|
|
|
// ==================== Notificaciones in-app (estándar multi-módulo) ====================
|
|
export interface AppNotification {
|
|
id: number;
|
|
module: string;
|
|
ticketId?: number;
|
|
notifRef?: string;
|
|
type: string;
|
|
title: string;
|
|
body?: string;
|
|
isRead: boolean;
|
|
createdAt: string;
|
|
}
|
|
|
|
export async function getNotifications(limit = 50, module = "", unreadOnly = false) {
|
|
const query = new URLSearchParams({ limit: String(limit) });
|
|
if (module) query.set("module", module);
|
|
if (unreadOnly) query.set("unreadOnly", "true");
|
|
return fetchApi<AppNotification[]>(`/api/notifications?${query.toString()}`);
|
|
}
|
|
|
|
export async function getUnreadCount(module = "") {
|
|
const query = new URLSearchParams();
|
|
if (module) query.set("module", module);
|
|
const qs = query.toString();
|
|
return fetchApi<{ count: number }>(`/api/notifications/unread-count${qs ? `?${qs}` : ""}`);
|
|
}
|
|
|
|
export async function markNotificationRead(id: number) {
|
|
return fetchApi<{ id: number }>(`/api/notifications/${id}/read`, {
|
|
method: "PUT",
|
|
});
|
|
}
|
|
|
|
export async function markAllNotificationsRead(module = "") {
|
|
const query = new URLSearchParams();
|
|
if (module) query.set("module", module);
|
|
const qs = query.toString();
|
|
return fetchApi<{ updated: number }>(`/api/notifications/read-all${qs ? `?${qs}` : ""}`, {
|
|
method: "PUT",
|
|
});
|
|
}
|