|
|
@@ -1,126 +1,98 @@
|
|
|
<template>
|
|
|
<div class="q-pa-md">
|
|
|
+ <input
|
|
|
+ ref="fileInputRef"
|
|
|
+ type="file"
|
|
|
+ accept=".pdf"
|
|
|
+ class="hidden"
|
|
|
+ @change="onFileSelected"
|
|
|
+ />
|
|
|
+
|
|
|
<div class="row q-col-gutter-md">
|
|
|
- <!-- Coluna esquerda: tabela + aditivos -->
|
|
|
+ <!-- Coluna esquerda: lista -->
|
|
|
<div class="col-12 col-md-5">
|
|
|
- <q-table
|
|
|
- :rows="contracts"
|
|
|
- :columns="columns"
|
|
|
- row-key="id"
|
|
|
- flat
|
|
|
- hide-bottom
|
|
|
- class="bg-transparent"
|
|
|
- :row-class="
|
|
|
- (_, index) =>
|
|
|
- index === selectedIndex
|
|
|
- ? 'contract-item-active cursor-pointer'
|
|
|
- : 'cursor-pointer'
|
|
|
- "
|
|
|
- @row-click="(_, __, index) => (selectedIndex = index)"
|
|
|
- >
|
|
|
- <template #body-cell-period="{ row }">
|
|
|
- <q-td align="left">
|
|
|
- {{ row.startDate }} - {{ row.endDate }}
|
|
|
- </q-td>
|
|
|
- </template>
|
|
|
-
|
|
|
- <template #body-cell-status="{ row }">
|
|
|
- <q-td align="center">
|
|
|
- <q-badge
|
|
|
- :color="row.status === 'Ativo' ? 'teal' : 'negative'"
|
|
|
- :label="row.status"
|
|
|
- style="border-radius: 12px; padding: 4px 10px; font-size: 12px"
|
|
|
- />
|
|
|
- </q-td>
|
|
|
- </template>
|
|
|
-
|
|
|
- <template #body-cell-actions>
|
|
|
- <q-td align="center">
|
|
|
- <div class="flex items-center justify-center q-gutter-x-xs">
|
|
|
- <q-icon
|
|
|
- name="mdi-file-document-outline"
|
|
|
- size="sm"
|
|
|
- color="grey-8"
|
|
|
- class="cursor-pointer"
|
|
|
- style="border: 1px solid #c9c9c9; border-radius: 8px; padding: 4px"
|
|
|
- @click.stop="console.log('view')"
|
|
|
- />
|
|
|
- <q-icon
|
|
|
- name="mdi-trash-can-outline"
|
|
|
- size="sm"
|
|
|
- color="grey-8"
|
|
|
- class="cursor-pointer"
|
|
|
- style="border: 1px solid #c9c9c9; border-radius: 8px; padding: 4px"
|
|
|
- @click.stop="console.log('trash')"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </q-td>
|
|
|
- </template>
|
|
|
- </q-table>
|
|
|
-
|
|
|
- <!-- Aditivos de Contrato -->
|
|
|
- <div class="q-mt-md">
|
|
|
- <p class="text-weight-medium q-mb-sm">Aditivos de Contrato</p>
|
|
|
- <q-list separator>
|
|
|
+ <div class="row justify-between items-center q-mb-md">
|
|
|
+ <span class="text-subtitle1 text-weight-medium">Contratos</span>
|
|
|
+ <q-btn
|
|
|
+ icon="add"
|
|
|
+ color="primary-2"
|
|
|
+ style="height: 40px; width: 40px; border-radius: 8px"
|
|
|
+ :loading="uploading"
|
|
|
+ @click="fileInputRef.click()"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="loading" class="row justify-center q-pa-xl">
|
|
|
+ <q-spinner color="primary" size="40px" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template v-else>
|
|
|
+ <div v-if="contracts.length === 0" class="text-center text-grey-6 q-pa-xl">
|
|
|
+ <q-icon name="mdi-file-pdf-box" size="48px" color="grey-4" />
|
|
|
+ <div class="q-mt-sm">Nenhum contrato adicionado.</div>
|
|
|
+ <div v-if="!unitId" class="text-caption q-mt-xs">
|
|
|
+ Os contratos serão enviados junto com a unidade.
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <q-list v-else separator>
|
|
|
<q-item
|
|
|
- v-for="(additive, i) in additives"
|
|
|
- :key="i"
|
|
|
+ v-for="(contract, index) in contracts"
|
|
|
+ :key="contract.id ?? index"
|
|
|
clickable
|
|
|
- dense
|
|
|
- class="q-py-sm"
|
|
|
+ :active="selectedIndex === index"
|
|
|
+ active-class="contract-item-active"
|
|
|
+ @click="selectedIndex = index"
|
|
|
>
|
|
|
<q-item-section avatar>
|
|
|
- <q-avatar
|
|
|
- size="24px"
|
|
|
- color="primary-2"
|
|
|
- text-color="white"
|
|
|
- style="font-size: 12px"
|
|
|
- >
|
|
|
- {{ i + 1 }}
|
|
|
- </q-avatar>
|
|
|
+ <q-icon name="mdi-file-pdf-box" color="negative" size="md" />
|
|
|
+ </q-item-section>
|
|
|
+
|
|
|
+ <q-item-section>
|
|
|
+ <q-item-label class="ellipsis" style="max-width: 180px">
|
|
|
+ {{ contract.name }}
|
|
|
+ </q-item-label>
|
|
|
+ <q-item-label caption>
|
|
|
+ <span v-if="contract.id">{{ formatDate(contract.created_at) }}</span>
|
|
|
+ <q-badge v-else color="orange" label="Pendente" />
|
|
|
+ </q-item-label>
|
|
|
</q-item-section>
|
|
|
- <q-item-section>{{ additive.title }}</q-item-section>
|
|
|
+
|
|
|
<q-item-section side>
|
|
|
- <q-icon
|
|
|
- name="mdi-file-document-outline"
|
|
|
+ <q-btn
|
|
|
+ flat
|
|
|
+ round
|
|
|
+ dense
|
|
|
+ icon="delete"
|
|
|
+ color="negative"
|
|
|
size="sm"
|
|
|
- color="grey-6"
|
|
|
- class="cursor-pointer"
|
|
|
- @click.stop="console.log('additive', i)"
|
|
|
+ @click.stop="onRemove(contract, index)"
|
|
|
/>
|
|
|
</q-item-section>
|
|
|
</q-item>
|
|
|
</q-list>
|
|
|
- </div>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
|
|
|
<!-- Coluna direita: pré-visualização -->
|
|
|
<div class="col-12 col-md-7">
|
|
|
- <div class="row justify-end q-mb-sm">
|
|
|
- <q-btn
|
|
|
- icon="add"
|
|
|
- color="primary-2"
|
|
|
- style="height: 40px; width: 40px; border-radius: 8px"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div class="preview-box q-pa-md">
|
|
|
- <span v-if="selectedIndex === null" class="text-grey-5">
|
|
|
- Pré - Visualização
|
|
|
- </span>
|
|
|
- <div v-else>
|
|
|
- <p class="text-weight-medium q-mb-xs">
|
|
|
- Contrato {{ contracts[selectedIndex].id }}
|
|
|
- </p>
|
|
|
- <p class="text-grey-7 q-mb-sm">
|
|
|
- {{ contracts[selectedIndex].startDate }} até
|
|
|
- {{ contracts[selectedIndex].endDate }}
|
|
|
- </p>
|
|
|
- <q-badge
|
|
|
- :color="contracts[selectedIndex].status === 'Ativo' ? 'teal' : 'negative'"
|
|
|
- :label="contracts[selectedIndex].status"
|
|
|
- style="border-radius: 12px; padding: 4px 10px; font-size: 12px"
|
|
|
- />
|
|
|
+ <div class="preview-box">
|
|
|
+ <div
|
|
|
+ v-if="selectedIndex === null || !contracts[selectedIndex]"
|
|
|
+ class="flex flex-center full-height text-grey-5"
|
|
|
+ style="min-height: 500px"
|
|
|
+ >
|
|
|
+ <div class="column items-center q-gutter-sm">
|
|
|
+ <q-icon name="mdi-file-pdf-box" size="64px" color="grey-3" />
|
|
|
+ <span>Selecione um contrato para visualizar</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <iframe
|
|
|
+ v-else
|
|
|
+ :src="getPreviewUrl(contracts[selectedIndex])"
|
|
|
+ style="width: 100%; min-height: 500px; border: none; border-radius: 8px"
|
|
|
+ />
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -128,65 +100,154 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref } from "vue";
|
|
|
+import { ref, onMounted, onUnmounted } from "vue";
|
|
|
+import { useQuasar } from "quasar";
|
|
|
+import { getContractsByUnit, createContract, deleteContract } from "src/api/unit_contract";
|
|
|
|
|
|
+const props = defineProps({
|
|
|
+ unitId: {
|
|
|
+ type: Number,
|
|
|
+ default: null,
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const pendingContracts = defineModel("contracts", { type: Array, default: () => [] });
|
|
|
+
|
|
|
+const $q = useQuasar();
|
|
|
+const fileInputRef = ref(null);
|
|
|
+const contracts = ref([]);
|
|
|
const selectedIndex = ref(null);
|
|
|
+const loading = ref(false);
|
|
|
+const uploading = ref(false);
|
|
|
|
|
|
-const columns = [
|
|
|
- { name: "id", label: "Contrato", field: "id", align: "left" },
|
|
|
- {
|
|
|
- name: "period",
|
|
|
- label: "Data Inicial - Final",
|
|
|
- field: "startDate",
|
|
|
- align: "left",
|
|
|
- },
|
|
|
- {
|
|
|
- name: "status",
|
|
|
- label: "Status Contrato",
|
|
|
- field: "status",
|
|
|
- align: "center",
|
|
|
- style: "width: 130px",
|
|
|
- },
|
|
|
- {
|
|
|
- name: "actions",
|
|
|
- label: "Ações",
|
|
|
- field: "actions",
|
|
|
- align: "center",
|
|
|
- style: "width: 90px",
|
|
|
- },
|
|
|
-];
|
|
|
-
|
|
|
-const contracts = ref([
|
|
|
- {
|
|
|
- id: "5789128",
|
|
|
- startDate: "17/02/2025",
|
|
|
- endDate: "17/02/2026",
|
|
|
- status: "Ativo",
|
|
|
- },
|
|
|
- {
|
|
|
- id: "5789128",
|
|
|
- startDate: "17/02/2025",
|
|
|
- endDate: "17/02/2026",
|
|
|
- status: "Inativo",
|
|
|
+// ─── Busca contratos salvos (modo edição) ───────────────────────────────────
|
|
|
+async function fetchContracts() {
|
|
|
+ if (!props.unitId) {
|
|
|
+ contracts.value = pendingContracts.value;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ contracts.value = await getContractsByUnit(props.unitId);
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e);
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Seleção de arquivo ─────────────────────────────────────────────────────
|
|
|
+async function onFileSelected(event) {
|
|
|
+ const file = event.target.files[0];
|
|
|
+ if (!file) return;
|
|
|
+
|
|
|
+ // Limpa o input para permitir selecionar o mesmo arquivo novamente
|
|
|
+ event.target.value = "";
|
|
|
+
|
|
|
+ if (props.unitId) {
|
|
|
+ // Modo edição: envia direto para a API
|
|
|
+ uploading.value = true;
|
|
|
+ try {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append("unit_id", props.unitId);
|
|
|
+ formData.append("file", file);
|
|
|
+ formData.append("name", file.name);
|
|
|
+
|
|
|
+ const created = await createContract(formData);
|
|
|
+ contracts.value.unshift(created);
|
|
|
+ selectedIndex.value = 0;
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e);
|
|
|
+ } finally {
|
|
|
+ uploading.value = false;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Modo criação: adiciona à lista pendente
|
|
|
+ const previewUrl = URL.createObjectURL(file);
|
|
|
+ const item = { name: file.name, file, previewUrl };
|
|
|
+ pendingContracts.value.push(item);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Remoção ────────────────────────────────────────────────────────────────
|
|
|
+function onRemove(contract, index) {
|
|
|
+ if (contract.id) {
|
|
|
+ // Modo edição: confirma e deleta via API
|
|
|
+ $q.dialog({
|
|
|
+ title: "Remover contrato",
|
|
|
+ message: `Deseja remover o contrato "${contract.name}"?`,
|
|
|
+ ok: { color: "negative", label: "Remover" },
|
|
|
+ cancel: { color: "primary", outline: true, label: "Cancelar" },
|
|
|
+ }).onOk(async () => {
|
|
|
+ try {
|
|
|
+ await deleteContract(contract.id);
|
|
|
+ contracts.value.splice(index, 1);
|
|
|
+ if (selectedIndex.value === index) selectedIndex.value = null;
|
|
|
+ else if (selectedIndex.value > index) selectedIndex.value--;
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // Modo criação: revoga object URL e remove da lista pendente
|
|
|
+ if (contract.previewUrl) URL.revokeObjectURL(contract.previewUrl);
|
|
|
+ pendingContracts.value.splice(index, 1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
|
+function getPreviewUrl(contract) {
|
|
|
+ return contract.previewUrl ?? contract.file_url;
|
|
|
+}
|
|
|
+
|
|
|
+function formatDate(dateStr) {
|
|
|
+ if (!dateStr) return "";
|
|
|
+ return new Date(dateStr).toLocaleDateString("pt-BR");
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Sincroniza lista pendente quando o pai atualiza ────────────────────────
|
|
|
+// (necessário pois v-show mantém o componente vivo mas não re-executa onMounted)
|
|
|
+import { watch } from "vue";
|
|
|
+
|
|
|
+watch(
|
|
|
+ pendingContracts,
|
|
|
+ (val) => {
|
|
|
+ if (!props.unitId) contracts.value = val;
|
|
|
},
|
|
|
-]);
|
|
|
-
|
|
|
-const additives = ref([
|
|
|
- { title: "Aditivo 1" },
|
|
|
- { title: "Aditivo 2" },
|
|
|
- { title: "Aditivo 3" },
|
|
|
- { title: "Aditivo 4" },
|
|
|
-]);
|
|
|
+ { immediate: true },
|
|
|
+);
|
|
|
+
|
|
|
+onMounted(fetchContracts);
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ // Libera object URLs de pendentes não enviados
|
|
|
+ if (!props.unitId) {
|
|
|
+ pendingContracts.value.forEach((c) => {
|
|
|
+ if (c.previewUrl) URL.revokeObjectURL(c.previewUrl);
|
|
|
+ });
|
|
|
+ }
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
+.hidden {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
.preview-box {
|
|
|
border: 1px solid #e0e0e0;
|
|
|
border-radius: 8px;
|
|
|
- min-height: 400px;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.contract-item-active {
|
|
|
background-color: rgba(255, 131, 64, 0.08);
|
|
|
}
|
|
|
+
|
|
|
+.ellipsis {
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
</style>
|