Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b0a791b

Browse files
committedJul 18, 2024·
Added Complete Product Service, Add Review,Manage Reviews,Add Question,Manage Product Question and Edit Product Question and Shimmer Image Effect
1 parent c2adb13 commit b0a791b

27 files changed

+841
-99
lines changed
 

‎Backend/EcommerceInventory/EcommerceInventory/Helpers.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
from rest_framework.exceptions import AuthenticationFailed,NotAuthenticated,PermissionDenied
55
from rest_framework.pagination import PageNumberPagination
66
from django.forms.models import model_to_dict
7+
from functools import wraps
8+
from django.db.models import Q
9+
from django.db import models
710

811
def getDynamicFormModels():
912
return {
@@ -107,4 +110,53 @@ def custom_exception_handler(exc, context):
107110

108111
class CustomPageNumberPagination(PageNumberPagination):
109112
page_size_query_param='pageSize'
110-
max_page_size=100
113+
max_page_size=100
114+
115+
116+
class CommonListAPIMixin:
117+
serializer_class=None
118+
pagination_class=CustomPageNumberPagination
119+
120+
def get_queryset(self):
121+
raise NotImplementedError('get_queryset method not implemented')
122+
123+
def common_list_decorator(serializer_class):
124+
def decorator(list_method):
125+
@wraps(list_method)
126+
def wrapped_list_method(self,request,*args,**kwargs):
127+
queryset=self.get_queryset()
128+
search_query=self.request.query_params.get('search',None)
129+
130+
if search_query:
131+
search_conditions=Q()
132+
133+
for field in serializer_class.Meta.model._meta.get_fields():
134+
if isinstance(field,(models.CharField,models.TextField)):
135+
search_conditions|=Q(**{f"{field.name}__icontains":search_query})
136+
queryset=queryset.filter(search_conditions)
137+
138+
ordering=self.request.query_params.get('ordering',None)
139+
140+
if ordering:
141+
queryset=queryset.order_by(ordering)
142+
143+
page=self.paginate_queryset(queryset)
144+
145+
if page is not None:
146+
serializer=self.get_serializer(page,many=True)
147+
data=serializer.data
148+
total_pages=self.paginator.page.paginator.num_pages
149+
current_page=self.paginator.page.number
150+
page_size=self.paginator.page.paginator.per_page
151+
total_items=self.paginator.page.paginator.count
152+
else:
153+
serializer=self.get_serializer(queryset,many=True)
154+
data=serializer.data
155+
total_pages=1
156+
current_page=1
157+
page_size=len(data)
158+
total_items=len(data)
159+
160+
return renderResponse(data={'data':data,'totalPages':total_pages,'currentPage':current_page,'pageSize':page_size,'totalItems':total_items},message='Data Retrieved Successfully',status=200)
161+
return wrapped_list_method
162+
return decorator
Binary file not shown.
Binary file not shown.
Binary file not shown.
Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from EcommerceInventory.Helpers import CustomPageNumberPagination, renderResponse
1+
from EcommerceInventory.Helpers import CommonListAPIMixin, CustomPageNumberPagination, renderResponse
22
from ProductServices.models import Categories
33
from rest_framework import generics
44
from rest_framework import serializers
@@ -33,46 +33,12 @@ class CategoryListView(generics.ListAPIView):
3333
serializer_class = CategorySerializer
3434
authentication_classes = [JWTAuthentication]
3535
permission_classes = [IsAuthenticated]
36-
pagination_class = CustomPageNumberPagination
3736

3837

3938
def get_queryset(self):
4039
queryset=Categories.objects.filter(parent_id__isnull=True).filter(domain_user_id=self.request.user.domain_user_id.id)
41-
search_query=self.request.query_params.get('search',None)
42-
43-
if search_query:
44-
search_conditions=Q()
45-
46-
for field in Categories._meta.get_fields():
47-
if isinstance(field,(models.CharField,models.TextField)):
48-
search_conditions|=Q(**{f"{field.name}__icontains":search_query})
49-
queryset=queryset.filter(search_conditions)
50-
51-
ordering=self.request.query_params.get('ordering',None)
52-
53-
if ordering:
54-
queryset=queryset.order_by(ordering)
55-
5640
return queryset
57-
58-
def list(self,request,*args,**kwargs):
59-
queryset=self.filter_queryset(self.get_queryset())
60-
61-
page=self.paginate_queryset(queryset)
6241

63-
if page is not None:
64-
serializer=self.get_serializer(page,many=True)
65-
data=serializer.data
66-
total_pages=self.paginator.page.paginator.num_pages
67-
current_page=self.paginator.page.number
68-
page_size=self.paginator.page.paginator.per_page
69-
total_items=self.paginator.page.paginator.count
70-
else:
71-
serializer=self.get_serializer(queryset,many=True)
72-
data=serializer.data
73-
total_pages=1
74-
current_page=1
75-
page_size=len(data)
76-
total_items=len(data)
77-
78-
return renderResponse(data={'data':data,'totalPages':total_pages,'currentPage':current_page,'pageSize':page_size,'totalItems':total_items},message='Categories Retrieved Successfully',status=200)
42+
@CommonListAPIMixin.common_list_decorator(CategorySerializer)
43+
def list(self,request,*args,**kwargs):
44+
return super().list(request,*args,**kwargs)
Lines changed: 103 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
1-
from EcommerceInventory.Helpers import CustomPageNumberPagination, renderResponse
2-
from ProductServices.models import Products
1+
from UserServices.models import Users
2+
from EcommerceInventory.Helpers import CommonListAPIMixin, CustomPageNumberPagination, renderResponse
3+
from ProductServices.models import ProductQuestions, ProductReviews, Products
34
from rest_framework import generics
45
from rest_framework import serializers
56
from rest_framework.permissions import IsAuthenticated
67
from rest_framework_simplejwt.authentication import JWTAuthentication
78
from django.db.models import Q
89
from django.db import models
910

11+
class ProductReviewSerializer(serializers.ModelSerializer):
12+
review_user_id=serializers.SerializerMethodField()
13+
class Meta:
14+
model=ProductReviews
15+
fields='__all__'
16+
17+
def get_review_user_id(self,obj):
18+
return "#"+str(obj.review_user_id.id)+" "+obj.review_user_id.username
19+
20+
class ProductQuestionSerializer(serializers.ModelSerializer):
21+
question_user_id=serializers.SerializerMethodField()
22+
answer_user_id=serializers.SerializerMethodField()
23+
class Meta:
24+
model=ProductQuestions
25+
fields='__all__'
26+
27+
def get_question_user_id(self,obj):
28+
return "#"+str(obj.question_user_id.id)+" "+obj.question_user_id.username
29+
30+
def get_answer_user_id(self,obj):
31+
return "#"+str(obj.answer_user_id.id)+" "+obj.answer_user_id.username
32+
1033
class ProductSerializer(serializers.ModelSerializer):
1134
category_id=serializers.SerializerMethodField()
1235
domain_user_id=serializers.SerializerMethodField()
@@ -33,41 +56,91 @@ class ProductListView(generics.ListAPIView):
3356

3457
def get_queryset(self):
3558
queryset=Products.objects.filter(domain_user_id=self.request.user.domain_user_id.id)
36-
search_query=self.request.query_params.get('search',None)
37-
38-
if search_query:
39-
search_conditions=Q()
59+
return queryset
60+
61+
@CommonListAPIMixin.common_list_decorator(ProductSerializer)
62+
def list(self,request,*args,**kwargs):
63+
return super().list(request,*args,**kwargs)
4064

41-
for field in Products._meta.get_fields():
42-
if isinstance(field,(models.CharField,models.TextField)):
43-
search_conditions|=Q(**{f"{field.name}__icontains":search_query})
44-
queryset=queryset.filter(search_conditions)
4565

46-
ordering=self.request.query_params.get('ordering',None)
66+
class ProductReviewListView(generics.ListAPIView):
67+
serializer_class = ProductReviewSerializer
68+
authentication_classes = [JWTAuthentication]
69+
permission_classes = [IsAuthenticated]
70+
pagination_class = CustomPageNumberPagination
4771

48-
if ordering:
49-
queryset=queryset.order_by(ordering)
72+
def get_queryset(self):
73+
queryset=ProductReviews.objects.filter(domain_user_id=self.request.user.domain_user_id.id,product_id=self.kwargs['product_id'])
74+
return queryset
75+
76+
@CommonListAPIMixin.common_list_decorator(ProductReviewSerializer)
77+
def list(self,request,*args,**kwargs):
78+
return super().list(request,*args,**kwargs)
79+
80+
class ProductQuestionsListView(generics.ListAPIView):
81+
serializer_class = ProductQuestionSerializer
82+
authentication_classes = [JWTAuthentication]
83+
permission_classes = [IsAuthenticated]
84+
pagination_class = CustomPageNumberPagination
5085

86+
def get_queryset(self):
87+
queryset=ProductQuestions.objects.filter(domain_user_id=self.request.user.domain_user_id.id,product_id=self.kwargs['product_id'])
5188
return queryset
5289

90+
@CommonListAPIMixin.common_list_decorator(ProductQuestionSerializer)
5391
def list(self,request,*args,**kwargs):
54-
queryset=self.filter_queryset(self.get_queryset())
92+
return super().list(request,*args,**kwargs)
93+
5594

56-
page=self.paginate_queryset(queryset)
95+
class CreateProductReviewView(generics.CreateAPIView):
96+
serializer_class = ProductReviewSerializer
97+
authentication_classes = [JWTAuthentication]
98+
permission_classes = [IsAuthenticated]
99+
100+
def perform_create(self,serializer):
101+
if self.request.data.get('review_user_id'):
102+
serializer.save(domain_user_id=self.request.user.domain_user_id,review_user_id=Users.objects.get(id=int(self.request.data.get('review_user_id'))),product_id=Products.objects.get(id=self.kwargs['product_id']))
103+
else:
104+
serializer.save(domain_user_id=self.request.user.domain_user_id,product_id=Products.objects.get(id=self.kwargs['product_id']),review_user_id=self.request.user)
105+
106+
class CreateProductQuestionsView(generics.CreateAPIView):
107+
serializer_class = ProductQuestionSerializer
108+
authentication_classes = [JWTAuthentication]
109+
permission_classes = [IsAuthenticated]
110+
111+
def perform_create(self,serializer):
112+
if self.request.data.get('question_user_id') and self.request.data.get('answer_user_id'):
113+
serializer.save(domain_user_id=self.request.user.domain_user_id,question_user_id=Users.objects.get(id=int(self.request.data.get('question_user_id'))),answer_user_id=Users.objects.get(id=int(self.request.data.get('answer_user_id'))),product_id=Products.objects.get(id=self.kwargs['product_id']))
114+
else:
115+
serializer.save(domain_user_id=self.request.user.domain_user_id,product_id=Products.objects.get(id=self.kwargs['product_id']),question_user_id=self.request.user,answer_user_id=self.request.user)
116+
117+
118+
119+
class UpdateProductReviewView(generics.UpdateAPIView):
120+
serializer_class = ProductReviewSerializer
121+
authentication_classes = [JWTAuthentication]
122+
permission_classes = [IsAuthenticated]
123+
124+
def get_queryset(self):
125+
return ProductReviews.objects.filter(domain_user_id=self.request.user.domain_user_id.id,product_id=self.kwargs['product_id'],id=self.kwargs['pk'])
126+
127+
def perform_update(self,serializer):
128+
serializer.save()
129+
130+
131+
class UpdateProductQuestionsView(generics.UpdateAPIView):
132+
serializer_class = ProductQuestionSerializer
133+
authentication_classes = [JWTAuthentication]
134+
permission_classes = [IsAuthenticated]
135+
136+
def get_queryset(self):
137+
return ProductQuestions.objects.filter(domain_user_id=self.request.user.domain_user_id.id,product_id=self.kwargs['product_id'],id=self.kwargs['pk'])
57138

58-
if page is not None:
59-
serializer=self.get_serializer(page,many=True)
60-
data=serializer.data
61-
total_pages=self.paginator.page.paginator.num_pages
62-
current_page=self.paginator.page.number
63-
page_size=self.paginator.page.paginator.per_page
64-
total_items=self.paginator.page.paginator.count
139+
def perform_update(self,serializer):
140+
if self.request.data.get('answer'):
141+
if self.request.data.get('answer_user_id'):
142+
serializer.save(answer_user_id=Users.objects.get(id=int(self.request.data.get('answer_user_id'))))
143+
else:
144+
serializer.save(answer_user_id=self.request.user)
65145
else:
66-
serializer=self.get_serializer(queryset,many=True)
67-
data=serializer.data
68-
total_pages=1
69-
current_page=1
70-
page_size=len(data)
71-
total_items=len(data)
72-
73-
return renderResponse(data={'data':data,'totalPages':total_pages,'currentPage':current_page,'pageSize':page_size,'totalItems':total_items},message='Products Retrieved Successfully',status=200)
146+
serializer.save()
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.0.6 on 2024-07-18 11:54
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('ProductServices', '0004_alter_categories_name'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='categories',
15+
name='image',
16+
field=models.JSONField(blank=True, null=True),
17+
),
18+
]

