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"
|
"ALL": "All"
|
||||||
},
|
},
|
||||||
"PREVIEW": "Preview",
|
"PREVIEW": "Preview",
|
||||||
|
"TOTAL": "total",
|
||||||
|
"PAGINATION": {
|
||||||
|
"PREV": "Prev",
|
||||||
|
"NEXT": "Next"
|
||||||
|
},
|
||||||
"MODAL": {
|
"MODAL": {
|
||||||
"TITLE": "Message preview",
|
"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"
|
"ALL": "Todas"
|
||||||
},
|
},
|
||||||
"PREVIEW": "Preview",
|
"PREVIEW": "Preview",
|
||||||
|
"TOTAL": "total",
|
||||||
|
"PAGINATION": {
|
||||||
|
"PREV": "Anterior",
|
||||||
|
"NEXT": "Próxima"
|
||||||
|
},
|
||||||
"MODAL": {
|
"MODAL": {
|
||||||
"TITLE": "Preview da mensagem",
|
"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>
|
<script setup>
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useI18n } from 'vue-i18n';
|
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 { 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h2 class="text-lg font-semibold">
|
<div class="flex items-center gap-4 mb-4">
|
||||||
{{ t('CAPTAIN_LIFECYCLE.TABS.HISTORY') }}
|
<label class="flex items-center gap-2 text-sm">
|
||||||
</h2>
|
{{ t('CAPTAIN_LIFECYCLE.HISTORY.FILTERS.STATUS') }}:
|
||||||
<!-- Implementation in Task 13 -->
|
<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>
|
</div>
|
||||||
</template>
|
</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