Merge branch 'main' into feature/dashboard

This commit is contained in:
Pattadon 2023-11-20 09:17:18 +07:00
commit 9379e71ce8
18 changed files with 395 additions and 99 deletions

View File

@ -52,6 +52,7 @@ INSTALLED_APPS = [
'tasks', 'tasks',
'users', 'users',
'authentications', 'authentications',
'dashboard',
'corsheaders', 'corsheaders',
'drf_spectacular', 'drf_spectacular',

View File

@ -27,4 +27,5 @@ urlpatterns = [
path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
path('api/', include('dashboard.urls')),
] ]

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class DashboardConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'dashboard'

View File

View File

@ -0,0 +1,7 @@
from rest_framework import serializers
from .models import UserStats
class UserStatsSerializer(serializers.ModelSerializer):
class Meta:
model = UserStats
fields = ['health', 'gold', 'experience', 'strength', 'intelligence', 'endurance', 'perception', 'luck', 'level']

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,6 @@
from django.urls import path
from .views import DashboardStatsAPIView
urlpatterns = [
path('dashboard/stats/', DashboardStatsAPIView.as_view(), name='dashboard-stats'),
]

View File

@ -0,0 +1,58 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from django.db.models import Count
from django.utils import timezone
from tasks.models import Todo, RecurrenceTask
class DashboardStatsAPIView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
user = request.user
# Calculate task usage statistics
todo_count = Todo.objects.filter(user=user).count()
recurrence_task_count = RecurrenceTask.objects.filter(user=user).count()
# Calculate how many tasks were completed in the last 7 days
completed_todo_count_last_week = Todo.objects.filter(user=user, completed=True, last_update__gte=timezone.now() - timezone.timedelta(days=7)).count()
completed_recurrence_task_count_last_week = RecurrenceTask.objects.filter(user=user, completed=True, last_update__gte=timezone.now() - timezone.timedelta(days=7)).count()
# Calculate subtask completion rate
total_subtasks = Todo.objects.filter(user=user).aggregate(total=Count('subtask__id'))['total']
completed_subtasks = Todo.objects.filter(user=user, subtask__completed=True).aggregate(total=Count('subtask__id'))['total']
# Calculate overall completion rate
total_tasks = todo_count + recurrence_task_count
completed_tasks = completed_todo_count_last_week + completed_recurrence_task_count_last_week
overall_completion_rate = (completed_tasks / total_tasks) * 100 if total_tasks > 0 else 0
data = {
'todo_count': todo_count,
'recurrence_task_count': recurrence_task_count,
'completed_todo_count_last_week': completed_todo_count_last_week,
'completed_recurrence_task_count_last_week': completed_recurrence_task_count_last_week,
'total_subtasks': total_subtasks,
'completed_subtasks': completed_subtasks,
'overall_completion_rate': overall_completion_rate,
}
return Response(data, status=status.HTTP_200_OK)
def post(self, request):
# Handle incoming data from the POST request
# Update the necessary information based on the data
task_id = request.data.get('task_id')
is_completed = request.data.get('is_completed')
try:
task = Todo.objects.get(id=task_id, user=request.user)
task.completed = is_completed
task.save()
return Response({'message': 'Task completion status updated successfully'}, status=status.HTTP_200_OK)
except Todo.DoesNotExist:
return Response({'error': 'Task not found'}, status=status.HTTP_404_NOT_FOUND)

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.6 on 2023-11-17 16:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tasks', '0013_alter_recurrencetask_recurrence_rule'),
]
operations = [
migrations.AddField(
model_name='recurrencetask',
name='completed',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='todo',
name='completed',
field=models.BooleanField(default=False),
),
]

View File

@ -18,7 +18,6 @@ class Task(models.Model):
:param title: Title of the task. :param title: Title of the task.
:param notes: Optional additional notes for the task. :param notes: Optional additional notes for the task.
:param tags: Associated tags for the task. :param tags: Associated tags for the task.
:param completed: A boolean field indicating whether the task is completed.
:param importance: The importance of the task (range: 1 to 5) :param importance: The importance of the task (range: 1 to 5)
:param difficulty: The difficulty of the task (range: 1 to 5). :param difficulty: The difficulty of the task (range: 1 to 5).
:param challenge: Associated challenge (optional). :param challenge: Associated challenge (optional).
@ -62,12 +61,14 @@ class Todo(Task):
NOT_IMPORTANT_URGENT = 3, 'Not Important & Urgent' NOT_IMPORTANT_URGENT = 3, 'Not Important & Urgent'
NOT_IMPORTANT_NOT_URGENT = 4, 'Not Important & Not Urgent' NOT_IMPORTANT_NOT_URGENT = 4, 'Not Important & Not Urgent'
completed = models.BooleanField(default=False)
priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT) priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
def __str__(self): def __str__(self):
return self.title return self.title
class RecurrenceTask(Task): class RecurrenceTask(Task):
completed = models.BooleanField(default=False)
recurrence_rule = models.CharField() recurrence_rule = models.CharField()
def __str__(self) -> str: def __str__(self) -> str:

