From c56859db5b7eedad6938bf040aeef1fe8c158682 Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Tue, 8 Jul 2025 17:33:39 +0900 Subject: [PATCH 1/3] report generating works again! --- STRIPE_WEBHOOK_SETUP.md | 0 src/lib/interfaces/IDeviceDataService.ts | 73 +++++++++----- src/lib/pdf/pdfDataTable.ts | 5 +- src/lib/services/DeviceDataService.ts | 95 +++++++++++++++++-- .../devices/[devEui]/data-jwt-pdf/+server.ts | 14 ++- .../devices/[devEui]/reports/pdf/+server.ts | 36 +++++-- static/build-info.json | 10 +- test-device-data-service.ts | 24 +++++ 8 files changed, 207 insertions(+), 50 deletions(-) delete mode 100644 STRIPE_WEBHOOK_SETUP.md create mode 100644 test-device-data-service.ts diff --git a/STRIPE_WEBHOOK_SETUP.md b/STRIPE_WEBHOOK_SETUP.md deleted file mode 100644 index e69de29b..00000000 diff --git a/src/lib/interfaces/IDeviceDataService.ts b/src/lib/interfaces/IDeviceDataService.ts index f32aabb9..47331509 100644 --- a/src/lib/interfaces/IDeviceDataService.ts +++ b/src/lib/interfaces/IDeviceDataService.ts @@ -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; - - /** - * 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; -} \ No newline at end of file + /** + * 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; + + /** + * 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; + + /** + * 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; + + /** + * Get alert points for a device from its reports + * @param devEui The device EUI + */ + getAlertPointsForDevice(devEui: string): Promise; +} diff --git a/src/lib/pdf/pdfDataTable.ts b/src/lib/pdf/pdfDataTable.ts index 321b689b..ba5c6463 100644 --- a/src/lib/pdf/pdfDataTable.ts +++ b/src/lib/pdf/pdfDataTable.ts @@ -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); @@ -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 diff --git a/src/lib/services/DeviceDataService.ts b/src/lib/services/DeviceDataService.ts index 27c63ba2..bd5d2e45 100644 --- a/src/lib/services/DeviceDataService.ts +++ b/src/lib/services/DeviceDataService.ts @@ -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 { if (!devEui) { throw new Error('Device EUI not specified'); @@ -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); @@ -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 { + 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 { // Placeholder for user access check logic // This should be implemented based on your authentication and authorization system diff --git a/src/routes/api/devices/[devEui]/data-jwt-pdf/+server.ts b/src/routes/api/devices/[devEui]/data-jwt-pdf/+server.ts index eddc24ae..bafc4d9b 100644 --- a/src/routes/api/devices/[devEui]/data-jwt-pdf/+server.ts +++ b/src/routes/api/devices/[devEui]/data-jwt-pdf/+server.ts @@ -87,9 +87,17 @@ export const GET: RequestHandler = async ({ params, url, request, locals: { supa 'Asia/Tokyo', 30 // Default interval in minutes ); - if (deviceDataResponse.device_data) { - deviceData = deviceDataResponse.device_data; - reportInfo = deviceDataResponse.report_info[0]; + if (deviceDataResponse && deviceDataResponse.length > 0) { + deviceData = deviceDataResponse; + + // Get alert points for the device + const alertPoints = await deviceDataService.getAlertPointsForDevice(devEui); + + // Create proper report info structure + reportInfo = { + dev_eui: devEui, + alert_points: alertPoints + }; } else { throw new Error( `No device data found for ${devEui} in the specified date range: ${startDate.toISOString()} to ${endDate.toISOString()}` diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/reports/pdf/+server.ts b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/reports/pdf/+server.ts index 152785a5..54c51778 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/reports/pdf/+server.ts +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/reports/pdf/+server.ts @@ -122,23 +122,41 @@ export const GET: RequestHandler = async ({ devEui, startDate, endDate, - 30, - 'Asia/Tokyo' + 'Asia/Tokyo', + 30 ); if (!deviceData || deviceData.length === 0) { throw error(404, 'No data found for the specified device'); } - // Create the PDF data table - const data = deviceData.device_data; + // Transform DeviceDataRecord[] to TableData[] format + const data = deviceData.map((record) => ({ + date: record.created_at, + values: Object.entries(record) + .filter( + ([key, value]) => + key !== 'dev_eui' && key !== 'created_at' && typeof value === 'number' && value !== null + ) + .map(([_, value]) => value as number) + })); + if (!data || data.length === 0) { throw error(404, 'No data found for the specified device'); } - if (!deviceData.report_info || deviceData.report_info.length === 0) { - throw error(404, 'No report info found for the specified device'); - } - const alertPoints = deviceData.report_info[0].alert_points; - createPDFDataTable(doc, data, alertPoints); + + // Get alert points for the device + const alertPoints = await deviceDataService.getAlertPointsForDevice(devEui); + + // Create the alert data structure + const alertData = { + alert_points: alertPoints, + created_at: new Date().toISOString(), + dev_eui: devEui, + id: 1, + name: 'Device Report', + report_id: '1' + }; + createPDFDataTable(doc, data, alertData); // Add generation timestamp doc diff --git a/static/build-info.json b/static/build-info.json index 835bd241..378534cd 100644 --- a/static/build-info.json +++ b/static/build-info.json @@ -1,9 +1,9 @@ { - "commit": "915f7fc", - "branch": "remove-iocconfig", - "author": "Kevin Cantrell", - "date": "2025-07-07T04:40:02.303Z", + "commit": "df838f8", + "branch": "pdfbuild-update", + "author": "CropWatch", + "date": "2025-07-08T08:29:13.143Z", "builder": "kevin@kevin-desktop", "ipAddress": "192.168.1.100", - "timestamp": 1751863202304 + "timestamp": 1751963353144 } diff --git a/test-device-data-service.ts b/test-device-data-service.ts new file mode 100644 index 00000000..da19822c --- /dev/null +++ b/test-device-data-service.ts @@ -0,0 +1,24 @@ +import { DeviceDataService } from './src/lib/services/DeviceDataService'; + +// This is just a compilation test to ensure the new method signature works +function testMethod() { + // Test with all parameters + const service = new DeviceDataService({} as any); + + const result1 = service.getDeviceDataForReport( + 'test', + new Date(), + new Date(), + 'UTC', + 30, + ['temperature_c'], + ['>'], + [25.0], + [null] + ); + + // Test with default parameters + const result2 = service.getDeviceDataForReport('test', new Date(), new Date(), 'UTC', 30); + + console.log('Method signatures compile correctly'); +} From 8d496eccbde6a4f94b150c911e6ecccc74c5cbfd Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Tue, 8 Jul 2025 18:38:15 +0900 Subject: [PATCH 2/3] able to now add data point key to report --- .../settings/reports/create/+page.server.ts | 22 +++++++++++++++++-- .../settings/reports/create/+page.svelte | 12 ++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.server.ts b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.server.ts index c8c89bd0..76abd693 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.server.ts +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.server.ts @@ -7,6 +7,7 @@ import { ReportUserScheduleRepository } from '$lib/repositories/ReportUserSchedu import { ErrorHandlingService } from '$lib/errors/ErrorHandlingService'; import { error, fail, redirect } from '@sveltejs/kit'; import type { ReportAlertPoint, ReportRecipient, ReportUserSchedule } from '$lib/models/Report'; +import { DeviceDataService } from '$lib/services/DeviceDataService'; export const load: PageServerLoad = async ({ params, locals, url }) => { try { @@ -28,15 +29,30 @@ export const load: PageServerLoad = async ({ params, locals, url }) => { throw error(401, 'Authentication required'); } + const errorHandler = new ErrorHandlingService(); + const deviceRepo = new ReportRepository(locals.supabase, errorHandler); + const dataService = new DeviceDataService(locals.supabase, deviceRepo); // If reportId is provided, load existing report data let report = null; let alertPoints: ReportAlertPoint[] = []; let recipients: ReportRecipient[] = []; let schedules: ReportUserSchedule[] = []; + let dataKeys = null; + const latestData = await dataService.getLatestDeviceData(devEui); // pull the latest data for the device keys + if (!latestData) { + throw error(404, 'No data found for this device'); + } + + dataKeys = Object.keys(latestData) + .filter((k) => latestData[k] != null) + .reduce((a, k) => ({ ...a, [k]: latestData[k] }), {}); + delete dataKeys['dev_eui']; // remove dev_eui as it's not a data key + delete dataKeys['created_at']; // remove created_at as it's not a data key + delete dataKeys['is_simulated']; // remove updated_at as it's not a data key + dataKeys = Object.keys(dataKeys); // get only the keys if (reportId) { // Create service dependencies - const errorHandler = new ErrorHandlingService(); const reportRepo = new ReportRepository(locals.supabase, errorHandler); const alertPointRepo = new ReportAlertPointRepository(locals.supabase, errorHandler); const recipientRepo = new ReportRecipientRepository(locals.supabase, errorHandler); @@ -73,6 +89,7 @@ export const load: PageServerLoad = async ({ params, locals, url }) => { alertPoints, recipients, schedules, + dataKeys, isEditing: !!reportId }; } catch (err) { @@ -171,7 +188,8 @@ export const actions: Actions = { min: point.min, max: point.max, value: point.value, - hex_color: point.color + hex_color: point.color, + data_point_key: point.data_point_key }); } diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte index f87ac170..416476c4 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte @@ -19,6 +19,7 @@ const report = $derived(data.report); const isEditing = $derived(data.isEditing); const recipientsData = $derived(data.recipients); + const dataKeys = $derived(data.dataKeys); // Form state let reportName = $state(''); @@ -33,6 +34,7 @@ value?: number; min?: number; max?: number; + data_point_key?: string; color: string; }> >([]); @@ -373,6 +375,16 @@ Alert Point {i + 1} +