Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/lib/components/WeatherCalendar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,7 @@
<span class="text-xs font-medium text-blue-500 dark:text-red-500"
>{weekdays[day.date.weekday % 7]}</span
>
<span class="text-sm font-semibold text-slate-900 dark:text-neutral-100"
>{day.date.day}</span
>
<span class="text-sm font-semibold text-red-900">{day.date.day}</span>
</div>

{#if day.weather}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export const load: PageServerLoad = async ({ url, params, parent, locals: { supa

const startParam = url.searchParams.get('start');
const endParam = url.searchParams.get('end');
const timezoneParam = url.searchParams.get('timezone') || 'UTC';
const inferredTimezone = dataTable === 'cw_traffic2' ? 'Asia/Tokyo' : 'UTC';
const timezoneParam = url.searchParams.get('timezone') || inferredTimezone;
let startDate: Date;
let endDate: Date;
const nowInZone = DateTime.now().setZone(timezoneParam);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { LocationService } from '$lib/services/LocationService';
import { DeviceService } from '$lib/services/DeviceService';
import { DeviceRepository } from '$lib/repositories/DeviceRepository';
import { LocationRepository } from '$lib/repositories/LocationRepository';
import { DeviceOwnersRepository } from '$lib/repositories/DeviceOwnersRepository';

/**
* Load the device owner’s ID and all locations for the current user.
Expand Down Expand Up @@ -51,16 +52,30 @@ export const actions: Actions = {
return { success: false, error: 'Authentication required' };
}

if (!sessionResult.user.id) {
return { success: false, error: 'User ID not found in session' };
}

if (!devEui) {
return { success: false, error: 'Device EUI is required' };
}
const deviceRepository = new DeviceRepository(locals.supabase, new ErrorHandlingService());
const deviceService = new DeviceService(deviceRepository);
const device = await deviceService.getDeviceByEui(devEui);
const deviceOwnersRepository = new DeviceOwnersRepository(
locals.supabase,
new ErrorHandlingService()
);
const owners = await deviceOwnersRepository.findByDeviceEui(devEui);

if (!device) {
return { success: false, error: 'Device not found' };
}
if (device.user_id !== sessionResult.user.id) {

let isOwner = owners.find(
(owner) => owner.user_id === sessionResult?.user?.id && owner.permission_level === 1
);
if (device.user_id !== sessionResult.user.id && !isOwner) {
return { success: false, error: 'Unauthorized to update this device' };
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
let { data } = $props();
const device = $derived(data.device);
const ownerId = $derived(data.ownerId);
const isOwner = $derived(device?.user_id === ownerId);
const isOwner = $derived<boolean>(
device?.user_id === ownerId ||
device?.cw_device_owners?.some(
(owner) => owner.user_id === ownerId && owner.permission_level === 1
)
);

// @todo Use a proper sensor datasheet link when wiki pages are created
const deviceLinkId = $derived(
Expand Down Expand Up @@ -51,156 +56,157 @@
<title>{$_('Device Settings')} - CropWatch</title>
</svelte:head>

<section class="flex flex-col gap-4">
<header class="flex flex-row items-center justify-between gap-4">
<div>
<h2 class="mb-1 text-2xl font-semibold">{$_('General')}</h2>
<p class="text-sm text-neutral-100">{$_('Manage the device information.')}</p>
</div>
</header>
<form
class="form-container !grid grid-cols-1 gap-4 md:grid-cols-2"
id="deviceSettingsForm"
method="POST"
action="?/updateGeneralSettings"
use:enhance={({ formElement, formData, action, cancel, submitter }) => {
return async ({ result, update }) => {
if (result.data.success) {
success('Settings updated successfully!');
update();
} else {
error('Failed to update settings');
}
};
}}
use:formValidation
>
<div class="flex flex-1 flex-col gap-1">
<span class="text-sm font-medium text-neutral-50">
{$_('Device Type')}
</span>
{#if device?.cw_device_type?.name}
<a
href="https://kb.cropwatch.io/doku.php?id={deviceLinkId}"
target="_blank"
class="text-gray-500/80 dark:text-gray-300/80"
>
{device.cw_device_type.name}
</a>
{:else}
{$_('Unknown')}
{/if}
</div>
<div class="flex flex-1 flex-col gap-1">
<span class="text-sm font-medium text-neutral-50">
{$_('EUI')}
</span>
{device?.dev_eui || $_('Unknown')}
</div>
<div class="flex flex-1 flex-col gap-1">
<span class="text-sm font-medium text-neutral-50">
{$_('Installed Date')}
</span>
{device?.installed_at ? formatDateOnly(device.installed_at) : $_('Unknown')}
</div>
<div class="flex flex-1 flex-col gap-1">
<span class="text-sm font-medium text-neutral-50">
{$_('Coordinates')}
</span>
{#if device?.lat && device?.long}
<!-- @todo Make the coordinates editable -->
{device.lat}, {device.long}
{:else}
{$_('Unknown')}
{/if}
</div>
<div class="flex flex-1 flex-col gap-1">
<label for="deviceName" class="text-sm font-medium text-neutral-50">
{$_('Device Name')}
</label>
{#if isOwner}
<TextInput
id="deviceName"
name="name"
placeholder="Enter device name"
value={device?.name}
/>
{:else}
{device?.name}
{/if}
</div>
<div class="flex flex-1 flex-col gap-1">
<label for="deviceName" class="text-sm font-medium text-neutral-50">
{$_('Device Location')}
</label>
{#await data.locations}
{$_('Loading...')}
{:then locations}
<div class="flex min-h-screen flex-col gap-6">
<section class="flex flex-col gap-4">
<header class="flex flex-row items-center justify-between gap-4">
<div>
<h2 class="mb-1 text-2xl font-semibold">{$_('General')}</h2>
<p class="text-sm text-neutral-100">{$_('Manage the device information.')}</p>
</div>
</header>
<form
class="form-container !grid grid-cols-1 gap-4 md:grid-cols-2"
id="deviceSettingsForm"
method="POST"
action="?/updateGeneralSettings"
use:enhance={({ formElement, formData, action, cancel, submitter }) => {
return async ({ result, update }) => {
if (result.data.success) {
success('Settings updated successfully!');
update();
} else {
error('Failed to update settings');
}
};
}}
use:formValidation
>
<div class="flex flex-1 flex-col gap-1">
<span class="text-sm font-medium text-neutral-50">
{$_('Device Type')}
</span>
{#if device?.cw_device_type?.name}
<a
href="https://kb.cropwatch.io/doku.php?id={deviceLinkId}"
target="_blank"
class="text-gray-500/80 dark:text-gray-300/80"
>
{device.cw_device_type.name}
</a>
{:else}
{$_('Unknown')}
{/if}
</div>
<div class="flex flex-1 flex-col gap-1">
<span class="text-sm font-medium text-neutral-50">
{$_('EUI')}
</span>
{device?.dev_eui || $_('Unknown')}
</div>
<div class="flex flex-1 flex-col gap-1">
<span class="text-sm font-medium text-neutral-50">
{$_('Installed Date')}
</span>
{device?.installed_at ? formatDateOnly(device.installed_at) : $_('Unknown')}
</div>
<div class="flex flex-1 flex-col gap-1">
<span class="text-sm font-medium text-neutral-50">
{$_('Coordinates')}
</span>
{#if device?.lat && device?.long}
<!-- @todo Make the coordinates editable -->
{device.lat}, {device.long}
{:else}
{$_('Unknown')}
{/if}
</div>
<div class="flex flex-1 flex-col gap-1">
<label for="deviceName" class="text-sm font-medium text-neutral-50">
{$_('Device Name')}
</label>
{#if isOwner}
<Select id="deviceLocation" name="location_id">
{#each locations as loc}
<option value={loc.location_id} selected={loc.location_id === device?.location_id}>
{loc.name} ({loc.location_id})
</option>
{/each}
</Select>
<TextInput
id="deviceName"
name="name"
placeholder="Enter device name"
value={device?.name}
/>
{:else}
{locations.find((loc) => loc.location_id === device?.location_id)?.name ||
'Unknown Location'}
{#if device?.location_id}
({device.location_id})
{device?.name}
{/if}
</div>
<div class="flex flex-1 flex-col gap-1">
<label for="deviceName" class="text-sm font-medium text-neutral-50">
{$_('Device Location')}
</label>
{#await data.locations}
{$_('Loading...')}
{:then locations}
{#if isOwner}
<Select id="deviceLocation" name="location_id">
{#each locations as loc (loc.location_id)}
<option value={loc.location_id} selected={loc.location_id === device?.location_id}>
{loc.name} ({loc.location_id})
</option>
{/each}
</Select>
{:else}
{locations.find((loc) => loc.location_id === device?.location_id)?.name ||
'Unknown Location'}
{#if device?.location_id}
({device.location_id})
{/if}
{/if}
{/await}
</div>
<div class="col-span-1 flex flex-1 flex-col items-end gap-1 md:col-span-2">
{#if isOwner}
<Button variant="primary" type="submit">{$_('Update')}</Button>
{/if}
{/await}
</div>
<div class="col-span-1 flex flex-1 flex-col items-end gap-1 md:col-span-2">
{#if isOwner}
<Button variant="primary" type="submit">{$_('Update')}</Button>
{/if}
</div>
</form>
</section>
</div>
</form>
</section>

{#if isOwner}
<span class="flex flex-1"></span>
<section class="border-danger/50 mt-6 flex flex-col gap-2 rounded-lg border p-4">
<h2 class="text-danger text-lg font-semibold">{$_('Dangerous Zone')}</h2>
<div>
<Button
variant="danger"
onclick={() => {
showDeleteDialog = true;
}}
>
{$_('Delete Device & Associated Data')}
</Button>
</div>
<Dialog bind:open={showDeleteDialog} size="sm">
{#snippet title()}
{$_('Delete Device & Associated Data')}
{/snippet}
{#snippet body()}
{$_('delete_device_warning')}
{/snippet}
{#snippet footer()}
<Button
variant="secondary"
onclick={() => {
showDeleteDialog = false;
}}
>
{$_('Cancel')}
</Button>
{#if isOwner}
<section class="border-danger/50 mt-auto flex flex-col gap-2 rounded-lg border p-4">
<h2 class="text-danger text-lg font-semibold">{$_('Dangerous Zone')}</h2>
<div>
<Button
variant="danger"
onclick={() => {
showDeleteDialog = false;
deleteDevice();
showDeleteDialog = true;
}}
>
{$_('Delete')}
{$_('Delete Device & Associated Data')}
</Button>
{/snippet}
</Dialog>
</section>
{/if}
</div>
<Dialog bind:open={showDeleteDialog} size="sm">
{#snippet title()}
{$_('Delete Device & Associated Data')}
{/snippet}
{#snippet body()}
{$_('delete_device_warning')}
{/snippet}
{#snippet footer()}
<Button
variant="secondary"
onclick={() => {
showDeleteDialog = false;
}}
>
{$_('Cancel')}
</Button>
<Button
variant="danger"
onclick={() => {
showDeleteDialog = false;
deleteDevice();
}}
>
{$_('Delete')}
</Button>
{/snippet}
</Dialog>
</section>
{/if}
</div>
Loading