feat(lifecycle): history tab with paginated list and preview modal

Implements Task 13 — replaces the stub History.vue with a real paginated
table filtered by status, and adds DeliveryPreviewModal to show rendered_body.
Also extends i18n keys (TOTAL, PAGINATION, MODAL labels) in en + pt_BR.
This commit is contained in:
Rodribm10 2026-04-15 10:57:56 -03:00
parent ad2255aba4
commit ae4647d1c2
4 changed files with 220 additions and 6 deletions

View File

@ -690,9 +690,22 @@
"ALL": "All"
},
"PREVIEW": "Preview",
"TOTAL": "total",
"PAGINATION": {
"PREV": "Prev",
"NEXT": "Next"
},
"MODAL": {
"TITLE": "Message preview",
"CLOSE": "Close"
"CLOSE": "Close",
"RULE": "Rule",
"STATUS": "Status",
"REASON": "Reason",
"ERROR": "Error",
"FIRE_AT": "Fire at",
"SENT_AT": "Sent at",
"RENDERED": "Rendered",
"RESERVATION_ID": "Reservation #"
}
}
}

View File

@ -692,9 +692,22 @@
"ALL": "Todas"
},
"PREVIEW": "Preview",
"TOTAL": "total",
"PAGINATION": {
"PREV": "Anterior",
"NEXT": "Próxima"
},
"MODAL": {
"TITLE": "Preview da mensagem",
"CLOSE": "Fechar"
"CLOSE": "Fechar",
"RULE": "Regra",
"STATUS": "Status",
"REASON": "Motivo",
"ERROR": "Erro",
"FIRE_AT": "Fire at",
"SENT_AT": "Sent at",
"RENDERED": "Rendered",
"RESERVATION_ID": "Reserva #"
}
}
}

View File

@ -1,14 +1,135 @@
<script setup>
import { computed, onMounted, ref, watch } from 'vue';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n';
import Button from 'dashboard/components-next/button/Button.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import DeliveryPreviewModal from './components/DeliveryPreviewModal.vue';
const store = useStore();
const { t } = useI18n();
const deliveries = useMapGetter('captainLifecycleDeliveries/getRecords');
const meta = useMapGetter('captainLifecycleDeliveries/getMeta');
const uiFlags = useMapGetter('captainLifecycleDeliveries/getUIFlags');
const status = ref('');
const page = ref(1);
const selectedDelivery = ref(null);
const STATUS_OPTIONS = [
{ value: '', key: 'ALL' },
{ value: 'scheduled', key: 'SCHEDULED' },
{ value: 'sent', key: 'SENT' },
{ value: 'skipped', key: 'SKIPPED' },
{ value: 'failed', key: 'FAILED' },
{ value: 'cancelled', key: 'CANCELLED' },
];
const fetchDeliveries = () => {
store.dispatch('captainLifecycleDeliveries/get', {
page: page.value,
...(status.value ? { status: status.value } : {}),
});
};
onMounted(fetchDeliveries);
watch([status, page], fetchDeliveries);
const isLoading = computed(() => uiFlags.value.fetchingList);
const totalCount = computed(() => meta.value.total_count || 0);
</script>
<template>
<div class="p-6">
<h2 class="text-lg font-semibold">
{{ t('CAPTAIN_LIFECYCLE.TABS.HISTORY') }}
</h2>
<!-- Implementation in Task 13 -->
<div class="flex items-center gap-4 mb-4">
<label class="flex items-center gap-2 text-sm">
{{ t('CAPTAIN_LIFECYCLE.HISTORY.FILTERS.STATUS') }}:
<select v-model="status" class="border rounded px-2 py-1">
<option
v-for="opt in STATUS_OPTIONS"
:key="opt.value"
:value="opt.value"
>
{{
opt.value
? t(`CAPTAIN_LIFECYCLE.HISTORY.STATUS.${opt.key}`)
: t('CAPTAIN_LIFECYCLE.HISTORY.FILTERS.ALL')
}}
</option>
</select>
</label>
<span class="text-sm text-n-slate-11">
{{ totalCount }} {{ t('CAPTAIN_LIFECYCLE.HISTORY.TOTAL') }}
</span>
</div>
<div v-if="isLoading" class="flex justify-center py-8">
<Spinner />
</div>
<div
v-else-if="deliveries.length === 0"
class="text-center py-8 text-n-slate-11"
>
{{ t('CAPTAIN_LIFECYCLE.HISTORY.EMPTY') }}
</div>
<table v-else class="w-full text-sm">
<thead class="text-left text-n-slate-11">
<tr>
<th class="py-2">
{{ t('CAPTAIN_LIFECYCLE.HISTORY.COLUMNS.RULE') }}
</th>
<th>{{ t('CAPTAIN_LIFECYCLE.HISTORY.COLUMNS.CUSTOMER') }}</th>
<th>{{ t('CAPTAIN_LIFECYCLE.HISTORY.COLUMNS.RESERVATION') }}</th>
<th>{{ t('CAPTAIN_LIFECYCLE.HISTORY.COLUMNS.STATUS') }}</th>
<th>{{ t('CAPTAIN_LIFECYCLE.HISTORY.COLUMNS.FIRE_AT') }}</th>
<th>{{ t('CAPTAIN_LIFECYCLE.HISTORY.COLUMNS.REASON') }}</th>
<th />
</tr>
</thead>
<tbody>
<tr
v-for="d in deliveries"
:key="d.id"
class="border-t border-n-slate-4"
>
<td class="py-2">{{ d.lifecycle_rule_name || '—' }}</td>
<td>{{ d.reservation?.customer_name || '—' }}</td>
<td>
{{ t('CAPTAIN_LIFECYCLE.HISTORY.MODAL.RESERVATION_ID')
}}{{ d.captain_reservation_id }}
</td>
<td>
{{
t(`CAPTAIN_LIFECYCLE.HISTORY.STATUS.${d.status.toUpperCase()}`)
}}
</td>
<td>{{ new Date(d.fire_at).toLocaleString('pt-BR') }}</td>
<td>{{ d.skip_reason || d.failure_reason || '' }}</td>
<td>
<Button size="sm" variant="ghost" @click="selectedDelivery = d">
{{ t('CAPTAIN_LIFECYCLE.HISTORY.PREVIEW') }}
</Button>
</td>
</tr>
</tbody>
</table>
<div class="flex justify-center gap-2 mt-4">
<Button :disabled="page <= 1" @click="page -= 1">
{{ t('CAPTAIN_LIFECYCLE.HISTORY.PAGINATION.PREV') }}
</Button>
<span class="text-sm self-center">{{ page }}</span>
<Button :disabled="deliveries.length < 25" @click="page += 1">
{{ t('CAPTAIN_LIFECYCLE.HISTORY.PAGINATION.NEXT') }}
</Button>
</div>
<DeliveryPreviewModal
:delivery="selectedDelivery"
@close="selectedDelivery = null"
/>
</div>
</template>

