initial commit

This commit is contained in:
jason
2026-03-12 17:09:22 -05:00
commit 4982e5392e
35 changed files with 9803 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
// GET /api/admin/settings - Fetch global settings
export async function GET() {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== "ADMIN") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const settings = await prisma.setting.findMany();
const settingsMap = settings.reduce((acc: any, curr: any) => ({ ...acc, [curr.key]: curr.value }), {});
return NextResponse.json(settingsMap);
}
// POST /api/admin/settings - Update or create setting
export async function POST(req: Request) {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== "ADMIN") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { key, value } = await req.json();
const setting = await prisma.setting.upsert({
where: { key },
update: { value },
create: { key, value },
});
return NextResponse.json(setting);
}

View File

@@ -0,0 +1,49 @@
import NextAuth, { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
}
return token;
},
async session({ session, user, token }) {
if (session.user) {
session.user.id = user?.id || (token?.sub as string);
// Fetch fresh role from DB if needed, or use token
const dbUser = await prisma.user.findUnique({ where: { id: session.user.id } });
session.user.role = dbUser?.role || 'EMPLOYEE';
}
return session;
},
},
events: {
async createUser({ user }) {
const userCount = await prisma.user.count();
if (userCount === 1) {
await prisma.user.update({
where: { id: user.id },
data: { role: 'ADMIN' },
});
}
},
},
pages: {
signIn: "/auth/signin",
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

View File

@@ -0,0 +1,56 @@
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
import { uploadToDrive, generateReportMarkdown } from "@/lib/google-drive";
import { getToken } from "next-auth/jwt";
export async function POST(
req: Request,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
// We need the raw access token from JWT for Google API
const token = await getToken({ req: req as any });
if (!session || !token?.accessToken) {
return NextResponse.json({ error: "Unauthorized or missing Google token" }, { status: 401 });
}
const report = await prisma.report.findUnique({
where: { id: params.id, userId: session.user.id },
include: { tasks: true, user: true },
});
if (!report) {
return NextResponse.json({ error: "Report not found" }, { status: 404 });
}
const markdown = generateReportMarkdown(report);
const fileName = `WFH_Report_${new Date(report.date).toISOString().split('T')[0]}_${report.user.name}`;
// Fetch designated folder ID from settings
const folderSetting = await prisma.setting.findUnique({
where: { key: 'GOOGLE_DRIVE_FOLDER_ID' }
});
try {
const driveFile = await uploadToDrive(
token.accessToken as string,
fileName,
markdown,
folderSetting?.value
);
// Update report status to SUBMITTED
await prisma.report.update({
where: { id: params.id },
data: { status: 'SUBMITTED' }
});
return NextResponse.json({ success: true, link: driveFile.webViewLink });
} catch (error) {
return NextResponse.json({ error: "Failed to upload to Drive" }, { status: 500 });
}
}

View File

@@ -0,0 +1,48 @@
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
// PATCH /api/reports/[id] - Update report status or manager
export async function PATCH(
req: Request,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { status, managerName } = await req.json();
const report = await prisma.report.update({
where: { id: params.id, userId: session.user.id },
data: { status, managerName },
});
return NextResponse.json(report);
}
// GET /api/reports/[id] - Fetch a specific report
export async function GET(
req: Request,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const report = await prisma.report.findUnique({
where: { id: params.id },
include: { tasks: true },
});
if (!report || (report.userId !== session.user.id && session.user.role !== "ADMIN")) {
return NextResponse.json({ error: "Not Found" }, { status: 404 });
}
return NextResponse.json(report);
}

View File

@@ -0,0 +1,59 @@
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
// GET /api/reports - Fetch reports for the logged-in user (or all for admin)
export async function GET() {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const where = session.user.role === "ADMIN" ? {} : { userId: session.user.id };
const reports = await prisma.report.findMany({
where,
include: { tasks: true },
orderBy: { date: "desc" },
});
return NextResponse.json(reports);
}
// POST /api/reports - Create or resume a report
export async function POST(req: Request) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await req.json();
const { managerName, date } = body;
// Check if a report already exists for this date and user
const reportDate = date ? new Date(date) : new Date();
reportDate.setHours(0, 0, 0, 0);
let report = await prisma.report.findFirst({
where: {
userId: session.user.id,
date: reportDate,
},
});
if (!report) {
report = await prisma.report.create({
data: {
userId: session.user.id,
managerName,
date: reportDate,
},
include: { tasks: true },
});
}
return NextResponse.json(report);
}

View File

@@ -0,0 +1,76 @@
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
// POST /api/tasks - Add a task to a report
export async function POST(req: Request) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { reportId, type, description, timeEstimate, notes, link, status } = await req.json();
// Verify ownership
const report = await prisma.report.findUnique({
where: { id: reportId, userId: session.user.id },
});
if (!report) {
return NextResponse.json({ error: "Report not found" }, { status: 404 });
}
const task = await prisma.task.create({
data: {
reportId,
type,
description,
timeEstimate,
notes,
link,
status: status || "PENDING",
},
});
return NextResponse.json(task);
}
// PATCH /api/tasks/[id] - Update a task
export async function PATCH(req: Request) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id, type, description, timeEstimate, notes, link, status } = await req.json();
const task = await prisma.task.update({
where: { id },
data: { type, description, timeEstimate, notes, link, status },
});
return NextResponse.json(task);
}
// DELETE /api/tasks/[id] - Delete a task
export async function DELETE(req: Request) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(req.url);
const id = searchParams.get("id");
if (!id) return NextResponse.json({ error: "ID required" }, { status: 400 });
await prisma.task.delete({
where: { id },
});
return NextResponse.json({ success: true });
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

76
src/app/globals.css Normal file
View File

@@ -0,0 +1,76 @@
@import "tailwindcss";
:root {
--bg-dark: #0f172a;
--bg-card: rgba(30, 41, 59, 0.7);
--accent-primary: #38bdf8;
--accent-secondary: #818cf8;
--text-main: #f1f5f9;
--text-dim: #94a3b8;
--glass-border: rgba(255, 255, 255, 0.1);
--glass-highlight: rgba(255, 255, 255, 0.05);
}
body {
background: radial-gradient(circle at top left, #1e293b, #0f172a);
color: var(--text-main);
min-height: 100vh;
}
.glass-card {
background: var(--bg-card);
backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
border-radius: 1rem;
}
.glass-input {
background: rgba(15, 23, 42, 0.5);
border: 1px solid var(--glass-border);
color: white;
transition: all 0.2s ease;
}
.glass-input:focus {
border-color: var(--accent-primary);
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.2);
outline: none;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
color: white;
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: 0.75rem;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
opacity: 0.9;
}
.btn-secondary {
background: var(--glass-highlight);
border: 1px solid var(--glass-border);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.75rem;
transition: all 0.2s ease;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.5s ease forwards;
}

25
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,25 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { AuthProvider } from "@/components/providers/AuthProvider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "WFH Daily Report",
description: "Sleek and modern work from home reporting tool",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}

9
src/app/page.tsx Normal file
View File

@@ -0,0 +1,9 @@
import ReportForm from "@/components/ReportForm";
export default function Home() {
return (
<main className="min-h-screen">
<ReportForm />
</main>
);
}