Initial commit.
This commit is contained in:
commit
2d11d0bd79
54 changed files with 6657 additions and 0 deletions
216
src/pages/FormResponsesPage.vue
Normal file
216
src/pages/FormResponsesPage.vue
Normal file
|
@ -0,0 +1,216 @@
|
|||
<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 v-slot: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 v-slot:body-cell-submittedAt="props">
|
||||
<q-td :props="props">
|
||||
{{ new Date(props.value).toLocaleString() }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- Slot for Actions column -->
|
||||
<template v-slot: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 v-slot: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 v-slot:avatar>
|
||||
<q-icon name="error" />
|
||||
</template>
|
||||
Form not found or could not load responses.
|
||||
<template v-slot: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 'axios';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const $q = useQuasar();
|
||||
const formTitle = ref('');
|
||||
const responses = ref([]);
|
||||
const columns = ref([]); // Columns will be generated dynamically
|
||||
const loading = ref(true);
|
||||
const filterText = ref(''); // Add ref for filter text
|
||||
|
||||
// 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/${props.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}`, // Unique name for column
|
||||
label: field.label,
|
||||
field: row => row.values[field.id]?.value ?? '', // Access nested value safely
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
// Add formatting based on field.type if needed
|
||||
});
|
||||
});
|
||||
});
|
||||
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/${props.id}/responses`);
|
||||
responses.value = responsesResponse.data; // API already groups them
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error fetching data for form ${props.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>
|
Loading…
Add table
Add a link
Reference in a new issue