fix
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
# Install OpenSSL for Prisma
|
||||
RUN apk add --no-cache openssl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
@@ -22,4 +25,4 @@ RUN npm run build
|
||||
EXPOSE 3001
|
||||
|
||||
# Start the application
|
||||
CMD ["sh", "-c", "npx prisma migrate deploy && npm start"]
|
||||
CMD ["sh", "-c", "npx prisma migrate deploy && npm start"]
|
||||
|
||||
875
backend/package-lock.json
generated
875
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,9 +10,9 @@
|
||||
"db:seed": "tsx prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.7.0",
|
||||
"@prisma/client": "^6.19.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"openid-client": "^5.6.1",
|
||||
@@ -22,9 +22,9 @@
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/node": "^20.10.5",
|
||||
"prisma": "^5.7.0",
|
||||
"@types/node": "^25.2.3",
|
||||
"prisma": "^6.19.2",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ model OngoingTimer {
|
||||
|
||||
userId String @map("user_id") @db.VarChar(255) @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
projectId String? @map("project_id")
|
||||
projectId String? @unique @map("project_id")
|
||||
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([userId])
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { authApi } from '@/api/auth';
|
||||
import type { User } from '@/types';
|
||||
import { createContext, useContext, useCallback, type ReactNode } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { authApi } from "@/api/auth";
|
||||
import type { User } from "@/types";
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
@@ -23,9 +16,9 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
const { data: user, isLoading } = useQuery({
|
||||
queryKey: ['currentUser'],
|
||||
queryKey: ["currentUser"],
|
||||
queryFn: authApi.getCurrentUser,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
@@ -36,12 +29,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
await authApi.logout();
|
||||
queryClient.setQueryData(['currentUser'], null);
|
||||
queryClient.setQueryData(["currentUser"], null);
|
||||
queryClient.clear();
|
||||
}, [queryClient]);
|
||||
|
||||
const refetchUser = useCallback(async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['currentUser'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["currentUser"] });
|
||||
}, [queryClient]);
|
||||
|
||||
return (
|
||||
@@ -63,7 +56,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
useEffect,
|
||||
useCallback,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { timerApi } from '@/api/timer';
|
||||
import type { OngoingTimer, TimeEntry } from '@/types';
|
||||
} from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { timerApi } from "@/api/timer";
|
||||
import type { OngoingTimer, TimeEntry } from "@/types";
|
||||
|
||||
interface TimerContextType {
|
||||
ongoingTimer: OngoingTimer | null;
|
||||
@@ -24,10 +24,12 @@ const TimerContext = createContext<TimerContextType | undefined>(undefined);
|
||||
export function TimerProvider({ children }: { children: ReactNode }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
||||
const [elapsedInterval, setElapsedInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
const [elapsedInterval, setElapsedInterval] = useState<ReturnType<
|
||||
typeof setInterval
|
||||
> | null>(null);
|
||||
|
||||
const { data: ongoingTimer, isLoading } = useQuery({
|
||||
queryKey: ['ongoingTimer'],
|
||||
queryKey: ["ongoingTimer"],
|
||||
queryFn: timerApi.getOngoing,
|
||||
refetchInterval: 60000, // Refetch every minute to sync with server
|
||||
});
|
||||
@@ -64,7 +66,7 @@ export function TimerProvider({ children }: { children: ReactNode }) {
|
||||
const startMutation = useMutation({
|
||||
mutationFn: timerApi.start,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ongoingTimer'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["ongoingTimer"] });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -72,7 +74,7 @@ export function TimerProvider({ children }: { children: ReactNode }) {
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: timerApi.update,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ongoingTimer'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["ongoingTimer"] });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -80,8 +82,8 @@ export function TimerProvider({ children }: { children: ReactNode }) {
|
||||
const stopMutation = useMutation({
|
||||
mutationFn: timerApi.stop,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ongoingTimer'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['timeEntries'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["ongoingTimer"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["timeEntries"] });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -89,14 +91,14 @@ export function TimerProvider({ children }: { children: ReactNode }) {
|
||||
async (projectId?: string) => {
|
||||
await startMutation.mutateAsync(projectId);
|
||||
},
|
||||
[startMutation]
|
||||
[startMutation],
|
||||
);
|
||||
|
||||
const updateTimerProject = useCallback(
|
||||
async (projectId?: string | null) => {
|
||||
await updateMutation.mutateAsync(projectId);
|
||||
},
|
||||
[updateMutation]
|
||||
[updateMutation],
|
||||
);
|
||||
|
||||
const stopTimer = useCallback(
|
||||
@@ -108,7 +110,7 @@ export function TimerProvider({ children }: { children: ReactNode }) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[stopMutation]
|
||||
[stopMutation],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -130,7 +132,7 @@ export function TimerProvider({ children }: { children: ReactNode }) {
|
||||
export function useTimer() {
|
||||
const context = useContext(TimerContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTimer must be used within a TimerProvider');
|
||||
throw new Error("useTimer must be used within a TimerProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Clock, Calendar, Briefcase, TrendingUp } from 'lucide-react';
|
||||
import { useTimeEntries } from '@/hooks/useTimeEntries';
|
||||
import { formatDate, formatDurationFromDates } from '@/utils/dateUtils';
|
||||
import { startOfDay, endOfDay, format as formatDateFns } from 'date-fns';
|
||||
import { Link } from "react-router-dom";
|
||||
import { Clock, Calendar, Briefcase, TrendingUp } from "lucide-react";
|
||||
import { useTimeEntries } from "@/hooks/useTimeEntries";
|
||||
import {
|
||||
formatDate,
|
||||
formatDurationFromDates,
|
||||
formatDuration,
|
||||
} from "@/utils/dateUtils";
|
||||
import { startOfDay, endOfDay } from "date-fns";
|
||||
|
||||
export function DashboardPage() {
|
||||
const today = new Date();
|
||||
@@ -16,9 +20,10 @@ export function DashboardPage() {
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
const totalTodaySeconds = todayEntries?.entries.reduce((total, entry) => {
|
||||
return total + calculateDuration(entry.startTime, entry.endTime);
|
||||
}, 0) || 0;
|
||||
const totalTodaySeconds =
|
||||
todayEntries?.entries.reduce((total, entry) => {
|
||||
return total + calculateDuration(entry.startTime, entry.endTime);
|
||||
}, 0) || 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -41,19 +46,23 @@ export function DashboardPage() {
|
||||
<StatCard
|
||||
icon={Calendar}
|
||||
label="Entries Today"
|
||||
value={todayEntries?.entries.length.toString() || '0'}
|
||||
value={todayEntries?.entries.length.toString() || "0"}
|
||||
color="green"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Briefcase}
|
||||
label="Active Projects"
|
||||
value={new Set(recentEntries?.entries.map(e => e.projectId)).size.toString() || '0'}
|
||||
value={
|
||||
new Set(
|
||||
recentEntries?.entries.map((e) => e.projectId),
|
||||
).size.toString() || "0"
|
||||
}
|
||||
color="purple"
|
||||
/>
|
||||
<StatCard
|
||||
icon={TrendingUp}
|
||||
label="Total Entries"
|
||||
value={recentEntries?.pagination.total.toString() || '0'}
|
||||
value={recentEntries?.pagination.total.toString() || "0"}
|
||||
color="orange"
|
||||
/>
|
||||
</div>
|
||||
@@ -61,7 +70,9 @@ export function DashboardPage() {
|
||||
{/* Recent Activity */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Recent Activity</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
Recent Activity
|
||||
</h2>
|
||||
<Link
|
||||
to="/time-entries"
|
||||
className="text-sm text-primary-600 hover:text-primary-700"
|
||||
@@ -97,7 +108,9 @@ export function DashboardPage() {
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full mr-2"
|
||||
style={{ backgroundColor: entry.project.color || '#6b7280' }}
|
||||
style={{
|
||||
backgroundColor: entry.project.color || "#6b7280",
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
@@ -130,15 +143,15 @@ interface StatCardProps {
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
value: string;
|
||||
color: 'blue' | 'green' | 'purple' | 'orange';
|
||||
color: "blue" | "green" | "purple" | "orange";
|
||||
}
|
||||
|
||||
function StatCard({ icon: Icon, label, value, color }: StatCardProps) {
|
||||
const colors = {
|
||||
blue: 'bg-blue-50 text-blue-600',
|
||||
green: 'bg-green-50 text-green-600',
|
||||
purple: 'bg-purple-50 text-purple-600',
|
||||
orange: 'bg-orange-50 text-orange-600',
|
||||
blue: "bg-blue-50 text-blue-600",
|
||||
green: "bg-green-50 text-green-600",
|
||||
purple: "bg-purple-50 text-purple-600",
|
||||
orange: "bg-orange-50 text-orange-600",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -157,5 +170,7 @@ function StatCard({ icon: Icon, label, value, color }: StatCardProps) {
|
||||
}
|
||||
|
||||
function calculateDuration(startTime: string, endTime: string): number {
|
||||
return Math.floor((new Date(endTime).getTime() - new Date(startTime).getTime()) / 1000);
|
||||
}
|
||||
return Math.floor(
|
||||
(new Date(endTime).getTime() - new Date(startTime).getTime()) / 1000,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { format, parseISO, differenceInSeconds, formatDuration as dateFnsFormatDuration } from 'date-fns';
|
||||
import { format, parseISO, differenceInSeconds } from "date-fns";
|
||||
|
||||
export function formatDate(date: string | Date): string {
|
||||
const d = typeof date === 'string' ? parseISO(date) : date;
|
||||
return format(d, 'MMM d, yyyy');
|
||||
const d = typeof date === "string" ? parseISO(date) : date;
|
||||
return format(d, "MMM d, yyyy");
|
||||
}
|
||||
|
||||
export function formatTime(date: string | Date): string {
|
||||
const d = typeof date === 'string' ? parseISO(date) : date;
|
||||
return format(d, 'h:mm a');
|
||||
const d = typeof date === "string" ? parseISO(date) : date;
|
||||
return format(d, "h:mm a");
|
||||
}
|
||||
|
||||
export function formatDateTime(date: string | Date): string {
|
||||
const d = typeof date === 'string' ? parseISO(date) : date;
|
||||
return format(d, 'MMM d, yyyy h:mm a');
|
||||
const d = typeof date === "string" ? parseISO(date) : date;
|
||||
return format(d, "MMM d, yyyy h:mm a");
|
||||
}
|
||||
|
||||
export function formatDuration(totalSeconds: number): string {
|
||||
@@ -22,12 +22,12 @@ export function formatDuration(totalSeconds: number): string {
|
||||
|
||||
const parts = [];
|
||||
if (hours > 0) {
|
||||
parts.push(hours.toString().padStart(2, '0'));
|
||||
parts.push(hours.toString().padStart(2, "0"));
|
||||
}
|
||||
parts.push(minutes.toString().padStart(2, '0'));
|
||||
parts.push(seconds.toString().padStart(2, '0'));
|
||||
parts.push(minutes.toString().padStart(2, "0"));
|
||||
parts.push(seconds.toString().padStart(2, "0"));
|
||||
|
||||
return parts.join(':');
|
||||
return parts.join(":");
|
||||
}
|
||||
|
||||
export function calculateDuration(startTime: string, endTime: string): number {
|
||||
@@ -36,14 +36,19 @@ export function calculateDuration(startTime: string, endTime: string): number {
|
||||
return differenceInSeconds(end, start);
|
||||
}
|
||||
|
||||
export function formatDurationFromDates(startTime: string, endTime: string): string {
|
||||
export function formatDurationFromDates(
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
): string {
|
||||
const seconds = calculateDuration(startTime, endTime);
|
||||
return formatDuration(seconds);
|
||||
}
|
||||
|
||||
export function getLocalISOString(date: Date = new Date()): string {
|
||||
const timezoneOffset = date.getTimezoneOffset() * 60000;
|
||||
const localISOTime = new Date(date.getTime() - timezoneOffset).toISOString().slice(0, 16);
|
||||
const localISOTime = new Date(date.getTime() - timezoneOffset)
|
||||
.toISOString()
|
||||
.slice(0, 16);
|
||||
return localISOTime;
|
||||
}
|
||||
|
||||
@@ -51,4 +56,4 @@ export function toISOTimezone(dateStr: string): string {
|
||||
// Convert a local datetime input to ISO string with timezone
|
||||
const date = new Date(dateStr);
|
||||
return date.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user