From c11db48dbfd5d6b2306d030c3f910dacad927edc Mon Sep 17 00:00:00 2001 From: Duradundi Hadimani Date: Fri, 14 Nov 2025 15:31:18 +0530 Subject: [PATCH] Lated Updated for QR code --- src/components/LinkField.tsx | 215 ++++++++++++++++++++++++----------- src/pages/AssetDetail.tsx | 206 ++++++++++++++++++++++++++++++--- src/services/apiService.ts | 1 + src/services/assetService.ts | 5 + 4 files changed, 348 insertions(+), 79 deletions(-) diff --git a/src/components/LinkField.tsx b/src/components/LinkField.tsx index 1b1836f..c5a11c9 100644 --- a/src/components/LinkField.tsx +++ b/src/components/LinkField.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; -import apiService from '../services/apiService'; // ✅ your ApiService +import apiService from '../services/apiService'; interface LinkFieldProps { label: string; @@ -8,6 +8,7 @@ interface LinkFieldProps { onChange: (value: string) => void; placeholder?: string; disabled?: boolean; + filters?: Record; } const LinkField: React.FC = ({ @@ -17,29 +18,43 @@ const LinkField: React.FC = ({ onChange, placeholder, disabled = false, + filters = {}, }) => { const [searchResults, setSearchResults] = useState<{ value: string; description?: string }[]>([]); const [searchText, setSearchText] = useState(''); const [isDropdownOpen, setDropdownOpen] = useState(false); const containerRef = useRef(null); - // Fetch link options from ERPNext + // Fetch link options from ERPNext with filters const searchLink = async (text: string = '') => { try { - const params = new URLSearchParams({ doctype, txt: text }); + const params = new URLSearchParams({ + doctype, + txt: text, + }); + + // Add filters if provided + if (filters && Object.keys(filters).length > 0) { + // Convert filters to JSON string for Frappe API + params.append('filters', JSON.stringify(filters)); + } + const response = await apiService.apiCall<{ value: string; description?: string }[]>( `/api/method/frappe.desk.search.search_link?${params.toString()}` ); setSearchResults(response || []); } catch (error) { console.error(`Error fetching ${doctype} links:`, error); + setSearchResults([]); } }; - // Fetch default options when dropdown opens + // Fetch default options when dropdown opens or filters change useEffect(() => { - if (isDropdownOpen) searchLink(''); - }, [isDropdownOpen]); + if (isDropdownOpen) { + searchLink(searchText || ''); + } + }, [isDropdownOpen, filters]); // Re-fetch when filters change // Close dropdown when clicking outside useEffect(() => { @@ -54,7 +69,9 @@ const LinkField: React.FC = ({ return (
- + = ({ placeholder={placeholder || `Select ${label}`} disabled={disabled} 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`} - onFocus={() => !disabled && setDropdownOpen(true)} - onChange={(e) => { - const text = e.target.value; - setSearchText(text); - searchLink(text); - onChange(text); - }} - /> - - {isDropdownOpen && searchResults.length > 0 && !disabled && ( -
    - {searchResults.map((item, idx) => ( -
  • { - onChange(item.value); - setDropdownOpen(false); - }} - className={`px-3 py-2 cursor-pointer - text-gray-900 dark:text-gray-100 - hover:bg-blue-500 dark:hover:bg-blue-600 - ${value === item.value ? 'bg-blue-50 dark:bg-blue-700 font-semibold' : ''}`} - > - {item.value} - {item.description && ( - {item.description} - )} -
  • - ))} -
- )} - - - {/* !disabled && setDropdownOpen(true)} onChange={(e) => { const text = e.target.value; @@ -110,17 +88,11 @@ const LinkField: React.FC = ({ searchLink(text); onChange(text); }} - className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 - disabled:bg-gray-100 dark:disabled:bg-gray-700 - border-gray-300 dark:border-gray-600 - bg-white dark:bg-gray-700 text-gray-900 dark:text-white`} /> - {isDropdownOpen && searchResults.length > 0 && ( -
    + {isDropdownOpen && searchResults.length > 0 && !disabled && ( +
      {searchResults.map((item, idx) => (
    • = ({ onChange(item.value); setDropdownOpen(false); }} - className={`px-3 py-2 hover:bg-blue-100 dark:hover:bg-gray-700 cursor-pointer - ${value === item.value ? 'bg-blue-50 dark:bg-gray-600 font-semibold' : ''}`} + className={`px-3 py-2 cursor-pointer + text-gray-900 dark:text-gray-100 + hover:bg-blue-500 dark:hover:bg-blue-600 hover:text-white + ${value === item.value ? 'bg-blue-50 dark:bg-blue-700 font-semibold' : ''}`} > -
      {item.value}
      + {item.value} {item.description && ( -
      {item.description}
      + + {item.description} + )}
    • ))}
    - )} */} - - + )} + {/* Show message when no results found */} + {isDropdownOpen && searchResults.length === 0 && !disabled && ( +
    + No results found +
    + )}
); }; export default LinkField; + + +// import React, { useState, useEffect, useRef } from 'react'; +// import apiService from '../services/apiService'; // ✅ your ApiService + +// interface LinkFieldProps { +// label: string; +// doctype: string; +// value: string; +// onChange: (value: string) => void; +// placeholder?: string; +// disabled?: boolean; +// filters?: Record +// } + +// const LinkField: React.FC = ({ +// label, +// doctype, +// value, +// onChange, +// placeholder, +// disabled = false, +// }) => { +// const [searchResults, setSearchResults] = useState<{ value: string; description?: string }[]>([]); +// const [searchText, setSearchText] = useState(''); +// const [isDropdownOpen, setDropdownOpen] = useState(false); +// const containerRef = useRef(null); + +// // Fetch link options from ERPNext +// const searchLink = async (text: string = '') => { +// try { +// const params = new URLSearchParams({ doctype, txt: text }); +// const response = await apiService.apiCall<{ value: string; description?: string }[]>( +// `/api/method/frappe.desk.search.search_link?${params.toString()}` +// ); +// setSearchResults(response || []); +// } catch (error) { +// console.error(`Error fetching ${doctype} links:`, error); +// } +// }; + +// // Fetch default options when dropdown opens +// useEffect(() => { +// if (isDropdownOpen) searchLink(''); +// }, [isDropdownOpen]); + +// // Close dropdown when clicking outside +// useEffect(() => { +// const handleClickOutside = (event: MouseEvent) => { +// if (containerRef.current && !containerRef.current.contains(event.target as Node)) { +// setDropdownOpen(false); +// } +// }; +// document.addEventListener('mousedown', handleClickOutside); +// return () => document.removeEventListener('mousedown', handleClickOutside); +// }, []); + +// return ( +//
+// + +// !disabled && setDropdownOpen(true)} +// onChange={(e) => { +// const text = e.target.value; +// setSearchText(text); +// searchLink(text); +// onChange(text); +// }} +// /> + +// {isDropdownOpen && searchResults.length > 0 && !disabled && ( +//
    +// {searchResults.map((item, idx) => ( +//
  • { +// onChange(item.value); +// setDropdownOpen(false); +// }} +// className={`px-3 py-2 cursor-pointer +// text-gray-900 dark:text-gray-100 +// hover:bg-blue-500 dark:hover:bg-blue-600 +// ${value === item.value ? 'bg-blue-50 dark:bg-blue-700 font-semibold' : ''}`} +// > +// {item.value} +// {item.description && ( +// {item.description} +// )} +//
  • +// ))} +//
+// )} +//
+// ); +// }; + +// export default LinkField; diff --git a/src/pages/AssetDetail.tsx b/src/pages/AssetDetail.tsx index c3064fc..1eb9f87 100644 --- a/src/pages/AssetDetail.tsx +++ b/src/pages/AssetDetail.tsx @@ -5,6 +5,7 @@ import { FaArrowLeft, FaSave, FaEdit, FaQrcode } from 'react-icons/fa'; import type { CreateAssetData } from '../services/assetService'; import LinkField from '../components/LinkField'; +import apiService from '../services/apiService'; // ✅ your ApiService const AssetDetail: React.FC = () => { @@ -21,8 +22,12 @@ const AssetDetail: React.FC = () => { isDuplicating ? duplicateFromAsset : (isNewAsset ? null : assetName || null) ); const { createAsset, updateAsset, loading: saving } = useAssetMutations(); - + const [isEditing, setIsEditing] = useState(isNewAsset); + + const [userSiteName, setUserSiteName] = useState(''); + const [departmentFilters, setDepartmentFilters] = useState>({}); + const [formData, setFormData] = useState({ asset_name: '', company: '', @@ -41,9 +46,53 @@ const AssetDetail: React.FC = () => { custom_modality: '', custom_attach_image: '', custom_site_contractor: '', - custom_total_amount: 0 + custom_total_amount: 0, + calculate_depreciation: false, + available_for_use_date: isNewAsset ? new Date().toISOString().split('T')[0] : undefined }); + // 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(); + }, []); + + // Update department filters when company or userSiteName changes + useEffect(() => { + const filters: Record = {}; + + // Base filter: company must match + if (formData.company) { + filters['company'] = formData.company; + } + + // Apply department name filters based on site name and company + const isMobileSite = + (userSiteName && userSiteName.startsWith('Mobile')) || + (formData.company && formData.company.startsWith('Mobile')); + + if (isMobileSite) { + // For Mobile sites, exclude Non Bio departments (show Bio departments) + // Frappe filter format: ['not like', 'pattern'] + filters['department_name'] = ['not like', 'Non Bio%']; + } else if (userSiteName || formData.company) { + // For non-Mobile sites, exclude Bio departments (show Non-Bio departments) + filters['department_name'] = ['not like', 'Bio%']; + } + + console.log('Department filters updated:', filters); // Debug log + + setDepartmentFilters(filters); + }, [formData.company, userSiteName]); + // Load asset data for editing or duplicating useEffect(() => { if (asset) { @@ -65,11 +114,52 @@ const AssetDetail: React.FC = () => { 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 + 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 }); } }, [asset, isDuplicating]); + const [qrCodeUrl, setQrCodeUrl] = useState(null); + + useEffect(() => { + if (!assetName || assetName === "new") return; + + const fetchQRCode = async () => { + try { + // Try fixed predictable URL + const directUrl = `/files/${assetName}-qr.png`; + console.log(directUrl) + + // Quickly test if file exists + const response = await fetch(directUrl, { method: "HEAD" }); + console.log(response) + + if (response.ok) { + setQrCodeUrl(directUrl); + return; + } + + // If not available, fallback to File doctype API + const fileRes = await apiService.apiCall( + `/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) => { const { name, value } = e.target; setFormData(prev => ({ @@ -98,6 +188,10 @@ const AssetDetail: React.FC = () => { 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!'; @@ -292,6 +386,7 @@ const AssetDetail: React.FC = () => { doctype="Asset Type" value={formData.custom_asset_type || ''} onChange={(val) => setFormData({ ...formData, custom_asset_type: val })} + disabled={!isEditing} /> @@ -319,6 +414,7 @@ const AssetDetail: React.FC = () => { doctype="Modality" value={formData.custom_modality || ''} onChange={(val) => setFormData({ ...formData, custom_modality: val })} + disabled={!isEditing} /> @@ -420,8 +516,9 @@ const AssetDetail: React.FC = () => { setFormData({ ...formData, manufacturer: val })} + value={formData.custom_manufacturer || ''} + onChange={(val) => setFormData({ ...formData, custom_manufacturer: val })} + disabled={!isEditing} />
@@ -478,7 +575,13 @@ const AssetDetail: React.FC = () => { label="Hospital" doctype="Company" value={formData.company || ''} - onChange={(val) => setFormData({ ...formData, company: val })} + onChange={(val) => { + setFormData({ ...formData, company: val, department: '' }); // Clear department when company changes + }} + disabled={!isEditing} + filters={{ domain: 'Healthcare' }} + // onChange={(val) => setFormData({ ...formData, company: val })} + // disabled={!isEditing} /> {/*
@@ -504,6 +607,16 @@ const AssetDetail: React.FC = () => { doctype="Department" value={formData.department || ''} onChange={(val) => setFormData({ ...formData, department: val })} + disabled={!isEditing} + filters={departmentFilters} + /> + + setFormData({ ...formData, location: val })} + disabled={!isEditing} />
@@ -604,10 +717,19 @@ const AssetDetail: React.FC = () => { Service Agreement
@@ -616,10 +738,17 @@ const AssetDetail: React.FC = () => { Service Coverage
@@ -688,7 +817,7 @@ const AssetDetail: React.FC = () => { />
-
+ {/*
@@ -698,18 +827,27 @@ const AssetDetail: React.FC = () => { > -
+
*/} + setFormData({ ...formData, supplier: val })} + disabled={!isEditing} + />
- + />
@@ -740,6 +878,11 @@ const AssetDetail: React.FC = () => { + setFormData((prev) => ({ ...prev, available_for_use_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" /> @@ -809,6 +952,29 @@ const AssetDetail: React.FC = () => { 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" />
+
+ + 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" + /> + +
+ @@ -868,12 +1034,22 @@ const AssetDetail: React.FC = () => { {/* QR Code */} +
- + {qrCodeUrl ? ( + QR Code + ) : ( + + )}
+