Added a todo, improved mobile experience.

This commit is contained in:
2026-01-27 23:49:56 +01:00
parent d2651c2be3
commit 53bb782bc9
14 changed files with 665 additions and 27 deletions

View File

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

View File

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

View File

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

View File

@@ -50,6 +50,21 @@ class Insert(models.Model):
def isDone(self): def isDone(self):
return self.done return self.done
class Todo(models.Model):
title = models.CharField(max_length=200, verbose_name="Task")
name = models.CharField(max_length=100, verbose_name="Име", blank=True, null=True)
phone = models.CharField(max_length=20, verbose_name="Телефон", blank=True, null=True)
scheduled_date = models.DateField(verbose_name="Датум", blank=True, null=True)
scheduled_time = models.TimeField(verbose_name="Време", blank=True, null=True)
is_completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
class Meta:
ordering = ['is_completed', 'scheduled_date', 'scheduled_time', '-created_at']
class TicketLog(models.Model): class TicketLog(models.Model):
ticket = models.ForeignKey(Insert, on_delete=models.CASCADE, related_name='logs') ticket = models.ForeignKey(Insert, on_delete=models.CASCADE, related_name='logs')
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)

View File

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

View File

@@ -33,7 +33,7 @@
<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"> <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" /> <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> </svg>
Активни налози Активни
</a> </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"> <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"> <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">
@@ -41,25 +41,31 @@
</svg> </svg>
Архива Архива
</a> </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 %} {% 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"> <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"> <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" /> <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> </svg>
Проверка на статус Статус
</a> </a>
</div> </div>
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a href="{% url 'insert' %}" class="bg-gray-900 text-white hover:bg-gray-800 px-4 py-2 rounded-md text-sm font-medium transition duration-150 ease-in-out shadow-sm flex items-center"> <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"> <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" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
Нов Налог Нов Налог
</a> </a>
<div class="ml-3 relative flex items-center space-x-3"> <div class="hidden sm:flex ml-3 items-center space-x-3">
<span class="text-sm text-gray-500">Здраво, <strong>{{ user.username }}</strong></span> <span class="text-sm text-gray-500">{{ user.username }}</span>
<a href="{% url 'logout' %}" class="text-gray-400 hover:text-gray-500"> <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"> <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" /> <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" />
@@ -67,10 +73,70 @@
</a> </a>
</div> </div>
{% endif %} {% 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> </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> </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"> <main class="max-w-7xl mx-auto py-8 sm:px-6 lg:px-8">
<div class="px-4 py-4 sm:px-0"> <div class="px-4 py-4 sm:px-0">

View File

@@ -2,10 +2,10 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="max-w-2xl mx-auto bg-white shadow overflow-hidden sm:rounded-lg"> <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 justify-between items-center"> <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> <div class="text-center sm:text-left">
<h3 class="text-lg leading-6 font-bold text-black">Сервисен Налог #{{ data.ticket_id }}</h3> <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 }} (Интерен ID: {{ data.id }})</p> <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>
<div class="flex space-x-3"> <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"> <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">
@@ -57,7 +57,7 @@
</div> </div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <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> <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 }}</dd> <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> </div>
{% endif %} {% endif %}
</dl> </dl>

View File

@@ -25,7 +25,7 @@
<div class="header"> <div class="header">
<img src="{% static 'fer-logo.png' %}" style="max-width: 150px;" alt="Logo"> <img src="{% static 'fer-logo.png' %}" style="max-width: 150px;" alt="Logo">
</div> </div>
<div>{{ ticket.date }}</div> <div>{{ ticket.date|date:"d/m/y" }}</div>
<div class="id">#{{ ticket.id }}</div> <div class="id">#{{ ticket.id }}</div>
<div style="font-size: 10px; font-family: monospace;">{{ ticket.ticket_id }}</div> <div style="font-size: 10px; font-family: monospace;">{{ ticket.ticket_id }}</div>

View File

