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_VALUE": "Value",
|
||||
"CONCIERGE_ADD_VARIABLE": "Add variable",
|
||||
"CONCIERGE_CONFIGURED": "Configured",
|
||||
"CONCIERGE_NOT_CONFIGURED": "Not configured",
|
||||
"SAVE": "Save changes",
|
||||
"TOAST": {
|
||||
"SAVED": "Settings saved.",
|
||||
|
||||
@ -660,6 +660,8 @@
|
||||
"CONCIERGE_VARIABLE_KEY": "Chave",
|
||||
"CONCIERGE_VARIABLE_VALUE": "Valor",
|
||||
"CONCIERGE_ADD_VARIABLE": "Adicionar variável",
|
||||
"CONCIERGE_CONFIGURED": "Configurado",
|
||||
"CONCIERGE_NOT_CONFIGURED": "Não configurado",
|
||||
"SAVE": "Salvar alterações",
|
||||
"TOAST": {
|
||||
"SAVED": "Configurações salvas.",
|
||||
|
||||
@ -1,14 +1,143 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useStore, 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 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 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>
|
||||
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<h2 class="text-lg font-semibold">
|
||||
{{ t('CAPTAIN_LIFECYCLE.TABS.SETTINGS') }}
|
||||
</h2>
|
||||
<!-- Implementation in Task 14 -->
|
||||
<div class="p-6 space-y-8">
|
||||
<section>
|
||||
<h3 class="text-base font-semibold mb-3">
|
||||
{{ t('CAPTAIN_LIFECYCLE.SETTINGS.GUARDS_TITLE') }}
|
||||
</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>
|
||||
</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