662 lines
28 KiB
TypeScript
662 lines
28 KiB
TypeScript
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
import { useParams, useNavigate, useSearchParams, useLocation } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useItemDetails, useItemMutations } from '../hooks/useItem';
|
|
import { FaArrowLeft, FaSave, FaEdit, FaCheck, FaTrashAlt, FaSync } from 'react-icons/fa';
|
|
import type { CreateItemData } from '../services/itemService';
|
|
import LinkField from '../components/LinkField';
|
|
import API_CONFIG from '../config/api';
|
|
import CommentSection from '../components/CommentSection';
|
|
import ActivityLog from '../components/ActivityLog';
|
|
|
|
import DeleteRequestButton from '../components/DeleteRequestButton';
|
|
import type { DeleteStatus } from '../services/deleteRequestService';
|
|
import apiService from '../services/apiService';
|
|
|
|
const ItemDetail: React.FC = () => {
|
|
const { t } = useTranslation();
|
|
// const { itemName } = useParams<{ itemName: string }>();
|
|
// const navigate = useNavigate();
|
|
// const [searchParams] = useSearchParams();
|
|
// const duplicateFromItem = searchParams.get('duplicate');
|
|
|
|
// const isNewItem = itemName === 'new';
|
|
// const isDuplicating = isNewItem && !!duplicateFromItem;
|
|
|
|
const { itemName: rawItemName } = useParams<{ itemName: string }>();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const [searchParams] = useSearchParams();
|
|
const duplicateFromItem = searchParams.get('duplicate');
|
|
|
|
// Extract item name from pathname directly to preserve # characters
|
|
// which browsers strip from useParams as URL fragments
|
|
// const itemName = useMemo(() => {
|
|
// const prefix = '/inventory/';
|
|
// const idx = location.pathname.indexOf(prefix);
|
|
// if (idx !== -1) {
|
|
// const encoded = location.pathname.slice(idx + prefix.length);
|
|
// const decoded = decodeURIComponent(encoded);
|
|
// return decoded;
|
|
// }
|
|
// return rawItemName || '';
|
|
// }, [location.pathname, rawItemName]);
|
|
|
|
const itemName = useMemo(() => {
|
|
if (rawItemName === 'new') return 'new';
|
|
|
|
// Use the raw encoded pathname and decode it ourselves
|
|
// to avoid React Router's automatic decoding losing # info
|
|
const prefix = '/inventory/';
|
|
const fullPath = window.location.pathname; // e.g. /asm_app/inventory/DELUGE%20VALVE%20%20NO%23%201
|
|
const idx = fullPath.indexOf(prefix);
|
|
if (idx !== -1) {
|
|
const encoded = fullPath.slice(idx + prefix.length);
|
|
try {
|
|
return decodeURIComponent(encoded);
|
|
} catch {
|
|
return encoded;
|
|
}
|
|
}
|
|
return rawItemName || '';
|
|
}, [rawItemName, location.pathname]);
|
|
|
|
const isNewItem = itemName === 'new';
|
|
const isDuplicating = isNewItem && !!duplicateFromItem;
|
|
|
|
// Balance Qty state (fetched from Bin doctype)
|
|
const [balanceQty, setBalanceQty] = useState<number>(0);
|
|
const [balanceQtyLoading, setBalanceQtyLoading] = useState<boolean>(false);
|
|
|
|
const [userRoles, setUserRoles] = useState<string[]>([]);
|
|
const [isSystemManager, setIsSystemManager] = useState(false);
|
|
const [rolesLoaded, setRolesLoaded] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const fetchRoles = async () => {
|
|
try {
|
|
const response = await apiService.apiCall<any>(
|
|
'/api/method/asset_lite.api.user_roles.get_user_roles'
|
|
);
|
|
const roles = Array.isArray(response) ? response : (response?.message || []);
|
|
setUserRoles(roles);
|
|
setIsSystemManager(roles.includes('System Manager'));
|
|
} catch (err) {
|
|
console.error('Error fetching roles:', err);
|
|
} finally {
|
|
setRolesLoaded(true);
|
|
}
|
|
};
|
|
fetchRoles();
|
|
}, []);
|
|
|
|
// Form data state
|
|
const [formData, setFormData] = useState<CreateItemData>({
|
|
item_code: '',
|
|
item_name: '',
|
|
item_group: '',
|
|
custom_technical_department: '',
|
|
custom_hospital_name: '',
|
|
custom_part_description: '',
|
|
stock_uom: 'Nos',
|
|
custom_item_cost_per_unit: 0,
|
|
disabled: 0,
|
|
is_stock_item: 1,
|
|
is_fixed_asset: 0,
|
|
opening_stock: 0,
|
|
valuation_rate: 0,
|
|
standard_rate: 0,
|
|
custom_last_calibration_date: '',
|
|
custom_next_due_calibration_date: '',
|
|
description: '',
|
|
brand: '',
|
|
custom_warranty_in_months: '',
|
|
valuation_method: '',
|
|
has_batch_no: 0,
|
|
has_serial_no: 0,
|
|
custom_serial_no: '',
|
|
custom_date_in: '',
|
|
custom_code: '',
|
|
custom_type: '',
|
|
custom_volts: undefined as number | undefined,
|
|
custom_w: undefined as number | undefined,
|
|
is_purchase_item: 1,
|
|
is_sales_item: 1,
|
|
country_of_origin: 'Saudi Arabia',
|
|
});
|
|
|
|
const { item, loading, error, refetch: refetchItem } = useItemDetails(
|
|
isDuplicating ? duplicateFromItem : (isNewItem ? null : itemName || null)
|
|
);
|
|
const { createItem, updateItem, submitItem, loading: saving } = useItemMutations();
|
|
|
|
const [isEditing, setIsEditing] = useState(isNewItem);
|
|
|
|
// Check document status
|
|
const docstatus = item?.docstatus ?? 0;
|
|
const isSubmitted = docstatus === 1;
|
|
const isCancelled = docstatus === 2;
|
|
const isDraft = docstatus === 0;
|
|
const hasDeleteRequest = !!(item?.custom_delete_status);
|
|
|
|
// Check if Calibration Information should be shown
|
|
const showCalibrationInfo = formData.item_group === 'Tools';
|
|
|
|
// Fetch Balance Qty from Bin doctype
|
|
const fetchBalanceQty = useCallback(async (itemCode: string) => {
|
|
if (!itemCode) return;
|
|
|
|
setBalanceQtyLoading(true);
|
|
try {
|
|
// Get CSRF token
|
|
let csrfToken: string | null = null;
|
|
if (typeof window !== 'undefined' && (window as any).csrf_token) {
|
|
csrfToken = (window as any).csrf_token;
|
|
}
|
|
|
|
// Build filters and fields for Frappe API
|
|
const filters = JSON.stringify([['item_code', '=', itemCode]]);
|
|
const fields = JSON.stringify(['actual_qty', 'warehouse']);
|
|
|
|
const url = `${API_CONFIG.BASE_URL}/api/resource/Bin?filters=${encodeURIComponent(filters)}&fields=${encodeURIComponent(fields)}&limit_page_length=0`;
|
|
|
|
const headers: Record<string, string> = {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (csrfToken) {
|
|
headers['X-Frappe-CSRF-Token'] = csrfToken;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
headers,
|
|
credentials: 'include', // Include cookies for session auth
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
// Sum up actual_qty from all warehouses
|
|
const totalQty = result.data?.reduce((sum: number, bin: any) => {
|
|
return sum + (bin.actual_qty || 0);
|
|
}, 0) || 0;
|
|
|
|
setBalanceQty(totalQty);
|
|
} catch (err) {
|
|
console.error('Failed to fetch balance qty:', err);
|
|
setBalanceQty(0);
|
|
} finally {
|
|
setBalanceQtyLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
// Fetch balance qty when item is loaded (for existing items)
|
|
useEffect(() => {
|
|
if (!isNewItem && item?.item_code) {
|
|
fetchBalanceQty(item.item_code);
|
|
}
|
|
}, [isNewItem, item?.item_code, fetchBalanceQty]);
|
|
|
|
// Load item data when item is fetched
|
|
useEffect(() => {
|
|
if (item && !isDuplicating) {
|
|
setFormData({
|
|
item_code: item.item_code || '',
|
|
item_name: item.item_name || '',
|
|
item_group: item.item_group || '',
|
|
custom_technical_department: item.custom_technical_department || '',
|
|
custom_hospital_name: item.custom_hospital_name || '',
|
|
custom_part_description: item.custom_part_description || '',
|
|
stock_uom: item.stock_uom || 'Nos',
|
|
custom_item_cost_per_unit: item.custom_item_cost_per_unit || 0,
|
|
disabled: item.disabled || 0,
|
|
is_stock_item: item.is_stock_item ?? 1,
|
|
is_fixed_asset: item.is_fixed_asset ?? 0,
|
|
opening_stock: item.opening_stock || 0,
|
|
valuation_rate: item.valuation_rate ?? 0,
|
|
standard_rate: item.standard_rate || 0,
|
|
custom_last_calibration_date: item.custom_last_calibration_date || '',
|
|
custom_next_due_calibration_date: item.custom_next_due_calibration_date || '',
|
|
description: item.description || '',
|
|
brand: item.brand || '',
|
|
custom_warranty_in_months: item.custom_warranty_in_months || '',
|
|
valuation_method: item.valuation_method || '',
|
|
has_batch_no: item.has_batch_no || 0,
|
|
has_serial_no: item.has_serial_no || 0,
|
|
is_purchase_item: item.is_purchase_item ?? 1,
|
|
is_sales_item: item.is_sales_item ?? 1,
|
|
country_of_origin: item.country_of_origin || 'Saudi Arabia',
|
|
uoms: item.uoms || [],
|
|
item_defaults: item.item_defaults || [],
|
|
custom_serial_no: item.custom_serial_no || '',
|
|
custom_date_in: item.custom_date_in || '',
|
|
custom_code: item.custom_code || '',
|
|
custom_type: item.custom_type || '',
|
|
custom_volts: item.custom_volts,
|
|
custom_w: item.custom_w,
|
|
});
|
|
setIsEditing(false);
|
|
} else if (isDuplicating && item) {
|
|
// When duplicating, copy data but clear name/code
|
|
setFormData({
|
|
item_code: '',
|
|
item_name: item.item_name || '',
|
|
item_group: item.item_group || '',
|
|
custom_technical_department: item.custom_technical_department || '',
|
|
custom_hospital_name: item.custom_hospital_name || '',
|
|
custom_part_description: item.custom_part_description || '',
|
|
stock_uom: item.stock_uom || 'Nos',
|
|
custom_item_cost_per_unit: item.custom_item_cost_per_unit || 0,
|
|
disabled: 0,
|
|
is_stock_item: item.is_stock_item ?? 1,
|
|
is_fixed_asset: item.is_fixed_asset ?? 0,
|
|
opening_stock: item.opening_stock || 0,
|
|
valuation_rate: item.valuation_rate ?? 0,
|
|
standard_rate: item.standard_rate || 0,
|
|
custom_last_calibration_date: item.custom_last_calibration_date || '',
|
|
custom_next_due_calibration_date: item.custom_next_due_calibration_date || '',
|
|
description: item.description || '',
|
|
brand: item.brand || '',
|
|
custom_warranty_in_months: item.custom_warranty_in_months || '',
|
|
valuation_method: item.valuation_method || '',
|
|
has_batch_no: item.has_batch_no || 0,
|
|
has_serial_no: item.has_serial_no || 0,
|
|
is_purchase_item: item.is_purchase_item ?? 1,
|
|
is_sales_item: item.is_sales_item ?? 1,
|
|
country_of_origin: item.country_of_origin || 'Saudi Arabia',
|
|
uoms: item.uoms || [],
|
|
item_defaults: item.item_defaults || [],
|
|
custom_serial_no: item.custom_serial_no || '',
|
|
custom_date_in: item.custom_date_in || '',
|
|
custom_code: item.custom_code || '',
|
|
custom_type: item.custom_type || '',
|
|
custom_volts: item.custom_volts,
|
|
custom_w: item.custom_w,
|
|
});
|
|
}
|
|
}, [item, isDuplicating]);
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
if (isNewItem) {
|
|
const newItem = await createItem(formData);
|
|
navigate(`/inventory/${newItem.name}`);
|
|
} else {
|
|
await updateItem(itemName!, formData);
|
|
await refetchItem();
|
|
// Refresh balance qty after update
|
|
if (formData.item_code) {
|
|
fetchBalanceQty(formData.item_code);
|
|
}
|
|
setIsEditing(false);
|
|
alert(t('items.itemUpdatedSuccessfully'));
|
|
}
|
|
} catch (err) {
|
|
alert(`${t('items.failedToSave')}: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!itemName || isNewItem) {
|
|
alert(t('items.pleaseSaveFirst'));
|
|
return;
|
|
}
|
|
try {
|
|
await submitItem(itemName);
|
|
await refetchItem();
|
|
setIsEditing(false);
|
|
alert(t('items.submittedSuccessfully'));
|
|
} catch (err) {
|
|
alert(`${t('items.failedToSubmit')}: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
}
|
|
};
|
|
|
|
const isFieldDisabled = useCallback((fieldname: string): boolean => {
|
|
if (!isEditing) return true;
|
|
if (isCancelled) return true;
|
|
if (hasDeleteRequest) return true;
|
|
if (isSubmitted) {
|
|
// For submitted items, most fields are read-only
|
|
// Only allow editing certain fields if needed
|
|
return true;
|
|
}
|
|
return false;
|
|
}, [isEditing, isCancelled, isSubmitted, hasDeleteRequest]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600 dark:text-gray-400">{t('items.loadingItem')}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error && !isNewItem) {
|
|
return (
|
|
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-6">
|
|
<h2 className="text-xl font-bold text-red-800 dark:text-red-300 mb-4">{t('items.errorLoadingItem')}</h2>
|
|
<p className="text-red-700 dark:text-red-400 mb-4">{error}</p>
|
|
<button
|
|
onClick={() => navigate(-1)}
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
|
|
>
|
|
{t('items.backToInventory')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const inputClassName = "w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white";
|
|
const labelClassName = "block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1";
|
|
const sectionHeaderClassName = "text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700";
|
|
const cardClassName = "bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700";
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
|
{/* Header */}
|
|
<div className="mb-6 flex justify-between items-center">
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={() => navigate(-1)}
|
|
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-2"
|
|
>
|
|
<FaArrowLeft />
|
|
<span className="text-gray-900 dark:text-white font-medium">
|
|
{isNewItem ? t('items.newItem') : item?.item_name || item?.item_code || t('items.title')}
|
|
</span>
|
|
</button>
|
|
{!isNewItem && (
|
|
<span className="px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300">
|
|
{item?.item_code || itemName}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex gap-3">
|
|
{!isNewItem && !isEditing && isDraft && !hasDeleteRequest && (
|
|
<button
|
|
onClick={() => setIsEditing(true)}
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
|
|
>
|
|
<FaEdit />
|
|
{t('common.edit')}
|
|
</button>
|
|
)}
|
|
{/* {!isNewItem && !isEditing && rolesLoaded && (
|
|
<DeleteRequestButton
|
|
doctype="Item"
|
|
docname={itemName}
|
|
currentDeleteStatus={(item?.custom_delete_status ?? null) as DeleteStatus}
|
|
userRoles={userRoles}
|
|
isSystemManager={isSystemManager}
|
|
inline
|
|
redirectOnDelete="/inventory"
|
|
onStatusChange={() => refetchItem()}
|
|
/>
|
|
)} */}
|
|
{isEditing && (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (isNewItem) navigate(-1);
|
|
else { setIsEditing(false); refetchItem(); }
|
|
}}
|
|
className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg"
|
|
>
|
|
{t('common.cancel')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 disabled:opacity-50"
|
|
>
|
|
<FaSave />
|
|
{saving ? t('common.saving') : t('common.save')}
|
|
</button>
|
|
{/* {!isNewItem && isDraft && (
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={saving}
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 disabled:opacity-50"
|
|
>
|
|
<FaCheck />
|
|
{t('common.submit')}
|
|
</button>
|
|
)} */}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Form - Grid Layout matching AssetDetail */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{/* COLUMN 1: Basic Information */}
|
|
<div className={cardClassName}>
|
|
<h2 className={sectionHeaderClassName}>{t('items.basicInformation')}</h2>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className={labelClassName}>{t('items.itemCode')} <span className="text-red-500">*</span></label>
|
|
<input
|
|
type="text"
|
|
value={formData.item_code}
|
|
onChange={(e) => setFormData({ ...formData, item_code: e.target.value })}
|
|
disabled={isFieldDisabled('item_code') || !isNewItem}
|
|
className={inputClassName}
|
|
required
|
|
/>
|
|
</div>
|
|
<LinkField
|
|
label={t('commonFields.hospital')}
|
|
doctype="Company"
|
|
value={formData.custom_hospital_name || ''}
|
|
onChange={(value) => setFormData({ ...formData, custom_hospital_name: value })}
|
|
disabled={isFieldDisabled('custom_hospital_name')}
|
|
placeholder={t('items.selectHospital')}
|
|
filters={{ domain: 'Healthcare' }}
|
|
/>
|
|
<LinkField
|
|
label={t('items.itemGroup')}
|
|
doctype="Item Group"
|
|
value={formData.item_group || ''}
|
|
onChange={(value) => setFormData({ ...formData, item_group: value })}
|
|
disabled={isFieldDisabled('item_group')}
|
|
placeholder={t('items.selectItemGroup')}
|
|
/>
|
|
<LinkField
|
|
label={t('items.technicalDepartment')}
|
|
doctype="Issue Type"
|
|
value={formData.custom_technical_department || ''}
|
|
onChange={(value) => setFormData({ ...formData, custom_technical_department: value })}
|
|
disabled={isFieldDisabled('custom_technical_department')}
|
|
placeholder={t('items.selectTechnicalDepartment')}
|
|
/>
|
|
<div>
|
|
<label className={labelClassName}>{t('items.stockUOM')}</label>
|
|
<input
|
|
type="text"
|
|
value={formData.stock_uom}
|
|
onChange={(e) => setFormData({ ...formData, stock_uom: e.target.value })}
|
|
disabled={isFieldDisabled('stock_uom')}
|
|
className={inputClassName}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className={labelClassName}>{t('items.partDescription')}</label>
|
|
<input
|
|
type="text"
|
|
value={formData.custom_part_description}
|
|
onChange={(e) => setFormData({ ...formData, custom_part_description: e.target.value })}
|
|
disabled={isFieldDisabled('custom_part_description')}
|
|
className={inputClassName}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* COLUMN 2: Inventory Details */}
|
|
<div className={cardClassName}>
|
|
<h2 className={sectionHeaderClassName}>{t('items.inventoryDetails')}</h2>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className={labelClassName}>{t('items.serialNo')}</label>
|
|
<input type="text" value={formData.custom_serial_no} onChange={(e) => setFormData({ ...formData, custom_serial_no: e.target.value })} disabled={isFieldDisabled('custom_serial_no')} className={inputClassName} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClassName}>{t('items.dateIn')}</label>
|
|
<input type="date" value={formData.custom_date_in} onChange={(e) => setFormData({ ...formData, custom_date_in: e.target.value })} disabled={isFieldDisabled('custom_date_in')} className={inputClassName} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClassName}>{t('items.watts')}</label>
|
|
<input type="number" step="0.01" value={formData.custom_w ?? ''} onChange={(e) => { const v = parseFloat(e.target.value); setFormData({ ...formData, custom_w: e.target.value === '' || isNaN(v) ? undefined : v }); }} disabled={isFieldDisabled('custom_w')} className={inputClassName} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClassName}>{t('items.volts')}</label>
|
|
<input type="number" step="0.01" value={formData.custom_volts ?? ''} onChange={(e) => { const v = parseFloat(e.target.value); setFormData({ ...formData, custom_volts: e.target.value === '' || isNaN(v) ? undefined : v }); }} disabled={isFieldDisabled('custom_volts')} className={inputClassName} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClassName}>{t('items.type')}</label>
|
|
<input type="text" value={formData.custom_type} onChange={(e) => setFormData({ ...formData, custom_type: e.target.value })} disabled={isFieldDisabled('custom_type')} className={inputClassName} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClassName}>{t('items.code')}</label>
|
|
<input type="text" value={formData.custom_code} onChange={(e) => setFormData({ ...formData, custom_code: e.target.value })} disabled={isFieldDisabled('custom_code')} className={inputClassName} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* COLUMN 3: Stock & Additional Information */}
|
|
<div className={cardClassName}>
|
|
<h2 className={sectionHeaderClassName}>{t('items.stockInformation')}</h2>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-2">
|
|
<input type="checkbox" id="is_stock_item" checked={formData.is_stock_item === 1} onChange={(e) => setFormData({ ...formData, is_stock_item: e.target.checked ? 1 : 0 })} disabled={isFieldDisabled('is_stock_item')} className="w-4 h-4" />
|
|
<label htmlFor="is_stock_item" className="text-sm font-medium text-gray-700 dark:text-gray-300">{t('items.isStockItem')}</label>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<input type="checkbox" id="is_fixed_asset" checked={formData.is_fixed_asset === 1} onChange={(e) => setFormData({ ...formData, is_fixed_asset: e.target.checked ? 1 : 0 })} disabled={isFieldDisabled('is_fixed_asset')} className="w-4 h-4" />
|
|
<label htmlFor="is_fixed_asset" className="text-sm font-medium text-gray-700 dark:text-gray-300">{t('items.isFixedAsset')}</label>
|
|
</div>
|
|
{isNewItem && formData.is_stock_item === 1 && (
|
|
<div>
|
|
<label className={labelClassName}>{t('items.openingStock')}</label>
|
|
<input type="number" value={formData.opening_stock} onChange={(e) => setFormData({ ...formData, opening_stock: parseFloat(e.target.value) || 0 })} disabled={isFieldDisabled('opening_stock')} className={inputClassName} />
|
|
</div>
|
|
)}
|
|
{formData.is_stock_item === 1 && (
|
|
<div>
|
|
<label className={labelClassName}>{t('items.valuationRate')}</label>
|
|
<input type="number" step="0.01" value={formData.valuation_rate} onChange={(e) => setFormData({ ...formData, valuation_rate: parseFloat(e.target.value) || 0 })} disabled={isFieldDisabled('valuation_rate')} className={inputClassName} />
|
|
</div>
|
|
)}
|
|
{!isNewItem && formData.is_stock_item === 1 && (
|
|
<div>
|
|
<label className={labelClassName}>{t('items.balanceQty')}</label>
|
|
<div className="flex items-center gap-2">
|
|
<input type="number" value={balanceQty} readOnly className={`${inputClassName} bg-gray-100 dark:bg-gray-800 cursor-not-allowed`} />
|
|
<button type="button" onClick={() => formData.item_code && fetchBalanceQty(formData.item_code)} disabled={balanceQtyLoading} className="p-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 disabled:opacity-50" title={t('items.refreshBalanceQty')}>
|
|
<FaSync className={balanceQtyLoading ? 'animate-spin' : ''} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Calibration - when Item Group is Tools */}
|
|
{showCalibrationInfo && (
|
|
<>
|
|
<h2 className={`${sectionHeaderClassName} mt-6`}>{t('items.calibrationInformation')}</h2>
|
|
<div className="space-y-4 mt-4">
|
|
<div>
|
|
<label className={labelClassName}>{t('items.lastCalibrationDate')}</label>
|
|
<input type="date" value={formData.custom_last_calibration_date} onChange={(e) => setFormData({ ...formData, custom_last_calibration_date: e.target.value })} disabled={isFieldDisabled('custom_last_calibration_date')} className={inputClassName} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClassName}>{t('items.nextCalibrationDate')}</label>
|
|
<input type="date" value={formData.custom_next_due_calibration_date} onChange={(e) => setFormData({ ...formData, custom_next_due_calibration_date: e.target.value })} disabled={isFieldDisabled('custom_next_due_calibration_date')} className={inputClassName} />
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<h2 className={`${sectionHeaderClassName} mt-6`}>{t('items.additionalInformation')}</h2>
|
|
<div className="space-y-4 mt-4">
|
|
<div>
|
|
<label className={labelClassName}>{t('commonFields.description')}</label>
|
|
<textarea value={formData.description} onChange={(e) => setFormData({ ...formData, description: e.target.value })} disabled={isFieldDisabled('description')} rows={3} className={inputClassName} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClassName}>{t('items.warrantyMonths')}</label>
|
|
<input type="text" value={formData.custom_warranty_in_months} onChange={(e) => setFormData({ ...formData, custom_warranty_in_months: e.target.value })} disabled={isFieldDisabled('custom_warranty_in_months')} className={inputClassName} />
|
|
</div>
|
|
</div>
|
|
</div> {/* ← closes grid */}
|
|
|
|
{/* Comments Section */}
|
|
{!isNewItem && (
|
|
<div className="mt-6">
|
|
<CommentSection
|
|
referenceDoctype="Item"
|
|
referenceName={itemName || null}
|
|
title="Comments & Discussion"
|
|
pollInterval={30000}
|
|
initialLimit={5}
|
|
collapsible={true}
|
|
startCollapsed={false}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Activity Log */}
|
|
{!isNewItem && !isDuplicating && (
|
|
<div className="mt-6">
|
|
<ActivityLog
|
|
doctype="Item"
|
|
docname={itemName || null}
|
|
creationDate={item?.creation}
|
|
createdBy={item?.owner}
|
|
compact={false}
|
|
initialVisible={5}
|
|
collapsible={true}
|
|
startCollapsed={true}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Delete Request */}
|
|
{!isNewItem && rolesLoaded && (
|
|
<div className="mt-6 max-w-sm">
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
|
<h2 className="text-base font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
|
Delete Request
|
|
</h2>
|
|
<DeleteRequestButton
|
|
doctype="Item"
|
|
docname={itemName}
|
|
currentDeleteStatus={(item?.custom_delete_status ?? null) as DeleteStatus}
|
|
userRoles={userRoles}
|
|
isSystemManager={isSystemManager}
|
|
redirectOnDelete="/inventory"
|
|
onStatusChange={() => refetchItem()}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div> {/* ← closes min-h-screen wrapper */}
|
|
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ItemDetail; |