Skip to content

Commit

Permalink
fix: remove graphs from operational insights, add line chart, fix wor…
Browse files Browse the repository at this point in the history
…dage
  • Loading branch information
CK-7vn committed Jan 9, 2025
1 parent 2948b37 commit 3e178c2
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 147 deletions.
40 changes: 28 additions & 12 deletions backend/src/database/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ func (db *DB) IncrementUserLogin(username string) error {
log.Errorf("Error getting user by username: %v", err)
return newGetRecordsDBError(err, "users")
}
if err := db.Debug().Exec(
if err := db.Exec(
`INSERT INTO login_metrics (user_id, total, last_login)
VALUES (?, 1, CURRENT_TIMESTAMP)
ON CONFLICT (user_id) DO UPDATE
Expand All @@ -206,7 +206,7 @@ func (db *DB) IncrementUserLogin(username string) error {
now := time.Now()
rounded := now.Truncate(time.Hour)

if err := db.Debug().Exec(
if err := db.Exec(
`INSERT INTO login_activity (time_interval, facility_id, total_logins)
VALUES (?, ?, ?)
ON CONFLICT (time_interval, facility_id)
Expand Down Expand Up @@ -239,17 +239,25 @@ func (db *DB) GetNumberOfActiveUsersForTimePeriod(active bool, days int, facilit
return count, nil
}

func (db *DB) NewUsersInTimePeriod(days int, facilityId *uint) (int64, error) {
var count int64
func (db *DB) NewUsersInTimePeriod(days int, facilityId *uint) (int64, int64, error) {
var admin_count int64
var user_count int64
daysAgo := time.Now().AddDate(0, 0, -days)
tx := db.Model(&models.User{}).Where("created_at >= ? AND role NOT IN ('system_admin', 'admin')", daysAgo)
if facilityId != nil {
tx = tx.Where("facility_id = ?", *facilityId)
}
if err := tx.Count(&count).Error; err != nil {
return 0, newGetRecordsDBError(err, "users")
if err := tx.Count(&user_count).Error; err != nil {
return 0, 0, newGetRecordsDBError(err, "users")
}
return count, nil
tx = db.Model(&models.User{}).Where("created_at >= ? AND role NOT IN ('student', 'system_admin')", daysAgo)
if facilityId != nil {
tx = tx.Where("facility_id = ?", *facilityId)
}
if err := tx.Count(&admin_count).Error; err != nil {
return 0, 0, newGetRecordsDBError(err, "users")
}
return user_count, admin_count, nil
}

func (db *DB) GetTotalLogins(days int, facilityId *uint) (int64, error) {
Expand All @@ -265,16 +273,24 @@ func (db *DB) GetTotalLogins(days int, facilityId *uint) (int64, error) {
return total, nil
}

func (db *DB) GetTotalUsers(facilityId *uint) (int64, error) {
var total int64
func (db *DB) GetTotalUsers(facilityId *uint) (int64, int64, error) {
var totalResidents int64
var totalAdmins int64
tx := db.Model(&models.User{}).Where("role NOT IN ('admin', 'system_admin')")
if facilityId != nil {
tx = tx.Where("facility_id = ?", *facilityId)
}
if err := tx.Count(&total).Error; err != nil {
return 0, newGetRecordsDBError(err, "users")
if err := tx.Count(&totalResidents).Error; err != nil {
return 0, 0, newGetRecordsDBError(err, "users")
}
return total, nil
tx = db.Model(&models.User{}).Where("role NOT IN ('student', 'system_admin')")
if facilityId != nil {
tx = tx.Where("facility_id = ?", *facilityId)
}
if err := tx.Count(&totalAdmins).Error; err != nil {
return 0, 0, newGetRecordsDBError(err, "users")
}
return totalResidents, totalAdmins, nil
}

func (db *DB) GetLoginActivity(days int, facilityID *uint) ([]models.LoginActivity, error) {
Expand Down
50 changes: 27 additions & 23 deletions backend/src/handlers/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,49 +89,53 @@ func (srv *Server) handleLoginMetrics(w http.ResponseWriter, r *http.Request, lo
key := fmt.Sprintf("%s-%d", facility, days)
cached, err := srv.buckets[LoginMetrics].Get(key)
if err != nil && errors.Is(err, nats.ErrKeyNotFound) || clearCache {
acitveUsers, err := srv.Db.GetNumberOfActiveUsersForTimePeriod(true, days, facilityId)
activeUsers, err := srv.Db.GetNumberOfActiveUsersForTimePeriod(true, days, facilityId)
if err != nil {
return newDatabaseServiceError(err)
}
totalLogins, err := srv.Db.GetTotalLogins(days, facilityId)
if err != nil {
return newDatabaseServiceError(err)
}
totalUsers, err := srv.Db.GetTotalUsers(facilityId)
totalResidents, totalAdmins, err := srv.Db.GetTotalUsers(facilityId)
if err != nil {
return newDatabaseServiceError(err)
}
newAdded, err := srv.Db.NewUsersInTimePeriod(days, facilityId)
newResidentsAdded, newAdminsAdded, err := srv.Db.NewUsersInTimePeriod(days, facilityId)
if err != nil {
return newDatabaseServiceError(err)
}
loginTimes, err := srv.Db.GetLoginActivity(days, facilityId)
if err != nil {
return newDatabaseServiceError(err)
}
if totalUsers == 0 {
totalUsers = 1
if totalResidents+totalAdmins == 0 {
totalResidents = 1
}
activityMetrics := struct {
ActiveUsers int64 `json:"active_users"`
TotalLogins int64 `json:"total_logins"`
LoginsPerDay int64 `json:"logins_per_day"`
PercentActive int64 `json:"percent_active"`
PercentInactive int64 `json:"percent_inactive"`
TotalUsers int64 `json:"total_users"`
Facility string `json:"facility,omitempty"`
NewStudentsAdded int64 `json:"new_residents_added"`
PeakLoginTimes []models.LoginActivity `json:"peak_login_times"`
ActiveUsers int64 `json:"active_users"`
TotalLogins int64 `json:"total_logins"`
LoginsPerDay int64 `json:"logins_per_day"`
PercentActive int64 `json:"percent_active"`
PercentInactive int64 `json:"percent_inactive"`
TotalResidents int64 `json:"total_residents"`
TotalAdmins int64 `json:"total_admins"`
Facility string `json:"facility,omitempty"`
NewResidentsAdded int64 `json:"new_residents_added"`
NewAdminsAdded int64 `json:"new_admins_added"`
PeakLoginTimes []models.LoginActivity `json:"peak_login_times"`
}{
ActiveUsers: acitveUsers,
TotalLogins: totalLogins,
LoginsPerDay: totalLogins / int64(days),
PercentActive: acitveUsers / totalUsers * 100,
PercentInactive: 100 - (acitveUsers / totalUsers * 100),
TotalUsers: totalUsers,
Facility: facilityName,
NewStudentsAdded: newAdded,
PeakLoginTimes: loginTimes,
ActiveUsers: activeUsers,
TotalLogins: totalLogins,
LoginsPerDay: totalLogins / int64(days),
PercentActive: activeUsers / totalResidents * 100,
PercentInactive: 100 - (activeUsers / totalResidents * 100),
TotalResidents: totalResidents,
TotalAdmins: totalAdmins,
Facility: facilityName,
NewResidentsAdded: newResidentsAdded,
NewAdminsAdded: newAdminsAdded,
PeakLoginTimes: loginTimes,
}
jsonB, err := json.Marshal(models.DefaultResource(activityMetrics))
if err != nil {
Expand Down
161 changes: 129 additions & 32 deletions frontend/src/Components/EngagementRateGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,135 @@
import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts';
import { useMemo, useContext, useState, useEffect } from 'react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip
} from 'recharts';
import { ThemeContext } from '@/Context/ThemeContext';
import { ResponsiveContainer } from 'recharts';

interface EngagementRateGraphProps {
peak_login_times: { time_interval: string; total_logins: number }[];
}

const EngagementRateGraph = ({
active,
inactive
}: {
active: number;
inactive: number;
}) => {
const data = [
{ name: 'Active', value: active },
{ name: 'Inactive', value: inactive }
];
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042'];
peak_login_times
}: EngagementRateGraphProps) => {
const { theme } = useContext(ThemeContext);

const strokeColor = theme === 'light' ? '#666' : '#CCC';
const lineColor = theme === 'light' ? '#18ABA0' : '#61BAB2';
const backgroundColor = theme === 'light' ? '#FFFFFF' : '#0F2926';

const [interval, setInterval] = useState(2);
useEffect(() => {
const handleResize = () => {
const width = window.outerWidth;
if (width < 640) {
setInterval(6);
} else if (width < 768) {
setInterval(4);
} else if (width < 1024) {
setInterval(3);
} else {
setInterval(2);
}
};

handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const fullDayData = useMemo(() => {
const hoursInDay = Array.from({ length: 24 }, (_, i) => i);
const dataMap = new Map(
hoursInDay.map((hour) => [
hour,
{
time: new Date(2023, 0, 1, hour, 0).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
hour12: true
}),
logins: 0
}
])
);

peak_login_times.forEach(({ time_interval, total_logins }) => {
const date = new Date(time_interval);
const localHour = date.getHours();
const localTime = date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
hour12: true
});

dataMap.set(localHour, {
time: localTime,
logins: total_logins
});
});

return Array.from(dataMap.values());
}, [peak_login_times]);
return (
<>
<PieChart width={400} height={350}>
<Pie
data={data}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={100}
fill="#8884d8"
label
>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</>
<ResponsiveContainer width="100%" height="100%" className="pt-2">
<LineChart
data={fullDayData}
margin={{ top: 20, right: 30, left: 50, bottom: 40 }}
>
<CartesianGrid stroke={strokeColor} strokeDasharray="3 3" />

<XAxis
dataKey="time"
interval={interval}
tickFormatter={(tick: string) => {
const [hours, minutes] = tick.split(':');
const date = new Date();
date.setHours(parseInt(hours), parseInt(minutes));
return date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
hour12: true
});
}}
stroke={strokeColor}
label={{
value: 'Time',
style: { fill: strokeColor },
dy: 20,
zIndex: 100
}}
/>
<YAxis
allowDecimals={false}
label={{
value: 'Logins',
angle: -90,
dx: -20,
style: {
fill: strokeColor,
textAnchor: 'middle'
}
}}
/>
<Tooltip
labelClassName="text-body"
contentStyle={{ backgroundColor: backgroundColor }}
/>
<Line
type="monotone"
dataKey="logins"
stroke={lineColor}
strokeWidth={3}
dot={{ r: 3 }}
activeDot={{ r: 3 }}
/>
</LineChart>
</ResponsiveContainer>
);
};

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/Components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export default function Navbar({
<li>
<Link to="/students">
<ULIComponent icon={AcademicCapIcon} />
Students
Residents
</Link>
</li>
<li>
Expand Down
Loading

0 comments on commit 3e178c2

Please sign in to comment.