From 0a9f589c05fe4960fb7129986eeaf160293aee8a Mon Sep 17 00:00:00 2001 From: "Akhib.Shaik" Date: Thu, 20 Nov 2025 12:39:42 +0530 Subject: [PATCH] added the metadata and gird into dundu branch with submit button and logic --- src/hooks/useAsset.ts | 22 +- src/hooks/useDocTypeMeta.ts | 76 ++ src/pages/AssetDetail.tsx | 1513 ++++++++++++++++++---------------- src/services/assetService.ts | 17 + 4 files changed, 896 insertions(+), 732 deletions(-) create mode 100644 src/hooks/useDocTypeMeta.ts diff --git a/src/hooks/useAsset.ts b/src/hooks/useAsset.ts index ce50a18..e4a26bc 100644 --- a/src/hooks/useAsset.ts +++ b/src/hooks/useAsset.ts @@ -201,7 +201,27 @@ export function useAssetMutations() { } }; - return { createAsset, updateAsset, deleteAsset, loading, error }; + const submitAsset = async (assetName: string) => { + try { + setLoading(true); + setError(null); + + console.log('[useAssetMutations] Submitting asset:', assetName); + const response = await assetService.submitAsset(assetName); + console.log('[useAssetMutations] Submit asset response:', response); + + return response; + } catch (err) { + console.error('[useAssetMutations] Submit asset error:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to submit asset'; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + return { createAsset, updateAsset, deleteAsset, submitAsset, loading, error }; } /** diff --git a/src/hooks/useDocTypeMeta.ts b/src/hooks/useDocTypeMeta.ts new file mode 100644 index 0000000..238bd06 --- /dev/null +++ b/src/hooks/useDocTypeMeta.ts @@ -0,0 +1,76 @@ +import { useState, useEffect } from 'react'; +import apiService from '../services/apiService'; + +export interface DocTypeField { + fieldname: string; + fieldtype: string; + label: string; + allow_on_submit: number; // 0 or 1 + reqd: number; // 0 or 1 for required + read_only: number; // 0 or 1 +} + +export const useDocTypeMeta = (doctype: string) => { + const [fields, setFields] = useState([]); + const [allowOnSubmitFields, setAllowOnSubmitFields] = useState>(new Set()); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchDocTypeMeta = async () => { + if (!doctype) { + setLoading(false); + return; + } + + try { + setLoading(true); + const response = await apiService.apiCall( + `/api/resource/DocType/${doctype}` + ); + + // Handle different response structures from Frappe API + // Response can be: { data: {...} } or directly {...} + const docTypeData = response.data || response; + const fieldsList: DocTypeField[] = docTypeData.fields || []; + + // Extract fields that allow editing on submit + const allowOnSubmitSet = new Set(); + fieldsList.forEach((field: DocTypeField) => { + // Check both number (1) and boolean (true) formats + if (field.allow_on_submit === 1 || field.allow_on_submit === true) { + allowOnSubmitSet.add(field.fieldname); + } + }); + + // Debug logging (development only) + if (import.meta.env.DEV) { + console.log(`[DocTypeMeta] Loaded ${fieldsList.length} fields for ${doctype}`); + console.log(`[DocTypeMeta] Fields with allow_on_submit:`, Array.from(allowOnSubmitSet)); + } + + setFields(fieldsList); + setAllowOnSubmitFields(allowOnSubmitSet); + setError(null); + } catch (err) { + console.error(`[DocTypeMeta] Error fetching DocType meta for ${doctype}:`, err); + setError(err instanceof Error ? err.message : 'Unknown error'); + // Don't block the UI if metadata fetch fails - allow all fields to be editable + // This is a graceful degradation + setFields([]); + setAllowOnSubmitFields(new Set()); + } finally { + setLoading(false); + } + }; + + fetchDocTypeMeta(); + }, [doctype]); + + const isAllowedOnSubmit = (fieldname: string): boolean => { + return allowOnSubmitFields.has(fieldname); + }; + + return { fields, allowOnSubmitFields, isAllowedOnSubmit, loading, error }; +}; + diff --git a/src/pages/AssetDetail.tsx b/src/pages/AssetDetail.tsx index ff5ed7b..a1a54d4 100644 --- a/src/pages/AssetDetail.tsx +++ b/src/pages/AssetDetail.tsx @@ -1,17 +1,12 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { useAssetDetails, useAssetMutations } from '../hooks/useAsset'; -import { FaArrowLeft, FaSave, FaEdit, FaQrcode } from 'react-icons/fa'; +import { useDocTypeMeta } from '../hooks/useDocTypeMeta'; +import { FaArrowLeft, FaSave, FaEdit, FaQrcode, FaCheck } from 'react-icons/fa'; import type { CreateAssetData } from '../services/assetService'; import LinkField from '../components/LinkField'; -import apiService from '../services/apiService'; // ✅ your ApiService - -// Helper function to get the base URL for files -const getFileBaseUrl = () => { - // Always use the full URL to avoid proxy path duplication issues - return import.meta.env.VITE_FRAPPE_BASE_URL || 'https://seeraasm-med.seeraarabia.com'; -}; +import apiService from '../services/apiService'; const AssetDetail: React.FC = () => { const { assetName } = useParams<{ assetName: string }>(); @@ -22,14 +17,61 @@ const AssetDetail: React.FC = () => { const isNewAsset = assetName === 'new'; const isDuplicating = isNewAsset && !!duplicateFromAsset; - // If duplicating, fetch the source asset - const { asset, loading, error } = useAssetDetails( + // 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, loading: saving } = useAssetMutations(); + 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>({}); @@ -56,7 +98,7 @@ const AssetDetail: React.FC = () => { available_for_use_date: isNewAsset ? new Date().toISOString().split('T')[0] : undefined }); - // Load user details on mount + // Load user details on mount useEffect(() => { async function loadUserDetails() { try { @@ -74,40 +116,30 @@ const AssetDetail: React.FC = () => { 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) { - // Debug: Log asset data to check if name field exists - console.log('Asset data loaded:', asset); - console.log('Asset name:', asset.name); setFormData({ asset_name: isDuplicating ? `${asset.asset_name} (Copy)` : (asset.asset_name || ''), company: asset.company || '', - custom_serial_number: isDuplicating ? '' : (asset.custom_serial_number || ''), // Clear serial number for duplicates + custom_serial_number: isDuplicating ? '' : (asset.custom_serial_number || ''), location: asset.location || '', custom_manufacturer: asset.custom_manufacturer || '', department: asset.department || '', @@ -123,7 +155,7 @@ const AssetDetail: React.FC = () => { 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, + gross_purchase_amount: asset.gross_purchase_amount || 0, available_for_use_date: asset.available_for_use_date || '', calculate_depreciation: asset.calculate_depreciation || false }); @@ -137,20 +169,15 @@ const AssetDetail: React.FC = () => { 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}"]]` ); @@ -158,7 +185,6 @@ const AssetDetail: React.FC = () => { if (fileRes?.data?.length > 0) { setQrCodeUrl(fileRes.data[0].file_url); } - } catch (error) { console.error("Error loading QR code:", error); } @@ -167,7 +193,6 @@ const AssetDetail: React.FC = () => { fetchQRCode(); }, [assetName, asset]); - const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData(prev => ({ @@ -179,7 +204,6 @@ const AssetDetail: React.FC = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // Validate required fields if (!formData.asset_name) { alert('Please enter an Asset Name'); return; @@ -190,7 +214,6 @@ const AssetDetail: React.FC = () => { return; } - // Show console log for debugging console.log('Submitting asset data:', formData); try { @@ -209,13 +232,13 @@ const AssetDetail: React.FC = () => { 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'; - // Check if it's an API deployment issue if (errorMessage.includes('404') || errorMessage.includes('not found') || errorMessage.includes('has no attribute') || errorMessage.includes('417')) { alert( @@ -235,6 +258,29 @@ const AssetDetail: React.FC = () => { } }; + 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 (
@@ -261,8 +307,7 @@ const AssetDetail: React.FC = () => {
); } - - // Show error for duplicate if source asset not found + if (error && isDuplicating) { return (
@@ -271,7 +316,7 @@ const AssetDetail: React.FC = () => { Source Asset Not Found

- The asset you're trying to duplicate could not be found. It may have been deleted or you may not have permission to access it. + The asset you're trying to duplicate could not be found.

+ + {/* Document Status Badge */} + {asset && ( + + {docstatus === 0 ? 'Draft' : docstatus === 1 ? 'Submitted' : 'Cancelled'} + + )}
- {!isNewAsset && !isEditing && ( - + {!isNewAsset && !isEditing && !isCancelled && ( + <> + + {/* Submit button - only show for Draft documents */} + {isDraft && ( + + )} + + )} + {isCancelled && ( + + Cancelled documents cannot be edited + )} {isEditing && ( <> @@ -327,7 +401,7 @@ const AssetDetail: React.FC = () => { setIsEditing(false); } }} - className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-6 py-2 rounded-lg" + className="bg-gray-300 hover:bg-gray-400 text-gray-700 dark:text-gray-800 px-6 py-2 rounded-lg" disabled={saving} > Cancel @@ -345,694 +419,267 @@ const AssetDetail: React.FC = () => {
-
-
- {/* Left Column - Asset Information & Technical Specs & Location */} -
- {/* Asset Information */} -
-

Asset Information

-
-
- - -
- - {/*
- - -
*/} - - setFormData({ ...formData, custom_asset_type: val })} - disabled={!isEditing} + + {/* 4-Column Grid Layout */} +
+ + {/* COLUMN 1: Asset Information */} +
+

+ Asset Information +

+
+
+ + - - - {/*
- - -
*/} - setFormData({ ...formData, custom_modality: val })} - disabled={!isEditing} - /> - - -
- - -
- -
- - - {isDuplicating && ( -

- 💡 Duplicating from: {duplicateFromAsset} -

- )} -
-
- {/* Technical Specs */} -
-

Technical Specs

-
-
- - -
+ setFormData({ ...formData, custom_asset_type: val })} + disabled={isFieldDisabled('custom_asset_type')} + /> -
- - -
+ setFormData({ ...formData, custom_modality: val })} + disabled={isFieldDisabled('custom_modality')} + /> -
- - -
- - {/*
- - -
*/} - - setFormData({ ...formData, custom_manufacturer: val })} - disabled={!isEditing} - /> - -
- - -
- -
- - -
+
+ +
-
- {/* Location */} -
-

Location

-
- {/*
- - -
*/} - - { - setFormData({ ...formData, company: val, department: '' }); // Clear department when company changes - }} - disabled={!isEditing} - filters={{ domain: 'Healthcare' }} - // onChange={(val) => setFormData({ ...formData, company: val })} - // disabled={!isEditing} +
+ + - - {/*
- - -
*/} - - setFormData({ ...formData, department: val })} - disabled={!isEditing} - filters={departmentFilters} - /> - - setFormData({ ...formData, location: val })} - disabled={!isEditing} - /> - -
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
- - {/* Coverage */} -
-

Coverage

-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -