Add Ratings Functionality
This commit is contained in:
parent
3f354d2271
commit
ab1aa64695
@ -11,7 +11,7 @@ export async function GET() {
|
|||||||
},
|
},
|
||||||
distinct: ['cid'],
|
distinct: ['cid'],
|
||||||
})
|
})
|
||||||
console.log(controllers)
|
// console.log(controllers)
|
||||||
return NextResponse.json(controllers)
|
return NextResponse.json(controllers)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch controller data:', error)
|
console.error('Failed to fetch controller data:', error)
|
||||||
|
@ -11,6 +11,22 @@ const facilityTypes = {
|
|||||||
6: "CTR",
|
6: "CTR",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const ratings = {
|
||||||
|
"-1": "INA",
|
||||||
|
"0": "SUS",
|
||||||
|
"1": "OBS",
|
||||||
|
"2": "S1",
|
||||||
|
"3": "S2",
|
||||||
|
"4": "S3",
|
||||||
|
"5": "C1",
|
||||||
|
"6": "C2",
|
||||||
|
"7": "C3",
|
||||||
|
"8": "I1",
|
||||||
|
"9": "I2",
|
||||||
|
"10": "I3",
|
||||||
|
"11": "SUP",
|
||||||
|
"12": "ADM",
|
||||||
|
}
|
||||||
const CZQM_AIRPORTS = ["CYHZ", "CYFC", "CYQM", "CYSJ", "CYZX", "CYYG", "CYYT", "CYQX", "CYYR", "LFVP", "CYQI", "CYAY", "CYDF", "CYJT"];
|
const CZQM_AIRPORTS = ["CYHZ", "CYFC", "CYQM", "CYSJ", "CYZX", "CYYG", "CYYT", "CYQX", "CYYR", "LFVP", "CYQI", "CYAY", "CYDF", "CYJT"];
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
@ -22,7 +38,7 @@ export async function GET() {
|
|||||||
// Filter controllers in CZQM airspace
|
// Filter controllers in CZQM airspace
|
||||||
const czqmControllers = data.controllers.filter((controller: any) => {
|
const czqmControllers = data.controllers.filter((controller: any) => {
|
||||||
const callsign = controller.callsign;
|
const callsign = controller.callsign;
|
||||||
return callsign.startsWith('CZQM_') ||
|
return callsign.startsWith('CZQM_') || callsign.startsWith('CZQX_') ||
|
||||||
CZQM_AIRPORTS.some(airport => callsign.startsWith(airport));
|
CZQM_AIRPORTS.some(airport => callsign.startsWith(airport));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -41,6 +57,7 @@ export async function GET() {
|
|||||||
callsign: controller.callsign,
|
callsign: controller.callsign,
|
||||||
facilityType: facilityType,
|
facilityType: facilityType,
|
||||||
frequency: controller.frequency,
|
frequency: controller.frequency,
|
||||||
|
rating: ratings[controller.rating],
|
||||||
airport: airport,
|
airport: airport,
|
||||||
lastSeen: new Date(),
|
lastSeen: new Date(),
|
||||||
logonTime: new Date(controller.logon_time),
|
logonTime: new Date(controller.logon_time),
|
||||||
|
@ -9,8 +9,23 @@ import {
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { formatDistanceToNow, format } from "date-fns";
|
import { formatDistanceToNow, format } from "date-fns";
|
||||||
import { Clock, Calendar, Radio } from "lucide-react";
|
import { Clock, Calendar, Radio, Star } from "lucide-react";
|
||||||
|
const ratings = {
|
||||||
|
"-1": "INA",
|
||||||
|
"0": "SUS",
|
||||||
|
"1": "OBS",
|
||||||
|
"2": "S1",
|
||||||
|
"3": "S2",
|
||||||
|
"4": "S3",
|
||||||
|
"5": "C1",
|
||||||
|
"6": "C2",
|
||||||
|
"7": "C3",
|
||||||
|
"8": "I1",
|
||||||
|
"9": "I2",
|
||||||
|
"10": "I3",
|
||||||
|
"11": "SUP",
|
||||||
|
"12": "ADM",
|
||||||
|
}
|
||||||
interface ControllerProfileProps {
|
interface ControllerProfileProps {
|
||||||
sessions: any[];
|
sessions: any[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@ -34,41 +49,48 @@ export function ControllerProfile({ sessions, loading }: ControllerProfileProps)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 p-6">
|
<div className="space-y-6 p-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
<Card className="p-6">
|
{[
|
||||||
<div className="flex items-center gap-3 mb-4">
|
{
|
||||||
<Clock className="h-5 w-5 text-blue-500" />
|
icon: <Clock className="h-5 w-5 text-blue-500" />,
|
||||||
<h3 className="text-lg font-semibold">Total Time</h3>
|
title: "Total Time",
|
||||||
</div>
|
value: `${totalHours}h ${totalMinutes}m`,
|
||||||
<p className="text-3xl font-bold">
|
},
|
||||||
{totalHours}h {totalMinutes}m
|
{
|
||||||
</p>
|
icon: <Calendar className="h-5 w-5 text-blue-500" />,
|
||||||
</Card>
|
title: "Total Sessions",
|
||||||
|
value: sessions.length,
|
||||||
<Card className="p-6">
|
},
|
||||||
<div className="flex items-center gap-3 mb-4">
|
{
|
||||||
<Calendar className="h-5 w-5 text-blue-500" />
|
icon: <Radio className="h-5 w-5 text-blue-500" />,
|
||||||
<h3 className="text-lg font-semibold">Total Sessions</h3>
|
title: "Positions",
|
||||||
</div>
|
value: (
|
||||||
<p className="text-3xl font-bold">{sessions.length}</p>
|
<div className="flex flex-wrap gap-2">
|
||||||
</Card>
|
{positions.map(position => (
|
||||||
|
<span
|
||||||
<Card className="p-6">
|
key={position}
|
||||||
<div className="flex items-center gap-3 mb-4">
|
className="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||||
<Radio className="h-5 w-5 text-blue-500" />
|
>
|
||||||
<h3 className="text-lg font-semibold">Positions</h3>
|
{position}
|
||||||
</div>
|
</span>
|
||||||
<div className="flex flex-wrap gap-2">
|
))}
|
||||||
{positions.map(position => (
|
</div>
|
||||||
<span
|
),
|
||||||
key={position}
|
},
|
||||||
className="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
{
|
||||||
>
|
icon: <Star className="h-5 w-5 text-blue-500" aria-hidden="true" />,
|
||||||
{position}
|
title: "Highest Rating",
|
||||||
</span>
|
value: sessions[0].rating || "N/A",
|
||||||
))}
|
},
|
||||||
</div>
|
].map(({ icon, title, value }, i) => (
|
||||||
</Card>
|
<Card key={i} className="p-6 h-full flex flex-col justify-between">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
{icon}
|
||||||
|
<h3 className="text-lg font-semibold">{title}</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold">{value}</p>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
@ -82,6 +104,7 @@ export function ControllerProfile({ sessions, loading }: ControllerProfileProps)
|
|||||||
<TableHead>Airport</TableHead>
|
<TableHead>Airport</TableHead>
|
||||||
<TableHead>Frequency</TableHead>
|
<TableHead>Frequency</TableHead>
|
||||||
<TableHead>Duration</TableHead>
|
<TableHead>Duration</TableHead>
|
||||||
|
<TableHead>Rating</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -97,6 +120,7 @@ export function ControllerProfile({ sessions, loading }: ControllerProfileProps)
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{formatSessionDuration(session.logonTime, session.lastSeen)}
|
{formatSessionDuration(session.logonTime, session.lastSeen)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>{session.rating}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@ -116,7 +140,8 @@ function formatSessionDuration(start: string, end: string) {
|
|||||||
function LoadingSkeleton() {
|
function LoadingSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 p-6">
|
<div className="space-y-6 p-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
|
<Skeleton className="h-[140px] rounded-lg" />
|
||||||
<Skeleton className="h-[140px] rounded-lg" />
|
<Skeleton className="h-[140px] rounded-lg" />
|
||||||
<Skeleton className="h-[140px] rounded-lg" />
|
<Skeleton className="h-[140px] rounded-lg" />
|
||||||
<Skeleton className="h-[140px] rounded-lg" />
|
<Skeleton className="h-[140px] rounded-lg" />
|
||||||
|
@ -36,6 +36,7 @@ export function ControllerTable({ data, loading }: ControllerTableProps) {
|
|||||||
<TableHead>Current Position</TableHead>
|
<TableHead>Current Position</TableHead>
|
||||||
<TableHead>Airport</TableHead>
|
<TableHead>Airport</TableHead>
|
||||||
<TableHead>Frequency</TableHead>
|
<TableHead>Frequency</TableHead>
|
||||||
|
<TableHead>Rating</TableHead>
|
||||||
<TableHead>Last Seen</TableHead>
|
<TableHead>Last Seen</TableHead>
|
||||||
<TableHead>Session Duration</TableHead>
|
<TableHead>Session Duration</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -52,6 +53,7 @@ export function ControllerTable({ data, loading }: ControllerTableProps) {
|
|||||||
<TableCell>{controller.callsign}</TableCell>
|
<TableCell>{controller.callsign}</TableCell>
|
||||||
<TableCell>{controller.airport}</TableCell>
|
<TableCell>{controller.airport}</TableCell>
|
||||||
<TableCell>{controller.frequency}</TableCell>
|
<TableCell>{controller.frequency}</TableCell>
|
||||||
|
<TableCell>{controller.rating}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{formatDistanceToNow(new Date(controller.lastSeen), { addSuffix: true })}
|
{formatDistanceToNow(new Date(controller.lastSeen), { addSuffix: true })}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -82,6 +84,7 @@ function LoadingSkeleton() {
|
|||||||
<Skeleton className="h-4 w-[120px]" />
|
<Skeleton className="h-4 w-[120px]" />
|
||||||
<Skeleton className="h-4 w-[80px]" />
|
<Skeleton className="h-4 w-[80px]" />
|
||||||
<Skeleton className="h-4 w-[100px]" />
|
<Skeleton className="h-4 w-[100px]" />
|
||||||
|
<Skeleton className="h-4 w-[80px]" />
|
||||||
<Skeleton className="h-4 w-[120px]" />
|
<Skeleton className="h-4 w-[120px]" />
|
||||||
<Skeleton className="h-4 w-[100px]" />
|
<Skeleton className="h-4 w-[100px]" />
|
||||||
</div>
|
</div>
|
||||||
|
22
prisma/migrations/20241129063208_/migration.sql
Normal file
22
prisma/migrations/20241129063208_/migration.sql
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "controller_sessions" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"cid" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"callsign" TEXT NOT NULL,
|
||||||
|
"facilityType" TEXT NOT NULL,
|
||||||
|
"frequency" TEXT NOT NULL,
|
||||||
|
"airport" TEXT NOT NULL,
|
||||||
|
"rating" TEXT NOT NULL,
|
||||||
|
"last_seen" TIMESTAMP(3) NOT NULL,
|
||||||
|
"logon_time" TIMESTAMP(3) NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "controller_sessions_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "controller_sessions_cid_idx" ON "controller_sessions"("cid");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "controller_sessions_last_seen_idx" ON "controller_sessions"("last_seen");
|
2
prisma/migrations/20241129063418_migrate/migration.sql
Normal file
2
prisma/migrations/20241129063418_migrate/migration.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "controller_sessions" ALTER COLUMN "rating" DROP NOT NULL;
|
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
@ -15,6 +15,7 @@ model ControllerSession {
|
|||||||
facilityType String
|
facilityType String
|
||||||
frequency String
|
frequency String
|
||||||
airport String
|
airport String
|
||||||
|
rating String?
|
||||||
lastSeen DateTime @map("last_seen")
|
lastSeen DateTime @map("last_seen")
|
||||||
logonTime DateTime @map("logon_time")
|
logonTime DateTime @map("logon_time")
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user