Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consultants home #8

Merged
merged 5 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
from flask_cors import CORS
from ariadne import load_schema_from_path, make_executable_schema, graphql_sync, snake_case_fallback_resolvers
from ariadne.explorer import ExplorerGraphiQL

from google.oauth2 import id_token
from google.auth.transport import requests
from functools import wraps
from settings import auth_settings # Import your auth settings

from api.queries import query
from api.mutations import mutation
import logging
Expand All @@ -10,6 +16,41 @@
from backend.api.execution_stats import ExecutionStatsExtension
import globals

def verify_token(token):
try:
# Use the client_id from your settings
idinfo = id_token.verify_oauth2_token(token, requests.Request(), auth_settings["client_id"])

if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
raise ValueError('Wrong issuer.')

# ID token is valid. Get the user's Google Account ID from the decoded token.
userid = idinfo['sub']
return userid
except ValueError:
# Invalid token
return None

def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = None
if 'Authorization' in request.headers:
auth_header = request.headers.get('Authorization', '')
if auth_header.startswith('Bearer '):
token = auth_header.split(' ', 1)[1]
else:
token = None
if not token:
return jsonify({'message': 'Token is missing!'}), 401

user_id = verify_token(token)
if not user_id:
return jsonify({'message': 'Token is invalid!'}), 401

return f(*args, **kwargs)
return decorated

app = Flask(__name__)
CORS(app)

Expand All @@ -29,6 +70,7 @@ def graphql_playground():
return explorer_html, 200

