Compare commits

...

5 commits

Author SHA1 Message Date
nquidox
5639f8031c zero prices period select + delete, tabs refactor
All checks were successful
/ Make image (push) Successful in 46s
2025-12-06 19:13:48 +03:00
nquidox
d42e21bae7 factor out origins colors 2025-12-06 19:13:22 +03:00
nquidox
504f215c5a zero prices fixes
All checks were successful
/ Make image (push) Successful in 4m7s
2025-11-04 16:46:37 +03:00
nquidox
54d814f9b2 image handling fixes 2025-11-04 14:20:08 +03:00
nquidox
7aa2ff1d3a refresh on delete
All checks were successful
/ Make image (push) Successful in 44s
2025-11-02 23:46:22 +03:00
10 changed files with 227 additions and 88 deletions

View file

@ -15,13 +15,26 @@ export const useZeroPrices = () => {
await apiClient.delete('/merch/zeroprices', list) await apiClient.delete('/merch/zeroprices', list)
} }
catch (error) { catch (error) {
console.log('Delete zero prices error: ', error) console.log('Delete target zero prices error: ', error)
throw error throw error
} }
} }
const deleteZeroPricesPeriod = async (start, end) => {
const params = new URLSearchParams({ start, end }).toString()
const url = `/merch/zeroprices/period${params ? `?${params}` : ''}`
const response = await apiClient.delete(url)
if (response.status === 200) {
return response
} else {
console.log('Delete period select zero prices error: ', response)
}
}
return { return {
getZeroPrices, getZeroPrices,
deleteZeroPrices, deleteZeroPrices,
deleteZeroPricesPeriod,
} }
} }

View file

@ -13,6 +13,7 @@ import {
} from 'chart.js' } from 'chart.js'
import { Line } from 'vue-chartjs' import { Line } from 'vue-chartjs'
import 'chartjs-adapter-date-fns' import 'chartjs-adapter-date-fns'
import { originColors } from '@/services/colors.js'
ChartJS.register( ChartJS.register(
Title, Title,
@ -33,11 +34,6 @@ const props = defineProps({
}, },
}) })
const originColors = {
surugaya: '#2d3081',
mandarake: '#924646',
}
const chartData = ref({ const chartData = ref({
datasets: [], datasets: [],
}) })

View file

@ -7,8 +7,10 @@ let refreshPromise = null
function createConfig(options = {}) { function createConfig(options = {}) {
const token = localStorage.getItem('accessToken'); const token = localStorage.getItem('accessToken');
const isFormData = options.body instanceof FormData;
const headers = { const headers = {
'Content-Type': 'application/json', ...(isFormData ? {} : { 'Content-Type': 'application/json' }),
...options.headers, ...options.headers,
}; };

4
src/services/colors.js Normal file
View file

@ -0,0 +1,4 @@
export const originColors = {
surugaya: '#2d3081',
mandarake: '#924646',
};

View file

@ -54,37 +54,21 @@ function onFileInputChange(event) {
event.target.value = '' event.target.value = ''
} }
async function handleUpload({ fileList: newFileList }) { async function fetchImage(bustCache = false) {
const file = newFileList[newFileList.length - 1]
try { try {
await uploadImage(props.merchUuid, file.file) let imgUrl = getImageUrl(props.merchUuid, 'full')
const { imgUrl } = await getImageUrl(props.merchUuid, 'full') if (bustCache) {
const separator = imgUrl.includes('?') ? '&' : '?'
message.success('Image uploaded successfully.') imgUrl += `${separator}_t=${Date.now()}`
fileList.value = [
{
name: file.name,
url: imgUrl,
status: 'finished',
},
]
} catch (error) {
message.error('Upload error: ' + (error.message || 'Unknown error.'))
} }
}
onMounted(async () => {
try {
const imgUrl = getImageUrl(props.merchUuid, 'full');
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const img = new Image(); const img = new Image()
img.src = imgUrl; img.src = imgUrl
img.onload = () => resolve(imgUrl); img.onload = () => resolve(imgUrl)
img.onerror = () => reject(new Error('Image not found')); img.onerror = () => reject(new Error('Image not found'))
}); })
fileList.value = [ fileList.value = [
{ {
@ -92,13 +76,28 @@ onMounted(async () => {
url: imgUrl, url: imgUrl,
status: 'finished', status: 'finished',
}, },
]; ]
} catch (error) { } catch (error) {
fileList.value = []; fileList.value = []
if (!error.message.includes('404')) { if (!error.message.includes('404')) {
console.error('Error getting image: ', error); console.error('Error getting image:', error)
} }
} }
}
async function handleUpload({ fileList: newFileList }) {
const file = newFileList[newFileList.length - 1]
try {
await uploadImage(props.merchUuid, file.file)
message.success('Image uploaded successfully.')
await fetchImage(true)
} catch (error) {
message.error('Upload error: ' + (error.message || 'Unknown error.'))
}
}
onMounted(async () => {
await fetchImage(false)
}); });
const showConfirmDelete = ref(false) const showConfirmDelete = ref(false)

