katılımcı listesi

This commit is contained in:
burakovec
2026-03-18 07:47:18 +03:00
parent 46d0cac892
commit a75158379d
8 changed files with 806 additions and 216 deletions

128
package-lock.json generated
View File

@ -15,8 +15,6 @@
"jquery": "^3.7.1",
"parchment": "^3.0.0",
"pinia": "^2.1.7",
"quill": "^2.0.3",
"quill-image-resize-module": "^3.0.0",
"summernote": "^0.9.1",
"uuid": "^11.1.0",
"vue": "^3.4.29",
@ -1718,14 +1716,6 @@
}
]
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -2006,11 +1996,6 @@
"node": ">=6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"node_modules/eventsource": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
@ -2019,11 +2004,6 @@
"node": ">=12.0.0"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"node_modules/fetch-cookie": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz",
@ -2359,26 +2339,6 @@
"node": ">=6"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -2752,94 +2712,6 @@
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"node_modules/quill": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
"integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
"dependencies": {
"eventemitter3": "^5.0.1",
"lodash-es": "^4.17.21",
"parchment": "^3.0.0",
"quill-delta": "^5.1.0"
},
"engines": {
"npm": ">=8.2.3"
}
},
"node_modules/quill-image-resize-module": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/quill-image-resize-module/-/quill-image-resize-module-3.0.0.tgz",
"integrity": "sha512-1TZBnUxU/WIx5dPyVjQ9yN7C6mLZSp04HyWBEMqT320DIq4MW4JgzlOPDZX5ZpBM3bU6sacU4kTLUc8VgYQZYw==",
"dependencies": {
"lodash": "^4.17.4",
"quill": "^1.2.2",
"raw-loader": "^0.5.1"
}
},
"node_modules/quill-image-resize-module/node_modules/eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg=="
},
"node_modules/quill-image-resize-module/node_modules/fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig=="
},
"node_modules/quill-image-resize-module/node_modules/parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg=="
},
"node_modules/quill-image-resize-module/node_modules/quill": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
"dependencies": {
"clone": "^2.1.1",
"deep-equal": "^1.0.1",
"eventemitter3": "^2.0.3",
"extend": "^3.0.2",
"parchment": "^1.1.4",
"quill-delta": "^3.6.2"
}
},
"node_modules/quill-image-resize-module/node_modules/quill-delta": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"dependencies": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/quill/node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="
},
"node_modules/quill/node_modules/quill-delta": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
"integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
"dependencies": {
"fast-diff": "^1.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/raw-loader": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz",
"integrity": "sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q=="
},
"node_modules/read-package-json-fast": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz",

View File

