Replace entire Barclays colour palette (navy #1A2142, lime #C3FB5A, violet #7A0FF9) with Oliver brand tokens: black #1A1A1A, gold #FFCB05, orange #FF5C00, azure #0487B6, sky #5DF5EA, grey #EFEFEF, green #09821F. - Switch font from Inter/Barclays Effra to Arial (system font) - Add new Oliver logo asset (BAR-ModComms-logo-v4.png) - Sidebar: black background, new logo, azure active state - Hero: orange "Intelligent Review" text, hide AI-Powered tagline - Hide ChecksOverview on Home page per Oliver design - Toast notification: orange background with black text - All tables: sky headers, alternating white/grey rows - Campaign badges: gold "In Progress", green "Completed" - Analytics: grey KPI cards, sky accent on Key Insight, oliver trend colours - All buttons: azure fill, pill-shaped (rounded-full) - All tabs/toggles/dropdowns: azure accent colour - Update HTML title to "Mod Comms - Intelligent Review" - Default border radius set to 10px Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
173 lines
No EOL
8.1 KiB
TypeScript
Executable file
173 lines
No EOL
8.1 KiB
TypeScript
Executable file
import React, { useState, useEffect } from 'react';
|
|
import { IPublicClientApplication } from '@azure/msal-browser';
|
|
import { LogoutIcon } from './icons/LogoutIcon';
|
|
import { QuestionMarkIcon } from './icons/QuestionMarkIcon';
|
|
import { getUserInfo, UserInfo } from '../services/authService';
|
|
import { apiService } from '../services/apiService';
|
|
import { useUser } from '../contexts/UserContext';
|
|
import type { UserRole } from '../types';
|
|
|
|
const ROLE_LABELS: Record<UserRole, string> = {
|
|
super_admin: 'Super Admin',
|
|
oversight_admin: 'Oversight Admin',
|
|
agency_admin: 'Agency Admin',
|
|
basic_user: 'Standard User',
|
|
};
|
|
|
|
interface ProfileProps {
|
|
onLogout: () => void;
|
|
msalInstance: IPublicClientApplication;
|
|
}
|
|
|
|
export const Profile: React.FC<ProfileProps> = ({ onLogout, msalInstance }) => {
|
|
const [isQuestionFormVisible, setIsQuestionFormVisible] = useState(false);
|
|
const [question, setQuestion] = useState('');
|
|
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [submitStatus, setSubmitStatus] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
|
|
const { user } = useUser();
|
|
|
|
useEffect(() => {
|
|
const info = getUserInfo(msalInstance);
|
|
setUserInfo(info);
|
|
}, [msalInstance]);
|
|
|
|
const getRoleDisplay = (): string => {
|
|
if (!user) return 'Loading...';
|
|
return ROLE_LABELS[user.role] ?? user.role;
|
|
};
|
|
|
|
const userDetails = userInfo ? {
|
|
'First Name': userInfo.firstName || 'N/A',
|
|
'Last Name': userInfo.lastName || 'N/A',
|
|
'Email': userInfo.email,
|
|
'Role': getRoleDisplay(),
|
|
'Agency': user?.agencyName || 'N/A',
|
|
} : {
|
|
'First Name': '',
|
|
'Last Name': '',
|
|
'Email': '',
|
|
'Role': 'Loading...',
|
|
'Agency': '',
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
onLogout();
|
|
};
|
|
|
|
const handleToggleQuestionForm = () => {
|
|
setIsQuestionFormVisible(prev => !prev);
|
|
};
|
|
|
|
const handleSubmitQuestion = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!question.trim()) {
|
|
setSubmitStatus({ type: 'error', message: 'Please enter a question before submitting.' });
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
setSubmitStatus(null);
|
|
|
|
try {
|
|
await apiService.sendSupportEmail({
|
|
message: question,
|
|
subject: 'Question from Mod Comms User',
|
|
user_name: userInfo ? `${userInfo.firstName} ${userInfo.lastName}`.trim() : undefined,
|
|
user_email: userInfo?.email,
|
|
});
|
|
setSubmitStatus({ type: 'success', message: 'Your question has been submitted. We\'ll get back to you shortly.' });
|
|
setQuestion('');
|
|
setTimeout(() => {
|
|
setIsQuestionFormVisible(false);
|
|
setSubmitStatus(null);
|
|
}, 3000);
|
|
} catch (error) {
|
|
setSubmitStatus({ type: 'error', message: 'Failed to submit question. Please try again later.' });
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="p-4 sm:p-6 lg:p-8 h-full bg-white">
|
|
<header className="mb-8">
|
|
<h1 className="text-3xl lg:text-4xl font-semibold text-oliver-black">Your Profile</h1>
|
|
<p className="text-base lg:text-lg text-oliver-black/60 mt-1">View your account details and manage settings.</p>
|
|
</header>
|
|
|
|
<div className="max-w-3xl">
|
|
<section className="p-6 sm:p-8">
|
|
<h2 className="text-2xl font-semibold text-oliver-black mb-6">Account Information</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-5">
|
|
{Object.entries(userDetails).map(([key, value]) => (
|
|
<div key={key}>
|
|
<p className="text-sm font-semibold text-oliver-black/60 tracking-wide uppercase">{key}</p>
|
|
<p className="text-lg text-oliver-black mt-1">{value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="border-t border-grey-300 mt-8 pt-6 flex flex-col sm:flex-row gap-3">
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex items-center justify-center gap-2 bg-error text-white font-semibold py-2 px-4 rounded-md hover:bg-error/90 transition-colors duration-300"
|
|
>
|
|
<LogoutIcon className="h-5 w-5" />
|
|
Logout
|
|
</button>
|
|
<button
|
|
onClick={handleToggleQuestionForm}
|
|
className="flex items-center justify-center gap-2 text-oliver-azure hover:text-oliver-azure/80 bg-transparent font-semibold py-2 px-4 rounded-md transition-colors duration-300"
|
|
>
|
|
<QuestionMarkIcon className="h-5 w-5" />
|
|
Got a question?
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
{isQuestionFormVisible && (
|
|
<section className="mt-8 p-6 sm:p-8">
|
|
<h2 className="text-2xl font-semibold text-oliver-black mb-4">Ask a Question</h2>
|
|
<form onSubmit={handleSubmitQuestion}>
|
|
<p className="text-oliver-black/60 mb-4">Your question will be sent to the OLIVER Agency support team.</p>
|
|
{submitStatus && (
|
|
<div className={`mb-4 p-3 rounded-md ${submitStatus.type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-700 border border-red-200'}`}>
|
|
{submitStatus.message}
|
|
</div>
|
|
)}
|
|
<textarea
|
|
value={question}
|
|
onChange={(e) => setQuestion(e.target.value)}
|
|
className="w-full p-3 border border-grey-300 rounded-md focus:ring-2 focus:ring-oliver-azure focus:border-oliver-azure transition text-oliver-black"
|
|
placeholder="Type your question here..."
|
|
rows={5}
|
|
required
|
|
disabled={isSubmitting}
|
|
/>
|
|
<div className="mt-4 flex justify-end">
|
|
<button
|
|
type="submit"
|
|
className="bg-oliver-azure text-white font-semibold py-2 px-5 rounded-md hover:bg-oliver-azure transition-colors duration-300 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center gap-2"
|
|
disabled={!question.trim() || isSubmitting}
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Submitting...
|
|
</>
|
|
) : (
|
|
'Submit Question'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}; |