changes
This commit is contained in:
@@ -16,8 +16,8 @@
|
|||||||
// Tooltip Styles
|
// Tooltip Styles
|
||||||
.tooltiptext {
|
.tooltiptext {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
background-color: var(--bulma-primary-95);
|
background-color: hsl(from var(--bulma-black-bis) h s l / 0.7);
|
||||||
color: var(--bulma-primary-bold);
|
color: var(--bulma-white-bis);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
padding: 0.4rem 0.6rem;
|
padding: 0.4rem 0.6rem;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control:hover .tooltiptext {
|
.control:hover .tooltiptext {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
class="modal-card-head px-4 py-3"
|
class="modal-card-head px-4 py-3"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
class="modal-card-title fs-17 font-semibold control is-expanded"
|
class="modal-card-title fs-17 font-semibold control is-expanded min-w-0 is-flex-shrink-1"
|
||||||
v-html="title"
|
v-html="title"
|
||||||
></p>
|
></p>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -145,7 +145,6 @@
|
|||||||
>
|
>
|
||||||
{{ submenu[lang] }}
|
{{ submenu[lang] }}
|
||||||
</a>
|
</a>
|
||||||
<hr class="my-0" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,9 +13,12 @@
|
|||||||
v-for="(v, i) in fields"
|
v-for="(v, i) in fields"
|
||||||
>
|
>
|
||||||
<td>{{ i }}</td>
|
<td>{{ i }}</td>
|
||||||
<td>
|
<td
|
||||||
|
@click="openField(v, i)"
|
||||||
|
class="is-clickable"
|
||||||
|
>
|
||||||
<p>{{ $stripHtml(v.label, 50) }}</p>
|
<p>{{ $stripHtml(v.label, 50) }}</p>
|
||||||
<a @click="openField(v, i)">{{ v.name }}</a>
|
<p class="has-text-primary">{{ v.name }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td style="vertical-align: middle">
|
<td style="vertical-align: middle">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
@@ -76,15 +79,15 @@ import { cloneDeep } from "es-toolkit";
|
|||||||
const emit = defineEmits(["close"]);
|
const emit = defineEmits(["close"]);
|
||||||
const { $stripHtml, $arrayMove, $remove } = useNuxtApp();
|
const { $stripHtml, $arrayMove, $remove } = useNuxtApp();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
var props = defineProps({
|
const props = defineProps({
|
||||||
pagename: String,
|
pagename: String,
|
||||||
});
|
});
|
||||||
var showmodal = ref();
|
const showmodal = ref();
|
||||||
var current;
|
let current;
|
||||||
var pagedata = store[props.pagename];
|
const pagedata = store[props.pagename];
|
||||||
var fields = ref(pagedata.fields);
|
const fields = ref(pagedata.fields);
|
||||||
function openField(v, i) {
|
function openField(v, i) {
|
||||||
current = { v: v, i: i };
|
current = { v, i };
|
||||||
showmodal.value = {
|
showmodal.value = {
|
||||||
component: "datatable/FieldAttribute",
|
component: "datatable/FieldAttribute",
|
||||||
title: `${v.name} / ${$stripHtml(v.label)}`,
|
title: `${v.name} / ${$stripHtml(v.label)}`,
|
||||||
@@ -137,3 +140,9 @@ function remove() {
|
|||||||
updateField();
|
updateField();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
td:nth-child(2):hover {
|
||||||
|
background-color: hsl(from var(--bulma-primary) h s l / 0.05);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
class="tooltiptext has-background-orange-soft has-text-orange-bold"
|
class="tooltiptext"
|
||||||
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
|
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
|
||||||
>
|
>
|
||||||
Nhập dữ liệu
|
Nhập dữ liệu
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
class="tooltiptext has-background-orange-soft has-text-orange-bold"
|
class="tooltiptext"
|
||||||
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
|
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
|
||||||
>
|
>
|
||||||
Thêm mới
|
Thêm mới
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
class="tooltiptext has-background-orange-soft has-text-orange-bold"
|
class="tooltiptext"
|
||||||
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
|
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
|
||||||
>
|
>
|
||||||
Xuất Excel
|
Xuất Excel
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
class="tooltiptext has-background-orange-soft has-text-orange-bold"
|
class="tooltiptext"
|
||||||
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
|
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
|
||||||
>
|
>
|
||||||
Làm mới
|
Làm mới
|
||||||
@@ -170,6 +170,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { isEmpty } from "es-toolkit/compat";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -231,10 +233,10 @@ export default {
|
|||||||
this.checkTimeopt();
|
this.checkTimeopt();
|
||||||
if (!this.enableTime) return this.$emit("option");
|
if (!this.enableTime) return this.$emit("option");
|
||||||
|
|
||||||
let found = this.$findapi(this.api);
|
const apiConfig = this.$findapi(this.api);
|
||||||
found.commit = undefined;
|
apiConfig.commit = undefined;
|
||||||
|
|
||||||
let filter = this.$copy(this.filter);
|
const filter = this.$copy(this.filter);
|
||||||
if (filter) {
|
if (filter) {
|
||||||
// dynamic parameter
|
// dynamic parameter
|
||||||
for (const [key, value] of Object.entries(filter)) {
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
@@ -244,35 +246,35 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found.params.filter) {
|
if (apiConfig.params.filter) {
|
||||||
if (!filter) filter = {};
|
if (!filter) filter = {};
|
||||||
for (const [key, value] of Object.entries(found.params.filter)) {
|
for (const [key, value] of Object.entries(apiConfig.params.filter)) {
|
||||||
filter[key] = value;
|
filter[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.options.map((v) => {
|
this.options.map((v) => {
|
||||||
let f = filter ? this.$copy(filter) : {};
|
const f = filter ? this.$copy(filter) : {};
|
||||||
f["create_time__date__gte"] = this.$dayjs().subtract(v.code, "day").format("YYYY-MM-DD");
|
f["create_time__date__gte"] = this.$dayjs().subtract(v.code, "day").format("YYYY-MM-DD");
|
||||||
v.filter = f;
|
v.filter = f;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$emit("option", this.$find(this.options, { code: this.current }));
|
this.$emit("option", this.$find(this.options, { code: this.current }));
|
||||||
|
|
||||||
let f = {};
|
const distinct_values = Object.fromEntries(
|
||||||
this.options.map((v) => {
|
this.options.map(({ code, filter }) => [
|
||||||
f[v.code] = {
|
code,
|
||||||
type: "Count",
|
{
|
||||||
field: "create_time__date",
|
type: "Count",
|
||||||
filter: v.filter,
|
field: "create_time__date",
|
||||||
};
|
filter,
|
||||||
});
|
},
|
||||||
|
]),
|
||||||
let params = { summary: "aggregate", distinct_values: f };
|
);
|
||||||
found.params = params;
|
apiConfig.params = { summary: "aggregate", distinct_values };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let rs = await this.$getapi([found]);
|
const rs = await this.$getapi([apiConfig]);
|
||||||
for (const [key, value] of Object.entries(rs[0].data.rows)) {
|
for (const [key, value] of Object.entries(rs[0].data.rows)) {
|
||||||
const found = this.$find(this.options, { code: Number(key) });
|
const found = this.$find(this.options, { code: Number(key) });
|
||||||
if (found) {
|
if (found) {
|
||||||
@@ -429,21 +431,22 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
checkTimeopt() {
|
checkTimeopt() {
|
||||||
if (this.timeopt > 0) {
|
if (typeof this.timeopt === "number" && this.timeopt > 0) {
|
||||||
let obj = this.$find(this.options, {
|
const defaultOption = this.$find(this.options, {
|
||||||
code: this.$parseNum(this.timeopt),
|
code: this.$parseNum(this.timeopt),
|
||||||
});
|
});
|
||||||
if (obj) this.current = obj.code;
|
if (defaultOption) this.current = defaultOption.code;
|
||||||
}
|
}
|
||||||
if (this.timeopt ? this.$empty(this.timeopt.disable) : true) return;
|
if (!this.timeopt) return;
|
||||||
if (this.timeopt.disable.indexOf("add") >= 0) this.enableAdd = false;
|
|
||||||
if (this.timeopt.disable.indexOf("time") >= 0) this.enableTime = false;
|
|
||||||
if (this.timeopt.time) {
|
if (this.timeopt.time) {
|
||||||
let obj = this.$find(this.options, {
|
let defaultOption = this.$find(this.options, {
|
||||||
code: this.$parseNum(this.timeopt.time),
|
code: this.$parseNum(this.timeopt.time),
|
||||||
});
|
});
|
||||||
if (obj) this.current = obj.code;
|
if (defaultOption) this.current = defaultOption.code;
|
||||||
}
|
}
|
||||||
|
if (isEmpty(this.timeopt.disable)) return;
|
||||||
|
if (this.timeopt.disable.indexOf("add") >= 0) this.enableAdd = false;
|
||||||
|
if (this.timeopt.disable.indexOf("time") >= 0) this.enableTime = false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ const key = ref(0);
|
|||||||
setting: 'imeis',
|
setting: 'imeis',
|
||||||
pagename: 'imeis',
|
pagename: 'imeis',
|
||||||
params: {
|
params: {
|
||||||
values: 'id,code,imei,variant,variant__code,create_time,update_time',
|
values:
|
||||||
|
'id,code,imei,variant,variant,variant__code,variant__product,variant__product__code,variant__product__name,variant__product__manufacturer,variant__product__os,variant__product__battery,variant__product__screen,variant__product__cpu,variant__product__gpu,variant__product__camera_system,variant__product__sim,variant__product__network_technology,variant__product__charging_technology,variant__product__external_storage,variant__product__ip_rating,variant__product__design,variant__product__creator,variant__product__updater,variant__product__deleted,variant__color,variant__color__code,variant__color__name,variant__color__hex_code,variant__color__deleted,variant__ram,variant__ram__code,variant__ram__capacity,variant__ram__deleted,variant__internal_storage,variant__internal_storage__code,variant__internal_storage__capacity,variant__internal_storage__deleted,variant__image,variant__image__code,variant__image__name,variant__image__path,variant__image__is_active,variant__image__deleted,variant__image__create_time,variant__price,variant__note,variant__creator,variant__updater,variant__deleted,variant__create_time,variant__update_time,warehouse,warehouse__code,warehouse__name,warehouse__address,warehouse__deleted,deleted,create_time,update_time',
|
||||||
filter: { variant: variant.id },
|
filter: { variant: variant.id },
|
||||||
sort: 'id',
|
sort: 'id',
|
||||||
},
|
},
|
||||||
|
|||||||
47
app/components/inventory/IMEICountByWarehouse.vue
Normal file
47
app/components/inventory/IMEICountByWarehouse.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
variant: Object,
|
||||||
|
col: String,
|
||||||
|
});
|
||||||
|
const { $getdata } = useNuxtApp();
|
||||||
|
const imeisSoldInWarehouse = ref([]);
|
||||||
|
const stock = computed(() => props.variant.imei_count - imeisSoldInWarehouse.value.length);
|
||||||
|
const reserved = ref(0);
|
||||||
|
const available = computed(() => stock.value - reserved.value);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// isLoading.value = true;
|
||||||
|
// const imeisSold = await $getdata("IMEI_Sold", {
|
||||||
|
// // across all warehouses
|
||||||
|
// filter: {
|
||||||
|
// variant: props.variant.id,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// const imeisSoldStr = imeisSold.map(({ imei }) => imei);
|
||||||
|
// // get records in IMEI with variant's id (id), warehouse id,
|
||||||
|
// imeisSoldInWarehouse.value = await $getdata("IMEI", {
|
||||||
|
// filter: {
|
||||||
|
// imei__in: imeisSoldStr,
|
||||||
|
// warehouse: props.variant.imeis__warehouse || undefined,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// isLoading.value = false;
|
||||||
|
// if (props.variant.id === 1) {
|
||||||
|
// // console.log("props.variant", props.variant);
|
||||||
|
// // console.log("props.variant.imeis__warehouse", props.variant.imeis__warehouse);
|
||||||
|
// // console.log("props.variant.imeis__warehouse__name", props.variant.imeis__warehouse__name);
|
||||||
|
// // console.log("imeisSold", imeisSold);
|
||||||
|
// // console.log(`imeisSoldInWarehouse ${props.variant.imeis__warehouse__name}`, imeisSoldInWarehouse);
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p class="has-text-right">
|
||||||
|
<span v-if="isLoading"></span>
|
||||||
|
<span v-else-if="col === 'stock'">{{ stock }}</span>
|
||||||
|
<span v-else-if="col === 'reserved'">{{ reserved }}</span>
|
||||||
|
<span v-else-if="col === 'available'">{{ available }}</span>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import DataView from "~/components/datatable/DataView.vue";
|
||||||
import InventoryHighlightCard from "~/components/inventory/InventoryHighlightCard.vue";
|
import InventoryHighlightCard from "~/components/inventory/InventoryHighlightCard.vue";
|
||||||
import InventoryTable from "~/components/inventory/InventoryTable.vue";
|
|
||||||
|
|
||||||
const inventoryHighlights = [
|
const inventoryHighlights = [
|
||||||
{
|
{
|
||||||
@@ -86,6 +86,43 @@ const inventoryHighlights = [
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<InventoryTable />
|
<div class="card is-clipped">
|
||||||
|
<div class="card-content p-0">
|
||||||
|
<p class="p-5 icon-text fs-16 font-semibold w-full">
|
||||||
|
<span class="icon">
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:deployed-code-outline"
|
||||||
|
:size="20"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>Danh sách tồn kho</span>
|
||||||
|
</p>
|
||||||
|
<div class="px-4 pb-4">
|
||||||
|
<DataView
|
||||||
|
v-bind="{
|
||||||
|
api: 'Product_Variant',
|
||||||
|
setting: 'inventory',
|
||||||
|
pagename: 'inventory',
|
||||||
|
params: {
|
||||||
|
values:
|
||||||
|
'imeis__warehouse,imeis__warehouse__name,imeis__warehouse__address,id,code,product,product__name,product__os__name,product__external_storage__max_capacity,color,color__code,color__name,color__hex_code,ram,ram__code,ram__capacity,internal_storage,internal_storage__code,internal_storage__capacity,image,image__path,price,note,create_time,update_time',
|
||||||
|
distinct_values: {
|
||||||
|
imei_count: { type: 'Count', field: 'imeis' },
|
||||||
|
},
|
||||||
|
summary: 'annotate',
|
||||||
|
sort: 'id',
|
||||||
|
},
|
||||||
|
timeopt: { time: 36000 },
|
||||||
|
modal: {
|
||||||
|
component: 'imports/AddProduct',
|
||||||
|
title: 'Tạo sản phẩm',
|
||||||
|
width: '90%',
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
166
app/components/inventory/InventoryItemDetails.vue
Normal file
166
app/components/inventory/InventoryItemDetails.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({ variant: Object });
|
||||||
|
|
||||||
|
const moveHistory = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 1,
|
||||||
|
type__name: "Nhập",
|
||||||
|
date: "2026-04-03",
|
||||||
|
code: "PO-009",
|
||||||
|
to__code: "WH-HCM/C-01",
|
||||||
|
delta: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: 2,
|
||||||
|
type__name: "Xuất",
|
||||||
|
date: "2026-04-04",
|
||||||
|
code: "SO-034",
|
||||||
|
from__code: "WH-HCM/C-01",
|
||||||
|
delta: -100,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="block is-flex is-gap-2 is-align-items-center">
|
||||||
|
<div class="is-flex is-gap-2 is-align-items-flex-start">
|
||||||
|
<NuxtImg
|
||||||
|
v-if="variant.image__path"
|
||||||
|
:src="variant.image__path"
|
||||||
|
width="45"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="has-background-purple size-12 rounded-md is-flex is-justify-content-center is-align-items-center"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:deployed-code-outline"
|
||||||
|
:size="24"
|
||||||
|
class="has-text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold">
|
||||||
|
{{ variant.product__name }} - {{ variant.color__code }} - {{ variant.ram__code }}
|
||||||
|
{{ variant.internal_storage__code }}
|
||||||
|
</p>
|
||||||
|
<p class="tag fs-13">{{ variant.code }}</p>
|
||||||
|
<p class="fs-12 has-text-grey mt-1">Category{{ variant.category }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6 is-flex is-gap-1">
|
||||||
|
<div class="px-4 py-3 is-flex-grow-1 rounded-md has-background-blue-95 has-text-blue-40">
|
||||||
|
<p class="fs-13">Giá đơn vị</p>
|
||||||
|
<p class="fs-16 font-semibold">
|
||||||
|
{{ $formatNum(variant.price, { hasUnit: true }) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-3 is-flex-grow-1 rounded-md has-background-purple-90 has-text-purple-40">
|
||||||
|
<p class="fs-13">Tổng giá trị</p>
|
||||||
|
<p class="fs-16 font-semibold">
|
||||||
|
{{ $formatNum(variant.total || variant.price * 25, { hasUnit: true }) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="fs-15 font-semibold mb-2">Tóm tắt tồn kho</p>
|
||||||
|
<div class="fs-14 is-flex is-flex-direction-column">
|
||||||
|
<div class="py-1 is-flex is-justify-content-space-between">
|
||||||
|
<p class="has-text-grey">Tồn hiện tại</p>
|
||||||
|
<p class="font-semibold">{{ variant.stock || 25 }}</p>
|
||||||
|
</div>
|
||||||
|
<hr class="my-1" />
|
||||||
|
<div class="py-1 is-flex is-justify-content-space-between">
|
||||||
|
<p class="has-text-grey">Đặt trước</p>
|
||||||
|
<p class="font-semibold has-text-orange">
|
||||||
|
{{ variant.preorder || 0 }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<hr class="my-1" />
|
||||||
|
<div class="py-1 is-flex is-justify-content-space-between">
|
||||||
|
<p class="has-text-grey">Khả dụng</p>
|
||||||
|
<p class="font-semibold has-text-green-40">
|
||||||
|
{{ variant.available || 25 }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="fs-15 font-semibold mb-4">Vị trí lưu kho</p>
|
||||||
|
<div class="p-4 rounded-md has-background-grey-100">
|
||||||
|
<div class="is-flex is-gap-1">
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:location-on-outline-rounded"
|
||||||
|
:size="20"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p>{{ variant.imeis__warehouse__name }}</p>
|
||||||
|
<p class="mt-1 fs-13 has-text-grey">
|
||||||
|
{{ variant.imeis__warehouse__address }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="fs-15 font-semibold mb-4">Lịch sử di chuyển</p>
|
||||||
|
<div
|
||||||
|
v-for="move in moveHistory"
|
||||||
|
:key="move.id"
|
||||||
|
class="p-4 mb-4 rounded-md has-background-grey-100 has-text-grey fs-12"
|
||||||
|
>
|
||||||
|
<div class="is-flex is-justify-content-space-between mb-1">
|
||||||
|
<div class="is-flex is-gap-1 is-align-items-center">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'p-1 rounded is-flex is-align-items-center',
|
||||||
|
`has-background-${move.delta > 0 ? 'success' : 'danger'}-soft`,
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
:name="move.delta > 0 ? 'ph:chart-line-up' : 'ph:chart-line-down'"
|
||||||
|
:size="18"
|
||||||
|
:class="`has-text-${move.delta > 0 ? 'success' : 'danger'}-40`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p :class="`has-text-${move.delta > 0 ? 'success' : 'danger'}-40`">
|
||||||
|
{{ move.type__name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p :class="['fs-14 font-semibold', `has-text-${move.delta > 0 ? 'success' : 'danger'}-40`]">
|
||||||
|
{{
|
||||||
|
new Intl.NumberFormat("en-US", {
|
||||||
|
signDisplay: "exceptZero",
|
||||||
|
}).format(move.delta)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p class="is-flex is-gap-0.5 is-align-items-center">
|
||||||
|
<span>{{ $dayjs(move.date).format("L") }}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span class="is-family-monospace">{{ move.code }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>{{ move.from__code ? "Từ" : "Đến" }}</span>
|
||||||
|
<span>: </span>
|
||||||
|
<span>{{ move.from__code || move.to__code }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="fs-15 font-semibold mb-4">Thao tác nhanh</p>
|
||||||
|
<div class="fixed-grid">
|
||||||
|
<div class="grid">
|
||||||
|
<button class="button fs-13 is-primary">Điều chỉnh</button>
|
||||||
|
<button class="button fs-13">Chuyển kho</button>
|
||||||
|
<button class="button fs-13">Xem báo cáo</button>
|
||||||
|
<button class="button fs-13">In nhãn</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
30
app/components/inventory/InventoryItemName.vue
Normal file
30
app/components/inventory/InventoryItemName.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({ variant: Object });
|
||||||
|
const showDetailsModal = ref();
|
||||||
|
|
||||||
|
function openDetailsModal() {
|
||||||
|
showDetailsModal.value = {
|
||||||
|
component: "inventory/InventoryItemDetails",
|
||||||
|
title: "Chi tiết sản phẩm",
|
||||||
|
width: "min(700px, 75%)",
|
||||||
|
vbind: {
|
||||||
|
variant: props.variant,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a @click="openDetailsModal">
|
||||||
|
{{ variant.product__name }}
|
||||||
|
</a>
|
||||||
|
<p class="has-text-grey">
|
||||||
|
{{ variant.color__code }} - {{ variant.ram__code }}
|
||||||
|
{{ variant.internal_storage__code }}
|
||||||
|
</p>
|
||||||
|
<Modal
|
||||||
|
v-if="showDetailsModal"
|
||||||
|
v-bind="showDetailsModal"
|
||||||
|
@close="showDetailsModal = null"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
const props = defineProps({
|
|
||||||
invItem: Object,
|
|
||||||
selected: Boolean,
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(["selectInvItem", "unselect"]);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<tr
|
|
||||||
:class="['is-clickable', selected && 'is-selected']"
|
|
||||||
@click="selected ? emit('unselect') : emit('selectInvItem', invItem.id)"
|
|
||||||
>
|
|
||||||
<td>
|
|
||||||
<div>
|
|
||||||
<p class="fs-14">{{ invItem.name }}</p>
|
|
||||||
<p class="fs-12 has-text-grey">{{ invItem.category }}</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<!-- <td class="is-family-monospace fs-12">{{ invItem.sku }}</td>
|
|
||||||
<td>{{ invItem.storage }}</td>
|
|
||||||
<td class="is-family-monospace fs-12">{{ invItem.storage__position }}</td> -->
|
|
||||||
<td class="has-text-right">{{ invItem.stock }}</td>
|
|
||||||
<td class="has-text-right">{{ invItem.preorder }}</td>
|
|
||||||
<td :class="['has-text-right', invItem.status === 'OK' ? 'has-text-success-30' : 'has-text-warning-30']">
|
|
||||||
{{ invItem.available }}
|
|
||||||
</td>
|
|
||||||
<!-- <td class="is-family-monospace fs-12">{{ invItem.batch }}</td>
|
|
||||||
<td>{{ $dayjs(invItem.expired).format("L") }}</td> -->
|
|
||||||
<td>
|
|
||||||
<span
|
|
||||||
:class="[
|
|
||||||
'tag is-rounded',
|
|
||||||
invItem.status === 'OK'
|
|
||||||
? 'has-background-success-soft has-text-success-bold'
|
|
||||||
: 'has-background-warning-soft has-text-warning-bold',
|
|
||||||
]"
|
|
||||||
>{{ invItem.status }}</span
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
<style scoped>
|
|
||||||
tr.is-selected {
|
|
||||||
--bulma-table-row-active-background-color: var(--bulma-primary-95);
|
|
||||||
color: unset;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
vertical-align: middle;
|
|
||||||
--bulma-table-cell-padding: 0.75em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,565 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import InventoryRow from "~/components/inventory/InventoryRow.vue";
|
|
||||||
import SelectedInvItem from "~/components/inventory/SelectedInvItem.vue";
|
|
||||||
|
|
||||||
const invItems = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "Dầu gội thảo mộc 500ml",
|
|
||||||
category: "Chăm sóc tóc",
|
|
||||||
sku: "DG-012",
|
|
||||||
storage: "Kho TP.HCM",
|
|
||||||
storage__position: "WH-HCM/C-01",
|
|
||||||
stock: 200,
|
|
||||||
preorder: 50,
|
|
||||||
available: 150,
|
|
||||||
unit_price: "120000.00",
|
|
||||||
total: "24000000.00",
|
|
||||||
batch: "BATCH-2024-012",
|
|
||||||
expired: "2026-08-20",
|
|
||||||
status: "OK",
|
|
||||||
moveHistory: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
type: 1,
|
|
||||||
type__name: "Nhập",
|
|
||||||
date: "2026-04-03",
|
|
||||||
code: "PO-009",
|
|
||||||
to__code: "WH-HCM/C-01",
|
|
||||||
delta: 300,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
type: 2,
|
|
||||||
type__name: "Xuất",
|
|
||||||
date: "2026-04-04",
|
|
||||||
code: "SO-034",
|
|
||||||
from__code: "WH-HCM/C-01",
|
|
||||||
delta: -100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Gel rửa mặt làm sạch sâu",
|
|
||||||
category: "Mỹ phẩm",
|
|
||||||
sku: "GRM-006",
|
|
||||||
storage: "Kho Hà Nội",
|
|
||||||
storage__position: "WH-HN/A-03",
|
|
||||||
stock: 12,
|
|
||||||
preorder: 5,
|
|
||||||
available: 7,
|
|
||||||
unit_price: "140000.00",
|
|
||||||
total: "1700000.00",
|
|
||||||
batch: "BATCH-2024-006",
|
|
||||||
expired: "2025-11-20",
|
|
||||||
status: "Thấp",
|
|
||||||
moveHistory: [
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
type: 2,
|
|
||||||
type__name: "Xuất",
|
|
||||||
date: "2026-03-28",
|
|
||||||
code: "SO-028",
|
|
||||||
to__code: "WH-HN/A-03",
|
|
||||||
delta: -50,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "Kem chống nắng SPF 50+",
|
|
||||||
category: "Mỹ phẩm",
|
|
||||||
sku: "KCN-001",
|
|
||||||
storage: "Kho Hà Nội",
|
|
||||||
storage__position: "WH-HN/A-01",
|
|
||||||
stock: 150,
|
|
||||||
preorder: 20,
|
|
||||||
available: 130,
|
|
||||||
unit_price: "280000.00",
|
|
||||||
total: "42000000.00",
|
|
||||||
batch: "BATCH-2024-001",
|
|
||||||
expired: "2025-10-15",
|
|
||||||
status: "OK",
|
|
||||||
moveHistory: [
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
type: 1,
|
|
||||||
type__name: "Nhập",
|
|
||||||
date: "2026-04-05",
|
|
||||||
code: "PO-001",
|
|
||||||
from__code: "WH-HN/A-01",
|
|
||||||
delta: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
type: 2,
|
|
||||||
type__name: "Xuất",
|
|
||||||
date: "2026-04-06",
|
|
||||||
code: "SO-023",
|
|
||||||
from__code: "WH-HN/A-01",
|
|
||||||
delta: -50,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "Kem dưỡng ẩm ban đêm",
|
|
||||||
category: "Dưỡng da",
|
|
||||||
sku: "KD-007",
|
|
||||||
storage: "Kho Đà Nẵng",
|
|
||||||
storage__position: "WH-DN/A-01",
|
|
||||||
stock: 8,
|
|
||||||
preorder: 3,
|
|
||||||
available: 5,
|
|
||||||
unit_price: "380000.00",
|
|
||||||
total: "30040000.00",
|
|
||||||
batch: "BATCH-2024-007",
|
|
||||||
expired: "2025-09-10",
|
|
||||||
status: "Thấp",
|
|
||||||
moveHistory: [
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
type: 2,
|
|
||||||
type__name: "Xuất",
|
|
||||||
date: "2026-03-25",
|
|
||||||
code: "SO-029",
|
|
||||||
from__code: "WH-DN/A-01",
|
|
||||||
delta: -40,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: "Kem mắt chống lão hóa",
|
|
||||||
category: "Dưỡng da",
|
|
||||||
sku: "KM-011",
|
|
||||||
storage: "Kho Hà Nội",
|
|
||||||
storage__position: "WH-HN/B-02",
|
|
||||||
stock: 30,
|
|
||||||
preorder: 28,
|
|
||||||
available: 2,
|
|
||||||
unit_price: "550000.00",
|
|
||||||
total: "16500000.00",
|
|
||||||
batch: "BATCH-2024-011",
|
|
||||||
expired: "2025-10-05",
|
|
||||||
status: "Thấp",
|
|
||||||
moveHistory: [
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
type: 2,
|
|
||||||
type__name: "Xuất",
|
|
||||||
date: "2026-03-27",
|
|
||||||
code: "SO-033",
|
|
||||||
from__code: "WH-HN/B-02",
|
|
||||||
delta: -25,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: "Mặt nạ collagen 10 miếng",
|
|
||||||
category: "Mặt nạ",
|
|
||||||
sku: "MN-004",
|
|
||||||
storage: "Kho TP.HCM",
|
|
||||||
storage__position: "WH-HCM/A-01",
|
|
||||||
stock: 45,
|
|
||||||
preorder: 15,
|
|
||||||
available: 30,
|
|
||||||
unit_price: "320000.00",
|
|
||||||
total: "14400000.00",
|
|
||||||
batch: "BATCH-2024-004",
|
|
||||||
expired: "2025-08-15",
|
|
||||||
status: "OK",
|
|
||||||
moveHistory: [
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
type: 1,
|
|
||||||
type__name: "Nhập",
|
|
||||||
date: "2026-03-30",
|
|
||||||
code: "PO-004",
|
|
||||||
to__code: "WH-HCM/A-01",
|
|
||||||
delta: 60,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
type: 2,
|
|
||||||
type__name: "Xuất",
|
|
||||||
date: "2026-04-01",
|
|
||||||
code: "SO-026",
|
|
||||||
from__code: "WH-HCM/A-01",
|
|
||||||
delta: -15,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
name: "Nước tẩy trang 3 trong 1",
|
|
||||||
category: "Tẩy trang",
|
|
||||||
sku: "NTT-010",
|
|
||||||
storage: "Kho Đà Nẵng",
|
|
||||||
storage__position: "WH-DN/A-02",
|
|
||||||
stock: 140,
|
|
||||||
preorder: 35,
|
|
||||||
available: 105,
|
|
||||||
unit_price: "195000.00",
|
|
||||||
total: "27300000.00",
|
|
||||||
batch: "BATCH-2024-010",
|
|
||||||
expired: "2026-04-30",
|
|
||||||
status: "OK",
|
|
||||||
moveHistory: [
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
type: 1,
|
|
||||||
type__name: "Nhập",
|
|
||||||
date: "2026-03-29",
|
|
||||||
code: "PO-008",
|
|
||||||
to__code: "WH-DN/A-02",
|
|
||||||
delta: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
type: 2,
|
|
||||||
type__name: "Xuất",
|
|
||||||
date: "2026-04-01",
|
|
||||||
code: "SO-032",
|
|
||||||
from__code: "WH-DN/A-02",
|
|
||||||
delta: -60,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
name: "Phấn nền BB cream",
|
|
||||||
category: "Trang điểm",
|
|
||||||
sku: "PN-009",
|
|
||||||
storage: "Kho TP.HCM",
|
|
||||||
storage__position: "WH-HCM/B-01",
|
|
||||||
stock: 65,
|
|
||||||
preorder: 20,
|
|
||||||
available: 45,
|
|
||||||
unit_price: "220000.00",
|
|
||||||
total: "14300000.00",
|
|
||||||
batch: "BATCH-2024-009",
|
|
||||||
expired: "2025-07-15",
|
|
||||||
status: "OK",
|
|
||||||
moveHistory: [
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
type: 1,
|
|
||||||
type__name: "Nhập",
|
|
||||||
date: "2026-04-01",
|
|
||||||
code: "PO-007",
|
|
||||||
to__code: "WH-HCM/B-01",
|
|
||||||
delta: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
type: 2,
|
|
||||||
type__name: "Xuất",
|
|
||||||
date: "2026-04-02",
|
|
||||||
code: "SO-031",
|
|
||||||
from__code: "WH-HCM/B-01",
|
|
||||||
delta: -35,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
name: "Serum Vitamin C 30ml",
|
|
||||||
category: "Dưỡng da",
|
|
||||||
sku: "SER-003",
|
|
||||||
storage: "Kho Hà Nội",
|
|
||||||
storage__position: "WH-HN/B-01",
|
|
||||||
stock: 220,
|
|
||||||
preorder: 40,
|
|
||||||
available: 180,
|
|
||||||
unit_price: "450000.00",
|
|
||||||
total: "99000000.00",
|
|
||||||
batch: "BATCH-2024-003",
|
|
||||||
expired: "2025-12-31",
|
|
||||||
status: "OK",
|
|
||||||
moveHistory: [
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
type: 1,
|
|
||||||
type__name: "Nhập",
|
|
||||||
date: "2026-04-01",
|
|
||||||
code: "PO-003",
|
|
||||||
to__code: "WH-HN/B-01",
|
|
||||||
delta: 300,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 15,
|
|
||||||
type: 2,
|
|
||||||
type__name: "Xuất",
|
|
||||||
date: "2026-04-02",
|
|
||||||
code: "SO-025",
|
|
||||||
from__code: "WH-HN/B-01",
|
|
||||||
delta: -80,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
name: "Son dưỡng môi vitamin E",
|
|
||||||
category: "Trang điểm",
|
|
||||||
sku: "SD-008",
|
|
||||||
storage: "Kho Hà Nội",
|
|
||||||
storage__position: "WH-HN/C-01",
|
|
||||||
stock: 95,
|
|
||||||
preorder: 45,
|
|
||||||
available: 50,
|
|
||||||
unit_price: "75000.00",
|
|
||||||
total: "7100000.00",
|
|
||||||
batch: "BATCH-2024-008",
|
|
||||||
expired: "2026-02-28",
|
|
||||||
status: "OK",
|
|
||||||
moveHistory: [
|
|
||||||
{
|
|
||||||
id: 16,
|
|
||||||
type: 1,
|
|
||||||
type__name: "Nhập",
|
|
||||||
date: "2026-04-04",
|
|
||||||
code: "PO-006",
|
|
||||||
to__code: "WH-HN/C-01",
|
|
||||||
delta: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 17,
|
|
||||||
type: 2,
|
|
||||||
type__name: "Xuất",
|
|
||||||
date: "2026-04-02",
|
|
||||||
code: "SO-030",
|
|
||||||
from__code: "WH-HN/C-01",
|
|
||||||
delta: -55,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
name: "Sữa rửa mặt trà xanh 150ml",
|
|
||||||
category: "Mỹ phẩm",
|
|
||||||
sku: "SRM-002",
|
|
||||||
storage: "Kho Hà Nội",
|
|
||||||
storage__position: "WH-HN/A-02",
|
|
||||||
stock: 85,
|
|
||||||
preorder: 30,
|
|
||||||
available: 55,
|
|
||||||
unit_price: "150000.00",
|
|
||||||
total: "12800000.00",
|
|
||||||
batch: "BATCH-2024-002",
|
|
||||||
expired: "2026-03-20",
|
|
||||||
status: "OK",
|
|
||||||
moveHistory: [
|
|
||||||
{
|
|
||||||
id: 18,
|
|
||||||
type: 1,
|
|
||||||
type__name: "Nhập",
|
|
||||||
date: "2026-04-03",
|
|
||||||
code: "PO-002",
|
|
||||||
to__code: "WH-HN/A-02",
|
|
||||||
delta: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 19,
|
|
||||||
type: 2,
|
|
||||||
type__name: "Xuất",
|
|
||||||
date: "2026-04-04",
|
|
||||||
code: "SO-025",
|
|
||||||
from__code: "WH-HN/A-02",
|
|
||||||
delta: -15,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
name: "Toner cân bằng da 200ml",
|
|
||||||
category: "Dưỡng da",
|
|
||||||
sku: "TN-005",
|
|
||||||
storage: "Kho TP.HCM",
|
|
||||||
storage__position: "WH-HCM/A-02",
|
|
||||||
stock: 180,
|
|
||||||
preorder: 25,
|
|
||||||
available: 155,
|
|
||||||
unit_price: "180000.00",
|
|
||||||
total: "32400000.00",
|
|
||||||
batch: "BATCH-2024-005",
|
|
||||||
expired: "2026-06-30",
|
|
||||||
status: "OK",
|
|
||||||
moveHistory: [
|
|
||||||
{
|
|
||||||
id: 20,
|
|
||||||
type: 1,
|
|
||||||
type__name: "Nhập",
|
|
||||||
date: "2026-04-02",
|
|
||||||
code: "PO-005",
|
|
||||||
to__code: "WH-HCM/A-02",
|
|
||||||
delta: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 21,
|
|
||||||
type: 2,
|
|
||||||
type__name: "Xuất",
|
|
||||||
date: "2026-04-04",
|
|
||||||
code: "SO-025",
|
|
||||||
from__code: "WH-HCM/A-02",
|
|
||||||
delta: -15,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const storages = ["Kho Hà Nội", "Kho TP.HCM", "Kho Đà Nẵng"];
|
|
||||||
|
|
||||||
const categories = ["Chăm sóc tóc", "Dưỡng da", "Mỹ phẩm", "Mặt nạ", "Trang điểm", "Tẩy trang"];
|
|
||||||
|
|
||||||
const statuses = ["OK", "Thấp"];
|
|
||||||
|
|
||||||
const input = ref();
|
|
||||||
const selectedStorage = ref();
|
|
||||||
const selectedCategory = ref();
|
|
||||||
const selectedStatus = ref();
|
|
||||||
|
|
||||||
const filteredInvItems = computed(() => {
|
|
||||||
const filteredByInput = invItems.filter((invItem) => {
|
|
||||||
if (!input.value) return true;
|
|
||||||
const values = Object.values(invItem);
|
|
||||||
const strValues = values.filter((v) => typeof v === "string");
|
|
||||||
return strValues.some((str) => str.toLowerCase().includes(input.value.toLowerCase()));
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredByStorage = filteredByInput.filter((invItem) => {
|
|
||||||
if (!selectedStorage.value) return true;
|
|
||||||
return invItem.storage === selectedStorage.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredByCategory = filteredByStorage.filter((invItem) => {
|
|
||||||
if (!selectedCategory.value) return true;
|
|
||||||
return invItem.category === selectedCategory.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredByStatus = filteredByCategory.filter((invItem) => {
|
|
||||||
if (!selectedStatus.value) return true;
|
|
||||||
return invItem.status === selectedStatus.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return filteredByStatus;
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedInvItem = ref(null);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="is-flex is-gap-2 is-align-items-center">
|
|
||||||
<div class="field is-flex-grow-1 mb-0">
|
|
||||||
<p class="control has-icons-left">
|
|
||||||
<input
|
|
||||||
v-model="input"
|
|
||||||
class="input"
|
|
||||||
type="text"
|
|
||||||
placeholder="Tìm kiếm theo tên, SKU, barcode..."
|
|
||||||
/>
|
|
||||||
<span class="icon is-small is-left">
|
|
||||||
<Icon
|
|
||||||
name="material-symbols:search-rounded"
|
|
||||||
:size="20"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="select">
|
|
||||||
<select v-model="selectedStorage">
|
|
||||||
<option :value="undefined">Tất cả kho</option>
|
|
||||||
<option
|
|
||||||
v-for="storage in storages"
|
|
||||||
:key="storage"
|
|
||||||
:value="storage"
|
|
||||||
>
|
|
||||||
{{ storage }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="select">
|
|
||||||
<select v-model="selectedCategory">
|
|
||||||
<option :value="undefined">Tất cả danh mục</option>
|
|
||||||
<option
|
|
||||||
v-for="category in categories"
|
|
||||||
:key="category"
|
|
||||||
:value="category"
|
|
||||||
>
|
|
||||||
{{ category }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="select">
|
|
||||||
<select v-model="selectedStatus">
|
|
||||||
<option :value="undefined">Trạng thái tồn</option>
|
|
||||||
<option
|
|
||||||
v-for="status in statuses"
|
|
||||||
:key="status"
|
|
||||||
:value="status"
|
|
||||||
>
|
|
||||||
{{ status }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fixed-grid has-3-cols">
|
|
||||||
<div class="grid">
|
|
||||||
<div :class="['cell', selectedInvItem ? 'is-col-span-2' : 'is-col-span-3']">
|
|
||||||
<div class="card is-clipped">
|
|
||||||
<div class="card-content p-0">
|
|
||||||
<p class="p-5 icon-text fs-16 font-semibold w-full">
|
|
||||||
<span class="icon">
|
|
||||||
<Icon
|
|
||||||
name="material-symbols:deployed-code-outline"
|
|
||||||
:size="20"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>Danh sách tồn kho ({{ filteredInvItems.length }})</span>
|
|
||||||
</p>
|
|
||||||
<table class="table is-fullwidth is-hoverable fs-13">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="font-semibold">Sản phẩm</th>
|
|
||||||
<!-- <th class="font-semibold">SKU</th>
|
|
||||||
<th class="font-semibold">Kho</th>
|
|
||||||
<th class="font-semibold">Vị trí</th> -->
|
|
||||||
<th class="font-semibold has-text-right">Tồn</th>
|
|
||||||
<th class="font-semibold has-text-right">Đặt trước</th>
|
|
||||||
<th class="font-semibold has-text-right">Khả dụng</th>
|
|
||||||
<!-- <th class="font-semibold">Batch</th>
|
|
||||||
<th class="font-semibold">Hạn sử dụng</th> -->
|
|
||||||
<th class="font-semibold">Trạng thái</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<InventoryRow
|
|
||||||
v-for="invItem in filteredInvItems"
|
|
||||||
:key="invItem.id"
|
|
||||||
:invItem="invItem"
|
|
||||||
:selected="invItem.id === selectedInvItem?.id"
|
|
||||||
@selectInvItem="
|
|
||||||
(id) => {
|
|
||||||
selectedInvItem = filteredInvItems.find((item) => item.id === id);
|
|
||||||
}
|
|
||||||
"
|
|
||||||
@unselect="selectedInvItem = null"
|
|
||||||
/>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SelectedInvItem
|
|
||||||
:invItem="selectedInvItem"
|
|
||||||
@unselect="selectedInvItem = null"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
20
app/components/inventory/Status.vue
Normal file
20
app/components/inventory/Status.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
variant: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = computed(() => {
|
||||||
|
const stock = props.variant.imei_count - 0; // imei sold
|
||||||
|
if (stock === 0) return { text: "Hết", color: "red" };
|
||||||
|
if (stock < 10) return { text: "Thấp", color: "yellow" };
|
||||||
|
return { text: "OK", color: "green" };
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
:class="['tag is-rounded is-light fs-12', `is-${status.color}`]"
|
||||||
|
:style="{ border: `1px solid var(--bulma-${status.color}-80)` }"
|
||||||
|
>{{ status.text }}</span
|
||||||
|
>
|
||||||
|
</template>
|
||||||
@@ -54,14 +54,14 @@ async function addToCart() {
|
|||||||
|
|
||||||
async function fetchImeis() {
|
async function fetchImeis() {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
const imeisFetched = await $getdata("IMEI", {
|
const [imeisRes, imeisSoldRes] = await Promise.all([
|
||||||
filter: { variant: props.variant.id },
|
$getdata("IMEI", { filter: { variant: props.variant.id } }),
|
||||||
});
|
$getdata("IMEI_Sold"),
|
||||||
const imeisSoldFetched = await $getdata("IMEI_Sold");
|
]);
|
||||||
|
|
||||||
imeis.value = imeisFetched.filter((imeiRec) => {
|
imeis.value = imeisRes.filter((imeiRec) => {
|
||||||
const inCart = activeCartItems.value.find((cartItem) => cartItem.imei === imeiRec.id);
|
const inCart = activeCartItems.value.find((cartItem) => cartItem.imei === imeiRec.id);
|
||||||
const sold = imeisSoldFetched.find((imeiSold) => imeiSold.imei === imeiRec.imei);
|
const sold = imeisSoldRes.find((imeiSold) => imeiSold.imei === imeiRec.imei);
|
||||||
return !inCart && !sold;
|
return !inCart && !sold;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,6 +105,7 @@ onMounted(fetchImeis);
|
|||||||
<th>STT</th>
|
<th>STT</th>
|
||||||
<th>IMEI</th>
|
<th>IMEI</th>
|
||||||
<th>Trạng thái</th>
|
<th>Trạng thái</th>
|
||||||
|
<th>Kho</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -130,6 +131,7 @@ onMounted(fetchImeis);
|
|||||||
imeiRec.deleted ? "Không có sẵn" : "Có sẵn"
|
imeiRec.deleted ? "Không có sẵn" : "Có sẵn"
|
||||||
}}</span>
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ imeiRec.warehouse__name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
params: { values },
|
params: { values },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// update store
|
|
||||||
if (api.commit) {
|
if (api.commit) {
|
||||||
if (store[api.commit]) {
|
if (store[api.commit]) {
|
||||||
const copy = $copy(store[api.commit]);
|
const copy = $copy(store[api.commit]);
|
||||||
@@ -117,6 +117,7 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
store.commit(api.commit, copy);
|
store.commit(api.commit, copy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notify) {
|
if (notify) {
|
||||||
$snackbar(
|
$snackbar(
|
||||||
store.lang === "en" ? "Data has been successfully saved to the system." : "Dữ liệu đã được lưu vào hệ thống",
|
store.lang === "en" ? "Data has been successfully saved to the system." : "Dữ liệu đã được lưu vào hệ thống",
|
||||||
@@ -138,12 +139,22 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
const api = findapi(name);
|
const api = findapi(name);
|
||||||
const curpath = getpath(api.path);
|
const curpath = getpath(api.path);
|
||||||
const updateUrl = api.url_detail || api.url;
|
const updateUrl = api.url_detail || api.url;
|
||||||
|
let rs;
|
||||||
|
|
||||||
const rs = await $fetch(`${curpath}${updateUrl}${data.id}/`, {
|
if (Array.isArray(data)) {
|
||||||
method: "PATCH",
|
const model = api.url.slice(5);
|
||||||
body: data,
|
rs = await $fetch(`${curpath}import-data/${model}`, {
|
||||||
params: { values: values || api.params?.values },
|
method: "POST",
|
||||||
});
|
body: data,
|
||||||
|
params: { values, action: "import" },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
rs = await $fetch(`${curpath}${updateUrl}${data.id}/`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: data,
|
||||||
|
params: { values: values || api.params?.values },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (api.commit) {
|
if (api.commit) {
|
||||||
const index = store[api.commit] ? store[api.commit].findIndex((v) => v.id === rs.id) : -1;
|
const index = store[api.commit] ? store[api.commit].findIndex((v) => v.id === rs.id) : -1;
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ import CustomerForm from "~/components/customer/CustomerForm.vue";
|
|||||||
import Dashboard from "~/components/dashboard/Dashboard.vue";
|
import Dashboard from "~/components/dashboard/Dashboard.vue";
|
||||||
import Orders from "~/components/orders/Orders.vue";
|
import Orders from "~/components/orders/Orders.vue";
|
||||||
import Inventory from "~/components/inventory/Inventory.vue";
|
import Inventory from "~/components/inventory/Inventory.vue";
|
||||||
|
import InventoryItemName from "~/components/inventory/InventoryItemName.vue";
|
||||||
|
import IMEICountByWarehouse from "~/components/inventory/IMEICountByWarehouse.vue";
|
||||||
|
import Status from "~/components/inventory/Status.vue";
|
||||||
import Rights from "~/components/rights/Rights.vue";
|
import Rights from "~/components/rights/Rights.vue";
|
||||||
import POS from "~/components/pos/POS.vue";
|
import POS from "~/components/pos/POS.vue";
|
||||||
import ChooseIMEIButton from "~/components/pos/ChooseIMEIButton.vue";
|
import ChooseIMEIButton from "~/components/pos/ChooseIMEIButton.vue";
|
||||||
@@ -80,6 +83,9 @@ const components = {
|
|||||||
Dashboard,
|
Dashboard,
|
||||||
Orders,
|
Orders,
|
||||||
Inventory,
|
Inventory,
|
||||||
|
InventoryItemName,
|
||||||
|
IMEICountByWarehouse,
|
||||||
|
Status,
|
||||||
Rights,
|
Rights,
|
||||||
POS,
|
POS,
|
||||||
ChooseIMEIButton,
|
ChooseIMEIButton,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const usePosStore = defineStore("pos", () => {
|
|||||||
id: activeCartId.value,
|
id: activeCartId.value,
|
||||||
customer: cusId,
|
customer: cusId,
|
||||||
},
|
},
|
||||||
|
notify: false,
|
||||||
});
|
});
|
||||||
await getCarts();
|
await getCarts();
|
||||||
isChangingCus.value = false;
|
isChangingCus.value = false;
|
||||||
|
|||||||
@@ -634,7 +634,7 @@ export default /** @type {const} */ ([
|
|||||||
params: {
|
params: {
|
||||||
sort: "id",
|
sort: "id",
|
||||||
values:
|
values:
|
||||||
"id,code,imei,variant,variant,variant__code,variant__product,variant__product__code,variant__product__name,variant__product__manufacturer,variant__product__os,variant__product__battery,variant__product__screen,variant__product__cpu,variant__product__gpu,variant__product__camera_system,variant__product__sim,variant__product__network_technology,variant__product__charging_technology,variant__product__external_storage,variant__product__ip_rating,variant__product__design,variant__product__creator,variant__product__updater,variant__product__deleted,variant__color,variant__color__code,variant__color__name,variant__color__hex_code,variant__color__deleted,variant__ram,variant__ram__code,variant__ram__capacity,variant__ram__deleted,variant__internal_storage,variant__internal_storage__code,variant__internal_storage__capacity,variant__internal_storage__deleted,variant__image,variant__image__code,variant__image__name,variant__image__path,variant__image__is_active,variant__image__deleted,variant__image__create_time,variant__price,variant__note,variant__creator,variant__updater,variant__deleted,variant__create_time,variant__update_time,deleted,create_time,update_time",
|
"id,code,imei,variant,variant,variant__code,variant__product,variant__product__code,variant__product__name,variant__product__manufacturer,variant__product__os,variant__product__battery,variant__product__screen,variant__product__cpu,variant__product__gpu,variant__product__camera_system,variant__product__sim,variant__product__network_technology,variant__product__charging_technology,variant__product__external_storage,variant__product__ip_rating,variant__product__design,variant__product__creator,variant__product__updater,variant__product__deleted,variant__color,variant__color__code,variant__color__name,variant__color__hex_code,variant__color__deleted,variant__ram,variant__ram__code,variant__ram__capacity,variant__ram__deleted,variant__internal_storage,variant__internal_storage__code,variant__internal_storage__capacity,variant__internal_storage__deleted,variant__image,variant__image__code,variant__image__name,variant__image__path,variant__image__is_active,variant__image__deleted,variant__image__create_time,variant__price,variant__note,variant__creator,variant__updater,variant__deleted,variant__create_time,variant__update_time,warehouse,warehouse__code,warehouse__name,warehouse__address,warehouse__deleted,deleted,create_time,update_time",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -718,6 +718,12 @@ export default /** @type {const} */ ([
|
|||||||
url_detail: "data-detail/Staff/",
|
url_detail: "data-detail/Staff/",
|
||||||
params: {},
|
params: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Warehouse",
|
||||||
|
url: "data/Warehouse/",
|
||||||
|
url_detail: "data-detail/Warehouse/",
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
ssr: false,
|
ssr: false,
|
||||||
css: ["./my-bulma-project.css", "./app/assets/styles/main.scss"],
|
css: ["./my-bulma-project.css", "./app/assets/styles/main.scss"],
|
||||||
future: {
|
|
||||||
compatibilityVersion: 4,
|
|
||||||
},
|
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.svg" }],
|
link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.svg" }],
|
||||||
|
|||||||
Reference in New Issue
Block a user