From 1751badb906d3bb61f21f7c447bf91e4fc69c552 Mon Sep 17 00:00:00 2001 From: David Bruno Vontor Date: Sun, 14 Dec 2025 03:49:16 +0100 Subject: [PATCH] Refactor frontend components and backend migrations - Removed TradingGraph component from frontend/src/components/trading. - Updated home page to import Services component and TradingGraph from new path. - Modified PortfolioPage to return null instead of PortfolioGrid. - Added initial migrations for account, advertisement, commerce, configuration, downloader, gopay, stripe, trading212, and zasilkovna apps in the backend. - Created Services component with subcomponents for Kinematografie, Drone Service, and Website Service. - Implemented TradingGraph component with dynamic data generation and canvas rendering. - Updated DonationShop component to display donation tiers with icons and descriptions. --- backend/account/migrations/0001_initial.py | 61 +++++++ backend/account/migrations/__init__.py | 0 backend/account/urls.py | 1 - backend/account/views.py | 44 ++--- .../advertisement/migrations/0001_initial.py | 23 +++ backend/advertisement/migrations/__init__.py | 0 backend/commerce/admin.py | 82 ++++++++- backend/commerce/migrations/0001_initial.py | 162 ++++++++++++++++++ backend/commerce/migrations/__init__.py | 0 backend/commerce/models.py | 16 +- backend/commerce/serializers.py | 4 +- backend/commerce/tasks.py | 18 +- .../configuration/migrations/0001_initial.py | 43 +++++ backend/social/chat/migrations/__init__.py | 0 .../downloader/migrations/0001_initial.py | 30 ++++ .../downloader/migrations/__init__.py | 0 .../gopay/migrations/0001_initial.py | 63 +++++++ .../thirdparty/gopay/migrations/__init__.py | 0 .../stripe/migrations/0001_initial.py | 26 +++ .../thirdparty/stripe/migrations/__init__.py | 0 backend/thirdparty/stripe/urls.py | 4 +- backend/thirdparty/stripe/views.py | 8 +- .../trading212/migrations/__init__.py | 0 .../zasilkovna/migrations/0001_initial.py | 39 +++++ .../zasilkovna/migrations/__init__.py | 0 backend/thirdparty/zasilkovna/models.py | 2 +- backend/thirdparty/zasilkovna/serializers.py | 5 +- backend/thirdparty/zasilkovna/views.py | 2 +- backend/vontor_cz/settings.py | 2 + .../components/portfolio/PortfolioGrid.tsx | 79 --------- .../src/components/trading/TradingGraph.tsx | 15 -- .../components/Services/Services.module.css | 0 .../home/components/Services/Services.tsx | 18 ++ .../home/components/Services/TradingGraph.tsx | 157 +++++++++++++++++ .../home/components/Services/droneServis.tsx | 10 ++ .../components/Services/kinematografie.tsx | 9 + .../pages/home/components/Services/webs.tsx | 28 +++ .../home}/components/donate/DonationShop.tsx | 0 frontend/src/pages/home/home.tsx | 7 +- .../src/pages/portfolio/PortfolioPage.tsx | 3 +- 40 files changed, 796 insertions(+), 165 deletions(-) create mode 100644 backend/account/migrations/0001_initial.py create mode 100644 backend/account/migrations/__init__.py create mode 100644 backend/advertisement/migrations/0001_initial.py create mode 100644 backend/advertisement/migrations/__init__.py create mode 100644 backend/commerce/migrations/0001_initial.py create mode 100644 backend/commerce/migrations/__init__.py create mode 100644 backend/configuration/migrations/0001_initial.py create mode 100644 backend/social/chat/migrations/__init__.py create mode 100644 backend/thirdparty/downloader/migrations/0001_initial.py create mode 100644 backend/thirdparty/downloader/migrations/__init__.py create mode 100644 backend/thirdparty/gopay/migrations/0001_initial.py create mode 100644 backend/thirdparty/gopay/migrations/__init__.py create mode 100644 backend/thirdparty/stripe/migrations/0001_initial.py create mode 100644 backend/thirdparty/stripe/migrations/__init__.py create mode 100644 backend/thirdparty/trading212/migrations/__init__.py create mode 100644 backend/thirdparty/zasilkovna/migrations/0001_initial.py create mode 100644 backend/thirdparty/zasilkovna/migrations/__init__.py delete mode 100644 frontend/src/components/portfolio/PortfolioGrid.tsx delete mode 100644 frontend/src/components/trading/TradingGraph.tsx create mode 100644 frontend/src/pages/home/components/Services/Services.module.css create mode 100644 frontend/src/pages/home/components/Services/Services.tsx create mode 100644 frontend/src/pages/home/components/Services/TradingGraph.tsx create mode 100644 frontend/src/pages/home/components/Services/droneServis.tsx create mode 100644 frontend/src/pages/home/components/Services/kinematografie.tsx create mode 100644 frontend/src/pages/home/components/Services/webs.tsx rename frontend/src/{ => pages/home}/components/donate/DonationShop.tsx (100%) diff --git a/backend/account/migrations/0001_initial.py b/backend/account/migrations/0001_initial.py new file mode 100644 index 0000000..c95b1fb --- /dev/null +++ b/backend/account/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# Generated by Django 5.2.9 on 2025-12-14 02:23 + +import account.models +import django.contrib.auth.validators +import django.core.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='CustomUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('is_deleted', models.BooleanField(default=False)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('role', models.CharField(choices=[('admin', 'cz#Administrátor'), ('mod', 'cz#Moderator'), ('regular', 'cz#Regular')], default='regular', max_length=20)), + ('phone_number', models.CharField(blank=True, max_length=16, null=True, unique=True, validators=[django.core.validators.RegexValidator('^\\+?\\d{9,15}$', message='Zadejte platné telefonní číslo.')])), + ('email_verified', models.BooleanField(default=False)), + ('email', models.EmailField(db_index=True, max_length=254, unique=True)), + ('email_verification_token', models.CharField(blank=True, db_index=True, max_length=128, null=True)), + ('email_verification_sent_at', models.DateTimeField(blank=True, null=True)), + ('gdpr', models.BooleanField(default=False)), + ('is_active', models.BooleanField(default=False)), + ('create_time', models.DateTimeField(auto_now_add=True)), + ('city', models.CharField(blank=True, max_length=100, null=True)), + ('street', models.CharField(blank=True, max_length=200, null=True)), + ('street_number', models.PositiveIntegerField(blank=True, null=True)), + ('country', models.CharField(blank=True, max_length=100, null=True)), + ('company_name', models.CharField(blank=True, max_length=255)), + ('ico', models.CharField(blank=True, max_length=20)), + ('dic', models.CharField(blank=True, max_length=20)), + ('postal_code', models.CharField(blank=True, max_length=5, null=True, validators=[django.core.validators.RegexValidator(code='invalid_postal_code', message='Postal code must contain exactly 5 digits.', regex='^\\d{5}$')])), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='customuser_set', related_query_name='customuser', to='auth.group')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='customuser_set', related_query_name='customuser', to='auth.permission')), + ], + options={ + 'abstract': False, + }, + managers=[ + ('objects', account.models.CustomUserManager()), + ('active', account.models.ActiveUserManager()), + ], + ), + ] diff --git a/backend/account/migrations/__init__.py b/backend/account/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/account/urls.py b/backend/account/urls.py index f43e03c..0237a93 100644 --- a/backend/account/urls.py +++ b/backend/account/urls.py @@ -16,7 +16,6 @@ urlpatterns = [ # Registration & email endpoints path('register/', views.UserRegistrationViewSet.as_view({'post': 'create'}), name='register'), path('verify-email///', views.EmailVerificationView.as_view(), name='verify-email'), - path('activate/', views.UserActivationViewSet.as_view(), name='activate-user'), # Password reset endpoints path('password-reset/', views.PasswordResetRequestView.as_view(), name='password-reset-request'), diff --git a/backend/account/views.py b/backend/account/views.py index 600e948..353ca1d 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -6,7 +6,7 @@ from .serializers import * from .permissions import * from .models import CustomUser from .tokens import * -from .tasks import send_password_reset_email_task, send_email_verification_task, send_email_clerk_accepted_task # FIXME: send_email_clerk_accepted_task neexistuje !!! +from .tasks import send_password_reset_email_task, send_email_verification_task from django.conf import settings import logging logger = logging.getLogger(__name__) @@ -186,7 +186,6 @@ class LogoutView(APIView): @extend_schema( tags=["User"], - get=extend_schema(tags=["public"]), summary="List, retrieve, update, and delete users.", description="Displays all users with filtering and ordering options. Requires authentication and appropriate role.", responses={ @@ -223,6 +222,15 @@ class UserView(viewsets.ModelViewSet): "is_active": {"help_text": "Stav aktivace uživatele."}, } + @extend_schema( + tags=["User"], + summary="Get permissions based on user role and action.", + description="Determines permissions for various actions based on user role and ownership.", + responses={ + 200: OpenApiResponse(description="Permissions determined successfully."), + 403: OpenApiResponse(description="Permission denied."), + }, + ) def get_permissions(self): # Only admin can list or create users if self.action in ['list', 'create']: @@ -243,6 +251,7 @@ class UserView(viewsets.ModelViewSet): return [OnlyRolesAllowed("admin")()] # Any authenticated user can retrieve (view) any user's profile + #FIXME: popřemýšlet co vše může získat elif self.action == 'retrieve': return [IsAuthenticated()] @@ -326,40 +335,13 @@ class EmailVerificationView(APIView): if account_activation_token.check_token(user, token): user.email_verified = True + user.is_active = True # Aktivace uživatele po ověření e-mailu user.save() - return Response({"detail": "E-mail byl úspěšně ověřen. Účet čeká na schválení."}) + return Response({"detail": "E-mail byl úspěšně ověřen. Účet je aktivován."}) else: return Response({"error": "Token je neplatný nebo expirovaný."}, status=400) -#3. seller activation API (var_symbol) -@extend_schema( - tags=["User Registration"], - summary="Activate user and set variable symbol (admin/cityClerk only)", - description="Activate user and set variable symbol. Only accessible by admin or cityClerk.", - request=UserActivationSerializer, - responses={ - 200: OpenApiResponse(response=UserActivationSerializer, description="User activated successfully."), - 400: OpenApiResponse(description="Invalid activation data."), - 404: OpenApiResponse(description="User not found."), - }, -) -class UserActivationViewSet(APIView): - permission_classes = [OnlyRolesAllowed('cityClerk', 'admin')] - - def patch(self, request, *args, **kwargs): - serializer = UserActivationSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = serializer.save() - - try: - send_email_clerk_accepted_task.delay(user.id) # posílaní emailu pro informování uživatele o dokončení registrace, uředník doplnil variabilní symbol - CELERY TASK - except Exception as e: - logger.error(f"Celery not available, using fallback. Error: {e}") - send_email_clerk_accepted_task(user.id) # posílaní emailu pro informování uživatele o dokončení registrace, uředník doplnil variabilní symbol - - return Response(serializer.to_representation(user), status=status.HTTP_200_OK) - #-------------------------------------------------END REGISTRACE------------------------------------------------------------- #1. PasswordReset + send Email diff --git a/backend/advertisement/migrations/0001_initial.py b/backend/advertisement/migrations/0001_initial.py new file mode 100644 index 0000000..b2360dc --- /dev/null +++ b/backend/advertisement/migrations/0001_initial.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.9 on 2025-12-14 02:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ContactMe', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('client_email', models.EmailField(max_length=254)), + ('content', models.TextField()), + ('sent_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/backend/advertisement/migrations/__init__.py b/backend/advertisement/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/commerce/admin.py b/backend/commerce/admin.py index 8093833..df93c51 100644 --- a/backend/commerce/admin.py +++ b/backend/commerce/admin.py @@ -1,14 +1,82 @@ from django.contrib import admin -from .models import Carrier, Product -# Register your models here. +from .models import ( + Category, Product, ProductImage, Order, OrderItem, + Carrier, Payment, DiscountCode, Refund, Invoice +) -@admin.register(Carrier) -class CarrierAdmin(admin.ModelAdmin): - list_display = ("name", "base_price", "is_active") +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ("name", "url", "parent") + search_fields = ("name", "description") + prepopulated_fields = {"url": ("name",)} @admin.register(Product) class ProductAdmin(admin.ModelAdmin): - list_display = ("name", "price", "currency", "stock", "is_active") - search_fields = ("name", "description") + list_display = ("name", "price", "stock", "is_active", "category", "created_at") + search_fields = ("name", "description", "code") + list_filter = ("is_active", "category", "created_at") + prepopulated_fields = {"url": ("name",)} + + +@admin.register(ProductImage) +class ProductImageAdmin(admin.ModelAdmin): + list_display = ("product", "is_main", "alt_text") + list_filter = ("is_main",) + search_fields = ("product__name", "alt_text") + + +class OrderItemInline(admin.TabularInline): + model = OrderItem + extra = 0 + readonly_fields = ("product", "quantity") + + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ("id", "user", "email", "status", "total_price", "currency", "created_at") + list_filter = ("status", "created_at", "country") + search_fields = ("email", "first_name", "last_name", "phone") + readonly_fields = ("created_at", "updated_at", "total_price") + inlines = [OrderItemInline] + + +@admin.register(OrderItem) +class OrderItemAdmin(admin.ModelAdmin): + list_display = ("order", "product", "quantity") + search_fields = ("order__id", "product__name") + + +@admin.register(Carrier) +class CarrierAdmin(admin.ModelAdmin): + list_display = ("id", "shipping_method", "state", "shipping_price", "weight") + list_filter = ("shipping_method", "state", "returning") + search_fields = ("id",) + + +@admin.register(Payment) +class PaymentAdmin(admin.ModelAdmin): + list_display = ("id", "payment_method", "created_at") + list_filter = ("payment_method", "created_at") + + +@admin.register(DiscountCode) +class DiscountCodeAdmin(admin.ModelAdmin): + list_display = ("code", "percent", "amount", "active", "valid_from", "valid_to", "used_count", "usage_limit") + list_filter = ("active", "valid_from", "valid_to") + search_fields = ("code", "description") + + +@admin.register(Refund) +class RefundAdmin(admin.ModelAdmin): + list_display = ("order", "reason_choice", "verified", "created_at") + list_filter = ("verified", "reason_choice", "created_at") + search_fields = ("order__id", "order__email", "reason_text") + + +@admin.register(Invoice) +class InvoiceAdmin(admin.ModelAdmin): + list_display = ("invoice_number", "issued_at", "due_date") + search_fields = ("invoice_number",) + readonly_fields = ("issued_at",) diff --git a/backend/commerce/migrations/0001_initial.py b/backend/commerce/migrations/0001_initial.py new file mode 100644 index 0000000..d435ad9 --- /dev/null +++ b/backend/commerce/migrations/0001_initial.py @@ -0,0 +1,162 @@ +# Generated by Django 5.2.9 on 2025-12-14 02:23 + +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('stripe', '0001_initial'), + ('zasilkovna', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Invoice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('invoice_number', models.CharField(max_length=50, unique=True)), + ('issued_at', models.DateTimeField(auto_now_add=True)), + ('due_date', models.DateTimeField()), + ('pdf_file', models.FileField(upload_to='invoices/')), + ], + ), + migrations.CreateModel( + name='Carrier', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('shipping_method', models.CharField(choices=[('packeta', 'cz#Zásilkovna'), ('store', 'cz#Osobní odběr')], default='store', max_length=20)), + ('state', models.CharField(choices=[('ordered', 'cz#Objednávka se připravuje'), ('shipped', 'cz#Odesláno'), ('delivered', 'cz#Doručeno'), ('ready_to_pickup', 'cz#Připraveno k vyzvednutí')], default='ordered', max_length=20)), + ('weight', models.DecimalField(blank=True, decimal_places=2, help_text='Hmotnost zásilky v kg', max_digits=10, null=True)), + ('returning', models.BooleanField(default=False, help_text='Zda je tato zásilka na vrácení')), + ('shipping_price', models.DecimalField(decimal_places=2, default=0, max_digits=10)), + ('zasilkovna', models.ManyToManyField(blank=True, related_name='carriers', to='zasilkovna.zasilkovnapacket')), + ], + ), + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('url', models.SlugField(unique=True)), + ('description', models.TextField(blank=True)), + ('image', models.ImageField(blank=True, upload_to='categories/')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subcategories', to='commerce.category')), + ], + options={ + 'verbose_name_plural': 'Categories', + }, + ), + migrations.CreateModel( + name='DiscountCode', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50, unique=True)), + ('description', models.CharField(blank=True, max_length=255)), + ('percent', models.PositiveIntegerField(blank=True, help_text='Procento sleva 0-100', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)])), + ('amount', models.DecimalField(blank=True, decimal_places=2, help_text='Fixní sleva v CZK', max_digits=10, null=True)), + ('valid_from', models.DateTimeField(default=django.utils.timezone.now)), + ('valid_to', models.DateTimeField(blank=True, null=True)), + ('active', models.BooleanField(default=True)), + ('usage_limit', models.PositiveIntegerField(blank=True, null=True)), + ('used_count', models.PositiveIntegerField(default=0)), + ('specific_categories', models.ManyToManyField(blank=True, related_name='discount_codes', to='commerce.category')), + ], + ), + migrations.CreateModel( + name='Payment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('payment_method', models.CharField(choices=[('Site', 'cz#Platba v obchodě'), ('stripe', 'cz#Bankovní převod'), ('cash_on_delivery', 'cz#Dobírka')], default='Site', max_length=30)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('stripe', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='payment', to='stripe.stripemodel')), + ], + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(blank=True, choices=[('created', 'cz#Vytvořeno'), ('cancelled', 'cz#Zrušeno'), ('completed', 'cz#Dokončeno'), ('refunding', 'cz#Vrácení v procesu'), ('refunded', 'cz#Vráceno')], default='created', max_length=20, null=True)), + ('total_price', models.DecimalField(decimal_places=2, default=0, max_digits=10)), + ('currency', models.CharField(default='CZK', max_length=10)), + ('first_name', models.CharField(max_length=100)), + ('last_name', models.CharField(max_length=100)), + ('email', models.EmailField(max_length=254)), + ('phone', models.CharField(blank=True, max_length=20)), + ('address', models.CharField(max_length=255)), + ('city', models.CharField(max_length=100)), + ('postal_code', models.CharField(max_length=20)), + ('country', models.CharField(default='Czech Republic', max_length=100)), + ('note', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('carrier', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='commerce.carrier')), + ('discount', models.ManyToManyField(blank=True, related_name='orders', to='commerce.discountcode')), + ('invoice', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='order', to='commerce.invoice')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='orders', to=settings.AUTH_USER_MODEL)), + ('payment', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='commerce.payment')), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True)), + ('code', models.CharField(blank=True, max_length=100, null=True, unique=True)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('url', models.SlugField(unique=True)), + ('stock', models.PositiveIntegerField(default=0)), + ('is_active', models.BooleanField(default=True)), + ('limited_to', models.DateTimeField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='commerce.category')), + ('default_carrier', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_for_products', to='commerce.carrier')), + ('variants', models.ManyToManyField(blank=True, help_text='Symetrické varianty produktu: pokud přidáte variantu A → B, Django automaticky přidá i variantu B → A. Všechny varianty jsou rovnocenné a zobrazí se vzájemně.', related_name='variant_of', to='commerce.product')), + ], + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='commerce.order')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='commerce.product')), + ], + ), + migrations.AddField( + model_name='discountcode', + name='specific_products', + field=models.ManyToManyField(blank=True, related_name='discount_codes', to='commerce.product'), + ), + migrations.CreateModel( + name='ProductImage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to='products/')), + ('alt_text', models.CharField(blank=True, max_length=150)), + ('is_main', models.BooleanField(default=False)), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='commerce.product')), + ], + ), + migrations.CreateModel( + name='Refund', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reason_choice', models.CharField(choices=[('retuning_before_fourteen_day_period', 'cz#Vrácení před uplynutím 14-ti denní lhůty'), ('damaged_product', 'cz#Poškozený produkt'), ('wrong_item', 'cz#Špatná položka'), ('other', 'cz#Jiný důvod')], max_length=40)), + ('reason_text', models.TextField(blank=True)), + ('verified', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='refunds', to='commerce.order')), + ], + ), + ] diff --git a/backend/commerce/migrations/__init__.py b/backend/commerce/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/commerce/models.py b/backend/commerce/models.py index 7eae1ae..0f76fc2 100644 --- a/backend/commerce/models.py +++ b/backend/commerce/models.py @@ -15,7 +15,7 @@ from configuration.models import SiteConfiguration from thirdparty.zasilkovna.models import ZasilkovnaPacket from thirdparty.stripe.models import StripeModel -from .tasks import notify_refund_accepted, notify_order_sended +from .tasks import notify_refund_accepted, notify_Ready_to_pickup, notify_zasilkovna_sended #FIXME: přidat soft delete pro všchny modely !!!! @@ -253,18 +253,20 @@ class Carrier(models.Model): self.returning = False self.save() + notify_zasilkovna_sended.delay(order=self.orders.first(), user=self.orders.first().user) + elif self.shipping_method == self.SHIPPING.STORE: self.state = self.STATE.READY_TO_PICKUP self.save() + notify_Ready_to_pickup.delay(order=self.orders.first(), user=self.orders.first().user) + else: raise ValidationError("Tato metoda dopravy nepodporuje objednání přepravy.") - notify_order_sended.delay(order=self.orders.first(), user=self.orders.first().user) - - #... další logika pro jiné způsoby dopravy - #TODO: přidat notifikace uživateli, jak pro zásilkovnu, tak pro vyzvednutí v obchodě! + #... další logika pro jiné způsoby dopravy (do budoucna!) + def ready_to_pickup(self): if self.shipping_method == self.SHIPPING.STORE: @@ -354,7 +356,7 @@ class DiscountCode(models.Model): class OrderItem(models.Model): order = models.ForeignKey(Order, related_name="items", on_delete=models.CASCADE) - product = models.ForeignKey("products.Product", on_delete=models.PROTECT) + product = models.ForeignKey("commerce.Product", on_delete=models.PROTECT) quantity = models.PositiveIntegerField(default=1) def get_total_price(self, discounts: list[DiscountCode] = None): @@ -465,7 +467,7 @@ class Refund(models.Model): DAMAGED_PRODUCT = "damaged_product", "cz#Poškozený produkt" WRONG_ITEM = "wrong_item", "cz#Špatná položka" OTHER = "other", "cz#Jiný důvod" - reason_choice = models.CharField(max_length=30, choices=Reason.choices) + reason_choice = models.CharField(max_length=40, choices=Reason.choices) reason_text = models.TextField(blank=True) diff --git a/backend/commerce/serializers.py b/backend/commerce/serializers.py index bf3fbe4..a53ceb5 100644 --- a/backend/commerce/serializers.py +++ b/backend/commerce/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from backend.thirdparty.stripe.client import StripeClient +from thirdparty.stripe.client import StripeClient from .models import Refund, Order, Invoice @@ -87,7 +87,7 @@ from .models import ( Payment, ) -from thirdparty.stripe.models import StripeModel, StripePayment +from thirdparty.stripe.models import StripeModel from thirdparty.zasilkovna.serializers import ZasilkovnaPacketSerializer from thirdparty.zasilkovna.models import ZasilkovnaPacket diff --git a/backend/commerce/tasks.py b/backend/commerce/tasks.py index 21bfdeb..a3778fd 100644 --- a/backend/commerce/tasks.py +++ b/backend/commerce/tasks.py @@ -1,14 +1,13 @@ -from account.models import User from account.tasks import send_email_with_context from celery import shared_task from django.apps import apps from django.utils import timezone -Order = apps.get_model('commerce', 'Order') - def delete_expired_orders(): + Order = apps.get_model('commerce', 'Order') + expired_orders = Order.objects.filter(status=Order.STATUS_CHOICES.CANCELLED, created_at__lt=timezone.now() - timezone.timedelta(hours=24)) count = expired_orders.count() expired_orders.delete() @@ -19,7 +18,7 @@ def delete_expired_orders(): # Zásilkovna @shared_task -def notify_zasilkovna_sended(order:Order = None, user:User = None, **kwargs): +def notify_zasilkovna_sended(order = None, user = None, **kwargs): if not order or not user: raise ValueError("Order and User must be provided for notification.") @@ -38,9 +37,10 @@ def notify_zasilkovna_sended(order:Order = None, user:User = None, **kwargs): pass + # Shop @shared_task -def notify_Ready_to_pickup(order:Order = None, user:User = None, **kwargs): +def notify_Ready_to_pickup(order = None, user = None, **kwargs): if not order or not user: raise ValueError("Order and User must be provided for notification.") @@ -63,7 +63,7 @@ def notify_Ready_to_pickup(order:Order = None, user:User = None, **kwargs): # -- NOTIFICATIONS ORDER -- @shared_task -def notify_order_successfuly_created(order:Order = None, user:User = None, **kwargs): +def notify_order_successfuly_created(order = None, user = None, **kwargs): if not order or not user: raise ValueError("Order and User must be provided for notification.") @@ -84,7 +84,7 @@ def notify_order_successfuly_created(order:Order = None, user:User = None, **kwa @shared_task -def notify_about_missing_payment(order:Order = None, user:User = None, **kwargs): +def notify_about_missing_payment(order = None, user = None, **kwargs): if not order or not user: raise ValueError("Order and User must be provided for notification.") @@ -104,7 +104,7 @@ def notify_about_missing_payment(order:Order = None, user:User = None, **kwargs) pass -def notify_refund_items_arrived(order:Order = None, user:User = None, **kwargs): +def notify_refund_items_arrived(order = None, user = None, **kwargs): if not order or not user: raise ValueError("Order and User must be provided for notification.") @@ -126,7 +126,7 @@ def notify_refund_items_arrived(order:Order = None, user:User = None, **kwargs): # Refund accepted, retuning money @shared_task -def notify_refund_accepted(order:Order = None, user:User = None, **kwargs): +def notify_refund_accepted(order = None, user = None, **kwargs): if not order or not user: raise ValueError("Order and User must be provided for notification.") diff --git a/backend/configuration/migrations/0001_initial.py b/backend/configuration/migrations/0001_initial.py new file mode 100644 index 0000000..aeae3ff --- /dev/null +++ b/backend/configuration/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# Generated by Django 5.2.9 on 2025-12-14 02:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='SiteConfiguration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='Shop name', max_length=100, unique=True)), + ('logo', models.ImageField(blank=True, null=True, upload_to='shop_logos/')), + ('favicon', models.ImageField(blank=True, null=True, upload_to='shop_favicons/')), + ('contact_email', models.EmailField(blank=True, max_length=254, null=True)), + ('contact_phone', models.CharField(blank=True, max_length=20, null=True)), + ('contact_address', models.TextField(blank=True, null=True)), + ('opening_hours', models.JSONField(blank=True, null=True)), + ('facebook_url', models.URLField(blank=True, null=True)), + ('instagram_url', models.URLField(blank=True, null=True)), + ('youtube_url', models.URLField(blank=True, null=True)), + ('tiktok_url', models.URLField(blank=True, null=True)), + ('whatsapp_number', models.CharField(blank=True, max_length=20, null=True)), + ('zasilkovna_shipping_price', models.DecimalField(decimal_places=2, default=50, max_digits=10)), + ('zasilkovna_api_key', models.CharField(blank=True, help_text='API klíč pro přístup k Zásilkovna API (zatím není využito)', max_length=255, null=True)), + ('zasilkovna_api_password', models.CharField(blank=True, help_text='API heslo pro přístup k Zásilkovna API (zatím není využito)', max_length=255, null=True)), + ('free_shipping_over', models.DecimalField(decimal_places=2, default=2000, max_digits=10)), + ('multiplying_coupons', models.BooleanField(default=True, help_text='Násobení kupónů v objednávce (ano/ne), pokud ne tak se použije pouze nejvyšší slevový kupón')), + ('addition_of_coupons_amount', models.BooleanField(default=False, help_text='Sčítání slevových kupónů v objednávce (ano/ne), pokud ne tak se použije pouze nejvyšší slevový kupón')), + ('currency', models.CharField(choices=[('CZK', 'cz#Czech Koruna'), ('EUR', 'cz#Euro')], default='CZK', max_length=10)), + ], + options={ + 'verbose_name': 'Shop Configuration', + 'verbose_name_plural': 'Shop Configuration', + }, + ), + ] diff --git a/backend/social/chat/migrations/__init__.py b/backend/social/chat/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/thirdparty/downloader/migrations/0001_initial.py b/backend/thirdparty/downloader/migrations/0001_initial.py new file mode 100644 index 0000000..38f4dda --- /dev/null +++ b/backend/thirdparty/downloader/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.9 on 2025-12-14 02:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='DownloaderRecord', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('url', models.URLField()), + ('download_time', models.DateTimeField(auto_now_add=True)), + ('format', models.CharField(max_length=50)), + ('length_of_media', models.IntegerField(help_text='Length of media in seconds')), + ('file_size', models.BigIntegerField(help_text='File size in bytes')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/backend/thirdparty/downloader/migrations/__init__.py b/backend/thirdparty/downloader/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/thirdparty/gopay/migrations/0001_initial.py b/backend/thirdparty/gopay/migrations/0001_initial.py new file mode 100644 index 0000000..162316c --- /dev/null +++ b/backend/thirdparty/gopay/migrations/0001_initial.py @@ -0,0 +1,63 @@ +# Generated by Django 5.2.9 on 2025-12-14 02:23 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='GoPayPayment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('gopay_id', models.CharField(db_index=True, max_length=64, unique=True)), + ('order_number', models.CharField(blank=True, default='', max_length=128)), + ('amount', models.BigIntegerField(help_text='Amount in minor units (e.g., CZK in haléř).')), + ('currency', models.CharField(max_length=10)), + ('status', models.CharField(db_index=True, default='', max_length=64)), + ('preauthorized', models.BooleanField(default=False)), + ('captured_amount', models.BigIntegerField(default=0)), + ('request_payload', models.JSONField(blank=True, default=dict)), + ('response_payload', models.JSONField(blank=True, default=dict)), + ('created_at', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='gopay_payments', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='GoPayRefund', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('gopay_refund_id', models.CharField(blank=True, default='', max_length=64)), + ('amount', models.BigIntegerField(help_text='Amount in minor units.')), + ('status', models.CharField(blank=True, default='', max_length=64)), + ('payload', models.JSONField(blank=True, default=dict)), + ('created_at', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ('payment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='refunds', to='gopay.gopaypayment')), + ], + ), + migrations.CreateModel( + name='GoPaySubscription', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('recurrence_id', models.CharField(blank=True, default='', max_length=64)), + ('status', models.CharField(blank=True, default='', max_length=64)), + ('interval', models.CharField(blank=True, default='', max_length=64)), + ('next_payment_on', models.DateTimeField(blank=True, null=True)), + ('payload', models.JSONField(blank=True, default=dict)), + ('canceled', models.BooleanField(default=False)), + ('canceled_at', models.DateTimeField(blank=True, null=True)), + ('created_at', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ('parent_payment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='gopay.gopaypayment')), + ], + ), + ] diff --git a/backend/thirdparty/gopay/migrations/__init__.py b/backend/thirdparty/gopay/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/thirdparty/stripe/migrations/0001_initial.py b/backend/thirdparty/stripe/migrations/0001_initial.py new file mode 100644 index 0000000..abd2ba4 --- /dev/null +++ b/backend/thirdparty/stripe/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.9 on 2025-12-14 02:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='StripeModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('pending', 'cz#Čeká se na platbu'), ('paid', 'cz#Zaplaceno'), ('failed', 'cz#Neúspěšné'), ('cancelled', 'cz#Zrušeno'), ('refunding', 'cz#Platba se vrací'), ('refunded', 'cz#Platba úspěšně vrácena')], default='pending', max_length=20)), + ('stripe_session_id', models.CharField(blank=True, max_length=255, null=True)), + ('stripe_payment_intent', models.CharField(blank=True, max_length=255, null=True)), + ('stripe_session_url', models.URLField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True, null=True)), + ], + ), + ] diff --git a/backend/thirdparty/stripe/migrations/__init__.py b/backend/thirdparty/stripe/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/thirdparty/stripe/urls.py b/backend/thirdparty/stripe/urls.py index 58182eb..4282239 100644 --- a/backend/thirdparty/stripe/urls.py +++ b/backend/thirdparty/stripe/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from .views import CreateCheckoutSessionView +from .views import StripeWebhook urlpatterns = [ - path("orders/create-checkout/", CreateCheckoutSessionView.as_view(), name="create-checkout"), + path("stripe-webhook/", StripeWebhook.as_view(), name="stripe-webhook"), ] \ No newline at end of file diff --git a/backend/thirdparty/stripe/views.py b/backend/thirdparty/stripe/views.py index bc7c362..4bd9490 100644 --- a/backend/thirdparty/stripe/views.py +++ b/backend/thirdparty/stripe/views.py @@ -9,7 +9,7 @@ from drf_spectacular.utils import extend_schema import os import logging -from .models import StripeTransaction +from .models import StripeModel from commerce.models import Order, Payment logger = logging.getLogger(__name__) @@ -48,7 +48,7 @@ class StripeWebhook(APIView): # ZAPLACENO if event['type'] == 'checkout.session.completed': - stripe_transaction = StripeTransaction.objects.get(stripe_session_id=session.id) + stripe_transaction = StripeModel.objects.get(stripe_session_id=session.id) if stripe_transaction: stripe_transaction.paid() @@ -60,7 +60,7 @@ class StripeWebhook(APIView): # EXPIRACE (zrušení objednávky) uživatel nezaplatil do 24 hodin! elif event['type'] == 'checkout.session.expired': - order = Order.objects.get(payment=Payment.objects.get(stripe=StripeTransaction.objects.get(stripe_session_id=session.id))) + order = Order.objects.get(payment=Payment.objects.get(stripe=StripeModel.objects.get(stripe_session_id=session.id))) order.status = Order.STATUS_CHOICES.CANCELLED order.save() @@ -71,7 +71,7 @@ class StripeWebhook(APIView): # REFUND POTVRZEN elif event['type'] == 'payment_intent.refunded': session = event['data']['object'] - stripe_transaction = StripeTransaction.objects.get(stripe_payment_intent=session.id) + stripe_transaction = StripeModel.objects.get(stripe_payment_intent=session.id) if stripe_transaction: stripe_transaction.refund_confirmed() diff --git a/backend/thirdparty/trading212/migrations/__init__.py b/backend/thirdparty/trading212/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/thirdparty/zasilkovna/migrations/0001_initial.py b/backend/thirdparty/zasilkovna/migrations/0001_initial.py new file mode 100644 index 0000000..41a057a --- /dev/null +++ b/backend/thirdparty/zasilkovna/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.9 on 2025-12-14 02:23 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ZasilkovnaPacket', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('state', models.CharField(choices=[('WAITING_FOR_ORDERING_SHIPMENT', 'cz#Čeká na objednání zásilkovny'), ('PENDING', 'cz#Podáno'), ('SENDED', 'cz#Odesláno'), ('ARRIVED', 'cz#Doručeno'), ('CANCELED', 'cz#Zrušeno'), ('RETURNING', 'cz#Posláno zpátky'), ('RETURNED', 'cz#Vráceno')], default='PENDING', max_length=35)), + ('addressId', models.IntegerField(blank=True, help_text='ID adresy/pointu, ve Widgetu zásilkovny který si vybere uživatel.', null=True)), + ('packet_id', models.IntegerField(blank=True, help_text='Číslo zásilky v Packetě (vraceno od API od Packety)', null=True)), + ('barcode', models.CharField(blank=True, help_text='Čárový kód zásilky od Packety', max_length=64, null=True)), + ('weight', models.IntegerField(default=0, help_text='Hmotnost zásilky v gramech')), + ('return_routing', models.JSONField(blank=True, default=list, help_text='Seznam 2 routing stringů pro vrácení zásilky', null=True)), + ('size_of_pdf', models.CharField(choices=[('A6 on A6', '105x148 mm (A6) label on a page of the same size'), ('A7 on A7', '105x74 mm (A7) label on a page of the same size'), ('A6 on A4', '105x148 mm (A6) label on a page of size 210x297 mm (A4)'), ('A7 on A4', '105x74 mm (A7) label on a page of size 210x297 mm (A4)'), ('A8 on A8', '50x74 mm (A8) label on a page of the same size')], default='A6 on A6', max_length=20)), + ], + ), + migrations.CreateModel( + name='ZasilkovnaShipment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('shipment_id', models.CharField(editable=False, help_text='ID zásilky v Packetě', max_length=255, unique=True)), + ('barcode', models.CharField(help_text='Čárový kód zásilky v Packetě (format: )', max_length=64, validators=[django.core.validators.RegexValidator('D-***-XM-', message='Neplatný formát čárového kódu.')])), + ('packets', models.ManyToManyField(help_text='Seznam zásilek v této zásilce (packet_id)', related_name='shipments', to='zasilkovna.zasilkovnapacket')), + ], + ), + ] diff --git a/backend/thirdparty/zasilkovna/migrations/__init__.py b/backend/thirdparty/zasilkovna/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/thirdparty/zasilkovna/models.py b/backend/thirdparty/zasilkovna/models.py index 0dee6f6..ff568d3 100644 --- a/backend/thirdparty/zasilkovna/models.py +++ b/backend/thirdparty/zasilkovna/models.py @@ -43,7 +43,7 @@ class ZasilkovnaPacket(models.Model): RETURNING = "RETURNING", "cz#Posláno zpátky" RETURNED = "RETURNED", "cz#Vráceno" - state = models.CharField(max_length=20, choices=STATE.choices, default=STATE.PENDING) + state = models.CharField(max_length=35, choices=STATE.choices, default=STATE.PENDING) # ------- API ------- # https://client.packeta.com/cs/senders (admin rozhraní) diff --git a/backend/thirdparty/zasilkovna/serializers.py b/backend/thirdparty/zasilkovna/serializers.py index e25b206..3cab181 100644 --- a/backend/thirdparty/zasilkovna/serializers.py +++ b/backend/thirdparty/zasilkovna/serializers.py @@ -35,7 +35,10 @@ class TrackingURLSerializer(serializers.Serializer): # -- SHIPMENT -- class ZasilkovnaShipmentSerializer(serializers.ModelSerializer): - packets = serializers.PrimaryKeyRelatedField(many=True) + packets = serializers.PrimaryKeyRelatedField( + many=True, + queryset=ZasilkovnaPacket.objects.all() + ) class Meta: model = ZasilkovnaShipment diff --git a/backend/thirdparty/zasilkovna/views.py b/backend/thirdparty/zasilkovna/views.py index 54c3797..d2aaf4a 100644 --- a/backend/thirdparty/zasilkovna/views.py +++ b/backend/thirdparty/zasilkovna/views.py @@ -5,7 +5,7 @@ from django.template import loader from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse, OpenApiParameter, OpenApiTypes -from backend.configuration.models import SiteConfiguration +from configuration.models import SiteConfiguration from .models import ZasilkovnaShipment, ZasilkovnaPacket from .serializers import ( diff --git a/backend/vontor_cz/settings.py b/backend/vontor_cz/settings.py index 6f3dce0..2ddef56 100644 --- a/backend/vontor_cz/settings.py +++ b/backend/vontor_cz/settings.py @@ -338,6 +338,8 @@ MY_CREATED_APPS = [ 'configuration', 'social.chat', + + 'advertisement', 'thirdparty.downloader', 'thirdparty.stripe', # register Stripe app so its models are recognized diff --git a/frontend/src/components/portfolio/PortfolioGrid.tsx b/frontend/src/components/portfolio/PortfolioGrid.tsx deleted file mode 100644 index 9ac1d85..0000000 --- a/frontend/src/components/portfolio/PortfolioGrid.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useState } from "react"; -import Modal from "../common/Modal"; - -type Project = { - id: string; - title: string; - image: string; // public/ path - description: string; - link?: string; -}; - -const projects: Project[] = [ - { - id: "perlica", - title: "Perlica", - image: "/portfolio/perlica.png", - description: "E-commerce redesign with modern UI and Django backend integration.", - link: "#", - }, - { - id: "epinger", - title: "Epinger", - image: "/portfolio/epinger.png", - description: "Landing page with responsive layout and animation system.", - link: "#", - }, - { - id: "davo1", - title: "Davo", - image: "/portfolio/davo1.png", - description: "Portfolio template and component library built with Vite + Tailwind.", - link: "#", - }, -]; - -export default function PortfolioGrid() { - const [active, setActive] = useState(null); - return ( -
-
-

