feat(lifecycle): settings tab with guards form and concierge per unit
Replaces stub Settings.vue with full implementation: anti-spam guard form (quiet hours, interval, pause-on-reply, opt-out label) and a collapsible ConciergeUnitCard per unit (inbox selector, persona name, knowledge base, key-value variables). Adds CONCIERGE_CONFIGURED / CONCIERGE_NOT_CONFIGURED i18n keys to en + pt_BR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ae4647d1c2
commit
94fdb5c318
@ -658,6 +658,8 @@
|
|||||||
"CONCIERGE_VARIABLE_KEY": "Key",
|
"CONCIERGE_VARIABLE_KEY": "Key",
|
||||||
"CONCIERGE_VARIABLE_VALUE": "Value",
|
"CONCIERGE_VARIABLE_VALUE": "Value",
|
||||||
"CONCIERGE_ADD_VARIABLE": "Add variable",
|
"CONCIERGE_ADD_VARIABLE": "Add variable",
|
||||||
|
"CONCIERGE_CONFIGURED": "Configured",
|
||||||
|
"CONCIERGE_NOT_CONFIGURED": "Not configured",
|
||||||
"SAVE": "Save changes",
|
"SAVE": "Save changes",
|
||||||
"TOAST": {
|
"TOAST": {
|
||||||
"SAVED": "Settings saved.",
|
"SAVED": "Settings saved.",
|
||||||
|
|||||||
@ -660,6 +660,8 @@
|
|||||||
"CONCIERGE_VARIABLE_KEY": "Chave",
|
"CONCIERGE_VARIABLE_KEY": "Chave",
|
||||||
"CONCIERGE_VARIABLE_VALUE": "Valor",
|
"CONCIERGE_VARIABLE_VALUE": "Valor",
|
||||||
"CONCIERGE_ADD_VARIABLE": "Adicionar variável",
|
"CONCIERGE_ADD_VARIABLE": "Adicionar variável",
|
||||||
|
"CONCIERGE_CONFIGURED": "Configurado",
|
||||||
|
"CONCIERGE_NOT_CONFIGURED": "Não configurado",
|
||||||
"SAVE": "Salvar alterações",
|
"SAVE": "Salvar alterações",
|
||||||
"TOAST": {
|
"TOAST": {
|
||||||
"SAVED": "Configurações salvas.",
|
"SAVED": "Configurações salvas.",
|
||||||
|
|||||||
@ -1,14 +1,143 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||||
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
|
import ConciergeUnitCard from './components/ConciergeUnitCard.vue';
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const config = useMapGetter('captainLifecycleConfig/getConfig');
|
||||||
|
const uiFlags = useMapGetter('captainLifecycleConfig/getUIFlags');
|
||||||
|
const units = useMapGetter('captainUnits/getUnits');
|
||||||
|
const labels = useMapGetter('labels/getLabels');
|
||||||
|
|
||||||
|
const form = ref({});
|
||||||
|
|
||||||
|
const syncForm = () => {
|
||||||
|
form.value = { ...config.value };
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(config, syncForm, { immediate: true });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.dispatch('captainLifecycleConfig/fetch');
|
||||||
|
store.dispatch('captainUnits/get');
|
||||||
|
store.dispatch('labels/get');
|
||||||
|
});
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
await store.dispatch('captainLifecycleConfig/update', {
|
||||||
|
quiet_hours_enabled: form.value.quiet_hours_enabled,
|
||||||
|
quiet_hours_from: form.value.quiet_hours_from,
|
||||||
|
quiet_hours_to: form.value.quiet_hours_to,
|
||||||
|
min_interval_minutes: Number(form.value.min_interval_minutes),
|
||||||
|
pause_on_customer_reply: form.value.pause_on_customer_reply,
|
||||||
|
pause_on_customer_reply_within_minutes: Number(
|
||||||
|
form.value.pause_on_customer_reply_within_minutes
|
||||||
|
),
|
||||||
|
opt_out_label_id: form.value.opt_out_label_id || null,
|
||||||
|
});
|
||||||
|
useAlert(t('CAPTAIN_LIFECYCLE.SETTINGS.TOAST.SAVED'));
|
||||||
|
} catch (e) {
|
||||||
|
useAlert(e.message || 'Error');
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6">
|
<div class="p-6 space-y-8">
|
||||||
<h2 class="text-lg font-semibold">
|
<section>
|
||||||
{{ t('CAPTAIN_LIFECYCLE.TABS.SETTINGS') }}
|
<h3 class="text-base font-semibold mb-3">
|
||||||
</h2>
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.GUARDS_TITLE') }}
|
||||||
<!-- Implementation in Task 14 -->
|
</h3>
|
||||||
|
<div v-if="uiFlags.fetching">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-3 max-w-xl">
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<Checkbox v-model="form.quiet_hours_enabled" />
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.QUIET_HOURS_ENABLED') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div v-if="form.quiet_hours_enabled" class="flex gap-3">
|
||||||
|
<label class="flex-1 text-sm">
|
||||||
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.QUIET_HOURS_FROM') }}
|
||||||
|
<input
|
||||||
|
v-model="form.quiet_hours_from"
|
||||||
|
type="time"
|
||||||
|
class="w-full border rounded px-2 py-1"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="flex-1 text-sm">
|
||||||
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.QUIET_HOURS_TO') }}
|
||||||
|
<input
|
||||||
|
v-model="form.quiet_hours_to"
|
||||||
|
type="time"
|
||||||
|
class="w-full border rounded px-2 py-1"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
v-model="form.min_interval_minutes"
|
||||||
|
type="number"
|
||||||
|
:label="t('CAPTAIN_LIFECYCLE.SETTINGS.MIN_INTERVAL')"
|
||||||
|
:message="t('CAPTAIN_LIFECYCLE.SETTINGS.MIN_INTERVAL_HELP')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<Checkbox v-model="form.pause_on_customer_reply" />
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.PAUSE_ON_REPLY') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
v-if="form.pause_on_customer_reply"
|
||||||
|
v-model="form.pause_on_customer_reply_within_minutes"
|
||||||
|
type="number"
|
||||||
|
:label="t('CAPTAIN_LIFECYCLE.SETTINGS.PAUSE_ON_REPLY_WINDOW')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label class="block text-sm">
|
||||||
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.OPT_OUT_LABEL') }}
|
||||||
|
<select
|
||||||
|
v-model="form.opt_out_label_id"
|
||||||
|
class="w-full border rounded px-2 py-1"
|
||||||
|
>
|
||||||
|
<option :value="null">—</option>
|
||||||
|
<option v-for="label in labels" :key="label.id" :value="label.id">
|
||||||
|
{{ label.title }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p class="text-xs text-n-slate-11">
|
||||||
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.MAX_PER_RESERVATION_INFO') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button :disabled="uiFlags.updating" @click="save">
|
||||||
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.SAVE') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3 class="text-base font-semibold mb-3">
|
||||||
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.CONCIERGE_TITLE') }}
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<ConciergeUnitCard v-for="u in units" :key="u.id" :unit="u" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -0,0 +1,144 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import CaptainUnitsAPI from 'dashboard/api/captain/units';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
unit: { type: Object, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const inboxes = useMapGetter('inboxes/getWhatsAppInboxes');
|
||||||
|
|
||||||
|
const expanded = ref(false);
|
||||||
|
const conciergeInboxId = ref(props.unit.concierge_inbox_id || null);
|
||||||
|
const personaName = ref(props.unit.concierge_config?.persona_name || 'Sofia');
|
||||||
|
const knowledge = ref(props.unit.concierge_config?.knowledge || '');
|
||||||
|
const variables = ref(
|
||||||
|
Object.entries(props.unit.concierge_config?.variables || {}).map(
|
||||||
|
([k, v]) => ({
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const addVariable = () => variables.value.push({ k: '', v: '' });
|
||||||
|
const removeVariable = i => variables.value.splice(i, 1);
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
const varsObj = Object.fromEntries(
|
||||||
|
variables.value.filter(x => x.k).map(x => [x.k, x.v])
|
||||||
|
);
|
||||||
|
await CaptainUnitsAPI.updateConcierge(props.unit.id, {
|
||||||
|
concierge_inbox_id: conciergeInboxId.value,
|
||||||
|
concierge_config: {
|
||||||
|
persona_name: personaName.value,
|
||||||
|
knowledge: knowledge.value,
|
||||||
|
variables: varsObj,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
useAlert(t('CAPTAIN_LIFECYCLE.SETTINGS.TOAST.CONCIERGE_SAVED'));
|
||||||
|
} catch (e) {
|
||||||
|
useAlert(e.message || 'Error saving concierge');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="border border-n-slate-4 rounded-lg p-4">
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center cursor-pointer"
|
||||||
|
@click="expanded = !expanded"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{{ unit.name }}</div>
|
||||||
|
<div class="text-xs text-n-slate-11">
|
||||||
|
{{
|
||||||
|
unit.concierge_inbox_id
|
||||||
|
? t('CAPTAIN_LIFECYCLE.SETTINGS.CONCIERGE_CONFIGURED')
|
||||||
|
: t('CAPTAIN_LIFECYCLE.SETTINGS.CONCIERGE_NOT_CONFIGURED')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>{{ expanded ? '▾' : '▸' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="expanded" class="mt-4 space-y-3">
|
||||||
|
<label class="block text-sm">
|
||||||
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.CONCIERGE_INBOX') }}
|
||||||
|
<select
|
||||||
|
v-model="conciergeInboxId"
|
||||||
|
class="w-full border rounded px-2 py-1"
|
||||||
|
>
|
||||||
|
<option :value="null">—</option>
|
||||||
|
<option v-for="ib in inboxes" :key="ib.id" :value="ib.id">
|
||||||
|
{{ ib.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
v-model="personaName"
|
||||||
|
:label="t('CAPTAIN_LIFECYCLE.SETTINGS.CONCIERGE_PERSONA')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label class="block text-sm">
|
||||||
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.CONCIERGE_KNOWLEDGE') }}
|
||||||
|
<textarea
|
||||||
|
v-model="knowledge"
|
||||||
|
rows="8"
|
||||||
|
class="w-full border rounded p-2 font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium mb-2">
|
||||||
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.CONCIERGE_VARIABLES') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(variable, i) in variables"
|
||||||
|
:key="i"
|
||||||
|
class="flex gap-2 mb-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="variable.k"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN_LIFECYCLE.SETTINGS.CONCIERGE_VARIABLE_KEY')
|
||||||
|
"
|
||||||
|
class="border rounded px-2 py-1 w-1/3"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="variable.v"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN_LIFECYCLE.SETTINGS.CONCIERGE_VARIABLE_VALUE')
|
||||||
|
"
|
||||||
|
class="border rounded px-2 py-1 flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
icon="i-lucide-x"
|
||||||
|
@click="removeVariable(i)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
:label="t('CAPTAIN_LIFECYCLE.SETTINGS.CONCIERGE_ADD_VARIABLE')"
|
||||||
|
@click="addVariable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button @click="save">
|
||||||
|
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.SAVE') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Loading…
Reference in New Issue
Block a user