Skip to content

Commit b1bc90e

Browse files
authored
Merge pull request #80 from splitly25/feat/mock-report
feat: report with ai
2 parents bbc6535 + bcde525 commit b1bc90e

5 files changed

Lines changed: 111 additions & 7 deletions

File tree

api/src/controllers/assistantController.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -268,19 +268,19 @@ const analysisByAssistant = async (userId) => {
268268

269269
const prompts = {
270270
debtAdvice: {
271-
prompt: `You are TingTing, a friendly assistant for managing shared expense bills. Read the user's debt data, including amounts owed, upcoming deadlines, and overdue bills. Provide 1 - 2 concise, actionable recommendations in Vietnamese to help the user prioritize payments, avoid overdue penalties, and manage debts effectively. Keep advice short, clear, and practical.`,
271+
prompt: `Bạn là TingTing, trợ lý thân thiện giúp quản lý hóa đơn chia tiền. Hãy đọc dữ liệu về các khoản nợ của người dùng, bao gồm số tiền nợ, hạn thanh toán sắp tới, và các hóa đơn quá hạn. Đưa ra 1-2 lời khuyên ngắn gọn, thiết thực bằng Tiếng Việt để giúp người dùng ưu tiên thanh toán, tránh bị phạt quá hạn, và quản lý nợ hiệu quả. Lời khuyên phải ngắn gọn, rõ ràng và dễ thực hiện.`,
272272
dataKey: 'debtsIOwe'
273273
},
274274
oweAdvice: {
275-
prompt: `You are TingTing, a friendly assistant for managing shared expense bills. Read the user's data on who owes them money, including overdue bills, amounts, and days overdue. Provide 1-2 concise, actionable recommendations in Vietnamese to help the user politely remind or collect payments early, prioritize the largest or oldest debts, and reduce risk of non-payment. Keep advice short, clear, and practical.`,
275+
prompt: `Bạn là TingTing, trợ lý thân thiện giúp quản lý hóa đơn chia tiền. Hãy đọc dữ liệu về những người nợ tiền người dùng, bao gồm hóa đơn quá hạn, số tiền, và số ngày quá hạn. Đưa ra 1-2 lời khuyên ngắn gọn, thiết thực bằng Tiếng Việt để giúp người dùng nhắc nhở lịch sự hoặc thu tiền sớm, ưu tiên các khoản nợ lớn nhất hoặc lâu nhất, và giảm rủi ro không thu được tiền. Lời khuyên phải ngắn gọn, rõ ràng và dễ thực hiện.`,
276276
dataKey: 'debtsOwedToMe'
277277
},
278278
monthlyAdvice: {
279-
prompt: `You are TingTing, a friendly assistant for managing shared expense bills. Read the user's spending data for this month, including categories, total amounts, and number of bills. Provide a short prediction for next month's spending and 1-2 practical recommendations in Vietnamese to help the user manage expenses better, optimize budgets, and avoid overspending. Keep advice concise, clear, and actionable.`,
279+
prompt: `Bạn là TingTing, trợ lý thân thiện giúp quản lý hóa đơn chia tiền. Hãy đọc dữ liệu chi tiêu của người dùng trong tháng này, bao gồm danh mục, tổng số tiền, và số lượng hóa đơn. Đưa ra dự đoán ngắn gọn về chi tiêu tháng sau và 1-2 lời khuyên thiết thực bằng Tiếng Việt để giúp người dùng quản lý chi tiêu tốt hơn, tối ưu ngân sách, và tránh chi tiêu quá mức. Lời khuyên phải ngắn gọn, rõ ràng và dễ thực hiện.`,
280280
dataKey: 'monthlyStats'
281281
},
282282
productAdvice: {
283-
prompt: `You are TingTing, a friendly assistant for managing shared expense bills. Analyze the user's spending data for this month, including categories, total amounts, and number of bills. Identify which areas the user is spending most and provide 1-2 practical recommendations in Vietnamese to balance their expenses, avoid overspending, and promote healthier financial habits. Keep advice concise, clear, and actionable.`,
283+
prompt: `Bạn là TingTing, trợ lý thân thiện giúp quản lý hóa đơn chia tiền. Hãy phân tích dữ liệu chi tiêu của người dùng trong tháng này, bao gồm danh mục, tổng số tiền, và số lượng hóa đơn. Xác định những lĩnh vực người dùng đang chi tiêu nhiều nhất và đưa ra 1-2 lời khuyên thiết thực bằng Tiếng Việt để cân bằng chi tiêu, tránh chi tiêu quá mức, và thúc đẩy thói quen tài chính lành mạnh hơn. Lời khuyên phải ngắn gọn, rõ ràng và dễ thực hiện.`,
284284
dataKey: 'productsThisMonth'
285285
}
286286
}
@@ -325,7 +325,26 @@ const analysisByAssistant = async (userId) => {
325325
}
326326
};
327327

