+
+
+
navigate(-1)}>
+
+ Back to Dashboard
+
+
+
+
+ {statusConfig[displayStatus].label}
+ {priority.label}
+
+ {isEditing ? (
+
setEditTitle(e.target.value)}
+ className="text-2xl h-auto py-1 font-semibold !border-blue-300 mb-2"
+ placeholder="Task title"
+ />
+ ) : (
+
{task.title}
+ )}
+ {isEditing ? (
+
+
+ {!isReadOnly && (
+ <>
+ {isEditing && (
+
+ Cancel
+
+ )}
+ {!isEditing && (
+
setIsDeleteDialogOpen(true)}
+ >
+
+ Delete Task
+
+ )}
+
+ {isEditing ? (
+ <>
+
+ {isSaving ? 'Saving...' : 'Save Changes'}
+ >
+ ) : (
+ <>
+
+ Edit Task
+ >
+ )}
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
Task Details
+
+
+
+
+
Assigned To
+ {isEditing ? (
+
+
+
+
+
+ {developers.map((dev) => (
+ {dev.name}
+ ))}
+
+
+ ) : (
+
{task.assignedDeveloper?.name || 'Unassigned'}
+ )}
+
+
+
+
+
+
Due Date
+ {isEditing ? (
+
setEditDueDate(e.target.value)}
+ className="!border-blue-300 mt-1 h-8 text-sm"
+ />
+ ) : (
+
{new Date(task.dueDate).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
+ )}
+
+
+
+
+
+
Created
+
{new Date(task.createdAt).toLocaleDateString()}
+
+
+
+
+
+
Last Updated
+
{new Date(task.updatedAt).toLocaleDateString()}
+
+
+
+
+
+
+
+
Time of completion
+
+
+
+
Estimated
+ {isEditing ? (
+
setEditEstimatedHours(e.target.value)}
+ className="!border-blue-300 h-8 text-sm w-24"
+ />
+ ) : (
+
{task.estimatedHours}h
+ )}
+
+ {showRealHours && (
+
+
Real
+ {isEditing ? (
+
setEditRealHours(e.target.value)}
+ className="!border-blue-300 h-8 text-sm w-24"
+ placeholder="0"
+ />
+ ) : (
+
{task.realHours !== null ? `${task.realHours}h` : 'Not logged'}
+ )}
+
+ )}
+
+ {!isEditing && showRealHours && (
+
+ {hourDelta === null &&
Real hours are not logged yet.
}
+ {hourDelta !== null && hourDelta <= 0 && (
+
Completed within estimate ({Math.abs(hourDelta)}h saved).
+ )}
+ {hourDelta !== null && hourDelta > 0 && (
+
Completed over estimate by {hourDelta}h.
+ )}
+
+ )}
+
+
+
+
+
+
Tags
+
{task.tags.map((tag) => {tag} )}
+
+
+ {isEditing && (
+
+
Status
+ setEditStatus(v as Status)}>
+
+
+
+
+ To Do
+ In Progress
+ Done
+
+
+
+ )}
+
+
+
Priority
+ {isEditing ? (
+
setEditPriority(v as Priority)}>
+
+
+
+
+ High
+ Medium
+ Low
+
+
+ ) : (
+
+
{priority.label}
+
+ {task.priority === 'high' && 'This task requires immediate attention and should be prioritized.'}
+ {task.priority === 'medium' && 'This task should be completed in a timely manner.'}
+ {task.priority === 'low' && 'This task can be completed when time permits.'}
+
+
+ )}
+
+
+
+ {error && {error}
}
+
+
+ {/* Delete confirmation dialog */}
+ {isDeleteDialogOpen && (
+
+
!isDeleting && setIsDeleteDialogOpen(false)} />
+
+
+
+
+
+
+
+
Delete Task
+
This action cannot be undone
+
+
+
+ Are you sure you want to delete “{task.title}” ? This will permanently remove the task and all associated data.
+
+
+
+ setIsDeleteDialogOpen(false)}
+ disabled={isDeleting}
+ >
+ Cancel
+
+
+
+ {isDeleting ? 'Deleting...' : 'Delete Task'}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/MtdrSpring/backend/src/main/frontend/src/app/routes.ts b/MtdrSpring/backend/src/main/frontend/src/app/routes.ts
new file mode 100644
index 000000000..f51a6829b
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/app/routes.ts
@@ -0,0 +1,49 @@
+import { createBrowserRouter } from "react-router";
+import RoleSelection from "./pages/RoleSelection";
+import ManagerDashboard from "./pages/ManagerDashboard";
+import ManagerDashboard2 from "./pages/ManagerDashboard2";
+import ManagerKanbanPage from "./pages/ManagerKanbanPage";
+import DeveloperDashboard from "./pages/DeveloperDashboard";
+import DeveloperTaskList from "./pages/DeveloperTaskList";
+import DeveloperKanbanPage from "./pages/DeveloperKanbanPage";
+import DeveloperDashboard2 from "./pages/DeveloperDashboard2";
+import TaskDetailView from "./pages/TaskDetailView";
+
+export const router = createBrowserRouter([
+ {
+ path: "/",
+ Component: RoleSelection,
+ },
+ {
+ path: "/manager",
+ Component: ManagerDashboard,
+ },
+ {
+ path: "/manager/kanban",
+ Component: ManagerKanbanPage,
+ },
+ {
+ path: "/manager/kpi",
+ Component: ManagerDashboard2,
+ },
+ {
+ path: "/developer/:developerId",
+ Component: DeveloperDashboard,
+ },
+ {
+ path: "/developer/:developerId/task-list",
+ Component: DeveloperTaskList,
+ },
+ {
+ path: "/developer/:developerId/kanban",
+ Component: DeveloperKanbanPage,
+ },
+ {
+ path: "/developer/:developerId/kpi",
+ Component: DeveloperDashboard2,
+ },
+ {
+ path: "/developer/task/:taskId",
+ Component: TaskDetailView,
+ },
+]);
diff --git a/MtdrSpring/backend/src/main/frontend/src/index.css b/MtdrSpring/backend/src/main/frontend/src/index.css
deleted file mode 100644
index b82c4de13..000000000
--- a/MtdrSpring/backend/src/main/frontend/src/index.css
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
-** Todo application version 1.0.
-**
-** Copyright (c) 2020, Oracle and/or its affiliates.
-** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
-*/
-body {
- /* from the redwood theme */
- background-color: #3A3632;
- width: 100%;
- max-width: 50rem;
- margin: 0 auto;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
- .App {
- background: #201E1C;
- color: #FEF9F2;
- display: flex;
- flex-direction: column;
- align-items: center;
- font-size: max(12px,min(2vw, 18px)); /*calc(1vw + 1vmin);*/
- margin: 2rem 0 4rem 0;
- padding: 1rem;
- position: relative;
- box-shadow: 0 10px 18px 0 rgba(0, 0, 0, 0.2), 0 4.5rem 8rem 0 rgba(0, 0, 0, 0.1);
- border-radius: 0.5rem;
- }
- div#maincontent, div#newinputform form {
- width: 95%;
- }
- div#maincontent {
- margin: 0;
- padding: 0;
- }
- h1 {
- margin: 0.5rem 0 1rem 0;
- padding: 0;
- }
- h2 {
- margin: 0.1rem 0 0.1rem 0;
- padding: 0;
- }
- #newiteminput {
- width: 100%;
- }
- div#newinputform {
- width: 100%;
- }
- div#newinputform form{
- display: flex;
- flex-direction: row;
- margin: 0 auto;
- }
- #donelist {
- margin: 0;
- padding: 0;
- }
- table#itemlistNotDone {
- margin-bottom: 2rem;
- }
- table#itemlistDone {
- margin-bottom: 3rem;
- }
- table.itemlist {
- margin-top: 0.7rem;
- border-collapse: collapse;
- margin-bottom: 1rem;
- }
- table.itemlist td {
- border-bottom: solid 1px #5B5652;
- padding: .5rem;
- }
- table.itemlist td.description {
- width: 100%;
- padding-left: 1rem;
- padding-right: 1rem;
- }
- table.itemlist td.date {
- font-size: max(10px,min(1.5vw, 14px));
- color: grey;
- white-space: nowrap;
- padding-right: 0;
- padding-left: 0;
- }
- table.itemlist tr:hover {
- background-color: #161513;
- }
- input {
- font-family: inherit;
- font-size: 100%;
- line-height: 1;
- margin: 0;
- }
- button,
- input {
- overflow: visible;
- }
- input[type="text"] {
- border-radius: 0.3rem;
- padding-left: 10px;
- }
- button.AddButton,
- button.DeleteButton,
- button.AddButton,
- button.DoneButton {
- font-size: max(8px,min(2vw, 12px));
- padding: 0.35em 0.5em;
- color:#161513;
- }
- /* from the redwood theme */
- button.AddButton {
- color: #FEF9F2;
- background-color: #5F7D4F;
- }
- button.AddButton:hover {
- background-color: #6F915D;
- }
- button.DeleteButton {
- color: #FEF9F2;
- background-color: #D63B25;
- }
- button.DeleteButton:hover {
- background-color: #EC4F3A
- }
- button.DoneButton {
- background-color: #FBF9F8;
- }
- button.DoneButton:hover {
- background-color: #D4CFCA;
- }
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/index.js b/MtdrSpring/backend/src/main/frontend/src/index.js
deleted file mode 100644
index 6d58062d5..000000000
--- a/MtdrSpring/backend/src/main/frontend/src/index.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
-## MyToDoReact version 1.0.
-##
-## Copyright (c) 2021 Oracle, Inc.
-## Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
-*/
-/*
- * @author jean.de.lavarene@oracle.com
- */
-
-import React from 'react';
-import ReactDOM from 'react-dom';
-import './index.css';
-import App from './App';
-
-ReactDOM.render(
-
-
- ,
- document.getElementById('root')
-);
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/main.tsx b/MtdrSpring/backend/src/main/frontend/src/main.tsx
new file mode 100644
index 000000000..34f838f18
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/main.tsx
@@ -0,0 +1,5 @@
+import { createRoot } from "react-dom/client";
+import App from "./app/App";
+import "./styles/index.css";
+
+createRoot(document.getElementById("root")!).render(
);
diff --git a/MtdrSpring/backend/src/main/frontend/src/styles/fonts.css b/MtdrSpring/backend/src/main/frontend/src/styles/fonts.css
new file mode 100644
index 000000000..368c6d93c
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/styles/fonts.css
@@ -0,0 +1 @@
+(/* intentionally left empty - placeholder for project fonts */)
diff --git a/MtdrSpring/backend/src/main/frontend/src/styles/index.css b/MtdrSpring/backend/src/main/frontend/src/styles/index.css
new file mode 100644
index 000000000..01e7b259a
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/styles/index.css
@@ -0,0 +1,3 @@
+@import './fonts.css';
+@import './tailwind.css';
+@import './theme.css';
diff --git a/MtdrSpring/backend/src/main/frontend/src/styles/tailwind.css b/MtdrSpring/backend/src/main/frontend/src/styles/tailwind.css
new file mode 100644
index 000000000..e2a61bbad
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/styles/tailwind.css
@@ -0,0 +1,4 @@
+@import 'tailwindcss' source(none);
+@source '../**/*.{js,ts,jsx,tsx}';
+
+@import 'tw-animate-css';
diff --git a/MtdrSpring/backend/src/main/frontend/src/styles/theme.css b/MtdrSpring/backend/src/main/frontend/src/styles/theme.css
new file mode 100644
index 000000000..c5d09441d
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/styles/theme.css
@@ -0,0 +1,181 @@
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ --font-size: 18px;
+ --background: #ffffff;
+ --foreground: #1f2937;
+ --card: #ffffff;
+ --card-foreground: #1f2937;
+ --popover: #ffffff;
+ --popover-foreground: #1f2937;
+ --primary: #b91c1c;
+ --primary-foreground: #ffffff;
+ --secondary: #fef2f2;
+ --secondary-foreground: #991b1b;
+ --muted: #fee2e2;
+ --muted-foreground: #991b1b;
+ --accent: #dc2626;
+ --accent-foreground: #ffffff;
+ --destructive: #dc2626;
+ --destructive-foreground: #ffffff;
+ --border: #fecaca;
+ --input: transparent;
+ --input-background: #ffffff;
+ --switch-background: #fca5a5;
+ --font-weight-medium: 500;
+ --font-weight-normal: 400;
+ --ring: #ef4444;
+ --chart-1: #dc2626;
+ --chart-2: #f97316;
+ --chart-3: #ef4444;
+ --chart-4: #b91c1c;
+ --chart-5: #991b1b;
+ --radius: 0.75rem;
+ --sidebar: #ffffff;
+ --sidebar-foreground: #1f2937;
+ --sidebar-primary: #b91c1c;
+ --sidebar-primary-foreground: #ffffff;
+ --sidebar-accent: #fef2f2;
+ --sidebar-accent-foreground: #991b1b;
+ --sidebar-border: #fecaca;
+ --sidebar-ring: #ef4444;
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.145 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.145 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.985 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.396 0.141 25.723);
+ --destructive-foreground: oklch(0.637 0.237 25.331);
+ --border: oklch(0.269 0 0);
+ --input: oklch(0.269 0 0);
+ --ring: oklch(0.439 0 0);
+ --font-weight-medium: 500;
+ --font-weight-normal: 400;
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(0.269 0 0);
+ --sidebar-ring: oklch(0.439 0 0);
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-input-background: var(--input-background);
+ --color-switch-background: var(--switch-background);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ }
+
+ /**
+ * Default typography styles for HTML elements (h1-h4, p, label, button, input).
+ * These are in @layer base, so Tailwind utility classes (like text-sm, text-lg) automatically override them.
+ */
+
+ html {
+ font-size: var(--font-size);
+ }
+
+ h1 {
+ font-size: var(--text-2xl);
+ font-weight: var(--font-weight-medium);
+ line-height: 1.5;
+ }
+
+ h2 {
+ font-size: var(--text-xl);
+ font-weight: var(--font-weight-medium);
+ line-height: 1.5;
+ }
+
+ h3 {
+ font-size: var(--text-lg);
+ font-weight: var(--font-weight-medium);
+ line-height: 1.5;
+ }
+
+ h4 {
+ font-size: var(--text-base);
+ font-weight: var(--font-weight-medium);
+ line-height: 1.5;
+ }
+
+ label {
+ font-size: var(--text-base);
+ font-weight: var(--font-weight-medium);
+ line-height: 1.5;
+ }
+
+ button {
+ font-size: var(--text-base);
+ font-weight: var(--font-weight-medium);
+ line-height: 1.5;
+ }
+
+ input {
+ font-size: var(--text-base);
+ font-weight: var(--font-weight-normal);
+ line-height: 1.5;
+ }
+}
diff --git a/MtdrSpring/backend/src/main/frontend/tailwind.config.js b/MtdrSpring/backend/src/main/frontend/tailwind.config.js
new file mode 100644
index 000000000..8019719f0
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/tailwind.config.js
@@ -0,0 +1,11 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ './index.html',
+ './src/**/*.{js,ts,jsx,tsx,html}'
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/MtdrSpring/backend/src/main/frontend/vite.config.ts b/MtdrSpring/backend/src/main/frontend/vite.config.ts
new file mode 100644
index 000000000..2f4577b88
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/vite.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+
+const apiTarget = process.env.VITE_API_PROXY_TARGET || 'http://localhost:8080'
+
+export default defineConfig({
+ base: '/',
+ plugins: [react(), tailwindcss()],
+ server: {
+ port: 5173,
+ proxy: {
+ '/api': {
+ target: apiTarget,
+ changeOrigin: true,
+ secure: false,
+ },
+ },
+ hmr: false,
+ },
+ build: {
+ outDir: 'build'
+ }
+})
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/DeepSeekConfig.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/DeepSeekConfig.java
index 976bd6120..46819881b 100644
--- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/DeepSeekConfig.java
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/DeepSeekConfig.java
@@ -1,28 +1,15 @@
package com.springboot.MyTodoList.config;
-import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DeepSeekConfig {
-
- @Value("${deepseek.api.key}")
- private String apiKey;
@Bean
public CloseableHttpClient httpClient() {
return HttpClients.createDefault();
}
-
- @Bean
- public HttpPost deepSeekRequest(@Value("${deepseek.api.url}") String apiUrl) {
- HttpPost request = new HttpPost(apiUrl);
- request.addHeader("Content-Type", "application/json");
- request.addHeader("Authorization", "Bearer " + apiKey);
- return request;
- }
}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/OciGenerativeAiConfig.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/OciGenerativeAiConfig.java
new file mode 100644
index 000000000..210dfc56a
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/OciGenerativeAiConfig.java
@@ -0,0 +1,4 @@
+package com.springboot.MyTodoList.config;
+
+public class OciGenerativeAiConfig {
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/OracleConfiguration.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/OracleConfiguration.java
index b672e55f3..6b72a1dba 100644
--- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/OracleConfiguration.java
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/OracleConfiguration.java
@@ -36,7 +36,7 @@ public DataSource dataSource() throws SQLException{
logger.info("Using URL: " + env.getProperty("db_url"));
ds.setUser(env.getProperty("db_user"));
logger.info("Using Username " + env.getProperty("db_user"));
- ds.setPassword(env.getProperty("dbpassword"));
+ ds.setPassword(env.getProperty("db_password"));
// For local testing
// ds.setDriverType(dbSettings.getDriver_class_name());
// logger.info("Using Driver " + dbSettings.getDriver_class_name());
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/AuthController.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/AuthController.java
new file mode 100644
index 000000000..8d26a83f1
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/AuthController.java
@@ -0,0 +1,65 @@
+package com.springboot.MyTodoList.controller;
+
+import com.springboot.MyTodoList.dto.LoginRequest;
+import com.springboot.MyTodoList.dto.LoginStepResponse;
+import com.springboot.MyTodoList.dto.LoginSuccessResponse;
+import com.springboot.MyTodoList.dto.OtpVerifyRequest;
+import com.springboot.MyTodoList.service.AuthService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/auth")
+public class AuthController {
+
+ @Autowired
+ private AuthService authService;
+
+ /**
+ * Step 1 – Validate email+password and send a 6-digit OTP to the user's email.
+ * Returns a session token that must be supplied in the verify-otp call.
+ */
+ @PostMapping("/login")
+ public ResponseEntity> login(@RequestBody LoginRequest request) {
+ if (request.getEmail() == null || request.getEmail().isBlank()
+ || request.getPassword() == null || request.getPassword().isBlank()) {
+ return ResponseEntity.badRequest().body(Map.of("error", "Email and password are required."));
+ }
+
+ try {
+ LoginStepResponse response = authService.initiateLogin(
+ request.getEmail().trim(), request.getPassword());
+ return ResponseEntity.ok(response);
+ } catch (IllegalArgumentException e) {
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
+ .body(Map.of("error", e.getMessage()));
+ }
+ }
+
+ /**
+ * Step 2 – Verify the OTP. On success, returns the user's role, userId, and (if developer) developerId.
+ */
+ @PostMapping("/verify-otp")
+ public ResponseEntity> verifyOtp(@RequestBody OtpVerifyRequest request) {
+ if (request.getSessionToken() == null || request.getSessionToken().isBlank()
+ || request.getOtp() == null || request.getOtp().isBlank()) {
+ return ResponseEntity.badRequest().body(Map.of("error", "Session token and OTP are required."));
+ }
+
+ try {
+ LoginSuccessResponse response = authService.verifyOtp(
+ request.getSessionToken().trim(), request.getOtp().trim());
+ return ResponseEntity.ok(response);
+ } catch (IllegalArgumentException e) {
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
+ .body(Map.of("error", e.getMessage()));
+ } catch (IllegalStateException e) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body(Map.of("error", e.getMessage()));
+ }
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/Dashboardcontroller.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/Dashboardcontroller.java
new file mode 100644
index 000000000..388f0170a
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/Dashboardcontroller.java
@@ -0,0 +1,51 @@
+package com.springboot.MyTodoList.controller;
+
+import com.springboot.MyTodoList.model.Task;
+import com.springboot.MyTodoList.service.DashboardService;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/dashboard")
+public class Dashboardcontroller {
+
+ private final DashboardService dashboardService;
+
+ public Dashboardcontroller(DashboardService dashboardService) {
+ this.dashboardService = dashboardService;
+ }
+
+ @GetMapping("/manager")
+ public ResponseEntity
getManagerDashboard(
+ @RequestParam(required = false) Integer projectID) {
+ return ResponseEntity.ok(dashboardService.getManagerDashboard(projectID));
+ }
+
+ @GetMapping("/developer/{developerID}")
+ public ResponseEntity getDeveloperDashboard(
+ @PathVariable Integer developerID,
+ @RequestParam(required = false) Integer projectID) {
+ return ResponseEntity.ok(dashboardService.getDeveloperDashboard(developerID, projectID));
+ }
+
+ @GetMapping("/tasks/{taskID}")
+ public ResponseEntity getTask(@PathVariable Integer taskID) {
+ return dashboardService.getTask(taskID)
+ .map(ResponseEntity::ok)
+ .orElse(ResponseEntity.notFound().build());
+ }
+
+ @PatchMapping("/tasks/{taskID}")
+ public ResponseEntity updateTask(
+ @PathVariable Integer taskID,
+ @RequestBody Map body) {
+ String status = body.get("status") != null ? body.get("status").toString() : null;
+ Integer timeSpent = body.get("timeSpent") != null
+ ? Integer.valueOf(body.get("timeSpent").toString()) : null;
+ return dashboardService.updateTask(taskID, status, timeSpent)
+ .map(ResponseEntity::ok)
+ .orElse(ResponseEntity.notFound().build());
+ }
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/DeveloperController.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/DeveloperController.java
new file mode 100644
index 000000000..59d9ad770
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/DeveloperController.java
@@ -0,0 +1,24 @@
+package com.springboot.MyTodoList.controller;
+
+import com.springboot.MyTodoList.dto.DeveloperSummaryDto;
+import com.springboot.MyTodoList.service.DeveloperService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/developers")
+public class DeveloperController {
+ private final DeveloperService developerService;
+
+ public DeveloperController(DeveloperService developerService) {
+ this.developerService = developerService;
+ }
+
+ @GetMapping
+ public List getDeveloperSummaries() {
+ return developerService.getDeveloperSummaries();
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/SpaController.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/SpaController.java
new file mode 100644
index 000000000..e416c0665
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/SpaController.java
@@ -0,0 +1,26 @@
+package com.springboot.MyTodoList.controller;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+/**
+ * Forwards all unmatched routes to index.html so React Router can handle
+ * client-side navigation (e.g. /manager, /developer/:id).
+ */
+@Controller
+public class SpaController {
+
+ @GetMapping({
+ "/",
+ "/login",
+ "/manager",
+ "/manager/**",
+ "/developer",
+ "/developer/**",
+ "/developer/{developerId}",
+ "/developer/task/{taskId}"
+ })
+ public String spa() {
+ return "forward:/index.html";
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/TaskController.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/TaskController.java
new file mode 100644
index 000000000..11c1a1fb1
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/TaskController.java
@@ -0,0 +1,80 @@
+package com.springboot.MyTodoList.controller;
+
+import com.springboot.MyTodoList.dto.TaskStatusUpdateRequest;
+import com.springboot.MyTodoList.model.Task;
+import com.springboot.MyTodoList.service.TaskService;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/tasks")
+public class TaskController {
+ private final TaskService taskService;
+
+ public TaskController(TaskService taskService) {
+ this.taskService = taskService;
+ }
+
+ @GetMapping
+ public List getTasks() {
+ return taskService.findAllTasks();
+ }
+
+ @PostMapping
+ public ResponseEntity createTask(@RequestBody Task task) {
+ try {
+ Task created = taskService.createTask(task);
+ return new ResponseEntity<>(created, HttpStatus.CREATED);
+ } catch (Exception ex) {
+ return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+ }
+ }
+
+ @PutMapping("/{id}")
+ public ResponseEntity updateTask(@PathVariable int id, @RequestBody Task updates) {
+ try {
+ return taskService.updateTask(id, updates)
+ .map(task -> new ResponseEntity<>(task, HttpStatus.OK))
+ .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
+ } catch (Exception ex) {
+ return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+ }
+ }
+
+ @DeleteMapping("/{id}")
+ public ResponseEntity deleteTask(@PathVariable int id) {
+ try {
+ if (taskService.findTaskById(id).isEmpty()) {
+ return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+ }
+ taskService.deleteTask(id);
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ } catch (Exception ex) {
+ return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+ }
+ }
+
+ @GetMapping("/{id}")
+ public ResponseEntity getTaskById(@PathVariable int id) {
+ return taskService.findTaskById(id)
+ .map(task -> new ResponseEntity<>(task, HttpStatus.OK))
+ .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
+ }
+
+ @PutMapping("/{id}/status")
+ public ResponseEntity updateTaskStatus(@PathVariable int id, @RequestBody TaskStatusUpdateRequest request) {
+ if (request == null || request.getStatus() == null) {
+ return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+ }
+ try {
+ return taskService.updateTaskStatus(id, request.getStatus())
+ .map(task -> new ResponseEntity<>(task, HttpStatus.OK))
+ .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
+ } catch (IllegalArgumentException ex) {
+ return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+ }
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/ToDoItemBotController.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/ToDoItemBotController.java
index 67e6bc49d..1393346eb 100644
--- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/ToDoItemBotController.java
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/controller/ToDoItemBotController.java
@@ -1,14 +1,15 @@
package com.springboot.MyTodoList.controller;
import com.springboot.MyTodoList.config.BotProps;
-import com.springboot.MyTodoList.service.DeepSeekService;
+import com.springboot.MyTodoList.service.GeminiService;
+import com.springboot.MyTodoList.service.TaskService;
+import com.springboot.MyTodoList.service.TelegramMessageService;
+import com.springboot.MyTodoList.service.TelegramSummaryService;
import com.springboot.MyTodoList.service.ToDoItemService;
import com.springboot.MyTodoList.util.BotActions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
-import org.telegram.telegrambots.client.okhttp.OkHttpTelegramClient;
import org.telegram.telegrambots.longpolling.BotSession;
import org.telegram.telegrambots.longpolling.interfaces.LongPollingUpdateConsumer;
import org.telegram.telegrambots.longpolling.starter.AfterBotRegistration;
@@ -22,30 +23,37 @@ public class ToDoItemBotController implements SpringLongPollingBot, LongPolling
private static final Logger logger = LoggerFactory.getLogger(ToDoItemBotController.class);
private ToDoItemService toDoItemService;
- private DeepSeekService deepSeekService;
+ private TaskService taskService;
+ private GeminiService geminiService;
+ private TelegramMessageService telegramMessageService;
+ private TelegramSummaryService telegramSummaryService;
private final TelegramClient telegramClient;
private final BotProps botProps;
- @Value("${telegram.bot.token}")
- private String telegramBotToken;
-
@Override
public String getBotToken() {
- if(telegramBotToken != null && !telegramBotToken.trim().isEmpty()){
- return telegramBotToken;
- }else{
- return botProps.getToken();
- }
+ return botProps.getToken();
}
- public ToDoItemBotController( BotProps bp, ToDoItemService tsvc, DeepSeekService ds) {
+ public ToDoItemBotController(
+ BotProps bp,
+ ToDoItemService tsvc,
+ TaskService taskSvc,
+ GeminiService gs,
+ TelegramMessageService tms,
+ TelegramSummaryService tss,
+ TelegramClient telegramClient
+ ) {
this.botProps = bp;
- telegramClient = new OkHttpTelegramClient(getBotToken());
+ this.telegramClient = telegramClient;
toDoItemService = tsvc;
- deepSeekService = ds;
+ taskService = taskSvc;
+ geminiService = gs;
+ telegramMessageService = tms;
+ telegramSummaryService = tss;
}
@Override
@@ -62,10 +70,25 @@ public void consume(Update update) {
String messageTextFromTelegram = update.getMessage().getText();
long chatId = update.getMessage().getChatId();
+ Integer messageId = update.getMessage().getMessageId();
+ Integer messageDate = update.getMessage().getDate();
+ Long telegramUserId = update.getMessage().getFrom() != null ? update.getMessage().getFrom().getId() : null;
+ if (!messageTextFromTelegram.trim().startsWith("/") && telegramUserId != null) {
+ telegramMessageService.saveIncomingMessage(chatId, messageId, telegramUserId, messageTextFromTelegram, messageDate);
+ }
- BotActions actions = new BotActions(telegramClient,toDoItemService,deepSeekService);
+ BotActions actions = new BotActions(
+ telegramClient,
+ toDoItemService,
+ taskService,
+ geminiService,
+ telegramMessageService,
+ telegramSummaryService
+ );
actions.setRequestText(messageTextFromTelegram);
actions.setChatId(chatId);
+ actions.setTelegramUserId(telegramUserId);
+ actions.setTelegramUsername(null);
if(actions.getTodoService()==null){
logger.info("todosvc error");
actions.setTodoService(toDoItemService);
@@ -73,14 +96,12 @@ public void consume(Update update) {
actions.fnStart();
- actions.fnDone();
- actions.fnUndo();
- actions.fnDelete();
actions.fnHide();
- actions.fnListAll();
- actions.fnAddItem();
+ actions.fnWhoAmI();
+ actions.fnPendingTasks();
+ actions.fnCreateTask();
+ actions.fnSummarize();
actions.fnLLM();
- actions.fnElse();
}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/DeveloperSummaryDto.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/DeveloperSummaryDto.java
new file mode 100644
index 000000000..34aed92e3
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/DeveloperSummaryDto.java
@@ -0,0 +1,27 @@
+package com.springboot.MyTodoList.dto;
+
+public class DeveloperSummaryDto {
+ private Integer developerId;
+ private String fullName;
+
+ public DeveloperSummaryDto(Integer developerId, String fullName) {
+ this.developerId = developerId;
+ this.fullName = fullName;
+ }
+
+ public Integer getDeveloperId() {
+ return developerId;
+ }
+
+ public void setDeveloperId(Integer developerId) {
+ this.developerId = developerId;
+ }
+
+ public String getFullName() {
+ return fullName;
+ }
+
+ public void setFullName(String fullName) {
+ this.fullName = fullName;
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/LoginRequest.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/LoginRequest.java
new file mode 100644
index 000000000..45d235e7a
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/LoginRequest.java
@@ -0,0 +1,12 @@
+package com.springboot.MyTodoList.dto;
+
+public class LoginRequest {
+ private String email;
+ private String password;
+
+ public String getEmail() { return email; }
+ public void setEmail(String email) { this.email = email; }
+
+ public String getPassword() { return password; }
+ public void setPassword(String password) { this.password = password; }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/LoginStepResponse.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/LoginStepResponse.java
new file mode 100644
index 000000000..9925f0344
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/LoginStepResponse.java
@@ -0,0 +1,17 @@
+package com.springboot.MyTodoList.dto;
+
+public class LoginStepResponse {
+ private String sessionToken;
+ private String message;
+
+ public LoginStepResponse(String sessionToken, String message) {
+ this.sessionToken = sessionToken;
+ this.message = message;
+ }
+
+ public String getSessionToken() { return sessionToken; }
+ public void setSessionToken(String sessionToken) { this.sessionToken = sessionToken; }
+
+ public String getMessage() { return message; }
+ public void setMessage(String message) { this.message = message; }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/LoginSuccessResponse.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/LoginSuccessResponse.java
new file mode 100644
index 000000000..55a82beca
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/LoginSuccessResponse.java
@@ -0,0 +1,32 @@
+package com.springboot.MyTodoList.dto;
+
+public class LoginSuccessResponse {
+ private String role;
+ private Integer userId;
+ private Integer developerId;
+ private Integer managerId;
+ private String name;
+
+ public LoginSuccessResponse(String role, Integer userId, Integer developerId, Integer managerId, String name) {
+ this.role = role;
+ this.userId = userId;
+ this.developerId = developerId;
+ this.managerId = managerId;
+ this.name = name;
+ }
+
+ public String getRole() { return role; }
+ public void setRole(String role) { this.role = role; }
+
+ public Integer getUserId() { return userId; }
+ public void setUserId(Integer userId) { this.userId = userId; }
+
+ public Integer getDeveloperId() { return developerId; }
+ public void setDeveloperId(Integer developerId) { this.developerId = developerId; }
+
+ public Integer getManagerId() { return managerId; }
+ public void setManagerId(Integer managerId) { this.managerId = managerId; }
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/OtpVerifyRequest.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/OtpVerifyRequest.java
new file mode 100644
index 000000000..dea4a4378
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/OtpVerifyRequest.java
@@ -0,0 +1,12 @@
+package com.springboot.MyTodoList.dto;
+
+public class OtpVerifyRequest {
+ private String sessionToken;
+ private String otp;
+
+ public String getSessionToken() { return sessionToken; }
+ public void setSessionToken(String sessionToken) { this.sessionToken = sessionToken; }
+
+ public String getOtp() { return otp; }
+ public void setOtp(String otp) { this.otp = otp; }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/TaskStatusUpdateRequest.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/TaskStatusUpdateRequest.java
new file mode 100644
index 000000000..53be9cbee
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/dto/TaskStatusUpdateRequest.java
@@ -0,0 +1,13 @@
+package com.springboot.MyTodoList.dto;
+
+public class TaskStatusUpdateRequest {
+ private String status;
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/Developer.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/Developer.java
new file mode 100644
index 000000000..387be39e4
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/Developer.java
@@ -0,0 +1,58 @@
+package com.springboot.MyTodoList.model;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "DEVELOPER")
+public class Developer {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "DEVELOPERID")
+ private Integer developerID;
+
+ @Column(name = "USERID", nullable = false)
+ private Integer userID;
+
+ @Column(name = "MANAGERID", nullable = false)
+ private Integer managerID;
+
+ @Column(name = "TEAMID", nullable = false)
+ private Integer teamID;
+
+ public Integer getDeveloperID() {
+ return developerID;
+ }
+
+ public void setDeveloperID(Integer developerID) {
+ this.developerID = developerID;
+ }
+
+ public Integer getUserID() {
+ return userID;
+ }
+
+ public void setUserID(Integer userID) {
+ this.userID = userID;
+ }
+
+ public Integer getManagerID() {
+ return managerID;
+ }
+
+ public void setManagerID(Integer managerID) {
+ this.managerID = managerID;
+ }
+
+ public Integer getTeamID() {
+ return teamID;
+ }
+
+ public void setTeamID(Integer teamID) {
+ this.teamID = teamID;
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/Task.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/Task.java
new file mode 100644
index 000000000..25001c68c
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/Task.java
@@ -0,0 +1,175 @@
+package com.springboot.MyTodoList.model;
+
+import jakarta.persistence.*;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "TASK")
+public class Task {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "TASKID")
+ private Integer taskID;
+
+ @Column(name = "NAME", nullable = false, length = 200)
+ private String name;
+
+ @Column(name = "DESCRIPTION", nullable = false, length = 300)
+ private String description;
+
+ @Column(name = "STATUS", nullable = false, length = 20)
+ private String status;
+
+ @Column(name = "TASKTYPE", nullable = false, length = 30)
+ private String taskType;
+
+ @Column(name = "STARTDATE", nullable = false)
+ private LocalDateTime startDate;
+
+ @Column(name = "DEADLINE", nullable = false)
+ private LocalDateTime deadline;
+
+ @Column(name = "DEVELOPERID", nullable = false)
+ private Integer developerID;
+
+ @Column(name = "ESTIMATEDTIME", nullable = false)
+ private Integer estimatedTime;
+
+ @Column(name = "TIMESPENT")
+ private Integer timeSpent;
+
+ @Column(name = "PRIORITY", nullable = false, length = 20)
+ private String priority;
+
+ @Column(name = "PROJECTID", nullable = false)
+ private Integer projectID;
+
+ @Column(name = "CREATED_AT")
+ private LocalDateTime createdAt;
+
+ @Column(name = "UPDATED_AT")
+ private LocalDateTime updatedAt;
+
+ @Column(name = "SPRINT")
+ private Integer sprint;
+
+ public Integer getTaskID() {
+ return taskID;
+ }
+
+ public void setTaskID(Integer taskID) {
+ this.taskID = taskID;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getTaskType() {
+ return taskType;
+ }
+
+ public void setTaskType(String taskType) {
+ this.taskType = taskType;
+ }
+
+ public LocalDateTime getStartDate() {
+ return startDate;
+ }
+
+ public void setStartDate(LocalDateTime startDate) {
+ this.startDate = startDate;
+ }
+
+ public LocalDateTime getDeadline() {
+ return deadline;
+ }
+
+ public void setDeadline(LocalDateTime deadline) {
+ this.deadline = deadline;
+ }
+
+ public Integer getDeveloperID() {
+ return developerID;
+ }
+
+ public void setDeveloperID(Integer developerID) {
+ this.developerID = developerID;
+ }
+
+ public Integer getEstimatedTime() {
+ return estimatedTime;
+ }
+
+ public void setEstimatedTime(Integer estimatedTime) {
+ this.estimatedTime = estimatedTime;
+ }
+
+ public Integer getTimeSpent() {
+ return timeSpent;
+ }
+
+ public void setTimeSpent(Integer timeSpent) {
+ this.timeSpent = timeSpent;
+ }
+
+ public String getPriority() {
+ return priority;
+ }
+
+ public void setPriority(String priority) {
+ this.priority = priority;
+ }
+
+ public Integer getProjectID() {
+ return projectID;
+ }
+
+ public void setProjectID(Integer projectID) {
+ this.projectID = projectID;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(LocalDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public LocalDateTime getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public void setUpdatedAt(LocalDateTime updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+
+ public Integer getSprint() {
+ return sprint;
+ }
+
+ public void setSprint(Integer sprint) {
+ this.sprint = sprint;
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/TelegramAccount.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/TelegramAccount.java
new file mode 100644
index 000000000..aa9c3d498
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/TelegramAccount.java
@@ -0,0 +1,47 @@
+package com.springboot.MyTodoList.model;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "TELEGRAM_ACCOUNT")
+public class TelegramAccount {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "TELEGRAMACCOUNTID")
+ private Long telegramAccountID;
+
+ @Column(name = "USERID", nullable = false)
+ private Integer userID;
+
+ @Column(name = "TELEGRAMUSERID", nullable = false)
+ private Long telegramUserId;
+
+ public Long getTelegramAccountID() {
+ return telegramAccountID;
+ }
+
+ public void setTelegramAccountID(Long telegramAccountID) {
+ this.telegramAccountID = telegramAccountID;
+ }
+
+ public Integer getUserID() {
+ return userID;
+ }
+
+ public void setUserID(Integer userID) {
+ this.userID = userID;
+ }
+
+ public Long getTelegramUserId() {
+ return telegramUserId;
+ }
+
+ public void setTelegramUserId(Long telegramUserId) {
+ this.telegramUserId = telegramUserId;
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/TelegramMessage.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/TelegramMessage.java
new file mode 100644
index 000000000..49b72fa79
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/TelegramMessage.java
@@ -0,0 +1,83 @@
+package com.springboot.MyTodoList.model;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Lob;
+import jakarta.persistence.Table;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "TELEGRAM_MESSAGE")
+public class TelegramMessage {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "TELEGRAMMESSAGEID")
+ private Long telegramMessageID;
+
+ @Column(name = "CHATID", nullable = false)
+ private Long chatId;
+
+ @Column(name = "MESSAGEID", nullable = false)
+ private Integer messageId;
+
+ @Column(name = "TELEGRAMUSERID", nullable = false)
+ private Long telegramUserId;
+
+ @Lob
+ @Column(name = "MESSAGETEXT", nullable = false)
+ private String messageText;
+
+ @Column(name = "CREATEDAT", nullable = false)
+ private LocalDateTime createdAt;
+
+ public Long getTelegramMessageID() {
+ return telegramMessageID;
+ }
+
+ public void setTelegramMessageID(Long telegramMessageID) {
+ this.telegramMessageID = telegramMessageID;
+ }
+
+ public Long getChatId() {
+ return chatId;
+ }
+
+ public void setChatId(Long chatId) {
+ this.chatId = chatId;
+ }
+
+ public Integer getMessageId() {
+ return messageId;
+ }
+
+ public void setMessageId(Integer messageId) {
+ this.messageId = messageId;
+ }
+
+ public Long getTelegramUserId() {
+ return telegramUserId;
+ }
+
+ public void setTelegramUserId(Long telegramUserId) {
+ this.telegramUserId = telegramUserId;
+ }
+
+ public String getMessageText() {
+ return messageText;
+ }
+
+ public void setMessageText(String messageText) {
+ this.messageText = messageText;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(LocalDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/TelegramSummary.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/TelegramSummary.java
new file mode 100644
index 000000000..23360f33e
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/TelegramSummary.java
@@ -0,0 +1,107 @@
+package com.springboot.MyTodoList.model;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Lob;
+import jakarta.persistence.Table;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "TELEGRAM_SUMMARY")
+public class TelegramSummary {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "SUMMARYID")
+ private Long summaryID;
+
+ @Column(name = "CHATID", nullable = false)
+ private Long chatId;
+
+ @Column(name = "REQUESTEDBYTELEGRAMUSERID")
+ private Long requestedByTelegramUserId;
+
+ @Column(name = "REQUESTEDAT", nullable = false)
+ private LocalDateTime requestedAt;
+
+ @Column(name = "WINDOWSIZE", nullable = false)
+ private Integer windowSize;
+
+ @Lob
+ @Column(name = "SUMMARYTEXT", nullable = false)
+ private String summaryText;
+
+ @Lob
+ @Column(name = "DECISIONSTEXT")
+ private String decisionsText;
+
+ @Lob
+ @Column(name = "ACTIONITEMSTEXT")
+ private String actionItemsText;
+
+ public Long getSummaryID() {
+ return summaryID;
+ }
+
+ public void setSummaryID(Long summaryID) {
+ this.summaryID = summaryID;
+ }
+
+ public Long getChatId() {
+ return chatId;
+ }
+
+ public void setChatId(Long chatId) {
+ this.chatId = chatId;
+ }
+
+ public Long getRequestedByTelegramUserId() {
+ return requestedByTelegramUserId;
+ }
+
+ public void setRequestedByTelegramUserId(Long requestedByTelegramUserId) {
+ this.requestedByTelegramUserId = requestedByTelegramUserId;
+ }
+
+ public LocalDateTime getRequestedAt() {
+ return requestedAt;
+ }
+
+ public void setRequestedAt(LocalDateTime requestedAt) {
+ this.requestedAt = requestedAt;
+ }
+
+ public Integer getWindowSize() {
+ return windowSize;
+ }
+
+ public void setWindowSize(Integer windowSize) {
+ this.windowSize = windowSize;
+ }
+
+ public String getSummaryText() {
+ return summaryText;
+ }
+
+ public void setSummaryText(String summaryText) {
+ this.summaryText = summaryText;
+ }
+
+ public String getDecisionsText() {
+ return decisionsText;
+ }
+
+ public void setDecisionsText(String decisionsText) {
+ this.decisionsText = decisionsText;
+ }
+
+ public String getActionItemsText() {
+ return actionItemsText;
+ }
+
+ public void setActionItemsText(String actionItemsText) {
+ this.actionItemsText = actionItemsText;
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/UserGeneral.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/UserGeneral.java
new file mode 100644
index 000000000..81907f175
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/model/UserGeneral.java
@@ -0,0 +1,46 @@
+package com.springboot.MyTodoList.model;
+
+import jakarta.persistence.*;
+
+@Entity
+@Table(name = "USERGENERAL")
+public class UserGeneral {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "USERID")
+ private Integer userId;
+
+ @Column(name = "PHONE")
+ private String phone;
+
+ @Column(name = "EMAIL")
+ private String email;
+
+ @Column(name = "NAME")
+ private String name;
+
+ @Column(name = "LASTNAME")
+ private String lastName;
+
+ @Column(name = "PASSWORD")
+ private String password;
+
+ public Integer getUserId() { return userId; }
+ public void setUserId(Integer userId) { this.userId = userId; }
+
+ public String getPhone() { return phone; }
+ public void setPhone(String phone) { this.phone = phone; }
+
+ public String getEmail() { return email; }
+ public void setEmail(String email) { this.email = email; }
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+
+ public String getLastName() { return lastName; }
+ public void setLastName(String lastName) { this.lastName = lastName; }
+
+ public String getPassword() { return password; }
+ public void setPassword(String password) { this.password = password; }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/DeveloperRepository.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/DeveloperRepository.java
new file mode 100644
index 000000000..a7e78eccf
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/DeveloperRepository.java
@@ -0,0 +1,27 @@
+package com.springboot.MyTodoList.repository;
+
+import com.springboot.MyTodoList.model.Developer;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import jakarta.transaction.Transactional;
+import java.util.List;
+
+@Repository
+@Transactional
+@EnableTransactionManagement
+public interface DeveloperRepository extends JpaRepository {
+ interface DeveloperSummaryProjection {
+ Integer getDeveloperId();
+ String getFullName();
+ }
+
+ @Query(value = "SELECT d.DEVELOPERID as developerId, " +
+ "COALESCE(NULLIF(TRIM(ug.NAME || ' ' || ug.LASTNAME), ''), 'Developer ' || TO_CHAR(d.DEVELOPERID)) as fullName " +
+ "FROM DEVELOPER d " +
+ "LEFT JOIN USERGENERAL ug ON ug.USERID = d.USERID " +
+ "ORDER BY d.DEVELOPERID", nativeQuery = true)
+ List findDeveloperSummaries();
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/TaskRepository.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/TaskRepository.java
new file mode 100644
index 000000000..0c5b100a0
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/TaskRepository.java
@@ -0,0 +1,53 @@
+package com.springboot.MyTodoList.repository;
+
+import com.springboot.MyTodoList.model.Task;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import jakarta.transaction.Transactional;
+import java.util.List;
+
+@Repository
+@Transactional
+@EnableTransactionManagement
+public interface TaskRepository extends JpaRepository {
+
+ @Query(value = "SELECT * FROM task WHERE status IN ('open', 'in_progress') ORDER BY deadline, taskID", nativeQuery = true)
+ List findPendingTasks();
+
+ @Query(value = "SELECT COUNT(*) FROM telegram_account ta WHERE ta.telegramUserId = :telegramUserId", nativeQuery = true)
+ long countTelegramAccountByTelegramUserId(@Param("telegramUserId") Long telegramUserId);
+
+ @Query(value = "SELECT COUNT(*) " +
+ "FROM telegram_account ta " +
+ "JOIN developer d ON d.userID = ta.userID " +
+ "WHERE ta.telegramUserId = :telegramUserId", nativeQuery = true)
+ long countDeveloperByTelegramUserId(@Param("telegramUserId") Long telegramUserId);
+
+ @Query(value = "SELECT t.* " +
+ "FROM task t " +
+ "JOIN developer d ON d.developerID = t.developerID " +
+ "JOIN telegram_account ta ON ta.userID = d.userID " +
+ "WHERE ta.telegramUserId = :telegramUserId " +
+ "AND t.status IN ('open', 'in_progress') " +
+ "ORDER BY t.deadline, t.taskID", nativeQuery = true)
+ List findPendingTasksByTelegramUserId(@Param("telegramUserId") Long telegramUserId);
+
+ @Query(value = "SELECT d.developerID " +
+ "FROM developer d " +
+ "JOIN telegram_account ta ON ta.userID = d.userID " +
+ "WHERE ta.telegramUserId = :telegramUserId", nativeQuery = true)
+ Integer findDeveloperIdByTelegramUserId(@Param("telegramUserId") Long telegramUserId);
+
+ @Modifying
+ @Query(value = "DELETE FROM comments WHERE taskID = :taskId", nativeQuery = true)
+ void deleteCommentsByTaskId(@Param("taskId") int taskId);
+
+ @Modifying
+ @Query(value = "DELETE FROM task_log WHERE taskID = :taskId", nativeQuery = true)
+ void deleteTaskLogsByTaskId(@Param("taskId") int taskId);
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/TelegramAccountRepository.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/TelegramAccountRepository.java
new file mode 100644
index 000000000..222e7cc74
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/TelegramAccountRepository.java
@@ -0,0 +1,30 @@
+package com.springboot.MyTodoList.repository;
+
+import com.springboot.MyTodoList.model.TelegramAccount;
+import java.util.List;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import jakarta.transaction.Transactional;
+
+@Repository
+@Transactional
+@EnableTransactionManagement
+public interface TelegramAccountRepository extends JpaRepository {
+ interface TelegramUserDisplayProjection {
+ Long getTelegramUserId();
+ String getDisplayName();
+ }
+
+ boolean existsByTelegramUserId(Long telegramUserId);
+
+ @Query(value = "SELECT ta.TELEGRAMUSERID as telegramUserId, " +
+ "COALESCE(NULLIF(TRIM(ug.NAME || ' ' || ug.LASTNAME), ''), 'User ' || TO_CHAR(ta.TELEGRAMUSERID)) as displayName " +
+ "FROM TELEGRAM_ACCOUNT ta " +
+ "LEFT JOIN USERGENERAL ug ON ug.USERID = ta.USERID " +
+ "WHERE ta.TELEGRAMUSERID IN (:telegramUserIds)", nativeQuery = true)
+ List findDisplayNamesByTelegramUserIds(@Param("telegramUserIds") List telegramUserIds);
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/TelegramMessageRepository.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/TelegramMessageRepository.java
new file mode 100644
index 000000000..b8e1effe8
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/TelegramMessageRepository.java
@@ -0,0 +1,19 @@
+package com.springboot.MyTodoList.repository;
+
+import com.springboot.MyTodoList.model.TelegramMessage;
+import java.util.List;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import jakarta.transaction.Transactional;
+
+@Repository
+@Transactional
+@EnableTransactionManagement
+public interface TelegramMessageRepository extends JpaRepository {
+ boolean existsByChatIdAndMessageId(Long chatId, Integer messageId);
+
+ List findByChatIdOrderByCreatedAtDescMessageIdDesc(Long chatId, Pageable pageable);
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/TelegramSummaryRepository.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/TelegramSummaryRepository.java
new file mode 100644
index 000000000..72123ac29
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/TelegramSummaryRepository.java
@@ -0,0 +1,14 @@
+package com.springboot.MyTodoList.repository;
+
+import com.springboot.MyTodoList.model.TelegramSummary;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import jakarta.transaction.Transactional;
+
+@Repository
+@Transactional
+@EnableTransactionManagement
+public interface TelegramSummaryRepository extends JpaRepository {
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/UserGeneralRepository.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/UserGeneralRepository.java
new file mode 100644
index 000000000..24a7cc614
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/repository/UserGeneralRepository.java
@@ -0,0 +1,24 @@
+package com.springboot.MyTodoList.repository;
+
+import com.springboot.MyTodoList.model.UserGeneral;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+import jakarta.transaction.Transactional;
+import java.util.Optional;
+
+@Repository
+@Transactional
+public interface UserGeneralRepository extends JpaRepository {
+
+ @Query(value = "SELECT * FROM USERGENERAL WHERE EMAIL = :email", nativeQuery = true)
+ Optional findByEmail(@Param("email") String email);
+
+ @Query(value = "SELECT DEVELOPERID FROM DEVELOPER WHERE USERID = :userId", nativeQuery = true)
+ Integer findDeveloperIdByUserId(@Param("userId") Integer userId);
+
+ @Query(value = "SELECT MANAGERID FROM PROJECTMANAGER WHERE USERID = :userId", nativeQuery = true)
+ Integer findManagerIdByUserId(@Param("userId") Integer userId);
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/AuthService.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/AuthService.java
new file mode 100644
index 000000000..04bbd2dff
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/AuthService.java
@@ -0,0 +1,152 @@
+package com.springboot.MyTodoList.service;
+
+import com.springboot.MyTodoList.dto.LoginStepResponse;
+import com.springboot.MyTodoList.dto.LoginSuccessResponse;
+import com.springboot.MyTodoList.model.UserGeneral;
+import com.springboot.MyTodoList.repository.UserGeneralRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Service
+public class AuthService {
+
+ private static final Logger log = LoggerFactory.getLogger(AuthService.class);
+ private static final long OTP_EXPIRY_SECONDS = 300; // 5 minutes
+
+ @Autowired
+ private UserGeneralRepository userGeneralRepository;
+
+ // Optional: only wired when spring.mail.host is configured
+ @Autowired(required = false)
+ private JavaMailSender mailSender;
+
+ // sessionToken -> pending OTP session
+ private final ConcurrentHashMap otpStore = new ConcurrentHashMap<>();
+
+ /**
+ * Validates email+password, generates an OTP, sends it to the user's email,
+ * and returns a session token for the OTP verification step.
+ */
+ public LoginStepResponse initiateLogin(String email, String password) {
+ UserGeneral user = userGeneralRepository.findByEmail(email)
+ .orElseThrow(() -> new IllegalArgumentException("Invalid credentials"));
+
+ if (!user.getPassword().equals(password)) {
+ throw new IllegalArgumentException("Invalid credentials");
+ }
+
+ String otp = String.format("%06d", new Random().nextInt(1_000_000));
+ String sessionToken = UUID.randomUUID().toString();
+ Instant expiry = Instant.now().plusSeconds(OTP_EXPIRY_SECONDS);
+
+ otpStore.put(sessionToken, new OtpSession(otp, user.getUserId(), expiry));
+
+ sendOtp(user.getEmail(), user.getName(), otp);
+
+ return new LoginStepResponse(sessionToken, "OTP sent to " + maskEmail(user.getEmail()));
+ }
+
+ /**
+ * Verifies the OTP for the given session token and returns the authenticated user's role info.
+ */
+ public LoginSuccessResponse verifyOtp(String sessionToken, String otp) {
+ OtpSession session = otpStore.get(sessionToken);
+
+ if (session == null || Instant.now().isAfter(session.getExpiry())) {
+ otpStore.remove(sessionToken);
+ throw new IllegalArgumentException("OTP expired or invalid. Please log in again.");
+ }
+
+ if (!session.getOtp().equals(otp)) {
+ throw new IllegalArgumentException("Incorrect OTP. Please try again.");
+ }
+
+ otpStore.remove(sessionToken);
+
+ Integer userId = session.getUserId();
+ UserGeneral user = userGeneralRepository.findById(userId)
+ .orElseThrow(() -> new IllegalStateException("User not found"));
+
+ Integer developerId = userGeneralRepository.findDeveloperIdByUserId(userId);
+ Integer managerId = userGeneralRepository.findManagerIdByUserId(userId);
+
+ String role;
+ if (developerId != null) {
+ role = "developer";
+ } else if (managerId != null) {
+ role = "manager";
+ } else {
+ throw new IllegalStateException("User has no assigned role.");
+ }
+
+ String fullName = user.getName() + " " + user.getLastName();
+ return new LoginSuccessResponse(role, userId, developerId, managerId, fullName);
+ }
+
+ private void sendOtp(String toEmail, String name, String otp) {
+ log.info("OTP for user [{}]: {}", toEmail, otp);
+
+ if (mailSender == null) {
+ log.warn("No mail sender configured (spring.mail.host not set). OTP logged above.");
+ return;
+ }
+
+ try {
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setTo(toEmail);
+ message.setSubject("Synkra – Your verification code");
+ message.setText(
+ "Hi " + name + ",\n\n" +
+ "Your Synkra verification code is:\n\n" +
+ " " + otp + "\n\n" +
+ "This code expires in 5 minutes. Do not share it with anyone.\n\n" +
+ "– The Synkra Team"
+ );
+ mailSender.send(message);
+ log.info("OTP email sent to {}", toEmail);
+ } catch (Exception e) {
+ log.error("Failed to send OTP email to {}: {}", toEmail, e.getMessage());
+ }
+ }
+
+ private String maskEmail(String email) {
+ int at = email.indexOf('@');
+ if (at <= 1) return email;
+ return email.charAt(0) + "***" + email.substring(at);
+ }
+
+ // ─── Internal record to hold pending OTP sessions ─────────────────────────
+
+ private static final class OtpSession {
+ private final String otp;
+ private final Integer userId;
+ private final Instant expiry;
+
+ private OtpSession(String otp, Integer userId, Instant expiry) {
+ this.otp = otp;
+ this.userId = userId;
+ this.expiry = expiry;
+ }
+
+ private String getOtp() {
+ return otp;
+ }
+
+ private Integer getUserId() {
+ return userId;
+ }
+
+ private Instant getExpiry() {
+ return expiry;
+ }
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/DashboardService.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/DashboardService.java
new file mode 100644
index 000000000..89e4d9436
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/DashboardService.java
@@ -0,0 +1,311 @@
+package com.springboot.MyTodoList.service;
+
+import com.springboot.MyTodoList.model.Developer;
+import com.springboot.MyTodoList.model.Task;
+import com.springboot.MyTodoList.repository.DeveloperRepository;
+import com.springboot.MyTodoList.repository.TaskRepository;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Service
+public class DashboardService {
+
+ private final TaskRepository taskRepository;
+ private final DeveloperRepository developerRepository;
+
+ public DashboardService(TaskRepository taskRepository, DeveloperRepository developerRepository) {
+ this.taskRepository = taskRepository;
+ this.developerRepository = developerRepository;
+ }
+
+ public static class DeveloperStats {
+ private Integer developerID;
+ private Integer userID;
+ private Integer teamID;
+ private long assignedTasksCount;
+ private long completedTasksCount;
+ private int hoursWorked;
+ private int estimatedHours;
+
+ // Constructors, getters, setters
+ public DeveloperStats() {}
+
+ public DeveloperStats(Integer developerID, Integer userID, Integer teamID,
+ long assignedTasksCount, long completedTasksCount,
+ int hoursWorked, int estimatedHours) {
+ this.developerID = developerID;
+ this.userID = userID;
+ this.teamID = teamID;
+ this.assignedTasksCount = assignedTasksCount;
+ this.completedTasksCount = completedTasksCount;
+ this.hoursWorked = hoursWorked;
+ this.estimatedHours = estimatedHours;
+ }
+
+ // Getters and setters
+ public Integer getDeveloperID() { return developerID; }
+ public void setDeveloperID(Integer developerID) { this.developerID = developerID; }
+
+ public Integer getUserID() { return userID; }
+ public void setUserID(Integer userID) { this.userID = userID; }
+
+ public Integer getTeamID() { return teamID; }
+ public void setTeamID(Integer teamID) { this.teamID = teamID; }
+
+ public long getAssignedTasksCount() { return assignedTasksCount; }
+ public void setAssignedTasksCount(long assignedTasksCount) { this.assignedTasksCount = assignedTasksCount; }
+
+ public long getCompletedTasksCount() { return completedTasksCount; }
+ public void setCompletedTasksCount(long completedTasksCount) { this.completedTasksCount = completedTasksCount; }
+
+ public int getHoursWorked() { return hoursWorked; }
+ public void setHoursWorked(int hoursWorked) { this.hoursWorked = hoursWorked; }
+
+ public int getEstimatedHours() { return estimatedHours; }
+ public void setEstimatedHours(int estimatedHours) { this.estimatedHours = estimatedHours; }
+ }
+
+ public static class SprintStats {
+ private Integer sprintId;
+ private Integer devId;
+ private long assignedTasksCount;
+ private long completedTasksCount;
+ private int hoursWorked;
+
+ // Constructors, getters, setters
+ public SprintStats() {}
+
+ public SprintStats(Integer sprintId, Integer devId, long assignedTasksCount,
+ long completedTasksCount, int hoursWorked) {
+ this.sprintId = sprintId;
+ this.devId = devId;
+ this.assignedTasksCount = assignedTasksCount;
+ this.completedTasksCount = completedTasksCount;
+ this.hoursWorked = hoursWorked;
+ }
+
+ // Getters and setters
+ public Integer getSprintId() { return sprintId; }
+ public void setSprintId(Integer sprintId) { this.sprintId = sprintId; }
+
+ public Integer getDevId() { return devId; }
+ public void setDevId(Integer devId) { this.devId = devId; }
+
+ public long getAssignedTasksCount() { return assignedTasksCount; }
+ public void setAssignedTasksCount(long assignedTasksCount) { this.assignedTasksCount = assignedTasksCount; }
+
+ public long getCompletedTasksCount() { return completedTasksCount; }
+ public void setCompletedTasksCount(long completedTasksCount) { this.completedTasksCount = completedTasksCount; }
+
+ public int getHoursWorked() { return hoursWorked; }
+ public void setHoursWorked(int hoursWorked) { this.hoursWorked = hoursWorked; }
+ }
+
+ public static class SprintInfo {
+ private Integer id;
+ private String name;
+
+ // Constructors, getters, setters
+ public SprintInfo() {}
+
+ public SprintInfo(Integer id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ // Getters and setters
+ public Integer getId() { return id; }
+ public void setId(Integer id) { this.id = id; }
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ }
+
+ public static class DashboardData {
+ private List developers;
+ private List sprintStats;
+ private List sprints;
+ private List tasks;
+
+ // Constructors, getters, setters
+ public DashboardData() {}
+
+ public DashboardData(List developers, List sprintStats,
+ List sprints, List tasks) {
+ this.developers = developers;
+ this.sprintStats = sprintStats;
+ this.sprints = sprints;
+ this.tasks = tasks;
+ }
+
+ // Getters and setters
+ public List getDevelopers() { return developers; }
+ public void setDevelopers(List developers) { this.developers = developers; }
+
+ public List getSprintStats() { return sprintStats; }
+ public void setSprintStats(List sprintStats) { this.sprintStats = sprintStats; }
+
+ public List getSprints() { return sprints; }
+ public void setSprints(List sprints) { this.sprints = sprints; }
+
+ public List getTasks() { return tasks; }
+ public void setTasks(List tasks) { this.tasks = tasks; }
+ }
+
+ public DashboardData getManagerDashboard(Integer projectID) {
+ List tasks;
+ List developers;
+
+ if (projectID != null) {
+ tasks = taskRepository.findAll().stream()
+ .filter(task -> projectID.equals(task.getProjectID()))
+ .collect(Collectors.toList());
+ developers = developerRepository.findAll().stream()
+ .filter(dev -> tasks.stream().anyMatch(task -> dev.getDeveloperID().equals(task.getDeveloperID())))
+ .collect(Collectors.toList());
+ } else {
+ tasks = taskRepository.findAll();
+ developers = developerRepository.findAll();
+ }
+
+ // Calculate developer stats
+ List developerStats = developers.stream()
+ .map(dev -> {
+ List devTasks = tasks.stream()
+ .filter(task -> dev.getDeveloperID().equals(task.getDeveloperID()))
+ .collect(Collectors.toList());
+
+ long assignedCount = devTasks.size();
+ long completedCount = devTasks.stream()
+ .filter(task -> "completed".equals(task.getStatus()))
+ .count();
+ int hoursWorked = devTasks.stream()
+ .mapToInt(task -> task.getTimeSpent() != null ? task.getTimeSpent() : 0)
+ .sum();
+ int estimatedHours = devTasks.stream()
+ .mapToInt(task -> task.getEstimatedTime() != null ? task.getEstimatedTime() : 0)
+ .sum();
+
+ return new DeveloperStats(dev.getDeveloperID(), dev.getUserID(), dev.getTeamID(),
+ assignedCount, completedCount, hoursWorked, estimatedHours);
+ })
+ .collect(Collectors.toList());
+
+ // Calculate sprint stats (simplified - assuming sprint field exists)
+ List sprintStats = tasks.stream()
+ .filter(task -> task.getSprint() != null)
+ .collect(Collectors.groupingBy(task -> task.getSprint()))
+ .entrySet().stream()
+ .flatMap(entry -> {
+ Integer sprintId = entry.getKey();
+ List sprintTasks = entry.getValue();
+
+ return sprintTasks.stream()
+ .collect(Collectors.groupingBy(Task::getDeveloperID))
+ .entrySet().stream()
+ .map(devEntry -> {
+ Integer devId = devEntry.getKey();
+ List devSprintTasks = devEntry.getValue();
+
+ long assignedCount = devSprintTasks.size();
+ long completedCount = devSprintTasks.stream()
+ .filter(task -> "completed".equals(task.getStatus()))
+ .count();
+ int hoursWorked = devSprintTasks.stream()
+ .mapToInt(task -> task.getTimeSpent() != null ? task.getTimeSpent() : 0)
+ .sum();
+
+ return new SprintStats(sprintId, devId, assignedCount, completedCount, hoursWorked);
+ });
+ })
+ .collect(Collectors.toList());
+
+ // Get unique sprints
+ List sprints = tasks.stream()
+ .filter(task -> task.getSprint() != null)
+ .map(task -> new SprintInfo(task.getSprint(), "Sprint " + task.getSprint()))
+ .distinct()
+ .collect(Collectors.toList());
+
+ return new DashboardData(developerStats, sprintStats, sprints, tasks);
+ }
+
+ public DashboardData getDeveloperDashboard(Integer developerID, Integer projectID) {
+ List tasks = taskRepository.findAll().stream()
+ .filter(task -> developerID.equals(task.getDeveloperID()))
+ .filter(task -> projectID == null || projectID.equals(task.getProjectID()))
+ .collect(Collectors.toList());
+
+ // For developer dashboard, we still need the same structure but filtered
+ // Developers list will contain only this developer
+ Optional developer = developerRepository.findById(developerID);
+ List developerStats = developer.map(dev -> {
+ long assignedCount = tasks.size();
+ long completedCount = tasks.stream()
+ .filter(task -> "completed".equals(task.getStatus()))
+ .count();
+ int hoursWorked = tasks.stream()
+ .mapToInt(task -> task.getTimeSpent() != null ? task.getTimeSpent() : 0)
+ .sum();
+ int estimatedHours = tasks.stream()
+ .mapToInt(task -> task.getEstimatedTime() != null ? task.getEstimatedTime() : 0)
+ .sum();
+
+ return new DeveloperStats(dev.getDeveloperID(), dev.getUserID(), dev.getTeamID(),
+ assignedCount, completedCount, hoursWorked, estimatedHours);
+ }).map(List::of).orElse(List.of());
+
+ // Sprint stats for this developer
+ List sprintStats = tasks.stream()
+ .filter(task -> task.getSprint() != null)
+ .collect(Collectors.groupingBy(task -> task.getSprint()))
+ .entrySet().stream()
+ .map(entry -> {
+ Integer sprintId = entry.getKey();
+ List sprintTasks = entry.getValue();
+
+ long assignedCount = sprintTasks.size();
+ long completedCount = sprintTasks.stream()
+ .filter(task -> "completed".equals(task.getStatus()))
+ .count();
+ int hoursWorked = sprintTasks.stream()
+ .mapToInt(task -> task.getTimeSpent() != null ? task.getTimeSpent() : 0)
+ .sum();
+
+ return new SprintStats(sprintId, developerID, assignedCount, completedCount, hoursWorked);
+ })
+ .collect(Collectors.toList());
+
+ // Get unique sprints
+ List sprints = tasks.stream()
+ .filter(task -> task.getSprint() != null)
+ .map(task -> new SprintInfo(task.getSprint(), "Sprint " + task.getSprint()))
+ .distinct()
+ .collect(Collectors.toList());
+
+ return new DashboardData(developerStats, sprintStats, sprints, tasks);
+ }
+
+ public Optional getTask(Integer taskID) {
+ return taskRepository.findById(taskID);
+ }
+
+ public Optional updateTask(Integer taskID, String status, Integer timeSpent) {
+ Optional taskOpt = taskRepository.findById(taskID);
+ if (taskOpt.isPresent()) {
+ Task task = taskOpt.get();
+ if (status != null) {
+ task.setStatus(status);
+ }
+ if (timeSpent != null) {
+ task.setTimeSpent(timeSpent);
+ }
+ task.setUpdatedAt(java.time.LocalDateTime.now());
+ return Optional.of(taskRepository.save(task));
+ }
+ return Optional.empty();
+ }
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/Dashboardservice b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/Dashboardservice
new file mode 100644
index 000000000..89e4d9436
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/Dashboardservice
@@ -0,0 +1,311 @@
+package com.springboot.MyTodoList.service;
+
+import com.springboot.MyTodoList.model.Developer;
+import com.springboot.MyTodoList.model.Task;
+import com.springboot.MyTodoList.repository.DeveloperRepository;
+import com.springboot.MyTodoList.repository.TaskRepository;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Service
+public class DashboardService {
+
+ private final TaskRepository taskRepository;
+ private final DeveloperRepository developerRepository;
+
+ public DashboardService(TaskRepository taskRepository, DeveloperRepository developerRepository) {
+ this.taskRepository = taskRepository;
+ this.developerRepository = developerRepository;
+ }
+
+ public static class DeveloperStats {
+ private Integer developerID;
+ private Integer userID;
+ private Integer teamID;
+ private long assignedTasksCount;
+ private long completedTasksCount;
+ private int hoursWorked;
+ private int estimatedHours;
+
+ // Constructors, getters, setters
+ public DeveloperStats() {}
+
+ public DeveloperStats(Integer developerID, Integer userID, Integer teamID,
+ long assignedTasksCount, long completedTasksCount,
+ int hoursWorked, int estimatedHours) {
+ this.developerID = developerID;
+ this.userID = userID;
+ this.teamID = teamID;
+ this.assignedTasksCount = assignedTasksCount;
+ this.completedTasksCount = completedTasksCount;
+ this.hoursWorked = hoursWorked;
+ this.estimatedHours = estimatedHours;
+ }
+
+ // Getters and setters
+ public Integer getDeveloperID() { return developerID; }
+ public void setDeveloperID(Integer developerID) { this.developerID = developerID; }
+
+ public Integer getUserID() { return userID; }
+ public void setUserID(Integer userID) { this.userID = userID; }
+
+ public Integer getTeamID() { return teamID; }
+ public void setTeamID(Integer teamID) { this.teamID = teamID; }
+
+ public long getAssignedTasksCount() { return assignedTasksCount; }
+ public void setAssignedTasksCount(long assignedTasksCount) { this.assignedTasksCount = assignedTasksCount; }
+
+ public long getCompletedTasksCount() { return completedTasksCount; }
+ public void setCompletedTasksCount(long completedTasksCount) { this.completedTasksCount = completedTasksCount; }
+
+ public int getHoursWorked() { return hoursWorked; }
+ public void setHoursWorked(int hoursWorked) { this.hoursWorked = hoursWorked; }
+
+ public int getEstimatedHours() { return estimatedHours; }
+ public void setEstimatedHours(int estimatedHours) { this.estimatedHours = estimatedHours; }
+ }
+
+ public static class SprintStats {
+ private Integer sprintId;
+ private Integer devId;
+ private long assignedTasksCount;
+ private long completedTasksCount;
+ private int hoursWorked;
+
+ // Constructors, getters, setters
+ public SprintStats() {}
+
+ public SprintStats(Integer sprintId, Integer devId, long assignedTasksCount,
+ long completedTasksCount, int hoursWorked) {
+ this.sprintId = sprintId;
+ this.devId = devId;
+ this.assignedTasksCount = assignedTasksCount;
+ this.completedTasksCount = completedTasksCount;
+ this.hoursWorked = hoursWorked;
+ }
+
+ // Getters and setters
+ public Integer getSprintId() { return sprintId; }
+ public void setSprintId(Integer sprintId) { this.sprintId = sprintId; }
+
+ public Integer getDevId() { return devId; }
+ public void setDevId(Integer devId) { this.devId = devId; }
+
+ public long getAssignedTasksCount() { return assignedTasksCount; }
+ public void setAssignedTasksCount(long assignedTasksCount) { this.assignedTasksCount = assignedTasksCount; }
+
+ public long getCompletedTasksCount() { return completedTasksCount; }
+ public void setCompletedTasksCount(long completedTasksCount) { this.completedTasksCount = completedTasksCount; }
+
+ public int getHoursWorked() { return hoursWorked; }
+ public void setHoursWorked(int hoursWorked) { this.hoursWorked = hoursWorked; }
+ }
+
+ public static class SprintInfo {
+ private Integer id;
+ private String name;
+
+ // Constructors, getters, setters
+ public SprintInfo() {}
+
+ public SprintInfo(Integer id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ // Getters and setters
+ public Integer getId() { return id; }
+ public void setId(Integer id) { this.id = id; }
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ }
+
+ public static class DashboardData {
+ private List developers;
+ private List sprintStats;
+ private List sprints;
+ private List tasks;
+
+ // Constructors, getters, setters
+ public DashboardData() {}
+
+ public DashboardData(List developers, List sprintStats,
+ List sprints, List tasks) {
+ this.developers = developers;
+ this.sprintStats = sprintStats;
+ this.sprints = sprints;
+ this.tasks = tasks;
+ }
+
+ // Getters and setters
+ public List getDevelopers() { return developers; }
+ public void setDevelopers(List developers) { this.developers = developers; }
+
+ public List getSprintStats() { return sprintStats; }
+ public void setSprintStats(List sprintStats) { this.sprintStats = sprintStats; }
+
+ public List getSprints() { return sprints; }
+ public void setSprints(List sprints) { this.sprints = sprints; }
+
+ public List getTasks() { return tasks; }
+ public void setTasks(List tasks) { this.tasks = tasks; }
+ }
+
+ public DashboardData getManagerDashboard(Integer projectID) {
+ List tasks;
+ List developers;
+
+ if (projectID != null) {
+ tasks = taskRepository.findAll().stream()
+ .filter(task -> projectID.equals(task.getProjectID()))
+ .collect(Collectors.toList());
+ developers = developerRepository.findAll().stream()
+ .filter(dev -> tasks.stream().anyMatch(task -> dev.getDeveloperID().equals(task.getDeveloperID())))
+ .collect(Collectors.toList());
+ } else {
+ tasks = taskRepository.findAll();
+ developers = developerRepository.findAll();
+ }
+
+ // Calculate developer stats
+ List developerStats = developers.stream()
+ .map(dev -> {
+ List devTasks = tasks.stream()
+ .filter(task -> dev.getDeveloperID().equals(task.getDeveloperID()))
+ .collect(Collectors.toList());
+
+ long assignedCount = devTasks.size();
+ long completedCount = devTasks.stream()
+ .filter(task -> "completed".equals(task.getStatus()))
+ .count();
+ int hoursWorked = devTasks.stream()
+ .mapToInt(task -> task.getTimeSpent() != null ? task.getTimeSpent() : 0)
+ .sum();
+ int estimatedHours = devTasks.stream()
+ .mapToInt(task -> task.getEstimatedTime() != null ? task.getEstimatedTime() : 0)
+ .sum();
+
+ return new DeveloperStats(dev.getDeveloperID(), dev.getUserID(), dev.getTeamID(),
+ assignedCount, completedCount, hoursWorked, estimatedHours);
+ })
+ .collect(Collectors.toList());
+
+ // Calculate sprint stats (simplified - assuming sprint field exists)
+ List sprintStats = tasks.stream()
+ .filter(task -> task.getSprint() != null)
+ .collect(Collectors.groupingBy(task -> task.getSprint()))
+ .entrySet().stream()
+ .flatMap(entry -> {
+ Integer sprintId = entry.getKey();
+ List sprintTasks = entry.getValue();
+
+ return sprintTasks.stream()
+ .collect(Collectors.groupingBy(Task::getDeveloperID))
+ .entrySet().stream()
+ .map(devEntry -> {
+ Integer devId = devEntry.getKey();
+ List devSprintTasks = devEntry.getValue();
+
+ long assignedCount = devSprintTasks.size();
+ long completedCount = devSprintTasks.stream()
+ .filter(task -> "completed".equals(task.getStatus()))
+ .count();
+ int hoursWorked = devSprintTasks.stream()
+ .mapToInt(task -> task.getTimeSpent() != null ? task.getTimeSpent() : 0)
+ .sum();
+
+ return new SprintStats(sprintId, devId, assignedCount, completedCount, hoursWorked);
+ });
+ })
+ .collect(Collectors.toList());
+
+ // Get unique sprints
+ List sprints = tasks.stream()
+ .filter(task -> task.getSprint() != null)
+ .map(task -> new SprintInfo(task.getSprint(), "Sprint " + task.getSprint()))
+ .distinct()
+ .collect(Collectors.toList());
+
+ return new DashboardData(developerStats, sprintStats, sprints, tasks);
+ }
+
+ public DashboardData getDeveloperDashboard(Integer developerID, Integer projectID) {
+ List tasks = taskRepository.findAll().stream()
+ .filter(task -> developerID.equals(task.getDeveloperID()))
+ .filter(task -> projectID == null || projectID.equals(task.getProjectID()))
+ .collect(Collectors.toList());
+
+ // For developer dashboard, we still need the same structure but filtered
+ // Developers list will contain only this developer
+ Optional developer = developerRepository.findById(developerID);
+ List developerStats = developer.map(dev -> {
+ long assignedCount = tasks.size();
+ long completedCount = tasks.stream()
+ .filter(task -> "completed".equals(task.getStatus()))
+ .count();
+ int hoursWorked = tasks.stream()
+ .mapToInt(task -> task.getTimeSpent() != null ? task.getTimeSpent() : 0)
+ .sum();
+ int estimatedHours = tasks.stream()
+ .mapToInt(task -> task.getEstimatedTime() != null ? task.getEstimatedTime() : 0)
+ .sum();
+
+ return new DeveloperStats(dev.getDeveloperID(), dev.getUserID(), dev.getTeamID(),
+ assignedCount, completedCount, hoursWorked, estimatedHours);
+ }).map(List::of).orElse(List.of());
+
+ // Sprint stats for this developer
+ List sprintStats = tasks.stream()
+ .filter(task -> task.getSprint() != null)
+ .collect(Collectors.groupingBy(task -> task.getSprint()))
+ .entrySet().stream()
+ .map(entry -> {
+ Integer sprintId = entry.getKey();
+ List sprintTasks = entry.getValue();
+
+ long assignedCount = sprintTasks.size();
+ long completedCount = sprintTasks.stream()
+ .filter(task -> "completed".equals(task.getStatus()))
+ .count();
+ int hoursWorked = sprintTasks.stream()
+ .mapToInt(task -> task.getTimeSpent() != null ? task.getTimeSpent() : 0)
+ .sum();
+
+ return new SprintStats(sprintId, developerID, assignedCount, completedCount, hoursWorked);
+ })
+ .collect(Collectors.toList());
+
+ // Get unique sprints
+ List sprints = tasks.stream()
+ .filter(task -> task.getSprint() != null)
+ .map(task -> new SprintInfo(task.getSprint(), "Sprint " + task.getSprint()))
+ .distinct()
+ .collect(Collectors.toList());
+
+ return new DashboardData(developerStats, sprintStats, sprints, tasks);
+ }
+
+ public Optional getTask(Integer taskID) {
+ return taskRepository.findById(taskID);
+ }
+
+ public Optional updateTask(Integer taskID, String status, Integer timeSpent) {
+ Optional taskOpt = taskRepository.findById(taskID);
+ if (taskOpt.isPresent()) {
+ Task task = taskOpt.get();
+ if (status != null) {
+ task.setStatus(status);
+ }
+ if (timeSpent != null) {
+ task.setTimeSpent(timeSpent);
+ }
+ task.setUpdatedAt(java.time.LocalDateTime.now());
+ return Optional.of(taskRepository.save(task));
+ }
+ return Optional.empty();
+ }
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/DeepSeekService.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/DeepSeekService.java
index 0e6a3b7aa..e47cabf34 100644
--- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/DeepSeekService.java
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/DeepSeekService.java
@@ -1,32 +1,4 @@
package com.springboot.MyTodoList.service;
-import java.io.IOException;
-import org.apache.hc.client5.http.classic.methods.HttpPost;
-import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
-import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
-import org.apache.hc.core5.http.io.entity.EntityUtils;
-import org.apache.hc.core5.http.io.entity.StringEntity;
-import org.springframework.stereotype.Service;
-
-@Service
-public class DeepSeekService{
- private final CloseableHttpClient httpClient;
- private final HttpPost httpPost;
-
- public DeepSeekService(CloseableHttpClient httpClient, HttpPost httpPost) {
- this.httpClient = httpClient;
- this.httpPost = httpPost;
- }
-
- public String generateText(String prompt) throws IOException, org.apache.hc.core5.http.ParseException {
- String requestBody = String.format("{\"model\": \"deepseek-chat\",\"messages\": [{\"role\": \"user\", \"content\": \"%s\"}]}", prompt);
-
- try {
- httpPost.setEntity(new StringEntity(requestBody));
- CloseableHttpResponse response = httpClient.execute(httpPost);
- return EntityUtils.toString(response.getEntity());
- } catch (IOException e) {
- throw e;
- }
- }
+public class DeepSeekService {
}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/DeveloperService.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/DeveloperService.java
new file mode 100644
index 000000000..6c8845e25
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/DeveloperService.java
@@ -0,0 +1,24 @@
+package com.springboot.MyTodoList.service;
+
+import com.springboot.MyTodoList.dto.DeveloperSummaryDto;
+import com.springboot.MyTodoList.repository.DeveloperRepository;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class DeveloperService {
+ private final DeveloperRepository developerRepository;
+
+ public DeveloperService(DeveloperRepository developerRepository) {
+ this.developerRepository = developerRepository;
+ }
+
+ public List getDeveloperSummaries() {
+ return developerRepository.findDeveloperSummaries()
+ .stream()
+ .map(row -> new DeveloperSummaryDto(row.getDeveloperId(), row.getFullName()))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/GeminiService.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/GeminiService.java
new file mode 100644
index 000000000..72456ed73
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/GeminiService.java
@@ -0,0 +1,88 @@
+package com.springboot.MyTodoList.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class GeminiService {
+
+ private static final Logger logger = LoggerFactory.getLogger(GeminiService.class);
+
+ private final CloseableHttpClient httpClient;
+ private final ObjectMapper objectMapper;
+
+ @Value("${gemini.api.url}")
+ private String apiUrl;
+
+ @Value("${gemini.max-output-tokens:1024}")
+ private Integer maxOutputTokens;
+
+ @Value("${gemini.temperature:0.7}")
+ private Double temperature;
+
+ public GeminiService(CloseableHttpClient httpClient, ObjectMapper objectMapper) {
+ this.httpClient = httpClient;
+ this.objectMapper = objectMapper;
+ }
+
+ public String generateText(String prompt) throws IOException {
+ HttpPost httpPost = new HttpPost(apiUrl);
+ httpPost.addHeader("Content-Type", "application/json");
+
+ Map requestBody = Map.of(
+ "contents", List.of(
+ Map.of(
+ "role", "user",
+ "parts", List.of(Map.of("text", prompt))
+ )
+ ),
+ "generationConfig", Map.of(
+ "temperature", temperature,
+ "maxOutputTokens", maxOutputTokens
+ )
+ );
+
+ httpPost.setEntity(new StringEntity(objectMapper.writeValueAsString(requestBody)));
+
+ try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+ String responseBody;
+ try {
+ responseBody = EntityUtils.toString(response.getEntity());
+ } catch (ParseException e) {
+ throw new IOException("Failed to parse Gemini response", e);
+ }
+ int statusCode = response.getCode();
+ if (statusCode < HttpStatus.SC_SUCCESS || statusCode >= HttpStatus.SC_REDIRECTION) {
+ throw new IOException("Gemini request failed with status " + statusCode + ": " + responseBody);
+ }
+
+ JsonNode root = objectMapper.readTree(responseBody);
+ JsonNode textNode = root.path("candidates").path(0).path("content").path("parts").path(0).path("text");
+ if (!textNode.isMissingNode() && !textNode.isNull()) {
+ return textNode.asText();
+ }
+
+ JsonNode errorNode = root.path("error").path("message");
+ if (!errorNode.isMissingNode() && !errorNode.isNull()) {
+ throw new IOException("Gemini API error: " + errorNode.asText());
+ }
+
+ logger.warn("Gemini response did not contain text content: {}", responseBody);
+ return responseBody;
+ }
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/OciGenerativeAiService.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/OciGenerativeAiService.java
new file mode 100644
index 000000000..fa55487a8
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/OciGenerativeAiService.java
@@ -0,0 +1,4 @@
+package com.springboot.MyTodoList.service;
+
+public class OciGenerativeAiService {
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/TaskService.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/TaskService.java
new file mode 100644
index 000000000..5f5bd3748
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/TaskService.java
@@ -0,0 +1,177 @@
+package com.springboot.MyTodoList.service;
+
+import com.springboot.MyTodoList.model.Task;
+import com.springboot.MyTodoList.repository.TaskRepository;
+import com.springboot.MyTodoList.util.TaskCreationState;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+import java.util.Optional;
+
+@Service
+public class TaskService {
+ private final TaskRepository taskRepository;
+
+ @Value("${bot.task.defaultProjectId:-1}")
+ private int defaultProjectId;
+
+ @Value("${bot.task.defaultDeveloperId:-1}")
+ private int defaultDeveloperId;
+
+ @Value("${bot.task.defaultTaskType:new-feature}")
+ private String defaultTaskType;
+
+ @Value("${bot.task.defaultPriority:MEDIUM}")
+ private String defaultPriority;
+
+ @Value("${bot.task.defaultEstimatedTime:1}")
+ private int defaultEstimatedTime;
+
+ @Value("${bot.task.defaultDeadlineDays:7}")
+ private int defaultDeadlineDays;
+
+ public TaskService(TaskRepository taskRepository) {
+ this.taskRepository = taskRepository;
+ }
+
+ public Task addTaskFromBot(String taskName) {
+ if (defaultProjectId <= 0 || defaultDeveloperId <= 0) {
+ throw new IllegalStateException("Missing bot.task.defaultProjectId or bot.task.defaultDeveloperId");
+ }
+
+ LocalDateTime now = LocalDateTime.now();
+ Task task = new Task();
+ task.setName(taskName);
+ task.setDescription(taskName);
+ task.setStatus("open");
+ task.setTaskType(defaultTaskType);
+ task.setStartDate(now);
+ task.setDeadline(now.plusDays(defaultDeadlineDays));
+ task.setDeveloperID(defaultDeveloperId);
+ task.setEstimatedTime(defaultEstimatedTime);
+ task.setPriority(defaultPriority);
+ task.setProjectID(defaultProjectId);
+ task.setCreatedAt(now);
+ task.setUpdatedAt(now);
+ return taskRepository.save(task);
+ }
+
+ public Task createTask(Task task) {
+ LocalDateTime now = LocalDateTime.now();
+ if (task.getStartDate() == null) {
+ task.setStartDate(now);
+ }
+ task.setCreatedAt(now);
+ task.setUpdatedAt(now);
+ if (task.getStatus() == null || task.getStatus().isBlank()) {
+ task.setStatus("open");
+ }
+ return taskRepository.save(task);
+ }
+
+ public List findAllTasks() {
+ return taskRepository.findAll();
+ }
+
+ public List findPendingTasks() {
+ return taskRepository.findPendingTasks();
+ }
+
+ public boolean isTelegramUserLinked(Long telegramUserId) {
+ if (telegramUserId == null) {
+ return false;
+ }
+ return taskRepository.countTelegramAccountByTelegramUserId(telegramUserId) > 0;
+ }
+
+ public boolean isTelegramUserDeveloper(Long telegramUserId) {
+ if (telegramUserId == null) {
+ return false;
+ }
+ return taskRepository.countDeveloperByTelegramUserId(telegramUserId) > 0;
+ }
+
+ public List findPendingTasksByTelegramUserId(Long telegramUserId) {
+ if (telegramUserId == null) {
+ return List.of();
+ }
+ return taskRepository.findPendingTasksByTelegramUserId(telegramUserId);
+ }
+
+ public Task createTaskFromBotWithAllFields(TaskCreationState state, Long telegramUserId) {
+ // Resolve developer ID from Telegram user ID
+ Integer developerID = taskRepository.findDeveloperIdByTelegramUserId(telegramUserId);
+ if (developerID == null) {
+ throw new IllegalStateException("Developer not found for Telegram user ID: " + telegramUserId);
+ }
+
+ LocalDateTime now = LocalDateTime.now();
+ Task task = new Task();
+ task.setName(state.getName());
+ task.setDescription(state.getDescription());
+ task.setStatus(state.getStatus());
+ task.setTaskType(state.getTaskType());
+ task.setStartDate(now);
+
+ // Parse deadline from string (YYYY-MM-DD) to LocalDateTime
+ try {
+ LocalDate deadlineDate = LocalDate.parse(state.getDeadline());
+ task.setDeadline(deadlineDate.atTime(LocalTime.of(23, 59, 59)));
+ } catch (Exception e) {
+ task.setDeadline(now.plusDays(7)); // fallback to 7 days from now
+ }
+
+ task.setDeveloperID(developerID);
+ task.setEstimatedTime(state.getEstimatedTime());
+ task.setPriority(state.getPriority());
+ task.setProjectID(state.getProjectId());
+ task.setSprint(state.getSprint());
+ task.setCreatedAt(now);
+ task.setUpdatedAt(now);
+
+ return taskRepository.save(task);
+ }
+
+ public Optional findTaskById(int id) {
+ return taskRepository.findById(id);
+ }
+
+ public void deleteTask(int id) {
+ taskRepository.deleteCommentsByTaskId(id);
+ taskRepository.deleteTaskLogsByTaskId(id);
+ taskRepository.deleteById(id);
+ }
+
+ public Optional updateTask(int id, Task updates) {
+ return taskRepository.findById(id).map(existing -> {
+ if (updates.getName() != null) existing.setName(updates.getName());
+ if (updates.getDescription() != null) existing.setDescription(updates.getDescription());
+ if (updates.getStatus() != null) existing.setStatus(updates.getStatus());
+ if (updates.getPriority() != null) existing.setPriority(updates.getPriority());
+ if (updates.getDeadline() != null) existing.setDeadline(updates.getDeadline());
+ if (updates.getEstimatedTime() != null) existing.setEstimatedTime(updates.getEstimatedTime());
+ if (updates.getTimeSpent() != null) existing.setTimeSpent(updates.getTimeSpent());
+ if (updates.getDeveloperID() != null) existing.setDeveloperID(updates.getDeveloperID());
+ existing.setUpdatedAt(LocalDateTime.now());
+ return taskRepository.save(existing);
+ });
+ }
+
+ public Optional updateTaskStatus(int id, String status) {
+ if (!"open".equals(status) && !"in_progress".equals(status) && !"closed".equals(status)) {
+ throw new IllegalArgumentException("Invalid task status: " + status);
+ }
+ Optional taskOpt = taskRepository.findById(id);
+ if (taskOpt.isEmpty()) {
+ return Optional.empty();
+ }
+ Task task = taskOpt.get();
+ task.setStatus(status);
+ task.setUpdatedAt(LocalDateTime.now());
+ return Optional.of(taskRepository.save(task));
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/TelegramMessageService.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/TelegramMessageService.java
new file mode 100644
index 000000000..9c6e520ed
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/TelegramMessageService.java
@@ -0,0 +1,175 @@
+package com.springboot.MyTodoList.service;
+
+import com.springboot.MyTodoList.model.TelegramMessage;
+import com.springboot.MyTodoList.repository.TelegramAccountRepository;
+import com.springboot.MyTodoList.repository.TelegramMessageRepository;
+import java.util.ArrayList;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.transaction.Transactional;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.domain.PageRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TelegramMessageService {
+ private static final Logger logger = LoggerFactory.getLogger(TelegramMessageService.class);
+ private static final int MAX_EMBEDDING_TEXT_LENGTH = 3500;
+
+ private final TelegramMessageRepository telegramMessageRepository;
+ private final TelegramAccountRepository telegramAccountRepository;
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @Value("${oracle.vector.embedding-model}")
+ private String embeddingModelName;
+
+ public TelegramMessageService(
+ TelegramMessageRepository telegramMessageRepository,
+ TelegramAccountRepository telegramAccountRepository
+ ) {
+ this.telegramMessageRepository = telegramMessageRepository;
+ this.telegramAccountRepository = telegramAccountRepository;
+ }
+
+ @Transactional
+ public void saveIncomingMessage(Long chatId, Integer messageId, Long telegramUserId, String messageText, Integer telegramTimestamp) {
+ if (chatId == null || messageId == null || telegramUserId == null || messageText == null || messageText.isBlank()) {
+ return;
+ }
+
+ if (!telegramAccountRepository.existsByTelegramUserId(telegramUserId)) {
+ return;
+ }
+
+ if (telegramMessageRepository.existsByChatIdAndMessageId(chatId, messageId)) {
+ return;
+ }
+
+ try {
+ TelegramMessage telegramMessage = new TelegramMessage();
+ telegramMessage.setChatId(chatId);
+ telegramMessage.setMessageId(messageId);
+ telegramMessage.setTelegramUserId(telegramUserId);
+ telegramMessage.setMessageText(messageText);
+ telegramMessage.setCreatedAt(toLocalDateTime(telegramTimestamp));
+ TelegramMessage savedMessage = telegramMessageRepository.saveAndFlush(telegramMessage);
+ populateEmbedding(savedMessage.getTelegramMessageID());
+ } catch (Exception ex) {
+ logger.warn("Unable to persist Telegram message chatId={} messageId={}", chatId, messageId, ex);
+ }
+ }
+
+ public List findRecentMessages(Long chatId, int limit) {
+ if (chatId == null || limit <= 0) {
+ return List.of();
+ }
+
+ List recentMessages = telegramMessageRepository.findByChatIdOrderByCreatedAtDescMessageIdDesc(
+ chatId,
+ PageRequest.of(0, limit)
+ );
+
+ List orderedMessages = new ArrayList<>(recentMessages);
+ Collections.reverse(orderedMessages);
+ return orderedMessages;
+ }
+
+ public List findRelatedMessages(Long chatId, List recentMessages, int limit) {
+ if (chatId == null || recentMessages == null || recentMessages.isEmpty() || limit <= 0) {
+ return List.of();
+ }
+
+ List excludedIds = recentMessages.stream()
+ .map(TelegramMessage::getTelegramMessageID)
+ .collect(Collectors.toList());
+
+ String queryText = recentMessages.stream()
+ .map(TelegramMessage::getMessageText)
+ .filter(text -> text != null && !text.isBlank())
+ .collect(Collectors.joining(" "));
+
+ if (queryText.isBlank()) {
+ return List.of();
+ }
+
+ queryText = truncateForEmbedding(queryText);
+
+ try {
+ String sql = "SELECT * " +
+ "FROM telegram_message tm " +
+ "WHERE tm.chatId = :chatId " +
+ "AND tm.embedding IS NOT NULL " +
+ "AND tm.telegramMessageID NOT IN (" + buildInClausePlaceholders(excludedIds.size()) + ") " +
+ "ORDER BY VECTOR_DISTANCE(tm.embedding, TO_VECTOR(VECTOR_EMBEDDING(" + sanitizeModelName(embeddingModelName) +
+ " USING :queryText AS data)), COSINE), tm.createdAt DESC, tm.messageId DESC " +
+ "FETCH FIRST " + limit + " ROWS ONLY";
+
+ Query query = entityManager.createNativeQuery(sql, TelegramMessage.class);
+ query.setParameter("chatId", chatId);
+ query.setParameter("queryText", queryText);
+ for (int i = 0; i < excludedIds.size(); i++) {
+ query.setParameter("excludedId" + i, excludedIds.get(i));
+ }
+ @SuppressWarnings("unchecked")
+ List matches = query.getResultList();
+ return matches;
+ } catch (Exception ex) {
+ logger.warn("Unable to retrieve related Telegram messages for chatId={}", chatId, ex);
+ return List.of();
+ }
+ }
+
+ private void populateEmbedding(Long telegramMessageId) {
+ if (telegramMessageId == null) {
+ return;
+ }
+ try {
+ String sql = "UPDATE telegram_message " +
+ "SET embedding = TO_VECTOR(VECTOR_EMBEDDING(" + sanitizeModelName(embeddingModelName) + " USING SUBSTR(messageText, 1, " + MAX_EMBEDDING_TEXT_LENGTH + ") AS data)) " +
+ "WHERE telegramMessageID = :telegramMessageId";
+ entityManager.createNativeQuery(sql)
+ .setParameter("telegramMessageId", telegramMessageId)
+ .executeUpdate();
+ } catch (Exception ex) {
+ logger.warn("Unable to populate embedding for telegramMessageId={}", telegramMessageId, ex);
+ }
+ }
+
+ private String buildInClausePlaceholders(int size) {
+ return java.util.stream.IntStream.range(0, size)
+ .mapToObj(i -> ":excludedId" + i)
+ .collect(Collectors.joining(", "));
+ }
+
+ private String sanitizeModelName(String modelName) {
+ if (modelName == null || !modelName.matches("[A-Za-z0-9_]+")) {
+ throw new IllegalArgumentException("Invalid Oracle embedding model name");
+ }
+ return modelName;
+ }
+
+ private String truncateForEmbedding(String text) {
+ if (text == null || text.length() <= MAX_EMBEDDING_TEXT_LENGTH) {
+ return text;
+ }
+ return text.substring(0, MAX_EMBEDDING_TEXT_LENGTH);
+ }
+
+ private LocalDateTime toLocalDateTime(Integer telegramTimestamp) {
+ if (telegramTimestamp == null) {
+ return LocalDateTime.now();
+ }
+ return LocalDateTime.ofInstant(Instant.ofEpochSecond(telegramTimestamp.longValue()), ZoneOffset.UTC);
+ }
+}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/TelegramSummaryService.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/TelegramSummaryService.java
new file mode 100644
index 000000000..3a95e3f4e
--- /dev/null
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/service/TelegramSummaryService.java
@@ -0,0 +1,362 @@
+package com.springboot.MyTodoList.service;
+
+import com.springboot.MyTodoList.model.TelegramMessage;
+import com.springboot.MyTodoList.model.TelegramSummary;
+import com.springboot.MyTodoList.repository.TelegramAccountRepository;
+import com.springboot.MyTodoList.repository.TelegramSummaryRepository;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TelegramSummaryService {
+
+ private static final Logger logger = LoggerFactory.getLogger(TelegramSummaryService.class);
+ private static final DateTimeFormatter MESSAGE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+ private static final String NONE_IDENTIFIED = "None identified";
+ private static final int MAX_PROMPT_MESSAGE_LENGTH = 500;
+
+ private final TelegramSummaryRepository telegramSummaryRepository;
+ private final TelegramAccountRepository telegramAccountRepository;
+ private final GeminiService geminiService;
+
+ public TelegramSummaryService(
+ TelegramSummaryRepository telegramSummaryRepository,
+ TelegramAccountRepository telegramAccountRepository,
+ GeminiService geminiService
+ ) {
+ this.telegramSummaryRepository = telegramSummaryRepository;
+ this.telegramAccountRepository = telegramAccountRepository;
+ this.geminiService = geminiService;
+ }
+
+ public TelegramSummary generateAndSaveSummary(Long chatId, Long requestedByTelegramUserId, int windowSize, List recentMessages) {
+ return generateAndSaveSummary(chatId, requestedByTelegramUserId, windowSize, recentMessages, List.of());
+ }
+
+ public TelegramSummary generateAndSaveSummary(
+ Long chatId,
+ Long requestedByTelegramUserId,
+ int windowSize,
+ List recentMessages,
+ List relatedMessages
+ ) {
+ try {
+ logger.info("Generating summary using Gemini API for chat {}", chatId);
+ String llmResponse = geminiService.generateText(buildPrompt(recentMessages, relatedMessages));
+ ParsedSummary parsedSummary = parseSummary(llmResponse);
+
+ TelegramSummary summary = new TelegramSummary();
+ summary.setChatId(chatId);
+ summary.setRequestedByTelegramUserId(requestedByTelegramUserId);
+ summary.setRequestedAt(LocalDateTime.now());
+ summary.setWindowSize(windowSize);
+ summary.setSummaryText(parsedSummary.getSummaryText());
+ summary.setDecisionsText(toNullableSection(parsedSummary.getDecisionsText()));
+ summary.setActionItemsText(toNullableSection(parsedSummary.getActionItemsText()));
+
+ logger.info("Summary generated and saved successfully for chat {}", chatId);
+ return telegramSummaryRepository.save(summary);
+ } catch (Exception ex) {
+ logger.error("Error generating summary using Gemini API for chat {}", chatId, ex);
+ throw new RuntimeException("Failed to generate summary: " + ex.getMessage(), ex);
+ }
+ }
+
+ public String buildPrompt(List recentMessages, List relatedMessages) {
+ Map displayNamesByTelegramUserId = resolveDisplayNames(recentMessages, relatedMessages);
+ StringBuilder prompt = new StringBuilder();
+ prompt.append("You are summarizing a Telegram group discussion for a software team.\n");
+ prompt.append("Reply only in English.\n");
+ prompt.append("Return two sections with this exact heading:\n");
+ prompt.append("Summary:\n- ...\n");
+ prompt.append("Write a concise, readable summary in 2 to 5 bullet points.\n");
+ prompt.append("Mention important commitments, due dates, and newly created tasks inside the summary when relevant.\n");
+ prompt.append("Do not add Decisions or Action Items sections.\n");
+ prompt.append("Do not repeat the title inside the bullet content.\n\n");
+ prompt.append("Recent Messages:\n");
+ prompt.append("\n\n\uD83D\uDCDD Suggested tasks:\n- ...\n");
+ prompt.append("Recommend which tasks shpould be created based on the discussion and generated summary\n");
+
+ for (TelegramMessage message : recentMessages) {
+ appendPromptMessage(prompt, message, displayNamesByTelegramUserId);
+ }
+
+ prompt.append("\nRelated Older Context:\n");
+ if (relatedMessages == null || relatedMessages.isEmpty()) {
+ prompt.append("None identified.\n");
+ } else {
+ for (TelegramMessage message : relatedMessages) {
+ appendPromptMessage(prompt, message, displayNamesByTelegramUserId);
+ }
+ }
+
+ return prompt.toString();
+ }
+
+ private void appendPromptMessage(StringBuilder prompt, TelegramMessage message, Map displayNamesByTelegramUserId) {
+ prompt.append("[")
+ .append(message.getCreatedAt() != null ? message.getCreatedAt().format(MESSAGE_TIME_FORMATTER) : "unknown-time")
+ .append("] ")
+ .append(resolveDisplayName(message.getTelegramUserId(), displayNamesByTelegramUserId))
+ .append(": ")
+ .append(truncateForPrompt(message.getMessageText()))
+ .append("\n");
+ }
+
+ private Map resolveDisplayNames(List recentMessages, List relatedMessages) {
+ LinkedHashSet telegramUserIds = new LinkedHashSet<>();
+ collectTelegramUserIds(telegramUserIds, recentMessages);
+ collectTelegramUserIds(telegramUserIds, relatedMessages);
+
+ if (telegramUserIds.isEmpty()) {
+ return Map.of();
+ }
+
+ return telegramAccountRepository.findDisplayNamesByTelegramUserIds(List.copyOf(telegramUserIds)).stream()
+ .filter(row -> row.getTelegramUserId() != null)
+ .collect(Collectors.toMap(
+ TelegramAccountRepository.TelegramUserDisplayProjection::getTelegramUserId,
+ TelegramAccountRepository.TelegramUserDisplayProjection::getDisplayName,
+ (left, right) -> left
+ ));
+ }
+
+ private void collectTelegramUserIds(LinkedHashSet target, List messages) {
+ if (messages == null || messages.isEmpty()) {
+ return;
+ }
+
+ messages.stream()
+ .map(TelegramMessage::getTelegramUserId)
+ .filter(id -> id != null)
+ .forEach(target::add);
+ }
+
+ private String resolveDisplayName(Long telegramUserId, Map displayNamesByTelegramUserId) {
+ if (telegramUserId == null) {
+ return "Unknown user";
+ }
+
+ String displayName = displayNamesByTelegramUserId.get(telegramUserId);
+ if (displayName == null || displayName.isBlank()) {
+ return "User " + telegramUserId;
+ }
+
+ return displayName.trim();
+ }
+
+ private ParsedSummary parseSummary(String llmResponse) {
+ ParsedSummary parsed = parseStructuredSections(llmResponse);
+ String summary = parsed.getSummaryText();
+
+ if (summary == null || summary.isBlank()) {
+ summary = llmResponse == null || llmResponse.isBlank() ? "Summary unavailable." : llmResponse.trim();
+ }
+
+ return new ParsedSummary(normalizeEmpty(summary), null, null);
+ }
+
+ private ParsedSummary parseStructuredSections(String input) {
+ if (input == null || input.isBlank()) {
+ return new ParsedSummary(null, null, null);
+ }
+
+ String normalized = input
+ .replace("\r\n", "\n")
+ .replace("```markdown", "")
+ .replace("```text", "")
+ .replace("```", "")
+ .trim();
+
+ String currentSection = null;
+ List summaryLines = new ArrayList<>();
+ List