Преглед изворни кода

feat: adiciona tabs financeiras, hgistorico e midia

ebagabee пре 2 недеља
родитељ
комит
60fdaaebce

+ 11 - 0
src/api/unit_financial.js

@@ -0,0 +1,11 @@
+import api from "src/api";
+
+export const getFinancialByUnit = async (unitId) => {
+  const { data } = await api.get("/unit-financial", { params: { unit_id: unitId } });
+  return data.payload;
+};
+
+export const upsertFinancial = async (payload) => {
+  const { data } = await api.post("/unit-financial", payload);
+  return data.payload;
+};

+ 21 - 0
src/api/unit_history.js

@@ -0,0 +1,21 @@
+import api from "src/api";
+
+export const getHistoriesByUnit = async (unitId) => {
+  const { data } = await api.get("/unit-history", { params: { unit_id: unitId } });
+  return data.payload;
+};
+
+export const createHistory = async (payload) => {
+  const { data } = await api.post("/unit-history", payload);
+  return data.payload;
+};
+
+export const updateHistory = async (id, payload) => {
+  const { data } = await api.put(`/unit-history/${id}`, payload);
+  return data.payload;
+};
+
+export const deleteHistory = async (id) => {
+  const { data } = await api.delete(`/unit-history/${id}`);
+  return data;
+};

+ 18 - 0
src/api/unit_media.js

@@ -0,0 +1,18 @@
+import api from "src/api";
+
+export const getMediasByUnit = async (unitId) => {
+  const { data } = await api.get("/unit-media", { params: { unit_id: unitId } });
+  return data.payload;
+};
+
+export const createMedia = async (formData) => {
+  const { data } = await api.post("/unit-media", formData, {
+    headers: { "Content-Type": "multipart/form-data" },
+  });
+  return data.payload;
+};
+
+export const deleteMedia = async (id) => {
+  const { data } = await api.delete(`/unit-media/${id}`);
+  return data;
+};

+ 24 - 23
src/pages/unit/UnitActionPage.vue

@@ -21,9 +21,9 @@
       v-model:contracts="form.contracts"
       :unit-id="unitId"
     />
-    <FinancialTab v-show="activeTab === 'financial'" />
-    <HistoryTab v-show="activeTab === 'history'" />
-    <MediasTab v-show="activeTab === 'medias'" />
+    <FinancialTab v-show="activeTab === 'financial'" :unit-id="unitId" />
+    <HistoryTab v-show="activeTab === 'history'" :unit-id="unitId" />
+    <MediasTab v-show="activeTab === 'medias'" :unit-id="unitId" />
   </div>
 </template>
 
@@ -43,26 +43,27 @@ import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
 const route = useRoute();
 const unitId = route.params.id ? Number(route.params.id) : null;
 
-const { form, getFormAsFormData, setUpdateFormAsOriginal } = useFormUpdateTracker({
-  fantasy_name: null,
-  social_reason: null,
-  cnpj: null,
-  state_registration: null,
-  name_responsible: null,
-  street: null,
-  address_number: null,
-  postal_code: null,
-  neighborhood: null,
-  complement: null,
-  city_id: null,
-  state_id: null,
-  email: null,
-  secondary_email: null,
-  phone_number: null,
-  cell_number: null,
-  partners: [],
-  contracts: [],
-});
+const { form, getFormAsFormData, setUpdateFormAsOriginal } =
+  useFormUpdateTracker({
+    fantasy_name: null,
+    social_reason: null,
+    cnpj: null,
+    state_registration: null,
+    name_responsible: null,
+    street: null,
+    address_number: null,
+    postal_code: null,
+    neighborhood: null,
+    complement: null,
+    city_id: null,
+    state_id: null,
+    email: null,
+    secondary_email: null,
+    phone_number: null,
+    cell_number: null,
+    partners: [],
+    contracts: [],
+  });
 
 const activeTab = ref("unit_data");
 

+ 87 - 0
src/pages/unit/components/AddEditHistoryDialog.vue

