Compare commits

...

3 Commits

40 changed files with 2644 additions and 225 deletions

4
.env.example Normal file
View File

@@ -0,0 +1,4 @@
SECRET_KEY=django-insecure-change-me-in-production-!@#$%^&*()
# Comma separated list of trusted origins (include protocol)
CSRF_TRUSTED_ORIGINS=https://servis.ferdzo.xyz,https://www.servis.ferdzo.xyz

58
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,58 @@
# ServiceCRM Copilot Instructions
## Project Overview
ServiceCRM is a simple Service Ticket Management System built with Django. It manages repair tickets ("Inserts") for service customers, tracking details like customer info, defect description, repair status, and notes.
## Architecture
- **Framework**: Django 4.x (compatible with 5.x/6.x in dependencies).
- **Structure**: Single-app project where the `serviceCRM` package contains both project settings and application logic (views, models, urls).
- **Patterns**: Model-View-Template (MVT).
- **Frontend**: Server-side rendered templates using Bootstrap 5 and `django-crispy-forms`.
## Key Components
### Data Model
- **Primary Model**: `Insert` (defined in `serviceCRM/models.py`).
- Represents a service ticket.
- Key fields: `name`, `phone`, `description` (defect), `repair` (resolution), `done` (status), `date`.
### Views & Controllers
- **Location**: `serviceCRM/views.py`.
- **Pattern**: Mix of Class-Based Views (CBV) and Function-Based Views (FBV).
- List views use `django_tables2.SingleTableView`.
- Create/Update use `generic.View` and `generic.UpdateView`.
- **Note**: Some views use a non-standard static method pattern (e.g., `InsertNew.insert` referenced in URLs).
### UI/UX
- **Tables**: `django-tables2` is used for listing records (`serviceCRM/tables.py`).
- **Forms**: `django-crispy-forms` with `crispy-bootstrap5` pack (`serviceCRM/forms.py`).
- **Templates**: Located in `templates/` and `templates/serviceCRM/`.
## Configuration & Environment
- **Settings**: `serviceCRM/settings.py` uses `django-environ`.
- **Environment**: Configuration reads from `.env` file (see `.env.example`).
- **Database**:
- Local/Dev: SQLite (configured via `settings.py` overriding env vars if needed).
- Production: Configurable via `DATABASE_URL` or individual env vars (PostgreSQL supported).
## Development Workflow
### Setup
1. Create `.env` from `.env.example`.
2. Install dependencies: `pip install -r requirements.txt`.
3. Run migrations: `python manage.py migrate`.
### Common Commands
- **Run Server**: `python manage.py runserver`
- **Make Migrations**: `python manage.py makemigrations` (Required when changing `models.py`)
- **Migrate**: `python manage.py migrate`
## Coding Conventions
- **Imports**: standard library -> third party -> django -> local apps.
- **Urls**: Defined in `serviceCRM/urls.py` (which is the root URLconf).
- **Routing**: Routes are mixed between CBV (`.as_view()`) and function references. Maintain consistency with existing patterns when adding new routes.
## Critical Patterns to Respect
1. **Forms**: Always use `InputForm` in `serviceCRM/forms.py` for ticket creation/editing to ensure consistent widget rendering with Bootstrap classes.
2. **Tables**: When adding lists, subclass `tables.Table` in `serviceCRM/tables.py` and use `SingleTableView` for display/sorting features.
3. **Template Inheritance**: All templates should extend `base.html`.

6
.gitignore vendored
View File

@@ -2,7 +2,7 @@
.idea/ .idea/
# Ignore __pycache__ folders and .pyc files # Ignore __pycache__ folders and .pyc files
__pycache__/ *__pycache__/
*.pyc *.pyc
# Ignore .env files # Ignore .env files
@@ -14,7 +14,9 @@ __pycache__/
# Ignore .DS_Store files # Ignore .DS_Store files
.DS_Store .DS_Store
.vscode\ .vscode
db.sqlite3
pyvenv.cfg pyvenv.cfg

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.14

19
.vscode/launch.json vendored
View File

@@ -1,19 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Django",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}\\manage.py",
"args": [
"runserver"
],
"django": true,
"autoStartBrowser": false
}
]
}

View File

@@ -1,4 +0,0 @@
{
"cmake.configureOnOpen": false,
"python.pythonPath": ".venv\\Scripts\\python.exe"
}

41
Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
# Django Backend Dockerfile
FROM ghcr.io/astral-sh/uv:python3.13-alpine AS builder
WORKDIR /app
ENV UV_COMPILE_BYTECODE=1
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
COPY serviceCRM/ ./serviceCRM/
COPY manage.py ./
RUN uv sync --frozen --no-dev
# Collect static files
RUN SECRET_KEY=dummy-key-for-build DEBUG=False uv run manage.py collectstatic --noinput
FROM python:3.13-alpine
WORKDIR /app
RUN apk add --no-cache postgresql-client
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/serviceCRM/ /app/serviceCRM/
COPY --from=builder /app/manage.py /app/
COPY --from=builder /app/staticfiles/ /app/staticfiles/
RUN adduser -D -u 1000 appuser && \
chown -R appuser:appuser /app
USER appuser
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "-k", "uvicorn.workers.UvicornWorker", "serviceCRM.asgi:application"]

12
docker-compose.yml Normal file
View File

@@ -0,0 +1,12 @@
services:
web:
build: .
ports:
- "8000:8000"
volumes:
- ./db.sqlite3:/app/db.sqlite3
env_file:
- .env
environment:
- DEBUG=False
- ALLOWED_HOSTS=*

37
pyproject.toml Normal file
View File

@@ -0,0 +1,37 @@
[project]
name = "servicecrm"
version = "0.1.0"
description = "A CRM application built with Django."
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"asgiref>=3.11.0",
"cffi>=2.0.0",
"crispy-bootstrap5>=2025.6",
"crispy-tailwind>=1.0.3",
"cryptography>=46.0.3",
"django>=6.0.1",
"django-crispy-forms>=2.5",
"django-datatable-view>=2.1.6",
"django-environ>=0.12.0",
"django-filter>=25.2",
"django-pipeline>=4.1.0",
"django-tables2>=2.8.0",
"dnspython>=2.8.0",
"feedparser>=6.0.12",
"gunicorn>=23.0.0",
"nanoid>=2.0.0",
"pillow>=12.1.0",
"publicsuffix>=1.1.1",
"pycparser>=2.23",
"python-dateutil>=2.9.0.post0",
"pytz>=2025.2",
"sgmllib3k>=1.0.0",
"six>=1.17.0",
"sqlparse>=0.5.5",
"tablib>=3.9.0",
"typing-extensions>=4.15.0",
"tzdata>=2025.3",
"uvicorn>=0.40.0",
"whitenoise>=6.11.0",
]

Binary file not shown.

View File

@@ -1,6 +1,27 @@
from .models import Insert from .models import Insert
import django_filters as filters import django_filters as filters
from django_filters import FilterSet from django_filters import FilterSet
from django.db.models import Q
from django import forms
class InsertFilter(FilterSet):
start_date = filters.DateFilter(field_name="date", lookup_expr='gte', label='Од датум', widget=forms.TextInput(attrs={'type': 'date'}))
end_date = filters.DateFilter(field_name="date", lookup_expr='lte', label='До датум', widget=forms.TextInput(attrs={'type': 'date'}))
search = filters.CharFilter(method='filter_search', label='Пребарај (Име, Тел, ID, Опис)')
class Meta:
model = Insert
fields = ['search', 'done']
def filter_search(self, queryset, name, value):
return queryset.filter(
Q(name__icontains=value) |
Q(phone__icontains=value) |
Q(description__icontains=value) |
Q(plateno__icontains=value) |
Q(ticket_id__icontains=value) |
Q(id__icontains=value)
)
class DoneTable(FilterSet): class DoneTable(FilterSet):
class Meta: class Meta:
@@ -11,4 +32,22 @@ class DoneTable(FilterSet):
if value: if value:
return queryset.filter(done=True) return queryset.filter(done=True)
else: else:
return queryset.filter(done=False) return queryset.filter(done=False)
class DoneFilter(FilterSet):
start_date = filters.DateFilter(field_name="date_close", lookup_expr='gte', label='Од датум (Затворено)', widget=forms.TextInput(attrs={'type': 'date'}))
end_date = filters.DateFilter(field_name="date_close", lookup_expr='lte', label='До датум (Затворено)', widget=forms.TextInput(attrs={'type': 'date'}))
search = filters.CharFilter(method='filter_search', label='Пребарај (Име, Тел, ID)')
class Meta:
model = Insert
fields = ['status']
def filter_search(self, queryset, name, value):
return queryset.filter(
Q(name__icontains=value) |
Q(phone__icontains=value) |
Q(description__icontains=value) |
Q(plateno__icontains=value) |
Q(ticket_id__icontains=value) |
Q(id__icontains=value)
)

View File