@@ -81,7 +81,7 @@
<div class="text-right"> <div class="text-right">
<div class="text-xl font-bold text-black">Налог #{{ ticket.id }}</div> <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-xs font-mono font-bold text-black mb-1">Реф: {{ ticket.ticket_id }}</div>
<div class="text-black font-bold text-sm">{{ ticket.date }}</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 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> </div>
@@ -138,7 +138,7 @@
<div class="text-right"> <div class="text-right">
<div class="text-xl font-bold text-black">Налог #{{ ticket.id }}</div> <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-xs font-mono font-bold text-black mb-1">Реф: {{ ticket.ticket_id }}</div>
<div class="text-black font-bold text-sm">{{ ticket.date }}</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 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> </div>

View File

@@ -7,13 +7,15 @@
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200"> <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"> <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 <span class="inline-flex items-center px-6 py-2 rounded-full text-lg font-bold shadow-sm
{% if ticket.done %} {% if ticket.done or ticket.status == 'READY' %}
bg-green-100 text-green-800 ring-1 ring-inset ring-green-600/20 bg-green-100 text-green-800 ring-1 ring-inset ring-green-600/20
{% else %} {% else %}
bg-yellow-100 text-yellow-800 ring-1 ring-inset ring-yellow-600/20 bg-yellow-100 text-yellow-800 ring-1 ring-inset ring-yellow-600/20
{% endif %}"> {% endif %}">
{% if ticket.done %} {% if ticket.done %}
✓ ЗАВРШЕНО ✓ ЗАВРШЕНО
{% elif ticket.status == 'READY' %}
✓ ГОТОВО ЗА ПОДИГАЊЕ
{% else %} {% else %}
ВО ИЗРАБОТКА ВО ИЗРАБОТКА
{% endif %} {% endif %}
@@ -63,7 +65,7 @@
Датум на прием Датум на прием
</dt> </dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2"> <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ ticket.date }} {{ ticket.date|date:"d/m/y" }}
</dd> </dd>
</div> </div>
@@ -82,7 +84,7 @@
Завршено на Завршено на
</dt> </dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2"> <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ ticket.date_close }} {{ ticket.date_close|date:"d/m/y" }}
</dd> </dd>
</div> </div>
{% endif %} {% endif %}

View File

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

View File

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

View File

@@ -39,4 +39,10 @@ urlpatterns = [
path("done/", view.Done.as_view(), name="done"), path("done/", view.Done.as_view(), name="done"),
path("done/<str:ticket_id>/", view.Done.done_by_id, name="done_detail"), path("done/<str:ticket_id>/", view.Done.done_by_id, name="done_detail"),
path("datatable/", view.DatatableView.as_view(), name="datatable"), path("datatable/", view.DatatableView.as_view(), name="datatable"),
# Todo List
path('todo/', view.todo_list, name='todo_list'),
path('todo/toggle/<int:pk>/', view.todo_toggle, name='todo_toggle'),
path('todo/edit/<int:pk>/', view.todo_edit, name='todo_edit'),
path('todo/delete/<int:pk>/', view.todo_delete, name='todo_delete'),
] ]

View File

@@ -6,8 +6,8 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth import logout from django.contrib.auth import logout
from .forms import InputForm, CloseForm from .forms import InputForm, CloseForm, TodoForm
from .models import Insert, TicketLog from .models import Insert, TicketLog, Todo
from .tables import DoneInsertTable, InsertTable from .tables import DoneInsertTable, InsertTable
from .filter import InsertFilter, DoneFilter from .filter import InsertFilter, DoneFilter
@@ -45,7 +45,7 @@ class InsertNew(generic.View):
TicketLog.objects.create(ticket=ticket, action="Креирано", details="Налогот е креиран во системот.") TicketLog.objects.create(ticket=ticket, action="Креирано", details="Налогот е креиран во системот.")
return HttpResponseRedirect(f"/ticket/{ticket.ticket_id}/") return HttpResponseRedirect(f"/ticket/{ticket.ticket_id}/")
else: else:
form = InputForm() form = InputForm(initial={'date': timezone.localdate()})
return render(request, InsertNew.template_name, {'form': form}) return render(request, InsertNew.template_name, {'form': form})
@@ -148,3 +148,86 @@ def home(request):
if request.user.is_authenticated: if request.user.is_authenticated:
return redirect('dashboard') return redirect('dashboard')
return track_ticket(request) 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')