OKR Management: - Database tables for OKRs, key results, and check-ins - Full CRUD API with progress auto-calculation - UI with quarter/year filtering, create/edit dialogs - Key result progress tracking with status indicators - Trigger to auto-update OKR progress from key results Time Tracking: - Database tables for time entries and timesheets - API with timer start/stop, manual entry creation - Week/month/all view with grouped entries by date - Stats for total hours, billable hours, avg per day - Real-time timer with running indicator Both features include RLS policies and proper indexes.
208 lines
6.8 KiB
TypeScript
208 lines
6.8 KiB
TypeScript
import { supabase } from "../_supabase.js";
|
|
|
|
export default async (req: Request) => {
|
|
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
|
|
if (!token) {
|
|
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
const { data: userData } = await supabase.auth.getUser(token);
|
|
if (!userData.user) {
|
|
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
const userId = userData.user.id;
|
|
const url = new URL(req.url);
|
|
|
|
try {
|
|
// GET - Fetch OKRs with key results
|
|
if (req.method === "GET") {
|
|
const quarter = url.searchParams.get("quarter");
|
|
const year = url.searchParams.get("year");
|
|
const status = url.searchParams.get("status");
|
|
|
|
let query = supabase
|
|
.from("staff_okrs")
|
|
.select(`
|
|
*,
|
|
key_results:staff_key_results(*)
|
|
`)
|
|
.or(`user_id.eq.${userId},owner_type.in.(team,company)`)
|
|
.order("created_at", { ascending: false });
|
|
|
|
if (quarter) query = query.eq("quarter", parseInt(quarter));
|
|
if (year) query = query.eq("year", parseInt(year));
|
|
if (status) query = query.eq("status", status);
|
|
|
|
const { data: okrs, error } = await query;
|
|
if (error) throw error;
|
|
|
|
// Calculate stats
|
|
const myOkrs = okrs?.filter(o => o.user_id === userId) || [];
|
|
const stats = {
|
|
total: myOkrs.length,
|
|
active: myOkrs.filter(o => o.status === "active").length,
|
|
completed: myOkrs.filter(o => o.status === "completed").length,
|
|
avgProgress: myOkrs.length > 0
|
|
? Math.round(myOkrs.reduce((sum, o) => sum + (o.progress || 0), 0) / myOkrs.length)
|
|
: 0
|
|
};
|
|
|
|
return new Response(JSON.stringify({ okrs: okrs || [], stats }), {
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
// POST - Create OKR or Key Result
|
|
if (req.method === "POST") {
|
|
const body = await req.json();
|
|
|
|
// Create new OKR
|
|
if (body.action === "create_okr") {
|
|
const { objective, description, quarter, year, team, owner_type } = body;
|
|
|
|
const { data: okr, error } = await supabase
|
|
.from("staff_okrs")
|
|
.insert({
|
|
user_id: userId,
|
|
objective,
|
|
description,
|
|
quarter,
|
|
year,
|
|
team,
|
|
owner_type: owner_type || "individual",
|
|
status: "draft"
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
return new Response(JSON.stringify({ okr }), { status: 201, headers: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
// Add key result to OKR
|
|
if (body.action === "add_key_result") {
|
|
const { okr_id, title, description, target_value, metric_type, unit, due_date } = body;
|
|
|
|
const { data: keyResult, error } = await supabase
|
|
.from("staff_key_results")
|
|
.insert({
|
|
okr_id,
|
|
title,
|
|
description,
|
|
target_value,
|
|
metric_type: metric_type || "percentage",
|
|
unit,
|
|
due_date
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
return new Response(JSON.stringify({ key_result: keyResult }), { status: 201, headers: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
// Update key result progress
|
|
if (body.action === "update_key_result") {
|
|
const { key_result_id, current_value, status } = body;
|
|
|
|
// Get target value to calculate progress
|
|
const { data: kr } = await supabase
|
|
.from("staff_key_results")
|
|
.select("target_value, start_value")
|
|
.eq("id", key_result_id)
|
|
.single();
|
|
|
|
const progress = kr ? Math.min(100, Math.round(((current_value - (kr.start_value || 0)) / (kr.target_value - (kr.start_value || 0))) * 100)) : 0;
|
|
|
|
const { data: keyResult, error } = await supabase
|
|
.from("staff_key_results")
|
|
.update({
|
|
current_value,
|
|
progress: Math.max(0, progress),
|
|
status: status || (progress >= 100 ? "completed" : progress >= 70 ? "on_track" : progress >= 40 ? "at_risk" : "behind"),
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq("id", key_result_id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
return new Response(JSON.stringify({ key_result: keyResult }), { headers: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
// Add check-in
|
|
if (body.action === "add_checkin") {
|
|
const { okr_id, notes, progress_snapshot } = body;
|
|
|
|
const { data: checkin, error } = await supabase
|
|
.from("staff_okr_checkins")
|
|
.insert({
|
|
okr_id,
|
|
user_id: userId,
|
|
notes,
|
|
progress_snapshot
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
return new Response(JSON.stringify({ checkin }), { status: 201, headers: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
return new Response(JSON.stringify({ error: "Invalid action" }), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
// PUT - Update OKR
|
|
if (req.method === "PUT") {
|
|
const body = await req.json();
|
|
const { id, objective, description, status, quarter, year } = body;
|
|
|
|
const { data: okr, error } = await supabase
|
|
.from("staff_okrs")
|
|
.update({
|
|
objective,
|
|
description,
|
|
status,
|
|
quarter,
|
|
year,
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq("id", id)
|
|
.eq("user_id", userId)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
return new Response(JSON.stringify({ okr }), { headers: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
// DELETE - Delete OKR or Key Result
|
|
if (req.method === "DELETE") {
|
|
const id = url.searchParams.get("id");
|
|
const type = url.searchParams.get("type") || "okr";
|
|
|
|
if (type === "key_result") {
|
|
const { error } = await supabase
|
|
.from("staff_key_results")
|
|
.delete()
|
|
.eq("id", id);
|
|
if (error) throw error;
|
|
} else {
|
|
const { error } = await supabase
|
|
.from("staff_okrs")
|
|
.delete()
|
|
.eq("id", id)
|
|
.eq("user_id", userId);
|
|
if (error) throw error;
|
|
}
|
|
|
|
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
|
|
} catch (err: any) {
|
|
console.error("OKR API error:", err);
|
|
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
}
|
|
};
|