@@ -1,27 +1,76 @@
from django import forms from django import forms
from .models import Insert from django.utils.translation import gettext_lazy as _
from .models import Insert, Todo
class DateInput(forms.DateInput): class DateInput(forms.DateInput):
input_type = 'date' input_type = 'date'
class TimeInput(forms.TimeInput):
input_type = 'time'
class InputForm(forms.ModelForm): class InputForm(forms.ModelForm):
class Meta: class Meta:
model = Insert model = Insert
fields = {"name", "phone", "description", "date", "note"} fields = ["name", "phone", "date", "description", "note"]
labels = {'name': "Name", 'phone': "Phone", 'date': "Date", 'description': "Description", 'note': "Note"} labels = {
'name': "Име",
'phone': "Телефон",
'date': "Датум на прием",
'description': "Опис на проблем",
'note': "Забелешка"
}
widgets = { widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}), 'date': DateInput(format='%Y-%m-%d'),
'phone': forms.TextInput(attrs={'class': 'form-control'}), 'description': forms.Textarea(attrs={'rows': 3}),
'date': DateInput(),
'description': forms.Textarea(attrs={'class': 'form-control'}),
'note': forms.TextInput(attrs={'class': 'form-control'})
} }
field_order = ["name", "phone", "date", "description", "done"] class CloseForm(forms.ModelForm):
class Meta:
model = Insert
fields = ["repair", "plateno"]
labels = {
'repair': "Детали за поправка",
'plateno': "Наплатено"
}
widgets = {
'repair': forms.Textarea(attrs={'rows': 4}),
'plateno': forms.TextInput(attrs={'placeholder': 'пр. 1500 МКД'})
}
class TodoForm(forms.ModelForm):
class Meta:
model = Todo
fields = ['title', 'name', 'phone', 'scheduled_date', 'scheduled_time']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Опис на задача...',
}),
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Име на лице...'
}),
'phone': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '070-xxx-xxx'
}),
'scheduled_date': DateInput(format='%Y-%m-%d'),
'scheduled_time': TimeInput(format='%H:%M', attrs={
'class': 'form-control'
})
}
labels = {
'title': 'Задача',
'name': 'Име',
'phone': 'Телефон',
'scheduled_date': 'Датум',
'scheduled_time': 'Време'
}
# class EditForm(forms.ModelForm): # class EditForm(forms.ModelForm):
# class Meta: # class Meta:
# model = Insert # model = Insert
# fields = {"name", "phone", "description", "done"} # fields = {"name", "phone", "description", "done"}

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.1 on 2026-01-08 11:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('serviceCRM', '0005_alter_insert_done'),
]
operations = [
migrations.AddField(
model_name='insert',
name='date_close',
field=models.DateField(blank=True, null=True, verbose_name='date closed'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.1 on 2026-01-08 11:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('serviceCRM', '0006_insert_date_close'),
]
operations = [
migrations.AddField(
model_name='insert',
name='status',
field=models.CharField(choices=[('RECEIVED', 'Received'), ('DIAGNOSING', 'Diagnosing'), ('WAITING_PARTS', 'Waiting for Parts'), ('READY', 'Ready for Pickup'), ('COMPLETED', 'Completed')], default='RECEIVED', max_length=20),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 6.0.1 on 2026-01-08 11:30
import serviceCRM.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('serviceCRM', '0007_insert_status'),
]
operations = [
migrations.AddField(
model_name='insert',
name='ticket_id',
field=models.CharField(default=serviceCRM.models.generate_nanoid, max_length=12, null=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 6.0.1 on 2026-01-08 11:32
import serviceCRM.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('serviceCRM', '0008_insert_ticket_id'),
]
operations = [
migrations.AlterField(
model_name='insert',
name='ticket_id',
field=models.CharField(default=serviceCRM.models.generate_nanoid, editable=False, max_length=12, unique=True),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 6.0.1 on 2026-01-08 13:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('serviceCRM', '0009_alter_insert_ticket_id'),
]
operations = [
migrations.CreateModel(
name='TicketLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now_add=True)),
('action', models.CharField(max_length=50)),
('details', models.TextField(blank=True, null=True)),
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='serviceCRM.insert')),
],
options={
'ordering': ['-timestamp'],
},
),
]

View File

@@ -0,0 +1,63 @@
# Generated by Django 6.0.1 on 2026-01-08 14:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('serviceCRM', '0010_ticketlog'),
]
operations = [
migrations.AlterField(
model_name='insert',
name='date',
field=models.DateField(verbose_name='Датум'),
),
migrations.AlterField(
model_name='insert',
name='date_close',
field=models.DateField(blank=True, null=True, verbose_name='Датум затворање'),
),
migrations.AlterField(
model_name='insert',
name='description',
field=models.CharField(max_length=300, verbose_name='Опис'),
),
migrations.AlterField(
model_name='insert',
name='done',
field=models.BooleanField(default=False, verbose_name='Завршено'),
),
migrations.AlterField(
model_name='insert',
name='name',
field=models.CharField(max_length=50, verbose_name='Име'),
),
migrations.AlterField(
model_name='insert',
name='note',
field=models.CharField(blank=True, default=None, max_length=100, null=True, verbose_name='Забелешка'),
),
migrations.AlterField(
model_name='insert',
name='phone',
field=models.CharField(max_length=20, verbose_name='Телефон'),
),
migrations.AlterField(
model_name='insert',
name='plateno',
field=models.CharField(blank=True, default=None, max_length=10, null=True, verbose_name='Плаќање/Рег.'),
),
migrations.AlterField(
model_name='insert',
name='repair',
field=models.CharField(blank=True, default=None, max_length=300, null=True, verbose_name='Поправка'),
),
migrations.AlterField(
model_name='insert',
name='status',
field=models.CharField(choices=[('RECEIVED', 'Примено'), ('DIAGNOSING', 'Дијагностика'), ('WAITING_PARTS', 'Чека делови'), ('READY', 'Готово за подигање'), ('COMPLETED', 'Завршено')], default='RECEIVED', max_length=20, verbose_name='Статус'),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 6.0.1 on 2026-01-27 19:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('serviceCRM', '0011_alter_insert_date_alter_insert_date_close_and_more'),
]
operations = [
migrations.CreateModel(
name='Todo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200, verbose_name='Task')),
('is_completed', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'ordering': ['is_completed', '-created_at'],
},
),
]

View File

@@ -0,0 +1,37 @@
# Generated by Django 6.0.1 on 2026-01-27 20:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('serviceCRM', '0012_todo'),
]
operations = [
migrations.AlterModelOptions(
name='todo',
options={'ordering': ['is_completed', 'scheduled_date', 'scheduled_time', '-created_at']},
),
migrations.AddField(
model_name='todo',
name='name',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Име'),
),
migrations.AddField(
model_name='todo',
name='phone',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Телефон'),
),
migrations.AddField(
model_name='todo',
name='scheduled_date',
field=models.DateField(blank=True, null=True, verbose_name='Датум'),
),
migrations.AddField(
model_name='todo',
name='scheduled_time',
field=models.TimeField(blank=True, null=True, verbose_name='Време'),
),
]

View File

@@ -1,19 +1,78 @@
from django.db import models from django.db import models
from django.contrib.auth.models import UserManager from django.contrib.auth.models import UserManager
from nanoid import generate
def generate_nanoid():
return generate(size=12)
class Insert(models.Model): class Insert(models.Model):
name = models.CharField(max_length=50) ticket_id = models.CharField(max_length=12, default=generate_nanoid, unique=True, editable=False)
phone = models.CharField(max_length=20) name = models.CharField(max_length=50, verbose_name="Име")
description = models.CharField(max_length=300) phone = models.CharField(max_length=20, verbose_name="Телефон")
note= models.CharField(max_length=100, default=None, blank=True, null=True) description = models.CharField(max_length=300, verbose_name="Опис")
date = models.DateField("date submitted") note= models.CharField(max_length=100, default=None, blank=True, null=True, verbose_name="Забелешка")
done = models.BooleanField(default=False) date = models.DateField("Датум")
repair = models.CharField(default=None, blank=True, null=True,max_length=300) done = models.BooleanField(default=False, verbose_name="Завршено")
plateno = models.CharField(max_length=10, default=None, blank=True, null=True)
STATUS_CHOICES = [
('RECEIVED', 'Примено'),
('DIAGNOSING', 'Дијагностика'),
('WAITING_PARTS', 'Чека делови'),
('READY', 'Готово за подигање'),
('COMPLETED', 'Завршено'),
]
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='RECEIVED', verbose_name="Статус")
date_close = models.DateField("Датум затворање", null=True, blank=True)
repair = models.CharField(default=None, blank=True, null=True,max_length=300, verbose_name="Поправка")
plateno = models.CharField(max_length=10, default=None, blank=True, null=True, verbose_name="Плаќање/Рег.")
def save(self, *args, **kwargs):
from django.utils import timezone
# Sync done and status fields
if self.status == 'COMPLETED':
self.done = True
if not self.date_close:
self.date_close = timezone.now().date()
elif self.done:
self.status = 'COMPLETED'
if not self.date_close:
self.date_close = timezone.now().date()
else:
self.done = False
super(Insert, self).save(*args, **kwargs)
def __str__(self): def __str__(self):
return "Ime: " + self.name + " Telefonski broj: " + self.phone + "\nDefekt: " + self.description + "\nDatum: \n" return "Ime: " + self.name + " Telefonski broj: " + self.phone + "\nDefekt: " + self.description + "\nDatum: \n"
def isDone(self): def isDone(self):
return self.done return self.done
class Todo(models.Model):
title = models.CharField(max_length=200, verbose_name="Task")
name = models.CharField(max_length=100, verbose_name="Име", blank=True, null=True)
phone = models.CharField(max_length=20, verbose_name="Телефон", blank=True, null=True)
scheduled_date = models.DateField(verbose_name="Датум", blank=True, null=True)
scheduled_time = models.TimeField(verbose_name="Време", blank=True, null=True)
is_completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
class Meta:
ordering = ['is_completed', 'scheduled_date', 'scheduled_time', '-created_at']
class TicketLog(models.Model):
ticket = models.ForeignKey(Insert, on_delete=models.CASCADE, related_name='logs')
timestamp = models.DateTimeField(auto_now_add=True)
action = models.CharField(max_length=50)
details = models.TextField(blank=True, null=True)
class Meta:
ordering = ['-timestamp']
def __str__(self):
return f"{self.ticket.ticket_id} - {self.action} at {self.timestamp}"

View File

@@ -25,10 +25,16 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = env('SECRET_KEY') SECRET_KEY = env('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = env.bool('DEBUG', default=False)
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*'])
CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=[])
# Trust the X-Forwarded-Proto header for SSL
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = True
USE_X_FORWARDED_PORT = True
# Application definition # Application definition
@@ -39,22 +45,31 @@ INSTALLED_APPS = [
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
# 'whitenoise.runserver_nostatic', # Removed to allow default Django static serving in dev
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'crispy_forms', 'crispy_forms',
'crispy_bootstrap5', 'crispy_tailwind',
'django_tables2', 'django_tables2',
'django_filters',
] ]
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind"
CRISPY_TEMPLATE_PACK = "bootstrap5" CRISPY_TEMPLATE_PACK = "tailwind"
DJANGO_TABLES2_TABLE_ATTRS = { DJANGO_TABLES2_TABLE_ATTRS = {
'class': 'table table-hover', 'class': 'min-w-full divide-y divide-gray-200 border',
'thead': { 'thead': {
'class': 'table-light', 'class': 'bg-gray-50',
},
'tbody': {
'class': 'bg-white divide-y divide-gray-200',
}, },
} }
DJANGO_TABLES2_TEMPLATE = "table_tailwind.html"
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # Add whitenoise middleware
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
@@ -88,12 +103,8 @@ WSGI_APPLICATION = 'serviceCRM.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': env('DATABASE_NAME'), 'NAME': BASE_DIR / 'db.sqlite3',
'USER': env('DATABASE_USER'),
'PASSWORD': env('DATABASE_PASS'),
'HOST': env('DATABASE_HOST'),
'PORT': env('DATABASE_PORT'),
} }
} }
@@ -120,9 +131,20 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/ # https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'mk'
TIME_ZONE = 'UTC' from django.utils.translation import gettext_lazy as _
LANGUAGES = [
('mk', _('Macedonian')),
('en', _('English')),
]
LOCALE_PATHS = [
BASE_DIR / 'locale',
]
TIME_ZONE = 'Europe/Skopje'
USE_I18N = True USE_I18N = True
@@ -132,9 +154,17 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/ # https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/' STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / "serviceCRM" / "static",
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/admin/login/'

View File