@@ -0,0 +1,87 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin overflow-hidden" style="width: 560px; max-width: 95vw">
+      <DefaultDialogHeader
+        :title="() => (history ? 'Editar Histórico' : 'Novo Histórico')"
+        @close="onDialogCancel"
+      />
+
+      <q-form ref="formRef" @submit="onOKClick">
+        <q-card-section class="q-pt-none">
+          <div class="column q-gutter-sm">
+            <DefaultInput
+              v-model="form.title"
+              label="Título"
+              outlined
+              :rules="[inputRules.required]"
+            />
+            <q-input
+              v-model="form.content"
+              label="Conteúdo"
+              outlined
+              type="textarea"
+              rows="5"
+              autogrow
+            />
+            <q-toggle
+              v-model="form.visible_to_franchisee"
+              label="Visível ao franqueado"
+              color="positive"
+            />
+          </div>
+        </q-card-section>
+
+        <q-card-actions align="right" class="q-pa-md">
+          <q-btn outline color="primary" label="Cancelar" @click="onDialogCancel" />
+          <q-btn color="primary-2" :label="history ? 'Salvar' : 'Adicionar'" type="submit" :loading="loading" />
+        </q-card-actions>
+      </q-form>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import { useInputRules } from "src/composables/useInputRules";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { createHistory, updateHistory } from "src/api/unit_history";
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { history, unitId } = defineProps({
+  history: { type: Object, default: null },
+  unitId: { type: Number, required: true },
+});
+
+const { inputRules } = useInputRules();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+
+const formRef = ref(null);
+
+const form = ref({
+  title: history?.title ?? "",
+  content: history?.content ?? "",
+  visible_to_franchisee: history?.visible_to_franchisee ?? false,
+});
+
+const { loading, execute } = useSubmitHandler({
+  formRef,
+  onSuccess: (result) => onDialogOK(result),
+});
+
+async function onOKClick() {
+  await execute(() => {
+    const payload = {
+      unit_id: unitId,
+      title: form.value.title,
+      content: form.value.content,
+      visible_to_franchisee: form.value.visible_to_franchisee,
+    };
+
+    return history ? updateHistory(history.id, payload) : createHistory(payload);
+  });
+}
+</script>

+ 86 - 0
src/pages/unit/components/AddMediaDialog.vue

@@ -0,0 +1,86 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin overflow-hidden" style="width: 480px; max-width: 95vw">
+      <DefaultDialogHeader :title="() => 'Adicionar Mídia'" @close="onDialogCancel" />
+
+      <q-form ref="formRef" @submit="onOKClick">
+        <q-card-section class="q-pt-none">
+          <div class="column q-gutter-sm">
+            <DefaultInput
+              v-model="form.title"
+              label="Título"
+              outlined
+              :rules="[inputRules.required]"
+            />
+
+            <q-file
+              v-model="selectedFile"
+              label="Arquivo"
+              outlined
+              accept="image/*,video/*,.pdf"
+              :rules="[inputRules.required]"
+            >
+              <template #prepend>
+                <q-icon name="attach_file" />
+              </template>
+            </q-file>
+
+            <q-toggle
+              v-model="form.visible_to_franchisee"
+              label="Visível ao franqueado"
+              color="positive"
+            />
+          </div>
+        </q-card-section>
+
+        <q-card-actions align="right" class="q-pa-md">
+          <q-btn outline color="primary" label="Cancelar" @click="onDialogCancel" />
+          <q-btn color="primary-2" label="Adicionar" type="submit" :loading="loading" />
+        </q-card-actions>
+      </q-form>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import { useInputRules } from "src/composables/useInputRules";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { createMedia } from "src/api/unit_media";
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { unitId } = defineProps({
+  unitId: { type: Number, required: true },
+});
+
+const { inputRules } = useInputRules();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+
+const formRef = ref(null);
+const selectedFile = ref(null);
+
+const form = ref({
+  title: "",
+  visible_to_franchisee: false,
+});
+
+const { loading, execute } = useSubmitHandler({
+  formRef,
+  onSuccess: (result) => onDialogOK(result),
+});
+
+async function onOKClick() {
+  await execute(() => {
+    const formData = new FormData();
+    formData.append("unit_id", unitId);
+    formData.append("title", form.value.title);
+    formData.append("file", selectedFile.value);
+    formData.append("visible_to_franchisee", form.value.visible_to_franchisee ? 1 : 0);
+    return createMedia(formData);
+  });
+}
+</script>