My Work

-

Selected projects combining engineering, design systems, performance optimization and infrastructure. Click a tile for details.

-
- {projects.map((p) => ( - - ))} -
-
- - setActive(null)} title={active?.title}> -
- {active && ( - <> - {active.title} -

{active.description}

- {active.link && ( - - Visit project - - )} - - )} -
-
-
- ); -} \ No newline at end of file diff --git a/frontend/src/components/trading/TradingGraph.tsx b/frontend/src/components/trading/TradingGraph.tsx deleted file mode 100644 index 5380b47..0000000 --- a/frontend/src/components/trading/TradingGraph.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export default function TradingGraph() { - return ( -
-
-

Live Performance

-
-
Trading212 graph placeholder
-
- Graph will appear here -
-
-
-
- ); -} \ No newline at end of file diff --git a/frontend/src/pages/home/components/Services/Services.module.css b/frontend/src/pages/home/components/Services/Services.module.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/pages/home/components/Services/Services.tsx b/frontend/src/pages/home/components/Services/Services.tsx new file mode 100644 index 0000000..6c45a63 --- /dev/null +++ b/frontend/src/pages/home/components/Services/Services.tsx @@ -0,0 +1,18 @@ +import { useState } from "react"; + +import KinematografieSection from "./kinematografie"; +import DroneServisSection from "./droneServis"; +import WebsiteServiceSection from "./webs"; + +import styles from "./Services.module.css"; + +export default function Services() { + const [active, setActive] = useState(null); + return ( +
+ + + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/home/components/Services/TradingGraph.tsx b/frontend/src/pages/home/components/Services/TradingGraph.tsx new file mode 100644 index 0000000..c00a8b2 --- /dev/null +++ b/frontend/src/pages/home/components/Services/TradingGraph.tsx @@ -0,0 +1,157 @@ +import { useEffect, useRef, useState } from "react"; + +export default function TradingGraph() { + const canvasRef = useRef(null); + const [data, setData] = useState([]); + + // Generate sample data for demo + useEffect(() => { + const generateData = () => { + const points = 50; + const newData: number[] = []; + let value = 100; + + for (let i = 0; i < points; i++) { + value += (Math.random() - 0.45) * 5; + newData.push(Math.max(50, Math.min(150, value))); + } + + setData(newData); + }; + + generateData(); + const interval = setInterval(generateData, 5000); + return () => clearInterval(interval); + }, []); + + // Draw the graph + useEffect(() => { + if (!canvasRef.current || data.length === 0) return; + + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + // Get CSS variables + const styles = getComputedStyle(document.documentElement); + const colorOther = styles.getPropertyValue("--c-other").trim(); + const colorLines = styles.getPropertyValue("--c-lines").trim(); + const colorBoxes = styles.getPropertyValue("--c-boxes").trim(); + const colorBg = styles.getPropertyValue("--c-background").trim(); + + // Set canvas size + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width * window.devicePixelRatio; + canvas.height = rect.height * window.devicePixelRatio; + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + + const width = rect.width; + const height = rect.height; + const padding = 40; + + // Clear canvas + ctx.clearRect(0, 0, width, height); + + // Draw grid + ctx.strokeStyle = colorLines; + ctx.lineWidth = 0.5; + ctx.globalAlpha = 0.2; + + // Horizontal grid lines + for (let i = 0; i <= 5; i++) { + const y = padding + (height - 2 * padding) * (i / 5); + ctx.beginPath(); + ctx.moveTo(padding, y); + ctx.lineTo(width - padding, y); + ctx.stroke(); + } + + // Vertical grid lines + for (let i = 0; i <= 10; i++) { + const x = padding + (width - 2 * padding) * (i / 10); + ctx.beginPath(); + ctx.moveTo(x, padding); + ctx.lineTo(x, height - padding); + ctx.stroke(); + } + + ctx.globalAlpha = 1; + + // Find min and max values + const minValue = Math.min(...data); + const maxValue = Math.max(...data); + const range = maxValue - minValue || 1; + + // Draw the line graph + ctx.strokeStyle = colorOther; + ctx.lineWidth = 3; // Thicker line + ctx.lineJoin = "round"; + ctx.lineCap = "round"; + + ctx.beginPath(); + data.forEach((value, index) => { + const x = padding + (width - 2 * padding) * (index / (data.length - 1)); + const y = height - padding - (height - 2 * padding) * ((value - minValue) / range); + + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + ctx.stroke(); + + // Draw area under the line with gradient + const gradient = ctx.createLinearGradient(0, padding, 0, height - padding); + gradient.addColorStop(0, colorBoxes + "80"); + gradient.addColorStop(1, colorBoxes + "00"); + + ctx.fillStyle = gradient; + ctx.lineTo(width - padding, height - padding); + ctx.lineTo(padding, height - padding); + ctx.closePath(); + ctx.fill(); + + // Draw data points + ctx.fillStyle = colorOther; + data.forEach((value, index) => { + const x = padding + (width - 2 * padding) * (index / (data.length - 1)); + const y = height - padding - (height - 2 * padding) * ((value - minValue) / range); + + ctx.beginPath(); + ctx.arc(x, y, 4, 0, Math.PI * 2); + ctx.fill(); + }); + + // Draw labels + ctx.fillStyle = colorLines; + ctx.font = "12px Inter, sans-serif"; + ctx.textAlign = "right"; + + // Y-axis labels + for (let i = 0; i <= 5; i++) { + const value = maxValue - (range * i / 5); + const y = padding + (height - 2 * padding) * (i / 5); + ctx.fillText(value.toFixed(0), padding - 10, y + 4); + } + + }, [data]); + + return ( +
+
+

Live Performance

+
+
Trading212 Performance Chart
+
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/home/components/Services/droneServis.tsx b/frontend/src/pages/home/components/Services/droneServis.tsx new file mode 100644 index 0000000..f83cba3 --- /dev/null +++ b/frontend/src/pages/home/components/Services/droneServis.tsx @@ -0,0 +1,10 @@ +import styles from "./Services.module.css"; + + +export default function DroneServisSection() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/home/components/Services/kinematografie.tsx b/frontend/src/pages/home/components/Services/kinematografie.tsx new file mode 100644 index 0000000..5970fad --- /dev/null +++ b/frontend/src/pages/home/components/Services/kinematografie.tsx @@ -0,0 +1,9 @@ +import styles from "./Services.module.css"; + +export default function KinematografieSection() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/home/components/Services/webs.tsx b/frontend/src/pages/home/components/Services/webs.tsx new file mode 100644 index 0000000..7d6ffc2 --- /dev/null +++ b/frontend/src/pages/home/components/Services/webs.tsx @@ -0,0 +1,28 @@ +import styles from "./Services.module.css"; +import TradingGraph from "./TradingGraph"; + +export default function WebsiteServiceSection() { + return ( +
+

Weby

+ +

+ Udělám weby na míru podle vašich představ, ať už jde o jednoduché statické stránky, e-shopy, správy systémů, nebo komplexní webové aplikace. + Používám moderní technologie jako React, Next.js, a další, abych zajistil rychlý a responzivní design. + Web lze hostovat na mém hostingu s rychlou odezvou a atraktivní cenou. +

+ + +
+

Trading 212 (API)

+ (realné data z Tradingu 212) +
+ {/* Trading graph component or placeholder */} +
+
+ + Příklady {/* reálné ukázky webů + dema + apps */} + Ceník +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/donate/DonationShop.tsx b/frontend/src/pages/home/components/donate/DonationShop.tsx similarity index 100% rename from frontend/src/components/donate/DonationShop.tsx rename to frontend/src/pages/home/components/donate/DonationShop.tsx diff --git a/frontend/src/pages/home/home.tsx b/frontend/src/pages/home/home.tsx index 760f83e..5282a08 100644 --- a/frontend/src/pages/home/home.tsx +++ b/frontend/src/pages/home/home.tsx @@ -1,10 +1,11 @@ import { useEffect } from "react"; -import PortfolioGrid from "../../components/portfolio/PortfolioGrid"; -import TradingGraph from "../../components/trading/TradingGraph"; -import DonationShop from "../../components/donate/DonationShop"; +import PortfolioGrid from "./components/Services/Services"; +import TradingGraph from "./components/Services/TradingGraph"; +import DonationShop from "./components/donate/DonationShop"; import ContactMeForm from "../../components/Forms/ContactMe/ContactMeForm"; import Services from "../../components/services/Services"; + export default function Home() { // Optional: keep spark effect for fun useEffect(() => { diff --git a/frontend/src/pages/portfolio/PortfolioPage.tsx b/frontend/src/pages/portfolio/PortfolioPage.tsx index b278ef6..fdbc8bb 100644 --- a/frontend/src/pages/portfolio/PortfolioPage.tsx +++ b/frontend/src/pages/portfolio/PortfolioPage.tsx @@ -1,4 +1,3 @@ -import PortfolioGrid from "../../components/portfolio/PortfolioGrid"; export default function PortfolioPage(){ - return ; + return null; } \ No newline at end of file