Handling logout cookies and other changes

This commit is contained in:
Vipeesh 2026-03-12 07:03:41 +00:00
parent 5274a58bd1
commit f952f65515
16 changed files with 2893 additions and 1791 deletions

View File

@ -48,6 +48,7 @@ import IssueList from './pages/IssueList';
import IssueDetail from './pages/IssueDetail';
import MaintenanceTeamList from './pages/MaintenanceTeamList';
import MaintenanceTeamDetail from './pages/MaintenanceTeamDetail';
import UserProfilePage from './pages/UserProfilePage';
// Layout with Sidebar and Header
const LayoutWithSidebar: React.FC<{ children: React.ReactNode }> = ({ children }) => {
@ -251,6 +252,12 @@ const App: React.FC = () => {
</ProtectedRoute>
} />
<Route path="/user-profile" element={
<ProtectedRoute>
<LayoutWithSidebar><UserProfilePage /></LayoutWithSidebar>
</ProtectedRoute>
} />
{/* Default redirect */}
<Route path="/" element={<Navigate to="/login" replace />} />

View File

@ -16,20 +16,52 @@ const Header: React.FC<HeaderProps> = ({ userEmail }) => {
const { language, changeLanguage } = useLanguage();
const { t } = useTranslation();
const handleLogout = () => {
// const handleLogout = () => {
// localStorage.removeItem('user');
// localStorage.removeItem('sid');
// navigate('/login');
// };
const handleLogout = async () => {
localStorage.removeItem('user');
localStorage.removeItem('sid');
navigate('/login');
try {
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('X-Frappe-CSRF-Token='))
?.split('=')[1] || '';
// Step 1: Kill server-side session in Redis
await fetch('/api/method/frappe.auth.logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrfToken,
},
credentials: 'include',
});
// Step 2: Clear Frappe web session cookies fully
await fetch('/?cmd=web_logout', {
credentials: 'include',
});
} catch (err) {
console.error('Logout error:', err);
} finally {
window.location.href = '/asm_app/login';
}
};
return (
<header className="h-14 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 flex items-center justify-end gap-2 flex-shrink-0">
{/* User Email (optional, can be shown on hover or always) */}
{userEmail && (
{/* {userEmail && (
<div className="hidden md:block text-sm text-gray-600 dark:text-gray-400 mr-2">
{userEmail}
</div>
)}
)} */}
{/* Notification Bell */}
<div className="relative">

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useLanguage } from '../contexts/LanguageContext';
import { useTranslation } from 'react-i18next';
import {
@ -14,7 +14,9 @@ import {
Users,
ShoppingCart,
FileText,
HelpCircle
HelpCircle,
UserCircle
} from 'lucide-react';
interface SidebarLink {
@ -32,9 +34,12 @@ interface SidebarProps {
const Sidebar: React.FC<SidebarProps> = ({ userEmail }) => {
const [isCollapsed, setIsCollapsed] = useState(false);
const location = useLocation();
const navigate = useNavigate();
const { isRTL } = useLanguage();
const { t } = useTranslation();
const [userFullName, setUserFullName] = useState<string>('');
// Get base URL for assets (handles both dev and production)
// BASE_URL in Vite already includes trailing slash in production, but not in dev
const baseUrl = import.meta.env.BASE_URL || '/';
@ -50,6 +55,51 @@ const Sidebar: React.FC<SidebarProps> = ({ userEmail }) => {
? `${baseUrl}sidebar-background.jpg${imageVersion}`
: `${baseUrl}/sidebar-background.jpg${imageVersion}`;
// ✅ Fetch user full name on mount
useEffect(() => {
const fetchUserFullName = async () => {
try {
// First get the logged-in user
const userResponse = await fetch('/api/method/frappe.auth.get_logged_user', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include'
});
const userData = await userResponse.json();
const userEmail = userData.message;
if (userEmail) {
// Then fetch the user's full name
const fullNameResponse = await fetch(`/api/resource/User/${encodeURIComponent(userEmail)}?fields=["full_name"]`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include'
});
const fullNameData = await fullNameResponse.json();
if (fullNameData.data?.full_name) {
setUserFullName(fullNameData.data.full_name);
} else {
// Fallback to email if full name not found
setUserFullName(userEmail);
}
}
} catch (error) {
console.error('Error fetching user full name:', error);
// Fallback to email prop if API fails
if (userEmail) {
setUserFullName(userEmail);
}
}
};
fetchUserFullName();
}, [userEmail]);
// Role-based visibility logic
// const isMaintenanceManagerKASH = userEmail === 'maintenancemanager-kash@gmail.com';
// const isMaintenanceManagerTH = userEmail === 'maintenancemanager-th@gmail.com';
@ -219,6 +269,13 @@ const Sidebar: React.FC<SidebarProps> = ({ userEmail }) => {
const isActive = (path: string) => {
return location.pathname === path;
};
// ✅ Handle User Profile click
const handleUserProfileClick = () => {
navigate('/user-profile');
};
return (
<div
@ -328,15 +385,61 @@ const Sidebar: React.FC<SidebarProps> = ({ userEmail }) => {
{/* User Info & Version (Bottom) */}
<div className={`${isCollapsed ? 'p-2' : 'p-4'} border-t border-white/10 backdrop-blur-sm bg-white/5 space-y-3 relative z-10`}>
{!isCollapsed && userEmail && (
{/* {!isCollapsed && userEmail && (
<div>
<div className="text-white/80 dark:text-white/80 text-xs truncate">
{t('sidebar.loggedInAs')}
</div>
<div className="text-white dark:text-white text-sm font-medium truncate">
{userEmail}
</div>
</div> */}
{!isCollapsed && (userFullName || userEmail) && (
<div>
<div className="text-white/80 dark:text-white/80 text-xs truncate">
{t('sidebar.loggedInAs')}
</div>
<div className="text-white dark:text-white text-sm font-medium truncate">
{userFullName || userEmail}
</div>
{/* ✅ User Profile Button */}
<button
onClick={handleUserProfileClick}
className={`
mt-3 w-full flex items-center justify-center gap-2
px-3 py-2
bg-white/20 hover:bg-white/30
text-white
rounded-lg
transition-all duration-200
text-sm font-medium
${isActive('/user-profile') ? 'bg-white/40 border border-white/50' : ''}
`}
>
<UserCircle size={18} />
<span>User Profile</span>
</button>
</div>
)}
{/* Collapsed state - just show icon button */}
{isCollapsed && (
<button
onClick={handleUserProfileClick}
className={`
w-full flex items-center justify-center
p-2
bg-white/20 hover:bg-white/30
text-white
rounded-lg
transition-all duration-200
${isActive('/user-profile') ? 'bg-white/40 border border-white/50' : ''}
`}
title="User Profile"
>
<UserCircle size={20} />
</button>
)}
{!isCollapsed && (

View File

@ -60,6 +60,8 @@ const AssetDetail: React.FC = () => {
custom_support_plan: '',
custom_spare_parts: [],
custom_total_spare_parts_amount: 0,
custom_moh_supervisor:'',
custom_team_leader: '',
is_existing_asset: true,
__islocal: false,
});
@ -430,6 +432,8 @@ const AssetDetail: React.FC = () => {
location: asset.location || '',
custom_manufacturer: asset.custom_manufacturer || '',
department: asset.department || '',
custom_moh_supervisor: asset.custom_moh_supervisor || '',
custom_team_leader: asset.custom_team_leader || '',
custom_asset_type: asset.custom_asset_type || '',
custom_manufacturing_year: asset.custom_manufacturing_year || '',
custom_model: asset.custom_model || '',
@ -3043,6 +3047,21 @@ const handlePPMPlan = async () => {
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<LinkField
label="MOH Supervisor"
doctype="User"
value={formData.custom_moh_supervisor || ''}
onChange={(val) => setFormData({ ...formData, custom_moh_supervisor: val })}
disabled={isFieldDisabled('custom_moh_supervisor')}
/>
<LinkField
label="Team Leader"
doctype="User"
value={formData.custom_team_leader || ''}
onChange={(val) => setFormData({ ...formData, custom_team_leader: val })}
disabled={isFieldDisabled('custom_team_leader')}
/>
<LinkField
label="Supplier/Vendor"

View File

@ -329,6 +329,7 @@ const AssetList: React.FC = () => {
{ key: 'asset_name', label: t('commonFields.assetName'), default: true },
{ key: 'custom_serial_number', label: t('commonFields.serialNumber'), default: true },
{ key: 'company', label: t('commonFields.company'), default: true },
{ key: 'custom_site', label: 'Site Name', default: false },
{ key: 'location', label: t('commonFields.location'), default: true },
{ key: 'department', label: t('commonFields.department'), default: false },
{ key: 'custom_device_status', label: t('commonFields.deviceStatus'), default: true },
@ -413,6 +414,7 @@ const AssetList: React.FC = () => {
// Filter states
const [filterAssetId, setFilterAssetId] = useState('');
const [filterCompany, setFilterCompany] = useState('');
const [filterSite, setFilterSite] = useState('');
const [filterManufacturer, setFilterManufacturer] = useState('');
const [filterSupplier, setFilterSupplier] = useState('');
const [filterLocation, setFilterLocation] = useState('');
@ -450,6 +452,7 @@ const AssetList: React.FC = () => {
const count = [
filterAssetId,
filterCompany,
filterSite,
filterLocation,
filterDepartment,
filterModality,
@ -464,6 +467,7 @@ const AssetList: React.FC = () => {
}, [
filterAssetId,
filterCompany,
filterSite,
filterLocation,
filterDepartment,
filterModality,
@ -479,6 +483,7 @@ const AssetList: React.FC = () => {
const filters: Record<string, any> = {};
if (filterAssetId) filters['name'] = filterAssetId;
if (filterCompany) filters['company'] = filterCompany;
if (filterSite) filters['custom_site'] = filterSite;
if (filterManufacturer) filters['custom_manufacturer'] = filterManufacturer;
if (filterSupplier) filters['supplier'] = filterSupplier;
if (filterLocation) filters['location'] = filterLocation;
@ -511,13 +516,13 @@ const AssetList: React.FC = () => {
if (page !== 0) {
setPage(0);
}
}, [filterAssetId, filterCompany, filterManufacturer, filterSupplier, filterLocation, filterDepartment, filterModality,
}, [filterAssetId, filterCompany, filterSite, filterManufacturer, filterSupplier, filterLocation, filterDepartment, filterModality,
filterDeviceStatus, filterAssetName, filterSerialNumber, searchTerm]);
// ✅ Clear selection when filters/page change
useEffect(() => {
setSelectedRows(new Set());
}, [filterAssetId, filterCompany, filterManufacturer, filterSupplier, filterLocation, filterDepartment, filterModality,
}, [filterAssetId, filterCompany, filterSite, filterManufacturer, filterSupplier, filterLocation, filterDepartment, filterModality,
filterDeviceStatus, filterAssetName, filterSerialNumber, searchTerm, page]);
// Debounce functions
@ -564,6 +569,7 @@ const AssetList: React.FC = () => {
filters: {
filterAssetId,
filterCompany,
filterSite,
filterLocation,
filterDepartment,
filterModality,
@ -588,6 +594,7 @@ const AssetList: React.FC = () => {
const f = preset.filters;
setFilterAssetId(f.filterAssetId || '');
setFilterCompany(f.filterCompany || '');
setFilterSite(f.filterSite || '');
setFilterLocation(f.filterLocation || '');
setFilterDepartment(f.filterDepartment || '');
setFilterModality(f.filterModality || '');
@ -868,6 +875,7 @@ const AssetList: React.FC = () => {
const handleClearFilters = () => {
setFilterAssetId('');
setFilterCompany('');
setFilterSite('');
setFilterManufacturer('');
setFilterSupplier('');
setFilterLocation('');
@ -884,7 +892,7 @@ const AssetList: React.FC = () => {
if (serialNumberDebounceRef.current) clearTimeout(serialNumberDebounceRef.current);
};
const hasActiveFilters = filterAssetId || filterCompany || filterManufacturer || filterSupplier ||
const hasActiveFilters = filterAssetId || filterCompany || filterSite || filterManufacturer || filterSupplier ||
filterLocation || filterDepartment || filterModality || filterDeviceStatus ||
filterAssetName || filterSerialNumber || searchTerm;
@ -1062,6 +1070,17 @@ const AssetList: React.FC = () => {
</button>
</span>
)}
{filterSite && (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-emerald-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
<span className="font-semibold">Site:</span> {filterSite}
<button
onClick={(e) => { e.stopPropagation(); setFilterSite(''); }}
className="hover:text-red-500 transition-colors"
>
<FaTimes className="text-[9px]" />
</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}
@ -1324,6 +1343,27 @@ const AssetList: React.FC = () => {
)}
</div>
{/* Site Name - NEW FILTER */}
<div className="relative z-[58]">
<LinkField
label="PHCC Site"
doctype="Mobile Team Site"
value={filterSite}
onChange={(val) => setFilterSite(val)}
placeholder="Select Site"
disabled={false}
compact={true}
/>
{filterSite && (
<button
onClick={() => setFilterSite('')}
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
>
<FaTimes size={10} />
</button>
)}
</div>
{/* Device Status */}
<div className="relative">
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">
@ -1349,7 +1389,7 @@ const AssetList: React.FC = () => {
</div>
{/* Location */}
<div className="relative z-[58]">
<div className="relative z-[57]">
<LinkField
label="Location"
doctype="Location"
@ -1370,7 +1410,7 @@ const AssetList: React.FC = () => {
</div>
{/* Department */}
<div className="relative z-[57]">
<div className="relative z-[56]">
<LinkField
label="Department"
doctype="Department"
@ -1391,7 +1431,7 @@ const AssetList: React.FC = () => {
</div>
{/* Modality */}
<div className="relative z-[56]">
<div className="relative z-[55]">
<LinkField
label="Modality"
doctype="Modality"
@ -1412,7 +1452,7 @@ const AssetList: React.FC = () => {
</div>
{/* Manufacturer */}
<div className="relative z-[55]">
<div className="relative z-[54]">
<LinkField
label="Manufacturer"
doctype="Manufacturer"
@ -1433,7 +1473,7 @@ const AssetList: React.FC = () => {
</div>
{/* Supplier */}
<div className="relative z-[54]">
<div className="relative z-[53]">
<LinkField
label="Supplier"
doctype="Supplier"

View File

@ -0,0 +1,649 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
FaArrowLeft,
FaSave,
FaUser,
FaEnvelope,
FaKey,
FaEye,
FaEyeSlash,
FaSpinner,
FaCheckCircle,
FaTimesCircle,
FaExclamationTriangle,
FaUserTag,
FaIdBadge,
FaShieldAlt
} from 'react-icons/fa';
import { toast, ToastContainer, Bounce } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import LinkField from '../components/LinkField';
import {
fetchCurrentUserProfile,
updateUserProfile,
changeUserPassword,
type UserProfile,
type UpdateUserProfileData
} from '../services/userProfileService';
const UserProfilePage: React.FC = () => {
const navigate = useNavigate();
// State
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [isSystemManager, setIsSystemManager] = useState(false);
const [changingPassword, setChangingPassword] = useState(false);
const [profile, setProfile] = useState<UserProfile | null>(null);
const [error, setError] = useState<string | null>(null);
// Form state
const [formData, setFormData] = useState({
first_name: '',
middle_name: '',
last_name: '',
role_profile_name: '',
custom_user_id : '',
});
// Password change state
const [showPasswordSection, setShowPasswordSection] = useState(false);
const [passwordData, setPasswordData] = useState({
old_password: '',
new_password: '',
confirm_password: '',
});
const [showOldPassword, setShowOldPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
// Fetch user profile and check role on mount
useEffect(() => {
const loadProfile = async () => {
try {
setLoading(true);
// NEW: Check if user is System Manager
const roleResponse = await fetch('/api/method/asset_lite.api.user_roles.check_has_role', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
roles: 'System Manager'
})
});
const roleData = await roleResponse.json();
const isSysManager = roleData.message?.has_role || false;
setIsSystemManager(isSysManager);
console.log('Is System Manager:', isSysManager);
// Fetch profile data
const data = await fetchCurrentUserProfile();
// Debug: Log everything
console.log('Full profile data:', JSON.stringify(data, null, 2));
console.log('role_profile_name:', data.role_profile_name);
console.log('All keys:', Object.keys(data));
setProfile(data);
setFormData({
first_name: data.first_name || '',
middle_name: data.middle_name || '',
last_name: data.last_name || '',
role_profile_name: data.role_profile_name || '',
custom_user_id: data.custom_user_id || '',
});
// Debug: Log formData after setting
console.log('FormData role_profile_name:', data.role_profile_name || '');
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to load profile';
setError(errorMsg);
toast.error(errorMsg, {
position: "top-right",
autoClose: 5000,
icon: <FaTimesCircle />
});
} finally {
setLoading(false);
}
};
loadProfile();
}, []);
// Handle form input changes
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
// Handle password input changes
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setPasswordData(prev => ({ ...prev, [name]: value }));
};
// Save profile changes
const handleSaveProfile = async () => {
if (!profile?.email) return;
try {
setSaving(true);
const updateData: UpdateUserProfileData = {
first_name: formData.first_name,
middle_name: formData.middle_name,
last_name: formData.last_name,
role_profile_name: formData.role_profile_name,
};
const updatedProfile = await updateUserProfile(profile.email, updateData);
setProfile(prev => prev ? { ...prev, ...updatedProfile } : updatedProfile);
toast.success('Profile updated successfully!', {
position: "top-right",
autoClose: 3000,
icon: <FaCheckCircle />
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to update profile';
toast.error(errorMsg, {
position: "top-right",
autoClose: 5000,
icon: <FaTimesCircle />
});
} finally {
setSaving(false);
}
};
// Change password
const handleChangePassword = async () => {
// Validation
if (!passwordData.old_password) {
toast.error('Please enter your current password', {
position: "top-right",
autoClose: 3000,
icon: <FaExclamationTriangle />
});
return;
}
if (!passwordData.new_password) {
toast.error('Please enter a new password', {
position: "top-right",
autoClose: 3000,
icon: <FaExclamationTriangle />
});
return;
}
if (passwordData.new_password.length < 8) {
toast.error('Password must be at least 8 characters long', {
position: "top-right",
autoClose: 3000,
icon: <FaExclamationTriangle />
});
return;
}
if (passwordData.new_password !== passwordData.confirm_password) {
toast.error('New passwords do not match', {
position: "top-right",
autoClose: 3000,
icon: <FaExclamationTriangle />
});
return;
}
try {
setChangingPassword(true);
await changeUserPassword(passwordData.new_password, passwordData.old_password);
toast.success('Password changed successfully!', {
position: "top-right",
autoClose: 3000,
icon: <FaCheckCircle />
});
// Clear password fields
setPasswordData({
old_password: '',
new_password: '',
confirm_password: '',
});
setShowPasswordSection(false);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to change password';
toast.error(errorMsg, {
position: "top-right",
autoClose: 5000,
icon: <FaTimesCircle />
});
} finally {
setChangingPassword(false);
}
};
// Loading state
if (loading) {
return (
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
<div className="text-center">
<FaSpinner className="animate-spin text-blue-500 text-4xl mx-auto" />
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading profile...</p>
</div>
</div>
);
}
// Error state
if (error && !profile) {
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">
<FaTimesCircle className="inline mr-2" />
Error Loading Profile
</h2>
<p className="text-red-700 dark:text-red-400">{error}</p>
<button
onClick={() => navigate(-1)}
className="mt-4 bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"
>
Go Back
</button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
<ToastContainer
position="top-right"
autoClose={4000}
hideProgressBar={false}
newestOnTop
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="colored"
transition={Bounce}
/>
{/* Header */}
<div className="mb-6 flex justify-between items-center">
<div className="flex items-center gap-4">
<button
onClick={() => navigate(-1)}
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-2"
>
<FaArrowLeft />
<span className="text-gray-900 dark:text-white text-xl font-semibold">User Profile</span>
</button>
</div>
<button
onClick={handleSaveProfile}
disabled={saving}
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 disabled:opacity-50 shadow-lg transition-all"
>
{saving ? (
<>
<FaSpinner className="animate-spin" />
Saving...
</>
) : (
<>
<FaSave />
Save Changes
</>
)}
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Profile Card */}
<div className="lg:col-span-1">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Profile Header */}
<div className="bg-gradient-to-r from-blue-500 to-blue-600 p-6 text-center">
<div className="w-24 h-24 bg-white/20 rounded-full mx-auto flex items-center justify-center mb-4">
{profile?.user_image ? (
<img
src={profile.user_image}
alt="Profile"
className="w-20 h-20 rounded-full object-cover"
/>
) : (
<FaUser className="text-white text-4xl" />
)}
</div>
<h2 className="text-xl font-bold text-white">{profile?.full_name || 'User'}</h2>
<p className="text-white/80 text-sm mt-1">{profile?.email}</p>
</div>
{/* Profile Stats */}
<div className="p-4 space-y-3">
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<FaIdBadge className="text-blue-500" />
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Username</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">
{profile?.username || '-'}
</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<FaUserTag className="text-green-500" />
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Role Profile</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">
{profile?.role_profile_name || '-'}
</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<FaShieldAlt className="text-purple-500" />
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Status</p>
<p className={`text-sm font-medium ${profile?.enabled ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{profile?.enabled ? 'Active' : 'Inactive'}
</p>
</div>
</div>
</div>
{/* User Roles */}
{/* {profile?.roles && profile.roles.length > 0 && (
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
Assigned Roles
</h3>
<div className="flex flex-wrap gap-2">
{profile.roles.map((role, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-full text-xs font-medium"
>
{role.role}
</span>
))}
</div>
</div>
)} */}
</div>
</div>
{/* Right Column - Edit Forms */}
<div className="lg:col-span-2 space-y-6">
{/* Basic Information */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700 flex items-center gap-2">
<FaUser className="text-blue-500" />
Basic Information
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Email (Read-only) */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Email
<span className="ml-1 text-gray-400">(Read-only)</span>
</label>
<div className="relative">
<FaEnvelope className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={14} />
<input
type="email"
value={profile?.email || ''}
disabled
className="w-full pl-10 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed"
/>
</div>
</div>
{/* Full Name (Read-only) */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Full Name
<span className="ml-1 text-gray-400">(Auto-generated)</span>
</label>
<input
type="text"
value={profile?.full_name || ''}
disabled
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed"
/>
</div>
{/* First Name */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
First Name <span className="text-red-500">*</span>
</label>
<input
type="text"
name="first_name"
value={formData.first_name}
onChange={handleInputChange}
placeholder="Enter first name"
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
{/* Middle Name */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Middle Name
</label>
<input
type="text"
name="middle_name"
value={formData.middle_name}
onChange={handleInputChange}
placeholder="Enter middle name"
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
{/* Last Name */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Last Name
</label>
<input
type="text"
name="last_name"
value={formData.last_name}
onChange={handleInputChange}
placeholder="Enter last name"
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 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">
User ID No
</label>
<input
type="text"
name="custom_user_id"
value={formData.custom_user_id}
onChange={handleInputChange}
placeholder="Enter User No"
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
{/* Role Profile */}
{/* <div>
<LinkField
label="Role Profile"
doctype="Role Profile"
value={formData.role_profile_name}
onChange={(val) => setFormData(prev => ({ ...prev, role_profile_name: val }))}
placeholder="Select Role Profile"
disabled={false}
/>
</div> */}
{/* Role Profile - Only System Manager can edit */}
<div>
<LinkField
label="Role Profile"
doctype="Role Profile"
value={formData.role_profile_name}
onChange={(val) => setFormData(prev => ({ ...prev, role_profile_name: val }))}
placeholder="Select Role Profile"
disabled={!isSystemManager}
/>
</div>
</div>
</div>
{/* Password Section */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center justify-between mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-800 dark:text-white flex items-center gap-2">
<FaKey className="text-orange-500" />
Change Password
</h2>
<button
onClick={() => setShowPasswordSection(!showPasswordSection)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
{showPasswordSection ? 'Cancel' : 'Change Password'}
</button>
</div>
{showPasswordSection ? (
<div className="space-y-4">
{/* Current Password */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Current Password <span className="text-red-500">*</span>
</label>
<div className="relative">
<input
type={showOldPassword ? 'text' : 'password'}
name="old_password"
value={passwordData.old_password}
onChange={handlePasswordChange}
placeholder="Enter current password"
className="w-full px-3 py-2 pr-10 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
<button
type="button"
onClick={() => setShowOldPassword(!showOldPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showOldPassword ? <FaEyeSlash size={14} /> : <FaEye size={14} />}
</button>
</div>
</div>
{/* New Password */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
New Password <span className="text-red-500">*</span>
</label>
<div className="relative">
<input
type={showNewPassword ? 'text' : 'password'}
name="new_password"
value={passwordData.new_password}
onChange={handlePasswordChange}
placeholder="Enter new password (min 8 characters)"
className="w-full px-3 py-2 pr-10 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
<button
type="button"
onClick={() => setShowNewPassword(!showNewPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showNewPassword ? <FaEyeSlash size={14} /> : <FaEye size={14} />}
</button>
</div>
{passwordData.new_password && passwordData.new_password.length < 8 && (
<p className="mt-1 text-xs text-red-500">Password must be at least 8 characters</p>
)}
</div>
{/* Confirm New Password */}
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Confirm New Password <span className="text-red-500">*</span>
</label>
<div className="relative">
<input
type={showConfirmPassword ? 'text' : 'password'}
name="confirm_password"
value={passwordData.confirm_password}
onChange={handlePasswordChange}
placeholder="Confirm new password"
className="w-full px-3 py-2 pr-10 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showConfirmPassword ? <FaEyeSlash size={14} /> : <FaEye size={14} />}
</button>
</div>
{passwordData.confirm_password && passwordData.new_password !== passwordData.confirm_password && (
<p className="mt-1 text-xs text-red-500">Passwords do not match</p>
)}
{passwordData.confirm_password && passwordData.new_password === passwordData.confirm_password && passwordData.new_password.length >= 8 && (
<p className="mt-1 text-xs text-green-500 flex items-center gap-1">
<FaCheckCircle size={10} /> Passwords match
</p>
)}
</div>
{/* Change Password Button */}
<div className="pt-2">
<button
onClick={handleChangePassword}
disabled={changingPassword}
className="w-full bg-orange-600 hover:bg-orange-700 text-white px-4 py-2.5 rounded-lg flex items-center justify-center gap-2 disabled:opacity-50 transition-colors"
>
{changingPassword ? (
<>
<FaSpinner className="animate-spin" />
Changing Password...
</>
) : (
<>
<FaKey />
Change Password
</>
)}
</button>
</div>
</div>
) : (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
<FaKey className="mx-auto text-3xl mb-2 text-gray-300 dark:text-gray-600" />
<p className="text-sm">Click "Change Password" to update your password</p>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default UserProfilePage;

View File

@ -439,7 +439,7 @@ const WorkOrderDetail: React.FC = () => {
try {
setIsLoadingAsset(true);
const response = await apiService.apiCall<any>(
`/api/resource/Asset?filters=[["custom_serial_number","=","${serialNumber}"]]&fields=["name","asset_name","company","department","custom_serial_number","custom_asset_type","custom_manufacturer","supplier","custom_site_contractor","custom_subcontractor","custom_model","custom_service_agreement","custom_service_coverage","custom_start_date","custom_end_date","custom_total_amount","custom_site"]&limit=1`
`/api/resource/Asset?filters=[["custom_serial_number","=","${serialNumber}"]]&fields=["name","asset_name","company","department","custom_serial_number","custom_asset_type","custom_manufacturer","supplier","custom_site_contractor","custom_subcontractor","custom_model","custom_service_agreement","custom_service_coverage","custom_start_date","custom_end_date","custom_total_amount","custom_site","custom_team_leader","custom_moh_supervisor"]&limit=1`
);
if (response?.data && response.data.length > 0) {
return response.data[0];
@ -483,6 +483,8 @@ const WorkOrderDetail: React.FC = () => {
custom_start_date: formatDateForInput(assetData.custom_start_date) || '',
custom_end_date: formatDateForInput(assetData.custom_end_date) || '',
custom_total_amount: assetData.custom_total_amount || 0,
custom_assigned_supervisor: assetData.custom_team_leader || '',
custom_moh_supervisor: assetData.custom_moh_supervisor || '',
}));
};
@ -569,6 +571,9 @@ const WorkOrderDetail: React.FC = () => {
custom_subcontractor: searchParams.get('subcontractor') || '',
company: searchParams.get('company') || '',
site_name: searchParams.get('site_name') || '',
// Supervisor fields from URL params (mapped from Asset)
custom_assigned_supervisor: searchParams.get('team_leader') || '',
custom_moh_supervisor: searchParams.get('moh_supervisor') || '',
};
setFormData(prev => ({
...prev,
@ -758,36 +763,35 @@ useEffect(() => {
}, [isNewWorkOrder]);
// Function to call assign_supervisor_or_technician API before workflow action
// This mirrors Frappe's before_workflow_action client script behavior
const callBeforeWorkflowAction = async (action: string): Promise<{ assigned_to: string | null } | null> => {
if (!workOrderName || isNewWorkOrder) return null;
// const callBeforeWorkflowAction = async (action: string): Promise<{ assigned_to: string | null } | null> => {
// if (!workOrderName || isNewWorkOrder) return null;
// Only call for specific actions that need assignment
const actionsNeedingAssignment = ['Apply', 'Send For Repair'];
if (!actionsNeedingAssignment.includes(action)) {
return null;
}
// // Only call for specific actions that need assignment
// const actionsNeedingAssignment = ['Apply', 'Send For Repair'];
// if (!actionsNeedingAssignment.includes(action)) {
// return null;
// }
try {
const response = await apiService.apiCall<any>(
`/api/method/assign_supervisor_or_technician`,
{
method: 'POST',
body: JSON.stringify({
work_order: workOrderName,
action: action,
asset_type: formData.asset_type || workOrder?.asset_type || ''
})
}
);
// try {
// const response = await apiService.apiCall<any>(
// `/api/method/assign_supervisor_or_technician`,
// {
// method: 'POST',
// body: JSON.stringify({
// work_order: workOrderName,
// action: action,
// asset_type: formData.asset_type || workOrder?.asset_type || ''
// })
// }
// );
return response?.message || null;
} catch (err) {
console.error('Error in before_workflow_action:', err);
// Don't block workflow action if this fails, just log it
return null;
}
};
// return response?.message || null;
// } catch (err) {
// console.error('Error in before_workflow_action:', err);
// // Don't block workflow action if this fails, just log it
// return null;
// }
// };
// Workflow action handler
const handleWorkflowAction = async (action: string, nextState?: string) => {
@ -804,19 +808,9 @@ useEffect(() => {
const loadingToastId = toast.loading(`Applying action "${action}"...`, {
position: "top-right"
});
// Call before_workflow_action logic (assign supervisor/technician)
// This mirrors Frappe's before_workflow_action client script
const assignmentResult = await callBeforeWorkflowAction(action);
if (assignmentResult?.assigned_to) {
toast.info(`Assigned to: ${assignmentResult.assigned_to}`, {
position: "top-right",
autoClose: 4000,
icon: <FaCheckCircle />
});
}
// NOTE: Supervisor/Technician assignment is now handled when Asset is selected
const success = await applyAction(action, nextState);
// Dismiss loading toast

View File

@ -339,6 +339,7 @@ const WorkOrderList: React.FC = () => {
{ key: 'asset_name', label: t('commonFields.assetName'), default: true },
{ key: 'work_order_type', label: t('commonFields.workOrderType'), default: true },
{ key: 'company', label: t('commonFields.company'), default: true },
{ key: 'site_name', label: t('commonFields.siteName') || 'Site Name', default: true },
{ key: 'department', label: t('commonFields.department'), default: true },
{ key: 'repair_status', label: t('commonFields.status'), default: true },
{ key: 'custom_priority_', label: t('commonFields.priority'), default: true },
@ -381,6 +382,7 @@ const WorkOrderList: React.FC = () => {
const [filterAsset, setFilterAsset] = useState('');
const [filterType, setFilterType] = useState('');
const [filterCompany, setFilterCompany] = useState('');
const [filterSiteName, setFilterSiteName] = useState('');
const [filterStatus, setFilterStatus] = useState('');
const [filterSerialNumber, setFilterSerialNumber] = useState('');
const [filterManufacturer, setFilterManufacturer] = useState('');
@ -406,10 +408,12 @@ const WorkOrderList: React.FC = () => {
const company = searchParams.get('company');
const priority = searchParams.get('priority');
const status = searchParams.get('status');
const siteName = searchParams.get('site_name');
if (company) setFilterCompany(company);
if (priority) setFilterPriority(priority);
if (status) setFilterStatus(status);
if (siteName) setFilterSiteName(siteName);
}, [searchParams]);
// ✅ Load asset filter from URL on mount
@ -434,6 +438,7 @@ const WorkOrderList: React.FC = () => {
filterAsset,
filterType,
filterCompany,
filterSiteName,
filterStatus,
filterSerialNumber,
filterManufacturer,
@ -447,6 +452,7 @@ const WorkOrderList: React.FC = () => {
filterAsset,
filterType,
filterCompany,
filterSiteName,
filterStatus,
filterSerialNumber,
filterManufacturer,
@ -461,6 +467,7 @@ const WorkOrderList: React.FC = () => {
if (filterAsset) filters['asset'] = filterAsset;
if (filterType) filters['work_order_type'] = filterType;
if (filterCompany) filters['company'] = filterCompany;
if (filterSiteName) filters['site_name'] = filterSiteName;
if (filterStatus) filters['repair_status'] = filterStatus;
if (filterSerialNumber) filters['custom_serial_number'] = ['like', `%${filterSerialNumber}%`];
if (filterManufacturer) filters['custom_manufacturer'] = filterManufacturer;
@ -472,7 +479,8 @@ const WorkOrderList: React.FC = () => {
filters,
limit,
page * limit,
'creation desc',
// 'creation desc',
'modified desc',
permissionFilters
);
@ -490,13 +498,13 @@ const WorkOrderList: React.FC = () => {
if (page !== 0) {
setPage(0);
}
}, [filterWorkOrderId, filterAsset, filterType, filterCompany, filterStatus,
}, [filterWorkOrderId, filterAsset, filterType, filterCompany, filterSiteName, filterStatus,
filterSerialNumber, filterManufacturer, filterSupplier, filterDepartment, filterPriority]);
// Clear selection when filters/page change
useEffect(() => {
setSelectedRows(new Set());
}, [filterWorkOrderId, filterAsset, filterType, filterCompany, filterStatus,
}, [filterWorkOrderId, filterAsset, filterType, filterCompany, filterSiteName, filterStatus,
filterSerialNumber, filterManufacturer, filterSupplier, filterDepartment, filterPriority, page]);
// Debounce function for serial number
@ -532,6 +540,7 @@ const WorkOrderList: React.FC = () => {
filterAsset,
filterType,
filterCompany,
filterSiteName,
filterStatus,
filterSerialNumber,
filterManufacturer,
@ -555,6 +564,7 @@ const WorkOrderList: React.FC = () => {
setFilterAsset(f.filterAsset || '');
setFilterType(f.filterType || '');
setFilterCompany(f.filterCompany || '');
setFilterSiteName(f.filterSiteName || '');
setFilterStatus(f.filterStatus || '');
setFilterSerialNumber(f.filterSerialNumber || '');
setTempSerialNumber(f.filterSerialNumber || '');
@ -780,6 +790,7 @@ const WorkOrderList: React.FC = () => {
setFilterAsset('');
setFilterType('');
setFilterCompany('');
setFilterSiteName('');
setFilterStatus('');
setFilterSerialNumber('');
setTempSerialNumber('');
@ -789,8 +800,9 @@ const WorkOrderList: React.FC = () => {
setFilterPriority('');
// ✅ Clear URL params when clearing all filters
if (searchParams.has('asset')) {
if (searchParams.has('asset') || searchParams.has('site_name')) {
searchParams.delete('asset');
searchParams.delete('site_name');
setSearchParams(searchParams);
}
@ -798,7 +810,7 @@ const WorkOrderList: React.FC = () => {
};
const hasActiveFilters = filterWorkOrderId || filterAsset || filterType || filterCompany ||
filterStatus || filterSerialNumber || filterManufacturer ||
filterSiteName || filterStatus || filterSerialNumber || filterManufacturer ||
filterSupplier || filterDepartment || filterPriority;
const formatDate = (dateString?: string) => {
@ -1034,6 +1046,14 @@ const WorkOrderList: React.FC = () => {
</button>
</span>
)}
{filterSiteName && (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white/90 text-emerald-700 rounded-full text-[10px] font-medium whitespace-nowrap shadow-sm">
<span className="font-semibold">Site:</span> {filterSiteName}
<button onClick={(e) => { e.stopPropagation(); setFilterSiteName(''); searchParams.delete('site_name'); setSearchParams(searchParams); }} className="hover:text-red-500 transition-colors">
<FaTimes className="text-[9px]" />
</button>
</span>
)}
{filterStatus && (
<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> {filterStatus}
@ -1237,6 +1257,27 @@ const WorkOrderList: React.FC = () => {
)}
</div>
{/* Site Name */}
<div className="relative z-[56]">
<LinkField
label="PHCC Site"
doctype="Mobile Team Site"
value={filterSiteName}
onChange={(val) => setFilterSiteName(val)}
placeholder="Select Site"
disabled={false}
compact={true}
/>
{filterSiteName && (
<button
onClick={() => { setFilterSiteName(''); searchParams.delete('site_name'); setSearchParams(searchParams); }}
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
>
<FaTimes size={10} />
</button>
)}
</div>
{/* Status */}
<div className="relative">
<label className="block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5">
@ -1297,7 +1338,7 @@ const WorkOrderList: React.FC = () => {
</div>
{/* Manufacturer */}
<div className="relative z-[56]">
<div className="relative z-[55]">
<LinkField
label="Manufacturer"
doctype="Manufacturer"
@ -1318,7 +1359,7 @@ const WorkOrderList: React.FC = () => {
</div>
{/* Supplier */}
<div className="relative z-[55]">
<div className="relative z-[54]">
<LinkField
label="Supplier"
doctype="Supplier"
@ -1339,7 +1380,7 @@ const WorkOrderList: React.FC = () => {
</div>
{/* Department */}
<div className="relative z-[54]">
<div className="relative z-[53]">
<LinkField
label="Department"
doctype="Department"
@ -1550,12 +1591,20 @@ const WorkOrderList: React.FC = () => {
)}
</button>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{/* <td className="px-6 py-4 whitespace-nowrap">
<div className="font-medium text-gray-900 dark:text-white">{workOrder.name}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{workOrder.creation ? new Date(workOrder.creation).toLocaleDateString() : ''}
</div>
</td> */}
<td className="px-6 py-4 whitespace-nowrap">
<div className="font-medium text-gray-900 dark:text-white">{workOrder.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{formatDate(workOrder.modified)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">{workOrder.asset_name || '-'}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{workOrder.asset || ''}</div>

View File

@ -3,6 +3,8 @@ import API_CONFIG from '../config/api';
// Asset Interfaces
export interface Asset {
custom_team_leader: string;
custom_moh_supervisor: string;
name: string;
asset_name: string;
company: string;

View File

@ -0,0 +1,190 @@
import apiService from './apiService';
export interface UserProfile {
custom_user_id: string;
name: string;
email: string;
first_name: string;
middle_name?: string;
last_name?: string;
full_name: string;
role_profile_name?: string;
username?: string;
language?: string;
time_zone?: string;
user_image?: string;
enabled: number;
roles: Array<{
role: string;
}>;
}
export interface UpdateUserProfileData {
first_name?: string;
middle_name?: string;
last_name?: string;
role_profile_name?: string;
new_password?: string;
custom_user_id?: string;
}
/**
* Fetch current logged-in user's profile
*/
export const fetchCurrentUserProfile = async (): Promise<UserProfile> => {
try {
// First get the current user's email
const sessionResponse = await fetch('/api/method/frappe.auth.get_logged_user', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
});
const sessionData = await sessionResponse.json();
const userEmail = sessionData.message;
if (!userEmail) {
throw new Error('No user logged in');
}
// Use frappe.client.get which returns all fields including role_profile_name
const response = await fetch('/api/method/frappe.client.get', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
doctype: 'User',
name: userEmail,
}),
});
const result = await response.json();
// Debug: Log the response
console.log('frappe.client.get response:', result);
console.log('role_profile_name:', result.message?.role_profile_name);
if (result.message) {
const userData = result.message as UserProfile;
// Roles are included in the response as a child table
if (!userData.roles) {
userData.roles = [];
}
return userData;
}
throw new Error('Failed to fetch user profile');
} catch (error) {
console.error('Error fetching user profile:', error);
throw error;
}
};
/**
* Update user profile
*/
export const updateUserProfile = async (
userEmail: string,
data: UpdateUserProfileData
): Promise<UserProfile> => {
try {
const response = await fetch(`/api/resource/User/${encodeURIComponent(userEmail)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.exc || result.message || 'Failed to update profile');
}
return result.data as UserProfile;
} catch (error) {
console.error('Error updating user profile:', error);
throw error;
}
};
/**
* Change user password using Frappe's update_password method
*/
export const changeUserPassword = async (
newPassword: string,
oldPassword?: string
): Promise<{ message: string }> => {
try {
const response = await fetch('/api/method/frappe.core.doctype.user.user.update_password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
new_password: newPassword,
old_password: oldPassword,
}),
});
const result = await response.json();
if (!response.ok) {
// Parse error message
let errorMsg = 'Failed to change password';
if (result.exc) {
// Try to extract meaningful error from exception
if (result.exc.includes('Incorrect Old Password')) {
errorMsg = 'Incorrect old password';
} else if (result.exc.includes('Password cannot be same')) {
errorMsg = 'New password cannot be the same as old password';
} else if (result._server_messages) {
try {
const messages = JSON.parse(result._server_messages);
errorMsg = JSON.parse(messages[0]).message || errorMsg;
} catch (e) {
// Use default error
}
}
}
throw new Error(errorMsg);
}
return { message: 'Password changed successfully' };
} catch (error) {
console.error('Error changing password:', error);
throw error;
}
};
/**
* Fetch role profiles for dropdown
*/
export const fetchRoleProfiles = async (): Promise<Array<{ name: string }>> => {
try {
const response = await apiService.apiCall<any>(
`/api/resource/Role Profile?fields=["name"]&limit_page_length=100`
);
return response?.data || [];
} catch (error) {
console.error('Error fetching role profiles:', error);
return [];
}
};
export default {
fetchCurrentUserProfile,
updateUserProfile,
changeUserPassword,
fetchRoleProfiles,
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,8 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Seera Arabia Asset Management System" />
<title>Seera Arabia - Asset Management System</title>
<script type="module" crossorigin src="/assets/asm_ui_app/asm_app/assets/index-DJlPs5uL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/asm_ui_app/asm_app/assets/index-vHgZyeJv.css">
<script type="module" crossorigin src="/assets/asm_ui_app/asm_app/assets/index-BIWpBVtY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/asm_ui_app/asm_app/assets/index-BOZnpaxf.css">
</head>
<body>
<div id="root"></div>

View File

@ -7,8 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Seera Arabia Asset Management System" />
<title>Seera Arabia - Asset Management System</title>
<script type="module" crossorigin src="/assets/asm_ui_app/asm_app/assets/index-DJlPs5uL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/asm_ui_app/asm_app/assets/index-vHgZyeJv.css">
<script type="module" crossorigin src="/assets/asm_ui_app/asm_app/assets/index-BIWpBVtY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/asm_ui_app/asm_app/assets/index-BOZnpaxf.css">
</head>
<body>
<div id="root"></div>