9 Commits

Author SHA1 Message Date
a10f6997eb Add 'Çekiliş Hakkı Adedi' field to piyangoKatilimciStore and implement validation in piyangoKatilimciValidationStore. Ensure the value is at least 1 to maintain data integrity during form submissions. 2026-03-26 12:38:56 +03:00
2bbb30525e Enhance error handling in useDataStore by adding skipErrorForStatuses option to bypass specific status codes in CheckApiError. This improves error management and allows for more flexible API response handling. 2026-03-26 12:38:43 +03:00
2ecda61ad4 Add removeUploadHandlers function to signalrService for improved memory management by detaching event handlers, preventing memory leaks and multiple triggers. 2026-03-26 12:38:29 +03:00
3c4db6d312 Update SaveKatilimciUser function to ensure 'Çekiliş Hakkı Adedi' is always at least 1, enhancing data integrity during user form submissions. 2026-03-26 12:38:18 +03:00
e9e83827dc Update PanelPiyangoKatilimci component to replace 'Katılım Sıra No' with 'Çekiliş Hakkı Adedi' for improved clarity. Add new input field for 'Çekiliş Hakkı Adedi' with validation support, enhancing user interaction and data entry accuracy. 2026-03-26 12:38:07 +03:00
1b8c10a8a0 Enhance PanelExcelUploadDetail component by adding detailed job statistics, including inserted, skipped, duplicate, and error counts. Update grid layout to accommodate new statistics for improved data presentation. 2026-03-26 12:37:56 +03:00
83acb64944 Add 'Çekiliş Hakkı Adedi' column to TabPiyangoTalihliListesi component for improved data display. 2026-03-26 12:37:45 +03:00
cbbd5b2abe Enhance TabPiyangoKatilimciListesi component by adding conditional rendering for job completion statistics, including inserted and duplicate counts. Implement toast notifications for handling upload conflicts and ensure proper cleanup of upload handlers on component unmount. This improves user feedback and data management during file uploads. 2026-03-26 12:37:32 +03:00
a75158379d katılımcı listesi 2026-03-18 07:47:18 +03:00
12 changed files with 913 additions and 220 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,54 @@
</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 v-if="job.status?.toLowerCase() === 'completed'" class="history-stats">
+{{ job.insertedCount ?? 0 }}
<template v-if="(job.duplicateCount ?? 0) > 0"> / {{ job.duplicateCount }} dpl</template>
</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 +131,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 +146,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()
@ -137,18 +168,26 @@
import { useDialogStore } from '@/components/global/dialogStore'
const dialogStore = useDialogStore()
const toastStore = useToastStore()
import { usePiyangoOnayStore } from '../stores/piyangoOnayStore'
const piyangoOnayStore = usePiyangoOnayStore()
import { usePiyangoOnayService } from '../service/piyangoOnayService'
const piyangoOnayService = usePiyangoOnayService()
import { connectToHub, onProgress, onCompleted } from '../service/signalrService'
import { onUnmounted } from 'vue'
import { connectToHub, onProgress, onInsertProgress, onCompleted, onError, removeUploadHandlers } from '../service/signalrService'
import { useToastStore } from '@/components/global/toastStore'
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 +294,57 @@
})
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
removeUploadHandlers()
// İş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 +364,59 @@
piyangoKatilimciStore.katilimciUserPanel = true
}
const FileUpload = async () => {
// Mevcut bağlantıyı kullan (AddNewDocument'te açıldı)
if (!piyangoKatilimciValidationStore.FileFormCheck()) {
piyangoKatilimciValidationStore.isFileFormValid = true
return
}
// Progress modal'ı
removeUploadHandlers()
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' }
headers: { 'Content-Type': 'multipart/form-data' },
skipErrorForStatuses: [409]
}
)
console.log('excel response', response)
if (response !== 'errorfalse') {
// Başarı işlemi zaten onCompleted içinde yapıldı
if (response?._error && response.status === 409) {
uploadProgressPanel.value = false
const msg = response.data?.message || 'Bu çekiliş için zaten devam eden bir Excel yükleme işlemi var.'
toastStore.AddToast(msg, 'alert', 6000)
if (response.data?.existingJobId) {
refreshUploadJobsAndPollProcessing()
OpenUploadDetail(response.data.existingJobId)
}
return
}
if (response !== 'errorfalse' && response?.jobId) {
startPolling(response.jobId)
} else {
// Hata olursa paneli kapat
uploadProgressPanel.value = false
}
}
@ -352,16 +453,92 @@
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)
}
}
onUnmounted(() => {
stopPolling()
removeUploadHandlers()
})
</script>
<style scoped>
.progress-bar {
width: 90%;
max-width: 400px;
@ -384,6 +561,7 @@
font-weight: 600;
color: #333;
}
.upload-warning {
background: #fff8db;
color: #856404;
@ -394,4 +572,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

@ -80,6 +80,11 @@
sort: true,
style: { width: '20%' }
},
{
name: 'cekilisHakkiAdedi',
title: 'Çekiliş Hakkı Adedi',
style: { width: '10%' }
},
{
name: 'sifreCode',

View File

@ -0,0 +1,339 @@
<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 class="detail-stat-item detail-stat-inserted">
<span class="detail-stat-value">{{ job.insertedCount ?? 0 }}</span>
<span class="detail-stat-label">Eklenen</span>
</div>
<div class="detail-stat-item detail-stat-skipped">
<span class="detail-stat-value">{{ job.skippedRows ?? 0 }}</span>
<span class="detail-stat-label">Atlanan</span>
</div>
<div class="detail-stat-item detail-stat-duplicate">
<span class="detail-stat-value">{{ job.duplicateCount ?? 0 }}</span>
<span class="detail-stat-label">Duplicate</span>
</div>
<div class="detail-stat-item detail-stat-error">
<span class="detail-stat-value">{{ job.errorCount ?? 0 }}</span>
<span class="detail-stat-label">Hata</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(5, 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

@ -141,10 +141,10 @@
"
half />
<form-display
v-model="piyangoKatilimciStore.piyangoKatilimciUserFormData.cekilisKatilimSiraNo"
label="Katılım Sora No"
v-model="piyangoKatilimciStore.piyangoKatilimciUserFormData.cekilisHakkiAdedi"
label="Çekiliş Hakkı Adedi"
:invalidText="
piyangoKatilimciValidationStore.userFormInvalidTexts.cekilisKatilimSiraNo
piyangoKatilimciValidationStore.userFormInvalidTexts.cekilisHakkiAdedi
"
half />
<form-display
@ -287,6 +287,16 @@
:invalidText="
piyangoKatilimciValidationStore.userFormInvalidTexts.cekilisKatilimSiraNo
" />
<form-input
@keyup="FormChanged"
modelKey="cekilisHakkiAdedi"
v-model="piyangoKatilimciStore.piyangoKatilimciUserFormData.cekilisHakkiAdedi"
half
label="Çekiliş Hakkı Adedi"
:invalidText="
piyangoKatilimciValidationStore.userFormInvalidTexts.cekilisHakkiAdedi
"
@keydown="validationStore.allowNumbersWithKeys" />
<form-input
@keyup="FormChanged"
modelKey="magazaKartNumarasi"

View File

@ -15,6 +15,10 @@ export const usePiyangoKatilimciService = defineStore('piyangoKatilimciService',
const SaveKatilimciUser = async () => {
if (piyangoKatilimciValidationStore.UserFormCheck()) {
let form: any
piyangoKatilimciStore.piyangoKatilimciUserFormData.cekilisHakkiAdedi = Math.max(
Number(piyangoKatilimciStore.piyangoKatilimciUserFormData.cekilisHakkiAdedi) || 1,
1
)
if (!piyangoKatilimciStore.isPiyangoKatilimciUserUpdate) {
delete piyangoKatilimciStore.piyangoKatilimciUserFormData.id
@ -82,5 +86,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,68 @@
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)
}
/** Upload event handler'larını kaldır (memory leak ve çoklu tetikleme önleme) */
export const removeUploadHandlers = () => {
if (connection) {
connection.off('ReceiveProgress')
connection.off('ReceiveInsertProgress')
connection.off('ReceiveCompleted')
connection.off('ReceiveError')
}
}

View File

@ -10,6 +10,7 @@ export const usePiyangoKatilimciStore = defineStore('piyangoKatilimciStore', ()
const piyangoKatilimciUserSafeFormData = reactive<Record<string, any>>({
ikramiyeId: null,
cekilisKatilimSiraNo: null,
cekilisHakkiAdedi: 1,
sifreCode: '',
magazaKartNumarasi: '',
adi: '',

View File

@ -96,6 +96,31 @@ export const usePiyangoKatilimciValidationStore = defineStore(
'Lütfen soyadını giriniz.'
)
validationStore.IsFieldEmpty(
piyangoKatilimciStore.piyangoKatilimciUserFormData,
userFormInvalidTexts,
'cekilisHakkiAdedi',
'Lütfen çekiliş hakkı adedini giriniz.'
)
if (
!validationStore.checkEmpty(
piyangoKatilimciStore.piyangoKatilimciUserFormData.cekilisHakkiAdedi
)
) {
const hakAdedi = Number(
piyangoKatilimciStore.piyangoKatilimciUserFormData.cekilisHakkiAdedi
)
if (!Number.isInteger(hakAdedi) || hakAdedi < 1) {
userFormInvalidTexts.cekilisHakkiAdedi =
'Çekiliş hakkı adedi en az 1 olmalıdır.'
isUserFormValid.value = false
} else {
delete userFormInvalidTexts.cekilisHakkiAdedi
}
}
var today: Date | string = new Date()
today = dateStore.dateFormat({
date: today,

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'
}
@ -64,6 +64,8 @@ export const useDataStore = defineStore('dataStore', () => {
options?: Record<string, any>
toast?: Record<string, any>
full?: boolean
/** Bu status kodları için CheckApiError atlanır, hata objesi döner */
skipErrorForStatuses?: number[]
} = {}
): Promise<any> => {
try {
@ -86,8 +88,11 @@ export const useDataStore = defineStore('dataStore', () => {
return response.data
}
} catch (error: any) {
CheckApiError(error.response.status, error.response.data)
const status = error.response?.status
if (data.skipErrorForStatuses?.includes(status)) {
return { _error: true, status, data: error.response?.data }
}
CheckApiError(status, error.response?.data)
console.error('Hata oluştu:', error)
return Promise.resolve('errorfalse')
}
@ -124,8 +129,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 +165,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 +192,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 () => {