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,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -34,9 +34,26 @@ a service powinien zajac sie reszta.
|
||||
```txt
|
||||
frontend/src/services/apiMode.js
|
||||
frontend/src/services/productsApi.js
|
||||
frontend/src/services/productsHttpApi.js
|
||||
frontend/src/services/productTimelineApi.js
|
||||
frontend/src/services/productTimelineHttpApi.js
|
||||
frontend/src/services/productSpecificationApi.js
|
||||
frontend/src/services/productSpecificationHttpApi.js
|
||||
frontend/src/services/dictionariesApi.js
|
||||
frontend/src/services/dictionariesHttpApi.js
|
||||
```
|
||||
|
||||
Pliki `*Api.js` sa fasadami. Wybieraja implementacje na podstawie `USE_MOCK_API`.
|
||||
|
||||
Pliki `*HttpApi.js` sa implementacja prawdziwego API przez Axios.
|
||||
|
||||
Implementacje mockow sa poza katalogiem `services`:
|
||||
|
||||
```txt
|
||||
frontend/src/mocks/api/productsMockApi.js
|
||||
frontend/src/mocks/api/productTimelineMockApi.js
|
||||
frontend/src/mocks/api/productSpecificationMockApi.js
|
||||
frontend/src/mocks/api/dictionariesMockApi.js
|
||||
```
|
||||
|
||||
## Przeplyw danych
|
||||
@@ -57,6 +74,7 @@ Przyklad:
|
||||
IndexPage.vue
|
||||
-> productsStore.fetchFirstPage()
|
||||
-> fetchProducts(params)
|
||||
-> productsMockApi albo productsHttpApi
|
||||
-> products.json albo GET /mayo-api/products
|
||||
```
|
||||
|
||||
@@ -90,6 +108,20 @@ To jest lepsze, bo:
|
||||
- latwiej dodac mocki,
|
||||
- latwiej testowac logike store.
|
||||
|
||||
Po ostatnim porzadkowaniu services sa fasadami. Przyklad:
|
||||
|
||||
```js
|
||||
export const fetchProducts = USE_MOCK_API ? fetchProductsFromMock : fetchProductsFromHttp
|
||||
```
|
||||
|
||||
Dzieki temu `productsStore` zawsze importuje to samo:
|
||||
|
||||
```js
|
||||
import { fetchProducts } from 'src/services/productsApi'
|
||||
```
|
||||
|
||||
ale realna implementacja moze byc mockowa albo HTTP.
|
||||
|
||||
## `apiMode.js`
|
||||
|
||||
Plik:
|
||||
@@ -140,18 +172,19 @@ frontend/src/mocks/specifications.json
|
||||
frontend/src/mocks/dictionaries.json
|
||||
```
|
||||
|
||||
Sa uzywane tylko przez warstwe `services`.
|
||||
Sa uzywane tylko przez implementacje mock API w `frontend/src/mocks/api/`.
|
||||
|
||||
Komponenty i store nie importuja mockow bezposrednio.
|
||||
Komponenty, store i fasady `services/*Api.js` nie importuja plikow JSON bezposrednio.
|
||||
|
||||
To jest wazne, bo komponent nie powinien wiedziec, skad przyszly dane. Dla komponentu dane z mocka i z backendu powinny wygladac identycznie.
|
||||
|
||||
## `productsApi.js`
|
||||
## `productsApi.js` i `productsHttpApi.js`
|
||||
|
||||
Plik:
|
||||
|
||||
```txt
|
||||
frontend/src/services/productsApi.js
|
||||
frontend/src/services/productsHttpApi.js
|
||||
```
|
||||
|
||||
Eksportuje:
|
||||
@@ -167,6 +200,14 @@ Odpowiedzialnosc:
|
||||
- obsluga `limit` i `offset`,
|
||||
- zwrocenie danych w formacie oczekiwanym przez `productsStore`.
|
||||
|
||||
`productsApi.js` jest fasada.
|
||||
|
||||
`productsHttpApi.js` zna Axios i endpoint:
|
||||
|
||||
```http
|
||||
GET /mayo-api/products
|
||||
```
|
||||
|
||||
Zwraca:
|
||||
|
||||
```js
|
||||
@@ -181,14 +222,20 @@ Zwraca:
|
||||
}
|
||||
```
|
||||
|
||||
W trybie mock implementacja jest w:
|
||||
|
||||
```txt
|
||||
frontend/src/mocks/api/productsMockApi.js
|
||||
```
|
||||
|
||||
W trybie mock:
|
||||
|
||||
- czyta `frontend/src/mocks/products.json`,
|
||||
- filtruje po `search`, `finish`, `year`, `productionList`,
|
||||
- filtruje po `orderSearch`, `finish`, `year`, `productionList`,
|
||||
- ucina wynik wedlug `limit` i `offset`,
|
||||
- zwraca `items` i `pageInfo`.
|
||||
|
||||
W trybie prawdziwego API:
|
||||
W trybie prawdziwego API implementacja jest w `productsHttpApi.js`:
|
||||
|
||||
```http
|
||||
GET /mayo-api/products
|
||||
@@ -201,6 +248,7 @@ z parametrami:
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
orderSearch,
|
||||
model,
|
||||
client,
|
||||
finish,
|
||||
@@ -209,12 +257,21 @@ z parametrami:
|
||||
}
|
||||
```
|
||||
|
||||
## `productTimelineApi.js`
|
||||
`orderSearch` powstaje w `productsStore` przez parser:
|
||||
|
||||
```txt
|
||||
frontend/src/utils/orderSearchParser.js
|
||||
```
|
||||
|
||||
Do HTTP `orderSearch` jest serializowane jako JSON string, bo endpoint listy jest typu `GET`.
|
||||
|
||||
## `productTimelineApi.js` i `productTimelineHttpApi.js`
|
||||
|
||||
Plik:
|
||||
|
||||
```txt
|
||||
frontend/src/services/productTimelineApi.js
|
||||
frontend/src/services/productTimelineHttpApi.js
|
||||
```
|
||||
|
||||
Eksportuje:
|
||||
@@ -228,13 +285,19 @@ createProductTimelineEvent(productId, payload)
|
||||
|
||||
Pobiera pelny timeline produktu.
|
||||
|
||||
W trybie mock implementacja jest w:
|
||||
|
||||
```txt
|
||||
frontend/src/mocks/api/productTimelineMockApi.js
|
||||
```
|
||||
|
||||
W trybie mock:
|
||||
|
||||
- czyta `frontend/src/mocks/timelines.json`,
|
||||
- zwraca timeline dla `productId`,
|
||||
- jesli brak danych, zwraca pusty timeline.
|
||||
|
||||
W trybie prawdziwego API:
|
||||
W trybie prawdziwego API implementacja jest w `productTimelineHttpApi.js`:
|
||||
|
||||
```http
|
||||
GET /mayo-api/products/:id/timeline
|
||||
@@ -274,12 +337,13 @@ To pozwala store od razu zaktualizowac:
|
||||
- pelny timeline,
|
||||
- preview na glownej karcie produktu.
|
||||
|
||||
## `productSpecificationApi.js`
|
||||
## `productSpecificationApi.js` i `productSpecificationHttpApi.js`
|
||||
|
||||
Plik:
|
||||
|
||||
```txt
|
||||
frontend/src/services/productSpecificationApi.js
|
||||
frontend/src/services/productSpecificationHttpApi.js
|
||||
```
|
||||
|
||||
Eksportuje:
|
||||
@@ -293,11 +357,17 @@ refreshProductSpecification(productId)
|
||||
|
||||
Pobiera specyfikacje produktu.
|
||||
|
||||
W trybie mock implementacja jest w:
|
||||
|
||||
```txt
|
||||
frontend/src/mocks/api/productSpecificationMockApi.js
|
||||
```
|
||||
|
||||
W trybie mock:
|
||||
|
||||
- czyta `frontend/src/mocks/specifications.json`.
|
||||
|
||||
W trybie prawdziwego API:
|
||||
W trybie prawdziwego API implementacja jest w `productSpecificationHttpApi.js`:
|
||||
|
||||
```http
|
||||
GET /mayo-api/products/:id/specification
|
||||
@@ -353,12 +423,13 @@ data.sections
|
||||
|
||||
Dzieki temu store nie musi znac szczegolow odpowiedzi HTTP.
|
||||
|
||||
## `dictionariesApi.js`
|
||||
## `dictionariesApi.js` i `dictionariesHttpApi.js`
|
||||
|
||||
Plik:
|
||||
|
||||
```txt
|
||||
frontend/src/services/dictionariesApi.js
|
||||
frontend/src/services/dictionariesHttpApi.js
|
||||
```
|
||||
|
||||
Eksportuje:
|
||||
@@ -372,11 +443,17 @@ Odpowiedzialnosc:
|
||||
- pobranie slownikow do filtrow i formularzy,
|
||||
- normalizacja pustych pol do pustych tablic.
|
||||
|
||||
W trybie mock implementacja jest w:
|
||||
|
||||
```txt
|
||||
frontend/src/mocks/api/dictionariesMockApi.js
|
||||
```
|
||||
|
||||
W trybie mock:
|
||||
|
||||
- czyta `frontend/src/mocks/dictionaries.json`.
|
||||
|
||||
W trybie prawdziwego API:
|
||||
W trybie prawdziwego API implementacja jest w `dictionariesHttpApi.js`:
|
||||
|
||||
```http
|
||||
GET /mayo-api/dictionaries
|
||||
@@ -405,20 +482,28 @@ Zrob nowy service:
|
||||
|
||||
```txt
|
||||
frontend/src/services/productPhotosApi.js
|
||||
frontend/src/services/productPhotosHttpApi.js
|
||||
frontend/src/mocks/api/productPhotosMockApi.js
|
||||
```
|
||||
|
||||
W nim:
|
||||
Fasada:
|
||||
|
||||
```js
|
||||
import { fetchProductPhotos as fetchProductPhotosFromHttp } from 'src/services/productPhotosHttpApi'
|
||||
import { fetchProductPhotos as fetchProductPhotosFromMock } from 'src/mocks/api/productPhotosMockApi'
|
||||
import { USE_MOCK_API } from 'src/services/apiMode'
|
||||
|
||||
export const fetchProductPhotos = USE_MOCK_API
|
||||
? fetchProductPhotosFromMock
|
||||
: fetchProductPhotosFromHttp
|
||||
```
|
||||
|
||||
Implementacja HTTP:
|
||||
|
||||
```js
|
||||
import { api } from 'src/boot/axios'
|
||||
import { USE_MOCK_API, waitForMockApi } from 'src/services/apiMode'
|
||||
|
||||
export async function fetchProductPhotos(productId) {
|
||||
if (USE_MOCK_API) {
|
||||
await waitForMockApi()
|
||||
return []
|
||||
}
|
||||
|
||||
const response = await api.get(`/mayo-api/products/${productId}/photos`)
|
||||
return response.data
|
||||
}
|
||||
@@ -463,12 +548,16 @@ Utworzono:
|
||||
```txt
|
||||
frontend/src/services/apiMode.js
|
||||
frontend/src/services/productsApi.js
|
||||
frontend/src/services/productsHttpApi.js
|
||||
frontend/src/services/productTimelineApi.js
|
||||
frontend/src/services/productTimelineHttpApi.js
|
||||
frontend/src/services/productSpecificationApi.js
|
||||
frontend/src/services/productSpecificationHttpApi.js
|
||||
frontend/src/services/dictionariesApi.js
|
||||
frontend/src/services/dictionariesHttpApi.js
|
||||
```
|
||||
|
||||
Utworzono mocki:
|
||||
Utworzono dane mockow:
|
||||
|
||||
```txt
|
||||
frontend/src/mocks/products.json
|
||||
@@ -477,6 +566,15 @@ frontend/src/mocks/specifications.json
|
||||
frontend/src/mocks/dictionaries.json
|
||||
```
|
||||
|
||||
Utworzono implementacje mock API:
|
||||
|
||||
```txt
|
||||
frontend/src/mocks/api/productsMockApi.js
|
||||
frontend/src/mocks/api/productTimelineMockApi.js
|
||||
frontend/src/mocks/api/productSpecificationMockApi.js
|
||||
frontend/src/mocks/api/dictionariesMockApi.js
|
||||
```
|
||||
|
||||
Zaktualizowano store tak, aby uzywaly services zamiast Axiosa bezposrednio.
|
||||
|
||||
Zaktualizowano widok glownej strony tak, aby pobieral dane ze store zamiast trzymac lokalne mocki w komponencie.
|
||||
|
||||
@@ -182,7 +182,6 @@ Jeśli na karcie produktu ma być widoczny timeline, ten store powinien trzymać
|
||||
},
|
||||
|
||||
filters: {
|
||||
search: '',
|
||||
modelId: null,
|
||||
clientId: null,
|
||||
finish: null,
|
||||
@@ -190,6 +189,19 @@ Jeśli na karcie produktu ma być widoczny timeline, ten store powinien trzymać
|
||||
year: null,
|
||||
},
|
||||
|
||||
searchQuery: '123/1',
|
||||
orderSearch: [
|
||||
{
|
||||
raw: '123/1',
|
||||
type: 'orderNumberIndex',
|
||||
match: 'exact',
|
||||
orderNumber: 123,
|
||||
orderNumberPrefix: null,
|
||||
orderYear: null,
|
||||
orderIndex: 1,
|
||||
},
|
||||
],
|
||||
|
||||
limit: 30,
|
||||
offset: 30,
|
||||
total: 184,
|
||||
@@ -204,7 +216,7 @@ Jeśli na karcie produktu ma być widoczny timeline, ten store powinien trzymać
|
||||
Endpoint:
|
||||
|
||||
```http
|
||||
GET /mayo-api/products?limit=30&offset=0&search=regius
|
||||
GET /mayo-api/products?limit=30&offset=0&search=123/1
|
||||
```
|
||||
|
||||
Odpowiedź:
|
||||
@@ -261,10 +273,22 @@ await productsStore.fetchNextPage()
|
||||
Pobiera kolejną porcję produktów. Używaj tego przy lazy loading, gdy użytkownik zbliża się do końca listy.
|
||||
|
||||
```js
|
||||
await productsStore.applyFilters({ search: 'regius' })
|
||||
await productsStore.applyFilters({ finish: 'GLOSS' })
|
||||
```
|
||||
|
||||
Ustawia filtry i pobiera listę od początku.
|
||||
Ustawia filtry i pobiera listę od początku. Searchbar nie jest zwyklym filtrem, bo ma osobny parser numerow zamowien.
|
||||
|
||||
```js
|
||||
productsStore.setSearchQuery('123/1')
|
||||
```
|
||||
|
||||
Zapisuje tekst z searchbara i parsuje go do `orderSearch`.
|
||||
|
||||
```js
|
||||
await productsStore.applySearch('123/1')
|
||||
```
|
||||
|
||||
Zapisuje tekst searchbara, parsuje go i pobiera listę od początku.
|
||||
|
||||
```js
|
||||
productsStore.applyTimelinePreviewUpdate(productId, timelinePreview)
|
||||
@@ -914,7 +938,7 @@ Komponent wywołuje:
|
||||
|
||||
```js
|
||||
productsStore.applyFilters({
|
||||
search: 'regius',
|
||||
finish: 'GLOSS',
|
||||
})
|
||||
```
|
||||
|
||||
@@ -924,6 +948,14 @@ Store:
|
||||
2. resetuje offset,
|
||||
3. pobiera pierwszą stronę listy od nowa.
|
||||
|
||||
Searchbar jest obslugiwany osobno:
|
||||
|
||||
```js
|
||||
productsStore.applySearch('123/1')
|
||||
```
|
||||
|
||||
To jest celowe, bo wyszukiwanie po numerze zamowienia ma wlasny parser i nie powinno mieszac sie z filtrami typu model, klient, finish albo lista produkcyjna.
|
||||
|
||||
### 4. Użytkownik otwiera specyfikację produktu
|
||||
|
||||
Komponent wywołuje:
|
||||
|
||||
26
questions/serachbar.md
Normal file
26
questions/serachbar.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# zachowanie paska wyszukiwania
|
||||
|
||||
Pasek wyszukiwnia sluzy do wyszukiwania produktow na liscie produktow.
|
||||
wyszukiwanie odbywa sie głównie po numerze zamoiwnia. Kazdy produkt ma unikalny numer zamowiania w postaci XXXX/YYYY/ZZ, gdzie:
|
||||
|
||||
- XXXX to numer zamowinia i przyjmuje wartosci od 1 do 1000. moze miec wiądoące zera.
|
||||
- YYYY to rok zlozenia zamówniea i przyjmuje wartosci wieksze od 2020.
|
||||
- ZZ to numer produktu w zamowieniu. moze miec wiadace zero, ale nie musi.
|
||||
|
||||
1. wyszukiwanie moze odbywac sie po pelnym nuemrze (0012/2025/1). lsita produktow powinna zaweirac jeden (gdy jest pasujace dopasowanie) lub zero produktów, jeżeli żaden produkt nie ma takiego numeru.
|
||||
2. wyszukiwanie moze odbywac sie po samym numerze zamowienai (0012). lista produktów poinna zawierac wszystkie produkty o numerze 12, z każdego roku.
|
||||
3. wyszukiwanie moze odbywac sie po skroconym zapisie (0012/1). lista produktow powinna zawierac liste produktow o pasujacym numerze i pozycji w zamowieniu z kazdego roku.
|
||||
4. wyszukiwanie moze odbywac sie po numerze zamowinia bez wiadacych zer. na przykład:
|
||||
- 1 wszystkie produkty gdzie numer zamowienia zaczyna sie liczba 1
|
||||
- 12 wszystkie produkty gdzie numer zamowienia zaczyna sie liczba 12
|
||||
- 123 wszystkie produkty gdzie numer zamowienia zaczyna sie liczba 123
|
||||
5. wyszukiwanie moze odbywac sie po numerze zamowinia bez wiadacych zer wraz z pozycja porduktu w zamoieniu. na przykład:
|
||||
- 1/1 wszystkie produkty gdzie numer zamowienia to 1, a pozycja produktu to 1. rok dowolny
|
||||
- 12/1 wszystkie produkty gdzie numer zamowienia to 12, a pozycja produktu to 1. rok dowolny
|
||||
- 123/1 wszystkie produkty gdzie numer zamowienia to 123, a pozycja produktu to 1. rok dowolny
|
||||
- 1/01 wszystkie produkty gdzie numer zamowienia to 1, a pozycja produktu to 1. rok dowolny
|
||||
- 12/01 wszystkie produkty gdzie numer zamowienia to 12, a pozycja produktu to 1. rok dowolny
|
||||
- 123/01 wszystkie produkty gdzie numer zamowienia to 123, a pozycja produktu to 1. rok dowolny
|
||||
6. wyszukiwanie moze odbywac podajac wiele numerow zamowien. separatorem może być spacja, kropka czy przecinek. na przykład:
|
||||
"123/1 0200/2 333/2025/3 444/2024"
|
||||
7. zawartosc pola wyszukiwania nie wplywa na filtry. filrtry takie jak model, klient, finish, lista prukdcyjna czy rok, sa ustawiane niezaleznie.
|
||||
Reference in New Issue
Block a user