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:
parent
ad2255aba4
commit
ae4647d1c2
@ -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 #"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 #"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user