View File

@ -16,6 +16,9 @@
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@fullcalendar/core": "^6.1.9", "@fullcalendar/core": "^6.1.9",
"@fullcalendar/daygrid": "^6.1.9", "@fullcalendar/daygrid": "^6.1.9",
"@fullcalendar/interaction": "^6.1.9", "@fullcalendar/interaction": "^6.1.9",

View File

@ -23,6 +23,15 @@ dependencies:
'@emotion/styled': '@emotion/styled':
specifier: ^11.11.0 specifier: ^11.11.0
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0)
'@fortawesome/fontawesome-svg-core':
specifier: ^6.4.2
version: 6.4.2
'@fortawesome/free-brands-svg-icons':
specifier: ^6.4.2
version: 6.4.2
'@fortawesome/react-fontawesome':
specifier: ^0.2.0
version: 0.2.0(@fortawesome/fontawesome-svg-core@6.4.2)(react@18.2.0)
'@fullcalendar/core': '@fullcalendar/core':
specifier: ^6.1.9 specifier: ^6.1.9
version: 6.1.9 version: 6.1.9
@ -855,6 +864,39 @@ packages:
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
dev: false dev: false
/@fortawesome/fontawesome-common-types@6.4.2:
resolution: {integrity: sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==}
engines: {node: '>=6'}
requiresBuild: true
dev: false
/@fortawesome/fontawesome-svg-core@6.4.2:
resolution: {integrity: sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==}
engines: {node: '>=6'}
requiresBuild: true
dependencies:
'@fortawesome/fontawesome-common-types': 6.4.2
dev: false
/@fortawesome/free-brands-svg-icons@6.4.2:
resolution: {integrity: sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg==}
engines: {node: '>=6'}
requiresBuild: true
dependencies:
'@fortawesome/fontawesome-common-types': 6.4.2
dev: false
/@fortawesome/react-fontawesome@0.2.0(@fortawesome/fontawesome-svg-core@6.4.2)(react@18.2.0):
resolution: {integrity: sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==}
peerDependencies:
'@fortawesome/fontawesome-svg-core': ~1 || ~6
react: '>=16.3'
dependencies:
'@fortawesome/fontawesome-svg-core': 6.4.2
prop-types: 15.8.1
react: 18.2.0
dev: false
/@fullcalendar/core@6.1.9: /@fullcalendar/core@6.1.9:
resolution: {integrity: sha512-eeG+z9BWerdsU9Ac6j16rpYpPnE0wxtnEHiHrh/u/ADbGTR3hCOjCD9PxQOfhOTHbWOVs7JQunGcksSPu5WZBQ==} resolution: {integrity: sha512-eeG+z9BWerdsU9Ac6j16rpYpPnE0wxtnEHiHrh/u/ADbGTR3hCOjCD9PxQOfhOTHbWOVs7JQunGcksSPu5WZBQ==}
dependencies: dependencies:

View File

