feat: implement stock keeping unit management form and list components
- Added StockKeepingUnitFormComponent for creating and editing stock keeping units. - Introduced StockKeepingUnitsComponent for listing stock keeping units with pagination. - Integrated form fields for code, name, VAT, and SKU type in the stock keeping unit form. - Created API routes for stock keeping units with CRUD operations. - Updated input components to handle numeric and price types with Persian/Arabic digit normalization. - Enhanced key-value component to support tag display instead of badge. - Adjusted layout styles for sidebar menu. - Added AGENT.md for repository-specific coding guidelines and practices. - Implemented good management form with file upload and category selection.
This commit is contained in:
@@ -67,6 +67,7 @@ export class InputComponent {
|
||||
@Input() maxLength?: number;
|
||||
@Input() min?: number;
|
||||
@Input() max?: number;
|
||||
@Input() fixed?: number;
|
||||
@Output() valueChange = new EventEmitter<string | number>();
|
||||
@Output() blur = new EventEmitter<void>();
|
||||
|
||||
@@ -111,12 +112,13 @@ export class InputComponent {
|
||||
|
||||
get inputMode(): string | null {
|
||||
switch (this.type) {
|
||||
case 'price':
|
||||
return 'decimal';
|
||||
case 'number':
|
||||
case 'mobile':
|
||||
case 'phone':
|
||||
case 'postalCode':
|
||||
case 'nationalId':
|
||||
case 'price':
|
||||
case 'number':
|
||||
return 'numeric';
|
||||
default:
|
||||
return 'text';
|
||||
@@ -169,11 +171,12 @@ export class InputComponent {
|
||||
return 'email';
|
||||
case 'mobile':
|
||||
case 'phone':
|
||||
case 'price':
|
||||
case 'postalCode':
|
||||
case 'nationalId':
|
||||
case 'number':
|
||||
return 'number';
|
||||
case 'price':
|
||||
case 'number':
|
||||
return 'text';
|
||||
case 'checkbox':
|
||||
return 'checkbox';
|
||||
case 'switch':
|
||||
@@ -190,59 +193,98 @@ export class InputComponent {
|
||||
onPriceInput($event: InputNumberInputEvent) {
|
||||
this.onInput($event.originalEvent, true);
|
||||
}
|
||||
|
||||
private toEnglishDigits(value: string): string {
|
||||
return value
|
||||
.replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 1776))
|
||||
.replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - 1632));
|
||||
}
|
||||
|
||||
private sanitizeNumericValue(value: string, allowDecimal: boolean): string {
|
||||
const normalized = this.toEnglishDigits(value).replace(/,/g, '');
|
||||
const cleaned = normalized.replace(allowDecimal ? /[^0-9.]/g : /[^0-9]/g, '');
|
||||
if (!allowDecimal) return cleaned;
|
||||
|
||||
const [integerPart = '', ...decimalParts] = cleaned.split('.');
|
||||
const mergedDecimals = decimalParts.join('');
|
||||
return decimalParts.length ? `${integerPart}.${mergedDecimals}` : integerPart;
|
||||
}
|
||||
|
||||
private applyFixed(value: string): string {
|
||||
if (this.fixed === undefined || this.fixed === null || this.fixed < 0 || value === '') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (this.fixed === 0) {
|
||||
return value.split('.')[0] || '0';
|
||||
}
|
||||
|
||||
const [integerPart = '0', decimalPart = ''] = value.split('.');
|
||||
const normalizedInteger = integerPart === '' ? '0' : integerPart;
|
||||
const fixedDecimal = decimalPart.slice(0, this.fixed).padEnd(this.fixed, '0');
|
||||
return `${normalizedInteger}.${fixedDecimal}`;
|
||||
}
|
||||
|
||||
onInput($event: Event, isPriceFormat?: boolean) {
|
||||
// @ts-ignore
|
||||
let value = $event.target.value as string;
|
||||
// @ts-ignore
|
||||
const isDotInput = $event.data === '.';
|
||||
const target = $event.target as HTMLInputElement | null;
|
||||
if (!target) return;
|
||||
|
||||
if (isPriceFormat) {
|
||||
value = value.replace(/,/g, '');
|
||||
let value = target.value ?? '';
|
||||
const allowDecimal = this.type === 'price' || this.type === 'number';
|
||||
|
||||
if (this.inputMode === 'numeric' || this.inputMode === 'decimal' || isPriceFormat) {
|
||||
value = this.sanitizeNumericValue(value, allowDecimal);
|
||||
value = this.applyFixed(value);
|
||||
}
|
||||
|
||||
let replacedTargetValue: Maybe<number | string> = null;
|
||||
if (this.inputMode === 'numeric' && !isDotInput) {
|
||||
let newValue = parseFloat(value || '0');
|
||||
const minValidator = (this.min || this.min === 0) && parseFloat(value) < this.min!;
|
||||
const maxValidator = (this.max || this.max === 0) && parseFloat(value) > this.max;
|
||||
if (minValidator || maxValidator) {
|
||||
if (maxValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند بیشتر از ${this.max} باشد.`,
|
||||
});
|
||||
}
|
||||
if (minValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند کمتر از ${this.min} باشد.`,
|
||||
});
|
||||
}
|
||||
console.log(value);
|
||||
|
||||
newValue = parseFloat(value.slice(0, value.length - 1));
|
||||
if (newValue < 0 || isNaN(newValue)) {
|
||||
newValue = 0;
|
||||
}
|
||||
if (this.preparedMaxLength && value.length > this.preparedMaxLength) {
|
||||
value = value.slice(0, this.preparedMaxLength);
|
||||
}
|
||||
|
||||
replacedTargetValue = isPriceFormat ? newValue.toLocaleString('en-US') : newValue;
|
||||
this.control.setValue(newValue);
|
||||
let numericValue = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
|
||||
const minValidator = (this.min || this.min === 0) && numericValue < this.min!;
|
||||
const maxValidator = (this.max || this.max === 0) && numericValue > this.max!;
|
||||
if (
|
||||
(this.inputMode === 'numeric' || this.inputMode === 'decimal') &&
|
||||
value !== '' &&
|
||||
(minValidator || maxValidator)
|
||||
) {
|
||||
if (maxValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند بیشتر از ${this.max} باشد.`,
|
||||
});
|
||||
}
|
||||
if (!['nationalId', 'phone', 'postalCode', 'mobile'].includes(this.type)) {
|
||||
this.control.setValue(newValue);
|
||||
if (minValidator) {
|
||||
this.toastService.warn({
|
||||
text: `مقدار ${this.label} نمیتواند کمتر از ${this.min} باشد.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.preparedMaxLength && this.preparedMaxLength < value.length) {
|
||||
let newValue = value.slice(0, value.length - 1);
|
||||
// @ts-ignore
|
||||
replacedTargetValue = newValue;
|
||||
this.control.setValue(newValue);
|
||||
value = value.slice(0, -1);
|
||||
numericValue = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
|
||||
}
|
||||
|
||||
if (replacedTargetValue !== null) {
|
||||
console.log(replacedTargetValue);
|
||||
const isIdentifierField = [
|
||||
'nationalId',
|
||||
'phone',
|
||||
'postalCode',
|
||||
'mobile',
|
||||
'email',
|
||||
'password',
|
||||
'simple',
|
||||
].includes(this.type);
|
||||
this.control.setValue(isIdentifierField ? value : value === '' ? '' : numericValue);
|
||||
|
||||
// @ts-ignore
|
||||
$event.value = replacedTargetValue;
|
||||
// @ts-ignore
|
||||
$event.target.value = replacedTargetValue;
|
||||
const viewValue = isPriceFormat && value !== '' ? numericValue.toLocaleString('en-US') : value;
|
||||
console.log('viewValue', viewValue);
|
||||
|
||||
target.value = viewValue;
|
||||
if (
|
||||
(this.inputMode === 'numeric' || this.inputMode === 'decimal') &&
|
||||
this.valueChange.observed
|
||||
) {
|
||||
this.valueChange.emit(isIdentifierField ? value : numericValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user