View file

@ -1,49 +1,20 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue' import ScrollToTopButton from '@/components/ScrollToTopButton.vue'
import { useZeroPrices } from '@/api/zeroPrices.js' import TargetZeroesTab from '@/views/ZeroPricesView/TargetZeroesTab.vue'
import ZeroPriceCard from '@/views/ZeroPricesView/ZeroPriceCard.vue' import PeriodSelectTab from '@/views/ZeroPricesView/PeriodSelectTab.vue'
import ZeroPricesToolbar from '@/views/ZeroPricesView/ZeroPricesToolbar.vue'
const { getZeroPrices } = useZeroPrices()
const zeroPrices = ref([])
const toDelete = ref([])
const handleToggle = ({ id, merch_uuid, checked }) => {
if (checked) {
toDelete.value.push({ id, merch_uuid });
} else {
toDelete.value = toDelete.value.filter(item => item.id !== id);
}
}
const fetchZeroPrices = async () => {
try {
const response = await getZeroPrices()
zeroPrices.value = response.data
} catch (error) {
console.error(error)
}
}
onMounted(() => {
fetchZeroPrices()
})
</script> </script>
<template> <template>
<div v-if="zeroPrices.length === 0"> <n-tabs type="line" animated>
<n-h2 class="text-center">Zero prices</n-h2> <n-tab-pane name="Target zeroes" tab="Target zeroes">
<n-h3 class="text-center">No data</n-h3> <TargetZeroesTab />
</div> </n-tab-pane>
<div v-else> <n-tab-pane name="Period select" tab="Period select">
<div class="sticky-search-container"> <PeriodSelectTab />
<ZeroPricesToolbar :selected="toDelete" /> </n-tab-pane>
</div> </n-tabs>
<div v-for="item in zeroPrices" :key="item.created_at"> <ScrollToTopButton />
<ZeroPriceCard :zero-price="item" @toggle="handleToggle" />
</div>
</div>
</template> </template>
<style scoped></style> <style scoped>
</style>

View file

@ -0,0 +1,54 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useZeroPrices } from '@/api/zeroPrices.js'
const range = ref(null)
const setTodayRange = () => {
const now = new Date()
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000)
range.value = [start.getTime(), end.getTime()]
}
onMounted(() => {
setTodayRange()
})
const toRFCtime = (timestamp) => {
return new Date(timestamp).toISOString()
}
const { deleteZeroPricesPeriod } = useZeroPrices()
const deleteEnabled = computed(() => {
return range.value === null
})
const deletePeriod = async () => {
if (range.value !== null) {
const start = toRFCtime(range.value[0])
const end = toRFCtime(range.value[1])
await deleteZeroPricesPeriod(start, end)
} else {
console.log('Delete period select zero prices error')
}
}
</script>
<template>
<n-date-picker
v-model:value="range"
type="datetimerange"
format="HH:mm:ss dd-MM-yyyy"
clearable
/>
<div class="button-container-center">
<n-button class="center-button w360" type="error" :disabled="deleteEnabled" @click="deletePeriod">Delete</n-button>
</div>
</template>
<style scoped></style>

View file

@ -0,0 +1,71 @@
<script setup>
import { onMounted, ref } from 'vue'
import { useZeroPrices } from '@/api/zeroPrices.js'
import ZeroPriceCard from '@/views/ZeroPricesView/ZeroPriceCard.vue'
import ZeroPricesToolbar from '@/views/ZeroPricesView/ZeroPricesToolbar.vue'
const { getZeroPrices } = useZeroPrices()
const zeroPrices = ref([])
const toDelete = ref([])
const handleToggle = ({ id, merch_uuid, checked }) => {
if (checked) {
toDelete.value.push({ id, merch_uuid });
} else {
toDelete.value = toDelete.value.filter(item => item.id !== id);
}
}
const handleSelectAll = () => {
toDelete.value = zeroPrices.value.map(item => ({
id: item.id,
merch_uuid: item.merch_uuid
}))
}
const fetchZeroPrices = async () => {
try {
const response = await getZeroPrices()
zeroPrices.value = Array.isArray(response.data) ? response.data : []
} catch (error) {
console.error(error)
}
}
const handleDeleted = () => {
toDelete.value = []
fetchZeroPrices()
}
onMounted(() => {
fetchZeroPrices()
})
</script>
<template>
<div v-if="zeroPrices.length === 0">
<n-h2 class="text-center">Zero prices</n-h2>
<n-h3 class="text-center">No data</n-h3>
</div>
<div v-else>
<div class="sticky-search-container">
<ZeroPricesToolbar
:selected="toDelete"
@deleted="handleDeleted"
@selectAll="handleSelectAll"
/>
</div>
<div v-for="item in zeroPrices" :key="item.created_at">
<ZeroPriceCard
:zero-price="item"
@toggle="handleToggle"
:checked="toDelete.some(t => t.id === item.id)"
/>
</div>
</div>
</template>
<style scoped>
</style>