@ -7,6 +7,8 @@ import { loadFull } from "tsparticles";
import refreshAccessToken from "./refreshAcesstoken"; import refreshAccessToken from "./refreshAcesstoken";
import axiosapi from "../../api/AuthenticationApi"; import axiosapi from "../../api/AuthenticationApi";
import { useAuth } from "../../hooks/authentication/IsAuthenticated"; import { useAuth } from "../../hooks/authentication/IsAuthenticated";
import { FcGoogle } from "react-icons/fc";
function LoginPage() { function LoginPage() {
const Navigate = useNavigate(); const Navigate = useNavigate();
@ -208,7 +210,7 @@ function LoginPage() {
className="btn btn-outline btn-secondary w-full " className="btn btn-outline btn-secondary w-full "
onClick={() => googleLoginImplicit()} onClick={() => googleLoginImplicit()}
> >
Login with Google <FcGoogle className="rounded-full bg-white"/>Login with Google
</button> </button>
{/* Forgot Password Link */} {/* Forgot Password Link */}
<div className="justify-left"> <div className="justify-left">

View File

@ -1,36 +1,29 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import axiosapi from "../../api/AuthenticationApi"; import axiosapi from "../../api/AuthenticationApi";
import { useCallback } from "react";
import Particles from "react-tsparticles";
import { loadFull } from "tsparticles";
import { FcGoogle } from "react-icons/fc";
import { useGoogleLogin } from "@react-oauth/google";
import Avatar from "@mui/material/Avatar";
import Button from "@mui/material/Button";
import CssBaseline from "@mui/material/CssBaseline";
import TextField from "@mui/material/TextField";
import FormControlLabel from "@mui/material/FormControlLabel";
import Checkbox from "@mui/material/Checkbox";
import Link from "@mui/material/Link";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import Typography from "@mui/material/Typography";
import Container from "@mui/material/Container";
import { createTheme, ThemeProvider } from "@mui/material/styles";
function Copyright(props) { function Copyright(props) {
return ( return (
<Typography variant="body2" color="text.secondary" align="center" {...props}> <div className="text-center text-sm text-gray-500" {...props}>
{"Copyright © "} {"Copyright © "}
<Link color="inherit" href="https://github.com/TurTaskProject/TurTaskWeb"> <a
href="https://github.com/TurTaskProject/TurTaskWeb"
className="text-blue-500 hover:underline"
>
TurTask TurTask
</Link>{" "} </a>{" "}
{new Date().getFullYear()} {new Date().getFullYear()}
{"."} {"."}
</Typography> </div>
); );
} }
const defaultTheme = createTheme();
export default function SignUp() { export default function SignUp() {
const Navigate = useNavigate(); const Navigate = useNavigate();
@ -42,7 +35,7 @@ export default function SignUp() {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async e => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
setError(null); setError(null);
@ -58,86 +51,194 @@ export default function SignUp() {
Navigate("/login"); Navigate("/login");
}; };
const handleChange = e => { const handleChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData({ ...formData, [name]: value }); setFormData({ ...formData, [name]: value });
}; };
{
/* Particles Loader*/
}
const particlesInit = useCallback(async (engine) => {
console.log(engine);
await loadFull(engine);
}, []);
const particlesLoaded = useCallback(async (container) => {
console.log(container);
}, []);
const googleLoginImplicit = useGoogleLogin({
flow: "auth-code",
redirect_uri: "postmessage",
onSuccess: async (response) => {
try {
const loginResponse = await axiosapi.googleLogin(response.code);
if (loginResponse && loginResponse.data) {
const { access_token, refresh_token } = loginResponse.data;
localStorage.setItem("access_token", access_token);
localStorage.setItem("refresh_token", refresh_token);
setIsAuthenticated(true);
Navigate("/");
}
} catch (error) {
console.error("Error with the POST request:", error);
setIsAuthenticated(false);
}
},
onError: (errorResponse) => console.log(errorResponse),
});
return ( return (
<ThemeProvider theme={defaultTheme}> <div
<Container component="main" maxWidth="xs"> data-theme="night"
<CssBaseline /> className="h-screen flex items-center justify-center"
<Box >
sx={{ {/* Particles Container */}
marginTop: 8, <div style={{ width: "0%", height: "100vh" }}>
display: "flex", <Particles
flexDirection: "column", id="particles"
alignItems: "center", init={particlesInit}
}}> loaded={particlesLoaded}
<Avatar sx={{ m: 1, bgcolor: "secondary.main" }}> className="-z-10"
<LockOutlinedIcon /> options={{
</Avatar> fpsLimit: 240,
<Typography component="h1" variant="h5"> interactivity: {
Sign up events: {
</Typography> onClick: {
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}> enable: true,
<Grid container spacing={2}> mode: "push",
<Grid item xs={12}> },
<TextField onHover: {
required enable: true,
fullWidth mode: "repulse",
id="email" },
label="Email Address" resize: true,
name="email" },
autoComplete="email" modes: {
onChange={handleChange} push: {
/> quantity: 4,
</Grid> },
<Grid item xs={12}> repulse: {
<TextField distance: 200,
autoComplete="username" duration: 0.4,
name="Username" },
required },
fullWidth },
id="Username" particles: {
label="Username" color: {
autoFocus value: "#023020",
onChange={handleChange} },
/> links: {
</Grid> color: "#228B22",
<Grid item xs={12}> distance: 150,
<TextField enable: true,
required opacity: 0.5,
fullWidth width: 1,
name="password" },
label="Password" move: {
type="password" direction: "none",
id="password" enable: true,
autoComplete="new-password" outModes: {
onChange={handleChange} default: "bounce",
/> },
</Grid> random: false,
<Grid item xs={12}> speed: 4,
<FormControlLabel straight: false,
control={<Checkbox value="allowExtraEmails" color="primary" />} },
label="I want to receive inspiration, marketing promotions and updates via email." number: {
/> density: {
</Grid> enable: true,
</Grid> area: 800,
<Button type="submit" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}> },
Sign Up value: 50,
</Button> },
<Grid container justifyContent="flex-end"> opacity: {
<Grid item> value: 0.5,
<Link href="#" variant="body2"> },
Already have an account? Sign in shape: {
</Link> type: "circle",
</Grid> },
</Grid> size: {
</Box> value: { min: 6, max: 8 },
</Box> },
<Copyright sx={{ mt: 5 }} /> },
</Container> detectRetina: true,
</ThemeProvider> }}
/>
</div>
<div className="w-1/4 h-1 flex items-center justify-center">
<div className="w-96 bg-neutral rounded-lg p-8 shadow-md space-y-4 z-10">
{/* Register Form */}
<h2 className="text-3xl font-bold text-center">Signup</h2>
{/* Email Input */}
<div className="form-control ">
<label className="label" htmlFor="email">
<p className="text-bold">
Email<span className="text-red-500 text-bold">*</span>
</p>
</label>
<input
className="input"
type="email"
id="email"
placeholder="Enter your email"
onChange={handleChange}
/>
</div>
{/* Username Input */}
<div className="form-control">
<label className="label" htmlFor="Username">
<p className="text-bold">
Username<span className="text-red-500 text-bold">*</span>
</p>
</label>
<input
className="input"
type="text"
id="Username"
placeholder="Enter your username"
onChange={handleChange}
/>
</div>
{/* Password Input */}
<div className="form-control">
<label className="label" htmlFor="password">
<p className="text-bold">
Password<span className="text-red-500 text-bold">*</span>
</p>
</label>
<input
className="input"
type="password"
id="password"
placeholder="Enter your password"
onChange={handleChange}
/>
</div>
<br></br>
{/* Login Button */}
<button className="btn btn-success w-full " onClick={handleSubmit}>
Signup
</button>
<div className="divider">OR</div>
{/* Login with Google Button */}
<button
className="btn btn-outline btn-secondary w-full "
onClick={() => googleLoginImplicit()}
>
<FcGoogle className="rounded-full bg-white"/>Login with Google
</button>
{/* Already have an account? */}
<div className="text-blue-500 flex justify-center text-sm">
<a href="login">
Already have an account?
</a>
</div>
<Copyright />
</div>
</div>
</div>
); );
} }