‎Backend/EcommerceInventory/ProductServices/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
class Categories(models.Model):
77
id=models.AutoField(primary_key=True)
88
name=models.CharField(max_length=255)
9-
image=models.TextField()
9+
image=models.JSONField(blank=True,null=True)
1010
description=models.TextField()
1111
display_order=models.IntegerField(default=0)
1212
parent_id=models.ForeignKey('self',on_delete=models.CASCADE,blank=True,null=True)
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
from .controller.CategoryController import CategoryListView
2-
from .controller.ProductController import ProductListView
2+
from .controller.ProductController import ProductListView,ProductReviewListView,CreateProductReviewView,UpdateProductReviewView,ProductQuestionsListView,CreateProductQuestionsView,UpdateProductQuestionsView
33
from django.urls import path
44

55
urlpatterns = [
66
path('categories/',CategoryListView.as_view(),name='category_list'),
7-
path('',ProductListView.as_view(),name='product_list')
7+
path('',ProductListView.as_view(),name='product_list'),
8+
# Product Review API List,Create,Update
9+
path('productReviews/<str:product_id>/',ProductReviewListView.as_view(),name='product_review_list'),
10+
path('createProductReview/<str:product_id>/',CreateProductReviewView.as_view(),name='product_review_create'),
11+
path('updateProductReview/<str:product_id>/<pk>/',UpdateProductReviewView.as_view(),name='product_review_update'),
12+
#Product Question API List,Create,Update
13+
path('productQuestions/<str:product_id>/',ProductQuestionsListView.as_view(),name='product_question_list'),
14+
path('createProductQuestion/<str:product_id>/',CreateProductQuestionsView.as_view(),name='product_question_create'),
15+
path('updateProductQuestion/<str:product_id>/<pk>/',UpdateProductQuestionsView.as_view(),name='product_question_update'),
816
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from EcommerceInventory.Helpers import renderResponse
2+
from UserServices.models import Users
3+
from rest_framework.views import APIView
4+
from rest_framework.permissions import IsAuthenticated
5+
from rest_framework_simplejwt.authentication import JWTAuthentication
6+
from rest_framework import serializers
7+
8+
class UserSerializer(serializers.ModelSerializer):
9+
class Meta:
10+
model=Users
11+
fields=['id','username','first_name','last_name','email','profile_pic']
12+
13+
class UserListView(APIView):
14+
permission_classes = [IsAuthenticated]
15+
authentication_classes = [JWTAuthentication]
16+
def get(self,request):
17+
users=Users.objects.filter(domain_user_id=request.user.domain_user_id.id)
18+
serializer=UserSerializer(users,many=True)
19+
return renderResponse(data=serializer.data,message="All Users",status=200)
Binary file not shown.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from django.urls import path
2-
from .Controller import AuthController
2+
from .Controller import AuthController,UserController
33

44
urlpatterns = [
55
path('login/',AuthController.LoginAPIView.as_view(),name='login'),
66
path('signup/',AuthController.SignupAPIView.as_view(),name='signup'),
77
path('publicApi/',AuthController.PublicAPIView.as_view(),name='publicapi'),
88
path('protectedApi/',AuthController.ProtectedAPIView.as_view(),name='protectedapi'),
99
path('superadminurl/',AuthController.SuperAdminCheckApi.as_view(),name='superadminurl'),
10+
path('users/',UserController.UserListView.as_view(),name='user_list'),
1011
]

‎Frontend/ecommerce_inventory/src/components/FileInputComponents.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const FileInputComponent = ({field}) => {
1414
const [selectedFiles,setSelectedFiles]=useState([]);
1515
const [filePreviews,setFilePreviews]=useState([]);
1616
const [fileUploaded,setFileUploaded]=useState(false);
17-
const [oldFiles,setOldFiles]=useState((checkIsJson(field.default) && Array.isArray(JSON.parse(field.default)))?JSON.parse(field.default):[])
17+
const [oldFiles,setOldFiles]=useState(Array.isArray(field.default)?field.default:[])
1818
const [oldFilePreviews,setOldFilePreviews]=useState([]);
1919
const [newFilesUrl,setNewFilesUrl]=useState([])
2020

@@ -64,7 +64,7 @@ const FileInputComponent = ({field}) => {
6464
const buildFileUrls=()=>{
6565
const finalUrl=[...oldFiles,...newFilesUrl];
6666
if(finalUrl.length>0){
67-
setValue(field.name,JSON.stringify(finalUrl));
67+
setValue(field.name,finalUrl);
6868
}
6969
else{
7070
resetField(field.name);
@@ -136,8 +136,8 @@ const FileInputComponent = ({field}) => {
136136
}
137137
{
138138
selectedFiles.length>0 && !fileUploaded && (
139-
loading?<LinearProgress sx={{width:'100%'}}/>:
140-
<Box mt={2} display="flex" justifyContent="space-between">
139+
loading?<LinearProgress sx={{width:'100%',mb:2}}/>:
140+
<Box mt={2} display="flex" justifyContent="space-between" mb={2}>
141141
<Button onClick={uploadFiles} variant='contained' color='primary'>Upload Files</Button>
142142
<Button onClick={deleteAllFiles} color='primary' variant='contained'>Delete All Files</Button>
143143
</Box>
@@ -160,7 +160,7 @@ const FileInputComponent = ({field}) => {
160160
))
161161
}
162162
{
163-
!!errors[field.name] && <Alert variant="outlined" severity='error' sx={{marginTop:'10px'}}>
163+
!!errors[field.name] && <Alert variant="outlined" severity='error' sx={{marginTop:'10px',marginBottom:'10px'}}>
164164
This Field is Required and Upload the Files if Already Selected
165165
</Alert>
166166
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React, {useState} from 'react'
2+
const Image=(props)=>{
3+
const [loaded,setLoaded]=useState(false)
4+
5+
const onImageLoad=()=>{
6+
setLoaded(true)
7+
}
8+
return <>
9+
{!loaded && <div className="shimmer" style={props.style}/>}
10+
{
11+
<img {...props} onLoad={onImageLoad}/>
12+
}
13+
</>
14+
}
15+
16+
export default Image;

‎Frontend/ecommerce_inventory/src/components/JsonInputComponent.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {useFormContext} from 'react-hook-form';
22
import { Box,Divider,Icon,TextField } from "@mui/material";
33
import { Delete } from '@mui/icons-material';
4-
import { useState } from 'react';
4+
import { useEffect, useState } from 'react';
55
import AddIcon from '@mui/icons-material/Add';
66
import { Button, IconButton } from '@mui/material';
77

@@ -12,6 +12,13 @@ const JsonInputComponent =({fields})=>{
1212
const newPairs=keyValuePairs.filter((_,i)=>i!==index);
1313
setKeyValuePairs(newPairs);
1414
}
15+
16+
useEffect(()=>{
17+
if(fields.default){
18+
setKeyValuePairs([...keyValuePairs,...fields.default]);
19+
}
20+
},[])
21+
1522
const handleKeyValueAdd=()=>{
1623
setKeyValuePairs([...keyValuePairs,{key:'',value:''}])
1724
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { isValidUrl,getImageUrl } from "../utils/Helper";
2+
import Image from "./Image";
23
import { Typography } from "@mui/material";
34
const RenderImage=({data,name})=>{
4-
return (data && data!=='' && isValidUrl(data))?<img src={getImageUrl(data)} alt={name} style={{width:70,height:70,padding:'5px'}}/>:<Typography variant="body2" pt={3} pb={3}>No Image</Typography>
5+
return (data && data!=='' && isValidUrl(data))?<Image src={getImageUrl(data)} alt={name} style={{width:70,height:70,padding:'5px'}}/>:<Typography variant="body2" pt={3} pb={3}>No Image</Typography>
56
}
67
export default RenderImage;

‎Frontend/ecommerce_inventory/src/layout/GlobalStyle.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,26 @@ export const GlobalStyles = () => {
111111
.active-sidebar svg{
112112
color:${theme.palette.background.light}!important;
113113
}
114-
114+
.shimmer{
115+
width:100%;
116+
height:100%;
117+
margin:10px;
118+
background: linear-gradient(to right, ${theme.palette.background.paper} 8%, ${theme.palette.background.default} 18%, ${theme.palette.background.paper} 33%);
119+
background-size: 800px 104px;
120+
animation:shimmer 1.2s infinite;
121+
border-radius:8px;
122+
}
123+
@keyframes shimmer{
124+
0%{
125+
background-position:-800px 0;
126+
}
127+
100%{
128+
background-position:800px 0;
129+
}
130+
}
131+
.MuiDialogContent-root{
132+
background-color:${theme.palette.background.default};
133+
}
115134
`}
116135
/>
117136
);

‎Frontend/ecommerce_inventory/src/pages/category/ExpandableRow.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { width } from "@mui/system"
22
import { Box, Collapse, Typography } from "@mui/material"
33
import { DataGrid, GridRow, GridToolbar } from "@mui/x-data-grid"
44
import { IconButton } from "@mui/material"
5-
import { Add, Delete, Edit } from "@mui/icons-material"
5+
import { Add, Delete, Edit, Panorama, PanoramaRounded } from "@mui/icons-material"
66
import RenderImage from "../../components/RenderImage"
77

8-
const ExpanableRow=({row,props,onEditClick,onDeleteClick})=>{
8+
const ExpanableRow=({row,props,onEditClick,onDeleteClick,setShowImages,setSelectedImages})=>{
99
let columns=[]
1010
if(row.children && row.children.length>0){
1111
columns=Object.keys(row.children[0]).map(key=>({
@@ -15,7 +15,7 @@ const ExpanableRow=({row,props,onEditClick,onDeleteClick})=>{
1515
})).filter((item)=>item.field!=='children').filter((item)=>item.field!=='image');
1616

1717
columns.push({field:'image',headerName:'Image',width:150,sortable:false,renderCell:(params)=>{
18-
return <RenderImage data={params.row.image} name={params.row.name}/>
18+
return <Box display={"flex"}><RenderImage data={params.row.image} name={params.row.name}/><IconButton onClick={()=>{ setShowImages && setShowImages(true); setSelectedImages && setSelectedImages(params.row.image) }}><PanoramaRounded/></IconButton></Box>
1919
}});
2020
columns=[{field:'action',headerName:'Action',width:180,sortable:false,renderCell:(params)=>{
2121
return <>

‎Frontend/ecommerce_inventory/src/pages/category/ManageCategories.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useState,useEffect } from "react";
1+
import { useState,useEffect, useRef } from "react";
22
import useApi from "../../hooks/APIHandler";
33
import { useNavigate } from "react-router-dom";
4-
import { Box, Breadcrumbs, IconButton, LinearProgress, TextField, Typography } from "@mui/material";
4+
import { Box, Breadcrumbs, Divider, Grid, IconButton, LinearProgress, TextField, Typography } from "@mui/material";
55
import { DataGrid, GridToolbar } from "@mui/x-data-grid";
66
import { isValidUrl } from "../../utils/Helper";
77
import Add from '@mui/icons-material/Add';
@@ -11,6 +11,8 @@ import ExpandLessRounded from '@mui/icons-material/ExpandLessRounded';
1111
import ExpandMoreRounded from '@mui/icons-material/ExpandMoreRounded';
1212
import ExpanableRow from "./ExpandableRow";
1313
import RenderImage from "../../components/RenderImage";
14+
import { Close, PanoramaRounded } from "@mui/icons-material";
15+
import Image from "../../components/Image";
1416

1517
const ManageCategories = () => {
1618
const [data,setData]=useState([]);
@@ -23,7 +25,10 @@ const ManageCategories = () => {
2325
const [searchQuery,setSearchQuery]=useState("");
2426
const [debounceSearch,setDebounceSearch]=useState("");
2527
const [ordering,setOrdering]=useState([{field:'id',sort:'desc'}]);
26-
const {error,loading,callApi}=useApi();
28+
const [showImages,setShowImages]=useState(false);
29+
const [selectedImages,setSelectedImages]=useState([]);
30+
const {error,loading,callApi}=useApi();
31+
const divImage=useRef();
2732
const navigate=useNavigate();
2833

2934
useEffect(()=>{
@@ -107,7 +112,7 @@ const ManageCategories = () => {
107112
}
108113
else if(key==='image'){
109114
columns.push({field:key,headerName:key.charAt(0).toUpperCase()+key.slice(1).replaceAll("_"," "),width:150,sortable:false,renderCell:(params)=>{
110-
return <RenderImage data={params.row.image} name={params.row.name}/>
115+
return <Box display={"flex"}><RenderImage data={params.row.image} name={params.row.name}/><IconButton onClick={()=>{ setSelectedImages(params.row.image); setShowImages(true); }}><PanoramaRounded/></IconButton></Box>
111116
}})
112117
}
113118
else{
@@ -122,6 +127,12 @@ const ManageCategories = () => {
122127
setOrdering(newModel);
123128
}
124129

130+
useEffect(()=>{
131+
if(showImages){
132+
divImage.current.scrollIntoView({behavior:'smooth'})
133+
}
134+
},[selectedImages])
135+
125136
useEffect(()=>{
126137
getCategories();
127138
},[paginationModel,debounceSearch,ordering])
@@ -132,11 +143,14 @@ const ManageCategories = () => {
132143
<Typography variant="body2" onClick={()=>navigate('/')}>Home</Typography>
133144
<Typography variant="body2" onClick={()=>navigate('/manage/category')}>Manage Category</Typography>
134145
</Breadcrumbs>
146+
<Grid container spacing={2}>
147+
<Grid item xs={12} sm={showImages?8:12} lg={showImages?9:12}>
135148
<TextField label="Search" variant="outlined" fullWidth onChange={(e)=>setSearchQuery(e.target.value)} margin="normal"/>
136149
<DataGrid
137150
rows={data}
138151
columns={columns}
139152
rowHeight={75}
153+
autoHeight={true}
140154
sortingOrder={['asc','desc']}
141155
sortModel={ordering}
142156
onSortModelChange={handleSorting}
@@ -163,12 +177,28 @@ const ManageCategories = () => {
163177
loadingOverlay:LinearProgress,
164178
toolbar:GridToolbar,
165179
row:(props)=>{
166-
return <ExpanableRow row={props.row} props={props} onEditClick={onEditClick} onDeleteClick={onDeleteClick}/>
180+
return <ExpanableRow row={props.row} props={props} onEditClick={onEditClick} onDeleteClick={onDeleteClick} setSelectedImages={setSelectedImages} setShowImages={setShowImages}/>
167181
}
168182
}
169183
}
170184

171185
/>
186+
</Grid>
187+
{showImages && <Grid item xs={12} sm={4} lg={3} sx={{height:'600px',overflowY:'auto'}} ref={divImage}>
188+
<Box m={2} display={"flex"} justifyContent={"space-between"}>
189+
<Typography variant="h6">Category Images</Typography>
190+
<IconButton onClick={()=>setShowImages(false)}><Close/></IconButton>
191+
</Box>
192+
<Divider/>
193+
{
194+
selectedImages.length>0 && selectedImages.map((image,index)=>(
195+
<Box key={index} display="flex" justifyContent="center" alignItems="center" p={1}>
196+
<Image src={image} style={{width:'100%'}} />
197+
</Box>
198+
))
199+
}
200+
</Grid>}
201+
</Grid>
172202
</Box>
173203
)
174204
}

‎Frontend/ecommerce_inventory/src/pages/products/ManageProducts.js

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
import { useState,useEffect } from "react";
1+
import { useState,useEffect, useRef } from "react";
22
import useApi from "../../hooks/APIHandler";
33
import { useNavigate } from "react-router-dom";
4-
import { Box, Breadcrumbs, Button, Dialog, DialogContent, Divider, IconButton, LinearProgress, Table, TextField, Typography } from "@mui/material";
4+
import { Box, Breadcrumbs, Button, Dialog, DialogContent, Divider, Grid, IconButton, LinearProgress, Table, TextField, Typography } from "@mui/material";
55
import { DataGrid, GridToolbar } from "@mui/x-data-grid";
66
import { isValidUrl } from "../../utils/Helper";
77
import Add from '@mui/icons-material/Add';
88
import Delete from '@mui/icons-material/Delete';
99
import Edit from '@mui/icons-material/Edit';
1010
import RenderImage from "../../components/RenderImage";
11-
import { Circle, Dashboard } from "@mui/icons-material";
11+
import { Circle, Close, Dashboard, PanoramaRounded } from "@mui/icons-material";
1212
import { set } from "react-hook-form";
1313
import React from "react";
1414
import ViewCompactIcon from '@mui/icons-material/ViewCompact';
15+
import Image from "../../components/Image";
16+
import ManageReviews from "./ManageReview";
17+
import ManageQuestions from "./ManageQuestions";
1518

1619
const ManageProducts = () => {
1720
const [data,setData]=useState([]);
@@ -30,6 +33,13 @@ const ManageProducts = () => {
3033
const [htmldata,setHtmldata]=useState('');
3134
const [openHtml,setOpenHtml]=useState(false);
3235
const [modalTitle,setModalTitle]=useState('');
36+
const [showImages,setShowImages]=useState(false);
37+
const [selectedImages,setSelectedImages]=useState([]);
38+
const [showReviews,setShowReviews]=useState(false);
39+
const [showQuestions,setShowQuestions]=useState(false);
40+
const [selectedProductId,setSelectedProductId]=useState(null);
41+
const divImage=useRef();
42+
3343
const navigate=useNavigate();
3444

3545
useEffect(()=>{
@@ -90,6 +100,13 @@ const ManageProducts = () => {
90100
setOpen(true);
91101
}
92102

103+
useEffect(()=>{
104+
if(showImages){
105+
divImage.current.scrollIntoView({behavior:'smooth'})
106+
}
107+
},[selectedImages])
108+
109+
93110
const generateColumns=(data)=>{
94111
if(data.length>0){
95112
let columns=[{field:'action',headerName:'Action',width:180,sortable:false,renderCell:(params)=>{
@@ -108,18 +125,18 @@ const ManageProducts = () => {
108125
for(const key in data[0]){
109126
if(key==='image'){
110127
columns.push({field:key,headerName:key.charAt(0).toUpperCase()+key.slice(1).replaceAll("_"," "),width:150,sortable:false,renderCell:(params)=>{
111-
return <RenderImage data={params.row.image} name={params.row.name}/>
128+
return <Box display={"flex"}><RenderImage data={params.row.image} name={params.row.name}/><IconButton onClick={()=>{ setSelectedImages(params.row.image); setShowImages(true); }}><PanoramaRounded/></IconButton></Box>
112129
}})
113130
}
114131
else{
115132
columns.push({field:key,headerName:key.charAt(0).toUpperCase()+key.slice(1).replaceAll("_"," "),width:200})
116133
}
117134
}
118135
columns.push({field:'questions',headerName:'Questions',width:150,sortable:false,renderCell:(params)=>{
119-
return <Button startIcon={<ViewCompactIcon/>} variant="contained">View</Button>
136+
return <Button startIcon={<ViewCompactIcon/>} variant="contained" onClick={()=>{ setShowQuestions(true);setShowReviews(false);setSelectedProductId(params.row.id) }}>View</Button>
120137
}})
121138
columns.push({field:'reviews',headerName:'Reviews',width:150,sortable:false,renderCell:(params)=>{
122-
return <Button startIcon={<ViewCompactIcon/>} variant="contained">View</Button>
139+
return <Button startIcon={<ViewCompactIcon/>} variant="contained" onClick={()=>{ setShowQuestions(false);setShowReviews(true);setSelectedProductId(params.row.id) }}>View</Button>
123140
}})
124141
columns=columns.map((column)=>{
125142
if(column.field==='specifications' || column.field==='highlights' || column.field==='seo_keywords' || column.field==='addition_details'){
@@ -153,10 +170,13 @@ const ManageProducts = () => {
153170
<Typography variant="body2" onClick={()=>navigate('/')}>Home</Typography>
154171
<Typography variant="body2" onClick={()=>navigate('/manage/product')}>Manage Products</Typography>
155172
</Breadcrumbs>
173+
<Grid container spacing={2}>
174+
<Grid item xs={12} sm={showImages?8:12} lg={showImages?9:12}>
156175
<TextField label="Search" variant="outlined" fullWidth onChange={(e)=>setSearchQuery(e.target.value)} margin="normal"/>
157176
<DataGrid
158177
rows={data}
159178
columns={columns}
179+
autoHeight={true}
160180
rowHeight={75}
161181
sortingOrder={['asc','desc']}
162182
sortModel={ordering}
@@ -188,7 +208,23 @@ const ManageProducts = () => {
188208
}
189209

190210
/>
191-
<Dialog open={open} fullWidth={true} onClose={handleClose} aria-labelledby="form-dialog-title">
211+
</Grid>
212+
{showImages && <Grid item xs={12} sm={4} lg={3} sx={{height:'600px',overflowY:'auto'}} ref={divImage}>
213+
<Box m={2} display={"flex"} justifyContent={"space-between"}>
214+
<Typography variant="h6">Product Images</Typography>
215+
<IconButton onClick={()=>setShowImages(false)}><Close/></IconButton>
216+
</Box>
217+
<Divider/>
218+
{
219+
selectedImages.length>0 && selectedImages.map((image,index)=>(
220+
<Box key={index} display="flex" justifyContent="center" alignItems="center" p={1}>
221+
<Image src={image} style={{width:'100%'}} />
222+
</Box>
223+
))
224+
}
225+
</Grid>}
226+
</Grid>
227+
<Dialog open={open} fullWidth={true} maxWidth={"lg"} onClose={handleClose} aria-labelledby="form-dialog-title">
192228
<DialogContent>
193229
<Typography variant="h5" mb={2}>{modalTitle} Details </Typography>
194230
<Divider />
@@ -202,13 +238,24 @@ const ManageProducts = () => {
202238
}
203239
</DialogContent>
204240
</Dialog>
205-
<Dialog open={openHtml} fullWidth={true} onClose={handleClose2} aria-labelledby="form-dialog-title">
241+
<Dialog open={openHtml} maxWidth={"lg"} fullWidth={true} onClose={handleClose2} aria-labelledby="form-dialog-title">
206242
<DialogContent>
207243
<Typography variant="h5" mb={2}>HTML Description </Typography>
208244
<Divider />
209245
<div dangerouslySetInnerHTML={{__html:htmldata}}/>
210246
</DialogContent>
211247
</Dialog>
248+
{showReviews && <Dialog maxWidth={"lg"} open={showReviews} fullWidth={true} onClose={()=>setShowReviews(false)} aria-labelledby="form-dialog-title">
249+
<DialogContent>
250+
<ManageReviews product_id={selectedProductId}/>
251+
<Divider />
252+
</DialogContent>
253+
</Dialog>}
254+
{showQuestions && <Dialog maxWidth={"lg"} open={showQuestions} fullWidth={true} onClose={()=>setShowQuestions(false)} aria-labelledby="form-dialog-title">
255+
<DialogContent>
256+
<ManageQuestions product_id={selectedProductId}/>
257+
</DialogContent>
258+
</Dialog>}
212259
</Box>
213260
)
214261
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { useState,useEffect, useRef } from "react";
2+
import useApi from "../../hooks/APIHandler";
3+
import { useNavigate } from "react-router-dom";
4+
import { Autocomplete, Box, Breadcrumbs, Button, Divider, Grid, IconButton, LinearProgress, Rating, TextField, Typography } from "@mui/material";
5+
import { DataGrid, GridToolbar } from "@mui/x-data-grid";
6+
import { isValidUrl } from "../../utils/Helper";
7+
import Add from '@mui/icons-material/Add';
8+
import Delete from '@mui/icons-material/Delete';
9+
import Edit from '@mui/icons-material/Edit';
10+
import ExpandLessRounded from '@mui/icons-material/ExpandLessRounded';
11+
import ExpandMoreRounded from '@mui/icons-material/ExpandMoreRounded';
12+
import RenderImage from "../../components/RenderImage";
13+
import { AddCircle, Close, EditNote, PanoramaRounded, SaveAltRounded } from "@mui/icons-material";
14+
import Image from "../../components/Image";
15+
import { Controller, FormProvider, useForm } from "react-hook-form";
16+
import FileInputComponent from "../../components/FileInputComponents";
17+
18+
const ManageQuestions = ({product_id}) => {
19+
const [data,setData]=useState([]);
20+
const [columns,setColumns]=useState([]);
21+
const [paginationModel,setPaginationModel]=useState({
22+
page:0,
23+
pageSize:5
24+
})
25+
const [totalItems,setTotalItems]=useState(0);
26+
const [searchQuery,setSearchQuery]=useState("");
27+
const [debounceSearch,setDebounceSearch]=useState("");
28+
const [ordering,setOrdering]=useState([{field:'id',sort:'desc'}]);
29+
const [showAddQuestions,setShowAddQuestions]=useState(false);
30+
const [editId,setEditId]=useState(null);
31+
const [userList,setUserList]=useState([]);
32+
const {error,loading,callApi}=useApi();
33+
const divImage=useRef();
34+
const navigate=useNavigate();
35+
const methods=useForm();
36+
const {register,watch,setValue,formState:{errors},control,reset}=methods;
37+
38+
useEffect(()=>{
39+
const timer=setTimeout(()=>{
40+
setDebounceSearch(searchQuery);
41+
},1000)
42+
43+
return ()=>{
44+
clearTimeout(timer);
45+
}
46+
},[searchQuery])
47+
48+
const onSubmitAddQuestion=async(data)=>{
49+
let result=null;
50+
if(editId){
51+
result=await callApi({url:`products/updateProductQuestion/${product_id}/${editId}/`,method:'PATCH',body:data});
52+
setEditId(null);
53+
}
54+
else{
55+
result=await callApi({url:`products/createProductQuestion/${product_id}/`,method:'POST',body:data});
56+
}
57+
if(result){
58+
reset()
59+
getQuestions();
60+
setShowAddQuestions(false);
61+
}
62+
}
63+
64+
const getUserList=async()=>{
65+
const result=await callApi({url:'auth/users/',method:'GET'});
66+
if(result){
67+
setUserList(result.data.data.map(item=>({id:item.id,value:item.email})));
68+
}
69+
70+
}
71+
72+
useEffect(()=>{
73+
getUserList();
74+
},[])
75+
76+
const getQuestions=async()=>{
77+
let order='-id';
78+
if(ordering.length>0){
79+
order=ordering[0].sort==='asc'?ordering[0].field:'-'+ordering[0].field
80+
}
81+
const result=await callApi({url:`products/productQuestions/${product_id}/`,method:'GET',params:{
82+
page:paginationModel.page+1,
83+
pageSize:paginationModel.pageSize,
84+
search:debounceSearch,
85+
ordering:order
86+
}})
87+
if(result){
88+
setData(result.data.data.data);
89+
setTotalItems(result.data.data.totalItems);
90+
generateColumns(result.data.data.data);
91+
}
92+
}
93+
94+
const toggleStatus=async(id,status)=>{
95+
const result=await callApi({url:`products/updateProductQuestion/${product_id}/${id}/`,method:'PATCH',body:{status:status}});
96+
if(result){
97+
getQuestions();
98+
}
99+
}
100+
101+
const editQuestion=(row)=>{
102+
setValue('question',row.question);
103+
setValue('answer',row.answer);
104+
setEditId(row.id);
105+
setShowAddQuestions(true);
106+
}
107+
108+
const generateColumns=(data)=>{
109+
if(data.length>0){
110+
const columns=[{
111+
field:'action',
112+
headerName:'Action',
113+
width:100,
114+
renderCell:(params)=>{
115+
return (
116+
<Box display="flex" justifyContent="center" alignItems="center" mt={3}>
117+
<IconButton onClick={()=>editQuestion(params.row)}><Edit color="primary"/></IconButton>
118+
</Box>
119+
)
120+
}
121+
}];
122+
for(const key in data[0]){
123+
if(key==='status'){
124+
columns.push({field:key,headerName:key.charAt(0).toUpperCase()+key.slice(1).replaceAll("_"," "),width:150,renderCell:(params)=>{
125+
return params.row.status==='ACTIVE'?<Button variant="contained" color="success" onClick={()=>toggleStatus(params.row.id,"INACTIVE")}>ACTIVE</Button>:<Button variant="contained" color="error" onClick={()=>toggleStatus(params.row.id,"ACTIVE")}>INACTIVE</Button>
126+
}})
127+
}
128+
else if(key==="question"){
129+
columns.push({field:"question",headerName:key.charAt(0).toUpperCase()+key.slice(1).replaceAll("_"," "),width:300})
130+
}
131+
else if(key==="answer"){
132+
columns.push({field:"answer",headerName:key.charAt(0).toUpperCase()+key.slice(1).replaceAll("_"," "),width:300})
133+
}
134+
else{
135+
columns.push({field:key,headerName:key.charAt(0).toUpperCase()+key.slice(1).replaceAll("_"," "),width:150})
136+
}
137+
}
138+
setColumns(columns);
139+
}
140+
}
141+
142+
const handleSorting=(newModel)=>{
143+
setOrdering(newModel);
144+
}
145+
146+
useEffect(()=>{
147+
if(showAddQuestions){
148+
divImage.current.scrollIntoView({behavior:'smooth'})
149+
}
150+
},[showAddQuestions])
151+
152+
useEffect(()=>{
153+
getQuestions();
154+
},[paginationModel,debounceSearch,ordering])
155+
156+
return (
157+
<Box component={"div"} sx={{width:'100%'}}>
158+
<Box display={"flex"} justifyContent={"space-between"}>
159+
<Typography variant="h5" mb={2}>Product Questions </Typography>
160+
<Button startIcon={<AddCircle/>} variant="contained" onClick={()=>{ setShowAddQuestions(true);setEditId(null); }}>Add Questions</Button>
161+
</Box>
162+
<Grid container spacing={2}>
163+
<Grid item xs={12} sm={(showAddQuestions)?7:12} lg={(showAddQuestions)?9:12}>
164+
<TextField label="Search" variant="outlined" fullWidth onChange={(e)=>setSearchQuery(e.target.value)} margin="normal"/>
165+
<DataGrid
166+
rows={data}
167+
columns={columns}
168+
rowHeight={75}
169+
autoHeight={true}
170+
sortingOrder={['asc','desc']}
171+
sortModel={ordering}
172+
onSortModelChange={handleSorting}
173+
paginationMode="server"
174+
initialState={{
175+
...data.initialState,
176+
pagination:{paginationModel:paginationModel}
177+
}}
178+
pageSizeOptions={[5,10,20]}
179+
pagination
180+
rowCount={totalItems}
181+
loading={loading}
182+
rowSelection={false}
183+
onPaginationModelChange={(pagedetails)=>{
184+
setPaginationModel({
185+
page:pagedetails.page,
186+
pageSize:pagedetails.pageSize
187+
188+
})
189+
}}
190+
slots={
191+
{
192+
193+
loadingOverlay:LinearProgress,
194+
toolbar:GridToolbar,
195+
}
196+
}
197+
198+
/>
199+
</Grid>
200+
{showAddQuestions && <Grid item xs={12} sm={5} lg={3} sx={{height:'600px',overflowY:'auto'}} ref={divImage}>
201+
<Box m={2} display={"flex"} justifyContent={"space-between"}>
202+
<Typography variant="h6">{editId?'Edit':'Add'} Question & Answer</Typography>
203+
<IconButton onClick={()=>setShowAddQuestions(false)}><Close/></IconButton>
204+
</Box>
205+
<Divider/>
206+
<FormProvider {...methods}>
207+
<form onSubmit={methods.handleSubmit(onSubmitAddQuestion)}>
208+
<TextField label="Question" sx={{marginBottom:'15px'}} variant="outlined" fullWidth margin="normal" {...register('question',{required:true})} error={!!errors['question']} helperText={!!error['question'] && 'This Field is Required'}/>
209+
<TextField label="Answer" sx={{marginBottom:'15px'}} variant="outlined" fullWidth margin="normal" {...register('answer',{required:true})} error={!!errors['answer']} helperText={!!error['answer'] && 'This Field is Required'}/>
210+
<Button sx={{marginBottom:'15px'}} variant="contained" type="submit" fullWidth startIcon={<SaveAltRounded/>}>{editId?'Update':'Add'} Question & Answer</Button>
211+
212+
</form>
213+
</FormProvider>
214+
<Divider/>
215+
</Grid>}
216+
</Grid>
217+
</Box>
218+
)
219+
}
220+
221+
export default ManageQuestions
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import { useState,useEffect, useRef } from "react";
2+
import useApi from "../../hooks/APIHandler";
3+
import { useNavigate } from "react-router-dom";
4+
import { Autocomplete, Box, Breadcrumbs, Button, Divider, Grid, IconButton, LinearProgress, Rating, TextField, Typography } from "@mui/material";
5+
import { DataGrid, GridToolbar } from "@mui/x-data-grid";
6+
import { isValidUrl } from "../../utils/Helper";
7+
import Add from '@mui/icons-material/Add';
8+
import Delete from '@mui/icons-material/Delete';
9+
import Edit from '@mui/icons-material/Edit';
10+
import ExpandLessRounded from '@mui/icons-material/ExpandLessRounded';
11+
import ExpandMoreRounded from '@mui/icons-material/ExpandMoreRounded';
12+
import RenderImage from "../../components/RenderImage";
13+
import { AddCircle, Close, PanoramaRounded, SaveAltRounded } from "@mui/icons-material";
14+
import Image from "../../components/Image";
15+
import { Controller, FormProvider, useForm } from "react-hook-form";
16+
import FileInputComponent from "../../components/FileInputComponents";
17+
18+
const ManageReviews = ({product_id}) => {
19+
const [data,setData]=useState([]);
20+
const [columns,setColumns]=useState([]);
21+
const [paginationModel,setPaginationModel]=useState({
22+
page:0,
23+
pageSize:5
24+
})
25+
const [totalItems,setTotalItems]=useState(0);
26+
const [searchQuery,setSearchQuery]=useState("");
27+
const [debounceSearch,setDebounceSearch]=useState("");
28+
const [ordering,setOrdering]=useState([{field:'id',sort:'desc'}]);
29+
const [showImages,setShowImages]=useState(false);
30+
const [showAddReviews,setShowAddReviews]=useState(false);
31+
const [selectedImages,setSelectedImages]=useState([]);
32+
const [userList,setUserList]=useState([]);
33+
const {error,loading,callApi}=useApi();
34+
const divImage=useRef();
35+
const navigate=useNavigate();
36+
const methods=useForm();
37+
const {register,watch,setValue,formState:{errors},control,reset}=methods;
38+
39+
useEffect(()=>{
40+
const timer=setTimeout(()=>{
41+
setDebounceSearch(searchQuery);
42+
},1000)
43+
44+
return ()=>{
45+
clearTimeout(timer);
46+
}
47+
},[searchQuery])
48+
49+
const onSubmitAddReview=async(data)=>{
50+
const result=await callApi({url:`products/createProductReview/${product_id}/`,method:'POST',body:data});
51+
if(result){
52+
reset()
53+
getReviews();
54+
setShowAddReviews(false);
55+
}
56+
}
57+
58+
const getUserList=async()=>{
59+
const result=await callApi({url:'auth/users/',method:'GET'});
60+
if(result){
61+
setUserList(result.data.data.map(item=>({id:item.id,value:item.email})));
62+
}
63+
64+
}
65+
66+
useEffect(()=>{
67+
getUserList();
68+
},[])
69+
70+
const getReviews=async()=>{
71+
let order='-id';
72+
if(ordering.length>0){
73+
order=ordering[0].sort==='asc'?ordering[0].field:'-'+ordering[0].field
74+
}
75+
const result=await callApi({url:`products/productReviews/${product_id}/`,method:'GET',params:{
76+
page:paginationModel.page+1,
77+
pageSize:paginationModel.pageSize,
78+
search:debounceSearch,
79+
ordering:order
80+
}})
81+
if(result){
82+
setData(result.data.data.data);
83+
setTotalItems(result.data.data.totalItems);
84+
generateColumns(result.data.data.data);
85+
}
86+
}
87+
88+
const toggleStatus=async(id,status)=>{
89+
const result=await callApi({url:`products/updateProductReview/${product_id}/${id}/`,method:'PATCH',body:{status:status}});
90+
if(result){
91+
getReviews();
92+
}
93+
}
94+
95+
const generateColumns=(data)=>{
96+
if(data.length>0){
97+
const columns=[];
98+
for(const key in data[0]){
99+
if(key==='review_images'){
100+
columns.push({field:'review_images',headerName:'Review Images',width:150,sortable:false,renderCell:(params)=>{
101+
return <Box display={"flex"}><RenderImage data={params.row.review_images} name={params.row.review}/><IconButton onClick={()=>{ setSelectedImages(params.row.review_images); setShowImages(true);setShowAddReviews(false) }}><PanoramaRounded/></IconButton></Box>
102+
}})
103+
}
104+
else if(key==='rating'){
105+
columns.push({field:'rating',headerName:'Rating',width:180,sortable:false,renderCell:(params)=>{
106+
return <Box display={"flex"} mt={3}><Rating value={params.row.rating} readOnly/><Typography> ({params.row.rating})</Typography></Box>
107+
}})
108+
}
109+
else if(key==='status'){
110+
columns.push({field:key,headerName:key.charAt(0).toUpperCase()+key.slice(1).replaceAll("_"," "),width:150,renderCell:(params)=>{
111+
return params.row.status==='ACTIVE'?<Button variant="contained" color="success" onClick={()=>toggleStatus(params.row.id,"INACTIVE")}>ACTIVE</Button>:<Button variant="contained" color="error" onClick={()=>toggleStatus(params.row.id,"ACTIVE")}>INACTIVE</Button>
112+
}})
113+
}
114+
else{
115+
columns.push({field:key,headerName:key.charAt(0).toUpperCase()+key.slice(1).replaceAll("_"," "),width:150})
116+
}
117+
}
118+
setColumns(columns);
119+
}
120+
}
121+
122+
const handleSorting=(newModel)=>{
123+
setOrdering(newModel);
124+
}
125+
126+
useEffect(()=>{
127+
if(showImages || showAddReviews){
128+
divImage.current.scrollIntoView({behavior:'smooth'})
129+
}
130+
},[selectedImages,showAddReviews])
131+
132+
useEffect(()=>{
133+
getReviews();
134+
},[paginationModel,debounceSearch,ordering])
135+
136+
return (
137+
<Box component={"div"} sx={{width:'100%'}}>
138+
<Box display={"flex"} justifyContent={"space-between"}>
139+
<Typography variant="h5" mb={2}>Product Reviews </Typography>
140+
<Button startIcon={<AddCircle/>} variant="contained" onClick={()=>{ setShowAddReviews(true);setShowImages(false) }}>Add Reviews</Button>
141+
</Box>
142+
<Grid container spacing={2}>
143+
<Grid item xs={12} sm={(showImages || showAddReviews)?8:12} lg={(showImages || showAddReviews)?9:12}>
144+
<TextField label="Search" variant="outlined" fullWidth onChange={(e)=>setSearchQuery(e.target.value)} margin="normal"/>
145+
<DataGrid
146+
rows={data}
147+
columns={columns}
148+
rowHeight={75}
149+
autoHeight={true}
150+
sortingOrder={['asc','desc']}
151+
sortModel={ordering}
152+
onSortModelChange={handleSorting}
153+
paginationMode="server"
154+
initialState={{
155+
...data.initialState,
156+
pagination:{paginationModel:paginationModel}
157+
}}
158+
pageSizeOptions={[5,10,20]}
159+
pagination
160+
rowCount={totalItems}
161+
loading={loading}
162+
rowSelection={false}
163+
onPaginationModelChange={(pagedetails)=>{
164+
setPaginationModel({
165+
page:pagedetails.page,
166+
pageSize:pagedetails.pageSize
167+
168+
})
169+
}}
170+
slots={
171+
{
172+
173+
loadingOverlay:LinearProgress,
174+
toolbar:GridToolbar,
175+
}
176+
}
177+
178+
/>
179+
</Grid>
180+
{showImages && <Grid item xs={12} sm={4} lg={3} sx={{height:'600px',overflowY:'auto'}} ref={divImage}>
181+
<Box m={2} display={"flex"} justifyContent={"space-between"}>
182+
<Typography variant="h6">Review Images</Typography>
183+
<IconButton onClick={()=>setShowImages(false)}><Close/></IconButton>
184+
</Box>
185+
<Divider/>
186+
{
187+
selectedImages.length>0 && selectedImages.map((image,index)=>(
188+
<Box key={index} display="flex" justifyContent="center" alignItems="center" p={1}>
189+
<Image src={image} style={{width:'100%'}} />
190+
</Box>
191+
))
192+
}
193+
</Grid>}
194+
{showAddReviews && <Grid item xs={12} sm={4} lg={3} sx={{height:'600px',overflowY:'auto'}} ref={divImage}>
195+
<Box m={2} display={"flex"} justifyContent={"space-between"}>
196+
<Typography variant="h6">Add Reviews</Typography>
197+
<IconButton onClick={()=>setShowAddReviews(false)}><Close/></IconButton>
198+
</Box>
199+
<Divider/>
200+
<FormProvider {...methods}>
201+
<form onSubmit={methods.handleSubmit(onSubmitAddReview)}>
202+
<TextField label="Review" sx={{marginBottom:'15px'}} variant="outlined" fullWidth margin="normal" {...register('reviews',{required:true})} error={!!errors['reviews']} helperText={!!error['reviews'] && 'This Field is Required'}/>
203+
<Controller
204+
name="rating"
205+
control={control}
206+
required={true}
207+
defaultValue={0}
208+
render={({field})=>(
209+
<Rating {...field} name="rating" sx={{marginBottom:'15px'}} defaultValue={0} precision={1}/>
210+
)}
211+
/>
212+
{
213+
!!errors['rating'] && <Typography variant="caption" color="error">This Field is Required</Typography>
214+
}
215+
<Autocomplete
216+
mb={2}
217+
{...register('review_user_id',{required:true})}
218+
options={userList}
219+
getOptionLabel={(option)=>option.value}
220+
defaultValue={userList.find(option=>option.id===watch('review_user_id')) || null}
221+
onChange={(event,newValue)=>{
222+
setValue("review_user_id",newValue?newValue.id:'')
223+
}}
224+
renderInput={(params)=>(
225+
<TextField
226+
{...params}
227+
sx={{marginBottom:'15px'}}
228+
label={"Review User"}
229+
variant='outlined'
230+
error={!!errors["review_user_id"]}
231+
helperText={!!errors["review_user_id"] && 'This Field is Required'}
232+
/>
233+
)}
234+
/>
235+
<FileInputComponent field={{name:"review_images",required:true,label:'Review Images'}}/>
236+
<Button sx={{marginBottom:'15px'}} variant="contained" type="submit" fullWidth startIcon={<SaveAltRounded/>}>Add Review</Button>
237+
238+
</form>
239+
</FormProvider>
240+
<Divider/>
241+
</Grid>}
242+
</Grid>
243+
</Box>
244+
)
245+
}
246+
247+
export default ManageReviews

‎Frontend/ecommerce_inventory/src/utils/Helper.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,14 @@ export const isValidUrl=(url)=>{
4545
try{
4646
if(Array.isArray(url)){
4747
let image=url.filter((item)=>item.match(/\.(jpeg|jpg|gif|png)$/)!=null);
48-
new URL(image[0]);
48+
if(image.length>0){
49+
new URL(image[0]);
50+
}
51+
else{
52+
if(url.length>0){
53+
new URL(url[0]);
54+
}
55+
}
4956
}
5057
else if(checkIsJson(url) && JSON.parse(url).length>0){
5158
let image=JSON.parse(url).filter((item)=>item.match(/\.(jpeg|jpg|gif|png)$/)!=null);
@@ -64,7 +71,17 @@ export const isValidUrl=(url)=>{
6471
export const getImageUrl=(url)=>{
6572
if(Array.isArray(url)){
6673
let image=url.filter((item)=>item.match(/\.(jpeg|jpg|gif|png)$/)!=null);
67-
return image[0];
74+
if(image.length>0){
75+
return image[0];
76+
}
77+
else{
78+
if(url.length>0){
79+
return url[0];
80+
}
81+
else{
82+
return url;
83+
}
84+
}
6885
}
6986
else if(checkIsJson(url) && JSON.parse(url).length>0){
7087
let image=JSON.parse(url).filter((item)=>item.match(/\.(jpeg|jpg|gif|png)$/)!=null);

0 commit comments

Comments
 (0)
Please sign in to comment.