PSTW_CentralizeSystem/Areas/Inventory/Views/InventoryMaster/ItemMovement.cshtml

1496 lines
86 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@{
ViewData["Title"] = "Item Movement";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<style>
@@font-face {
font-family: 'OCR-A';
src: url('../assets/fonts/ocraext.ttf');
}
.QrPrintFont {
font-family: 'OCR-A', monospace;
}
.table td img {
display: block !important;
}
.text-true {
color: green;
}
.text-false {
color: red;
}
.text-primary {
color: blue; /* Warna asal untuk 'Receive' */
}
.text-warning {
color: orange; /* Warna oren untuk 'Return' */
}
.text-success {
color: greenyellow;
}
.text-numb {
color: brown;
}
.text-weird {
color: purple;
}
.ms-auto {
margin-left: auto !important; /* Push Complete/Incomplete to right */
}
.dropdown {
position: relative;
width: 100%;
}
.dropdown-toggle-box {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 5px;
overflow: hidden;
}
.dropdown-toggle-box input {
flex: 1;
border: none;
padding: 8px;
}
.dropdown-btn {
border: none;
background-color: #007bff;
color: white;
padding: 8px 12px;
cursor: pointer;
}
.dropdown-content {
position: absolute;
width: 100%;
max-height: 200px;
overflow-y: auto;
background: #fff;
border: 1px solid #ddd;
z-index: 10;
}
.dropdown-content option {
padding: 10px;
cursor: pointer;
display: block;
}
.dropdown-content option:hover {
background-color: #f1f1f1;
}
</style>
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartial.cshtml")
<div id="registerItem" class="row">
<div class="row mb-3" >
<h2 for="sortSelect" class="col-sm-1 col-form-h2" style="min-width:140px;">Sort by:</h2>
<div class="col-sm-4">
<select id="sortSelect" class="form-control" v-model="sortBy" v-on:change="handleSorting">
<option value="all" >All</option>
<option value="item">Item</option>
<option value="station">Station</option>
<option value="logs" v-if="currentRole == 'Super Admin'">Logs</option>
</select>
</div>
</div>
<div class="row mb-3" v-if="sortBy === 'item'">
<h4 class="col-sm-1 col-form-h2" style="min-width:140px;">Search Item:</h4>
<div class="col-sm-4">
<input type="text" class="form-control" v-model="searchQuery" placeholder="Search by item code...">
</div>
</div>
<div class="row mb-3" v-if="sortBy === 'station'">
<h4 class="col-sm-1 col-form-h2" style="min-width:150px;">Search Station:</h4>
<div class="col-sm-4">
<div class="dropdown" v-click-outside="closeDropdown">
<!-- Button + Input dalam satu box -->
<div class="dropdown-toggle-box" v-on:click="dropdownOpen = !dropdownOpen">
<input type="text" class="form-control" v-model="searchQueryStation"
placeholder="Search Station..." v-on:focus="dropdownOpen = true" v-on:click.stop />
<button type="button" class="btn btn-primary dropdown-btn" v-on:click.stop="dropdownOpen = !dropdownOpen">
</button>
</div>
<!-- Dropdown list -->
<div v-if="dropdownOpen" class="dropdown-content" v-on:click.stop>
<div v-for="(item, index) in stations"
:key="index" class="dropdown-item" v-on:mousedown.prevent="selectStation(item)">
{{ item.stationName }}
</div>
</div>
</div>
</div>
</div>
<div v-if="sortBy === 'all'">
<div class="row card">
<div class="card-header">
<h2>Pending Item Transit</h2>
</div>
<div class="card-body">
<div v-if="loading">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<table class="table table-bordered table-hover table-striped no-wrap" id="itemMovementNotCompleteDatatable" style="width:100%;border-style: solid; border-width: 1px"></table>
</div>
</div>
<div class="row card">
<div class="card-header">
<h2>Complete Item Movement</h2>
</div>
<div class="card-header">
<h4>All Item Movement</h4>
</div>
<div class="card-body">
<div v-if="loading">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<table class="table table-bordered table-hover table-striped no-wrap" id="itemMovementCompleteDatatable" style="width:100%;border-style: solid; border-width: 1px"></table>
</div>
<div class="card-header">
<h4>Assign Station</h4>
</div>
<div class="card-body">
<div v-if="loading">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<table class="table table-bordered table-hover table-striped no-wrap" id="assignStationDatatable"
style="width:100%;border-style: solid; border-width: 1px"></table>
</div>
</div>
@* All Item Table *@
<div class="row card mt-3">
<div class="card-header">
<h2>All Item</h2>
</div>
<div class="card-body">
<div class="row mb-4 align-items-center">
<label class="col-sm-2 col-form-label" style="font-weight: bold;">Sort Location :</label>
<div class="col-sm-4">
<select class="form-select form-control" v-model="selectedLocation" v-on:change="updateAllItemsTable">
<option value="all">All Locations</option>
<option value="user">My Items</option>
<option v-for="station in PicStations" :key="station.stationId" :value="'station_' + station.stationId">
{{ station.stationName }}
</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered table-hover table-striped no-wrap" id="allItemsListDatatable" style="width:100%; border-style: solid; border-width: 1px"></table>
</div>
</div>
</div>
</div>
<div v-if="sortBy === 'logs'">
<div v-if="loading">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="row card">
<div class="card-header">
<h2>Item Movement List</h2>
</div>
<div class="card-body">
<table class="table table-bordered table-hover table-striped no-wrap" id="itemDatatable"
style=" width:100%;border-style: solid; border-width: 1px"></table>
</div>
</div>
</div>
<div v-if="sortBy === 'item'">
<div v-if="loading">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div v-for="(group, itemId) in paginatedItems" :key="itemId" class="row card">
<div class="card-header d-flex justify-content-between align-items-center">
@* <h2>Item : {{ group.uniqueID }}</h2> *@
<h2 v-if="group.uniqueID">Item : {{ group.uniqueID }}</h2>
<h2 v-else>Item : No Item Tracked</h2>
<button class="btn btn-light" v-on:click="toggleCategory(itemId)">
<i :class="categoryVisible[itemId] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i> Show Details
</button>
</div>
<!-- Hide all details unless button is clicked -->
<div v-show="categoryVisible[itemId]" class="card-body">
<div v-for="(movement, index) in group.movements.sort((a, b) => a.id - b.id).reverse()" :key="movement.id" class="movement-row">
<!-- 📌 Show Only Latest Movement -->
<div v-if="index === 0" class="row">
<strong>Latest Movement</strong>
<div class="col-md-12 d-flex flex-wrap align-items-center gap-3 p-2 border-bottom">
<!-- Movement Type -->
<h3 :class="{'text-primary': movement.toOther === 'On Delivery', 'text-warning': movement.toOther === 'Return',
'text-success': movement.lastStation !== null, 'text-info': movement.action === 'Assign' && movement.lastStation === null,
'text-numb': movement.toOther === 'Faulty' || movement.toOther === 'Calibration' || movement.toOther === 'Repair',
'text-weird': movement.action === 'Register'}"
class="flex-shrink-0 text-nowrap" style="max-width:140px; min-width:90px;">
{{ movement.toOther === 'Return' ? 'Return' : (movement.toOther === 'On Delivery' ? 'Receive' : ( movement.lastStation !== null ? 'Change' : ( movement.toOther == 'Faulty' || movement.toOther == 'Calibration' || movement.toOther == 'Repair' ? movement.toOther : ( movement.action == 'Register' ? 'Register' : 'Assign')))) }}
</h3>
<!-- Send Date -->
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:310px; min-width:310px;">
<h4 class="fixed-label m-0 text-nowrap">{{movement.action === 'Assign' ? 'Assign Date' : movement.action === 'Register' ? 'Register Date' : 'Send Date'}}</h4>
<span class="fixed-value text-truncate">{{movement.action !== 'Register' ? formatDate(movement.sendDate) : movement.date }}</span>
</div>
<!-- Receive Date -->
<div v-if="movement.action !== 'Assign' && movement.action !== 'Register'" class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:290px; min-width:290px;">
<h4 class="fixed-label m-0 text-nowrap">Receive Date:</h4>
<span class="fixed-value text-truncate" style="max-width:160px;">{{ formatDate(movement.receiveDate) || 'Not arrive' }}</span>
</div>
<!-- Action -->
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:150px; min-width:150px;">
<h4 class="fixed-labelStatus m-0 text-nowrap">Action:</h4>
<span class="fixed-value text-truncate">{{ movement.action }}</span>
</div>
<!-- Status -->
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:160px; min-width:160px;">
<h4 class="fixed-labelStatus m-0 text-nowrap">Status:</h4>
<span class="fixed-value text-truncate" style="max-width:90px;">{{ movement.latestStatus || movement.toOther }}</span>
</div>
<!-- More Details Button -->
<button class="btn btn-info btn-sm ms-auto" v-on:click="toggleDetails(movement.id)">
More Details
</button>
<!-- Completion Status -->
<h4 :class="movement.action == 'Register' ? 'text-success' : movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'text-success' : movement.toOther === 'Repair' || movement.toOther === 'Calibration' && movement.latestStatus === 'Ready To Deploy' ? 'text-success' :'text-danger'"
class="text-nowrap ms-3">
{{ movement.action == 'Register' ? 'Complete' : (movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'Complete' : ( movement.toOther === 'Repair' || movement.toOther === 'Calibration' && movement.latestStatus === 'Ready To Deploy' ? 'Complete' : (movement.latestStatus === 'Ready To Deploy' ? 'Canceled' : 'Incomplete'))) }}
</h4>
</div>
<div v-show="detailsVisible[movement.id]" class="col-md-12 mt-2">
<div class="row">
<div class="col-md-4 text-center">
<!-- Conditionally render Start Icon -->
<i v-if="movement.lastStation" class="fas fa-map-marker-alt"></i>
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-user fa-2x"></i>
<i v-else class="fas fa-warehouse fa-2x"></i>
<p><strong>Start</strong></p>
<p><strong>User:</strong> {{ movement.lastUserName }}</p>
<p><strong>Station:</strong> {{ movement.lastStationName }}</p>
<p><strong>Store:</strong> {{ movement.lastStoreName }}</p>
</div>
<div class="col-md-4 text-center">
<p></p>
<i class="fas fa-arrow-right fa-2x"></i>
<p>{{ movement.latestStatus || movement.toOther }}</p>
<p>
<button class="btn btn-info btn-sm ms-auto" v-on:click="remark(movement.remark)" v-if="movement.toOther !== 'On Delivery'">
Remark
</button>
</p>
<p>
<button class="btn btn-info btn-sm ms-auto" v-on:click="consignmentNote(movement.consignmentNote)" v-if="movement.toOther !== 'On Delivery'">
Consignment Note
</button>
</p>
</div>
<div class="col-md-4 text-center">
<!-- Conditionally render End Icon -->
<i v-if="movement.toStation" class="fas fa-map-marker-alt"></i>
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-warehouse fa-2x"></i>
<i v-else class="fas fa-user fa-2x"></i>
<p><strong>End</strong></p>
<p v-if="movement.toUser !== null"><strong>User:</strong> {{ movement.toUserName }}</p>
<p v-if="movement.toStation !== null"><strong>Station:</strong> {{ movement.toStationName }}</p>
<p v-if="movement.toStore !== null"><strong>Store:</strong> {{ movement.toStoreName }}</p>
</div>
</div>
</div>
</div>
</div>
<!-- 📌 Single View History Button -->
<button class="btn btn-light w-100 text-left" v-on:click="toggleHistory(itemId)">
<i :class="historyVisible[itemId] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i> View History
</button>
<div v-show="historyVisible[itemId]" class="history-row">
<div v-for="(movement, i) in group.movements.slice(1)" :key="i" class="row mt-2">
<div class="col-md-12 d-flex flex-wrap align-items-center gap-3 p-2 border-bottom">
<!-- Movement Type -->
<h3 :class="{'text-primary': movement.toOther === 'On Delivery', 'text-warning': movement.toOther === 'Return',
'text-success': movement.lastStation !== null, 'text-info': movement.action === 'Assign' && movement.lastStation === null,
'text-numb': movement.toOther === 'Faulty' || movement.toOther === 'Calibration' || movement.toOther === 'Repair',
'text-weird': movement.action === 'Register'}"
class="flex-shrink-0 text-nowrap" style="max-width:140px; min-width:90px;">
{{ movement.toOther === 'Return' ? 'Return' : (movement.toOther === 'On Delivery' ? 'Receive' :
( movement.lastStation !== null ? 'Change' :
( movement.toOther == 'Faulty' || movement.toOther == 'Calibration' || movement.toOther == 'Repair' ? movement.toOther :
( movement.action == 'Register' ? 'Register' : 'Assign')))) }}
</h3>
<!-- Send Date -->
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:310px; min-width:310px;">
<h4 class="fixed-label m-0 text-nowrap">{{movement.action === 'Assign' ? 'Assign Date' : movement.action === 'Register' ? 'Register Date' : 'Send Date'}}</h4>
<span class="fixed-value text-truncate">{{movement.action !== 'Register' ? formatDate(movement.sendDate) : movement.date }}</span>
</div>
<!-- Receive Date -->
<div v-if="movement.action !== 'Assign' && movement.action !== 'Register'" class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:290px; min-width:290px;">
<h4 class="fixed-label m-0 text-nowrap">Receive Date:</h4>
<span class="fixed-value text-truncate" style="max-width:160px;">{{ formatDate(movement.receiveDate) || 'Not arrive' }}</span>
</div>
<!-- Action -->
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:150px; min-width:150px;">
<h4 class="fixed-labelStatus m-0 text-nowrap">Action:</h4>
<span class="fixed-value text-truncate">{{ movement.action }}</span>
</div>
<!-- Status -->
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:160px; min-width:160px;">
<h4 class="fixed-labelStatus m-0 text-nowrap">Status:</h4>
<span class="fixed-value text-truncate" style="max-width:90px;">{{ movement.latestStatus || movement.toOther }}</span>
</div>
<!-- More Details Button -->
<button class="btn btn-info btn-sm ms-auto" v-on:click="toggleDetails(movement.id)">
More Details
</button>
<!-- Completion Status -->
<h4 :class="movement.action == 'Register' ? 'text-success' : movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'text-success' :
movement.toOther === 'Repair' || movement.toOther === 'Calibration' && movement.latestStatus === 'Ready To Deploy' ? 'text-success' :'text-danger'"
class="text-nowrap ms-3">
{{ movement.action == 'Register' ? 'Complete' :
(movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'Complete' :
( movement.toOther === 'Repair' || movement.toOther === 'Calibration' && movement.latestStatus === 'Ready To Deploy' ? 'Complete' :
(movement.latestStatus === 'Ready To Deploy' ? 'Canceled' : 'Incomplete'))) }}
</h4>
</div>
<!-- 📌 Details Section (Hidden by Default) -->
<div v-show="detailsVisible[movement.id]" class="col-md-12 mt-2">
<div class="row">
<div class="col-md-4 text-center">
<!-- Conditionally render Start Icon -->
<i v-if="movement.lastStation" class="fas fa-map-marker-alt"></i>
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-user fa-2x"></i>
<i v-else class="fas fa-warehouse fa-2x"></i>
<p><strong>Start</strong></p>
<p><strong>User:</strong> {{ movement.lastUserName }}</p>
<p><strong>Station:</strong> {{ movement.lastStationName }}</p>
<p><strong>Store:</strong> {{ movement.lastStoreName }}</p>
</div>
<div class="col-md-4 text-center">
<p></p>
<i class="fas fa-arrow-right fa-2x"></i>
<p>{{ movement.latestStatus || movement.toOther }}</p>
<p>
<button class="btn btn-info btn-sm ms-auto" v-on:click="remark(movement.remark)" v-if="movement.toOther !== 'On Delivery'">
Remark
</button>
</p>
<p>
<button class="btn btn-info btn-sm ms-auto" v-on:click="consignmentNote(movement.consignmentNote)" v-if="movement.toOther !== 'On Delivery'">
Consignment Note
</button>
</p>
</div>
<div class="col-md-4 text-center">
<!-- Conditionally render End Icon -->
<i v-if="movement.toStation" class="fas fa-map-marker-alt"></i>
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-warehouse fa-2x"></i>
<i v-else class="fas fa-user fa-2x"></i>
<p><strong>End</strong></p>
<p v-if="movement.toUser !== null"><strong>User:</strong> {{ movement.toUserName }}</p>
<p v-if="movement.toStation !== null"><strong>Station:</strong> {{ movement.toStationName }}</p>
<p v-if="movement.toStore !== null"><strong>Store:</strong> {{ movement.toStoreName }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!--------------------------------------------STATION CATEGORY---------------------------------------------------------------------->
<div v-if="sortBy === 'station'">
<div v-for="(items, station) in filteredStation " :key="stationName"
:class="{'bg-light-gray': station === 'Unassign Station', 'bg-white': station !== 'Unassign Station'}" class="station-category card mt-3">
<!-- Station Header -->
<div class="card-header d-flex justify-content-between align-items-center">
<h3>{{ station }}</h3>
<button class="btn btn-light" v-on:click="toggleCategory(station)">
<i :class="categoryVisible[station] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i> Show Items
</button>
</div>
<!-- Show Items Under Each Station -->
<div v-show="categoryVisible[station]" class="card-body">
<div v-for="(group, itemId) in paginatedItemsStation(items)" :key="itemId" class="row card">
<!-- Item Header -->
<div class="card-header d-flex justify-content-between align-items-center">
@* <h2>Item : {{ group.uniqueID }}</h2> *@
<h2 v-if="group.uniqueID">Item : {{ group.uniqueID }}</h2>
<h2 v-else>Item : No Item Tracked</h2>
<button class="btn btn-light" v-on:click="toggleCategory(itemId)">
<i :class="categoryVisible[itemId] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i> Show Details
</button>
</div>
<!-- Show Movements for Each Item -->
<div v-show="categoryVisible[itemId]" class="card-body">
<div v-for="(movement, index) in group.movements.sort((a, b) => a.id - b.id).reverse()" :key="movement.id" class="movement-row">
<div v-if="index === 0" class="row">
<strong>Latest Movement</strong>
<div class="col-md-12 d-flex flex-wrap align-items-center gap-3 p-2 border-bottom">
<h3 :class="{'text-primary': movement.toOther === 'On Delivery', 'text-warning': movement.toOther === 'Return',
'text-success': movement.lastStation !== null, 'text-info': movement.action === 'Assign' && movement.lastStation === null,
'text-numb': movement.toOther === 'Faulty' || movement.toOther === 'Calibration' || movement.toOther === 'Repair',
'text-weird': movement.action === 'Register'}"
class="flex-shrink-0 text-nowrap" style="max-width:140px; min-width:90px;">
{{ movement.toOther === 'Return' ? 'Return' : (movement.toOther === 'On Delivery' ? 'Receive' : ( movement.lastStation !== null ? 'Change' : ( movement.toOther == 'Faulty' || movement.toOther == 'Calibration' || movement.toOther == 'Repair' ? movement.toOther : ( movement.action == 'Register' ? 'Register' : 'Assign')))) }}
</h3>
<!-- Send Date -->
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:310px; min-width:310px;">
<h4 class="fixed-label m-0 text-nowrap">{{movement.action === 'Assign' ? 'Assign Date' : movement.action === 'Register' ? 'Register Date' : 'Send Date'}}</h4>
<span class="fixed-value text-truncate">{{movement.action !== 'Register' ? formatDate(movement.sendDate) : movement.date }}</span>
</div>
<!-- Receive Date -->
<div v-if="movement.action !== 'Assign' && movement.action !== 'Register'" class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:290px; min-width:290px;">
<h4 class="fixed-label m-0 text-nowrap">Receive Date:</h4>
<span class="fixed-value text-truncate" style="max-width:160px;">{{ formatDate(movement.receiveDate) || 'Not arrive' }}</span>
</div>
<!-- Action -->
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:150px; min-width:150px;">
<h4 class="fixed-labelStatus m-0 text-nowrap">Action:</h4>
<span class="fixed-value text-truncate">{{ movement.action }}</span>
</div>
<!-- Status -->
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:160px; min-width:160px;">
<h4 class="fixed-labelStatus m-0 text-nowrap">Status:</h4>
<span class="fixed-value text-truncate" style="max-width:90px;">{{ movement.latestStatus || movement.toOther }}</span>
</div>
<!-- More Details Button -->
<button class="btn btn-info btn-sm ms-auto" v-on:click="toggleDetails(movement.id)">
More Details
</button>
<!-- Completion Status -->
<h4 :class="movement.action == 'Register' ? 'text-success' : movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'text-success' : movement.toOther === 'Repair' || movement.toOther === 'Calibration' && movement.latestStatus === 'Ready To Deploy' ? 'text-success' :'text-danger'"
class="text-nowrap ms-3">
{{ movement.action == 'Register' ? 'Complete' : (movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'Complete' : ( movement.toOther === 'Repair' || movement.toOther === 'Calibration' && movement.latestStatus === 'Ready To Deploy' ? 'Complete' : (movement.latestStatus === 'Ready To Deploy' ? 'Canceled' : 'Incomplete'))) }}
</h4>
</div>
<div v-show="detailsVisible[movement.id]" class="col-md-12 mt-2">
<div class="row">
<div class="col-md-4 text-center">
<i v-if="movement.lastStation" class="fas fa-map-marker-alt"></i>
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-user fa-2x"></i>
<i v-else class="fas fa-warehouse fa-2x"></i>
<p><strong>Start</strong></p>
<p v-if="movement.lastUser !== null"><strong>User:</strong> {{ movement.lastUserName }}</p>
<p v-if="movement.lastStation !== null"><strong>Station:</strong> {{ movement.lastStationName }}</p>
<p v-if="movement.lastStore !== null"><strong>Store:</strong> {{ movement.lastStoreName }}</p>
</div>
<div class="col-md-4 text-center">
<i class="fas fa-arrow-right fa-2x"></i>
<p>{{ movement.latestStatus || movement.toOther }}</p>
<p>
<button class="btn btn-info btn-sm ms-auto" v-on:click="remark(movement.remark)">
Remark
</button>
</p>
<p>
<button class="btn btn-info btn-sm ms-auto" v-on:click="consignmentNote(movement.consignmentNote)">
Consignment Note
</button>
</p>
</div>
<div class="col-md-4 text-center">
<i v-if="movement.toStation" class="fas fa-map-marker-alt"></i>
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-warehouse fa-2x"></i>
<i v-else class="fas fa-user fa-2x"></i>
<p><strong>End</strong></p>
<p v-if="movement.toUser !== null"><strong>User:</strong> {{ movement.toUserName }}</p>
<p v-if="movement.toStation !== null"><strong>Station:</strong> {{ movement.toStationName }}</p>
<p v-if="movement.toStore !== null"><strong>Store:</strong> {{ movement.toStoreName }}</p>
</div>
</div>
</div>
</div>
</div>
<!-- Single View History Button -->
<button class="btn btn-light w-100 text-left" v-on:click="toggleHistory(itemId)">
<i :class="historyVisible[itemId] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i> View History
</button>
<div v-show="historyVisible[itemId]" class="history-row">
<div v-for="(movement, i) in group.movements.slice(1)" :key="i" class="row mt-2">
<div class="col-md-12 d-flex flex-wrap align-items-center gap-3 p-2 border-bottom">
<!-- Movement Type -->
<h3 :class="{'text-primary': movement.toOther === 'On Delivery', 'text-warning': movement.toOther === 'Return',
'text-success': movement.lastStation !== null, 'text-info': movement.action === 'Assign' && movement.lastStation === null,
'text-numb': movement.toOther === 'Faulty' || movement.toOther === 'Calibration' || movement.toOther === 'Repair',
'text-weird': movement.action === 'Register'}"
class="flex-shrink-0 text-nowrap" style="max-width:140px; min-width:90px;">
{{ movement.toOther === 'Return' ? 'Return' : (movement.toOther === 'On Delivery' ? 'Receive' : ( movement.lastStation !== null ? 'Change' : ( movement.toOther == 'Faulty' || movement.toOther == 'Calibration' || movement.toOther == 'Repair' ? movement.toOther : ( movement.action == 'Register' ? 'Register' : 'Assign')))) }}
</h3>
<!-- Send Date -->
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:310px; min-width:310px;">
<h4 class="fixed-label m-0 text-nowrap">{{movement.action === 'Assign' ? 'Assign Date' : movement.action === 'Register' ? 'Register Date' : 'Send Date'}}</h4>
<span class="fixed-value text-truncate">{{movement.action !== 'Register' ? formatDate(movement.sendDate) : movement.date }}</span>
</div>
<!-- Receive Date -->
<div v-if="movement.action !== 'Assign' && movement.action !== 'Register'" class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:290px; min-width:290px;">
<h4 class="fixed-label m-0 text-nowrap">Receive Date:</h4>
<span class="fixed-value text-truncate" style="max-width:160px;">{{ formatDate(movement.receiveDate) || 'Not arrive' }}</span>
</div>
<!-- Action -->
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:150px; min-width:150px;">
<h4 class="fixed-labelStatus m-0 text-nowrap">Action:</h4>
<span class="fixed-value text-truncate">{{ movement.action }}</span>
</div>
<!-- Status -->
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:160px; min-width:160px;">
<h4 class="fixed-labelStatus m-0 text-nowrap">Status:</h4>
<span class="fixed-value text-truncate" style="max-width:90px;">{{ movement.latestStatus || movement.toOther }}</span>
</div>
<!-- More Details Button -->
<button class="btn btn-info btn-sm ms-auto" v-on:click="toggleDetails(movement.id)">
More Details
</button>
<!-- Completion Status -->
<h4 :class="movement.action == 'Register' ? 'text-success' : movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'text-success' : movement.toOther === 'Repair' || movement.toOther === 'Calibration' && movement.latestStatus === 'Ready To Deploy' ? 'text-success' :'text-danger'"
class="text-nowrap ms-3">
{{ movement.action == 'Register' ? 'Complete' : (movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'Complete' : ( movement.toOther === 'Repair' || movement.toOther === 'Calibration' && movement.latestStatus === 'Ready To Deploy' ? 'Complete' : (movement.latestStatus === 'Ready To Deploy' ? 'Canceled' : 'Incomplete'))) }}
</h4>
</div>
<!-- Details Section (Hidden by Default) -->
<div v-show="detailsVisible[movement.id]" class="col-md-12 mt-2">
<div class="row">
<div class="col-md-4 text-center">
<!-- Conditionally render Start Icon -->
<i v-if="movement.toOther !== 'On Delivery'" class="fas fa-user fa-2x"></i>
<i v-else class="fas fa-warehouse fa-2x"></i>
<p><strong>Start</strong></p>
<p v-if="movement.lastUser !== null"><strong>User:</strong> {{ movement.lastUserName }}</p>
<p v-if="movement.lastStation !== null"><strong>Station:</strong> {{ movement.lastStationName }}</p>
<p v-if="movement.lastStore !== null"><strong>Store:</strong> {{ movement.lastStoreName }}</p>
</div>
<div class="col-md-4 text-center">
<p></p>
<i class="fas fa-arrow-right fa-2x"></i>
<p>{{ movement.latestStatus || movement.toOther }}</p>
<p>
<button class="btn btn-info btn-sm ms-auto" v-on:click="remark(movement.remark)">
Remark
</button>
</p>
<p>
<button class="btn btn-info btn-sm ms-auto" v-on:click="consignmentNote(movement.consignmentNote)">
Consignment Note
</button>
</p>
</div>
<div class="col-md-4 text-center">
<!-- Conditionally render End Icon -->
<i v-if="movement.toStation" class="fas fa-map-marker-alt"></i>
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-warehouse fa-2x"></i>
<i v-else class="fas fa-user fa-2x"></i>
<p><strong>End</strong></p>
<p v-if="movement.toUser !== null"><strong>User:</strong> {{ movement.toUserName }}</p>
<p v-if="movement.toStation !== null"><strong>Station:</strong> {{ movement.toStationName }}</p>
<p v-if="movement.toStore !== null"><strong>Store:</strong> {{ movement.toStoreName }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="remarkModal" tabindex="-1" aria-labelledby="remarkModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="remarkModalLabel">Remark</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="remarkContent">
<!-- Remark Content Here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="consignmentModal" tabindex="-1" aria-labelledby="consignmentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="consignmentModalLabel">Consignment Note</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img v-if="/\.(jpeg|jpg|png|gif)$/i.test(consignmentNoteUrl)" :src="consignmentNoteUrl" class="img-fluid" alt="Consignment Note Image">
<iframe v-else-if="/\.pdf$/i.test(consignmentNoteUrl)" :src="consignmentNoteUrl" style="width:100%; height: 80vh;"></iframe>
<a v-else class="btn btn-primary">There's no Folder or Picture</a>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-center align-items-center mt-3" v-if="sortBy === 'item' && totalPages > 0">
<button class="btn btn-secondary mx-1" v-on:click="goToPage(currentPage - 1)" :disabled="currentPage === 1">
Previous
</button>
<span class="mx-2">Page {{ currentPage }} of {{ totalPages }}</span>
<button class="btn btn-secondary mx-1" v-on:click="goToPage(currentPage + 1)" :disabled="currentPage === totalPages">
Next
</button>
</div>
<div class="d-flex justify-content-center align-items-center mt-3" v-if="sortBy === 'station' && totalPagesStation > 0">
<button class="btn btn-secondary mx-1" v-on:click="goToPageStation(currentPageStation - 1)" :disabled="currentPageStation === 1">
Previous
</button>
<span class="mx-2">Page {{ currentPageStation }} of {{ itemsPerPageStation }}</span>
<button class="btn btn-secondary mx-1" v-on:click="goToPageStation(currentPageStation + 1)" :disabled="currentPageStation === totalPages">
Next
</button>
</div>
@* QR Model *@
<div class="modal fade" id="zoomQrModal" tabindex="-1" aria-labelledby="zoomQrModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="zoomQrModalLabel">QR Code</h5>
<button type="button" class="btn-close closeModal" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body d-flex justify-content-center">
<div id="zoomedQrContainer"></div>
</div>
<div class="modal-footer justify-content-center">
<h4 id="zoomedQrText" style="margin: 0; font-family: 'OCR A', monospace;"></h4>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
$(function () {
app.mount('#registerItem');
// Attach a click event listener to elements with the class 'btn-success'.
$('#addItemBtn').on('click', function () {
// Show the modal with the ID 'addManufacturerModal'.
$('#itemMovementModal').modal('show');
});
$('.closeModal').on('click', function () {
// Show the modal with the ID 'addManufacturerModal'.
$('.modal').modal('hide');
});
});
const app = Vue.createApp({
data() {
return {
warranty: 0,
EndWDate: null,
invoiceNo: null,
invoiceDate: null,
partNumber: null,
products: [],
depts: [],
stations: [],
stores: [],
users:[],
isModalOpen: false,
selectedProduct: "",
selectedSupplier: "",
selectedCompany: "",
selectedDepartment: "",
selectedTeamType: "",
selectedlastStation: "",
consignmentNoteUrl: "",
assignStationDatatable: null,
showItemModal: false,
loading: false,
items: [],
currentUser: null,
currentUserCompanyDept: null,
sortBy: "all",
searchQuery: "",
searchQueryStation: "",
categoryVisible: {},
historyVisible: {},
detailsVisible: {},
currentRole:"",
stationName: "",
searchStation: "",
currentPage: 1,
itemsPerPage: 10,
currentPageStation : 1,
itemsPerPageStation: 10,
dropdownOpen: false,
selectedLocation: "all",
allItemsListDatatable: null,
}
},
mounted() {
this.fetchItem();
this.fetchStation();
},
computed: {
PicStations() {
if (!this.currentUser) return [];
// Use == instead of === to safely compare string and int
return this.stations.filter(s => s.stationPicID == this.currentUser.id);
},
paginatedItems() {
const start = (this.currentPage - 1) * this.itemsPerPage;
const end = start + this.itemsPerPage;
return Object.fromEntries(
Object.entries(this.filteredItems).slice(start, end)
);
},
totalPages() {
return Math.ceil(Object.keys(this.filteredItems).length / this.itemsPerPage);
},
totalPagesStation() {
return Math.ceil(Object.keys(this.filteredStation).length / this.itemsPerPage);
},
groupedItems() {
let grouped = this.items.reduce((acc, movement) => {
if (!acc[movement.itemId]) {
acc[movement.itemId] = {
uniqueID: movement.uniqueID,
movements: [],
};
}
acc[movement.itemId].movements.push(movement);
return acc;
}, {});
// Sort items from newest to oldest & filter them
for (let itemId in grouped) {
let movements = grouped[itemId].movements
.sort((a, b) => b.id - a.id); // Newest to oldest
// Ensure at least 3 movements before stopping
let stopIndex = movements.slice(3).findIndex(m => m.toOther === 'Return' && m.movementComplete == 1);
if (stopIndex !== -1) {
stopIndex += 3; // Adjust index since we sliced after the first 3
movements = movements.slice(0, stopIndex + 1); // Keep at least 3 + the "Return" row
}
grouped[itemId].movements = movements;
}
return grouped;
},
groupedByStation() {
let groupedByItem = this.items.reduce((acc, movement) => {
if (!acc[movement.uniqueID]) {
acc[movement.uniqueID] = {
uniqueID: movement.uniqueID,
movements: [],
};
}
acc[movement.uniqueID].movements.push(movement);
return acc;
}, {});
let groupedByStation = {};
Object.keys(groupedByItem).forEach(itemId => {
let movements = groupedByItem[itemId].movements
.sort((a, b) => b.id - a.id); // Newest → Oldest
// Ensure at least 3 movements before stopping
let stopIndex = movements.slice(3).findIndex(m => m.toOther === 'Return' && m.movementComplete == 1);
// Remove older movements
if (stopIndex !== -1) {
stopIndex += 3; // Adjust index since we sliced after the first 3
movements = movements.slice(0, stopIndex);
}
if (movements.length > 0) {
let latestMovement = movements[0];
let station = latestMovement.toStationName || latestMovement.lastStationName || "Not Assigned";
if (!groupedByStation[station]) { groupedByStation[station] = {}; }
groupedByStation[station][itemId] = { uniqueID: itemId, movements };
}
});
// 4⃣ **Sort stations & move 'Unassign Station' to last**
let sortedKeys = Object.keys(groupedByStation).sort((a, b) => {
if (a === "Unassign Station") return 1;
if (b === "Unassign Station") return -1;
return a.localeCompare(b);
});
let sortedGrouped = {};
sortedKeys.forEach(key => {
sortedGrouped[key] = groupedByStation[key];
});
return sortedGrouped;
},
filteredItems() {
if (!this.searchQuery.trim()) {
return this.groupedItems;
}
const searchLower = this.searchQuery.toLowerCase();
return Object.fromEntries(
Object.entries(this.groupedItems).filter(([_, group]) =>
group.uniqueID.toLowerCase().includes(searchLower)
)
);
},
filteredStation() {
if (!this.searchQueryStation) {
return this.groupedByStation;
}
let searchQueryStation = String(this.searchQueryStation).toLowerCase();
let grouped = this.groupedByStation;
let filtered = {};
Object.keys(grouped).forEach(station => {
if (station.toLowerCase().includes(searchQueryStation)) {
filtered[station] = grouped[station];
}
});
return filtered;
},
},
methods: {
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
paginatedItemsStation(item) {
const start = (this.currentPageStation - 1) * this.itemsPerPageStation;
const end = start + this.itemsPerPageStation;
return Object.fromEntries(
Object.entries(item).slice(start, end)
);
},
selectStation(item) {
this.searchQueryStation = item.stationName;
this.dropdownOpen = false;
},
closeDropdown() {
this.dropdownOpen = false; // Tutup dropdown
},
goToPage(page) {
if (page >= 1 && page <= this.totalPages) {
this.currentPage = page;
}
},
goToPageStation(page) {
if (page >= 1 && page <= this.itemsPerPageStation) {
this.currentPageStation = page;
}
},
remark(remark) {
document.getElementById("remarkContent").innerText = remark || "No remark message provide.";
let modal = new bootstrap.Modal(document.getElementById("remarkModal"));
modal.show();
},
updateAllItemsTable() {
this.initAllTables();
},
consignmentNote(consignmentNote) {
if (!consignmentNote) {
this.consignmentNoteUrl = "No consignment note available.";
new bootstrap.Modal(document.getElementById('consignmentModal')).show();
return;
}
// Pastikan URL betul
this.consignmentNoteUrl = consignmentNote;
// Tunggu Vue update sebelum buka modal
this.$nextTick(() => {
new bootstrap.Modal(document.getElementById('consignmentModal')).show();
});
},
initiateTable() {
this.loading = true;
self = this;
this.itemDatatable = $('#itemDatatable').DataTable({
"data": this.items,
"columns": [
{ title: "Unique Id", data: "id" },
{ title: "Product Code", data: "uniqueID" },
{ title: "From User", data: "lastUserName" },
{ title: "Last User", data: "toUserName" },
{ title: "Completion", data: "movementComplete", render: function (data) {return data ? "Completed" : "Pending";}},
{ title: "From Station", data: "lastStationName" },
{ title: "Last Station", data: "toStationName" },
{ title: "From Store", data: "lastStoreName" },
{ title: "Last Store", data: "toStoreName" },
{ title: "Action", data: "action" },
{ title: "Start Status", data: "toOther" },
{ title: "Latest Status", data: "latestStatus" },
{ title: "Product Category", data: "productCategory" },
{ title: "Qty", data: "quantity" },
{ title: "Send Date", data: "sendDate", render: this.formatDate.bind(this) },
{ title: "Receive Date", data: "receiveDate", render: this.formatDate.bind(this) },
{ title: "Note",
data: "consignmentNote",
render: function (data, type, full, meta) {
if (!data) {
return "No Document";
}
// Check if the document is an image based on file extension
var isImage = /\.(jpeg|jpg|png|gif)$/i.test(data);
var isPdf = /\.pdf$/i.test(data);
if (isImage) {
return `<a href="${data}" target="_blank" data-lightbox="image-1">
<img src="${data}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
</a>`;
}
else if (isPdf) {
return `<a href="${data}" target="_blank">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/87/PDF_file_icon.svg"
alt="PDF Document" class="img-thumbnail"
style="width: 50px; height: 50px;" />
<br>View PDF
</a>`;
} else {
return `<a href="${data}" target="_blank">Download File</a>`;
}
},
},
{ title: "Remark", data: "remark" },
],
order: [[0, "desc"]], // Sorting by "Product Code" (uniqueID) in descending order
responsive: true,
drawCallback: function (settings) {
// Generate QR codes after rows are rendered
// const api = this.api();
// api.rows().every(function () {
// const data = this.data();
// const containerId = `qr${data.id}`; containerid is by increments from API
// const container = $(`#${containerId}`);
// container.empty();
// container.append(`${data.item.itemId}`);
// if (container.length) {
// new QRCode(container[0], {
// text: data.qrString,
// width: 100,
// height: 100,
// colorDark: "#000000",
// colorLight: "#ffffff",
// correctLevel: QRCode.CorrectLevel.M,
// });
// }
// container.on('click', function() {
// window.open(data.qrString, '_blank');
// });
// });
},
})
this.loading = false;
},
async fetchItem() {
this.loading = true;
await this.fetchUser();
try {
const response = await fetch('/InvMainAPI/ItemMovementList', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) throw new Error('Failed to fetch item');
const data = await response.json();
if(this.currentRole == "Super Admin"){
this.items = data;
} else {
const myStationIds = this.stations
.filter(s => s.stationPicID == this.currentUser.id)
.map(s => s.stationId);
this.items = data.filter(item =>
item.lastUser === this.currentUser.id ||
item.toUser === this.currentUser.id ||
(item.lastStation && myStationIds.includes(item.lastStation)) ||
(item.toStation && myStationIds.includes(item.toStation)) ||
item.lastStore === this.currentUser.store ||
item.toStore === this.currentUser.store
);
}
this.initAllTables();
if (this.itemDatatable) {
this.itemDatatable.clear().destroy();
}
this.initiateTable();
}
catch (error) {
console.error('Error fetching item:', error);
}
this.loading = false;
},
async fetchUser() {
try {
const response = await fetch(`/IdentityAPI/GetUserInformation/`, {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
this.currentUser = data?.userInfo || null;
const companyDeptData = await this.currentUser.department;
this.currentUserCompanyDept = companyDeptData;
this.selectedCompany = companyDeptData?.companyId || "";
this.selectedDepartment = companyDeptData?.departmentId || "";
const roles = this.currentUser.role;
if (roles.includes("SuperAdmin")) {
this.currentRole = "Super Admin";
// this.sortBy = 'logs';
} else if (roles.includes("Inventory Master")) {
this.currentRole = "Inventory Master";
// this.sortBy = "item";
}
}
else {
console.error(`Failed to fetch user: ${response.statusText}`);
}
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
},
async fetchStation() {
try {
const response = await fetch('/InvMainAPI/StationList', {
method: 'POST', // Specify the HTTP method
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch Station');
}
const data = await response.json();
if (this.currentRole !== "Super Admin" && this.currentUser) {
this.stations = data.filter(s => s.stationPicID === this.currentUser.id);
} else {
this.stations = data;
}
} catch (error) {
console.error('Error fetching Station:', error);
}
},
handleSorting() {
this.renderTables();
},
renderTables() {
this.initAllTables();
this.initiateTable();
},
initAllTables() {
if (this.itemMovementNotCompleteDatatable) {
this.itemMovementNotCompleteDatatable.destroy();
}
if (this.itemMovementCompleteDatatable) {
this.itemMovementCompleteDatatable.destroy();
}
if (this.assignStationDatatable) {
this.assignStationDatatable.destroy();
}
this.itemMovementNotCompleteDatatable = $("#itemMovementNotCompleteDatatable").DataTable({
data: this.items.filter((m) => m.movementComplete == 0),
columns: [
{ title: "Unique Id", data: "id" },
{ title: "Product Name", data: "productName", render: (data, type, full) => { return `${data} <br> ${renderFile(full.productImage)}`; } },
{ title: "Product Code", data: "uniqueID" },
{ title: "Action", data: "action" },
{ title: "Send Date", data: "sendDate" , render: this.formatDate.bind(this)},
{ title: "From User", data: "lastUserName" },
{ title: "From Station", data: "lastStationName" },
{ title: "From Store", data: "lastStoreName" },
{ title: "Last User", data: "toUserName" },
{ title: "Last Station", data: "toStationName" },
{ title: "Last Store", data: "toStoreName" },
{ title: "Start Status", data: "toOther" },
{ title: "Product Category", data: "productCategory" },
{ title: "Qty", data: "quantity" },
{ title: "Note", data: "consignmentNote", render: renderFile },
{ title: "Remark", data: "remark" },
],
order: [[0, "desc"]],
responsive: true,
});
this.itemMovementCompleteDatatable = $("#itemMovementCompleteDatatable").DataTable({
data: this.items.filter((m) => m.movementComplete == 1 && m.action !== "Assign"),
columns: [
{ title: "Unique Id", data: "id" },
{ title: "Product Name", data: "productName", render: (data, type, full) => { return `${data} <br> ${renderFile(full.productImage)}`; } },
{ title: "Product Code", data: "uniqueID" },
{ title: "Send Date", data: "sendDate", render: this.formatDate.bind(this) },
{ title: "Receive Date", data: "receiveDate", render: this.formatDate.bind(this) },
{ title: "Action", data: "action" },
{ title: "From User", data: "lastUserName" },
{ title: "From Station", data: "lastStationName" },
{ title: "From Store", data: "lastStoreName" },
{ title: "Last User", data: "toUserName" },
{ title: "Last Station", data: "toStationName" },
{ title: "Last Store", data: "toStoreName" },
{ title: "Start Status", data: "toOther" },
{ title: "Latest Status", data: "latestStatus" },
{ title: "Product Category", data: "productCategory" },
{ title: "Note", data: "consignmentNote", render: renderFile },
{ title: "Remark", data: "remark" },
],
order: [[0, "desc"]],
responsive: true,
});
this.assignStationDatatable = $("#assignStationDatatable").DataTable({
data: this.items.filter((m) =>
m.action === "Assign" ||
m.action === "Change" ||
m.toStation !== null
),
columns: [
{ title: "Unique Id", data: "id" },
{ title: "Product Name", data: "productName", render: (data, type, full) => { return `${data} <br> ${renderFile(full.productImage)}`; } },
{ title: "Product Code", data: "uniqueID" },
{ title: "Assign Date", data: "sendDate", render: this.formatDate.bind(this) },
{ title: "Action", data: "action" },
{ title: "Station User PIC", data: "lastUserName" },
{ title: "From Station", data: "lastStationName" },
{ title: "Last Station", data: "toStationName" },
{ title: "Qty", data: "quantity" },
{ title: "Note", data: "consignmentNote", render: renderFile },
{ title: "Remark", data: "remark" },
],
responsive: true,
});
if (this.allItemsListDatatable) {
this.allItemsListDatatable.destroy();
}
let myStationIds = this.PicStations ? this.PicStations.map(s => s.stationId) : [];
let currentUserId = this.currentUser ? this.currentUser.id : null;
let currentUserStoreId = this.currentUser ? this.currentUser.store : null;
let latestMovementsMap = {};
this.items.forEach(movement => {
let id = movement.itemId;
if (!latestMovementsMap[id] || latestMovementsMap[id].id < movement.id) {
latestMovementsMap[id] = movement;
}
});
let latestMovements = Object.values(latestMovementsMap);
let allItemsData = [];
latestMovements.forEach(movement => {
let isDelivered = movement.latestStatus === 'Delivered';
let isReturned = movement.toOther === 'Return' &&
(movement.latestStatus === null || movement.latestStatus === 'Ready To Deploy');
if (isDelivered && !isReturned && movement.movementComplete == 1) {
// Check ownership (Is it with the user or their station or store)
let isWithUser = movement.toUser === currentUserId;
let isDeployedToUserStation = myStationIds.includes(movement.toStation);
let isAtUserStore = (currentUserStoreId !== null && movement.toStore === currentUserStoreId);
if (isWithUser || isDeployedToUserStation || isAtUserStore) {
if (this.selectedLocation === "all" || this.selectedLocation === "" || !this.selectedLocation) {
allItemsData.push(movement);
}
else if (this.selectedLocation === "user") {
// Under "My Items" - show items assigned to the user OR their store
if (isWithUser || isAtUserStore) {
allItemsData.push(movement);
}
}
else if (this.selectedLocation.startsWith("station_")) {
let stationId = parseInt(this.selectedLocation.split("_")[1]);
if (movement.toStation === stationId) {
allItemsData.push(movement);
}
}
}
}
});
this.allItemsListDatatable = $("#allItemsListDatatable").DataTable({
data: allItemsData,
columns: [
{
title: "Product Code",
data: "uniqueID",
render: function (data, type, row) {
const displayData = data ? data : "N/A";
return `<strong>${displayData}</strong><div id="qr_movement_${row.id}" class="mt-2"></div>`;
}
},
{
title: "Product Name",
data: "productName",
render: (data, type, full) => {
return `${data || 'Unknown'} <br> ${renderFile(full.productImage)}`;
}
},
{ title: "Current Location", data: "toStationName" },
{ title: "Receive Date", data: "receiveDate", render: this.formatDate.bind(this) },
{ title: "Send Date", data: "sendDate", render: this.formatDate.bind(this) },
{ title: "Action", data: "action" },
{ title: "From User", data: "lastUserName" },
{ title: "From Station", data: "lastStationName" },
{ title: "From Store", data: "lastStoreName" },
{ title: "Last User", data: "toUserName" },
{ title: "Last Station", data: "toStationName" },
{ title: "Last Store", data: "toStoreName" },
{ title: "Qty", data: "quantity" },
],
order: [[0, "desc"]],
responsive: true,
drawCallback: function (settings) {
setTimeout(() => {
const api = this.api();
api.rows().every(function () {
const data = this.data();
// Use data.id to ensure the element ID is unique per row
const containerId = `qr_movement_${data.id}`;
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = "";
// Construct the full URL for the QR code dynamically
if (!data.uniqueID) return;
const qrText = `${window.location.origin}/I/${data.uniqueID}`;
// Generate Small QR
new QRCode(container, {
text: qrText,
width: 100,
height: 100,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.M
});
container.style.cursor = "pointer";
container.title = "Click to enlarge";
// Open Modal with Large QR
container.onclick = function () {
const zoomContainer = document.getElementById("zoomedQrContainer");
zoomContainer.innerHTML = "";
new QRCode(zoomContainer, {
text: qrText,
width: 250,
height: 250,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.M
});
document.getElementById("zoomedQrText").innerText = data.uniqueID;
$('#zoomQrModal').modal('show');
};
});
}, 100);
}
});
function renderFile(data, type, full, meta) {
if (!data) {
return "No Document";
}
// Check if the document is an image based on file extension
var isImage = /\.(jpeg|jpg|png|gif)$/i.test(data);
var isPdf = /\.pdf$/i.test(data);
if (isImage) {
return `<a href="${data}" target="_blank" data-lightbox="image-1">
<img src="${data}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" /></a>`;
}
else if (isPdf) {
return `<a href="${data}" target="_blank">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/87/PDF_file_icon.svg"
alt="PDF Document" class="img-thumbnail"
style="width: 50px; height: 50px;" />
<br>View PDF</a>`;
} else {
return `<a href="${data}" target="_blank">Download File</a>`;
}
}
},
toggleCategory(itemId) {
this.categoryVisible[itemId] = !this.categoryVisible[itemId];
this.detailsVisible = {};
this.historyVisible = {};
},
toggleHistory(itemId) {
// Jika item yang ditekan sudah terbuka, tutup
if (this.historyVisible[itemId]) {
this.historyVisible[itemId] = false;
} else {
// Tutup semua history lain dahulu
this.historyVisible = {};
// Buka hanya item yang ditekan
this.historyVisible[itemId] = true;
}
},
toggleDetails(movementId) {
this.detailsVisible[movementId] = !this.detailsVisible[movementId];
},
},
directives: {
clickOutside: {
beforeMount(el, binding) {
el.clickOutsideEvent = (event) => {
if (!(el.contains(event.target))) {
binding.value?.(); // Guna optional chaining untuk elak error
}
};
document.body.addEventListener("click", el.clickOutsideEvent);
},
unmounted(el) {
document.body.removeEventListener("click", el.clickOutsideEvent);
}
}
}
});
</script>
}