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.
This commit is contained in:
2025-12-14 03:49:16 +01:00
parent 564418501c
commit 1751badb90
40 changed files with 796 additions and 165 deletions

View File

@@ -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()),
],
),
]

View File

View File

@@ -16,7 +16,6 @@ urlpatterns = [
# Registration & email endpoints
path('register/', views.UserRegistrationViewSet.as_view({'post': 'create'}), name='register'),
path('verify-email/<uidb64>/<token>/', 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'),

View File

@@ -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

View File

@@ -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)),
],
),
]

View File

@@ -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",)

View File

@@ -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')),
],
),
]

View File

View File

@@ -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 (do budoucna!)
#... další logika pro jiné způsoby dopravy
#TODO: přidat notifikace uživateli, jak pro zásilkovnu, tak pro vyzvednutí v obchodě!
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)

View File

@@ -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

View File

@@ -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.")

View File

@@ -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',
},
),
]

View File

@@ -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,
},
),
]

View File

View File

@@ -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')),
],
),
]

View File

View File

@@ -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)),
],
),
]

View File

View File

@@ -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"),
]

View File

@@ -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()

View File

View File

@@ -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-<id>', 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')),
],
),
]

View File

View File

@@ -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í)

View File

@@ -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

View File

@@ -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 (

View File

@@ -339,6 +339,8 @@ MY_CREATED_APPS = [
'social.chat',
'advertisement',
'thirdparty.downloader',
'thirdparty.stripe', # register Stripe app so its models are recognized
'thirdparty.trading212',

View File

@@ -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<Project | null>(null);
return (
<section id="portfolio" className="section">
<div className="container">
<h2 className="text-2xl md:text-3xl font-bold mb-2 text-rainbow">My Work</h2>
<p className="text-[var(--c-lines)] mb-6 max-w-2xl">Selected projects combining engineering, design systems, performance optimization and infrastructure. Click a tile for details.</p>
<div className="grid gap-6 md:grid-cols-3">
{projects.map((p) => (
<button
key={p.id}
className="card overflow-hidden text-left"
onClick={() => setActive(p)}
>
<div className="aspect-[16/10] w-full overflow-hidden">
<img src={p.image} alt={p.title} className="w-full h-full object-cover hover:scale-105 transition-transform" />
</div>
<div className="p-4">
<h3 className="text-lg font-semibold tracking-wide text-rainbow">{p.title}</h3>
<p className="text-xs text-[var(--c-lines)] mt-1 uppercase">View details </p>
</div>
</button>
))}
</div>
</div>
<Modal open={!!active} onClose={() => setActive(null)} title={active?.title}>
<div className="space-y-3">
{active && (
<>
<img src={active.image} alt={active.title} className="w-full rounded" />
<p>{active.description}</p>
{active.link && (
<a href={active.link} target="_blank" rel="noreferrer" className="inline-block px-4 py-2 glow border rounded">
Visit project
</a>
)}
</>
)}
</div>
</Modal>
</section>
);
}

View File

@@ -1,15 +0,0 @@
export default function TradingGraph() {
return (
<section id="live" className="py-20 bg-gradient-to-b from-brand-bg to-brand-bgLight">
<div className="container mx-auto px-4">
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-rainbow">Live Performance</h2>
<div className="card p-4 md:p-6">
<div className="mb-3 text-sm text-brand-text/60">Trading212 graph placeholder</div>
<div className="aspect-[16/9] w-full rounded border border-brand-lines/50 bg-brand-bg/40 grid place-items-center">
<span className="text-brand-text/60">Graph will appear here</span>
</div>
</div>
</div>
</section>
);
}

View File

@@ -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<Project | null>(null);
return (
<article id="services" className="section">
<WebsiteServiceSection />
<KinematografieSection />
<DroneServisSection />
</article>
);
}

View File

@@ -0,0 +1,157 @@
import { useEffect, useRef, useState } from "react";
export default function TradingGraph() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [data, setData] = useState<number[]>([]);
// 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 (
<section id="live" className="py-20 bg-gradient-to-b from-brand-bg to-brand-bgLight">
<div className="container mx-auto px-4">
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-rainbow">Live Performance</h2>
<div className="card p-4 md:p-6">
<div className="mb-3 text-sm text-brand-text/60">Trading212 Performance Chart</div>
<div className="aspect-[16/9] w-full rounded border border-brand-lines/50 bg-brand-bg/40 overflow-hidden">
<canvas
ref={canvasRef}
className="w-full h-full"
style={{ display: "block" }}
/>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,10 @@
import styles from "./Services.module.css";
export default function DroneServisSection() {
return (
<article id="drone-servis" className="section">
</article>
);
}

View File

@@ -0,0 +1,9 @@
import styles from "./Services.module.css";
export default function KinematografieSection() {
return (
<article id="kinematografie" className="section">
</article>
);
}

View File

@@ -0,0 +1,28 @@
import styles from "./Services.module.css";
import TradingGraph from "./TradingGraph";
export default function WebsiteServiceSection() {
return (
<article id="web-services" className="section">
<h1>Weby</h1>
<p>
Udělám weby na míru podle vašich představ, 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.
</p>
<section>
<h2>Trading 212 (API)</h2>
<small>(realné data z Tradingu 212)</small>
<div id="T212Graph">
{/* Trading graph component or placeholder */}
</div>
</section>
<a href="">Příklady</a> {/* reálné ukázky webů + dema + apps */}
<a href="">Ceník</a>
</article>
);
}

View File

@@ -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(() => {

View File

@@ -1,4 +1,3 @@
import PortfolioGrid from "../../components/portfolio/PortfolioGrid";
export default function PortfolioPage(){
return <PortfolioGrid />;
return null;
}