@@ -6,18 +6,36 @@ from .models import Insert
class InsertTable(tables.Table): class InsertTable(tables.Table):
actions = TemplateColumn(template_code='<a class="btn btn-secondary" href="{% url \'update\' record.id %}">Edit</a> <a class="btn btn-secondary" href="{% url \'done\' record.id %}">Details</a>') actions = TemplateColumn(template_code='''
<div class="flex flex-wrap gap-1 sm:gap-2">
<a class="text-indigo-600 hover:text-indigo-900 text-sm" href="{% url 'nalog' ticket_id=record.ticket_id %}">Види</a>
<a class="text-gray-600 hover:text-gray-900 text-sm" href="{% url 'update' ticket_id=record.ticket_id %}">Уреди</a>
{% if not record.done %}
<a class="text-green-600 hover:text-green-900 font-medium text-sm" href="{% url 'close_ticket' ticket_id=record.ticket_id %}">Затвори</a>
{% endif %}
</div>
''', orderable=False)
phone = TemplateColumn(template_code='''
<a href="tel:{{ record.phone }}" class="text-blue-600 hover:text-blue-800 inline-flex items-center">
<svg class="w-4 h-4 mr-1 sm:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
<span class="hidden sm:inline">{{ record.phone }}</span>
<span class="sm:hidden">Повикај</span>
</a>
''', orderable=False)
class Meta: class Meta:
model = Insert model = Insert
fields = ("id","name","phone","description","date","done") fields = ("ticket_id","name","phone","description","date","status","done")
per_page = 5 per_page = 5
class DoneInsertTable(InsertTable): class DoneInsertTable(InsertTable):
class Meta: class Meta:
model = Insert model = Insert
fields = ("id","name","phone","description","date","done") fields = ("ticket_id","name","phone","description","date", "date_close", "done")
per_page = 5 per_page = 5
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@@ -1,43 +1,158 @@
<!doctype html> <!doctype html>
<html lang="en"> {% load static i18n %}
<head> <html lang="mk">
<meta charset="utf-8"> <head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <meta charset="UTF-8">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"/> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service </title> <title>Service CRM</title>
</head> <script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
<body> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="http://code.jquery.com/jquery-3.3.1.min.js"></script> <style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js"></script> body { font-family: 'Inter', sans-serif; }
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"></script> </style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.full.min.js"></script> </head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"> <body class="bg-gray-50 text-gray-900">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script> <nav class="bg-white shadow">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<nav class="navbar navbar-inverse"> <div class="flex justify-between h-16">
<div class="container-fluid"> <div class="flex">
<div class="navbar-header"> <div class="flex-shrink-0 flex items-center">
<a class="navbar-brand" href="/">Service CRM</a> {% if user.is_authenticated %}
<a href="{% url 'dashboard' %}">
<img src="{% static 'fer-logo.png' %}" alt="Logo" class="h-8 w-auto">
</a>
{% else %}
<a href="{% url 'home' %}">
<img src="{% static 'fer-logo.png' %}" alt="Logo" class="h-8 w-auto">
</a>
{% endif %}
</div>
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
{% if user.is_authenticated %}
<a href="{% url 'dashboard' %}" class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
<svg class="-ml-1 mr-2 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
Активни
</a>
<a href="{% url 'done' %}" class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
<svg class="-ml-1 mr-2 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
Архива
</a>
<a href="{% url 'todo_list' %}" class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
<svg class="-ml-1 mr-2 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
Задачи
</a>
{% endif %}
<a href="{% url 'track_ticket' %}" class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
<svg class="-ml-1 mr-2 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
Статус
</a>
</div>
</div>
<div class="flex items-center">
{% if user.is_authenticated %}
<a href="{% url 'insert' %}" class="hidden sm:flex bg-gray-900 text-white hover:bg-gray-800 px-4 py-2 rounded-md text-sm font-medium transition shadow-sm items-center">
<svg class="-ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Нов Налог
</a>
<div class="hidden sm:flex ml-3 items-center space-x-3">
<span class="text-sm text-gray-500">{{ user.username }}</span>
<a href="{% url 'logout' %}" class="text-gray-400 hover:text-gray-500">
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</a>
</div>
{% endif %}
<!-- Mobile menu button -->
<button onclick="toggleMobileMenu()" class="sm:hidden ml-2 inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path id="menuIcon" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
<path id="closeIcon" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile menu -->
<div id="mobileMenu" class="hidden sm:hidden border-t border-gray-200">
<div class="pt-2 pb-3 space-y-1">
{% if user.is_authenticated %}
<a href="{% url 'dashboard' %}" class="bg-gray-50 border-l-4 border-gray-900 text-gray-900 block pl-3 pr-4 py-2 text-base font-medium">
Активни налози
</a>
<a href="{% url 'done' %}" class="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium">
Архива
</a>
<a href="{% url 'todo_list' %}" class="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium">
Задачи
</a>
{% endif %}
<a href="{% url 'track_ticket' %}" class="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium">
Проверка на статус
</a>
</div>
{% if user.is_authenticated %}
<div class="pt-4 pb-3 border-t border-gray-200">
<div class="flex items-center px-4">
<div class="flex-shrink-0">
<svg class="h-10 w-10 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
</div>
<div class="ml-3">
<div class="text-base font-medium text-gray-800">{{ user.username }}</div>
</div>
</div>
<div class="mt-3 space-y-1">
<a href="{% url 'insert' %}" class="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100">
Нов налог
</a>
<a href="{% url 'logout' %}" class="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100">
Одјава
</a>
</div>
</div>
{% endif %}
</div> </div>
<ul class="nav navbar-nav navbar-right">
<li><a href="/insert"><span class="glyphicon glyphicon-user"></span>Insert</a></li>
<li><a href="/admin"><span class="glyphicon glyphicon-log-in"></span> Admin</a></li>
<li><a href="/done"><span class="glyphicon glyphicon-log-in"></span> Done</a></li>
</ul>
</div>
</nav> </nav>
<div class="container">
{% comment %} <h1 class="mt-2">Service CRM</h1>
<a href='/insert'><button type="button" class="btn btn-dark">Insert</button></a>
<hr class="mt-0 mb-4"> {% endcomment %}
<div class="row justify-content-center"> <script>
<div class="col-8"> function toggleMobileMenu() {
{% block content %} const menu = document.getElementById('mobileMenu');
{% endblock %} const menuIcon = document.getElementById('menuIcon');
const closeIcon = document.getElementById('closeIcon');
menu.classList.toggle('hidden');
menuIcon.classList.toggle('hidden');
closeIcon.classList.toggle('hidden');
}
</script>
<main class="max-w-7xl mx-auto py-8 sm:px-6 lg:px-8">
<div class="px-4 py-4 sm:px-0">
{% if messages %}
<div class="mb-6 space-y-2">
{% for message in messages %}
<div class="p-4 rounded-md shadow-sm {% if message.tags == 'success' %}bg-green-50 text-green-800 border-l-4 border-green-400{% elif message.tags == 'error' %}bg-red-50 text-red-800 border-l-4 border-red-400{% else %}bg-blue-50 text-blue-800 border-l-4 border-blue-400{% endif %}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% block content %}
{% endblock %}
</div> </div>
</div> </main>
</div> </body>
</body>
</html> </html>

View File

@@ -0,0 +1,34 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}Затвори налог #{{ object.id }}{% endblock %}
{% block content %}
<div class="max-w-3xl mx-auto">
<div class="bg-white shadow sm:rounded-lg mb-6">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-2">Затвори налог #{{ object.id }}</h3>
<p class="text-sm text-gray-500 mb-6">
Завршување на поправка за <strong>{{ object.name }}</strong> ({{ object.description|truncatechars:50 }})
</p>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="mt-5 flex space-x-3">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-1 mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Заврши и Затвори
</button>
<a href="{% url 'home' %}" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
Откажи
</a>
</div>
</form>
</div>
</div>
</div>
{% endblock%}

View File

@@ -1,6 +1,31 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load crispy_forms_tags %}
{% block content %} {% block content %}
{% render_table table %} <div class="mb-6 bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Пребарување Архива</h3>
<form method="get" class="space-y-4 md:space-y-0 md:grid md:grid-cols-12 md:gap-6 items-end">
<div class="md:col-span-3">
{{ filter.form.start_date|as_crispy_field }}
</div>
<div class="md:col-span-3">
{{ filter.form.end_date|as_crispy_field }}
</div>
<div class="md:col-span-4">
{{ filter.form.search|as_crispy_field }}
</div>
<div class="md:col-span-2">
<button type="submit" class="w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
Филтрирај
</button>
</div>
</form>
</div>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
{% render_table table %}
</div>
{%endblock%} {%endblock%}

View File

@@ -1,13 +1,17 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Edit{% endblock %} {% load crispy_forms_tags %}
{% block title %}Уреди{% endblock %}
{% block content %} {% block content %}
{% csrf_token %} <div class="bg-white shadow sm:rounded-lg mb-6">
<div class="px-4 py-5 sm:p-6">
<form method="post"> <form method="post">
<div class="form-group">
{% csrf_token %} {% csrf_token %}
{{form.as_p}} {{ form|crispy }}
<input type="submit" class="btn btn-success" value="Update"> <div class="mt-5 flex space-x-3">
<input type="submit" class="btn btn-danger" value="Delete" formaction="{% url 'delete' id=object.id %}"> <input type="submit" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 cursor-pointer" value="Ажурирај">
<input type="submit" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 cursor-pointer" value="Избриши" formaction="{% url 'delete' ticket_id=object.ticket_id %}">
</div> </div>
</form> </form>
</div>
</div>
{% endblock%} {% endblock%}

View File

@@ -1,11 +1,16 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %} {% block content %}
{% csrf_token %} <div class="bg-white shadow sm:rounded-lg mb-6">
<div class="px-4 py-5 sm:p-6">
<form action="/insert/" method="post"> <form action="/insert/" method="post">
<div class="form-group">
{% csrf_token %} {% csrf_token %}
{{form.as_p}} {{ form|crispy }}
<input type="submit" class="btn btn-success" value="Submit"> <div class="mt-5">
<input type="submit" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 cursor-pointer" value="Внеси">
</div> </div>
</form> </form>
</div>
</div>
{% endblock%} {% endblock%}

View File

@@ -1,5 +1,55 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load crispy_forms_tags %}
{% block content %} {% block content %}
<!-- Dashboard Stats -->
<div class="grid grid-cols-1 gap-5 sm:grid-cols-3 mb-6">
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dt class="text-sm font-medium text-gray-500 truncate">Активни налози</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">{{ stats_active }}</dd>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dt class="text-sm font-medium text-gray-500 truncate">Завршени денес</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">{{ stats_today_closed }}</dd>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dt class="text-sm font-medium text-gray-500 truncate">Спремни за подигање</dt>
<dd class="mt-1 text-3xl font-semibold text-yellow-600">{{ stats_ready }}</dd>
</div>
</div>
</div>
<div class="bg-white shadow sm:rounded-lg mb-6">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Пребарај налози</h3>
<form method="get" class="space-y-4 md:space-y-0 md:grid md:grid-cols-12 md:gap-6 items-end">
<div class="md:col-span-3">
{{ filter.form.start_date|as_crispy_field }}
</div>
<div class="md:col-span-3">
{{ filter.form.end_date|as_crispy_field }}
</div>
<div class="md:col-span-4">
{{ filter.form.search|as_crispy_field }}
</div>
<!-- Hidden done field if needed, or included in search/other filters -->
<div class="md:col-span-2">
<button type="submit" class="w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
<svg class="-ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
</svg>
Пребарај
</button>
</div>
</form>
</div>
</div>
{% render_table table %} {% render_table table %}
{% endblock%} {% endblock%}

View File

