diff --git a/.ai_agency/memory/backlog.json b/.ai_agency/memory/backlog.json index 35252c7..571c586 100644 --- a/.ai_agency/memory/backlog.json +++ b/.ai_agency/memory/backlog.json @@ -648,14 +648,90 @@ "sub_steps": [ { "index": 1, "name": "update_refill_dashboard_and_ai_monitor", "description": "Render course reserve badge and next-order voucher info in dashboard, pet profile, and admin panel", "status": "done" } ] + }, + { + "id": "EPIC-17-TASK-01", + "title": "PDF Catalog Link Replacement to Online Catalog Route", + "description": "Replace all static /canina.pdf links in Footer.tsx and SmartAdvisor.tsx with /catalog route.", + "architectural_layer": "presentation_ui", + "assigned_role": "06_dev_frontend", + "priority": "HIGH", + "status": "completed", + "max_files_allowed": 3, + "estimated_minutes": 10, + "dependency_task_ids": ["EPIC-16-TASK-03"], + "acceptance_criteria": [ + "انتقال تمام لینک‌های کاتالوگ در فوتور و مشاور هوشمند به مسیر /catalog", + "عدم وجود لینک شکسته‌شده به فایل pdf محلی" + ], + "sub_steps": [ + { "index": 1, "name": "replace_pdf_catalog_links", "description": "Update Footer.tsx and SmartAdvisor.tsx hrefs to /catalog", "status": "done" } + ] + }, + { + "id": "EPIC-17-TASK-02", + "title": "Backend Video Module, Prisma Schema & Initial Seed Data", + "description": "Create Video model in Prisma schema and build NestJS Video Module, Controller, and Service with public/admin CRUD endpoints.", + "architectural_layer": "business_logic", + "assigned_role": "05_dev_backend", + "priority": "HIGH", + "status": "completed", + "max_files_allowed": 5, + "estimated_minutes": 25, + "dependency_task_ids": ["EPIC-17-TASK-01"], + "acceptance_criteria": [ + "ایجاد جدول Video در دیتابیس PostgreSQL و Prisma", + "ارائه APIهای کامل دریافت، ایجاد، ویرایش و حذف ویدئوها" + ], + "sub_steps": [ + { "index": 1, "name": "create_backend_video_module", "description": "Add Video model in schema.prisma and build NestJS VideoModule", "status": "done" } + ] + }, + { + "id": "EPIC-17-TASK-03", + "title": "Admin Panel Video Management Page & Navigation Integration", + "description": "Build Videos.tsx in admin-panel featuring video table, create/edit video modal, delete confirmation, and sidebar menu link.", + "architectural_layer": "presentation_ui", + "assigned_role": "06_dev_frontend", + "priority": "HIGH", + "status": "completed", + "max_files_allowed": 4, + "estimated_minutes": 25, + "dependency_task_ids": ["EPIC-17-TASK-02"], + "acceptance_criteria": [ + "افزوده شدن گزینه مدیریت ویدئوها به منوی اصلی پنل مدیریت", + "مدیریت کامل ویدئوها (ایجاد، مشاهده، تغییر پوستر، ویدئو، توضیحات و فیلد ویژه)" + ], + "sub_steps": [ + { "index": 1, "name": "build_admin_videos_page", "description": "Create Videos.tsx and link in Sidebar.tsx and App.tsx", "status": "done" } + ] + }, + { + "id": "EPIC-17-TASK-04", + "title": "Frontend Dynamic Video Fetching & Synchronization", + "description": "Create videoService.ts and update VetGallery.tsx and VideosPage.tsx to fetch dynamic video data from NestJS API instead of hardcoded mock arrays.", + "architectural_layer": "presentation_ui", + "assigned_role": "06_dev_frontend", + "priority": "HIGH", + "status": "completed", + "max_files_allowed": 4, + "estimated_minutes": 20, + "dependency_task_ids": ["EPIC-17-TASK-03"], + "acceptance_criteria": [ + "بارگذاری پویا ویدئوها در صفحه اصلی و صفحه اختصاصی /videos از دیتابیس بک‌اند", + "هماهنگی ۱۰۰٪ داده‌های ویدئو بین تمام صفحات سایت و پنل مدیریت" + ], + "sub_steps": [ + { "index": 1, "name": "connect_frontend_videos_api", "description": "Create videoService.ts and wire VetGallery.tsx and VideosPage.tsx to NestJS backend API", "status": "done" } + ] } ], "metadata": { - "total": 33, - "completed": 33, + "total": 37, + "completed": 37, "in_progress": 0, "pending": 0, - "generated_at": "2026-07-27T11:45:00Z", - "decomposition_pass": 14 + "generated_at": "2026-07-27T11:55:00Z", + "decomposition_pass": 15 } } \ No newline at end of file diff --git a/.ai_agency/memory/state.json b/.ai_agency/memory/state.json index da504a5..bc008b9 100644 --- a/.ai_agency/memory/state.json +++ b/.ai_agency/memory/state.json @@ -7,12 +7,12 @@ "status": "COMPLETE", "checkpoint": { "active_agent": "01_auditor", - "current_ticket_id": "EPIC-16-TASK-03", + "current_ticket_id": "EPIC-17-TASK-04", "sub_step": { "index": 1, "total": 1, - "name": "update_refill_dashboard_and_ai_monitor", - "description": "Smart Refill Course Reservation model, next-order 5% cashback voucher, and abuse prevention complete and 100% verified" + "name": "connect_frontend_videos_api", + "description": "PDF catalog link replacement to /catalog and Full-stack Video Management System (NestJS API, Prisma DB, Admin Videos page, and frontend sync) complete and 100% verified" } }, "review_phase": { diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 1670507..a26455e 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -374,3 +374,19 @@ model SmartAdvisorRule { @@map("smart_advisor_rules") } + +model Video { + id String @id @default(uuid()) @db.Uuid + title String @db.VarChar(200) + doctor String @db.VarChar(150) + duration String @default("02:00") @db.VarChar(20) + thumbnail String? @db.Text + videoUrl String @map("video_url") @db.Text + description String? @db.Text + viewsCount Int @default(0) @map("views_count") + isFeatured Boolean @default(false) @map("is_featured") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz() + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz() + + @@map("videos") +} diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index 7b8a7e7..dc595c5 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -315,6 +315,58 @@ async function main() { console.error('Failed to seed home components:', err); } + console.log('Seeding Initial Videos...'); + await prisma.video.deleteMany(); + await prisma.video.createMany({ + data: [ + { + title: "نحوه آماده‌سازی کانی‌هیدروکس GAG", + doctor: "دکتر کلاوس هنینگ", + duration: "۰۱:۴۵", + thumbnail: "https://images.unsplash.com/photo-1576091160550-217359f42f8c?auto=format&fit=crop&q=80&w=600", + videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4", + description: "آموزش گام به گام آماده‌سازی پودر Canhydrox GAG برای جذب حداکثری در دستگاه گوارش.", + isFeatured: true, + }, + { + title: "اهمیت صدف لب‌سبز برای بازسازی غضروف", + doctor: "دکتر النا اشمیت", + duration: "۰۲:۲۰", + thumbnail: "https://images.unsplash.com/photo-1599443015574-be5fe8a05783?auto=format&fit=crop&q=80&w=600", + videoUrl: "https://www.w3schools.com/html/movie.mp4", + description: "چرا صدف لب‌سبز نیوزیلندی نایاب‌ترین و موثرترین ماده برای بازسازی مفصل است؟", + isFeatured: true, + }, + { + title: "تغذیه هوشمند توله‌سگ‌های نژاد بزرگ", + doctor: "دکتر هلگا فیشر", + duration: "۰۳:۱۰", + thumbnail: "/assets/images/regenerated_image_1779109861747.png", + videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4", + description: "دوز مصرفی مکمل‌های کلسیم در نژادهای بزرگ برای جلوگیری از بدشکلی‌های استخوانی.", + isFeatured: true, + }, + { + title: "تاثیر امگا ۳ بر ریزش مو فصلی", + doctor: "دکتر ماری کونیگ", + duration: "۰۵:۱۵", + thumbnail: "https://images.unsplash.com/photo-1516734212186-a967f81ad0d7?auto=format&fit=crop&q=80&w=600", + videoUrl: "https://www.w3schools.com/html/movie.mp4", + description: "چربی‌های ضروری چگونه لایه محافظ پوست را بازسازی می‌کنند.", + isFeatured: false, + }, + { + title: "سم‌زدایی گوارشی با اسید هومیک", + doctor: "دکتر ریکاردو کوخ", + duration: "۰۴:۰۰", + thumbnail: "https://images.unsplash.com/photo-1581594693702-fbdc51b2763b?auto=format&fit=crop&q=80&w=600", + videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4", + description: "مکانیزم عمل Moor-Tranke در جذب توکسین‌های روده‌ای.", + isFeatured: false, + } + ] + }); + console.log('All Seeding completed successfully!'); } diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index d4d4240..e3a212f 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -18,6 +18,7 @@ import { WikiModule } from './wiki/wiki.module'; import { SeoModule } from './seo/seo.module'; import { CmsModule } from './cms/cms.module'; import { WholesaleModule } from './wholesale/wholesale.module'; +import { VideosModule } from './videos/videos.module'; @Module({ imports: [ @@ -40,6 +41,7 @@ import { WholesaleModule } from './wholesale/wholesale.module'; SeoModule, CmsModule, WholesaleModule, + VideosModule, ], controllers: [MetricsController], providers: [ diff --git a/backend/src/videos/dto/create-video.dto.ts b/backend/src/videos/dto/create-video.dto.ts new file mode 100644 index 0000000..ccad3d5 --- /dev/null +++ b/backend/src/videos/dto/create-video.dto.ts @@ -0,0 +1,41 @@ +import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateVideoDto { + @ApiProperty({ description: 'عنوان ویدئوی آموزشی', example: 'نحوه آماده‌سازی کانی‌هیدروکس GAG' }) + @IsString() + @IsNotEmpty() + title: string; + + @ApiProperty({ description: 'نام دکتر/ارائه‌دهنده', example: 'دکتر کلاوس هنینگ' }) + @IsString() + @IsNotEmpty() + doctor: string; + + @ApiPropertyOptional({ description: 'مدت زمان ویدئو', example: '۰۲:۳۰' }) + @IsString() + @IsOptional() + duration?: string; + + @ApiPropertyOptional({ description: 'آدرس تصویر کاور', example: 'https://example.com/thumb.jpg' }) + @IsString() + @IsOptional() + thumbnail?: string; + + @ApiProperty({ description: 'آدرس فایل ویدئو', example: 'https://example.com/video.mp4' }) + @IsString() + @IsNotEmpty() + videoUrl: string; + + @ApiPropertyOptional({ description: 'توضیحات تکمیلی ویدئو' }) + @IsString() + @IsOptional() + description?: string; + + @ApiPropertyOptional({ description: 'آیا ویدئوی ویژه/صفحه اصلی است؟', example: true }) + @IsBoolean() + @IsOptional() + isFeatured?: boolean; +} + +export class UpdateVideoDto extends CreateVideoDto {} diff --git a/backend/src/videos/videos.controller.ts b/backend/src/videos/videos.controller.ts new file mode 100644 index 0000000..aa33e06 --- /dev/null +++ b/backend/src/videos/videos.controller.ts @@ -0,0 +1,51 @@ +import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common'; +import { VideosService } from './videos.service'; +import { CreateVideoDto } from './dto/create-video.dto'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { ApiTags, ApiOperation, ApiQuery, ApiBearerAuth } from '@nestjs/swagger'; + +@ApiTags('Videos - مشاوره ویدئویی و آکادمی') +@Controller('videos') +export class VideosController { + constructor(private readonly videosService: VideosService) {} + + @Get() + @ApiOperation({ summary: 'دریافت لیست ویدئوها (عمومی)' }) + @ApiQuery({ name: 'page', required: false }) + @ApiQuery({ name: 'limit', required: false }) + @ApiQuery({ name: 'search', required: false }) + @ApiQuery({ name: 'featured', required: false }) + findAll(@Query() query: any) { + return this.videosService.findAll(query); + } + + @Get(':id') + @ApiOperation({ summary: 'دریافت جزئیات یک ویدئو' }) + findOne(@Param('id') id: string) { + return this.videosService.findOne(id); + } + + @Post() + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'افزودن ویدئوی جدید (ادمین)' }) + create(@Body() createVideoDto: CreateVideoDto) { + return this.videosService.create(createVideoDto); + } + + @Put(':id') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'ویرایش ویدئو (ادمین)' }) + update(@Param('id') id: string, @Body() updateVideoDto: Partial) { + return this.videosService.update(id, updateVideoDto); + } + + @Delete(':id') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'حذف ویدئو (ادمین)' }) + remove(@Param('id') id: string) { + return this.videosService.remove(id); + } +} diff --git a/backend/src/videos/videos.module.ts b/backend/src/videos/videos.module.ts new file mode 100644 index 0000000..12f3faa --- /dev/null +++ b/backend/src/videos/videos.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { VideosController } from './videos.controller'; +import { VideosService } from './videos.service'; + +@Module({ + controllers: [VideosController], + providers: [VideosService], + exports: [VideosService], +}) +export class VideosModule {} diff --git a/backend/src/videos/videos.service.ts b/backend/src/videos/videos.service.ts new file mode 100644 index 0000000..0c64fff --- /dev/null +++ b/backend/src/videos/videos.service.ts @@ -0,0 +1,98 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreateVideoDto } from './dto/create-video.dto'; + +@Injectable() +export class VideosService { + constructor(private readonly prisma: PrismaService) {} + + async findAll(query: any) { + const page = Number(query.page) || 1; + const limit = Number(query.limit) || 20; + const skip = (page - 1) * limit; + + const where: any = {}; + if (query.search) { + where.OR = [ + { title: { contains: query.search, mode: 'insensitive' } }, + { doctor: { contains: query.search, mode: 'insensitive' } }, + { description: { contains: query.search, mode: 'insensitive' } }, + ]; + } + if (query.featured !== undefined) { + where.isFeatured = query.featured === 'true' || query.featured === true; + } + + const [data, total] = await Promise.all([ + this.prisma.video.findMany({ + where, + skip, + take: limit, + orderBy: [{ isFeatured: 'desc' }, { createdAt: 'desc' }], + }), + this.prisma.video.count({ where }), + ]); + + return { + data, + meta: { + total, + page, + limit, + lastPage: Math.ceil(total / limit), + }, + }; + } + + async findOne(id: string) { + const video = await this.prisma.video.findUnique({ + where: { id }, + }); + if (!video) { + throw new NotFoundException('ویدئوی مورد نظر یافت نشد'); + } + // Increment view count + await this.prisma.video.update({ + where: { id }, + data: { viewsCount: { increment: 1 } }, + }); + return video; + } + + async create(dto: CreateVideoDto) { + return this.prisma.video.create({ + data: { + title: dto.title, + doctor: dto.doctor, + duration: dto.duration || '۰۲:۰۰', + thumbnail: dto.thumbnail, + videoUrl: dto.videoUrl, + description: dto.description, + isFeatured: dto.isFeatured ?? false, + }, + }); + } + + async update(id: string, dto: Partial) { + await this.findOne(id); + return this.prisma.video.update({ + where: { id }, + data: { + title: dto.title, + doctor: dto.doctor, + duration: dto.duration, + thumbnail: dto.thumbnail, + videoUrl: dto.videoUrl, + description: dto.description, + isFeatured: dto.isFeatured, + }, + }); + } + + async remove(id: string) { + await this.findOne(id); + return this.prisma.video.delete({ + where: { id }, + }); + } +} diff --git a/frontend/admin-panel/src/App.tsx b/frontend/admin-panel/src/App.tsx index d470c42..603ec41 100644 --- a/frontend/admin-panel/src/App.tsx +++ b/frontend/admin-panel/src/App.tsx @@ -18,6 +18,7 @@ import UITexts from './pages/UITexts'; import Media from './pages/Media'; import CMS from './pages/CMS'; import WholesaleApplications from './pages/WholesaleApplications'; +import Videos from './pages/Videos'; function App() { return ( @@ -37,6 +38,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/admin-panel/src/components/Sidebar.tsx b/frontend/admin-panel/src/components/Sidebar.tsx index ea6fc7a..3b539d2 100644 --- a/frontend/admin-panel/src/components/Sidebar.tsx +++ b/frontend/admin-panel/src/components/Sidebar.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; -import { Users, ShoppingCart, Tag, Settings, LogOut, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages, Image } from 'lucide-react'; +import { Users, ShoppingCart, Tag, Settings, LogOut, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages, Image, Video } from 'lucide-react'; import api from '../services/api'; const menuGroups = [ @@ -33,6 +33,7 @@ const menuGroups = [ items: [ { icon: FileText, label: 'وبلاگ', path: '/blogs' }, { icon: BookOpen, label: 'دانشنامه', path: '/wiki' }, + { icon: Video, label: 'مدیریت ویدئوها', path: '/videos' }, { icon: Image, label: 'بنرها و نظرات (CMS)', path: '/cms' }, ] }, diff --git a/frontend/admin-panel/src/pages/Videos.tsx b/frontend/admin-panel/src/pages/Videos.tsx new file mode 100644 index 0000000..601ba79 --- /dev/null +++ b/frontend/admin-panel/src/pages/Videos.tsx @@ -0,0 +1,341 @@ +import React, { useState, useEffect } from 'react'; +import { Plus, Search, Edit2, Trash2, Video as VideoIcon, Star, CheckCircle, X } from 'lucide-react'; +import api from '../services/api'; + +interface Video { + id: string; + title: string; + doctor: string; + duration: string; + thumbnail: string; + videoUrl: string; + description: string; + viewsCount: number; + isFeatured: boolean; + createdAt: string; +} + +export default function Videos() { + const [videos, setVideos] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingVideo, setEditingVideo] = useState