329 lines
9.7 KiB
Vue
329 lines
9.7 KiB
Vue
<script lang="ts" setup>
|
|
import { computed, ref } from 'vue'
|
|
import dayjs from 'dayjs'
|
|
import { Box } from '@/components/ui/box'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
DialogRoot,
|
|
DialogContent,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogCloseTrigger,
|
|
} from '@/components/ui/dialog'
|
|
import { Field, FieldLabel, FieldError } from '@/components/ui/field'
|
|
import { Lucide } from '@/components/ui/lucide'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import { usePermissions } from '@/composables/usePermissions'
|
|
import { getApiErrorMessage, getApiValidationErrors } from '@/core/utils/getApiErrorMessage'
|
|
import { createActivityReport, deleteActivityReport, updateActivityReport } from '../services/activity.service'
|
|
import type { ActivityReport } from '../types/activity.types'
|
|
|
|
type ReportModalMode = 'create' | 'edit'
|
|
|
|
const props = defineProps<{
|
|
activityId: string
|
|
reports: ActivityReport[]
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
changed: []
|
|
}>()
|
|
|
|
const { hasPermission } = usePermissions()
|
|
|
|
const canCreateReport = computed(() => hasPermission('tambah laporan aktiviti'))
|
|
const canEditReport = computed(() => hasPermission('kemaskini laporan aktiviti'))
|
|
const canDeleteReport = computed(() => hasPermission('hapus laporan aktiviti'))
|
|
|
|
const reportModalOpen = ref(false)
|
|
const reportModalMode = ref<ReportModalMode>('create')
|
|
const editingReportId = ref<string | null>(null)
|
|
const reportText = ref('')
|
|
const reportFieldError = ref<string | null>(null)
|
|
const savingReport = ref(false)
|
|
|
|
const deleteConfirmationOpen = ref(false)
|
|
const reportToDelete = ref<ActivityReport | null>(null)
|
|
const deletingReport = ref(false)
|
|
const deleteError = ref<string | null>(null)
|
|
|
|
const isEditMode = computed(() => reportModalMode.value === 'edit')
|
|
|
|
const reportModalTitle = computed(() =>
|
|
isEditMode.value ? 'Kemaskini Laporan Aktiviti' : 'Tambah Laporan Aktiviti',
|
|
)
|
|
|
|
const reportModalDescription = computed(() =>
|
|
isEditMode.value
|
|
? 'Kemaskini kandungan laporan aktiviti ini.'
|
|
: 'Rekod laporan untuk aktiviti ini. Laporan akan dikaitkan dengan akaun anda.',
|
|
)
|
|
|
|
const reportSubmitLabel = computed(() => {
|
|
if (savingReport.value) return 'Menyimpan...'
|
|
return isEditMode.value ? 'Simpan Perubahan' : 'Simpan Laporan'
|
|
})
|
|
|
|
function formatDateTime(value: string | null): string {
|
|
if (!value) return '-'
|
|
return dayjs(value).format('DD MMM YYYY, HH:mm')
|
|
}
|
|
|
|
function resetReportForm() {
|
|
reportText.value = ''
|
|
reportFieldError.value = null
|
|
editingReportId.value = null
|
|
reportModalMode.value = 'create'
|
|
}
|
|
|
|
function openCreateReportModal() {
|
|
resetReportForm()
|
|
reportModalMode.value = 'create'
|
|
reportModalOpen.value = true
|
|
}
|
|
|
|
function openEditReportModal(report: ActivityReport) {
|
|
reportFieldError.value = null
|
|
reportModalMode.value = 'edit'
|
|
editingReportId.value = report.id
|
|
reportText.value = report.report_text
|
|
reportModalOpen.value = true
|
|
}
|
|
|
|
function handleReportModalOpenChange(details: { open: boolean }) {
|
|
if (savingReport.value && !details.open) {
|
|
reportModalOpen.value = true
|
|
return
|
|
}
|
|
|
|
reportModalOpen.value = details.open
|
|
|
|
if (!details.open) {
|
|
resetReportForm()
|
|
}
|
|
}
|
|
|
|
async function handleSubmitReport() {
|
|
if (savingReport.value) return
|
|
|
|
const trimmedText = reportText.value.trim()
|
|
if (!trimmedText) {
|
|
reportFieldError.value = 'Laporan diperlukan.'
|
|
return
|
|
}
|
|
|
|
savingReport.value = true
|
|
reportFieldError.value = null
|
|
|
|
try {
|
|
if (isEditMode.value && editingReportId.value) {
|
|
await updateActivityReport(editingReportId.value, {
|
|
activity_id: props.activityId,
|
|
report_text: trimmedText,
|
|
})
|
|
} else {
|
|
await createActivityReport({
|
|
activity_id: props.activityId,
|
|
report_text: trimmedText,
|
|
})
|
|
}
|
|
|
|
reportModalOpen.value = false
|
|
resetReportForm()
|
|
emit('changed')
|
|
} catch (err) {
|
|
const validationErrors = getApiValidationErrors(err)
|
|
if (validationErrors?.report_text?.[0]) {
|
|
reportFieldError.value = validationErrors.report_text[0]
|
|
} else {
|
|
reportFieldError.value = getApiErrorMessage(
|
|
err,
|
|
isEditMode.value
|
|
? 'Gagal mengemaskini laporan aktiviti.'
|
|
: 'Gagal menambah laporan aktiviti.',
|
|
)
|
|
}
|
|
} finally {
|
|
savingReport.value = false
|
|
}
|
|
}
|
|
|
|
function openDeleteConfirmation(report: ActivityReport) {
|
|
reportToDelete.value = report
|
|
deleteError.value = null
|
|
deleteConfirmationOpen.value = true
|
|
}
|
|
|
|
function handleDeleteConfirmationOpenChange(details: { open: boolean }) {
|
|
if (deletingReport.value && !details.open) {
|
|
deleteConfirmationOpen.value = true
|
|
return
|
|
}
|
|
|
|
deleteConfirmationOpen.value = details.open
|
|
|
|
if (!details.open) {
|
|
reportToDelete.value = null
|
|
deleteError.value = null
|
|
}
|
|
}
|
|
|
|
async function confirmDeleteReport() {
|
|
if (!reportToDelete.value || deletingReport.value) return
|
|
|
|
deletingReport.value = true
|
|
deleteError.value = null
|
|
|
|
try {
|
|
await deleteActivityReport(reportToDelete.value.id)
|
|
deleteConfirmationOpen.value = false
|
|
reportToDelete.value = null
|
|
emit('changed')
|
|
} catch (err) {
|
|
deleteError.value = getApiErrorMessage(err, 'Gagal memadam laporan aktiviti.')
|
|
} finally {
|
|
deletingReport.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Box class="p-5 sm:p-6">
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<div class="font-medium">Laporan Aktiviti</div>
|
|
<p class="mt-1 text-sm opacity-70">{{ reports.length }} laporan</p>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<Button
|
|
v-if="canCreateReport"
|
|
type="button"
|
|
look="outline"
|
|
size="sm"
|
|
@click="openCreateReportModal"
|
|
>
|
|
<Lucide class="mr-2 size-4" icon="Plus" />
|
|
Tambah Laporan
|
|
</Button>
|
|
<Lucide class="size-5 opacity-50" icon="FileText" />
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!reports.length" class="mt-5 opacity-70">Tiada laporan aktiviti.</div>
|
|
|
|
<div v-else class="mt-5 space-y-4">
|
|
<div
|
|
v-for="report in reports"
|
|
:key="report.id"
|
|
class="rounded-lg border border-foreground/10 p-4"
|
|
>
|
|
<div
|
|
class="flex flex-wrap items-start justify-between gap-3 border-b border-foreground/10 pb-3"
|
|
>
|
|
<div class="min-w-0">
|
|
<div class="text-sm font-medium">
|
|
{{ report.prepared_by_user?.name ?? 'Pengguna' }}
|
|
</div>
|
|
<div v-if="report.prepared_by_user?.email" class="mt-0.5 text-xs opacity-70">
|
|
{{ report.prepared_by_user.email }}
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="text-xs opacity-70">
|
|
{{ formatDateTime(report.created_at) }}
|
|
</div>
|
|
<Button
|
|
v-if="canEditReport"
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
class="size-8 p-0"
|
|
title="Kemaskini laporan"
|
|
@click="openEditReportModal(report)"
|
|
>
|
|
<Lucide class="size-4 opacity-70" icon="Pencil" />
|
|
</Button>
|
|
<Button
|
|
v-if="canDeleteReport"
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
class="size-8 p-0"
|
|
title="Padam laporan"
|
|
@click="openDeleteConfirmation(report)"
|
|
>
|
|
<Lucide class="size-4 text-danger opacity-80" icon="Trash2" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<p class="mt-3 whitespace-pre-line text-sm leading-relaxed">
|
|
{{ report.report_text }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Box>
|
|
|
|
<DialogRoot :open="reportModalOpen" @openChange="handleReportModalOpenChange">
|
|
<DialogContent class="max-w-lg">
|
|
<DialogTitle>{{ reportModalTitle }}</DialogTitle>
|
|
<DialogDescription>{{ reportModalDescription }}</DialogDescription>
|
|
|
|
<form class="mt-4 space-y-4" @submit.prevent="handleSubmitReport">
|
|
<Field>
|
|
<FieldLabel for="activity-report-text">Laporan</FieldLabel>
|
|
<Textarea
|
|
id="activity-report-text"
|
|
v-model="reportText"
|
|
rows="6"
|
|
placeholder="Tulis laporan aktiviti..."
|
|
:disabled="savingReport"
|
|
required
|
|
/>
|
|
<FieldError v-if="reportFieldError">{{ reportFieldError }}</FieldError>
|
|
</Field>
|
|
|
|
<div class="flex justify-end gap-2 pt-2">
|
|
<DialogCloseTrigger look="outline" variant="secondary" :disabled="savingReport">
|
|
Batal
|
|
</DialogCloseTrigger>
|
|
<Button type="submit" variant="primary" :disabled="savingReport">
|
|
{{ reportSubmitLabel }}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</DialogRoot>
|
|
|
|
<DialogRoot :open="deleteConfirmationOpen" @openChange="handleDeleteConfirmationOpenChange">
|
|
<DialogContent>
|
|
<div class="p-5 text-center">
|
|
<Lucide class="text-danger mx-auto mt-3 size-16 stroke-1" icon="CircleX" />
|
|
<div class="mt-5 text-2xl font-medium">Padam Laporan Aktiviti?</div>
|
|
<div class="mt-2 opacity-70">
|
|
Adakah anda benar-benar mahu memadam laporan ini?
|
|
<br />
|
|
Proses ini tidak boleh dibatalkan.
|
|
</div>
|
|
<div v-if="deleteError" class="mt-4 text-sm text-danger">
|
|
{{ deleteError }}
|
|
</div>
|
|
</div>
|
|
<div class="px-5 pb-8 text-center">
|
|
<DialogCloseTrigger class="mr-2 w-24" :disabled="deletingReport">
|
|
Batal
|
|
</DialogCloseTrigger>
|
|
<Button
|
|
class="w-24"
|
|
type="button"
|
|
variant="danger"
|
|
look="outline"
|
|
:disabled="deletingReport"
|
|
@click="confirmDeleteReport"
|
|
>
|
|
{{ deletingReport ? 'Memadam...' : 'Padam' }}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</DialogRoot>
|
|
</template>
|