This commit is contained in:
Flatlogic Bot 2026-01-08 12:48:49 +00:00
parent 5aa8382fbf
commit a07e0b335c
19 changed files with 712 additions and 294 deletions

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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);

View File

@ -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",
},
}

View File

@ -8,5 +8,5 @@ type Props = {
}
export default function CardBoxComponentBody({ noPadding = false, className, children, id }: Props) {
return <div id={id} className={`flex-1 ${noPadding ? '' : 'p-6'} ${className}`}>{children}</div>
return <div id={id} className={`flex-1 ${noPadding ? '' : 'p-2'} ${className}`}>{children}</div>
}

View File

@ -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 (
<div className="mb-6 last:mb-0">
<div className="mb-2 last:mb-0">
{props.label && (
<label
htmlFor={props.labelFor}
className={`block font-bold mb-2 ${props.labelFor ? 'cursor-pointer' : ''}`}
className={`block font-bold mb-1 ${props.labelFor ? 'cursor-pointer' : ''}`}
>
{props.label}
</label>

View File

@ -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
);
}

View File

@ -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 (
<Link href={href}>
<div className={styles['info-card']}>
<div className={styles['info-card-content']}>
<div className={styles['info-card-header']}>
<div className={styles['info-card-title']}>{title}</div>
{WrenchIcon && (
<div className={styles['info-card-wrench']}>
<WrenchIcon />
</div>
)}
</div>
<div className={styles['info-card-value']}>{value}</div>
</div>
<div className={styles['info-card-icon']}>
<BaseIcon className={iconsColor} w="w-16" h="h-16" size={48} path={icon} />
</div>
</div>
</Link>
);
};
export default InfoCard;

View File

@ -6,5 +6,5 @@ type Props = {
}
export default function SectionMain({ children }: Props) {
return <section className={`p-6 ${containerMaxW}`}>{children}</section>
return <section className={`p-2 ${containerMaxW}`}>{children}</section>
}

View File

@ -17,7 +17,7 @@ const SectionTitle = ({ custom = false, first = false, last = false, children }:
}
return (
<section className={`py-24 px-6 lg:px-0 lg:max-w-2xl lg:mx-auto text-center ${classAddon}`}>
<section className={`py-8 px-2 lg:px-0 lg:max-w-2xl lg:mx-auto text-center ${classAddon}`}>
{custom && children}
{!custom && <h1 className="text-2xl text-gray-500 dark:text-slate-400">{children}</h1>}
</section>

View File

@ -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 (
<CardBox className="mb-6">
<table>
<thead>
<tr>
<th>Title</th>
<th>Project</th>
<th>Status</th>
<th>Due Date</th>
</tr>
</thead>
<tbody>
{tasksPaginated.map((task: Task) => (
<tr key={task.id}>
<td data-label="Title">{task.title}</td>
<td data-label="Project">{task.project?.name}</td>
<td data-label="Status">{task.status}</td>
<td data-label="Due Date" className="lg:w-1 whitespace-nowrap">
<small className="text-gray-500 dark:text-slate-400">
{new Date(task.due_date).toLocaleDateString()}
</small>
</td>
</tr>
))}
</tbody>
</table>
<div className="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800">
<div className="flex flex-col md:flex-row items-center justify-between py-3 md:py-0">
<BaseButtons>
{pagesList.map((page) => (
<BaseButton
key={page}
active={page === currentPage}
label={page + 1}
color={page === currentPage ? 'lightDark' : 'whiteDark'}
small
onClick={() => setCurrentPage(page)}
/>
))}
</BaseButtons>
<small className="mt-6 md:mt-0">
Page {currentPage + 1} of {numPages}
</small>
</div>
</div>
</CardBox>
)
}
export default TableSampleTasks

View File

@ -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;
}

View File

@ -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',

View File

@ -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 && <hr className='my-6 ' />}
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Users
</div>
<div className="text-3xl leading-tight font-semibold">
{users}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiAccountGroup || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Roles
</div>
<div className="text-3xl leading-tight font-semibold">
{roles}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Permissions
</div>
<div className="text-3xl leading-tight font-semibold">
{permissions}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiShieldAccountOutline || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_TEAMS') && <Link href={'/teams/teams-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Teams
</div>
<div className="text-3xl leading-tight font-semibold">
{teams}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PROJECTS') && <Link href={'/projects/projects-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Projects
</div>
<div className="text-3xl leading-tight font-semibold">
{projects}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiFolderMultiple' in icon ? icon['mdiFolderMultiple' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_TASKS') && <Link href={'/tasks/tasks-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Tasks
</div>
<div className="text-3xl leading-tight font-semibold">
{tasks}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiClipboardText' in icon ? icon['mdiClipboardText' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_COMMENTS') && <Link href={'/comments/comments-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Comments
</div>
<div className="text-3xl leading-tight font-semibold">
{comments}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiCommentText' in icon ? icon['mdiCommentText' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_REPORTS') && <Link href={'/reports/reports-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Reports
</div>
<div className="text-3xl leading-tight font-semibold">
{reports}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiFileDocument' in icon ? icon['mdiFileDocument' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
</div>
<div id="dashboard" className="grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6">
{hasPermission(currentUser, 'READ_USERS') && (
<InfoCard
href="/users/users-list"
title="Users"
value={users}
icon={icon.mdiAccountGroup}
/>
)}
{hasPermission(currentUser, 'READ_ROLES') && (
<InfoCard
href="/roles/roles-list"
title="Roles"
value={roles}
icon={icon.mdiShieldAccountVariantOutline}
/>
)}
{hasPermission(currentUser, 'READ_PERMISSIONS') && (
<InfoCard
href="/permissions/permissions-list"
title="Permissions"
value={permissions}
icon={icon.mdiShieldAccountOutline}
/>
)}
{hasPermission(currentUser, 'READ_TEAMS') && (
<InfoCard
href="/teams/teams-list"
title="Teams"
value={teams}
icon={icon.mdiAccountGroup}
/>
)}
{hasPermission(currentUser, 'READ_PROJECTS') && (
<InfoCard
href="/projects/projects-list"
title="Projects"
value={projects}
icon={icon.mdiFolderMultiple}
/>
)}
{hasPermission(currentUser, 'READ_TASKS') && (
<InfoCard
href="/tasks/tasks-list"
title="Tasks"
value={tasks}
icon={icon.mdiClipboardText}
/>
)}
{hasPermission(currentUser, 'READ_COMMENTS') && (
<InfoCard
href="/comments/comments-list"
title="Comments"
value={comments}
icon={icon.mdiCommentText}
/>
)}
{hasPermission(currentUser, 'READ_REPORTS') && (
<InfoCard
href="/reports/reports-list"
title="Reports"
value={reports}
icon={icon.mdiFileDocument}
/>
)}
</div>
<SectionTitleLineWithButton icon={icon.mdiClipboardText} title="My Tasks" />
<TableSampleTasks />
</SectionMain>
</>
)