@app.route("/graphql", methods=["POST"])
@token_required
def graphql_server():
data = request.get_json()
success, result = graphql_sync(
Expand Down Expand Up @@ -56,3 +98,4 @@ def graphql_server():
app.logger.info("Starting the application")
globals.update()
app.run(debug=args.verbose)

4 changes: 3 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
Flask
Flask-CORS
ariadne
ariadne

google-auth
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export default function ConsultantsAndEngineers() {

return (
<Link
href={`/analytics/datasets/timesheet-this-month?WorkerName=${encodeURIComponent(worker.name)}`}
href={`/home/${encodeURIComponent(worker.slug)}`}
className="block transition-all duration-300 ease-in-out"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
Expand Down
18 changes: 17 additions & 1 deletion frontend/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,29 @@ const authOptions: NextAuthOptions = {
authorization: {
params: {
prompt: "select_account",
access_type: "offline",
response_type: "code",
},
},
}),
],
secret: process.env.NEXTAUTH_SECRET,
debug: true,
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
token.idToken = account.id_token;
}
return token;
},
async session({ session, token }) {
return {
...session,
accessToken: token.accessToken as string,
idToken: token.idToken as string,
};
},
async signIn({ account, profile }) {
return true;
},
Expand All @@ -25,4 +41,4 @@ const authOptions: NextAuthOptions = {

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
export { handler as GET, handler as POST };
112 changes: 41 additions & 71 deletions frontend/src/app/components/ClientStatsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,88 +20,58 @@ const ClientStatsSection: React.FC<ClientStatsSectionProps> = ({ data, selectedS
}
};

const renderStat = (statName: string, title: string, value: string, color?: string, total?: number) => (
<div
className={`${getStatClassName(statName)} transform`}
onClick={() => handleStatClick(statName)}
>
<Stat
title={title}
value={value}
color={color}
total={total}
/>
</div>
);

const stats = [
{ name: 'total', title: 'Active Clients', value: data.timesheet.uniqueClients.toString() },
{ name: 'consulting', title: 'Consulting', value: data.timesheet.byKind.consulting.uniqueClients.toString(), color: '#F59E0B' },
{ name: 'handsOn', title: 'Hands-On', value: data.timesheet.byKind.handsOn.uniqueClients.toString(), color: '#8B5CF6' },
{ name: 'squad', title: 'Squad', value: data.timesheet.byKind.squad.uniqueClients.toString(), color: '#3B82F6' },
{ name: 'internal', title: 'Internal', value: data.timesheet.byKind.internal.uniqueClients.toString(), color: '#10B981' },
];

const selectedStatIndex = stats.findIndex(stat => stat.name === selectedStat);
if (selectedStatIndex !== -1) {
const selectedStat = stats.splice(selectedStatIndex, 1)[0];
stats.unshift(selectedStat);
}

return (
<div className="grid grid-cols-6 gap-4 mb-8">
<div className="col-span-6">
<div className="grid grid-cols-1 lg:grid-cols-6 gap-4">
<div className="lg:col-span-1">
<div className="flex items-center mb-3">
<p className="text-sm font-semibold text-gray-900 uppercase">
ALL TIME
</p>
<div className="flex-grow h-px bg-gray-200 ml-2"></div>
</div>
<div
className={`${getStatClassName('allClients')} transform`}
onClick={() => handleStatClick('allClients')}
>
<Stat
title="All Clients"
value={data.clients.length.toString()}
/>
<div className={`grid grid-cols-1 ${data.clients ? 'lg:grid-cols-6' : 'lg:grid-cols-5'} gap-4`}>
{data.clients && (
<div className="lg:col-span-1">
<div className="flex items-center mb-3">
<p className="text-sm font-semibold text-gray-900 uppercase">
ALL TIME
</p>
<div className="flex-grow h-px bg-gray-200 ml-2"></div>
</div>
{renderStat('allClients', 'All Clients', data.clients.length.toString())}
</div>
</div>
<div className="lg:col-span-5">
)}
<div className={data.clients ? 'lg:col-span-5' : 'lg:col-span-6'}>
<div className="flex items-center mb-3">
<p className="text-sm font-semibold text-gray-900 uppercase">
ACTIVE <span className="text-xs text-gray-600 uppercase">LAST SIX WEEKS</span>
</p>
<div className="flex-grow h-px bg-gray-200 ml-2"></div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-4">
<div
className={`${getStatClassName('total')} transform`}
onClick={() => handleStatClick('total')}
>
<Stat
title="Active Clients"
value={data.timesheet.uniqueClients.toString()}
/>
</div>
<div
className={`${getStatClassName('consulting')} transform`}
onClick={() => handleStatClick('consulting')}
>
<Stat
title="Consulting"
value={data.timesheet.byKind.consulting.uniqueClients.toString()}
color="#F59E0B"
total={data.timesheet.uniqueClients}
/>
</div>
<div
className={`${getStatClassName('handsOn')} transform`}
onClick={() => handleStatClick('handsOn')}
>
<Stat
title="Hands-On"
value={data.timesheet.byKind.handsOn.uniqueClients.toString()}
color="#8B5CF6"
total={data.timesheet.uniqueClients}
/>
</div>
<div
className={`${getStatClassName('squad')} transform`}
onClick={() => handleStatClick('squad')}
>
<Stat
title="Squad"
value={data.timesheet.byKind.squad.uniqueClients.toString()}
color="#3B82F6"
total={data.timesheet.uniqueClients}
/>
</div>
<div
className={`${getStatClassName('internal')} transform`}
onClick={() => handleStatClick('internal')}
>
<Stat
title="Internal"
value={data.timesheet.byKind.internal.uniqueClients.toString()}
color="#10B981"
total={data.timesheet.uniqueClients}
/>
</div>
{stats.map(stat => renderStat(stat.name, stat.title, stat.value, stat.color, data.timesheet.uniqueClients))}
</div>
</div>
</div>
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/app/home/[[...slug]]/AccountManagerHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { Stat } from "@/app/components/analytics/stat";
import Masonry, { ResponsiveMasonry } from "react-responsive-masonry";

import { GET_CLIENT_STATS } from "./AccountManagerHomeQueries";
import TopClients from "./TopClients";
import TopSponsors from "./TopSponsors";
import TopWorkers from "./TopWorkers";
import CasesByContractEnd from "./CasesByContractEnd";
import CasesUpdates from "./CasesUpdates";
import TopClients from "./panels/TopClients";
import TopSponsors from "./panels/TopSponsors";
import TopWorkers from "./panels/TopWorkers";
import CasesByContractEnd from "./panels/CasesByContractEnd";
import CasesUpdates from "./panels/CasesUpdates";

interface AccountManagerHomeProps {
user: {
Expand Down
Loading
Loading