Added user Permissions logic and Updated filters
This commit is contained in:
parent
39e49543e3
commit
a0124f24f6
@ -167,109 +167,3 @@ const LinkField: React.FC<LinkFieldProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default LinkField;
|
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<string, any>
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const LinkField: React.FC<LinkFieldProps> = ({
|
|
||||||
// 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<HTMLDivElement>(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 (
|
|
||||||
// <div ref={containerRef} className="relative w-full mb-4">
|
|
||||||
// <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{label}</label>
|
|
||||||
|
|
||||||
// <input
|
|
||||||
// type="text"
|
|
||||||
// value={value}
|
|
||||||
// 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 && (
|
|
||||||
// <ul className="absolute z-50 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
|
|
||||||
// rounded-md mt-1 max-h-48 overflow-auto w-full shadow-lg">
|
|
||||||
// {searchResults.map((item, idx) => (
|
|
||||||
// <li
|
|
||||||
// key={idx}
|
|
||||||
// onClick={() => {
|
|
||||||
// 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 && (
|
|
||||||
// <span className="text-gray-600 dark:text-gray-300 text-xs ml-2">{item.description}</span>
|
|
||||||
// )}
|
|
||||||
// </li>
|
|
||||||
// ))}
|
|
||||||
// </ul>
|
|
||||||
// )}
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export default LinkField;
|
|
||||||
|
|||||||
@ -75,7 +75,16 @@ const API_CONFIG: ApiConfig = {
|
|||||||
CSRF_TOKEN: '/api/method/frappe.sessions.get_csrf_token',
|
CSRF_TOKEN: '/api/method/frappe.sessions.get_csrf_token',
|
||||||
|
|
||||||
// File Upload
|
// File Upload
|
||||||
UPLOAD_FILE: '/api/method/upload_file'
|
UPLOAD_FILE: '/api/method/upload_file',
|
||||||
|
|
||||||
|
// User Permission Management - Generic (only these are needed!)
|
||||||
|
GET_USER_PERMISSIONS: '/api/method/asset_lite.api.userperm_api.get_user_permissions',
|
||||||
|
GET_PERMISSION_FILTERS: '/api/method/asset_lite.api.userperm_api.get_permission_filters',
|
||||||
|
GET_ALLOWED_VALUES: '/api/method/asset_lite.api.userperm_api.get_allowed_values',
|
||||||
|
CHECK_DOCUMENT_ACCESS: '/api/method/asset_lite.api.userperm_api.check_document_access',
|
||||||
|
GET_CONFIGURED_DOCTYPES: '/api/method/asset_lite.api.userperm_api.get_configured_doctypes',
|
||||||
|
GET_USER_DEFAULTS: '/api/method/asset_lite.api.userperm_api.get_user_defaults',
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Request Configuration
|
// Request Configuration
|
||||||
|
|||||||
@ -3,13 +3,57 @@ import assetService from '../services/assetService';
|
|||||||
import type { Asset, AssetFilters, AssetFilterOptions, AssetStats, CreateAssetData } from '../services/assetService';
|
import type { Asset, AssetFilters, AssetFilterOptions, AssetStats, CreateAssetData } from '../services/assetService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to fetch list of assets with filters and pagination
|
* Merge user filters with permission filters
|
||||||
|
* Permission filters take precedence for security
|
||||||
|
*/
|
||||||
|
const mergeFilters = (
|
||||||
|
userFilters: AssetFilters | undefined,
|
||||||
|
permissionFilters: Record<string, any>
|
||||||
|
): AssetFilters => {
|
||||||
|
const merged: AssetFilters = { ...(userFilters || {}) };
|
||||||
|
|
||||||
|
// Apply permission filters (they take precedence for security)
|
||||||
|
for (const [field, value] of Object.entries(permissionFilters)) {
|
||||||
|
if (!merged[field as keyof AssetFilters]) {
|
||||||
|
// No user filter on this field, apply permission filter directly
|
||||||
|
(merged as any)[field] = value;
|
||||||
|
} else if (Array.isArray(value) && value[0] === 'in') {
|
||||||
|
// Permission filter is ["in", [...values]]
|
||||||
|
const permittedValues = value[1] as string[];
|
||||||
|
const userValue = merged[field as keyof AssetFilters];
|
||||||
|
|
||||||
|
if (typeof userValue === 'string') {
|
||||||
|
// User selected a specific value, check if it's permitted
|
||||||
|
if (!permittedValues.includes(userValue)) {
|
||||||
|
// User selected a value they don't have permission for
|
||||||
|
// Set to empty array to return no results
|
||||||
|
(merged as any)[field] = ['in', []];
|
||||||
|
}
|
||||||
|
// If permitted, keep the user's specific selection
|
||||||
|
} else if (Array.isArray(userValue) && userValue[0] === 'in') {
|
||||||
|
// Both are ["in", [...]] format, intersect them
|
||||||
|
const userValues = userValue[1] as string[];
|
||||||
|
const intersection = userValues.filter(v => permittedValues.includes(v));
|
||||||
|
(merged as any)[field] = ['in', intersection];
|
||||||
|
} else {
|
||||||
|
// Other filter types, apply permission filter
|
||||||
|
(merged as any)[field] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch list of assets with filters, pagination, and permission-based filtering
|
||||||
*/
|
*/
|
||||||
export function useAssets(
|
export function useAssets(
|
||||||
filters?: AssetFilters,
|
filters?: AssetFilters,
|
||||||
limit: number = 20,
|
limit: number = 20,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
orderBy?: string
|
orderBy?: string,
|
||||||
|
permissionFilters: Record<string, any> = {} // ← NEW: Permission filters parameter
|
||||||
) {
|
) {
|
||||||
const [assets, setAssets] = useState<Asset[]>([]);
|
const [assets, setAssets] = useState<Asset[]>([]);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
@ -21,6 +65,7 @@ export function useAssets(
|
|||||||
|
|
||||||
// Stringify filters to prevent object reference changes from causing re-renders
|
// Stringify filters to prevent object reference changes from causing re-renders
|
||||||
const filtersJson = JSON.stringify(filters);
|
const filtersJson = JSON.stringify(filters);
|
||||||
|
const permissionFiltersJson = JSON.stringify(permissionFilters); // ← NEW
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Prevent fetching if already attempted and has error
|
// Prevent fetching if already attempted and has error
|
||||||
@ -35,7 +80,14 @@ export function useAssets(
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const response = await assetService.getAssets(filters, undefined, limit, offset, orderBy);
|
// ✅ NEW: Merge user filters with permission filters
|
||||||
|
const mergedFilters = mergeFilters(filters, permissionFilters);
|
||||||
|
|
||||||
|
console.log('[useAssets] User filters:', filters);
|
||||||
|
console.log('[useAssets] Permission filters:', permissionFilters);
|
||||||
|
console.log('[useAssets] Merged filters:', mergedFilters);
|
||||||
|
|
||||||
|
const response = await assetService.getAssets(mergedFilters, undefined, limit, offset, orderBy);
|
||||||
|
|
||||||
if (!isCancelled) {
|
if (!isCancelled) {
|
||||||
setAssets(response.assets);
|
setAssets(response.assets);
|
||||||
@ -72,7 +124,7 @@ export function useAssets(
|
|||||||
isCancelled = true;
|
isCancelled = true;
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [filtersJson, limit, offset, orderBy, refetchTrigger]);
|
}, [filtersJson, permissionFiltersJson, limit, offset, orderBy, refetchTrigger]); // ← Added permissionFiltersJson
|
||||||
|
|
||||||
const refetch = useCallback(() => {
|
const refetch = useCallback(() => {
|
||||||
hasAttemptedRef.current = false; // Reset to allow refetch
|
hasAttemptedRef.current = false; // Reset to allow refetch
|
||||||
@ -325,4 +377,3 @@ export function useAssetSearch() {
|
|||||||
|
|
||||||
return { results, loading, error, search, clearResults };
|
return { results, loading, error, search, clearResults };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
188
src/hooks/useUserPermissions.ts
Normal file
188
src/hooks/useUserPermissions.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
import apiService from '../services/apiService';
|
||||||
|
|
||||||
|
interface RestrictionInfo {
|
||||||
|
field: string;
|
||||||
|
values: string[];
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PermissionsState {
|
||||||
|
isAdmin: boolean;
|
||||||
|
restrictions: Record<string, RestrictionInfo>;
|
||||||
|
permissionFilters: Record<string, any>;
|
||||||
|
targetDoctype: string;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic hook for user permissions - works with ANY doctype
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* const { permissionFilters, restrictions } = useUserPermissions('Asset');
|
||||||
|
* const { permissionFilters, restrictions } = useUserPermissions('Work Order');
|
||||||
|
* const { permissionFilters, restrictions } = useUserPermissions('Project');
|
||||||
|
*/
|
||||||
|
export const useUserPermissions = (targetDoctype: string = 'Asset') => {
|
||||||
|
const [state, setState] = useState<PermissionsState>({
|
||||||
|
isAdmin: false,
|
||||||
|
restrictions: {},
|
||||||
|
permissionFilters: {},
|
||||||
|
targetDoctype,
|
||||||
|
loading: true,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchPermissions = useCallback(async (doctype?: string) => {
|
||||||
|
const dt = doctype || targetDoctype;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(prev => ({ ...prev, loading: true, error: null, targetDoctype: dt }));
|
||||||
|
|
||||||
|
const response = await apiService.getPermissionFilters(dt);
|
||||||
|
|
||||||
|
setState({
|
||||||
|
isAdmin: response.is_admin,
|
||||||
|
restrictions: response.restrictions || {},
|
||||||
|
permissionFilters: response.filters || {},
|
||||||
|
targetDoctype: dt,
|
||||||
|
loading: false,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error fetching permissions for ${dt}:`, err);
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
loading: false,
|
||||||
|
error: err instanceof Error ? err.message : 'Failed to fetch permissions'
|
||||||
|
}));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [targetDoctype]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPermissions();
|
||||||
|
}, [fetchPermissions]);
|
||||||
|
|
||||||
|
// Get allowed values for a permission type (e.g., "Company", "Location")
|
||||||
|
const getAllowedValues = useCallback((permissionType: string): string[] => {
|
||||||
|
return state.restrictions[permissionType]?.values || [];
|
||||||
|
}, [state.restrictions]);
|
||||||
|
|
||||||
|
// Check if user has restriction on a permission type
|
||||||
|
const hasRestriction = useCallback((permissionType: string): boolean => {
|
||||||
|
if (state.isAdmin) return false;
|
||||||
|
return !!state.restrictions[permissionType];
|
||||||
|
}, [state.isAdmin, state.restrictions]);
|
||||||
|
|
||||||
|
// Check if any restrictions exist
|
||||||
|
const hasAnyRestrictions = useMemo(() => {
|
||||||
|
return !state.isAdmin && Object.keys(state.restrictions).length > 0;
|
||||||
|
}, [state.isAdmin, state.restrictions]);
|
||||||
|
|
||||||
|
// Merge user filters with permission filters
|
||||||
|
const mergeFilters = useCallback((userFilters: Record<string, any>): Record<string, any> => {
|
||||||
|
if (state.isAdmin) return userFilters;
|
||||||
|
|
||||||
|
const merged = { ...userFilters };
|
||||||
|
|
||||||
|
for (const [field, value] of Object.entries(state.permissionFilters)) {
|
||||||
|
if (!merged[field]) {
|
||||||
|
merged[field] = value;
|
||||||
|
} else if (Array.isArray(value) && value[0] === 'in') {
|
||||||
|
const permittedValues = value[1] as string[];
|
||||||
|
if (typeof merged[field] === 'string' && !permittedValues.includes(merged[field])) {
|
||||||
|
merged[field] = ['in', []]; // Return empty - value not permitted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}, [state.isAdmin, state.permissionFilters]);
|
||||||
|
|
||||||
|
// Get summary of restrictions for display
|
||||||
|
const restrictionsList = useMemo(() => {
|
||||||
|
return Object.entries(state.restrictions).map(([type, info]) => ({
|
||||||
|
type,
|
||||||
|
field: info.field,
|
||||||
|
values: info.values,
|
||||||
|
count: info.count
|
||||||
|
}));
|
||||||
|
}, [state.restrictions]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
refetch: fetchPermissions,
|
||||||
|
switchDoctype: fetchPermissions,
|
||||||
|
getAllowedValues,
|
||||||
|
hasRestriction,
|
||||||
|
hasAnyRestrictions,
|
||||||
|
mergeFilters,
|
||||||
|
restrictionsList
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to check access to a specific document
|
||||||
|
*/
|
||||||
|
export const useDocumentAccess = (doctype: string | null, docname: string | null) => {
|
||||||
|
const [hasAccess, setHasAccess] = useState<boolean | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!doctype || !docname) {
|
||||||
|
setHasAccess(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const check = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await apiService.checkDocumentAccess(doctype, docname);
|
||||||
|
setHasAccess(response.has_access);
|
||||||
|
if (!response.has_access && response.error) {
|
||||||
|
setError(response.error);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to check access');
|
||||||
|
setHasAccess(false);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
check();
|
||||||
|
}, [doctype, docname]);
|
||||||
|
|
||||||
|
return { hasAccess, loading, error };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to get user's default values
|
||||||
|
*/
|
||||||
|
export const useUserDefaults = () => {
|
||||||
|
const [defaults, setDefaults] = useState<Record<string, string>>({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetch = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiService.getUserDefaults();
|
||||||
|
setDefaults(response.defaults || {});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch user defaults:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetch();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { defaults, loading, getDefault: (type: string) => defaults[type] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUserPermissions;
|
||||||
@ -2996,179 +2996,6 @@ const handlePPMPlan = async () => {
|
|||||||
</div>
|
</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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAssets, useAssetMutations } from '../hooks/useAsset';
|
import { useAssets, useAssetMutations } from '../hooks/useAsset';
|
||||||
import { FaPlus, FaSearch, FaEdit, FaEye, FaTrash, FaCopy, FaEllipsisV, FaDownload, FaPrint, FaFileExport, FaTimes, FaFilter, FaChevronDown, FaChevronUp, FaSave, FaStar } from 'react-icons/fa';
|
import { FaPlus, FaSearch, FaEdit, FaEye, FaTrash, FaCopy, FaEllipsisV, FaDownload, FaPrint, FaFileExport, FaTimes, FaFilter, FaChevronDown, FaChevronUp, FaSave,
|
||||||
|
FaStar, FaLock } from 'react-icons/fa';
|
||||||
import LinkField from '../components/LinkField';
|
import LinkField from '../components/LinkField';
|
||||||
|
import { useUserPermissions } from '../hooks/useUserPermissions';
|
||||||
|
|
||||||
|
|
||||||
const AssetList: React.FC = () => {
|
const AssetList: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -13,6 +16,18 @@ const AssetList: React.FC = () => {
|
|||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const limit = 20;
|
const limit = 20;
|
||||||
|
|
||||||
|
// ✅ NEW: Get user permissions for Asset doctype
|
||||||
|
const {
|
||||||
|
isAdmin,
|
||||||
|
permissionFilters,
|
||||||
|
restrictions,
|
||||||
|
loading: permissionsLoading,
|
||||||
|
error: permissionsError,
|
||||||
|
hasRestriction,
|
||||||
|
getAllowedValues,
|
||||||
|
hasAnyRestrictions
|
||||||
|
} = useUserPermissions('Asset');
|
||||||
|
|
||||||
// Filter states
|
// Filter states
|
||||||
const [filterAssetId, setFilterAssetId] = useState('');
|
const [filterAssetId, setFilterAssetId] = useState('');
|
||||||
const [filterCompany, setFilterCompany] = useState('');
|
const [filterCompany, setFilterCompany] = useState('');
|
||||||
@ -96,7 +111,8 @@ const AssetList: React.FC = () => {
|
|||||||
filters,
|
filters,
|
||||||
limit,
|
limit,
|
||||||
page * limit,
|
page * limit,
|
||||||
'creation desc'
|
'creation desc',
|
||||||
|
permissionFilters // ← Pass permission filters here
|
||||||
);
|
);
|
||||||
|
|
||||||
const { deleteAsset, loading: mutationLoading } = useAssetMutations();
|
const { deleteAsset, loading: mutationLoading } = useAssetMutations();
|
||||||
@ -314,6 +330,106 @@ const AssetList: React.FC = () => {
|
|||||||
filterLocation || filterDepartment || filterModality || filterDeviceStatus ||
|
filterLocation || filterDepartment || filterModality || filterDeviceStatus ||
|
||||||
filterAssetName || filterSerialNumber || searchTerm;
|
filterAssetName || filterSerialNumber || searchTerm;
|
||||||
|
|
||||||
|
// ✅ NEW: Helper component for restricted select fields
|
||||||
|
const RestrictedSelect: React.FC<{
|
||||||
|
label: string;
|
||||||
|
permissionType: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
doctype: string;
|
||||||
|
placeholder?: string;
|
||||||
|
filters?: Record<string, any>;
|
||||||
|
}> = ({ label, permissionType, value, onChange, doctype, placeholder, filters: linkFilters }) => {
|
||||||
|
const isRestricted = hasRestriction(permissionType);
|
||||||
|
const allowedValues = getAllowedValues(permissionType);
|
||||||
|
|
||||||
|
if (isRestricted && allowedValues.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5 flex items-center gap-1">
|
||||||
|
{label}
|
||||||
|
<FaLock size={8} className="text-amber-500" title={`Restricted to ${allowedValues.length} value(s)`} />
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="w-full px-2 py-1 text-xs border border-amber-300 dark:border-amber-600 rounded focus:outline-none focus:ring-2 focus:ring-amber-500 bg-amber-50 dark:bg-amber-900/20 text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
<option value="">All Allowed ({allowedValues.length})</option>
|
||||||
|
{allowedValues.map(val => (
|
||||||
|
<option key={val} value={val}>{val}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{value && (
|
||||||
|
<button
|
||||||
|
onClick={() => onChange('')}
|
||||||
|
className="absolute right-8 top-6 text-gray-400 hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes size={10} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not restricted - show normal LinkField
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<LinkField
|
||||||
|
label={label}
|
||||||
|
doctype={doctype}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={placeholder || `Select ${label}`}
|
||||||
|
disabled={false}
|
||||||
|
compact={true}
|
||||||
|
filters={linkFilters}
|
||||||
|
/>
|
||||||
|
{value && (
|
||||||
|
<button
|
||||||
|
onClick={() => onChange('')}
|
||||||
|
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes size={10} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ NEW: Loading state for permissions
|
||||||
|
if (permissionsLoading) {
|
||||||
|
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 permissions...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ NEW: Error state for permissions
|
||||||
|
if (permissionsError) {
|
||||||
|
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">⚠️ Permission Error</h2>
|
||||||
|
<div className="text-red-700 dark:text-red-400 space-y-3">
|
||||||
|
<p><strong>Unable to load user permissions.</strong></p>
|
||||||
|
<p>{permissionsError}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="mt-2 bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (loading && page === 0) {
|
if (loading && page === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
@ -358,6 +474,13 @@ const AssetList: React.FC = () => {
|
|||||||
<h1 className="text-3xl font-bold text-gray-800 dark:text-white">Assets</h1>
|
<h1 className="text-3xl font-bold text-gray-800 dark:text-white">Assets</h1>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
Total: {totalCount} asset{totalCount !== 1 ? 's' : ''}
|
Total: {totalCount} asset{totalCount !== 1 ? 's' : ''}
|
||||||
|
{/* ✅ NEW: Show permission indicator */}
|
||||||
|
{/* {hasAnyRestrictions && (
|
||||||
|
<span className="ml-2 inline-flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
|
||||||
|
<FaLock size={10} />
|
||||||
|
Filtered by permissions
|
||||||
|
</span>
|
||||||
|
)} */}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
@ -380,51 +503,152 @@ const AssetList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Advanced Filter Panel */}
|
{/* Advanced Filter Panel */}
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 mb-4 overflow-hidden">
|
{/* Advanced Filter Panel */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 mb-4">
|
||||||
{/* Filter Header */}
|
{/* Filter Header */}
|
||||||
<div className="bg-gradient-to-r from-blue-500 to-blue-600 dark:from-blue-600 dark:to-blue-700 px-4 py-3">
|
<div className="bg-gradient-to-r from-blue-500 to-blue-600 dark:from-blue-600 dark:to-blue-700 px-4 py-3 rounded-t-lg">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-3">
|
{/* Left Side - Toggle & Title */}
|
||||||
|
<div className="flex items-center gap-3 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsFilterExpanded(!isFilterExpanded)}
|
onClick={() => setIsFilterExpanded(!isFilterExpanded)}
|
||||||
className="text-white hover:bg-blue-600 dark:hover:bg-blue-700 p-2 rounded-lg transition-all"
|
className="text-white hover:bg-white/20 p-2 rounded-lg transition-all"
|
||||||
>
|
>
|
||||||
{isFilterExpanded ? <FaChevronUp size={14} /> : <FaChevronDown size={14} />}
|
{isFilterExpanded ? <FaChevronUp size={14} /> : <FaChevronDown size={14} />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaFilter className="text-white" size={16} />
|
<FaFilter className="text-white" size={16} />
|
||||||
<h3 className="text-white font-semibold text-sm">Advanced Filters</h3>
|
<h3 className="text-white font-semibold text-sm">Filters</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeFilterCount > 0 && (
|
{activeFilterCount > 0 && (
|
||||||
<span className="bg-white text-blue-600 px-2 py-0.5 rounded-full text-xs font-bold animate-pulse">
|
<span className="bg-white text-blue-600 px-2 py-0.5 rounded-full text-xs font-bold">
|
||||||
{activeFilterCount}
|
{activeFilterCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
{/* Center - Active Filter Tags (scrollable) */}
|
||||||
{/* Quick Filter Buttons */}
|
{hasActiveFilters && (
|
||||||
<div className="flex gap-1">
|
<div className="flex-1 overflow-x-auto scrollbar-hide mx-2">
|
||||||
|
<div className="flex items-center gap-2 py-1">
|
||||||
|
{filterAssetId && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-blue-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
|
||||||
|
<span className="font-semibold">Asset ID:</span> {filterAssetId}
|
||||||
<button
|
<button
|
||||||
onClick={() => applyQuickFilter('active')}
|
onClick={(e) => { e.stopPropagation(); setFilterAssetId(''); }}
|
||||||
className="px-3 py-1 bg-white/20 hover:bg-white/30 text-white rounded-md text-xs font-medium transition-all flex items-center gap-1"
|
className="hover:text-red-500 transition-colors"
|
||||||
title="Active Assets"
|
|
||||||
>
|
>
|
||||||
<span>✓</span>
|
<FaTimes className="text-[9px]" />
|
||||||
<span className="hidden lg:inline">Active</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{filterCompany && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-green-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
|
||||||
|
<span className="font-semibold">Hospital:</span> {filterCompany}
|
||||||
<button
|
<button
|
||||||
onClick={() => applyQuickFilter('down')}
|
onClick={(e) => { e.stopPropagation(); setFilterCompany(''); }}
|
||||||
className="px-3 py-1 bg-white/20 hover:bg-white/30 text-white rounded-md text-xs font-medium transition-all flex items-center gap-1"
|
className="hover:text-red-500 transition-colors"
|
||||||
title="Down Assets"
|
|
||||||
>
|
>
|
||||||
<span>⚠</span>
|
<FaTimes className="text-[9px]" />
|
||||||
<span className="hidden lg:inline">Down</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{filterAssetName && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-cyan-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
|
||||||
|
<span className="font-semibold">Name:</span> {filterAssetName}
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setFilterAssetName(''); setTempAssetName(''); }}
|
||||||
|
className="hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="text-[9px]" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{filterSerialNumber && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-lime-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
|
||||||
|
<span className="font-semibold">Serial:</span> {filterSerialNumber}
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setFilterSerialNumber(''); setTempSerialNumber(''); }}
|
||||||
|
className="hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="text-[9px]" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{filterDeviceStatus && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-orange-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
|
||||||
|
<span className="font-semibold">Status:</span> {filterDeviceStatus}
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setFilterDeviceStatus(''); }}
|
||||||
|
className="hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="text-[9px]" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{filterLocation && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-purple-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
|
||||||
|
<span className="font-semibold">Location:</span> {filterLocation}
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setFilterLocation(''); }}
|
||||||
|
className="hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="text-[9px]" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{filterDepartment && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-yellow-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
|
||||||
|
<span className="font-semibold">Dept:</span> {filterDepartment}
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setFilterDepartment(''); }}
|
||||||
|
className="hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="text-[9px]" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{filterModality && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-pink-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
|
||||||
|
<span className="font-semibold">Modality:</span> {filterModality}
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setFilterModality(''); }}
|
||||||
|
className="hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="text-[9px]" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{filterManufacturer && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-indigo-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
|
||||||
|
<span className="font-semibold">Mfr:</span> {filterManufacturer}
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setFilterManufacturer(''); }}
|
||||||
|
className="hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="text-[9px]" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{filterSupplier && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-teal-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
|
||||||
|
<span className="font-semibold">Supplier:</span> {filterSupplier}
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setFilterSupplier(''); }}
|
||||||
|
className="hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="text-[9px]" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Right Side - Action Buttons */}
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
{/* Save Filter Button */}
|
{/* Save Filter Button */}
|
||||||
{activeFilterCount > 0 && (
|
{activeFilterCount > 0 && (
|
||||||
<button
|
<button
|
||||||
@ -432,7 +656,7 @@ const AssetList: React.FC = () => {
|
|||||||
className="px-3 py-1.5 bg-white text-blue-600 hover:bg-blue-50 rounded-md text-xs font-medium transition-all flex items-center gap-1.5"
|
className="px-3 py-1.5 bg-white text-blue-600 hover:bg-blue-50 rounded-md text-xs font-medium transition-all flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
<FaSave size={12} />
|
<FaSave size={12} />
|
||||||
Save
|
<span className="hidden sm:inline">Save</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -443,7 +667,7 @@ const AssetList: React.FC = () => {
|
|||||||
className="px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded-md text-xs font-medium transition-all flex items-center gap-1.5"
|
className="px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded-md text-xs font-medium transition-all flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
<FaTimes size={12} />
|
<FaTimes size={12} />
|
||||||
Clear All
|
<span className="hidden sm:inline">Clear</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -451,11 +675,7 @@ const AssetList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expandable Filter Content */}
|
{/* Expandable Filter Content */}
|
||||||
<div
|
{isFilterExpanded && (
|
||||||
className={`transition-all duration-300 ease-in-out ${
|
|
||||||
isFilterExpanded ? 'max-h-[800px] opacity-100' : 'max-h-0 opacity-0'
|
|
||||||
} overflow-hidden`}
|
|
||||||
>
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{/* Saved Filter Presets */}
|
{/* Saved Filter Presets */}
|
||||||
{savedFilters.length > 0 && (
|
{savedFilters.length > 0 && (
|
||||||
@ -488,16 +708,12 @@ const AssetList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Filter Grid - Organized by Category */}
|
{/* Filter Grid */}
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
{/* Asset Identification Group */}
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-900/50 p-3 rounded-lg">
|
<div className="bg-gray-50 dark:bg-gray-900/50 p-3 rounded-lg">
|
||||||
{/* <h4 className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center gap-2">
|
|
||||||
<span className="w-1 h-4 bg-blue-500 rounded-full"></span>
|
|
||||||
Asset Identification
|
|
||||||
</h4> */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-3">
|
||||||
<div className="relative">
|
{/* Asset ID */}
|
||||||
|
<div className="relative z-[60]">
|
||||||
<LinkField
|
<LinkField
|
||||||
label="Asset ID"
|
label="Asset ID"
|
||||||
doctype="Asset"
|
doctype="Asset"
|
||||||
@ -510,13 +726,14 @@ const AssetList: React.FC = () => {
|
|||||||
{filterAssetId && (
|
{filterAssetId && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilterAssetId('')}
|
onClick={() => setFilterAssetId('')}
|
||||||
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors"
|
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
|
||||||
>
|
>
|
||||||
<FaTimes size={10} />
|
<FaTimes size={10} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Asset Name */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">
|
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">
|
||||||
Asset Name
|
Asset Name
|
||||||
@ -547,6 +764,7 @@ const AssetList: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Serial Number */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">
|
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">
|
||||||
Serial Number
|
Serial Number
|
||||||
@ -576,7 +794,9 @@ const AssetList: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
|
||||||
|
{/* Hospital */}
|
||||||
|
<div className="relative z-[59]">
|
||||||
<LinkField
|
<LinkField
|
||||||
label="Hospital"
|
label="Hospital"
|
||||||
doctype="Company"
|
doctype="Company"
|
||||||
@ -590,13 +810,14 @@ const AssetList: React.FC = () => {
|
|||||||
{filterCompany && (
|
{filterCompany && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilterCompany('')}
|
onClick={() => setFilterCompany('')}
|
||||||
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors"
|
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
|
||||||
>
|
>
|
||||||
<FaTimes size={10} />
|
<FaTimes size={10} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Device Status */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">
|
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">
|
||||||
Device Status
|
Device Status
|
||||||
@ -619,7 +840,9 @@ const AssetList: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
|
||||||
|
{/* Location */}
|
||||||
|
<div className="relative z-[58]">
|
||||||
<LinkField
|
<LinkField
|
||||||
label="Location"
|
label="Location"
|
||||||
doctype="Location"
|
doctype="Location"
|
||||||
@ -632,13 +855,15 @@ const AssetList: React.FC = () => {
|
|||||||
{filterLocation && (
|
{filterLocation && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilterLocation('')}
|
onClick={() => setFilterLocation('')}
|
||||||
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors"
|
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
|
||||||
>
|
>
|
||||||
<FaTimes size={10} />
|
<FaTimes size={10} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
|
||||||
|
{/* Department */}
|
||||||
|
<div className="relative z-[57]">
|
||||||
<LinkField
|
<LinkField
|
||||||
label="Department"
|
label="Department"
|
||||||
doctype="Department"
|
doctype="Department"
|
||||||
@ -651,13 +876,15 @@ const AssetList: React.FC = () => {
|
|||||||
{filterDepartment && (
|
{filterDepartment && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilterDepartment('')}
|
onClick={() => setFilterDepartment('')}
|
||||||
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors"
|
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
|
||||||
>
|
>
|
||||||
<FaTimes size={10} />
|
<FaTimes size={10} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
|
||||||
|
{/* Modality */}
|
||||||
|
<div className="relative z-[56]">
|
||||||
<LinkField
|
<LinkField
|
||||||
label="Modality"
|
label="Modality"
|
||||||
doctype="Modality"
|
doctype="Modality"
|
||||||
@ -670,14 +897,15 @@ const AssetList: React.FC = () => {
|
|||||||
{filterModality && (
|
{filterModality && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilterModality('')}
|
onClick={() => setFilterModality('')}
|
||||||
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors"
|
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
|
||||||
>
|
>
|
||||||
<FaTimes size={10} />
|
<FaTimes size={10} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
{/* Manufacturer */}
|
||||||
|
<div className="relative z-[55]">
|
||||||
<LinkField
|
<LinkField
|
||||||
label="Manufacturer"
|
label="Manufacturer"
|
||||||
doctype="Manufacturer"
|
doctype="Manufacturer"
|
||||||
@ -690,14 +918,15 @@ const AssetList: React.FC = () => {
|
|||||||
{filterManufacturer && (
|
{filterManufacturer && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilterManufacturer('')}
|
onClick={() => setFilterManufacturer('')}
|
||||||
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors"
|
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
|
||||||
>
|
>
|
||||||
<FaTimes size={10} />
|
<FaTimes size={10} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
{/* Supplier */}
|
||||||
|
<div className="relative z-[54]">
|
||||||
<LinkField
|
<LinkField
|
||||||
label="Supplier"
|
label="Supplier"
|
||||||
doctype="Supplier"
|
doctype="Supplier"
|
||||||
@ -710,7 +939,7 @@ const AssetList: React.FC = () => {
|
|||||||
{filterSupplier && (
|
{filterSupplier && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilterSupplier('')}
|
onClick={() => setFilterSupplier('')}
|
||||||
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors"
|
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
|
||||||
>
|
>
|
||||||
<FaTimes size={10} />
|
<FaTimes size={10} />
|
||||||
</button>
|
</button>
|
||||||
@ -719,109 +948,9 @@ const AssetList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Active Filters Summary */}
|
|
||||||
{hasActiveFilters && (
|
|
||||||
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h4 className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
|
||||||
Active Filters ({activeFilterCount})
|
|
||||||
</h4>
|
|
||||||
<button
|
|
||||||
onClick={handleClearFilters}
|
|
||||||
className="text-xs text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 font-medium flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<FaTimes size={10} />
|
|
||||||
Clear All
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{filterAssetId && (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-blue-100 to-blue-200 dark:from-blue-900 dark:to-blue-800 text-blue-800 dark:text-blue-200 rounded-full text-[10px] font-medium shadow-sm">
|
|
||||||
<span className="font-semibold">Asset ID:</span> {filterAssetId}
|
|
||||||
<button onClick={() => setFilterAssetId('')} className="hover:text-blue-600 dark:hover:text-blue-400">
|
|
||||||
<FaTimes className="text-[9px]" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filterCompany && (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-green-100 to-green-200 dark:from-green-900 dark:to-green-800 text-green-800 dark:text-green-200 rounded-full text-[10px] font-medium shadow-sm">
|
|
||||||
<span className="font-semibold">Hospital:</span> {filterCompany}
|
|
||||||
<button onClick={() => setFilterCompany('')} className="hover:text-green-600">
|
|
||||||
<FaTimes className="text-[9px]" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filterAssetName && (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-cyan-100 to-cyan-200 dark:from-cyan-900 dark:to-cyan-800 text-cyan-800 dark:text-cyan-200 rounded-full text-[10px] font-medium shadow-sm">
|
|
||||||
<span className="font-semibold">Name:</span> {filterAssetName}
|
|
||||||
<button onClick={() => { setFilterAssetName(''); setTempAssetName(''); }}>
|
|
||||||
<FaTimes className="text-[9px]" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filterSerialNumber && (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-lime-100 to-lime-200 dark:from-lime-900 dark:to-lime-800 text-lime-800 dark:text-lime-200 rounded-full text-[10px] font-medium shadow-sm">
|
|
||||||
<span className="font-semibold">Serial:</span> {filterSerialNumber}
|
|
||||||
<button onClick={() => { setFilterSerialNumber(''); setTempSerialNumber(''); }}>
|
|
||||||
<FaTimes className="text-[9px]" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filterDeviceStatus && (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-orange-100 to-orange-200 dark:from-orange-900 dark:to-orange-800 text-orange-800 dark:text-orange-200 rounded-full text-[10px] font-medium shadow-sm">
|
|
||||||
<span className="font-semibold">Status:</span> {filterDeviceStatus}
|
|
||||||
<button onClick={() => setFilterDeviceStatus('')}>
|
|
||||||
<FaTimes className="text-[9px]" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filterLocation && (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-purple-100 to-purple-200 dark:from-purple-900 dark:to-purple-800 text-purple-800 dark:text-purple-200 rounded-full text-[10px] font-medium shadow-sm">
|
|
||||||
<span className="font-semibold">Location:</span> {filterLocation}
|
|
||||||
<button onClick={() => setFilterLocation('')}>
|
|
||||||
<FaTimes className="text-[9px]" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filterDepartment && (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-yellow-100 to-yellow-200 dark:from-yellow-900 dark:to-yellow-800 text-yellow-800 dark:text-yellow-200 rounded-full text-[10px] font-medium shadow-sm">
|
|
||||||
<span className="font-semibold">Department:</span> {filterDepartment}
|
|
||||||
<button onClick={() => setFilterDepartment('')}>
|
|
||||||
<FaTimes className="text-[9px]" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filterModality && (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-pink-100 to-pink-200 dark:from-pink-900 dark:to-pink-800 text-pink-800 dark:text-pink-200 rounded-full text-[10px] font-medium shadow-sm">
|
|
||||||
<span className="font-semibold">Modality:</span> {filterModality}
|
|
||||||
<button onClick={() => setFilterModality('')}>
|
|
||||||
<FaTimes className="text-[9px]" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filterManufacturer && (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-indigo-100 to-indigo-200 dark:from-indigo-900 dark:to-indigo-800 text-indigo-800 dark:text-indigo-200 rounded-full text-[10px] font-medium shadow-sm">
|
|
||||||
<span className="font-semibold">Manufacturer:</span> {filterManufacturer}
|
|
||||||
<button onClick={() => setFilterManufacturer('')}>
|
|
||||||
<FaTimes className="text-[9px]" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filterSupplier && (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-teal-100 to-teal-200 dark:from-teal-900 dark:to-teal-800 text-teal-800 dark:text-teal-200 rounded-full text-[10px] font-medium shadow-sm">
|
|
||||||
<span className="font-semibold">Supplier:</span> {filterSupplier}
|
|
||||||
<button onClick={() => setFilterSupplier('')}>
|
|
||||||
<FaTimes className="text-[9px]" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Save Filter Modal */}
|
{/* Save Filter Modal */}
|
||||||
{showSaveFilterModal && (
|
{showSaveFilterModal && (
|
||||||
@ -1025,6 +1154,11 @@ const AssetList: React.FC = () => {
|
|||||||
{Math.min((page + 1) * limit, totalCount)}
|
{Math.min((page + 1) * limit, totalCount)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
of <span className="font-medium">{totalCount}</span> results
|
of <span className="font-medium">{totalCount}</span> results
|
||||||
|
|
||||||
|
{hasAnyRestrictions && (
|
||||||
|
<span className="ml-1 text-amber-600 dark:text-amber-400">(filtered)</span>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -121,6 +121,47 @@ interface RequestOptions {
|
|||||||
body?: any;
|
body?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// USER PERMISSION INTERFACES
|
||||||
|
interface RestrictionInfo {
|
||||||
|
field: string;
|
||||||
|
values: string[];
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PermissionFiltersResponse {
|
||||||
|
is_admin: boolean;
|
||||||
|
filters: Record<string, any>;
|
||||||
|
restrictions: Record<string, RestrictionInfo>;
|
||||||
|
target_doctype: string;
|
||||||
|
user?: string;
|
||||||
|
total_restrictions?: number;
|
||||||
|
warning?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AllowedValuesResponse {
|
||||||
|
is_admin: boolean;
|
||||||
|
allowed_values: string[];
|
||||||
|
default_value?: string | null;
|
||||||
|
has_restriction: boolean;
|
||||||
|
allow_doctype: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocumentAccessResponse {
|
||||||
|
has_access: boolean;
|
||||||
|
is_admin?: boolean;
|
||||||
|
no_restrictions?: boolean;
|
||||||
|
error?: string;
|
||||||
|
denied_by?: string;
|
||||||
|
field?: string;
|
||||||
|
document_value?: string;
|
||||||
|
allowed_values?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserDefaultsResponse {
|
||||||
|
is_admin: boolean;
|
||||||
|
defaults: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
private baseURL: string;
|
private baseURL: string;
|
||||||
private endpoints: Record<string, string>;
|
private endpoints: Record<string, string>;
|
||||||
@ -381,6 +422,39 @@ class ApiService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// USER PERMISSION METHODS
|
||||||
|
async getUserPermissions(userId?: string): Promise<any> {
|
||||||
|
const params = userId ? `?user=${encodeURIComponent(userId)}` : '';
|
||||||
|
return this.apiCall(`${this.endpoints.GET_USER_PERMISSIONS}${params}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPermissionFilters(targetDoctype: string, userId?: string): Promise<PermissionFiltersResponse> {
|
||||||
|
const params = new URLSearchParams({ target_doctype: targetDoctype });
|
||||||
|
if (userId) params.append('user', userId);
|
||||||
|
return this.apiCall<PermissionFiltersResponse>(`${this.endpoints.GET_PERMISSION_FILTERS}?${params}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllowedValues(allowDoctype: string, userId?: string): Promise<AllowedValuesResponse> {
|
||||||
|
const params = new URLSearchParams({ allow_doctype: allowDoctype });
|
||||||
|
if (userId) params.append('user', userId);
|
||||||
|
return this.apiCall<AllowedValuesResponse>(`${this.endpoints.GET_ALLOWED_VALUES}?${params}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkDocumentAccess(doctype: string, docname: string, userId?: string): Promise<DocumentAccessResponse> {
|
||||||
|
const params = new URLSearchParams({ doctype, docname });
|
||||||
|
if (userId) params.append('user', userId);
|
||||||
|
return this.apiCall<DocumentAccessResponse>(`${this.endpoints.CHECK_DOCUMENT_ACCESS}?${params}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfiguredDoctypes(): Promise<any> {
|
||||||
|
return this.apiCall(this.endpoints.GET_CONFIGURED_DOCTYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserDefaults(userId?: string): Promise<UserDefaultsResponse> {
|
||||||
|
const params = userId ? `?user=${encodeURIComponent(userId)}` : '';
|
||||||
|
return this.apiCall<UserDefaultsResponse>(`${this.endpoints.GET_USER_DEFAULTS}${params}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Utility Methods
|
// Utility Methods
|
||||||
isAuthenticated(): boolean {
|
isAuthenticated(): boolean {
|
||||||
// Check if user is authenticated (implement based on your auth strategy)
|
// Check if user is authenticated (implement based on your auth strategy)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user