+ 94 - 94
src/pages/unit/tabs/FinancialTab.vue

@@ -6,9 +6,7 @@
         <!-- Dados Bancários -->
         <div class="row q-col-gutter-sm">
           <div class="col-12">
-            <span class="text-subtitle1 text-weight-medium"
-              >Dados Bancários</span
-            >
+            <span class="text-subtitle1 text-weight-medium">Dados Bancários</span>
           </div>
 
           <DefaultSelect
@@ -21,25 +19,9 @@
             map-options
           />
 
-          <DefaultInput
-            v-model="form.bank"
-            label="Banco"
-            class="col-12"
-            outlined
-          />
-
-          <DefaultInput
-            v-model="form.agency"
-            label="Agência"
-            class="col-12"
-            outlined
-          />
-          <DefaultInput
-            v-model="form.account"
-            label="Conta"
-            class="col-12"
-            outlined
-          />
+          <DefaultInput v-model="form.bank" label="Banco" class="col-12" outlined />
+          <DefaultInput v-model="form.agency" label="Agência" class="col-12" outlined />
+          <DefaultInput v-model="form.account" label="Conta" class="col-12" outlined />
 
           <DefaultSelect
             v-model="form.account_type"
@@ -51,26 +33,14 @@
             map-options
           />
 
-          <DefaultInput
-            v-model="form.account_holder"
-            label="Titular da Conta"
-            class="col-12"
-            outlined
-          />
-          <DefaultInput
-            v-model="form.pix_key"
-            label="Chave Pix"
-            class="col-12"
-            outlined
-          />
+          <DefaultInput v-model="form.account_holder" label="Titular da Conta" class="col-12" outlined />
+          <DefaultInput v-model="form.pix_key" label="Chave Pix" class="col-12" outlined />
         </div>
 
         <!-- Dados para Faturamento -->
         <div class="row q-col-gutter-sm">
           <div class="col-12">
-            <span class="text-subtitle1 text-weight-medium"
-              >Dados para Faturamento</span
-            >
+            <span class="text-subtitle1 text-weight-medium">Dados para Faturamento</span>
           </div>
 
           <DefaultSelect
@@ -83,18 +53,8 @@
             map-options
           />
 
-          <DefaultInput
-            v-model="form.due_date"
-            label="Data de Vencimento"
-            class="col-12"
-            outlined
-          />
-          <DefaultInput
-            v-model="form.financial_email"
-            label="E-mail Financeiro"
-            class="col-12"
-            outlined
-          />
+          <DefaultInput v-model="form.due_date" label="Data de Vencimento" class="col-12" outlined />
+          <DefaultInput v-model="form.financial_email" label="E-mail Financeiro" class="col-12" outlined />
         </div>
       </div>
 
@@ -103,9 +63,7 @@
         <!-- Dados do Contrato -->
         <div class="row q-col-gutter-sm">
           <div class="col-12">
-            <span class="text-subtitle1 text-weight-medium"
-              >Dados do Contrato</span
-            >
+            <span class="text-subtitle1 text-weight-medium">Dados do Contrato</span>
           </div>
 
           <DefaultSelect
@@ -118,64 +76,52 @@
             map-options
           />
 
-          <DefaultInput
-            v-model="form.maintenance_fee"
-            label="Taxa de Manutenção"
-            class="col-12"
-            outlined
-          />
-          <DefaultInput
-            v-model="form.marketing_fund"
-            label="Fundo de Marketing"
-            class="col-12"
-            outlined
-          />
-          <DefaultInput
-            v-model="form.tbr"
-            label="TBR"
-            class="col-12"
-            outlined
-          />
+          <DefaultInput v-model="form.maintenance_fee" label="Taxa de Manutenção" class="col-12" outlined type="number" />
+          <DefaultInput v-model="form.marketing_fund" label="Fundo de Marketing" class="col-12" outlined type="number" />
+          <DefaultInput v-model="form.tbr" label="TBR" class="col-12" outlined type="number" />
         </div>
 
         <div class="row q-col-gutter-sm">
           <div class="col-12">
