mirror of
https://github.com/ferdzo/serviceCRM.git
synced 2026-04-05 13:16:24 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9556ebef38 | ||
|
|
fbb5265dd0 | ||
|
|
2e515c267a | ||
|
|
f001621cdc |
@@ -1,4 +0,0 @@
|
||||
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
58
.github/copilot-instructions.md
vendored
@@ -1,58 +0,0 @@
|
||||
# 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`.
|
||||
27
.gitignore
vendored
27
.gitignore
vendored
@@ -1,24 +1,15 @@
|
||||
# Ignore .idea folder created by PyCharm
|
||||
.idea/
|
||||
|
||||
# Ignore __pycache__ folders and .pyc files
|
||||
*__pycache__/
|
||||
*.pyc
|
||||
# Ignore venv directories
|
||||
venv/
|
||||
|
||||
# Ignore .env files
|
||||
*.env
|
||||
|
||||
# Ignore .venv folder
|
||||
.venv/
|
||||
.env
|
||||
# Ignore log files
|
||||
*.log
|
||||
.vscode
|
||||
.idea/
|
||||
|
||||
# Ignore .DS_Store files
|
||||
.DS_Store
|
||||
|
||||
.vscode
|
||||
|
||||
db.sqlite3
|
||||
|
||||
pyvenv.cfg
|
||||
|
||||
|
||||
|
||||
# Ignore .pytest_cache directory
|
||||
__pycache__/
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
3.14
|
||||
4
.unotes/templates/title_date.hbs
Normal file
4
.unotes/templates/title_date.hbs
Normal file
@@ -0,0 +1,4 @@
|
||||
# {{capitalizeAll title}}
|
||||
|
||||
{{formatDate date "llll"}}
|
||||
|
||||
19
.vscode/launch.json
vendored
Normal file
19
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
// 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
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"cmake.configureOnOpen": false,
|
||||
"python.pythonPath": ".venv\\Scripts\\python.exe"
|
||||
}
|
||||
41
Dockerfile
41
Dockerfile
@@ -1,41 +0,0 @@
|
||||
# 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"]
|
||||
22
backend/.gitignore
vendored
Normal file
22
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Ignore .idea folder created by PyCharm
|
||||
.idea/
|
||||
|
||||
# Ignore __pycache__ folders and .pyc files
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Ignore .env files
|
||||
*.env
|
||||
|
||||
# Ignore .venv folder
|
||||
.venv/
|
||||
|
||||
# Ignore .DS_Store files
|
||||
.DS_Store
|
||||
|
||||
.vscode\
|
||||
|
||||
pyvenv.cfg
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
14
backend/serviceCRM/filter.py
Normal file
14
backend/serviceCRM/filter.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from .models import Insert
|
||||
import django_filters as filters
|
||||
from django_filters import FilterSet
|
||||
|
||||
class DoneTable(FilterSet):
|
||||
class Meta:
|
||||
model = Insert
|
||||
fields = ["done"]
|
||||
|
||||
def filter_done(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(done=True)
|
||||
else:
|
||||
return queryset.filter(done=False)
|
||||
35
backend/serviceCRM/forms.py
Normal file
35
backend/serviceCRM/forms.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django import forms
|
||||
from .models import Insert
|
||||
|
||||
|
||||
class DateInput(forms.DateInput):
|
||||
input_type = 'date'
|
||||
|
||||
|
||||
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"}
|
||||
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'})
|
||||
}
|
||||
|
||||
field_order = ["name", "phone", "date", "description", "done"]
|
||||
|
||||
# class EditForm(forms.ModelForm):
|
||||
# class Meta:
|
||||
# model = Insert
|
||||
# fields = {"name", "phone", "description", "done"}
|
||||
# labels = {'name': "Name", 'phone': "Phone", 'description': "Description", 'done': "Done"}
|
||||
# widgets = {
|
||||
# 'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
# 'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
# 'description': forms.Textarea(attrs={'class': 'form-control'})
|
||||
# }
|
||||
|
||||
# field_order = ["name", "phone", "description", "done"]
|
||||
19
backend/serviceCRM/models.py
Normal file
19
backend/serviceCRM/models.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import UserManager
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return "Ime: " + self.name + " Telefonski broj: " + self.phone + "\nDefekt: " + self.description + "\nDatum: \n"
|
||||
|
||||
def isDone(self):
|
||||
return self.done
|
||||
@@ -13,7 +13,7 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
from pathlib import Path
|
||||
import environ
|
||||
env = environ.Env()
|
||||
environ.Env.read_env('.env')
|
||||
environ.Env.read_env('../.env')
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
@@ -25,16 +25,10 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
SECRET_KEY = env('SECRET_KEY')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = env.bool('DEBUG', default=False)
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*'])
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
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
|
||||
|
||||
@@ -45,36 +39,29 @@ 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_tailwind',
|
||||
'django_tables2',
|
||||
'django_filters',
|
||||
'rest_framework',
|
||||
'corsheaders',
|
||||
]
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind"
|
||||
CRISPY_TEMPLATE_PACK = "tailwind"
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||
DJANGO_TABLES2_TABLE_ATTRS = {
|
||||
'class': 'min-w-full divide-y divide-gray-200 border',
|
||||
'class': 'table table-hover',
|
||||
'thead': {
|
||||
'class': 'bg-gray-50',
|
||||
},
|
||||
'tbody': {
|
||||
'class': 'bg-white divide-y divide-gray-200',
|
||||
'class': 'table-light',
|
||||
},
|
||||
}
|
||||
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',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'serviceCRM.urls'
|
||||
@@ -103,12 +90,16 @@ WSGI_APPLICATION = 'serviceCRM.wsgi.application'
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
'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'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
@@ -131,20 +122,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'mk'
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
LANGUAGES = [
|
||||
('mk', _('Macedonian')),
|
||||
('en', _('English')),
|
||||
]
|
||||
|
||||
LOCALE_PATHS = [
|
||||
BASE_DIR / 'locale',
|
||||
]
|
||||
|
||||
TIME_ZONE = 'Europe/Skopje'
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@@ -154,17 +134,9 @@ USE_TZ = True
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / "serviceCRM" / "static",
|
||||
]
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
# 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/'
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
25
backend/serviceCRM/tables.py
Normal file
25
backend/serviceCRM/tables.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2 import TemplateColumn
|
||||
from django_filters import FilterSet
|
||||
from django.db.models.query import QuerySet # Add missing import
|
||||
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>')
|
||||
|
||||
class Meta:
|
||||
model = Insert
|
||||
fields = ("id","name","phone","description","date","done")
|
||||
per_page = 5
|
||||
|
||||
|
||||
class DoneInsertTable(InsertTable):
|
||||
class Meta:
|
||||
model = Insert
|
||||
fields = ("id","name","phone","description","date","done")
|
||||
per_page = 5
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
print(self.data)
|
||||
43
backend/serviceCRM/templates/base.html
Normal file
43
backend/serviceCRM/templates/base.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!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>
|
||||
</div>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="/insert"><span class="glyphicon glyphicon-user"></span>Insert</a></li>
|
||||
<li><a href="/admin"><span class="glyphicon glyphicon-log-in"></span> Admin</a></li>
|
||||
<li><a href="/done"><span class="glyphicon glyphicon-log-in"></span> Done</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<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">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
6
backend/serviceCRM/templates/serviceCRM/done.html
Normal file
6
backend/serviceCRM/templates/serviceCRM/done.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block content %}
|
||||
{% render_table table %}
|
||||
{%endblock%}
|
||||
13
backend/serviceCRM/templates/serviceCRM/edit.html
Normal file
13
backend/serviceCRM/templates/serviceCRM/edit.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Edit{% endblock %}
|
||||
{% block content %}
|
||||
{% csrf_token %}
|
||||
<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 %}">
|
||||
</div>
|
||||
</form>
|
||||
{% endblock%}
|
||||
11
backend/serviceCRM/templates/serviceCRM/form.html
Normal file
11
backend/serviceCRM/templates/serviceCRM/form.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
{% csrf_token %}
|
||||
<form action="/insert/" method="post">
|
||||
<div class="form-group">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<input type="submit" class="btn btn-success" value="Submit">
|
||||
</div>
|
||||
</form>
|
||||
{% endblock%}
|
||||
5
backend/serviceCRM/templates/serviceCRM/list.html
Normal file
5
backend/serviceCRM/templates/serviceCRM/list.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% block content %}
|
||||
{% render_table table %}
|
||||
{% endblock%}
|
||||
83
backend/serviceCRM/templates/serviceCRM/nalog.html
Normal file
83
backend/serviceCRM/templates/serviceCRM/nalog.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% block content %}
|
||||
{% 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">
|
||||
</div>
|
||||
<div class="ticket">
|
||||
<div class="ticket-info">
|
||||
<label>Име и презиме:</label> {{ name }}
|
||||
</div>
|
||||
<div class="ticket-info">
|
||||
<label>Телефонски број:</label> {{phone}}
|
||||
</div>
|
||||
<div class="ticket-info">
|
||||
<label>Датум:</label> {{date}}
|
||||
</div>
|
||||
<div class="ticket-description">
|
||||
<label><b>Опис на дефект:</b></label> {{ desc }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="height:150px;"></div>
|
||||
<div class="ticket">
|
||||
<div class="ticket-info">
|
||||
<label>Име и презиме:</label> {{ name }}
|
||||
</div>
|
||||
<div class="ticket-info">
|
||||
<label>Телефонски број:</label> {{ phone }}
|
||||
</div>
|
||||
<div class="ticket-info">
|
||||
<label>Датум:</label> {{ date }}
|
||||
</div>
|
||||
<div class="ticket-description">
|
||||
<label><b>Опис на дефект:</b></label> {{ desc }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
34
backend/serviceCRM/urls.py
Normal file
34
backend/serviceCRM/urls.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
URL configuration for serviceCRM project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views.py. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views.py
|
||||
1. Add an import: from my_app import views.py
|
||||
2. Add a URL to urlpatterns: path('', views.py.home, name='home')
|
||||
Class-based views.py
|
||||
1. Add an import: from other_app.views.py import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
import serviceCRM.views as view
|
||||
from rest_framework import routers
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("", view.InsertListView.as_view(), name="index"),
|
||||
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("done/", view.Done.as_view(), name="done"),
|
||||
path("done/<int:id>/", view.Done.done_by_id, name="done"),
|
||||
path("api/", view.get_all_inserts, name="api"),
|
||||
# path("datatable/", view.DatatableView.as_view(), name="datatable"),
|
||||
]
|
||||
79
backend/serviceCRM/views.py
Normal file
79
backend/serviceCRM/views.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.views import generic
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from .forms import InputForm
|
||||
from .models import Insert
|
||||
from .tables import DoneInsertTable, InsertTable
|
||||
|
||||
from django_tables2 import SingleTableView
|
||||
# from datatableview.views import DatatableView
|
||||
|
||||
class InsertListView(SingleTableView):
|
||||
model = Insert
|
||||
table_class = InsertTable
|
||||
template_name = 'serviceCRM/list.html'
|
||||
|
||||
class InsertNew(generic.View):
|
||||
model = Insert
|
||||
template_name = "serviceCRM/form.html"
|
||||
|
||||
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}/")
|
||||
else:
|
||||
form = InputForm()
|
||||
|
||||
return render(request, InsertNew.template_name, {'form': form})
|
||||
|
||||
class Update(generic.UpdateView):
|
||||
model = Insert
|
||||
template_name = "serviceCRM/edit.html"
|
||||
fields = ["name", "phone", "description","note", "done", "repair", "plateno"]
|
||||
success_url = '/'
|
||||
|
||||
|
||||
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):
|
||||
model = Insert
|
||||
table_data = Insert.objects.filter(done=True)
|
||||
table_class = DoneInsertTable
|
||||
template_name = 'serviceCRM/done.html'
|
||||
|
||||
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")
|
||||
|
||||
class Delete(generic.View):
|
||||
model = Insert
|
||||
|
||||
def delete(request, id):
|
||||
req = get_object_or_404(Insert, id=id)
|
||||
req.delete()
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
# class DatatableView(DatatableView):
|
||||
# model = Insert
|
||||
# template_name = 'serviceCRM/Insert_list.html'
|
||||
|
||||
@api_view(["GET"])
|
||||
def get_all_inserts(request):
|
||||
inserts = Insert.objects.all()
|
||||
return Response({"inserts": list(inserts.values())})
|
||||
@@ -1,12 +0,0 @@
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./db.sqlite3:/app/db.sqlite3
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DEBUG=False
|
||||
- ALLOWED_HOSTS=*
|
||||
23
frontend/servicecrm/.gitignore
vendored
Normal file
23
frontend/servicecrm/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
70
frontend/servicecrm/README.md
Normal file
70
frontend/servicecrm/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
33921
frontend/servicecrm/package-lock.json
generated
Normal file
33921
frontend/servicecrm/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
frontend/servicecrm/package.json
Normal file
49
frontend/servicecrm/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "servicecrm",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.8.2",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.6.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap5": "^1.1.9",
|
||||
"framer-motion": "^11.0.18",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^3.4.1"
|
||||
}
|
||||
}
|
||||
BIN
frontend/servicecrm/public/favicon.ico
Normal file
BIN
frontend/servicecrm/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
54
frontend/servicecrm/public/index.html
Normal file
54
frontend/servicecrm/public/index.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/react/umd/react.production.min.js" crossorigin></script>
|
||||
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/react-dom/umd/react-dom.production.min.js"
|
||||
crossorigin></script>
|
||||
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/react-bootstrap@next/dist/react-bootstrap.min.js"
|
||||
crossorigin></script>
|
||||
|
||||
<script>var Alert = ReactBootstrap.Alert;</script>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
BIN
frontend/servicecrm/public/logo192.png
Normal file
BIN
frontend/servicecrm/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/servicecrm/public/logo512.png
Normal file
BIN
frontend/servicecrm/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
frontend/servicecrm/public/manifest.json
Normal file
25
frontend/servicecrm/public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
frontend/servicecrm/public/robots.txt
Normal file
3
frontend/servicecrm/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
38
frontend/servicecrm/src/App.css
Normal file
38
frontend/servicecrm/src/App.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
21
frontend/servicecrm/src/App.jsx
Normal file
21
frontend/servicecrm/src/App.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import './App.css';
|
||||
import NavigationBar from './components/NavigationBar';
|
||||
import Table from './components/Table';
|
||||
import MyButton from './components/Btn';
|
||||
import Search from './components/Search';
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
|
||||
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<NavigationBar/>
|
||||
|
||||
<Table/>
|
||||
<MyButton/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
8
frontend/servicecrm/src/App.test.js
Normal file
8
frontend/servicecrm/src/App.test.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
26
frontend/servicecrm/src/components/Btn.jsx
Normal file
26
frontend/servicecrm/src/components/Btn.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import {useState} from 'react';
|
||||
|
||||
function MyButton(){
|
||||
const [count, setCount]=useState(0);
|
||||
|
||||
function handleClick(){
|
||||
setCount(count+1);
|
||||
}
|
||||
|
||||
return(
|
||||
<div>
|
||||
<Dugme count={count} handleClick={handleClick}/>
|
||||
<Dugme count={count} handleClick={handleClick}/>
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
function Dugme({count, handleClick}){
|
||||
return(
|
||||
<button onClick={handleClick}>Clicked {count} times</button>
|
||||
)
|
||||
}
|
||||
export default MyButton;
|
||||
23
frontend/servicecrm/src/components/NavigationBar.jsx
Normal file
23
frontend/servicecrm/src/components/NavigationBar.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Navbar, Nav } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
|
||||
|
||||
function NavigationBar() {
|
||||
return (
|
||||
<Navbar bg="light" expand="lg">
|
||||
<Navbar.Brand href="#">Navbar</Navbar.Brand>
|
||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||
<Navbar.Collapse id="basic-navbar-nav">
|
||||
<Nav className="mr-auto">
|
||||
<Nav.Link href="/">Home</Nav.Link>
|
||||
<Nav.Link href="/done">Done</Nav.Link>
|
||||
<Nav.Link href="http://localhost:8000/admin">Admin</Nav.Link>
|
||||
<Nav.Link href="#" disabled>Disabled</Nav.Link>
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavigationBar;
|
||||
20
frontend/servicecrm/src/components/Row.jsx
Normal file
20
frontend/servicecrm/src/components/Row.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
function TableRow({ item }) {
|
||||
const isDone = item.done? "✅":"❌";
|
||||
return (
|
||||
<tr>
|
||||
<th scope="row">{item.id}</th>
|
||||
<td>{item.name}</td>
|
||||
<td>{item.date}</td>
|
||||
<td>{item.phone}</td>
|
||||
<td>{item.description}</td>
|
||||
<td>{item.repair}</td>
|
||||
<td>{item.plateno}</td>
|
||||
<td>{item.note}</td>
|
||||
<td>{isDone}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default TableRow;
|
||||
30
frontend/servicecrm/src/components/Search.jsx
Normal file
30
frontend/servicecrm/src/components/Search.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
function Search( props ) {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const onChange = (event) => {
|
||||
setValue(event.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
props.handleValue(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Search;
|
||||
65
frontend/servicecrm/src/components/Table.jsx
Normal file
65
frontend/servicecrm/src/components/Table.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import Row from "./Row";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import Search from "./Search";
|
||||
|
||||
export default function Table() {
|
||||
const [data, setData] = useState([]);
|
||||
const [filteredData, setFilteredData] = useState([]);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
axios.get("http://localhost:8000/api/").then((response) => {
|
||||
setData(response.data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchValue){
|
||||
debugger;
|
||||
for (const item in data) {
|
||||
if (item.name.toLowerCase().includes(searchValue.toLowerCase())){
|
||||
setFilteredData((prev) => {
|
||||
return [...prev, item];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [searchValue, data]);
|
||||
|
||||
const Done = data?.inserts?.filter((item) => item.done === true);
|
||||
const NotDone = data?.inserts?.filter((item) => item.done === false);
|
||||
var isDone = true;
|
||||
|
||||
const handleValue = (value) => {
|
||||
setSearchValue(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Search data={data} handleValue={handleValue} />
|
||||
|
||||
<table className="table"></table>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Phone</th>
|
||||
<th scope="col">Description</th>
|
||||
<th scope="col">Repair</th>
|
||||
<th scope="col">Plateno</th>
|
||||
<th scope="col">Note</th>
|
||||
<th scope="col">Done</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredData.map((item) => {
|
||||
return <Row key={item.id} item={item} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
frontend/servicecrm/src/index.css
Normal file
13
frontend/servicecrm/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
17
frontend/servicecrm/src/index.js
Normal file
17
frontend/servicecrm/src/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
1
frontend/servicecrm/src/logo.svg
Normal file
1
frontend/servicecrm/src/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
13
frontend/servicecrm/src/reportWebVitals.js
Normal file
13
frontend/servicecrm/src/reportWebVitals.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
5
frontend/servicecrm/src/setupTests.js
Normal file
5
frontend/servicecrm/src/setupTests.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
9
frontend/servicecrm/tailwind.config.js
Normal file
9
frontend/servicecrm/tailwind.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
[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",
|
||||
]
|
||||
@@ -1,53 +0,0 @@
|
||||
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:
|
||||
model = Insert
|
||||
fields = ["done"]
|
||||
|
||||
def filter_done(self, queryset, name, value):
|
||||
if value:
|
||||
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,84 +0,0 @@
|
||||
from django import forms
|
||||
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", "date", "description", "note"]
|
||||
labels = {
|
||||
'name': "Име",
|
||||
'phone': "Телефон",
|
||||
'date': "Датум на прием",
|
||||
'description': "Опис на проблем",
|
||||
'note': "Забелешка"
|
||||
}
|
||||
widgets = {
|
||||
'date': DateInput(format='%Y-%m-%d'),
|
||||
'description': forms.Textarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
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"}
|
||||
# labels = {'name': "Name", 'phone': "Phone", 'description': "Description", 'done': "Done"}
|
||||
# widgets = {
|
||||
# 'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
# 'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
# 'description': forms.Textarea(attrs={'class': 'form-control'})
|
||||
# }
|
||||
|
||||
# field_order = ["name", "phone", "description", "done"]
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,63 +0,0 @@
|
||||
# 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='Статус'),
|
||||
),
|
||||
]
|
||||
@@ -1,25 +0,0 @@
|
||||
# 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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='Време'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,78 +0,0 @@
|
||||
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):
|
||||
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}"
|
||||
@@ -1,43 +0,0 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2 import TemplateColumn
|
||||
from django_filters import FilterSet
|
||||
from django.db.models.query import QuerySet # Add missing import
|
||||
from .models import Insert
|
||||
|
||||
class InsertTable(tables.Table):
|
||||
|
||||
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 = ("ticket_id","name","phone","description","date","status","done")
|
||||
per_page = 5
|
||||
|
||||
|
||||
class DoneInsertTable(InsertTable):
|
||||
class Meta:
|
||||
model = Insert
|
||||
fields = ("ticket_id","name","phone","description","date", "date_close", "done")
|
||||
per_page = 5
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
print(self.data)
|
||||
@@ -1,158 +0,0 @@
|
||||
<!doctype html>
|
||||
{% 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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,34 +0,0 @@
|
||||
{% 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,31 +0,0 @@
|
||||
{% 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,17 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% block title %}Уреди{% endblock %}
|
||||
{% block content %}
|
||||
<div class="bg-white shadow sm:rounded-lg mb-6">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ 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,16 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-white shadow sm:rounded-lg mb-6">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<form action="/insert/" method="post">
|
||||
{% csrf_token %}
|
||||
{{ 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,55 +0,0 @@
|
||||
{% 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,107 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% 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="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 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="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 border-t border-gray-200">
|
||||
<dt class="text-sm font-bold text-black">Телефонски број</dt>
|
||||
<dd class="mt-1 text-sm font-bold text-black sm:mt-0 sm:col-span-2">{{ data.phone }}</dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 border-t border-gray-200">
|
||||
<dt class="text-sm font-bold text-black">Опис на дефект</dt>
|
||||
<dd class="mt-1 text-sm font-bold text-black sm:mt-0 sm:col-span-2">{{ data.description }}</dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 border-t border-gray-200">
|
||||
<dt class="text-sm font-bold text-black">Статус</dt>
|
||||
<dd class="mt-1 text-sm font-bold text-black sm:mt-0 sm:col-span-2">{{ data.get_status_display }}</dd>
|
||||
</div>
|
||||
{% if data.note %}
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 border-t border-gray-200">
|
||||
<dt class="text-sm font-bold text-black">Интерна забелешка</dt>
|
||||
<dd class="mt-1 text-sm font-bold text-black sm:mt-0 sm:col-span-2">{{ data.note }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if data.done %}
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">Детали за поправка</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ data.repair }}</dd>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">Плаќање/Регистарски број</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ data.plateno }}</dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">Датум на затворање</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ data.date_close|date:"d/m/y" }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Ticket History -->
|
||||
<div class="bg-gray-50 px-4 py-5 sm:px-6 border-t border-gray-200">
|
||||
<h4 class="text-md leading-6 font-medium text-gray-900 mb-4">Историја на активности</h4>
|
||||
<div class="flow-root">
|
||||
<ul role="list" class="-mb-8">
|
||||
{% for log in data.logs.all %}
|
||||
<li>
|
||||
<div class="relative pb-8">
|
||||
{% if not forloop.last %}
|
||||
<span class="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200" aria-hidden="true"></span>
|
||||
{% endif %}
|
||||
<div class="relative flex space-x-3">
|
||||
<div>
|
||||
<span class="h-8 w-8 rounded-full bg-gray-500 flex items-center justify-center ring-8 ring-white">
|
||||
<svg class="h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">{{ log.details }} <span class="font-medium text-gray-900">({{ log.action }})</span></p>
|
||||
</div>
|
||||
<div class="text-right text-sm whitespace-nowrap text-gray-500">
|
||||
<time datetime="{{ log.timestamp|date:'Y-m-d H:i' }}">{{ log.timestamp|date:"M d, H:i" }}</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="text-sm text-gray-500">Нема историја.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3 bg-white border-t border-gray-200 text-right sm:px-6">
|
||||
<a href="{% url 'dashboard' %}" class="text-indigo-600 hover:text-indigo-900">Назад кон листа</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,41 +0,0 @@
|
||||
{% 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>
|
||||
@@ -1,209 +0,0 @@
|
||||
{% 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>
|
||||
@@ -1,40 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,101 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,91 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,267 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,125 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,48 +0,0 @@
|
||||
"""
|
||||
URL configuration for serviceCRM project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views.py. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views.py
|
||||
1. Add an import: from my_app import views.py
|
||||
2. Add a URL to urlpatterns: path('', views.py.home, name='home')
|
||||
Class-based views.py
|
||||
1. Add an import: from other_app.views.py import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
import serviceCRM.views as view
|
||||
|
||||
urlpatterns = [
|
||||
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("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/<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,233 +0,0 @@
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
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, CloseForm, TodoForm
|
||||
from .models import Insert, TicketLog, Todo
|
||||
from .tables import DoneInsertTable, InsertTable
|
||||
from .filter import InsertFilter, DoneFilter
|
||||
|
||||
from django_tables2.views import SingleTableMixin,SingleTableView
|
||||
from django_filters.views import FilterView
|
||||
from datatableview.views import DatatableView
|
||||
|
||||
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()
|
||||
TicketLog.objects.create(ticket=ticket, action="Креирано", details="Налогот е креиран во системот.")
|
||||
return HttpResponseRedirect(f"/ticket/{ticket.ticket_id}/")
|
||||
else:
|
||||
form = InputForm(initial={'date': timezone.localdate()})
|
||||
|
||||
return render(request, InsertNew.template_name, {'form': form})
|
||||
|
||||
class Update(LoginRequiredMixin, generic.UpdateView):
|
||||
model = Insert
|
||||
template_name = "serviceCRM/edit.html"
|
||||
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
|
||||
|
||||
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_class = DoneInsertTable
|
||||
template_name = 'serviceCRM/done.html'
|
||||
filterset_class = DoneFilter
|
||||
|
||||
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
|
||||
|
||||
@login_required
|
||||
def delete(request, ticket_id):
|
||||
req = get_object_or_404(Insert, ticket_id=ticket_id)
|
||||
req.delete()
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
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
559
uv.lock
generated
@@ -1,559 +0,0 @@
|
||||
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