| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633 |
- <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;
- "
- >
- <DefaultDialogHeader
- :title="card ? 'Editar Atividade' : 'Adicionar Atividade'"
- @close="onDialogCancel"
- />
- <DefaultForm
- ref="formRef"
- style="
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- "
- @submit="onOKClick"
- @validation-error="handleValidationError"
- >
- <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"
- />
- <div
- v-show="currentTab === 'atividade'"
- class="col-12"
- >
- <div class="row q-col-gutter-md">
- <DefaultInput
- v-model="form.title"
- v-model:error="validationErrors.title"
- class="col-12"
- label="Título da Tarefa"
- :error-message="validationErrors.title"
- :rules="[inputRules.required, maxLengthRule(255)]"
- />
- <DefaultSelect
- v-model="form.scope"
- v-model:error="validationErrors.scope"
- class="col-12 col-sm-6"
- emit-value
- label="Destino"
- map-options
- :error-message="validationErrors.scope"
- :options="scopeOptions"
- :rules="[inputRules.required]"
- />
- <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"
- :error-message="validationErrors.due_date"
- :rules="[dateRule]"
- />
- <UnitSelect
- v-if="form.scope === 'specific'"
- v-model="selectedUnit"
- v-model:error="validationErrors.target_unit_id"
- class="col-12 col-sm-6"
- :error-message="validationErrors.target_unit_id"
- />
- <DefaultSelect
- v-model="form.priority"
- v-model:error="validationErrors.priority"
- class="col-12 col-sm-6"
- emit-value
- label="Prioridade"
- map-options
- :error-message="validationErrors.priority"
- :options="priorityOptions"
- :rules="[inputRules.required]"
- />
- <DefaultSelect
- v-model="form.phase"
- v-model:error="validationErrors.phase"
- class="col-12 col-sm-6"
- emit-value
- label="Fase"
- map-options
- :error-message="validationErrors.phase"
- :options="phaseOptions"
- :rules="[inputRules.required]"
- />
- <DefaultInput
- v-model="form.sector"
- v-model:error="validationErrors.sector"
- class="col-12 col-sm-6"
- label="Setor"
- :error-message="validationErrors.sector"
- :rules="[maxLengthRule(255)]"
- />
- <DefaultInput
- v-model="form.description"
- v-model:error="validationErrors.description"
- class="col-12"
- label="Descrição"
- type="textarea"
- :error-message="validationErrors.description"
- />
- </div>
- </div>
- <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"
- style="width: 40px; height: 40px"
- unelevated
- @click="onAddComment"
- />
- </div>
- <div
- style="
- display: flex;
- flex-direction: column;
- gap: 8px;
- "
- >
- <template v-if="replies.length">
- <KanbanCommentCard
- v-for="reply in replies"
- :key="reply.id"
- :can-delete="canDelete"
- :can-edit="canEdit"
- :created-at="reply.created_at"
- :reply="reply.reply"
- :user-name="reply.user_name"
- @delete="onDeleteComment(reply)"
- @edit="onEditComment(reply)"
- />
- </template>
- <div
- v-else
- class="flex flex-center full-height text-grey-5 text-body2"
- >
- Nenhum comentário registrado.
- </div>
- </div>
- </div>
- <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"
- style="width: 40px; height: 40px"
- unelevated
- :loading="uploadingMedia"
- @click="triggerFileInput"
- />
- <input
- ref="fileInputRef"
- style="display: none"
- type="file"
- @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"
- :can-delete="canDelete"
- :created-at="media.created_at"
- :file-name="media.file_name"
- :file-url="media.file_url"
- :mime-type="media.mime_type"
- :user-name="media.user_name"
- @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-separator />
- <q-card-actions
- align="right"
- class="q-pa-md q-gutter-sm"
- style="flex-shrink: 0"
- >
- <q-btn
- 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>
- </DefaultForm>
- </q-card>
- </div>
- </q-dialog>
- </template>
- <script setup>
- import {
- createKanban,
- updateKanban,
- } from "src/api/kanban";
- import {
- createKanbanReply,
- deleteKanbanReply,
- getKanbanReplies,
- updateKanbanReply,
- } from "src/api/kanban_reply";
- import {
- deleteKanbanMedia,
- getKanbanMedias,
- uploadKanbanMedia,
- } 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: {
- default: null,
- type: Object,
- },
- initialPhase: {
- default: "a_fazer",
- type: String,
- },
- });
- const { dialogRef, onDialogCancel, onDialogHide, onDialogOK } =
- useDialogPluginComponent();
- const $q = useQuasar();
- const { inputRules } = useInputRules();
- const { scrollToComponent } = useScroll();
- const permissions = permissionStore();
- const fileInputRef = ref(null);
- const formRef = ref(null);
- 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 medias = ref([]);
- const replies = ref([]);
- const uploadingMedia = ref(false);
- const tabs = computed(() => [
- {
- label: "Atividade",
- name: "atividade",
- },
- {
- label: "Comentários",
- name: "comentarios",
- },
- {
- label: "Mídias",
- name: "midias",
- },
- ]);
- 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" },
- { 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 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 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 handleValidationError = async (component) => {
- currentTab.value = "atividade";
- await nextTick();
- if (component) {
- component.focus?.();
- scrollToComponent(component, scrollAreaRef);
- }
- };
- const loadMedias = async () => {
- if (!card?.id) return;
- medias.value = await getKanbanMedias(card.id);
- };
- const loadReplies = async () => {
- if (!card?.id) return;
- replies.value = await getKanbanReplies(card.id);
- };
- 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",
- }).onOk(async (text) => {
- if (!text?.trim()) return;
- await createKanbanReply(card.id, {
- reply: text.trim(),
- });
- loadReplies();
- });
- };
- const onDeleteComment = (reply) => {
- $q.dialog({
- 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 onDeleteMedia = (media) => {
- $q.dialog({
- 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 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 onFileSelected = async (event) => {
- const file = event.target.files?.[0];
- if (!file) return;
- uploadingMedia.value = true;
- try {
- await uploadKanbanMedia(card.id, file);
- await loadMedias();
- } finally {
- 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(() => {
- loadMedias();
- loadReplies();
- });
- </script>
|