-            <span class="text-subtitle1 text-weight-medium"
-              >Dados de Contato</span
-            >
+            <span class="text-subtitle1 text-weight-medium">Dados de Contato</span>
           </div>
 
-          <div
-            v-for="(partner, index) in contactPartners"
-            :key="index"
-            class="col-6"
-          >
-            <PartnerCardComponent :partner />
+          <div v-if="loadingPartners" class="col-12 row justify-center q-pa-md">
+            <q-spinner color="primary" size="32px" />
           </div>
+
+          <template v-else>
+            <div v-for="(partner, index) in partners" :key="index" class="col-6">
+              <PartnerCardComponent :partner />
+            </div>
+            <div v-if="partners.length === 0" class="col-12 text-grey-6 text-center q-pa-md">
+              Nenhum sócio cadastrado.
+            </div>
+          </template>
         </div>
       </div>
     </div>
 
     <div class="row justify-end q-mt-md items-end full-width q-px-xs">
       <div class="row q-gutter-sm">
-        <q-btn label="Cancelar" color="primary" outline />
-        <q-btn label="Salvar" color="primary-2" />
-        <q-btn
-          icon="mdi-paperclip-plus"
-          color="primary-2"
-          style="height: 40px; width: 40px"
-        />
+        <q-btn label="Cancelar" color="primary" outline @click="resetForm" />
+        <q-btn label="Salvar" color="primary-2" :loading="saving" @click="onSave" />
       </div>
     </div>
   </div>
 </template>
 
 <script setup>
