Add new stock management modal and edit itemm card stock fields [SCI-9415] (#6474)

* Add edit fetaure to item card stock field [SCI-9415]

* Replace manage stock modal [SCI-9415]

* Fix issue displaying item card [SCI-9415]

* Minor improvements [SCI-9415]

* Enable stock modal in assigned inventories [SCI-9415]

* Use toggleable reminder value [SCI-9415]
This commit is contained in:
wandji 2023-10-27 09:31:38 +01:00 committed by GitHub
parent 7c54509c58
commit c22d1e226b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 570 additions and 482 deletions

View file

@ -210,17 +210,18 @@ $.fn.dataTable.render.RepositoryStockValue = function(data) {
if (data) {
if (data.value) {
if (data.stock_managable) {
return `<a class="manage-repository-stock-value-link stock-value-view-render stock-${data.stock_status}">
return `<a class="manage-repository-stock-value-link stock-value-view-render stock-${data.stock_status}"
data-manage-stock-url=${data.value.stock_url}>
${data.value.stock_formatted}
</a>`;
}
return `<span class="stock-value-view-render
return `<span class="stock-value-view-render data-manage-stock-url=${data.value.stock_url}
${data.displayWarnings ? `stock-${data.stock_status}` : ''}">
${data.value.stock_formatted}
</span>`;
}
if (data.stock_managable) {
return `<a class="manage-repository-stock-value-link not-assigned-stock">
return `<a class="manage-repository-stock-value-link not-assigned-stock" data-manage-stock-url=${data.stock_url}>
<i class="fas fa-box-open"></i>
${I18n.t('libraries.manange_modal_column.stock_type.add_stock')}
</a>`;

View file

@ -1,231 +0,0 @@
/* global dropdownSelector GLOBAL_CONSTANTS I18n SmartAnnotation formatDecimalValue Decimal */
var RepositoryStockValues = (function() {
const UNIT_SELECTOR = '#repository-stock-value-units';
function updateChangeAmount($element) {
if (!$element.val()) {
$('.stock-final-container .value').text('-');
return;
}
if (!($element.val() >= 0)) return;
let currentAmount = new Decimal($element.data('currentAmount') || 0);
let inputAmount = new Decimal($element.val());
let newAmount;
switch ($element.data('operator')) {
case 'set':
newAmount = inputAmount;
break;
case 'add':
newAmount = currentAmount.plus(inputAmount);
break;
case 'remove':
newAmount = currentAmount.minus(inputAmount);
break;
default:
newAmount = currentAmount;
break;
}
$('#change_amount').val(inputAmount);
$('#repository_stock_value_amount').val(newAmount);
$('.stock-final-container').toggleClass('negative', newAmount < 0);
$('.stock-final-container .value').text(
formatDecimalValue(String(newAmount), $('#stock-input-amount').data('decimals'))
);
}
function initManageAction() {
let amountChanged = false;
$('.repository-show').on('click', '.manage-repository-stock-value-link', function() {
let colIndex = this.parentNode.cellIndex;
let rowIndex = this.parentNode.parentNode.rowIndex;
$.ajax({
url: $(this).closest('tr').data('manage-stock-url'),
type: 'GET',
dataType: 'json',
success: (result) => {
var $manageModal = $('#manage-repository-stock-value-modal');
$manageModal.find('.modal-content').html(result.html);
dropdownSelector.init(UNIT_SELECTOR, {
singleSelect: true,
closeOnSelect: true,
noEmptyOption: true,
selectAppearance: 'simple',
onChange: function() {
let unit = '';
if (dropdownSelector.getValues(UNIT_SELECTOR) > 0) {
unit = dropdownSelector.getLabels(UNIT_SELECTOR);
}
$('.stock-final-container .units').text(unit);
$('.repository-stock-reminder-value .units').text(
I18n.t('repository_stock_values.manage_modal.units_remaining', {
unit: unit
})
);
}
});
$manageModal.find(`
.dropdown-selector-container .input-field,
.dropdown-selector-container .search-field
`).attr('tabindex', 2);
$manageModal.find('form').on('ajax:success', function(_, data) {
$manageModal.modal('hide');
let $cell = $('.dataTable').find(
`tr:nth-child(${rowIndex}) td:nth-child(${colIndex + 1})`
);
$cell.parent().data('manage-stock-url', data.manageStockUrl);
$cell.html(
$.fn.dataTable.render.RepositoryStockValue(data)
);
});
$('.stock-operator-option').click(function() {
var $stockInput = $('#stock-input-amount');
$('.stock-operator-option').removeClass('btn-primary').addClass('btn-secondary');
$(this).removeClass('btn-secondary').addClass('btn-primary');
$stockInput.data('operator', $(this).data('operator'));
dropdownSelector.selectValues(UNIT_SELECTOR, $('#initial_units').val());
$('#operator').val($(this).data('operator'));
switch ($(this).data('operator')) {
case 'set':
dropdownSelector.enableSelector(UNIT_SELECTOR);
if (!amountChanged) { $stockInput.val($stockInput.data('currentAmount')); }
break;
case 'add':
if (!amountChanged) { $stockInput.val(''); }
dropdownSelector.disableSelector(UNIT_SELECTOR);
break;
case 'remove':
if (!amountChanged) { $stockInput.val(''); }
dropdownSelector.disableSelector(UNIT_SELECTOR);
break;
default:
break;
}
updateChangeAmount($('#stock-input-amount'));
});
$('#stock-input-amount, #low_stock_threshold').on('input focus', function() {
let decimals = $(this).data('decimals');
this.value = formatDecimalValue(this.value, decimals);
});
SmartAnnotation.init($('#repository-stock-value-comment')[0], false);
$('#repository-stock-value-comment').on('input', function() {
$(this).closest('.sci-input-container').toggleClass(
'error',
this.value.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH
);
$('.update-repository-stock').toggleClass(
'disabled',
this.value.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH
);
});
$('#reminder-selector-checkbox').on('change', function() {
let valueContainer = $('.repository-stock-reminder-value');
valueContainer.toggleClass('hidden', !this.checked);
if (!this.checked) {
$(this).data('reminder-value', valueContainer.find('input').val());
valueContainer.find('input').val(null);
} else {
valueContainer.find('input').val($(this).data('reminder-value'));
valueContainer.find('input').focus();
}
});
$('.update-repository-stock').on('click', function() {
let reminderError = $('#reminder-selector-checkbox')[0].checked
&& $('.repository-stock-reminder-value').find('input').val() === '';
$('.repository-stock-reminder-value').find('.sci-input-container').toggleClass('error', reminderError);
});
$('#stock-input-amount').on('input', function() {
amountChanged = true;
updateChangeAmount($(this));
});
$manageModal.on('ajax:beforeSend', 'form', function() {
let status = true;
if (!(dropdownSelector.getValues(UNIT_SELECTOR) > 0)) {
dropdownSelector.showError(UNIT_SELECTOR, I18n.t('repository_stock_values.manage_modal.unit_error'));
status = false;
} else {
dropdownSelector.hideError(UNIT_SELECTOR);
}
let stockInput = $('#stock-input-amount');
if (stockInput.val().length && stockInput.val() >= 0) {
stockInput.parent().removeClass('error');
} else {
stockInput.parent().addClass('error');
if (stockInput.val().length === 0) {
stockInput.parent()
.attr(
'data-error-text',
I18n.t('repository_stock_values.manage_modal.amount_error')
);
} else {
stockInput.parent()
.attr(
'data-error-text',
I18n.t('repository_stock_values.manage_modal.negative_error')
);
}
status = false;
}
let reminderInput = $('.repository-stock-reminder-value input');
if ($('#reminder-selector-checkbox')[0].checked) {
if (reminderInput.val().length && reminderInput.val() >= 0) {
reminderInput.parent().removeClass('error');
} else {
reminderInput.parent().addClass('error');
if (reminderInput.val().length === 0) {
reminderInput.parent()
.attr(
'data-error-text',
I18n.t('repository_stock_values.manage_modal.amount_error')
);
} else {
reminderInput.parent()
.attr(
'data-error-text',
I18n.t('repository_stock_values.manage_modal.negative_error')
);
}
status = false;
}
}
return status;
});
$manageModal.modal('show');
amountChanged = false;
$('#stock-input-amount').focus();
$('#stock-input-amount')[0].selectionStart = $('#stock-input-amount')[0].value.length;
$('#stock-input-amount')[0].selectionEnd = $('#stock-input-amount')[0].value.length;
}
});
});
}
return {
init: () => {
initManageAction();
}
};
}());
RepositoryStockValues.init();

View file

@ -69,4 +69,24 @@
$('#modal-info-repository-row').modal('hide');
}
});
$(document).on('click', '.manage-repository-stock-value-link', (e) => {
e.preventDefault();
window.initManageStockValueModalComponent();
if (window.manageStockModalComponent) {
const $link = $(e.target).parents('a')[0] ? $(e.target).parents('a') : $(e.target);
const stockValueUrl = $link.data('manage-stock-url');
let updateCallback;
if (stockValueUrl) {
updateCallback = (data) => {
if (!data?.value) return;
// reload dataTable
if ($('.dataTable')[0]) $('.dataTable').DataTable().ajax.reload();
// update item card stock column
window.manageStockCallback && window.manageStockCallback(data.value)
};
window.manageStockModalComponent.showModal(stockValueUrl, updateCallback);
}
}
});
}());

View file

@ -43,6 +43,10 @@
}
}
&.error {
border-color: var(--sn-delete-red);
}
.sn-select__options {
display: none;
}

View file

@ -1,3 +1,16 @@
/* Hide arrows on number type input field */
@layer utilities {
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
}
@layer components {
.sci-btn-group {
@apply inline-flex items-center gap-2 relative;

View file

@ -6,33 +6,9 @@ class RepositoryStockValuesController < ApplicationController
before_action :load_vars
before_action :check_manage_permissions
def new
render json: {
html: render_to_string(
partial: 'repository_stock_values/manage_modal_content',
locals: {
repository_row: @repository_row,
repository_stock_column: @repository_column,
unit_items: @repository_column.repository_stock_unit_items,
repository_stock_value: RepositoryStockValue.new
}
)
}
end
def new; end
def edit
render json: {
html: render_to_string(
partial: 'repository_stock_values/manage_modal_content',
locals: {
repository_row: @repository_row,
repository_stock_column: @repository_column,
unit_items: @repository_column.repository_stock_unit_items,
repository_stock_value: @repository_stock_value
}
)
}
end
def edit; end
def create_or_update
ActiveRecord::Base.transaction do
@ -50,8 +26,12 @@ class RepositoryStockValuesController < ApplicationController
render json: {
stock_managable: true,
stock_status: @repository_stock_value.status,
manageStockUrl: edit_repository_stock_repository_repository_row_url(@repository, @repository_row)
}.merge(serialize_repository_cell_value(@repository_stock_value.repository_cell, current_team, @repository))
}.merge(
serialize_repository_cell_value(
@repository_stock_value.repository_cell, current_team, @repository,
reminders_enabled: Repository.reminders_enabled?
)
)
end
private

View file

@ -2,6 +2,7 @@
module RepositoryDatatableHelper
include InputSanitizeHelper
include Rails.application.routes.url_helpers
def prepare_row_columns(repository_rows, repository, columns_mappings, team, options = {})
has_stock_management = repository.has_stock_management?
@ -47,20 +48,6 @@ module RepositoryDatatableHelper
end
if has_stock_management
row['manageStockUrl'] = if record.has_stock?
Rails.application.routes.url_helpers
.edit_repository_stock_repository_repository_row_url(
repository,
record
)
else
Rails.application.routes.url_helpers
.new_repository_stock_repository_repository_row_url(
repository,
record
)
end
stock_cell = record.repository_cells.find { |cell| cell.value_type == 'RepositoryStockValue' }
# always add stock cell, even if empty
@ -68,7 +55,7 @@ module RepositoryDatatableHelper
if stock_cell.present?
serialize_repository_cell_value(record.repository_stock_cell, team, repository)
else
{}
{ stock_url: new_repository_stock_repository_repository_row_url(repository, record) }
end
row['stock'][:stock_managable] = stock_managable
row['stock']['displayWarnings'] = display_stock_warnings?(repository)

View file

@ -0,0 +1,24 @@
import TurbolinksAdapter from 'vue-turbolinks';
import Vue from 'vue/dist/vue.esm';
import PerfectScrollbar from 'vue2-perfect-scrollbar';
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css';
import ManageStockValueModal from '../../vue/repository_row/manage_stock_value_modal.vue';
Vue.use(PerfectScrollbar);
Vue.use(TurbolinksAdapter);
Vue.prototype.i18n = window.I18n;
window.initManageStockValueModalComponent = () => {
if (window.manageStockModalComponent) return;
if (notTurbolinksPreview()) {
new Vue({
el: '#manageStockValueModal',
components: {
'manage-stock-value-modal': ManageStockValueModal,
},
});
}
};
initManageStockValueModalComponent();

View file

@ -1,12 +1,10 @@
<template>
<div v-if="customColumns?.length > 0" class="flex flex-col gap-4 w-[350px] h-auto">
<div v-for="(column, index) in customColumns" :key="column.id" class="flex flex-col gap-4 w-[350px] h-auto relative">
<span class="absolute right-2 top-6" v-if="column?.value?.reminder === true">
<Reminder :value="column?.value" :valueType="column?.value_type" />
</span>
<component
:is="column.data_type"
:key="index"
:actions="actions"
:data_type="column.data_type"
:colId="column.id"
:colName="column.name"
@ -18,7 +16,6 @@
:optionsPath="column.options_path"
:inArchivedRepositoryRow="inArchivedRepositoryRow"
:editingField="editingField"
:actions="actions"
@setEditingField="editingField = $event"
@update="update"
/>
@ -31,7 +28,6 @@
</template>
<script>
import Reminder from './reminder.vue'
import RepositoryStockValue from './repository_values/RepositoryStockValue.vue';
import RepositoryTextValue from './repository_values/RepositoryTextValue.vue';
import RepositoryNumberValue from './repository_values/RepositoryNumberValue.vue';
@ -49,7 +45,6 @@
export default {
name: 'CustomColumns',
components: {
Reminder,
RepositoryStockValue,
RepositoryTextValue,
RepositoryNumberValue,

View file

@ -1,4 +1,4 @@
<template v-if="value.reminder === true">
<template v-if="value?.reminder === true">
<div class="inline-block float-right cursor-pointer relative" :title="reminderTitle"
tabindex='-1'>
<i class="sn-icon sn-icon-notifications row-reminders-icon"></i>
@ -14,14 +14,14 @@
},
computed: {
reminderColor() {
if (this.value.reminder && (this.value.stock_amount > 0 || this.value.days_left > 0)) {
if (this.value?.reminder && (this.value?.stock_amount > 0 || this.value?.days_left > 0)) {
return 'bg-sn-alert-brittlebush';
}
return 'bg-sn-alert-passion';
},
reminderTitle() {
let title = this.value.reminder_text
if (this.value.reminder_message) title = `${title}\n${this.value.reminder_message}`;
let title = this.value?.reminder_text
if (this.value?.reminder_message) title = `${title}\n${this.value?.reminder_message}`;
return title;
}

View file

@ -8,23 +8,49 @@
{{ i18n.t('repositories.item_card.stock_export') }}
</a>
</div>
<div v-if="colVal?.stock_formatted" class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ colVal?.stock_formatted }}
<a style="text-decoration: none;"
class="text-sn-dark-grey font-inter text-sm font-normal leading-5 w-full rounded relative block"
:class="editableClassName"
@click="enableEditing"
:data-manage-stock-url="stockValueUrl"
:data-repository-row-id="repositoryId"
>
<div v-if="values?.stock_formatted" class="text-sn-dark-grey font-inter text-sm font-normal leading-5 stock-value">
{{ values.stock_formatted }}
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5 stock-value">
{{ i18n.t('repositories.item_card.repository_stock_value.no_stock') }}
</div>
<span class="absolute right-2 reminder" :class="{ 'top-1.5': permissions?.can_manage, 'top-0': !permissions?.can_manage, hidden: !values?.reminder }">
<Reminder :value="values" />
</span>
</a>
</div>
</template>
<script>
import Reminder from '../reminder.vue';
export default {
name: 'RepositoryStockValue',
components: {
Reminder
},
computed: {
editableClassName() {
const className = 'border-solid border-[1px] p-2 manage-repository-stock-value-link sci-cursor-edit'
if (this.permissions.can_manage && this.isEditing) return `${className} border-sn-science-blue`;
if (this.permissions.can_manage) return `${className} border-sn-light-grey hover:border-sn-sleepy-grey`;
return ''
}
},
data() {
return {
stock_formatted: null,
stock_amount: null,
low_stock_threshold: null
low_stock_threshold: null,
isEditing: null,
values: null,
stockValueUrl: null
}
},
props: {
@ -34,14 +60,29 @@ export default {
colVal: Object,
repositoryId: Number,
repositoryRowId: null,
permissions: null
permissions: null,
actions: null,
},
created() {
if (!this.colVal) return
this.stock_formatted = this.colVal.stock_formatted
this.stock_amount = this.colVal.stock_amount
this.low_stock_threshold = this.colVal.low_stock_threshold
mounted() {
this.values = this.colVal;
this.stockValueUrl = this.actions.stock.stock_value_url
window.manageStockCallback = this.submitCallback;
},
unmounted(){
delete window.manageStockCallback
},
methods: {
enableEditing(){
this.isEditing = true
const $this = this;
// disable edit
$('#manageStockValueModal').on('hide.bs.modal', function() {
$this.isEditing = false;
})
},
submitCallback(values) {
if (values) this.values = values;
}
}
}
</script>

View file

@ -0,0 +1,278 @@
<template>
<div
ref="modal"
class="modal fade"
tabindex="-1"
role="dialog"
aria-labelledby="manage-stock-value"
>
<div class="modal-dialog" role="document" v-if="stockValue">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" :aria-label="i18n.t('general.close')">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title">
<template v-if="!!stockValue?.id">
{{ i18n.t('repository_stock_values.manage_modal.title', { item: repositoryRowName }) }}
</template>
<template v-else>
{{ i18n.t('repository_stock_values.manage_modal.edit_title', { item: repositoryRowName }) }}
</template>
</h4>
</div>
<div class="modal-body">
<p class="text-sm pb-6"> {{ i18n.t('repository_stock_values.manage_modal.enter_amount') }}</p>
<form class="flex flex-col gap-6" @submit.prevent novalidate>
<fieldset class="w-full flex justify-between">
<div class="flex flex-col w-40">
<label class="text-sn-grey text-sm font-normal" for="operations">{{ i18n.t('repository_stock_values.manage_modal.operation') }}</label>
<Select
:disabled="!stockValue?.id"
:value="operation"
:options="operations"
@change="setOperation"
></Select>
</div>
<div class="flex flex-col w-40">
<Input
@input="amount = $event"
name="stock_amount"
id="stock-amount"
:inputClass="`sci-input-container-v2 ${errors.amount ? 'error' : ''}`"
:labelClass="`text-sm font-normal ${errors.amount ? 'text-sn-delete-red' : 'text-sn-grey'}`"
type="number"
:value="amount"
:decimals="stockValue.decimals"
:placeholder="i18n.t('repository_stock_values.manage_modal.amount_placeholder_new')"
required
:label="i18n.t('repository_stock_values.manage_modal.amount')"
showLabel
autoFocus
:error="errors.amount"
/>
</div>
<div class="flex flex-col w-40">
<label :class="`text-sm font-normal ${errors.unit ? 'text-sn-delete-red' : 'text-sn-grey'}`" for="stock-unit">
{{ i18n.t('repository_stock_values.manage_modal.unit') }}
</label>
<Select
:disabled="[2, 3].includes(operation)"
:value="unit"
:options="units"
:placeholder="i18n.t('repository_stock_values.manage_modal.unit_prompt')"
@change="unit = $event"
:className="`${errors.unit ? 'error' : ''}`"
></Select>
<div class="text-sn-delete-red text-xs" :class="{ visible: errors.unit, invisible: !errors.unit }">
{{ errors.unit }}
</div>
</div>
</fieldset>
<template v-if="stockValue?.id">
<div class="flex justify-between w-full items-center">
<div class="flex flex-col w-[220px] h-24 border-rounded bg-sn-super-light-grey justify-between text-center">
<span class="text-sm text-sn-grey leading-5">{{ i18n.t('repository_stock_values.manage_modal.current_stock') }}</span>
<span class="text-2xl text-sn-black font-semibold leading-8" :class="{ 'text-sn-delete-red': stockValue.amount < 0 }">{{ stockValue.amount }}</span>
<span class="text-sm text0sn-black leading-5">{{ initUnitLabel }}</span>
</div>
<i class="sn-icon sn-icon-arrow-right"></i>
<div class="flex flex-col w-[220px] h-24 border-rounded bg-sn-super-light-grey justify-between text-center">
<span class="text-sm text-sn-grey leading-5">{{ i18n.t('repository_stock_values.manage_modal.new_stock') }}</span>
<span class="text-2xl text-sn-black font-semibold leading-8" :class="{ 'text-sn-delete-red': newAmount < 0 }">
{{ (newAmount || newAmount === 0) ? newAmount : '-' }}
</span>
<span class="text-sm text0sn-black leading-5">{{ unitLabel }}</span>
</div>
</div>
</template>
<div class="repository-stock-reminder-selector">
<div class="sci-checkbox-container">
<input type="checkbox" name="reminder-enabled" tabindex="4" class="sci-checkbox" id="reminder-selector-checkbox" :checked="reminderEnabled" @change="reminderEnabled = $event.target.checked"/>
<span class="sci-checkbox-label"></span>
</div>
<span class="ml-2">{{ i18n.t('repository_stock_values.manage_modal.create_reminder') }}</span>
</div>
<div v-if="reminderEnabled" class="stock-reminder-value flex gap-2 items-center">
<Input
@input="lowStockTreshold = $event"
name="treshold_amount"
id="treshold-amount"
fieldClass="flex gap-2"
inputClass="sci-input-container-v2 w-40"
labelClass="text-sm font-normal flex items-center"
type="number"
:value="lowStockTreshold"
:decimals="stockValue.decimals"
:placeholder="i18n.t('repository_stock_values.manage_modal.amount_placeholder_new')"
required
:label="i18n.t('repository_stock_values.manage_modal.reminder_at')"
showLabel
:error="errors.tresholdAmount"
/>
<span class="text-sm font-normal">
{{ unitLabel }}
</span>
</div>
<div class="sci-input-container flex flex-col" :data-error-text="i18n.t('repository_stock_values.manage_modal.comment_limit')">
<label class="text-sn-grey text-sm font-normal" for="stock-value-comment">{{ i18n.t('repository_stock_values.manage_modal.comment') }}</label>
<input class="sci-input-field"
@input="comment = e.target.value"
type="text"
name="comment"
id="stock-value-comment"
:placeholder="i18n.t('repository_stock_values.manage_modal.comment_placeholder')"
/>
</div>
</form>
</div>
<div class="modal-footer">
<button type='button' class='btn btn-secondary' data-dismiss='modal'>
{{ i18n.t('general.cancel') }}
</button>
<button class="btn btn-primary" @click="validateAndsaveStockValue">
{{ i18n.t('repository_stock_values.manage_modal.save_stock') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import Select from './../shared/select.vue';
import Input from './../shared/input.vue';
export default {
name: 'ManageStockValueModal',
components: {
Select,
Input
},
data() {
return {
operation: null,
operations: [],
stockValue: null,
amount: 0,
repositoryRowName: null,
stockUrl: null,
units: null,
unit: null,
reminderEnabled: false,
lowStockTreshold: null,
comment: null,
errors: {}
}
},
computed: {
unitLabel: function() {
const currentUnit = this.units?.find(option => option[0] === this.unit);
return currentUnit ? currentUnit[1] : ''
},
initUnitLabel: function() {
const unit = this.units?.find(option => option[0] === this.stockValue?.unit);
return unit ? unit[1] : ''
},
newAmount: function() {
switch (this.operation) {
case 2:
if (this.amount) return parseFloat(this.stockValue.amount) + this.amount
case 3:
if(this.amount) return parseFloat(this.stockValue.amount) - this.amount
default:
return this.amount
}
}
},
created() {
window.manageStockModalComponent = this;
},
beforeDestroy() {
delete window.manageStockModalComponent;
},
mounted() {
// Focus stock amount input field
$(this.$refs.modal).on('show.bs.modal', function() {
setTimeout(() => {
$('#stock-amount')[0]?.focus()
}, 500)
});
},
methods: {
setOperation($event) {
this.operation = $event;
if ([2, 3].includes($event)) {
this.unit = this.stockValue.unit;
}
},
fetchStockValueData(stockValueUrl) {
if (!stockValueUrl) return;
$.ajax({
method: 'GET',
url: stockValueUrl,
dataType: 'json',
success: (result) => {
this.repositoryRowName = result.repository_row_name
this.stockValue = result.stock_value
this.amount = parseFloat(result.stock_value.amount)
this.units = result.stock_value.units
this.unit = result.stock_value.unit
this.reminderEnabled = result.stock_value.reminder_enabled
this.lowStockTreshold = result.stock_value.low_stock_treshold
this.operation = 1;
this.stockUrl = result.stock_url;
this.operations = [[1, 'set'], [2, 'add'], [3, 'remove']];
this.errors = {};
}
});
},
closeModal() {
$(this.$refs.modal).modal('hide');
},
showModal(stockValueUrl, closeCallback) {
$(this.$refs.modal).modal('show');
this.fetchStockValueData(stockValueUrl);
this.closeCallback = closeCallback;
},
validateAndsaveStockValue() {
let newErrors = {};
this.errors = newErrors;
if (!this.unit)
newErrors['unit'] = I18n.t('repository_stock_values.manage_modal.unit_error');
if (!this.amount)
newErrors['amount'] = I18n.t('repository_stock_values.manage_modal.amount_error');
if (parseFloat(this.amount) && this.amount < 0)
newErrors['amount'] = I18n.t('repository_stock_values.manage_modal.negative_error');
if (this.reminderEnabled && !this.lowStockTreshold)
newErrors['tresholdAmount'] = I18n.t('repository_stock_values.manage_modal.amount_error');
this.errors = newErrors;
if (!$.isEmptyObject(newErrors)) return;
const $this = this
$.ajax({
method: 'POST',
url: this.stockUrl,
dataType: 'json',
data: {
repository_stock_value: {
unit_item_id: this.unit,
amount: this.newAmount,
comment: this.comment,
low_stock_threshold: this.reminderEnabled ? this.lowStockTreshold : null
},
operator: this.operations.find(operation => operation[0] = this.operation)?.[1],
change_amount: Math.abs(this.amount),
},
success: function(result) {
$this.stockValue = null;
$this.closeModal();
$this.closeCallback && $this.closeCallback(result);
}
})
}
}
}
</script>

View file

@ -0,0 +1,73 @@
<template>
<div class="relative" :class="fieldClass">
<label v-if="showLabel" :class="labelClass" :for="id">{{ label }}</label>
<div :class="inputClass">
<input ref="input"
:id="id"
:type="type"
:name="name"
:value="value"
:class="`${error ? 'error' : ''}`"
:placeholder="placeholder"
:required="required"
@input="updateValue"
/>
<div
class="mt-2 text-sn-delete-red whitespace-nowrap truncate text-xs font-normal absolute bottom-[-1rem] w-full"
:title="error"
:class="{ visible: error, invisible: !error}"
>
{{ error }}
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Input',
props: {
id: { type: String, required: false },
fieldClass: { type: String, default: '' },
inputClass: { type: String, default: '' },
labelClass: { type: String, default: '' },
type: { type: String, default: 'text' },
name: { type: String, required: true },
value: { type: [String, Number], required: false },
decimals: { type: [Number, String], default: 0 },
placeholder: { type: String, default: '' },
required: { type: Boolean, default: false },
showLabel: { type: Boolean, default: false },
label: { type: String, required: false },
autoFocus: { type: Boolean, default: false },
error: { type: String, required: false }
},
watch: {
value: function(newVal) {
this.inputValue = newVal;
}
},
methods: {
updateValue($event) {
switch (this.type) {
case 'text':
this.$emit('input', $event.target.value);
break;
case 'number':
const newValue = this.formatDecimalValue($event.target.value);
this.$emit('input', parseFloat(newValue));
break
default:
break;
}
},
formatDecimalValue(value) {
let decimalValue = value.replace(/[^-0-9.]/g, '');
if (this.decimals === 0) {
return decimalValue.split('.')[0];
}
return decimalValue.match(new RegExp(`^-?\\d*(\\.\\d{0,${this.decimals}})?`))[0];
},
}
}
</script>

View file

@ -8,7 +8,8 @@
'sn-select--blank': !valueLabel,
'disabled cursor-default': disabled,
'cursor-pointer': !withEditCursor,
'sci-cursor-edit hover:border-sn-sleepy-grey': !disabled && !isOpen && withEditCursor
'sci-cursor-edit hover:border-sn-sleepy-grey': !disabled && !isOpen && withEditCursor,
[className]: true
}">
<slot>
<button ref="focusElement" class="sn-select__value">
@ -66,6 +67,7 @@
initialValue: { type: [String, Number] },
placeholder: { type: String },
noOptionsPlaceholder: { type: String },
className: { type: String, default: '' },
disabled: { type: Boolean, default: false }
},
directives: {

View file

@ -3,27 +3,37 @@
module RepositoryDatatable
class RepositoryStockValueSerializer < RepositoryBaseValueSerializer
include Canaid::Helpers::PermissionsHelper
include Rails.application.routes.url_helpers
def value
data = {
stock_formatted: value_object.formatted,
stock_amount: value_object.data,
low_stock_threshold: value_object.low_stock_threshold
low_stock_threshold: value_object.low_stock_threshold,
stock_url: edit_repository_stock_repository_repository_row_url(scope[:repository],
value_object.repository_row)
}
data.merge(reminder_values)
end
private
def reminder_values
data = {}
if scope.dig(:options, :reminders_enabled) &&
!scope[:repository].is_a?(RepositorySnapshot) &&
value_object.data.present? &&
value_object.low_stock_threshold.present?
data[:reminder] = value_object.low_stock_threshold > value_object.data
if data[:reminder] && (data[:stock_amount]).positive?
if data[:reminder] && value_object.data&.positive?
data[:reminder_text] =
I18n.t('repositories.item_card.reminders.stock_low', stock_formated: data[:stock_formatted])
I18n.t('repositories.item_card.reminders.stock_low', stock_formated: value_object.formatted)
elsif data[:reminder]
data[:reminder_text] = I18n.t('repositories.item_card.reminders.stock_empty')
end
end
if data[:stock_amount].zero?
if value_object.data && value_object.data <= 0
data[:reminder] = true
data[:reminder_text] = I18n.t('repositories.item_card.reminders.stock_empty')
end

View file

@ -152,8 +152,13 @@
<!-- Delete file modal -->
<%= render partial: 'assets/asset_delete_modal' %>
<!-- Manage Stock Modal -->
<%= render partial: 'shared/manage_stock_value_modal' %>
<!-- Consume Stock Modal -->
<%= render partial: 'my_modules/repositories/consume_stock_modal'%>
<%= javascript_include_tag 'inputmask' %>
<%= stylesheet_link_tag 'datatables' %>
<%= javascript_include_tag "handsontable.full" %>
<%= render partial: "shared/formulas_libraries" %>

View file

@ -73,12 +73,12 @@
locals: { repository: @repository } %>
<%= render partial: 'repository_columns/manage_column_modal', locals: { my_module_page: false } %>
<%= render partial: "repository_stock_values/manage_modal" %>
<%= render partial: "toolbar_buttons" %>
<%= render partial: "assign_items_to_task_modal" %>
<%= render partial: 'repository_filters' %>
<%= render partial: 'save_repository_filter_modal' %>
<%= render partial: 'shared/manage_stock_value_modal' %>
<%= javascript_include_tag 'vue_components_action_toolbar' %>

View file

@ -1,5 +1,6 @@
# frozen_string_literal: true
json.id @repository_row.id
json.repository do
json.id @repository.id
json.name @repository.name
@ -20,6 +21,13 @@ json.actions do
end
end
json.direct_file_upload_path rails_direct_uploads_url
json.stock do
if @repository_row.has_stock?
json.stock_value_url edit_repository_stock_repository_repository_row_url(@repository, @repository_row)
elsif @repository.has_stock_management?
json.stock_value_url new_repository_stock_repository_repository_row_url(@repository, @repository_row)
end
end
end
json.default_columns do
@ -58,8 +66,10 @@ json.custom_columns do
end
if repository_cell
json.merge! **serialize_repository_cell_value(repository_cell, @repository.team, @repository, reminders_enabled: @reminders_present).merge(
**repository_cell.repository_column.as_json(only: %i(id name data_type))
json.merge! serialize_repository_cell_value(
repository_cell, @repository.team, @repository, reminders_enabled: @reminders_present
).merge(
repository_cell.repository_column.as_json(only: %i(id name data_type))
).merge(options)
else
json.merge! repository_column.as_json(only: %i(id name data_type)).merge(options)

View file

@ -1,11 +0,0 @@
<div class="modal repository-stock-modal"
id="manage-repository-stock-value-modal"
tabindex="-1"
role="dialog"
aria-labelledby="manageRepositoryStockValueLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
</div>
</div>
</div>
<%= javascript_include_tag 'repositories/stock' %>

View file

@ -1,143 +0,0 @@
<%= form_tag update_repository_stock_repository_repository_row_path(@repository, @repository_row), method: :post, remote: true, novalidate: true do %>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<%= t('general.close') %>">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title" id="modal-delete-module-label">
<% if repository_stock_value.new_record? %>
<%= t('repository_stock_values.manage_modal.title', item: repository_row.name) %>
<% else %>
<%= t('repository_stock_values.manage_modal.edit_title', item: repository_row.name) %>
<% end %>
</h4>
</div>
<div class="modal-body">
<%= hidden_field_tag 'repository_stock_value[amount]', repository_stock_value.amount %>
<%= hidden_field_tag 'initial_units', repository_stock_value.repository_stock_unit_item_id %>
<%= hidden_field_tag 'change_amount' %>
<%= hidden_field_tag 'operator', 'set' %>
<p><%= t('repository_stock_values.manage_modal.enter_amount') %></p>
<div class="row">
<div class="col-sm-5 !pr-0">
<label><%= t('repository_stock_values.manage_modal.operation') %></label>
<% if repository_stock_value.id %>
<div class="btn-group" role="group" aria-label="Operator group">
<button type="button" data-operator="set" class="btn btn-primary stock-operator-option"><%= t('repository_stock_values.manage_modal.set') %></button>
<button type="button" data-operator="add" class="btn btn-secondary stock-operator-option"><%= t('repository_stock_values.manage_modal.add') %></button>
<button type="button" data-operator="remove" class="btn btn-secondary stock-operator-option"><%= t('repository_stock_values.manage_modal.remove') %></button>
</div>
<% else %>
<div class="btn-group" role="group" aria-label="Operator group">
<button type="button" data-operator="set" class="btn btn-primary"><%= t('repository_stock_values.manage_modal.set') %></button>
<button type="button" data-operator="add" class="btn btn-secondary disabled"><%= t('repository_stock_values.manage_modal.add') %></button>
<button type="button" data-operator="remove" class="btn btn-secondary disabled"><%= t('repository_stock_values.manage_modal.remove') %></button>
</div>
<% end %>
</div>
<div class="col-sm-7">
<div class="row">
<div class="col-sm-6">
<div class="sci-input-container" data-error-text="<%= t('repository_stock_values.manage_modal.amount_error') %>">
<label><%= t('repository_stock_values.manage_modal.amount') %></label>
<input id="stock-input-amount"
class="sci-input-field"
type="text"
autocomplete="off"
name="input-amount"
data-operator="set"
tabindex="1"
data-current-amount="<%= repository_stock_value.amount %>"
data-decimals="<%= repository_stock_column.metadata['decimals'] %>"
value="<%= repository_stock_value.formatted_value %>"
placeholder="<%= t("repository_stock_values.manage_modal.amount_placeholder_new") %>"
required />
</div>
</div>
<div class="col-sm-6">
<label><%= t('repository_stock_values.manage_modal.unit') %></label>
<select class="form-control" name="repository_stock_value[unit_item_id]" id="repository-stock-value-units" required>
<option valus=""><%= t('repository_stock_values.manage_modal.unit_prompt') %></option>
<% unit_items.each do |unit_item| %>
<option value="<%= unit_item.id %>" <%= 'selected' if repository_stock_value.repository_stock_unit_item == unit_item || unit_items.one?%>>
<%= unit_item.data %>
</option>
<% end %>
</select>
<div class="input-error-message">
<%= t('repository_stock_values.manage_modal.unit_error') %>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="stock-comment-field sci-input-container" data-error-text="<%= t('repository_stock_values.manage_modal.comment_limit') %>">
<label><%= t('repository_stock_values.manage_modal.comment') %></label>
<input class="sci-input-field"
type="text"
tabindex="3"
name="repository_stock_value[comment]"
id="repository-stock-value-comment"
placeholder="<%= t('repository_stock_values.manage_modal.comment_placeholder') %>">
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="repository-stock-reminder-selector">
<div class="sci-checkbox-container">
<input type="checkbox" name="reminder-enabled" tabindex="4" class="sci-checkbox" id="reminder-selector-checkbox" <%= "checked" if repository_stock_value.low_stock_threshold.present? %>/>
<span class="sci-checkbox-label"></span>
</div>
<%= t('repository_stock_values.manage_modal.create_reminder') %>
</div>
<div class="repository-stock-reminder-value <%= "hidden" if repository_stock_value.low_stock_threshold.blank? %>">
<span><%= t('repository_stock_values.manage_modal.reminder_at') %></span>
<div class="sci-input-container" data-error-text="<%= t('repository_stock_values.manage_modal.enter_ammount') %>">
<input type="text"
autocomplete="off"
id="low_stock_threshold"
name="repository_stock_value[low_stock_threshold]"
value="<%= repository_stock_value.formatted_treshold %>"
data-decimals="<%= repository_stock_column.metadata['decimals'] %>"
class="sci-input-field"
tabindex="5"
placeholder="Enter amount"/>
</div>
<span class="units">
</span>
</div>
</div>
</div>
<% unless repository_stock_value.new_record? %>
<div class="row">
<div class="col-sm-12">
<div class="stock-update-view">
<div class="stock-initial-container <%= 'negative' if repository_stock_value.amount.negative? %>">
<span class="subtitle"><%= t('repository_stock_values.manage_modal.current_stock') %></span>
<span class="value"><%= repository_stock_value.formatted_value %></span>
<span class="units"><%= repository_stock_value.repository_stock_unit_item&.data %></span>
</div>
<div class="stock-arrow">
<i class="sn-icon sn-icon-arrow-right"></i>
</div>
<div class="stock-final-container <%= 'negative' if repository_stock_value.amount.negative? %> ">
<span class="subtitle"><%= t('repository_stock_values.manage_modal.new_stock') %></span>
<span class="value"><%= repository_stock_value.formatted_value %></span>
<span class="units"><%= repository_stock_value.repository_stock_unit_item&.data %></span>
</div>
</div>
</div>
</div>
<% end %>
</div>
<div class="modal-footer">
<button type="button"
id="cancel"
class="btn btn-secondary"
tabindex="7"
data-dismiss="modal"><%=t 'general.cancel' %></button>
<%= submit_tag t('repository_stock_values.manage_modal.save_stock'), class: "btn btn-primary update-repository-stock", tabindex: "6" %>
</div>
<% end %>

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
json.stock_url update_repository_stock_repository_repository_row_url(@repository, @repository_row)
json.repository_row_name @repository_row.name
json.stock_value do
json.id @repository_stock_value.id
json.amount @repository_stock_value.formatted_value
json.decimals @repository_column.metadata['decimals']
json.units @repository_column.repository_stock_unit_items.pluck(:id, :data)
json.unit @repository_stock_value.repository_stock_unit_item_id
json.reminder_enabled @repository_stock_value.low_stock_threshold.present?
json.low_stock_treshold @repository_stock_value.formatted_treshold
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
json.stock_url update_repository_stock_repository_repository_row_url(@repository, @repository_row)
json.repository_row_name @repository_row.name
json.stock_value do
json.decimals @repository_column.metadata['decimals']
json.units @repository_column.repository_stock_unit_items.pluck(:id, :data)
end

View file

@ -0,0 +1,5 @@
<div id="manageStockValueModal" data-behaviour="vue">
<manage-stock-value-modal />
</div>
<%= javascript_include_tag('vue_components_manage_stock_value_modal') %>

View file

@ -81,7 +81,6 @@ Rails.application.config.assets.precompile += %w(label_printers/zebra_settings.j
Rails.application.config.assets.precompile += %w(repositories/index.js)
Rails.application.config.assets.precompile += %w(repositories/share_modal.js)
Rails.application.config.assets.precompile += %w(repositories/edit.js)
Rails.application.config.assets.precompile += %w(repositories/stock.js)
Rails.application.config.assets.precompile += %w(repositories/repository_datatable.js)
Rails.application.config.assets.precompile += %w(global_activities/index.js)
Rails.application.config.assets.precompile += %w(repositories/show.js)

View file

@ -2294,6 +2294,10 @@ en:
expand: 'Expand'
collapse: 'Collapse'
dropdown_placeholder: 'Select'
date_time:
errors:
set_all_or_none: 'Needs to set both or none'
not_valid_range: 'Range is not valid.'
highlight_component:
information_label: 'Information'
custom_columns_label: 'Custom columns'

View file

@ -43,7 +43,8 @@ const entryList = {
vue_components_open_vector_editor: './app/javascript/packs/vue/open_vector_editor.js',
vue_navigation_breadcrumbs: './app/javascript/packs/vue/navigation/breadcrumbs.js',
vue_protocol_file_import_modal: './app/javascript/packs/vue/protocol_file_import_modal.js',
vue_components_export_stock_consumption_modal: './app/javascript/packs/vue/export_stock_consumption_modal.js'
vue_components_export_stock_consumption_modal: './app/javascript/packs/vue/export_stock_consumption_modal.js',
vue_components_manage_stock_value_modal: './app/javascript/packs/vue/manage_stock_value_modal.js'
}
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949