@@ -1,83 +1,107 @@
{% block content %} {% extends 'base.html' %}
{% load static %} {% load static %}
<html lang="en"> {% block content %}
<head> <div class="max-w-2xl mx-auto bg-white shadow overflow-hidden sm:rounded-lg">
<meta charset="UTF-8"> <div class="px-4 py-5 sm:px-6 flex flex-col sm:flex-row justify-between items-center space-y-4 sm:space-y-0">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <div class="text-center sm:text-left">
<title>Servicing Ticket</title> <h3 class="text-lg leading-6 font-bold text-black">Сервисен Налог #{{ data.ticket_id }}</h3>
<style> <p class="mt-1 max-w-2xl text-sm font-medium text-black">Креиран на {{ data.date|date:"d/m/y" }} (Интерен ID: {{ data.id }})</p>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.header img {
max-width: 150px;
}
.ticket {
border: 2px solid #000;
padding: 20px;
margin-bottom: 20px;
}
.ticket-info {
margin-bottom: 20px;
}
.ticket-info label {
font-weight: bold;
}
.ticket-description {
border-top: 2px solid #000;
padding-top: 10px;
}
.center {
display: block;
margin-left: auto;
margin-right: auto;
max-width: 100%;
max-height: 100%;
margin-bottom: 5 0px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<img src="{% static 'fer-logo.png' %}" class="center" alt="Company Logo">
</div> </div>
<div class="ticket"> <div class="flex space-x-3">
<div class="ticket-info"> <a href="{% url 'print_label' data.ticket_id %}" target="_blank" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-black bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
<label>Име и презиме:</label> {{ name }} <svg xmlns="http://www.w3.org/2000/svg" class="-ml-1 mr-2 h-5 w-5 text-black" fill="none" viewBox="0 0 24 24" stroke="currentColor">
</div> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
<div class="ticket-info"> </svg>
<label>Телефонски број:</label> {{phone}} Печати Налепница
</div> </a>
<div class="ticket-info"> <a href="{% url 'print_receipt' data.ticket_id %}" target="_blank" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-black hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
<label>Датум:</label> {{date}} <svg xmlns="http://www.w3.org/2000/svg" class="-ml-1 mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
</div> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
<div class="ticket-description"> </svg>
<label><b>Опис на дефект:</b></label> {{ desc }} Печати Потврда
</div> </a>
</div>
<div style="height:150px;"></div>
<div class="ticket">
<div class="ticket-info">
<label>Име и презиме:</label> {{ name }}
</div>
<div class="ticket-info">
<label>Телефонски број:</label> {{ phone }}
</div>
<div class="ticket-info">
<label>Датум:</label> {{ date }}
</div>
<div class="ticket-description">
<label><b>Опис на дефект:</b></label> {{ desc }}
</div>
</div> </div>
</div> </div>
</body> <div class="border-t border-gray-200">
</html> <dl>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-bold text-black">Име на клиент</dt>
<dd class="mt-1 text-sm font-bold text-black sm:mt-0 sm:col-span-2">{{ data.name }}</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 border-t border-gray-200">
<dt class="text-sm font-bold text-black">Телефонски број</dt>
<dd class="mt-1 text-sm font-bold text-black sm:mt-0 sm:col-span-2">{{ data.phone }}</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 border-t border-gray-200">
<dt class="text-sm font-bold text-black">Опис на дефект</dt>
<dd class="mt-1 text-sm font-bold text-black sm:mt-0 sm:col-span-2">{{ data.description }}</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 border-t border-gray-200">
<dt class="text-sm font-bold text-black">Статус</dt>
<dd class="mt-1 text-sm font-bold text-black sm:mt-0 sm:col-span-2">{{ data.get_status_display }}</dd>
</div>
{% if data.note %}
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 border-t border-gray-200">
<dt class="text-sm font-bold text-black">Интерна забелешка</dt>
<dd class="mt-1 text-sm font-bold text-black sm:mt-0 sm:col-span-2">{{ data.note }}</dd>
</div>
{% endif %}
{% if data.done %}
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Детали за поправка</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ data.repair }}</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Плаќање/Регистарски број</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ data.plateno }}</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Датум на затворање</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ data.date_close|date:"d/m/y" }}</dd>
</div>
{% endif %}
</dl>
</div>
<!-- Ticket History -->
<div class="bg-gray-50 px-4 py-5 sm:px-6 border-t border-gray-200">
<h4 class="text-md leading-6 font-medium text-gray-900 mb-4">Историја на активности</h4>
<div class="flow-root">
<ul role="list" class="-mb-8">
{% for log in data.logs.all %}
<li>
<div class="relative pb-8">
{% if not forloop.last %}
<span class="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200" aria-hidden="true"></span>
{% endif %}
<div class="relative flex space-x-3">
<div>
<span class="h-8 w-8 rounded-full bg-gray-500 flex items-center justify-center ring-8 ring-white">
<svg class="h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
</svg>
</span>
</div>
<div class="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
<div>
<p class="text-sm text-gray-500">{{ log.details }} <span class="font-medium text-gray-900">({{ log.action }})</span></p>
</div>
<div class="text-right text-sm whitespace-nowrap text-gray-500">
<time datetime="{{ log.timestamp|date:'Y-m-d H:i' }}">{{ log.timestamp|date:"M d, H:i" }}</time>
</div>
</div>
</div>
</div>
</li>
{% empty %}
<li class="text-sm text-gray-500">Нема историја.</li>
{% endfor %}
</ul>
</div>
</div>
<div class="px-4 py-3 bg-white border-t border-gray-200 text-right sm:px-6">
<a href="{% url 'dashboard' %}" class="text-indigo-600 hover:text-indigo-900">Назад кон листа</a>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,41 @@
{% load static %}
<!DOCTYPE html>
<html lang="mk">
<head>
<meta charset="UTF-8">
<title>Налепница #{{ ticket.id }}</title>
<style>
body {
font-family: sans-serif;
margin: 0;
padding: 10px;
width: 300px; /* Adjust for label printer width (e.g., BROTHER QL) */
text-align: center;
}
.header { font-size: 1.2em; font-weight: bold; margin-bottom: 5px; }
.id { font-size: 2em; font-weight: bold; margin: 10px 0; border: 2px solid black; display: inline-block; padding: 5px 20px; }
.details { font-size: 0.9em; text-align: left; margin-top: 10px; }
@media print {
body { width: 100%; margin: 0; padding: 0; }
button { display: none; }
}
</style>
</head>
<body onload="window.print()">
<div class="header">
<img src="{% static 'fer-logo.png' %}" style="max-width: 150px;" alt="Logo">
</div>
<div>{{ ticket.date|date:"d/m/y" }}</div>
<div class="id">#{{ ticket.id }}</div>
<div style="font-size: 10px; font-family: monospace;">{{ ticket.ticket_id }}</div>
<div class="header">{{ ticket.name }}</div>
<div>{{ ticket.phone }}</div>
<div class="details">
<strong>Дефект:</strong><br>
{{ ticket.description|truncatechars:50 }}
</div>
</body>
</html>

View File