View File

@ -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<HTMLDivElement>(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 (
<>
<Head>
<title>{getPageTitle('Project Statistics')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Project Statistics" main>
</SectionTitleLineWithButton>
<CardBox className="mb-6">
{loading ? (
<p>Loading...</p>
) : (
<p>Total projects: {projects.length}</p>
)}
</CardBox>
<CardBox>
<div className="flex flex-col h-[500px]">
{/* Chat messages area */}
<div ref={chatMessagesContainerRef} className="flex-grow p-4 overflow-y-auto">
{messages.map((message, index) => (
<div
key={index}
className={`flex items-end mb-4 ${
message.sender === 'user' ? 'justify-end' : ''
}`}
>
{message.sender === 'ai' && (
<div className="flex-shrink-0">
{/* Placeholder for AI avatar */}
</div>
)}
<div
className={`p-3 rounded-lg ${
message.sender === 'user'
? 'bg-blue-500 text-white mr-3'
: 'bg-gray-200 dark:bg-slate-700 ml-3'
}`}
>
<p className="text-sm">{message.text}</p>
</div>
{message.sender === 'user' && (
<div className="flex-shrink-0">
{/* Placeholder for user avatar */}
</div>
)}
</div>
))}
{isTyping && (
<div className="flex items-end mb-4">
<div className="flex-shrink-0"></div>
<div className="ml-3 p-3 rounded-lg bg-gray-200 dark:bg-slate-700">
<p className="text-sm">AI is typing...</p>
</div>
</div>
)}
</div>
{/* Chat input */}
<div className="p-4 bg-gray-100 dark:bg-slate-800">
<div className="flex items-center">
<FormField>
<input
className="w-full bg-transparent p-2 border border-gray-300 dark:border-slate-600 rounded-lg"
placeholder="Type your message..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
disabled={isAskingResponse}
/>
</FormField>
<BaseButton
label="Send"
color="info"
className="ml-2"
onClick={handleSendMessage}
disabled={isAskingResponse}
/>
</div>
</div>
</div>
</CardBox>
</SectionMain>
</>
);
};
ProjectStatisticsPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_PROJECTS'}
>
{page}
</LayoutAuthenticated>
)
}
export default ProjectStatisticsPage;

View File

@ -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 (
<>
<Head>
<title>Test Page - Flatlogic</title>
</Head>
<SectionTitle>Test Page</SectionTitle>
<CardBox className="ml-6">
<div className="flex flex-col">
<div className="flex-grow p-4 overflow-y-auto">
{/* Message list will go here */}
<div className="flex items-end mb-4">
<div className="flex-shrink-0 w-8 h-8 bg-gray-300 rounded-full"></div>
<div className="ml-3 p-3 bg-gray-200 rounded-lg">
<p className="text-sm">Hello!</p>
</div>
</div>
<div className="flex items-end justify-end mb-4">
<div className="mr-3 p-3 bg-blue-500 text-white rounded-lg">
<p className="text-sm">Hi there!</p>
</div>
<div className="flex-shrink-0 w-8 h-8 bg-gray-300 rounded-full"></div>
</div>
</div>
<div className="p-4 bg-gray-100">
<div className="flex items-center">
<div className="flex-grow">
<input
type="text"
className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring focus:border-blue-300"
placeholder="Type your message..."
/>
</div>
<div className="ml-3">
<button
className="px-4 py-2 text-white bg-blue-500 rounded-lg hover:bg-blue-600 focus:outline-none focus:ring focus:bg-blue-600"
>
Send
</button>
</div>
</div>
</div>
</div>
</CardBox>
</>
)
}
TestPage.getLayout = (page) => <LayoutAuthenticated>{page}</LayoutAuthenticated>
export default TestPage

View File

@ -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;

View File

@ -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: '',

View File

@ -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'
},
}
},