|
|
@@ -23,117 +23,141 @@
|
|
|
overflow: hidden;
|
|
|
"
|
|
|
@submit="onOKClick"
|
|
|
+ @validation-error="handleValidationError"
|
|
|
>
|
|
|
- <q-scroll-area class="form-dialog-scroll-area">
|
|
|
- <q-card-section class="q-pt-sm">
|
|
|
+ <q-scroll-area ref="scrollAreaRef" class="form-dialog-scroll-area">
|
|
|
+ <q-card-section class="row q-col-gutter-md q-pt-none">
|
|
|
<CustomTabComponent
|
|
|
v-if="card?.id"
|
|
|
v-model:active-tab="currentTab"
|
|
|
+ class="col-12 q-mb-md"
|
|
|
:tabs="tabs"
|
|
|
- class="q-mb-md"
|
|
|
/>
|
|
|
|
|
|
- <!-- Tab: Atividade -->
|
|
|
- <div v-show="currentTab === 'atividade'">
|
|
|
- <div class="row q-col-gutter-sm">
|
|
|
- <!-- 1. Título -->
|
|
|
+ <div
|
|
|
+ v-show="currentTab === 'atividade'"
|
|
|
+ class="col-12"
|
|
|
+ >
|
|
|
+ <div class="row q-col-gutter-md">
|
|
|
<DefaultInput
|
|
|
v-model="form.title"
|
|
|
- label="Título da Tarefa"
|
|
|
+ v-model:error="validationErrors.title"
|
|
|
class="col-12"
|
|
|
- :rules="[(val) => !!val || 'Campo obrigatório']"
|
|
|
+ label="Título da Tarefa"
|
|
|
+ :error-message="validationErrors.title"
|
|
|
+ :rules="[inputRules.required, maxLengthRule(255)]"
|
|
|
/>
|
|
|
|
|
|
- <!-- 2. Destino (primeiro campo de configuração) -->
|
|
|
<DefaultSelect
|
|
|
v-model="form.scope"
|
|
|
- label="Destino"
|
|
|
- :options="scopeOptions"
|
|
|
+ v-model:error="validationErrors.scope"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
emit-value
|
|
|
+ label="Destino"
|
|
|
map-options
|
|
|
- class="col-6"
|
|
|
- :rules="[(val) => !!val || 'Campo obrigatório']"
|
|
|
+ :error-message="validationErrors.scope"
|
|
|
+ :options="scopeOptions"
|
|
|
+ :rules="[inputRules.required]"
|
|
|
/>
|
|
|
|
|
|
- <!-- Prazo ao lado do Destino -->
|
|
|
<DefaultInputDatePicker
|
|
|
v-model="form.due_date_display"
|
|
|
v-model:untreated-date="form.due_date"
|
|
|
+ v-model:error="validationErrors.due_date"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
label="Prazo de Entrega"
|
|
|
- class="col-6"
|
|
|
+ :error-message="validationErrors.due_date"
|
|
|
+ :rules="[dateRule]"
|
|
|
/>
|
|
|
|
|
|
- <!-- Unidade específica — só aparece quando scope = specific -->
|
|
|
<UnitSelect
|
|
|
v-if="form.scope === 'specific'"
|
|
|
v-model="selectedUnit"
|
|
|
- class="col-6"
|
|
|
+ v-model:error="validationErrors.target_unit_id"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
+ :error-message="validationErrors.target_unit_id"
|
|
|
/>
|
|
|
|
|
|
- <!-- 3. Prioridade + Fase -->
|
|
|
<DefaultSelect
|
|
|
v-model="form.priority"
|
|
|
- label="Prioridade"
|
|
|
- :options="priorityOptions"
|
|
|
+ v-model:error="validationErrors.priority"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
emit-value
|
|
|
+ label="Prioridade"
|
|
|
map-options
|
|
|
- class="col-6"
|
|
|
- :rules="[(val) => !!val || 'Campo obrigatório']"
|
|
|
+ :error-message="validationErrors.priority"
|
|
|
+ :options="priorityOptions"
|
|
|
+ :rules="[inputRules.required]"
|
|
|
/>
|
|
|
|
|
|
<DefaultSelect
|
|
|
v-model="form.phase"
|
|
|
- label="Fase"
|
|
|
- :options="phaseOptions"
|
|
|
+ v-model:error="validationErrors.phase"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
emit-value
|
|
|
+ label="Fase"
|
|
|
map-options
|
|
|
- class="col-6"
|
|
|
- :rules="[(val) => !!val || 'Campo obrigatório']"
|
|
|
+ :error-message="validationErrors.phase"
|
|
|
+ :options="phaseOptions"
|
|
|
+ :rules="[inputRules.required]"
|
|
|
/>
|
|
|
|
|
|
- <!-- 4. Setor -->
|
|
|
<DefaultInput
|
|
|
v-model="form.sector"
|
|
|
+ v-model:error="validationErrors.sector"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
label="Setor"
|
|
|
- class="col-6"
|
|
|
+ :error-message="validationErrors.sector"
|
|
|
+ :rules="[maxLengthRule(255)]"
|
|
|
/>
|
|
|
|
|
|
- <!-- 5. Descrição -->
|
|
|
<DefaultInput
|
|
|
v-model="form.description"
|
|
|
+ v-model:error="validationErrors.description"
|
|
|
+ class="col-12"
|
|
|
label="Descrição"
|
|
|
type="textarea"
|
|
|
- class="col-12"
|
|
|
+ :error-message="validationErrors.description"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Tab: Comentários -->
|
|
|
- <div v-show="currentTab === 'comentarios'">
|
|
|
+ <div
|
|
|
+ v-show="currentTab === 'comentarios'"
|
|
|
+ class="col-12"
|
|
|
+ >
|
|
|
<div class="flex justify-end q-mb-sm">
|
|
|
<q-btn
|
|
|
v-if="canAdd"
|
|
|
color="primary"
|
|
|
icon="mdi-plus"
|
|
|
- unelevated
|
|
|
style="width: 40px; height: 40px"
|
|
|
+ unelevated
|
|
|
@click="onAddComment"
|
|
|
/>
|
|
|
</div>
|
|
|
- <div style="display: flex; flex-direction: column; gap: 8px">
|
|
|
+
|
|
|
+ <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"
|
|
|
+ :can-delete="canDelete"
|
|
|
+ :can-edit="canEdit"
|
|
|
:created-at="reply.created_at"
|
|
|
+ :reply="reply.reply"
|
|
|
:user-name="reply.user_name"
|
|
|
- :can-edit="canEdit"
|
|
|
- :can-delete="canDelete"
|
|
|
- @edit="onEditComment(reply)"
|
|
|
@delete="onDeleteComment(reply)"
|
|
|
+ @edit="onEditComment(reply)"
|
|
|
/>
|
|
|
</template>
|
|
|
+
|
|
|
<div
|
|
|
v-else
|
|
|
class="flex flex-center full-height text-grey-5 text-body2"
|
|
|
@@ -143,39 +167,50 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Tab: Mídias -->
|
|
|
- <div v-show="currentTab === 'midias'">
|
|
|
+ <div
|
|
|
+ v-show="currentTab === 'midias'"
|
|
|
+ class="col-12"
|
|
|
+ >
|
|
|
<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"
|
|
|
+ unelevated
|
|
|
:loading="uploadingMedia"
|
|
|
@click="triggerFileInput"
|
|
|
/>
|
|
|
+
|
|
|
<input
|
|
|
ref="fileInputRef"
|
|
|
- type="file"
|
|
|
style="display: none"
|
|
|
+ type="file"
|
|
|
@change="onFileSelected"
|
|
|
/>
|
|
|
</div>
|
|
|
- <div style="display: flex; flex-direction: column; gap: 8px">
|
|
|
+
|
|
|
+ <div
|
|
|
+ style="
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ "
|
|
|
+ >
|
|
|
<template v-if="medias.length">
|
|
|
<KanbanMediaCard
|
|
|
v-for="media in medias"
|
|
|
:key="media.id"
|
|
|
+ :can-delete="canDelete"
|
|
|
+ :created-at="media.created_at"
|
|
|
: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"
|
|
|
@@ -187,22 +222,28 @@
|
|
|
</q-card-section>
|
|
|
</q-scroll-area>
|
|
|
|
|
|
+ <q-separator />
|
|
|
+
|
|
|
<q-card-actions
|
|
|
align="right"
|
|
|
- class="q-px-md q-pb-md"
|
|
|
+ class="q-pa-md q-gutter-sm"
|
|
|
style="flex-shrink: 0"
|
|
|
>
|
|
|
<q-btn
|
|
|
- outline
|
|
|
- color="primary"
|
|
|
+ color="negative"
|
|
|
label="Cancelar"
|
|
|
+ no-caps
|
|
|
+ outline
|
|
|
@click="onDialogCancel"
|
|
|
/>
|
|
|
+
|
|
|
<q-btn
|
|
|
v-if="card?.id ? canEdit : canAdd"
|
|
|
color="primary"
|
|
|
label="Salvar"
|
|
|
+ no-caps
|
|
|
type="submit"
|
|
|
+ unelevated
|
|
|
:loading="loading"
|
|
|
/>
|
|
|
</q-card-actions>
|
|
|
@@ -213,70 +254,153 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, computed, onMounted } from "vue";
|
|
|
-import { useDialogPluginComponent, useQuasar } from "quasar";
|
|
|
-
|
|
|
-import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
|
|
|
-import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
|
|
|
-import DefaultInput from "src/components/defaults/DefaultInput.vue";
|
|
|
-import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
|
|
|
-import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
|
|
|
-import UnitSelect from "src/components/selects/UnitSelect.vue";
|
|
|
-import KanbanCommentCard from "./KanbanCommentCard.vue";
|
|
|
-import KanbanMediaCard from "./KanbanMediaCard.vue";
|
|
|
+import {
|
|
|
+ createKanban,
|
|
|
+ updateKanban,
|
|
|
+} from "src/api/kanban";
|
|
|
|
|
|
-import { createKanban, updateKanban } from "src/api/kanban";
|
|
|
import {
|
|
|
- getKanbanReplies,
|
|
|
createKanbanReply,
|
|
|
- updateKanbanReply,
|
|
|
deleteKanbanReply,
|
|
|
+ getKanbanReplies,
|
|
|
+ updateKanbanReply,
|
|
|
} from "src/api/kanban_reply";
|
|
|
+
|
|
|
import {
|
|
|
+ deleteKanbanMedia,
|
|
|
getKanbanMedias,
|
|
|
uploadKanbanMedia,
|
|
|
- deleteKanbanMedia,
|
|
|
} from "src/api/kanban_media";
|
|
|
+
|
|
|
+import { computed, nextTick, onMounted, ref, watch } from "vue";
|
|
|
import { permissionStore } from "src/stores/permission";
|
|
|
|
|
|
+import {
|
|
|
+ useDialogPluginComponent,
|
|
|
+ useQuasar,
|
|
|
+} from "quasar";
|
|
|
+
|
|
|
+import { useForm } from "src/composables/useForm";
|
|
|
+import { useInputRules } from "src/composables/useInputRules";
|
|
|
+import { useScroll } from "src/composables/useScroll";
|
|
|
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
|
|
|
+
|
|
|
+import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
|
|
|
+
|
|
|
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
|
|
|
+import DefaultForm from "src/components/defaults/DefaultForm.vue";
|
|
|
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
|
|
|
+import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
|
|
|
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
|
|
|
+
|
|
|
+import KanbanCommentCard from "./KanbanCommentCard.vue";
|
|
|
+import KanbanMediaCard from "./KanbanMediaCard.vue";
|
|
|
+import UnitSelect from "src/components/selects/UnitSelect.vue";
|
|
|
+
|
|
|
defineEmits([...useDialogPluginComponent.emits]);
|
|
|
|
|
|
const { card, initialPhase } = defineProps({
|
|
|
- card: { type: Object, default: null },
|
|
|
- initialPhase: { type: String, default: "a_fazer" },
|
|
|
+ card: {
|
|
|
+ default: null,
|
|
|
+ type: Object,
|
|
|
+ },
|
|
|
+ initialPhase: {
|
|
|
+ default: "a_fazer",
|
|
|
+ type: String,
|
|
|
+ },
|
|
|
});
|
|
|
|
|
|
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
|
|
|
+const { dialogRef, onDialogCancel, onDialogHide, onDialogOK } =
|
|
|
useDialogPluginComponent();
|
|
|
|
|
|
const $q = useQuasar();
|
|
|
+const { inputRules } = useInputRules();
|
|
|
+const { scrollToComponent } = useScroll();
|
|
|
+
|
|
|
const permissions = permissionStore();
|
|
|
-const canAdd = permissions.getAccess("franchisor_activities", "add");
|
|
|
-const canEdit = permissions.getAccess("franchisor_activities", "edit");
|
|
|
-const canDelete = permissions.getAccess("franchisor_activities", "delete");
|
|
|
|
|
|
+const fileInputRef = ref(null);
|
|
|
const formRef = ref(null);
|
|
|
-const loading = ref(false);
|
|
|
+const scrollAreaRef = ref(null);
|
|
|
+
|
|
|
+const formatDisplayDate = (rawDate) => {
|
|
|
+ if (!rawDate) return null;
|
|
|
+
|
|
|
+ const [year, month, day] = rawDate.split("-");
|
|
|
+
|
|
|
+ return `${day}/${month}/${year}`;
|
|
|
+};
|
|
|
+
|
|
|
+const { form, getUpdatedFields } = useForm(
|
|
|
+ {
|
|
|
+ description: card?.description ?? null,
|
|
|
+ due_date: card?.due_date ?? null,
|
|
|
+ due_date_display: formatDisplayDate(card?.due_date),
|
|
|
+ phase: card?.phase ?? initialPhase,
|
|
|
+ priority: card?.priority ?? "normal",
|
|
|
+ responsible_user_id: card?.responsible_user_id ?? null,
|
|
|
+ scope: card?.scope ?? "internal",
|
|
|
+ sector: card?.sector ?? null,
|
|
|
+ target_unit_id: card?.target_unit_id ?? null,
|
|
|
+ title: card?.title ?? null,
|
|
|
+ },
|
|
|
+ { containerRef: scrollAreaRef },
|
|
|
+);
|
|
|
+
|
|
|
+const scrollToActivityComponent = async (
|
|
|
+ component,
|
|
|
+ containerRef,
|
|
|
+ options,
|
|
|
+) => {
|
|
|
+ currentTab.value = "atividade";
|
|
|
+
|
|
|
+ await nextTick();
|
|
|
+
|
|
|
+ return scrollToComponent(component, containerRef, options);
|
|
|
+};
|
|
|
+
|
|
|
+const {
|
|
|
+ loading,
|
|
|
+ validationErrors,
|
|
|
+ execute,
|
|
|
+} = useSubmitHandler({
|
|
|
+ containerRef: scrollAreaRef,
|
|
|
+ formRef,
|
|
|
+ onSuccess: () => onDialogOK(true),
|
|
|
+ scrollFn: scrollToActivityComponent,
|
|
|
+});
|
|
|
+
|
|
|
+const selectedUnit = ref(
|
|
|
+ card?.target_unit_id
|
|
|
+ ? {
|
|
|
+ value: card.target_unit_id,
|
|
|
+ }
|
|
|
+ : null,
|
|
|
+);
|
|
|
+
|
|
|
const currentTab = ref("atividade");
|
|
|
-const replies = ref([]);
|
|
|
const medias = ref([]);
|
|
|
+const replies = ref([]);
|
|
|
const uploadingMedia = ref(false);
|
|
|
-const fileInputRef = ref(null);
|
|
|
-const selectedUnit = ref(
|
|
|
- card?.target_unit_id ? { value: card.target_unit_id } : null,
|
|
|
-);
|
|
|
|
|
|
const tabs = computed(() => [
|
|
|
- { name: "atividade", label: "Atividade" },
|
|
|
- { name: "comentarios", label: "Comentários" },
|
|
|
- { name: "midias", label: "Mídias" },
|
|
|
+ {
|
|
|
+ label: "Atividade",
|
|
|
+ name: "atividade",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "Comentários",
|
|
|
+ name: "comentarios",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "Mídias",
|
|
|
+ name: "midias",
|
|
|
+ },
|
|
|
]);
|
|
|
|
|
|
-const priorityOptions = [
|
|
|
- { label: "Alta", value: "alta" },
|
|
|
- { label: "Normal", value: "normal" },
|
|
|
- { label: "Baixa", value: "baixa" },
|
|
|
-];
|
|
|
+const canAdd = permissions.getAccess("franchisor_activities", "add");
|
|
|
+const canDelete = permissions.getAccess("franchisor_activities", "delete");
|
|
|
+const canEdit = permissions.getAccess("franchisor_activities", "edit");
|
|
|
|
|
|
const phaseOptions = [
|
|
|
{ label: "A Fazer", value: "a_fazer" },
|
|
|
@@ -286,138 +410,224 @@ const phaseOptions = [
|
|
|
{ label: "Demandas Especiais", value: "demandas_especiais" },
|
|
|
];
|
|
|
|
|
|
+const priorityOptions = [
|
|
|
+ { label: "Alta", value: "alta" },
|
|
|
+ { label: "Normal", value: "normal" },
|
|
|
+ { label: "Baixa", value: "baixa" },
|
|
|
+];
|
|
|
+
|
|
|
const scopeOptions = [
|
|
|
{ label: "Interno", value: "internal" },
|
|
|
{ label: "Todas as Unidades", value: "all" },
|
|
|
{ label: "Unidade Específica", value: "specific" },
|
|
|
];
|
|
|
|
|
|
-const formatDisplayDate = (rawDate) => {
|
|
|
- if (!rawDate) return null;
|
|
|
- const [y, m, d] = rawDate.split("-");
|
|
|
- return `${d}/${m}/${y}`;
|
|
|
+const buildPayload = () => ({
|
|
|
+ description: form.description || null,
|
|
|
+ due_date: form.due_date || null,
|
|
|
+ phase: form.phase,
|
|
|
+ priority: form.priority,
|
|
|
+ responsible_user_id: form.responsible_user_id,
|
|
|
+ scope: form.scope,
|
|
|
+ sector: form.sector || null,
|
|
|
+ target_unit_id: form.scope === "specific" ? form.target_unit_id : null,
|
|
|
+ title: form.title,
|
|
|
+});
|
|
|
+
|
|
|
+const dateRule = (value) => {
|
|
|
+ if (!value) return true;
|
|
|
+
|
|
|
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
|
+
|
|
|
+ if (!match) return "Informe uma data válida";
|
|
|
+
|
|
|
+ const [, year, month, day] = match.map(Number);
|
|
|
+ const date = new Date(year, month - 1, day);
|
|
|
+
|
|
|
+ return (
|
|
|
+ (date.getFullYear() === year &&
|
|
|
+ date.getMonth() === month - 1 &&
|
|
|
+ date.getDate() === day) ||
|
|
|
+ "Informe uma data válida"
|
|
|
+ );
|
|
|
};
|
|
|
|
|
|
-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,
|
|
|
- due_date_display: formatDisplayDate(card?.due_date),
|
|
|
-});
|
|
|
+const handleValidationError = async (component) => {
|
|
|
+ currentTab.value = "atividade";
|
|
|
|
|
|
-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,
|
|
|
-});
|
|
|
+ await nextTick();
|
|
|
|
|
|
-const loadReplies = async () => {
|
|
|
- if (!card?.id) return;
|
|
|
- replies.value = await getKanbanReplies(card.id);
|
|
|
+ if (component) {
|
|
|
+ component.focus?.();
|
|
|
+ scrollToComponent(component, scrollAreaRef);
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
const loadMedias = async () => {
|
|
|
if (!card?.id) return;
|
|
|
+
|
|
|
medias.value = await getKanbanMedias(card.id);
|
|
|
};
|
|
|
|
|
|
-const triggerFileInput = () => {
|
|
|
- fileInputRef.value?.click();
|
|
|
-};
|
|
|
+const loadReplies = async () => {
|
|
|
+ if (!card?.id) return;
|
|
|
|
|
|
-const onFileSelected = async (event) => {
|
|
|
- const file = event.target.files?.[0];
|
|
|
- if (!file) return;
|
|
|
- uploadingMedia.value = true;
|
|
|
- try {
|
|
|
- await uploadKanbanMedia(card.id, file);
|
|
|
- await loadMedias();
|
|
|
- } finally {
|
|
|
- uploadingMedia.value = false;
|
|
|
- event.target.value = "";
|
|
|
- }
|
|
|
+ replies.value = await getKanbanReplies(card.id);
|
|
|
};
|
|
|
|
|
|
-const onDeleteMedia = (media) => {
|
|
|
- $q.dialog({
|
|
|
- title: "Excluir Mídia",
|
|
|
- message: `Deseja excluir "${media.file_name}"?`,
|
|
|
- cancel: { outline: true, color: "primary", label: "Cancelar" },
|
|
|
- ok: { color: "negative", label: "Excluir" },
|
|
|
- }).onOk(async () => {
|
|
|
- await deleteKanbanMedia(card.id, media.id);
|
|
|
- loadMedias();
|
|
|
- });
|
|
|
-};
|
|
|
+const maxLengthRule = (maximum) => (value) =>
|
|
|
+ !value ||
|
|
|
+ String(value).length <= maximum ||
|
|
|
+ `O campo deve ter no máximo ${maximum} caracteres`;
|
|
|
|
|
|
const onAddComment = () => {
|
|
|
$q.dialog({
|
|
|
+ cancel: {
|
|
|
+ color: "primary",
|
|
|
+ label: "Cancelar",
|
|
|
+ outline: true,
|
|
|
+ },
|
|
|
+ ok: {
|
|
|
+ color: "primary",
|
|
|
+ label: "Salvar",
|
|
|
+ },
|
|
|
+ prompt: {
|
|
|
+ label: "Comentário *",
|
|
|
+ model: "",
|
|
|
+ type: "textarea",
|
|
|
+ },
|
|
|
title: "Adicionar Comentário",
|
|
|
- prompt: { model: "", type: "textarea", label: "Comentário" },
|
|
|
- ok: { label: "Salvar", color: "primary" },
|
|
|
- cancel: { label: "Cancelar", color: "primary", outline: true },
|
|
|
}).onOk(async (text) => {
|
|
|
if (!text?.trim()) return;
|
|
|
- await createKanbanReply(card.id, { reply: text.trim() });
|
|
|
+
|
|
|
+ await createKanbanReply(card.id, {
|
|
|
+ reply: text.trim(),
|
|
|
+ });
|
|
|
+
|
|
|
loadReplies();
|
|
|
});
|
|
|
};
|
|
|
|
|
|
-const onEditComment = (reply) => {
|
|
|
+const onDeleteComment = (reply) => {
|
|
|
$q.dialog({
|
|
|
- title: "Editar Comentário",
|
|
|
- prompt: { model: reply.reply, type: "textarea", label: "Comentário" },
|
|
|
- ok: { label: "Salvar", color: "primary" },
|
|
|
- cancel: { label: "Cancelar", color: "primary", outline: true },
|
|
|
- }).onOk(async (text) => {
|
|
|
- if (!text?.trim()) return;
|
|
|
- await updateKanbanReply(card.id, reply.id, { reply: text.trim() });
|
|
|
+ cancel: {
|
|
|
+ color: "primary",
|
|
|
+ label: "Cancelar",
|
|
|
+ outline: true,
|
|
|
+ },
|
|
|
+ message: "Tem certeza que deseja excluir este comentário?",
|
|
|
+ ok: {
|
|
|
+ color: "negative",
|
|
|
+ label: "Excluir",
|
|
|
+ },
|
|
|
+ title: "Excluir Comentário",
|
|
|
+ }).onOk(async () => {
|
|
|
+ await deleteKanbanReply(card.id, reply.id);
|
|
|
+
|
|
|
loadReplies();
|
|
|
});
|
|
|
};
|
|
|
|
|
|
-const onDeleteComment = (reply) => {
|
|
|
+const onDeleteMedia = (media) => {
|
|
|
$q.dialog({
|
|
|
- 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: {
|
|
|
+ color: "primary",
|
|
|
+ label: "Cancelar",
|
|
|
+ outline: true,
|
|
|
+ },
|
|
|
+ message: `Deseja excluir "${media.file_name}"?`,
|
|
|
+ ok: {
|
|
|
+ color: "negative",
|
|
|
+ label: "Excluir",
|
|
|
+ },
|
|
|
+ title: "Excluir Mídia",
|
|
|
}).onOk(async () => {
|
|
|
- await deleteKanbanReply(card.id, reply.id);
|
|
|
+ await deleteKanbanMedia(card.id, media.id);
|
|
|
+
|
|
|
+ loadMedias();
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const onEditComment = (reply) => {
|
|
|
+ $q.dialog({
|
|
|
+ cancel: {
|
|
|
+ color: "primary",
|
|
|
+ label: "Cancelar",
|
|
|
+ outline: true,
|
|
|
+ },
|
|
|
+ ok: {
|
|
|
+ color: "primary",
|
|
|
+ label: "Salvar",
|
|
|
+ },
|
|
|
+ prompt: {
|
|
|
+ label: "Comentário",
|
|
|
+ model: reply.reply,
|
|
|
+ type: "textarea",
|
|
|
+ },
|
|
|
+ title: "Editar Comentário",
|
|
|
+ }).onOk(async (text) => {
|
|
|
+ if (!text?.trim()) return;
|
|
|
+
|
|
|
+ await updateKanbanReply(card.id, reply.id, {
|
|
|
+ reply: text.trim(),
|
|
|
+ });
|
|
|
+
|
|
|
loadReplies();
|
|
|
});
|
|
|
};
|
|
|
|
|
|
-const onOKClick = async () => {
|
|
|
- loading.value = true;
|
|
|
+const onFileSelected = async (event) => {
|
|
|
+ const file = event.target.files?.[0];
|
|
|
+
|
|
|
+ if (!file) return;
|
|
|
+
|
|
|
+ uploadingMedia.value = true;
|
|
|
+
|
|
|
try {
|
|
|
- const payload = buildPayload();
|
|
|
- if (card?.id) {
|
|
|
- await updateKanban(card.id, payload);
|
|
|
- } else {
|
|
|
- await createKanban(payload);
|
|
|
- }
|
|
|
- onDialogOK(true);
|
|
|
+ await uploadKanbanMedia(card.id, file);
|
|
|
+ await loadMedias();
|
|
|
} finally {
|
|
|
- loading.value = false;
|
|
|
+ event.target.value = "";
|
|
|
+ uploadingMedia.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+const triggerFileInput = () => {
|
|
|
+ fileInputRef.value?.click();
|
|
|
+};
|
|
|
+
|
|
|
+const onOKClick = async () => {
|
|
|
+ await execute(() => {
|
|
|
+ if (card?.id) {
|
|
|
+ const payload = {
|
|
|
+ ...getUpdatedFields.value,
|
|
|
+ };
|
|
|
+
|
|
|
+ delete payload.due_date_display;
|
|
|
+
|
|
|
+ return updateKanban(card.id, payload);
|
|
|
+ }
|
|
|
+
|
|
|
+ return createKanban(buildPayload());
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+watch(selectedUnit, (unit) => {
|
|
|
+ form.target_unit_id = unit?.value ?? null;
|
|
|
+});
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => form.scope,
|
|
|
+ (scope) => {
|
|
|
+ if (scope !== "specific") {
|
|
|
+ selectedUnit.value = null;
|
|
|
+ form.target_unit_id = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+);
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
- loadReplies();
|
|
|
loadMedias();
|
|
|
+ loadReplies();
|
|
|
});
|
|
|
-</script>
|
|
|
+</script>
|