@@ -0,0 +1,209 @@
{% load static %}
<!DOCTYPE html>
<html lang="mk">
<head>
<meta charset="UTF-8">
<title>Потврда #{{ ticket.id }}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<style>
@media print {
@page {
size: A4 portrait;
margin: 0;
}
html, body {
width: 210mm;
height: 296mm; /* Slightly less than 297 to prevent overflow */
margin: 0;
padding: 0;
overflow: hidden;
}
.no-print { display: none !important; }
.page-container {
width: 100%;
height: 100%;
margin: 0;
display: flex;
flex-direction: column;
}
.a5-copy {
flex: 1;
height: 50%;
padding: 10mm; /* Reduced padding to fit better */
box-sizing: border-box;
border-bottom: 1px dashed #999;
overflow: hidden;
position: relative;
}
.a5-copy:last-child {
border-bottom: none;
}
.cut-line { display: none; }
}
/* Screen preview styles */
body { background: #f3f4f6; }
.page-container {
background: white;
width: 210mm;
min-height: 297mm;
margin: 20px auto;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.a5-copy {
height: 148mm;
padding: 15mm;
position: relative;
background: white;
border-bottom: 1px dashed #eee;
}
</style>
</head>
<body onload="window.print()">
<div class="page-container">
<!-- Copy 1: Customer -->
<div class="a5-copy">
<!-- Header -->
<div class="flex justify-between items-start border-b-2 border-black pb-2 mb-4">
<div>
<img src="{% static 'fer-logo.png' %}" alt="Logo" class="h-10 w-auto mb-1">
<h1 class="text-2xl font-bold text-black">СЕРВИСНА ПОТВРДА</h1>
<span class="text-xs font-bold text-black uppercase tracking-widest">Примерок за Клиент</span>
</div>
<div class="flex gap-4 items-center">
<div id="qr-client" class="bg-white"></div>
<div class="text-right">
<div class="text-xl font-bold text-black">Налог #{{ ticket.id }}</div>
<div class="text-xs font-mono font-bold text-black mb-1">Реф: {{ ticket.ticket_id }}</div>
<div class="text-black font-bold text-sm">{{ ticket.date|date:"d/m/y" }}</div>
<div class="mt-1 inline-block px-2 py-0.5 border border-black rounded text-xs font-bold text-black">{{ ticket.get_status_display }}</div>
</div>
</div>
</div>
<!-- Content Grid -->
<div class="flex gap-6 mb-4">
<div class="w-1/3">
<h3 class="text-black font-bold text-xs uppercase tracking-wide border-b border-black mb-1">Клиент</h3>
<p class="font-bold text-black">{{ ticket.name }}</p>
<p class="text-sm font-bold text-black">{{ ticket.phone }}</p>
</div>
<div class="w-2/3">
<h3 class="text-black font-bold text-xs uppercase tracking-wide border-b border-black mb-1">Детали за сервис</h3>
<div class="p-2 rounded text-sm border border-black">
<p class="font-bold text-xs text-black">Пријавен дефект:</p>
<p class="mb-2 font-bold text-black">{{ ticket.description }}</p>
{% if ticket.plateno %}
<p class="font-bold text-xs text-black">Проценка/Наплатено:</p>
<p class="font-mono font-bold text-black">{{ ticket.plateno }}</p>
{% endif %}
</div>
</div>
</div>
<!-- Terms (Condensed) -->
<div class="text-[10px] leading-tight text-justify text-black font-bold mb-8 border-t border-black pt-2 absolute bottom-[12mm] left-[10mm] right-[10mm]">
<p><strong>УСЛОВИ:</strong> Со потпис потврдувате дека сте согласни со условите. Сервисот не одговара за загуба на податоци. Дијагностика се наплаќа ако поправката е одбиена. Уреди неподигнати 30 дена по известувањето се отстрануваат.</p>
<!-- Signatures inline with terms to save space -->
<div class="grid grid-cols-2 gap-8 mt-2 pt-2">
<div class="border-t border-black pt-1">
<p class="text-xs font-bold text-black">Потпис на клиент</p>
</div>
<div class="border-t border-black pt-1 text-right">
<p class="text-xs font-bold text-black">Сервисер</p>
</div>
</div>
</div>
</div>
<!-- Copy 2: Service -->
<div class="a5-copy">
<!-- Header -->
<div class="flex justify-between items-start border-b-2 border-black pb-2 mb-4">
<div>
<img src="{% static 'fer-logo.png' %}" alt="Logo" class="h-10 w-auto mb-1">
<h1 class="text-2xl font-bold text-black">СЕРВИСНА ПОТВРДА</h1>
<span class="text-xs font-bold text-black uppercase tracking-widest">Примерок за Сервис</span>
</div>
<div class="flex gap-4 items-center">
<div id="qr-service" class="bg-white"></div>
<div class="text-right">
<div class="text-xl font-bold text-black">Налог #{{ ticket.id }}</div>
<div class="text-xs font-mono font-bold text-black mb-1">Реф: {{ ticket.ticket_id }}</div>
<div class="text-black font-bold text-sm">{{ ticket.date|date:"d/m/y" }}</div>
<div class="mt-1 inline-block px-2 py-0.5 border border-black rounded text-xs font-bold text-black">{{ ticket.get_status_display }}</div>
</div>
</div>
</div>
<!-- Content Grid -->
<div class="flex gap-6 mb-4">
<div class="w-1/3">
<h3 class="text-black font-bold text-xs uppercase tracking-wide border-b border-black mb-1">Клиент</h3>
<p class="font-bold text-black">{{ ticket.name }}</p>
<p class="text-sm font-bold text-black">{{ ticket.phone }}</p>
</div>
<div class="w-2/3">
<h3 class="text-black font-bold text-xs uppercase tracking-wide border-b border-black mb-1">Детали за сервис</h3>
<div class="p-2 rounded text-sm border border-black">
<p class="font-bold text-xs text-black">Пријавен дефект:</p>
<p class="mb-2 font-bold text-black">{{ ticket.description }}</p>
{% if ticket.plateno %}
<p class="font-bold text-xs text-black">Проценка/Наплатено:</p>
<p class="font-mono font-bold text-black">{{ ticket.plateno }}</p>
{% endif %}
</div>
</div>
</div>
<!-- Terms (Condensed) -->
<div class="text-[10px] leading-tight text-justify text-black font-bold mb-8 border-t border-black pt-2 absolute bottom-[12mm] left-[10mm] right-[10mm]">
<p><strong>УСЛОВИ:</strong> Со потпис потврдувате дека сте согласни со условите. Сервисот не одговара за загуба на податоци. Дијагностика се наплаќа ако поправката е одбиена. Уреди неподигнати 30 дена по известувањето се отстрануваат.</p>
<!-- Signatures -->
<div class="grid grid-cols-2 gap-8 mt-2 pt-2">
<div class="border-t border-black pt-1">
<p class="text-xs font-bold text-black">Потпис на клиент</p>
</div>
<div class="border-t border-black pt-1 text-right">
<p class="text-xs font-bold text-black">Сервисер</p>
</div>
</div>
</div>
</div>
</div>
<!-- Print Control -->
<div class="fixed bottom-4 right-4 no-print flex gap-2">
<button onclick="window.print()" class="bg-gray-900 text-white px-4 py-2 rounded shadow hover:bg-gray-800">Печати</button>
<button onclick="window.close()" class="bg-gray-500 text-white px-4 py-2 rounded shadow hover:bg-gray-600">Затвори</button>
</div>
<script type="text/javascript">
// Generate tracking URL dynamically based on current host
const trackingUrl = "{{ request.scheme }}://{{ request.get_host }}{% url 'track_ticket' %}?ticket_id={{ ticket.ticket_id }}";
// Common config
const qrConfig = {
text: trackingUrl,
width: 80,
height: 80,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.L
};
new QRCode(document.getElementById("qr-client"), qrConfig);
new QRCode(document.getElementById("qr-service"), qrConfig);
</script>
</body>
</html>

View File

@@ -0,0 +1,40 @@
{% extends 'base.html' %}
{% block content %}
<div class="min-h-full flex flex-col justify-center py-6 sm:py-12 px-4 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md bg-white rounded-lg shadow px-4 py-8">
<div class="text-center">
<h2 class="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">
Проверка на статус
</h2>
<p class="mt-2 text-base text-gray-600">
Скенирајте го QR кодот од вашата потврда или внесете го рачно бројот на налогот.
</p>
</div>
<form class="mt-8 space-y-6" action="{% url 'track_ticket' %}" method="get">
<div>
<label for="ticket_id" class="block text-sm font-medium text-gray-700">
Број на налог
</label>
<div class="mt-2 relative rounded-md shadow-sm">
<input type="text"
name="ticket_id"
id="ticket_id"
class="block w-full rounded-md border-0 py-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 sm:text-lg sm:leading-6 text-center tracking-wider"
placeholder="XXXX-XXXX"
required
autofocus
autocapitalize="characters">
</div>
</div>
<div>
<button type="submit" class="w-full flex justify-center rounded-md bg-gray-900 px-3 py-3 text-base font-semibold leading-6 text-white shadow-sm hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600">
Провери статус
</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,101 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow overflow-hidden rounded-xl">
<!-- Status Header (Mobile Friendly) -->
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
<div class="flex flex-col items-center justify-center space-y-3">
<span class="inline-flex items-center px-6 py-2 rounded-full text-lg font-bold shadow-sm
{% if ticket.done or ticket.status == 'READY' %}
bg-green-100 text-green-800 ring-1 ring-inset ring-green-600/20
{% else %}
bg-yellow-100 text-yellow-800 ring-1 ring-inset ring-yellow-600/20
{% endif %}">
{% if ticket.done %}
✓ ЗАВРШЕНО
{% elif ticket.status == 'READY' %}
✓ ГОТОВО ЗА ПОДИГАЊЕ
{% else %}
ВО ИЗРАБОТКА
{% endif %}
</span>
<div class="text-center">
<p class="text-xs text-gray-500 uppercase tracking-widest font-semibold">Број на налог</p>
<p class="font-mono text-xl font-bold text-gray-900 tracking-wider">{{ ticket.ticket_id }}</p>
</div>
</div>
</div>
<div class="border-t border-gray-200">
<dl class="divide-y divide-gray-100">
<!-- Device -->
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 hover:bg-gray-50 bg-white">
<dt class="text-sm font-medium text-gray-500">
Уред
</dt>
<dd class="mt-1 text-base text-gray-900 font-semibold sm:mt-0 sm:col-span-2">
{{ ticket.name }}
</dd>
</div>
<!-- Defect -->
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 hover:bg-gray-50 bg-white">
<dt class="text-sm font-medium text-gray-500">
Опис на дефект
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2 leading-relaxed">
{{ ticket.description }}
</dd>
</div>
<!-- Detailed Status Message -->
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 hover:bg-gray-50 bg-white">
<dt class="text-sm font-medium text-gray-500">
Статус на обработка
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2 font-medium">
{{ ticket.get_status_display }}
</dd>
</div>
<!-- Dates -->
<div class="px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 hover:bg-gray-50 bg-white">
<dt class="text-sm font-medium text-gray-500">
Датум на прием
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ ticket.date|date:"d/m/y" }}
</dd>
</div>
{% if ticket.done %}
<!-- Resolution -->
<div class="px-4 py-4 bg-green-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 border-t border-green-100">
<dt class="text-sm font-bold text-green-900">
Опис на поправка
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2 font-medium">
{{ ticket.repair|default:"Уредот е поправен според спецификација." }}
</dd>
</div>
<div class="px-4 py-4 bg-white sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
Завршено на
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ ticket.date_close|date:"d/m/y" }}
</dd>
</div>
{% endif %}
</dl>
</div>
<div class="px-4 py-4 bg-gray-50 border-t border-gray-200">
<a href="{% url 'track_ticket' %}" class="w-full flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
&larr; Назад
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,91 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<!-- Edit Task Modal -->
<div class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<!-- Background overlay -->
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
<!-- Modal Container -->
<div class="flex min-h-full items-end sm:items-center justify-center p-0 sm:p-4">
<!-- Modal Content -->
<div class="relative bg-white w-full sm:max-w-2xl sm:rounded-lg shadow-xl transform transition-all">
<!-- Header -->
<div class="bg-gray-50 px-4 py-3 sm:px-6 border-b border-gray-200 flex justify-between items-center">
<div>
<h3 class="text-lg font-semibold text-gray-900">Уреди задача</h3>
<p class="text-xs sm:text-sm text-gray-500">Промени ги деталите за задачата</p>
</div>
<a href="{% url 'todo_list' %}" class="text-gray-400 hover:text-gray-500">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</a>
</div>
<!-- Form -->
<div class="px-4 py-5 sm:px-6 max-h-[70vh] sm:max-h-[80vh] overflow-y-auto">
<form method="POST">
{% csrf_token %}
<div class="space-y-3 sm:space-y-4">
<!-- Main Task Input -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ form.title.label }}</label>
{{ form.title }}
</div>
<!-- Other Fields Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ form.name.label }}</label>
{{ form.name }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ form.phone.label }}</label>
{{ form.phone }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ form.scheduled_date.label }}</label>
{{ form.scheduled_date }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ form.scheduled_time.label }}</label>
{{ form.scheduled_time }}
</div>
</div>
<!-- Submit Buttons -->
<div class="pt-4 flex flex-col sm:flex-row gap-2 sm:gap-3">
<button type="submit" class="flex-1 sm:flex-initial inline-flex justify-center items-center px-6 py-2.5 sm:py-3 border border-transparent text-sm sm:text-base font-medium rounded-md text-white bg-black hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 transition-colors">
<svg class="h-4 w-4 sm:h-5 sm:w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
Зачувај промени
</button>
<a href="{% url 'todo_list' %}" class="sm:flex-initial inline-flex justify-center items-center px-6 py-2.5 sm:py-3 border border-gray-300 text-sm sm:text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors">
Откажи
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
// Auto-focus first input
setTimeout(() => {
const firstInput = document.querySelector('textarea, input[type="text"]');
if (firstInput) firstInput.focus();
}, 100);
// Close modal on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
window.location.href = "{% url 'todo_list' %}";
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,267 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="max-w-5xl mx-auto px-2 sm:px-4 py-4">
<!-- Header with Stats -->
<div class="bg-white shadow overflow-hidden sm:rounded-lg mb-4 sm:mb-6">
<div class="px-3 sm:px-4 py-4 sm:py-5 border-b border-gray-200">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
<div>
<h3 class="text-xl sm:text-2xl font-bold text-gray-900">Задачи</h3>
<p class="mt-1 text-xs sm:text-sm text-gray-500">Управувајте со вашите дневни задачи</p>
</div>
<div class="text-left sm:text-right">
<div class="text-2xl sm:text-3xl font-bold text-gray-900">{{ completed }}<span class="text-gray-400">/{{ total }}</span></div>
<div class="text-xs text-gray-500 uppercase tracking-wide">Завршени</div>
</div>
</div>
{% if total > 0 %}
<div class="mt-3 sm:mt-4">
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-medium text-gray-600">Прогрес</span>
<span class="text-xs font-medium text-gray-600">{{ progress }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-black rounded-full h-2 transition-all duration-500" style="width: {{ progress }}%"></div>
</div>
</div>
{% endif %}
</div>
<!-- Filter Tabs -->
<div class="px-3 sm:px-4 py-3 bg-gray-50 overflow-x-auto">
<div class="flex gap-2 min-w-max sm:min-w-0">
<a href="?filter=active" class="{% if filter_type == 'active' %}bg-black text-white{% else %}bg-white text-gray-700 hover:bg-gray-100{% endif %} px-3 sm:px-4 py-2 rounded-lg text-xs sm:text-sm font-medium border border-gray-200 transition-colors inline-flex items-center whitespace-nowrap">
<svg class="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-1.5 sm:mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Активни
{% if active_count > 0 %}
<span class="ml-1.5 sm:ml-2 {% if filter_type == 'active' %}bg-white text-black{% else %}bg-gray-200 text-gray-700{% endif %} px-1.5 sm:px-2 py-0.5 rounded-full text-xs font-bold">{{ active_count }}</span>
{% endif %}
</a>
<a href="?filter=completed" class="{% if filter_type == 'completed' %}bg-black text-white{% else %}bg-white text-gray-700 hover:bg-gray-100{% endif %} px-3 sm:px-4 py-2 rounded-lg text-xs sm:text-sm font-medium border border-gray-200 transition-colors inline-flex items-center whitespace-nowrap">
<svg class="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-1.5 sm:mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Завршени
{% if completed > 0 %}
<span class="ml-1.5 sm:ml-2 {% if filter_type == 'completed' %}bg-white text-black{% else %}bg-gray-200 text-gray-700{% endif %} px-1.5 sm:px-2 py-0.5 rounded-full text-xs font-bold">{{ completed }}</span>
{% endif %}
</a>
<a href="?filter=all" class="{% if filter_type == 'all' %}bg-black text-white{% else %}bg-white text-gray-700 hover:bg-gray-100{% endif %} px-3 sm:px-4 py-2 rounded-lg text-xs sm:text-sm font-medium border border-gray-200 transition-colors inline-flex items-center whitespace-nowrap">
<svg class="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-1.5 sm:mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
Сите
{% if total > 0 %}
<span class="ml-1.5 sm:ml-2 {% if filter_type == 'all' %}bg-white text-black{% else %}bg-gray-200 text-gray-700{% endif %} px-1.5 sm:px-2 py-0.5 rounded-full text-xs font-bold">{{ total }}</span>
{% endif %}
</a>
</div>
</div>
</div>
<!-- Add Task Button (Mobile & Desktop) -->
<div class="mb-4 sm:mb-6">
<button onclick="toggleTaskForm()" class="w-full sm:w-auto inline-flex justify-center items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-black hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 transition-colors shadow-lg">
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Додади нова задача
</button>
</div>
<!-- Task Form Modal/Slide (Hidden by default) -->
<div id="taskFormModal" class="hidden fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<!-- Background overlay -->
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onclick="toggleTaskForm()"></div>
<!-- Modal/Slide Container -->
<div class="flex min-h-full items-end sm:items-center justify-center p-0 sm:p-4 overflow-y-auto">
<!-- Modal Content -->
<div class="relative bg-white w-full sm:max-w-2xl sm:my-8 sm:rounded-lg shadow-xl transform transition-all">
<!-- Header -->
<div class="bg-gray-50 px-4 py-3 sm:px-6 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-semibold text-gray-900">Додади нова задача</h3>
<button onclick="toggleTaskForm()" class="text-gray-400 hover:text-gray-500">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- Form -->
<div class="px-4 py-5 sm:px-6 max-h-[70vh] sm:max-h-[80vh] overflow-y-auto">
<form method="POST" id="todoForm">
{% csrf_token %}
<div class="space-y-3 sm:space-y-4">
<!-- Main Task Input -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ form.title.label }}</label>
<textarea name="title" rows="3" class="w-full rounded-md border-gray-300 shadow-sm focus:border-black focus:ring-black text-sm sm:text-base px-3 sm:px-4 py-2 sm:py-3" placeholder="Детален опис на задачата..." required>{{ form.title.value|default:'' }}</textarea>
</div>
<!-- Other Fields Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ form.name.label }}</label>
{{ form.name }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ form.phone.label }}</label>
{{ form.phone }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ form.scheduled_date.label }}</label>
{{ form.scheduled_date }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ form.scheduled_time.label }}</label>
{{ form.scheduled_time }}
</div>
</div>
<!-- Submit Buttons -->
<div class="pt-4 flex flex-col sm:flex-row gap-2 sm:gap-3">
<button type="submit" class="flex-1 sm:flex-initial inline-flex justify-center items-center px-6 py-2.5 sm:py-3 border border-transparent text-sm sm:text-base font-medium rounded-md text-white bg-black hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 transition-colors">
<svg class="h-4 w-4 sm:h-5 sm:w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Додади задача
</button>
<button type="button" onclick="toggleTaskForm()" class="sm:flex-initial inline-flex justify-center items-center px-6 py-2.5 sm:py-3 border border-gray-300 text-sm sm:text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors">
Откажи
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
function toggleTaskForm() {
const modal = document.getElementById('taskFormModal');
modal.classList.toggle('hidden');
// Toggle body scroll
if (!modal.classList.contains('hidden')) {
document.body.style.overflow = 'hidden';
setTimeout(() => {
const firstInput = modal.querySelector('textarea, input');
if (firstInput) firstInput.focus();
}, 100);
} else {
document.body.style.overflow = '';
}
}
// Close modal on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('taskFormModal');
if (!modal.classList.contains('hidden')) {
toggleTaskForm();
}
}
});
</script>
<!-- Task List -->
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<ul class="divide-y divide-gray-200">
{% for todo in todos %}
<li class="hover:bg-gray-50 transition-colors">
<div class="px-4 py-4 sm:px-6 flex items-center justify-between">
<div class="flex items-center flex-1 min-w-0">
<a href="{% url 'todo_toggle' todo.pk %}" class="flex-shrink-0 mr-4">
{% if todo.is_completed %}
<svg class="h-6 w-6 text-green-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
{% else %}
<svg class="h-6 w-6 text-gray-300 hover:text-black transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
{% endif %}
</a>
<div class="flex-1 min-w-0">
<p class="text-base font-medium text-gray-900 {% if todo.is_completed %}line-through text-gray-400{% endif %}">
{{ todo.title }}
</p>
{% if todo.name or todo.phone %}
<div class="flex items-center gap-3 mt-1 text-sm text-gray-600">
{% if todo.name %}
<span class="flex items-center">
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
{{ todo.name }}
</span>
{% endif %}
{% if todo.phone %}
<span class="flex items-center">
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
{{ todo.phone }}
</span>
{% endif %}
</div>
{% endif %}
<div class="flex items-center gap-3 mt-1 text-xs text-gray-500">
{% if todo.scheduled_date %}
<span class="flex items-center font-medium">
<svg class="h-3.5 w-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
{{ todo.scheduled_date|date:"d/m/y" }}
</span>
{% endif %}
{% if todo.scheduled_time %}
<span class="flex items-center font-medium">
<svg class="h-3.5 w-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{{ todo.scheduled_time|time:"H:i" }}
</span>
{% endif %}
<span class="text-gray-400">• Креирано: {{ todo.created_at|date:"d/m/y" }}</span>
</div>
</div>
</div>
<div class="ml-4 flex-shrink-0 flex gap-2">
<a href="{% url 'todo_edit' todo.pk %}"
class="inline-flex items-center p-2 text-gray-400 hover:text-blue-600 transition-colors"
title="Edit Task">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</a>
<a href="{% url 'todo_delete' todo.pk %}"
class="inline-flex items-center p-2 text-gray-400 hover:text-red-600 transition-colors"
onclick="return confirm('Delete this task?')">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</a>
</div>
</div>
</li>
{% empty %}
<li class="px-4 py-12 sm:px-6">
<div class="text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No tasks</h3>
<p class="mt-1 text-sm text-gray-500">Get started by creating a new task.</p>
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,125 @@
{% load django_tables2 %}
{% load i18n %}
{% block table-wrapper %}
<div class="overflow-x-auto shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
{% block table %}
<table {% render_attrs table.attrs %}>
{% block table.thead %}
{% if table.show_header %}
<thead {{ table.attrs.thead.as_html }}>
<tr>
{% for column in table.columns %}
<th {{ column.attrs.th.as_html }} class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
{% if column.orderable %}
<a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}" class="group inline-flex">
{{ column.header }}
<span class="ml-2 flex-none rounded bg-gray-200 text-gray-900 group-hover:bg-gray-300">
<!-- Sort icon placeholder -->
</span>
</a>
{% else %}
{{ column.header }}
{% endif %}
</th>
{% endfor %}
</tr>
</thead>
{% endif %}
{% endblock table.thead %}
{% block table.tbody %}
<tbody {{ table.attrs.tbody.as_html }}>
{% for row in table.paginated_rows %}
{% block table.tbody.row %}
<tr {{ row.attrs.as_html }}>
{% for column, cell in row.items %}
<td {{ column.attrs.td.as_html }} class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}
</td>
{% endfor %}
</tr>
{% endblock table.tbody.row %}
{% empty %}
{% if table.empty_text %}
{% block table.tbody.empty_text %}
<tr><td colspan="{{ table.columns|length }}" class="px-3 py-4 text-sm text-gray-500 text-center">{{ table.empty_text }}</td></tr>
{% endblock table.tbody.empty_text %}
{% endif %}
{% endfor %}
</tbody>
{% endblock table.tbody %}
{% block table.tfoot %}
{% if table.has_footer %}
<tfoot {{ table.attrs.tfoot.as_html }}>
<tr>
{% for column in table.columns %}
<td {{ column.attrs.tf.as_html }} class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">{{ column.footer }}</td>
{% endfor %}
</tr>
</tfoot>
{% endif %}
{% endblock table.tfoot %}
</table>
{% endblock table %}
{% block pagination %}
{% if table.page and table.paginated_rows|length > 0 %}
<div class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6">
<div class="flex flex-1 justify-between sm:hidden">
{% if table.page.has_previous %}
<a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}" class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">Previous</a>
{% endif %}
{% if table.page.has_next %}
<a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}" class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">Next</a>
{% endif %}
</div>
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Showing
<span class="font-medium">{{ table.page.start_index }}</span>
to
<span class="font-medium">{{ table.page.end_index }}</span>
of
<span class="font-medium">{{ table.paginated_rows.total_count }}</span>
results
</p>
</div>
<div>
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
{% if table.page.has_previous %}
<a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}" class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
<span class="sr-only">Previous</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
</svg>
</a>
{% endif %}
{% for p in table.page|table_page_range:table.paginator %}
{% if p == '...' %}
<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300 focus:outline-offset-0">...</span>
{% else %}
<a href="{% querystring table.prefixed_page_field=p %}" class="relative inline-flex items-center px-4 py-2 text-sm font-semibold {% if table.page.number == p %} z-10 bg-gray-900 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-900 {% else %} text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-offset-0 {% endif %}">
{{ p }}
</a>
{% endif %}
{% endfor %}
{% if table.page.has_next %}
<a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}" class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
<span class="sr-only">Next</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
</svg>
</a>
{% endif %}
</nav>
</div>
</div>
</div>
{% endif %}
{% endblock pagination %}
</div>
{% endblock table-wrapper %}

View File

@@ -19,13 +19,30 @@ from django.urls import path
import serviceCRM.views as view import serviceCRM.views as view
urlpatterns = [ urlpatterns = [
path("", view.InsertListView.as_view(), name="index"), path("", view.home, name="home"),
path("dashboard/", view.InsertListView.as_view(), name="dashboard"),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path("insert/", view.InsertNew.insert, name="insert"), path("insert/", view.InsertNew.insert, name="insert"),
path("edit/<int:pk>/", view.Update.as_view(), name="update"), path("logout/", view.logout_view, name="logout"),
path("nalog/<int:id>/", view.Nalog, name="nalog"), path("track/", view.track_ticket, name="track_ticket"),
path("delete/<int:id>/", view.Delete.delete, name="delete"), path("ticket/<str:ticket_id>/", view.Nalog, name="nalog"),
# Secure public links
path("print/label/<str:ticket_id>/", view.print_label, name="print_label"),
path("print/receipt/<str:ticket_id>/", view.print_receipt, name="print_receipt"),
# Secure staff actions
path("edit/<str:ticket_id>/", view.Update.as_view(), name="update"),
path("close/<str:ticket_id>/", view.CloseTicketView.as_view(), name="close_ticket"),
path("delete/<str:ticket_id>/", view.Delete.delete, name="delete"),
path("done/", view.Done.as_view(), name="done"), path("done/", view.Done.as_view(), name="done"),
path("done/<int:id>/", view.Done.done_by_id, name="done"), path("done/<str:ticket_id>/", view.Done.done_by_id, name="done_detail"),
path("datatable/", view.DatatableView.as_view(), name="datatable"), path("datatable/", view.DatatableView.as_view(), name="datatable"),
# Todo List
path('todo/', view.todo_list, name='todo_list'),
path('todo/toggle/<int:pk>/', view.todo_toggle, name='todo_toggle'),
path('todo/edit/<int:pk>/', view.todo_edit, name='todo_edit'),
path('todo/delete/<int:pk>/', view.todo_delete, name='todo_delete'),
] ]

View File