-import { ref } from "vue";
+import { ref, onMounted } from "vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 import PartnerCardComponent from "src/components/shared/PartnerCardComponent.vue";
-import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
+import { getFinancialByUnit, upsertFinancial } from "src/api/unit_financial";
+import { getPartnersByUnit } from "src/api/unit_partner";
+
+const props = defineProps({
+  unitId: { type: Number, default: null },
+});
 
 const taxRegimeOptions = [
   { label: "Selecione", value: null },
@@ -201,12 +147,7 @@ const billingMethodOptions = [
 
 const groupOptions = [{ label: "Selecione", value: null }];
 
-const contactPartners = ref([
-  { social_name: null, role: null, avatarUrl: null, color: "#ff8340" },
-  { social_name: null, role: null, avatarUrl: null, color: "#4caf50" },
-]);
-
-const { form } = useFormUpdateTracker({
+const defaultForm = () => ({
   tax_regime: null,
   bank: null,
   agency: null,
@@ -222,4 +163,63 @@ const { form } = useFormUpdateTracker({
   marketing_fund: null,
   tbr: null,
 });
+
+const form = ref(defaultForm());
+const partners = ref([]);
+const saving = ref(false);
+const loadingPartners = ref(false);
+
+let originalForm = defaultForm();
+
+async function fetchData() {
+  if (!props.unitId) return;
+
+  const [financial] = await Promise.allSettled([
+    loadFinancial(),
+    loadPartners(),
+  ]);
+}
+
+async function loadFinancial() {
+  try {
+    const data = await getFinancialByUnit(props.unitId);
+    if (data) {
+      Object.assign(form.value, data);
+      originalForm = { ...data };
+    }
+  } catch (e) {
+    console.error(e);
+  }
+}
+
+async function loadPartners() {
+  loadingPartners.value = true;
+  try {
+    partners.value = await getPartnersByUnit(props.unitId);
+  } catch (e) {
+    console.error(e);
+  } finally {
+    loadingPartners.value = false;
+  }
+}
+
+async function onSave() {
+  if (!props.unitId) return;
+  saving.value = true;
+  try {
+    const saved = await upsertFinancial({ unit_id: props.unitId, ...form.value });
+    Object.assign(form.value, saved);
+    originalForm = { ...saved };
+  } catch (e) {
+    console.error(e);
+  } finally {
+    saving.value = false;
+  }
+}
+
+function resetForm() {
+  Object.assign(form.value, defaultForm(), originalForm);
+}
+
+onMounted(fetchData);
 </script>

+ 143 - 112
src/pages/unit/tabs/HistoryTab.vue

@@ -1,96 +1,92 @@
 <template>
   <div class="q-pa-md">
     <div class="row q-col-gutter-md">
+      <!-- Coluna esquerda: lista -->
       <div class="col-12 col-md-5">
-        <q-table
-          :rows="history"
-          :columns="columns"
-          row-key="title"
-          flat
-          hide-bottom
-          class="bg-transparent"
-          :row-class="
-            (_, index) =>
-              index === selectedIndex
-                ? 'history-item-active cursor-pointer'
-                : 'cursor-pointer'
-          "
-          @row-click="(_, __, index) => (selectedIndex = index)"
-        >
-          <template #body-cell-actions>
-            <q-td align="center">
-              <div class="flex items-center justify-center q-gutter-x-xs">
-                <q-icon
-                  name="mdi-file-edit-outline"
-                  size="sm"
-                  color="grey-8"
-                  class="cursor-pointer"
-                  style="
-                    border: 1px solid #c9c9c9;
-                    border-radius: 8px;
-                    padding: 4px;
-                  "
-                  @click.stop="console.log('edit')"
-                />
-                <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>
-
-          <template #body-cell-franchisee>
-            <q-td align="center">
-              <div class="flex items-center justify-center q-gutter-x-xs">
-                <q-icon
-                  name="mdi-check"
-                  size="sm"
-                  color="suface"
-                  class="cursor-pointer bg-approved"
-                  style="border: 1px solid #c9c9c9; border-radius: 8px"
-                  @click.stop="console.log('edit')"
-                />
-                <q-icon
-                  name="mdi-close-circle-outline"
-                  size="sm"
-                  color="declined"
-                  class="cursor-pointer"
-                  style="border: 1px solid red; border-radius: 8px"
-                  @click.stop="console.log('trash')"
-                />
-              </div>
-            </q-td>
-          </template>
-        </q-table>
-      </div>
-
-      <div class="col-12 col-md-7">
-        <div class="row justify-end q-mb-sm">
+        <div class="row justify-between items-center q-mb-md">
+          <span class="text-subtitle1 text-weight-medium">Histórico</span>
           <q-btn
             icon="add"
             color="primary-2"
             style="height: 40px; width: 40px; border-radius: 8px"
+            :disable="!unitId"
+            @click="openDialog(null)"
           />
         </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="histories.length === 0" class="text-center text-grey-6 q-pa-xl">
+            <q-icon name="mdi-history" size="48px" color="grey-4" />
+            <div class="q-mt-sm">Nenhum histórico registrado.</div>
+          </div>
+
+          <q-list v-else separator>
+            <q-item
+              v-for="(item, index) in histories"
+              :key="item.id"
+              clickable
+              :active="selectedIndex === index"
+              active-class="history-item-active"
+              @click="selectedIndex = index"
+            >
+              <q-item-section avatar>
+                <q-icon name="mdi-text-box-outline" color="primary-2" size="md" />
+              </q-item-section>
+
+              <q-item-section>
+                <q-item-label class="ellipsis" style="max-width: 180px">
+                  {{ item.title }}
+                </q-item-label>
+                <q-item-label caption>
+                  {{ formatDate(item.created_at) }}
+                </q-item-label>
+              </q-item-section>
+
+              <q-item-section side>
+                <div class="row q-gutter-xs">
+                  <q-btn flat round dense icon="edit" color="grey-7" size="sm" @click.stop="openDialog(item)" />
+                  <q-btn flat round dense icon="delete" color="negative" size="sm" @click.stop="onRemove(item, index)" />
+                </div>
+              </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 q-pa-md">
-          <span v-if="selectedIndex === null" class="text-grey-5"
-            >Pré - Visualização</span
+          <div
+            v-if="selectedIndex === null || !histories[selectedIndex]"
+            class="flex flex-center full-height text-grey-5"
+            style="min-height: 400px"
           >
+            <div class="column items-center q-gutter-sm">
+              <q-icon name="mdi-history" size="64px" color="grey-3" />
+              <span>Selecione um item para visualizar</span>
+            </div>
+          </div>
+
           <div v-else>
-            <p class="text-weight-medium q-mb-sm">
-              {{ history[selectedIndex].title }}
+            <div class="row justify-between items-start q-mb-sm">
+              <p class="text-weight-medium text-subtitle1 q-mb-none">
+                {{ histories[selectedIndex].title }}
+              </p>
+              <q-badge
+                v-if="histories[selectedIndex].visible_to_franchisee"
+                color="positive"
+                label="Visível ao franqueado"
+              />
+            </div>
+            <p class="text-caption text-grey-6 q-mb-md">
+              {{ formatDate(histories[selectedIndex].created_at) }}
             </p>
-            <p class="text-grey-7" style="white-space: pre-wrap">
-              {{ history[selectedIndex].content }}
+            <p class="text-grey-8" style="white-space: pre-wrap">
+              {{ histories[selectedIndex].content }}
             </p>
           </div>
         </div>
@@ -100,42 +96,71 @@
 </template>
 
 <script setup>
-import { ref } from "vue";
+import { ref, onMounted } from "vue";
+import { useQuasar } from "quasar";
+import { getHistoriesByUnit, deleteHistory } from "src/api/unit_history";
+import AddEditHistoryDialog from "src/pages/unit/components/AddEditHistoryDialog.vue";
 
+const props = defineProps({
+  unitId: { type: Number, default: null },
+});
+
+const $q = useQuasar();
+const histories = ref([]);
 const selectedIndex = ref(null);
+const loading = ref(false);
+
+async function fetchHistories() {
+  if (!props.unitId) return;
+  loading.value = true;
+  try {
+    histories.value = await getHistoriesByUnit(props.unitId);
+  } catch (e) {
+    console.error(e);
+  } finally {
+    loading.value = false;
+  }
+}
+
+function openDialog(item) {
+  $q.dialog({
+    component: AddEditHistoryDialog,
+    componentProps: { history: item, unitId: props.unitId },
+  }).onOk((result) => {
+    if (item) {
+      const idx = histories.value.findIndex((h) => h.id === item.id);
+      if (idx !== -1) histories.value.splice(idx, 1, result);
+    } else {
+      histories.value.unshift(result);
+      selectedIndex.value = 0;
+    }
+  });
+}
 
-const columns = [
-  {
-    name: "date",
-    label: "Data",
-    field: "date",
-    align: "left",
-    style: "width: 90px",
-  },
-  { name: "title", label: "Título", field: "title", align: "left" },
-  {
-    name: "actions",
-    label: "Ações",
-    field: "actions",
-    align: "center",
-    style: "width: 90px",
-  },
-  {
-    name: "franchisee",
-    label: "Franqueado",
-    field: "franchisee",
-    align: "left",
-    style: "width: 100px",
-  },
-];
-
-const history = ref([
-  {
-    date: "15/01/2026",
-    title: "Alteração de Razão Social",
-    content: "",
-  },
-]);
+function onRemove(item, index) {
+  $q.dialog({
+    title: "Remover histórico",
+    message: `Deseja remover o item "${item.title}"?`,
+    ok: { color: "negative", label: "Remover" },
+    cancel: { color: "primary", outline: true, label: "Cancelar" },
+  }).onOk(async () => {
+    try {
+      await deleteHistory(item.id);
+      histories.value.splice(index, 1);
+      if (selectedIndex.value === index) selectedIndex.value = null;
+      else if (selectedIndex.value > index) selectedIndex.value--;
+    } catch (e) {
+      console.error(e);
+    }
+  });
+}
+
+function formatDate(dateStr) {
+  if (!dateStr) return "";
+  return new Date(dateStr).toLocaleDateString("pt-BR");
+}
+
+onMounted(fetchHistories);
 </script>
 
 <style scoped>
@@ -148,4 +173,10 @@ const history = ref([
 .history-item-active {
   background-color: rgba(255, 131, 64, 0.08);
 }
+
+.ellipsis {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
 </style>

+ 162 - 120
src/pages/unit/tabs/MediasTab.vue

@@ -1,100 +1,95 @@
 <template>
   <div class="q-pa-md">
     <div class="row q-col-gutter-md">
+      <!-- Coluna esquerda: lista -->
       <div class="col-12 col-md-5">
-        <q-table
-          :rows="medias"
-          :columns="columns"
-          row-key="title"
-          flat
-          hide-bottom
-          class="bg-transparent"
-          :row-class="
-            (_, index) =>
-              index === selectedIndex
-                ? 'media-item-active cursor-pointer'
-                : 'cursor-pointer'
-          "
-          @row-click="(_, __, index) => (selectedIndex = index)"
-        >
-          <template #body-cell-actions>
-            <q-td align="center">
-              <div class="flex items-center justify-center q-gutter-x-xs">
-                <q-icon
-                  name="mdi-file-edit-outline"
-                  size="sm"
-                  color="grey-8"
-                  class="cursor-pointer"
-                  style="
-                    border: 1px solid #c9c9c9;
-                    border-radius: 8px;
-                    padding: 4px;
-                  "
-                  @click.stop="console.log('edit')"
-                />
-                <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>
-
-          <template #body-cell-franchisee>
-            <q-td align="center">
-              <div class="flex items-center justify-center q-gutter-x-xs">
-                <q-icon
-                  name="mdi-check"
-                  size="sm"
-                  color="suface"
-                  class="cursor-pointer bg-approved"
-                  style="border: 1px solid #c9c9c9; border-radius: 8px"
-                  @click.stop="console.log('edit')"
-                />
-                <q-icon
-                  name="mdi-close-circle-outline"
-                  size="sm"
-                  color="declined"
-                  class="cursor-pointer"
-                  style="border: 1px solid red; border-radius: 8px"
-                  @click.stop="console.log('trash')"
-                />
-              </div>
-            </q-td>
-          </template>
-        </q-table>
-      </div>
-
-      <div class="col-12 col-md-7">
-        <div class="row justify-end q-mb-sm">
+        <div class="row justify-between items-center q-mb-md">
+          <span class="text-subtitle1 text-weight-medium">Mídias</span>
           <q-btn
             icon="add"
             color="primary-2"
             style="height: 40px; width: 40px; border-radius: 8px"
+            :disable="!unitId"
+            @click="openAddDialog"
           />
         </div>
-        <div class="preview-box q-pa-md">
-          <span v-if="selectedIndex === null" class="text-grey-5"
-            >Pré - Visualização</span
+
+        <div v-if="loading" class="row justify-center q-pa-xl">
+          <q-spinner color="primary" size="40px" />
+        </div>
+
+        <template v-else>
+          <div v-if="medias.length === 0" class="text-center text-grey-6 q-pa-xl">
+            <q-icon name="mdi-image-multiple-outline" size="48px" color="grey-4" />
+            <div class="q-mt-sm">Nenhuma mídia adicionada.</div>
+          </div>
+
+          <q-list v-else separator>
+            <q-item
+              v-for="(item, index) in medias"
+              :key="item.id"
+              clickable
+              :active="selectedIndex === index"
+              active-class="media-item-active"
+              @click="selectedIndex = index"
+            >
+              <q-item-section avatar>
+                <q-icon :name="getFileIcon(item.mime_type)" :color="getFileColor(item.mime_type)" size="md" />
+              </q-item-section>
+
+              <q-item-section>
+                <q-item-label class="ellipsis" style="max-width: 180px">
+                  {{ item.title }}
+                </q-item-label>
+                <q-item-label caption>
+                  {{ formatDate(item.created_at) }}
+                </q-item-label>
+              </q-item-section>
+
+              <q-item-section side>
+                <q-btn
+                  flat round dense icon="delete"
+                  color="negative" size="sm"
+                  @click.stop="onRemove(item, 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 || !medias[selectedIndex]"
+            class="flex flex-center full-height text-grey-5"
+            style="min-height: 500px"
           >
-          <div v-else>
-            <p class="text-weight-medium q-mb-sm">
-              {{ medias[selectedIndex].title }}
-            </p>
+            <div class="column items-center q-gutter-sm">
+              <q-icon name="mdi-image-multiple-outline" size="64px" color="grey-3" />
+              <span>Selecione uma mídia para visualizar</span>
+            </div>
+          </div>
+
+          <template v-else>
             <img
-              v-if="medias[selectedIndex].url"
-              :src="medias[selectedIndex].url"
-              style="max-width: 100%; border-radius: 4px"
+              v-if="isImage(medias[selectedIndex].mime_type)"
+              :src="medias[selectedIndex].file_url"
+              style="width: 100%; border-radius: 8px; display: block"
             />
-          </div>
+            <video
+              v-else-if="isVideo(medias[selectedIndex].mime_type)"
+              :src="medias[selectedIndex].file_url"
+              controls
+              style="width: 100%; border-radius: 8px; display: block"
+            />
+            <iframe
+              v-else
+              :src="medias[selectedIndex].file_url"
+              style="width: 100%; min-height: 500px; border: none; border-radius: 8px"
+            />
+          </template>
         </div>
       </div>
     </div>
@@ -102,56 +97,103 @@
 </template>
 
 <script setup>
-import { ref } from "vue";
+import { ref, onMounted } from "vue";
+import { useQuasar } from "quasar";
+import { getMediasByUnit, deleteMedia } from "src/api/unit_media";
+import AddMediaDialog from "src/pages/unit/components/AddMediaDialog.vue";
+
+const props = defineProps({
+  unitId: { type: Number, default: null },
+});
 
+const $q = useQuasar();
+const medias = ref([]);
 const selectedIndex = ref(null);
+const loading = ref(false);
+
+async function fetchMedias() {
+  if (!props.unitId) return;
+  loading.value = true;
+  try {
+    medias.value = await getMediasByUnit(props.unitId);
+  } catch (e) {
+    console.error(e);
+  } finally {
+    loading.value = false;
+  }
+}
+
+function openAddDialog() {
+  $q.dialog({
+    component: AddMediaDialog,
+    componentProps: { unitId: props.unitId },
+  }).onOk((result) => {
+    medias.value.unshift(result);
+    selectedIndex.value = 0;
+  });
+}
+
+function onRemove(item, index) {
+  $q.dialog({
+    title: "Remover mídia",
+    message: `Deseja remover a mídia "${item.title}"?`,
+    ok: { color: "negative", label: "Remover" },
+    cancel: { color: "primary", outline: true, label: "Cancelar" },
+  }).onOk(async () => {
+    try {
+      await deleteMedia(item.id);
+      medias.value.splice(index, 1);
+      if (selectedIndex.value === index) selectedIndex.value = null;
+      else if (selectedIndex.value > index) selectedIndex.value--;
+    } catch (e) {
+      console.error(e);
+    }
+  });
+}
+
+function isImage(mimeType) {
+  return mimeType?.startsWith("image/");
+}
+
+function isVideo(mimeType) {
+  return mimeType?.startsWith("video/");
+}
+
+function getFileIcon(mimeType) {
+  if (isImage(mimeType)) return "mdi-image-outline";
+  if (isVideo(mimeType)) return "mdi-video-outline";
+  return "mdi-file-pdf-box";
+}
+
+function getFileColor(mimeType) {
+  if (isImage(mimeType)) return "teal";
+  if (isVideo(mimeType)) return "blue";
+  return "negative";
+}
+
+function formatDate(dateStr) {
+  if (!dateStr) return "";
+  return new Date(dateStr).toLocaleDateString("pt-BR");
+}
 
-const columns = [
-  {
-    name: "date",
-    label: "Data",
-    field: "date",
-    align: "left",
-    style: "width: 90px",
-  },
-  { name: "title", label: "Título", field: "title", align: "left" },
-  {
-    name: "actions",
-    label: "Ações",
-    field: "actions",
-    align: "center",
-    style: "width: 90px",
-  },
-  {
-    name: "franchisee",
-    label: "Franqueado",
-    field: "franchisee",
-    align: "center",
-    style: "width: 100px",
-  },
-];
-
-const medias = ref([
-  {
-    date: "15/01/2026",
-    title: "Imagens Sede",
-    url: "",
-  },
-]);
+onMounted(fetchMedias);
 </script>
 
 <style scoped>
 .preview-box {
   border: 1px solid #e0e0e0;
   border-radius: 8px;
-  min-height: 400px;
+  overflow: hidden;
+  min-height: 500px;
 }
 
 .media-item-active {
   background-color: rgba(255, 131, 64, 0.08);
 }
 
-.transparent-header :deep(thead tr th) {
-  background-color: transparent;
+.ellipsis {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 </style>