DONE: layout letter with letterhead and footer, notification for newly...
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user