Dev/v1.1 (#2)
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local> Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { CircleAlert, CircleCheck, Search, Eye, Pencil } from '@lucide/vue'
|
||||
import { CircleAlert, CircleCheck, Search, Eye, Pencil, FileText, Download } from '@lucide/vue'
|
||||
import dayjs from 'dayjs'
|
||||
import * as select from '@zag-js/select'
|
||||
import {
|
||||
@@ -26,16 +26,27 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DialogRoot, DialogContent, DialogCloseTrigger } from '@/components/ui/dialog'
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
|
||||
import DataTable from '@/components/ui/usage/DataTable.vue'
|
||||
import type { TableHeader } from '@/components/ui/usage/DataTable.vue'
|
||||
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
|
||||
import { getApiErrorMessage, getApiValidationErrors } from '@/core/utils/getApiErrorMessage'
|
||||
import axios from 'axios'
|
||||
import { useMembershipApplicationList } from '../composables/useMembershipApplicationList'
|
||||
import { usePermissions } from '@/composables/usePermissions'
|
||||
import { batchCompleteMembershipApplications } from '../services/membership-application.service'
|
||||
import {
|
||||
batchCompleteMembershipApplications,
|
||||
downloadMembershipApplicationDocument,
|
||||
generateMembershipApplicationResultLetter,
|
||||
} from '../services/membership-application.service'
|
||||
import {
|
||||
boardResultBadgeVariant,
|
||||
statusBadgeLook,
|
||||
statusBadgeVariant,
|
||||
} from '../utils/membership-application-badge.utils'
|
||||
import type {
|
||||
BatchCompleteFailedItem,
|
||||
BatchCompleteResponse,
|
||||
GenerateResultLetterResponse,
|
||||
MembershipApplicationBoardResult,
|
||||
MembershipApplicationListItem,
|
||||
MembershipApplicationStatus,
|
||||
@@ -94,11 +105,19 @@ const {
|
||||
} = useMembershipApplicationList()
|
||||
|
||||
const canBatchComplete = computed(() => hasPermission('selesaikan permohonan keahlian'))
|
||||
const canGenerateResultLetter = computed(() => hasPermission('jana surat keputusan keahlian'))
|
||||
const selectedIds = ref<string[]>([])
|
||||
const batchSubmitting = ref(false)
|
||||
const batchConfirmOpen = ref(false)
|
||||
const batchSuccessMessage = ref<string | null>(null)
|
||||
const letterSuccessMessage = ref<string | null>(null)
|
||||
const batchFailedItems = ref<BatchCompleteFailedItem[]>([])
|
||||
const generateDialogOpen = ref(false)
|
||||
const generateSubmitting = ref(false)
|
||||
const boardMeetingReference = ref('')
|
||||
const boardMeetingReferenceError = ref<string | null>(null)
|
||||
const generateTarget = ref<MembershipApplicationListItem | null>(null)
|
||||
const downloadingResultLetterId = ref<string | null>(null)
|
||||
|
||||
const selectableApplications = computed(() =>
|
||||
applications.value.filter((item) => item.status === 'PENDING_NOTIFICATION'),
|
||||
@@ -203,25 +222,12 @@ function statusLabel(status: MembershipApplicationStatus): string {
|
||||
return labels[status] ?? status
|
||||
}
|
||||
|
||||
function statusBadgeVariant(status: MembershipApplicationStatus) {
|
||||
if (status === 'COMPLETED') return 'success'
|
||||
if (status === 'MANAGEMENT_REJECTED') return 'danger'
|
||||
if (status === 'PENDING_BOARD' || status === 'PENDING_NOTIFICATION') return 'pending'
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
function boardResultLabel(result: MembershipApplicationBoardResult | null): string {
|
||||
if (result === 'PASS') return 'Lulus'
|
||||
if (result === 'FAIL') return 'Gagal'
|
||||
return '-'
|
||||
}
|
||||
|
||||
function boardResultBadgeVariant(result: MembershipApplicationBoardResult | null) {
|
||||
if (result === 'PASS') return 'success'
|
||||
if (result === 'FAIL') return 'danger'
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
function formatSubmittedAt(value: string | null): string {
|
||||
if (!value) return '-'
|
||||
return dayjs(value).format('DD/MM/YYYY HH:mm')
|
||||
@@ -235,47 +241,131 @@ function goToApplicationEdit(id: string) {
|
||||
router.push({ name: 'edit-membership-application', params: { id } })
|
||||
}
|
||||
|
||||
function canGenerateLetter(item: MembershipApplicationListItem): boolean {
|
||||
return (
|
||||
canGenerateResultLetter.value &&
|
||||
item.status === 'COMPLETED' &&
|
||||
!item.has_result_letter &&
|
||||
(item.board_result === 'PASS' || item.board_result === 'FAIL')
|
||||
)
|
||||
}
|
||||
|
||||
function canDownloadLetter(item: MembershipApplicationListItem): boolean {
|
||||
return !!item.has_result_letter && !!item.result_letter_document
|
||||
}
|
||||
|
||||
function openGenerateDialog(item: MembershipApplicationListItem) {
|
||||
generateTarget.value = item
|
||||
boardMeetingReference.value = ''
|
||||
boardMeetingReferenceError.value = null
|
||||
generateDialogOpen.value = true
|
||||
}
|
||||
|
||||
function closeGenerateDialog() {
|
||||
if (generateSubmitting.value) return
|
||||
generateDialogOpen.value = false
|
||||
generateTarget.value = null
|
||||
boardMeetingReference.value = ''
|
||||
boardMeetingReferenceError.value = null
|
||||
}
|
||||
|
||||
async function confirmGenerateLetter() {
|
||||
if (!generateTarget.value || generateSubmitting.value) return
|
||||
|
||||
const reference = boardMeetingReference.value.trim()
|
||||
if (!reference) {
|
||||
boardMeetingReferenceError.value = 'Rujukan mesyuarat lembaga diperlukan.'
|
||||
return
|
||||
}
|
||||
|
||||
generateSubmitting.value = true
|
||||
boardMeetingReferenceError.value = null
|
||||
error.value = null
|
||||
letterSuccessMessage.value = null
|
||||
|
||||
try {
|
||||
const response = await generateMembershipApplicationResultLetter(generateTarget.value.id, {
|
||||
board_meeting_reference: reference,
|
||||
})
|
||||
|
||||
letterSuccessMessage.value = response.message
|
||||
closeGenerateDialog()
|
||||
await fetchApplications(page.value)
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err) && err.response?.data) {
|
||||
const responseData = err.response.data as GenerateResultLetterResponse
|
||||
const validationErrors = getApiValidationErrors(err)
|
||||
boardMeetingReferenceError.value = validationErrors?.board_meeting_reference?.[0] ?? null
|
||||
error.value = responseData.message ?? getApiErrorMessage(err, 'Gagal menjana surat keputusan.')
|
||||
} else {
|
||||
error.value = getApiErrorMessage(err, 'Gagal menjana surat keputusan.')
|
||||
}
|
||||
} finally {
|
||||
generateSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDownloadResultLetter(item: MembershipApplicationListItem) {
|
||||
const document = item.result_letter_document
|
||||
if (!document) return
|
||||
|
||||
downloadingResultLetterId.value = item.id
|
||||
|
||||
try {
|
||||
await downloadMembershipApplicationDocument(
|
||||
item.id,
|
||||
document.id,
|
||||
document.name,
|
||||
document.mime_type,
|
||||
)
|
||||
} catch (err) {
|
||||
error.value = getApiErrorMessage(err, 'Gagal memuat turun surat keputusan.')
|
||||
} finally {
|
||||
downloadingResultLetterId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const headers = computed<TableHeader[]>(() => {
|
||||
const base: TableHeader[] = [
|
||||
{ title: 'Bil.', key: '#', sortable: false },
|
||||
{ title: 'No. Permohonan', key: 'application_number', sortable: true },
|
||||
{
|
||||
title: 'Nama Pemohon',
|
||||
key: 'applicant_name',
|
||||
sortable: false,
|
||||
exportValue: (item) => item.applicant?.name ?? '',
|
||||
},
|
||||
{
|
||||
title: 'Emel',
|
||||
key: 'applicant_email',
|
||||
sortable: false,
|
||||
exportValue: (item) => item.applicant?.email ?? '',
|
||||
},
|
||||
{
|
||||
title: 'No. IC',
|
||||
key: 'applicant_ic_number',
|
||||
sortable: false,
|
||||
exportValue: (item) => item.applicant?.ic_number ?? '',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
sortable: true,
|
||||
exportValue: (item) => statusLabel(item.status),
|
||||
},
|
||||
{
|
||||
title: 'Keputusan Lembaga',
|
||||
key: 'board_result',
|
||||
sortable: false,
|
||||
exportValue: (item) => boardResultLabel(item.board_result),
|
||||
},
|
||||
{
|
||||
title: 'Tarikh Hantar',
|
||||
key: 'submitted_at',
|
||||
sortable: true,
|
||||
exportValue: (item) => formatSubmittedAt(item.submitted_at),
|
||||
},
|
||||
{ title: 'Tindakan', key: 'actions', sortable: false },
|
||||
{ title: 'No. Permohonan', key: 'application_number', sortable: true },
|
||||
{
|
||||
title: 'Nama Pemohon',
|
||||
key: 'applicant_name',
|
||||
sortable: false,
|
||||
exportValue: (item) => item.applicant?.name ?? '',
|
||||
},
|
||||
{
|
||||
title: 'Emel',
|
||||
key: 'applicant_email',
|
||||
sortable: false,
|
||||
exportValue: (item) => item.applicant?.email ?? '',
|
||||
},
|
||||
{
|
||||
title: 'No. IC',
|
||||
key: 'applicant_ic_number',
|
||||
sortable: false,
|
||||
exportValue: (item) => item.applicant?.ic_number ?? '',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
sortable: true,
|
||||
exportValue: (item) => statusLabel(item.status),
|
||||
},
|
||||
{
|
||||
title: 'Keputusan Lembaga',
|
||||
key: 'board_result',
|
||||
sortable: false,
|
||||
exportValue: (item) => boardResultLabel(item.board_result),
|
||||
},
|
||||
{
|
||||
title: 'Tarikh Hantar',
|
||||
key: 'submitted_at',
|
||||
sortable: true,
|
||||
exportValue: (item) => formatSubmittedAt(item.submitted_at),
|
||||
},
|
||||
{ title: 'Tindakan', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
if (canBatchComplete.value) {
|
||||
@@ -294,6 +384,13 @@ const headers = computed<TableHeader[]>(() => {
|
||||
<p class="mt-1 text-sm opacity-70">Urus dan semak permohonan keahlian koperasi.</p>
|
||||
</div>
|
||||
|
||||
<AlertRoot v-if="letterSuccessMessage" variant="success">
|
||||
<CircleCheck />
|
||||
<AlertTitle>Berjaya</AlertTitle>
|
||||
<AlertDescription>{{ letterSuccessMessage }}</AlertDescription>
|
||||
<AlertCloseTrigger @click="letterSuccessMessage = null" />
|
||||
</AlertRoot>
|
||||
|
||||
<AlertRoot v-if="batchSuccessMessage" variant="success">
|
||||
<CircleCheck />
|
||||
<AlertTitle>Berjaya</AlertTitle>
|
||||
@@ -321,41 +418,20 @@ const headers = computed<TableHeader[]>(() => {
|
||||
<AlertCloseTrigger @click="error = null" />
|
||||
</AlertRoot>
|
||||
|
||||
<DataTable
|
||||
:headers="headers"
|
||||
:items="applications"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:current-sort="sortBy"
|
||||
show-pagination
|
||||
exportable
|
||||
export-file-name="permohonan-keahlian"
|
||||
v-model:page="page"
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
@update:sort-by="handleSortUpdate"
|
||||
>
|
||||
<DataTable :headers="headers" :items="applications" :loading="loading" :pagination="pagination"
|
||||
:current-sort="sortBy" show-pagination exportable export-file-name="permohonan-keahlian" v-model:page="page"
|
||||
v-model:items-per-page="itemsPerPage" @update:sort-by="handleSortUpdate">
|
||||
<template #toolbar>
|
||||
<div class="flex w-full flex-wrap items-center gap-3">
|
||||
<div class="relative w-full max-w-md flex-1">
|
||||
<Search
|
||||
class="pointer-events-none absolute top-1/2 left-3 z-10 size-4 -translate-y-1/2 text-foreground/50"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Input
|
||||
v-model="search"
|
||||
type="search"
|
||||
placeholder="Cari no. permohonan, nama, emel, IC..."
|
||||
class="w-full pl-9"
|
||||
aria-label="Cari permohonan keahlian"
|
||||
/>
|
||||
<Search class="pointer-events-none absolute top-1/2 left-3 z-10 size-4 -translate-y-1/2 text-foreground/50"
|
||||
aria-hidden="true" />
|
||||
<Input v-model="search" type="search" placeholder="Cari no. permohonan, nama, emel, IC..."
|
||||
class="w-full pl-9" aria-label="Cari permohonan keahlian" />
|
||||
</div>
|
||||
|
||||
<SelectRoot
|
||||
class="w-full sm:w-56"
|
||||
:collection="statusFilterCollection"
|
||||
:default-value="statusFilterInitial"
|
||||
@value-change="setStatusFilterValue"
|
||||
>
|
||||
<SelectRoot class="w-full sm:w-56" :collection="statusFilterCollection" :default-value="statusFilterInitial"
|
||||
@value-change="setStatusFilterValue">
|
||||
<SelectControl>
|
||||
<SelectTrigger aria-label="Tapis status">
|
||||
<SelectValueText placeholder="Semua Status" />
|
||||
@@ -364,11 +440,7 @@ const headers = computed<TableHeader[]>(() => {
|
||||
<SelectContent>
|
||||
<SelectItemGroup>
|
||||
<SelectItemGroupLabel>Status</SelectItemGroupLabel>
|
||||
<SelectItem
|
||||
v-for="item in statusFilterCollection.items"
|
||||
:key="item.label"
|
||||
:item="item"
|
||||
>
|
||||
<SelectItem v-for="item in statusFilterCollection.items" :key="item.label" :item="item">
|
||||
<SelectItemText>{{ item.label }}</SelectItemText>
|
||||
</SelectItem>
|
||||
</SelectItemGroup>
|
||||
@@ -376,21 +448,13 @@ const headers = computed<TableHeader[]>(() => {
|
||||
</SelectRoot>
|
||||
|
||||
<template v-if="canBatchComplete">
|
||||
<Button
|
||||
type="button"
|
||||
look="outline"
|
||||
variant="secondary"
|
||||
<Button type="button" look="outline" variant="secondary"
|
||||
:disabled="!selectableApplications.length || loading"
|
||||
@click="toggleSelectAllOnPage(!allSelectableSelected)"
|
||||
>
|
||||
@click="toggleSelectAllOnPage(!allSelectableSelected)">
|
||||
{{ allSelectableSelected ? 'Nyahpilih Halaman' : 'Pilih Halaman' }}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
:disabled="!selectedIds.length || loading || batchSubmitting"
|
||||
@click="openBatchConfirm"
|
||||
>
|
||||
<Button type="button" variant="primary" :disabled="!selectedIds.length || loading || batchSubmitting"
|
||||
@click="openBatchConfirm">
|
||||
Selesaikan Terpilih ({{ selectedIds.length }})
|
||||
</Button>
|
||||
</template>
|
||||
@@ -398,11 +462,9 @@ const headers = computed<TableHeader[]>(() => {
|
||||
</template>
|
||||
|
||||
<template v-if="canBatchComplete" #item.select="{ item }">
|
||||
<CheckboxRoot
|
||||
v-if="isSelectable(item as MembershipApplicationListItem)"
|
||||
<CheckboxRoot v-if="isSelectable(item as MembershipApplicationListItem)"
|
||||
:checked="isSelected((item as MembershipApplicationListItem).id)"
|
||||
@checked-change="({ checked }) => toggleSelection((item as MembershipApplicationListItem).id)"
|
||||
>
|
||||
@checked-change="({ checked }) => toggleSelection((item as MembershipApplicationListItem).id)">
|
||||
<CheckboxControl />
|
||||
</CheckboxRoot>
|
||||
</template>
|
||||
@@ -422,6 +484,7 @@ const headers = computed<TableHeader[]>(() => {
|
||||
<template #item.status="{ item }">
|
||||
<Badge
|
||||
:variant="statusBadgeVariant((item as MembershipApplicationListItem).status)"
|
||||
:look="statusBadgeLook((item as MembershipApplicationListItem).status)"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
{{ statusLabel((item as MembershipApplicationListItem).status) }}
|
||||
@@ -429,11 +492,9 @@ const headers = computed<TableHeader[]>(() => {
|
||||
</template>
|
||||
|
||||
<template #item.board_result="{ item }">
|
||||
<Badge
|
||||
v-if="(item as MembershipApplicationListItem).board_result"
|
||||
<Badge v-if="(item as MembershipApplicationListItem).board_result"
|
||||
:variant="boardResultBadgeVariant((item as MembershipApplicationListItem).board_result)"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
class="whitespace-nowrap">
|
||||
{{ boardResultLabel((item as MembershipApplicationListItem).board_result) }}
|
||||
</Badge>
|
||||
<span v-else class="opacity-50">-</span>
|
||||
@@ -445,36 +506,44 @@ const headers = computed<TableHeader[]>(() => {
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
v-if="hasPermission('lihat permohonan keahlian')"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="bg-green-600 text-white"
|
||||
title="Lihat butiran permohonan"
|
||||
@click="goToApplicationDetail((item as MembershipApplicationListItem).id)"
|
||||
>
|
||||
<Button v-if="hasPermission('lihat permohonan keahlian')" type="button" variant="ghost" size="sm"
|
||||
class="bg-green-600 text-white" title="Lihat butiran permohonan"
|
||||
@click="goToApplicationDetail((item as MembershipApplicationListItem).id)">
|
||||
<Eye class="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="hasPermission('kemaskini permohonan keahlian')"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="bg-blue-600 text-white"
|
||||
title="Kemaskini permohonan"
|
||||
@click="goToApplicationEdit((item as MembershipApplicationListItem).id)"
|
||||
>
|
||||
<Button v-if="hasPermission('kemaskini permohonan keahlian')" type="button" variant="ghost" size="sm"
|
||||
class="bg-blue-600 text-white" title="Kemaskini permohonan"
|
||||
@click="goToApplicationEdit((item as MembershipApplicationListItem).id)">
|
||||
<Pencil class="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="canGenerateLetter(item as MembershipApplicationListItem)"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="bg-amber-600 text-white"
|
||||
title="Jana surat keputusan"
|
||||
@click="openGenerateDialog(item as MembershipApplicationListItem)"
|
||||
>
|
||||
<FileText class="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="canDownloadLetter(item as MembershipApplicationListItem)"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="bg-purple-600 text-white"
|
||||
title="Muat turun surat keputusan"
|
||||
:disabled="downloadingResultLetterId === (item as MembershipApplicationListItem).id"
|
||||
@click="handleDownloadResultLetter(item as MembershipApplicationListItem)"
|
||||
>
|
||||
<Download class="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<DialogRoot
|
||||
:open="batchConfirmOpen"
|
||||
@openChange="(details) => { batchConfirmOpen = details.open }"
|
||||
>
|
||||
<DialogRoot :open="batchConfirmOpen" @openChange="(details) => { batchConfirmOpen = details.open }">
|
||||
<DialogContent>
|
||||
<div class="p-5 text-center">
|
||||
<div class="mt-2 text-2xl font-medium">Selesaikan Permohonan Terpilih?</div>
|
||||
@@ -486,14 +555,49 @@ const headers = computed<TableHeader[]>(() => {
|
||||
<DialogCloseTrigger class="mr-2 w-32" :disabled="batchSubmitting">
|
||||
Batal
|
||||
</DialogCloseTrigger>
|
||||
<Button class="w-32" type="button" variant="primary" :disabled="batchSubmitting"
|
||||
@click="confirmBatchComplete">
|
||||
{{ batchSubmitting ? 'Memproses...' : 'Sahkan' }}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
|
||||
<DialogRoot :open="generateDialogOpen" @openChange="(details) => { if (!details.open) closeGenerateDialog() }">
|
||||
<DialogContent>
|
||||
<div class="p-5">
|
||||
<div class="text-2xl font-medium">Jana Surat Keputusan</div>
|
||||
<p v-if="generateTarget" class="mt-2 text-sm opacity-70">
|
||||
{{ generateTarget.application_number }} · {{ generateTarget.applicant?.name ?? '-' }}
|
||||
</p>
|
||||
<Field class="mt-5">
|
||||
<FieldLabel for="board-meeting-reference">Rujukan Mesyuarat Lembaga</FieldLabel>
|
||||
<Input
|
||||
id="board-meeting-reference"
|
||||
v-model="boardMeetingReference"
|
||||
type="text"
|
||||
placeholder="Contoh: Mesyuarat Lembaga Bil. 3/2026"
|
||||
:disabled="generateSubmitting"
|
||||
@input="boardMeetingReferenceError = null"
|
||||
/>
|
||||
<FieldError v-if="boardMeetingReferenceError">{{ boardMeetingReferenceError }}</FieldError>
|
||||
</Field>
|
||||
<p class="mt-3 text-sm opacity-70">
|
||||
Surat hanya boleh dijana sekali dan akan disimpan sebagai dokumen permohonan.
|
||||
</p>
|
||||
</div>
|
||||
<div class="px-5 pb-8 text-center">
|
||||
<DialogCloseTrigger class="mr-2 w-32" :disabled="generateSubmitting" @click="closeGenerateDialog">
|
||||
Batal
|
||||
</DialogCloseTrigger>
|
||||
<Button
|
||||
class="w-32"
|
||||
type="button"
|
||||
variant="primary"
|
||||
:disabled="batchSubmitting"
|
||||
@click="confirmBatchComplete"
|
||||
:disabled="generateSubmitting"
|
||||
@click="confirmGenerateLetter"
|
||||
>
|
||||
{{ batchSubmitting ? 'Memproses...' : 'Sahkan' }}
|
||||
{{ generateSubmitting ? 'Menjana...' : 'Jana Surat' }}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user