View File

@ -39,7 +39,7 @@ function NavBar() {
</label> </label>
<ul <ul
tabIndex={0} tabIndex={0}
className="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52"> className="mt-3 z-[10] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52">
<li> <li>
<a href={settings.Profile} className="justify-between"> <a href={settings.Profile} className="justify-between">
Profile Profile

View File

@ -0,0 +1,39 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faGoogle, faGithub } from '@fortawesome/free-brands-svg-icons';
function Signup() {
return (
<div className="flex items-center justify-center h-screen">
<div className="flex flex-col items-center bg-white p-10 rounded-lg shadow-md">
<h1 className="text-4xl font-semibold mb-6">Create your account</h1>
<p className="text-gray-700 mb-6 text-lg">
Start spending more time on your own table.
</p>
<div className='font-bold'>
<div className="mb-4">
<button className="flex items-center justify-center bg-blue-500 text-white px-14 py-3 rounded-lg">
<span className="mr-3"><FontAwesomeIcon icon={faGoogle} /></span>
Sign Up with Google
</button>
</div>
<div className="mb-4">
<button className="flex items-center justify-center bg-gray-800 text-white px-14 py-3 rounded-lg">
<span className="mr-3"><FontAwesomeIcon icon={faGithub} /></span>
Sign Up with Github
</button>
</div>
<div>
<button className="bg-green-500 text-white px-14 py-3 rounded-lg">
Sign Up with your email.
</button>
</div>
</div>
</div>
</div>
);
}
export default Signup;