@questwork/q-inventory 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.js +23 -0
- package/.storybook/preview.js +17 -0
- package/README.md +16 -0
- package/app/qInventory/App.vue +164 -0
- package/app/qInventory/index.html +12 -0
- package/app/qInventory/main.ts +4 -0
- package/app/qInventory/styles.scss +59 -0
- package/dist/components/InventoryAssetDetail.vue.d.ts +8 -0
- package/dist/components/InventoryAssetDetail.vue.d.ts.map +1 -0
- package/dist/components/InventoryLocationAdmin.vue.d.ts +10 -0
- package/dist/components/InventoryLocationAdmin.vue.d.ts.map +1 -0
- package/dist/components/InventoryLocationDetail.vue.d.ts +8 -0
- package/dist/components/InventoryLocationDetail.vue.d.ts.map +1 -0
- package/dist/components/InventoryOverview.vue.d.ts +15 -0
- package/dist/components/InventoryOverview.vue.d.ts.map +1 -0
- package/dist/components/InventoryScanner.vue.d.ts +21 -0
- package/dist/components/InventoryScanner.vue.d.ts.map +1 -0
- package/dist/components/QInventory.vue.d.ts +24 -0
- package/dist/components/QInventory.vue.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/mixins/CSSMixin.d.ts +7 -0
- package/dist/mixins/CSSMixin.d.ts.map +1 -0
- package/dist/models/index.d.ts +62 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/inventoryAssetRepoAxios.d.ts +18 -0
- package/dist/models/inventoryAssetRepoAxios.d.ts.map +1 -0
- package/dist/models/inventoryAuthRepoAxios.d.ts +26 -0
- package/dist/models/inventoryAuthRepoAxios.d.ts.map +1 -0
- package/dist/models/inventoryAxiosBase.d.ts +41 -0
- package/dist/models/inventoryAxiosBase.d.ts.map +1 -0
- package/dist/models/inventoryLocationRepoAxios.d.ts +13 -0
- package/dist/models/inventoryLocationRepoAxios.d.ts.map +1 -0
- package/dist/models/inventoryMovementRepoAxios.d.ts +13 -0
- package/dist/models/inventoryMovementRepoAxios.d.ts.map +1 -0
- package/dist/q-inventory.esm.js +961 -0
- package/dist/q-inventory.esm.js.map +1 -0
- package/dist/q-inventory.min.cjs +2 -0
- package/dist/q-inventory.min.cjs.map +1 -0
- package/dist/q-inventory.min.css +1 -0
- package/dist/q-inventory.min.js +2 -0
- package/dist/q-inventory.min.js.map +1 -0
- package/dist-app/qInventory/assets/q-inventory-app.css +1 -0
- package/dist-app/qInventory/assets/q-inventory-app.js +18 -0
- package/dist-app/qInventory/index.html +13 -0
- package/package.json +58 -0
- package/src/components/InventoryAssetDetail.vue +55 -0
- package/src/components/InventoryLocationAdmin.vue +56 -0
- package/src/components/InventoryLocationDetail.vue +51 -0
- package/src/components/InventoryOverview.vue +81 -0
- package/src/components/InventoryScanner.vue +315 -0
- package/src/components/QInventory.vue +117 -0
- package/src/index.ts +36 -0
- package/src/mixins/CSSMixin.ts +14 -0
- package/src/models/index.ts +114 -0
- package/src/models/inventoryAssetRepoAxios.ts +38 -0
- package/src/models/inventoryAuthRepoAxios.ts +48 -0
- package/src/models/inventoryAxiosBase.ts +149 -0
- package/src/models/inventoryLocationRepoAxios.ts +25 -0
- package/src/models/inventoryMovementRepoAxios.ts +24 -0
- package/src/styles/index.scss +148 -0
- package/stories/stories.scss +9 -0
- package/stories/stories.ts +362 -0
- package/tsconfig.json +29 -0
- package/vite.app.config.js +37 -0
- package/vite.config.js +68 -0
- package/vitest.config.js +11 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="q-inventory">
|
|
3
|
+
<div class="q-inventory__toolbar">
|
|
4
|
+
<button
|
|
5
|
+
v-for="item in tabs"
|
|
6
|
+
:key="item.key"
|
|
7
|
+
class="q-inventory__tab"
|
|
8
|
+
:class="{ 'q-inventory__tab--active': activeTab === item.key }"
|
|
9
|
+
type="button"
|
|
10
|
+
@click="activeTab = item.key"
|
|
11
|
+
>
|
|
12
|
+
{{ item.label }}
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
<inventory-scanner
|
|
16
|
+
v-show="activeTab === 'scanner'"
|
|
17
|
+
:asset-api-client="assetApiClient"
|
|
18
|
+
:assets="assets"
|
|
19
|
+
:current-user-code="currentUserCode"
|
|
20
|
+
@moved="onMoved"
|
|
21
|
+
@scan="onScan"
|
|
22
|
+
/>
|
|
23
|
+
<inventory-overview
|
|
24
|
+
v-if="activeTab === 'overview'"
|
|
25
|
+
:asset-api-client="assetApiClient"
|
|
26
|
+
:assets="assets"
|
|
27
|
+
:locations="locations"
|
|
28
|
+
@select-asset="onSelectAsset"
|
|
29
|
+
@select-location="onSelectLocation"
|
|
30
|
+
/>
|
|
31
|
+
<inventory-asset-detail
|
|
32
|
+
v-if="activeTab === 'asset'"
|
|
33
|
+
:movement-api-client="movementApiClient"
|
|
34
|
+
:asset="selectedAsset"
|
|
35
|
+
/>
|
|
36
|
+
<inventory-location-detail
|
|
37
|
+
v-if="activeTab === 'location'"
|
|
38
|
+
:asset-api-client="assetApiClient"
|
|
39
|
+
:location="selectedLocation"
|
|
40
|
+
/>
|
|
41
|
+
<inventory-location-admin
|
|
42
|
+
v-if="activeTab === 'locations'"
|
|
43
|
+
:locations="locations"
|
|
44
|
+
@save-location="onSaveLocation"
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script setup lang="ts">
|
|
50
|
+
import { computed, ref } from 'vue'
|
|
51
|
+
import InventoryScanner from './InventoryScanner.vue'
|
|
52
|
+
import InventoryOverview from './InventoryOverview.vue'
|
|
53
|
+
import InventoryAssetDetail from './InventoryAssetDetail.vue'
|
|
54
|
+
import InventoryLocationDetail from './InventoryLocationDetail.vue'
|
|
55
|
+
import InventoryLocationAdmin from './InventoryLocationAdmin.vue'
|
|
56
|
+
import type {
|
|
57
|
+
InventoryAssetApiClient,
|
|
58
|
+
InventoryLocationApiClient,
|
|
59
|
+
InventoryMovementApiClient,
|
|
60
|
+
} from '../models/index.js'
|
|
61
|
+
|
|
62
|
+
defineOptions({
|
|
63
|
+
name: 'q-inventory',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const props = defineProps<{
|
|
67
|
+
assetApiClient?: InventoryAssetApiClient
|
|
68
|
+
locationApiClient?: InventoryLocationApiClient
|
|
69
|
+
movementApiClient?: InventoryMovementApiClient
|
|
70
|
+
assets?: unknown[]
|
|
71
|
+
currentUserCode?: string
|
|
72
|
+
locations?: unknown[]
|
|
73
|
+
}>()
|
|
74
|
+
|
|
75
|
+
const emit = defineEmits<{
|
|
76
|
+
moved: [payload: unknown]
|
|
77
|
+
saveLocation: [payload: unknown]
|
|
78
|
+
scan: [payload: unknown]
|
|
79
|
+
selectAsset: [payload: unknown]
|
|
80
|
+
selectLocation: [payload: unknown]
|
|
81
|
+
}>()
|
|
82
|
+
|
|
83
|
+
const activeTab = ref('scanner')
|
|
84
|
+
const selectedAsset = ref<unknown>(null)
|
|
85
|
+
const selectedLocation = ref<unknown>(null)
|
|
86
|
+
const tabs = computed(() => [
|
|
87
|
+
{ key: 'scanner', label: 'Scan' },
|
|
88
|
+
{ key: 'overview', label: 'Overview' },
|
|
89
|
+
{ key: 'asset', label: 'Asset' },
|
|
90
|
+
{ key: 'location', label: 'Location' },
|
|
91
|
+
{ key: 'locations', label: 'Locations' },
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
function onMoved(payload: unknown) {
|
|
95
|
+
emit('moved', payload)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function onScan(payload: unknown) {
|
|
99
|
+
emit('scan', payload)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function onSaveLocation(payload: unknown) {
|
|
103
|
+
emit('saveLocation', payload)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function onSelectAsset(payload: unknown) {
|
|
107
|
+
selectedAsset.value = payload
|
|
108
|
+
activeTab.value = 'asset'
|
|
109
|
+
emit('selectAsset', payload)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function onSelectLocation(payload: unknown) {
|
|
113
|
+
selectedLocation.value = payload
|
|
114
|
+
activeTab.value = 'location'
|
|
115
|
+
emit('selectLocation', payload)
|
|
116
|
+
}
|
|
117
|
+
</script>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { App } from 'vue'
|
|
2
|
+
import QInventory from './components/QInventory.vue'
|
|
3
|
+
import InventoryScanner from './components/InventoryScanner.vue'
|
|
4
|
+
import InventoryOverview from './components/InventoryOverview.vue'
|
|
5
|
+
import InventoryAssetDetail from './components/InventoryAssetDetail.vue'
|
|
6
|
+
import InventoryLocationDetail from './components/InventoryLocationDetail.vue'
|
|
7
|
+
import InventoryLocationAdmin from './components/InventoryLocationAdmin.vue'
|
|
8
|
+
import './styles/index.scss'
|
|
9
|
+
|
|
10
|
+
export * from '@questwork/q-inventory-model'
|
|
11
|
+
export * from './models/index.js'
|
|
12
|
+
|
|
13
|
+
function install(app: App) {
|
|
14
|
+
app.component('q-inventory', QInventory)
|
|
15
|
+
app.component('inventory-scanner', InventoryScanner)
|
|
16
|
+
app.component('inventory-overview', InventoryOverview)
|
|
17
|
+
app.component('inventory-asset-detail', InventoryAssetDetail)
|
|
18
|
+
app.component('inventory-location-detail', InventoryLocationDetail)
|
|
19
|
+
app.component('inventory-location-admin', InventoryLocationAdmin)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
QInventory.install = install
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
install,
|
|
26
|
+
InventoryAssetDetail,
|
|
27
|
+
InventoryLocationAdmin,
|
|
28
|
+
InventoryLocationDetail,
|
|
29
|
+
InventoryOverview,
|
|
30
|
+
InventoryScanner,
|
|
31
|
+
QInventory,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default {
|
|
35
|
+
install,
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CssBlock {
|
|
2
|
+
block?: string
|
|
3
|
+
element?: string
|
|
4
|
+
modifier?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getCssBlock(base: string, css?: CssBlock): string[] {
|
|
8
|
+
return [
|
|
9
|
+
base,
|
|
10
|
+
css?.block,
|
|
11
|
+
css?.element,
|
|
12
|
+
css?.modifier,
|
|
13
|
+
].filter(Boolean) as string[]
|
|
14
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { InventoryAssetRepoAxios } from './inventoryAssetRepoAxios.js'
|
|
2
|
+
import { InventoryLocationRepoAxios } from './inventoryLocationRepoAxios.js'
|
|
3
|
+
import { InventoryMovementRepoAxios } from './inventoryMovementRepoAxios.js'
|
|
4
|
+
import type { InventoryRepoAxiosOptions } from './inventoryAxiosBase.js'
|
|
5
|
+
|
|
6
|
+
export interface InventoryAssetApiClient {
|
|
7
|
+
findAll?: (query?: Record<string, unknown>) => Promise<unknown[]>
|
|
8
|
+
findOne?: (id: string) => Promise<unknown[]>
|
|
9
|
+
saveOne?: (doc: Record<string, unknown>) => Promise<unknown>
|
|
10
|
+
move?: (payload: InventoryMovePayload) => Promise<unknown>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface InventoryLocationApiClient {
|
|
14
|
+
findAll?: (query?: Record<string, unknown>) => Promise<unknown[]>
|
|
15
|
+
findOne?: (id: string) => Promise<unknown[]>
|
|
16
|
+
saveOne?: (doc: Record<string, unknown>) => Promise<unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface InventoryMovementApiClient {
|
|
20
|
+
findAll?: (query?: Record<string, unknown>) => Promise<unknown[]>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Backward-compatible union type for consumers that still expect one object
|
|
24
|
+
export type InventoryApiClient = InventoryAssetApiClient & InventoryLocationApiClient & InventoryMovementApiClient
|
|
25
|
+
|
|
26
|
+
export interface InventoryMovePayload {
|
|
27
|
+
assetTag: string
|
|
28
|
+
fromLocationCode?: string
|
|
29
|
+
fromLocationKind?: string
|
|
30
|
+
movedBy?: string
|
|
31
|
+
toLocationCode: string
|
|
32
|
+
toLocationKind?: string
|
|
33
|
+
movementType?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface InventoryMoveWarning {
|
|
37
|
+
actualLocationCode?: string
|
|
38
|
+
assetTag?: string
|
|
39
|
+
code: string
|
|
40
|
+
message: string
|
|
41
|
+
requestedFromLocationCode?: string
|
|
42
|
+
requestedFromLocationKind?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface InventoryMoveResult {
|
|
46
|
+
warnings?: InventoryMoveWarning[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface InventoryDisplayLocation {
|
|
50
|
+
code: string
|
|
51
|
+
label: string
|
|
52
|
+
type?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export * from './inventoryAxiosBase.js'
|
|
56
|
+
export * from './inventoryAssetRepoAxios.js'
|
|
57
|
+
export * from './inventoryAuthRepoAxios.js'
|
|
58
|
+
export * from './inventoryLocationRepoAxios.js'
|
|
59
|
+
export * from './inventoryMovementRepoAxios.js'
|
|
60
|
+
|
|
61
|
+
// Backward-compat wrapper so existing callers don't break immediately.
|
|
62
|
+
// Internally it delegates to the focused repos above.
|
|
63
|
+
export class InventoryRepoAxios implements InventoryApiClient {
|
|
64
|
+
private assetRepo: InventoryAssetRepoAxios
|
|
65
|
+
private locationRepo: InventoryLocationRepoAxios
|
|
66
|
+
private movementRepo: InventoryMovementRepoAxios
|
|
67
|
+
|
|
68
|
+
constructor(options: InventoryRepoAxiosOptions = {}) {
|
|
69
|
+
this.assetRepo = new InventoryAssetRepoAxios(options)
|
|
70
|
+
this.locationRepo = new InventoryLocationRepoAxios(options)
|
|
71
|
+
this.movementRepo = new InventoryMovementRepoAxios(options)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async findAll(query?: Record<string, unknown>) {
|
|
75
|
+
return this.assetRepo.findAll(query)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async findOne(id: string) {
|
|
79
|
+
return this.assetRepo.findOne(id)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async saveOne(doc: Record<string, unknown>) {
|
|
83
|
+
return this.assetRepo.saveOne(doc)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async move(payload: InventoryMovePayload) {
|
|
87
|
+
return this.assetRepo.move(payload)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Legacy alias for asset findAll
|
|
91
|
+
async findAssets(query?: Record<string, unknown>) {
|
|
92
|
+
return this.assetRepo.findAll(query)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Legacy alias for location findAll
|
|
96
|
+
async findLocations(query?: Record<string, unknown>) {
|
|
97
|
+
return this.locationRepo.findAll(query)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Legacy alias — location assets are just assets filtered by currentLocationCode
|
|
101
|
+
async findLocationAssets(locationCode: string) {
|
|
102
|
+
return this.assetRepo.findAll({ currentLocationCode: locationCode })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Legacy alias for movement history
|
|
106
|
+
async findAssetHistory(assetTag: string) {
|
|
107
|
+
return this.movementRepo.findAll({ assetTag })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Legacy alias for move
|
|
111
|
+
async moveAsset(payload: InventoryMovePayload) {
|
|
112
|
+
return this.assetRepo.move(payload)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { InventoryRepoAxiosBase, type InventoryRepoAxiosOptions } from './inventoryAxiosBase.js'
|
|
2
|
+
import type { InventoryMovePayload } from './index.js'
|
|
3
|
+
|
|
4
|
+
export interface InventoryAssetFindAllQuery extends Record<string, unknown> {
|
|
5
|
+
assetTag?: string
|
|
6
|
+
assetTags?: string[]
|
|
7
|
+
category?: string
|
|
8
|
+
currentLocationCode?: string
|
|
9
|
+
currentLocationKind?: string
|
|
10
|
+
currentLocationType?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class InventoryAssetRepoAxios extends InventoryRepoAxiosBase {
|
|
14
|
+
constructor(options: InventoryRepoAxiosOptions = {}) {
|
|
15
|
+
super(options)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findAll(query: InventoryAssetFindAllQuery = {}) {
|
|
19
|
+
return this.get('/qInventoryAsset', this.withDefaults(query))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async findOne(assetTag: string) {
|
|
23
|
+
return this.get(`/qInventoryAsset/${encodeURIComponent(assetTag)}`, this.withDefaults())
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async saveOne(doc: Record<string, unknown>) {
|
|
27
|
+
return this.post('/qInventoryAsset', this.withDefaults(doc))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async move(payload: InventoryMovePayload) {
|
|
31
|
+
const assetTag = String(payload.assetTag || '')
|
|
32
|
+
const movedBy = payload.movedBy || this.movedBy
|
|
33
|
+
return this.post(`/qInventoryAsset/${encodeURIComponent(assetTag)}/move`, this.withDefaults({
|
|
34
|
+
...payload,
|
|
35
|
+
movedBy,
|
|
36
|
+
}))
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { InventoryRepoAxiosBase, type InventoryRepoAxiosOptions } from './inventoryAxiosBase.js'
|
|
2
|
+
|
|
3
|
+
export interface InventoryTokenResult {
|
|
4
|
+
token: string
|
|
5
|
+
tokenExpiry: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface InventoryLoginLinkResult {
|
|
9
|
+
email: string
|
|
10
|
+
loginUrl: string
|
|
11
|
+
sent: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface InventoryUserTokenResult {
|
|
15
|
+
email: string
|
|
16
|
+
tenantCode: string
|
|
17
|
+
userCode: string
|
|
18
|
+
userToken: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class InventoryAuthRepoAxios extends InventoryRepoAxiosBase {
|
|
22
|
+
constructor(options: InventoryRepoAxiosOptions = {}) {
|
|
23
|
+
super(options)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getBrowserToken() {
|
|
27
|
+
const result = await this.post('/qInventory/token', {
|
|
28
|
+
clientAppId: 'qInventoryBrowser',
|
|
29
|
+
clientAppName: 'qInventoryBrowser',
|
|
30
|
+
}) as { data?: InventoryTokenResult[] }
|
|
31
|
+
return result.data?.[0]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async requestLoginLink({ email, tenantCode = this.tenantCode }: { email: string, tenantCode?: string }) {
|
|
35
|
+
const result = await this.post('/qInventory/login-link', {
|
|
36
|
+
email,
|
|
37
|
+
tenantCode,
|
|
38
|
+
}) as { data?: InventoryLoginLinkResult[] }
|
|
39
|
+
return result.data?.[0]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async verifyLoginToken(loginToken: string) {
|
|
43
|
+
const result = await this.post('/qInventory/login-token/verify', {
|
|
44
|
+
loginToken,
|
|
45
|
+
}) as { data?: InventoryUserTokenResult[] }
|
|
46
|
+
return result.data?.[0]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export interface InventoryAxiosLike {
|
|
2
|
+
get: (url: string, config?: { headers?: Record<string, string>, params?: Record<string, unknown> }) => Promise<{ data: unknown }>
|
|
3
|
+
post: (url: string, body?: Record<string, unknown>, config?: { headers?: Record<string, string> }) => Promise<{ data: unknown }>
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface InventoryRepoAxiosOptions {
|
|
7
|
+
axios?: InventoryAxiosLike
|
|
8
|
+
authenticatorTokenProvider?: () => Promise<string> | string
|
|
9
|
+
baseURL?: string
|
|
10
|
+
movedBy?: string
|
|
11
|
+
qRepo?: string
|
|
12
|
+
tenantCode?: string
|
|
13
|
+
userTokenProvider?: () => Promise<string> | string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export abstract class InventoryRepoAxiosBase {
|
|
17
|
+
axios?: InventoryAxiosLike
|
|
18
|
+
authenticatorTokenProvider?: () => Promise<string> | string
|
|
19
|
+
baseURL: string
|
|
20
|
+
movedBy: string
|
|
21
|
+
qRepo?: string
|
|
22
|
+
tenantCode: string
|
|
23
|
+
userTokenProvider?: () => Promise<string> | string
|
|
24
|
+
|
|
25
|
+
constructor(options: InventoryRepoAxiosOptions = {}) {
|
|
26
|
+
this.axios = options.axios
|
|
27
|
+
this.authenticatorTokenProvider = options.authenticatorTokenProvider
|
|
28
|
+
this.baseURL = (options.baseURL || '').replace(/\/$/, '')
|
|
29
|
+
this.movedBy = options.movedBy || 'currentUser'
|
|
30
|
+
this.qRepo = options.qRepo
|
|
31
|
+
this.tenantCode = options.tenantCode || 'tenantCode'
|
|
32
|
+
this.userTokenProvider = options.userTokenProvider
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected async get(path: string, query: Record<string, unknown> = {}) {
|
|
36
|
+
const headers = await this.getAuthHeaders()
|
|
37
|
+
if (this.axios) {
|
|
38
|
+
const response = await this.axios.get(this.getUrl(path), { headers, params: query })
|
|
39
|
+
return unwrapQSystemData(response.data)
|
|
40
|
+
}
|
|
41
|
+
const url = new URL(this.getUrl(path), getUrlBase())
|
|
42
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
43
|
+
if (typeof value !== 'undefined' && value !== null && value !== '') {
|
|
44
|
+
url.searchParams.set(key, String(value))
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
const response = await fetch(this.getFetchUrl(url), { headers })
|
|
48
|
+
return unwrapQSystemData(await response.json(), response)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
protected async post(path: string, body: Record<string, unknown> = {}) {
|
|
52
|
+
const headers = {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
...await this.getAuthHeaders(),
|
|
55
|
+
}
|
|
56
|
+
if (this.axios) {
|
|
57
|
+
const response = await this.axios.post(this.getUrl(path), body, { headers })
|
|
58
|
+
return unwrapQSystemResponse(response.data)
|
|
59
|
+
}
|
|
60
|
+
const response = await fetch(this.getUrl(path), {
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
headers,
|
|
63
|
+
method: 'POST',
|
|
64
|
+
})
|
|
65
|
+
return unwrapQSystemResponse(await response.json(), response)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
protected getUrl(path: string) {
|
|
69
|
+
return `${this.baseURL}/api/v1${path}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected getFetchUrl(url: URL) {
|
|
73
|
+
return this.baseURL ? url.toString() : `${url.pathname}${url.search}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
protected withDefaults(obj: Record<string, unknown> = {}) {
|
|
77
|
+
return removeEmptyValues({
|
|
78
|
+
...obj,
|
|
79
|
+
qRepo: obj.qRepo || this.qRepo,
|
|
80
|
+
tenantCode: obj.tenantCode || this.tenantCode,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async getAuthHeaders() {
|
|
85
|
+
const headers: Record<string, string> = {}
|
|
86
|
+
const authenticatorToken = await resolveTokenProvider(this.authenticatorTokenProvider)
|
|
87
|
+
const userToken = await resolveTokenProvider(this.userTokenProvider)
|
|
88
|
+
if (authenticatorToken) {
|
|
89
|
+
headers['X-Q-Authenticator'] = authenticatorToken
|
|
90
|
+
}
|
|
91
|
+
if (userToken) {
|
|
92
|
+
headers['X-Q-Inventory-User-Token'] = userToken
|
|
93
|
+
}
|
|
94
|
+
return headers
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function resolveTokenProvider(provider?: () => Promise<string> | string) {
|
|
99
|
+
if (!provider) {
|
|
100
|
+
return ''
|
|
101
|
+
}
|
|
102
|
+
return provider()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function unwrapQSystemResponse(json: unknown, response?: Response) {
|
|
106
|
+
const data = json as Record<string, unknown>
|
|
107
|
+
const err = data?.err as Record<string, unknown> | undefined
|
|
108
|
+
const errors = data?.errors as Record<string, unknown> | undefined
|
|
109
|
+
if ((response && !response.ok) || err?.hasError || errors) {
|
|
110
|
+
throw new Error(getResponseErrorMessage({ data, err, errors, response }))
|
|
111
|
+
}
|
|
112
|
+
return data
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function unwrapQSystemData(json: unknown, response?: Response): unknown[] {
|
|
116
|
+
const data = unwrapQSystemResponse(json, response)
|
|
117
|
+
return Array.isArray(data?.data) ? data.data : []
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getUrlBase() {
|
|
121
|
+
if (typeof window !== 'undefined') {
|
|
122
|
+
return window.location.origin
|
|
123
|
+
}
|
|
124
|
+
return 'http://localhost'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getResponseErrorMessage({ data, err, errors, response }: {
|
|
128
|
+
data: Record<string, unknown>
|
|
129
|
+
err?: Record<string, unknown>
|
|
130
|
+
errors?: Record<string, unknown>
|
|
131
|
+
response?: Response
|
|
132
|
+
}) {
|
|
133
|
+
const code = String(response?.status || err?.statusCode || err?.code || errors?.name || data?.code || 'REQUEST_ERROR')
|
|
134
|
+
const message = formatErrorMessage(err?.message || errors?.message || data?.message || 'QInventory request failed')
|
|
135
|
+
return `${code}: ${message}`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatErrorMessage(value: unknown) {
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
return value.join('; ')
|
|
141
|
+
}
|
|
142
|
+
return String(value)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function removeEmptyValues(obj: Record<string, unknown>) {
|
|
146
|
+
return Object.fromEntries(
|
|
147
|
+
Object.entries(obj).filter(([, value]) => typeof value !== 'undefined' && value !== null && value !== ''),
|
|
148
|
+
)
|
|
149
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { InventoryRepoAxiosBase, type InventoryRepoAxiosOptions } from './inventoryAxiosBase.js'
|
|
2
|
+
|
|
3
|
+
export interface InventoryLocationFindAllQuery extends Record<string, unknown> {
|
|
4
|
+
locationCode?: string
|
|
5
|
+
locationType?: string
|
|
6
|
+
parentLocationCode?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class InventoryLocationRepoAxios extends InventoryRepoAxiosBase {
|
|
10
|
+
constructor(options: InventoryRepoAxiosOptions = {}) {
|
|
11
|
+
super(options)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async findAll(query: InventoryLocationFindAllQuery = {}) {
|
|
15
|
+
return this.get('/qInventoryLocation', this.withDefaults(query))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findOne(locationCode: string) {
|
|
19
|
+
return this.get(`/qInventoryLocation/${encodeURIComponent(locationCode)}`, this.withDefaults())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async saveOne(doc: Record<string, unknown>) {
|
|
23
|
+
return this.post('/qInventoryLocation', this.withDefaults(doc))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { InventoryRepoAxiosBase, type InventoryRepoAxiosOptions } from './inventoryAxiosBase.js'
|
|
2
|
+
|
|
3
|
+
export interface InventoryMovementFindAllQuery extends Record<string, unknown> {
|
|
4
|
+
assetTag?: string
|
|
5
|
+
fromLocationCode?: string
|
|
6
|
+
toLocationCode?: string
|
|
7
|
+
movedBy?: string
|
|
8
|
+
movementType?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class InventoryMovementRepoAxios extends InventoryRepoAxiosBase {
|
|
12
|
+
constructor(options: InventoryRepoAxiosOptions = {}) {
|
|
13
|
+
super(options)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async findAll(query: InventoryMovementFindAllQuery = {}) {
|
|
17
|
+
const assetTag = String(query.assetTag || '')
|
|
18
|
+
if (!assetTag) {
|
|
19
|
+
throw new Error('assetTag is required for movement findAll')
|
|
20
|
+
}
|
|
21
|
+
const { assetTag: _omit, ...rest } = query
|
|
22
|
+
return this.get(`/qInventoryAsset/${encodeURIComponent(assetTag)}/movements`, this.withDefaults(rest))
|
|
23
|
+
}
|
|
24
|
+
}
|