@@ -1,74 +1,233 @@
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render, redirect
from django.views import generic from django.views import generic
from django.utils import timezone
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.contrib.auth import logout
from .forms import InputForm from .forms import InputForm, CloseForm, TodoForm
from .models import Insert from .models import Insert, TicketLog, Todo
from .tables import DoneInsertTable, InsertTable from .tables import DoneInsertTable, InsertTable
from .filter import InsertFilter, DoneFilter
from django_tables2 import SingleTableView from django_tables2.views import SingleTableMixin,SingleTableView
from django_filters.views import FilterView
from datatableview.views import DatatableView from datatableview.views import DatatableView
class InsertListView(SingleTableView): class InsertListView(LoginRequiredMixin, SingleTableMixin, FilterView):
model = Insert model = Insert
table_class = InsertTable table_class = InsertTable
template_name = 'serviceCRM/list.html' template_name = 'serviceCRM/list.html'
filterset_class = InsertFilter
def get_queryset(self):
return Insert.objects.filter(done=False).order_by('-date', '-id')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Dashboard Widgets Data
context['stats_active'] = Insert.objects.filter(done=False).count()
context['stats_today_closed'] = Insert.objects.filter(done=True, date_close=timezone.now().date()).count()
context['stats_ready'] = Insert.objects.filter(status='READY', done=False).count()
return context
class InsertNew(generic.View): class InsertNew(generic.View):
model = Insert model = Insert
template_name = "serviceCRM/form.html" template_name = "serviceCRM/form.html"
@login_required
def insert(request): def insert(request):
if request.method == 'POST': if request.method == 'POST':
form = InputForm(request.POST) form = InputForm(request.POST)
if form.is_valid(): if form.is_valid():
ticket = form.save() ticket = form.save()
print("Raboti") TicketLog.objects.create(ticket=ticket, action="Креирано", details="Налогот е креиран во системот.")
return HttpResponseRedirect(f"/nalog/{ticket.id}/") return HttpResponseRedirect(f"/ticket/{ticket.ticket_id}/")
else: else:
form = InputForm() form = InputForm(initial={'date': timezone.localdate()})
return render(request, InsertNew.template_name, {'form': form}) return render(request, InsertNew.template_name, {'form': form})
class Update(generic.UpdateView): class Update(LoginRequiredMixin, generic.UpdateView):
model = Insert model = Insert
template_name = "serviceCRM/edit.html" template_name = "serviceCRM/edit.html"
fields = ["name", "phone", "description","note", "done", "repair", "plateno"] fields = ["status", "name", "phone", "description","note", "repair", "plateno", "done"]
success_url = '/' success_url = '/'
slug_field = 'ticket_id'
slug_url_kwarg = 'ticket_id'
def form_valid(self, form):
response = super().form_valid(form)
if 'status' in form.changed_data:
TicketLog.objects.create(ticket=self.object, action="Промена на статус", details=f"Статусот е променет во {self.object.get_status_display()}")
else:
TicketLog.objects.create(ticket=self.object, action="Ажурирано", details="Деталите за налогот се ажурирани.")
return response
def Nalog(request, id): class CloseTicketView(LoginRequiredMixin, generic.UpdateView):
try: model = Insert
data = Insert.objects.get(id=id) form_class = CloseForm
except: template_name = "serviceCRM/close_ticket.html"
return HttpResponseRedirect("/") success_url = '/done/'
template = "serviceCRM/nalog.html" slug_field = 'ticket_id'
context = {"name": data.name, "phone": data.phone, "desc": data.description, "date": data.date} slug_url_kwarg = 'ticket_id'
return render(request, template, context)
def form_valid(self, form):
class Done(SingleTableView): form.instance.done = True
form.instance.date_close = timezone.now().date()
response = super().form_valid(form)
TicketLog.objects.create(ticket=self.object, action="Затворено", details="Налогот и поправката се завршени.")
return response
@login_required
@login_required
def print_label(request, ticket_id):
ticket = get_object_or_404(Insert, ticket_id=ticket_id)
return render(request, 'serviceCRM/print_label.html', {'ticket': ticket})
@login_required
def print_receipt(request, ticket_id):
ticket = get_object_or_404(Insert, ticket_id=ticket_id)
return render(request, 'serviceCRM/print_receipt.html', {'ticket': ticket})
def Nalog(request, ticket_id):
data = get_object_or_404(Insert, ticket_id=ticket_id)
# If user is staff/logged in, show the internal Nalog view (with print buttons, notes etc)
if request.user.is_authenticated:
template = "serviceCRM/nalog.html"
context = {"data": data}
return render(request, template, context)
# If public user, show the public tracking result
else:
return render(request, 'serviceCRM/public_track_result.html', {'ticket': data})
class Done(LoginRequiredMixin, SingleTableMixin, FilterView):
model = Insert model = Insert
table_data = Insert.objects.filter(done=True)
table_class = DoneInsertTable table_class = DoneInsertTable
template_name = 'serviceCRM/done.html' template_name = 'serviceCRM/done.html'
filterset_class = DoneFilter
def done_by_id(request, id): def get_queryset(self):
try: return Insert.objects.filter(done=True)
req = get_object_or_404(Insert, id=id)
except: @login_required
return HttpResponseRedirect("/done/") def done_by_id(request, ticket_id):
context = {"name": req.name, "phone": req.phone, "desc": req.description, "date": req.date} req = get_object_or_404(Insert, ticket_id=ticket_id)
return HttpResponse(f"Report ID: {id} \nName: {req.name} \nPhone: {req.phone} \nDescription: {req.description} \n Note:{req.note} \nDate: {req.date} \nDone: {req.done} \nRepair: {req.repair} \n Plateno: {req.plateno} \n") return HttpResponse(f"Report ID: {ticket_id} \nName: {req.name} \nPhone: {req.phone} \nDescription: {req.description} \n Note:{req.note} \nDate: {req.date} \nDone: {req.done} \nRepair: {req.repair} \n Plateno: {req.plateno} \n")
class Delete(generic.View): class Delete(generic.View):
model = Insert model = Insert
def delete(request, id): @login_required
req = get_object_or_404(Insert, id=id) def delete(request, ticket_id):
req = get_object_or_404(Insert, ticket_id=ticket_id)
req.delete() req.delete()
return HttpResponseRedirect("/") return HttpResponseRedirect("/")
class DatatableView(DatatableView): class DatatableView(LoginRequiredMixin, DatatableView):
model = Insert model = Insert
template_name = 'serviceCRM/Insert_list.html' template_name = 'serviceCRM/Insert_list.html'
def track_ticket(request):
if request.method == 'GET' and 'ticket_id' in request.GET:
ticket_id = request.GET.get('ticket_id').strip()
try:
ticket = Insert.objects.get(ticket_id=ticket_id)
return render(request, 'serviceCRM/public_track_result.html', {'ticket': ticket})
except Insert.DoesNotExist:
return render(request, 'serviceCRM/public_track_form.html', {'error': 'Налогот не е пронајден. Проверете го бројот на налогот.'})
return render(request, 'serviceCRM/public_track_form.html')
def logout_view(request):
logout(request)
return redirect('/')
def home(request):
if request.user.is_authenticated:
return redirect('dashboard')
return track_ticket(request)
@login_required
def todo_list(request):
# Get filter from query parameter (default: 'active' - not done only)
filter_type = request.GET.get('filter', 'active')
# Filter todos based on selection
if filter_type == 'completed':
todos = Todo.objects.filter(is_completed=True)
elif filter_type == 'all':
todos = Todo.objects.all()
else: # 'active' - default
todos = Todo.objects.filter(is_completed=False)
# Calculate progress for the UI (always based on all tasks)
all_todos = Todo.objects.all()
total = all_todos.count()
completed = all_todos.filter(is_completed=True).count()
progress = int((completed / total * 100)) if total > 0 else 0
form = TodoForm(initial={'scheduled_date': timezone.localdate()})
if request.method == 'POST':
form = TodoForm(request.POST)
if form.is_valid():
form.save()
return redirect('todo_list')
context = {
'todos': todos,
'form': form,
'total': total,
'completed': completed,
'progress': progress,
'filter_type': filter_type,
'active_count': all_todos.filter(is_completed=False).count(),
}
return render(request, 'serviceCRM/todo_list.html', context)
@login_required
def todo_edit(request, pk):
todo = get_object_or_404(Todo, pk=pk)
if request.method == 'POST':
form = TodoForm(request.POST, instance=todo)
if form.is_valid():
form.save()
return redirect('todo_list')
else:
form = TodoForm(instance=todo)
context = {
'form': form,
'todo': todo,
'is_edit': True
}
return render(request, 'serviceCRM/todo_edit.html', context)
@login_required
def todo_toggle(request, pk):
todo = get_object_or_404(Todo, pk=pk)
todo.is_completed = not todo.is_completed
todo.save()
return redirect('todo_list')
@login_required
def todo_delete(request, pk):
todo = get_object_or_404(Todo, pk=pk)
todo.delete()
return redirect('todo_list')
context = {'todos': todo, 'form': form}
return render(request, 'serviceCRM/todo_list.html', context)
@login_required
def todo_toggle(request, pk):
todo = get_object_or_404(Todo, pk=pk)
todo.is_completed = not todo.is_completed
todo.save()
return redirect('todo_list')
@login_required
def todo_delete(request, pk):
todo = get_object_or_404(Todo, pk=pk)
todo.delete()
return redirect('todo_list')

559
uv.lock generated Normal file
View File

