diff --git a/app/controllers/api/v1/accounts/captain/units/reservations_sync_controller.rb b/app/controllers/api/v1/accounts/captain/units/reservations_sync_controller.rb new file mode 100644 index 0000000..ba8225c --- /dev/null +++ b/app/controllers/api/v1/accounts/captain/units/reservations_sync_controller.rb @@ -0,0 +1,21 @@ +module Api + module V1 + module Accounts + module Captain + module Units + class ReservationsSyncController < Api::V1::Accounts::BaseController + def create + unit = Current.account.captain_units.find(params[:unit_id]) + ::Captain::Reservations::SyncService.new(unit).perform + head :ok + rescue ActiveRecord::RecordNotFound + render_not_found_error('Unit not found') + rescue StandardError => e + render_error(e.message) + end + end + end + end + end + end +end diff --git a/app/controllers/api/v1/accounts/captain/units_controller.rb b/app/controllers/api/v1/accounts/captain/units_controller.rb new file mode 100644 index 0000000..39f0f5d --- /dev/null +++ b/app/controllers/api/v1/accounts/captain/units_controller.rb @@ -0,0 +1,65 @@ +module Api + module V1 + module Accounts + module Captain + class UnitsController < Api::V1::Accounts::BaseController + def index + @units = Current.account.captain_units + end + + def show + @unit = Current.account.captain_units.find(params[:id]) + end + + def create + @unit = Current.account.captain_units.new(unit_params) + @unit.captain_brand = Current.account.captain_brands.first # Default brand logic for now + + if @unit.save + render 'show', status: :created + else + render_error_response(@unit) + end + end + + def update + @unit = Current.account.captain_units.find(params[:id]) + + if @unit.update(unit_params) + render 'show' + else + render_error_response(@unit) + end + end + + def destroy + @unit = Current.account.captain_units.find(params[:id]) + @unit.destroy + head :ok + end + + private + + def unit_params + params.require(:unit).permit( + :name, + :status, + :reservations_sync_enabled, + :plug_play_id, + :plug_play_token, + :webhook_url, + :leader_whatsapp, + :reservation_source_tag, + :inter_client_id, + :inter_client_secret, + :inter_pix_key, + :inter_account_number, + visible_suite_categories: [], + suite_category_images: {} + ) + end + end + end + end + end +end diff --git a/app/javascript/dashboard/api/captain/inboxes.js b/app/javascript/dashboard/api/captain/inboxes.js index 2a7438c..b9bdbee 100755 --- a/app/javascript/dashboard/api/captain/inboxes.js +++ b/app/javascript/dashboard/api/captain/inboxes.js @@ -11,9 +11,9 @@ class CaptainInboxes extends ApiClient { } create(params = {}) { - const { assistantId, inboxId } = params; + const { assistantId, inboxId, captain_unit_id } = params; return axios.post(`${this.url}/${assistantId}/inboxes`, { - inbox: { inbox_id: inboxId }, + inbox: { inbox_id: inboxId, captain_unit_id }, }); } @@ -23,9 +23,9 @@ class CaptainInboxes extends ApiClient { } update(inboxId, params = {}) { - const { assistantId, always_use_reminder_tool } = params; + const { assistantId, captain_unit_id, always_use_reminder_tool } = params; return axios.patch(`${this.url}/${assistantId}/inboxes/${inboxId}`, { - inbox: { always_use_reminder_tool }, + inbox: { captain_unit_id, always_use_reminder_tool }, }); } } diff --git a/app/javascript/dashboard/api/captain/units.js b/app/javascript/dashboard/api/captain/units.js index cfe4ae0..47375a4 100644 --- a/app/javascript/dashboard/api/captain/units.js +++ b/app/javascript/dashboard/api/captain/units.js @@ -12,6 +12,10 @@ class UnitsAPI extends ApiClient { update(id, data) { return window.axios.patch(`${this.url}/${id}`, data); } + + syncReservations(id) { + return window.axios.post(`${this.url}/${id}/reservations/sync`); + } } export default new UnitsAPI(); diff --git a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue index e381a04..7ba4a91 100755 --- a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue @@ -68,6 +68,12 @@ const inboxName = computed(() => { }); const menuItems = computed(() => [ + { + label: t('CAPTAIN.INBOXES.OPTIONS.EDIT'), + value: 'edit', + action: 'edit', + icon: 'i-lucide-pencil-line', + }, { label: t('CAPTAIN.INBOXES.OPTIONS.DISCONNECT'), value: 'delete', @@ -86,6 +92,33 @@ const handleAction = ({ action, value }) => { emit('action', { action, value, id: props.id }); }; +const unitName = ref(''); + +const fetchUnit = async () => { + const unitId = props.inbox?.captain_inbox?.captain_unit_id; + if (!unitId) return; + + try { + const accountId = window.chatwootConfig?.account_id; + if (!accountId) return; + + const { data } = await window.axios.get( + `/api/v1/accounts/${accountId}/captain/units/${unitId}` + ); + unitName.value = data.name; + } catch (error) { + // Ignore error + } +}; + +watch( + () => props.inbox?.captain_inbox?.captain_unit_id, + () => { + fetchUnit(); + }, + { immediate: true } +); + const toggleReminderTool = async value => { if (isUpdating.value) return; isUpdating.value = true; @@ -147,6 +180,14 @@ const toggleReminderTool = async value => { @update:model-value="toggleReminderTool" /> +
+ + + {{ unitName }} + +