@ -392,9 +392,22 @@
const searchChanging = ref<boolean>(false)
const filterChanging = ref<boolean>(false)
const isQueryEqual = (a: Record<string, any>, b: Record<string, any>) => {
const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})])
for (const k of keys) {
const va = a?.[k]
const vb = b?.[k]
if (Array.isArray(va) && Array.isArray(vb)) {
if (va.length !== vb.length || va.some((v: any, i: number) => v !== vb[i]))
return false
} else if (String(va ?? '') !== String(vb ?? '')) return false
}
return true
}
const RoutePageControl = () => {
if (props.isUseRoute) {
const q = { ...route.query }
const q = { ...route.query } as Record<string, any>
pageNumberChanging.value = true
if (q.pageNumber !== undefined && q.pageNumber !== '') {
@ -404,7 +417,9 @@
localPagination.value.pageNumber = localPagination.value.pageNumber || 1
}
router.push({ query: q })
if (!isQueryEqual(q, route.query as Record<string, any>)) {
router.push({ query: q })
}
nextTick(() => {
pageNumberChanging.value = false
})
@ -413,7 +428,7 @@
const RouteSortControl = () => {
if (props.isUseRoute) {
const q = { ...route.query }
const q = { ...route.query } as Record<string, any>
sortChanging.value = true
if (q.sortOrder !== undefined && q.sortOrder !== null && q.sortOrder !== '') {
localSort.value.sortColumn = q.sortColumn as string
@ -424,7 +439,9 @@
delete localSort.value.sortColumn
delete localSort.value.sortOrder
}
router.push({ query: q })
if (!isQueryEqual(q, route.query as Record<string, any>)) {
router.push({ query: q })
}
nextTick(() => {
sortChanging.value = false
})
@ -433,7 +450,7 @@
const RouteSearchControl = () => {
if (props.isUseRoute) {
const q = { ...route.query }
const q = { ...route.query } as Record<string, any>
searchChanging.value = true
if (
q.searchString !== undefined &&
@ -445,7 +462,9 @@
localQuery.value = ''
delete q.searchString
}
router.push({ query: q })
if (!isQueryEqual(q, route.query as Record<string, any>)) {
router.push({ query: q })
}
nextTick(() => {
searchChanging.value = false
})

View File

@ -59,9 +59,8 @@ axios.interceptors.response.use(
const usersStore = useUsersStore()
const dataStore = useDataStore()
dataStore.isLoading = false
// Yanıtta hata oluşursa burada yakalanır
// error.status kodu undefined geliyor
if (error.response.status === 401) {
// Yanıtta hata oluşursa burada yakalanır (401 login'e yönlendir, diğerleri dataStore catch'te toast gösterir)
if (error.response?.status === 401) {
const token = sessionStorage.getItem(usersStore.userStorageKeys.TOKEN)
if (token !== undefined) {
usersStore.ResetUserData()

View File

@ -51,11 +51,50 @@
</template>
</list-table-content>
<panel-wrapper
wide
v-if="piyangoKatilimciStore.katilimciFilePanel"
v-model="piyangoKatilimciStore.katilimciFilePanel"
panel-title="Katılımcı Dosyası Yükle">
<template #panelContent>
<panel-katilimci-document />
<div class="upload-panel-content">
<div v-if="uploadProgressPanel" class="upload-progress-section">
<h4 class="upload-progress-title">Aktif Yükleme</h4>
<div class="upload-warning">
İşlem sırasında tarayıcıyı veya bu sekmeyi kapatmayınız. Sayfa yenilerseniz bu paneli tekrar açarak takip edebilirsiniz.
</div>
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: uploadProgressValue + '%' }"></div>
</div>
<div class="progress-text">
<template v-if="uploadProgressValue === 0">
Dosya içeriği okunuyor, yükleme başlatılıyor...
</template>
<template v-else>{{ uploadProgressValue }}%</template>
</div>
</div>
<div v-if="uploadJobsLoading || uploadJobs.length > 0" class="upload-history-section">
<h4 class="upload-history-title">Yükleme Geçmişi</h4>
<div v-if="uploadJobsLoading" class="history-loading">Yükleniyor...</div>
<div v-else-if="uploadJobs.length > 0" class="history-items">
<div
v-for="job in uploadJobs"
:key="job.guid"
:class="['history-item', job.status?.toLowerCase() === 'processing' ? 'processing' : '']"
@click="OpenUploadDetail(job.guid)">
<span class="history-file">{{ job.fileName }}</span>
<span :class="['history-status', 'status-' + job.status?.toLowerCase()]">{{ statusText(job.status) }}</span>
<span class="history-progress">{{ job.progress }}%</span>
<span class="history-date">{{ formatDate(job.createdAt) }}</span>
</div>
</div>
</div>
<div class="upload-form-section">
<h4 class="upload-form-title">Yeni Dosya Yükle</h4>
<panel-katilimci-document />
</div>
</div>
</template>
<template #footerButton>
<button
@ -88,26 +127,12 @@
</template>
</panel-wrapper>
<panel-wrapper
v-if="uploadProgressPanel"
v-model="uploadProgressPanel"
panel-title="Yükleme Durumu">
wide
v-if="uploadDetailPanel"
v-model="uploadDetailPanel"
panel-title="Yükleme Detayı">
<template #panelContent>
<div class="progress-container">
<div class="upload-warning">
📌 İşlem sırasında tarayıcıyı veya bu sekmeyi kapatmayınız.
</div>
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: uploadProgressValue + '%' }"></div>
</div>
<div class="progress-text">
<template v-if="uploadProgressValue === 0">
Dosya içeriği okunuyor, yükleme başlatılıyor...
</template>
<template v-else>{{ uploadProgressValue }}%</template>
</div>
</div>
<panel-excel-upload-detail :jobGuid="selectedJobGuid" />
</template>
</panel-wrapper>
</section>
@ -117,6 +142,8 @@
import PanelWrapper from '@/components/PanelWrapper.vue'
import PanelKatilimciDocument from '@/module/cekilisler/components/panel/PanelKatilimciDocument.vue'
import PanelPiyangoKatilimci from '@/module/cekilisler/components/panel/PanelPiyangoKatilimci.vue'
import PanelExcelUploadDetail from '@/module/cekilisler/components/panel/PanelExcelUploadDetail.vue'
import { useDateStore } from '@/stores/dateStore'
import { useUsersStore } from '@/stores/usersStore'
const usersStore = useUsersStore()
@ -143,12 +170,17 @@
import { usePiyangoOnayService } from '../service/piyangoOnayService'
const piyangoOnayService = usePiyangoOnayService()
import { connectToHub, onProgress, onCompleted } from '../service/signalrService'
import { connectToHub, onProgress, onInsertProgress, onCompleted, onError } from '../service/signalrService'
const uploadProgressValue = ref(0)
const uploadProgressPanel = ref(false)
const uploadDetailPanel = ref(false)
const uploadJobs = ref<Record<string, any>[]>([])
const uploadJobsLoading = ref(false)
const selectedJobGuid = ref('')
const connectionId = ref('')
const dateStore = useDateStore()
let pollInterval: ReturnType<typeof setInterval> | null = null
const tableHeader = ref<Record<string, any>[]>([
{
@ -255,12 +287,56 @@
})
const AddNewDocument = async () => {
connectionId.value = await connectToHub()
dataStore.panelData = {
title: '',
file: ''
try {
connectionId.value = await connectToHub()
} catch {
connectionId.value = ''
}
dataStore.panelData = { title: '', file: '' }
piyangoKatilimciStore.katilimciFilePanel = true
uploadJobsLoading.value = true
const cekilisId = piyangoStore.selectedLottery
uploadJobs.value = cekilisId !== null ? await piyangoKatilimciService.GetUploadJobs(cekilisId) : []
uploadJobsLoading.value = false
// İşleniyor durumundaki job varsa SignalR ile abone ol (gerçek zamanlı progress almak için)
const processingJob = uploadJobs.value.find(
(j: Record<string, any>) => j.status?.toLowerCase() === 'processing'
)
if (processingJob?.guid && connectionId.value) {
const updateProgress = (data: any) => {
const percent = data.Percent ?? 0
uploadProgressValue.value = percent
const idx = uploadJobs.value.findIndex((j: Record<string, any>) => j.guid === processingJob.guid)
if (idx >= 0) {
uploadJobs.value[idx] = {
...uploadJobs.value[idx],
progress: percent,
processedRows: data.Current ?? data.InsertedCount,
totalRows: data.Total ?? data.TotalCount
}
}
}
onProgress(updateProgress)
onInsertProgress(updateProgress)
onCompleted(() => {
stopPolling()
uploadProgressPanel.value = false
piyangoKatilimciStore.refreshPiyangoKatilimciList = true
piyangoKatilimciStore.katilimciFilePanel = true
refreshUploadJobsAndPollProcessing()
})
onError(() => {
stopPolling()
uploadProgressPanel.value = false
refreshUploadJobsAndPollProcessing()
})
uploadProgressPanel.value = true
uploadProgressValue.value = processingJob.progress ?? 0
await piyangoKatilimciService.SubscribeToJob(processingJob.guid, connectionId.value)
}
startPollingForAnyProcessingJob()
}
const AddNewKatilimci = () => {
@ -280,42 +356,46 @@
piyangoKatilimciStore.katilimciUserPanel = true
}
const FileUpload = async () => {
// Mevcut bağlantıyı kullan (AddNewDocument'te açıldı)
if (!piyangoKatilimciValidationStore.FileFormCheck()) {
piyangoKatilimciValidationStore.isFileFormValid = true
return
}
// Progress modal'ı
uploadProgressValue.value = 0
uploadProgressPanel.value = true
onProgress((data) => {
uploadProgressValue.value = data.Percent
console.log('Progress:', data.Percent)
})
onCompleted((data) => {
console.log('Tamamlandı:', data)
onCompleted(() => {
stopPolling()
uploadProgressPanel.value = false
piyangoKatilimciStore.refreshPiyangoKatilimciList = true
piyangoKatilimciStore.katilimciFilePanel = false
piyangoKatilimciStore.katilimciFilePanel = true
refreshUploadJobsAndPollProcessing()
})
onError(() => {
stopPolling()
uploadProgressPanel.value = false
})
const formData = new FormData()
formData.append(
'excelFile',
piyangoKatilimciStore.piyangoKatilimciFileFormData.excelFile
)
console.log(dataStore.panelData)
formData.append('excelFile', piyangoKatilimciStore.piyangoKatilimciFileFormData.excelFile)
const connId = connectionId.value || ''
const response = await dataStore.dataPost(
`Katilimci/ExcelleYukle/${piyangoStore.selectedLottery}?connectionId=${connectionId.value}`,
`Katilimci/ExcelleYukle/${piyangoStore.selectedLottery}?connectionId=${connId}`,
{
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
}
)
console.log('excel response', response)
if (response !== 'errorfalse') {
// Başarı işlemi zaten onCompleted içinde yapıldı
if (response !== 'errorfalse' && response?.jobId) {
startPolling(response.jobId)
} else {
// Hata olursa paneli kapat
uploadProgressPanel.value = false
}
}
@ -352,16 +432,87 @@
piyangoOnayStore.piyangoOnayForm.aciklama = ''
await piyangoOnayService.SaveOnayDurum()
}
</script>
<style>
.progress-container {
width: 100%;
padding: 20px 0;
display: flex;
flex-direction: column;
align-items: center;
const statusText = (status: string) => {
const map: Record<string, string> = {
Pending: 'Bekliyor',
Processing: 'İşleniyor',
Completed: 'Tamamlandı',
Failed: 'Başarısız'
}
return map[status] || status
}
const formatDate = (val: string | Date) => {
if (!val) return '-'
return dateStore.dateFormat({ date: new Date(val), pattern: 'dd.mm.yyyy HH:MM' })
}
const OpenUploadDetail = (guid: string) => {
selectedJobGuid.value = guid
uploadDetailPanel.value = true
}
const stopPolling = () => {
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
}
}
const startPolling = (jobId: string) => {
stopPolling()
pollInterval = setInterval(async () => {
const status = await piyangoKatilimciService.GetUploadJobStatus(jobId)
if (status && (status.status === 'Completed' || status.status === 'Failed')) {
stopPolling()
uploadProgressValue.value = status.status === 'Completed' ? 100 : status.progress || 0
uploadProgressPanel.value = false
piyangoKatilimciStore.refreshPiyangoKatilimciList = true
piyangoKatilimciStore.katilimciFilePanel = true
refreshUploadJobsAndPollProcessing()
} else if (status) {
uploadProgressValue.value = status.progress || 0
}
}, 2000)
}
const startPollingForExistingJob = (jobGuid: string) => {
stopPolling()
pollInterval = setInterval(async () => {
const status = await piyangoKatilimciService.GetUploadJobStatus(jobGuid)
if (status) {
const idx = uploadJobs.value.findIndex((j: Record<string, any>) => j.guid === jobGuid)
if (idx >= 0) {
uploadJobs.value[idx] = { ...uploadJobs.value[idx], ...status }
}
if (status.status === 'Completed' || status.status === 'Failed') {
stopPolling()
piyangoKatilimciStore.refreshPiyangoKatilimciList = true
refreshUploadJobsAndPollProcessing()
}
}
}, 2000)
}
const refreshUploadJobsAndPollProcessing = async () => {
const cekilisId = piyangoStore.selectedLottery
if (cekilisId !== null) {
uploadJobs.value = await piyangoKatilimciService.GetUploadJobs(cekilisId)
startPollingForAnyProcessingJob()
}
}
const startPollingForAnyProcessingJob = () => {
const processingJob = uploadJobs.value.find(
(j: Record<string, any>) => j.status?.toLowerCase() === 'processing'
)
if (processingJob?.guid) {
startPollingForExistingJob(processingJob.guid)
}
}
</script>
<style scoped>
.progress-bar {
width: 90%;
max-width: 400px;
@ -384,6 +535,7 @@
font-weight: 600;
color: #333;
}
.upload-warning {
background: #fff8db;
color: #856404;
@ -394,4 +546,164 @@
font-size: 14px;
text-align: center;
}
.upload-panel-content {
display: flex;
flex-direction: column;
gap: 32px;
padding: 24px 24px 24px 24px;
}
.upload-progress-section {
background: #f0f9ff;
border: 1px solid #b6d4fe;
border-radius: 8px;
padding: 16px 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.upload-progress-title {
margin: 0 0 12px;
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
align-self: flex-start;
}
.upload-progress-section .upload-warning {
margin-bottom: 16px;
width: 100%;
}
.upload-progress-section .progress-bar {
width: 100%;
max-width: 100%;
}
.upload-progress-section .progress-text {
margin-top: 12px;
color: #1a1a2e;
}
.upload-history-section {
padding-bottom: 20px;
border-bottom: 1px solid #e8eaed;
}
.upload-history-title,
.upload-form-title {
margin: 0 0 14px;
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
letter-spacing: 0.02em;
}
.upload-form-section {
padding-top: 4px;
}
.upload-form-section :deep(.panel-documents-item) {
margin-bottom: 0;
}
.upload-form-section :deep(.form-item-description) {
margin-top: 8px;
font-size: 13px;
color: #5f6368;
line-height: 1.4;
}
.history-loading {
text-align: center;
padding: 20px;
color: #5f6368;
font-size: 14px;
}
.history-items {
display: flex;
flex-direction: column;
gap: 8px;
}
.history-item {
display: grid;
grid-template-columns: 1fr auto auto auto;
align-items: center;
gap: 16px;
padding: 12px 16px;
border: 1px solid #e8eaed;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
background: #fafbfc;
}
.history-item:hover {
background: #f1f3f5;
border-color: #d0d5dd;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.history-file {
font-weight: 500;
font-size: 14px;
color: #1a1a2e;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-status {
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 6px;
white-space: nowrap;
}
.history-status.status-completed {
color: #0d6832;
background: #d4edda;
}
.history-status.status-failed {
color: #721c24;
background: #f8d7da;
}
.history-status.status-processing {
color: #004085;
background: #cce5ff;
}
.history-status.status-pending {
color: #856404;
background: #fff3cd;
}
.history-progress {
font-size: 13px;
font-weight: 500;
color: #5f6368;
min-width: 36px;
text-align: right;
}
.history-date {
font-size: 12px;
color: #80868b;
min-width: 100px;
text-align: right;
}
/* İşleniyor durumunda progress bar */
.history-item.processing .history-progress {
font-weight: 600;
color: #007bff;
}
</style>

View File

@ -0,0 +1,323 @@
<template>
<div class="panel-excel-upload-detail">
<div v-if="loading" class="detail-loading">Yükleniyor...</div>
<template v-else-if="job">
<div class="detail-summary">
<div class="detail-row detail-file-row">
<span class="detail-label">Dosya:</span>
<span class="detail-file-name">{{ job.fileName }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Durum:</span>
<span :class="['detail-status', 'status-' + job.status?.toLowerCase()]">
{{ statusText(job.status) }}
</span>
</div>
<div class="detail-row">
<span class="detail-label">İlerleme:</span>
<span>{{ job.progress }}%</span>
</div>
<div class="detail-stats">
<div class="detail-stat-item">
<span class="detail-stat-value">{{ job.totalRows }}</span>
<span class="detail-stat-label">Toplam Satır</span>
</div>
</div>
<div v-if="job.errorMessage" class="detail-error-box">
<span class="detail-error-label">Hata:</span>
<span>{{ job.errorMessage }}</span>
</div>
</div>
<div class="detail-logs" v-if="logs && logs.length > 0">
<h4 class="detail-logs-title">İşlem Logları</h4>
<div class="logs-list">
<div
v-for="log in logs"
:key="log.id"
:class="['log-item', 'log-' + log.logLevel?.toLowerCase()]">
<span :class="['log-level', 'log-level-' + log.logLevel?.toLowerCase()]">{{ log.logLevel }}</span>
<span v-if="log.rowNumber" class="log-row">Satır {{ log.rowNumber }}</span>
<span class="log-message">{{ log.message }}</span>
<span v-if="log.details" class="log-details">{{ log.details }}</span>
<span class="log-time">{{ formatDate(log.createdAt) }}</span>
</div>
</div>
</div>
<div v-else class="detail-no-logs">Log kaydı bulunamadı.</div>
</template>
<div v-else class="detail-not-found">Yükleme detayı bulunamadı.</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { usePiyangoKatilimciService } from '../../service/piyangoKatilimciService'
import { useDateStore } from '@/stores/dateStore'
const props = defineProps<{
jobGuid: string
}>()
const piyangoKatilimciService = usePiyangoKatilimciService()
const dateStore = useDateStore()
const loading = ref(true)
const job = ref<Record<string, any> | null>(null)
const logs = ref<Record<string, any>[]>([])
const statusText = (status: string) => {
const map: Record<string, string> = {
Pending: 'Bekliyor',
Processing: 'İşleniyor',
Completed: 'Tamamlandı',
Failed: 'Başarısız'
}
return map[status] || status
}
const formatDate = (val: string | Date) => {
if (!val) return '-'
return dateStore.dateFormat({ date: new Date(val), pattern: 'dd.mm.yyyy HH:MM' })
}
const loadDetail = async () => {
if (!props.jobGuid) return
loading.value = true
const data = await piyangoKatilimciService.GetUploadJobDetail(props.jobGuid)
if (data) {
job.value = data.job
logs.value = data.logs || []
} else {
job.value = null
logs.value = []
}
loading.value = false
}
onMounted(() => loadDetail())
watch(() => props.jobGuid, () => loadDetail())
</script>
<style scoped>
.panel-excel-upload-detail {
padding: 24px 24px 24px 24px;
}
.detail-loading {
text-align: center;
padding: 32px;
color: #5f6368;
font-size: 14px;
}
.detail-summary {
margin-bottom: 24px;
padding: 20px;
background: #fafbfc;
border: 1px solid #e8eaed;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.detail-row {
display: grid;
grid-template-columns: 120px 1fr;
gap: 12px;
margin-bottom: 10px;
align-items: center;
}
.detail-file-row {
margin-bottom: 12px;
}
.detail-file-name {
font-weight: 600;
color: #1a1a2e;
word-break: break-all;
}
.detail-label {
font-weight: 600;
color: #5f6368;
font-size: 13px;
}
.detail-status {
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 6px;
display: inline-block;
width: fit-content;
}
.detail-status.status-completed {
color: #0d6832;
background: #d4edda;
}
.detail-status.status-failed {
color: #721c24;
background: #f8d7da;
}
.detail-status.status-processing {
color: #004085;
background: #cce5ff;
}
.detail-status.status-pending {
color: #856404;
background: #fff3cd;
}
.detail-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin: 16px 0;
padding: 16px 0;
border-top: 1px solid #e8eaed;
border-bottom: 1px solid #e8eaed;
}
.detail-stat-item {
text-align: center;
padding: 8px;
background: #f1f3f5;
border-radius: 6px;
}
.detail-stat-value {
display: block;
font-size: 18px;
font-weight: 700;
color: #1a1a2e;
}
.detail-stat-label {
font-size: 12px;
color: #5f6368;
}
.detail-error-box {
margin-top: 16px;
padding: 12px 16px;
background: #fff5f5;
border: 1px solid #f8d7da;
border-radius: 6px;
color: #721c24;
font-size: 13px;
display: flex;
gap: 8px;
}
.detail-error-label {
font-weight: 600;
flex-shrink: 0;
}
.detail-not-found {
padding: 24px;
text-align: center;
color: #721c24;
background: #fff5f5;
border: 1px solid #f8d7da;
border-radius: 8px;
font-weight: 500;
}
.detail-logs-title {
margin: 0 0 12px;
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
}
.logs-list {
max-height: 400px;
overflow-y: auto;
border: 1px solid #e8eaed;
border-radius: 8px;
background: #fafbfc;
}
.log-item {
padding: 10px 14px;
border-bottom: 1px solid #e8eaed;
font-size: 13px;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: baseline;
}
.log-item:last-child {
border-bottom: none;
}
.log-item.log-error {
background: #fff5f5;
}
.log-item.log-warning {
background: #fffbf0;
}
.log-item.log-info {
background: #f0f9ff;
}
.log-level {
font-weight: 600;
min-width: 60px;
font-size: 11px;
padding: 2px 8px;
border-radius: 4px;
}
.log-level.log-level-error {
background: #f8d7da;
color: #721c24;
}
.log-level.log-level-warning {
background: #fff3cd;
color: #856404;
}
.log-level.log-level-info {
background: #cce5ff;
color: #004085;
}
.log-row {
color: #5f6368;
font-family: ui-monospace, monospace;
font-size: 12px;
}
.log-message {
flex: 1;
min-width: 0;
}
.log-details {
font-size: 12px;
color: #5f6368;
width: 100%;
}
.log-time {
font-size: 11px;
color: #80868b;
margin-left: auto;
}
.detail-no-logs {
padding: 24px;
color: #80868b;
text-align: center;
font-style: italic;
font-size: 14px;
}
</style>

View File

@ -82,5 +82,46 @@ export const usePiyangoKatilimciService = defineStore('piyangoKatilimciService',
}
}
return { SaveKatilimciUser, CreateOnlineDraw, KatilimciData, KatilimciFileUpload }
const GetUploadJobs = async (cekilisId: number): Promise<Record<string, any>[]> => {
const dt = await dataStore.dataGet(`ExcelUploadJob/my-jobs?cekilisId=${cekilisId}`)
if (dt !== 'errorfalse' && Array.isArray(dt)) {
return dt
}
return []
}
const GetUploadJobDetail = async (guid: string): Promise<Record<string, any> | null> => {
const dt = await dataStore.dataGet(`ExcelUploadJob/${guid}`)
if (dt !== 'errorfalse') {
return dt
}
return null
}
const GetUploadJobStatus = async (guid: string): Promise<Record<string, any> | null> => {
const dt = await dataStore.dataGet(`ExcelUploadJob/status/${guid}`)
if (dt !== 'errorfalse') {
return dt
}
return null
}
const SubscribeToJob = async (guid: string, connectionId: string): Promise<boolean> => {
const result = await dataStore.dataPost(
`ExcelUploadJob/subscribe?guid=${guid}&connectionId=${encodeURIComponent(connectionId)}`,
{}
)
return result !== 'errorfalse'
}
return {
SaveKatilimciUser,
CreateOnlineDraw,
KatilimciData,
KatilimciFileUpload,
GetUploadJobs,
GetUploadJobDetail,
GetUploadJobStatus,
SubscribeToJob
}
})

View File

@ -1,48 +1,58 @@
import * as signalR from '@microsoft/signalr'
let connection: signalR.HubConnection
let connectionId = ''
let connection: signalR.HubConnection | null = null
export const connectToHub = async () => {
export const connectToHub = async (): Promise<string> => {
console.log('Connecting to SignalR Hub...')
// Mevcut bağlantı varsa kapat
if (connection && connection.state === signalR.HubConnectionState.Connected) {
await connection.stop()
// Mevcut bağlantı varsa her durumda kapat (Connected, Reconnecting, Disconnected)
if (connection) {
try {
await connection.stop()
} catch {
// Bağlantı zaten kapalı olabilir, yoksay
}
connection = null
}
connection = new signalR.HubConnectionBuilder()
.withUrl(import.meta.env.VITE_SOCKET_URL, {
withCredentials: false,
skipNegotiation: true, // WebSocket kullanırken negotiation atlanabilir
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets
})
.withAutomaticReconnect()
.build()
// Eventleri ekle
onProgress((data) => console.log('progress', data))
onInsertProgress((data) => console.log('insert progress', data))
onCompleted((data) => console.log('completed', data))
onError((data) => console.log('error', data))
// Eventleri ekle (connection artık tanımlı)
connection.on('ReceiveProgress', (data: any) => console.log('progress', data))
connection.on('ReceiveInsertProgress', (data: any) => console.log('insert progress', data))
connection.on('ReceiveCompleted', (data: any) => console.log('completed', data))
connection.on('ReceiveError', (data: any) => console.log('error', data))
await connection.start()
connectionId = await connection.invoke<string>('GetConnectionId')
console.log('Connected to SignalR Hub with Connection ID:', connectionId)
return connectionId
try {
await connection.start()
const connectionId = await connection.invoke<string>('GetConnectionId')
console.log('Connected to SignalR Hub with Connection ID:', connectionId)
return connectionId
} catch (err) {
connection = null
throw err
}
}
export const onProgress = (callback: (data: any) => void) => {
connection.on('ReceiveProgress', callback)
if (connection) connection.on('ReceiveProgress', callback)
}
export const onInsertProgress = (callback: (data: any) => void) => {
connection.on('ReceiveInsertProgress', callback)
if (connection) connection.on('ReceiveInsertProgress', callback)
}
export const onCompleted = (callback: (data: any) => void) => {
connection.on('ReceiveCompleted', callback)
if (connection) connection.on('ReceiveCompleted', callback)
}
export const onError = (callback: (data: any) => void) => {
connection.on('ReceiveError', callback)
if (connection) connection.on('ReceiveError', callback)
}

View File

@ -49,7 +49,7 @@ export const useDataStore = defineStore('dataStore', () => {
return response.data
}
} catch (error: any) {
CheckApiError(error.response.status, error.response.data)
CheckApiError(error.response?.status, error.response?.data)
console.error('Hata oluştu -:', error)
return 'errorfalse'
}
@ -86,8 +86,7 @@ export const useDataStore = defineStore('dataStore', () => {
return response.data
}
} catch (error: any) {
CheckApiError(error.response.status, error.response.data)
CheckApiError(error.response?.status, error.response?.data)
console.error('Hata oluştu:', error)
return Promise.resolve('errorfalse')
}
@ -124,8 +123,7 @@ export const useDataStore = defineStore('dataStore', () => {
return response.data
}
} catch (error: any) {
CheckApiError(error.response.status, error.response.data)
CheckApiError(error.response?.status, error.response?.data)
console.error('Hata oluştu:', error)
return Promise.resolve('errorfalse')
}
@ -161,16 +159,20 @@ export const useDataStore = defineStore('dataStore', () => {
return response.data
}
} catch (error: any) {
CheckApiError(error.response.status, error.response.data)
CheckApiError(error.response?.status, error.response?.data)
console.error('Hata oluştu:', error)
return Promise.resolve('errorfalse')
}
}
const CheckApiError = async (status: number, data: Record<string, any>) => {
const CheckApiError = (status?: number, data?: Record<string, any>) => {
if (status === undefined) {
toastStore.AddToast("Bağlantı hatası. İnternet bağlantınızı kontrol edin.", "alert", 8000);
return;
}
if (status === 401) return; // Axios interceptor login'e yönlendirir
if (status === 400) {
const errorKey = typeof data === "string" ? data : data?.hata || data?.errors;
if (errorKey !== undefined) {
if (Array.isArray(errorKey)) {
errorKey.forEach((el: string) => {
@ -184,7 +186,19 @@ export const useDataStore = defineStore('dataStore', () => {
} else {
toastStore.AddToast("Bir hata oluştu.", "alert");
}
return;
}
const httpErrorMessages: Record<number, string> = {
413: "Dosya boyutu çok büyük. Lütfen daha küçük bir dosya yükleyin veya sunucu limitlerini kontrol edin.",
404: "İstenen kaynak bulunamadı.",
500: "Sunucu hatası oluştu. Lütfen daha sonra tekrar deneyin.",
502: "Sunucu geçici olarak yanıt vermiyor. Lütfen daha sonra tekrar deneyin.",
503: "Servis şu an kullanılamıyor. Lütfen daha sonra tekrar deneyin.",
504: "İstek zaman aşımına uğradı. Lütfen tekrar deneyin.",
408: "İstek zaman aşımına uğradı. Lütfen tekrar deneyin."
};
const message = httpErrorMessages[status] || `Beklenmeyen hata (${status}). Lütfen tekrar deneyin.`;
toastStore.AddToast(message, "alert", 8000);
};
const GetCustomerTipList = async () => {