initial commit
This commit is contained in:
37
src/app/api/admin/settings/route.ts
Normal file
37
src/app/api/admin/settings/route.ts
Normal 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);
|
||||
}
|
||||
49
src/app/api/auth/[...nextauth]/route.ts
Normal file
49
src/app/api/auth/[...nextauth]/route.ts
Normal 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 };
|
||||
56
src/app/api/reports/[id]/export/route.ts
Normal file
56
src/app/api/reports/[id]/export/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
48
src/app/api/reports/[id]/route.ts
Normal file
48
src/app/api/reports/[id]/route.ts
Normal 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);
|
||||
}
|
||||
59
src/app/api/reports/route.ts
Normal file
59
src/app/api/reports/route.ts
Normal 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);
|
||||
}
|
||||
76
src/app/api/tasks/route.ts
Normal file
76
src/app/api/tasks/route.ts
Normal 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
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
76
src/app/globals.css
Normal file
76
src/app/globals.css
Normal 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
25
src/app/layout.tsx
Normal 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
9
src/app/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import ReportForm from "@/components/ReportForm";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<ReportForm />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user