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
Empty file removed STRIPE_WEBHOOK_SETUP.md
Empty file.
59 changes: 41 additions & 18 deletions src/lib/components/GlobalSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import MaterialIcon from '$lib/components/UI/icons/MaterialIcon.svelte';
import { _ } from 'svelte-i18n';
import { onMount } from 'svelte';
import LanguageSelector from './UI/form/LanguageSelector.svelte';
import { globalLoading, startLoading } from '$lib/stores/loadingStore';
import ThemeToggle from './theme/ThemeToggle.svelte';

// Navigation items for the sidebar
const navigationItems = [
Expand Down Expand Up @@ -134,7 +137,10 @@
{$_('Navigation')}
</h2>
<button
onclick={() => sidebarStore.toggle()}
onclick={() => {
sidebarStore.toggle();
startLoading();
}}
class="rounded-lg p-2 transition-all duration-200 hover:scale-105 {getDarkMode()
? 'text-slate-400 hover:bg-slate-700/50 hover:text-white'
: 'text-gray-500 hover:bg-gray-100 hover:text-gray-900'}"
Expand All @@ -146,12 +152,16 @@
{/if}

<!-- Navigation Items -->
<nav class="flex flex-1 flex-col overflow-y-auto p-2">
<nav class="z-10 flex flex-1 flex-col overflow-y-auto pr-2" style="z-index: 10000;">
<ul class="space-y-1">
{#each navigationItems as item, index}
<li>
<a
href={item.href}
onclick={() => {
sidebarStore.close();
startLoading();
}}
class="decoration-none flex items-center gap-3 rounded-lg px-3 py-2 no-underline transition-all duration-200 hover:scale-105
{isActiveRoute(item.matcher)
? getDarkMode()
Expand Down Expand Up @@ -180,7 +190,10 @@
</ul>
<span class="flex flex-auto"></span>
<button
onclick={() => sidebarStore.toggle()}
onclick={() => {
sidebarStore.toggle();
startLoading();
}}
class="flex items-center gap-3 rounded-lg px-3 py-2 transition-colors duration-200
{getDarkMode() ? 'bg-emerald-600/30 text-emerald-400' : 'bg-emerald-100 text-emerald-700'}
{getDarkMode()
Expand All @@ -197,22 +210,32 @@
<ul class="space-y-1">
<li class="mx-3 border-t {getDarkMode() ? 'border-slate-500/30' : 'border-gray-200/30'}"></li>
<li>
<a
href="/app/account-settings/general"
class="decoration-none flex items-center gap-3 rounded-lg px-3 py-2 no-underline transition-all duration-200 hover:scale-105
<div class="flex flex-row">
<a
href="/app/account-settings/general"
onclick={() => {
sidebarStore.close();
startLoading();
}}
class="decoration-none flex items-center gap-3 rounded-lg px-3 py-2 no-underline transition-all duration-200 hover:scale-105
{isActiveRoute('/app/account-settings/general')
? getDarkMode()
? 'bg-emerald-600/30 text-emerald-400'
: 'bg-emerald-100 text-emerald-700'
: getDarkMode()
? 'text-slate-300 hover:bg-slate-700/50 hover:text-white'
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'}"
aria-current={isActiveRoute('/app/account-settings/general') ? 'page' : undefined}
title={sidebarStore.isSmallIconMode && !sidebarStore.isOpen ? $_('settings') : ''}
>
<MaterialIcon name="settings" size="medium" />
<input type="hidden" value={sidebarStore.isOpen} />
</a>
? getDarkMode()
? 'bg-emerald-600/30 text-emerald-400'
: 'bg-emerald-100 text-emerald-700'
: getDarkMode()
? 'text-slate-300 hover:bg-slate-700/50 hover:text-white'
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'}"
aria-current={isActiveRoute('/app/account-settings/general') ? 'page' : undefined}
title={sidebarStore.isSmallIconMode && !sidebarStore.isOpen ? $_('settings') : ''}
>
<MaterialIcon name="settings" size="medium" />
<input type="hidden" value={sidebarStore.isOpen} />
</a>
<span class="flex flex-1 border-l md:hidden"></span>
<ThemeToggle class="flex w-full md:hidden" outlineColor="black" />
<span class="flex flex-1 border-l md:hidden"></span>
<LanguageSelector class="flex w-full md:hidden" />
</div>
</li>
</ul>
</nav>
Expand Down
4 changes: 1 addition & 3 deletions src/lib/components/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,7 @@
<!-- Mobile menu button -->
<button
onclick={toggleSidebar}
class="mobile-menu-btn rounded-lg p-2 transition-colors lg:hidden {getDarkMode()
? 'text-white hover:bg-white/10'
: 'text-gray-700 hover:bg-gray-200/50'}"
class="mobile-menu-btn rounded-lg p-2 text-white transition-colors lg:hidden"
aria-label="Toggle mobile menu"
aria-expanded={mobileMenuOpen}
aria-controls="mobile-menu"
Expand Down
5 changes: 4 additions & 1 deletion src/lib/components/UI/form/LanguageSelector.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script lang="ts">
import { locale, locales } from 'svelte-i18n';

let { class: className = '' } = $props();

const languageOptions = [
{ code: 'en', label: '🇺🇸 English' },
{ code: 'ja', label: '🇯🇵 日本語' }
Expand All @@ -16,9 +18,10 @@
</script>

<select
id="language-selector"
bind:value={selected}
onchange={handleChange}
class="user-select-none border-none bg-transparent text-slate-400 outline-none hover:text-green-400 dark:bg-transparent"
class="{className} user-select-none border-none bg-transparent text-slate-400 outline-none hover:text-green-400 dark:bg-transparent"
aria-label="Select language"
>
{#each languageOptions as option}
Expand Down
8 changes: 5 additions & 3 deletions src/lib/components/theme/ThemeToggle.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script>
import { getDarkMode, toggleTheme } from './theme.svelte';

let { class: className = '', outlineColor = 'white' } = $props();

// Use the darkMode state from theme.svelte.ts
$effect(() => {
// This effect will run whenever getDarkMode() changes
Expand All @@ -12,7 +14,7 @@

<button
type="button"
class="text-text-light flex items-center justify-center rounded-full p-2 text-slate-200 transition-colors duration-200"
class="{className} text-text-light flex items-center justify-center rounded-full p-2 text-slate-200 transition-colors duration-200"
onclick={toggleTheme}
aria-label={getDarkMode() ? 'Switch to light mode' : 'Switch to dark mode'}
>
Expand All @@ -23,7 +25,7 @@
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke={outlineColor}
>
<path
stroke-linecap="round"
Expand All @@ -39,7 +41,7 @@
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke={outlineColor}
>
<path
stroke-linecap="round"
Expand Down
73 changes: 50 additions & 23 deletions src/lib/interfaces/IDeviceDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,53 @@ import type { DeviceDataRecord } from '../models/DeviceDataRecord';
* Interface for dynamic device data retrieval based on device type
*/
export interface IDeviceDataService {
/**
* Get the latest data for a device based on its type
* @param devEui The device EUI
* @param deviceType The device type information containing data_table_v2
*/
getLatestDeviceData(
devEui: string,
deviceType: DeviceType
): Promise<DeviceDataRecord | null>;

/**
* Get device data within a date range based on device type
* @param devEui The device EUI
* @param deviceType The device type information containing data_table_v2
* @param startDate The start date
* @param endDate The end date
*/
getDeviceDataByDateRange(
devEui: string,
startDate: Date,
endDate: Date
): Promise<DeviceDataRecord[]>;
}
/**
* Get the latest data for a device based on its type
* @param devEui The device EUI
* @param deviceType The device type information containing data_table_v2
*/
getLatestDeviceData(devEui: string, deviceType: DeviceType): Promise<DeviceDataRecord | null>;

/**
* Get device data within a date range based on device type
* @param devEui The device EUI
* @param deviceType The device type information containing data_table_v2
* @param startDate The start date
* @param endDate The end date
*/
getDeviceDataByDateRange(
devEui: string,
startDate: Date,
endDate: Date
): Promise<DeviceDataRecord[]>;

/**
* Get device data for report with optional filtering
* @param devEui The device EUI
* @param startDate The start date
* @param endDate The end date
* @param timezone The timezone
* @param intervalMinutes The interval in minutes
* @param columns Optional columns to filter
* @param ops Optional operators for filtering
* @param mins Optional minimum values
* @param maxs Optional maximum values
*/
getDeviceDataForReport(
devEui: string,
startDate: Date,
endDate: Date,
timezone: string,
intervalMinutes: number,
columns?: string[],
ops?: string[],
mins?: number[],
maxs?: (number | null)[]
): Promise<DeviceDataRecord[]>;

/**
* Get alert points for a device from its reports
* @param devEui The device EUI
*/
getAlertPointsForDevice(devEui: string): Promise<any[]>;
}
5 changes: 3 additions & 2 deletions src/lib/pdf/pdfDataTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ export function createPDFDataTable(
});
const legendStartY = conf.margin + 15;
// Draw alert points legend
alertPointData.alert_points.forEach((point, index) => {
const alertPoints = alertPointData.alert_points || [];
alertPoints.forEach((point, index) => {
const color = point.hex_color || '#ffffff';
const legendX =
conf.margin + (index % finalColumnsPerPage) * (conf.cellWidth + conf.columnMargin);
Expand Down Expand Up @@ -213,7 +214,7 @@ function drawColumn(
let bgColor = '#ffffff';

// Check alert points for this value
for (const alertPoint of alertPointData.alert_points) {
for (const alertPoint of alertPointData.alert_points || []) {
if (evaluateAlertCondition(value, alertPoint)) {
bgColor = alertPoint.hex_color || '#ffffff';
break; // Use the first matching alert point
Expand Down
95 changes: 87 additions & 8 deletions src/lib/services/DeviceDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,11 @@ export class DeviceDataService implements IDeviceDataService {
startDate: Date,
endDate: Date,
timezone: string,
intervalMinutes: number
intervalMinutes: number,
columns?: string[],
ops?: string[],
mins?: number[],
maxs?: (number | null)[]
): Promise<DeviceDataRecord[]> {
if (!devEui) {
throw new Error('Device EUI not specified');
Expand All @@ -272,13 +276,59 @@ export class DeviceDataService implements IDeviceDataService {
}

try {
const { data, error: deviceError } = await this.supabase.rpc('get_report_data_for_device', {
input_dev_eui: devEui,
input_start: startDate,
input_end: endDate,
input_timezone: timezone,
input_interval_minutes: intervalMinutes
});
// If no filtering parameters provided, get alert points from the device's reports
let p_columns = columns || ['temperature_c', 'humidity'];
let p_ops = ops || ['>', 'BETWEEN'];
let p_mins = mins || [25.0, 55.0];
let p_maxs = maxs || [null, 65.0];

// If no explicit filters provided, try to get from device reports
if (!columns && !ops && !mins && !maxs) {
try {
// Get the first report for this device to extract alert points
const { data: reports, error: reportError } = await this.supabase
.from('reports')
.select('report_id, report_alert_points(*)')
.eq('dev_eui', devEui)
.limit(1);

if (!reportError && reports && reports.length > 0 && reports[0].report_alert_points) {
const alertPoints = reports[0].report_alert_points;
if (alertPoints && alertPoints.length > 0) {
p_columns = [];
p_ops = [];
p_mins = [];
p_maxs = [];

alertPoints.forEach((point: any) => {
if (point.data_point_key) {
p_columns.push(point.data_point_key);
p_ops.push(point.operator || '>');
p_mins.push(point.min || point.value || 0);
p_maxs.push(point.max || null);
}
});
}
}
} catch (alertError) {
// If we can't get alert points, use default values
console.warn('Could not load alert points, using defaults:', alertError);
}
}

const { data, error: deviceError } = await this.supabase.rpc(
'get_filtered_device_report_data_multi',
{
p_dev_id: devEui,
p_start_time: startDate,
p_end_time: endDate,
p_interval_minutes: intervalMinutes,
p_columns: p_columns,
p_ops: p_ops,
p_mins: p_mins,
p_maxs: p_maxs
}
);

if (deviceError) {
this.errorHandler.logError(deviceError);
Expand Down Expand Up @@ -313,6 +363,35 @@ export class DeviceDataService implements IDeviceDataService {
}
}

/**
* Get alert points for a device from its reports
* @param devEui The device EUI
*/
public async getAlertPointsForDevice(devEui: string): Promise<any[]> {
if (!devEui) {
throw new Error('Device EUI not specified');
}
if (!this.checkUserHasAccess(devEui)) {
throw new Error('User does not have access to this device');
}

try {
const { data: reports, error: reportError } = await this.supabase
.from('reports')
.select('report_id, name, report_alert_points(*)')
.eq('dev_eui', devEui)
.limit(1);

if (!reportError && reports && reports.length > 0 && reports[0].report_alert_points) {
return reports[0].report_alert_points || [];
}
return [];
} catch (error) {
this.errorHandler.logError(error as Error);
return [];
}
}

private async checkUserHasAccess(dev_eui: string): Promise<boolean> {
// Placeholder for user access check logic
// This should be implemented based on your authentication and authorization system
Expand Down
Loading