mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-22 14:29:36 +02:00
feat: Update Visit model to use DateTimeField for start and end dates, and enhance AdventureModal with datetime-local inputs
This commit is contained in:
parent
6f720a154f
commit
9fd2a142cb
6 changed files with 152 additions and 35 deletions
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 5.0.8 on 2025-03-17 21:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('adventures', '0024_alter_attachment_file'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='visit',
|
||||||
|
name='end_date',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='visit',
|
||||||
|
name='start_date',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -76,8 +76,8 @@ User = get_user_model()
|
||||||
class Visit(models.Model):
|
class Visit(models.Model):
|
||||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||||
adventure = models.ForeignKey('Adventure', on_delete=models.CASCADE, related_name='visits')
|
adventure = models.ForeignKey('Adventure', on_delete=models.CASCADE, related_name='visits')
|
||||||
start_date = models.DateField(null=True, blank=True)
|
start_date = models.DateTimeField(null=True, blank=True)
|
||||||
end_date = models.DateField(null=True, blank=True)
|
end_date = models.DateTimeField(null=True, blank=True)
|
||||||
notes = models.TextField(blank=True, null=True)
|
notes = models.TextField(blank=True, null=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
|
@ -136,9 +136,11 @@ class AdventureSerializer(CustomModelSerializer):
|
||||||
def get_is_visited(self, obj):
|
def get_is_visited(self, obj):
|
||||||
current_date = timezone.now().date()
|
current_date = timezone.now().date()
|
||||||
for visit in obj.visits.all():
|
for visit in obj.visits.all():
|
||||||
if visit.start_date and visit.end_date and (visit.start_date <= current_date):
|
start_date = visit.start_date.date() if isinstance(visit.start_date, timezone.datetime) else visit.start_date
|
||||||
|
end_date = visit.end_date.date() if isinstance(visit.end_date, timezone.datetime) else visit.end_date
|
||||||
|
if start_date and end_date and (start_date <= current_date):
|
||||||
return True
|
return True
|
||||||
elif visit.start_date and not visit.end_date and (visit.start_date <= current_date):
|
elif start_date and not end_date and (start_date <= current_date):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,16 @@
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
export let collection: Collection | null = null;
|
export let collection: Collection | null = null;
|
||||||
|
|
||||||
|
let fullStartDate: string = '';
|
||||||
|
let fullEndDate: string = '';
|
||||||
|
let allDay: boolean = false;
|
||||||
|
|
||||||
|
// Set full start and end dates from collection
|
||||||
|
if (collection && collection.start_date && collection.end_date) {
|
||||||
|
fullStartDate = `${collection.start_date}T00:00`;
|
||||||
|
fullEndDate = `${collection.end_date}T23:59`;
|
||||||
|
}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let images: { id: string; image: string; is_primary: boolean }[] = [];
|
let images: { id: string; image: string; is_primary: boolean }[] = [];
|
||||||
|
@ -72,7 +82,7 @@
|
||||||
|
|
||||||
import ActivityComplete from './ActivityComplete.svelte';
|
import ActivityComplete from './ActivityComplete.svelte';
|
||||||
import CategoryDropdown from './CategoryDropdown.svelte';
|
import CategoryDropdown from './CategoryDropdown.svelte';
|
||||||
import { findFirstValue } from '$lib';
|
import { findFirstValue, isAllDay } from '$lib';
|
||||||
import MarkdownEditor from './MarkdownEditor.svelte';
|
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||||
import ImmichSelect from './ImmichSelect.svelte';
|
import ImmichSelect from './ImmichSelect.svelte';
|
||||||
import Star from '~icons/mdi/star';
|
import Star from '~icons/mdi/star';
|
||||||
|
@ -379,7 +389,10 @@
|
||||||
let new_start_date: string = '';
|
let new_start_date: string = '';
|
||||||
let new_end_date: string = '';
|
let new_end_date: string = '';
|
||||||
let new_notes: string = '';
|
let new_notes: string = '';
|
||||||
|
|
||||||
|
// Function to add a new visit.
|
||||||
function addNewVisit() {
|
function addNewVisit() {
|
||||||
|
// If an end date isn’t provided, assume it’s the same as start.
|
||||||
if (new_start_date && !new_end_date) {
|
if (new_start_date && !new_end_date) {
|
||||||
new_end_date = new_start_date;
|
new_end_date = new_start_date;
|
||||||
}
|
}
|
||||||
|
@ -391,15 +404,31 @@
|
||||||
addToast('error', $t('adventures.no_start_date'));
|
addToast('error', $t('adventures.no_start_date'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Convert input to UTC if not already.
|
||||||
|
if (new_start_date && !new_start_date.includes('Z')) {
|
||||||
|
new_start_date = new Date(new_start_date).toISOString();
|
||||||
|
}
|
||||||
|
if (new_end_date && !new_end_date.includes('Z')) {
|
||||||
|
new_end_date = new Date(new_end_date).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the visit is all day, force the times to midnight.
|
||||||
|
if (allDay) {
|
||||||
|
new_start_date = new_start_date.split('T')[0] + 'T00:00:00.000Z';
|
||||||
|
new_end_date = new_end_date.split('T')[0] + 'T00:00:00.000Z';
|
||||||
|
}
|
||||||
|
|
||||||
adventure.visits = [
|
adventure.visits = [
|
||||||
...adventure.visits,
|
...adventure.visits,
|
||||||
{
|
{
|
||||||
start_date: new_start_date,
|
start_date: new_start_date,
|
||||||
end_date: new_end_date,
|
end_date: new_end_date,
|
||||||
notes: new_notes,
|
notes: new_notes,
|
||||||
id: ''
|
id: '' // or generate an id as needed
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Clear the input fields.
|
||||||
new_start_date = '';
|
new_start_date = '';
|
||||||
new_end_date = '';
|
new_end_date = '';
|
||||||
new_notes = '';
|
new_notes = '';
|
||||||
|
@ -669,13 +698,23 @@
|
||||||
on:change={() => (constrainDates = !constrainDates)}
|
on:change={() => (constrainDates = !constrainDates)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
<span class="label-text">All Day</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-primary"
|
||||||
|
id="constrain_dates"
|
||||||
|
name="constrain_dates"
|
||||||
|
on:change={() => (allDay = !allDay)}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex gap-2 mb-1">
|
<div class="flex gap-2 mb-1">
|
||||||
{#if !constrainDates}
|
{#if !allDay}
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="datetime-local"
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered w-full"
|
||||||
placeholder="Start Date"
|
placeholder={$t('adventures.start_date')}
|
||||||
|
min={constrainDates ? fullStartDate : ''}
|
||||||
|
max={constrainDates ? fullEndDate : ''}
|
||||||
bind:value={new_start_date}
|
bind:value={new_start_date}
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
|
@ -685,10 +724,12 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="datetime-local"
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered w-full"
|
||||||
placeholder={$t('adventures.end_date')}
|
placeholder={$t('adventures.end_date')}
|
||||||
bind:value={new_end_date}
|
bind:value={new_end_date}
|
||||||
|
min={constrainDates ? fullStartDate : ''}
|
||||||
|
max={constrainDates ? fullEndDate : ''}
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -701,8 +742,8 @@
|
||||||
type="date"
|
type="date"
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered w-full"
|
||||||
placeholder={$t('adventures.start_date')}
|
placeholder={$t('adventures.start_date')}
|
||||||
min={collection?.start_date}
|
min={constrainDates ? fullStartDate : ''}
|
||||||
max={collection?.end_date}
|
max={constrainDates ? fullEndDate : ''}
|
||||||
bind:value={new_start_date}
|
bind:value={new_start_date}
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
|
@ -716,8 +757,8 @@
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered w-full"
|
||||||
placeholder={$t('adventures.end_date')}
|
placeholder={$t('adventures.end_date')}
|
||||||
bind:value={new_end_date}
|
bind:value={new_end_date}
|
||||||
min={collection?.start_date}
|
min={constrainDates ? fullStartDate : ''}
|
||||||
max={collection?.end_date}
|
max={constrainDates ? fullEndDate : ''}
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -742,6 +783,30 @@
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div role="alert" class="alert shadow-lg bg-neutral mt-2 mb-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="stroke-info h-6 w-6 shrink-0"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
{$t('lodging.current_timezone')}:
|
||||||
|
{(() => {
|
||||||
|
const tz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
const [continent, city] = tz.split('/');
|
||||||
|
return `${continent} (${city.replace('_', ' ')})`;
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button type="button" class="btn btn-neutral" on:click={addNewVisit}
|
<button type="button" class="btn btn-neutral" on:click={addNewVisit}
|
||||||
>{$t('adventures.add')}</button
|
>{$t('adventures.add')}</button
|
||||||
|
@ -749,23 +814,33 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if adventure.visits.length > 0}
|
{#if adventure.visits.length > 0}
|
||||||
<h2 class=" font-bold text-xl mt-2">{$t('adventures.my_visits')}</h2>
|
<h2 class="font-bold text-xl mt-2">{$t('adventures.my_visits')}</h2>
|
||||||
{#each adventure.visits as visit}
|
{#each adventure.visits as visit}
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2 items-center">
|
||||||
<p>
|
<p>
|
||||||
{new Date(visit.start_date).toLocaleDateString(undefined, {
|
{#if isAllDay(visit.start_date)}
|
||||||
timeZone: 'UTC'
|
<!-- For all-day events, show just the date -->
|
||||||
})}
|
{new Date(visit.start_date).toLocaleDateString(undefined, {
|
||||||
|
timeZone: 'UTC'
|
||||||
|
})}
|
||||||
|
{:else}
|
||||||
|
<!-- For timed events, show date and time -->
|
||||||
|
{new Date(visit.start_date).toLocaleDateString()} ({new Date(
|
||||||
|
visit.start_date
|
||||||
|
).toLocaleTimeString()})
|
||||||
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
{#if visit.end_date && visit.end_date !== visit.start_date}
|
{#if visit.end_date && visit.end_date !== visit.start_date}
|
||||||
<p>
|
<p>
|
||||||
{new Date(visit.end_date).toLocaleDateString(undefined, {
|
{new Date(visit.end_date).toLocaleDateString(undefined, {
|
||||||
timeZone: 'UTC'
|
timeZone: 'UTC'
|
||||||
})}
|
})}
|
||||||
|
{#if !isAllDay(visit.end_date)}
|
||||||
|
({new Date(visit.end_date).toLocaleTimeString()})
|
||||||
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -338,6 +338,12 @@ export let LODGING_TYPES_ICONS = {
|
||||||
other: '❓'
|
other: '❓'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper to check if a given date string represents midnight (all-day)
|
||||||
|
export function isAllDay(dateStr: string | string[]) {
|
||||||
|
// Checks for the pattern "T00:00:00.000Z"
|
||||||
|
return dateStr.includes('T00:00:00Z') || dateStr.includes('T00:00:00.000Z');
|
||||||
|
}
|
||||||
|
|
||||||
export function getAdventureTypeLabel(type: string) {
|
export function getAdventureTypeLabel(type: string) {
|
||||||
// return the emoji ADVENTURE_TYPE_ICONS label for the given type if not found return ? emoji
|
// return the emoji ADVENTURE_TYPE_ICONS label for the given type if not found return ? emoji
|
||||||
if (type in ADVENTURE_TYPE_ICONS) {
|
if (type in ADVENTURE_TYPE_ICONS) {
|
||||||
|
|
|
@ -91,6 +91,7 @@
|
||||||
import AdventureModal from '$lib/components/AdventureModal.svelte';
|
import AdventureModal from '$lib/components/AdventureModal.svelte';
|
||||||
import ImageDisplayModal from '$lib/components/ImageDisplayModal.svelte';
|
import ImageDisplayModal from '$lib/components/ImageDisplayModal.svelte';
|
||||||
import AttachmentCard from '$lib/components/AttachmentCard.svelte';
|
import AttachmentCard from '$lib/components/AttachmentCard.svelte';
|
||||||
|
import { isAllDay } from '$lib';
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (data.props.adventure) {
|
if (data.props.adventure) {
|
||||||
|
@ -410,23 +411,33 @@
|
||||||
</p>
|
</p>
|
||||||
<!-- show each visit start and end date as well as notes -->
|
<!-- show each visit start and end date as well as notes -->
|
||||||
{#each adventure.visits as visit}
|
{#each adventure.visits as visit}
|
||||||
<div class="grid gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-sm text-muted-foreground">
|
<div class="flex gap-2 items-center">
|
||||||
{visit.start_date
|
<p>
|
||||||
? new Date(visit.start_date).toLocaleDateString(undefined, {
|
{#if isAllDay(visit.start_date)}
|
||||||
|
<!-- For all-day events, show just the date -->
|
||||||
|
{new Date(visit.start_date).toLocaleDateString(undefined, {
|
||||||
timeZone: 'UTC'
|
timeZone: 'UTC'
|
||||||
})
|
})}
|
||||||
: ''}
|
{:else}
|
||||||
{visit.end_date &&
|
<!-- For timed events, show date and time -->
|
||||||
visit.end_date !== '' &&
|
{new Date(visit.start_date).toLocaleDateString()} ({new Date(
|
||||||
visit.end_date !== visit.start_date
|
visit.start_date
|
||||||
? ' - ' +
|
).toLocaleTimeString()})
|
||||||
new Date(visit.end_date).toLocaleDateString(undefined, {
|
{/if}
|
||||||
|
</p>
|
||||||
|
{#if visit.end_date && visit.end_date !== visit.start_date}
|
||||||
|
<p>
|
||||||
|
- {new Date(visit.end_date).toLocaleDateString(undefined, {
|
||||||
timeZone: 'UTC'
|
timeZone: 'UTC'
|
||||||
})
|
})}
|
||||||
: ''}
|
{#if !isAllDay(visit.end_date)}
|
||||||
</p>
|
({new Date(visit.end_date).toLocaleTimeString()})
|
||||||
<p class="text-sm text-muted-foreground -mt-2 mb-2">{visit.notes}</p>
|
{/if}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<p class="whitespace-pre-wrap -mt-2 mb-2">{visit.notes}</p>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue