Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03bf5cace7 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -133,7 +133,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = 'uk'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,18 @@
|
||||
from django.contrib import admin
|
||||
from .models import Product, Variation
|
||||
|
||||
# Register your models here.
|
||||
class VariationInline(admin.TabularInline):
|
||||
model = Variation
|
||||
extra = 0
|
||||
|
||||
@admin.register(Product)
|
||||
class ProductAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'sku', 'price', 'stock_status', 'total_sales')
|
||||
search_fields = ('name', 'sku', 'wp_id')
|
||||
list_filter = ('stock_status',)
|
||||
inlines = [VariationInline]
|
||||
|
||||
@admin.register(Variation)
|
||||
class VariationAdmin(admin.ModelAdmin):
|
||||
list_display = ('product', 'size', 'price', 'stock_quantity')
|
||||
search_fields = ('product__name', 'size')
|
||||
0
core/management/__init__.py
Normal file
0
core/management/__init__.py
Normal file
BIN
core/management/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/management/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
0
core/management/commands/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
BIN
core/management/commands/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/management/commands/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/management/commands/__pycache__/sync_woo.cpython-311.pyc
Normal file
BIN
core/management/commands/__pycache__/sync_woo.cpython-311.pyc
Normal file
Binary file not shown.
93
core/management/commands/sync_woo.py
Normal file
93
core/management/commands/sync_woo.py
Normal file
@ -0,0 +1,93 @@
|
||||
import requests
|
||||
from django.core.management.base import BaseCommand
|
||||
from core.models import Product, Variation
|
||||
from django.conf import settings
|
||||
from decimal import Decimal
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Sync products and variations from WooCommerce REST API"
|
||||
|
||||
CK = "ck_e4107000984fd8752473cdd7974e41d227705215"
|
||||
CS = "cs_c200854aaf7a9798feae9c0844219a36cb5e46bd"
|
||||
BASE_URL = "https://theothers.com.ua/wp-json/wc/v3/"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write("Starting sync from WooCommerce...")
|
||||
|
||||
# 1. Fetch Products
|
||||
page = 1
|
||||
while True:
|
||||
response = requests.get(
|
||||
f"{self.BASE_URL}products",
|
||||
auth=(self.CK, self.CS),
|
||||
params={"page": page, "per_page": 100}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
self.stderr.write(f"Error fetching products: {response.text}")
|
||||
break
|
||||
|
||||
products_data = response.json()
|
||||
if not products_data:
|
||||
break
|
||||
|
||||
for p in products_data:
|
||||
# Map product fields
|
||||
wp_id = str(p.get("id"))
|
||||
name = p.get("name")
|
||||
sku = p.get("sku")
|
||||
price = Decimal(p.get("price") or 0)
|
||||
stock_status = p.get("stock_status", "outofstock")
|
||||
total_sales = p.get("total_sales", 0)
|
||||
image_url = p.get("images")[0]["src"] if p.get("images") else None
|
||||
|
||||
product, created = Product.objects.update_or_create(
|
||||
wp_id=wp_id,
|
||||
defaults={
|
||||
"name": name,
|
||||
"sku": sku,
|
||||
"price": price,
|
||||
"stock_status": stock_status,
|
||||
"total_sales": total_sales,
|
||||
"image": image_url,
|
||||
}
|
||||
)
|
||||
|
||||
# 2. Fetch Variations for variable products
|
||||
if p.get("type") == "variable":
|
||||
v_response = requests.get(
|
||||
f"{self.BASE_URL}products/{wp_id}/variations",
|
||||
auth=(self.CK, self.CS),
|
||||
params={"per_page": 100}
|
||||
)
|
||||
|
||||
if v_response.status_code == 200:
|
||||
variations_data = v_response.json()
|
||||
# Clear old variations to keep it clean (optional, but safer for simple sync)
|
||||
# Variation.objects.filter(product=product).delete()
|
||||
|
||||
for v in variations_data:
|
||||
# Use attributes to find size
|
||||
attributes = v.get("attributes", [])
|
||||
size = "Default"
|
||||
for attr in attributes:
|
||||
if attr.get("name").lower() in ["size", "розмір"]:
|
||||
size = attr.get("option")
|
||||
break
|
||||
|
||||
v_price = Decimal(v.get("price") or 0)
|
||||
v_stock = v.get("stock_quantity") or 0
|
||||
|
||||
Variation.objects.update_or_create(
|
||||
product=product,
|
||||
size=size,
|
||||
defaults={
|
||||
"price": v_price,
|
||||
"stock_quantity": v_stock,
|
||||
}
|
||||
)
|
||||
|
||||
self.stdout.write(f"Processed page {page}")
|
||||
page += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Successfully synced WooCommerce data!"))
|
||||
49
core/migrations/0001_initial.py
Normal file
49
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-22 20:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Product',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('wp_id', models.CharField(max_length=50, unique=True, verbose_name='ID WordPress')),
|
||||
('image', models.URLField(blank=True, max_length=500, null=True, verbose_name='Фото')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Назва товару')),
|
||||
('sku', models.CharField(blank=True, max_length=100, null=True, verbose_name='Артикул (SKU)')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Ціна (грн)')),
|
||||
('stock_status', models.CharField(choices=[('instock', 'В наявності'), ('outofstock', 'Немає в наявності')], max_length=20, verbose_name='Статус')),
|
||||
('total_sales', models.IntegerField(default=0, verbose_name='Продано')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Товар',
|
||||
'verbose_name_plural': 'Товари',
|
||||
'ordering': ['-total_sales'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Variation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('size', models.CharField(max_length=50, verbose_name='Розмір')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Ціна варіації')),
|
||||
('stock_quantity', models.IntegerField(default=0, verbose_name='Залишок на складі')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variations', to='core.product', verbose_name='Основний товар')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Варіація',
|
||||
'verbose_name_plural': 'Варіації',
|
||||
},
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -1,3 +1,39 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
class Product(models.Model):
|
||||
STOCK_STATUS_CHOICES = [
|
||||
('instock', 'В наявності'),
|
||||
('outofstock', 'Немає в наявності'),
|
||||
]
|
||||
|
||||
wp_id = models.CharField(max_length=50, unique=True, verbose_name="ID WordPress")
|
||||
image = models.URLField(max_length=500, blank=True, null=True, verbose_name="Фото")
|
||||
name = models.CharField(max_length=255, verbose_name="Назва товару")
|
||||
sku = models.CharField(max_length=100, blank=True, null=True, verbose_name="Артикул (SKU)")
|
||||
price = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Ціна (грн)")
|
||||
stock_status = models.CharField(max_length=20, choices=STOCK_STATUS_CHOICES, verbose_name="Статус")
|
||||
total_sales = models.IntegerField(default=0, verbose_name="Продано")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Товар"
|
||||
verbose_name_plural = "Товари"
|
||||
ordering = ['-total_sales']
|
||||
|
||||
class Variation(models.Model):
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='variations', verbose_name="Основний товар")
|
||||
size = models.CharField(max_length=50, verbose_name="Розмір")
|
||||
price = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Ціна варіації")
|
||||
stock_quantity = models.IntegerField(default=0, verbose_name="Залишок на складі")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.product.name} - {self.size}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Варіація"
|
||||
verbose_name_plural = "Варіації"
|
||||
@ -1,25 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<html lang="uk">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
{% block head %}{% endblock %}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Адмін-панель WooCommerce{% endblock %}</title>
|
||||
|
||||
<!-- Inter Font -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS CDN -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<nav class="navbar navbar-expand-lg sticky-top">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand fw-bold" href="{% url 'core:index' %}" style="color: var(--apple-text);">
|
||||
<span style="color: var(--apple-accent);">The</span>Others Admin
|
||||
</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<a href="/admin/" class="text-decoration-none text-muted small me-3">Django Admin</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container my-4">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle CDN -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@ -1,145 +1,77 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ project_name }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.runtime code {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block title %}Товари — Адмін-панель{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your app…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 fw-bold m-0">Товари</h1>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<form action="{% url 'core:index' %}" method="get" class="d-flex gap-2">
|
||||
<input type="text" name="q" value="{{ query|default:'' }}" class="form-control search-input" placeholder="Пошук за назвою або SKU..." style="min-width: 300px;">
|
||||
<button type="submit" class="btn btn-primary d-none">Пошук</button>
|
||||
</form>
|
||||
</div>
|
||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||
<p class="runtime">
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">Фото</th>
|
||||
<th>Назва товару</th>
|
||||
<th>Артикул (SKU)</th>
|
||||
<th>Ціна (грн)</th>
|
||||
<th>Статус</th>
|
||||
<th class="text-center">Продано</th>
|
||||
<th style="width: 100px;">Дії</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in products %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if p.image %}
|
||||
<img src="{{ p.image }}" alt="{{ p.name }}" class="product-img">
|
||||
{% else %}
|
||||
<div class="product-img d-flex align-items-center justify-content-center text-muted small">N/A</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">{{ p.name }}</div>
|
||||
<div class="small text-muted">WP ID: {{ p.wp_id }}</div>
|
||||
</td>
|
||||
<td><code>{{ p.sku|default:"-" }}</code></td>
|
||||
<td>
|
||||
<div class="fw-bold">{{ p.price }} ₴</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-{{ p.stock_status }}">
|
||||
{% if p.stock_status == 'instock' %}
|
||||
В наявності
|
||||
{% else %}
|
||||
Немає
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge rounded-pill bg-light text-dark px-3">{{ p.total_sales }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'core:product_detail' p.pk %}" class="btn btn-sm btn-outline-secondary rounded-3 px-3">Деталі</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-5">
|
||||
<div class="text-muted">Товарів не знайдено.</div>
|
||||
<div class="small mt-2">Спробуйте інший запит або запустіть синхронізацію.</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
95
core/templates/core/product_detail.html
Normal file
95
core/templates/core/product_detail.html
Normal file
@ -0,0 +1,95 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ product.name }} — Деталі товару{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-4">
|
||||
<a href="{% url 'core:index' %}" class="text-decoration-none text-muted small d-flex align-items-center mb-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left me-1" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
|
||||
</svg>
|
||||
Назад до списку
|
||||
</a>
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<h1 class="h3 fw-bold m-0">{{ product.name }}</h1>
|
||||
<div class="text-muted small">WordPress ID: {{ product.wp_id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Left Column: Product Info -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-4 border-0">
|
||||
{% if product.image %}
|
||||
<img src="{{ product.image }}" alt="{{ product.name }}" class="card-img-top img-fluid" style="border-radius: 12px 12px 0 0;">
|
||||
{% else %}
|
||||
<div class="d-flex align-items-center justify-content-center bg-light text-muted py-5" style="border-radius: 12px 12px 0 0;">N/A</div>
|
||||
{% endif %}
|
||||
<div class="p-4">
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-1">Ціна</div>
|
||||
<div class="h4 fw-bold mb-0 text-primary">{{ product.price }} ₴</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-1">Артикул (SKU)</div>
|
||||
<div class="fw-medium">{{ product.sku|default:"-" }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-1">Статус</div>
|
||||
<span class="status-{{ product.stock_status }} fw-bold">
|
||||
{% if product.stock_status == 'instock' %}
|
||||
В наявності
|
||||
{% else %}
|
||||
Немає в наявності
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<div class="text-muted small mb-1">Усього продано</div>
|
||||
<div class="badge bg-light text-dark px-3 py-2 border fw-medium">{{ product.total_sales }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Variations Table -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-white p-4 border-0 pb-0">
|
||||
<h2 class="h5 fw-bold m-0">Варіації (Розміри)</h2>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="table-responsive">
|
||||
<table class="table border rounded">
|
||||
<thead>
|
||||
<tr class="bg-light">
|
||||
<th class="py-3 px-4 text-uppercase small text-muted">Розмір</th>
|
||||
<th class="py-3 px-4 text-uppercase small text-muted text-end">Ціна варіації</th>
|
||||
<th class="py-3 px-4 text-uppercase small text-muted text-end">Залишок на складі</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for v in variations %}
|
||||
<tr>
|
||||
<td class="py-3 px-4 fw-medium">{{ v.size }}</td>
|
||||
<td class="py-3 px-4 text-end fw-bold text-primary">{{ v.price }} ₴</td>
|
||||
<td class="py-3 px-4 text-end">
|
||||
<span class="badge rounded-pill {% if v.stock_quantity > 0 %}bg-success-subtle text-success{% else %}bg-danger-subtle text-danger{% endif %} px-3">
|
||||
{{ v.stock_quantity }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-4 text-muted small">Варіацій для цього товару не знайдено.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,7 +1,9 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
from .views import home
|
||||
app_name = 'core'
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
]
|
||||
path('', views.product_list, name='index'),
|
||||
path('product/<int:pk>/', views.product_detail, name='product_detail'),
|
||||
]
|
||||
@ -1,25 +1,26 @@
|
||||
import os
|
||||
import platform
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from .models import Product, Variation
|
||||
from django.db.models import Q
|
||||
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
def product_list(request):
|
||||
query = request.GET.get('q')
|
||||
products = Product.objects.all()
|
||||
|
||||
if query:
|
||||
products = products.filter(
|
||||
Q(name__icontains=query) | Q(sku__icontains=query)
|
||||
)
|
||||
|
||||
return render(request, 'core/index.html', {
|
||||
'products': products,
|
||||
'query': query
|
||||
})
|
||||
|
||||
|
||||
def home(request):
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
now = timezone.now()
|
||||
|
||||
context = {
|
||||
"project_name": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
}
|
||||
return render(request, "core/index.html", context)
|
||||
def product_detail(request, pk):
|
||||
product = get_object_or_404(Product, pk=pk)
|
||||
variations = product.variations.all()
|
||||
|
||||
return render(request, 'core/product_detail.html', {
|
||||
'product': product,
|
||||
'variations': variations
|
||||
})
|
||||
@ -1,4 +1,101 @@
|
||||
/* Custom styles for the application */
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
:root {
|
||||
--apple-bg: #FBFBFB;
|
||||
--apple-card: #FFFFFF;
|
||||
--apple-text: #1D1D1F;
|
||||
--apple-text-muted: #86868B;
|
||||
--apple-accent: #0071E3;
|
||||
--apple-border: rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--apple-bg);
|
||||
color: var(--apple-text);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
border-bottom: 1px solid var(--apple-border);
|
||||
padding: 0.8rem 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--apple-card);
|
||||
border: 1px solid var(--apple-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background: #F5F5F7;
|
||||
border-bottom: 1px solid var(--apple-border);
|
||||
color: var(--apple-text-muted);
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.table tbody td {
|
||||
padding: 16px;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid var(--apple-border);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--apple-accent);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0077ED;
|
||||
}
|
||||
|
||||
.product-img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.status-instock {
|
||||
color: #34C759;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-outofstock {
|
||||
color: #FF3B30;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--apple-border);
|
||||
background-color: #F5F5F7;
|
||||
padding: 10px 16px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.1);
|
||||
border-color: var(--apple-accent);
|
||||
outline: none;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user