Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker
This commit is contained in:
commit
f7e77f895c
|
|
@ -225,3 +225,49 @@ ipcMain.handle('capture-specific-window', async (event, appTitle: string) => {
|
|||
console.log(appTitle, "IPC HANDLE")
|
||||
return await captureSpecificApp(appTitle)
|
||||
});
|
||||
|
||||
|
||||
ipcMain.handle("get-profile", async () => {
|
||||
try {
|
||||
const response = await api.get("/profile");
|
||||
console.log(response.data)
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(" Profile fetch error:", error.response?.data || error.message);
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.message || "Something went wrong",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
ipcMain.handle("change-password", async (event, data) => {
|
||||
console.log("data-change-password", data)
|
||||
|
||||
|
||||
// Build final payload
|
||||
const payload = {
|
||||
userDetails: {
|
||||
oldPassword: data.oldPassword,
|
||||
password: data.password,
|
||||
cPassword: data.cPassword,
|
||||
},
|
||||
};
|
||||
|
||||
console.log("Change Password payload" , payload)
|
||||
try {
|
||||
const response = await api.patch("/profile", payload);
|
||||
console.log("Chnage-passowrd-response", response)
|
||||
return { success: true, data: response.data };
|
||||
} catch (error: any) {
|
||||
console.error("❌ Change password error:", error.message);
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.message || "Password update failed",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ contextBridge.exposeInMainWorld('electron', {
|
|||
toDate?: string
|
||||
) => ipcRenderer.invoke("get-project-logs", { projectID, fromDate, toDate }),
|
||||
|
||||
getProfile: () => ipcRenderer.invoke("get-profile"),
|
||||
|
||||
changePassword: (data: { oldPassword: string; password: string; cPassword: string }) =>
|
||||
ipcRenderer.invoke("change-password", data),
|
||||
|
||||
} satisfies Window['electron']);
|
||||
|
||||
// function ipcInvoke<Key extends keyof EventPayloadMapping>(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { useDispatch, useSelector } from "react-redux";
|
|||
import { RootState } from "../store";
|
||||
import { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice } from "../store/projectSlice";
|
||||
import { clearUser } from "../store/userSlice";
|
||||
import { setError, setLoading, setProfile } from "../store/profileSlice";
|
||||
|
||||
// interface User {
|
||||
// email: string;
|
||||
|
|
@ -132,6 +133,23 @@ const DashboardLayout = () => {
|
|||
|
||||
const [projectsData, setProjectsData] = useState<Project[]>([]);
|
||||
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
dispatch(setLoading(true));
|
||||
const res = await window.electron.getProfile();
|
||||
console.log("Profile", res)
|
||||
if (res.success && res.data) {
|
||||
dispatch(setProfile(res.data));
|
||||
} else {
|
||||
dispatch(setError("Failed to fetch profile"));
|
||||
}
|
||||
} catch (err: any) {
|
||||
dispatch(setError(err.message));
|
||||
} finally {
|
||||
dispatch(setLoading(false));
|
||||
}
|
||||
};
|
||||
|
||||
// fetch the project list of VA
|
||||
useEffect(() => {
|
||||
const fetchProjects = async () => {
|
||||
|
|
@ -199,6 +217,8 @@ const DashboardLayout = () => {
|
|||
// Set the initial elapsed time in the component state
|
||||
setElapsedTime(initialElapsedTime);
|
||||
}
|
||||
fetchProfile()
|
||||
|
||||
} else {
|
||||
console.warn("⚠️ Unexpected response:", res);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
// src/ui/pages/Settings/ChangePasswordModal.tsx
|
||||
import React, { useState } from "react";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
|
||||
interface ChangePasswordModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const [oldPassword, setOldPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (newPassword !== confirmPassword) {
|
||||
alert("New passwords do not match!");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload = {
|
||||
oldPassword,
|
||||
password: newPassword,
|
||||
cPassword: confirmPassword,
|
||||
};
|
||||
|
||||
// 🔗 Call the Electron bridge function
|
||||
const response = await window.electron.changePassword(payload);
|
||||
|
||||
if (response.success) {
|
||||
alert("Password changed successfully!");
|
||||
onClose();
|
||||
} else {
|
||||
alert(response.message || "Failed to change password.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Password change error:", error);
|
||||
alert("Error changing password.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<dialog open className="modal modal-open">
|
||||
<div className="modal-box w-96">
|
||||
<h3 className="font-bold text-lg mb-3">Change Password</h3>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
{/* Old Password */}
|
||||
<div>
|
||||
<label className="label text-xs font-medium">Current Password</label>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
className="input input-bordered w-full"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* New Password */}
|
||||
<div>
|
||||
<label className="label text-xs font-medium">New Password</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
className="input input-bordered w-full pr-10"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2 text-gray-500"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirm Password */}
|
||||
<div>
|
||||
<label className="label text-xs font-medium">Confirm New Password</label>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
className="input input-bordered w-full"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="modal-action">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-ghost"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`btn btn-sm btn-primary ${loading ? "loading" : ""}`}
|
||||
>
|
||||
{loading ? "Updating..." : "Update"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form method="dialog" className="modal-backdrop" onClick={onClose}>
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePasswordModal;
|
||||
|
|
@ -1,57 +1,136 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { User, Mail } from "lucide-react";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { User, Mail, Phone, Calendar, MapPin, CreditCard, Globe, Lock } from "lucide-react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../store";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
import ChangePasswordModal from "./ChangePasswordModal";
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
const { profile } = useSelector((state: RootState) => state.profile);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
const user = useSelector((state: RootState) => state.user.user);
|
||||
console.log(user , "Line 16")
|
||||
const fullName = `${profile?.firstName || ""} ${profile?.lastName || ""}`.trim();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 ">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-md font-bold flex items-center gap-2">
|
||||
Profile Settings
|
||||
<User size={18} /> Profile Settings
|
||||
</h2>
|
||||
<button className="btn btn-sm btn-outline btn-primary">
|
||||
Change Password
|
||||
<button
|
||||
onClick={() => setModalOpen(true)}
|
||||
className="btn btn-sm btn-outline btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Lock size={14} /> Change Password
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Profile Section */}
|
||||
<div className=" card bg-base-100 border border-base-300 shadow-sm p-4 flex flex-col md:flex-row gap-4">
|
||||
{/* Profile Picture */}
|
||||
<div className="flex flex-col items-center md:w-1/3">
|
||||
{/* Profile Card */}
|
||||
<div className="card bg-base-100 border border-base-300 shadow-sm p-4">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
{/* Compact Avatar Section */}
|
||||
<div className="flex flex-col items-center md:items-start md:w-48 shrink-0">
|
||||
<div className="avatar">
|
||||
<div className="w-28 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 flex justify-center items-center">
|
||||
<User size={60} />
|
||||
<div className="w-20 rounded-full ring ring-primary ring-offset-base-100 ring-offset-1 flex justify-center items-center bg-base-200">
|
||||
<User size={40} className="text-base-content/50" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mt-3 text-lg font-semibold">{user?.name}</h3>
|
||||
</div>
|
||||
|
||||
<div className="divider md:divider-horizontal" />
|
||||
|
||||
{/* Details Section */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 flex-1">
|
||||
<div className="p-2 rounded-md bg-base-200/50">
|
||||
<p className="text-xs text-base-content/70 flex items-center gap-2">
|
||||
<Mail size={14} /> Email
|
||||
<div className="mt-2 text-center md:text-left">
|
||||
<h3 className="text-sm font-semibold">{fullName || "N/A"}</h3>
|
||||
<p className="text-xs text-base-content/60 flex items-center gap-1 justify-center md:justify-start mt-0.5">
|
||||
<Mail size={10} /> {profile?.email || "No Email"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divider md:divider-horizontal my-0 md:mx-2" />
|
||||
|
||||
{/* Compact Details Grid */}
|
||||
<div className="flex-1 grid grid-cols-2 sm:grid-cols-3 gap-2.5">
|
||||
{/* Basic Info */}
|
||||
<div className="p-2 rounded bg-base-200/40">
|
||||
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
|
||||
<Calendar size={11} /> DOB
|
||||
</p>
|
||||
<p className="text-xs font-medium">{profile?.dob || "N/A"}</p>
|
||||
</div>
|
||||
|
||||
<div className="p-2 rounded bg-base-200/40">
|
||||
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
|
||||
🎂 Age
|
||||
</p>
|
||||
<p className="text-xs font-medium">{profile?.age || "N/A"}</p>
|
||||
</div>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div className="p-2 rounded bg-base-200/40">
|
||||
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
|
||||
<Phone size={11} /> Phone
|
||||
</p>
|
||||
<p className="text-xs font-medium truncate" title={profile?.phone || "N/A"}>
|
||||
{profile?.phone || "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-2 rounded bg-base-200/40">
|
||||
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
|
||||
💬 Skype
|
||||
</p>
|
||||
<p className="text-xs font-medium truncate" title={profile?.skype || "Not Linked"}>
|
||||
{profile?.skype || "Not Linked"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Payment Info */}
|
||||
<div className="p-2 rounded bg-base-200/40">
|
||||
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
|
||||
<CreditCard size={11} /> Payment
|
||||
</p>
|
||||
<p className="text-xs font-medium capitalize truncate">
|
||||
{profile?.paymentMethod || "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{profile?.paymentMethod === "payoneer" && (
|
||||
<div className="p-2 rounded bg-base-200/40">
|
||||
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
|
||||
💳 Payoneer
|
||||
</p>
|
||||
<p className="text-xs font-medium truncate" title={profile?.payoneerAccount || "N/A"}>
|
||||
{profile?.payoneerAccount || "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{profile?.paymentMethod === "veem" && (
|
||||
<div className="p-2 rounded bg-base-200/40">
|
||||
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
|
||||
🏦 Veem
|
||||
</p>
|
||||
<p className="text-xs font-medium truncate" title={profile?.veemAccount || "N/A"}>
|
||||
{profile?.veemAccount || "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Location - Full Width */}
|
||||
<div className="p-2 rounded bg-base-200/40 col-span-2 sm:col-span-3">
|
||||
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
|
||||
<MapPin size={11} /> Address
|
||||
</p>
|
||||
<p className="text-xs font-medium line-clamp-2">
|
||||
{[profile?.address, profile?.city, profile?.state, profile?.country]
|
||||
.filter(Boolean)
|
||||
.join(", ") || "N/A"}
|
||||
</p>
|
||||
<p className="text-sm font-medium">{user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Change Password Modal */}
|
||||
<ChangePasswordModal isOpen={modalOpen} onClose={() => setModalOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import { configureStore } from "@reduxjs/toolkit";
|
|||
import windowReducer from "./windowSlice";
|
||||
import projectsReducer from "./projectSlice";
|
||||
import userReducer from "./userSlice";
|
||||
import profileReducer from "./profileSlice";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
window: windowReducer,
|
||||
projects: projectsReducer,
|
||||
user: userReducer,
|
||||
profile: profileReducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
interface ProfileData {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
dob: string;
|
||||
age: string;
|
||||
address: string;
|
||||
bankName: string;
|
||||
accountHolderName: string;
|
||||
accountNumber: string;
|
||||
routingNumber: string;
|
||||
phone:string;
|
||||
skype:string;
|
||||
city:string;
|
||||
state:string;
|
||||
country:string;
|
||||
paymentMethod:string;
|
||||
payoneerAccount:string;
|
||||
veemAccount:string;
|
||||
_id: string;
|
||||
}
|
||||
|
||||
interface ProfileState {
|
||||
profile: ProfileData | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: ProfileState = {
|
||||
profile: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const profileSlice = createSlice({
|
||||
name: "profile",
|
||||
initialState,
|
||||
reducers: {
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload;
|
||||
},
|
||||
setProfile: (state, action: PayloadAction<ProfileData>) => {
|
||||
state.profile = action.payload;
|
||||
state.error = null;
|
||||
},
|
||||
setError: (state, action: PayloadAction<string | null>) => {
|
||||
state.error = action.payload;
|
||||
},
|
||||
clearProfile: (state) => {
|
||||
state.profile = null;
|
||||
state.error = null;
|
||||
state.loading = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setLoading, setProfile, setError, clearProfile } = profileSlice.actions;
|
||||
export default profileSlice.reducer;
|
||||
|
|
@ -63,6 +63,28 @@ interface ProjectLogResponse {
|
|||
}
|
||||
|
||||
|
||||
interface ProfileData {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
dob: string;
|
||||
age: string;
|
||||
address: string;
|
||||
bankName: string;
|
||||
accountHolderName: string;
|
||||
accountNumber: string;
|
||||
routingNumber: string;
|
||||
phone:string;
|
||||
skype:string;
|
||||
city:string;
|
||||
state:string;
|
||||
country:string;
|
||||
paymentMethod:string;
|
||||
payoneerAccount:string;
|
||||
veemAccount:string;
|
||||
_id: string;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electron: {
|
||||
login: (credentials: { email: string; password: string }) => Promise<any>;
|
||||
|
|
@ -109,6 +131,20 @@ interface Window {
|
|||
}
|
||||
>;
|
||||
|
||||
getProfile: () => Promise<{
|
||||
success: boolean;
|
||||
data?: ProfileData;
|
||||
}>;
|
||||
|
||||
|
||||
changePassword: (data: {
|
||||
oldPassword: string;
|
||||
password: string;
|
||||
cPassword: string;
|
||||
}) => Promise<{ success: boolean; message?: string; data?: any }>;
|
||||
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue