1405 lines
64 KiB
TypeScript
1405 lines
64 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { useAssetDetails, useAssetMutations } from '../hooks/useAsset';
|
|
import { useDocTypeMeta } from '../hooks/useDocTypeMeta';
|
|
import { FaArrowLeft, FaSave, FaEdit, FaQrcode, FaCheck } from 'react-icons/fa';
|
|
import type { CreateAssetData,AssetFinanceBookRow } from '../services/assetService';
|
|
|
|
import LinkField from '../components/LinkField';
|
|
import apiService from '../services/apiService';
|
|
|
|
const AssetDetail: React.FC = () => {
|
|
const { assetName } = useParams<{ assetName: string }>();
|
|
const navigate = useNavigate();
|
|
const [searchParams] = useSearchParams();
|
|
const duplicateFromAsset = searchParams.get('duplicate');
|
|
|
|
const isNewAsset = assetName === 'new';
|
|
const isDuplicating = isNewAsset && !!duplicateFromAsset;
|
|
|
|
// Fetch DocType metadata to check allow_on_submit fields
|
|
const { isAllowedOnSubmit } = useDocTypeMeta('Asset');
|
|
|
|
const { asset, loading, error, refetch: refetchAsset } = useAssetDetails(
|
|
isDuplicating ? duplicateFromAsset : (isNewAsset ? null : assetName || null)
|
|
);
|
|
const { createAsset, updateAsset, submitAsset, loading: saving } = useAssetMutations();
|
|
|
|
const [isEditing, setIsEditing] = useState(isNewAsset);
|
|
|
|
// Check document status (matching Frappe behavior)
|
|
const docstatus = asset?.docstatus ?? 0; // Default to 0 (Draft) if not set
|
|
const isSubmitted = docstatus === 1;
|
|
const isCancelled = docstatus === 2;
|
|
const isDraft = docstatus === 0;
|
|
|
|
// Debug logging (development only)
|
|
useEffect(() => {
|
|
if (import.meta.env.DEV && asset) {
|
|
console.log(`[AssetDetail] Document Status:`, {
|
|
docstatus,
|
|
isDraft,
|
|
isSubmitted,
|
|
isCancelled,
|
|
isEditing
|
|
});
|
|
}
|
|
}, [asset, docstatus, isDraft, isSubmitted, isCancelled, isEditing]);
|
|
|
|
// Helper function to determine if a field should be disabled
|
|
// This matches Frappe's behavior:
|
|
// - Draft (0): All fields editable when in edit mode
|
|
// - Submitted (1): Only fields with allow_on_submit can be edited
|
|
// - Cancelled (2): No fields can be edited
|
|
const isFieldDisabled = (fieldname: string): boolean => {
|
|
// Always disabled when not in edit mode
|
|
if (!isEditing) return true;
|
|
|
|
// Cancelled documents cannot be edited at all
|
|
if (isCancelled) return true;
|
|
|
|
// Submitted documents: only allow editing fields marked as allow_on_submit
|
|
if (isSubmitted) {
|
|
return !isAllowedOnSubmit(fieldname);
|
|
}
|
|
|
|
// Draft documents: all fields are editable when in edit mode
|
|
if (isDraft) {
|
|
return false;
|
|
}
|
|
|
|
// Default: disable if status is unknown
|
|
return true;
|
|
};
|
|
|
|
const [userSiteName, setUserSiteName] = useState('');
|
|
const [departmentFilters, setDepartmentFilters] = useState<Record<string, any>>({});
|
|
|
|
const [formData, setFormData] = useState<CreateAssetData>({
|
|
asset_name: '',
|
|
company: '',
|
|
custom_serial_number: '',
|
|
location: '',
|
|
custom_manufacturer: '',
|
|
department: '',
|
|
custom_asset_type: '',
|
|
custom_manufacturing_year: '',
|
|
custom_model: '',
|
|
custom_class: '',
|
|
custom_device_status: '',
|
|
custom_down_time: 0,
|
|
asset_owner_company: '',
|
|
custom_up_time: 0,
|
|
custom_modality: '',
|
|
custom_attach_image: '',
|
|
custom_site_contractor: '',
|
|
custom_total_amount: 0,
|
|
calculate_depreciation: false,
|
|
available_for_use_date: isNewAsset ? new Date().toISOString().split('T')[0] : undefined,
|
|
finance_books:[]
|
|
});
|
|
|
|
|
|
// Load user details on mount
|
|
useEffect(() => {
|
|
async function loadUserDetails() {
|
|
try {
|
|
const user = await apiService.getUserDetails();
|
|
setUserSiteName(user.custom_site_name || '');
|
|
} catch (err) {
|
|
console.error('Error loading user details', err);
|
|
}
|
|
}
|
|
|
|
loadUserDetails();
|
|
}, []);
|
|
|
|
const addFinanceRow = () => {
|
|
// Get today's date in YYYY-MM-DD format for date input
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
const newRow: AssetFinanceBookRow = {
|
|
finance_book: 'Depreciation Entries',
|
|
depreciation_method: 'Straight Line',
|
|
total_number_of_depreciations: 10,
|
|
frequency_of_depreciation: 12,
|
|
depreciation_start_date: today
|
|
};
|
|
|
|
setFormData(prev => ({
|
|
...prev,
|
|
finance_books: [
|
|
...(prev.finance_books || []),
|
|
newRow
|
|
]
|
|
}));
|
|
};
|
|
|
|
const removeFinanceRow = (index: number) => {
|
|
setFormData(prev => {
|
|
const rows = [...(prev.finance_books || [])];
|
|
rows.splice(index, 1);
|
|
return { ...prev, finance_books: rows };
|
|
});
|
|
};
|
|
|
|
const updateFinanceRow = (index: number, patch: Partial<AssetFinanceBookRow>) => {
|
|
setFormData(prev => {
|
|
const rows = [...(prev.finance_books || [])];
|
|
rows[index] = { ...(rows[index] || {}), ...patch };
|
|
return { ...prev, finance_books: rows };
|
|
});
|
|
};
|
|
|
|
// Update department filters when company or userSiteName changes
|
|
useEffect(() => {
|
|
const filters: Record<string, any> = {};
|
|
|
|
if (formData.company) {
|
|
filters['company'] = formData.company;
|
|
}
|
|
|
|
const isMobileSite =
|
|
(userSiteName && userSiteName.startsWith('Mobile')) ||
|
|
(formData.company && formData.company.startsWith('Mobile'));
|
|
|
|
if (isMobileSite) {
|
|
filters['department_name'] = ['not like', 'Non Bio%'];
|
|
} else if (userSiteName || formData.company) {
|
|
filters['department_name'] = ['not like', 'Bio%'];
|
|
}
|
|
|
|
setDepartmentFilters(filters);
|
|
}, [formData.company, userSiteName]);
|
|
|
|
// Load asset data for editing or duplicating
|
|
useEffect(() => {
|
|
if (asset) {
|
|
setFormData({
|
|
asset_name: isDuplicating ? `${asset.asset_name} (Copy)` : (asset.asset_name || ''),
|
|
company: asset.company || '',
|
|
custom_serial_number: isDuplicating ? '' : (asset.custom_serial_number || ''),
|
|
location: asset.location || '',
|
|
custom_manufacturer: asset.custom_manufacturer || '',
|
|
department: asset.department || '',
|
|
custom_asset_type: asset.custom_asset_type || '',
|
|
custom_manufacturing_year: asset.custom_manufacturing_year || '',
|
|
custom_model: asset.custom_model || '',
|
|
custom_class: asset.custom_class || '',
|
|
custom_device_status: asset.custom_device_status || '',
|
|
custom_down_time: asset.custom_down_time || 0,
|
|
asset_owner_company: asset.asset_owner_company || '',
|
|
custom_up_time: asset.custom_up_time || 0,
|
|
custom_modality: asset.custom_modality || '',
|
|
custom_attach_image: asset.custom_attach_image || '',
|
|
custom_site_contractor: asset.custom_site_contractor || '',
|
|
custom_total_amount: asset.custom_total_amount || 0,
|
|
gross_purchase_amount: asset.gross_purchase_amount || 0,
|
|
available_for_use_date: asset.available_for_use_date || '',
|
|
calculate_depreciation: asset.calculate_depreciation || false,
|
|
finance_books: asset.finance_books || []
|
|
});
|
|
}
|
|
}, [asset, isDuplicating]);
|
|
|
|
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!assetName || assetName === "new") return;
|
|
|
|
const fetchQRCode = async () => {
|
|
try {
|
|
const directUrl = `/files/${assetName}-qr.png`;
|
|
|
|
const response = await fetch(directUrl, { method: "HEAD" });
|
|
|
|
if (response.ok) {
|
|
setQrCodeUrl(directUrl);
|
|
return;
|
|
}
|
|
|
|
const fileRes = await apiService.apiCall<any>(
|
|
`/api/resource/File?filters=[["File","attached_to_name","=","${assetName}"]]`
|
|
);
|
|
|
|
if (fileRes?.data?.length > 0) {
|
|
setQrCodeUrl(fileRes.data[0].file_url);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading QR code:", error);
|
|
}
|
|
};
|
|
|
|
fetchQRCode();
|
|
}, [assetName, asset]);
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
|
const { name, value } = e.target;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[name]: value
|
|
}));
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!formData.asset_name) {
|
|
alert('Please enter an Asset Name');
|
|
return;
|
|
}
|
|
|
|
if (!formData.custom_asset_type) {
|
|
alert('Please select a Category');
|
|
return;
|
|
}
|
|
|
|
console.log('Submitting asset data:', formData);
|
|
|
|
try {
|
|
if (isNewAsset || isDuplicating) {
|
|
const newAsset = await createAsset(formData);
|
|
if (newAsset.name) {
|
|
const qrUrl = `/files/${newAsset.name}-qr.png`;
|
|
setQrCodeUrl(qrUrl);
|
|
}
|
|
const successMessage = isDuplicating
|
|
? 'Asset duplicated successfully!'
|
|
: 'Asset created successfully!';
|
|
alert(successMessage);
|
|
navigate(`/assets/${newAsset.name}`);
|
|
} else if (assetName) {
|
|
await updateAsset(assetName, formData);
|
|
alert('Asset updated successfully!');
|
|
setIsEditing(false);
|
|
// Refresh asset data to get updated docstatus
|
|
refetchAsset();
|
|
}
|
|
} catch (err) {
|
|
console.error('Asset save error:', err);
|
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
|
|
if (errorMessage.includes('404') || errorMessage.includes('not found') ||
|
|
errorMessage.includes('has no attribute') || errorMessage.includes('417')) {
|
|
alert(
|
|
'⚠️ Asset API Not Deployed\n\n' +
|
|
'The Asset API endpoint (asset_api.py) is not deployed on your Frappe server yet.\n\n' +
|
|
'To fix this:\n' +
|
|
'1. SSH into your Frappe server\n' +
|
|
'2. Navigate to: frappe-bench/apps/asset_lite/asset_lite/api/\n' +
|
|
'3. Create the file: asset_api.py\n' +
|
|
'4. Copy the content from frappe_asset_api.py in this project\n' +
|
|
'5. Restart Frappe: bench restart\n\n' +
|
|
'Error: ' + errorMessage
|
|
);
|
|
} else {
|
|
alert('Failed to save asset:\n\n' + errorMessage);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleSubmitDocument = async () => {
|
|
if (!assetName || isNewAsset) {
|
|
alert('Cannot submit: Asset not saved yet');
|
|
return;
|
|
}
|
|
|
|
if (!window.confirm('Are you sure you want to submit this asset? Once submitted, only fields marked as "Allow on Submit" can be edited.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await submitAsset(assetName);
|
|
alert('Asset submitted successfully!');
|
|
// Refresh asset data to get updated docstatus
|
|
refetchAsset();
|
|
setIsEditing(false);
|
|
} catch (err) {
|
|
console.error('Asset submit error:', err);
|
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
alert('Failed to submit asset:\n\n' + errorMessage);
|
|
}
|
|
};
|
|
|
|
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">Loading asset details...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error && !isNewAsset && !isDuplicating) {
|
|
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-4">
|
|
<p className="text-red-600 dark:text-red-400">Error: {error}</p>
|
|
<button
|
|
onClick={() => navigate('/assets')}
|
|
className="mt-2 text-red-700 dark:text-red-400 underline hover:text-red-800 dark:hover:text-red-300"
|
|
>
|
|
Back to assets list
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error && isDuplicating) {
|
|
return (
|
|
<div className="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
|
<h3 className="text-lg font-semibold text-yellow-800 dark:text-yellow-300 mb-2">
|
|
Source Asset Not Found
|
|
</h3>
|
|
<p className="text-yellow-700 dark:text-yellow-400">
|
|
The asset you're trying to duplicate could not be found.
|
|
</p>
|
|
<div className="mt-4 flex gap-3">
|
|
<button
|
|
onClick={() => navigate('/assets/new')}
|
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
|
>
|
|
Create New Asset
|
|
</button>
|
|
<button
|
|
onClick={() => navigate('/assets')}
|
|
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors"
|
|
>
|
|
Back to Assets List
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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('/assets')}
|
|
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">
|
|
{isDuplicating ? 'Duplicate Asset' : (isNewAsset ? 'New Asset Details' : 'Asset Details')}
|
|
</span>
|
|
</button>
|
|
|
|
{/* Document Status Badge */}
|
|
{asset && (
|
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
|
|
docstatus === 0 ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300' :
|
|
docstatus === 1 ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' :
|
|
'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'
|
|
}`}>
|
|
{docstatus === 0 ? 'Draft' : docstatus === 1 ? 'Submitted' : 'Cancelled'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{!isNewAsset && !isEditing && !isCancelled && (
|
|
<>
|
|
<button
|
|
onClick={() => setIsEditing(true)}
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2"
|
|
>
|
|
<FaEdit />
|
|
Edit
|
|
</button>
|
|
{/* Submit button - only show for Draft documents */}
|
|
{isDraft && (
|
|
<button
|
|
onClick={handleSubmitDocument}
|
|
disabled={saving}
|
|
className="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 disabled:opacity-50"
|
|
>
|
|
<FaCheck />
|
|
{saving ? 'Submitting...' : 'Submit'}
|
|
</button>
|
|
)}
|
|
</>
|
|
)}
|
|
{isCancelled && (
|
|
<span className="text-sm text-gray-500 dark:text-gray-400 italic">
|
|
Cancelled documents cannot be edited
|
|
</span>
|
|
)}
|
|
{isEditing && (
|
|
<>
|
|
<button
|
|
onClick={() => {
|
|
if (isNewAsset) {
|
|
navigate('/assets');
|
|
} else {
|
|
setIsEditing(false);
|
|
}
|
|
}}
|
|
className="bg-gray-300 hover:bg-gray-400 text-gray-700 dark:text-gray-800 px-6 py-2 rounded-lg"
|
|
disabled={saving}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={saving}
|
|
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 disabled:opacity-50"
|
|
>
|
|
<FaSave />
|
|
{saving ? 'Saving...' : 'Save Changes'}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
{/* 4-Column Grid Layout */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
|
|
{/* COLUMN 1: Asset Information */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<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">
|
|
Asset Information
|
|
</h2>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Asset Name <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="asset_name"
|
|
value={formData.asset_name}
|
|
onChange={handleChange}
|
|
placeholder="e.g. Laptop Model X"
|
|
required
|
|
disabled={isFieldDisabled('asset_name')}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<LinkField
|
|
label="Category"
|
|
doctype="Asset Type"
|
|
value={formData.custom_asset_type || ''}
|
|
onChange={(val) => setFormData({ ...formData, custom_asset_type: val })}
|
|
disabled={isFieldDisabled('custom_asset_type')}
|
|
/>
|
|
|
|
<LinkField
|
|
label="Modality"
|
|
doctype="Modality"
|
|
value={formData.custom_modality || ''}
|
|
onChange={(val) => setFormData({ ...formData, custom_modality: val })}
|
|
disabled={isFieldDisabled('custom_modality')}
|
|
/>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Class
|
|
</label>
|
|
<select
|
|
name="custom_class"
|
|
value={formData.custom_class}
|
|
onChange={handleChange}
|
|
disabled={isFieldDisabled('custom_class')}
|
|
className="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"
|
|
>
|
|
<option value="">Select class</option>
|
|
<option value="Class A">Class A</option>
|
|
<option value="Class B">Class B</option>
|
|
<option value="Class C">Class C</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Asset ID
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={isNewAsset || isDuplicating ? 'Auto-generated' : asset?.name}
|
|
disabled
|
|
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400"
|
|
/>
|
|
{isDuplicating && (
|
|
<p className="mt-1 text-xs text-blue-600 dark:text-blue-400">
|
|
💡 Duplicating from: {duplicateFromAsset}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Device Status <span className="text-red-500">*</span>
|
|
</label>
|
|
<select
|
|
name="custom_device_status"
|
|
value={formData.custom_device_status}
|
|
onChange={handleChange}
|
|
disabled={!isEditing}
|
|
className="w-full px-3 py-2 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"
|
|
>
|
|
{/* <option value="">Select class</option> */}
|
|
<option value="Up">Up</option>
|
|
<option value="Down">Down</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* COLUMN 2: Technical Specs */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<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">
|
|
Technical Specs
|
|
</h2>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Serial Code
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="custom_serial_number"
|
|
value={formData.custom_serial_number}
|
|
onChange={handleChange}
|
|
placeholder="e.g. SN-12345"
|
|
disabled={isFieldDisabled('custom_serial_number')}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
System ID
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. SYS-755"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Serial No.2
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. SR-V021-A"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<LinkField
|
|
label="Manufacturer"
|
|
doctype="Manufacturer"
|
|
value={formData.custom_manufacturer || ''}
|
|
onChange={(val) => setFormData({ ...formData, custom_manufacturer: val })}
|
|
disabled={isFieldDisabled('custom_manufacturer')}
|
|
/>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Model
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="custom_model"
|
|
value={formData.custom_model}
|
|
onChange={handleChange}
|
|
placeholder="Model number"
|
|
disabled={isFieldDisabled('custom_model')}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Model Number
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Model number"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* COLUMN 3: Location */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<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">
|
|
Location
|
|
</h2>
|
|
<div className="space-y-4">
|
|
<LinkField
|
|
label="Hospital"
|
|
doctype="Company"
|
|
value={formData.company || ''}
|
|
onChange={(val) => {
|
|
setFormData({ ...formData, company: val, department: '' });
|
|
}}
|
|
disabled={isFieldDisabled('company')}
|
|
filters={{ domain: 'Healthcare' }}
|
|
/>
|
|
|
|
<LinkField
|
|
label="Department"
|
|
doctype="Department"
|
|
value={formData.department || ''}
|
|
onChange={(val) => setFormData({ ...formData, department: val })}
|
|
disabled={isFieldDisabled('department')}
|
|
filters={departmentFilters}
|
|
/>
|
|
|
|
<LinkField
|
|
label="Location"
|
|
doctype="Location"
|
|
value={formData.location || ''}
|
|
onChange={(val) => setFormData({ ...formData, location: val })}
|
|
disabled={isFieldDisabled('location')}
|
|
/>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Building
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Building name"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Area/Unit
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Area or unit"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Room Number
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Room 001-002"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Assigned To
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Person or department"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* COLUMN 4: More Details */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<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">
|
|
More Details
|
|
</h2>
|
|
<div className="space-y-4">
|
|
{/* <div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Condition
|
|
</label>
|
|
<select
|
|
name="custom_device_status"
|
|
value={formData.custom_device_status}
|
|
onChange={handleChange}
|
|
disabled={isFieldDisabled('custom_device_status')}
|
|
className="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"
|
|
>
|
|
<option value="">Select status</option>
|
|
<option value="Operational">Operational</option>
|
|
<option value="Under Maintenance">Under Maintenance</option>
|
|
<option value="Decommissioned">Decommissioned</option>
|
|
</select>
|
|
</div> */}
|
|
|
|
{/* QR Code */}
|
|
<div className="flex flex-col items-center my-4">
|
|
<div className="border-2 border-gray-300 dark:border-gray-600 p-3 rounded-lg bg-white dark:bg-gray-700">
|
|
{qrCodeUrl ? (
|
|
<>
|
|
<img
|
|
src={qrCodeUrl}
|
|
alt={`QR Code for ${asset?.name || 'Asset'}`}
|
|
className="w-[120px] h-[120px] object-contain"
|
|
onError={(e) => {
|
|
const target = e.target as HTMLImageElement;
|
|
target.style.display = 'none';
|
|
const fallback = target.nextElementSibling as HTMLElement;
|
|
if (fallback) {
|
|
fallback.style.display = 'flex';
|
|
}
|
|
}}
|
|
/>
|
|
<div className="w-[120px] h-[120px] hidden items-center justify-center bg-gray-100 dark:bg-gray-700 rounded">
|
|
<FaQrcode size={80} className="text-gray-400 dark:text-gray-500" />
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="w-[120px] h-[120px] flex items-center justify-center bg-gray-100 dark:bg-gray-700 rounded">
|
|
<FaQrcode size={80} className="text-gray-400 dark:text-gray-500" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
{asset?.name && (
|
|
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400 text-center">
|
|
Asset ID: {asset.name}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
rows={4}
|
|
placeholder="Brief description of the asset"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bottom Sections - 2 Columns */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
|
{/* Coverage */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<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">
|
|
Coverage
|
|
</h2>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Site Contractor
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="custom_site_contractor"
|
|
value={formData.custom_site_contractor}
|
|
onChange={handleChange}
|
|
disabled={isFieldDisabled('custom_site_contractor')}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Contract Number
|
|
</label>
|
|
<input
|
|
type="text"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Service Agreement
|
|
</label>
|
|
<select
|
|
name="custom_service_agreement"
|
|
value={formData.custom_service_agreement || ''}
|
|
onChange={handleChange}
|
|
disabled={isFieldDisabled('custom_service_agreement')}
|
|
className="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"
|
|
>
|
|
<option value="">Select Service Agreement</option>
|
|
<option value="Warranty">Warranty</option>
|
|
<option value="Contract">Contract</option>
|
|
<option value="Frame Work">Frame Work</option>
|
|
<option value="Out of warranty">Out of warranty</option>
|
|
<option value="Under Dismantle">Under Dismantle</option>
|
|
<option value="Under Installation">Under Installation</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Service Coverage
|
|
</label>
|
|
<select
|
|
name="custom_service_coverage"
|
|
value={formData.custom_service_coverage || ''}
|
|
onChange={handleChange}
|
|
disabled={isFieldDisabled('custom_service_coverage')}
|
|
className="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"
|
|
>
|
|
<option value="">Select Service Coverage</option>
|
|
<option value="PM Only">PM Only</option>
|
|
<option value="Labour">Labour</option>
|
|
<option value="Labour & Parts">Labour & Parts</option>
|
|
<option value="Comprehensive">Comprehensive</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Start Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
End Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Total Amount
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="custom_total_amount"
|
|
value={formData.custom_total_amount}
|
|
onChange={handleChange}
|
|
disabled={isFieldDisabled('custom_total_amount')}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Comments
|
|
</label>
|
|
<textarea
|
|
rows={2}
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Acquisition Details */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<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">
|
|
Acquisition Details
|
|
</h2>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Purchase Order Number
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="PO number"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<LinkField
|
|
label="Supplier/Vendor"
|
|
doctype="Supplier"
|
|
value={formData.supplier || ''}
|
|
onChange={(val) => setFormData({ ...formData, supplier: val })}
|
|
disabled={isFieldDisabled('supplier')}
|
|
/>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Gross Purchase Amount
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="gross_purchase_amount"
|
|
value={formData.gross_purchase_amount || 0}
|
|
onChange={handleChange}
|
|
disabled={isFieldDisabled('gross_purchase_amount')}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Purchase Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Installation Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Available For Use Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
name="available_for_use_date"
|
|
value={formData.available_for_use_date || ''}
|
|
onChange={(e) =>
|
|
setFormData((prev) => ({ ...prev, available_for_use_date: e.target.value }))
|
|
}
|
|
disabled={isFieldDisabled('available_for_use_date')}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* End-of-Life Details */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mt-6">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<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">
|
|
End-of-Life Details
|
|
</h2>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Expected End-of-Life Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Disposal Method
|
|
</label>
|
|
<select
|
|
disabled={!isEditing}
|
|
className="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"
|
|
>
|
|
<option value="">Recycling</option>
|
|
<option value="Donation">Donation</option>
|
|
<option value="Sale">Sale</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
{/* Updated Financial Details */}
|
|
<div className="mt-6">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">Financial Details</h2>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
The depreciation method is an accounting method used to allocate the cost of a tangible asset over its useful life.
|
|
</p>
|
|
|
|
<div className="flex items-center mb-6">
|
|
<input
|
|
id="calculate_depreciation"
|
|
type="checkbox"
|
|
checked={formData.calculate_depreciation}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
calculate_depreciation: e.target.checked,
|
|
})
|
|
}
|
|
disabled={!isEditing}
|
|
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
|
/>
|
|
<label
|
|
htmlFor="calculate_depreciation"
|
|
className="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
>
|
|
Calculate Depreciation
|
|
</label>
|
|
</div>
|
|
|
|
{/* Asset Finance Book child table — shown only when checkbox checked */}
|
|
{formData.calculate_depreciation && (
|
|
<div className="border-t pt-4">
|
|
{/* Header with Add Row button */}
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-md font-semibold text-gray-800 dark:text-white">
|
|
Asset Finance Books
|
|
</h3>
|
|
{isEditing && (
|
|
<button
|
|
type="button"
|
|
onClick={addFinanceRow}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center gap-2"
|
|
>
|
|
<span>+</span> Add Row
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Show message if no rows */}
|
|
{(!formData.finance_books || formData.finance_books.length === 0) && (
|
|
<div className="text-center py-8 text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
|
No finance books added yet. Click "Add Row" to add one.
|
|
</div>
|
|
)}
|
|
|
|
{/* TABLE - Full width desktop view with overflow fix */}
|
|
{formData.finance_books && formData.finance_books.length > 0 && (
|
|
<div className="overflow-visible">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full border-collapse">
|
|
<thead>
|
|
<tr className="bg-gray-100 dark:bg-gray-700 border-b border-gray-300 dark:border-gray-600">
|
|
<th className="text-left px-4 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300 min-w-[200px]">
|
|
Finance Book
|
|
</th>
|
|
<th className="text-left px-4 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300 min-w-[200px]">
|
|
Depreciation Method*
|
|
</th>
|
|
<th className="text-left px-4 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300 min-w-[180px]">
|
|
Total Depreciations*
|
|
</th>
|
|
<th className="text-left px-4 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300 min-w-[180px]">
|
|
Frequency (Months)*
|
|
</th>
|
|
<th className="text-left px-4 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300 min-w-[200px]">
|
|
Depreciation Posting Date*
|
|
</th>
|
|
{isEditing && (
|
|
<th className="text-center px-4 py-3 text-sm font-semibold text-gray-700 dark:text-gray-300 min-w-[120px]">
|
|
Action
|
|
</th>
|
|
)}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{formData.finance_books.map((row: AssetFinanceBookRow, idx: number) => (
|
|
<tr
|
|
key={idx}
|
|
className="border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-750"
|
|
>
|
|
{/* Finance Book - with overflow visible */}
|
|
<td className="px-4 py-3 relative" style={{ overflow: 'visible' }}>
|
|
<div className="relative z-20">
|
|
<LinkField
|
|
label=""
|
|
doctype="Finance Book"
|
|
value={row.finance_book || ''}
|
|
onChange={(val) => updateFinanceRow(idx, { finance_book: val })}
|
|
disabled={!isEditing}
|
|
/>
|
|
</div>
|
|
</td>
|
|
|
|
{/* Depreciation Method */}
|
|
<td className="px-4 py-3">
|
|
<select
|
|
value={row.depreciation_method || ''}
|
|
onChange={(e) => updateFinanceRow(idx, { depreciation_method: e.target.value })}
|
|
disabled={!isEditing}
|
|
className="w-full px-3 py-2 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"
|
|
>
|
|
<option value="">Select Method</option>
|
|
<option value="Straight Line">Straight Line</option>
|
|
<option value="Double Declining Balance">Double Declining Balance</option>
|
|
<option value="Written Down Value">Written Down Value</option>
|
|
<option value="Manual">Manual</option>
|
|
</select>
|
|
</td>
|
|
|
|
{/* Total Depreciations */}
|
|
<td className="px-4 py-3">
|
|
<input
|
|
type="number"
|
|
value={row.total_number_of_depreciations ?? ''}
|
|
onChange={(e) =>
|
|
updateFinanceRow(idx, {
|
|
total_number_of_depreciations: Number(e.target.value),
|
|
})
|
|
}
|
|
disabled={!isEditing}
|
|
placeholder="0"
|
|
className="w-full px-3 py-2 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"
|
|
/>
|
|
</td>
|
|
|
|
{/* Frequency */}
|
|
<td className="px-4 py-3">
|
|
<input
|
|
type="number"
|
|
value={row.frequency_of_depreciation ?? ''}
|
|
onChange={(e) =>
|
|
updateFinanceRow(idx, {
|
|
frequency_of_depreciation: Number(e.target.value),
|
|
})
|
|
}
|
|
disabled={!isEditing}
|
|
placeholder="0"
|
|
className="w-full px-3 py-2 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"
|
|
/>
|
|
</td>
|
|
|
|
{/* Start Date */}
|
|
<td className="px-4 py-3">
|
|
<input
|
|
type="date"
|
|
value={row.depreciation_start_date || ''}
|
|
onChange={(e) =>
|
|
updateFinanceRow(idx, { depreciation_start_date: e.target.value })
|
|
}
|
|
disabled={!isEditing}
|
|
className="w-full px-3 py-2 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"
|
|
/>
|
|
</td>
|
|
|
|
{/* REMOVE BUTTON */}
|
|
{isEditing && (
|
|
<td className="px-4 py-3 text-center">
|
|
<button
|
|
type="button"
|
|
onClick={() => removeFinanceRow(idx)}
|
|
className="px-3 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
|
>
|
|
Remove
|
|
</button>
|
|
</td>
|
|
)}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Financial Details - Full Width */}
|
|
{/* <div className="mt-6">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<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">
|
|
Financial Details
|
|
</h2>
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Depreciation Method
|
|
</label>
|
|
<select
|
|
disabled={!isEditing}
|
|
className="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"
|
|
>
|
|
<option value="">Straight Line</option>
|
|
<option value="Double Declining Balance">Double Declining Balance</option>
|
|
<option value="Written Down Value">Written Down Value</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Depreciation Rate (%)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
placeholder="0.00"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Annual Rate
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
placeholder="0.00"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Current Value
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
placeholder="0.00"
|
|
disabled={!isEditing}
|
|
className="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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center mb-6">
|
|
<input
|
|
id="calculate_depreciation"
|
|
type="checkbox"
|
|
checked={formData.calculate_depreciation || false}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
calculate_depreciation: e.target.checked,
|
|
})
|
|
}
|
|
disabled={isFieldDisabled('calculate_depreciation')}
|
|
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
|
/>
|
|
<label
|
|
htmlFor="calculate_depreciation"
|
|
className="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
>
|
|
Calculate Depreciation
|
|
</label>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full border-collapse border border-gray-300 dark:border-gray-600">
|
|
<thead>
|
|
<tr className="bg-gray-100 dark:bg-gray-700">
|
|
<th className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300">
|
|
Period
|
|
</th>
|
|
<th className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300">
|
|
Opening Value
|
|
</th>
|
|
<th className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300">
|
|
Depreciation Amount
|
|
</th>
|
|
<th className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300">
|
|
Accumulated Depreciation
|
|
</th>
|
|
<th className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300">
|
|
Closing Value
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
|
|
<tr className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
Year 1
|
|
</td>
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
-
|
|
</td>
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
-
|
|
</td>
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
-
|
|
</td>
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
-
|
|
</td>
|
|
</tr>
|
|
<tr className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
Year 2
|
|
</td>
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
-
|
|
</td>
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
-
|
|
</td>
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
-
|
|
</td>
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
-
|
|
</td>
|
|
</tr>
|
|
<tr className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
Year 3
|
|
</td>
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
-
|
|
</td>
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
-
|
|
</td>
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
-
|
|
</td>
|
|
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-xs text-gray-700 dark:text-gray-300">
|
|
-
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
{!asset && (
|
|
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400 text-center">
|
|
Depreciation schedule will be calculated when asset is saved
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div> */}
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AssetDetail;
|