Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -3,9 +3,6 @@
|
||||
<div class="top-bar row items-center justify-between q-mb-xs">
|
||||
<div>
|
||||
<div class="text-subtitle1 text-weight-bold">Fiyat Carpani Kurallari</div>
|
||||
<div class="text-caption text-grey-7">
|
||||
MSSQL urun kombinasyonlari ve bu kombinasyonlara bagli para birimi bazli fiyat kurallari.
|
||||
</div>
|
||||
</div>
|
||||
<q-btn
|
||||
flat
|
||||
@@ -19,9 +16,17 @@
|
||||
|
||||
<div class="action-bar row items-center justify-between q-mb-xs">
|
||||
<div class="text-caption text-grey-8">
|
||||
{{ filteredRows.length }} / {{ rows.length }} kombinasyon gosteriliyor. Degistirilen satirlar otomatik secilir.
|
||||
Satir: {{ filteredRows.length }} / {{ rows.length }} | Degisen: {{ selectedDirtyCount }} | Kopya: {{ copySelectedCount }} | Secili: {{ selectedCount }}
|
||||
</div>
|
||||
<div class="row items-center q-gutter-xs">
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<div class="row items-center q-gutter-xs action-legend">
|
||||
<q-chip dense square color="orange-1" text-color="deep-orange-8" icon="content_copy">
|
||||
Kopya secimi
|
||||
</q-chip>
|
||||
<q-chip dense square color="light-green-1" text-color="green-9" icon="task_alt">
|
||||
Kaydetme secimi
|
||||
</q-chip>
|
||||
</div>
|
||||
<q-btn
|
||||
flat
|
||||
color="primary"
|
||||
@@ -30,6 +35,38 @@
|
||||
:disable="!hasAnyFilter"
|
||||
@click="clearAllFilters"
|
||||
/>
|
||||
<q-btn
|
||||
color="secondary"
|
||||
unelevated
|
||||
icon="content_copy"
|
||||
label="Kopyala"
|
||||
:disable="!canCopySelected"
|
||||
@click="copySelectedToSelected"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
flat
|
||||
icon="download"
|
||||
label="Sayfayi Excel'e Aktar"
|
||||
:disable="filteredRows.length === 0"
|
||||
@click="exportCurrentView"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
outline
|
||||
icon="download_for_offline"
|
||||
label="Tum Filtreyi Excel'e Aktar"
|
||||
:disable="rows.length === 0"
|
||||
@click="exportAllFiltered"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
outline
|
||||
icon="upload_file"
|
||||
label="Verileri CSV'den Yukle"
|
||||
:disable="loading || rows.length === 0"
|
||||
@click="openImportDialog"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
unelevated
|
||||
@@ -39,6 +76,13 @@
|
||||
:label="`Kaydet (${selectedDirtyCount})`"
|
||||
@click="saveSelected"
|
||||
/>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept=".csv,text/csv"
|
||||
class="hidden-file-input"
|
||||
@change="onImportFileChange"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,10 +119,17 @@
|
||||
<q-checkbox
|
||||
v-if="col.name === 'select'"
|
||||
dense
|
||||
size="24px"
|
||||
class="rule-select-checkbox"
|
||||
color="positive"
|
||||
:model-value="allSelectedVisible"
|
||||
:indeterminate="someSelectedVisible && !allSelectedVisible"
|
||||
@update:model-value="toggleSelectAllVisible"
|
||||
/>
|
||||
<div v-else-if="col.name === 'copy_select'" class="selection-header-copy">
|
||||
<q-icon name="content_copy" size="16px" />
|
||||
<span>Kopya</span>
|
||||
</div>
|
||||
<div v-else class="header-with-filter">
|
||||
<span>{{ col.label }}</span>
|
||||
<q-btn
|
||||
@@ -183,9 +234,32 @@
|
||||
:class="[col.classes, { 'sticky-col': isStickyCol(col.name), 'sticky-boundary': isStickyBoundary(col.name) }]"
|
||||
:style="getBodyCellStyle(col)"
|
||||
>
|
||||
<div
|
||||
v-if="col.name === 'copy_select'"
|
||||
class="row items-center no-wrap justify-center copy-cell-wrap"
|
||||
>
|
||||
<q-checkbox
|
||||
dense
|
||||
size="24px"
|
||||
class="rule-select-checkbox"
|
||||
color="secondary"
|
||||
:model-value="isCopySelected(props.row)"
|
||||
@update:model-value="(value) => toggleCopySelected(props.row, value)"
|
||||
/>
|
||||
<q-badge
|
||||
v-if="copyRoleLabel(props.row)"
|
||||
color="deep-orange"
|
||||
class="q-ml-xs"
|
||||
>
|
||||
{{ copyRoleLabel(props.row) }}
|
||||
</q-badge>
|
||||
</div>
|
||||
<q-checkbox
|
||||
v-if="col.name === 'select'"
|
||||
v-else-if="col.name === 'select'"
|
||||
dense
|
||||
size="24px"
|
||||
class="rule-select-checkbox"
|
||||
color="positive"
|
||||
:model-value="isRowSelected(props.row)"
|
||||
@update:model-value="(value) => setRowSelected(props.row, value)"
|
||||
/>
|
||||
@@ -219,7 +293,7 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { Notify } from 'quasar'
|
||||
import api from 'src/services/api'
|
||||
import api, { download } from 'src/services/api'
|
||||
import { usePermissionStore } from 'stores/permissionStore'
|
||||
|
||||
const perm = usePermissionStore()
|
||||
@@ -228,7 +302,9 @@ const canUpdate = computed(() => perm.hasApiPermission('pricing:update'))
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const rows = ref([])
|
||||
const selected = ref([])
|
||||
const fileInputRef = ref(null)
|
||||
const selectedKeyMap = ref({})
|
||||
const copySelectedKeys = ref([])
|
||||
const tablePagination = ref({ rowsPerPage: 0, sortBy: 'urun_ilk_grubu', descending: false })
|
||||
let emptyRetryTimer = null
|
||||
|
||||
@@ -238,6 +314,46 @@ const numericFields = new Set([
|
||||
'eur_base', 'eur1', 'eur2', 'eur3', 'eur4', 'eur5', 'eur6', 'eur_step'
|
||||
])
|
||||
|
||||
const importKeyFieldLabels = [
|
||||
['askili_yan', 'ASKILI YAN'],
|
||||
['kategori', 'KATEGORI'],
|
||||
['urun_ilk_grubu', 'URUN ILK GRUBU'],
|
||||
['urun_ana_grubu', 'URUN ANA GRUBU'],
|
||||
['urun_alt_grubu', 'URUN ALT GRUBU'],
|
||||
['icerik', 'ICERIK'],
|
||||
['marka', 'MARKA'],
|
||||
['brand_code', 'BRAND CODE'],
|
||||
['brand_group', 'MARKA GRUBU']
|
||||
]
|
||||
|
||||
const importFieldMap = {
|
||||
AKTIF: 'is_active',
|
||||
'TRY YUVARLAMA': 'try_step',
|
||||
'TRY TABAN': 'try_base',
|
||||
'TRY 1': 'try1',
|
||||
'TRY 2': 'try2',
|
||||
'TRY 3': 'try3',
|
||||
'TRY 4': 'try4',
|
||||
'TRY 5': 'try5',
|
||||
'TRY 6': 'try6',
|
||||
'USD YUVARLAMA': 'usd_step',
|
||||
'USD TABAN': 'usd_base',
|
||||
'USD 1': 'usd1',
|
||||
'USD 2': 'usd2',
|
||||
'USD 3': 'usd3',
|
||||
'USD 4': 'usd4',
|
||||
'USD 5': 'usd5',
|
||||
'USD 6': 'usd6',
|
||||
'EUR YUVARLAMA': 'eur_step',
|
||||
'EUR TABAN': 'eur_base',
|
||||
'EUR 1': 'eur1',
|
||||
'EUR 2': 'eur2',
|
||||
'EUR 3': 'eur3',
|
||||
'EUR 4': 'eur4',
|
||||
'EUR 5': 'eur5',
|
||||
'EUR 6': 'eur6'
|
||||
}
|
||||
|
||||
const multiFilterFields = [
|
||||
'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu', 'urun_ana_grubu',
|
||||
'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group'
|
||||
@@ -265,7 +381,8 @@ function col (name, label, field, width, extra = {}) {
|
||||
}
|
||||
|
||||
const columns = [
|
||||
col('select', '', 'select', 34, { sortable: false, classes: 'selection-col', headerClasses: 'selection-col' }),
|
||||
col('copy_select', 'KOPYA', 'copy_select', 86, { sortable: false, classes: 'copy-selection-col', headerClasses: 'copy-selection-col' }),
|
||||
col('select', 'KAYDET', 'select', 72, { sortable: false, classes: 'save-selection-col', headerClasses: 'save-selection-col' }),
|
||||
col('has_rule', 'DURUM', 'has_rule', 62, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('is_active', 'AKTIF', 'is_active', 48, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('askili_yan', 'ASKILI YAN', 'askili_yan', 86, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
@@ -307,7 +424,7 @@ const columns = [
|
||||
]
|
||||
|
||||
const stickyColumnNames = [
|
||||
'select', 'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu',
|
||||
'copy_select', 'select', 'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu',
|
||||
'urun_ana_grubu', 'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group'
|
||||
]
|
||||
const stickyBoundaryColumnName = 'brand_group'
|
||||
@@ -387,11 +504,32 @@ const filteredRows = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const exportableColumns = computed(() => columns.filter(col => col.name !== 'copy_select' && col.name !== 'select'))
|
||||
const exportedRows = computed(() => {
|
||||
const list = [...filteredRows.value]
|
||||
const sortBy = String(tablePagination.value?.sortBy || '').trim()
|
||||
const descending = Boolean(tablePagination.value?.descending)
|
||||
if (!sortBy) return list
|
||||
|
||||
list.sort((a, b) => {
|
||||
const av = exportSortValue(a, sortBy)
|
||||
const bv = exportSortValue(b, sortBy)
|
||||
if (typeof av === 'number' && typeof bv === 'number') return av - bv
|
||||
return String(av ?? '').localeCompare(String(bv ?? ''), 'tr', { numeric: true, sensitivity: 'base' })
|
||||
})
|
||||
|
||||
return descending ? list.reverse() : list
|
||||
})
|
||||
|
||||
const copySelectedKeySet = computed(() => new Set(copySelectedKeys.value))
|
||||
const visibleRowKeys = computed(() => filteredRows.value.map(row => row._row_key))
|
||||
const selectedVisibleCount = computed(() => visibleRowKeys.value.filter(key => selected.value.some(row => row._row_key === key)).length)
|
||||
const selectedVisibleCount = computed(() => visibleRowKeys.value.reduce((count, key) => count + (selectedKeyMap.value?.[key] ? 1 : 0), 0))
|
||||
const allSelectedVisible = computed(() => visibleRowKeys.value.length > 0 && selectedVisibleCount.value === visibleRowKeys.value.length)
|
||||
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
||||
const selectedDirtyCount = computed(() => selected.value.filter(row => row?._dirty).length)
|
||||
const selectedDirtyCount = computed(() => rows.value.reduce((count, row) => count + (selectedKeyMap.value?.[row._row_key] && row?._dirty ? 1 : 0), 0))
|
||||
const selectedCount = computed(() => Object.keys(selectedKeyMap.value || {}).length)
|
||||
const copySelectedCount = computed(() => copySelectedKeys.value.length)
|
||||
const canCopySelected = computed(() => copySelectedCount.value >= 2)
|
||||
const hasAnyFilter = computed(() => {
|
||||
return [...headerFilterFieldSet].some(field => hasFilter(field))
|
||||
})
|
||||
@@ -456,21 +594,54 @@ function getBodyCellStyle (column) {
|
||||
}
|
||||
|
||||
function isRowSelected (row) {
|
||||
return selected.value.some(item => item._row_key === row._row_key)
|
||||
return !!selectedKeyMap.value?.[row._row_key]
|
||||
}
|
||||
|
||||
function isCopySelected (row) {
|
||||
return copySelectedKeySet.value.has(row._row_key)
|
||||
}
|
||||
|
||||
function setRowSelected (row, value) {
|
||||
const key = row?._row_key
|
||||
if (!key) return
|
||||
const next = { ...(selectedKeyMap.value || {}) }
|
||||
if (value) {
|
||||
if (!isRowSelected(row)) selected.value = [...selected.value, row]
|
||||
return
|
||||
next[key] = true
|
||||
} else {
|
||||
delete next[key]
|
||||
}
|
||||
selected.value = selected.value.filter(item => item._row_key !== row._row_key)
|
||||
selectedKeyMap.value = next
|
||||
}
|
||||
|
||||
function toggleCopySelected (row, value) {
|
||||
const key = row?._row_key
|
||||
if (!key) return
|
||||
const next = [...copySelectedKeys.value]
|
||||
const idx = next.indexOf(key)
|
||||
if (value) {
|
||||
if (idx === -1) next.push(key)
|
||||
} else if (idx >= 0) {
|
||||
next.splice(idx, 1)
|
||||
}
|
||||
copySelectedKeys.value = next
|
||||
}
|
||||
|
||||
function copyRoleLabel (row) {
|
||||
const key = row?._row_key
|
||||
if (!key) return ''
|
||||
const idx = copySelectedKeys.value.indexOf(key)
|
||||
if (idx === 0) return 'Kaynak'
|
||||
if (idx > 0) return 'Hedef'
|
||||
return ''
|
||||
}
|
||||
|
||||
function toggleSelectAllVisible (value) {
|
||||
const keys = new Set(visibleRowKeys.value)
|
||||
const remaining = selected.value.filter(row => !keys.has(row._row_key))
|
||||
selected.value = value ? [...remaining, ...filteredRows.value] : remaining
|
||||
const next = { ...(selectedKeyMap.value || {}) }
|
||||
for (const key of visibleRowKeys.value) {
|
||||
if (value) next[key] = true
|
||||
else delete next[key]
|
||||
}
|
||||
selectedKeyMap.value = next
|
||||
}
|
||||
|
||||
function selectDirtyRow (row) {
|
||||
@@ -487,6 +658,294 @@ function updateNumber (row, field, value) {
|
||||
markDirty(row)
|
||||
}
|
||||
|
||||
function exportSortValue (row, field) {
|
||||
if (field === 'has_rule') return row?.has_rule ? 1 : 0
|
||||
if (field === 'is_active') return row?.is_active ? 1 : 0
|
||||
if (numericFields.has(field)) return finiteNumber(row?.[field], 0)
|
||||
return String(row?.[field] ?? '')
|
||||
}
|
||||
|
||||
function exportCellValue (row, field) {
|
||||
if (field === 'has_rule') return row?.has_rule ? 'Tanimli' : 'Yeni'
|
||||
if (field === 'is_active') return row?.is_active ? 'Aktif' : 'Pasif'
|
||||
if (numericFields.has(field)) {
|
||||
const value = row?.[field]
|
||||
if (value === '' || value === null || value === undefined) return '0'
|
||||
return String(finiteNumber(value, 0))
|
||||
}
|
||||
return String(row?.[field] ?? '').trim()
|
||||
}
|
||||
|
||||
function csvSafe (value) {
|
||||
let text = String(value ?? '').replaceAll('\r', ' ').replaceAll('\n', ' ').trim()
|
||||
if (text.includes(';') || text.includes('"')) {
|
||||
text = `"${text.replaceAll('"', '""')}"`
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
function exportCurrentView () {
|
||||
const cols = exportableColumns.value
|
||||
const list = exportedRows.value
|
||||
if (cols.length === 0 || list.length === 0) return
|
||||
|
||||
const lines = [cols.map(col => csvSafe(col.label)).join(';')]
|
||||
for (const row of list) {
|
||||
lines.push(cols.map(col => csvSafe(exportCellValue(row, col.field))).join(';'))
|
||||
}
|
||||
|
||||
const bom = '\uFEFF'
|
||||
const blob = new Blob([bom + lines.join('\n')], { type: 'text/csv;charset=utf-8;' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `pricing_rules_${new Date().toISOString().slice(0, 10)}.csv`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
async function exportAllFiltered () {
|
||||
try {
|
||||
const params = {
|
||||
askili_yan: (columnFilters.value.askili_yan || []).join(','),
|
||||
kategori: (columnFilters.value.kategori || []).join(','),
|
||||
urun_ilk_grubu: (columnFilters.value.urun_ilk_grubu || []).join(','),
|
||||
urun_ana_grubu: (columnFilters.value.urun_ana_grubu || []).join(','),
|
||||
urun_alt_grubu: (columnFilters.value.urun_alt_grubu || []).join(','),
|
||||
icerik: (columnFilters.value.icerik || []).join(','),
|
||||
marka: (columnFilters.value.marka || []).join(','),
|
||||
brand_code: (columnFilters.value.brand_code || []).join(','),
|
||||
brand_group: (columnFilters.value.brand_group || []).join(','),
|
||||
sort_by: tablePagination.value?.sortBy || '',
|
||||
desc: tablePagination.value?.descending ? 1 : 0
|
||||
}
|
||||
|
||||
for (const field of numericFields) {
|
||||
const min = String(numberRangeFilters.value[field]?.min || '').trim()
|
||||
const max = String(numberRangeFilters.value[field]?.max || '').trim()
|
||||
if (min) params[`${field}_min`] = min
|
||||
if (max) params[`${field}_max`] = max
|
||||
}
|
||||
|
||||
const blob = await download('/pricing/pricing-rules/export-all', params)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `pricing_rules_all_${new Date().toISOString().slice(0, 10)}.csv`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.message || 'Tum filtre export alinamadi' })
|
||||
}
|
||||
}
|
||||
|
||||
function openImportDialog () {
|
||||
fileInputRef.value?.click?.()
|
||||
}
|
||||
|
||||
function parseCsvRows (text) {
|
||||
const clean = String(text || '').replace(/^\uFEFF/, '')
|
||||
const rows = []
|
||||
let cell = ''
|
||||
let row = []
|
||||
let inQuotes = false
|
||||
|
||||
for (let i = 0; i < clean.length; i++) {
|
||||
const ch = clean[i]
|
||||
if (inQuotes) {
|
||||
if (ch === '"') {
|
||||
if (clean[i + 1] === '"') {
|
||||
cell += '"'
|
||||
i += 1
|
||||
} else {
|
||||
inQuotes = false
|
||||
}
|
||||
} else {
|
||||
cell += ch
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (ch === '"') {
|
||||
inQuotes = true
|
||||
continue
|
||||
}
|
||||
if (ch === ';') {
|
||||
row.push(cell)
|
||||
cell = ''
|
||||
continue
|
||||
}
|
||||
if (ch === '\n') {
|
||||
row.push(cell)
|
||||
rows.push(row)
|
||||
row = []
|
||||
cell = ''
|
||||
continue
|
||||
}
|
||||
if (ch !== '\r') {
|
||||
cell += ch
|
||||
}
|
||||
}
|
||||
|
||||
if (cell !== '' || row.length > 0) {
|
||||
row.push(cell)
|
||||
rows.push(row)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
function normalizeImportText (value) {
|
||||
return String(value ?? '').trim().toLocaleUpperCase('tr')
|
||||
}
|
||||
|
||||
function buildImportRowKeyFromObject (row) {
|
||||
return importKeyFieldLabels
|
||||
.map(([field]) => normalizeImportText(row?.[field] ?? ''))
|
||||
.join('|')
|
||||
}
|
||||
|
||||
function parseImportedNumber (value) {
|
||||
const text = String(value ?? '').trim().replace(/\s/g, '')
|
||||
if (!text) return 0
|
||||
|
||||
const lastComma = text.lastIndexOf(',')
|
||||
const lastDot = text.lastIndexOf('.')
|
||||
let normalized = text
|
||||
|
||||
if (lastComma >= 0 && lastDot >= 0) {
|
||||
if (lastComma > lastDot) normalized = text.replace(/\./g, '').replace(',', '.')
|
||||
else normalized = text.replace(/,/g, '')
|
||||
} else if (lastComma >= 0) {
|
||||
normalized = text.replace(/\./g, '').replace(',', '.')
|
||||
} else {
|
||||
normalized = text.replace(/,/g, '')
|
||||
}
|
||||
|
||||
const parsed = Number(normalized)
|
||||
return Number.isFinite(parsed) ? parsed : 0
|
||||
}
|
||||
|
||||
function parseImportedBoolean (value) {
|
||||
const normalized = normalizeImportText(value)
|
||||
if (!normalized) return null
|
||||
if (['AKTIF', 'TRUE', '1', 'EVET', 'YES'].includes(normalized)) return true
|
||||
if (['PASIF', 'FALSE', '0', 'HAYIR', 'NO'].includes(normalized)) return false
|
||||
return null
|
||||
}
|
||||
|
||||
async function onImportFileChange (event) {
|
||||
const input = event?.target
|
||||
const file = input?.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const text = await file.text()
|
||||
const matrix = parseCsvRows(text).filter(row => row.some(cell => String(cell || '').trim() !== ''))
|
||||
if (matrix.length < 2) {
|
||||
Notify.create({ type: 'negative', message: 'CSV bos veya gecersiz' })
|
||||
return
|
||||
}
|
||||
|
||||
const headers = matrix[0].map(cell => String(cell || '').trim())
|
||||
const keyHeaderIndexes = {}
|
||||
for (const [, label] of importKeyFieldLabels) {
|
||||
keyHeaderIndexes[label] = headers.indexOf(label)
|
||||
}
|
||||
|
||||
const missingKeyHeaders = importKeyFieldLabels
|
||||
.map(([, label]) => label)
|
||||
.filter(label => keyHeaderIndexes[label] < 0)
|
||||
if (missingKeyHeaders.length > 0) {
|
||||
Notify.create({ type: 'negative', message: `Eksik kolon: ${missingKeyHeaders.join(', ')}` })
|
||||
return
|
||||
}
|
||||
|
||||
const rowMap = new Map(rows.value.map(row => [buildImportRowKeyFromObject(row), row]))
|
||||
let matched = 0
|
||||
let updated = 0
|
||||
let unmatched = 0
|
||||
|
||||
for (let i = 1; i < matrix.length; i++) {
|
||||
const csvRow = matrix[i]
|
||||
const identity = {}
|
||||
for (const [field, label] of importKeyFieldLabels) {
|
||||
identity[field] = csvRow[keyHeaderIndexes[label]] ?? ''
|
||||
}
|
||||
|
||||
const target = rowMap.get(buildImportRowKeyFromObject(identity))
|
||||
if (!target) {
|
||||
unmatched += 1
|
||||
continue
|
||||
}
|
||||
matched += 1
|
||||
|
||||
let changed = false
|
||||
for (const [headerLabel, field] of Object.entries(importFieldMap)) {
|
||||
const idx = headers.indexOf(headerLabel)
|
||||
if (idx < 0) continue
|
||||
const rawValue = csvRow[idx] ?? ''
|
||||
|
||||
if (field === 'is_active') {
|
||||
const next = parseImportedBoolean(rawValue)
|
||||
if (next === null || next === target.is_active) continue
|
||||
target.is_active = next
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
|
||||
const next = parseImportedNumber(rawValue)
|
||||
if (Number(target[field] || 0) === next) continue
|
||||
target[field] = next
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
markDirty(target)
|
||||
updated += 1
|
||||
}
|
||||
}
|
||||
|
||||
if (updated === 0 && matched === 0) {
|
||||
Notify.create({ type: 'warning', message: 'CSV satirlari ekrandaki kurallarla eslesmedi' })
|
||||
return
|
||||
}
|
||||
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: `CSV yüklendi. Eslesen: ${matched}, guncellenen: ${updated}, eslesmeyen: ${unmatched}`
|
||||
})
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.message || 'CSV okunamadi' })
|
||||
} finally {
|
||||
if (input) input.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function copySelectedToSelected () {
|
||||
const keys = [...copySelectedKeys.value]
|
||||
if (keys.length < 2) return
|
||||
const sourceKey = keys[0]
|
||||
const source = rows.value.find(row => row._row_key === sourceKey)
|
||||
if (!source) return
|
||||
|
||||
for (let i = 1; i < keys.length; i++) {
|
||||
const target = rows.value.find(row => row._row_key === keys[i])
|
||||
if (!target) continue
|
||||
target.is_active = Boolean(source.is_active)
|
||||
for (const field of numericFields) {
|
||||
target[field] = source[field]
|
||||
}
|
||||
markDirty(target)
|
||||
}
|
||||
|
||||
Notify.create({ type: 'positive', message: 'Kopyalama tamamlandi' })
|
||||
}
|
||||
|
||||
function isHeaderFilterField (field) {
|
||||
return headerFilterFieldSet.has(field)
|
||||
}
|
||||
@@ -576,7 +1035,8 @@ async function loadRows () {
|
||||
timeout: 180000
|
||||
})
|
||||
rows.value = (Array.isArray(res?.data) ? res.data : []).map(normalizeWorksheetRow)
|
||||
selected.value = []
|
||||
selectedKeyMap.value = {}
|
||||
copySelectedKeys.value = []
|
||||
if (rows.value.length === 0) {
|
||||
emptyRetryTimer = setTimeout(loadRows, 10000)
|
||||
}
|
||||
@@ -588,7 +1048,7 @@ async function loadRows () {
|
||||
}
|
||||
|
||||
async function saveSelected () {
|
||||
const dirty = selected.value.filter(row => row?._dirty)
|
||||
const dirty = rows.value.filter(row => selectedKeyMap.value?.[row._row_key] && row?._dirty)
|
||||
if (dirty.length === 0) return
|
||||
saving.value = true
|
||||
try {
|
||||
@@ -789,6 +1249,16 @@ onBeforeUnmount(() => {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.selection-header-copy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: #bf5b04;
|
||||
}
|
||||
|
||||
.excel-filter-menu {
|
||||
min-width: 230px;
|
||||
padding: 8px;
|
||||
@@ -829,6 +1299,24 @@ onBeforeUnmount(() => {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rules-table :deep(th.copy-selection-col),
|
||||
.rules-table :deep(td.copy-selection-col) {
|
||||
background: #fff3e8;
|
||||
color: #bf5b04;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.rules-table :deep(th.save-selection-col),
|
||||
.rules-table :deep(td.save-selection-col) {
|
||||
background: #eef9ef;
|
||||
color: #1b7f3a;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.rules-table :deep(th.selection-col),
|
||||
.rules-table :deep(td.selection-col) {
|
||||
background: #fff;
|
||||
@@ -843,6 +1331,20 @@ onBeforeUnmount(() => {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.copy-cell-wrap {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.rules-table :deep(.rule-select-checkbox) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rules-table :deep(.rule-select-checkbox .q-checkbox__inner) {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.rules-table :deep(th.usd-col),
|
||||
.rules-table :deep(td.usd-col) {
|
||||
background: #ecf9f0;
|
||||
@@ -889,4 +1391,13 @@ onBeforeUnmount(() => {
|
||||
outline: none;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
.action-legend :deep(.q-chip) {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hidden-file-input {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user