View file

@ -1,36 +1,49 @@
<script setup> <script setup>
import { ref } from 'vue' import { computed } from 'vue'
import { originColors } from '@/services/colors.js'
const props = defineProps({ const props = defineProps({
zeroPrice: { zeroPrice: {
type: Object, type: Object,
required: true, required: true,
}, },
checked: {
type: Boolean,
default: false,
},
}) })
const isChecked = ref(false)
const emit = defineEmits(['toggle']) const emit = defineEmits(['toggle'])
const handleCheckboxChange = () => {
isChecked.value = !isChecked.value const handleCheckboxChange = (newValue) => {
emit('toggle', { emit('toggle', {
id: props.zeroPrice.id, id: props.zeroPrice.id,
merch_uuid: props.zeroPrice.merch_uuid, merch_uuid: props.zeroPrice.merch_uuid,
checked: isChecked.value, checked: newValue,
}) })
} }
const currentOriginColor = computed(() => {
return originColors[props.zeroPrice.origin] || '#fff';
});
</script> </script>
<template> <template>
<div class="zeroPriceCard mt-10"> <div class="zeroPriceCard mt-10">
<n-grid responsive="screen" item-responsive cols="4" :x-gap="16" :y-gap="16" class="shift"> <n-grid responsive="screen" item-responsive cols="4" :x-gap="16" :y-gap="16" class="shift">
<n-gi> <n-gi>
<n-checkbox :checked="isChecked" @update:checked="handleCheckboxChange"> <n-checkbox :checked="checked" @update:checked="handleCheckboxChange">
<strong>Delete</strong> <strong>Delete</strong>
</n-checkbox> </n-checkbox>
</n-gi> </n-gi>
<n-gi><strong>Name:</strong> {{ props.zeroPrice.name }}</n-gi> <n-gi><strong>Name:</strong> {{ props.zeroPrice.name }}</n-gi>
<n-gi><strong>Created:</strong> {{ props.zeroPrice.created_at }}</n-gi> <n-gi><strong>Created:</strong> {{ props.zeroPrice.created_at }}</n-gi>
<n-gi><strong>Origin:</strong> {{ props.zeroPrice.origin }}</n-gi> <n-gi
><strong>Origin:</strong>
<span class="bordered" :style="{ borderColor: currentOriginColor }">
{{ props.zeroPrice.origin }}
</span>
</n-gi>
</n-grid> </n-grid>
</div> </div>
</template> </template>
@ -48,4 +61,11 @@ const handleCheckboxChange = () => {
gap: 12px; gap: 12px;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
} }
.bordered {
border: 1px solid;
border-radius: 4px;
padding: 2px;
margin: 3px;
}
</style> </style>

View file

@ -10,20 +10,26 @@ const props = defineProps({
const messages = useMessage() const messages = useMessage()
const { deleteZeroPrices } = useZeroPrices() const { deleteZeroPrices } = useZeroPrices()
const emit = defineEmits(['deleted', 'selectAll'])
const handleDelete = async () => { const handleDelete = async () => {
try { try {
await deleteZeroPrices(props.selected) await deleteZeroPrices(props.selected)
messages.success("Selected zero prices deleted") messages.success("Selected zero prices deleted")
emit('deleted')
} catch (error) { } catch (error) {
console.log(error) console.log(error)
messages.error("Error deleting selected prices") messages.error("Error deleting selected prices")
} }
} }
const handleSelectAll = async () => {
emit('selectAll')
}
</script> </script>
<template> <template>
{{ props.selected }}
<div class="toolbar button-container-evenly padding-lr-30"> <div class="toolbar button-container-evenly padding-lr-30">
<div v-if="props.selected.length === 0" class="toolbar-item"> <div v-if="props.selected.length === 0" class="toolbar-item">
<span>Select records to delete</span> <span>Select records to delete</span>
@ -31,6 +37,9 @@ const handleDelete = async () => {
<div v-else class="toolbar-item"> <div v-else class="toolbar-item">
{{ props.selected.length }} items selected {{ props.selected.length }} items selected
</div> </div>
<div class="toolbar-item">
<span @click="handleSelectAll">Click here to select all</span>
</div>
<div class="toolbar-item"> <div class="toolbar-item">
<n-button <n-button