diff --git a/backend/src/db/models/project.js b/backend/src/db/models/project.js
new file mode 100644
index 0000000..87dd34e
--- /dev/null
+++ b/backend/src/db/models/project.js
@@ -0,0 +1,37 @@
+const { DataTypes } = require('sequelize');
+
+module.exports = (sequelize) => {
+ const Project = sequelize.define(
+ 'project',
+ {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ name: {
+ type: DataTypes.STRING(255),
+ allowNull: false,
+ validate: {
+ len: [2, 255],
+ notEmpty: true,
+ },
+ },
+ description: {
+ type: DataTypes.TEXT,
+ },
+ },
+ {
+ timestamps: true,
+ paranoid: true,
+ },
+ );
+
+ Project.associate = (models) => {
+ models.project.hasMany(models.task, {
+ as: 'tasks',
+ });
+ };
+
+ return Project;
+};
diff --git a/backend/src/db/models/task.js b/backend/src/db/models/task.js
new file mode 100644
index 0000000..72e8b76
--- /dev/null
+++ b/backend/src/db/models/task.js
@@ -0,0 +1,48 @@
+const { DataTypes } = require('sequelize');
+
+module.exports = (sequelize) => {
+ const Task = sequelize.define(
+ 'task',
+ {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ title: {
+ type: DataTypes.STRING(255),
+ allowNull: false,
+ validate: {
+ notEmpty: true,
+ },
+ },
+ description: {
+ type: DataTypes.TEXT,
+ },
+ status: {
+ type: DataTypes.ENUM,
+ values: ['todo', 'in_progress', 'done'],
+ defaultValue: 'todo',
+ },
+ deadline: {
+ type: DataTypes.DATE,
+ },
+ },
+ {
+ timestamps: true,
+ paranoid: true,
+ },
+ );
+
+ Task.associate = (models) => {
+ models.task.belongsTo(models.project, {
+ as: 'project',
+ });
+
+ models.task.belongsTo(models.users, {
+ as: 'assignee',
+ });
+ };
+
+ return Task;
+};
diff --git a/backend/src/routes/openai.js b/backend/src/routes/openai.js
index 2d47d9f..743d885 100644
--- a/backend/src/routes/openai.js
+++ b/backend/src/routes/openai.js
@@ -254,7 +254,8 @@ router.post(
const response = await LocalAIApi.createResponse(payload, options);
if (response.success) {
- return res.status(200).send(response);
+ const text = LocalAIApi.extractText(response);
+ return res.status(200).send({ text });
}
console.error('AI proxy error:', response);
diff --git a/frontend/src/colors.ts b/frontend/src/colors.ts
index 71e116a..2cec3a8 100644
--- a/frontend/src/colors.ts
+++ b/frontend/src/colors.ts
@@ -1,40 +1,40 @@
import type { ColorButtonKey } from './interfaces'
-export const gradientBgBase = 'bg-gradient-to-tr'
-export const colorBgBase = "bg-violet-50/50"
-export const gradientBgPurplePink = `${gradientBgBase} from-purple-400 via-pink-500 to-red-500`
-export const gradientBgViolet = `${gradientBgBase} ${colorBgBase}`
-export const gradientBgDark = `${gradientBgBase} from-dark-700 via-dark-900 to-dark-800`;
-export const gradientBgPinkRed = `${gradientBgBase} from-pink-400 via-red-500 to-yellow-500`
+export const gradientBgBase = 'bg-gray-100 dark:bg-dark-900'
+export const colorBgBase = "bg-gray-100 dark:bg-dark-900"
+export const gradientBgPurplePink = `bg-gray-100 dark:bg-dark-900`
+export const gradientBgViolet = `bg-gray-100 dark:bg-dark-900`
+export const gradientBgDark = `bg-dark-900`;
+export const gradientBgPinkRed = `bg-gray-100 dark:bg-dark-900`
export const colorsBgLight = {
white: 'bg-white text-black',
light: ' bg-white text-black text-black dark:bg-dark-900 dark:text-white',
contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black',
- success: 'bg-emerald-500 border-emerald-500 dark:bg-pavitra-blue dark:border-pavitra-blue text-white',
- danger: 'bg-red-500 border-red-500 text-white',
- warning: 'bg-yellow-500 border-yellow-500 text-white',
- info: 'bg-blue-500 border-blue-500 dark:bg-pavitra-blue dark:border-pavitra-blue text-white',
+ success: 'bg-gray-500 border-gray-500 dark:bg-gray-700 dark:border-gray-700 text-white',
+ danger: 'bg-gray-500 border-gray-500 text-white',
+ warning: 'bg-gray-500 border-gray-500 text-white',
+ info: 'bg-gray-500 border-gray-500 dark:bg-gray-700 dark:border-gray-700 text-white',
}
export const colorsText = {
white: 'text-black dark:text-slate-100',
light: 'text-gray-700 dark:text-slate-400',
contrast: 'dark:text-white',
- success: 'text-emerald-500',
- danger: 'text-red-500',
- warning: 'text-yellow-500',
- info: 'text-blue-500',
+ success: 'text-gray-500',
+ danger: 'text-gray-500',
+ warning: 'text-gray-500',
+ info: 'text-gray-500',
};
export const colorsOutline = {
white: [colorsText.white, 'border-gray-100'].join(' '),
light: [colorsText.light, 'border-gray-100'].join(' '),
contrast: [colorsText.contrast, 'border-gray-900 dark:border-slate-100'].join(' '),
- success: [colorsText.success, 'border-emerald-500'].join(' '),
- danger: [colorsText.danger, 'border-red-500'].join(' '),
- warning: [colorsText.warning, 'border-yellow-500'].join(' '),
- info: [colorsText.info, 'border-blue-500'].join(' '),
+ success: [colorsText.success, 'border-gray-500'].join(' '),
+ danger: [colorsText.danger, 'border-gray-500'].join(' '),
+ warning: [colorsText.warning, 'border-gray-500'].join(' '),
+ info: [colorsText.info, 'border-gray-500'].join(' '),
};
export const getButtonColor = (
@@ -53,30 +53,30 @@ export const getButtonColor = (
whiteDark: 'ring-gray-200 dark:ring-dark-500',
lightDark: 'ring-gray-200 dark:ring-gray-500',
contrast: 'ring-gray-300 dark:ring-gray-400',
- success: 'ring-emerald-300 dark:ring-pavitra-blue',
- danger: 'ring-red-300 dark:ring-red-700',
- warning: 'ring-yellow-300 dark:ring-yellow-700',
- info: "ring-blue-300 dark:ring-pavitra-blue",
+ success: 'ring-gray-300 dark:ring-gray-700',
+ danger: 'ring-gray-300 dark:ring-gray-700',
+ warning: 'ring-gray-300 dark:ring-gray-700',
+ info: "ring-gray-300 dark:ring-gray-700",
},
active: {
white: 'bg-gray-100',
whiteDark: 'bg-gray-100 dark:bg-dark-800',
lightDark: 'bg-gray-200 dark:bg-slate-700',
contrast: 'bg-gray-700 dark:bg-slate-100',
- success: 'bg-emerald-700 dark:bg-pavitra-blue',
- danger: 'bg-red-700 dark:bg-red-600',
- warning: 'bg-yellow-700 dark:bg-yellow-600',
- info: 'bg-blue-700 dark:bg-pavitra-blue',
+ success: 'bg-gray-700 dark:bg-gray-800',
+ danger: 'bg-gray-700 dark:bg-gray-600',
+ warning: 'bg-gray-700 dark:bg-gray-600',
+ info: 'bg-gray-700 dark:bg-gray-800',
},
bg: {
white: 'bg-white text-black',
whiteDark: 'bg-white text-black dark:bg-dark-900 dark:text-white',
lightDark: 'bg-gray-100 text-black dark:bg-slate-800 dark:text-white',
contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black',
- success: 'bg-emerald-600 dark:bg-pavitra-blue text-white',
- danger: 'bg-red-600 text-white dark:bg-red-500 ',
- warning: 'bg-yellow-600 dark:bg-yellow-500 text-white',
- info: " bg-blue-600 dark:bg-pavitra-blue text-white ",
+ success: 'bg-gray-600 dark:bg-gray-700 text-white',
+ danger: 'bg-gray-600 text-white dark:bg-gray-500 ',
+ warning: 'bg-gray-600 dark:bg-gray-500 text-white',
+ info: " bg-gray-600 dark:bg-gray-700 text-white ",
},
bgHover: {
white: 'hover:bg-gray-100',
@@ -84,39 +84,39 @@ export const getButtonColor = (
lightDark: 'hover:bg-gray-200 hover:dark:bg-slate-700',
contrast: 'hover:bg-gray-700 hover:dark:bg-slate-100',
success:
- 'hover:bg-emerald-700 hover:border-emerald-700 hover:dark:bg-pavitra-blue hover:dark:border-pavitra-blue',
+ 'hover:bg-gray-700 hover:border-gray-700 hover:dark:bg-gray-800 hover:dark:border-gray-800',
danger:
- 'hover:bg-red-700 hover:border-red-700 hover:dark:bg-red-600 hover:dark:border-red-600',
+ 'hover:bg-gray-700 hover:border-gray-700 hover:dark:bg-gray-600 hover:dark:border-gray-600',
warning:
- 'hover:bg-yellow-700 hover:border-yellow-700 hover:dark:bg-yellow-600 hover:dark:border-yellow-600',
- info: "hover:bg-blue-700 hover:border-blue-700 hover:dark:bg-pavitra-blue/80 hover:dark:border-pavitra-blue/80",
+ 'hover:bg-gray-700 hover:border-gray-700 hover:dark:bg-gray-600 hover:dark:border-gray-600',
+ info: "hover:bg-gray-700 hover:border-gray-700 hover:dark:bg-gray-800/80 hover:dark:border-gray-800/80",
},
borders: {
white: 'border-white',
whiteDark: 'border-white dark:border-dark-900',
lightDark: 'border-gray-100 dark:border-slate-800',
contrast: 'border-gray-800 dark:border-white',
- success: 'border-emerald-600 dark:border-pavitra-blue',
- danger: 'border-red-600 dark:border-red-500',
- warning: 'border-yellow-600 dark:border-yellow-500',
- info: "border-blue-600 border-blue-600 dark:border-pavitra-blue",
+ success: 'border-gray-600 dark:border-gray-700',
+ danger: 'border-gray-600 dark:border-gray-500',
+ warning: 'border-gray-600 dark:border-gray-500',
+ info: "border-gray-600 border-gray-600 dark:border-gray-700",
},
text: {
contrast: 'dark:text-slate-100',
- success: 'text-emerald-600 dark:text-pavitra-blue',
- danger: 'text-red-600 dark:text-red-500',
- warning: 'text-yellow-600 dark:text-yellow-500',
- info: 'text-blue-600 dark:text-pavitra-blue',
+ success: 'text-gray-600 dark:text-gray-400',
+ danger: 'text-gray-600 dark:text-gray-400',
+ warning: 'text-gray-600 dark:text-gray-400',
+ info: 'text-gray-600 dark:text-gray-400',
},
outlineHover: {
contrast:
'hover:bg-gray-800 hover:text-gray-100 hover:dark:bg-slate-100 hover:dark:text-black',
- success: 'hover:bg-emerald-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-pavitra-blue',
+ success: 'hover:bg-gray-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-gray-700',
danger:
- 'hover:bg-red-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-red-600',
+ 'hover:bg-gray-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-gray-600',
warning:
- 'hover:bg-yellow-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-yellow-600',
- info: "hover:bg-blue-600 hover:bg-blue-600 hover:text-white hover:dark:text-white hover:dark:border-pavitra-blue",
+ 'hover:bg-gray-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-gray-600',
+ info: "hover:bg-gray-600 hover:bg-gray-600 hover:text-white hover:dark:text-white hover:dark:border-gray-700",
},
}
diff --git a/frontend/src/components/CardBoxComponentBody.tsx b/frontend/src/components/CardBoxComponentBody.tsx
index 4c89896..e1fd4ae 100644
--- a/frontend/src/components/CardBoxComponentBody.tsx
+++ b/frontend/src/components/CardBoxComponentBody.tsx
@@ -8,5 +8,5 @@ type Props = {
}
export default function CardBoxComponentBody({ noPadding = false, className, children, id }: Props) {
- return
{children}
+ return {children}
}
diff --git a/frontend/src/components/FormField.tsx b/frontend/src/components/FormField.tsx
index 988ac39..57d36ee 100644
--- a/frontend/src/components/FormField.tsx
+++ b/frontend/src/components/FormField.tsx
@@ -27,14 +27,14 @@ const FormField = ({ icons = [], ...props }: Props) => {
switch (childrenCount) {
case 2:
- elementWrapperClass = 'grid grid-cols-1 gap-3 md:grid-cols-2'
+ elementWrapperClass = 'grid grid-cols-1 gap-1 md:grid-cols-2'
break
case 3:
- elementWrapperClass = 'grid grid-cols-1 gap-3 md:grid-cols-3'
+ elementWrapperClass = 'grid grid-cols-1 gap-1 md:grid-cols-3'
}
const controlClassName = [
- `px-3 py-2 max-w-full border-gray-300 dark:border-dark-700 ${corners} w-full dark:placeholder-gray-400`,
+ `px-2 py-1 max-w-full border-gray-300 dark:border-dark-700 ${corners} w-full dark:placeholder-gray-400`,
`${focusRing}`,
props.hasTextareaHeight ? 'h-24' : 'h-12',
props.isBorderless ? 'border-0' : 'border',
@@ -44,11 +44,11 @@ const FormField = ({ icons = [], ...props }: Props) => {
].join(' ');
return (
-
+
{props.label && (
diff --git a/frontend/src/components/InfoCard.module.css b/frontend/src/components/InfoCard.module.css
new file mode 100644
index 0000000..6287176
--- /dev/null
+++ b/frontend/src/components/InfoCard.module.css
@@ -0,0 +1,93 @@
+
+.info-card {
+ position: relative;
+ overflow: hidden;
+ background: #fff;
+ border-radius: 1rem;
+ padding: 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ }
+
+ .info-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
+ }
+
+ .info-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -150%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.4),
+ transparent
+ );
+ transition: left 0.8s ease;
+ }
+
+ .info-card:hover::before {
+ left: 150%;
+ }
+
+ .info-card-content {
+ z-index: 1;
+ }
+
+ .info-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 0.5rem;
+ }
+
+ .info-card-title {
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+ color: #6b7280;
+ }
+
+ .dark .info-card-title {
+ color: #9ca3af;
+ }
+
+ .info-card-wrench {
+ margin-left: 0.5rem;
+ }
+
+ .info-card-value {
+ font-size: 1.875rem;
+ line-height: 2.25rem;
+ font-weight: 600;
+ }
+
+ .info-card-icon {
+ z-index: 1;
+ opacity: 0.8;
+ }
+
+ .dark .info-card {
+ background: #2d3748;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
+ }
+
+ .dark .info-card:hover {
+ box-shadow: 0 10px 15px rgba(0, 0, 0, 0.3);
+ }
+
+ .dark .info-card::before {
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.1),
+ transparent
+ );
+ }
+
\ No newline at end of file
diff --git a/frontend/src/components/InfoCard.tsx b/frontend/src/components/InfoCard.tsx
new file mode 100644
index 0000000..0c6de05
--- /dev/null
+++ b/frontend/src/components/InfoCard.tsx
@@ -0,0 +1,41 @@
+
+import React from 'react';
+import Link from 'next/link';
+import BaseIcon from './BaseIcon';
+import { useAppSelector } from '../stores/hooks';
+import styles from './InfoCard.module.css';
+
+type InfoCardProps = {
+ href: string;
+ title: string;
+ value: string | number;
+ icon: string;
+ WrenchIcon?: any;
+};
+
+const InfoCard = ({ href, title, value, icon, WrenchIcon }: InfoCardProps) => {
+ const iconsColor = useAppSelector((state) => state.style.iconsColor);
+
+ return (
+
+
+
+
+
{title}
+ {WrenchIcon && (
+
+
+
+ )}
+
+
{value}
+
+
+
+
+
+
+ );
+};
+
+export default InfoCard;
diff --git a/frontend/src/components/SectionMain.tsx b/frontend/src/components/SectionMain.tsx
index 2b18097..67a5bd7 100644
--- a/frontend/src/components/SectionMain.tsx
+++ b/frontend/src/components/SectionMain.tsx
@@ -6,5 +6,5 @@ type Props = {
}
export default function SectionMain({ children }: Props) {
- return
+ return
}
diff --git a/frontend/src/components/SectionTitle.tsx b/frontend/src/components/SectionTitle.tsx
index c441e9e..ef7cb83 100644
--- a/frontend/src/components/SectionTitle.tsx
+++ b/frontend/src/components/SectionTitle.tsx
@@ -17,7 +17,7 @@ const SectionTitle = ({ custom = false, first = false, last = false, children }:
}
return (
-
+
{custom && children}
{!custom && {children}
}
diff --git a/frontend/src/components/TableSampleTasks.tsx b/frontend/src/components/TableSampleTasks.tsx
new file mode 100644
index 0000000..006ff33
--- /dev/null
+++ b/frontend/src/components/TableSampleTasks.tsx
@@ -0,0 +1,79 @@
+import React, { useEffect, useState } from 'react'
+import { useAppDispatch, useAppSelector } from '../stores/hooks'
+import { fetch } from '../stores/tasks/tasksSlice'
+import { Task } from '../interfaces'
+import BaseButton from './BaseButton'
+import BaseButtons from './BaseButtons'
+import CardBox from './CardBox'
+
+const TableSampleTasks = () => {
+ const dispatch = useAppDispatch()
+ const { currentUser } = useAppSelector((state) => state.auth)
+ const { tasks } = useAppSelector((state) => state.tasks)
+
+ useEffect(() => {
+ if (currentUser) {
+ dispatch(fetch({ query: `?assignee=${currentUser.id}&sort=due_date&order=asc` }))
+ }
+ }, [dispatch, currentUser])
+
+ const perPage = 5
+ const [currentPage, setCurrentPage] = useState(0)
+
+ const tasksPaginated = tasks.slice(perPage * currentPage, perPage * (currentPage + 1))
+ const numPages = Math.ceil(tasks.length / perPage)
+ const pagesList = []
+ for (let i = 0; i < numPages; i++) {
+ pagesList.push(i)
+ }
+
+ return (
+
+
+
+
+ | Title |
+ Project |
+ Status |
+ Due Date |
+
+
+
+ {tasksPaginated.map((task: Task) => (
+
+ | {task.title} |
+ {task.project?.name} |
+ {task.status} |
+
+
+ {new Date(task.due_date).toLocaleDateString()}
+
+ |
+
+ ))}
+
+
+
+
+
+ {pagesList.map((page) => (
+ setCurrentPage(page)}
+ />
+ ))}
+
+
+ Page {currentPage + 1} of {numPages}
+
+
+
+
+ )
+}
+
+export default TableSampleTasks
diff --git a/frontend/src/interfaces/index.ts b/frontend/src/interfaces/index.ts
index 0c7dd74..4a9e537 100644
--- a/frontend/src/interfaces/index.ts
+++ b/frontend/src/interfaces/index.ts
@@ -107,3 +107,24 @@ export type UserForm = {
name: string
email: string
}
+
+export interface Project {
+ id: string;
+ name: string;
+ description: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface Task {
+ id: string;
+ title: string;
+ description: string;
+ status: 'todo' | 'in_progress' | 'done';
+ deadline: string;
+ createdAt: string;
+ updatedAt: string;
+ assignee?: User;
+ project?: Project;
+ due_date: string;
+}
diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts
index 5da93c0..3b7f0e9 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -7,6 +7,16 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
},
+ {
+ href: '/test',
+ label: 'Test Page',
+ icon: icon.mdiTestTube,
+ },
+ {
+ href: '/project-statistics',
+ label: 'Project Statistics',
+ icon: icon.mdiChartBar,
+ },
{
href: '/users/users-list',
diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx
index 6feba56..2907af9 100644
--- a/frontend/src/pages/dashboard.tsx
+++ b/frontend/src/pages/dashboard.tsx
@@ -9,11 +9,13 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
import BaseIcon from "../components/BaseIcon";
import { getPageTitle } from '../config'
import Link from "next/link";
+import InfoCard from '../components/InfoCard';
import { hasPermission } from "../helpers/userPermissions";
import { fetchWidgets } from '../stores/roles/rolesSlice';
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
+import TableSampleTasks from '../components/TableSampleTasks';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
const Dashboard = () => {
@@ -140,235 +142,76 @@ const Dashboard = () => {
{!!rolesWidgets.length &&
}
-
-
-
- {hasPermission(currentUser, 'READ_USERS') &&
-
-
-
-
- Users
-
-
- {users}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_ROLES') &&
-
-
-
-
- Roles
-
-
- {roles}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PERMISSIONS') &&
-
-
-
-
- Permissions
-
-
- {permissions}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_TEAMS') &&
-
-
-
-
- Teams
-
-
- {teams}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PROJECTS') &&
-
-
-
-
- Projects
-
-
- {projects}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_TASKS') &&
-
-
-
-
- Tasks
-
-
- {tasks}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_COMMENTS') &&
-
-
-
-
- Comments
-
-
- {comments}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_REPORTS') &&
-
-
-
-
- Reports
-
-
- {reports}
-
-
-
-
-
-
-
- }
-
-
-
+
+ {hasPermission(currentUser, 'READ_USERS') && (
+
+ )}
+ {hasPermission(currentUser, 'READ_ROLES') && (
+
+ )}
+ {hasPermission(currentUser, 'READ_PERMISSIONS') && (
+
+ )}
+ {hasPermission(currentUser, 'READ_TEAMS') && (
+
+ )}
+ {hasPermission(currentUser, 'READ_PROJECTS') && (
+
+ )}
+ {hasPermission(currentUser, 'READ_TASKS') && (
+
+ )}
+ {hasPermission(currentUser, 'READ_COMMENTS') && (
+
+ )}
+ {hasPermission(currentUser, 'READ_REPORTS') && (
+
+ )}
+
+
+
+
+
>
)
diff --git a/frontend/src/pages/project-statistics.tsx b/frontend/src/pages/project-statistics.tsx
new file mode 100644
index 0000000..f180a2f
--- /dev/null
+++ b/frontend/src/pages/project-statistics.tsx
@@ -0,0 +1,175 @@
+import React, { ReactElement, useEffect, useState, useRef } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import LayoutAuthenticated from '../layouts/Authenticated';
+import CardBox from '../components/CardBox';
+import { fetch } from '../stores/projects/projectsSlice';
+import { RootState } from '../stores/store';
+import SectionMain from '../components/SectionMain';
+import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
+import FormField from '../components/FormField';
+import BaseButton from '../components/BaseButton';
+import { mdiChartTimelineVariant } from '@mdi/js';
+import Head from 'next/head';
+import { getPageTitle } from '../config';
+import { aiResponse, resetAiResponse } from '../stores/openAiSlice';
+
+const ProjectStatisticsPage = () => {
+ const dispatch = useDispatch();
+ const { projects, loading } = useSelector((state: RootState) => state.projects);
+ const { aiResponse: aiResult, isAskingResponse, errorMessage } = useSelector((state: RootState) => state.openAi);
+
+ const [messages, setMessages] = useState([
+ { text: 'Hello! How can I help you today?', sender: 'ai' },
+ ]);
+ const [inputValue, setInputValue] = useState('');
+ const [isTyping, setIsTyping] = useState(false);
+ const chatMessagesContainerRef = useRef(null);
+
+ useEffect(() => {
+ dispatch(fetch({}));
+ }, [dispatch]);
+
+ useEffect(() => {
+ if (chatMessagesContainerRef.current) {
+ chatMessagesContainerRef.current.scrollTop = chatMessagesContainerRef.current.scrollHeight;
+ }
+ }, [messages, isTyping]);
+
+ useEffect(() => {
+ if (isAskingResponse) {
+ setIsTyping(true);
+ } else {
+ setIsTyping(false);
+ }
+
+ if (aiResult) {
+ const text = aiResult.text;
+ if (text) {
+ setMessages((prevMessages) => [...prevMessages, { text, sender: 'ai' }]);
+ } else {
+ const errorMessageText = aiResult.message || 'An error occurred processing the AI response.';
+ setMessages((prevMessages) => [...prevMessages, { text: errorMessageText, sender: 'ai' }]);
+ }
+ dispatch(resetAiResponse());
+ }
+
+ if (errorMessage) {
+ setMessages((prevMessages) => [...prevMessages, { text: errorMessage, sender: 'ai' }]);
+ dispatch(resetAiResponse());
+ }
+ }, [aiResult, isAskingResponse, errorMessage, dispatch]);
+
+ const handleSendMessage = () => {
+ if (inputValue.trim()) {
+ const userMessage = { text: inputValue, sender: 'user' as const };
+ setMessages([...messages, userMessage]);
+
+ const payload = {
+ input: [...messages.map(m => ({ role: m.sender === 'user' ? 'user' : 'assistant', content: m.text })), { role: 'user', content: inputValue }],
+ };
+
+ setInputValue('');
+ dispatch(resetAiResponse());
+ dispatch(aiResponse(payload));
+ }
+ };
+
+ return (
+ <>
+
+ {getPageTitle('Project Statistics')}
+
+
+
+
+
+
+ {loading ? (
+ Loading...
+ ) : (
+ Total projects: {projects.length}
+ )}
+
+
+
+
+ {/* Chat messages area */}
+
+ {messages.map((message, index) => (
+
+ {message.sender === 'ai' && (
+
+ {/* Placeholder for AI avatar */}
+
+ )}
+
+ {message.sender === 'user' && (
+
+ {/* Placeholder for user avatar */}
+
+ )}
+
+ ))}
+ {isTyping && (
+
+ )}
+
+
+ {/* Chat input */}
+
+
+
+ setInputValue(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
+ disabled={isAskingResponse}
+ />
+
+
+
+
+
+
+
+ >
+ );
+};
+
+ProjectStatisticsPage.getLayout = function getLayout(page: ReactElement) {
+ return (
+
+ {page}
+
+ )
+}
+
+export default ProjectStatisticsPage;
diff --git a/frontend/src/pages/test.tsx b/frontend/src/pages/test.tsx
new file mode 100644
index 0000000..46993bb
--- /dev/null
+++ b/frontend/src/pages/test.tsx
@@ -0,0 +1,60 @@
+
+import { NextPage } from 'next'
+import Head from 'next/head'
+import SectionTitle from '../components/SectionTitle'
+import LayoutAuthenticated from '../layouts/Authenticated'
+import CardBox from '../components/CardBox'
+
+const TestPage: NextPage = () => {
+ return (
+ <>
+
+ Test Page - Flatlogic
+
+
+ Test Page
+
+
+
+
+ {/* Message list will go here */}
+
+
+
+
+
+
+ >
+ )
+}
+
+TestPage.getLayout = (page) => {page}
+
+export default TestPage
diff --git a/frontend/src/stores/openAiSlice.ts b/frontend/src/stores/openAiSlice.ts
index 1328235..24e7eda 100644
--- a/frontend/src/stores/openAiSlice.ts
+++ b/frontend/src/stores/openAiSlice.ts
@@ -92,6 +92,10 @@ export const openAiSlice = createSlice({
setErrorNotification: (state, action) => {
fulfilledNotify(state, action.payload, 'error');
},
+ resetAiResponse: (state) => {
+ state.aiResponse = null;
+ state.errorMessage = '';
+ },
},
extraReducers: (builder) => {
builder.addCase(aiPrompt.pending, (state) => {
@@ -146,6 +150,6 @@ export const openAiSlice = createSlice({
});
// Action creators are generated for each case reducer function
-export const { resetNotify, setErrorNotification } = openAiSlice.actions;
+export const { resetNotify, setErrorNotification, resetAiResponse } = openAiSlice.actions;
export default openAiSlice.reducer;
diff --git a/frontend/src/styles.ts b/frontend/src/styles.ts
index a969b60..d3b659d 100644
--- a/frontend/src/styles.ts
+++ b/frontend/src/styles.ts
@@ -39,8 +39,7 @@ export const white: StyleObject = {
bgLayoutColor: 'bg-gray-50',
iconsColor: 'text-blue-500',
cardsColor: 'bg-white',
- focusRingColor: 'focus:ring focus:ring-blue-600 focus:border-blue-600 focus:outline-none border-gray-300 dark:focus:ring-blue-600 dark:focus:border-blue-600',
- corners: 'rounded',
+ corners: '',
cardsStyle: 'bg-white border border-pavitra-400',
linkColor: 'text-blue-600',
websiteHeder: 'border-b border-gray-200',
@@ -67,13 +66,13 @@ export const dataGridStyles = {
},
'& .MuiDataGrid-columnHeaders': {
paddingY: 4,
- borderStartStartRadius: 7,
- borderStartEndRadius: 7,
+ borderStartStartRadius: 0,
+ borderStartEndRadius: 0,
},
'& .MuiDataGrid-footerContainer': {
paddingY: 0.5,
- borderEndStartRadius: 7,
- borderEndEndRadius: 7,
+ borderEndStartRadius: 0,
+ borderEndEndRadius: 0,
},
'& .MuiDataGrid-root': {
border: 'none',
@@ -96,7 +95,7 @@ export const basic: StyleObject = {
iconsColor: 'text-blue-500',
cardsColor: 'bg-white',
focusRingColor: 'focus:ring focus:ring-blue-600 focus:border-blue-600 focus:outline-none dark:focus:ring-blue-600 border-gray-300 dark:focus:border-blue-600',
- corners: 'rounded',
+ corners: '',
cardsStyle: 'bg-white border border-pavitra-400',
linkColor: 'text-black',
websiteHeder: '',
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
index a06c7d1..aa9db33 100644
--- a/frontend/tailwind.config.js
+++ b/frontend/tailwind.config.js
@@ -68,8 +68,15 @@ module.exports = {
},
- borderRadius: {
- '3xl': '2rem',
+ borderRadius: {
+ 'none': '0',
+ 'sm': '0',
+ 'md': '0',
+ 'lg': '0',
+ 'xl': '0',
+ '2xl': '0',
+ '3xl': '0',
+ DEFAULT: '0'
},
}
},