mirror of
https://github.com/ferdzo/serviceCRM.git
synced 2026-04-05 13:16:24 +00:00
Compare commits
3 Commits
a95f7d44c8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 53bb782bc9 | |||
| d2651c2be3 | |||
| 45dc3703d7 |
4
.env.example
Normal file
4
.env.example
Normal 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
58
.github/copilot-instructions.md
vendored
Normal 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
6
.gitignore
vendored
@@ -2,7 +2,7 @@
|
||||
.idea/
|
||||
|
||||
# Ignore __pycache__ folders and .pyc files
|
||||
__pycache__/
|
||||
*__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Ignore .env files
|
||||
@@ -14,7 +14,9 @@ __pycache__/
|
||||
# Ignore .DS_Store files
|
||||
.DS_Store
|
||||
|
||||
.vscode\
|
||||
.vscode
|
||||
|
||||
db.sqlite3
|
||||
|
||||
pyvenv.cfg
|
||||
|
||||
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.14
|
||||
19
.vscode/launch.json
vendored
19
.vscode/launch.json
vendored
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"cmake.configureOnOpen": false,
|
||||
"python.pythonPath": ".venv\\Scripts\\python.exe"
|
||||
}
|
||||
41
Dockerfile
Normal file
41
Dockerfile
Normal 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
12
docker-compose.yml
Normal 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
37
pyproject.toml
Normal 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",
|
||||
]
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@@ -1,6 +1,27 @@
|
||||
from .models import Insert
|
||||
import django_filters as filters
|
||||
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 Meta:
|
||||
@@ -12,3 +33,21 @@ class DoneTable(FilterSet):
|
||||
return queryset.filter(done=True)
|
||||
else:
|
||||
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)
|
||||
)
|
||||
|
||||
@@ -1,27 +1,76 @@
|
||||
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):
|
||||
input_type = 'date'
|
||||
|
||||
class TimeInput(forms.TimeInput):
|
||||
input_type = 'time'
|
||||
|
||||
|
||||
class InputForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Insert
|
||||
fields = {"name", "phone", "description", "date", "note"}
|
||||
labels = {'name': "Name", 'phone': "Phone", 'date': "Date", 'description': "Description", 'note': "Note"}
|
||||
fields = ["name", "phone", "date", "description", "note"]
|
||||
labels = {
|
||||
'name': "Име",
|
||||
'phone': "Телефон",
|
||||
'date': "Датум на прием",
|
||||
'description': "Опис на проблем",
|
||||
'note': "Забелешка"
|
||||
}
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'date': DateInput(),
|
||||
'description': forms.Textarea(attrs={'class': 'form-control'}),
|
||||
'note': forms.TextInput(attrs={'class': 'form-control'})
|
||||
'date': DateInput(format='%Y-%m-%d'),
|
||||
'description': forms.Textarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
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 Meta:
|
||||
# model = Insert
|
||||
# fields = {"name", "phone", "description", "done"}
|
||||
|
||||
18
serviceCRM/migrations/0006_insert_date_close.py
Normal file
18
serviceCRM/migrations/0006_insert_date_close.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
serviceCRM/migrations/0007_insert_status.py
Normal file
18
serviceCRM/migrations/0007_insert_status.py
Normal 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),
|
||||
),
|
||||
]
|
||||
19
serviceCRM/migrations/0008_insert_ticket_id.py
Normal file
19
serviceCRM/migrations/0008_insert_ticket_id.py
Normal 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),
|
||||
),
|
||||
]
|
||||
19
serviceCRM/migrations/0009_alter_insert_ticket_id.py
Normal file
19
serviceCRM/migrations/0009_alter_insert_ticket_id.py
Normal 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),
|
||||
),
|
||||
]
|
||||
27
serviceCRM/migrations/0010_ticketlog.py
Normal file
27
serviceCRM/migrations/0010_ticketlog.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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='Статус'),
|
||||
),
|
||||
]
|
||||
25
serviceCRM/migrations/0012_todo.py
Normal file
25
serviceCRM/migrations/0012_todo.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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='Време'),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +1,78 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import UserManager
|
||||
from nanoid import generate
|
||||
|
||||
def generate_nanoid():
|
||||
return generate(size=12)
|
||||
|
||||
class Insert(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
phone = models.CharField(max_length=20)
|
||||
description = models.CharField(max_length=300)
|
||||
note= models.CharField(max_length=100, default=None, blank=True, null=True)
|
||||
date = models.DateField("date submitted")
|
||||
done = models.BooleanField(default=False)
|
||||
repair = models.CharField(default=None, blank=True, null=True,max_length=300)
|
||||
plateno = models.CharField(max_length=10, default=None, blank=True, null=True)
|
||||
ticket_id = models.CharField(max_length=12, default=generate_nanoid, unique=True, editable=False)
|
||||
name = models.CharField(max_length=50, verbose_name="Име")
|
||||
phone = models.CharField(max_length=20, verbose_name="Телефон")
|
||||
description = models.CharField(max_length=300, verbose_name="Опис")
|
||||
note= models.CharField(max_length=100, default=None, blank=True, null=True, verbose_name="Забелешка")
|
||||
date = models.DateField("Датум")
|
||||
done = models.BooleanField(default=False, verbose_name="Завршено")
|
||||
|
||||
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):
|
||||
return "Ime: " + self.name + " Telefonski broj: " + self.phone + "\nDefekt: " + self.description + "\nDatum: \n"
|
||||
|
||||
def isDone(self):
|
||||
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}"
|
||||
|
||||
@@ -25,10 +25,16 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
SECRET_KEY = env('SECRET_KEY')
|
||||
|
||||
# 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
|
||||
|
||||
@@ -39,22 +45,31 @@ INSTALLED_APPS = [
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
# 'whitenoise.runserver_nostatic', # Removed to allow default Django static serving in dev
|
||||
'django.contrib.staticfiles',
|
||||
'crispy_forms',
|
||||
'crispy_bootstrap5',
|
||||
'crispy_tailwind',
|
||||
'django_tables2',
|
||||
'django_filters',
|
||||
]
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind"
|
||||
CRISPY_TEMPLATE_PACK = "tailwind"
|
||||
DJANGO_TABLES2_TABLE_ATTRS = {
|
||||
'class': 'table table-hover',
|
||||
'class': 'min-w-full divide-y divide-gray-200 border',
|
||||
'thead': {
|
||||
'class': 'table-light',
|
||||
'class': 'bg-gray-50',
|
||||
},
|
||||
'tbody': {
|
||||
'class': 'bg-white divide-y divide-gray-200',
|
||||
},
|
||||
}
|
||||
DJANGO_TABLES2_TEMPLATE = "table_tailwind.html"
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware', # Add whitenoise middleware
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
@@ -88,12 +103,8 @@ WSGI_APPLICATION = 'serviceCRM.wsgi.application'
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': env('DATABASE_NAME'),
|
||||
'USER': env('DATABASE_USER'),
|
||||
'PASSWORD': env('DATABASE_PASS'),
|
||||
'HOST': env('DATABASE_HOST'),
|
||||
'PORT': env('DATABASE_PORT'),
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +131,20 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
# Internationalization
|
||||
# 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
|
||||
|
||||
@@ -132,9 +154,17 @@ USE_TZ = True
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# 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
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGIN_URL = '/admin/login/'
|
||||
|
||||
@@ -6,18 +6,36 @@ from .models import Insert
|
||||
|
||||
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:
|
||||
model = Insert
|
||||
fields = ("id","name","phone","description","date","done")
|
||||
fields = ("ticket_id","name","phone","description","date","status","done")
|
||||
per_page = 5
|
||||
|
||||
|
||||
class DoneInsertTable(InsertTable):
|
||||
class Meta:
|
||||
model = Insert
|
||||
fields = ("id","name","phone","description","date","done")
|
||||
fields = ("ticket_id","name","phone","description","date", "date_close", "done")
|
||||
per_page = 5
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -1,43 +1,158 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"/>
|
||||
<title>Service </title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="http://code.jquery.com/jquery-3.3.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.full.min.js"></script>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
|
||||
<nav class="navbar navbar-inverse">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">Service CRM</a>
|
||||
{% load static i18n %}
|
||||
<html lang="mk">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Service CRM</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900">
|
||||
<nav class="bg-white shadow">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
{% 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>
|
||||
<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>
|
||||
<!-- 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>
|
||||
</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">
|
||||
<div class="col-8">
|
||||
<script>
|
||||
function toggleMobileMenu() {
|
||||
const menu = document.getElementById('mobileMenu');
|
||||
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>
|
||||
</body>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
34
serviceCRM/templates/serviceCRM/close_ticket.html
Normal file
34
serviceCRM/templates/serviceCRM/close_ticket.html
Normal 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%}
|
||||
@@ -1,6 +1,31 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<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%}
|
||||
@@ -1,13 +1,17 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Edit{% endblock %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% block title %}Уреди{% endblock %}
|
||||
{% 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">
|
||||
<div class="form-group">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<input type="submit" class="btn btn-success" value="Update">
|
||||
<input type="submit" class="btn btn-danger" value="Delete" formaction="{% url 'delete' id=object.id %}">
|
||||
{{ form|crispy }}
|
||||
<div class="mt-5 flex space-x-3">
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock%}
|
||||
@@ -1,11 +1,16 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% 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">
|
||||
<div class="form-group">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<input type="submit" class="btn btn-success" value="Submit">
|
||||
{{ form|crispy }}
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock%}
|
||||
@@ -1,5 +1,55 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% 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 %}
|
||||
{% endblock%}
|
||||
@@ -1,83 +1,107 @@
|
||||
{% block content %}
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Servicing Ticket</title>
|
||||
<style>
|
||||
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">
|
||||
{% block content %}
|
||||
<div class="max-w-2xl mx-auto bg-white shadow overflow-hidden sm:rounded-lg">
|
||||
<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">
|
||||
<div class="text-center sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-bold text-black">Сервисен Налог #{{ data.ticket_id }}</h3>
|
||||
<p class="mt-1 max-w-2xl text-sm font-medium text-black">Креиран на {{ data.date|date:"d/m/y" }} (Интерен ID: {{ data.id }})</p>
|
||||
</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 class="flex space-x-3">
|
||||
<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">
|
||||
<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">
|
||||
<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" />
|
||||
</svg>
|
||||
Печати Налепница
|
||||
</a>
|
||||
<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">
|
||||
<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="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" />
|
||||
</svg>
|
||||
Печати Потврда
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height:150px;"></div>
|
||||
<div class="ticket">
|
||||
<div class="ticket-info">
|
||||
<label>Име и презиме:</label> {{ name }}
|
||||
<div class="border-t border-gray-200">
|
||||
<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="ticket-info">
|
||||
<label>Телефонски број:</label> {{ phone }}
|
||||
<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="ticket-info">
|
||||
<label>Датум:</label> {{ date }}
|
||||
<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="ticket-description">
|
||||
<label><b>Опис на дефект:</b></label> {{ desc }}
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
</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 %}
|
||||
|
||||
41
serviceCRM/templates/serviceCRM/print_label.html
Normal file
41
serviceCRM/templates/serviceCRM/print_label.html
Normal 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>
|
||||
209
serviceCRM/templates/serviceCRM/print_receipt.html
Normal file
209
serviceCRM/templates/serviceCRM/print_receipt.html
Normal 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>
|
||||
40
serviceCRM/templates/serviceCRM/public_track_form.html
Normal file
40
serviceCRM/templates/serviceCRM/public_track_form.html
Normal 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 %}
|
||||
101
serviceCRM/templates/serviceCRM/public_track_result.html
Normal file
101
serviceCRM/templates/serviceCRM/public_track_result.html
Normal 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">
|
||||
← Назад
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
91
serviceCRM/templates/serviceCRM/todo_edit.html
Normal file
91
serviceCRM/templates/serviceCRM/todo_edit.html
Normal 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 %}
|
||||
267
serviceCRM/templates/serviceCRM/todo_list.html
Normal file
267
serviceCRM/templates/serviceCRM/todo_list.html
Normal 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 %}
|
||||
125
serviceCRM/templates/table_tailwind.html
Normal file
125
serviceCRM/templates/table_tailwind.html
Normal 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 %}
|
||||
@@ -19,13 +19,30 @@ from django.urls import path
|
||||
import serviceCRM.views as view
|
||||
|
||||
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("insert/", view.InsertNew.insert, name="insert"),
|
||||
path("edit/<int:pk>/", view.Update.as_view(), name="update"),
|
||||
path("nalog/<int:id>/", view.Nalog, name="nalog"),
|
||||
path("delete/<int:id>/", view.Delete.delete, name="delete"),
|
||||
path("logout/", view.logout_view, name="logout"),
|
||||
path("track/", view.track_ticket, name="track_ticket"),
|
||||
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/<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"),
|
||||
|
||||
# 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'),
|
||||
]
|
||||
|
||||
@@ -1,74 +1,233 @@
|
||||
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.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 .models import Insert
|
||||
from .forms import InputForm, CloseForm, TodoForm
|
||||
from .models import Insert, TicketLog, Todo
|
||||
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
|
||||
|
||||
class InsertListView(SingleTableView):
|
||||
class InsertListView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
||||
model = Insert
|
||||
table_class = InsertTable
|
||||
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):
|
||||
model = Insert
|
||||
template_name = "serviceCRM/form.html"
|
||||
|
||||
@login_required
|
||||
def insert(request):
|
||||
if request.method == 'POST':
|
||||
form = InputForm(request.POST)
|
||||
if form.is_valid():
|
||||
ticket = form.save()
|
||||
print("Raboti")
|
||||
return HttpResponseRedirect(f"/nalog/{ticket.id}/")
|
||||
TicketLog.objects.create(ticket=ticket, action="Креирано", details="Налогот е креиран во системот.")
|
||||
return HttpResponseRedirect(f"/ticket/{ticket.ticket_id}/")
|
||||
else:
|
||||
form = InputForm()
|
||||
form = InputForm(initial={'date': timezone.localdate()})
|
||||
|
||||
return render(request, InsertNew.template_name, {'form': form})
|
||||
|
||||
class Update(generic.UpdateView):
|
||||
class Update(LoginRequiredMixin, generic.UpdateView):
|
||||
model = Insert
|
||||
template_name = "serviceCRM/edit.html"
|
||||
fields = ["name", "phone", "description","note", "done", "repair", "plateno"]
|
||||
fields = ["status", "name", "phone", "description","note", "repair", "plateno", "done"]
|
||||
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):
|
||||
try:
|
||||
data = Insert.objects.get(id=id)
|
||||
except:
|
||||
return HttpResponseRedirect("/")
|
||||
template = "serviceCRM/nalog.html"
|
||||
context = {"name": data.name, "phone": data.phone, "desc": data.description, "date": data.date}
|
||||
return render(request, template, context)
|
||||
|
||||
class Done(SingleTableView):
|
||||
class CloseTicketView(LoginRequiredMixin, generic.UpdateView):
|
||||
model = Insert
|
||||
form_class = CloseForm
|
||||
template_name = "serviceCRM/close_ticket.html"
|
||||
success_url = '/done/'
|
||||
slug_field = 'ticket_id'
|
||||
slug_url_kwarg = 'ticket_id'
|
||||
|
||||
def form_valid(self, form):
|
||||
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
|
||||
table_data = Insert.objects.filter(done=True)
|
||||
table_class = DoneInsertTable
|
||||
template_name = 'serviceCRM/done.html'
|
||||
filterset_class = DoneFilter
|
||||
|
||||
def done_by_id(request, id):
|
||||
try:
|
||||
req = get_object_or_404(Insert, id=id)
|
||||
except:
|
||||
return HttpResponseRedirect("/done/")
|
||||
context = {"name": req.name, "phone": req.phone, "desc": req.description, "date": req.date}
|
||||
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")
|
||||
def get_queryset(self):
|
||||
return Insert.objects.filter(done=True)
|
||||
|
||||
@login_required
|
||||
def done_by_id(request, ticket_id):
|
||||
req = get_object_or_404(Insert, ticket_id=ticket_id)
|
||||
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):
|
||||
model = Insert
|
||||
|
||||
def delete(request, id):
|
||||
req = get_object_or_404(Insert, id=id)
|
||||
@login_required
|
||||
def delete(request, ticket_id):
|
||||
req = get_object_or_404(Insert, ticket_id=ticket_id)
|
||||
req.delete()
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
class DatatableView(DatatableView):
|
||||
class DatatableView(LoginRequiredMixin, DatatableView):
|
||||
model = Insert
|
||||
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
559
uv.lock
generated
Normal 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" },
|
||||
]
|
||||
Reference in New Issue
Block a user