|
|
@@ -1,7 +1,14 @@
|
|
|
<template>
|
|
|
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
|
|
<div style="width: 100%; max-width: 1100px">
|
|
|
- <q-card style="height: 560px; display: flex; flex-direction: column; overflow: hidden">
|
|
|
+ <q-card
|
|
|
+ style="
|
|
|
+ height: 560px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+ "
|
|
|
+ >
|
|
|
<DefaultDialogHeader
|
|
|
:title="card ? 'Editar Atividade' : 'Adicionar Atividade'"
|
|
|
@close="onDialogCancel"
|
|
|
@@ -9,185 +16,195 @@
|
|
|
|
|
|
<DefaultForm
|
|
|
ref="formRef"
|
|
|
- style="flex: 1; display: flex; flex-direction: column; overflow: hidden"
|
|
|
+ style="
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+ "
|
|
|
@submit="onOKClick"
|
|
|
>
|
|
|
- <q-card-section class="q-pt-sm" style="flex: 1; overflow-y: auto; min-height: 0">
|
|
|
- <CustomTabComponent
|
|
|
- v-if="card?.id"
|
|
|
- v-model:active-tab="currentTab"
|
|
|
- :tabs="tabs"
|
|
|
- class="q-mb-md"
|
|
|
- />
|
|
|
+ <q-scroll-area class="form-dialog-scroll-area">
|
|
|
+ <q-card-section class="q-pt-sm">
|
|
|
+ <CustomTabComponent
|
|
|
+ v-if="card?.id"
|
|
|
+ v-model:active-tab="currentTab"
|
|
|
+ :tabs="tabs"
|
|
|
+ class="q-mb-md"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- Tab: Atividade -->
|
|
|
+ <div v-show="currentTab === 'atividade'">
|
|
|
+ <div class="row q-col-gutter-sm">
|
|
|
+ <!-- 1. Título -->
|
|
|
+ <DefaultInput
|
|
|
+ v-model="form.title"
|
|
|
+ label="Título da Tarefa"
|
|
|
+ class="col-12"
|
|
|
+ :rules="[(val) => !!val || 'Campo obrigatório']"
|
|
|
+ />
|
|
|
|
|
|
- <!-- Tab: Atividade -->
|
|
|
- <div v-show="currentTab === 'atividade'">
|
|
|
- <div class="row q-col-gutter-sm">
|
|
|
- <!-- 1. Título -->
|
|
|
- <DefaultInput
|
|
|
- v-model="form.title"
|
|
|
- label="Título da Tarefa"
|
|
|
- class="col-12"
|
|
|
- :rules="[val => !!val || 'Campo obrigatório']"
|
|
|
- />
|
|
|
-
|
|
|
- <!-- 2. Destino (primeiro campo de configuração) -->
|
|
|
- <DefaultSelect
|
|
|
- v-model="form.scope"
|
|
|
- label="Destino"
|
|
|
- :options="scopeOptions"
|
|
|
- emit-value
|
|
|
- map-options
|
|
|
- class="col-6"
|
|
|
- :rules="[val => !!val || 'Campo obrigatório']"
|
|
|
- />
|
|
|
-
|
|
|
- <!-- Prazo ao lado do Destino -->
|
|
|
- <DefaultInputDatePicker
|
|
|
- v-model="form.due_date_display"
|
|
|
- v-model:untreated-date="form.due_date"
|
|
|
- label="Prazo de Entrega"
|
|
|
- class="col-6"
|
|
|
- />
|
|
|
-
|
|
|
- <!-- Unidade específica — só aparece quando scope = specific -->
|
|
|
- <UnitSelect
|
|
|
- v-if="form.scope === 'specific'"
|
|
|
- v-model="selectedUnit"
|
|
|
- class="col-6"
|
|
|
- />
|
|
|
-
|
|
|
- <!-- 3. Prioridade + Fase -->
|
|
|
- <DefaultSelect
|
|
|
- v-model="form.priority"
|
|
|
- label="Prioridade"
|
|
|
- :options="priorityOptions"
|
|
|
- emit-value
|
|
|
- map-options
|
|
|
- class="col-6"
|
|
|
- :rules="[val => !!val || 'Campo obrigatório']"
|
|
|
- />
|
|
|
-
|
|
|
- <DefaultSelect
|
|
|
- v-model="form.phase"
|
|
|
- label="Fase"
|
|
|
- :options="phaseOptions"
|
|
|
- emit-value
|
|
|
- map-options
|
|
|
- class="col-6"
|
|
|
- :rules="[val => !!val || 'Campo obrigatório']"
|
|
|
- />
|
|
|
-
|
|
|
- <!-- 4. Setor -->
|
|
|
- <DefaultInput
|
|
|
- v-model="form.sector"
|
|
|
- label="Setor"
|
|
|
- class="col-6"
|
|
|
- />
|
|
|
-
|
|
|
- <!-- 5. Descrição -->
|
|
|
- <DefaultInput
|
|
|
- v-model="form.description"
|
|
|
- label="Descrição"
|
|
|
- type="textarea"
|
|
|
- class="col-12"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- Tab: Comentários -->
|
|
|
- <div v-show="currentTab === 'comentarios'">
|
|
|
- <div class="flex justify-end q-mb-sm">
|
|
|
- <q-btn
|
|
|
- v-if="canAdd"
|
|
|
- color="primary"
|
|
|
- icon="mdi-plus"
|
|
|
- unelevated
|
|
|
- style="width: 40px; height: 40px"
|
|
|
- @click="onAddComment"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div
|
|
|
- style="
|
|
|
- height: 100%;
|
|
|
- max-height: 380px;
|
|
|
- overflow-y: auto;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 8px;
|
|
|
- "
|
|
|
- >
|
|
|
- <template v-if="replies.length">
|
|
|
- <KanbanCommentCard
|
|
|
- v-for="reply in replies"
|
|
|
- :key="reply.id"
|
|
|
- :reply="reply.reply"
|
|
|
- :created-at="reply.created_at"
|
|
|
- :user-name="reply.user_name"
|
|
|
- :can-edit="canEdit"
|
|
|
- :can-delete="canDelete"
|
|
|
- @edit="onEditComment(reply)"
|
|
|
- @delete="onDeleteComment(reply)"
|
|
|
+ <!-- 2. Destino (primeiro campo de configuração) -->
|
|
|
+ <DefaultSelect
|
|
|
+ v-model="form.scope"
|
|
|
+ label="Destino"
|
|
|
+ :options="scopeOptions"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ class="col-6"
|
|
|
+ :rules="[(val) => !!val || 'Campo obrigatório']"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- Prazo ao lado do Destino -->
|
|
|
+ <DefaultInputDatePicker
|
|
|
+ v-model="form.due_date_display"
|
|
|
+ v-model:untreated-date="form.due_date"
|
|
|
+ label="Prazo de Entrega"
|
|
|
+ class="col-6"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- Unidade específica — só aparece quando scope = specific -->
|
|
|
+ <UnitSelect
|
|
|
+ v-if="form.scope === 'specific'"
|
|
|
+ v-model="selectedUnit"
|
|
|
+ class="col-6"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 3. Prioridade + Fase -->
|
|
|
+ <DefaultSelect
|
|
|
+ v-model="form.priority"
|
|
|
+ label="Prioridade"
|
|
|
+ :options="priorityOptions"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ class="col-6"
|
|
|
+ :rules="[(val) => !!val || 'Campo obrigatório']"
|
|
|
+ />
|
|
|
+
|
|
|
+ <DefaultSelect
|
|
|
+ v-model="form.phase"
|
|
|
+ label="Fase"
|
|
|
+ :options="phaseOptions"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ class="col-6"
|
|
|
+ :rules="[(val) => !!val || 'Campo obrigatório']"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 4. Setor -->
|
|
|
+ <DefaultInput
|
|
|
+ v-model="form.sector"
|
|
|
+ label="Setor"
|
|
|
+ class="col-6"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 5. Descrição -->
|
|
|
+ <DefaultInput
|
|
|
+ v-model="form.description"
|
|
|
+ label="Descrição"
|
|
|
+ type="textarea"
|
|
|
+ class="col-12"
|
|
|
/>
|
|
|
- </template>
|
|
|
- <div v-else class="flex flex-center full-height text-grey-5 text-body2">
|
|
|
- Nenhum comentário registrado.
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- Tab: Mídias -->
|
|
|
- <div v-show="currentTab === 'midias'">
|
|
|
- <div class="flex justify-end q-mb-sm">
|
|
|
- <q-btn
|
|
|
- v-if="canAdd"
|
|
|
- color="primary"
|
|
|
- icon="mdi-upload-outline"
|
|
|
- unelevated
|
|
|
- style="width: 40px; height: 40px"
|
|
|
- :loading="uploadingMedia"
|
|
|
- @click="triggerFileInput"
|
|
|
- />
|
|
|
- <input
|
|
|
- ref="fileInputRef"
|
|
|
- type="file"
|
|
|
- style="display: none"
|
|
|
- @change="onFileSelected"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div
|
|
|
- style="
|
|
|
- height: 100%;
|
|
|
- max-height: 380px;
|
|
|
- overflow-y: auto;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 8px;
|
|
|
- "
|
|
|
- >
|
|
|
- <template v-if="medias.length">
|
|
|
- <KanbanMediaCard
|
|
|
- v-for="media in medias"
|
|
|
- :key="media.id"
|
|
|
- :file-name="media.file_name"
|
|
|
- :file-url="media.file_url"
|
|
|
- :mime-type="media.mime_type"
|
|
|
- :created-at="media.created_at"
|
|
|
- :user-name="media.user_name"
|
|
|
- :can-delete="canDelete"
|
|
|
- @delete="onDeleteMedia(media)"
|
|
|
+
|
|
|
+ <!-- Tab: Comentários -->
|
|
|
+ <div v-show="currentTab === 'comentarios'">
|
|
|
+ <div class="flex justify-end q-mb-sm">
|
|
|
+ <q-btn
|
|
|
+ v-if="canAdd"
|
|
|
+ color="primary"
|
|
|
+ icon="mdi-plus"
|
|
|
+ unelevated
|
|
|
+ style="width: 40px; height: 40px"
|
|
|
+ @click="onAddComment"
|
|
|
/>
|
|
|
- </template>
|
|
|
- <div v-else class="flex flex-center full-height text-grey-5 text-body2">
|
|
|
- Nenhuma mídia anexada.
|
|
|
+ </div>
|
|
|
+ <div style="display: flex; flex-direction: column; gap: 8px">
|
|
|
+ <template v-if="replies.length">
|
|
|
+ <KanbanCommentCard
|
|
|
+ v-for="reply in replies"
|
|
|
+ :key="reply.id"
|
|
|
+ :reply="reply.reply"
|
|
|
+ :created-at="reply.created_at"
|
|
|
+ :user-name="reply.user_name"
|
|
|
+ :can-edit="canEdit"
|
|
|
+ :can-delete="canDelete"
|
|
|
+ @edit="onEditComment(reply)"
|
|
|
+ @delete="onDeleteComment(reply)"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <div
|
|
|
+ v-else
|
|
|
+ class="flex flex-center full-height text-grey-5 text-body2"
|
|
|
+ >
|
|
|
+ Nenhum comentário registrado.
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </q-card-section>
|
|
|
|
|
|
- <q-card-actions align="right" class="q-px-md q-pb-md" style="flex-shrink: 0">
|
|
|
- <q-btn outline color="primary" label="Cancelar" @click="onDialogCancel" />
|
|
|
- <q-btn v-if="card?.id ? canEdit : canAdd" color="primary" label="Salvar" type="submit" :loading="loading" />
|
|
|
+ <!-- Tab: Mídias -->
|
|
|
+ <div v-show="currentTab === 'midias'">
|
|
|
+ <div class="flex justify-end q-mb-sm">
|
|
|
+ <q-btn
|
|
|
+ v-if="canAdd"
|
|
|
+ color="primary"
|
|
|
+ icon="mdi-upload-outline"
|
|
|
+ unelevated
|
|
|
+ style="width: 40px; height: 40px"
|
|
|
+ :loading="uploadingMedia"
|
|
|
+ @click="triggerFileInput"
|
|
|
+ />
|
|
|
+ <input
|
|
|
+ ref="fileInputRef"
|
|
|
+ type="file"
|
|
|
+ style="display: none"
|
|
|
+ @change="onFileSelected"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div style="display: flex; flex-direction: column; gap: 8px">
|
|
|
+ <template v-if="medias.length">
|
|
|
+ <KanbanMediaCard
|
|
|
+ v-for="media in medias"
|
|
|
+ :key="media.id"
|
|
|
+ :file-name="media.file_name"
|
|
|
+ :file-url="media.file_url"
|
|
|
+ :mime-type="media.mime_type"
|
|
|
+ :created-at="media.created_at"
|
|
|
+ :user-name="media.user_name"
|
|
|
+ :can-delete="canDelete"
|
|
|
+ @delete="onDeleteMedia(media)"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <div
|
|
|
+ v-else
|
|
|
+ class="flex flex-center full-height text-grey-5 text-body2"
|
|
|
+ >
|
|
|
+ Nenhuma mídia anexada.
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </q-card-section>
|
|
|
+ </q-scroll-area>
|
|
|
+
|
|
|
+ <q-card-actions
|
|
|
+ align="right"
|
|
|
+ class="q-px-md q-pb-md"
|
|
|
+ style="flex-shrink: 0"
|
|
|
+ >
|
|
|
+ <q-btn
|
|
|
+ outline
|
|
|
+ color="primary"
|
|
|
+ label="Cancelar"
|
|
|
+ @click="onDialogCancel"
|
|
|
+ />
|
|
|
+ <q-btn
|
|
|
+ v-if="card?.id ? canEdit : canAdd"
|
|
|
+ color="primary"
|
|
|
+ label="Salvar"
|
|
|
+ type="submit"
|
|
|
+ :loading="loading"
|
|
|
+ />
|
|
|
</q-card-actions>
|
|
|
</DefaultForm>
|
|
|
</q-card>
|
|
|
@@ -225,7 +242,7 @@ import { permissionStore } from "src/stores/permission";
|
|
|
defineEmits([...useDialogPluginComponent.emits]);
|
|
|
|
|
|
const { card, initialPhase } = defineProps({
|
|
|
- card: { type: Object, default: null },
|
|
|
+ card: { type: Object, default: null },
|
|
|
initialPhase: { type: String, default: "a_fazer" },
|
|
|
});
|
|
|
|
|
|
@@ -238,38 +255,40 @@ const canAdd = permissions.getAccess("franchisor_activities", "add");
|
|
|
const canEdit = permissions.getAccess("franchisor_activities", "edit");
|
|
|
const canDelete = permissions.getAccess("franchisor_activities", "delete");
|
|
|
|
|
|
-const formRef = ref(null);
|
|
|
-const loading = ref(false);
|
|
|
-const currentTab = ref("atividade");
|
|
|
-const replies = ref([]);
|
|
|
-const medias = ref([]);
|
|
|
+const formRef = ref(null);
|
|
|
+const loading = ref(false);
|
|
|
+const currentTab = ref("atividade");
|
|
|
+const replies = ref([]);
|
|
|
+const medias = ref([]);
|
|
|
const uploadingMedia = ref(false);
|
|
|
const fileInputRef = ref(null);
|
|
|
-const selectedUnit = ref(card?.target_unit_id ? { value: card.target_unit_id } : null);
|
|
|
+const selectedUnit = ref(
|
|
|
+ card?.target_unit_id ? { value: card.target_unit_id } : null,
|
|
|
+);
|
|
|
|
|
|
const tabs = computed(() => [
|
|
|
- { name: "atividade", label: "Atividade" },
|
|
|
+ { name: "atividade", label: "Atividade" },
|
|
|
{ name: "comentarios", label: "Comentários" },
|
|
|
- { name: "midias", label: "Mídias" },
|
|
|
+ { name: "midias", label: "Mídias" },
|
|
|
]);
|
|
|
|
|
|
const priorityOptions = [
|
|
|
- { label: "Alta", value: "alta" },
|
|
|
+ { label: "Alta", value: "alta" },
|
|
|
{ label: "Normal", value: "normal" },
|
|
|
- { label: "Baixa", value: "baixa" },
|
|
|
+ { label: "Baixa", value: "baixa" },
|
|
|
];
|
|
|
|
|
|
const phaseOptions = [
|
|
|
- { label: "A Fazer", value: "a_fazer" },
|
|
|
- { label: "Em Progresso", value: "em_progresso" },
|
|
|
- { label: "Em Revisão", value: "em_revisao" },
|
|
|
- { label: "Concluído", value: "concluido" },
|
|
|
+ { label: "A Fazer", value: "a_fazer" },
|
|
|
+ { label: "Em Progresso", value: "em_progresso" },
|
|
|
+ { label: "Em Revisão", value: "em_revisao" },
|
|
|
+ { label: "Concluído", value: "concluido" },
|
|
|
{ label: "Demandas Especiais", value: "demandas_especiais" },
|
|
|
];
|
|
|
|
|
|
const scopeOptions = [
|
|
|
- { label: "Interno", value: "internal" },
|
|
|
- { label: "Todas as Unidades", value: "all" },
|
|
|
+ { label: "Interno", value: "internal" },
|
|
|
+ { label: "Todas as Unidades", value: "all" },
|
|
|
{ label: "Unidade Específica", value: "specific" },
|
|
|
];
|
|
|
|
|
|
@@ -280,28 +299,29 @@ const formatDisplayDate = (rawDate) => {
|
|
|
};
|
|
|
|
|
|
const form = ref({
|
|
|
- title: card?.title ?? null,
|
|
|
- priority: card?.priority ?? "normal",
|
|
|
- phase: card?.phase ?? initialPhase,
|
|
|
- scope: card?.scope ?? "internal",
|
|
|
- sector: card?.sector ?? null,
|
|
|
- description: card?.description ?? null,
|
|
|
- due_date: card?.due_date ?? null,
|
|
|
+ title: card?.title ?? null,
|
|
|
+ priority: card?.priority ?? "normal",
|
|
|
+ phase: card?.phase ?? initialPhase,
|
|
|
+ scope: card?.scope ?? "internal",
|
|
|
+ sector: card?.sector ?? null,
|
|
|
+ description: card?.description ?? null,
|
|
|
+ due_date: card?.due_date ?? null,
|
|
|
due_date_display: formatDisplayDate(card?.due_date),
|
|
|
});
|
|
|
|
|
|
const buildPayload = () => ({
|
|
|
- title: form.value.title,
|
|
|
- priority: form.value.priority,
|
|
|
- phase: form.value.phase,
|
|
|
- scope: form.value.scope,
|
|
|
- sector: form.value.sector || null,
|
|
|
- description: form.value.description || null,
|
|
|
- due_date: form.value.due_date || null,
|
|
|
- responsible_user_id: null, // Matriz never assigns a responsible
|
|
|
- target_unit_id: form.value.scope === "specific"
|
|
|
- ? (selectedUnit.value?.value ?? null)
|
|
|
- : null,
|
|
|
+ title: form.value.title,
|
|
|
+ priority: form.value.priority,
|
|
|
+ phase: form.value.phase,
|
|
|
+ scope: form.value.scope,
|
|
|
+ sector: form.value.sector || null,
|
|
|
+ description: form.value.description || null,
|
|
|
+ due_date: form.value.due_date || null,
|
|
|
+ responsible_user_id: null, // Matriz never assigns a responsible
|
|
|
+ target_unit_id:
|
|
|
+ form.value.scope === "specific"
|
|
|
+ ? (selectedUnit.value?.value ?? null)
|
|
|
+ : null,
|
|
|
});
|
|
|
|
|
|
const loadReplies = async () => {
|
|
|
@@ -333,10 +353,10 @@ const onFileSelected = async (event) => {
|
|
|
|
|
|
const onDeleteMedia = (media) => {
|
|
|
$q.dialog({
|
|
|
- title: "Excluir Mídia",
|
|
|
+ title: "Excluir Mídia",
|
|
|
message: `Deseja excluir "${media.file_name}"?`,
|
|
|
- cancel: { outline: true, color: "primary", label: "Cancelar" },
|
|
|
- ok: { color: "negative", label: "Excluir" },
|
|
|
+ cancel: { outline: true, color: "primary", label: "Cancelar" },
|
|
|
+ ok: { color: "negative", label: "Excluir" },
|
|
|
}).onOk(async () => {
|
|
|
await deleteKanbanMedia(card.id, media.id);
|
|
|
loadMedias();
|
|
|
@@ -347,7 +367,7 @@ const onAddComment = () => {
|
|
|
$q.dialog({
|
|
|
title: "Adicionar Comentário",
|
|
|
prompt: { model: "", type: "textarea", label: "Comentário" },
|
|
|
- ok: { label: "Salvar", color: "primary" },
|
|
|
+ ok: { label: "Salvar", color: "primary" },
|
|
|
cancel: { label: "Cancelar", color: "primary", outline: true },
|
|
|
}).onOk(async (text) => {
|
|
|
if (!text?.trim()) return;
|
|
|
@@ -360,7 +380,7 @@ const onEditComment = (reply) => {
|
|
|
$q.dialog({
|
|
|
title: "Editar Comentário",
|
|
|
prompt: { model: reply.reply, type: "textarea", label: "Comentário" },
|
|
|
- ok: { label: "Salvar", color: "primary" },
|
|
|
+ ok: { label: "Salvar", color: "primary" },
|
|
|
cancel: { label: "Cancelar", color: "primary", outline: true },
|
|
|
}).onOk(async (text) => {
|
|
|
if (!text?.trim()) return;
|
|
|
@@ -371,10 +391,10 @@ const onEditComment = (reply) => {
|
|
|
|
|
|
const onDeleteComment = (reply) => {
|
|
|
$q.dialog({
|
|
|
- title: "Excluir Comentário",
|
|
|
+ title: "Excluir Comentário",
|
|
|
message: "Tem certeza que deseja excluir este comentário?",
|
|
|
- cancel: { outline: true, color: "primary", label: "Cancelar" },
|
|
|
- ok: { color: "negative", label: "Excluir" },
|
|
|
+ cancel: { outline: true, color: "primary", label: "Cancelar" },
|
|
|
+ ok: { color: "negative", label: "Excluir" },
|
|
|
}).onOk(async () => {
|
|
|
await deleteKanbanReply(card.id, reply.id);
|
|
|
loadReplies();
|