Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a07e0b335c |
37
backend/src/db/models/project.js
Normal file
37
backend/src/db/models/project.js
Normal 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;
|
||||||
|
};
|
||||||
48
backend/src/db/models/task.js
Normal file
48
backend/src/db/models/task.js
Normal 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;
|
||||||
|
};
|
||||||
@ -254,7 +254,8 @@ router.post(
|
|||||||
const response = await LocalAIApi.createResponse(payload, options);
|
const response = await LocalAIApi.createResponse(payload, options);
|
||||||
|
|
||||||
if (response.success) {
|
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);
|
console.error('AI proxy error:', response);
|
||||||
|
|||||||
@ -1,40 +1,40 @@
|
|||||||
import type { ColorButtonKey } from './interfaces'
|
import type { ColorButtonKey } from './interfaces'
|
||||||
|
|
||||||
export const gradientBgBase = 'bg-gradient-to-tr'
|
export const gradientBgBase = 'bg-gray-100 dark:bg-dark-900'
|
||||||
export const colorBgBase = "bg-violet-50/50"
|
export const colorBgBase = "bg-gray-100 dark:bg-dark-900"
|
||||||
export const gradientBgPurplePink = `${gradientBgBase} from-purple-400 via-pink-500 to-red-500`
|
export const gradientBgPurplePink = `bg-gray-100 dark:bg-dark-900`
|
||||||
export const gradientBgViolet = `${gradientBgBase} ${colorBgBase}`
|
export const gradientBgViolet = `bg-gray-100 dark:bg-dark-900`
|
||||||
export const gradientBgDark = `${gradientBgBase} from-dark-700 via-dark-900 to-dark-800`;
|
export const gradientBgDark = `bg-dark-900`;
|
||||||
export const gradientBgPinkRed = `${gradientBgBase} from-pink-400 via-red-500 to-yellow-500`
|
export const gradientBgPinkRed = `bg-gray-100 dark:bg-dark-900`
|
||||||
|
|
||||||
export const colorsBgLight = {
|
export const colorsBgLight = {
|
||||||
white: 'bg-white text-black',
|
white: 'bg-white text-black',
|
||||||
light: ' bg-white text-black text-black dark:bg-dark-900 dark:text-white',
|
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',
|
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',
|
success: 'bg-gray-500 border-gray-500 dark:bg-gray-700 dark:border-gray-700 text-white',
|
||||||
danger: 'bg-red-500 border-red-500 text-white',
|
danger: 'bg-gray-500 border-gray-500 text-white',
|
||||||
warning: 'bg-yellow-500 border-yellow-500 text-white',
|
warning: 'bg-gray-500 border-gray-500 text-white',
|
||||||
info: 'bg-blue-500 border-blue-500 dark:bg-pavitra-blue dark:border-pavitra-blue text-white',
|
info: 'bg-gray-500 border-gray-500 dark:bg-gray-700 dark:border-gray-700 text-white',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const colorsText = {
|
export const colorsText = {
|
||||||
white: 'text-black dark:text-slate-100',
|
white: 'text-black dark:text-slate-100',
|
||||||
light: 'text-gray-700 dark:text-slate-400',
|
light: 'text-gray-700 dark:text-slate-400',
|
||||||
contrast: 'dark:text-white',
|
contrast: 'dark:text-white',
|
||||||
success: 'text-emerald-500',
|
success: 'text-gray-500',
|
||||||
danger: 'text-red-500',
|
danger: 'text-gray-500',
|
||||||
warning: 'text-yellow-500',
|
warning: 'text-gray-500',
|
||||||
info: 'text-blue-500',
|
info: 'text-gray-500',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const colorsOutline = {
|
export const colorsOutline = {
|
||||||
white: [colorsText.white, 'border-gray-100'].join(' '),
|
white: [colorsText.white, 'border-gray-100'].join(' '),
|
||||||
light: [colorsText.light, 'border-gray-100'].join(' '),
|
light: [colorsText.light, 'border-gray-100'].join(' '),
|
||||||
contrast: [colorsText.contrast, 'border-gray-900 dark:border-slate-100'].join(' '),
|
contrast: [colorsText.contrast, 'border-gray-900 dark:border-slate-100'].join(' '),
|
||||||
success: [colorsText.success, 'border-emerald-500'].join(' '),
|
success: [colorsText.success, 'border-gray-500'].join(' '),
|
||||||
danger: [colorsText.danger, 'border-red-500'].join(' '),
|
danger: [colorsText.danger, 'border-gray-500'].join(' '),
|
||||||
warning: [colorsText.warning, 'border-yellow-500'].join(' '),
|
warning: [colorsText.warning, 'border-gray-500'].join(' '),
|
||||||
info: [colorsText.info, 'border-blue-500'].join(' '),
|
info: [colorsText.info, 'border-gray-500'].join(' '),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getButtonColor = (
|
export const getButtonColor = (
|
||||||
@ -53,30 +53,30 @@ export const getButtonColor = (
|
|||||||
whiteDark: 'ring-gray-200 dark:ring-dark-500',
|
whiteDark: 'ring-gray-200 dark:ring-dark-500',
|
||||||
lightDark: 'ring-gray-200 dark:ring-gray-500',
|
lightDark: 'ring-gray-200 dark:ring-gray-500',
|
||||||
contrast: 'ring-gray-300 dark:ring-gray-400',
|
contrast: 'ring-gray-300 dark:ring-gray-400',
|
||||||
success: 'ring-emerald-300 dark:ring-pavitra-blue',
|
success: 'ring-gray-300 dark:ring-gray-700',
|
||||||
danger: 'ring-red-300 dark:ring-red-700',
|
danger: 'ring-gray-300 dark:ring-gray-700',
|
||||||
warning: 'ring-yellow-300 dark:ring-yellow-700',
|
warning: 'ring-gray-300 dark:ring-gray-700',
|
||||||
info: "ring-blue-300 dark:ring-pavitra-blue",
|
info: "ring-gray-300 dark:ring-gray-700",
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
white: 'bg-gray-100',
|
white: 'bg-gray-100',
|
||||||
whiteDark: 'bg-gray-100 dark:bg-dark-800',
|
whiteDark: 'bg-gray-100 dark:bg-dark-800',
|
||||||
lightDark: 'bg-gray-200 dark:bg-slate-700',
|
lightDark: 'bg-gray-200 dark:bg-slate-700',
|
||||||
contrast: 'bg-gray-700 dark:bg-slate-100',
|
contrast: 'bg-gray-700 dark:bg-slate-100',
|
||||||
success: 'bg-emerald-700 dark:bg-pavitra-blue',
|
success: 'bg-gray-700 dark:bg-gray-800',
|
||||||
danger: 'bg-red-700 dark:bg-red-600',
|
danger: 'bg-gray-700 dark:bg-gray-600',
|
||||||
warning: 'bg-yellow-700 dark:bg-yellow-600',
|
warning: 'bg-gray-700 dark:bg-gray-600',
|
||||||
info: 'bg-blue-700 dark:bg-pavitra-blue',
|
info: 'bg-gray-700 dark:bg-gray-800',
|
||||||
},
|
},
|
||||||
bg: {
|
bg: {
|
||||||
white: 'bg-white text-black',
|
white: 'bg-white text-black',
|
||||||
whiteDark: 'bg-white text-black dark:bg-dark-900 dark:text-white',
|
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',
|
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',
|
contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black',
|
||||||
success: 'bg-emerald-600 dark:bg-pavitra-blue text-white',
|
success: 'bg-gray-600 dark:bg-gray-700 text-white',
|
||||||
danger: 'bg-red-600 text-white dark:bg-red-500 ',
|
danger: 'bg-gray-600 text-white dark:bg-gray-500 ',
|
||||||
warning: 'bg-yellow-600 dark:bg-yellow-500 text-white',
|
warning: 'bg-gray-600 dark:bg-gray-500 text-white',
|
||||||
info: " bg-blue-600 dark:bg-pavitra-blue text-white ",
|
info: " bg-gray-600 dark:bg-gray-700 text-white ",
|
||||||
},
|
},
|
||||||
bgHover: {
|
bgHover: {
|
||||||
white: 'hover:bg-gray-100',
|
white: 'hover:bg-gray-100',
|
||||||
@ -84,39 +84,39 @@ export const getButtonColor = (
|
|||||||
lightDark: 'hover:bg-gray-200 hover:dark:bg-slate-700',
|
lightDark: 'hover:bg-gray-200 hover:dark:bg-slate-700',
|
||||||
contrast: 'hover:bg-gray-700 hover:dark:bg-slate-100',
|
contrast: 'hover:bg-gray-700 hover:dark:bg-slate-100',
|
||||||
success:
|
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:
|
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:
|
warning:
|
||||||
'hover:bg-yellow-700 hover:border-yellow-700 hover:dark:bg-yellow-600 hover:dark:border-yellow-600',
|
'hover:bg-gray-700 hover:border-gray-700 hover:dark:bg-gray-600 hover:dark:border-gray-600',
|
||||||
info: "hover:bg-blue-700 hover:border-blue-700 hover:dark:bg-pavitra-blue/80 hover:dark:border-pavitra-blue/80",
|
info: "hover:bg-gray-700 hover:border-gray-700 hover:dark:bg-gray-800/80 hover:dark:border-gray-800/80",
|
||||||
},
|
},
|
||||||
borders: {
|
borders: {
|
||||||
white: 'border-white',
|
white: 'border-white',
|
||||||
whiteDark: 'border-white dark:border-dark-900',
|
whiteDark: 'border-white dark:border-dark-900',
|
||||||
lightDark: 'border-gray-100 dark:border-slate-800',
|
lightDark: 'border-gray-100 dark:border-slate-800',
|
||||||
contrast: 'border-gray-800 dark:border-white',
|
contrast: 'border-gray-800 dark:border-white',
|
||||||
success: 'border-emerald-600 dark:border-pavitra-blue',
|
success: 'border-gray-600 dark:border-gray-700',
|
||||||
danger: 'border-red-600 dark:border-red-500',
|
danger: 'border-gray-600 dark:border-gray-500',
|
||||||
warning: 'border-yellow-600 dark:border-yellow-500',
|
warning: 'border-gray-600 dark:border-gray-500',
|
||||||
info: "border-blue-600 border-blue-600 dark:border-pavitra-blue",
|
info: "border-gray-600 border-gray-600 dark:border-gray-700",
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
contrast: 'dark:text-slate-100',
|
contrast: 'dark:text-slate-100',
|
||||||
success: 'text-emerald-600 dark:text-pavitra-blue',
|
success: 'text-gray-600 dark:text-gray-400',
|
||||||
danger: 'text-red-600 dark:text-red-500',
|
danger: 'text-gray-600 dark:text-gray-400',
|
||||||
warning: 'text-yellow-600 dark:text-yellow-500',
|
warning: 'text-gray-600 dark:text-gray-400',
|
||||||
info: 'text-blue-600 dark:text-pavitra-blue',
|
info: 'text-gray-600 dark:text-gray-400',
|
||||||
},
|
},
|
||||||
outlineHover: {
|
outlineHover: {
|
||||||
contrast:
|
contrast:
|
||||||
'hover:bg-gray-800 hover:text-gray-100 hover:dark:bg-slate-100 hover:dark:text-black',
|
'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:
|
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:
|
warning:
|
||||||
'hover:bg-yellow-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-yellow-600',
|
'hover:bg-gray-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-gray-600',
|
||||||
info: "hover:bg-blue-600 hover:bg-blue-600 hover:text-white hover:dark:text-white hover:dark:border-pavitra-blue",
|
info: "hover:bg-gray-600 hover:bg-gray-600 hover:text-white hover:dark:text-white hover:dark:border-gray-700",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,5 +8,5 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CardBoxComponentBody({ noPadding = false, className, children, id }: 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>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,14 +27,14 @@ const FormField = ({ icons = [], ...props }: Props) => {
|
|||||||
|
|
||||||
switch (childrenCount) {
|
switch (childrenCount) {
|
||||||
case 2:
|
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
|
break
|
||||||
case 3:
|
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 = [
|
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}`,
|
`${focusRing}`,
|
||||||
props.hasTextareaHeight ? 'h-24' : 'h-12',
|
props.hasTextareaHeight ? 'h-24' : 'h-12',
|
||||||
props.isBorderless ? 'border-0' : 'border',
|
props.isBorderless ? 'border-0' : 'border',
|
||||||
@ -44,11 +44,11 @@ const FormField = ({ icons = [], ...props }: Props) => {
|
|||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 last:mb-0">
|
<div className="mb-2 last:mb-0">
|
||||||
{props.label && (
|
{props.label && (
|
||||||
<label
|
<label
|
||||||
htmlFor={props.labelFor}
|
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}
|
{props.label}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
93
frontend/src/components/InfoCard.module.css
Normal file
93
frontend/src/components/InfoCard.module.css
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
41
frontend/src/components/InfoCard.tsx
Normal file
41
frontend/src/components/InfoCard.tsx
Normal 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;
|
||||||
@ -6,5 +6,5 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SectionMain({ children }: Props) {
|
export default function SectionMain({ children }: Props) {
|
||||||
return <section className={`p-6 ${containerMaxW}`}>{children}</section>
|
return <section className={`p-2 ${containerMaxW}`}>{children}</section>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const SectionTitle = ({ custom = false, first = false, last = false, children }:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 && children}
|
||||||
{!custom && <h1 className="text-2xl text-gray-500 dark:text-slate-400">{children}</h1>}
|
{!custom && <h1 className="text-2xl text-gray-500 dark:text-slate-400">{children}</h1>}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
79
frontend/src/components/TableSampleTasks.tsx
Normal file
79
frontend/src/components/TableSampleTasks.tsx
Normal 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
|
||||||
@ -107,3 +107,24 @@ export type UserForm = {
|
|||||||
name: string
|
name: string
|
||||||
email: 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;
|
||||||
|
}
|
||||||
|
|||||||
@ -7,6 +7,16 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/test',
|
||||||
|
label: 'Test Page',
|
||||||
|
icon: icon.mdiTestTube,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/project-statistics',
|
||||||
|
label: 'Project Statistics',
|
||||||
|
icon: icon.mdiChartBar,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
href: '/users/users-list',
|
||||||
|
|||||||
@ -9,11 +9,13 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
|
|||||||
import BaseIcon from "../components/BaseIcon";
|
import BaseIcon from "../components/BaseIcon";
|
||||||
import { getPageTitle } from '../config'
|
import { getPageTitle } from '../config'
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import InfoCard from '../components/InfoCard';
|
||||||
|
|
||||||
import { hasPermission } from "../helpers/userPermissions";
|
import { hasPermission } from "../helpers/userPermissions";
|
||||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
||||||
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
||||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
||||||
|
import TableSampleTasks from '../components/TableSampleTasks';
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
@ -140,235 +142,76 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
||||||
|
|
||||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
<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 />
|
||||||
|
|
||||||
{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>
|
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
175
frontend/src/pages/project-statistics.tsx
Normal file
175
frontend/src/pages/project-statistics.tsx
Normal 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;
|
||||||
60
frontend/src/pages/test.tsx
Normal file
60
frontend/src/pages/test.tsx
Normal 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
|
||||||
@ -92,6 +92,10 @@ export const openAiSlice = createSlice({
|
|||||||
setErrorNotification: (state, action) => {
|
setErrorNotification: (state, action) => {
|
||||||
fulfilledNotify(state, action.payload, 'error');
|
fulfilledNotify(state, action.payload, 'error');
|
||||||
},
|
},
|
||||||
|
resetAiResponse: (state) => {
|
||||||
|
state.aiResponse = null;
|
||||||
|
state.errorMessage = '';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(aiPrompt.pending, (state) => {
|
builder.addCase(aiPrompt.pending, (state) => {
|
||||||
@ -146,6 +150,6 @@ export const openAiSlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Action creators are generated for each case reducer function
|
// 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;
|
export default openAiSlice.reducer;
|
||||||
|
|||||||
@ -39,8 +39,7 @@ export const white: StyleObject = {
|
|||||||
bgLayoutColor: 'bg-gray-50',
|
bgLayoutColor: 'bg-gray-50',
|
||||||
iconsColor: 'text-blue-500',
|
iconsColor: 'text-blue-500',
|
||||||
cardsColor: 'bg-white',
|
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: '',
|
||||||
corners: 'rounded',
|
|
||||||
cardsStyle: 'bg-white border border-pavitra-400',
|
cardsStyle: 'bg-white border border-pavitra-400',
|
||||||
linkColor: 'text-blue-600',
|
linkColor: 'text-blue-600',
|
||||||
websiteHeder: 'border-b border-gray-200',
|
websiteHeder: 'border-b border-gray-200',
|
||||||
@ -67,13 +66,13 @@ export const dataGridStyles = {
|
|||||||
},
|
},
|
||||||
'& .MuiDataGrid-columnHeaders': {
|
'& .MuiDataGrid-columnHeaders': {
|
||||||
paddingY: 4,
|
paddingY: 4,
|
||||||
borderStartStartRadius: 7,
|
borderStartStartRadius: 0,
|
||||||
borderStartEndRadius: 7,
|
borderStartEndRadius: 0,
|
||||||
},
|
},
|
||||||
'& .MuiDataGrid-footerContainer': {
|
'& .MuiDataGrid-footerContainer': {
|
||||||
paddingY: 0.5,
|
paddingY: 0.5,
|
||||||
borderEndStartRadius: 7,
|
borderEndStartRadius: 0,
|
||||||
borderEndEndRadius: 7,
|
borderEndEndRadius: 0,
|
||||||
},
|
},
|
||||||
'& .MuiDataGrid-root': {
|
'& .MuiDataGrid-root': {
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@ -96,7 +95,7 @@ export const basic: StyleObject = {
|
|||||||
iconsColor: 'text-blue-500',
|
iconsColor: 'text-blue-500',
|
||||||
cardsColor: 'bg-white',
|
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',
|
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',
|
cardsStyle: 'bg-white border border-pavitra-400',
|
||||||
linkColor: 'text-black',
|
linkColor: 'text-black',
|
||||||
websiteHeder: '',
|
websiteHeder: '',
|
||||||
|
|||||||
@ -68,8 +68,15 @@ module.exports = {
|
|||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
'3xl': '2rem',
|
'none': '0',
|
||||||
|
'sm': '0',
|
||||||
|
'md': '0',
|
||||||
|
'lg': '0',
|
||||||
|
'xl': '0',
|
||||||
|
'2xl': '0',
|
||||||
|
'3xl': '0',
|
||||||
|
DEFAULT: '0'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user