250 lines
6 KiB
Vue
250 lines
6 KiB
Vue
<template>
|
|
<q-page padding>
|
|
<q-inner-loading :showing="loading">
|
|
<q-spinner-gears
|
|
size="50px"
|
|
color="primary"
|
|
/>
|
|
</q-inner-loading>
|
|
|
|
<div v-if="!loading && formTitle">
|
|
<div class="row justify-between items-center q-mb-md">
|
|
<div class="text-h4">
|
|
Responses for: {{ formTitle }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Search Input -->
|
|
<q-input
|
|
v-if="responses.length > 0"
|
|
outlined
|
|
dense
|
|
debounce="300"
|
|
v-model="filterText"
|
|
placeholder="Search responses..."
|
|
class="q-mb-md"
|
|
>
|
|
<template #append>
|
|
<q-icon name="search" />
|
|
</template>
|
|
</q-input>
|
|
|
|
<q-table
|
|
v-if="responses.length > 0"
|
|
:rows="formattedResponses"
|
|
:columns="columns"
|
|
row-key="id"
|
|
flat
|
|
bordered
|
|
separator="cell"
|
|
wrap-cells
|
|
:filter="filterText"
|
|
>
|
|
<template #body-cell-submittedAt="props">
|
|
<q-td :props="props">
|
|
{{ new Date(props.value).toLocaleString() }}
|
|
</q-td>
|
|
</template>
|
|
|
|
<!-- Slot for Actions column -->
|
|
<template #body-cell-actions="props">
|
|
<q-td :props="props">
|
|
<q-btn
|
|
flat
|
|
dense
|
|
round
|
|
icon="download"
|
|
color="primary"
|
|
@click="downloadResponsePdf(props.row.id)"
|
|
aria-label="Download PDF"
|
|
>
|
|
<q-tooltip>Download PDF</q-tooltip>
|
|
</q-btn>
|
|
</q-td>
|
|
</template>
|
|
</q-table>
|
|
|
|
<q-banner
|
|
v-else
|
|
class=""
|
|
>
|
|
<template #avatar>
|
|
<q-icon
|
|
name="info"
|
|
color="info"
|
|
/>
|
|
</template>
|
|
No responses have been submitted for this form yet.
|
|
</q-banner>
|
|
</div>
|
|
<q-banner
|
|
v-else-if="!loading && !formTitle"
|
|
class="bg-negative text-white"
|
|
>
|
|
<template #avatar>
|
|
<q-icon name="error" />
|
|
</template>
|
|
Form not found or could not load responses.
|
|
<template #action>
|
|
<q-btn
|
|
flat
|
|
color="white"
|
|
label="Back to Forms"
|
|
:to="{ name: 'formList' }"
|
|
/>
|
|
</template>
|
|
</q-banner>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed } from 'vue';
|
|
import axios from 'boot/axios';
|
|
import { useQuasar } from 'quasar';
|
|
|
|
const componentProps = defineProps({
|
|
id: {
|
|
type: [String, Number],
|
|
required: true
|
|
}
|
|
});
|
|
|
|
const $q = useQuasar();
|
|
const formTitle = ref('');
|
|
const responses = ref([]);
|
|
const columns = ref([]);
|
|
const loading = ref(true);
|
|
const filterText = ref('');
|
|
|
|
// Fetch both form details (for title and field labels/order) and responses
|
|
async function fetchData()
|
|
{
|
|
loading.value = true;
|
|
formTitle.value = '';
|
|
responses.value = [];
|
|
columns.value = [];
|
|
|
|
try
|
|
{
|
|
// Fetch form details first to get the structure
|
|
const formDetailsResponse = await axios.get(`/api/forms/${componentProps.id}`);
|
|
const form = formDetailsResponse.data;
|
|
formTitle.value = form.title;
|
|
|
|
// Generate columns based on form fields in correct order
|
|
const generatedColumns = [{ name: 'submittedAt', label: 'Submitted At', field: 'submittedAt', align: 'left', sortable: true }];
|
|
form.categories.forEach(cat =>
|
|
{
|
|
cat.fields.forEach(field =>
|
|
{
|
|
generatedColumns.push({
|
|
name: `field_${field.id}`,
|
|
label: field.label,
|
|
field: row => row.values[field.id]?.value ?? '',
|
|
align: 'left',
|
|
sortable: true,
|
|
});
|
|
});
|
|
});
|
|
columns.value = generatedColumns;
|
|
|
|
// Add Actions column
|
|
columns.value.push({
|
|
name: 'actions',
|
|
label: 'Actions',
|
|
field: 'actions',
|
|
align: 'center'
|
|
});
|
|
|
|
// Fetch responses
|
|
const responsesResponse = await axios.get(`/api/forms/${componentProps.id}/responses`);
|
|
responses.value = responsesResponse.data;
|
|
|
|
}
|
|
catch (error)
|
|
{
|
|
console.error(`Error fetching data for form ${componentProps.id}:`, error);
|
|
$q.notify({
|
|
color: 'negative',
|
|
position: 'top',
|
|
message: 'Failed to load form responses.',
|
|
icon: 'report_problem'
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
// Computed property to match the structure expected by QTable rows
|
|
const formattedResponses = computed(() =>
|
|
{
|
|
return responses.value.map(response =>
|
|
{
|
|
const row = {
|
|
id: response.id,
|
|
submittedAt: response.submittedAt,
|
|
// Flatten values for direct access by field function in columns
|
|
values: response.values
|
|
};
|
|
return row;
|
|
});
|
|
});
|
|
|
|
// Function to download a single response as PDF
|
|
async function downloadResponsePdf(responseId)
|
|
{
|
|
try
|
|
{
|
|
const response = await axios.get(`/api/responses/${responseId}/export/pdf`, {
|
|
responseType: 'blob', // Important for handling file downloads
|
|
});
|
|
|
|
// Create a URL for the blob
|
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
|
|
// Try to get filename from content-disposition header
|
|
const contentDisposition = response.headers['content-disposition'];
|
|
let filename = `response-${responseId}.pdf`; // Default filename
|
|
if (contentDisposition)
|
|
{
|
|
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i);
|
|
if (filenameMatch && filenameMatch.length > 1)
|
|
{
|
|
filename = filenameMatch[1];
|
|
}
|
|
}
|
|
|
|
link.setAttribute('download', filename);
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
|
|
// Clean up
|
|
link.parentNode.removeChild(link);
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
$q.notify({
|
|
color: 'positive',
|
|
position: 'top',
|
|
message: `Downloaded ${filename}`,
|
|
icon: 'check_circle'
|
|
});
|
|
|
|
}
|
|
catch (error)
|
|
{
|
|
console.error(`Error downloading PDF for response ${responseId}:`, error);
|
|
$q.notify({
|
|
color: 'negative',
|
|
position: 'top',
|
|
message: 'Failed to download PDF.',
|
|
icon: 'report_problem'
|
|
});
|
|
}
|
|
}
|
|
|
|
onMounted(fetchData);
|
|
</script>
|