Internal
Public Access
Bring offline mode to full parity with the online dashboard
Fixes subtasks appearing flattened into the main task list (missing parent filter), and adds everything else needed for full offline functionality: tag chips/assignment/creation, full field editing (description, due time, recurrence presets), time tracking with a live-updating timer, and properly nested subtasks -- all queued locally and synced via the existing /api/sync/ protocol. The offline page now reuses the real dashboard's app-layout/header/ sidebar/detail-pane structure instead of a bespoke layout, with an amber-tinted header and banner so it's unmistakable which mode you're in. The detail panel mirrors _task_detail.html's fields and actions. Also fixes a real backend gap this feature depends on: apply_conflict_data() in sync/views.py silently no-op'd on time_entry conflicts. And computes duration_seconds client-side on merge, since TimeEntrySyncSerializer doesn't include it in the wire format at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
fc045a0c4b
commit
a6f227e931
+25
-1
@@ -6,7 +6,7 @@ from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from tasks.models import Task
|
||||
from tasks.models import Task, TimeEntry
|
||||
from sync.models import SyncLog, SyncConflict
|
||||
|
||||
User = get_user_model()
|
||||
@@ -147,3 +147,27 @@ class SyncEndpointTests(TestCase):
|
||||
self.assertEqual(task.title, 'Server title')
|
||||
conflict.refresh_from_db()
|
||||
self.assertEqual(conflict.status, 'resolved_server')
|
||||
|
||||
def test_resolve_conflict_local_applies_time_entry_data(self):
|
||||
task = Task.objects.create(user=self.user, title='Timed task')
|
||||
start = timezone.now() - timedelta(hours=1)
|
||||
entry = TimeEntry.objects.create(user=self.user, task=task, started_at=start, notes='server notes')
|
||||
conflict = SyncConflict.objects.create(
|
||||
user=self.user,
|
||||
entity_type='time_entry',
|
||||
entity_id=entry.id,
|
||||
local_data={'sync_id': str(entry.sync_id), 'notes': 'local notes'},
|
||||
server_data={'notes': 'server notes'},
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
f'/api/sync/conflicts/{conflict.id}/resolve/',
|
||||
{'resolution': 'local'},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
entry.refresh_from_db()
|
||||
self.assertEqual(entry.notes, 'local notes')
|
||||
conflict.refresh_from_db()
|
||||
self.assertEqual(conflict.status, 'resolved_local')
|
||||
|
||||
@@ -474,3 +474,21 @@ def apply_conflict_data(entity_type, entity_id, data):
|
||||
tag.save()
|
||||
except Tag.DoesNotExist:
|
||||
pass
|
||||
elif entity_type == 'time_entry':
|
||||
try:
|
||||
entry = TimeEntry.objects.get(id=entity_id)
|
||||
|
||||
started_at = data.get('started_at', entry.started_at)
|
||||
if isinstance(started_at, str):
|
||||
started_at = parse_datetime(started_at)
|
||||
|
||||
ended_at = data.get('ended_at', entry.ended_at)
|
||||
if ended_at and isinstance(ended_at, str):
|
||||
ended_at = parse_datetime(ended_at)
|
||||
|
||||
entry.started_at = started_at
|
||||
entry.ended_at = ended_at
|
||||
entry.notes = data.get('notes', entry.notes)
|
||||
entry.save()
|
||||
except TimeEntry.DoesNotExist:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user