From 7314b69cfc21a5fcfb4d30272144f53516d8a85b Mon Sep 17 00:00:00 2001 From: ril3y Date: Sat, 11 Oct 2025 10:30:21 -0400 Subject: [PATCH 01/18] fix: properly hide current password field for admin editing other users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROOT CAUSE: The conditional logic was correct but the variable was being recalculated inside the handler function instead of using the component-level variable. FIXES: - Use single source of truth: requireCurrentPassword = !isAdmin || isEditingSelf - Admin editing another user: !true || false = FALSE → hide field ✅ - Admin editing self: !true || true = TRUE → show field ✅ - Regular user: !false || ? = TRUE → show field ✅ - Update UI conditional to use requireCurrentPassword directly - Add comprehensive debug logging to console DEBUG ADDED: Console logs now show: - Current User ID - Editing User ID - User roles - isAdmin flag - isEditingSelf flag - requireCurrentPassword result This makes it crystal clear what the modal is detecting. DO NOT PUSH - TESTING LOCALLY FIRST --- .../src/components/users/EditUserModal.tsx | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/MakerMatrix/frontend/src/components/users/EditUserModal.tsx b/MakerMatrix/frontend/src/components/users/EditUserModal.tsx index a8f318f0..824a315a 100644 --- a/MakerMatrix/frontend/src/components/users/EditUserModal.tsx +++ b/MakerMatrix/frontend/src/components/users/EditUserModal.tsx @@ -36,6 +36,25 @@ const EditUserModal = ({ // Check if editing own profile const isEditingSelf = currentUser?.id === user?.id + // Determine if current password is required + // Admin editing another user = NO current password needed + // Anyone editing themselves = YES current password needed + const requireCurrentPassword = !isAdmin || isEditingSelf + + // Debug logging + useEffect(() => { + if (isOpen && user) { + console.log('=== EditUserModal Debug ===') + console.log('Current User ID:', currentUser?.id) + console.log('Editing User ID:', user?.id) + console.log('Current User Roles:', currentUser?.roles?.map(r => r.name)) + console.log('isAdmin:', isAdmin) + console.log('isEditingSelf:', isEditingSelf) + console.log('requireCurrentPassword:', requireCurrentPassword) + console.log('=========================') + } + }, [isOpen, user, currentUser, isAdmin, isEditingSelf, requireCurrentPassword]) + useEffect(() => { if (user) { setSelectedRoleIds(user.roles.map((r) => r.id)) @@ -97,9 +116,7 @@ const EditUserModal = ({ const handlePasswordChange = async () => { if (!user) return - // Validation - admins editing others don't need current password - const requireCurrentPassword = isEditingSelf || !isAdmin - + // Validation - use the requireCurrentPassword from component state if (requireCurrentPassword && !currentPassword) { toast.error('Please enter your current password') return @@ -234,8 +251,8 @@ const EditUserModal = ({ {showPasswordSection && (
- {/* Admin Notice */} - {isAdmin && !isEditingSelf && ( + {/* Admin Notice - show when admin is editing another user */} + {!requireCurrentPassword && (

ℹ️ As an admin, you can change this user's password without their current password. @@ -243,8 +260,8 @@ const EditUserModal = ({

)} - {/* Current Password - only show if editing self or not admin */} - {(isEditingSelf || !isAdmin) && ( + {/* Current Password - only show if required */} + {requireCurrentPassword && (
)}
+ + {/* Delete Parts */} +
+
+ setFormData({ ...formData, deleteParts: e.target.checked })} + className="w-4 h-4 rounded border-red-500" + /> + +
+ {formData.deleteParts && ( +
+
+ +
+

+ Warning: This will permanently delete all selected parts +

+

+ This action cannot be undone. Associated images and datasheets will also be deleted. +

+
+
+
+ )} +
) diff --git a/MakerMatrix/frontend/src/components/parts/PartEnrichmentModal.tsx b/MakerMatrix/frontend/src/components/parts/PartEnrichmentModal.tsx index 92e76bc9..a6cfa810 100644 --- a/MakerMatrix/frontend/src/components/parts/PartEnrichmentModal.tsx +++ b/MakerMatrix/frontend/src/components/parts/PartEnrichmentModal.tsx @@ -61,6 +61,8 @@ const PartEnrichmentModal = ({ const [requirementCheck, setRequirementCheck] = useState(null) const [isCheckingRequirements, setIsCheckingRequirements] = useState(false) + const [missingFieldValues, setMissingFieldValues] = useState>({}) + const [isSavingFields, setIsSavingFields] = useState(false) // Capability definitions with icons and descriptions const capabilityDefinitions = { @@ -237,6 +239,15 @@ const PartEnrichmentModal = ({ const check = await partsService.checkEnrichmentRequirements(part.id, selectedSupplier) setRequirementCheck(check) console.log('Enrichment requirement check:', check) + + // Initialize missing field values object + const initialValues: Record = {} + check.required_checks.forEach(reqCheck => { + if (!reqCheck.is_present) { + initialValues[reqCheck.field_name] = '' + } + }) + setMissingFieldValues(initialValues) } catch (error) { console.error('Failed to check enrichment requirements:', error) setRequirementCheck(null) @@ -245,6 +256,63 @@ const PartEnrichmentModal = ({ } } + // Check if all required fields are filled + const areAllRequiredFieldsFilled = (): boolean => { + if (!requirementCheck || requirementCheck.can_enrich) { + return true // No missing required fields + } + + // Check if all missing required fields have been filled in + const missingRequiredFields = requirementCheck.required_checks.filter( + (check) => !check.is_present + ) + + return missingRequiredFields.every((check) => { + const value = missingFieldValues[check.field_name] + return value && value.trim().length > 0 + }) + } + + // Save missing field values to the part (used before starting enrichment) + const saveMissingFieldsIfNeeded = async (): Promise => { + if (!part.id || areAllRequiredFieldsFilled()) { + return true // Nothing to save or already valid + } + + try { + setIsSavingFields(true) + + // Build update payload + const updateData: any = {} + Object.entries(missingFieldValues).forEach(([fieldName, value]) => { + if (value && value.trim()) { + updateData[fieldName] = value.trim() + } + }) + + if (Object.keys(updateData).length === 0) { + return false + } + + // Update the part + await partsService.updatePart({ + id: part.id, + ...updateData + }) + + // Notify parent of updated part + onPartUpdated({ ...part, ...updateData }) + + return true + } catch (error) { + console.error('Failed to save missing fields:', error) + alert('Failed to save field values. Please try again.') + return false + } finally { + setIsSavingFields(false) + } + } + const loadSupplierCapabilities = async () => { try { const response = await tasksService.getSupplierCapabilities() @@ -273,6 +341,15 @@ const PartEnrichmentModal = ({ return } + // Save missing fields first if needed + if (!areAllRequiredFieldsFilled()) { + const saved = await saveMissingFieldsIfNeeded() + if (!saved) { + alert('Please fill in all required fields') + return + } + } + setIsEnriching(true) setEnrichmentResults([]) setTaskProgress(0) @@ -490,49 +567,69 @@ const PartEnrichmentModal = ({ {/* Enrichment Requirements Warning */} {requirementCheck && !requirementCheck.can_enrich && ( -
+
- +
-

- Missing Required Fields +

+ Required Fields

-

- This part is missing required information for enrichment from{' '} +

+ Please fill in the required information to enrich from{' '} {requirementCheck.supplier_name}:

-
    - {requirementCheck.required_checks.map((check) => ( -
  • - - {check.is_present ? '✓' : '✗'} - - - {check.display_name} - - {!check.is_present && check.validation_message && ( - - - {check.validation_message} - - )} -
  • - ))} -
+ + {/* Input fields for missing required data */} +
+ {requirementCheck.required_checks + .filter((check) => !check.is_present) + .map((check) => { + const isFilled = missingFieldValues[check.field_name]?.trim().length > 0 + return ( +
+ +
+ + setMissingFieldValues({ + ...missingFieldValues, + [check.field_name]: e.target.value, + }) + } + placeholder={`Enter ${check.display_name.toLowerCase()}`} + className={`w-full px-3 py-2 pr-10 border rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors ${ + isFilled + ? 'border-green-500 dark:border-green-600' + : 'border-gray-300 dark:border-gray-600' + }`} + /> + {isFilled && ( + + )} +
+ {check.validation_message && ( +

+ {check.validation_message} +

+ )} +
+ ) + })} +
+ {requirementCheck.suggestions && requirementCheck.suggestions.length > 0 && ( -
-

+

+

Suggestions:

    {requirementCheck.suggestions.map((suggestion, idx) => ( -
  • +
  • • {suggestion}
  • ))} @@ -649,7 +746,8 @@ const PartEnrichmentModal = ({ selectedCapabilities.length === 0 || !selectedSupplier || isCheckingRequirements || - (requirementCheck && !requirementCheck.can_enrich) + isSavingFields || + !areAllRequiredFieldsFilled() } className="px-4 py-2 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-lg transition-colors flex items-center gap-2" title={ @@ -657,19 +755,25 @@ const PartEnrichmentModal = ({ ? 'Select a supplier to start enrichment' : isCheckingRequirements ? 'Checking enrichment requirements...' - : requirementCheck && !requirementCheck.can_enrich - ? `Cannot enrich: Missing required fields (${requirementCheck.missing_required.join(', ')})` - : selectedCapabilities.length === 0 - ? 'Select at least one capability to enrich (e.g., fetch datasheet, fetch image)' - : `Start enriching ${part.name} using ${selectedSupplier} with ${selectedCapabilities.length} selected capabilities` + : isSavingFields + ? 'Saving field values...' + : !areAllRequiredFieldsFilled() + ? 'Fill in all required fields to continue' + : selectedCapabilities.length === 0 + ? 'Select at least one capability to enrich (e.g., fetch datasheet, fetch image)' + : `Start enriching ${part.name} using ${selectedSupplier} with ${selectedCapabilities.length} selected capabilities` } > - {isCheckingRequirements ? ( + {isCheckingRequirements || isSavingFields ? ( ) : ( )} - {isCheckingRequirements ? 'Checking...' : 'Start Enrichment'} + {isCheckingRequirements + ? 'Checking...' + : isSavingFields + ? 'Saving...' + : 'Start Enrichment'}
diff --git a/MakerMatrix/frontend/src/components/tools/ToolDetailModal.tsx b/MakerMatrix/frontend/src/components/tools/ToolDetailModal.tsx new file mode 100644 index 00000000..515151ad --- /dev/null +++ b/MakerMatrix/frontend/src/components/tools/ToolDetailModal.tsx @@ -0,0 +1,525 @@ +import { useState, useEffect } from 'react' +import { + Wrench, + X, + Edit3, + Trash2, + MapPin, + Calendar, + DollarSign, + User, + FileText, + AlertCircle, + CircleCheck, + CircleX, + Package, + CheckCircle, + XCircle, +} from 'lucide-react' +import Modal from '@/components/ui/Modal' +import { toolsService } from '@/services/tools.service' +import { useAuthStore } from '@/store/authStore' +import type { Tool } from '@/types/tools' +import toast from 'react-hot-toast' + +interface ToolDetailModalProps { + isOpen: boolean + onClose: () => void + toolId: string + onEdit?: (tool: Tool) => void + onDelete?: (toolId: string) => void +} + +const ToolDetailModal = ({ isOpen, onClose, toolId, onEdit, onDelete }: ToolDetailModalProps) => { + const [tool, setTool] = useState(null) + const [loading, setLoading] = useState(true) + const [checkoutNotes, setCheckoutNotes] = useState('') + const [checkinNotes, setCheckinNotes] = useState('') + const [isCheckoutMode, setIsCheckoutMode] = useState(false) + const [isCheckinMode, setIsCheckinMode] = useState(false) + const [processingCheckout, setProcessingCheckout] = useState(false) + const { user } = useAuthStore() + + useEffect(() => { + if (isOpen && toolId) { + loadTool() + } + }, [isOpen, toolId]) + + const loadTool = async () => { + try { + setLoading(true) + const data = await toolsService.getTool(toolId) + setTool(data) + } catch (error: any) { + console.error('Failed to load tool:', error) + toast.error('Failed to load tool details') + } finally { + setLoading(false) + } + } + + const handleCheckout = async () => { + if (!tool || !user) return + + try { + setProcessingCheckout(true) + const updatedTool = await toolsService.checkoutTool(tool.id, user.username, checkoutNotes) + setTool(updatedTool) + setCheckoutNotes('') + setIsCheckoutMode(false) + toast.success('Tool checked out successfully') + } catch (error: any) { + console.error('Failed to checkout tool:', error) + toast.error(error.message || 'Failed to checkout tool') + } finally { + setProcessingCheckout(false) + } + } + + const handleCheckin = async () => { + if (!tool) return + + try { + setProcessingCheckout(true) + const updatedTool = await toolsService.checkinTool(tool.id, checkinNotes) + setTool(updatedTool) + setCheckinNotes('') + setIsCheckinMode(false) + toast.success('Tool checked in successfully') + } catch (error: any) { + console.error('Failed to checkin tool:', error) + toast.error(error.message || 'Failed to checkin tool') + } finally { + setProcessingCheckout(false) + } + } + + const formatDate = (dateString?: string) => { + if (!dateString) return '-' + return new Date(dateString).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }) + } + + const formatCurrency = (amount?: number) => { + if (amount === undefined) return '-' + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(amount) + } + + const getConditionColor = (condition: string) => { + switch (condition) { + case 'new': + return 'text-green-500' + case 'good': + return 'text-blue-500' + case 'fair': + return 'text-yellow-500' + case 'poor': + return 'text-orange-500' + case 'broken': + return 'text-red-500' + default: + return 'text-gray-500' + } + } + + const getStatusIcon = (status: string) => { + switch (status) { + case 'available': + return + case 'checked_out': + return + case 'maintenance': + return + case 'retired': + return + default: + return null + } + } + + if (!tool && !loading) { + return null + } + + return ( + + {loading ? ( +
+
+

Loading tool details...

+
+ ) : tool ? ( +
+ {/* Header with status and actions */} +
+
+ +
+

{tool.name}

+ {tool.tool_number && ( +

Tool #{tool.tool_number}

+ )} +
+
+
+ {getStatusIcon(tool.status)} + {tool.status.replace('_', ' ')} +
+
+ + {/* Tool Image */} + {tool.image_url && ( +
+ {tool.name} +
+ )} + + {/* Checkout/Checkin Section */} + {tool.status === 'available' && !isCheckoutMode && ( +
+
+
+ + + Tool is available for checkout + +
+ +
+
+ )} + + {tool.status === 'checked_out' && tool.checked_out_by === user?.username && !isCheckinMode && ( +
+
+
+
+ + + You have this tool checked out + +
+ {tool.checkout_date && ( +

+ Since {formatDate(tool.checkout_date)} +

+ )} +
+ +
+
+ )} + + {tool.status === 'checked_out' && tool.checked_out_by !== user?.username && ( +
+
+ + + Checked out by: {tool.checked_out_by} + + {tool.checkout_date && ( + + (Since {formatDate(tool.checkout_date)}) + + )} +
+ {tool.expected_return_date && ( +

+ Expected return: {formatDate(tool.expected_return_date)} +

+ )} +
+ )} + + {/* Checkout Form */} + {isCheckoutMode && ( +
+

Checkout Tool

+