328+
const getAIAnalysis = async (req, res, next) => {
329+
try {
330+
const { userId } = req.params;
331+
332+
// Security check: Verify that the authenticated user is requesting their own data
333+
if (req.jwtDecoded._id !== userId) {
334+
throw new ApiError(StatusCodes.FORBIDDEN, 'You can only access your own analysis data');
335+
}
336+
337+
const analysis = await analysisByAssistant(userId);
338+
res.status(StatusCodes.OK).json(analysis);
339+
} catch (error) {
340+
console.error('Error in getAIAnalysis:', error);
341+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
342+
const customError = new ApiError(StatusCodes.INTERNAL_SERVER_ERROR, errorMessage);
343+
next(customError);
344+
}
345+
};
346+
328347
export const assistantController = {
329348
processAIRequest,
330-
analysisByAssistant
349+
analysisByAssistant: getAIAnalysis
331350
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import express from 'express'
22
import { assistantController } from '~/controllers/assistantController.js'
33
import { assistantValidation } from '~/validations/assistantValidation'
4+
import { authMiddleware } from '~/middlewares/authMiddleware.js'
45

56
const Router = express.Router()
67

78
Router.route('/').post(assistantValidation.validateAIRequest, assistantController.processAIRequest)
89

10+
// AI Analysis endpoint for reports
11+
Router.route('/analysis/:userId').get(authMiddleware.isAuthorized, assistantController.analysisByAssistant)
12+
913
export const assistantRoute = Router

web/src/apis/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ export const fetchMonthlyReportAPI = async (userId, year, month) => {
2424
return response.data
2525
}
2626

27+
/**
28+
* Fetch AI-powered analysis and recommendations for a user
29+
* @param {string} userId - User ID
30+
* @returns {Promise<Object>} AI analysis including debt advice, spending predictions, and recommendations
31+
*/
32+
export const fetchAIAnalysisAPI = async (userId) => {
33+
const response = await authorizedAxiosInstance.get(`${API_ROOT}/v1/assistant/analysis/${userId}`)
34+
return response.data
35+
}
36+
2737
export const fetchHistoryDataAPI = async (userId, numPage, limit, search, settled) => {
2838
const response = await authorizedAxiosInstance.get(
2939
`${API_ROOT}/v1/history/${userId}?page=${numPage}&limit=${limit}&search=${search}&settled=${settled}`

web/src/pages/Report/Report.jsx

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
2828
import dayjs from 'dayjs'
2929
import 'dayjs/locale/vi'
3030
import { COLORS } from '~/theme'
31-
import { fetchMonthlyReportAPI } from '~/apis'
31+
import { fetchMonthlyReportAPI, fetchAIAnalysisAPI } from '~/apis'
3232
import { toast } from 'react-toastify'
3333
import SpendingTrendChart from '~/components/charts/SpendingTrendChart'
3434
import CategorySpendingChart from '~/components/charts/CategorySpendingChart'
@@ -239,7 +239,9 @@ const Report = () => {
239239
const currentUser = useSelector((state) => state.user.currentUser)
240240
const [selectedMonth, setSelectedMonth] = useState(dayjs())
241241
const [reportData, setReportData] = useState(null)
242+
const [aiAnalysis, setAiAnalysis] = useState(null)
242243
const [loading, setLoading] = useState(true)
244+
const [loadingAI, setLoadingAI] = useState(false)
243245

244246
// Fetch report data when month changes
245247
useEffect(() => {
@@ -265,6 +267,27 @@ const Report = () => {
265267
fetchReportData()
266268
}, [selectedMonth, currentUser])
267269

270+
// Fetch AI analysis once when component mounts
271+
useEffect(() => {
272+
const fetchAIAnalysis = async () => {
273+
if (!currentUser?._id) return
274+
275+
try {
276+
setLoadingAI(true)
277+
const analysis = await fetchAIAnalysisAPI(currentUser._id)
278+
setAiAnalysis(analysis)
279+
} catch (error) {
280+
console.error('Error fetching AI analysis:', error)
281+
// Don't show error toast for AI analysis - it's not critical
282+
setAiAnalysis(null)
283+
} finally {
284+
setLoadingAI(false)
285+
}
286+
}
287+
288+
fetchAIAnalysis()
289+
}, [currentUser])
290+
268291
const handleMonthChange = (newValue) => {
269292
setSelectedMonth(newValue)
270293
}
@@ -654,7 +677,55 @@ const Report = () => {
654677
</Box>
655678

656679
{/* AI Insight Section */}
657-
{reportData?.aiInsights && reportData.aiInsights.length > 0 ? (
680+
{loadingAI ? (
681+
<Card
682+
sx={{
683+
borderRadius: '16px',
684+
border: '1px solid',
685+
borderColor: 'divider',
686+
backgroundColor: 'background.paper',
687+
mb: 3,
688+
p: 4,
689+
display: 'flex',
690+
justifyContent: 'center',
691+
alignItems: 'center',
692+
}}
693+
>
694+
<CircularProgress sx={{ color: COLORS.primary }} size={30} />
695+
<Typography sx={{ ml: 2, color: 'text.secondary' }}>Đang phân tích dữ liệu...</Typography>
696+
</Card>
697+
) : aiAnalysis ? (
698+
<>
699+
{aiAnalysis.monthlyAdvice && (
700+
<AIInsightCard
701+
title="Dự đoán chi tiêu tháng tới"
702+
description={aiAnalysis.monthlyAdvice}
703+
suggestion="Dựa trên phân tích từ TingTing AI"
704+
/>
705+
)}
706+
{aiAnalysis.productAdvice && (
707+
<AIInsightCard
708+
title="Phân tích chi tiêu theo danh mục"
709+
description={aiAnalysis.productAdvice}
710+
suggestion="Cân bằng chi tiêu giữa các danh mục"
711+
/>
712+
)}
713+
{aiAnalysis.debtAdvice && (
714+
<AIInsightCard
715+
title="Quản lý khoản nợ của bạn"
716+
description={aiAnalysis.debtAdvice}
717+
suggestion="Ưu tiên thanh toán đúng hạn"
718+
/>
719+
)}
720+
{aiAnalysis.oweAdvice && (
721+
<AIInsightCard
722+
title="Quản lý khoản cho vay"
723+
description={aiAnalysis.oweAdvice}
724+
suggestion="Nhắc nhở một cách lịch sự"
725+
/>
726+
)}
727+
</>
728+
) : reportData?.aiInsights && reportData.aiInsights.length > 0 ? (
658729
reportData.aiInsights.map((insight, index) => (
659730
<AIInsightCard key={index} {...insight} />
660731
))

web/src/pages/Report/index.jsx

Whitespace-only changes.

0 commit comments

Comments
 (0)