459 lines
13 KiB
Vue
459 lines
13 KiB
Vue
<template>
|
|
<q-page padding>
|
|
<q-card
|
|
flat
|
|
bordered
|
|
class="q-mb-xl"
|
|
>
|
|
<q-card-section class="row items-center justify-between q-gutter-md">
|
|
<div class="text-h4">
|
|
Mantis Tickets
|
|
</div>
|
|
<div class="row items-center q-gutter-sm">
|
|
<!-- Filters -->
|
|
<q-select
|
|
dense
|
|
outlined
|
|
v-model="selectedStatus"
|
|
:options="statusOptions"
|
|
label="Status"
|
|
clearable
|
|
emit-value
|
|
map-options
|
|
style="min-width: 120px"
|
|
@update:model-value="applyFilters"
|
|
/>
|
|
<q-select
|
|
dense
|
|
outlined
|
|
v-model="selectedPriority"
|
|
:options="priorityOptions"
|
|
label="Priority"
|
|
clearable
|
|
emit-value
|
|
map-options
|
|
style="min-width: 120px"
|
|
@update:model-value="applyFilters"
|
|
/>
|
|
<q-select
|
|
dense
|
|
outlined
|
|
v-model="selectedSeverity"
|
|
:options="severityOptions"
|
|
label="Severity"
|
|
clearable
|
|
emit-value
|
|
map-options
|
|
style="min-width: 120px"
|
|
@update:model-value="applyFilters"
|
|
/>
|
|
<q-select
|
|
dense
|
|
outlined
|
|
v-model="selectedReporter"
|
|
:options="reporterOptions"
|
|
label="Reporter"
|
|
clearable
|
|
emit-value
|
|
map-options
|
|
style="min-width: 150px"
|
|
@update:model-value="applyFilters"
|
|
/>
|
|
<!-- Search Input -->
|
|
<q-input
|
|
dense
|
|
outlined
|
|
debounce="300"
|
|
v-model="searchTerm"
|
|
placeholder="Search tickets..."
|
|
@update:model-value="applyFilters"
|
|
clearable
|
|
style="width: 250px"
|
|
>
|
|
<template #append>
|
|
<q-icon name="search" />
|
|
</template>
|
|
</q-input>
|
|
</div>
|
|
</q-card-section>
|
|
|
|
<q-table
|
|
:rows="tickets"
|
|
:columns="columns"
|
|
row-key="id"
|
|
v-model:pagination="pagination"
|
|
:loading="loading"
|
|
@request="handleTableRequest"
|
|
binary-state-sort
|
|
flat
|
|
bordered
|
|
@row-click="onRowClick"
|
|
class="cursor-pointer"
|
|
>
|
|
<template #body-cell-status="statusProps">
|
|
<q-td :props="statusProps">
|
|
<q-badge
|
|
:color="getStatusColor(statusProps.row.status)"
|
|
:label="statusProps.row.status"
|
|
class="text-bold text-shadow text-subtitle2 text-uppercase"
|
|
/>
|
|
</q-td>
|
|
</template>
|
|
<template #body-cell-priority="priorityProps">
|
|
<q-td :props="priorityProps">
|
|
<q-badge
|
|
:color="getPriorityColor(priorityProps.row.priority)"
|
|
:label="priorityProps.row.priority"
|
|
class="text-bold text-shadow text-subtitle2 text-uppercase"
|
|
/>
|
|
</q-td>
|
|
</template>
|
|
<template #body-cell-severity="severityProps">
|
|
<q-td :props="severityProps">
|
|
<q-badge
|
|
:color="getSeverityColor(severityProps.row.severity)"
|
|
:label="severityProps.row.severity"
|
|
class="text-bold text-shadow text-subtitle2 text-uppercase"
|
|
/>
|
|
</q-td>
|
|
</template>
|
|
<template #body-cell-actions="actionsProps">
|
|
<q-td
|
|
:props="actionsProps"
|
|
class="text-center"
|
|
>
|
|
<q-btn
|
|
size="md"
|
|
color="info"
|
|
icon="summarize"
|
|
round
|
|
flat
|
|
@click.stop="showTicketSummary(actionsProps.row.id)"
|
|
:title="'Show summary for ticket ' + actionsProps.row.id"
|
|
/>
|
|
</q-td>
|
|
</template>
|
|
</q-table>
|
|
</q-card>
|
|
|
|
<mantis-ticket-dialog
|
|
v-model="showDialog"
|
|
:ticket-id="selectedTicketId"
|
|
@close="closeDialog"
|
|
/>
|
|
|
|
<!-- New Summary Dialog -->
|
|
<q-dialog
|
|
v-model="showSummaryDialog"
|
|
>
|
|
<q-card
|
|
style="max-width: max(75%, 500px)"
|
|
>
|
|
<q-card-section class="row items-center q-pb-none q-mb-md">
|
|
<div class="text-h4">
|
|
Summary for Ticket #{{ summaryTicketId }}
|
|
</div>
|
|
<q-space />
|
|
<q-btn
|
|
icon="close"
|
|
flat
|
|
round
|
|
dense
|
|
v-close-popup
|
|
/>
|
|
</q-card-section>
|
|
<q-separator />
|
|
|
|
<q-card-section class="summary-content">
|
|
<div v-html="summaryContent" />
|
|
</q-card-section>
|
|
</q-card>
|
|
</q-dialog>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, watch, defineProps } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import axios from 'axios';
|
|
import { useQuasar } from 'quasar';
|
|
import MantisTicketDialog from 'src/components/MantisTicketDialog.vue';
|
|
import { marked } from 'marked';
|
|
|
|
const props = defineProps({
|
|
ticketId: {
|
|
type: [String, Number],
|
|
'default': null
|
|
}
|
|
});
|
|
|
|
const $q = useQuasar();
|
|
const tickets = ref([]);
|
|
const loading = ref(false);
|
|
const searchTerm = ref('');
|
|
const showDialog = ref(false);
|
|
const selectedTicketId = ref(null);
|
|
const showSummaryDialog = ref(false); // New state for summary dialog
|
|
const summaryContent = ref(''); // New state for summary content
|
|
const summaryTicketId = ref(null); // New state for summary ticket ID
|
|
|
|
// Filter refs
|
|
const selectedStatus = ref(null);
|
|
const selectedPriority = ref(null);
|
|
const selectedSeverity = ref(null);
|
|
const selectedReporter = ref(null);
|
|
const statusOptions = ref([]);
|
|
const priorityOptions = ref([]);
|
|
const severityOptions = ref([]);
|
|
const reporterOptions = ref([]);
|
|
|
|
const router = useRouter();
|
|
|
|
const pagination = ref({
|
|
sortBy: 'updatedAt',
|
|
descending: true,
|
|
page: 1,
|
|
rowsPerPage: 25,
|
|
rowsNumber: 0 // Total number of rows/tickets
|
|
});
|
|
|
|
const columns = [
|
|
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
|
|
{ name: 'title', label: 'Title', field: 'title', align: 'left', sortable: true },
|
|
{ name: 'status', label: 'Status', field: 'status', align: 'center', sortable: true },
|
|
{ name: 'priority', label: 'Priority', field: 'priority', align: 'center', sortable: true },
|
|
{ name: 'severity', label: 'Severity', field: 'severity', align: 'center', sortable: true },
|
|
{ name: 'reporterUsername', label: 'Reporter', field: 'reporterUsername', align: 'left', sortable: true },
|
|
{ name: 'updatedAt', label: 'Last Updated', field: 'updatedAt', align: 'left', sortable: true, format: (val) => new Date(val).toLocaleString() },
|
|
{ name: 'actions', label: 'Actions', field: 'id', align: 'center' },
|
|
];
|
|
|
|
// Fetch distinct filter values
|
|
const fetchFilterOptions = async() =>
|
|
{
|
|
try
|
|
{
|
|
const [statusRes, priorityRes, severityRes, reporterRes] = await Promise.all([
|
|
axios.get('/api/mantis/filters/statuses'),
|
|
axios.get('/api/mantis/filters/priorities'),
|
|
axios.get('/api/mantis/filters/severities'),
|
|
axios.get('/api/mantis/filters/reporters')
|
|
]);
|
|
|
|
// Format options for q-select
|
|
const formatOptions = (data) => data.map(value => ({ label: value, value }));
|
|
|
|
statusOptions.value = formatOptions(statusRes.data);
|
|
priorityOptions.value = formatOptions(priorityRes.data);
|
|
severityOptions.value = formatOptions(severityRes.data);
|
|
reporterOptions.value = formatOptions(reporterRes.data);
|
|
}
|
|
catch (error)
|
|
{
|
|
console.error('Error fetching filter options:', error);
|
|
$q.notify({
|
|
type: 'negative',
|
|
message: 'Failed to load filter options.'
|
|
});
|
|
}
|
|
};
|
|
|
|
const fetchTickets = async(page = pagination.value.page) =>
|
|
{
|
|
loading.value = true;
|
|
try
|
|
{
|
|
const params = {
|
|
page: page,
|
|
limit: pagination.value.rowsPerPage,
|
|
search: searchTerm.value || undefined,
|
|
sortBy: pagination.value.sortBy, // Add sortBy
|
|
sortOrder: pagination.value.descending ? 'desc' : 'asc', // Add sortOrder
|
|
// Add filter parameters
|
|
status: selectedStatus.value || undefined,
|
|
priority: selectedPriority.value || undefined,
|
|
severity: selectedSeverity.value || undefined,
|
|
reporterUsername: selectedReporter.value || undefined,
|
|
};
|
|
// Remove undefined params
|
|
Object.keys(params).forEach(key => params[key] === undefined && delete params[key]);
|
|
|
|
const response = await axios.get('/api/mantis', { params });
|
|
tickets.value = response.data.data;
|
|
pagination.value.rowsNumber = response.data.pagination.total;
|
|
pagination.value.page = response.data.pagination.page; // Update current page
|
|
}
|
|
catch (error)
|
|
{
|
|
console.error('Error fetching Mantis tickets:', error);
|
|
$q.notify({
|
|
type: 'negative',
|
|
message: 'Failed to fetch Mantis tickets.'
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Function to apply filters and reset pagination
|
|
const applyFilters = () =>
|
|
{
|
|
pagination.value.page = 1; // Reset to first page when filters change
|
|
fetchTickets();
|
|
};
|
|
|
|
const handleTableRequest = (props) =>
|
|
{
|
|
const { page, rowsPerPage, sortBy, descending } = props.pagination;
|
|
pagination.value.page = page;
|
|
pagination.value.rowsPerPage = rowsPerPage;
|
|
pagination.value.sortBy = sortBy;
|
|
pagination.value.descending = descending;
|
|
fetchTickets(page);
|
|
};
|
|
|
|
const onRowClick = (evt, row) =>
|
|
{
|
|
//Change the route
|
|
router.push({ name: 'mantis', params: { ticketId: row.id } });
|
|
};
|
|
|
|
const openDialogForTicket = (id) =>
|
|
{
|
|
const ticketNum = Number(id);
|
|
if (!isNaN(ticketNum) && ticketNum > 0)
|
|
{
|
|
selectedTicketId.value = ticketNum;
|
|
showDialog.value = true;
|
|
}
|
|
};
|
|
|
|
const closeDialog = () =>
|
|
{
|
|
showDialog.value = false;
|
|
selectedTicketId.value = null;
|
|
|
|
//Reset the route to remove the ticketId from the URL
|
|
router.push({ name: 'mantis' });
|
|
};
|
|
|
|
// Watch for changes in the ticketId prop
|
|
watch(() => props.ticketId, (newTicketId) =>
|
|
{
|
|
if (newTicketId)
|
|
{
|
|
openDialogForTicket(newTicketId);
|
|
}
|
|
else
|
|
{
|
|
closeDialog();
|
|
}
|
|
});
|
|
|
|
onMounted(() =>
|
|
{
|
|
fetchFilterOptions(); // Fetch filter options on mount
|
|
fetchTickets();
|
|
// Check initial prop value on mount
|
|
if (props.ticketId)
|
|
{
|
|
openDialogForTicket(props.ticketId);
|
|
}
|
|
});
|
|
|
|
// Helper functions for badge colors (customize as needed)
|
|
const getStatusColor = (status) =>
|
|
{
|
|
const lowerStatus = status?.toLowerCase();
|
|
if (lowerStatus === 'new') return 'blue';
|
|
if (lowerStatus === 'feedback') return 'orange';
|
|
if (lowerStatus === 'acknowledged') return 'purple';
|
|
if (lowerStatus === 'confirmed') return 'cyan';
|
|
if (lowerStatus === 'assigned') return 'teal';
|
|
if (lowerStatus === 'resolved') return 'green';
|
|
if (lowerStatus === 'closed') return 'grey';
|
|
return 'primary';
|
|
};
|
|
|
|
const getPriorityColor = (priority) =>
|
|
{
|
|
const lowerPriority = priority?.toLowerCase();
|
|
if (lowerPriority === 'none') return 'grey';
|
|
if (lowerPriority === 'low') return 'blue';
|
|
if (lowerPriority === 'normal') return 'green';
|
|
if (lowerPriority === 'high') return 'orange';
|
|
if (lowerPriority === 'urgent') return 'red';
|
|
if (lowerPriority === 'immediate') return 'purple';
|
|
return 'primary';
|
|
};
|
|
|
|
const getSeverityColor = (severity) =>
|
|
{
|
|
const lowerSeverity = severity?.toLowerCase();
|
|
if (lowerSeverity === 'feature') return 'info';
|
|
if (lowerSeverity === 'trivial') return 'grey';
|
|
if (lowerSeverity === 'text') return 'blue-grey';
|
|
if (lowerSeverity === 'tweak') return 'light-blue';
|
|
if (lowerSeverity === 'minor') return 'lime-9';
|
|
if (lowerSeverity === 'major') return 'amber';
|
|
if (lowerSeverity === 'crash') return 'deep-orange';
|
|
if (lowerSeverity === 'block') return 'red';
|
|
return 'primary';
|
|
};
|
|
|
|
// Function to fetch and display ticket summary
|
|
const showTicketSummary = async(ticketId) =>
|
|
{
|
|
$q.loading.show({
|
|
message: 'Generating Ticket Summary...',
|
|
// spinner: QSpinnerGears,
|
|
spinnerColor: 'white'
|
|
});
|
|
if (!ticketId)
|
|
{
|
|
$q.loading.hide(); // Hide loading if no ticketId
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
const response = await axios.get(`/api/mantis/summary/${ticketId}`);
|
|
|
|
// Store summary data and show the dialog
|
|
summaryContent.value = marked(response.data?.summary || 'No summary available');
|
|
//Turn anything surrounded by `[]` into a span with a class of "label"
|
|
summaryContent.value = summaryContent.value.replace(/\[(.*?)\]/g, '<span class="label">[$1]</span>');
|
|
summaryTicketId.value = ticketId;
|
|
showSummaryDialog.value = true;
|
|
}
|
|
catch (error)
|
|
{
|
|
console.error('Error fetching ticket summary:', error);
|
|
$q.notify({
|
|
type: 'negative',
|
|
message: `Failed to fetch summary for ticket #${ticketId}: ${error.response?.data?.message || error.message}`
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
$q.loading.hide();
|
|
}
|
|
};
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Add any specific styles here */
|
|
.summary-content:deep(h6) {
|
|
margin: 0;
|
|
}
|
|
|
|
.summary-content:deep(span.label) {
|
|
font-family: monospace;
|
|
}
|
|
|
|
.summary-content:deep(span.label):after {
|
|
content: ' - ';
|
|
}
|
|
</style>
|