|
|
@@ -1,253 +1,77 @@
|
|
|
<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: lista -->
|
|
|
- <div class="col-12 col-md-5">
|
|
|
- <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="(contract, index) in contracts"
|
|
|
- :key="contract.id ?? index"
|
|
|
- clickable
|
|
|
- :active="selectedIndex === index"
|
|
|
- active-class="contract-item-active"
|
|
|
- @click="selectedIndex = index"
|
|
|
- >
|
|
|
- <q-item-section 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 side>
|
|
|
- <q-btn
|
|
|
- flat
|
|
|
- round
|
|
|
- dense
|
|
|
- icon="delete"
|
|
|
- color="negative"
|
|
|
- size="sm"
|
|
|
- @click.stop="onRemove(contract, index)"
|
|
|
- />
|
|
|
- </q-item-section>
|
|
|
- </q-item>
|
|
|
- </q-list>
|
|
|
- </template>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- Coluna direita: pré-visualização -->
|
|
|
- <div class="col-12 col-md-7">
|
|
|
- <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>
|
|
|
+ <DefaultTable
|
|
|
+ :columns="columns"
|
|
|
+ :no-api-call="true"
|
|
|
+ :show-search-field="true"
|
|
|
+ :add-item="true"
|
|
|
+ :female="false"
|
|
|
+ title="Contratos"
|
|
|
+ description="Contratos"
|
|
|
+ >
|
|
|
+ <template #body-cell-contract_dates="{ row }">
|
|
|
+ <q-td>{{ row.contract_start_date }} - {{ row.contract_end_date }}</q-td>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template #body-cell-actions="{ row: contract }">
|
|
|
+ <q-td auto-width>
|
|
|
+ <div class="row no-wrap" style="gap: 4px">
|
|
|
+ <q-btn flat round dense icon="mdi-eye-outline" size="sm" />
|
|
|
+ <q-btn
|
|
|
+ v-show="contract.status !== 'active'"
|
|
|
+ flat
|
|
|
+ round
|
|
|
+ dense
|
|
|
+ icon="mdi-file-edit-outline"
|
|
|
+ size="sm"
|
|
|
+ />
|
|
|
</div>
|
|
|
-
|
|
|
- <iframe
|
|
|
- v-else
|
|
|
- :src="getPreviewUrl(contracts[selectedIndex])"
|
|
|
- style="width: 100%; min-height: 500px; border: none; border-radius: 8px"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ </q-td>
|
|
|
+ </template>
|
|
|
+ </DefaultTable>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, onMounted, onUnmounted } from "vue";
|
|
|
-import { useQuasar } from "quasar";
|
|
|
-import { getContractsByUnit, createContract, deleteContract } from "src/api/unit_contract";
|
|
|
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
|
|
|
|
|
|
-const props = defineProps({
|
|
|
+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);
|
|
|
-
|
|
|
-// ─── 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 columns = [
|
|
|
+ {
|
|
|
+ name: "contract_number",
|
|
|
+ label: "Contrato",
|
|
|
+ field: "contract_number",
|
|
|
+ align: "left",
|
|
|
},
|
|
|
- { 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);
|
|
|
- });
|
|
|
- }
|
|
|
-});
|
|
|
+ {
|
|
|
+ name: "contract_dates",
|
|
|
+ label: "Data Inicial - Final",
|
|
|
+ field: "contract_start_date",
|
|
|
+ align: "left",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "contract_tbr",
|
|
|
+ label: "TBR",
|
|
|
+ field: "contract_tbr",
|
|
|
+ align: "left",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "contract_status",
|
|
|
+ label: "Status Contrato",
|
|
|
+ field: "contract_status",
|
|
|
+ align: "left",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "actions",
|
|
|
+ label: "Ações",
|
|
|
+ field: "actions",
|
|
|
+ align: "center",
|
|
|
+ },
|
|
|
+];
|
|
|
</script>
|
|
|
-
|
|
|
-<style scoped>
|
|
|
-.hidden {
|
|
|
- display: none;
|
|
|
-}
|
|
|
-
|
|
|
-.preview-box {
|
|
|
- border: 1px solid #e0e0e0;
|
|
|
- border-radius: 8px;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-
|
|
|
-.contract-item-active {
|
|
|
- background-color: rgba(255, 131, 64, 0.08);
|
|
|
-}
|
|
|
-
|
|
|
-.ellipsis {
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
-}
|
|
|
-</style>
|