@@ -0,0 +1,559 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "asgiref"
version = "3.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" },
]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
name = "click"
version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "crispy-bootstrap5"
version = "2025.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "django-crispy-forms" },
]
sdist = { url = "https://files.pythonhosted.org/packages/97/30/36cc4144b6dff91bb54490a3b474897b7469bcda9517bf9f54681ea91011/crispy_bootstrap5-2025.6.tar.gz", hash = "sha256:f1bde7cac074c650fc82f31777d4a4cfd0df2512c68bc4128f259c75d3daada4", size = 23950, upload-time = "2025-06-08T07:43:35.461Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/d4/8cf1ba773a91fc17bab1fd46b75bbdef780c4cccbbb8230e617980a0362c/crispy_bootstrap5-2025.6-py3-none-any.whl", hash = "sha256:a343aa128b4383f35f00295b94de2b10862f2a4f24eda21fa6ead45234c07050", size = 24794, upload-time = "2025-06-08T07:43:34.206Z" },
]
[[package]]
name = "crispy-tailwind"
version = "1.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "django-crispy-forms" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/ac/a307ae5ce869d7151b90d4b8b042a48eb454a936dacc695f6418486e5bd8/crispy-tailwind-1.0.3.tar.gz", hash = "sha256:2bc9f616d406e4b003f25d46fcb0079f1c2522719d97adb107667271d849459a", size = 19172, upload-time = "2024-02-13T09:52:37.895Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/ca/11f65e24f3c182dfaf90fd3710d2dcca0fbc3026923e47b43f52a4a2349b/crispy_tailwind-1.0.3-py3-none-any.whl", hash = "sha256:31427f66b1c4fd0d6fb040f4197cfb97d104cdbe7641ea2dea940c0057c4db4b", size = 25700, upload-time = "2024-02-13T09:52:35.928Z" },
]
[[package]]
name = "cryptography"
version = "46.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
{ url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
{ url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
{ url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
{ url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
{ url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
{ url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
{ url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
{ url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
{ url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
{ url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
{ url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
{ url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
{ url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
{ url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
{ url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
{ url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
{ url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
{ url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
{ url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
{ url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
{ url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
{ url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
{ url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
{ url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
{ url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
{ url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
{ url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
{ url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
{ url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
{ url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
{ url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
{ url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
{ url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
]
[[package]]
name = "django"
version = "6.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b5/9b/016f7e55e855ee738a352b05139d4f8b278d0b451bd01ebef07456ef3b0e/django-6.0.1.tar.gz", hash = "sha256:ed76a7af4da21551573b3d9dfc1f53e20dd2e6c7d70a3adc93eedb6338130a5f", size = 11069565, upload-time = "2026-01-06T18:55:53.069Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/b5/814ed98bd21235c116fd3436a7ed44d47560329a6d694ec8aac2982dbb93/django-6.0.1-py3-none-any.whl", hash = "sha256:a92a4ff14f664a896f9849009cb8afaca7abe0d6fc53325f3d1895a15253433d", size = 8338791, upload-time = "2026-01-06T18:55:46.175Z" },
]
[[package]]
name = "django-crispy-forms"
version = "2.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/a1/6a638d13717e4d4f8df169dade0fa51bdc65d9825df39d98ce709a776b49/django_crispy_forms-2.5.tar.gz", hash = "sha256:066e72a8f152a1334f1c811cc37740868efe3265e5a218f79079ef89f848c3d8", size = 1097999, upload-time = "2025-11-06T20:44:01.921Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ac3a11950baaf75c1f3242e3af9dfe45201f6ee10c113dd37a9c000876d2/django_crispy_forms-2.5-py3-none-any.whl", hash = "sha256:adc99d5901baca09479c53bf536b3909e80a9f2bb299438a223de4c106ebf1f9", size = 31464, upload-time = "2025-11-06T20:44:00.795Z" },
]
[[package]]
name = "django-datatable-view"
version = "2.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "python-dateutil" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/cc/f26f6bf8cf70110fe40d86039ac01a6f407260eca1ea67ad56e529a39071/django-datatable-view-2.1.6.tar.gz", hash = "sha256:b84ba6809b35b1d018eb502b3fed5b9dec219ed87120ee54f04a601551a341af", size = 50474, upload-time = "2021-05-28T21:21:45.338Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/ae/026ee8aee1ad6b3d06407cccb2c3036d47a6b319b5b03986d221059e48bf/django_datatable_view-2.1.6-py2.py3-none-any.whl", hash = "sha256:74b4ac9e99ebba50f5558b62434a21fa28c681b0c9f40b875c96ff497aeba11b", size = 59862, upload-time = "2021-05-28T21:21:43.278Z" },
]
[[package]]
name = "django-environ"
version = "0.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/04/65d2521842c42f4716225f20d8443a50804920606aec018188bbee30a6b0/django_environ-0.12.0.tar.gz", hash = "sha256:227dc891453dd5bde769c3449cf4a74b6f2ee8f7ab2361c93a07068f4179041a", size = 56804, upload-time = "2025-01-13T17:03:37.74Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/b3/0a3bec4ecbfee960f39b1842c2f91e4754251e0a6ed443db9fe3f666ba8f/django_environ-0.12.0-py2.py3-none-any.whl", hash = "sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca", size = 19957, upload-time = "2025-01-13T17:03:32.918Z" },
]
[[package]]
name = "django-filter"
version = "25.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2c/e4/465d2699cd388c0005fb8d6ae6709f239917c6d8790ac35719676fffdcf3/django_filter-25.2.tar.gz", hash = "sha256:760e984a931f4468d096f5541787efb8998c61217b73006163bf2f9523fe8f23", size = 143818, upload-time = "2025-10-05T09:51:31.521Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3", size = 94145, upload-time = "2025-10-05T09:51:29.728Z" },
]
[[package]]
name = "django-pipeline"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "setuptools" },
{ name = "wheel" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1c/7b/2f78ccdad9f45a3d4f709950160f9d7e5009b2b8bec8ef636025ec89b62e/django_pipeline-4.1.0.tar.gz", hash = "sha256:aa1d79df6f215b78396cdd50ed162f8741dc4993e9fba2c78483d9b6f1e722b4", size = 72180, upload-time = "2025-09-13T11:47:45.332Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/89/fd40adbf5cc16550007ad67bffd7493c9976f7e576bf431d1bc537cfa976/django_pipeline-4.1.0-py3-none-any.whl", hash = "sha256:bdb84feb8db73b9fe8298fd9d0f6e50f30d78eb28a8ed28f73ca5d154080c3d5", size = 75523, upload-time = "2025-09-13T11:42:55.754Z" },
]
[[package]]
name = "django-tables2"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/28/ab/3533314e818b04a9e1f04ae167845fec509f4e78ebaa6cf507d12eabcce3/django_tables2-2.8.0.tar.gz", hash = "sha256:0dea3401bb99a0164ba09e20d59a7d90856fdc05e5ae2da9a14d0fa14d99257c", size = 129204, upload-time = "2025-11-21T10:17:43.813Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/8b/047bb29359953b1c76f8886377845538144033d87da622c6b743f597a50b/django_tables2-2.8.0-py3-none-any.whl", hash = "sha256:860633b0f448216af73fca6005c7e38dc9b46931dc36c08a9281a71ee250b1ee", size = 95968, upload-time = "2025-11-21T10:17:42.363Z" },
]
[[package]]
name = "dnspython"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
]
[[package]]
name = "feedparser"
version = "6.0.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sgmllib3k" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/79/db7edb5e77d6dfbc54d7d9df72828be4318275b2e580549ff45a962f6461/feedparser-6.0.12.tar.gz", hash = "sha256:64f76ce90ae3e8ef5d1ede0f8d3b50ce26bcce71dd8ae5e82b1cd2d4a5f94228", size = 286579, upload-time = "2025-09-10T13:33:59.486Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/eb/c96d64137e29ae17d83ad2552470bafe3a7a915e85434d9942077d7fd011/feedparser-6.0.12-py3-none-any.whl", hash = "sha256:6bbff10f5a52662c00a2e3f86a38928c37c48f77b3c511aedcd51de933549324", size = 81480, upload-time = "2025-09-10T13:33:58.022Z" },
]
[[package]]
name = "gunicorn"
version = "23.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "nanoid"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/0250bf5935d88e214df469d35eccc0f6ff7e9db046fc8a9aeb4b2a192775/nanoid-2.0.0.tar.gz", hash = "sha256:5a80cad5e9c6e9ae3a41fa2fb34ae189f7cb420b2a5d8f82bd9d23466e4efa68", size = 3290, upload-time = "2018-11-20T14:45:51.578Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/0d/8630f13998638dc01e187fadd2e5c6d42d127d08aeb4943d231664d6e539/nanoid-2.0.0-py3-none-any.whl", hash = "sha256:90aefa650e328cffb0893bbd4c236cfd44c48bc1f2d0b525ecc53c3187b653bb", size = 5844, upload-time = "2018-11-20T14:45:50.165Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pillow"
version = "12.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" },
{ url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" },
{ url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" },
{ url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" },
{ url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" },
{ url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" },
{ url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" },
{ url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" },
{ url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" },
{ url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" },
{ url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" },
{ url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" },
{ url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" },
{ url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" },
{ url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" },
{ url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" },
{ url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" },
{ url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" },
{ url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" },
{ url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" },
{ url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" },
{ url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" },
{ url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" },
{ url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" },
{ url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" },
{ url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" },
{ url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" },
{ url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" },
{ url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" },
{ url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" },
{ url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" },
{ url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" },
{ url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" },
{ url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" },
{ url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" },
{ url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" },
{ url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" },
{ url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" },
{ url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" },
{ url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" },
{ url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" },
{ url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" },
{ url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" },
{ url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" },
{ url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" },
]
[[package]]
name = "publicsuffix"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/70/8124bab47b4b83c5846e124e91e8958696200acabc7404d3765f44212f8d/publicsuffix-1.1.1.tar.gz", hash = "sha256:22ce1d65ab6af5e9b2122e2443facdb93fb5c4abf24138099cb10fe7989f43b6", size = 66870, upload-time = "2019-12-01T13:44:51.867Z" }
[[package]]
name = "pycparser"
version = "2.23"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "pytz"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
[[package]]
name = "servicecrm"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "asgiref" },
{ name = "cffi" },
{ name = "crispy-bootstrap5" },
{ name = "crispy-tailwind" },
{ name = "cryptography" },
{ name = "django" },
{ name = "django-crispy-forms" },
{ name = "django-datatable-view" },
{ name = "django-environ" },
{ name = "django-filter" },
{ name = "django-pipeline" },
{ name = "django-tables2" },
{ name = "dnspython" },
{ name = "feedparser" },
{ name = "gunicorn" },
{ name = "nanoid" },
{ name = "pillow" },
{ name = "publicsuffix" },
{ name = "pycparser" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "sgmllib3k" },
{ name = "six" },
{ name = "sqlparse" },
{ name = "tablib" },
{ name = "typing-extensions" },
{ name = "tzdata" },
{ name = "uvicorn" },
{ name = "whitenoise" },
]
[package.metadata]
requires-dist = [
{ name = "asgiref", specifier = ">=3.11.0" },
{ name = "cffi", specifier = ">=2.0.0" },
{ name = "crispy-bootstrap5", specifier = ">=2025.6" },
{ name = "crispy-tailwind", specifier = ">=1.0.3" },
{ name = "cryptography", specifier = ">=46.0.3" },
{ name = "django", specifier = ">=6.0.1" },
{ name = "django-crispy-forms", specifier = ">=2.5" },
{ name = "django-datatable-view", specifier = ">=2.1.6" },
{ name = "django-environ", specifier = ">=0.12.0" },
{ name = "django-filter", specifier = ">=25.2" },
{ name = "django-pipeline", specifier = ">=4.1.0" },
{ name = "django-tables2", specifier = ">=2.8.0" },
{ name = "dnspython", specifier = ">=2.8.0" },
{ name = "feedparser", specifier = ">=6.0.12" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "nanoid", specifier = ">=2.0.0" },
{ name = "pillow", specifier = ">=12.1.0" },
{ name = "publicsuffix", specifier = ">=1.1.1" },
{ name = "pycparser", specifier = ">=2.23" },
{ name = "python-dateutil", specifier = ">=2.9.0.post0" },
{ name = "pytz", specifier = ">=2025.2" },
{ name = "sgmllib3k", specifier = ">=1.0.0" },
{ name = "six", specifier = ">=1.17.0" },
{ name = "sqlparse", specifier = ">=0.5.5" },
{ name = "tablib", specifier = ">=3.9.0" },
{ name = "typing-extensions", specifier = ">=4.15.0" },
{ name = "tzdata", specifier = ">=2025.3" },
{ name = "uvicorn", specifier = ">=0.40.0" },
{ name = "whitenoise", specifier = ">=6.11.0" },
]
[[package]]
name = "setuptools"
version = "80.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
]
[[package]]
name = "sgmllib3k"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750, upload-time = "2010-08-24T14:33:52.445Z" }
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "sqlparse"
version = "0.5.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
]
[[package]]
name = "tablib"
version = "3.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/11/00/416d2ba54d7d58a7f7c61bf62dfeb48fd553cf49614daf83312f2d2c156e/tablib-3.9.0.tar.gz", hash = "sha256:1b6abd8edb0f35601e04c6161d79660fdcde4abb4a54f66cc9f9054bd55d5fe2", size = 125565, upload-time = "2025-10-15T18:21:56.263Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/66/6b/32e51d847148b299088fc42d3d896845fd09c5247190133ea69dbe71ba51/tablib-3.9.0-py3-none-any.whl", hash = "sha256:eda17cd0d4dda614efc0e710227654c60ddbeb1ca92cdcfc5c3bd1fc5f5a6e4a", size = 49580, upload-time = "2025-10-15T18:21:44.185Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "tzdata"
version = "2025.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
]
[[package]]
name = "uvicorn"
version = "0.40.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" },
]
[[package]]
name = "wheel"
version = "0.45.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" },
]
[[package]]
name = "whitenoise"
version = "6.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/95/8c81ec6b6ebcbf8aca2de7603070ccf37dbb873b03f20708e0f7c1664bc6/whitenoise-6.11.0.tar.gz", hash = "sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f", size = 26432, upload-time = "2025-09-18T09:16:10.995Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/e9/4366332f9295fe0647d7d3251ce18f5615fbcb12d02c79a26f8dba9221b3/whitenoise-6.11.0-py3-none-any.whl", hash = "sha256:b2aeb45950597236f53b5342b3121c5de69c8da0109362aee506ce88e022d258", size = 20197, upload-time = "2025-09-18T09:16:09.754Z" },
]