diff --git a/backend/eventsync_api/api/serializers/theme_room_serializers.py b/backend/eventsync_api/api/serializers/theme_room_serializers.py new file mode 100644 index 0000000..fd41cbd --- /dev/null +++ b/backend/eventsync_api/api/serializers/theme_room_serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from core.models import ThemeRoom + +class ThemeRoomSerializer(serializers.ModelSerializer): + + class Meta: + model = ThemeRoom + fields = '__all__' \ No newline at end of file diff --git a/backend/eventsync_api/api/urls.py b/backend/eventsync_api/api/urls.py index 1a9cfe0..65bd5d1 100644 --- a/backend/eventsync_api/api/urls.py +++ b/backend/eventsync_api/api/urls.py @@ -8,27 +8,31 @@ from .views.root_view import ApiRootView from .views import locals_view as locv from .views import event_view as evtview +from .views import theme_room_view as themerv from .views import sponsor_view as spview from .views import sponsorship_view as spsview +from .views import theme_room_view as themerv from .views import form_view as formview from .views import question_view as qview from .views import registration_presence_view as rpview urlpatterns = [ - path('', ApiRootView.as_view(), name='api-root'), - path('users/', userv.UserList.as_view(), name='user_list'), - path('users//', userv.UserDetail.as_view(), name='user_detail'), - path('locals/', locv.LocalListView.as_view(), name='local_list'), - path('locals//', locv.LocalDetailView.as_view(), name='local_detail'), - path('events/', evtview.EventListView.as_view(), name='event_list'), - path('events//', evtview.EventDetailView.as_view(), name='event_detail'), - path('sponsors/', spview.SponsorListView.as_view(), name='sponsor_list'), - path('sponsors//', spview.SponsorDetailView.as_view(), - name='sponsor_detail'), - path('sponsorships/', spsview.SponsorShipListView.as_view(), - name='sponsorship_list'), - path('sponsorships//', - spsview.SponsorShipDetailView.as_view(), name='sponsorship_detail'), + path('', ApiRootView.as_view(), name='api-root'), + path('users/', userv.UserList.as_view(), name='user_list'), + path('users//', userv.UserDetail.as_view(), name='user_detail'), + path('locals/', locv.LocalListView.as_view(), name='local_list'), + path('locals//', locv.LocalDetailView.as_view(), name='local_detail'), + path('events/', evtview.EventListView.as_view(), name='event_list'), + path('events//', evtview.EventDetailView.as_view(), name='event_detail'), + path('themeRoom/', themerv.ThemeRoomListView.as_view(), name='theme_room_list'), + path('themeRoom//', themerv.ThemeRoomDetailView.as_view(), name='theme_room_detail'), + path('sponsors/', spview.SponsorListView.as_view(), name='sponsor_list'), + path('sponsors//', spview.SponsorDetailView.as_view(), + name='sponsor_detail'), + path('sponsorships/', spsview.SponsorShipListView.as_view(), + name='sponsorship_list'), + path('sponsorships//', + spsview.SponsorShipDetailView.as_view(), name='sponsorship_detail'), path('forms/', formview.FormsRegisterList.as_view(), name='form_list'), path('forms//', formview.FormsRegisterDetail.as_view(), name='forms_detail'), path('questions/', qview.QuestionList.as_view(), name='question_list'), diff --git a/backend/eventsync_api/api/views/theme_room_view.py b/backend/eventsync_api/api/views/theme_room_view.py new file mode 100644 index 0000000..e8117e8 --- /dev/null +++ b/backend/eventsync_api/api/views/theme_room_view.py @@ -0,0 +1,169 @@ +from core.models import ThemeRoom, Event +from django.http import Http404 +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..permissions import ReadOnly +from ..serializers.theme_room_serializers import ThemeRoomSerializer +from datetime import date, datetime + + +class ThemeRoomListView(APIView): + """ + List all Theme Rooms for a specific Event, or create a new Theme Room for that Event. + """ + permission_classes = [IsAuthenticated | ReadOnly] + + @extend_schema( + responses={200: ThemeRoomSerializer(many=True)}, + parameters=[ + OpenApiParameter(name='event_id', description='Event ID', required=True, type=int), + + OpenApiParameter(name='page', description='Page number', required=False, type=int), + + OpenApiParameter(name='page_size', description='Page size', required=False, type=int), + ], + ) + def get(self, request, format=None): + # Obtenha o ID do evento da query string + event_id = request.query_params.get('event_id') + + if not event_id: + return Response({"detail": "Event ID is required."}, status=status.HTTP_400_BAD_REQUEST) + + # Filtrar as Theme Rooms pelo evento específico + theme_rooms = ThemeRoom.objects.filter(event__id=event_id).order_by("id") + + # Serializar os dados + serializer = ThemeRoomSerializer(theme_rooms, many=True) + + return Response(serializer.data) + + @extend_schema( + request=ThemeRoomSerializer, + + responses={201: ThemeRoomSerializer}, + ) + def post(self, request, format=None): + # Obtenha o ID do evento do corpo da requisição + event_id = request.data.get('event') + + # if not event_id: + # return Response({"detail": "Event ID is required."}, status=status.HTTP_400_BAD_REQUEST) + + try: + # Busque o evento pelo ID + event = Event.objects.get(id=event_id) + + except Event.DoesNotExist: + + return Response({"detail": "Event not found."}, status=status.HTTP_404_NOT_FOUND) + + # Obtenha as datas do evento + # event_start_date = event.start_date + + # event_end_date = event.end_date + + # # Obtenha a data de início e fim da Theme Room + # theme_room_start_date = request.data.get('start_date') + + # theme_room_end_date = request.data.get('end_date') + + # # Valide se as datas da Theme Room estão dentro do intervalo do evento + # if theme_room_start_date and theme_room_end_date: + + # # Converta as datas para objetos date, se necessário + # theme_room_start_date = date.fromisoformat(theme_room_start_date) + + # theme_room_end_date = date.fromisoformat(theme_room_end_date) + + # if theme_room_start_date < event_start_date or theme_room_end_date > event_end_date: + # return Response( + # {"detail": "Theme room dates must be within the event's date range."}, + # status=status.HTTP_400_BAD_REQUEST, + # ) + + # # Serializar e salvar os dados + serializer = ThemeRoomSerializer(data=request.data) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class ThemeRoomDetailView(APIView): + """ + Retrieve, update or delete a Theme Room. + """ + permission_classes = [IsAuthenticated | ReadOnly] + + def get_object(self, pk): + try: + return ThemeRoom.objects.get(pk=pk) + + except ThemeRoom.DoesNotExist: + raise Http404 + + @extend_schema( + responses={200: ThemeRoomSerializer}, + ) + def get(self, request, pk, format=None): + themeRoom = self.get_object(pk) + serializer = ThemeRoomSerializer(themeRoom) + return Response(serializer.data) + + + @extend_schema( + request=ThemeRoomSerializer, + responses={200: ThemeRoomSerializer}, + ) + def patch(self, request, pk, format=None): + themeRoom = self.get_object(pk) + + # Obtenha o evento associado a esta Theme Room + event = themeRoom.event + + # Obtenha as datas do evento + event_start_date = event.start_date + + event_end_date = event.end_date + + # Obtenha a data de início e fim da Theme Room do request + theme_room_start_date = request.data.get('start_date', themeRoom.start_date) + + theme_room_end_date = request.data.get('end_date', themeRoom.end_date) + + # Valide se as novas datas da Theme Room estão dentro do intervalo do evento + if theme_room_start_date and theme_room_end_date: + # Converta as datas para objetos date, se necessário + theme_room_start_date = date.fromisoformat(theme_room_start_date) if isinstance(theme_room_start_date, str) else theme_room_start_date + + theme_room_end_date = date.fromisoformat(theme_room_end_date) if isinstance(theme_room_end_date, str) else theme_room_end_date + + if theme_room_start_date < event_start_date or theme_room_end_date > event_end_date: + return Response( + {"detail": "Theme room dates must be within the event's date range."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Atualize a Theme Room com os dados do request + serializer = ThemeRoomSerializer(themeRoom, data=request.data, partial=True) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @extend_schema( + responses={204: None}, + ) + def delete(self, request, pk, format=None): + themeRoom = self.get_object(pk) + themeRoom.delete() + return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/backend/eventsync_api/core/admin.py b/backend/eventsync_api/core/admin.py index 96d7302..610b308 100644 --- a/backend/eventsync_api/core/admin.py +++ b/backend/eventsync_api/core/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.utils.translation import gettext_lazy as _ -from .models import ESUser, Local, Event, Sponsor, Sponsorship, RegistrationPresence +from .models import ESUser, Local, Event, Sponsor, Sponsorship, RegistrationPresence, ThemeRoom from .forms import CustomUserCreationForm, CustomUserChangeForm @@ -47,13 +47,18 @@ class EventAdmin(admin.ModelAdmin): search_fields = ('name', 'description') date_hierarchy = 'start_date' +@admin.register(ThemeRoom) +class ThemeRoomAdmin(admin.ModelAdmin): + list_display = ('event', 'start_time', 'name', 'start_date', 'end_date', 'local', 'status', 'event_type') + list_filter = ('status', 'event_type', 'local') + search_fields = ('name', 'description',) + date_hierarchy = 'start_date' @admin.register(Sponsor) class SponsorAdmin(admin.ModelAdmin): list_display = ('name', 'phone', 'email') search_fields = ('name', 'phone', 'email') - @admin.register(Sponsorship) class SponsorshipAdmin(admin.ModelAdmin): list_display = ('event', 'sponsor') diff --git a/backend/eventsync_api/core/management/commands/initrooms.py b/backend/eventsync_api/core/management/commands/initrooms.py new file mode 100644 index 0000000..86879eb --- /dev/null +++ b/backend/eventsync_api/core/management/commands/initrooms.py @@ -0,0 +1,41 @@ +import random +from datetime import datetime, timedelta + +from django.core.management.base import BaseCommand +from django.utils import timezone + +from core.models import ThemeRoom, Event + +class Command(BaseCommand): + help = 'Create 5 Local and 20 Event records for testing' + + def handle(self, *args, **kwargs): + # Criar 5 Locais + + # Criar 20 Eventos + event_statuses = ['upcoming', 'ongoing', 'completed', 'cancelled'] + event_types = ['conference', 'workshop', 'seminar', 'meetup'] + + for i in range(20): + start_date = timezone.now().date() + timedelta(days=random.randint(1, 30)) + end_date = start_date + timedelta(days=random.randint(1, 5)) + start_time= timezone.now() + + ThemeRoom.objects.create( + event = Event.objects.get(pk=1), + name=f'Sala Tematica {i+1}', + speaker=f'Palestrante {i+1}', + start_date=start_date, + end_date=end_date, + start_time=start_time, + max_quantity=random.randint(10, 100), + min_quantity=random.randint(1, 10), + hours_quantity=random.randint(1, 8), + description=f'Descrição da Sala {i+1}', + local=f'Bloco A, Sala A34', + audiences='Alunos', + status=random.choice(event_statuses), + event_type=random.choice(event_types) + ) + + self.stdout.write(self.style.SUCCESS('Successfully 20 rooms in your dev database event 1')) \ No newline at end of file diff --git a/backend/eventsync_api/core/migrations/0006_sponsor_formsregister_question_sponsorship_themeroom.py b/backend/eventsync_api/core/migrations/0006_sponsor_formsregister_question_sponsorship_themeroom.py new file mode 100644 index 0000000..5f07a14 --- /dev/null +++ b/backend/eventsync_api/core/migrations/0006_sponsor_formsregister_question_sponsorship_themeroom.py @@ -0,0 +1,100 @@ +# Generated by Django 5.0.4 on 2024-08-18 17:36 + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_event'), + ] + + operations = [ + migrations.CreateModel( + name='Sponsor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('logo', models.ImageField(blank=True, null=True, upload_to='logos/')), + ('phone', models.CharField(max_length=11, validators=[django.core.validators.RegexValidator(message='Phone number must be 11 digits.', regex='^\\d{11}$')])), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), + ('description', models.TextField()), + ], + options={ + 'verbose_name': 'Sponsor', + 'verbose_name_plural': 'Sponsors', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='FormsRegister', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=150)), + ('description', models.TextField()), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.event')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Form', + 'verbose_name_plural': 'Forms', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Question', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ('type', models.CharField(choices=[('Discursiva', 'Discursiva'), ('Múltipla escolha', 'Múltipla escolha'), ('Objetiva', 'Objetiva')], default='Discursiva', max_length=20)), + ('options', models.JSONField(blank=True, default=list)), + ('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.formsregister')), + ], + options={ + 'verbose_name': 'Question', + 'verbose_name_plural': 'Questions', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Sponsorship', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.event')), + ('sponsor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.sponsor')), + ], + options={ + 'verbose_name': 'Sponsor', + 'verbose_name_plural': 'Sponsors', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='ThemeRoom', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_date', models.DateField()), + ('end_date', models.DateField()), + ('start_time', models.TimeField()), + ('speaker', models.CharField(max_length=100)), + ('name', models.CharField(max_length=150)), + ('description', models.TextField()), + ('hours_quantity', models.IntegerField()), + ('audiences', models.TextField()), + ('max_quantity', models.IntegerField()), + ('min_quantity', models.IntegerField()), + ('local', models.TextField()), + ('status', models.CharField(choices=[('upcoming', 'Upcoming'), ('ongoing', 'Ongoing'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='upcoming', max_length=20)), + ('event_type', models.CharField(choices=[('conference', 'Conference'), ('workshop', 'Workshop'), ('seminar', 'Seminar'), ('meetup', 'Meetup')], default='conference', max_length=20)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.event')), + ], + options={ + 'verbose_name': 'Theme Room', + 'verbose_name_plural': 'Theme Rooms', + 'ordering': ['id'], + }, + ), + ] diff --git a/backend/eventsync_api/core/migrations/0006_sponsor_sponsorship.py b/backend/eventsync_api/core/migrations/0006_sponsor_sponsorship.py deleted file mode 100644 index 651e311..0000000 --- a/backend/eventsync_api/core/migrations/0006_sponsor_sponsorship.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 5.0.4 on 2024-07-30 22:19 - -import django.core.validators -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0005_event'), - ] - - operations = [ - migrations.CreateModel( - name='Sponsor', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('logo', models.ImageField(blank=True, null=True, upload_to='logos/')), - ('phone', models.CharField(max_length=11, validators=[django.core.validators.RegexValidator(message='Phone number must be 11 digits.', regex='^\\d{11}$')])), - ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), - ('description', models.TextField()), - ], - options={ - 'verbose_name': 'Sponsor', - 'verbose_name_plural': 'Sponsors', - 'ordering': ['id'], - }, - ), - migrations.CreateModel( - name='Sponsorship', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.event')), - ('sponsor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.sponsor')), - ], - options={ - 'verbose_name': 'Sponsor', - 'verbose_name_plural': 'Sponsors', - 'ordering': ['id'], - }, - ), - ] diff --git a/backend/eventsync_api/core/migrations/0007_formsregister_question.py b/backend/eventsync_api/core/migrations/0007_formsregister_question.py deleted file mode 100644 index 43a18ce..0000000 --- a/backend/eventsync_api/core/migrations/0007_formsregister_question.py +++ /dev/null @@ -1,89 +0,0 @@ -# Generated by Django 5.0.4 on 2024-08-11 17:43 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0006_sponsor_sponsorship"), - ] - - operations = [ - migrations.CreateModel( - name="FormsRegister", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=150)), - ("description", models.TextField()), - ( - "event", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="core.event" - ), - ), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "verbose_name": "Form", - "verbose_name_plural": "Forms", - "ordering": ["id"], - }, - ), - migrations.CreateModel( - name="Question", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("text", models.TextField()), - ( - "type", - models.CharField( - choices=[ - ("Discursiva", "Discursiva"), - ("Múltipla escolha", "Múltipla escolha"), - ("Objetiva", "Objetiva"), - ], - default="Discursiva", - max_length=20, - ), - ), - ("options", models.JSONField(blank=True, default=list)), - ( - "form", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="core.formsregister", - ), - ), - ], - options={ - "verbose_name": "Question", - "verbose_name_plural": "Questions", - "ordering": ["id"], - }, - ), - ] diff --git a/backend/eventsync_api/core/models.py b/backend/eventsync_api/core/models.py index 24e55c6..eba05b5 100644 --- a/backend/eventsync_api/core/models.py +++ b/backend/eventsync_api/core/models.py @@ -3,6 +3,7 @@ from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError from .managers import ESUserManager @@ -99,6 +100,38 @@ def update_status(self): else: self.status = 'completed' +class ThemeRoom(models.Model): + event = models.ForeignKey(Event, on_delete=models.CASCADE) + + start_date = models.DateField() + end_date = models.DateField() + start_time = models.TimeField() + + speaker = models.CharField(max_length=100) + name = models.CharField(max_length=150) + description = models.TextField() + hours_quantity = models.IntegerField() + audiences = models.TextField() + + max_quantity = models.IntegerField() + min_quantity = models.IntegerField() + local = models.TextField() + + status = models.CharField(max_length=20, choices=EVENT_STATUS_CHOICES, default='upcoming') + event_type = models.CharField(max_length=20, choices=EVENT_TYPE_CHOICES, default='conference') + + class Meta: + verbose_name = "Theme Room" + verbose_name_plural = "Theme Rooms" + ordering = ['id'] + + def clean(self): + # Call the parent class's clean method to ensure any inherited validation is executed + super().clean() + + # Ensure start_date and end_date are within the Event's date range + if self.start_date < self.event.start_date or self.end_date > self.event.end_date: + raise ValidationError('The start date and end date of the Theme Room must be within the Event\'s date range.') class Sponsor(models.Model): name = models.CharField(max_length=100) logo = models.ImageField(upload_to='logos/', blank=True, null=True) diff --git a/frontend/eventsync_front/package-lock.json b/frontend/eventsync_front/package-lock.json index db18704..6371636 100644 --- a/frontend/eventsync_front/package-lock.json +++ b/frontend/eventsync_front/package-lock.json @@ -354,108 +354,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -754,160 +652,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz", - "integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz", - "integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz", - "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz", - "integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz", - "integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz", - "integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz", - "integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz", - "integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz", - "integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz", - "integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz", - "integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz", @@ -936,48 +680,6 @@ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz", - "integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz", - "integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz", - "integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@rushstack/eslint-patch": { "version": "1.10.3", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", @@ -1619,9 +1321,9 @@ "license": "MIT" }, "node_modules/@vuepic/vue-datepicker": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-9.0.0.tgz", - "integrity": "sha512-8ChUOw6F5Sk4iPzu30OOeEYjalSnnOEJKbq9ZehJdkpCEz1s5GFXuxc2A/JsMMdxW9L0Ek9H9MZbJDNTV5RfFA==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-9.0.1.tgz", + "integrity": "sha512-5sSdwib5cY8cE4Y7SCh+Zemfp+U/m6BDcgaPwd5Vmdv5LAASyV0wugn9sTb6NWX0sIQEdrGDl/RmD9EjcIke3A==", "license": "MIT", "dependencies": { "date-fns": "^3.6.0" @@ -1780,9 +1482,9 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2800,21 +2502,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", diff --git a/frontend/eventsync_front/src/components/EventForm.vue b/frontend/eventsync_front/src/components/EventForm.vue index 9c4e96a..f040f74 100644 --- a/frontend/eventsync_front/src/components/EventForm.vue +++ b/frontend/eventsync_front/src/components/EventForm.vue @@ -244,4 +244,4 @@ const submitForm = async () => { margin-left: 8px; } - \ No newline at end of file + diff --git a/frontend/eventsync_front/src/components/Home.vue b/frontend/eventsync_front/src/components/Home.vue index 21d655c..25c254e 100644 --- a/frontend/eventsync_front/src/components/Home.vue +++ b/frontend/eventsync_front/src/components/Home.vue @@ -5,6 +5,7 @@ + Ver mais Eventos diff --git a/frontend/eventsync_front/src/components/ThemeRoom/CreateRooms.vue b/frontend/eventsync_front/src/components/ThemeRoom/CreateRooms.vue new file mode 100644 index 0000000..6f1b1c8 --- /dev/null +++ b/frontend/eventsync_front/src/components/ThemeRoom/CreateRooms.vue @@ -0,0 +1,225 @@ + + + + + + + diff --git a/frontend/eventsync_front/src/components/ThemeRoom/ListRooms.vue b/frontend/eventsync_front/src/components/ThemeRoom/ListRooms.vue new file mode 100644 index 0000000..0f1927b --- /dev/null +++ b/frontend/eventsync_front/src/components/ThemeRoom/ListRooms.vue @@ -0,0 +1,194 @@ + + + + + \ No newline at end of file diff --git a/frontend/eventsync_front/src/router/index.ts b/frontend/eventsync_front/src/router/index.ts index 178fa72..106e9b2 100644 --- a/frontend/eventsync_front/src/router/index.ts +++ b/frontend/eventsync_front/src/router/index.ts @@ -1,5 +1,6 @@ import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' + import SponsorsView from '@/views/SponsorsView.vue' import EventListView from '@/views/EventListView.vue' import LoginView from '@/views/LoginView.vue' @@ -46,6 +47,21 @@ const router = createRouter({ // para views que sejam privadas usar esse exemplo // meta: { requiresAuth: true } }, + { + path: '/events/:id/create/theme-room', + name: 'create-theme-room', + component: () => import('../views/ThemeRoomViews/CreateTrView.vue') + }, + { + path: '/events/:id/list/theme-room', + name: 'list-theme-room', + component: () => import('../views/ThemeRoomViews/ListTrView.vue') + }, + { + path: '/events/:id/edit/theme-room/:room_id', + name: 'edit-theme-room', + component: () => import('../views/ThemeRoomViews/EditTrView.vue') + }, { path: '/events', name: 'events', diff --git a/frontend/eventsync_front/src/services/themRoomService.ts b/frontend/eventsync_front/src/services/themRoomService.ts new file mode 100644 index 0000000..7697e70 --- /dev/null +++ b/frontend/eventsync_front/src/services/themRoomService.ts @@ -0,0 +1,33 @@ +import api from '@/services/api' +import { useAuthStore } from '@/stores/auth' + +export const fetchThemeRooms = (eventId: number) => { + return api.get('themeRoom/', { + params: { event_id: eventId } + }) +} + +export const fetchThemeRoom = (id: number) => { + return api.get(`themeRoom/${id}/`) +} + +export const addThemeRoom = (formData: FormData) => { + const { token } = useAuthStore() + return api.post('themeRoom/', formData, { + headers: { Authorization: `Bearer ${token}` } + }) +} + +export const updateThemeRoom = (id: number, formData: FormData) => { + const { token } = useAuthStore() + return api.patch(`themeRoom/${id}/`, formData, { + headers: { Authorization: `Bearer ${token}` } + }) +} + +export const deleteThemeRoom = (id: number) => { + const { token } = useAuthStore() + return api.delete(`themeRoom/${id}/`, { + headers: { Authorization: `Bearer ${token}` } + }) +} diff --git a/frontend/eventsync_front/src/stores/validatorRoom.ts b/frontend/eventsync_front/src/stores/validatorRoom.ts new file mode 100644 index 0000000..306f67b --- /dev/null +++ b/frontend/eventsync_front/src/stores/validatorRoom.ts @@ -0,0 +1,55 @@ +import type { ThemeRoom } from '@/types/themeRoom' +import { ref } from 'vue' + + +export const snackbar = ref(false) +export const snackbarText = ref('') +export const snackbarColor = ref('') + +const showSnackbar = (message: string, type: string) => { + snackbarText.value = message + snackbarColor.value = type === 'success' ? 'green' : 'red' + snackbar.value = true +} + +export const validateFields = (formValues: ThemeRoom): boolean => { + const { name, start_time, start_date, end_date, local, min_quantity, max_quantity, hours_quantity, event_type, description, status } = formValues + + const fields = [ + { value: name, message: 'Nome da Sala Temática é obrigatório' }, + { value: start_time, message: 'Horário da Sala Temática é obrigatório' }, + { value: start_date, message: 'Data de Início da Sala Temática é obrigatória' }, + { value: end_date, message: 'Data de Fim da Sala Temática é obrigatória' }, + { value: local, message: 'Local da Sala Temática é obrigatório' }, + { value: min_quantity && min_quantity > 0, message: 'Quantidade Mínima de Participantes é obrigatória e deve ser maior que 0' }, + { value: max_quantity && max_quantity > 0, message: 'Quantidade Máxima de Participantes é obrigatória e deve ser maior que 0' }, + { value: hours_quantity && hours_quantity > 0, message: 'Quantidade de Horas é obrigatória e deve ser maior que 0' }, + { value: event_type, message: 'Tipo da Sala Temática é obrigatório' }, + { value: description, message: 'Descrição da Sala Temática é obrigatória' }, + ] + + + for (const field of fields) { + if (!field.value) { + showSnackbar(field.message, 'error') + return false + } + } + + + const startDate = new Date(start_date) + const endDate = new Date(end_date) + if (startDate > endDate) { + showSnackbar('A Data de Início da Sala Temática deve ser anterior à Data de Fim', 'error') + return false + } + + return true +} + +export const validateNumberInput = (event: KeyboardEvent) => { + const key = event.key + if (!/[0-9]/.test(key)) { + event.preventDefault() + } +} diff --git a/frontend/eventsync_front/src/tests/ThemeRoom.test.ts b/frontend/eventsync_front/src/tests/ThemeRoom.test.ts new file mode 100644 index 0000000..23a93dd --- /dev/null +++ b/frontend/eventsync_front/src/tests/ThemeRoom.test.ts @@ -0,0 +1,50 @@ +import { mount } from "@vue/test-utils"; +import { describe, it, expect } from "vitest"; +import ThemeRoom from "@/components/ThemeRoom/CreateRooms.vue"; + +describe("ThemeRoom.vue", () => { + const wrapper = mount(ThemeRoom); + + it("renders correctly", () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + it('contains the event title', () => { + const title = wrapper.find('h1'); + expect(title.exists()).toBe(true); + expect(title.text()).toBe('Evento: Nome do Evento'); + }); + + it('contains the form title', () => { + const formTitle = wrapper.find('h1.text-center'); + expect(formTitle.exists()).toBe(true); + expect(formTitle.text()).toBe('Criar sala temática.'); + }); + + it('contains all form fields', () => { + const labels = [ + 'Local da sala', + 'Nome da sala', + 'Descrição', + 'Público Alvo', + 'Data', + 'Horário', + 'Número Min. de Participantes', + 'Número Máx. de Participantes' + ]; + + labels.forEach(label => { + expect(wrapper.find(`input[label="${label}"]`).exists()) + }); + }); + + it('contains create and back buttons', () => { + const createButton = wrapper.find('v-btn[color="primary"]'); + const backButton = wrapper.find('v-btn[color="secondary"]'); + + expect(createButton.exists()).toBe(true); + expect(createButton.text()).toBe('Criar'); + + expect(backButton.exists()).toBe(true); + expect(backButton.text()).toBe('Voltar'); + }); +}) \ No newline at end of file diff --git a/frontend/eventsync_front/src/tests/__snapshots__/Home.test.ts.snap b/frontend/eventsync_front/src/tests/__snapshots__/Home.test.ts.snap index fae0b7f..4ffcf02 100644 --- a/frontend/eventsync_front/src/tests/__snapshots__/Home.test.ts.snap +++ b/frontend/eventsync_front/src/tests/__snapshots__/Home.test.ts.snap @@ -169,6 +169,7 @@ exports[`Home.vue > renders correctly 1`] = ` + Ver mais Eventos diff --git a/frontend/eventsync_front/src/tests/__snapshots__/ThemeRoom.test.ts.snap b/frontend/eventsync_front/src/tests/__snapshots__/ThemeRoom.test.ts.snap new file mode 100644 index 0000000..118f4ef --- /dev/null +++ b/frontend/eventsync_front/src/tests/__snapshots__/ThemeRoom.test.ts.snap @@ -0,0 +1,24 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ThemeRoom.vue > renders correctly 1`] = ` +" + +

Evento: Nome do Evento

+

Criar sala temática.

+ + + + + + + + + + + Criar + Voltar + + +
+
" +`; diff --git a/frontend/eventsync_front/src/types/themeRoom.ts b/frontend/eventsync_front/src/types/themeRoom.ts new file mode 100644 index 0000000..1b54dbb --- /dev/null +++ b/frontend/eventsync_front/src/types/themeRoom.ts @@ -0,0 +1,17 @@ +export interface ThemeRoom { + id?: number + event?: number, + start_time: string, + name: string, + speaker: string, + start_date: string, + end_date: string, + max_quantity: number, + min_quantity: number, + hours_quantity: number, + description: string + local: number, + status: string, + event_type: string, + audiences: string, +} \ No newline at end of file diff --git a/frontend/eventsync_front/src/views/CreateForm.vue b/frontend/eventsync_front/src/views/CreateForm.vue index a73c400..afbe26a 100644 --- a/frontend/eventsync_front/src/views/CreateForm.vue +++ b/frontend/eventsync_front/src/views/CreateForm.vue @@ -17,4 +17,3 @@ import EventForm from '@/components/EventForm.vue'; } } - \ No newline at end of file diff --git a/frontend/eventsync_front/src/views/EventDetailView.vue b/frontend/eventsync_front/src/views/EventDetailView.vue index 4757676..ebfd415 100644 --- a/frontend/eventsync_front/src/views/EventDetailView.vue +++ b/frontend/eventsync_front/src/views/EventDetailView.vue @@ -6,6 +6,7 @@
+ @@ -15,6 +16,7 @@ import EventDetail from '@/components/events/EventDetail.vue' import FooterVue from '../components/Footer.vue' import NavBar from '../components/NavBar.vue' +import ListRooms from '@/components/ThemeRoom/ListRooms.vue'; + + \ No newline at end of file diff --git a/frontend/eventsync_front/src/views/ThemeRoomViews/EditTrView.vue b/frontend/eventsync_front/src/views/ThemeRoomViews/EditTrView.vue new file mode 100644 index 0000000..f299fce --- /dev/null +++ b/frontend/eventsync_front/src/views/ThemeRoomViews/EditTrView.vue @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/frontend/eventsync_front/src/views/ThemeRoomViews/ListTrView.vue b/frontend/eventsync_front/src/views/ThemeRoomViews/ListTrView.vue new file mode 100644 index 0000000..276e0c7 --- /dev/null +++ b/frontend/eventsync_front/src/views/ThemeRoomViews/ListTrView.vue @@ -0,0 +1,13 @@ + + + \ No newline at end of file