From a07e0b335ce1979224ec69f78a7940823957530f Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 8 Jan 2026 12:48:49 +0000 Subject: [PATCH] v1 --- backend/src/db/models/project.js | 37 +++ backend/src/db/models/task.js | 48 +++ backend/src/routes/openai.js | 3 +- frontend/src/colors.ts | 92 +++--- .../src/components/CardBoxComponentBody.tsx | 2 +- frontend/src/components/FormField.tsx | 10 +- frontend/src/components/InfoCard.module.css | 93 ++++++ frontend/src/components/InfoCard.tsx | 41 +++ frontend/src/components/SectionMain.tsx | 2 +- frontend/src/components/SectionTitle.tsx | 2 +- frontend/src/components/TableSampleTasks.tsx | 79 +++++ frontend/src/interfaces/index.ts | 21 ++ frontend/src/menuAside.ts | 10 + frontend/src/pages/dashboard.tsx | 301 +++++------------- frontend/src/pages/project-statistics.tsx | 175 ++++++++++ frontend/src/pages/test.tsx | 60 ++++ frontend/src/stores/openAiSlice.ts | 6 +- frontend/src/styles.ts | 13 +- frontend/tailwind.config.js | 11 +- 19 files changed, 712 insertions(+), 294 deletions(-) create mode 100644 backend/src/db/models/project.js create mode 100644 backend/src/db/models/task.js create mode 100644 frontend/src/components/InfoCard.module.css create mode 100644 frontend/src/components/InfoCard.tsx create mode 100644 frontend/src/components/TableSampleTasks.tsx create mode 100644 frontend/src/pages/project-statistics.tsx create mode 100644 frontend/src/pages/test.tsx 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
{children}
+ return
{children}
} 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 ( + + + + + + + + + + + + {tasksPaginated.map((task: Task) => ( + + + + + + + ))} + +
TitleProjectStatusDue Date
{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.text}

+
+ {message.sender === 'user' && ( +
+ {/* Placeholder for user avatar */} +
+ )} +
+ ))} + {isTyping && ( +
+
+
+

AI is typing...

+
+
+ )} +
+ + {/* 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 */} +
+
+
+

Hello!

+
+
+
+
+

Hi there!

+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+ + ) +} + +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' }, } },