1.0.0
This commit is contained in:
@@ -1,4 +0,0 @@
|
|||||||
import Database from 'better-sqlite3';
|
|
||||||
const db = new Database('zamaan.db');
|
|
||||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
||||||
console.log('Tables:', tables);
|
|
||||||
@@ -18,7 +18,6 @@ const selectedTags = ref<string[]>([]);
|
|||||||
let interval: ReturnType<typeof setInterval> | null = null;
|
let interval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
function formatTime(ms: number) {
|
function formatTime(ms: number) {
|
||||||
// Round to nearest minute (10 seconds = 1 minute)
|
|
||||||
const totalMinutes = Math.round(ms / 1000 / 60);
|
const totalMinutes = Math.round(ms / 1000 / 60);
|
||||||
const minutes = totalMinutes % 60;
|
const minutes = totalMinutes % 60;
|
||||||
const hours = Math.floor(totalMinutes / 60);
|
const hours = Math.floor(totalMinutes / 60);
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ export async function validateSession(sessionId: string) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extend session if close to expiry (optional, skipping for simplicity)
|
|
||||||
|
|
||||||
return { session, user };
|
return { session, user };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ if (!user || !user.isSiteAdmin) {
|
|||||||
return Astro.redirect('/dashboard');
|
return Astro.redirect('/dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current settings
|
|
||||||
const registrationSetting = await db.select()
|
const registrationSetting = await db.select()
|
||||||
.from(siteSettings)
|
.from(siteSettings)
|
||||||
.where(eq(siteSettings.key, 'registration_enabled'))
|
.where(eq(siteSettings.key, 'registration_enabled'))
|
||||||
@@ -17,7 +16,6 @@ const registrationSetting = await db.select()
|
|||||||
|
|
||||||
const registrationEnabled = registrationSetting?.value === 'true';
|
const registrationEnabled = registrationSetting?.value === 'true';
|
||||||
|
|
||||||
// Get all users
|
|
||||||
const allUsers = await db.select().from(users).all();
|
const allUsers = await db.select().from(users).all();
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,12 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const registrationEnabled = formData.get('registration_enabled') === 'on';
|
const registrationEnabled = formData.get('registration_enabled') === 'on';
|
||||||
|
|
||||||
// Check if setting exists
|
|
||||||
const existingSetting = await db.select()
|
const existingSetting = await db.select()
|
||||||
.from(siteSettings)
|
.from(siteSettings)
|
||||||
.where(eq(siteSettings.key, 'registration_enabled'))
|
.where(eq(siteSettings.key, 'registration_enabled'))
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (existingSetting) {
|
if (existingSetting) {
|
||||||
// Update
|
|
||||||
await db.update(siteSettings)
|
await db.update(siteSettings)
|
||||||
.set({
|
.set({
|
||||||
value: registrationEnabled ? 'true' : 'false',
|
value: registrationEnabled ? 'true' : 'false',
|
||||||
@@ -28,7 +26,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
})
|
})
|
||||||
.where(eq(siteSettings.key, 'registration_enabled'));
|
.where(eq(siteSettings.key, 'registration_enabled'));
|
||||||
} else {
|
} else {
|
||||||
// Insert
|
|
||||||
await db.insert(siteSettings).values({
|
await db.insert(siteSettings).values({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
key: 'registration_enabled',
|
key: 'registration_enabled',
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ import { eq, count, sql } from 'drizzle-orm';
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request, cookies, redirect }) => {
|
export const POST: APIRoute = async ({ request, cookies, redirect }) => {
|
||||||
// Check if this is the first user
|
|
||||||
const userCountResult = await db.select({ count: count() }).from(users).get();
|
const userCountResult = await db.select({ count: count() }).from(users).get();
|
||||||
const isFirstUser = userCountResult ? userCountResult.count === 0 : true;
|
const isFirstUser = userCountResult ? userCountResult.count === 0 : true;
|
||||||
|
|
||||||
// If not first user, check if registration is enabled
|
|
||||||
if (!isFirstUser) {
|
if (!isFirstUser) {
|
||||||
const registrationSetting = await db.select()
|
const registrationSetting = await db.select()
|
||||||
.from(siteSettings)
|
.from(siteSettings)
|
||||||
@@ -33,7 +31,6 @@ export const POST: APIRoute = async ({ request, cookies, redirect }) => {
|
|||||||
return new Response('Missing fields', { status: 400 });
|
return new Response('Missing fields', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user exists
|
|
||||||
const existingUser = await db.select().from(users).where(eq(users.email, email)).get();
|
const existingUser = await db.select().from(users).where(eq(users.email, email)).get();
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
return new Response('User already exists', { status: 400 });
|
return new Response('User already exists', { status: 400 });
|
||||||
@@ -42,7 +39,6 @@ export const POST: APIRoute = async ({ request, cookies, redirect }) => {
|
|||||||
const passwordHash = await hashPassword(password);
|
const passwordHash = await hashPassword(password);
|
||||||
const userId = nanoid();
|
const userId = nanoid();
|
||||||
|
|
||||||
// Create user
|
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: userId,
|
id: userId,
|
||||||
name,
|
name,
|
||||||
@@ -51,21 +47,18 @@ export const POST: APIRoute = async ({ request, cookies, redirect }) => {
|
|||||||
isSiteAdmin: isFirstUser,
|
isSiteAdmin: isFirstUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create default organization
|
|
||||||
const orgId = nanoid();
|
const orgId = nanoid();
|
||||||
await db.insert(organizations).values({
|
await db.insert(organizations).values({
|
||||||
id: orgId,
|
id: orgId,
|
||||||
name: `${name}'s Organization`,
|
name: `${name}'s Organization`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add user to organization
|
|
||||||
await db.insert(members).values({
|
await db.insert(members).values({
|
||||||
userId,
|
userId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
role: 'owner',
|
role: 'owner',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create session
|
|
||||||
const { sessionId, expiresAt } = await createSession(userId);
|
const { sessionId, expiresAt } = await createSession(userId);
|
||||||
|
|
||||||
cookies.set('session_id', sessionId, {
|
cookies.set('session_id', sessionId, {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export const POST: APIRoute = async ({ locals, redirect, params }) => {
|
|||||||
|
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
// Get user's organization
|
|
||||||
const userOrg = await db.select()
|
const userOrg = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -26,7 +25,6 @@ export const POST: APIRoute = async ({ locals, redirect, params }) => {
|
|||||||
return new Response('Forbidden', { status: 403 });
|
return new Response('Forbidden', { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if category has time entries
|
|
||||||
const hasEntries = await db.select()
|
const hasEntries = await db.select()
|
||||||
.from(timeEntries)
|
.from(timeEntries)
|
||||||
.where(eq(timeEntries.categoryId, id!))
|
.where(eq(timeEntries.categoryId, id!))
|
||||||
@@ -36,7 +34,6 @@ export const POST: APIRoute = async ({ locals, redirect, params }) => {
|
|||||||
return new Response('Cannot delete category with time entries', { status: 400 });
|
return new Response('Cannot delete category with time entries', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete category
|
|
||||||
await db.delete(categories)
|
await db.delete(categories)
|
||||||
.where(and(
|
.where(and(
|
||||||
eq(categories.id, id!),
|
eq(categories.id, id!),
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export const POST: APIRoute = async ({ request, locals, redirect, params }) => {
|
|||||||
return new Response('Name is required', { status: 400 });
|
return new Response('Name is required', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's organization
|
|
||||||
const userOrg = await db.select()
|
const userOrg = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -33,7 +32,6 @@ export const POST: APIRoute = async ({ request, locals, redirect, params }) => {
|
|||||||
return new Response('Forbidden', { status: 403 });
|
return new Response('Forbidden', { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update category
|
|
||||||
await db.update(categories)
|
await db.update(categories)
|
||||||
.set({
|
.set({
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('Name is required', { status: 400 });
|
return new Response('Name is required', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's first organization
|
|
||||||
const userOrg = await db.select()
|
const userOrg = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -28,7 +27,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('No organization found', { status: 400 });
|
return new Response('No organization found', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create category
|
|
||||||
await db.insert(categories).values({
|
await db.insert(categories).values({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
organizationId: userOrg.organizationId,
|
organizationId: userOrg.organizationId,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('Name is required', { status: 400 });
|
return new Response('Name is required', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's first organization
|
|
||||||
const userOrg = await db.select()
|
const userOrg = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -28,7 +27,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('No organization found', { status: 400 });
|
return new Response('No organization found', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create client
|
|
||||||
await db.insert(clients).values({
|
await db.insert(clients).values({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
organizationId: userOrg.organizationId,
|
organizationId: userOrg.organizationId,
|
||||||
|
|||||||
@@ -16,14 +16,12 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('Name is required', { status: 400 });
|
return new Response('Name is required', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create organization
|
|
||||||
const orgId = nanoid();
|
const orgId = nanoid();
|
||||||
await db.insert(organizations).values({
|
await db.insert(organizations).values({
|
||||||
id: orgId,
|
id: orgId,
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add user as owner
|
|
||||||
await db.insert(members).values({
|
await db.insert(members).values({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('Unauthorized', { status: 401 });
|
return new Response('Unauthorized', { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is admin
|
|
||||||
const userMembership = await db.select()
|
const userMembership = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -31,7 +30,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('Invalid role', { status: 400 });
|
return new Response('Invalid role', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can't change owner's role
|
|
||||||
const targetMember = await db.select()
|
const targetMember = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(and(
|
.where(and(
|
||||||
@@ -48,7 +46,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('Cannot change owner role', { status: 403 });
|
return new Response('Cannot change owner role', { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update role
|
|
||||||
await db.update(members)
|
await db.update(members)
|
||||||
.set({ role: newRole })
|
.set({ role: newRole })
|
||||||
.where(and(
|
.where(and(
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('Unauthorized', { status: 401 });
|
return new Response('Unauthorized', { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is admin
|
|
||||||
const userMembership = await db.select()
|
const userMembership = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -31,7 +30,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('Invalid role', { status: 400 });
|
return new Response('Invalid role', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find user by email
|
|
||||||
const invitedUser = await db.select()
|
const invitedUser = await db.select()
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(eq(users.email, email))
|
.where(eq(users.email, email))
|
||||||
@@ -41,7 +39,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('User not found. They must create an account first.', { status: 404 });
|
return new Response('User not found. They must create an account first.', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if already a member
|
|
||||||
const existingMember = await db.select()
|
const existingMember = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(and(
|
.where(and(
|
||||||
@@ -54,7 +51,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('User is already a member', { status: 400 });
|
return new Response('User is already a member', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to organization
|
|
||||||
await db.insert(members).values({
|
await db.insert(members).values({
|
||||||
userId: invitedUser.id,
|
userId: invitedUser.id,
|
||||||
organizationId: userMembership.organizationId,
|
organizationId: userMembership.organizationId,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('Unauthorized', { status: 401 });
|
return new Response('Unauthorized', { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is admin
|
|
||||||
const userMembership = await db.select()
|
const userMembership = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -26,12 +25,10 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('Missing user ID', { status: 400 });
|
return new Response('Missing user ID', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can't remove self
|
|
||||||
if (targetUserId === user.id) {
|
if (targetUserId === user.id) {
|
||||||
return new Response('Cannot remove yourself', { status: 403 });
|
return new Response('Cannot remove yourself', { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can't remove owner
|
|
||||||
const targetMember = await db.select()
|
const targetMember = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(and(
|
.where(and(
|
||||||
@@ -48,7 +45,6 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
|||||||
return new Response('Cannot remove owner', { status: 403 });
|
return new Response('Cannot remove owner', { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove member
|
|
||||||
await db.delete(members)
|
await db.delete(members)
|
||||||
.where(and(
|
.where(and(
|
||||||
eq(members.userId, targetUserId),
|
eq(members.userId, targetUserId),
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export const POST: APIRoute = async ({ params, locals, redirect }) => {
|
|||||||
return new Response(JSON.stringify({ error: 'Entry ID required' }), { status: 400 });
|
return new Response(JSON.stringify({ error: 'Entry ID required' }), { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the entry belongs to the user
|
|
||||||
const entry = await db.select()
|
const entry = await db.select()
|
||||||
.from(timeEntries)
|
.from(timeEntries)
|
||||||
.where(and(
|
.where(and(
|
||||||
@@ -27,7 +26,6 @@ export const POST: APIRoute = async ({ params, locals, redirect }) => {
|
|||||||
return new Response(JSON.stringify({ error: 'Entry not found' }), { status: 404 });
|
return new Response(JSON.stringify({ error: 'Entry not found' }), { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the entry
|
|
||||||
await db.delete(timeEntries)
|
await db.delete(timeEntries)
|
||||||
.where(eq(timeEntries.id, entryId))
|
.where(eq(timeEntries.id, entryId))
|
||||||
.run();
|
.run();
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|||||||
return new Response('Category is required', { status: 400 });
|
return new Response('Category is required', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for running entry
|
|
||||||
const runningEntry = await db.select().from(timeEntries).where(
|
const runningEntry = await db.select().from(timeEntries).where(
|
||||||
and(
|
and(
|
||||||
eq(timeEntries.userId, locals.user.id),
|
eq(timeEntries.userId, locals.user.id),
|
||||||
@@ -33,13 +32,11 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|||||||
return new Response('Timer already running', { status: 400 });
|
return new Response('Timer already running', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get default org (first one)
|
|
||||||
const member = await db.select().from(members).where(eq(members.userId, locals.user.id)).limit(1).get();
|
const member = await db.select().from(members).where(eq(members.userId, locals.user.id)).limit(1).get();
|
||||||
if (!member) {
|
if (!member) {
|
||||||
return new Response('No organization found', { status: 400 });
|
return new Response('No organization found', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify category belongs to user's organization
|
|
||||||
const category = await db.select().from(categories).where(
|
const category = await db.select().from(categories).where(
|
||||||
and(
|
and(
|
||||||
eq(categories.id, categoryId),
|
eq(categories.id, categoryId),
|
||||||
@@ -64,7 +61,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|||||||
description,
|
description,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add tags if provided
|
|
||||||
if (tags.length > 0) {
|
if (tags.length > 0) {
|
||||||
await db.insert(timeEntryTags).values(
|
await db.insert(timeEntryTags).values(
|
||||||
tags.map((tagId: string) => ({
|
tags.map((tagId: string) => ({
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { eq } from 'drizzle-orm';
|
|||||||
const user = Astro.locals.user;
|
const user = Astro.locals.user;
|
||||||
if (!user) return Astro.redirect('/login');
|
if (!user) return Astro.redirect('/login');
|
||||||
|
|
||||||
// Get user's first organization
|
|
||||||
const userMembership = await db.select()
|
const userMembership = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -15,7 +14,6 @@ const userMembership = await db.select()
|
|||||||
|
|
||||||
if (!userMembership) return Astro.redirect('/dashboard');
|
if (!userMembership) return Astro.redirect('/dashboard');
|
||||||
|
|
||||||
// Get all categories for the organization
|
|
||||||
const allCategories = await db.select()
|
const allCategories = await db.select()
|
||||||
.from(categories)
|
.from(categories)
|
||||||
.where(eq(categories.organizationId, userMembership.organizationId))
|
.where(eq(categories.organizationId, userMembership.organizationId))
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { eq, and } from 'drizzle-orm';
|
|||||||
const user = Astro.locals.user;
|
const user = Astro.locals.user;
|
||||||
if (!user) return Astro.redirect('/login');
|
if (!user) return Astro.redirect('/login');
|
||||||
|
|
||||||
// Get user's organizations
|
|
||||||
const userOrgs = await db.select()
|
const userOrgs = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -15,7 +14,6 @@ const userOrgs = await db.select()
|
|||||||
|
|
||||||
const orgIds = userOrgs.map(m => m.organizationId);
|
const orgIds = userOrgs.map(m => m.organizationId);
|
||||||
|
|
||||||
// Get all clients for user's organizations
|
|
||||||
const allClients = orgIds.length > 0
|
const allClients = orgIds.length > 0
|
||||||
? await db.select()
|
? await db.select()
|
||||||
.from(clients)
|
.from(clients)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const userOrgs = await db.select({
|
|||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
// Get stats for first organization
|
|
||||||
const firstOrg = userOrgs[0];
|
const firstOrg = userOrgs[0];
|
||||||
let stats = {
|
let stats = {
|
||||||
totalTimeThisWeek: 0,
|
totalTimeThisWeek: 0,
|
||||||
@@ -30,12 +29,10 @@ let stats = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (firstOrg) {
|
if (firstOrg) {
|
||||||
// Calculate date ranges
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
// Get time entries for this week
|
|
||||||
const weekEntries = await db.select()
|
const weekEntries = await db.select()
|
||||||
.from(timeEntries)
|
.from(timeEntries)
|
||||||
.where(and(
|
.where(and(
|
||||||
@@ -51,7 +48,6 @@ if (firstOrg) {
|
|||||||
return sum;
|
return sum;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Get time entries for this month
|
|
||||||
const monthEntries = await db.select()
|
const monthEntries = await db.select()
|
||||||
.from(timeEntries)
|
.from(timeEntries)
|
||||||
.where(and(
|
.where(and(
|
||||||
@@ -67,7 +63,6 @@ if (firstOrg) {
|
|||||||
return sum;
|
return sum;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Count active timers
|
|
||||||
const activeCount = await db.select()
|
const activeCount = await db.select()
|
||||||
.from(timeEntries)
|
.from(timeEntries)
|
||||||
.where(and(
|
.where(and(
|
||||||
@@ -78,7 +73,6 @@ if (firstOrg) {
|
|||||||
|
|
||||||
stats.activeTimers = activeCount.length;
|
stats.activeTimers = activeCount.length;
|
||||||
|
|
||||||
// Count clients
|
|
||||||
const clientCount = await db.select()
|
const clientCount = await db.select()
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.where(eq(clients.organizationId, firstOrg.organizationId))
|
.where(eq(clients.organizationId, firstOrg.organizationId))
|
||||||
@@ -86,7 +80,6 @@ if (firstOrg) {
|
|||||||
|
|
||||||
stats.totalClients = clientCount.length;
|
stats.totalClients = clientCount.length;
|
||||||
|
|
||||||
// Get recent entries
|
|
||||||
stats.recentEntries = await db.select({
|
stats.recentEntries = await db.select({
|
||||||
entry: timeEntries,
|
entry: timeEntries,
|
||||||
client: clients,
|
client: clients,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { eq, and, gte, lte, sql, desc } from 'drizzle-orm';
|
|||||||
const user = Astro.locals.user;
|
const user = Astro.locals.user;
|
||||||
if (!user) return Astro.redirect('/login');
|
if (!user) return Astro.redirect('/login');
|
||||||
|
|
||||||
// Get user's organization
|
|
||||||
const userMembership = await db.select()
|
const userMembership = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -19,7 +18,6 @@ const userMembership = await db.select()
|
|||||||
|
|
||||||
if (!userMembership) return Astro.redirect('/dashboard');
|
if (!userMembership) return Astro.redirect('/dashboard');
|
||||||
|
|
||||||
// Get all team members
|
|
||||||
const teamMembers = await db.select({
|
const teamMembers = await db.select({
|
||||||
id: users.id,
|
id: users.id,
|
||||||
name: users.name,
|
name: users.name,
|
||||||
@@ -30,26 +28,22 @@ const teamMembers = await db.select({
|
|||||||
.where(eq(members.organizationId, userMembership.organizationId))
|
.where(eq(members.organizationId, userMembership.organizationId))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
// Get all categories
|
|
||||||
const allCategories = await db.select()
|
const allCategories = await db.select()
|
||||||
.from(categories)
|
.from(categories)
|
||||||
.where(eq(categories.organizationId, userMembership.organizationId))
|
.where(eq(categories.organizationId, userMembership.organizationId))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
// Get all clients
|
|
||||||
const allClients = await db.select()
|
const allClients = await db.select()
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.where(eq(clients.organizationId, userMembership.organizationId))
|
.where(eq(clients.organizationId, userMembership.organizationId))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
// Parse filter parameters
|
|
||||||
const url = new URL(Astro.request.url);
|
const url = new URL(Astro.request.url);
|
||||||
const selectedMemberId = url.searchParams.get('member') || '';
|
const selectedMemberId = url.searchParams.get('member') || '';
|
||||||
const selectedCategoryId = url.searchParams.get('category') || '';
|
const selectedCategoryId = url.searchParams.get('category') || '';
|
||||||
const selectedClientId = url.searchParams.get('client') || '';
|
const selectedClientId = url.searchParams.get('client') || '';
|
||||||
const timeRange = url.searchParams.get('range') || 'week';
|
const timeRange = url.searchParams.get('range') || 'week';
|
||||||
|
|
||||||
// Calculate date range
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let startDate = new Date();
|
let startDate = new Date();
|
||||||
let endDate = new Date();
|
let endDate = new Date();
|
||||||
@@ -77,7 +71,6 @@ switch (timeRange) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build query conditions
|
|
||||||
const conditions = [
|
const conditions = [
|
||||||
eq(timeEntries.organizationId, userMembership.organizationId),
|
eq(timeEntries.organizationId, userMembership.organizationId),
|
||||||
gte(timeEntries.startTime, startDate),
|
gte(timeEntries.startTime, startDate),
|
||||||
@@ -96,7 +89,6 @@ if (selectedClientId) {
|
|||||||
conditions.push(eq(timeEntries.clientId, selectedClientId));
|
conditions.push(eq(timeEntries.clientId, selectedClientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch detailed entries
|
|
||||||
const entries = await db.select({
|
const entries = await db.select({
|
||||||
entry: timeEntries,
|
entry: timeEntries,
|
||||||
user: users,
|
user: users,
|
||||||
@@ -111,7 +103,6 @@ const entries = await db.select({
|
|||||||
.orderBy(desc(timeEntries.startTime))
|
.orderBy(desc(timeEntries.startTime))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
// Calculate statistics by member
|
|
||||||
const statsByMember = teamMembers.map(member => {
|
const statsByMember = teamMembers.map(member => {
|
||||||
const memberEntries = entries.filter(e => e.user.id === member.id);
|
const memberEntries = entries.filter(e => e.user.id === member.id);
|
||||||
const totalTime = memberEntries.reduce((sum, e) => {
|
const totalTime = memberEntries.reduce((sum, e) => {
|
||||||
@@ -128,7 +119,6 @@ const statsByMember = teamMembers.map(member => {
|
|||||||
};
|
};
|
||||||
}).sort((a, b) => b.totalTime - a.totalTime);
|
}).sort((a, b) => b.totalTime - a.totalTime);
|
||||||
|
|
||||||
// Calculate statistics by category
|
|
||||||
const statsByCategory = allCategories.map(category => {
|
const statsByCategory = allCategories.map(category => {
|
||||||
const categoryEntries = entries.filter(e => e.category.id === category.id);
|
const categoryEntries = entries.filter(e => e.category.id === category.id);
|
||||||
const totalTime = categoryEntries.reduce((sum, e) => {
|
const totalTime = categoryEntries.reduce((sum, e) => {
|
||||||
@@ -145,7 +135,6 @@ const statsByCategory = allCategories.map(category => {
|
|||||||
};
|
};
|
||||||
}).sort((a, b) => b.totalTime - a.totalTime);
|
}).sort((a, b) => b.totalTime - a.totalTime);
|
||||||
|
|
||||||
// Calculate statistics by client
|
|
||||||
const statsByClient = allClients.map(client => {
|
const statsByClient = allClients.map(client => {
|
||||||
const clientEntries = entries.filter(e => e.client.id === client.id);
|
const clientEntries = entries.filter(e => e.client.id === client.id);
|
||||||
const totalTime = clientEntries.reduce((sum, e) => {
|
const totalTime = clientEntries.reduce((sum, e) => {
|
||||||
@@ -162,7 +151,6 @@ const statsByClient = allClients.map(client => {
|
|||||||
};
|
};
|
||||||
}).sort((a, b) => b.totalTime - a.totalTime);
|
}).sort((a, b) => b.totalTime - a.totalTime);
|
||||||
|
|
||||||
// Calculate total time
|
|
||||||
const totalTime = entries.reduce((sum, e) => {
|
const totalTime = entries.reduce((sum, e) => {
|
||||||
if (e.entry.endTime) {
|
if (e.entry.endTime) {
|
||||||
return sum + (e.entry.endTime.getTime() - e.entry.startTime.getTime());
|
return sum + (e.entry.endTime.getTime() - e.entry.startTime.getTime());
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { eq } from 'drizzle-orm';
|
|||||||
const user = Astro.locals.user;
|
const user = Astro.locals.user;
|
||||||
if (!user) return Astro.redirect('/login');
|
if (!user) return Astro.redirect('/login');
|
||||||
|
|
||||||
// Get user's first organization
|
|
||||||
const userMembership = await db.select()
|
const userMembership = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -16,7 +15,6 @@ const userMembership = await db.select()
|
|||||||
|
|
||||||
if (!userMembership) return Astro.redirect('/dashboard');
|
if (!userMembership) return Astro.redirect('/dashboard');
|
||||||
|
|
||||||
// Get all team members for this organization
|
|
||||||
const teamMembers = await db.select({
|
const teamMembers = await db.select({
|
||||||
member: members,
|
member: members,
|
||||||
user: users,
|
user: users,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { eq } from 'drizzle-orm';
|
|||||||
const user = Astro.locals.user;
|
const user = Astro.locals.user;
|
||||||
if (!user) return Astro.redirect('/login');
|
if (!user) return Astro.redirect('/login');
|
||||||
|
|
||||||
// Get user's membership to check if they're admin
|
|
||||||
const userMembership = await db.select()
|
const userMembership = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { eq } from 'drizzle-orm';
|
|||||||
const user = Astro.locals.user;
|
const user = Astro.locals.user;
|
||||||
if (!user) return Astro.redirect('/login');
|
if (!user) return Astro.redirect('/login');
|
||||||
|
|
||||||
// Get user's membership to check if they're admin
|
|
||||||
const userMembership = await db.select()
|
const userMembership = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -19,7 +18,6 @@ if (!userMembership) return Astro.redirect('/dashboard');
|
|||||||
const isAdmin = userMembership.role === 'owner' || userMembership.role === 'admin';
|
const isAdmin = userMembership.role === 'owner' || userMembership.role === 'admin';
|
||||||
if (!isAdmin) return Astro.redirect('/dashboard/team');
|
if (!isAdmin) return Astro.redirect('/dashboard/team');
|
||||||
|
|
||||||
// Get all categories for the organization
|
|
||||||
const allCategories = await db.select()
|
const allCategories = await db.select()
|
||||||
.from(categories)
|
.from(categories)
|
||||||
.where(eq(categories.organizationId, userMembership.organizationId))
|
.where(eq(categories.organizationId, userMembership.organizationId))
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
import DashboardLayout from '../../../../../layouts/DashboardLayout.astro';
|
import DashboardLayout from '../../../../../../layouts/DashboardLayout.astro';
|
||||||
import { Icon } from 'astro-icon/components';
|
import { Icon } from 'astro-icon/components';
|
||||||
import { db } from '../../../../../db';
|
import { db } from '../../../../../../db';
|
||||||
import { categories, members } from '../../../../../db/schema';
|
import { categories, members } from '../../../../../../db/schema';
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
|
||||||
const user = Astro.locals.user;
|
const user = Astro.locals.user;
|
||||||
@@ -10,7 +10,6 @@ if (!user) return Astro.redirect('/login');
|
|||||||
|
|
||||||
const { id } = Astro.params;
|
const { id } = Astro.params;
|
||||||
|
|
||||||
// Get user's membership
|
|
||||||
const userMembership = await db.select()
|
const userMembership = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -21,7 +20,6 @@ if (!userMembership) return Astro.redirect('/dashboard');
|
|||||||
const isAdmin = userMembership.role === 'owner' || userMembership.role === 'admin';
|
const isAdmin = userMembership.role === 'owner' || userMembership.role === 'admin';
|
||||||
if (!isAdmin) return Astro.redirect('/dashboard/team/settings');
|
if (!isAdmin) return Astro.redirect('/dashboard/team/settings');
|
||||||
|
|
||||||
// Get category
|
|
||||||
const category = await db.select()
|
const category = await db.select()
|
||||||
.from(categories)
|
.from(categories)
|
||||||
.where(and(
|
.where(and(
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { eq, desc, asc, and, sql, or, like } from 'drizzle-orm';
|
|||||||
const user = Astro.locals.user;
|
const user = Astro.locals.user;
|
||||||
if (!user) return Astro.redirect('/login');
|
if (!user) return Astro.redirect('/login');
|
||||||
|
|
||||||
// Get user's first organization
|
|
||||||
const userOrg = await db.select()
|
const userOrg = await db.select()
|
||||||
.from(members)
|
.from(members)
|
||||||
.where(eq(members.userId, user.id))
|
.where(eq(members.userId, user.id))
|
||||||
@@ -17,25 +16,22 @@ const userOrg = await db.select()
|
|||||||
|
|
||||||
if (!userOrg) return Astro.redirect('/dashboard');
|
if (!userOrg) return Astro.redirect('/dashboard');
|
||||||
|
|
||||||
// Get all clients for the organization
|
|
||||||
const allClients = await db.select()
|
const allClients = await db.select()
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.where(eq(clients.organizationId, userOrg.organizationId))
|
.where(eq(clients.organizationId, userOrg.organizationId))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
// Get all categories for the organization
|
|
||||||
const allCategories = await db.select()
|
const allCategories = await db.select()
|
||||||
.from(categories)
|
.from(categories)
|
||||||
.where(eq(categories.organizationId, userOrg.organizationId))
|
.where(eq(categories.organizationId, userOrg.organizationId))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
// Get all tags for the organization
|
|
||||||
const allTags = await db.select()
|
const allTags = await db.select()
|
||||||
.from(tags)
|
.from(tags)
|
||||||
.where(eq(tags.organizationId, userOrg.organizationId))
|
.where(eq(tags.organizationId, userOrg.organizationId))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
// Parse query parameters for filtering, sorting, and pagination
|
// Query params
|
||||||
const url = new URL(Astro.request.url);
|
const url = new URL(Astro.request.url);
|
||||||
const page = parseInt(url.searchParams.get('page') || '1');
|
const page = parseInt(url.searchParams.get('page') || '1');
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
@@ -43,11 +39,10 @@ const offset = (page - 1) * pageSize;
|
|||||||
|
|
||||||
const filterClient = url.searchParams.get('client') || '';
|
const filterClient = url.searchParams.get('client') || '';
|
||||||
const filterCategory = url.searchParams.get('category') || '';
|
const filterCategory = url.searchParams.get('category') || '';
|
||||||
const filterStatus = url.searchParams.get('status') || ''; // completed, running, all
|
const filterStatus = url.searchParams.get('status') || '';
|
||||||
const sortBy = url.searchParams.get('sort') || 'start-desc'; // start-desc, start-asc, duration-desc, duration-asc
|
const sortBy = url.searchParams.get('sort') || 'start-desc';
|
||||||
const searchTerm = url.searchParams.get('search') || '';
|
const searchTerm = url.searchParams.get('search') || '';
|
||||||
|
|
||||||
// Build query conditions
|
|
||||||
const conditions = [eq(timeEntries.organizationId, userOrg.organizationId)];
|
const conditions = [eq(timeEntries.organizationId, userOrg.organizationId)];
|
||||||
|
|
||||||
if (filterClient) {
|
if (filterClient) {
|
||||||
@@ -68,7 +63,6 @@ if (searchTerm) {
|
|||||||
conditions.push(like(timeEntries.description, `%${searchTerm}%`));
|
conditions.push(like(timeEntries.description, `%${searchTerm}%`));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get total count for pagination
|
|
||||||
const totalCount = await db.select({ count: sql<number>`count(*)` })
|
const totalCount = await db.select({ count: sql<number>`count(*)` })
|
||||||
.from(timeEntries)
|
.from(timeEntries)
|
||||||
.where(and(...conditions))
|
.where(and(...conditions))
|
||||||
@@ -76,7 +70,6 @@ const totalCount = await db.select({ count: sql<number>`count(*)` })
|
|||||||
|
|
||||||
const totalPages = Math.ceil((totalCount?.count || 0) / pageSize);
|
const totalPages = Math.ceil((totalCount?.count || 0) / pageSize);
|
||||||
|
|
||||||
// Build order by
|
|
||||||
let orderBy;
|
let orderBy;
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
case 'start-asc':
|
case 'start-asc':
|
||||||
@@ -88,7 +81,7 @@ switch (sortBy) {
|
|||||||
case 'duration-asc':
|
case 'duration-asc':
|
||||||
orderBy = asc(sql`(CASE WHEN ${timeEntries.endTime} IS NULL THEN 0 ELSE ${timeEntries.endTime} - ${timeEntries.startTime} END)`);
|
orderBy = asc(sql`(CASE WHEN ${timeEntries.endTime} IS NULL THEN 0 ELSE ${timeEntries.endTime} - ${timeEntries.startTime} END)`);
|
||||||
break;
|
break;
|
||||||
default: // start-desc
|
default:
|
||||||
orderBy = desc(timeEntries.startTime);
|
orderBy = desc(timeEntries.startTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +122,6 @@ function formatDuration(start: Date, end: Date | null) {
|
|||||||
return `${hours}h ${mins}m`;
|
return `${hours}h ${mins}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate pagination page numbers
|
|
||||||
function getPaginationPages(currentPage: number, totalPages: number): number[] {
|
function getPaginationPages(currentPage: number, totalPages: number): number[] {
|
||||||
const pages: number[] = [];
|
const pages: number[] = [];
|
||||||
const numPagesToShow = Math.min(5, totalPages);
|
const numPagesToShow = Math.min(5, totalPages);
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ if (Astro.locals.user) {
|
|||||||
return Astro.redirect('/dashboard');
|
return Astro.redirect('/dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this would be the first user
|
|
||||||
const userCountResult = await db.select({ count: count() }).from(users).get();
|
const userCountResult = await db.select({ count: count() }).from(users).get();
|
||||||
const isFirstUser = userCountResult ? userCountResult.count === 0 : true;
|
const isFirstUser = userCountResult ? userCountResult.count === 0 : true;
|
||||||
|
|
||||||
// Check if registration is enabled (only if not first user)
|
|
||||||
let registrationDisabled = false;
|
let registrationDisabled = false;
|
||||||
if (!isFirstUser) {
|
if (!isFirstUser) {
|
||||||
const registrationSetting = await db.select()
|
const registrationSetting = await db.select()
|
||||||
|
|||||||
Reference in New Issue
Block a user