feat: Implement Incident Management and Tab Dashboard
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
|
||||
import { IncidentItem } from './types';
|
||||
|
||||
// Export functions to update and invalidate the cache
|
||||
export const updateCache = (data: IncidentItem[]) => {
|
||||
console.log(`Updating cache with ${data.length} incidents`);
|
||||
// The actual cache is now maintained in incidentFetch.ts
|
||||
};
|
||||
|
||||
// Reset cache and request state
|
||||
export const invalidateCache = () => {
|
||||
console.log('Invalidating incidents cache');
|
||||
// The invalidation is handled in incidentFetch.ts implementation
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
|
||||
import { pb } from '@/lib/pocketbase';
|
||||
import { IncidentItem } from './types';
|
||||
import { normalizeFetchedItem } from './incidentUtils';
|
||||
import { updateCache, invalidateCache } from './incidentCache';
|
||||
|
||||
// Internal state variables instead of importing them
|
||||
let incidentsCache: {
|
||||
data: IncidentItem[];
|
||||
timestamp: number;
|
||||
} | null = null;
|
||||
let pendingRequest: Promise<any> | null = null;
|
||||
let isRequestInProgress = false;
|
||||
|
||||
// Cache duration in milliseconds (30 minutes)
|
||||
const CACHE_DURATION = 30 * 60 * 1000;
|
||||
|
||||
// Check if cache is valid
|
||||
export const isCacheValid = (): boolean => {
|
||||
const now = Date.now();
|
||||
return !!(incidentsCache && (now - incidentsCache.timestamp < CACHE_DURATION));
|
||||
};
|
||||
|
||||
// Get all incidents with caching and error handling
|
||||
export const getAllIncidents = async (forceRefresh = false): Promise<IncidentItem[]> => {
|
||||
// If a request is in progress, wait for it to complete rather than making a new one
|
||||
if (isRequestInProgress) {
|
||||
console.log('Request already in progress, waiting for completion');
|
||||
try {
|
||||
if (pendingRequest) {
|
||||
await pendingRequest;
|
||||
}
|
||||
return incidentsCache?.data || [];
|
||||
} catch (error) {
|
||||
console.error('Error in existing incidents request:', error);
|
||||
return incidentsCache?.data || [];
|
||||
}
|
||||
}
|
||||
|
||||
// Use cache if available, not expired, and not forced refresh
|
||||
if (!forceRefresh && isCacheValid()) {
|
||||
console.log('Using cached incidents data from', new Date(incidentsCache!.timestamp).toLocaleTimeString());
|
||||
return incidentsCache!.data;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Fetching all incidents from API...');
|
||||
isRequestInProgress = true;
|
||||
|
||||
// Implement timeout for the request
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Request timeout')), 20000); // Longer timeout (20s)
|
||||
});
|
||||
|
||||
// Create the fetch promise with a unique request key to prevent conflicts
|
||||
const now = Date.now();
|
||||
const requestKey = `incidents-${now}`;
|
||||
const fetchPromise = pb.collection('incidents').getList(1, 100, {
|
||||
sort: '-created',
|
||||
requestKey,
|
||||
$cancelKey: requestKey,
|
||||
});
|
||||
|
||||
// Store the pending request
|
||||
pendingRequest = Promise.race([fetchPromise, timeoutPromise]);
|
||||
|
||||
// Race between fetch and timeout
|
||||
const result = await pendingRequest;
|
||||
|
||||
// Clear request flags
|
||||
pendingRequest = null;
|
||||
isRequestInProgress = false;
|
||||
|
||||
if (!result || !result.items) {
|
||||
console.warn('No incidents found in API response');
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizedItems = result.items.map(normalizeFetchedItem);
|
||||
|
||||
// Update cache
|
||||
updateCache(normalizedItems);
|
||||
|
||||
console.log(`Fetched ${normalizedItems.length} incidents at ${new Date().toLocaleTimeString()}`);
|
||||
return normalizedItems;
|
||||
} catch (error) {
|
||||
if ((error as any)?.isAbort) {
|
||||
console.log("Request aborted:", error);
|
||||
return incidentsCache?.data || [];
|
||||
}
|
||||
|
||||
console.error('Error fetching incidents:', error);
|
||||
|
||||
// Clear states to allow retry
|
||||
pendingRequest = null;
|
||||
isRequestInProgress = false;
|
||||
|
||||
// Improve error message
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to fetch incidents: ${error.message}`);
|
||||
}
|
||||
|
||||
// Still return cached data even on error
|
||||
if (incidentsCache) {
|
||||
console.log('Returning stale cached data after error');
|
||||
return incidentsCache.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Get incident by id
|
||||
export const getIncidentById = async (id: string): Promise<IncidentItem | null> => {
|
||||
try {
|
||||
console.log(`Fetching incident with ID: ${id}`);
|
||||
|
||||
// First check if the incident exists in the cache
|
||||
if (isCacheValid() && incidentsCache) {
|
||||
const cachedIncident = incidentsCache.data.find(incident => incident.id === id);
|
||||
if (cachedIncident) {
|
||||
console.log('Incident found in cache');
|
||||
return cachedIncident;
|
||||
}
|
||||
}
|
||||
|
||||
// If not in cache, fetch from API
|
||||
const result = await pb.collection('incidents').getOne(id);
|
||||
|
||||
if (!result) {
|
||||
console.warn(`No incident found with ID: ${id}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedIncident = normalizeFetchedItem(result);
|
||||
return normalizedIncident;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error fetching incident with ID ${id}:`, error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to fetch incident: ${error.message}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
|
||||
import { pb } from '@/lib/pocketbase';
|
||||
import { CreateIncidentInput, IncidentItem } from './types';
|
||||
import { formatStatus } from './incidentUtils';
|
||||
import { invalidateCache } from './incidentCache';
|
||||
|
||||
// Update incident status
|
||||
export const updateIncidentStatus = async (id: string, status: string): Promise<void> => {
|
||||
try {
|
||||
const formattedStatus = formatStatus(status);
|
||||
console.log(`Updating incident ${id} status to ${status} (formatted: ${formattedStatus})`);
|
||||
|
||||
// Update both status and impact_status fields
|
||||
await pb.collection('incidents').update(id, {
|
||||
status: formattedStatus,
|
||||
impact_status: status.toLowerCase(), // Set impact_status to the lowercase status value
|
||||
...(status.toLowerCase() === 'resolved' ? { resolution_time: new Date().toISOString() } : {})
|
||||
});
|
||||
|
||||
// Invalidate cache after update
|
||||
invalidateCache();
|
||||
|
||||
console.log(`Incident ${id} status updated successfully to ${status}`);
|
||||
} catch (error) {
|
||||
console.error('Error updating incident status:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Delete incident
|
||||
export const deleteIncident = async (id: string): Promise<void> => {
|
||||
try {
|
||||
await pb.collection('incidents').delete(id);
|
||||
|
||||
// Invalidate cache after deletion
|
||||
invalidateCache();
|
||||
|
||||
console.log(`Incident ${id} deleted successfully`);
|
||||
} catch (error) {
|
||||
console.error('Error deleting incident:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Create incident
|
||||
export const createIncident = async (data: CreateIncidentInput): Promise<void> => {
|
||||
try {
|
||||
// Format the payload according to API requirements
|
||||
const payload = {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
status: formatStatus(data.status),
|
||||
impact_status: data.status.toLowerCase(),
|
||||
// Use lowercase for impact and priority to match API expectations
|
||||
impact: data.impact.toLowerCase(),
|
||||
affected_systems: data.affected_systems,
|
||||
priority: data.priority.toLowerCase(),
|
||||
service_id: data.service_id || '',
|
||||
assigned_to: data.assigned_to || '', // Direct user ID assignment
|
||||
root_cause: data.root_cause || '',
|
||||
resolution_steps: data.resolution_steps || '',
|
||||
lessons_learned: data.lessons_learned || '',
|
||||
timestamp: data.timestamp || new Date().toISOString(),
|
||||
created_by: data.created_by,
|
||||
resolution_time: data.status.toLowerCase() === 'resolved' ? new Date().toISOString() : null,
|
||||
};
|
||||
|
||||
console.log('Creating new incident with payload:', payload);
|
||||
await pb.collection('incidents').create(payload);
|
||||
|
||||
// Invalidate cache after create
|
||||
invalidateCache();
|
||||
|
||||
console.log('Incident created successfully');
|
||||
} catch (error) {
|
||||
console.error('Error creating incident:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Update incident
|
||||
export const updateIncident = async (id: string, data: Partial<IncidentItem>): Promise<void> => {
|
||||
try {
|
||||
console.log(`Updating incident ${id} with:`, data);
|
||||
|
||||
// Make sure impact and priority are lowercase
|
||||
const payload = {
|
||||
...data,
|
||||
impact: data.impact?.toLowerCase(),
|
||||
priority: data.priority?.toLowerCase(),
|
||||
status: data.status ? formatStatus(data.status) : undefined,
|
||||
impact_status: data.status ? data.status.toLowerCase() : undefined,
|
||||
...(data.status?.toLowerCase() === 'resolved' && !data.resolution_time
|
||||
? { resolution_time: new Date().toISOString() }
|
||||
: {})
|
||||
};
|
||||
|
||||
console.log("Final payload for update:", payload);
|
||||
await pb.collection('incidents').update(id, payload);
|
||||
|
||||
// Invalidate cache after update
|
||||
invalidateCache();
|
||||
|
||||
console.log(`Incident ${id} updated successfully`);
|
||||
} catch (error) {
|
||||
console.error('Error updating incident:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { generateIncidentPDF } from './pdf';
|
||||
|
||||
export { generateIncidentPDF };
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
import { CreateIncidentInput, IncidentItem, UpdateIncidentInput } from './types';
|
||||
import { createIncident, updateIncident, updateIncidentStatus, deleteIncident } from './incidentOperations';
|
||||
import { getAllIncidents, getIncidentById } from './incidentFetch';
|
||||
import { generateIncidentPDF } from './incidentPdfService';
|
||||
|
||||
export const incidentService = {
|
||||
// Fetch operations
|
||||
getAllIncidents,
|
||||
getIncidentById,
|
||||
|
||||
// CRUD operations
|
||||
createIncident,
|
||||
updateIncident,
|
||||
updateIncidentStatus,
|
||||
deleteIncident,
|
||||
|
||||
// PDF operations
|
||||
generateIncidentPDF,
|
||||
};
|
||||
|
||||
export default incidentService;
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
import { pb } from '@/lib/pocketbase';
|
||||
import { IncidentItem } from './types';
|
||||
|
||||
// Helper function to get the API URL
|
||||
export const getApiUrl = (): string => {
|
||||
try {
|
||||
return pb.baseUrl;
|
||||
} catch (error) {
|
||||
console.error('Error getting API URL:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// Normalize fetched items to ensure consistent data structure
|
||||
export const normalizeFetchedItem = (item: any): IncidentItem => {
|
||||
return {
|
||||
...item,
|
||||
affected_systems: item.affected_systems || '',
|
||||
status: item.impact_status || item.status || 'Investigating',
|
||||
impact: item.impact || 'Low',
|
||||
priority: item.priority || 'Low',
|
||||
} as IncidentItem;
|
||||
};
|
||||
|
||||
// Format status with first letter capitalized
|
||||
export const formatStatus = (status: string): string => {
|
||||
return status.charAt(0).toUpperCase() + status.slice(1);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
export * from './types';
|
||||
export * from './incidentFetch';
|
||||
export * from './incidentOperations';
|
||||
export * from './incidentCache';
|
||||
export * from './incidentUtils';
|
||||
export * from './incidentPdfService';
|
||||
export * from './incidentService';
|
||||
|
||||
// Export the incidentService as the default export
|
||||
export { incidentService } from './incidentService';
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
|
||||
import jsPDF from 'jspdf';
|
||||
import { IncidentItem } from '../types';
|
||||
import {
|
||||
addBasicInfoSection,
|
||||
addDescriptionSection,
|
||||
addAffectedSystemsSection,
|
||||
addRootCauseSection,
|
||||
addResolutionSection,
|
||||
addAssignmentSection,
|
||||
addLessonsLearnedSection
|
||||
} from './sections';
|
||||
import { addHeader, addFooter } from './headerFooter';
|
||||
|
||||
/**
|
||||
* Generate a PDF for an incident report
|
||||
*/
|
||||
export const generatePdf = async (incident: IncidentItem): Promise<string> => {
|
||||
// Validate incident data
|
||||
if (!incident?.id) {
|
||||
console.error('Invalid incident data for PDF generation');
|
||||
throw new Error('Invalid incident data');
|
||||
}
|
||||
|
||||
try {
|
||||
// Create new PDF document with portrait orientation
|
||||
const doc = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4',
|
||||
});
|
||||
|
||||
// Set title and filename
|
||||
const title = incident.title || `Incident Report #${incident.id}`;
|
||||
const filename = `incident-report-${incident.id}.pdf`;
|
||||
|
||||
// Add metadata
|
||||
doc.setProperties({
|
||||
title: title,
|
||||
subject: 'Incident Report',
|
||||
author: 'ReamStack System',
|
||||
creator: 'ReamStack',
|
||||
});
|
||||
|
||||
// Add header section
|
||||
let yPos = addHeader(doc, incident);
|
||||
|
||||
// Add basic information section
|
||||
yPos = addBasicInfoSection(doc, incident, yPos);
|
||||
|
||||
// Add description section
|
||||
yPos = addDescriptionSection(doc, incident, yPos);
|
||||
|
||||
// Check if we need to add a new page
|
||||
if (yPos > 250) {
|
||||
doc.addPage();
|
||||
yPos = 20;
|
||||
}
|
||||
|
||||
// Add affected systems section
|
||||
yPos = addAffectedSystemsSection(doc, incident, yPos);
|
||||
|
||||
// Check if we need to add a new page
|
||||
if (yPos > 250) {
|
||||
doc.addPage();
|
||||
yPos = 20;
|
||||
}
|
||||
|
||||
// Add root cause section
|
||||
yPos = addRootCauseSection(doc, incident, yPos);
|
||||
|
||||
// Check if we need to add a new page
|
||||
if (yPos > 250) {
|
||||
doc.addPage();
|
||||
yPos = 20;
|
||||
}
|
||||
|
||||
// Add resolution steps section
|
||||
yPos = addResolutionSection(doc, incident, yPos);
|
||||
|
||||
// Check if we need to add a new page
|
||||
if (yPos > 250) {
|
||||
doc.addPage();
|
||||
yPos = 20;
|
||||
}
|
||||
|
||||
// Add assignment section
|
||||
yPos = addAssignmentSection(doc, incident, yPos);
|
||||
|
||||
// Check if we need to add a new page
|
||||
if (yPos > 250) {
|
||||
doc.addPage();
|
||||
yPos = 20;
|
||||
}
|
||||
|
||||
// Add lessons learned section if available
|
||||
addLessonsLearnedSection(doc, incident, yPos);
|
||||
|
||||
// Add footer to all pages
|
||||
addFooter(doc);
|
||||
|
||||
// Save the PDF
|
||||
doc.save(filename);
|
||||
|
||||
console.log('PDF generated successfully:', filename);
|
||||
return filename;
|
||||
} catch (error) {
|
||||
console.error('Error generating incident PDF:', error);
|
||||
throw new Error(`Failed to generate PDF: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
import jsPDF from 'jspdf';
|
||||
import { IncidentItem } from '../types';
|
||||
import { fonts } from './utils';
|
||||
|
||||
/**
|
||||
* Add the PDF header with title and metadata
|
||||
*/
|
||||
export const addHeader = (doc: jsPDF, incident: IncidentItem): number => {
|
||||
// Set initial y position
|
||||
let yPos = 15;
|
||||
|
||||
// Add header with title
|
||||
doc.setFont(fonts.bold);
|
||||
doc.setFontSize(18);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.text('INCIDENT REPORT', 105, yPos, { align: 'center' });
|
||||
|
||||
// Add incident title
|
||||
yPos += 8;
|
||||
doc.setFontSize(14);
|
||||
const title = incident.title || `Incident Report #${incident.id}`;
|
||||
doc.text(title, 105, yPos, { align: 'center' });
|
||||
|
||||
// Add current date
|
||||
yPos += 8;
|
||||
doc.setFont(fonts.normal);
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Generated on: ${new Date().toLocaleDateString()}`, 105, yPos, { align: 'center' });
|
||||
|
||||
// Add ReamStack logo or text
|
||||
yPos += 8;
|
||||
doc.setFontSize(12);
|
||||
doc.setFont(fonts.italic);
|
||||
doc.text('ReamStack Incident Management', 105, yPos, { align: 'center' });
|
||||
|
||||
// Add horizontal line
|
||||
yPos += 5;
|
||||
doc.setLineWidth(0.5);
|
||||
doc.line(15, yPos, 195, yPos);
|
||||
|
||||
// Return next Y position with some padding
|
||||
return yPos + 10;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add footer to all pages
|
||||
*/
|
||||
export const addFooter = (doc: jsPDF): void => {
|
||||
const pageCount = doc.getNumberOfPages();
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i);
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(100, 100, 100);
|
||||
doc.text(
|
||||
`Page ${i} of ${pageCount} | Generated by ReamStack Incident Management System`,
|
||||
105,
|
||||
285,
|
||||
{ align: 'center' }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
import { generatePdf } from './generator';
|
||||
import { IncidentItem } from '../types';
|
||||
|
||||
/**
|
||||
* Generate and download PDF for incident report
|
||||
*/
|
||||
export const generateIncidentPDF = async (incident: IncidentItem): Promise<void> => {
|
||||
await generatePdf(incident);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
|
||||
import jsPDF from 'jspdf';
|
||||
import { IncidentItem } from '../types';
|
||||
import { fonts, formatDate, capitalize } from './utils';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
|
||||
/**
|
||||
* Add the incident overview section to the PDF
|
||||
*/
|
||||
export const addBasicInfoSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
|
||||
doc.setFont(fonts.bold);
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(30, 64, 175); // Blue-800
|
||||
doc.text('Incident Overview', 15, yPos);
|
||||
|
||||
// Draw a line under the heading
|
||||
doc.line(15, yPos + 2, 195, yPos + 2);
|
||||
yPos += 10;
|
||||
|
||||
// Basic information table
|
||||
autoTable(doc, {
|
||||
startY: yPos,
|
||||
head: [['Field', 'Information']],
|
||||
body: [
|
||||
['ID', incident.id],
|
||||
['Status', capitalize(incident.status || 'Unknown')],
|
||||
['Priority', capitalize(incident.priority || 'Unknown')],
|
||||
['Impact', capitalize(incident.impact || 'Unknown')],
|
||||
['Created', formatDate(incident.created)],
|
||||
['Last Updated', formatDate(incident.updated)],
|
||||
],
|
||||
headStyles: {
|
||||
fillColor: [30, 64, 175], // Blue-800
|
||||
textColor: [255, 255, 255],
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
alternateRowStyles: {
|
||||
fillColor: [241, 245, 249], // Slate-100
|
||||
},
|
||||
margin: { left: 15, right: 15 },
|
||||
});
|
||||
|
||||
// Return the new Y position after the table
|
||||
return (doc as any).lastAutoTable.finalY + 10;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the description section to the PDF
|
||||
*/
|
||||
export const addDescriptionSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
|
||||
doc.setFont(fonts.bold);
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(30, 64, 175); // Blue-800
|
||||
doc.text('Description', 15, yPos);
|
||||
|
||||
// Draw a line under the heading
|
||||
doc.line(15, yPos + 2, 195, yPos + 2);
|
||||
|
||||
// Add incident description
|
||||
doc.setFont(fonts.normal);
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
const description = incident.description || 'No description provided';
|
||||
const splitDescription = doc.splitTextToSize(description, 180);
|
||||
doc.text(splitDescription, 15, yPos + 10);
|
||||
|
||||
// Update y position after the description
|
||||
return yPos + 15 + (splitDescription.length * 5);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the affected systems section to the PDF
|
||||
*/
|
||||
export const addAffectedSystemsSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
|
||||
doc.setFont(fonts.bold);
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(30, 64, 175); // Blue-800
|
||||
doc.text('Affected Systems', 15, yPos);
|
||||
|
||||
// Draw a line under the heading
|
||||
doc.line(15, yPos + 2, 195, yPos + 2);
|
||||
|
||||
// Add affected systems
|
||||
doc.setFont(fonts.normal);
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
const affectedSystems = incident.affected_systems || 'None specified';
|
||||
const systemsList = affectedSystems.split(',').map(system => system.trim());
|
||||
|
||||
let currentY = yPos + 10;
|
||||
systemsList.forEach((system) => {
|
||||
doc.text(`• ${system}`, 15, currentY);
|
||||
currentY += 5;
|
||||
});
|
||||
|
||||
return currentY + 5; // Return updated Y position with some padding
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the root cause section to the PDF
|
||||
*/
|
||||
export const addRootCauseSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
|
||||
doc.setFont(fonts.bold);
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(30, 64, 175); // Blue-800
|
||||
doc.text('Root Cause', 15, yPos);
|
||||
|
||||
// Draw a line under the heading
|
||||
doc.line(15, yPos + 2, 195, yPos + 2);
|
||||
|
||||
// Add root cause
|
||||
doc.setFont(fonts.normal);
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
const rootCause = incident.root_cause || 'Root cause not identified yet';
|
||||
const splitRootCause = doc.splitTextToSize(rootCause, 180);
|
||||
doc.text(splitRootCause, 15, yPos + 10);
|
||||
|
||||
// Update y position after the root cause
|
||||
return yPos + 15 + (splitRootCause.length * 5);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the resolution steps section to the PDF
|
||||
*/
|
||||
export const addResolutionSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
|
||||
doc.setFont(fonts.bold);
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(30, 64, 175); // Blue-800
|
||||
doc.text('Resolution Steps', 15, yPos);
|
||||
|
||||
// Draw a line under the heading
|
||||
doc.line(15, yPos + 2, 195, yPos + 2);
|
||||
|
||||
// Create resolution data table - Using resolution_steps
|
||||
const resolutionSteps = incident.resolution_steps || 'No resolution steps provided';
|
||||
const splitResolutionSteps = doc.splitTextToSize(resolutionSteps, 180);
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.text(splitResolutionSteps, 15, yPos + 10);
|
||||
|
||||
return yPos + 20 + (splitResolutionSteps.length * 5);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the assignment information section to the PDF
|
||||
*/
|
||||
export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(30, 64, 175); // Blue-800
|
||||
doc.text('Assignment Information', 15, yPos);
|
||||
|
||||
// Draw a line under the heading
|
||||
doc.line(15, yPos + 2, 195, yPos + 2);
|
||||
|
||||
// Add assigned user info
|
||||
yPos += 10;
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
const assignedTo = incident.assigned_to || 'Not assigned';
|
||||
doc.text(`Assigned to: ${assignedTo}`, 15, yPos);
|
||||
|
||||
return yPos + 10;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the lessons learned section to the PDF if available
|
||||
*/
|
||||
export const addLessonsLearnedSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
|
||||
if (!incident.lessons_learned) {
|
||||
return yPos; // No lessons learned, return current position
|
||||
}
|
||||
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(30, 64, 175); // Blue-800
|
||||
doc.text('Lessons Learned', 15, yPos);
|
||||
|
||||
// Draw a line under the heading
|
||||
doc.line(15, yPos + 2, 195, yPos + 2);
|
||||
|
||||
// Add lessons learned
|
||||
const lessonsLearned = incident.lessons_learned;
|
||||
const splitLessonsLearned = doc.splitTextToSize(lessonsLearned, 180);
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.text(splitLessonsLearned, 15, yPos + 10);
|
||||
|
||||
return yPos + 15 + (splitLessonsLearned.length * 5);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
import { format } from 'date-fns';
|
||||
|
||||
// Font configuration for the PDF
|
||||
export const fonts = {
|
||||
normal: 'Helvetica',
|
||||
bold: 'Helvetica-Bold',
|
||||
italic: 'Helvetica-Oblique',
|
||||
};
|
||||
|
||||
// Helper function to format dates
|
||||
export const formatDate = (dateString: string | undefined): string => {
|
||||
if (!dateString) return 'N/A';
|
||||
try {
|
||||
return format(new Date(dateString), 'PPp');
|
||||
} catch (e) {
|
||||
return dateString || 'N/A';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to capitalize first letter
|
||||
export const capitalize = (str: string): string => {
|
||||
if (!str) return '';
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
// Define the type for incident item
|
||||
export type IncidentItem = {
|
||||
id: string;
|
||||
service_id?: string;
|
||||
timestamp?: string;
|
||||
description: string;
|
||||
assigned_to?: string;
|
||||
resolution_time?: string;
|
||||
impact: string;
|
||||
affected_systems: string;
|
||||
root_cause?: string;
|
||||
resolution_steps?: string;
|
||||
lessons_learned?: string;
|
||||
operational_status_id?: string;
|
||||
server_id?: string;
|
||||
priority: string;
|
||||
status: string;
|
||||
impact_status?: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
category?: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
// Define the input type for creating an incident
|
||||
export type CreateIncidentInput = {
|
||||
title: string;
|
||||
description: string;
|
||||
status: string;
|
||||
impact: string;
|
||||
affected_systems: string;
|
||||
priority: string;
|
||||
service_id?: string;
|
||||
assigned_to?: string;
|
||||
root_cause?: string;
|
||||
resolution_steps?: string;
|
||||
lessons_learned?: string;
|
||||
timestamp?: string;
|
||||
created_by: string;
|
||||
};
|
||||
|
||||
// Define the input type for updating an incident
|
||||
export type UpdateIncidentInput = {
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
impact?: string;
|
||||
affected_systems?: string;
|
||||
priority?: string;
|
||||
service_id?: string;
|
||||
assigned_to?: string;
|
||||
root_cause?: string;
|
||||
resolution_steps?: string;
|
||||
lessons_learned?: string;
|
||||
};
|
||||
Reference in New Issue
Block a user