{{ t('CAPTAIN.INBOXES.REMINDER_TOOL.HELP') }}

diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CustomToolForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CustomToolForm.vue index a7620c9..9d5a4c4 100755 --- a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CustomToolForm.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CustomToolForm.vue @@ -253,8 +253,8 @@ const handleSubmit = async () => { v-for="(header, index) in customHeaders" :key="index" ref="headersRef" - v-model:key="header.key" - v-model:value="header.value" + v-model:header-key="header.key" + v-model:header-value="header.value" @remove="removeHeader(index)" /> diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/HeaderRow.vue b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/HeaderRow.vue index f2dce8f..b8d33e0 100644 --- a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/HeaderRow.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/HeaderRow.vue @@ -8,27 +8,27 @@ const emit = defineEmits(['remove']); const { t } = useI18n(); const showErrors = ref(false); -const key = defineModel('key', { +const headerKey = defineModel('headerKey', { type: String, required: true, }); -const value = defineModel('value', { +const headerValue = defineModel('headerValue', { type: String, required: true, }); const validationError = computed(() => { - if (!key.value || key.value.trim() === '') { + if (!headerKey.value || headerKey.value.trim() === '') { return 'HEADER_KEY_REQUIRED'; } - if (!value.value || value.value.trim() === '') { + if (!headerValue.value || headerValue.value.trim() === '') { return 'HEADER_VALUE_REQUIRED'; } return null; }); -watch([key, value], () => { +watch([headerKey, headerValue], () => { showErrors.value = false; }); @@ -52,11 +52,11 @@ defineExpose({ validate });
-import { ref } from 'vue'; +import { ref, computed } from 'vue'; import { useStore } from 'dashboard/composables/store'; import { useAlert } from 'dashboard/composables'; import { useI18n } from 'vue-i18n'; @@ -7,11 +7,19 @@ import { useI18n } from 'vue-i18n'; import Dialog from 'dashboard/components-next/dialog/Dialog.vue'; import ConnectInboxForm from './ConnectInboxForm.vue'; -defineProps({ +const props = defineProps({ assistantId: { type: Number, required: true, }, + type: { + type: String, + default: 'create', + }, + inbox: { + type: Object, + default: null, + }, }); const emit = defineEmits(['close']); const { t } = useI18n(); @@ -20,15 +28,19 @@ const store = useStore(); const dialogRef = ref(null); const connectForm = ref(null); -const i18nKey = 'CAPTAIN.INBOXES.CREATE'; +const i18nKey = computed(() => + props.type === 'edit' ? 'CAPTAIN.INBOXES.EDIT' : 'CAPTAIN.INBOXES.CREATE' +); const handleSubmit = async payload => { try { - await store.dispatch('captainInboxes/create', payload); - useAlert(t(`${i18nKey}.SUCCESS_MESSAGE`)); + const action = + props.type === 'edit' ? 'captainInboxes/update' : 'captainInboxes/create'; + await store.dispatch(action, payload); + useAlert(t(`${i18nKey.value}.SUCCESS_MESSAGE`)); dialogRef.value.close(); } catch (error) { - const errorMessage = error?.message || t(`${i18nKey}.ERROR_MESSAGE`); + const errorMessage = error?.message || t(`${i18nKey.value}.ERROR_MESSAGE`); useAlert(errorMessage); } }; @@ -48,7 +60,7 @@ defineExpose({ dialogRef }); diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue index 5e48ef5..2a4b920 100755 --- a/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue @@ -14,6 +14,10 @@ const props = defineProps({ type: Number, required: true, }, + inbox: { + type: Object, + default: null, + }, }); const emit = defineEmits(['submit', 'cancel']); @@ -28,8 +32,8 @@ const formState = { }; const initialState = { - inboxId: null, - captainUnitId: null, + inboxId: props.inbox?.captain_inbox?.inbox_id || null, + captainUnitId: props.inbox?.captain_inbox?.captain_unit_id || null, }; const state = reactive({ ...initialState }); @@ -46,21 +50,65 @@ const accountId = computed(() => { const inboxList = computed(() => { const captainInboxIds = formState.captainInboxes.value.map(inbox => inbox.id); - return formState.inboxes.value + // Filter available inboxes from the store + const availableInboxes = formState.inboxes.value .filter(inbox => !captainInboxIds.includes(inbox.id)) .map(inbox => ({ value: inbox.id, label: inbox.name, })); + + // If we are editing, we MUST ensure the current inbox is in the list + if (props.inbox) { + const currentInboxId = props.inbox.id; + + // Check if it's already in the list (it shouldn't be if it's in captainInboxes) + const alreadyInList = availableInboxes.find( + i => i.value === currentInboxId + ); + + if (!alreadyInList) { + // We use the name directly from props.inbox to avoid store lookup issues + const label = + props.inbox.name || props.inbox.phone_number || 'Caixa de Entrada'; + + // Add to the beginning of the list + availableInboxes.unshift({ + value: currentInboxId, + label: label, + }); + } + } + + return availableInboxes; }); const unitList = computed(() => { - return units.map(unit => ({ - value: unit.id, - label: unit.name, - })); + return [ + { value: null, label: 'Sem Unidade' }, + ...units.map(unit => ({ + value: unit.id, + label: unit.name, + })), + ]; }); +watch( + () => props.inbox, + newInbox => { + if (newInbox) { + // Use inbox.id directly for the inboxId + state.inboxId = newInbox.id; + + // Use captain_inbox data for other fields if available + if (newInbox.captain_inbox) { + state.captainUnitId = newInbox.captain_inbox.captain_unit_id; + } + } + }, + { immediate: true, deep: true } +); + const v$ = useVuelidate(validationRules, state); const isLoading = computed(() => formState.uiFlags.value.creatingItem); @@ -82,6 +130,7 @@ const prepareInboxPayload = () => ({ inboxId: state.inboxId, captain_unit_id: state.captainUnitId, assistantId: props.assistantId, + ...(props.inbox ? { id: props.inbox.captain_inbox.id } : {}), }); const handleSubmit = async () => { @@ -136,6 +185,7 @@ watch(accountId, () => { :placeholder="t('CAPTAIN.INBOXES.FORM.INBOX.PLACEHOLDER')" class="[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:dark:outline-n-weak [&>div>button:not(.focused)]:hover:!outline-n-slate-6" :message="formErrors.inboxId" + :disabled="!!inbox" />
@@ -169,7 +219,11 @@ watch(accountId, () => { />
-
+
{{ guestName }} + + {{ sourceTag }} +
@@ -206,6 +213,7 @@ const showMenu = ref(false); {{ formatCurrency(reservation.total_amount) }} +
{ dialogType.value = 'create'; nextTick(() => connectInboxDialog.value.dialogRef.open()); }; +const handleCreateClose = () => { + dialogType.value = ''; + selectedInbox.value = null; +}; + +// Handle edit action +const handleUpdate = () => { + dialogType.value = 'edit'; + nextTick(() => connectInboxDialog.value.dialogRef.open()); +}; + const handleAction = ({ action, id }) => { selectedInbox.value = captainInboxes.value.find( inbox => id === inbox.captain_inbox.id @@ -42,15 +53,12 @@ const handleAction = ({ action, id }) => { nextTick(() => { if (action === 'delete') { handleDelete(); + } else if (action === 'edit') { + handleUpdate(); } }); }; -const handleCreateClose = () => { - dialogType.value = ''; - selectedInbox.value = null; -}; - onMounted(() => store.dispatch('captainInboxes/get', { assistantId: assistantId.value, @@ -103,6 +111,7 @@ onMounted(() => ref="connectInboxDialog" :assistant-id="assistantId" :type="dialogType" + :inbox="selectedInbox" @close="handleCreateClose" /> diff --git a/app/javascript/dashboard/routes/dashboard/captain/assistants/tools/Index.vue b/app/javascript/dashboard/routes/dashboard/captain/assistants/tools/Index.vue index 6f8ef87..9535553 100644 --- a/app/javascript/dashboard/routes/dashboard/captain/assistants/tools/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/captain/assistants/tools/Index.vue @@ -236,7 +236,7 @@ onMounted(() => { :placeholder=" $t('CAPTAIN.ASSISTANTS.SKILLS.FALLBACK.PLACEHOLDER') " - :max-length="400" + :max-length="800" show-character-count @blur="handleFallbackUpdate(tool)" /> diff --git a/app/javascript/dashboard/routes/dashboard/captain/reservations/Index.vue b/app/javascript/dashboard/routes/dashboard/captain/reservations/Index.vue index 43089c9..9b59c0a 100644 --- a/app/javascript/dashboard/routes/dashboard/captain/reservations/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/captain/reservations/Index.vue @@ -27,6 +27,7 @@ const isLoading = ref(false); const isFetchingUnits = ref(false); const isUpdating = ref(false); const isCreating = ref(false); +const isSyncing = ref(false); const reservations = ref([]); const units = ref([]); @@ -226,6 +227,20 @@ const fetchReservations = async () => { } }; +const handleSync = async () => { + if (!filters.unit_id) return; + isSyncing.value = true; + try { + await CaptainUnitsAPI.syncReservations(filters.unit_id); + alert('Sincronização iniciada com sucesso!'); + fetchReservations(); + } catch (error) { + alert('Erro ao sincronizar reservas.'); + } finally { + isSyncing.value = false; + } +}; + // --- Kanban Logic --- const isInShiftWindow = dateString => { @@ -594,6 +609,18 @@ watch( title="Atualizar" @click="fetchReservations" /> + +
diff --git a/app/javascript/dashboard/routes/dashboard/captain/units/UnitModal.vue b/app/javascript/dashboard/routes/dashboard/captain/units/UnitModal.vue index 73fda6b..565f863 100644 --- a/app/javascript/dashboard/routes/dashboard/captain/units/UnitModal.vue +++ b/app/javascript/dashboard/routes/dashboard/captain/units/UnitModal.vue @@ -41,10 +41,15 @@ export default { inter_key_path: '', inter_account_number: '', webhook_url: '', + leader_whatsapp: '', + reservation_source_tag: '', inbox_id: '', brands: [], inboxes: [], isLoadingBrands: false, + plug_play_id: '', + plug_play_token: '', + reservations_sync_enabled: false, }; }, validations() { @@ -68,7 +73,13 @@ export default { this.inter_key_path = this.unit.inter_key_path; this.inter_account_number = this.unit.inter_account_number; this.webhook_url = this.unit.webhook_url; + this.leader_whatsapp = this.unit.leader_whatsapp || ''; + this.reservation_source_tag = this.unit.reservation_source_tag || ''; this.inbox_id = this.unit.inbox_id; + this.plug_play_id = this.unit.plug_play_id || ''; + this.plug_play_token = this.unit.plug_play_token || ''; + this.reservations_sync_enabled = + this.unit.reservations_sync_enabled || false; } else { this.resetForm(); } @@ -86,7 +97,12 @@ export default { this.inter_key_path = ''; this.inter_account_number = ''; this.webhook_url = ''; + this.leader_whatsapp = ''; + this.reservation_source_tag = ''; this.inbox_id = ''; + this.plug_play_id = ''; + this.plug_play_token = ''; + this.reservations_sync_enabled = false; this.v$.$reset(); }, async fetchInboxes() { @@ -126,7 +142,12 @@ export default { inter_key_path: this.inter_key_path, inter_account_number: this.inter_account_number, webhook_url: this.webhook_url, + leader_whatsapp: this.leader_whatsapp, + reservation_source_tag: this.reservation_source_tag, inbox_id: this.inbox_id, + plug_play_id: this.plug_play_id, + plug_play_token: this.plug_play_token, + reservations_sync_enabled: this.reservations_sync_enabled, }; try { @@ -181,6 +202,12 @@ export default { :error="v$.name.$error ? $t('CAPTAIN.UNITS.FORM.NAME_ERROR') : ''" /> + +