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:
61
backend/account/migrations/0001_initial.py
Normal file
61
backend/account/migrations/0001_initial.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
backend/account/migrations/__init__.py
Normal file
0
backend/account/migrations/__init__.py
Normal 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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
23
backend/advertisement/migrations/0001_initial.py
Normal file
23
backend/advertisement/migrations/0001_initial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
backend/advertisement/migrations/__init__.py
Normal file
0
backend/advertisement/migrations/__init__.py
Normal 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",)
|
||||
|
||||
162
backend/commerce/migrations/0001_initial.py
Normal file
162
backend/commerce/migrations/0001_initial.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
backend/commerce/migrations/__init__.py
Normal file
0
backend/commerce/migrations/__init__.py
Normal 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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
43
backend/configuration/migrations/0001_initial.py
Normal file
43
backend/configuration/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backend/social/chat/migrations/__init__.py
Normal file
0
backend/social/chat/migrations/__init__.py
Normal file
30
backend/thirdparty/downloader/migrations/0001_initial.py
vendored
Normal file
30
backend/thirdparty/downloader/migrations/0001_initial.py
vendored
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backend/thirdparty/downloader/migrations/__init__.py
vendored
Normal file
0
backend/thirdparty/downloader/migrations/__init__.py
vendored
Normal file
63
backend/thirdparty/gopay/migrations/0001_initial.py
vendored
Normal file
63
backend/thirdparty/gopay/migrations/0001_initial.py
vendored
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
backend/thirdparty/gopay/migrations/__init__.py
vendored
Normal file
0
backend/thirdparty/gopay/migrations/__init__.py
vendored
Normal file
26
backend/thirdparty/stripe/migrations/0001_initial.py
vendored
Normal file
26
backend/thirdparty/stripe/migrations/0001_initial.py
vendored
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
backend/thirdparty/stripe/migrations/__init__.py
vendored
Normal file
0
backend/thirdparty/stripe/migrations/__init__.py
vendored
Normal file
4
backend/thirdparty/stripe/urls.py
vendored
4
backend/thirdparty/stripe/urls.py
vendored
@@ -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"),
|
||||
]
|
||||
8
backend/thirdparty/stripe/views.py
vendored
8
backend/thirdparty/stripe/views.py
vendored
@@ -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()
|
||||
|
||||
0
backend/thirdparty/trading212/migrations/__init__.py
vendored
Normal file
0
backend/thirdparty/trading212/migrations/__init__.py
vendored
Normal file
39
backend/thirdparty/zasilkovna/migrations/0001_initial.py
vendored
Normal file
39
backend/thirdparty/zasilkovna/migrations/0001_initial.py
vendored
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
backend/thirdparty/zasilkovna/migrations/__init__.py
vendored
Normal file
0
backend/thirdparty/zasilkovna/migrations/__init__.py
vendored
Normal file
2
backend/thirdparty/zasilkovna/models.py
vendored
2
backend/thirdparty/zasilkovna/models.py
vendored
@@ -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í)
|
||||
|
||||
5
backend/thirdparty/zasilkovna/serializers.py
vendored
5
backend/thirdparty/zasilkovna/serializers.py
vendored
@@ -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
|
||||
|
||||
2
backend/thirdparty/zasilkovna/views.py
vendored
2
backend/thirdparty/zasilkovna/views.py
vendored
@@ -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 (
|
||||
|
||||
@@ -339,6 +339,8 @@ MY_CREATED_APPS = [
|
||||
|
||||
'social.chat',
|
||||
|
||||
'advertisement',
|
||||
|
||||
'thirdparty.downloader',
|
||||
'thirdparty.stripe', # register Stripe app so its models are recognized
|
||||
'thirdparty.trading212',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
18
frontend/src/pages/home/components/Services/Services.tsx
Normal file
18
frontend/src/pages/home/components/Services/Services.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
157
frontend/src/pages/home/components/Services/TradingGraph.tsx
Normal file
157
frontend/src/pages/home/components/Services/TradingGraph.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
10
frontend/src/pages/home/components/Services/droneServis.tsx
Normal file
10
frontend/src/pages/home/components/Services/droneServis.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import styles from "./Services.module.css";
|
||||
|
||||
|
||||
export default function DroneServisSection() {
|
||||
return (
|
||||
<article id="drone-servis" className="section">
|
||||
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import styles from "./Services.module.css";
|
||||
|
||||
export default function KinematografieSection() {
|
||||
return (
|
||||
<article id="kinematografie" className="section">
|
||||
|
||||
</article>
|
||||
);
|
||||
}
|
||||
28
frontend/src/pages/home/components/Services/webs.tsx
Normal file
28
frontend/src/pages/home/components/Services/webs.tsx
Normal 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, 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.
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import PortfolioGrid from "../../components/portfolio/PortfolioGrid";
|
||||
export default function PortfolioPage(){
|
||||
return <PortfolioGrid />;
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user