View File

@ -0,0 +1,67 @@
<script setup>
import { useI18n } from 'vue-i18n';
import Button from 'dashboard/components-next/button/Button.vue';
defineProps({
delivery: { type: Object, default: null },
});
const emit = defineEmits(['close']);
const { t } = useI18n();
</script>
<template>
<Teleport to="body">
<div
v-if="delivery"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
@click.self="emit('close')"
>
<div
class="bg-n-solid-1 rounded-xl p-6 w-[560px] max-h-[80vh] overflow-auto shadow-xl"
>
<h3 class="text-lg font-semibold mb-4">
{{ t('CAPTAIN_LIFECYCLE.HISTORY.MODAL.TITLE') }}
</h3>
<div class="space-y-3 text-sm">
<div>
<strong>{{ t('CAPTAIN_LIFECYCLE.HISTORY.MODAL.RULE') }}:</strong>
{{ delivery.lifecycle_rule_name || '—' }}
</div>
<div>
<strong>{{ t('CAPTAIN_LIFECYCLE.HISTORY.MODAL.STATUS') }}:</strong>
{{ delivery.status }}
</div>
<div v-if="delivery.skip_reason">
<strong>{{ t('CAPTAIN_LIFECYCLE.HISTORY.MODAL.REASON') }}:</strong>
{{ delivery.skip_reason }}
</div>
<div v-if="delivery.failure_reason">
<strong>{{ t('CAPTAIN_LIFECYCLE.HISTORY.MODAL.ERROR') }}:</strong>
{{ delivery.failure_reason }}
</div>
<div>
<strong>{{ t('CAPTAIN_LIFECYCLE.HISTORY.MODAL.FIRE_AT') }}:</strong>
{{ delivery.fire_at }}
</div>
<div v-if="delivery.sent_at">
<strong>{{ t('CAPTAIN_LIFECYCLE.HISTORY.MODAL.SENT_AT') }}:</strong>
{{ delivery.sent_at }}
</div>
<div>
<strong
>{{ t('CAPTAIN_LIFECYCLE.HISTORY.MODAL.RENDERED') }}:</strong
>
<pre class="mt-1 p-3 bg-n-alpha-2 rounded whitespace-pre-wrap">{{
delivery.rendered_body || '—'
}}</pre>
</div>
</div>
<div class="flex justify-end mt-6">
<Button variant="outline" @click="emit('close')">
{{ t('CAPTAIN_LIFECYCLE.HISTORY.MODAL.CLOSE') }}
</Button>
</div>
</div>
</div>
</Teleport>
</template>