refaktoryzacja service api.
wydzielenie mockow. dodanie parsowania wyszukiwania
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"lint": "eslint -c ./eslint.config.js \"./src*/**/*.{js,cjs,mjs,vue}\"",
|
||||
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"test": "node --test \"src/**/*.test.js\"",
|
||||
"dev": "quasar dev",
|
||||
"build": "quasar build",
|
||||
"postinstall": "quasar prepare"
|
||||
@@ -41,4 +41,4 @@
|
||||
"yarn": ">= 1.21.1",
|
||||
"pnpm": ">= 10.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
frontend/src/mocks/api/dictionariesMockApi.js
Normal file
19
frontend/src/mocks/api/dictionariesMockApi.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import mockDictionaries from 'src/mocks/dictionaries.json'
|
||||
import { waitForMockApi } from 'src/services/apiMode'
|
||||
|
||||
export async function fetchDictionaries() {
|
||||
await waitForMockApi()
|
||||
|
||||
return normalizeDictionaries(mockDictionaries)
|
||||
}
|
||||
|
||||
function normalizeDictionaries(data) {
|
||||
return {
|
||||
models: data.models ?? [],
|
||||
clients: data.clients ?? [],
|
||||
finishes: data.finishes ?? [],
|
||||
productionLists: data.productionLists ?? [],
|
||||
operations: data.operations ?? [],
|
||||
colors: data.colors ?? [],
|
||||
}
|
||||
}
|
||||
43
frontend/src/mocks/api/productSpecificationMockApi.js
Normal file
43
frontend/src/mocks/api/productSpecificationMockApi.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import mockSpecifications from 'src/mocks/specifications.json'
|
||||
import { waitForMockApi } from 'src/services/apiMode'
|
||||
|
||||
export async function fetchProductSpecification(productId) {
|
||||
await waitForMockApi()
|
||||
|
||||
return normalizeSpecificationResponse(getMockSpecification(productId))
|
||||
}
|
||||
|
||||
export async function refreshProductSpecification(productId) {
|
||||
await waitForMockApi()
|
||||
|
||||
return normalizeSpecificationResponse({
|
||||
...getMockSpecification(productId),
|
||||
lastFetchedAt: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
function getMockSpecification(productId) {
|
||||
return (
|
||||
mockSpecifications[productId] ?? {
|
||||
productId,
|
||||
orderId: null,
|
||||
sourceUrl: null,
|
||||
lastFetchedAt: null,
|
||||
specification: {
|
||||
sections: [],
|
||||
},
|
||||
diff: [],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeSpecificationResponse(data) {
|
||||
return {
|
||||
productId: data.productId,
|
||||
orderId: data.orderId ?? null,
|
||||
sourceUrl: data.sourceUrl ?? null,
|
||||
lastFetchedAt: data.lastFetchedAt ?? null,
|
||||
sections: data.specification?.sections ?? data.sections ?? [],
|
||||
diff: data.diff ?? [],
|
||||
}
|
||||
}
|
||||
81
frontend/src/mocks/api/productTimelineMockApi.js
Normal file
81
frontend/src/mocks/api/productTimelineMockApi.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import mockTimelines from 'src/mocks/timelines.json'
|
||||
import { waitForMockApi } from 'src/services/apiMode'
|
||||
|
||||
const timelinesByProductId = structuredClone(mockTimelines)
|
||||
|
||||
export async function fetchProductTimeline(productId) {
|
||||
await waitForMockApi()
|
||||
|
||||
return getMockTimeline(productId)
|
||||
}
|
||||
|
||||
export async function createProductTimelineEvent(productId, payload) {
|
||||
await waitForMockApi()
|
||||
|
||||
const timeline = getMockTimeline(productId)
|
||||
const event = createMockEvent(productId, payload)
|
||||
|
||||
timeline.events.push(event)
|
||||
timeline.timelinePreview = createTimelinePreview(timeline.events)
|
||||
|
||||
return {
|
||||
event,
|
||||
timelinePreview: timeline.timelinePreview,
|
||||
}
|
||||
}
|
||||
|
||||
function getMockTimeline(productId) {
|
||||
if (!timelinesByProductId[productId]) {
|
||||
timelinesByProductId[productId] = {
|
||||
productId,
|
||||
events: [],
|
||||
timelinePreview: {
|
||||
body: [],
|
||||
neck: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return timelinesByProductId[productId]
|
||||
}
|
||||
|
||||
function createMockEvent(productId, payload) {
|
||||
const now = Date.now()
|
||||
|
||||
return {
|
||||
id: now,
|
||||
productId,
|
||||
partId: payload.partId ?? null,
|
||||
partType: normalizePartType(payload.partType),
|
||||
type: payload.type ?? 'operation',
|
||||
operationId: payload.operationId ?? null,
|
||||
operationCode: payload.operationCode ?? payload.code ?? 'NEW',
|
||||
operationName: payload.operationName ?? payload.label ?? 'Nowa operacja',
|
||||
date: payload.date ?? new Date().toISOString().slice(0, 10),
|
||||
note: payload.note ?? null,
|
||||
photosCount: payload.photosCount ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
function createTimelinePreview(events) {
|
||||
return {
|
||||
body: createPartPreview(events, 'BODY'),
|
||||
neck: createPartPreview(events, 'NECK'),
|
||||
}
|
||||
}
|
||||
|
||||
function createPartPreview(events, partType) {
|
||||
return events
|
||||
.filter((event) => event.type === 'operation' && event.partType === partType)
|
||||
.map((event) => ({
|
||||
id: event.id,
|
||||
code: event.operationCode,
|
||||
label: event.operationName,
|
||||
date: event.date,
|
||||
status: 'done',
|
||||
}))
|
||||
}
|
||||
|
||||
function normalizePartType(partType) {
|
||||
return String(partType ?? 'BODY').toUpperCase()
|
||||
}
|
||||
83
frontend/src/mocks/api/productsMockApi.js
Normal file
83
frontend/src/mocks/api/productsMockApi.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import mockProducts from 'src/mocks/products.json'
|
||||
import { waitForMockApi } from 'src/services/apiMode'
|
||||
|
||||
const mockItems = [...mockProducts.items]
|
||||
|
||||
export async function fetchProducts(params) {
|
||||
await waitForMockApi()
|
||||
|
||||
const filteredItems = filterMockProducts(mockItems, params)
|
||||
const offset = Number(params?.offset ?? 0)
|
||||
const limit = Number(params?.limit ?? filteredItems.length)
|
||||
const items = filteredItems.slice(offset, offset + limit)
|
||||
|
||||
return {
|
||||
items,
|
||||
pageInfo: {
|
||||
limit,
|
||||
offset,
|
||||
hasMore: offset + limit < filteredItems.length,
|
||||
total: filteredItems.length,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function filterMockProducts(items, params = {}) {
|
||||
return items.filter((product) => {
|
||||
if (!matchesOrderSearch(product, params)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (params.finish && product.finish !== params.finish) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (params.year && product.orderYear !== Number(params.year)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (params.productionList && !product.productionLists.includes(params.productionList)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function matchesOrderSearch(product, params = {}) {
|
||||
const search = params.search?.trim()
|
||||
const orderSearch = params.orderSearch ?? []
|
||||
|
||||
if (!search) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!orderSearch.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return orderSearch.some((condition) => matchesOrderCondition(product, condition))
|
||||
}
|
||||
|
||||
function matchesOrderCondition(product, condition) {
|
||||
const productOrderNumber = Number(product.orderNumber)
|
||||
const productOrderNumberText = String(productOrderNumber)
|
||||
|
||||
if (condition.type === 'orderNumberPrefix') {
|
||||
return productOrderNumberText.startsWith(condition.orderNumberPrefix)
|
||||
}
|
||||
|
||||
if (condition.orderNumber !== null && productOrderNumber !== condition.orderNumber) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (condition.orderYear !== null && product.orderYear !== condition.orderYear) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (condition.orderIndex !== null && product.orderIndex !== condition.orderIndex) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -78,7 +78,7 @@ import { useProductsStore } from 'src/stores/productsStore'
|
||||
|
||||
const productsStore = useProductsStore()
|
||||
const uiStore = useUiStore()
|
||||
const searchQuery = ref(productsStore.filters.search)
|
||||
const searchQuery = ref(productsStore.searchQuery)
|
||||
const activeMonth = ref('all')
|
||||
|
||||
const monthFilters = [
|
||||
@@ -98,7 +98,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
watch(searchQuery, (value) => {
|
||||
productsStore.setFilters({ search: value })
|
||||
productsStore.setSearchQuery(value)
|
||||
})
|
||||
|
||||
function openAdvancedSearch() {
|
||||
@@ -135,7 +135,7 @@ function openProductionStatuses({ product, partType } = {}) {
|
||||
}
|
||||
|
||||
function applySearch() {
|
||||
productsStore.applyFilters({ search: searchQuery.value })
|
||||
productsStore.applySearch(searchQuery.value)
|
||||
}
|
||||
|
||||
function selectMonth(month) {
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
import { api } from 'src/boot/axios'
|
||||
import mockDictionaries from 'src/mocks/dictionaries.json'
|
||||
import { USE_MOCK_API, waitForMockApi } from 'src/services/apiMode'
|
||||
import { fetchDictionaries as fetchDictionariesFromHttp } from 'src/services/dictionariesHttpApi'
|
||||
import { fetchDictionaries as fetchDictionariesFromMock } from 'src/mocks/api/dictionariesMockApi'
|
||||
import { USE_MOCK_API } from 'src/services/apiMode'
|
||||
|
||||
export async function fetchDictionaries() {
|
||||
if (USE_MOCK_API) {
|
||||
await waitForMockApi()
|
||||
|
||||
return normalizeDictionaries(mockDictionaries)
|
||||
}
|
||||
|
||||
const response = await api.get('/mayo-api/dictionaries')
|
||||
|
||||
return normalizeDictionaries(response.data)
|
||||
}
|
||||
|
||||
function normalizeDictionaries(data) {
|
||||
return {
|
||||
models: data.models ?? [],
|
||||
clients: data.clients ?? [],
|
||||
finishes: data.finishes ?? [],
|
||||
productionLists: data.productionLists ?? [],
|
||||
operations: data.operations ?? [],
|
||||
colors: data.colors ?? [],
|
||||
}
|
||||
}
|
||||
export const fetchDictionaries = USE_MOCK_API ? fetchDictionariesFromMock : fetchDictionariesFromHttp
|
||||
|
||||
18
frontend/src/services/dictionariesHttpApi.js
Normal file
18
frontend/src/services/dictionariesHttpApi.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { api } from 'src/boot/axios'
|
||||
|
||||
export async function fetchDictionaries() {
|
||||
const response = await api.get('/mayo-api/dictionaries')
|
||||
|
||||
return normalizeDictionaries(response.data)
|
||||
}
|
||||
|
||||
function normalizeDictionaries(data) {
|
||||
return {
|
||||
models: data.models ?? [],
|
||||
clients: data.clients ?? [],
|
||||
finishes: data.finishes ?? [],
|
||||
productionLists: data.productionLists ?? [],
|
||||
operations: data.operations ?? [],
|
||||
colors: data.colors ?? [],
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,17 @@
|
||||
import { api } from 'src/boot/axios'
|
||||
import mockSpecifications from 'src/mocks/specifications.json'
|
||||
import { USE_MOCK_API, waitForMockApi } from 'src/services/apiMode'
|
||||
import {
|
||||
fetchProductSpecification as fetchProductSpecificationFromHttp,
|
||||
refreshProductSpecification as refreshProductSpecificationFromHttp,
|
||||
} from 'src/services/productSpecificationHttpApi'
|
||||
import {
|
||||
fetchProductSpecification as fetchProductSpecificationFromMock,
|
||||
refreshProductSpecification as refreshProductSpecificationFromMock,
|
||||
} from 'src/mocks/api/productSpecificationMockApi'
|
||||
import { USE_MOCK_API } from 'src/services/apiMode'
|
||||
|
||||
function normalizeSpecificationResponse(data) {
|
||||
return {
|
||||
productId: data.productId,
|
||||
orderId: data.orderId ?? null,
|
||||
sourceUrl: data.sourceUrl ?? null,
|
||||
lastFetchedAt: data.lastFetchedAt ?? null,
|
||||
sections: data.specification?.sections ?? data.sections ?? [],
|
||||
diff: data.diff ?? [],
|
||||
}
|
||||
}
|
||||
export const fetchProductSpecification = USE_MOCK_API
|
||||
? fetchProductSpecificationFromMock
|
||||
: fetchProductSpecificationFromHttp
|
||||
|
||||
export async function fetchProductSpecification(productId) {
|
||||
if (USE_MOCK_API) {
|
||||
await waitForMockApi()
|
||||
|
||||
return normalizeSpecificationResponse(getMockSpecification(productId))
|
||||
}
|
||||
|
||||
const response = await api.get(`/mayo-api/products/${productId}/specification`)
|
||||
|
||||
return normalizeSpecificationResponse(response.data)
|
||||
}
|
||||
|
||||
export async function refreshProductSpecification(productId) {
|
||||
if (USE_MOCK_API) {
|
||||
await waitForMockApi()
|
||||
|
||||
return normalizeSpecificationResponse({
|
||||
...getMockSpecification(productId),
|
||||
lastFetchedAt: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
const response = await api.post(`/mayo-api/products/${productId}/specification/refresh`)
|
||||
|
||||
return normalizeSpecificationResponse(response.data)
|
||||
}
|
||||
|
||||
function getMockSpecification(productId) {
|
||||
return (
|
||||
mockSpecifications[productId] ?? {
|
||||
productId,
|
||||
orderId: null,
|
||||
sourceUrl: null,
|
||||
lastFetchedAt: null,
|
||||
specification: {
|
||||
sections: [],
|
||||
},
|
||||
diff: [],
|
||||
}
|
||||
)
|
||||
}
|
||||
export const refreshProductSpecification = USE_MOCK_API
|
||||
? refreshProductSpecificationFromMock
|
||||
: refreshProductSpecificationFromHttp
|
||||
|
||||
24
frontend/src/services/productSpecificationHttpApi.js
Normal file
24
frontend/src/services/productSpecificationHttpApi.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { api } from 'src/boot/axios'
|
||||
|
||||
export async function fetchProductSpecification(productId) {
|
||||
const response = await api.get(`/mayo-api/products/${productId}/specification`)
|
||||
|
||||
return normalizeSpecificationResponse(response.data)
|
||||
}
|
||||
|
||||
export async function refreshProductSpecification(productId) {
|
||||
const response = await api.post(`/mayo-api/products/${productId}/specification/refresh`)
|
||||
|
||||
return normalizeSpecificationResponse(response.data)
|
||||
}
|
||||
|
||||
function normalizeSpecificationResponse(data) {
|
||||
return {
|
||||
productId: data.productId,
|
||||
orderId: data.orderId ?? null,
|
||||
sourceUrl: data.sourceUrl ?? null,
|
||||
lastFetchedAt: data.lastFetchedAt ?? null,
|
||||
sections: data.specification?.sections ?? data.sections ?? [],
|
||||
diff: data.diff ?? [],
|
||||
}
|
||||
}
|
||||
@@ -1,104 +1,17 @@
|
||||
import { api } from 'src/boot/axios'
|
||||
import mockTimelines from 'src/mocks/timelines.json'
|
||||
import { USE_MOCK_API, waitForMockApi } from 'src/services/apiMode'
|
||||
import {
|
||||
createProductTimelineEvent as createProductTimelineEventFromHttp,
|
||||
fetchProductTimeline as fetchProductTimelineFromHttp,
|
||||
} from 'src/services/productTimelineHttpApi'
|
||||
import {
|
||||
createProductTimelineEvent as createProductTimelineEventFromMock,
|
||||
fetchProductTimeline as fetchProductTimelineFromMock,
|
||||
} from 'src/mocks/api/productTimelineMockApi'
|
||||
import { USE_MOCK_API } from 'src/services/apiMode'
|
||||
|
||||
const timelinesByProductId = structuredClone(mockTimelines)
|
||||
export const fetchProductTimeline = USE_MOCK_API
|
||||
? fetchProductTimelineFromMock
|
||||
: fetchProductTimelineFromHttp
|
||||
|
||||
export async function fetchProductTimeline(productId) {
|
||||
if (USE_MOCK_API) {
|
||||
await waitForMockApi()
|
||||
|
||||
return timelinesByProductId[productId] ?? {
|
||||
productId,
|
||||
events: [],
|
||||
timelinePreview: {
|
||||
body: [],
|
||||
neck: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const response = await api.get(`/mayo-api/products/${productId}/timeline`)
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function createProductTimelineEvent(productId, payload) {
|
||||
if (USE_MOCK_API) {
|
||||
await waitForMockApi()
|
||||
|
||||
const timeline = ensureMockTimeline(productId)
|
||||
const event = createMockEvent(productId, payload)
|
||||
|
||||
timeline.events.push(event)
|
||||
timeline.timelinePreview = createTimelinePreview(timeline.events)
|
||||
|
||||
return {
|
||||
event,
|
||||
timelinePreview: timeline.timelinePreview,
|
||||
}
|
||||
}
|
||||
|
||||
const response = await api.post(`/mayo-api/products/${productId}/timeline/events`, payload)
|
||||
|
||||
return {
|
||||
event: response.data.event ?? response.data,
|
||||
timelinePreview: response.data.timelinePreview,
|
||||
}
|
||||
}
|
||||
|
||||
function ensureMockTimeline(productId) {
|
||||
if (!timelinesByProductId[productId]) {
|
||||
timelinesByProductId[productId] = {
|
||||
productId,
|
||||
events: [],
|
||||
timelinePreview: {
|
||||
body: [],
|
||||
neck: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return timelinesByProductId[productId]
|
||||
}
|
||||
|
||||
function createMockEvent(productId, payload) {
|
||||
const now = Date.now()
|
||||
|
||||
return {
|
||||
id: now,
|
||||
productId,
|
||||
partId: payload.partId ?? null,
|
||||
partType: normalizePartType(payload.partType),
|
||||
type: payload.type ?? 'operation',
|
||||
operationId: payload.operationId ?? null,
|
||||
operationCode: payload.operationCode ?? payload.code ?? 'NEW',
|
||||
operationName: payload.operationName ?? payload.label ?? 'Nowa operacja',
|
||||
date: payload.date ?? new Date().toISOString().slice(0, 10),
|
||||
note: payload.note ?? null,
|
||||
photosCount: payload.photosCount ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
function createTimelinePreview(events) {
|
||||
return {
|
||||
body: createPartPreview(events, 'BODY'),
|
||||
neck: createPartPreview(events, 'NECK'),
|
||||
}
|
||||
}
|
||||
|
||||
function createPartPreview(events, partType) {
|
||||
return events
|
||||
.filter((event) => event.type === 'operation' && event.partType === partType)
|
||||
.map((event) => ({
|
||||
id: event.id,
|
||||
code: event.operationCode,
|
||||
label: event.operationName,
|
||||
date: event.date,
|
||||
status: 'done',
|
||||
}))
|
||||
}
|
||||
|
||||
function normalizePartType(partType) {
|
||||
return String(partType ?? 'BODY').toUpperCase()
|
||||
}
|
||||
export const createProductTimelineEvent = USE_MOCK_API
|
||||
? createProductTimelineEventFromMock
|
||||
: createProductTimelineEventFromHttp
|
||||
|
||||
16
frontend/src/services/productTimelineHttpApi.js
Normal file
16
frontend/src/services/productTimelineHttpApi.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { api } from 'src/boot/axios'
|
||||
|
||||
export async function fetchProductTimeline(productId) {
|
||||
const response = await api.get(`/mayo-api/products/${productId}/timeline`)
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function createProductTimelineEvent(productId, payload) {
|
||||
const response = await api.post(`/mayo-api/products/${productId}/timeline/events`, payload)
|
||||
|
||||
return {
|
||||
event: response.data.event ?? response.data,
|
||||
timelinePreview: response.data.timelinePreview,
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,5 @@
|
||||
import { api } from 'src/boot/axios'
|
||||
import mockProducts from 'src/mocks/products.json'
|
||||
import { USE_MOCK_API, waitForMockApi } from 'src/services/apiMode'
|
||||
import { fetchProducts as fetchProductsFromHttp } from 'src/services/productsHttpApi'
|
||||
import { fetchProducts as fetchProductsFromMock } from 'src/mocks/api/productsMockApi'
|
||||
import { USE_MOCK_API } from 'src/services/apiMode'
|
||||
|
||||
const mockItems = [...mockProducts.items]
|
||||
|
||||
export async function fetchProducts(params) {
|
||||
if (USE_MOCK_API) {
|
||||
await waitForMockApi()
|
||||
|
||||
const filteredItems = filterMockProducts(mockItems, params)
|
||||
const offset = Number(params?.offset ?? 0)
|
||||
const limit = Number(params?.limit ?? filteredItems.length)
|
||||
const items = filteredItems.slice(offset, offset + limit)
|
||||
|
||||
return {
|
||||
items,
|
||||
pageInfo: {
|
||||
limit,
|
||||
offset,
|
||||
hasMore: offset + limit < filteredItems.length,
|
||||
total: filteredItems.length,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const response = await api.get('/mayo-api/products', { params })
|
||||
|
||||
return {
|
||||
items: response.data.items ?? response.data,
|
||||
pageInfo: response.data.pageInfo ?? {},
|
||||
}
|
||||
}
|
||||
|
||||
function filterMockProducts(items, params = {}) {
|
||||
return items.filter((product) => {
|
||||
const search = params.search?.toLowerCase()
|
||||
const productionList = params.productionList
|
||||
|
||||
if (search && !matchesSearch(product, search)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (params.finish && product.finish !== params.finish) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (params.year && product.orderYear !== Number(params.year)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (productionList && !product.productionLists.includes(productionList)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function matchesSearch(product, search) {
|
||||
return [product.orderId, product.model, product.client, product.finish]
|
||||
.filter(Boolean)
|
||||
.some((value) => value.toLowerCase().includes(search))
|
||||
}
|
||||
export const fetchProducts = USE_MOCK_API ? fetchProductsFromMock : fetchProductsFromHttp
|
||||
|
||||
19
frontend/src/services/productsHttpApi.js
Normal file
19
frontend/src/services/productsHttpApi.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { api } from 'src/boot/axios'
|
||||
|
||||
export async function fetchProducts(params) {
|
||||
const response = await api.get('/mayo-api/products', {
|
||||
params: serializeProductsParams(params),
|
||||
})
|
||||
|
||||
return {
|
||||
items: response.data.items ?? response.data,
|
||||
pageInfo: response.data.pageInfo ?? {},
|
||||
}
|
||||
}
|
||||
|
||||
function serializeProductsParams(params = {}) {
|
||||
return {
|
||||
...params,
|
||||
orderSearch: params.orderSearch?.length ? JSON.stringify(params.orderSearch) : undefined,
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
import { fetchProducts } from 'src/services/productsApi'
|
||||
import { parseOrderSearchQuery } from 'src/utils/orderSearchParser'
|
||||
|
||||
const DEFAULT_LIMIT = 30
|
||||
|
||||
function createDefaultFilters() {
|
||||
return {
|
||||
search: '',
|
||||
modelId: null,
|
||||
clientId: null,
|
||||
finish: null,
|
||||
@@ -19,6 +19,8 @@ export const useProductsStore = defineStore('products', () => {
|
||||
const ids = ref([])
|
||||
const byId = ref({})
|
||||
const filters = ref(createDefaultFilters())
|
||||
const searchQuery = ref('')
|
||||
const orderSearch = ref([])
|
||||
const limit = ref(DEFAULT_LIMIT)
|
||||
const offset = ref(0)
|
||||
const total = ref(null)
|
||||
@@ -33,7 +35,8 @@ export const useProductsStore = defineStore('products', () => {
|
||||
return {
|
||||
limit: limit.value,
|
||||
offset: offset.value,
|
||||
search: filters.value.search || undefined,
|
||||
search: searchQuery.value || undefined,
|
||||
orderSearch: orderSearch.value.length ? orderSearch.value : undefined,
|
||||
model: filters.value.modelId || undefined,
|
||||
client: filters.value.clientId || undefined,
|
||||
finish: filters.value.finish || undefined,
|
||||
@@ -95,11 +98,21 @@ export const useProductsStore = defineStore('products', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function setSearchQuery(value) {
|
||||
searchQuery.value = value
|
||||
orderSearch.value = parseOrderSearchQuery(value)
|
||||
}
|
||||
|
||||
async function applyFilters(nextFilters) {
|
||||
setFilters(nextFilters)
|
||||
await fetchFirstPage()
|
||||
}
|
||||
|
||||
async function applySearch(value) {
|
||||
setSearchQuery(value)
|
||||
await fetchFirstPage()
|
||||
}
|
||||
|
||||
function updateProduct(productId, patch) {
|
||||
const current = byId.value[productId]
|
||||
|
||||
@@ -133,6 +146,8 @@ export const useProductsStore = defineStore('products', () => {
|
||||
ids,
|
||||
byId,
|
||||
filters,
|
||||
searchQuery,
|
||||
orderSearch,
|
||||
limit,
|
||||
offset,
|
||||
total,
|
||||
@@ -144,7 +159,9 @@ export const useProductsStore = defineStore('products', () => {
|
||||
fetchFirstPage,
|
||||
fetchNextPage,
|
||||
setFilters,
|
||||
setSearchQuery,
|
||||
applyFilters,
|
||||
applySearch,
|
||||
updateProduct,
|
||||
applyTimelinePreviewUpdate,
|
||||
clear,
|
||||
|
||||
141
frontend/src/utils/orderSearchParser.js
Normal file
141
frontend/src/utils/orderSearchParser.js
Normal file
@@ -0,0 +1,141 @@
|
||||
const TOKEN_SEPARATOR_PATTERN = /[\s,.]+/
|
||||
const ORDER_NUMBER_PATTERN = /^\d{1,4}$/
|
||||
const YEAR_PATTERN = /^\d{4}$/
|
||||
const ORDER_INDEX_PATTERN = /^\d{1,2}$/
|
||||
const MIN_ORDER_YEAR = 2021
|
||||
|
||||
export function parseOrderSearchQuery(query) {
|
||||
return tokenizeSearchQuery(query).map(parseOrderSearchToken).filter(Boolean)
|
||||
}
|
||||
|
||||
export function tokenizeSearchQuery(query) {
|
||||
return String(query ?? '')
|
||||
.trim()
|
||||
.split(TOKEN_SEPARATOR_PATTERN)
|
||||
.map((token) => token.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function parseOrderSearchToken(token) {
|
||||
const parts = token.split('/').map((part) => part.trim())
|
||||
|
||||
if (parts.length === 1) {
|
||||
return parseOrderNumberOnly(parts[0], token)
|
||||
}
|
||||
|
||||
if (parts.length === 2) {
|
||||
return parseTwoPartOrderToken(parts, token)
|
||||
}
|
||||
|
||||
if (parts.length === 3) {
|
||||
return parseFullOrderToken(parts, token)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function parseOrderNumberOnly(orderNumberText, raw) {
|
||||
if (!isValidOrderNumber(orderNumberText)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const normalizedOrderNumber = normalizeNumber(orderNumberText)
|
||||
const hasLeadingZeros = orderNumberText.length > 1 && orderNumberText.startsWith('0')
|
||||
const isFullWidthOrderNumber = orderNumberText.length === 4
|
||||
|
||||
if (hasLeadingZeros || isFullWidthOrderNumber) {
|
||||
return {
|
||||
raw,
|
||||
type: 'orderNumber',
|
||||
match: 'exact',
|
||||
orderNumber: normalizedOrderNumber,
|
||||
orderNumberPrefix: null,
|
||||
orderYear: null,
|
||||
orderIndex: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
raw,
|
||||
type: 'orderNumberPrefix',
|
||||
match: 'prefix',
|
||||
orderNumber: null,
|
||||
orderNumberPrefix: orderNumberText,
|
||||
orderYear: null,
|
||||
orderIndex: null,
|
||||
}
|
||||
}
|
||||
|
||||
function parseTwoPartOrderToken(parts, raw) {
|
||||
const [orderNumberText, secondPart] = parts
|
||||
|
||||
if (!isValidOrderNumber(orderNumberText)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isValidOrderYear(secondPart)) {
|
||||
return {
|
||||
raw,
|
||||
type: 'orderNumberYear',
|
||||
match: 'exact',
|
||||
orderNumber: normalizeNumber(orderNumberText),
|
||||
orderNumberPrefix: null,
|
||||
orderYear: normalizeNumber(secondPart),
|
||||
orderIndex: null,
|
||||
}
|
||||
}
|
||||
|
||||
if (isValidOrderIndex(secondPart)) {
|
||||
return {
|
||||
raw,
|
||||
type: 'orderNumberIndex',
|
||||
match: 'exact',
|
||||
orderNumber: normalizeNumber(orderNumberText),
|
||||
orderNumberPrefix: null,
|
||||
orderYear: null,
|
||||
orderIndex: normalizeNumber(secondPart),
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function parseFullOrderToken(parts, raw) {
|
||||
const [orderNumberText, orderYearText, orderIndexText] = parts
|
||||
|
||||
if (
|
||||
!isValidOrderNumber(orderNumberText) ||
|
||||
!isValidOrderYear(orderYearText) ||
|
||||
!isValidOrderIndex(orderIndexText)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
raw,
|
||||
type: 'fullOrderId',
|
||||
match: 'exact',
|
||||
orderNumber: normalizeNumber(orderNumberText),
|
||||
orderNumberPrefix: null,
|
||||
orderYear: normalizeNumber(orderYearText),
|
||||
orderIndex: normalizeNumber(orderIndexText),
|
||||
}
|
||||
}
|
||||
|
||||
function isValidOrderNumber(value) {
|
||||
return ORDER_NUMBER_PATTERN.test(value) && normalizeNumber(value) >= 1
|
||||
}
|
||||
|
||||
function isValidOrderYear(value) {
|
||||
return YEAR_PATTERN.test(value) && normalizeNumber(value) >= MIN_ORDER_YEAR
|
||||
}
|
||||
|
||||
function isValidOrderIndex(value) {
|
||||
const normalized = normalizeNumber(value)
|
||||
|
||||
return ORDER_INDEX_PATTERN.test(value) && normalized >= 1 && normalized <= 99
|
||||
}
|
||||
|
||||
function normalizeNumber(value) {
|
||||
return Number.parseInt(value, 10)
|
||||
}
|
||||
134
frontend/src/utils/orderSearchParser.test.js
Normal file
134
frontend/src/utils/orderSearchParser.test.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
import {
|
||||
parseOrderSearchQuery,
|
||||
parseOrderSearchToken,
|
||||
tokenizeSearchQuery,
|
||||
} from './orderSearchParser.js'
|
||||
|
||||
describe('tokenizeSearchQuery', () => {
|
||||
it('splits tokens by spaces, commas and dots', () => {
|
||||
assert.deepEqual(tokenizeSearchQuery('123/1 0200/2,333/2025/3.444/2024'), [
|
||||
'123/1',
|
||||
'0200/2',
|
||||
'333/2025/3',
|
||||
'444/2024',
|
||||
])
|
||||
})
|
||||
|
||||
it('ignores empty input', () => {
|
||||
assert.deepEqual(tokenizeSearchQuery(' , . '), [])
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseOrderSearchToken', () => {
|
||||
it('parses full order id', () => {
|
||||
assert.deepEqual(parseOrderSearchToken('0012/2025/01'), {
|
||||
raw: '0012/2025/01',
|
||||
type: 'fullOrderId',
|
||||
match: 'exact',
|
||||
orderNumber: 12,
|
||||
orderNumberPrefix: null,
|
||||
orderYear: 2025,
|
||||
orderIndex: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('parses padded order number as exact order number', () => {
|
||||
assert.deepEqual(parseOrderSearchToken('0012'), {
|
||||
raw: '0012',
|
||||
type: 'orderNumber',
|
||||
match: 'exact',
|
||||
orderNumber: 12,
|
||||
orderNumberPrefix: null,
|
||||
orderYear: null,
|
||||
orderIndex: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('parses unpadded order number as prefix search', () => {
|
||||
assert.deepEqual(parseOrderSearchToken('12'), {
|
||||
raw: '12',
|
||||
type: 'orderNumberPrefix',
|
||||
match: 'prefix',
|
||||
orderNumber: null,
|
||||
orderNumberPrefix: '12',
|
||||
orderYear: null,
|
||||
orderIndex: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('parses order number with year', () => {
|
||||
assert.deepEqual(parseOrderSearchToken('444/2024'), {
|
||||
raw: '444/2024',
|
||||
type: 'orderNumberYear',
|
||||
match: 'exact',
|
||||
orderNumber: 444,
|
||||
orderNumberPrefix: null,
|
||||
orderYear: 2024,
|
||||
orderIndex: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('parses order number with product index', () => {
|
||||
assert.deepEqual(parseOrderSearchToken('12/01'), {
|
||||
raw: '12/01',
|
||||
type: 'orderNumberIndex',
|
||||
match: 'exact',
|
||||
orderNumber: 12,
|
||||
orderNumberPrefix: null,
|
||||
orderYear: null,
|
||||
orderIndex: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects invalid values', () => {
|
||||
assert.equal(parseOrderSearchToken('0000'), null)
|
||||
assert.equal(parseOrderSearchToken('12/2019/1'), null)
|
||||
assert.equal(parseOrderSearchToken('12/2025/100'), null)
|
||||
assert.equal(parseOrderSearchToken('12/2025/1/2'), null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseOrderSearchQuery', () => {
|
||||
it('parses many order numbers in one query', () => {
|
||||
assert.deepEqual(parseOrderSearchQuery('123/1 0200/2 333/2025/3 444/2024'), [
|
||||
{
|
||||
raw: '123/1',
|
||||
type: 'orderNumberIndex',
|
||||
match: 'exact',
|
||||
orderNumber: 123,
|
||||
orderNumberPrefix: null,
|
||||
orderYear: null,
|
||||
orderIndex: 1,
|
||||
},
|
||||
{
|
||||
raw: '0200/2',
|
||||
type: 'orderNumberIndex',
|
||||
match: 'exact',
|
||||
orderNumber: 200,
|
||||
orderNumberPrefix: null,
|
||||
orderYear: null,
|
||||
orderIndex: 2,
|
||||
},
|
||||
{
|
||||
raw: '333/2025/3',
|
||||
type: 'fullOrderId',
|
||||
match: 'exact',
|
||||
orderNumber: 333,
|
||||
orderNumberPrefix: null,
|
||||
orderYear: 2025,
|
||||
orderIndex: 3,
|
||||
},
|
||||
{
|
||||
raw: '444/2024',
|
||||
type: 'orderNumberYear',
|
||||
match: 'exact',
|
||||
orderNumber: 444,
|
||||
orderNumberPrefix: null,
|
||||
orderYear: 2024,
|
||||
orderIndex: null,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user