diff --git a/STRIPE_WEBHOOK_SETUP.md b/STRIPE_WEBHOOK_SETUP.md new file mode 100644 index 00000000..e69de29b diff --git a/check-webhook-data.mjs b/check-webhook-data.mjs new file mode 100644 index 00000000..e69de29b diff --git a/create-test-user.mjs b/create-test-user.mjs new file mode 100644 index 00000000..e69de29b diff --git a/database.types.ts b/database.types.ts index 51d5b20d..43ddae51 100644 --- a/database.types.ts +++ b/database.types.ts @@ -1,1684 +1,2006 @@ -export type Json = - | string - | number - | boolean - | null - | { [key: string]: Json | undefined } - | Json[] +export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]; export type Database = { - public: { - Tables: { - api_keys: { - Row: { - api_key: string | null - created_at: string - Description: string | null - expires_at: string | null - id: number - owner_id: string - } - Insert: { - api_key?: string | null - created_at?: string - Description?: string | null - expires_at?: string | null - id?: number - owner_id?: string - } - Update: { - api_key?: string | null - created_at?: string - Description?: string | null - expires_at?: string | null - id?: number - owner_id?: string - } - Relationships: [ - { - foreignKeyName: "api_keys_owner_id_fkey" - columns: ["owner_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - ] - } - babylon_notifiers: { - Row: { - api_key: string | null - created_at: string - host: string | null - id: number - isSecure: boolean - name: string - notifier_id: number | null - password: string | null - port: number | null - type: number | null - username: string | null - } - Insert: { - api_key?: string | null - created_at?: string - host?: string | null - id?: number - isSecure?: boolean - name: string - notifier_id?: number | null - password?: string | null - port?: number | null - type?: number | null - username?: string | null - } - Update: { - api_key?: string | null - created_at?: string - host?: string | null - id?: number - isSecure?: boolean - name?: string - notifier_id?: number | null - password?: string | null - port?: number | null - type?: number | null - username?: string | null - } - Relationships: [] - } - cw_air_data: { - Row: { - battery_level: number | null - co: number | null - co2: number | null - created_at: string - dev_eui: string - humidity: number | null - is_simulated: boolean - lux: number | null - pressure: number | null - rainfall: number | null - smoke_detected: boolean | null - temperature_c: number | null - uv_index: number | null - vape_detected: boolean | null - wind_direction: number | null - wind_speed: number | null - } - Insert: { - battery_level?: number | null - co?: number | null - co2?: number | null - created_at?: string - dev_eui: string - humidity?: number | null - is_simulated?: boolean - lux?: number | null - pressure?: number | null - rainfall?: number | null - smoke_detected?: boolean | null - temperature_c?: number | null - uv_index?: number | null - vape_detected?: boolean | null - wind_direction?: number | null - wind_speed?: number | null - } - Update: { - battery_level?: number | null - co?: number | null - co2?: number | null - created_at?: string - dev_eui?: string - humidity?: number | null - is_simulated?: boolean - lux?: number | null - pressure?: number | null - rainfall?: number | null - smoke_detected?: boolean | null - temperature_c?: number | null - uv_index?: number | null - vape_detected?: boolean | null - wind_direction?: number | null - wind_speed?: number | null - } - Relationships: [] - } - cw_air_thvd: { - Row: { - created_at: string - dev_eui: string - dewPointC: number | null - humidity: number - id: number - profile_id: string | null - temperatureC: number - vpd: number | null - } - Insert: { - created_at?: string - dev_eui: string - dewPointC?: number | null - humidity: number - id?: number - profile_id?: string | null - temperatureC: number - vpd?: number | null - } - Update: { - created_at?: string - dev_eui?: string - dewPointC?: number | null - humidity?: number - id?: number - profile_id?: string | null - temperatureC?: number - vpd?: number | null - } - Relationships: [ - { - foreignKeyName: "cw_air_thvd_dev_eui_fkey" - columns: ["dev_eui"] - isOneToOne: false - referencedRelation: "cw_devices" - referencedColumns: ["dev_eui"] - }, - { - foreignKeyName: "public_cw_air_thvd_profile_id_fkey" - columns: ["profile_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - ] - } - cw_co2_alerts: { - Row: { - action: string - cleared: boolean - created_at: string - dev_eui: string - id: number - OneSignalID: string | null - operator: string - profile_id: string - receiver: string - subject: string - value: number - } - Insert: { - action: string - cleared: boolean - created_at?: string - dev_eui: string - id?: number - OneSignalID?: string | null - operator: string - profile_id: string - receiver: string - subject: string - value: number - } - Update: { - action?: string - cleared?: boolean - created_at?: string - dev_eui?: string - id?: number - OneSignalID?: string | null - operator?: string - profile_id?: string - receiver?: string - subject?: string - value?: number - } - Relationships: [ - { - foreignKeyName: "cw_co2_alerts_dev_eui_fkey" - columns: ["dev_eui"] - isOneToOne: false - referencedRelation: "cw_devices" - referencedColumns: ["dev_eui"] - }, - { - foreignKeyName: "cw_co2_alerts_profile_id_fkey" - columns: ["profile_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - ] - } - cw_co2_uplinks: { - Row: { - battery: number | null - co2_level: number | null - created_at: string - dev_eui: string | null - humidity: number - id: number - pressure: number | null - profile_id: string | null - temperature: number - } - Insert: { - battery?: number | null - co2_level?: number | null - created_at?: string - dev_eui?: string | null - humidity: number - id?: number - pressure?: number | null - profile_id?: string | null - temperature: number - } - Update: { - battery?: number | null - co2_level?: number | null - created_at?: string - dev_eui?: string | null - humidity?: number - id?: number - pressure?: number | null - profile_id?: string | null - temperature?: number - } - Relationships: [ - { - foreignKeyName: "cw_co2_uplinks_dev_eui_fkey" - columns: ["dev_eui"] - isOneToOne: false - referencedRelation: "cw_devices" - referencedColumns: ["dev_eui"] - }, - ] - } - cw_data_metadata: { - Row: { - adder: number - created_at: string - formatting: string | null - icon: string | null - id: number - multiplier: number - name: string - notation: string - public_name: string | null - } - Insert: { - adder?: number - created_at?: string - formatting?: string | null - icon?: string | null - id?: number - multiplier?: number - name: string - notation?: string - public_name?: string | null - } - Update: { - adder?: number - created_at?: string - formatting?: string | null - icon?: string | null - id?: number - multiplier?: number - name?: string - notation?: string - public_name?: string | null - } - Relationships: [] - } - cw_device_owners: { - Row: { - dev_eui: string - id: number - owner_id: number - permission_level: number - user_id: string - } - Insert: { - dev_eui: string - id?: number - owner_id?: number - permission_level?: number - user_id: string - } - Update: { - dev_eui?: string - id?: number - owner_id?: number - permission_level?: number - user_id?: string - } - Relationships: [ - { - foreignKeyName: "cw_device_owners_dev_eui_fkey" - columns: ["dev_eui"] - isOneToOne: false - referencedRelation: "cw_devices" - referencedColumns: ["dev_eui"] - }, - { - foreignKeyName: "cw_device_owners_user_id_fkey" - columns: ["user_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - ] - } - cw_device_type: { - Row: { - created_at: string - data_table: string | null - data_table_v2: string - decoder: string | null - default_upload_interval: number | null - device_app: string | null - id: number - isActive: boolean - manufacturer: string | null - model: string | null - name: string - primary_data: string | null - primary_data_notation: string - primary_data_v2: string - primary_divider: number - primary_multiplier: number | null - secondary_data: string - secondary_data_notation: string - secondary_data_v2: string - secondary_divider: number - secondary_multiplier: number - TTI_application_id: string | null - } - Insert: { - created_at?: string - data_table?: string | null - data_table_v2: string - decoder?: string | null - default_upload_interval?: number | null - device_app?: string | null - id?: number - isActive?: boolean - manufacturer?: string | null - model?: string | null - name: string - primary_data?: string | null - primary_data_notation?: string - primary_data_v2: string - primary_divider?: number - primary_multiplier?: number | null - secondary_data?: string - secondary_data_notation?: string - secondary_data_v2: string - secondary_divider?: number - secondary_multiplier?: number - TTI_application_id?: string | null - } - Update: { - created_at?: string - data_table?: string | null - data_table_v2?: string - decoder?: string | null - default_upload_interval?: number | null - device_app?: string | null - id?: number - isActive?: boolean - manufacturer?: string | null - model?: string | null - name?: string - primary_data?: string | null - primary_data_notation?: string - primary_data_v2?: string - primary_divider?: number - primary_multiplier?: number | null - secondary_data?: string - secondary_data_notation?: string - secondary_data_v2?: string - secondary_divider?: number - secondary_multiplier?: number - TTI_application_id?: string | null - } - Relationships: [] - } - cw_device_x_cw_data_metadata: { - Row: { - created_at: string - cw_data_metadata: number - device_type_id: number - id: number - relation_id: number - } - Insert: { - created_at?: string - cw_data_metadata: number - device_type_id: number - id?: number - relation_id?: number - } - Update: { - created_at?: string - cw_data_metadata?: number - device_type_id?: number - id?: number - relation_id?: number - } - Relationships: [ - { - foreignKeyName: "cw_device_x_cw_data_metadata_cw_data_metadata_fkey" - columns: ["cw_data_metadata"] - isOneToOne: false - referencedRelation: "cw_data_metadata" - referencedColumns: ["id"] - }, - { - foreignKeyName: "cw_device_x_cw_data_metadata_device_type_id_fkey" - columns: ["device_type_id"] - isOneToOne: false - referencedRelation: "cw_device_type" - referencedColumns: ["id"] - }, - ] - } - cw_devices: { - Row: { - ai_provider: string | null - battery_changed_at: string | null - dev_eui: string - installed_at: string | null - lat: number | null - location_id: number | null - long: number | null - name: string - report_endpoint: string | null - serial_number: string | null - type: number | null - upload_interval: number | null - user_id: string | null - warranty_start_date: string | null - } - Insert: { - ai_provider?: string | null - battery_changed_at?: string | null - dev_eui: string - installed_at?: string | null - lat?: number | null - location_id?: number | null - long?: number | null - name?: string - report_endpoint?: string | null - serial_number?: string | null - type?: number | null - upload_interval?: number | null - user_id?: string | null - warranty_start_date?: string | null - } - Update: { - ai_provider?: string | null - battery_changed_at?: string | null - dev_eui?: string - installed_at?: string | null - lat?: number | null - location_id?: number | null - long?: number | null - name?: string - report_endpoint?: string | null - serial_number?: string | null - type?: number | null - upload_interval?: number | null - user_id?: string | null - warranty_start_date?: string | null - } - Relationships: [ - { - foreignKeyName: "cw_devices_location_id_fkey" - columns: ["location_id"] - isOneToOne: false - referencedRelation: "cw_locations" - referencedColumns: ["location_id"] - }, - { - foreignKeyName: "cw_devices_type_fkey" - columns: ["type"] - isOneToOne: false - referencedRelation: "cw_device_type" - referencedColumns: ["id"] - }, - ] - } - cw_gateways: { - Row: { - created_at: string - gateway_id: string - gateway_name: string - id: number - is_online: boolean - is_public: boolean - updated_at: string | null - } - Insert: { - created_at?: string - gateway_id: string - gateway_name: string - id?: number - is_online: boolean - is_public?: boolean - updated_at?: string | null - } - Update: { - created_at?: string - gateway_id?: string - gateway_name?: string - id?: number - is_online?: boolean - is_public?: boolean - updated_at?: string | null - } - Relationships: [] - } - cw_gateways_owners: { - Row: { - created_at: string - gateway_id: number - id: number - user_id: string - } - Insert: { - created_at?: string - gateway_id: number - id?: number - user_id: string - } - Update: { - created_at?: string - gateway_id?: number - id?: number - user_id?: string - } - Relationships: [ - { - foreignKeyName: "cw_gateways_owners_gateway_id_fkey" - columns: ["gateway_id"] - isOneToOne: false - referencedRelation: "cw_gateways" - referencedColumns: ["id"] - }, - { - foreignKeyName: "cw_gateways_owners_user_id_fkey" - columns: ["user_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - ] - } - cw_location_owners: { - Row: { - description: string | null - id: number - is_active: boolean | null - location_id: number - owner_id: number - permission_level: number | null - user_id: string - } - Insert: { - description?: string | null - id?: number - is_active?: boolean | null - location_id: number - owner_id?: number - permission_level?: number | null - user_id: string - } - Update: { - description?: string | null - id?: number - is_active?: boolean | null - location_id?: number - owner_id?: number - permission_level?: number | null - user_id?: string - } - Relationships: [ - { - foreignKeyName: "cw_location_owners_location_id_fkey" - columns: ["location_id"] - isOneToOne: false - referencedRelation: "cw_locations" - referencedColumns: ["location_id"] - }, - { - foreignKeyName: "cw_location_owners_user_id_fkey" - columns: ["user_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - ] - } - cw_locations: { - Row: { - created_at: string - description: string | null - lat: number | null - location_id: number - long: number | null - map_zoom: number | null - name: string - owner_id: string | null - } - Insert: { - created_at?: string - description?: string | null - lat?: number | null - location_id?: number - long?: number | null - map_zoom?: number | null - name: string - owner_id?: string | null - } - Update: { - created_at?: string - description?: string | null - lat?: number | null - location_id?: number - long?: number | null - map_zoom?: number | null - name?: string - owner_id?: string | null - } - Relationships: [ - { - foreignKeyName: "cw_locations_owner_id_fkey" - columns: ["owner_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - ] - } - cw_notifier_types: { - Row: { - created_at: string - id: number - name: string - notifier_id: number - } - Insert: { - created_at?: string - id?: number - name: string - notifier_id?: number - } - Update: { - created_at?: string - id?: number - name?: string - notifier_id?: number - } - Relationships: [] - } - cw_permission_level_types: { - Row: { - created_at: string - id: number - name: string - permission_level_id: number - } - Insert: { - created_at?: string - id?: number - name: string - permission_level_id?: number - } - Update: { - created_at?: string - id?: number - name?: string - permission_level_id?: number - } - Relationships: [] - } - cw_rule_criteria: { - Row: { - created_at: string - criteria_id: number | null - id: number - operator: string - parent_id: string | null - reset_value: number | null - ruleGroupId: string - subject: string - trigger_value: number - } - Insert: { - created_at?: string - criteria_id?: number | null - id?: number - operator: string - parent_id?: string | null - reset_value?: number | null - ruleGroupId: string - subject: string - trigger_value: number - } - Update: { - created_at?: string - criteria_id?: number | null - id?: number - operator?: string - parent_id?: string | null - reset_value?: number | null - ruleGroupId?: string - subject?: string - trigger_value?: number - } - Relationships: [ - { - foreignKeyName: "public_cw_rule_criteria_ruleGroupId_fkey" - columns: ["ruleGroupId"] - isOneToOne: false - referencedRelation: "cw_rules" - referencedColumns: ["ruleGroupId"] - }, - ] - } - cw_rules: { - Row: { - action_recipient: string - created_at: string - dev_eui: string | null - id: number - is_triggered: boolean - last_triggered: string | null - name: string - notifier_type: number - profile_id: string - ruleGroupId: string - trigger_count: number - } - Insert: { - action_recipient: string - created_at?: string - dev_eui?: string | null - id?: number - is_triggered?: boolean - last_triggered?: string | null - name: string - notifier_type: number - profile_id?: string - ruleGroupId: string - trigger_count?: number - } - Update: { - action_recipient?: string - created_at?: string - dev_eui?: string | null - id?: number - is_triggered?: boolean - last_triggered?: string | null - name?: string - notifier_type?: number - profile_id?: string - ruleGroupId?: string - trigger_count?: number - } - Relationships: [ - { - foreignKeyName: "cw_rules_dev_eui_fkey" - columns: ["dev_eui"] - isOneToOne: false - referencedRelation: "cw_devices" - referencedColumns: ["dev_eui"] - }, - { - foreignKeyName: "cw_rules_notifier_type_fkey" - columns: ["notifier_type"] - isOneToOne: false - referencedRelation: "cw_notifier_types" - referencedColumns: ["notifier_id"] - }, - { - foreignKeyName: "public_cw_rules_profile_id_fkey" - columns: ["profile_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - ] - } - cw_soil_data: { - Row: { - created_at: string - dev_eui: string - ec: number | null - moisture: number | null - ph: number | null - temperature_c: number | null - } - Insert: { - created_at?: string - dev_eui: string - ec?: number | null - moisture?: number | null - ph?: number | null - temperature_c?: number | null - } - Update: { - created_at?: string - dev_eui?: string - ec?: number | null - moisture?: number | null - ph?: number | null - temperature_c?: number | null - } - Relationships: [] - } - cw_soil_uplinks: { - Row: { - battery: number | null - created_at: string - dev_eui: string | null - ec: number | null - id: number - internal_temp: number | null - k: number | null - moisture: number - n: number | null - p: number | null - ph: number | null - read_attempts: number | null - real_duration: number | null - temperature: number - } - Insert: { - battery?: number | null - created_at?: string - dev_eui?: string | null - ec?: number | null - id?: number - internal_temp?: number | null - k?: number | null - moisture: number - n?: number | null - p?: number | null - ph?: number | null - read_attempts?: number | null - real_duration?: number | null - temperature: number - } - Update: { - battery?: number | null - created_at?: string - dev_eui?: string | null - ec?: number | null - id?: number - internal_temp?: number | null - k?: number | null - moisture?: number - n?: number | null - p?: number | null - ph?: number | null - read_attempts?: number | null - real_duration?: number | null - temperature?: number - } - Relationships: [ - { - foreignKeyName: "cw_soil_uplinks_dev_eui_fkey" - columns: ["dev_eui"] - isOneToOne: false - referencedRelation: "cw_devices" - referencedColumns: ["dev_eui"] - }, - ] - } - cw_traffic: { - Row: { - created_at: string - dev_eui: string - id: number - object_type: string - period_in: number - period_out: number - period_total: number - } - Insert: { - created_at?: string - dev_eui: string - id?: number - object_type: string - period_in?: number - period_out?: number - period_total?: number - } - Update: { - created_at?: string - dev_eui?: string - id?: number - object_type?: string - period_in?: number - period_out?: number - period_total?: number - } - Relationships: [] - } - cw_traffic2: { - Row: { - bicycle_count: number - bus_count: number - car_count: number - created_at: string - dev_eui: string - id: number - people_count: number - traffic_hour: string | null - truck_count: number - } - Insert: { - bicycle_count?: number - bus_count?: number - car_count?: number - created_at?: string - dev_eui: string - id?: number - people_count?: number - traffic_hour?: string | null - truck_count?: number - } - Update: { - bicycle_count?: number - bus_count?: number - car_count?: number - created_at?: string - dev_eui?: string - id?: number - people_count?: number - traffic_hour?: string | null - truck_count?: number - } - Relationships: [ - { - foreignKeyName: "cw_traffic2_dev_eui_fkey" - columns: ["dev_eui"] - isOneToOne: false - referencedRelation: "cw_devices" - referencedColumns: ["dev_eui"] - }, - ] - } - cw_water_data: { - Row: { - created_at: string - deapth_cm: number | null - dev_eui: string - id: number - pressure: number | null - spo2: number | null - temperature_c: number | null - } - Insert: { - created_at?: string - deapth_cm?: number | null - dev_eui: string - id?: number - pressure?: number | null - spo2?: number | null - temperature_c?: number | null - } - Update: { - created_at?: string - deapth_cm?: number | null - dev_eui?: string - id?: number - pressure?: number | null - spo2?: number | null - temperature_c?: number | null - } - Relationships: [ - { - foreignKeyName: "cw_water_data_dev_eui_fkey" - columns: ["dev_eui"] - isOneToOne: false - referencedRelation: "cw_devices" - referencedColumns: ["dev_eui"] - }, - ] - } - cw_watermeter_uplinks: { - Row: { - battery_level: number | null - count: number - created_at: string - dev_eui: string - id: number - internal_temp: number | null - } - Insert: { - battery_level?: number | null - count: number - created_at?: string - dev_eui: string - id?: number - internal_temp?: number | null - } - Update: { - battery_level?: number | null - count?: number - created_at?: string - dev_eui?: string - id?: number - internal_temp?: number | null - } - Relationships: [ - { - foreignKeyName: "cw_watermeter_uplinks_dev_eui_fkey" - columns: ["dev_eui"] - isOneToOne: false - referencedRelation: "cw_devices" - referencedColumns: ["dev_eui"] - }, - ] - } - ip_log: { - Row: { - created_at: string - dev_eui: string | null - device_id: string - id: number - ip: string | null - timestamp: string | null - } - Insert: { - created_at?: string - dev_eui?: string | null - device_id: string - id?: number - ip?: string | null - timestamp?: string | null - } - Update: { - created_at?: string - dev_eui?: string | null - device_id?: string - id?: number - ip?: string | null - timestamp?: string | null - } - Relationships: [ - { - foreignKeyName: "ip_log_dev_eui_fkey" - columns: ["dev_eui"] - isOneToOne: false - referencedRelation: "cw_devices" - referencedColumns: ["dev_eui"] - }, - ] - } - locations: { - Row: { - created_at: string | null - description: string | null - dev_eui: string | null - id: number - lat: number | null - lng: number | null - name: string - profile_id: string | null - sensor_type: string | null - } - Insert: { - created_at?: string | null - description?: string | null - dev_eui?: string | null - id?: number - lat?: number | null - lng?: number | null - name: string - profile_id?: string | null - sensor_type?: string | null - } - Update: { - created_at?: string | null - description?: string | null - dev_eui?: string | null - id?: number - lat?: number | null - lng?: number | null - name?: string - profile_id?: string | null - sensor_type?: string | null - } - Relationships: [] - } - netvox_ra02a: { - Row: { - battery: number - created_at: string - dev_eui: string - fireAlarm: number - gateway_count: number | null - highTempAlarm: number - id: number - profile_id: string | null - rssi: number | null - snr: number | null - temperatureC: number - } - Insert: { - battery: number - created_at?: string - dev_eui: string - fireAlarm: number - gateway_count?: number | null - highTempAlarm: number - id?: number - profile_id?: string | null - rssi?: number | null - snr?: number | null - temperatureC: number - } - Update: { - battery?: number - created_at?: string - dev_eui?: string - fireAlarm?: number - gateway_count?: number | null - highTempAlarm?: number - id?: number - profile_id?: string | null - rssi?: number | null - snr?: number | null - temperatureC?: number - } - Relationships: [ - { - foreignKeyName: "netvox_ra02a_profile_id_fkey" - columns: ["profile_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - ] - } - permissions: { - Row: { - allowed_by_profile_id: string | null - allowed_profile_id: string | null - created_at: string | null - description: string | null - id: number - resource: string - role_id: number - } - Insert: { - allowed_by_profile_id?: string | null - allowed_profile_id?: string | null - created_at?: string | null - description?: string | null - id?: number - resource: string - role_id: number - } - Update: { - allowed_by_profile_id?: string | null - allowed_profile_id?: string | null - created_at?: string | null - description?: string | null - id?: number - resource?: string - role_id?: number - } - Relationships: [ - { - foreignKeyName: "permissions_allowed_by_profile_id_fkey" - columns: ["allowed_by_profile_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - { - foreignKeyName: "permissions_allowed_profile_id_fkey" - columns: ["allowed_profile_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - ] - } - profiles: { - Row: { - accepted_agreements: boolean - avatar_url: string | null - discord: string | null - email: string | null - employer: string | null - full_name: string | null - id: string - last_login: string | null - updated_at: string | null - username: string | null - website: string | null - } - Insert: { - accepted_agreements?: boolean - avatar_url?: string | null - discord?: string | null - email?: string | null - employer?: string | null - full_name?: string | null - id: string - last_login?: string | null - updated_at?: string | null - username?: string | null - website?: string | null - } - Update: { - accepted_agreements?: boolean - avatar_url?: string | null - discord?: string | null - email?: string | null - employer?: string | null - full_name?: string | null - id?: string - last_login?: string | null - updated_at?: string | null - username?: string | null - website?: string | null - } - Relationships: [] - } - report_alert_points: { - Row: { - created_at: string - hex_color: string | null - id: number - max: number | null - min: number | null - name: string - operator: string | null - report_id: string - value: number | null - } - Insert: { - created_at?: string - hex_color?: string | null - id?: number - max?: number | null - min?: number | null - name: string - operator?: string | null - report_id: string - value?: number | null - } - Update: { - created_at?: string - hex_color?: string | null - id?: number - max?: number | null - min?: number | null - name?: string - operator?: string | null - report_id?: string - value?: number | null - } - Relationships: [ - { - foreignKeyName: "report_alert_points_report_id_fkey" - columns: ["report_id"] - isOneToOne: false - referencedRelation: "reports" - referencedColumns: ["report_id"] - }, - ] - } - report_user_schedule: { - Row: { - created_at: string - dev_eui: string - end_of_month: boolean - end_of_week: boolean - id: number - is_active: boolean - report_id: string - report_user_schedule_id: number - user_id: string - } - Insert: { - created_at?: string - dev_eui: string - end_of_month?: boolean - end_of_week?: boolean - id?: number - is_active?: boolean - report_id: string - report_user_schedule_id?: number - user_id?: string - } - Update: { - created_at?: string - dev_eui?: string - end_of_month?: boolean - end_of_week?: boolean - id?: number - is_active?: boolean - report_id?: string - report_user_schedule_id?: number - user_id?: string - } - Relationships: [ - { - foreignKeyName: "report_user_schedule_dev_eui_fkey" - columns: ["dev_eui"] - isOneToOne: false - referencedRelation: "cw_devices" - referencedColumns: ["dev_eui"] - }, - { - foreignKeyName: "report_user_schedule_report_id_fkey" - columns: ["report_id"] - isOneToOne: false - referencedRelation: "reports" - referencedColumns: ["report_id"] - }, - { - foreignKeyName: "report_user_schedule_user_id_fkey" - columns: ["user_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - ] - } - reports: { - Row: { - created_at: string - id: number - name: string - report_id: string - } - Insert: { - created_at?: string - id?: number - name: string - report_id?: string - } - Update: { - created_at?: string - id?: number - name?: string - report_id?: string - } - Relationships: [] - } - reports_templates: { - Row: { - created_at: string - dev_eui: string | null - id: number - name: string - owner_id: string - recipients: string | null - template: Json - } - Insert: { - created_at?: string - dev_eui?: string | null - id?: number - name: string - owner_id?: string - recipients?: string | null - template: Json - } - Update: { - created_at?: string - dev_eui?: string | null - id?: number - name?: string - owner_id?: string - recipients?: string | null - template?: Json - } - Relationships: [ - { - foreignKeyName: "reports_templates_dev_eui_fkey" - columns: ["dev_eui"] - isOneToOne: false - referencedRelation: "cw_devices" - referencedColumns: ["dev_eui"] - }, - { - foreignKeyName: "reports_templates_owner_id_fkey" - columns: ["owner_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] - }, - ] - } - user_discord_connections: { - Row: { - access_token: string - avatar: string | null - created_at: string | null - discord_user_id: string - discord_username: string - id: string - token_type: string - updated_at: string | null - user_id: string - } - Insert: { - access_token: string - avatar?: string | null - created_at?: string | null - discord_user_id: string - discord_username: string - id?: string - token_type: string - updated_at?: string | null - user_id: string - } - Update: { - access_token?: string - avatar?: string | null - created_at?: string | null - discord_user_id?: string - discord_username?: string - id?: string - token_type?: string - updated_at?: string | null - user_id?: string - } - Relationships: [] - } - } - Views: { - [_ in never]: never - } - Functions: { - delete_avatar: { - Args: { avatar_url: string } - Returns: Record - } - delete_storage_object: { - Args: { bucket: string; object: string } - Returns: Record - } - get_hloc_data: { - Args: - | { - p_dev_eui: string - p_bucket_interval: string - p_time_range: string - p_metric: string - } - | { - p_dev_eui: string - p_bucket_interval: string - p_time_range: string - p_metric: string - p_table: string - } - | { - start_time: string - end_time: string - time_interval: string - table_name: string - device_eui: string - } - Returns: { - bucket: string - dev_eui: string - open_val: number - close_val: number - low_val: number - high_val: number - }[] - } - get_location_for_user: { - Args: { user_id: string } - Returns: number[] - } - get_road_events: { - Args: { time_grouping: string } - Returns: { - group_period: string - event_count: number - }[] - } - get_road_events_summary1: { - Args: { - classes: string[] - end_date: string - line_id: string - start_date: string - time_span: string - } - Returns: { - period_start: string - count: number - }[] - } - } - Enums: { - [_ in never]: never - } - CompositeTypes: { - [_ in never]: never - } - } -} + public: { + Tables: { + api_keys: { + Row: { + api_key: string | null; + created_at: string; + Description: string | null; + expires_at: string | null; + id: number; + owner_id: string; + }; + Insert: { + api_key?: string | null; + created_at?: string; + Description?: string | null; + expires_at?: string | null; + id?: number; + owner_id?: string; + }; + Update: { + api_key?: string | null; + created_at?: string; + Description?: string | null; + expires_at?: string | null; + id?: number; + owner_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'api_keys_owner_id_fkey'; + columns: ['owner_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + communication_methods: { + Row: { + communication_method_id: number; + created_at: string; + id: number; + is_active: boolean; + name: string; + }; + Insert: { + communication_method_id?: number; + created_at?: string; + id?: number; + is_active?: boolean; + name: string; + }; + Update: { + communication_method_id?: number; + created_at?: string; + id?: number; + is_active?: boolean; + name?: string; + }; + Relationships: []; + }; + customers: { + Row: { + created_at: string | null; + email: string | null; + id: string; + stripe_customer_id: string; + updated_at: string | null; + user_id: string; + }; + Insert: { + created_at?: string | null; + email?: string | null; + id: string; + stripe_customer_id: string; + updated_at?: string | null; + user_id: string; + }; + Update: { + created_at?: string | null; + email?: string | null; + id?: string; + stripe_customer_id?: string; + updated_at?: string | null; + user_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'customers_user_id_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + cw_air_data: { + Row: { + co: number | null; + co2: number | null; + created_at: string; + dev_eui: string; + humidity: number | null; + is_simulated: boolean; + lux: number | null; + pressure: number | null; + rainfall: number | null; + temperature_c: number | null; + uv_index: number | null; + wind_direction: number | null; + wind_speed: number | null; + }; + Insert: { + co?: number | null; + co2?: number | null; + created_at?: string; + dev_eui: string; + humidity?: number | null; + is_simulated?: boolean; + lux?: number | null; + pressure?: number | null; + rainfall?: number | null; + temperature_c?: number | null; + uv_index?: number | null; + wind_direction?: number | null; + wind_speed?: number | null; + }; + Update: { + co?: number | null; + co2?: number | null; + created_at?: string; + dev_eui?: string; + humidity?: number | null; + is_simulated?: boolean; + lux?: number | null; + pressure?: number | null; + rainfall?: number | null; + temperature_c?: number | null; + uv_index?: number | null; + wind_direction?: number | null; + wind_speed?: number | null; + }; + Relationships: []; + }; + cw_air_thvd: { + Row: { + created_at: string; + dev_eui: string; + dewPointC: number | null; + humidity: number; + id: number; + profile_id: string | null; + temperatureC: number; + vpd: number | null; + }; + Insert: { + created_at?: string; + dev_eui: string; + dewPointC?: number | null; + humidity: number; + id?: number; + profile_id?: string | null; + temperatureC: number; + vpd?: number | null; + }; + Update: { + created_at?: string; + dev_eui?: string; + dewPointC?: number | null; + humidity?: number; + id?: number; + profile_id?: string | null; + temperatureC?: number; + vpd?: number | null; + }; + Relationships: [ + { + foreignKeyName: 'cw_air_thvd_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + }, + { + foreignKeyName: 'public_cw_air_thvd_profile_id_fkey'; + columns: ['profile_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + cw_co2_alerts: { + Row: { + action: string; + cleared: boolean; + created_at: string; + dev_eui: string; + id: number; + OneSignalID: string | null; + operator: string; + profile_id: string; + receiver: string; + subject: string; + value: number; + }; + Insert: { + action: string; + cleared: boolean; + created_at?: string; + dev_eui: string; + id?: number; + OneSignalID?: string | null; + operator: string; + profile_id: string; + receiver: string; + subject: string; + value: number; + }; + Update: { + action?: string; + cleared?: boolean; + created_at?: string; + dev_eui?: string; + id?: number; + OneSignalID?: string | null; + operator?: string; + profile_id?: string; + receiver?: string; + subject?: string; + value?: number; + }; + Relationships: [ + { + foreignKeyName: 'cw_co2_alerts_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + }, + { + foreignKeyName: 'cw_co2_alerts_profile_id_fkey'; + columns: ['profile_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + cw_co2_uplinks: { + Row: { + battery: number | null; + co2_level: number | null; + created_at: string; + dev_eui: string | null; + humidity: number; + id: number; + pressure: number | null; + profile_id: string | null; + temperature: number; + }; + Insert: { + battery?: number | null; + co2_level?: number | null; + created_at?: string; + dev_eui?: string | null; + humidity: number; + id?: number; + pressure?: number | null; + profile_id?: string | null; + temperature: number; + }; + Update: { + battery?: number | null; + co2_level?: number | null; + created_at?: string; + dev_eui?: string | null; + humidity?: number; + id?: number; + pressure?: number | null; + profile_id?: string | null; + temperature?: number; + }; + Relationships: [ + { + foreignKeyName: 'cw_co2_uplinks_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + } + ]; + }; + cw_data_metadata: { + Row: { + adder: number; + created_at: string; + formatting: string | null; + icon: string | null; + id: number; + multiplier: number; + name: string; + notation: string; + public_name: string | null; + }; + Insert: { + adder?: number; + created_at?: string; + formatting?: string | null; + icon?: string | null; + id?: number; + multiplier?: number; + name: string; + notation?: string; + public_name?: string | null; + }; + Update: { + adder?: number; + created_at?: string; + formatting?: string | null; + icon?: string | null; + id?: number; + multiplier?: number; + name?: string; + notation?: string; + public_name?: string | null; + }; + Relationships: []; + }; + cw_device_owners: { + Row: { + dev_eui: string; + id: number; + owner_id: number; + permission_level: number; + user_id: string; + }; + Insert: { + dev_eui: string; + id?: number; + owner_id?: number; + permission_level?: number; + user_id: string; + }; + Update: { + dev_eui?: string; + id?: number; + owner_id?: number; + permission_level?: number; + user_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'cw_device_owners_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + }, + { + foreignKeyName: 'cw_device_owners_user_id_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + cw_device_type: { + Row: { + created_at: string; + data_table: string | null; + data_table_v2: string; + decoder: string | null; + default_upload_interval: number | null; + device_app: string | null; + id: number; + isActive: boolean; + manufacturer: string | null; + model: string | null; + name: string; + primary_data: string | null; + primary_data_notation: string; + primary_data_v2: string; + primary_divider: number; + primary_multiplier: number | null; + secondary_data: string; + secondary_data_notation: string; + secondary_data_v2: string; + secondary_divider: number; + secondary_multiplier: number; + TTI_application_id: string | null; + }; + Insert: { + created_at?: string; + data_table?: string | null; + data_table_v2: string; + decoder?: string | null; + default_upload_interval?: number | null; + device_app?: string | null; + id?: number; + isActive?: boolean; + manufacturer?: string | null; + model?: string | null; + name: string; + primary_data?: string | null; + primary_data_notation?: string; + primary_data_v2: string; + primary_divider?: number; + primary_multiplier?: number | null; + secondary_data?: string; + secondary_data_notation?: string; + secondary_data_v2: string; + secondary_divider?: number; + secondary_multiplier?: number; + TTI_application_id?: string | null; + }; + Update: { + created_at?: string; + data_table?: string | null; + data_table_v2?: string; + decoder?: string | null; + default_upload_interval?: number | null; + device_app?: string | null; + id?: number; + isActive?: boolean; + manufacturer?: string | null; + model?: string | null; + name?: string; + primary_data?: string | null; + primary_data_notation?: string; + primary_data_v2?: string; + primary_divider?: number; + primary_multiplier?: number | null; + secondary_data?: string; + secondary_data_notation?: string; + secondary_data_v2?: string; + secondary_divider?: number; + secondary_multiplier?: number; + TTI_application_id?: string | null; + }; + Relationships: []; + }; + cw_device_x_cw_data_metadata: { + Row: { + created_at: string; + cw_data_metadata: number; + device_type_id: number; + id: number; + relation_id: number; + }; + Insert: { + created_at?: string; + cw_data_metadata: number; + device_type_id: number; + id?: number; + relation_id?: number; + }; + Update: { + created_at?: string; + cw_data_metadata?: number; + device_type_id?: number; + id?: number; + relation_id?: number; + }; + Relationships: [ + { + foreignKeyName: 'cw_device_x_cw_data_metadata_cw_data_metadata_fkey'; + columns: ['cw_data_metadata']; + isOneToOne: false; + referencedRelation: 'cw_data_metadata'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'cw_device_x_cw_data_metadata_device_type_id_fkey'; + columns: ['device_type_id']; + isOneToOne: false; + referencedRelation: 'cw_device_type'; + referencedColumns: ['id']; + } + ]; + }; + cw_devices: { + Row: { + ai_provider: string | null; + battery_changed_at: string | null; + dev_eui: string; + installed_at: string | null; + lat: number | null; + location_id: number | null; + long: number | null; + name: string; + report_endpoint: string | null; + serial_number: string | null; + tti_name: string | null; + type: number | null; + upload_interval: number | null; + user_id: string | null; + warranty_start_date: string | null; + }; + Insert: { + ai_provider?: string | null; + battery_changed_at?: string | null; + dev_eui: string; + installed_at?: string | null; + lat?: number | null; + location_id?: number | null; + long?: number | null; + name?: string; + report_endpoint?: string | null; + serial_number?: string | null; + tti_name?: string | null; + type?: number | null; + upload_interval?: number | null; + user_id?: string | null; + warranty_start_date?: string | null; + }; + Update: { + ai_provider?: string | null; + battery_changed_at?: string | null; + dev_eui?: string; + installed_at?: string | null; + lat?: number | null; + location_id?: number | null; + long?: number | null; + name?: string; + report_endpoint?: string | null; + serial_number?: string | null; + tti_name?: string | null; + type?: number | null; + upload_interval?: number | null; + user_id?: string | null; + warranty_start_date?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'cw_devices_location_id_fkey'; + columns: ['location_id']; + isOneToOne: false; + referencedRelation: 'cw_locations'; + referencedColumns: ['location_id']; + }, + { + foreignKeyName: 'cw_devices_type_fkey'; + columns: ['type']; + isOneToOne: false; + referencedRelation: 'cw_device_type'; + referencedColumns: ['id']; + } + ]; + }; + cw_gateways: { + Row: { + created_at: string; + gateway_id: string; + gateway_name: string; + id: number; + is_online: boolean; + is_public: boolean; + updated_at: string | null; + }; + Insert: { + created_at?: string; + gateway_id: string; + gateway_name: string; + id?: number; + is_online: boolean; + is_public?: boolean; + updated_at?: string | null; + }; + Update: { + created_at?: string; + gateway_id?: string; + gateway_name?: string; + id?: number; + is_online?: boolean; + is_public?: boolean; + updated_at?: string | null; + }; + Relationships: []; + }; + cw_gateways_owners: { + Row: { + created_at: string; + gateway_id: number; + id: number; + user_id: string; + }; + Insert: { + created_at?: string; + gateway_id: number; + id?: number; + user_id: string; + }; + Update: { + created_at?: string; + gateway_id?: number; + id?: number; + user_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'cw_gateways_owners_gateway_id_fkey'; + columns: ['gateway_id']; + isOneToOne: false; + referencedRelation: 'cw_gateways'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'cw_gateways_owners_user_id_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + cw_location_owners: { + Row: { + description: string | null; + id: number; + is_active: boolean | null; + location_id: number; + owner_id: number; + permission_level: number | null; + user_id: string; + }; + Insert: { + description?: string | null; + id?: number; + is_active?: boolean | null; + location_id: number; + owner_id?: number; + permission_level?: number | null; + user_id: string; + }; + Update: { + description?: string | null; + id?: number; + is_active?: boolean | null; + location_id?: number; + owner_id?: number; + permission_level?: number | null; + user_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'cw_location_owners_location_id_fkey'; + columns: ['location_id']; + isOneToOne: false; + referencedRelation: 'cw_locations'; + referencedColumns: ['location_id']; + }, + { + foreignKeyName: 'cw_location_owners_user_id_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + cw_locations: { + Row: { + created_at: string; + description: string | null; + lat: number | null; + location_id: number; + long: number | null; + map_zoom: number | null; + name: string; + owner_id: string | null; + }; + Insert: { + created_at?: string; + description?: string | null; + lat?: number | null; + location_id?: number; + long?: number | null; + map_zoom?: number | null; + name: string; + owner_id?: string | null; + }; + Update: { + created_at?: string; + description?: string | null; + lat?: number | null; + location_id?: number; + long?: number | null; + map_zoom?: number | null; + name?: string; + owner_id?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'cw_locations_owner_id_fkey'; + columns: ['owner_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + cw_notifier_types: { + Row: { + created_at: string; + id: number; + name: string; + notifier_id: number; + }; + Insert: { + created_at?: string; + id?: number; + name: string; + notifier_id?: number; + }; + Update: { + created_at?: string; + id?: number; + name?: string; + notifier_id?: number; + }; + Relationships: []; + }; + cw_permission_level_types: { + Row: { + created_at: string; + id: number; + name: string; + permission_level_id: number; + }; + Insert: { + created_at?: string; + id?: number; + name: string; + permission_level_id?: number; + }; + Update: { + created_at?: string; + id?: number; + name?: string; + permission_level_id?: number; + }; + Relationships: []; + }; + cw_relay_data: { + Row: { + created_at: string; + dev_eui: string; + id: number; + relay_1: boolean; + relay_2: boolean | null; + relay_3: boolean | null; + }; + Insert: { + created_at?: string; + dev_eui: string; + id?: number; + relay_1?: boolean; + relay_2?: boolean | null; + relay_3?: boolean | null; + }; + Update: { + created_at?: string; + dev_eui?: string; + id?: number; + relay_1?: boolean; + relay_2?: boolean | null; + relay_3?: boolean | null; + }; + Relationships: [ + { + foreignKeyName: 'cw_relay_data_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + } + ]; + }; + cw_rule_criteria: { + Row: { + created_at: string; + criteria_id: number | null; + id: number; + operator: string; + parent_id: string | null; + reset_value: number | null; + ruleGroupId: string; + subject: string; + trigger_value: number; + }; + Insert: { + created_at?: string; + criteria_id?: number | null; + id?: number; + operator: string; + parent_id?: string | null; + reset_value?: number | null; + ruleGroupId: string; + subject: string; + trigger_value: number; + }; + Update: { + created_at?: string; + criteria_id?: number | null; + id?: number; + operator?: string; + parent_id?: string | null; + reset_value?: number | null; + ruleGroupId?: string; + subject?: string; + trigger_value?: number; + }; + Relationships: [ + { + foreignKeyName: 'public_cw_rule_criteria_ruleGroupId_fkey'; + columns: ['ruleGroupId']; + isOneToOne: false; + referencedRelation: 'cw_rules'; + referencedColumns: ['ruleGroupId']; + } + ]; + }; + cw_rules: { + Row: { + action_recipient: string; + created_at: string; + dev_eui: string | null; + id: number; + is_triggered: boolean; + last_triggered: string | null; + name: string; + notifier_type: number; + profile_id: string; + ruleGroupId: string; + trigger_count: number; + }; + Insert: { + action_recipient: string; + created_at?: string; + dev_eui?: string | null; + id?: number; + is_triggered?: boolean; + last_triggered?: string | null; + name: string; + notifier_type: number; + profile_id?: string; + ruleGroupId: string; + trigger_count?: number; + }; + Update: { + action_recipient?: string; + created_at?: string; + dev_eui?: string | null; + id?: number; + is_triggered?: boolean; + last_triggered?: string | null; + name?: string; + notifier_type?: number; + profile_id?: string; + ruleGroupId?: string; + trigger_count?: number; + }; + Relationships: [ + { + foreignKeyName: 'cw_rules_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + }, + { + foreignKeyName: 'cw_rules_notifier_type_fkey'; + columns: ['notifier_type']; + isOneToOne: false; + referencedRelation: 'cw_notifier_types'; + referencedColumns: ['notifier_id']; + }, + { + foreignKeyName: 'public_cw_rules_profile_id_fkey'; + columns: ['profile_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + cw_soil_data: { + Row: { + created_at: string; + dev_eui: string; + ec: number | null; + moisture: number | null; + ph: number | null; + temperature_c: number | null; + }; + Insert: { + created_at?: string; + dev_eui: string; + ec?: number | null; + moisture?: number | null; + ph?: number | null; + temperature_c?: number | null; + }; + Update: { + created_at?: string; + dev_eui?: string; + ec?: number | null; + moisture?: number | null; + ph?: number | null; + temperature_c?: number | null; + }; + Relationships: []; + }; + cw_soil_uplinks: { + Row: { + battery: number | null; + created_at: string; + dev_eui: string | null; + ec: number | null; + id: number; + internal_temp: number | null; + k: number | null; + moisture: number; + n: number | null; + p: number | null; + ph: number | null; + read_attempts: number | null; + real_duration: number | null; + temperature: number; + }; + Insert: { + battery?: number | null; + created_at?: string; + dev_eui?: string | null; + ec?: number | null; + id?: number; + internal_temp?: number | null; + k?: number | null; + moisture: number; + n?: number | null; + p?: number | null; + ph?: number | null; + read_attempts?: number | null; + real_duration?: number | null; + temperature: number; + }; + Update: { + battery?: number | null; + created_at?: string; + dev_eui?: string | null; + ec?: number | null; + id?: number; + internal_temp?: number | null; + k?: number | null; + moisture?: number; + n?: number | null; + p?: number | null; + ph?: number | null; + read_attempts?: number | null; + real_duration?: number | null; + temperature?: number; + }; + Relationships: [ + { + foreignKeyName: 'cw_soil_uplinks_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + } + ]; + }; + cw_traffic: { + Row: { + created_at: string; + dev_eui: string; + id: number; + object_type: string; + period_in: number; + period_out: number; + period_total: number; + }; + Insert: { + created_at?: string; + dev_eui: string; + id?: number; + object_type: string; + period_in?: number; + period_out?: number; + period_total?: number; + }; + Update: { + created_at?: string; + dev_eui?: string; + id?: number; + object_type?: string; + period_in?: number; + period_out?: number; + period_total?: number; + }; + Relationships: []; + }; + cw_traffic2: { + Row: { + bicycle_count: number; + bus_count: number; + car_count: number; + created_at: string; + dev_eui: string; + id: number; + people_count: number; + traffic_hour: string | null; + truck_count: number; + }; + Insert: { + bicycle_count?: number; + bus_count?: number; + car_count?: number; + created_at?: string; + dev_eui: string; + id?: number; + people_count?: number; + traffic_hour?: string | null; + truck_count?: number; + }; + Update: { + bicycle_count?: number; + bus_count?: number; + car_count?: number; + created_at?: string; + dev_eui?: string; + id?: number; + people_count?: number; + traffic_hour?: string | null; + truck_count?: number; + }; + Relationships: [ + { + foreignKeyName: 'cw_traffic2_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + } + ]; + }; + cw_water_data: { + Row: { + created_at: string; + deapth_cm: number | null; + dev_eui: string; + id: number; + pressure: number | null; + spo2: number | null; + temperature_c: number | null; + }; + Insert: { + created_at?: string; + deapth_cm?: number | null; + dev_eui: string; + id?: number; + pressure?: number | null; + spo2?: number | null; + temperature_c?: number | null; + }; + Update: { + created_at?: string; + deapth_cm?: number | null; + dev_eui?: string; + id?: number; + pressure?: number | null; + spo2?: number | null; + temperature_c?: number | null; + }; + Relationships: [ + { + foreignKeyName: 'cw_water_data_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + } + ]; + }; + cw_watermeter_uplinks: { + Row: { + battery_level: number | null; + count: number; + created_at: string; + dev_eui: string; + id: number; + internal_temp: number | null; + }; + Insert: { + battery_level?: number | null; + count: number; + created_at?: string; + dev_eui: string; + id?: number; + internal_temp?: number | null; + }; + Update: { + battery_level?: number | null; + count?: number; + created_at?: string; + dev_eui?: string; + id?: number; + internal_temp?: number | null; + }; + Relationships: [ + { + foreignKeyName: 'cw_watermeter_uplinks_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + } + ]; + }; + device_data_sample: { + Row: { + co2_level: number | null; + created_at: string; + dev_eui: string; + humidity: number | null; + id: number; + pressure: number | null; + temperature_c: number | null; + }; + Insert: { + co2_level?: number | null; + created_at: string; + dev_eui: string; + humidity?: number | null; + id?: number; + pressure?: number | null; + temperature_c?: number | null; + }; + Update: { + co2_level?: number | null; + created_at?: string; + dev_eui?: string; + humidity?: number | null; + id?: number; + pressure?: number | null; + temperature_c?: number | null; + }; + Relationships: []; + }; + ip_log: { + Row: { + created_at: string; + dev_eui: string | null; + device_id: string; + id: number; + ip: string | null; + timestamp: string | null; + }; + Insert: { + created_at?: string; + dev_eui?: string | null; + device_id: string; + id?: number; + ip?: string | null; + timestamp?: string | null; + }; + Update: { + created_at?: string; + dev_eui?: string | null; + device_id?: string; + id?: number; + ip?: string | null; + timestamp?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'ip_log_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + } + ]; + }; + locations: { + Row: { + created_at: string | null; + description: string | null; + dev_eui: string | null; + id: number; + lat: number | null; + lng: number | null; + name: string; + profile_id: string | null; + sensor_type: string | null; + }; + Insert: { + created_at?: string | null; + description?: string | null; + dev_eui?: string | null; + id?: number; + lat?: number | null; + lng?: number | null; + name: string; + profile_id?: string | null; + sensor_type?: string | null; + }; + Update: { + created_at?: string | null; + description?: string | null; + dev_eui?: string | null; + id?: number; + lat?: number | null; + lng?: number | null; + name?: string; + profile_id?: string | null; + sensor_type?: string | null; + }; + Relationships: []; + }; + netvox_ra02a: { + Row: { + battery: number; + created_at: string; + dev_eui: string; + fireAlarm: number; + gateway_count: number | null; + highTempAlarm: number; + id: number; + profile_id: string | null; + rssi: number | null; + snr: number | null; + temperatureC: number; + }; + Insert: { + battery: number; + created_at?: string; + dev_eui: string; + fireAlarm: number; + gateway_count?: number | null; + highTempAlarm: number; + id?: number; + profile_id?: string | null; + rssi?: number | null; + snr?: number | null; + temperatureC: number; + }; + Update: { + battery?: number; + created_at?: string; + dev_eui?: string; + fireAlarm?: number; + gateway_count?: number | null; + highTempAlarm?: number; + id?: number; + profile_id?: string | null; + rssi?: number | null; + snr?: number | null; + temperatureC?: number; + }; + Relationships: [ + { + foreignKeyName: 'netvox_ra02a_profile_id_fkey'; + columns: ['profile_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + payments: { + Row: { + amount: number; + created_at: string | null; + currency: string; + id: string; + status: string; + stripe_invoice_id: string | null; + stripe_payment_intent_id: string; + subscription_id: string | null; + updated_at: string | null; + user_id: string; + }; + Insert: { + amount: number; + created_at?: string | null; + currency?: string; + id: string; + status: string; + stripe_invoice_id?: string | null; + stripe_payment_intent_id: string; + subscription_id?: string | null; + updated_at?: string | null; + user_id: string; + }; + Update: { + amount?: number; + created_at?: string | null; + currency?: string; + id?: string; + status?: string; + stripe_invoice_id?: string | null; + stripe_payment_intent_id?: string; + subscription_id?: string | null; + updated_at?: string | null; + user_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'payments_subscription_id_fkey'; + columns: ['subscription_id']; + isOneToOne: false; + referencedRelation: 'subscriptions'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'payments_user_id_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + permissions: { + Row: { + allowed_by_profile_id: string | null; + allowed_profile_id: string | null; + created_at: string | null; + description: string | null; + id: number; + resource: string; + role_id: number; + }; + Insert: { + allowed_by_profile_id?: string | null; + allowed_profile_id?: string | null; + created_at?: string | null; + description?: string | null; + id?: number; + resource: string; + role_id: number; + }; + Update: { + allowed_by_profile_id?: string | null; + allowed_profile_id?: string | null; + created_at?: string | null; + description?: string | null; + id?: number; + resource?: string; + role_id?: number; + }; + Relationships: [ + { + foreignKeyName: 'permissions_allowed_by_profile_id_fkey'; + columns: ['allowed_by_profile_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'permissions_allowed_profile_id_fkey'; + columns: ['allowed_profile_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + profiles: { + Row: { + accepted_agreements: boolean; + avatar_url: string | null; + discord: string | null; + email: string | null; + employer: string | null; + full_name: string | null; + id: string; + last_login: string | null; + updated_at: string | null; + username: string | null; + website: string | null; + }; + Insert: { + accepted_agreements?: boolean; + avatar_url?: string | null; + discord?: string | null; + email?: string | null; + employer?: string | null; + full_name?: string | null; + id: string; + last_login?: string | null; + updated_at?: string | null; + username?: string | null; + website?: string | null; + }; + Update: { + accepted_agreements?: boolean; + avatar_url?: string | null; + discord?: string | null; + email?: string | null; + employer?: string | null; + full_name?: string | null; + id?: string; + last_login?: string | null; + updated_at?: string | null; + username?: string | null; + website?: string | null; + }; + Relationships: []; + }; + report_alert_points: { + Row: { + created_at: string; + data_point_key: string | null; + hex_color: string | null; + id: number; + max: number | null; + min: number | null; + name: string; + operator: string | null; + report_id: string; + }; + Insert: { + created_at?: string; + data_point_key?: string | null; + hex_color?: string | null; + id?: number; + max?: number | null; + min?: number | null; + name: string; + operator?: string | null; + report_id: string; + }; + Update: { + created_at?: string; + data_point_key?: string | null; + hex_color?: string | null; + id?: number; + max?: number | null; + min?: number | null; + name?: string; + operator?: string | null; + report_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'report_alert_points_report_id_fkey'; + columns: ['report_id']; + isOneToOne: false; + referencedRelation: 'reports'; + referencedColumns: ['report_id']; + } + ]; + }; + report_recipients: { + Row: { + communication_method: number; + created_at: string; + id: number; + profile_id: string; + report_id: string; + }; + Insert: { + communication_method: number; + created_at?: string; + id?: number; + profile_id: string; + report_id: string; + }; + Update: { + communication_method?: number; + created_at?: string; + id?: number; + profile_id?: string; + report_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'report_recipients_communication_method_fkey'; + columns: ['communication_method']; + isOneToOne: false; + referencedRelation: 'communication_methods'; + referencedColumns: ['communication_method_id']; + }, + { + foreignKeyName: 'report_recipients_profile_id_fkey'; + columns: ['profile_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'report_recipients_report_id_fkey'; + columns: ['report_id']; + isOneToOne: false; + referencedRelation: 'reports'; + referencedColumns: ['report_id']; + } + ]; + }; + report_user_schedule: { + Row: { + created_at: string; + dev_eui: string; + end_of_month: boolean; + end_of_week: boolean; + id: number; + is_active: boolean; + report_id: string; + report_user_schedule_id: number; + user_id: string; + }; + Insert: { + created_at?: string; + dev_eui: string; + end_of_month?: boolean; + end_of_week?: boolean; + id?: number; + is_active?: boolean; + report_id: string; + report_user_schedule_id?: number; + user_id?: string; + }; + Update: { + created_at?: string; + dev_eui?: string; + end_of_month?: boolean; + end_of_week?: boolean; + id?: number; + is_active?: boolean; + report_id?: string; + report_user_schedule_id?: number; + user_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'report_user_schedule_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + }, + { + foreignKeyName: 'report_user_schedule_report_id_fkey'; + columns: ['report_id']; + isOneToOne: false; + referencedRelation: 'reports'; + referencedColumns: ['report_id']; + }, + { + foreignKeyName: 'report_user_schedule_user_id_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + reports: { + Row: { + created_at: string; + dev_eui: string; + id: number; + name: string; + report_id: string; + }; + Insert: { + created_at?: string; + dev_eui: string; + id?: number; + name: string; + report_id?: string; + }; + Update: { + created_at?: string; + dev_eui?: string; + id?: number; + name?: string; + report_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'reports_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + } + ]; + }; + reports_templates: { + Row: { + created_at: string; + dev_eui: string | null; + id: number; + name: string; + owner_id: string; + recipients: string | null; + template: Json; + }; + Insert: { + created_at?: string; + dev_eui?: string | null; + id?: number; + name: string; + owner_id?: string; + recipients?: string | null; + template: Json; + }; + Update: { + created_at?: string; + dev_eui?: string | null; + id?: number; + name?: string; + owner_id?: string; + recipients?: string | null; + template?: Json; + }; + Relationships: [ + { + foreignKeyName: 'reports_templates_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + }, + { + foreignKeyName: 'reports_templates_owner_id_fkey'; + columns: ['owner_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + subscription_items: { + Row: { + created_at: string | null; + id: string; + quantity: number | null; + stripe_price_id: string; + stripe_subscription_item_id: string; + subscription_id: string; + updated_at: string | null; + }; + Insert: { + created_at?: string | null; + id: string; + quantity?: number | null; + stripe_price_id: string; + stripe_subscription_item_id: string; + subscription_id: string; + updated_at?: string | null; + }; + Update: { + created_at?: string | null; + id?: string; + quantity?: number | null; + stripe_price_id?: string; + stripe_subscription_item_id?: string; + subscription_id?: string; + updated_at?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'subscription_items_subscription_id_fkey'; + columns: ['subscription_id']; + isOneToOne: false; + referencedRelation: 'subscriptions'; + referencedColumns: ['id']; + } + ]; + }; + subscriptions: { + Row: { + cancel_at_period_end: boolean | null; + canceled_at: string | null; + created_at: string | null; + current_period_end: string | null; + current_period_start: string | null; + customer_id: string; + id: string; + status: string; + stripe_price_id: string; + stripe_subscription_id: string; + updated_at: string | null; + user_id: string; + }; + Insert: { + cancel_at_period_end?: boolean | null; + canceled_at?: string | null; + created_at?: string | null; + current_period_end?: string | null; + current_period_start?: string | null; + customer_id: string; + id: string; + status: string; + stripe_price_id: string; + stripe_subscription_id: string; + updated_at?: string | null; + user_id: string; + }; + Update: { + cancel_at_period_end?: boolean | null; + canceled_at?: string | null; + created_at?: string | null; + current_period_end?: string | null; + current_period_start?: string | null; + customer_id?: string; + id?: string; + status?: string; + stripe_price_id?: string; + stripe_subscription_id?: string; + updated_at?: string | null; + user_id?: string; + }; + Relationships: [ + { + foreignKeyName: 'subscriptions_user_id_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + } + ]; + }; + user_discord_connections: { + Row: { + access_token: string; + avatar: string | null; + created_at: string | null; + discord_user_id: string; + discord_username: string; + id: string; + token_type: string; + updated_at: string | null; + user_id: string; + }; + Insert: { + access_token: string; + avatar?: string | null; + created_at?: string | null; + discord_user_id: string; + discord_username: string; + id?: string; + token_type: string; + updated_at?: string | null; + user_id: string; + }; + Update: { + access_token?: string; + avatar?: string | null; + created_at?: string | null; + discord_user_id?: string; + discord_username?: string; + id?: string; + token_type?: string; + updated_at?: string | null; + user_id?: string; + }; + Relationships: []; + }; + }; + Views: { + [_ in never]: never; + }; + Functions: { + delete_avatar: { + Args: { avatar_url: string }; + Returns: Record; + }; + delete_storage_object: { + Args: { bucket: string; object: string }; + Returns: Record; + }; + get_filtered_device_report_data: { + Args: { + dev_id: string; + start_time: string; + end_time: string; + interval_minutes: number; + target_column: string; + compare_operator: string; + compare_value_min: number; + compare_value_max?: number; + }; + Returns: Json[]; + }; + get_filtered_device_report_data_multi: { + Args: { + p_dev_id: string; + p_start_time: string; + p_end_time: string; + p_interval_minutes: number; + p_columns: string[]; + p_ops: string[]; + p_mins: number[]; + p_maxs: number[]; + }; + Returns: Json[]; + }; + get_hloc_data: { + Args: + | { + p_dev_eui: string; + p_bucket_interval: string; + p_time_range: string; + p_metric: string; + } + | { + p_dev_eui: string; + p_bucket_interval: string; + p_time_range: string; + p_metric: string; + p_table: string; + } + | { + start_time: string; + end_time: string; + time_interval: string; + table_name: string; + device_eui: string; + }; + Returns: { + bucket: string; + dev_eui: string; + open_val: number; + close_val: number; + low_val: number; + high_val: number; + }[]; + }; + get_location_for_user: { + Args: { user_id: string }; + Returns: number[]; + }; + get_multi_condition_device_report_data: { + Args: { + p_dev_id: string; + p_report_id: string; + p_start_time: string; + p_end_time: string; + p_interval_minutes: number; + }; + Returns: Json[]; + }; + get_report_data_for_device: { + Args: + | { input_dev_eui: string; input_start: string; input_end: string } + | { + input_dev_eui: string; + input_start: string; + input_end: string; + input_timezone?: string; + } + | { + input_dev_eui: string; + input_start: string; + input_end: string; + input_timezone?: string; + input_interval_minutes?: number; + }; + Returns: Json; + }; + get_road_events: { + Args: { time_grouping: string }; + Returns: { + group_period: string; + event_count: number; + }[]; + }; + get_road_events_summary1: { + Args: { + classes: string[]; + end_date: string; + line_id: string; + start_date: string; + time_span: string; + }; + Returns: { + period_start: string; + count: number; + }[]; + }; + }; + Enums: { + [_ in never]: never; + }; + CompositeTypes: { + [_ in never]: never; + }; + }; +}; -type DefaultSchema = Database[Extract] +type DefaultSchema = Database[Extract]; export type Tables< - DefaultSchemaTableNameOrOptions extends - | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) - | { schema: keyof Database }, - TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof Database - } - ? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & - Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) - : never = never, + DefaultSchemaTableNameOrOptions extends + | keyof (DefaultSchema['Tables'] & DefaultSchema['Views']) + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database; + } + ? keyof (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & + Database[DefaultSchemaTableNameOrOptions['schema']]['Views']) + : never = never > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } - ? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & - Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends { - Row: infer R - } - ? R - : never - : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] & - DefaultSchema["Views"]) - ? (DefaultSchema["Tables"] & - DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends { - Row: infer R - } - ? R - : never - : never + ? (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & + Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends { + Row: infer R; + } + ? R + : never + : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] & DefaultSchema['Views']) + ? (DefaultSchema['Tables'] & DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends { + Row: infer R; + } + ? R + : never + : never; export type TablesInsert< - DefaultSchemaTableNameOrOptions extends - | keyof DefaultSchema["Tables"] - | { schema: keyof Database }, - TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof Database - } - ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] - : never = never, + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema['Tables'] + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database; + } + ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] + : never = never > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } - ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { - Insert: infer I - } - ? I - : never - : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] - ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { - Insert: infer I - } - ? I - : never - : never + ? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { + Insert: infer I; + } + ? I + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables'] + ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends { + Insert: infer I; + } + ? I + : never + : never; export type TablesUpdate< - DefaultSchemaTableNameOrOptions extends - | keyof DefaultSchema["Tables"] - | { schema: keyof Database }, - TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof Database - } - ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] - : never = never, + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema['Tables'] + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database; + } + ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] + : never = never > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } - ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { - Update: infer U - } - ? U - : never - : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] - ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { - Update: infer U - } - ? U - : never - : never + ? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { + Update: infer U; + } + ? U + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables'] + ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends { + Update: infer U; + } + ? U + : never + : never; export type Enums< - DefaultSchemaEnumNameOrOptions extends - | keyof DefaultSchema["Enums"] - | { schema: keyof Database }, - EnumName extends DefaultSchemaEnumNameOrOptions extends { - schema: keyof Database - } - ? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] - : never = never, + DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums'] | { schema: keyof Database }, + EnumName extends DefaultSchemaEnumNameOrOptions extends { + schema: keyof Database; + } + ? keyof Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'] + : never = never > = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database } - ? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName] - : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"] - ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions] - : never + ? Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName] + : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums'] + ? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions] + : never; export type CompositeTypes< - PublicCompositeTypeNameOrOptions extends - | keyof DefaultSchema["CompositeTypes"] - | { schema: keyof Database }, - CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { - schema: keyof Database - } - ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] - : never = never, + PublicCompositeTypeNameOrOptions extends + | keyof DefaultSchema['CompositeTypes'] + | { schema: keyof Database }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof Database; + } + ? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'] + : never = never > = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } - ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] - : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"] - ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] - : never + ? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes'] + ? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions] + : never; export const Constants = { - public: { - Enums: {}, - }, -} as const + public: { + Enums: {} + } +} as const; diff --git a/package.json b/package.json index 6ca369d8..6b25d84c 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@layerstack/utils": "^1.0.0", "@mdi/js": "^7.4.47", "@stencil/store": "^2.1.3", + "@stripe/stripe-js": "^7.4.0", "@supabase/ssr": "^0.6.1", "@supabase/supabase-js": "^2.49.4", "@types/lodash": "^4.17.16", @@ -84,6 +85,7 @@ "luxon": "^3.6.1", "pdfkit": "^0.17.1", "reflect-metadata": "^0.2.2", + "stripe": "^18.3.0", "svelte-i18n": "^4.0.1", "svelte-ux": "^1.0.4", "swagger-ui": "^5.21.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 072ab6c4..8d428598 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@stencil/store': specifier: ^2.1.3 version: 2.1.3(@stencil/core@4.35.1) + '@stripe/stripe-js': + specifier: ^7.4.0 + version: 7.4.0 '@supabase/ssr': specifier: ^0.6.1 version: 0.6.1(@supabase/supabase-js@2.50.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)) @@ -74,6 +77,9 @@ importers: reflect-metadata: specifier: ^0.2.2 version: 0.2.2 + stripe: + specifier: ^18.3.0 + version: 18.3.0(@types/node@24.0.3) svelte-i18n: specifier: ^4.0.1 version: 4.0.1(svelte@5.34.7) @@ -1485,6 +1491,10 @@ packages: peerDependencies: '@stencil/core': '>=2.0.0 || >=3.0.0 || >= 4.0.0-beta.0 || >= 4.0.0' + '@stripe/stripe-js@7.4.0': + resolution: {integrity: sha512-lQHQPfXPTBeh0XFjq6PqSBAyR7umwcJbvJhXV77uGCUDD6ymXJU/f2164ydLMLCCceNuPlbV9b+1smx98efwWQ==} + engines: {node: '>=12.16'} + '@supabase/auth-js@2.70.0': resolution: {integrity: sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==} @@ -4144,6 +4154,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -4595,6 +4609,15 @@ packages: strip-literal@3.0.0: resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + stripe@18.3.0: + resolution: {integrity: sha512-FkxrTUUcWB4CVN2yzgsfF/YHD6WgYHduaa7VmokCy5TLCgl5UNJkwortxcedrxSavQ8Qfa4Ir4JxcbIYiBsyLg==} + engines: {node: '>=12.*'} + peerDependencies: + '@types/node': '>=12.x.x' + peerDependenciesMeta: + '@types/node': + optional: true + style-to-object@1.0.9: resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} @@ -6617,6 +6640,8 @@ snapshots: dependencies: '@stencil/core': 4.35.1 + '@stripe/stripe-js@7.4.0': {} + '@supabase/auth-js@2.70.0': dependencies: '@supabase/node-fetch': 2.6.15 @@ -9639,6 +9664,10 @@ snapshots: punycode@2.3.1: {} + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -10143,6 +10172,12 @@ snapshots: dependencies: js-tokens: 9.0.1 + stripe@18.3.0(@types/node@24.0.3): + dependencies: + qs: 6.14.0 + optionalDependencies: + '@types/node': 24.0.3 + style-to-object@1.0.9: dependencies: inline-style-parser: 0.2.4 diff --git a/scripts/build-info.js b/scripts/build-info.js index 71cad40c..5adf1bea 100644 --- a/scripts/build-info.js +++ b/scripts/build-info.js @@ -35,27 +35,35 @@ function getLocalIP() { } const ipAddress = getLocalIP(); -// format output -const lines = [ - '=== Build Info ===', - `Commit : ${commitHash}`, - `Branch : ${branch}`, - `Author : ${commitUser}`, - `Date : ${buildDate}`, - `Builder : ${user}@${host}`, - `IP Address : ${ipAddress}`, - '==================' -]; -const output = lines.join('\n'); +// format output as JSON +const buildInfo = { + commit: commitHash, + branch: branch, + author: commitUser, + date: buildDate, + builder: `${user}@${host}`, + ipAddress: ipAddress, + timestamp: Date.now() +}; + +const jsonOutput = JSON.stringify(buildInfo, null, 2); // print to console -console.log('\n\x1b[33m' + lines[0] + '\x1b[0m'); -lines - .slice(1, -1) - .forEach((line) => console.log(` • ${line.split(': ')[0].padEnd(12)} ${line.split(': ')[1]}`)); -console.log('\x1b[33m' + lines[lines.length - 1] + '\x1b[0m\n'); - -// write to file at project root -const filePath = path.join(process.cwd(), 'build-info.txt'); -fs.writeFileSync(filePath, output + '\n', { encoding: 'utf8' }); +console.log('\n\x1b[33m=== Build Info ===\x1b[0m'); +console.log(` • Commit : ${buildInfo.commit}`); +console.log(` • Branch : ${buildInfo.branch}`); +console.log(` • Author : ${buildInfo.author}`); +console.log(` • Date : ${buildInfo.date}`); +console.log(` • Builder : ${buildInfo.builder}`); +console.log(` • IP Address : ${buildInfo.ipAddress}`); +console.log('\x1b[33m==================\x1b[0m\n'); + +// write to JSON file in static directory +const staticDir = path.join(process.cwd(), 'static'); +if (!fs.existsSync(staticDir)) { + fs.mkdirSync(staticDir, { recursive: true }); +} + +const filePath = path.join(staticDir, 'build-info.json'); +fs.writeFileSync(filePath, jsonOutput + '\n', { encoding: 'utf8' }); console.log(`Build info written to ${filePath}\n`); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index f132baee..4f58d343 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -8,6 +8,7 @@ const PUBLIC_ROUTES = [ '/offline.html', '/auth', // All routes under /auth/ '/api/auth', // Only authentication-related API routes + '/api/webhook', // Webhook endpoints (authenticated via webhook signatures) '/static', // All static assets '/static/icons', '/static/screenshots' diff --git a/src/lib/components/GlobalSidebar.svelte b/src/lib/components/GlobalSidebar.svelte new file mode 100644 index 00000000..91e742ce --- /dev/null +++ b/src/lib/components/GlobalSidebar.svelte @@ -0,0 +1,246 @@ + + + +{#if isOpen} +
sidebarStore.close()} + aria-hidden="true" + >
+{/if} + + + + + diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 32e0f2f2..13a668e1 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -9,6 +9,8 @@ import ThemeToggle from './theme/ThemeToggle.svelte'; import Button from './UI/buttons/Button.svelte'; import MaterialIcon from './UI/icons/MaterialIcon.svelte'; + import { sidebarStore } from '$lib/stores/SidebarStore.svelte'; + import LanguageSelector from './UI/form/LanguageSelector.svelte'; let { userName } = $props(); @@ -23,6 +25,11 @@ announcementVisible = false; } + // Toggle sidebar when hamburger menu is clicked + function toggleSidebar() { + sidebarStore.toggle(); + } + // Close mobile menu when clicking outside function handleClickOutside(event) { if (!event.target.closest('.mobile-menu') && !event.target.closest('.mobile-menu-btn')) { @@ -60,12 +67,12 @@
-
+
- CropWatch® +

𝘾𝙧𝙤𝙥𝙒𝙖𝙩𝙘𝙝®

@@ -108,6 +115,8 @@
diff --git a/src/lib/components/UI/dashboard/AllDevices.svelte b/src/lib/components/UI/dashboard/AllDevices.svelte index 50d6dcb6..6b04576e 100644 --- a/src/lib/components/UI/dashboard/AllDevices.svelte +++ b/src/lib/components/UI/dashboard/AllDevices.svelte @@ -17,12 +17,6 @@ // Enhanced device type with latest sensor data interface DeviceWithSensorData extends DeviceWithType { latestData: AirData | SoilData | null; - cw_device_type?: { - name: string; - default_upload_interval?: number; - primary_data_notation?: string; - secondary_data_notation?: string; - }; cw_rules?: any[]; } @@ -60,7 +54,7 @@ (device: DeviceWithSensorData) => device.name?.toLowerCase().includes(uiStore.search.toLowerCase()) || device.dev_eui?.toLowerCase().includes(uiStore.search.toLowerCase()) || - device.deviceType?.name?.toLowerCase().includes(uiStore.search.toLowerCase()) + device.cw_device_type?.name?.toLowerCase().includes(uiStore.search.toLowerCase()) ); } @@ -115,12 +109,13 @@ ...device, latestData: device.latestData || {}, cw_device_type: { - name: device.deviceType?.name || 'Unknown', - default_upload_interval: device.deviceType?.default_upload_interval || 10, - primary_data_notation: device.deviceType?.primary_data_notation || '', - secondary_data_notation: device.deviceType?.secondary_data_notation || undefined, - primary_data_v2: device.deviceType?.primary_data_v2 || undefined, - secondary_data_v2: device.deviceType?.secondary_data_v2 || undefined + name: device.cw_device_type?.name || 'Unknown', + default_upload_interval: device.cw_device_type?.default_upload_interval || 10, + primary_data_notation: device.cw_device_type?.primary_data_notation || '', + secondary_data_notation: + device.cw_device_type?.secondary_data_notation || undefined, + primary_data_v2: device.cw_device_type?.primary_data_v2 || undefined, + secondary_data_v2: device.cw_device_type?.secondary_data_v2 || undefined } }} diff --git a/src/lib/components/UI/dashboard/DashboardCard.svelte b/src/lib/components/UI/dashboard/DashboardCard.svelte index bb6a742e..c2a4df90 100644 --- a/src/lib/components/UI/dashboard/DashboardCard.svelte +++ b/src/lib/components/UI/dashboard/DashboardCard.svelte @@ -25,8 +25,8 @@
+
{#if device.latestData}
{#if secondaryDataKey} +
{nameToEmoji(secondaryDataKey)} + import type { SupabaseClient } from '@supabase/supabase-js'; + import { createEventDispatcher } from 'svelte'; + export let size = 10; + export let url: string; + export let supabase: SupabaseClient; + let avatarUrl: string | null = null; + let uploading = false; + let files: FileList; + const dispatch = createEventDispatcher(); + const downloadImage = async (path: string) => { + try { + const { data, error } = await supabase.storage.from('avatars').download(path); + if (error) { + throw error; + } + const url = URL.createObjectURL(data); + avatarUrl = url; + } catch (error) { + if (error instanceof Error) { + console.log('Error downloading image: ', error.message); + } + } + }; + const uploadAvatar = async () => { + try { + uploading = true; + if (!files || files.length === 0) { + throw new Error('You must select an image to upload.'); + } + const file = files[0]; + const fileExt = file.name.split('.').pop(); + const filePath = `${Math.random()}.${fileExt}`; + const { error } = await supabase.storage.from('avatars').upload(filePath, file); + if (error) { + throw error; + } + url = filePath; + setTimeout(() => { + dispatch('upload'); + }, 100); + } catch (error) { + if (error instanceof Error) { + alert(error.message); + } + } finally { + uploading = false; + } + }; + $: if (url) downloadImage(url); + + +
+ {#if avatarUrl} + {avatarUrl + {:else} +
+ {/if} + +
+ + +
+
diff --git a/src/lib/components/UI/form/LanguageSelector.svelte b/src/lib/components/UI/form/LanguageSelector.svelte new file mode 100644 index 00000000..4ec19c1b --- /dev/null +++ b/src/lib/components/UI/form/LanguageSelector.svelte @@ -0,0 +1,29 @@ + + + diff --git a/src/lib/components/UI/form/TextInput.svelte b/src/lib/components/UI/form/TextInput.svelte index 42f7dfe6..bb70e006 100644 --- a/src/lib/components/UI/form/TextInput.svelte +++ b/src/lib/components/UI/form/TextInput.svelte @@ -25,6 +25,6 @@ bind:value {placeholder} {disabled} - class="rounded border border-gray-300 bg-white p-2 text-gray-900 text-inherit dark:border-zinc-700 dark:bg-zinc-800 dark:text-white {className}" + class="min-h-9 rounded border border-gray-300 bg-white px-2 py-0 text-gray-900 text-inherit dark:border-zinc-700 dark:bg-zinc-800 dark:text-white {className}" {...rest} /> diff --git a/src/lib/components/dashboard/CameraStream.svelte b/src/lib/components/dashboard/CameraStream.svelte new file mode 100644 index 00000000..2b9ccd52 --- /dev/null +++ b/src/lib/components/dashboard/CameraStream.svelte @@ -0,0 +1,28 @@ + + +{#if device?.ip_log.length} +
+

{$_('Camera Stream')}

+ {#if device?.ip_log.length > 0} + {#each device.ip_log as log} + Camera Stream + {/each} + {:else} +

{$_('No stream available')}

+ {/if} +
+{/if} diff --git a/src/lib/components/dashboard/DateRangeSelector.svelte b/src/lib/components/dashboard/DateRangeSelector.svelte new file mode 100644 index 00000000..d18b1cdd --- /dev/null +++ b/src/lib/components/dashboard/DateRangeSelector.svelte @@ -0,0 +1,85 @@ + + +
+ +
+ +
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+ +
+
+ {#if error} +

{'error'}

+ {/if} +
+
diff --git a/src/lib/components/dashboard/DeviceMap.svelte b/src/lib/components/dashboard/DeviceMap.svelte new file mode 100644 index 00000000..98f5d8a0 --- /dev/null +++ b/src/lib/components/dashboard/DeviceMap.svelte @@ -0,0 +1,26 @@ + + +{#if device} +
+

{$_('Map')}

+
+ +
+
+{/if} diff --git a/src/lib/components/dashboard/LocationSidebar.svelte b/src/lib/components/dashboard/LocationSidebar.svelte index 758a722f..8448af96 100644 --- a/src/lib/components/dashboard/LocationSidebar.svelte +++ b/src/lib/components/dashboard/LocationSidebar.svelte @@ -202,7 +202,6 @@ >
- - {#if !collapsed}

{$_('Locations')}

@@ -232,10 +230,8 @@ {/if}
-
{#if collapsed} - {:else} -
@@ -278,7 +273,6 @@ {/if}
- + @@ -82,8 +86,11 @@ onclick={() => { startDownload(); modalOpen = false; - }}>{$_('download_csv')} + + {$_('download_csv')} +
diff --git a/src/lib/i18n/locales/en.clean.ts b/src/lib/i18n/locales/en.clean.ts deleted file mode 100644 index ad366679..00000000 --- a/src/lib/i18n/locales/en.clean.ts +++ /dev/null @@ -1,332 +0,0 @@ -export const strings = { - soil_moisture: 'Volumetric Water Content', - moisture: 'Volumetric Water Content', - Moisture: 'Volumetric Water Content', - soil_humidity: 'Volumetric Water Content', - humidity: 'Humidity', - Humidity: 'Humidity', - dew_point: 'Dew Point', - dew_pointC: 'Dew Point', - dewPointC: 'Dew Point', - Temperature: 'Temperature', - temperature: 'Temperature', - temperatureC: 'Temperature', - soil_temperatureC: 'Temperature', - temperature_c: 'Temperature', - soil_temperature: 'Temperature', - 'Soil Temperature': 'Temperature', - 'Temperature and Moisture': 'Soil Temperature/Volumetric Water Content', - 'Temperature and Humidity': 'Temperature/Humidity', - 'temperature and humidity': 'Temperature/Humidity', - temperature_and_humidity: 'Temperature/Humidity', - 'Temperature & Humidity': 'Temperature & Humidity', - 'temperature & humidity': 'Temperature & Humidity', - soil_EC: 'Soil Electrical Conductivity', - soil_ec: 'Soil Electrical Conductivity', - soil_N: 'Soil Nutrients', - soil_P: 'Soil Nutrients', - soil_K: 'Soil Nutrients', - soil_n: 'Soil Nutrients', - soil_p: 'Soil Nutrients', - soil_k: 'Soil Nutrients', - soil_PH: 'Soil pH', - soil_ph: 'Soil pH', - co2_level: 'CO2 Concentration', - vpd: 'Vapor Pressure Deficit', - rainfall: 'Rainfall', - deapth_cm: 'Depth (mm)', - depth_cm: 'Depth (mm)', - pressure: 'Atmospheric Pressure', - created_at: 'Last Updated', - wind_speed: 'Wind Speed', - lux: 'Illuminance', - uv: 'Ultraviolet', - uv_index: 'UV Index', - wind_direction: 'Wind Direction', - water_level: 'Water Level', - battery_level: 'Battery Level', - battery: 'Battery Level', - Battery: 'Battery Level', - sos: 'Emergency Signal', - fire: 'Fire', - people_count: 'People', - car_count: 'Vehicles', - bicycle_count: 'Bicycles', - truck_count: 'Trucks', - bus_count: 'Buses', - 'Data Date Range': 'Data Date Range', - data_date_range: 'Data Date Range', - - // CSVDownloadButton translations - select_date_range: 'Select Date Range', - select_start_end_dates: 'Select start and end dates', - start_date: 'Start Date', - end_date: 'End Date', - Close: 'Close', - close: 'Close', - Open: 'Open', - open: 'Open', - Search: 'Search', - search: 'Search', - 'Sort By': 'Sort By', - 'sort by': 'Sort By', - 'Load Selected Data': 'Load Selected Data', - load_selected_data: 'Load Selected Data', - 'Load Selected Range': 'Load Selected Range', - load_selected_range: 'Load Selected Range', - 'Download CSV': 'Download CSV', - download_csv: 'Download CSV', - 'Download Excel': 'Download Excel', - download_excel: 'Download Excel', - 'Download PDF': 'Download PDF', - download_pdf: 'Download PDF', - 'CSV Download': 'CSV Download', - 'Report Download': 'Report Download', - Report: 'Report', - report: 'Report', - Reports: 'Reports', - Settings: 'Settings', - settings: 'Settings', - Back: 'Back', - back: 'Back', - 'Back to Device': 'Back to Device', - 'Last Seen': 'Last Seen', - 'Last seen': 'Last Seen', - 'last seen': 'Last Seen', - Counts: 'Counts', - counts: 'Counts', - ago: 'ago', - Dashboard: 'Dashboard', - dashboard: 'Dashboard', - Login: 'Login', - login: 'Login', - Logout: 'Logout', - logout: 'Logout', - Register: 'Register', - register: 'Register', - 'Registering...': 'Registering...', - 'Create your account': 'Create your account', - Or: 'Or', - 'sign in to your existing account': 'sign in to your existing account', - 'First Name': 'First Name', - 'Last Name': 'Last Name', - John: 'John', - Doe: 'Doe', - 'Email address': 'Email address', - 'you@example.com': 'you@example.com', - Password: 'Password', - 'Confirm Password': 'Confirm Password', - Company: 'Company', - 'Acme Inc.': 'Acme Inc.', - 'I agree to the': 'I agree to the', - 'Terms of Service': 'Terms of Service', - 'Privacy Policy': 'Privacy Policy', - 'Cookie Policy': 'Cookie Policy', - 'First name is required': 'First name is required', - 'Last name is required': 'Last name is required', - 'Email is required': 'Email is required', - 'Please enter a valid email address': 'Please enter a valid email address', - 'Password is required': 'Password is required', - 'Password must be at least 8 characters': 'Password must be at least 8 characters', - 'Passwords do not match': 'Passwords do not match', - 'Company name is required': 'Company name is required', - 'You must agree to all terms and policies': 'You must agree to all terms and policies', - 'An unexpected error occurred. Please try again.': - 'An unexpected error occurred. Please try again.', - 'Registration failed. Please try again.': 'Registration failed. Please try again.', - - // Header - 'IoT Dashboard': 'IoT Dashboard', - Welcome: 'Welcome', - Pricing: 'Pricing', - Devices: 'Devices', - Resources: 'Resources', - About: 'About', - Demo: 'Demo', - 'Get Started': 'Get Started', - - // Stats labels - Min: 'Min', - Avg: 'Avg', - Max: 'Max', - Count: 'Count', - Median: 'Median', - 'Std Dev': 'Std Dev', - Range: 'Range', - 'Click to expand': 'Click to expand', - 'Click to collapse': 'Click to collapse', - - // Login page - 'Go to Dashboard': 'Go to Dashboard', - 'Go to API': 'Go to API', - 'Create an account': 'Create an account', - 'Login with Discord': 'Login with Discord', - 'N/A': 'N/A', - 'Enter your email': 'Enter your email', - 'Enter your password': 'Enter your password', - 'Logging in...': 'Logging in...', - 'You are already logged in': 'You are already logged in', - 'Login successful! Redirecting to dashboard...': 'Login successful! Redirecting to dashboard...', - Locations: 'Locations', - Details: 'Details', - 'View Details': 'View Details', - 'No locations found.': 'No locations found.', - 'Latest Sensor Readings': 'Latest Sensor Readings', - 'No recent data available': 'No recent data available', - 'Camera Stream': 'Camera Stream', - 'No stream available': 'No stream available', - Map: 'Map', - 'Device Info': 'Device Info', - 'Type:': 'Type:', - 'EUI:': 'EUI:', - 'Location ID:': 'Location ID:', - 'Installed:': 'Installed:', - 'Sensor Datasheet': 'Sensor Datasheet', - 'Last updated:': 'Last updated:', - 'Stats Summary': 'Stats Summary', - 'Data Chart': 'Data Chart', - 'All Sensor Data Over Time': 'All Sensor Data Over Time', - 'Weather & Data': 'Weather & Data', - 'Loading historical data...': 'Loading historical data...', - 'No historical data available for the selected date range.': - 'No historical data available for the selected date range.', - 'Device Settings': 'Device Settings', - General: 'General', - 'Manage the device information.': 'Manage the device information.', - Update: 'Update', - 'Dangerous Zone': 'Dangerous Zone', - 'Delete Device & Associated Data': 'Delete Device & Associated Data', - Delete: 'Delete', - Cancel: 'Cancel', - delete_device_warning: - 'DELETING THIS DEVICE WILL BE PERMANENT AND IRREVERSIBLE. ALL DATA ASSOCIATED WITH THIS DEVICE WILL BE LOST. WE WILL NOT BE ABLE TO RECOVER THIS DATA. DO NOT DO THIS UNLESS YOU KNOW WHAT YOU ARE DOING!', - 'Loading...': 'Loading...', - 'Unknown Location': 'Unknown Location', - 'Settings updated successfully!': 'Settings updated successfully!', - 'Failed to update settings': 'Failed to update settings', - 'Failed to delete device': 'Failed to delete device', - Permissions: 'Permissions', - manage_permissions_description: 'Manage who has access and their permission levels.', - Notifications: 'Notifications', - current_users: 'Current Users', - no_additional_users: 'No additional users.', - owner: 'Owner', - updating: 'Updating...', - remove_user: 'Remove User', - confirm_remove_user: 'Are you sure you want to remove this user?', - cancel: 'Cancel', - remove: 'Remove', - user_removed_success: 'User removed successfully', - user_remove_error: 'An error occurred while removing the user', - permission_update_success: 'Permission updated successfully', - permission_update_error: 'Failed to update permission', - 'Location Settings': 'Location Settings', - 'Location Details': 'Location Details', - 'Add Device': 'Add Device', - 'Location ID': 'Location ID', - Created: 'Created', - Coordinates: 'Coordinates', - 'Not set': 'Not set', - 'Update Location': 'Update Location', - 'Updating...': 'Updating...', - 'Add New User': 'Add New User', - 'Email Address': 'Email Address', - 'Permission Level': 'Permission Level', - 'Admin:': 'Admin:', - 'User:': 'User:', - 'Viewer:': 'Viewer:', - 'Disabled:': 'Disabled:', - 'Apply same permission to all devices in this location': - 'Apply same permission to all devices in this location', - 'If unchecked, user will be added with "Disabled" permission to all devices.': - 'If unchecked, user will be added with "Disabled" permission to all devices.', - 'Adding...': 'Adding...', - 'Add User': 'Add User', - 'Remove Location': 'Remove Location', - 'Are you sure you want to remove this location? This action cannot be undone.': - 'Are you sure you want to remove this location? This action cannot be undone.', - 'Location details updated successfully': 'Location details updated successfully', - 'Failed to update location details': 'Failed to update location details', - 'User added successfully': 'User added successfully', - 'Failed to add user': 'Failed to add user', - 'Location removed successfully': 'Location removed successfully', - 'Failed to remove location': 'Failed to remove location', - 'An error occurred': 'An error occurred', - 'An error occurred while removing the location': 'An error occurred while removing the location', - 'No name set': 'No name set', - 'Full control over location and devices': 'Full control over location and devices', - 'Can use and configure devices': 'Can use and configure devices', - 'Can only view data': 'Can only view data', - 'No access': 'No access', - 'No devices found for this location.': 'No devices found for this location.', - '« Back to Location Overview': '« Back to Location Overview', - 'Create a New Device': 'Create a New Device', - 'Create a new device to be used in the location {location}.': - 'Create a new device to be used in the location {location}.', - 'Device Name': 'Device Name', - 'Dev EUI': 'Dev EUI', - Latitude: 'Latitude', - Longitude: 'Longitude', - 'Device Type': 'Device Type', - 'Select a device type...': 'Select a device type...', - 'User Permissions': 'User Permissions', - 'Set All {label}': 'Set All {label}', - 'Set All Admin': 'Set All Admin', - 'Set All User': 'Set All User', - 'Set All Viewer': 'Set All Viewer', - 'Set All Disabled': 'Set All Disabled', - User: 'User', - 'Creating...': 'Creating...', - 'Create Device': 'Create Device', - 'Get notified when the device meets a specific condition.': - 'Get notified when the device meets a specific condition.', - 'Add Rule': 'Add Rule', - 'No rules found.': 'No rules found.', - Name: 'Name', - Edit: 'Edit', - Actions: 'Actions', - 'No recipients': 'No recipients', - 'No criteria defined': 'No criteria defined', - 'Delete Rule': 'Delete Rule', - 'Are you sure you want to delete this rule? This action cannot be undone.': - 'Are you sure you want to delete this rule? This action cannot be undone.', - 'Deleting...': 'Deleting...', - 'Edit Rule': 'Edit Rule', - 'Create New Rule': 'Create New Rule', - 'Rule deleted successfully': 'Rule deleted successfully', - 'Failed to delete rule': 'Failed to delete rule', - 'Create Rule': 'Create Rule', - 'Update Rule': 'Update Rule', - 'Rule created successfully': 'Rule created successfully', - 'Rule updated successfully': 'Rule updated successfully', - 'Failed to create rule': 'Failed to create rule', - 'Failed to update rule': 'Failed to update rule', - Add: 'Add', - Conditions: 'Conditions', - 'Define one or more conditions that will trigger this rule.': - 'Define one or more conditions that will trigger this rule.', - 'Remove condition': 'Remove condition', - Subject: 'Subject', - Operation: 'Operation', - 'Trigger Value': 'Trigger Value', - 'Reset Value': 'Reset Value', - 'Add Another Condition': 'Add Another Condition', - 'Enter email address': 'Enter email address', - Email: 'Email', - SMS: 'SMS', - 'Push Notification': 'Push Notification', - Method: 'Method', - Recipients: 'Recipients', - - // Error pages - 'Device Error': 'Device Error', - 'There was an error loading the device. Please try again or contact our support team for assistance.': - 'There was an error loading the device. Please try again or contact our support team for assistance.', - 'Back to Devices': 'Back to Devices', - 'Contact Support': 'Contact Support', - 'Go to Device Settings': 'Go to Device Settings', - 'Start Date:': 'Start Date:', - 'End Date:': 'End Date:', - 'Expand sidebar to search': 'Expand sidebar to search', - 'Collapse sidebar': 'Collapse sidebar' -}; diff --git a/src/lib/i18n/locales/en.ts b/src/lib/i18n/locales/en.ts index e68870bb..fe0cb635 100644 --- a/src/lib/i18n/locales/en.ts +++ b/src/lib/i18n/locales/en.ts @@ -73,8 +73,8 @@ export const strings = { load_selected_data: 'Load Selected Data', 'Load Selected Range': 'Load Selected Range', load_selected_range: 'Load Selected Range', - 'Download CSV': 'Download CSV', - download_csv: 'Download CSV', + 'Download CSV': 'CSV', + download_csv: 'CSV', 'Download Excel': 'Download Excel', download_excel: 'Download Excel', 'Download PDF': 'Download PDF', @@ -175,12 +175,6 @@ export const strings = { 'Camera Stream': 'Camera Stream', 'No stream available': 'No stream available', Map: 'Map', - 'Device Info': 'Device Info', - 'Type:': 'Type:', - 'EUI:': 'EUI:', - 'Location ID:': 'Location ID:', - 'Installed:': 'Installed:', - 'Sensor Datasheet': 'Sensor Datasheet', 'Last updated:': 'Last updated:', 'Stats Summary': 'Stats Summary', 'Data Chart': 'Data Chart', @@ -316,6 +310,13 @@ export const strings = { Method: 'Method', Recipients: 'Recipients', + // Settings page + Unknown: 'Unknown', + EUI: 'EUI', + 'Installed Date': 'Installed Date', + 'Device Location': 'Device Location', + 'Units & Display Settings': 'Units & Display Settings', + // Error pages 'Device Error': 'Device Error', 'There was an error loading the device. Please try again or contact our support team for assistance.': @@ -326,5 +327,72 @@ export const strings = { 'Start Date:': 'Start Date:', 'End Date:': 'End Date:', 'Expand sidebar to search': 'Expand sidebar to search', - 'Collapse sidebar': 'Collapse sidebar' + 'Collapse sidebar': 'Collapse sidebar', + + // Account Settings + 'General Account Settings': 'General Account Settings', + 'Settings that affect your entire account, including your profile and preferences.': + 'Settings that affect your entire account, including your profile and preferences.', + 'Profile Information': 'Profile Information', + 'Profile Picture': 'Profile Picture', + 'Click to upload a new profile picture': 'Click to upload a new profile picture', + 'Your email address cannot be changed': 'Your email address cannot be changed', + 'Enter your full name': 'Enter your full name', + 'Enter your username': 'Enter your username', + 'https://example.com': 'https://example.com', + 'Account Actions': 'Account Actions', + 'Need help with your account?': 'Need help with your account?', + 'Contact support': 'Contact support', + + // Display Settings + 'Choose your preferred units of measurement and display options for your account.': + 'Choose your preferred units of measurement and display options for your account.', + 'Unit Preferences': 'Unit Preferences', + 'Display Preferences': 'Display Preferences', + Timezone: 'Timezone', + UTC: 'UTC', + 'Eastern Time (EST)': 'Eastern Time (EST)', + 'Central Time (CST)': 'Central Time (CST)', + 'Mountain Time (MT)': 'Mountain Time (MT)', + 'Pacific Time (PST)': 'Pacific Time (PST)', + 'Greenwich Mean Time (GMT)': 'Greenwich Mean Time (GMT)', + 'Central European Summer Time (CEST)': 'Central European Summer Time (CEST)', + 'Central European Time (CET)': 'Central European Time (CET)', + 'Indian Standard Time (IST)': 'Indian Standard Time (IST)', + 'Japan Standard Time (JST)': 'Japan Standard Time (JST)', + 'Australian Eastern Standard Time (AEST)': 'Australian Eastern Standard Time (AEST)', + 'New Zealand Standard Time (NZST)': 'New Zealand Standard Time (NZST)', + 'Temperature Unit': 'Temperature Unit', + Celsius: 'Celsius', + Fahrenheit: 'Fahrenheit', + Kelvin: 'Kelvin', + 'Distance Unit': 'Distance Unit', + Meters: 'Meters', + Millimeters: 'Millimeters', + Centimetre: 'Centimetre', + Feet: 'Feet', + Inches: 'Inches', + Yards: 'Yards', + 'EC Unit': 'EC Unit', + 'Date Format': 'Date Format', + 'DD-MM-YYYY': 'DD-MM-YYYY', + 'MM-DD-YYYY': 'MM-DD-YYYY', + 'YYYY-MM-DD': 'YYYY-MM-DD', + 'DD/MM/YYYY': 'DD/MM/YYYY', + 'MM/DD/YYYY': 'MM/DD/YYYY', + 'YYYY/MM/DD': 'YYYY/MM/DD', + 'DD.MM.YYYY': 'DD.MM.YYYY', + 'MM.DD.YYYY': 'MM.DD.YYYY', + 'YYYY.MM.DD': 'YYYY.MM.DD', + 'ISO 8601 (YYYY-MM-DDTHH:mm:ssZ)': 'ISO 8601 (YYYY-MM-DDTHH:mm:ssZ)', + 'Time Format': 'Time Format', + '12-Hour': '12-Hour', + '24-Hour': '24-Hour', + 'ISO 8601 (HH:mm:ss)': 'ISO 8601 (HH:mm:ss)', + 'Number Format': 'Number Format', + 'No Decimal Places': 'No Decimal Places', + 'One Decimal Place': 'One Decimal Place', + 'Two Decimal Places': 'Two Decimal Places', + 'Three Decimal Places': 'Three Decimal Places', + 'Save Settings': 'Save Settings' }; diff --git a/src/lib/i18n/locales/ja.ts b/src/lib/i18n/locales/ja.ts index 71dd3f97..9d3ea485 100644 --- a/src/lib/i18n/locales/ja.ts +++ b/src/lib/i18n/locales/ja.ts @@ -69,8 +69,8 @@ export const strings = { load_selected_data: '選択したデータを読み込む', 'Load Selected Range': '選択したデータを読み込む', load_selected_range: '選択したデータを読み込む', - 'Download CSV': 'CSVをダウンロード', - download_csv: 'CSVをダウンロード', + 'Download CSV': 'CSV', + download_csv: 'CSV', 'Download Excel': 'Excelをダウンロード', download_excel: 'Excelをダウンロード', 'Download PDF': 'PDFをダウンロード', @@ -215,12 +215,6 @@ export const strings = { 'Camera Stream': 'カメラストリーム', 'No stream available': 'ストリームは利用できません', Map: '地図', - 'Device Info': 'デバイス情報', - 'Type:': 'タイプ:', - 'EUI:': 'EUI:', - 'Location ID': 'ロケーションID', - 'Installed:': '設置日:', - 'Sensor Datasheet': 'センサーデータシート', 'Last updated:': '最終更新:', 'Stats Summary': '統計概要', 'Data Chart': 'データチャート', @@ -367,6 +361,13 @@ export const strings = { 'Expand sidebar to search': 'サイドバーを展開して検索', 'Collapse sidebar': 'サイドバーを折りたたむ', + // Settings page + Unknown: '不明', + EUI: 'EUI', + 'Installed Date': '設置日', + 'Device Location': 'デバイスの場所', + 'Units & Display Settings': '単位と表示設定', + // Error page 'Device Error': 'デバイスエラー', 'There was an error loading the device. Please try again or contact our support team for assistance.': @@ -394,5 +395,72 @@ export const strings = { 'request a new verification email': '新しい確認メールをリクエスト', 'Return to Login': 'ログインに戻る', loginErrorMessage: 'ログインエラー:メールアドレスまたはパスワードが正しくありません', - 'Invalid email or password': 'メールアドレスまたはパスワードが正しくありません' + 'Invalid email or password': 'メールアドレスまたはパスワードが正しくありません', + + // Account Settings + 'General Account Settings': '一般アカウント設定', + 'Settings that affect your entire account, including your profile and preferences.': + 'プロフィールや設定を含む、アカウント全体に影響する設定。', + 'Profile Information': 'プロフィール情報', + 'Profile Picture': 'プロフィール画像', + 'Click to upload a new profile picture': '新しいプロフィール画像をアップロードするにはクリック', + 'Your email address cannot be changed': 'メールアドレスは変更できません', + 'Enter your full name': 'フルネームを入力', + 'Enter your username': 'ユーザー名を入力', + 'https://example.com': 'https://example.com', + 'Account Actions': 'アカウントアクション', + 'Need help with your account?': 'アカウントについてお困りですか?', + 'Contact support': 'サポートに連絡', + + // Display Settings + 'Choose your preferred units of measurement and display options for your account.': + 'アカウントの測定単位と表示オプションを選択してください。', + 'Unit Preferences': '単位設定', + 'Display Preferences': '表示設定', + Timezone: 'タイムゾーン', + UTC: 'UTC', + 'Eastern Time (EST)': '東部時間 (EST)', + 'Central Time (CST)': '中部時間 (CST)', + 'Mountain Time (MT)': '山岳時間 (MT)', + 'Pacific Time (PST)': '太平洋時間 (PST)', + 'Greenwich Mean Time (GMT)': 'グリニッジ標準時 (GMT)', + 'Central European Summer Time (CEST)': '中央ヨーロッパ夏時間 (CEST)', + 'Central European Time (CET)': '中央ヨーロッパ時間 (CET)', + 'Indian Standard Time (IST)': 'インド標準時 (IST)', + 'Japan Standard Time (JST)': '日本標準時 (JST)', + 'Australian Eastern Standard Time (AEST)': 'オーストラリア東部標準時 (AEST)', + 'New Zealand Standard Time (NZST)': 'ニュージーランド標準時 (NZST)', + 'Temperature Unit': '温度単位', + Celsius: '摂氏', + Fahrenheit: '華氏', + Kelvin: 'ケルビン', + 'Distance Unit': '距離単位', + Meters: 'メートル', + Millimeters: 'ミリメートル', + Centimetre: 'センチメートル', + Feet: 'フィート', + Inches: 'インチ', + Yards: 'ヤード', + 'EC Unit': 'EC単位', + 'Date Format': '日付形式', + 'DD-MM-YYYY': 'DD-MM-YYYY', + 'MM-DD-YYYY': 'MM-DD-YYYY', + 'YYYY-MM-DD': 'YYYY-MM-DD', + 'DD/MM/YYYY': 'DD/MM/YYYY', + 'MM/DD/YYYY': 'MM/DD/YYYY', + 'YYYY/MM/DD': 'YYYY/MM/DD', + 'DD.MM.YYYY': 'DD.MM.YYYY', + 'MM.DD.YYYY': 'MM.DD.YYYY', + 'YYYY.MM.DD': 'YYYY.MM.DD', + 'ISO 8601 (YYYY-MM-DDTHH:mm:ssZ)': 'ISO 8601 (YYYY-MM-DDTHH:mm:ssZ)', + 'Time Format': '時間形式', + '12-Hour': '12時間制', + '24-Hour': '24時間制', + 'ISO 8601 (HH:mm:ss)': 'ISO 8601 (HH:mm:ss)', + 'Number Format': '数値形式', + 'No Decimal Places': '小数点なし', + 'One Decimal Place': '小数点第1位', + 'Two Decimal Places': '小数点第2位', + 'Three Decimal Places': '小数点第3位', + 'Save Settings': '設定を保存' }; diff --git a/src/lib/services/SubscriptionService.ts b/src/lib/services/SubscriptionService.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/lib/stores/LocationsStore.svelte.ts b/src/lib/stores/LocationsStore.svelte.ts index cd35972a..b24179a2 100644 --- a/src/lib/stores/LocationsStore.svelte.ts +++ b/src/lib/stores/LocationsStore.svelte.ts @@ -6,12 +6,6 @@ import type { SoilData } from '$lib/models/SoilData'; // Define DeviceWithSensorData type for use in the store interface DeviceWithSensorData extends DeviceWithType { latestData: AirData | SoilData | null; - cw_device_type?: { - name: string; - default_upload_interval?: number; - primary_data_notation?: string; - secondary_data_notation?: string; - }; cw_rules?: any[]; } @@ -175,6 +169,51 @@ function updateLocationDevices(locationId: number, updatedDevices: DeviceWithSen } } +function updateSingleDevice(devEui: string, updatedData: AirData | SoilData) { + if (!devEui || !updatedData) return; + + // Filter out null values and unwanted properties + const newData = Object.fromEntries( + Object.entries(updatedData).filter( + ([k, v]) => v != null && k !== 'is_simulated' && k !== 'dev_eui' + ) + ) as AirData | SoilData; + + console.log('Updating device:', devEui, 'with data:', newData); + // Update device in the devices array (current view) + if (devices && devices.length > 0) { + const deviceIndex = devices.findIndex((dev) => dev.dev_eui === devEui); + if (deviceIndex >= 0) { + // Create a new device object to ensure reactivity + devices[deviceIndex] = { + ...devices[deviceIndex], + latestData: newData + }; + } + } + + // Also update the device in the locations array to keep data consistent + if (locations && locations.length > 0) { + for (const location of locations) { + if (location.cw_devices && location.cw_devices.length > 0) { + const deviceIndex = location.cw_devices.findIndex((dev) => dev.dev_eui === devEui); + if (deviceIndex >= 0) { + // Create a new device object to ensure reactivity + location.cw_devices[deviceIndex] = { + ...location.cw_devices[deviceIndex], + latestData: newData + }; + // Trigger reactivity by reassigning the array + location.cw_devices = [...location.cw_devices]; + break; + } + } + } + // Trigger reactivity by reassigning the locations array + locations = [...locations]; + } +} + // Export the store functions and state export function getLocationsStore() { return { @@ -210,6 +249,7 @@ export function getLocationsStore() { loadDevicesForLocation, loadAllDevices, refreshDevicesForLocation, - updateLocationDevices + updateLocationDevices, + updateSingleDevice }; } diff --git a/src/lib/stores/SidebarStore.svelte.ts b/src/lib/stores/SidebarStore.svelte.ts new file mode 100644 index 00000000..92844f12 --- /dev/null +++ b/src/lib/stores/SidebarStore.svelte.ts @@ -0,0 +1,115 @@ +import { browser } from '$app/environment'; + +/** + * Global sidebar state management + */ +export class SidebarStore { + // Sidebar open/closed state + isOpen = $state(browser ? this.getInitialState() : false); + + // Small icon mode (collapsed but still visible) + isSmallIconMode = $state(browser ? this.getInitialSmallIconMode() : false); + + private getInitialSmallIconMode(): boolean { + // Check if user has a saved preference + const savedMode = localStorage.getItem('sidebar_small_icon_mode'); + if (savedMode !== null) { + return savedMode === 'true'; + } + + // Default to small icon mode on desktop if sidebar is not open + const isMobile = window.innerWidth < 1024; + const savedOpen = localStorage.getItem('sidebar_open'); + + if (!isMobile && (savedOpen === null || savedOpen === 'false')) { + return true; // Default to small icon mode on desktop + } + + return false; + } + + private getInitialState(): boolean { + // Check if we're on mobile vs desktop (align with CSS breakpoints) + const isMobile = window.innerWidth < 1024; // lg breakpoint to match CSS + + // For mobile: closed by default + // For desktop: small icon mode by default (unless user has preference) + const defaultState = !isMobile; + + // Check if user has a saved preference + const savedState = localStorage.getItem('sidebar_open'); + if (savedState !== null) { + return savedState === 'true'; + } + + return defaultState; + } + + toggle() { + this.isOpen = !this.isOpen; + if (browser) { + localStorage.setItem('sidebar_open', this.isOpen.toString()); + } + } + + open() { + this.isOpen = true; + if (browser) { + localStorage.setItem('sidebar_open', 'true'); + } + } + + close() { + this.isOpen = false; + if (browser) { + localStorage.setItem('sidebar_open', 'false'); + } + } + + toggleSmallIconMode() { + this.isSmallIconMode = !this.isSmallIconMode; + if (browser) { + localStorage.setItem('sidebar_small_icon_mode', this.isSmallIconMode.toString()); + } + } + + // Reactive getter for checking if sidebar should be visible + get shouldShowSidebar() { + return this.isOpen || this.isSmallIconMode; + } + + // Initialize responsive behavior + initializeResponsive() { + if (!browser) return () => {}; + + const handleResize = () => { + const isMobile = window.innerWidth < 1024; // Align with CSS breakpoint + + // Auto-close on mobile, enable small icon mode on desktop if no user preference + if (isMobile && this.isOpen && !localStorage.getItem('sidebar_open')) { + this.close(); + } else if ( + !isMobile && + !this.isOpen && + !this.isSmallIconMode && + !localStorage.getItem('sidebar_open') + ) { + // On desktop, default to small icon mode + this.isSmallIconMode = true; + } + }; + + window.addEventListener('resize', handleResize); + + // Initial check + handleResize(); + + // Return cleanup function + return () => { + window.removeEventListener('resize', handleResize); + }; + } +} + +// Global instance +export const sidebarStore = new SidebarStore(); diff --git a/src/lib/utilities/deviceTimerManager.ts b/src/lib/utilities/deviceTimerManager.ts index 8ed0a585..d9c464be 100644 --- a/src/lib/utilities/deviceTimerManager.ts +++ b/src/lib/utilities/deviceTimerManager.ts @@ -7,12 +7,6 @@ import type { SoilData } from '$lib/models/SoilData'; // Define DeviceWithSensorData type interface DeviceWithSensorData extends DeviceWithType { latestData: AirData | SoilData | null; - cw_device_type?: { - name: string; - default_upload_interval?: number; - primary_data_notation?: string; - secondary_data_notation?: string; - }; cw_rules?: any[]; } @@ -61,7 +55,10 @@ export class DeviceTimerManager { // Use provided uploadInterval or fallback to device settings const effectiveInterval = - uploadInterval || device.upload_interval || device.deviceType?.default_upload_interval || 10; + uploadInterval || + device.upload_interval || + device.cw_device_type?.default_upload_interval || + 10; // Create a closure to capture the current timestamp const currentTimestamp = device.latestData.created_at; diff --git a/src/lib/utilities/deviceTimerSetup.ts b/src/lib/utilities/deviceTimerSetup.ts index c5f63a62..86e5d60b 100644 --- a/src/lib/utilities/deviceTimerSetup.ts +++ b/src/lib/utilities/deviceTimerSetup.ts @@ -5,14 +5,8 @@ import type { SoilData } from '$lib/models/SoilData'; // Define the DeviceWithSensorData type export interface DeviceWithSensorData extends DeviceWithType { - latestData: AirData | SoilData | null; - cw_device_type?: { - name: string; - default_upload_interval?: number; - primary_data_notation?: string; - secondary_data_notation?: string; - }; - cw_rules?: any[]; + latestData: AirData | SoilData | null; + cw_rules?: any[]; } /** @@ -22,24 +16,24 @@ export interface DeviceWithSensorData extends DeviceWithType { * @param deviceActiveStatus The record of device active status */ export function setupDeviceActiveTimer( - device: DeviceWithSensorData, - timerManager: DeviceTimerManager, - deviceActiveStatus: Record + device: DeviceWithSensorData, + timerManager: DeviceTimerManager, + deviceActiveStatus: Record ) { - if (!device.latestData?.created_at) return; - const deviceId = device.dev_eui as string; + if (!device.latestData?.created_at) return; + const deviceId = device.dev_eui as string; - // Get the upload interval from the device - const uploadInterval = - device.upload_interval || device.cw_device_type?.default_upload_interval || 10; + // Get the upload interval from the device + const uploadInterval = + device.upload_interval || device.cw_device_type?.default_upload_interval || 10; - // Use the timer manager to set up a timer for this device - timerManager.setupDeviceActiveTimer( - device, - uploadInterval, - (deviceId: string, isActive: boolean | null) => { - // Update the device active status in our component state - deviceActiveStatus[deviceId] = isActive === null ? false : isActive; - } - ); + // Use the timer manager to set up a timer for this device + timerManager.setupDeviceActiveTimer( + device, + uploadInterval, + (deviceId: string, isActive: boolean | null) => { + // Update the device active status in our component state + deviceActiveStatus[deviceId] = isActive === null ? false : isActive; + } + ); } diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 00000000..db47e68e --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,62 @@ + + + + {$_('Page Not Found')} | CropWatch + + +
+
+
+
+ + + +
+ +

404

+

{$_('Page Not Found')}

+ +

+ {$_( + 'The page you are looking for does not exist. It might have been moved, deleted, or you entered the wrong URL.' + )} +

+ +
+ +
+ +
+ +
+
+
+
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 3a2b3494..ee0bef47 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,11 +3,14 @@ import { page } from '$app/state'; import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'; import Header from '$lib/components/Header.svelte'; + import GlobalSidebar from '$lib/components/GlobalSidebar.svelte'; import ToastContainer from '$lib/components/Toast/ToastContainer.svelte'; import { i18n } from '$lib/i18n/index.svelte'; + import { sidebarStore } from '$lib/stores/SidebarStore.svelte'; import { createBrowserClient } from '@supabase/ssr'; import { onMount } from 'svelte'; import '../app.css'; + import { warning } from '$lib/stores/toast.svelte'; // No preloading needed - dashboard will load its data when navigated to @@ -25,6 +28,17 @@ let session = $derived(data.session); let user = $derived(data.user); + // Check if we should show the sidebar (not on auth pages) + let showSidebar = $derived(!page.url.pathname.startsWith('/auth')); + + // Dynamic margin based on sidebar state + let mainMargin = $derived(() => { + if (!showSidebar) return ''; + if (sidebarStore.isOpen) return 'lg:ml-64'; + if (sidebarStore.isSmallIconMode) return 'lg:ml-16'; + return 'lg:ml-16'; // default collapsed state on desktop + }); + // Log user updates without creating an infinite loop $effect(() => { if (user) { @@ -43,6 +57,7 @@ session = _session; user = _session.user; } else { + warning('Your session has expired. Please login again.'); console.warn('No session found during auth state change'); session = null; user = null; @@ -66,12 +81,18 @@ {#if !page.url.pathname.startsWith('/auth')}
{/if} -
+ {/if} +
{@render children()} -
+
{/if} @@ -88,4 +109,12 @@ opacity: 1; } } + + /* Mobile: no sidebar margin */ + @media (max-width: 1023px) { + main { + margin-left: 0 !important; + padding-top: 119px !important; + } + } diff --git a/src/routes/api/webhook/stripe/+server.ts b/src/routes/api/webhook/stripe/+server.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/routes/app/account-settings/+layout.svelte b/src/routes/app/account-settings/+layout.svelte new file mode 100644 index 00000000..829f91b0 --- /dev/null +++ b/src/routes/app/account-settings/+layout.svelte @@ -0,0 +1,58 @@ + + +
+ +
+
+

{$_('Device Settings')}

+ +
+
+ {@render children()} +
+
+ + diff --git a/src/routes/app/account-settings/display-settings/+page.svelte b/src/routes/app/account-settings/display-settings/+page.svelte new file mode 100644 index 00000000..9a1374f4 --- /dev/null +++ b/src/routes/app/account-settings/display-settings/+page.svelte @@ -0,0 +1,227 @@ + + +
+ +
+
+

+ 🌍 {$_('Units & Display Settings')} +

+

+ {$_('Choose your preferred units of measurement and display options for your account.')} +

+
+
+
+ + +
+ +
+

+ {$_('Unit Preferences')} +

+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+
+ + +
+

+ {$_('Display Preferences')} +

+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+
+
+
diff --git a/src/routes/app/account-settings/general/+page.server.ts b/src/routes/app/account-settings/general/+page.server.ts new file mode 100644 index 00000000..af0d8f36 --- /dev/null +++ b/src/routes/app/account-settings/general/+page.server.ts @@ -0,0 +1,53 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => { + const { session } = await safeGetSession(); + if (!session) { + redirect(303, '/'); + } + const { data: profile } = await supabase + .from('profiles') + .select(`username, full_name, website, avatar_url`) + .eq('id', session.user.id) + .single(); + return { session, profile }; +}; +export const actions: Actions = { + update: async ({ request, locals: { supabase, safeGetSession } }) => { + const formData = await request.formData(); + const fullName = formData.get('fullName') as string; + const username = formData.get('username') as string; + const website = formData.get('website') as string; + const avatarUrl = formData.get('avatarUrl') as string; + const { session } = await safeGetSession(); + const { error } = await supabase.from('profiles').upsert({ + id: session?.user.id, + full_name: fullName, + username, + website, + avatar_url: avatarUrl, + updated_at: new Date() + }); + if (error) { + return fail(500, { + fullName, + username, + website, + avatarUrl + }); + } + return { + fullName, + username, + website, + avatarUrl + }; + }, + signout: async ({ locals: { supabase, safeGetSession } }) => { + const { session } = await safeGetSession(); + if (session) { + await supabase.auth.signOut(); + redirect(303, '/'); + } + } +}; diff --git a/src/routes/app/account-settings/general/+page.svelte b/src/routes/app/account-settings/general/+page.svelte new file mode 100644 index 00000000..6287a8c2 --- /dev/null +++ b/src/routes/app/account-settings/general/+page.svelte @@ -0,0 +1,206 @@ + + + + {$_('General Account Settings')} + + +
+ +
+
+

+ ⚙️ {$_('General Account Settings')} +

+

+ {$_('Settings that affect your entire account, including your profile and preferences.')} +

+
+
+
+ + +
+ +
+
+

+ {$_('Profile Information')} +

+ +
+ +
+ { + profileForm.requestSubmit(); + }} + /> +
+

+ {$_('Profile Picture')} +

+

+ {$_('Click to upload a new profile picture')} +

+
+
+ + +
+ + +

+ {$_('Your email address cannot be changed')} +

+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+
+
+ + +
+
+

+ {$_('Account Actions')} +

+ +
+ +
+ +
+ + +
+ {$_('Need help with your account?')} + + {$_('Contact support')} + +
+
+
+
+
+
diff --git a/src/routes/app/account-settings/payment/+page.server.ts b/src/routes/app/account-settings/payment/+page.server.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/routes/app/account-settings/payment/+page.svelte b/src/routes/app/account-settings/payment/+page.svelte new file mode 100644 index 00000000..b57594c7 --- /dev/null +++ b/src/routes/app/account-settings/payment/+page.svelte @@ -0,0 +1,322 @@ + + +
+ +
+
+

💳 Account Payment Settings

+

+ Add or update your payment method securely with Stripe. +

+
+
+ + + ➡️ Add Subscription + + + +
+ +
+

+ 📋 Active Subscriptions +

+

+ Manage your active subscriptions and assign devices. +

+ + {#if subscriptions.length === 0} +
+ + + +

No subscriptions

+

+ You don't have any active subscriptions. +

+
+ {:else} +
+ {#each subscriptions as subscription (subscription.id)} +
+
+
+

+ {subscription.name} +

+
+ + ${subscription.amount}/{subscription.interval} + + + {subscription.status} + + {#if subscription.pausedUntil} + + Paused until {subscription.pausedUntil} + + {/if} +
+

+ Next billing: {subscription.current_period_end} +

+
+
+ +
+ +
+ +
+ +
+ + + +
+
+
+ {/each} +
+ {/if} +
+
+
diff --git a/src/routes/app/account-settings/payment/PauseSubscription.svelte b/src/routes/app/account-settings/payment/PauseSubscription.svelte new file mode 100644 index 00000000..a0dae1e8 --- /dev/null +++ b/src/routes/app/account-settings/payment/PauseSubscription.svelte @@ -0,0 +1,84 @@ + + + +
+

+ ⏸️ Pause All Subscriptions +

+

+ Temporarily pause all your active subscriptions. You won't be charged during the pause period. +

+ +
+
+ + +
+ + +
+
diff --git a/src/routes/app/account-settings/payment/add-subscription/+page.server.ts b/src/routes/app/account-settings/payment/add-subscription/+page.server.ts new file mode 100644 index 00000000..200c7d72 --- /dev/null +++ b/src/routes/app/account-settings/payment/add-subscription/+page.server.ts @@ -0,0 +1,187 @@ +import { redirect } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; +import type { Actions, PageServerLoad } from './$types'; +import Stripe from 'stripe'; +import { PRIVATE_STRIPE_SECRET_KEY } from '$env/static/private'; +import { PUBLIC_DOMAIN } from '$env/static/public'; + +// This is your test secret API key. +const stripe = new Stripe(PRIVATE_STRIPE_SECRET_KEY, { + apiVersion: '2025-06-30.basil', // Use the latest API version or a specific one + typescript: true +}); + +const YOUR_DOMAIN = PUBLIC_DOMAIN; + +export const load: PageServerLoad = async () => { + try { + // Fetch all active subscription products with their prices + const products = await stripe.products.list({ + active: true, + expand: ['data.default_price'] + }); + + // Fetch all prices for subscription products + const prices = await stripe.prices.list({ + active: true, + type: 'recurring', + expand: ['data.product'] + }); + + // Group prices by product + const subscriptionProducts = products.data + .filter((product) => product.type === 'service' || product.type === 'good') + .map((product) => { + const productPrices = prices.data.filter( + (price) => typeof price.product === 'object' && price.product.id === product.id + ); + + return { + id: product.id, + name: product.name, + description: product.description, + images: product.images, + metadata: product.metadata, + prices: productPrices.map((price) => ({ + id: price.id, + unit_amount: price.unit_amount, + currency: price.currency, + recurring: price.recurring, + lookup_key: price.lookup_key, + nickname: price.nickname + })) + }; + }) + .filter((product) => product.prices.length > 0); // Only include products with prices + + return { + subscriptionProducts + }; + } catch (error) { + console.error('Error loading subscription products:', error); + return { + subscriptionProducts: [], + error: 'Failed to load subscription products' + }; + } +}; + +export const actions: Actions = { + 'create-checkout-session': async ({ request, url, locals }) => { + const formData = await request.formData(); + const price_id = formData.get('price_id') as string; + + try { + // Get the current user + const { session: userSession, user } = await locals.safeGetSession(); + + if (!userSession || !user) { + return { + error: 'You must be logged in to create a subscription' + }; + } + + // Create or retrieve customer + let customerId; + try { + const customers = await stripe.customers.list({ + email: user.email, + limit: 1 + }); + + if (customers.data.length > 0) { + customerId = customers.data[0].id; + } else { + const customer = await stripe.customers.create({ + email: user.email, + name: + user.user_metadata?.full_name || + `${user.user_metadata?.first_name || ''} ${user.user_metadata?.last_name || ''}`.trim(), + metadata: { + user_id: user.id + } + }); + customerId = customer.id; + } + } catch (customerError) { + console.error('Error handling customer:', customerError); + return { + error: 'Failed to create or retrieve customer' + }; + } + + const session = await stripe.checkout.sessions.create({ + customer: customerId, + billing_address_collection: 'auto', + line_items: [ + { + price: price_id, + quantity: 1 + } + ], + mode: 'subscription', + success_url: `${YOUR_DOMAIN}/app/account-settings/payment/success?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${YOUR_DOMAIN}/app/account-settings/payment/cancel`, + metadata: { + user_id: user.id + } + }); + + if (session.url) { + return { + success: true, + redirectUrl: session.url + }; + } else { + return { + error: 'No session URL returned from Stripe' + }; + } + } catch (error) { + console.error('Error creating checkout session:', error); + return { + error: 'Failed to create checkout session' + }; + } + }, + + 'create-portal-session': async ({ request }) => { + const formData = await request.formData(); + const session_id = formData.get('session_id') as string; + + try { + // For demonstration purposes, we're using the Checkout session to retrieve the customer ID. + // Typically this is stored alongside the authenticated user in your database. + const checkoutSession = await stripe.checkout.sessions.retrieve(session_id); + + if (!checkoutSession.customer) { + return { + error: 'No customer found in checkout session' + }; + } + + // This is the url to which the customer will be redirected when they're done + // managing their billing with the portal. + const returnUrl = `${YOUR_DOMAIN}/app/account-settings/payment`; + + const portalSession = await stripe.billingPortal.sessions.create({ + customer: checkoutSession.customer as string, + return_url: returnUrl + }); + + return { + success: true, + redirectUrl: portalSession.url + }; + } catch (error) { + console.error('Error creating portal session:', error); + return { + error: 'Failed to create portal session' + }; + } + } +}; + +// Note: Webhook handling should be moved to a separate API route +// Create /src/routes/api/webhook/+server.ts for webhook handling +// This is because webhooks need raw body access and special headers handling diff --git a/src/routes/app/account-settings/payment/add-subscription/+page.svelte b/src/routes/app/account-settings/payment/add-subscription/+page.svelte new file mode 100644 index 00000000..c149fd23 --- /dev/null +++ b/src/routes/app/account-settings/payment/add-subscription/+page.svelte @@ -0,0 +1,139 @@ + + + + Add Subscription - CropWatch + + +
+

Add Subscription

+ + {#if form?.error} +
+ {form.error} +
+ {/if} + + {#if data.error} +
+ {data.error} +
+ {/if} + +
+ {#if data.subscriptionProducts.length === 0} +
+

No subscription products available at this time.

+
+ {:else} +
+ {#each data.subscriptionProducts as product} +
+ {#if product.images.length > 0} + {product.name} + {/if} + +
+ + + + + + + + +
+

{product.name}

+ {#if product.description} +

{product.description}

+ {/if} +
+
+ + +
+ {#each product.prices as price} +
+
+ + {formatPrice(price.unit_amount, price.currency)} + {formatBillingPeriod(price.recurring)} + + {#if price.nickname} + {price.nickname} + {/if} +
+ +
{ + return async ({ result }) => { + if ( + result.type === 'success' && + result.data && + 'redirectUrl' in result.data && + result.data.redirectUrl + ) { + // Redirect to Stripe Checkout + window.location.href = result.data.redirectUrl as string; + } + }; + }} + > + + +
+
+ {/each} +
+
+ {/each} +
+ {/if} +
+
diff --git a/src/routes/app/account-settings/payment/cancel/+page.svelte b/src/routes/app/account-settings/payment/cancel/+page.svelte new file mode 100644 index 00000000..8144a032 --- /dev/null +++ b/src/routes/app/account-settings/payment/cancel/+page.svelte @@ -0,0 +1,28 @@ + + Payment Cancelled - CropWatch + + +
+
+
+

Payment Cancelled

+

Your subscription setup was cancelled. No charges were made.

+
+ + +
+
diff --git a/src/routes/app/account-settings/payment/success/+page.svelte b/src/routes/app/account-settings/payment/success/+page.svelte new file mode 100644 index 00000000..16cad8ad --- /dev/null +++ b/src/routes/app/account-settings/payment/success/+page.svelte @@ -0,0 +1,48 @@ + + + + Payment Success - CropWatch + + +
+
+
+

Payment Successful!

+

Your subscription has been created successfully.

+
+ + {#if sessionId} +
+

Manage Your Subscription

+
+ + +
+
+ {/if} + + + Back to Payment Settings + +
+
diff --git a/src/routes/app/all-devices/+page.server.ts b/src/routes/app/all-devices/+page.server.ts new file mode 100644 index 00000000..fee8ac43 --- /dev/null +++ b/src/routes/app/all-devices/+page.server.ts @@ -0,0 +1,21 @@ +import { redirect, fail } from '@sveltejs/kit'; +import { SessionService } from '$lib/services/SessionService'; +import { ErrorHandlingService } from '$lib/errors/ErrorHandlingService'; +import { DeviceRepository } from '$lib/repositories/DeviceRepository'; +import { DeviceService } from '$lib/services/DeviceService'; +import type { PageServerLoad } from '../../$types'; + +export const load: PageServerLoad = async ({ locals: { supabase } }) => { + const sessionService = new SessionService(supabase); + const { session, user } = await sessionService.getSafeSession(); + if (!session || !user) { + throw redirect(302, '/auth/login'); + } + + const errorHandler = new ErrorHandlingService(); + const deviceRepository = new DeviceRepository(supabase, errorHandler); + const deviceService = new DeviceService(deviceRepository); + const allDevicesPromise = deviceService.getAllDevices(); + + return { allDevicesPromise }; +}; diff --git a/src/routes/app/all-devices/+page.svelte b/src/routes/app/all-devices/+page.svelte new file mode 100644 index 00000000..9d92cb90 --- /dev/null +++ b/src/routes/app/all-devices/+page.svelte @@ -0,0 +1,353 @@ + + +
+ +
+
+

All Devices

+

+ Manage and view all your monitoring locations +

+
+
+
+ + +
+
+
+ + {#await allDevicesPromise} +
+

Loading devices...

+
+ {:then devices} + {@const filteredDevices = filterDevices(devices)} +
+ {#if filteredDevices.length > 0} + {#each filteredDevices as device, index (device.device_id || device.dev_eui || device.name || index)} +
+
+
+ +
+

+ {device.name || device.dev_eui || 'Unnamed Device'} +

+ +
+
+
+ {#if device.device_id || device.dev_eui} +
+ + ID: {device.device_id || device.dev_eui || 'N/A'} +
+ {/if} + {#if device.location_id} +
+ + Location ID: {device.location_id} +
+ {/if} + {#if device.cw_device_type?.name} +
+ + Type: {device.cw_device_type.name} +
+ {/if} +
+
+
+ {/each} + {:else} +
+ +

+ {searchTerm ? 'No devices match your search.' : 'No devices found.'} +

+
+ {/if} +
+ {:catch error} +
+

Error loading devices: {error.message}

+
+ {/await} +
+ + diff --git a/src/routes/app/all-gateways/+page.svelte b/src/routes/app/all-gateways/+page.svelte new file mode 100644 index 00000000..3a5727c2 --- /dev/null +++ b/src/routes/app/all-gateways/+page.svelte @@ -0,0 +1,24 @@ + + +
+ +
+
+

All Gateways

+

Watch the status of all your gateways

+
+
+
+ +
+
+
+ +
diff --git a/src/routes/app/all-notifications/+page.svelte b/src/routes/app/all-notifications/+page.svelte new file mode 100644 index 00000000..6d356d5f --- /dev/null +++ b/src/routes/app/all-notifications/+page.svelte @@ -0,0 +1,33 @@ + + +
+ +
+
+

All Notifications

+

+ Search and manage all your notifications across all devices +

+
+
+
+ +
+
+
+ +
diff --git a/src/routes/app/all-reports/+page.svelte b/src/routes/app/all-reports/+page.svelte index 0f59a376..70b61979 100644 --- a/src/routes/app/all-reports/+page.svelte +++ b/src/routes/app/all-reports/+page.svelte @@ -1,55 +1,481 @@ -
-

Reports

- Create New Report - - {#if reports.length === 0} -

No reports found.

- {:else} -
    - {#each reports as report (report.id)} -
  • - {report.name} -
    - View - Edit - - Delete - - - - Delete {report.name}? - This action cannot be undone. -
    - Cancel - deleteReport(report.id)} class="px-3 py-1 bg-red-600 text-white rounded">Delete -
    -
    -
    -
    -
    -
  • - {/each} -
- {/if} +
+ +
+
+

All Reports

+

+ Manage and view all your configured reports +

+
+
+
+
+ + +
+
+
+
+ +
+ {#if filterReports(reports).length > 0} + {@const filteredReports = filterReports(reports)} + {#each filteredReports as report (report.id)} +
+
+
+ +
+

+ {report.name} +

+
+ + + + + + + + + + Delete {report.name}? + This action cannot be undone. +
+ Cancel + deleteReport(report.id)} + class="rounded bg-red-600 px-3 py-1 text-white">Delete +
+
+
+
+
+
+
+
+
+ + Created: {formatDate(report.created_at)} +
+ {#if report.dev_eui} +
+ + Device: {report.dev_eui} +
+ {/if} + {#if report.recipients} +
+ + Recipients: {report.recipients} +
+ {/if} +
+
+
+ {/each} + {:else} +
+ +

+ {searchTerm ? 'No reports match your search.' : 'No reports found.'} +

+
+ {/if} +
+ + diff --git a/src/routes/app/dashboard/+page.svelte b/src/routes/app/dashboard/+page.svelte index 0c9d2b1a..671a5dbf 100644 --- a/src/routes/app/dashboard/+page.svelte +++ b/src/routes/app/dashboard/+page.svelte @@ -31,6 +31,7 @@ onToggleCollapse: () => void; } import AllDevices from '$lib/components/UI/dashboard/AllDevices.svelte'; + import type { RealtimeChannel } from '@supabase/supabase-js'; // Enhanced location type with deviceCount property interface LocationWithCount extends LocationWithDevices { @@ -40,18 +41,14 @@ // Enhanced device type with latest sensor data interface DeviceWithSensorData extends DeviceWithType { latestData: AirData | SoilData | null; - cw_device_type?: { - name: string; - default_upload_interval?: number; - primary_data_notation?: string; - secondary_data_notation?: string; - }; cw_rules?: any[]; } // Create a timer manager instance const timerManager = new DeviceTimerManager(); + let channel: RealtimeChannel | undefined = $state(); + // Initialize stores and managers // Use writable store for device active status - initialize as null (unknown) for all devices const deviceActiveStatus = $state>({}); @@ -79,11 +76,85 @@ // Sidebar collapsed state let sidebarCollapsed = $state(false); - // Toggle sidebar collapsed state - function toggleSidebar() { - sidebarCollapsed = !sidebarCollapsed; - if (browser) { - localStorage.setItem('sidebar_collapsed', sidebarCollapsed.toString()); + // Real-time channel for database updates + let realtimeChannel: any = null; + + // Setup real-time subscriptions with retry logic + function setupRealtimeSubscription(retryCount = 0) { + if (!browser) return; + + console.log('🔄 Setting up real-time subscription...'); + channel = data.supabase + .channel('db-changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'cw_air_data' + }, + (payload) => { + // Handle real-time updates for messages + if (payload.eventType === 'INSERT') { + handleRealtimeUpdate(payload); + } + } + ) + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'cw_soil_data' + }, + (payload) => { + // Handle real-time updates for users + if (payload.eventType === 'INSERT') { + // You can handle user updates here if needed + handleRealtimeUpdate(payload); + } + } + ) + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'cw_traffic2' + }, + (payload) => { + // Handle real-time updates for users + if (payload.eventType === 'UPDATE' || payload.eventType === 'INSERT') { + // You can handle user updates here if needed + handleRealtimeUpdate(payload); + } + } + ) + .subscribe(); + } + + // Handle real-time update + function handleRealtimeUpdate(payload: any) { + // Only process if we have valid data + if (payload.new && payload.new.dev_eui) { + try { + locationsStore.updateSingleDevice(payload.new.dev_eui, payload.new as AirData | SoilData); + + // Update device active timer for the updated device + const device = locationsStore.devices.find((d) => d.dev_eui === payload.new.dev_eui); + if (device && device.latestData?.created_at) { + setupDeviceActiveTimer(device, timerManager, deviceActiveStatus); + } + } catch (error) { + console.error('Error updating device from real-time:', error); + } + } + } + + function cleanupRealtimeSubscription() { + if (realtimeChannel) { + data.supabase.removeAllChannels(); + realtimeChannel = null; } } @@ -97,6 +168,13 @@ } } }); + onDestroy(() => { + console.log('the component is being destroyed'); + data.supabase.removeAllChannels(); + if (channel) { + data.supabase.realtime.removeChannel(channel); + } + }); // Persist UI store values to localStorage when they change $effect(() => { @@ -114,12 +192,60 @@ onDestroy(() => { //console.log('Cleaning up dashboard resources'); cleanupTimers(); + cleanupRealtimeSubscription(); }); + // // Refresh session if needed + // async function refreshSessionIfNeeded() { + // try { + // const { data: sessionData, error: sessionError } = await data.supabase.auth.getSession(); + + // if (sessionError) { + // console.error('❌ Error getting session:', sessionError); + // return false; + // } + + // if (!sessionData.session) { + // console.error('❌ No session found'); + // return false; + // } + + // const expiresAt = sessionData.session.expires_at; + // if (expiresAt) { + // const expirationDate = new Date(expiresAt * 1000); + // const now = new Date(); + // const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000); + + // // If expired or expiring soon, refresh + // if (expirationDate < fiveMinutesFromNow) { + // console.log('🔄 Session expiring soon, refreshing...'); + // const { data: refreshData, error: refreshError } = + // await data.supabase.auth.refreshSession(); + + // if (refreshError) { + // console.error('❌ Failed to refresh session:', refreshError); + // return false; + // } + + // console.log('✅ Session refreshed successfully'); + // return true; + // } + // } + + // return true; + // } catch (error) { + // console.error('❌ Error refreshing session:', error); + // return false; + // } + // } + // Initialize dashboard on mount // This is the main onMount function for the dashboard onMount(async () => { try { + // Setup real-time subscription + setupRealtimeSubscription(); + // Fetch locations using the store - this also selects the first location await locationsStore.fetchLocations(user.id); @@ -181,12 +307,13 @@ // Update the refresh timestamp in the timer manager timerManager.updateRefreshTimestamp(locationId); - //console.log('Devices refreshed:', { - // deviceCount: locationsStore.devices.length, - // activeCount: locationsStore.devices.filter( - // (d: DeviceWithSensorData) => deviceActiveStatus[d.dev_eui as string] - // ).length - // }); + console.log('Devices refreshed:', { + locationId, + deviceCount: locationsStore.devices.length, + activeCount: locationsStore.devices.filter( + (d: DeviceWithSensorData) => deviceActiveStatus[d.dev_eui as string] + ).length + }); } return true; @@ -247,9 +374,8 @@ {:else if locationsStore.locationError}
{locationsStore.locationError}
{:else} -
- - --> + -
- - {#if locationsStore.loadingLocations} -
Loading locations and devices...
- {:else if locationsStore.locationError} -
{locationsStore.locationError}
- {:else if locationsStore.locations.length > 0} - {#if locationsStore.selectedLocationId !== null} - - {@const selectedLoc = locationsStore.locations.find( - (loc) => loc.location_id === locationsStore.selectedLocationId - )} - {#if selectedLoc} - - {:else} -
Selected location not found.
- {/if} + /> --> + +
+ + {#if locationsStore.loadingLocations} +
Loading locations and devices...
+ {:else if locationsStore.locationError} +
{locationsStore.locationError}
+ {:else if locationsStore.locations.length > 0} + {#if locationsStore.selectedLocationId !== null} + + {@const selectedLoc = locationsStore.locations.find( + (loc) => loc.location_id === locationsStore.selectedLocationId + )} + {#if selectedLoc} + {:else} - - +
Selected location not found.
{/if} {:else} -

No locations found.

+ + {/if} -
+ {:else} +

No locations found.

+ {/if}
+ {/if}
\ No newline at end of file + /* Custom table styling */ + .table-container { + scrollbar-width: thin; + scrollbar-color: rgb(156 163 175) transparent; + } + + .table-container::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + .table-container::-webkit-scrollbar-track { + background: transparent; + } + + .table-container::-webkit-scrollbar-thumb { + background-color: rgb(156 163 175); + border-radius: 3px; + } + + .table-container::-webkit-scrollbar-thumb:hover { + background-color: rgb(107 114 128); + } + diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/+page.svelte b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/+page.svelte index d0c6685c..8805b528 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/+page.svelte +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/+page.svelte @@ -1,27 +1,30 @@
{device.name}
-
{@render controls?.()}
{#if children} {@render children?.()} diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts index cb4219c1..4cec730e 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts @@ -34,8 +34,33 @@ export function setupDeviceDetail() { let ApexCharts = $state(undefined); // Chart instances - let mainChartInstance: any = null; - let brushChartInstance: any = null; + const mainChartInstances: Record = {}; + const brushChartInstances: Record = {}; + + /** + * Gets all numeric keys from the historical data, excluding specified ignored keys. + * @param historicalData Historical data array. + * @param ignoredDataKeys Array of keys to ignore. + * @returns Array of numeric keys found in the data. + */ + function getNumericKeys( + historicalData: any[], + ignoredDataKeys: string[] = ['id', 'dev_eui', 'created_at'] + ): string[] { + if (!historicalData || !historicalData.length) { + return []; + } + + const sample = historicalData.find((row) => row && typeof row === 'object'); + + if (!sample) { + return []; + } + + return Object.keys(sample).filter( + (key) => !ignoredDataKeys.includes(key) && typeof sample[key] === 'number' + ); + } // Function to process historical data and calculate stats function processHistoricalData(historicalData: any[]) { @@ -51,10 +76,7 @@ export function setupDeviceDetail() { .reverse(); // Determine numeric keys dynamically (excluding dev_eui, created_at) - const excludeKeys = ['dev_eui', 'created_at']; - const numericKeys = Object.keys(historicalData[0] || {}).filter( - (key) => !excludeKeys.includes(key) && typeof historicalData[0][key] === 'number' - ); + const numericKeys = getNumericKeys(historicalData); // Reset chartData and stats for all numeric keys numericKeys.forEach((key) => { @@ -192,42 +214,47 @@ export function setupDeviceDetail() { } /** - * Renders a generic ApexCharts line chart with a brush (range selector) below. - * This function is fully generic: it will plot all numeric keys in the data (except for ignored keys). - * - * @param historicalData Array of objects (rows) with at least a 'created_at' timestamp and numeric fields - * @param dataType (Unused, for compatibility) - * @param latestData (Unused, for compatibility) - * @param chart1Element HTMLElement to render the main chart into - * @param chart1BrushElement HTMLElement to render the brush chart into - * @param _ignored (Unused, for compatibility) - * @param ignoredDataKeys Array of keys to ignore (default: ['id', 'dev_eui', 'created_at']) + * Renders a generic ApexCharts line chart with a brush (range selector) below. This function is + * fully generic: it will plot all numeric keys in the data (except for ignored keys). + * @param params + * @param params.historicalData Array of objects (rows) with at least a 'created_at' timestamp and + * numeric fields + * @param params.chart1Element HTMLElement to render the main chart into + * @param params.chart1BrushElement HTMLElement to render the brush chart into + * @param params.key Optional key to render a specific chart (if not provided, all numeric keys + * will be rendered) + * @param params.ignoredDataKeys Array of keys to ignore (default: ['id', 'dev_eui', + * 'created_at']) */ - async function renderVisualization( - historicalData: any[], - dataType: string, // ignored for charting - latestData: any, // ignored for charting - chart1Element: HTMLElement | undefined, - chart1BrushElement: HTMLElement | undefined, - _ignored?: any, // placeholder for removed dataGridElement - ignoredDataKeys: string[] = ['id', 'dev_eui', 'created_at'] - ) { + async function renderVisualization({ + historicalData, + chart1Element, + chart1BrushElement, + key = '_all', + ignoredDataKeys = ['id', 'dev_eui', 'created_at'] + }: { + historicalData: any[]; + chart1Element?: HTMLElement; + chart1BrushElement?: HTMLElement; + key?: string; + ignoredDataKeys?: string[]; + }) { if (!browser || !historicalData || historicalData.length === 0) return; await new Promise((resolve) => setTimeout(resolve, 50)); if (!chart1Element || !chart1BrushElement) return; // Destroy previous chart instances if they exist - if (mainChartInstance) { + if (mainChartInstances[key]) { try { - mainChartInstance.destroy(); + mainChartInstances[key].destroy(); } catch {} - mainChartInstance = null; + mainChartInstances[key] = null; } - if (brushChartInstance) { + if (brushChartInstances[key]) { try { - brushChartInstance.destroy(); + brushChartInstances[key].destroy(); } catch {} - brushChartInstance = null; + brushChartInstances[key] = null; } if (!ApexCharts) { @@ -235,15 +262,22 @@ export function setupDeviceDetail() { } // Find all numeric keys in the data (excluding ignored keys) - const sample = historicalData.find((row) => row && typeof row === 'object'); - if (!sample) return; - const numericKeys = Object.keys(sample).filter( - (key) => !ignoredDataKeys.includes(key) && typeof sample[key] === 'number' - ); + const numericKeys = getNumericKeys(historicalData, ignoredDataKeys); if (numericKeys.length === 0) return; + let keys = [...numericKeys]; + + if (key !== '_all') { + // Filter numeric keys based on the provided key + if (numericKeys.includes(key)) { + keys = [key]; + } else { + return; + } + } + // Build series for each numeric key - const series = numericKeys.map((key) => ({ + const series = keys.map((key) => ({ name: get(_)(key), data: historicalData .filter((row) => typeof row[key] === 'number' && row[key] !== null && row['created_at']) @@ -251,14 +285,17 @@ export function setupDeviceDetail() { })); series.forEach((s) => s.data.sort((a, b) => a.x - b.x)); - const colorMap = Object.fromEntries(numericKeys.map((key) => [key, getTextColorByKey(key)])); + const colorMap = Object.fromEntries(keys.map((key) => [key, getTextColorByKey(key)])); const colors = Object.values(colorMap); // Y-axis config for each series - const yaxis = numericKeys.map((key, idx) => ({ + const yaxis = keys.map((key, idx) => ({ seriesName: key, opposite: idx % 2 === 1, - title: { text: get(_)(key), style: { color: colorMap[key] } }, + title: + key === '_all' + ? { text: get(_)(key), style: { fontSize: '16px', color: colorMap[key] } } + : {}, labels: { style: { colors: colorMap[key] } } })); @@ -273,15 +310,15 @@ export function setupDeviceDetail() { const mainChartOptions = { series, chart: { - id: 'mainChart', + id: `chart-${key}-main`, type: 'line', - height: 350, + height: 200, toolbar: { autoSelected: 'pan', show: false }, animations: { enabled: false }, zoom: { enabled: false } }, colors, - stroke: { curve: 'smooth', width: numericKeys.map(() => 1) }, + stroke: { curve: 'smooth', width: keys.map(() => 1) }, dataLabels: { enabled: false }, markers: { size: 1, strokeWidth: 0, hover: { size: 4 } }, xaxis: { type: 'datetime', labels: { datetimeUTC: false } }, @@ -305,15 +342,15 @@ export function setupDeviceDetail() { const brushChartOptions = { series, chart: { - id: 'brushChart', - height: 150, + id: `chart-${key}-brush`, + height: 100, type: 'area', - brush: { target: 'mainChart', enabled: true }, + brush: { target: `chart-${key}-main`, enabled: true }, selection: { enabled: true, xaxis: { min: minDate, max: maxDate } } }, colors, fill: { type: 'gradient', gradient: { opacityFrom: 0.7, opacityTo: 0.3 } }, - stroke: { width: numericKeys.map(() => 1) }, + stroke: { width: keys.map(() => 1) }, xaxis: { type: 'datetime', tooltip: { enabled: false }, labels: { datetimeUTC: false } }, yaxis: { show: false, tickAmount: 2 }, grid: { borderColor: '#2a2a2a', strokeDashArray: 2, yaxis: { lines: { show: false } } }, @@ -321,23 +358,23 @@ export function setupDeviceDetail() { }; // Render charts - mainChartInstance = new ApexCharts(chart1Element, mainChartOptions); - brushChartInstance = new ApexCharts(chart1BrushElement, brushChartOptions); + mainChartInstances[key] = new ApexCharts(chart1Element, mainChartOptions); + brushChartInstances[key] = new ApexCharts(chart1BrushElement, brushChartOptions); try { - await mainChartInstance.render(); - await brushChartInstance.render(); + await mainChartInstances[key].render(); + await brushChartInstances[key].render(); } catch (err) { - if (mainChartInstance) { + if (mainChartInstances[key]) { try { - mainChartInstance.destroy(); + mainChartInstances[key].destroy(); } catch {} - mainChartInstance = null; + mainChartInstances[key] = null; } - if (brushChartInstance) { + if (brushChartInstances[key]) { try { - brushChartInstance.destroy(); + brushChartInstances[key].destroy(); } catch {} - brushChartInstance = null; + brushChartInstances[key] = null; } } } @@ -387,6 +424,7 @@ export function setupDeviceDetail() { // Functions formatDateForDisplay, hasValue, + getNumericKeys, processHistoricalData, fetchDataForDateRange, renderVisualization, diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/realtime.svelte.ts b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/realtime.svelte.ts new file mode 100644 index 00000000..ec1469d0 --- /dev/null +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/realtime.svelte.ts @@ -0,0 +1,51 @@ +import { browser } from '$app/environment'; +import type { + RealtimeChannel, + RealtimePostgresInsertPayload, + SupabaseClient +} from '@supabase/supabase-js'; + +let channel: RealtimeChannel | null = $state(null); + +export function setupRealtimeSubscription( + supabase: SupabaseClient, + deviceDataTable: string, + devEui: string, + onDataUpdate: (newData: any) => void, + retryCount = 0 +) { + if (!browser) return; + + console.log('🔄 Setting up real-time subscription...'); + channel = supabase + .channel(`${devEui}-changes`) + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: deviceDataTable, + filter: `dev_eui=eq.${devEui}` + }, + (payload) => { + // Handle real-time updates for users + if (payload.eventType === 'UPDATE' || payload.eventType === 'INSERT') { + console.log('📡 Real-time data received:', payload.new); + onDataUpdate(payload.new); + } + } + ) + .subscribe(); + return channel; +} + +export function removeRealtimeSubscription(supabase: SupabaseClient) { + if (channel) { + console.log('🔄 Removing real-time subscription...'); + supabase.removeChannel(channel); + channel.unsubscribe(); + channel = null; + } else { + console.warn('No active real-time channel to remove.'); + } +} diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/reports/+page.svelte b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/reports/+page.svelte index f7c1eebe..0dbb3486 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/reports/+page.svelte +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/reports/+page.svelte @@ -242,7 +242,6 @@ } async function generateServerPDF() { - debugger; serverLoading = true; error = ''; diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/+page.svelte b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/+page.svelte index 81a7b9b5..fbe80a53 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/+page.svelte +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/+page.svelte @@ -8,13 +8,19 @@ import TextInput from '$lib/components/UI/form/TextInput.svelte'; import Dialog from '$lib/components/UI/overlay/Dialog.svelte'; import { error, success } from '$lib/stores/toast.svelte.js'; - import { _, locale } from 'svelte-i18n'; + import { formatDateOnly } from '$lib/utilities/helpers.js'; + import { _ } from 'svelte-i18n'; let { data } = $props(); const device = $derived(data.device); const ownerId = $derived(data.ownerId); const isOwner = $derived(device?.user_id === ownerId); + // @todo Use a proper sensor datasheet link when wiki pages are created + const deviceLinkId = $derived( + device?.cw_device_type?.data_table_v2 === 'cw_soil_data' ? 'soil_sensors' : 'co2_sensors' + ); + let showDeleteDialog = $state(false); let devEui = page.params.devEui; @@ -42,7 +48,7 @@ - Device Settings - CropWatch + {$_('Device Settings')} - CropWatch
@@ -53,7 +59,7 @@
- + + {$_('Device Type')} + + {#if device?.cw_device_type?.name} + + {device.cw_device_type.name} + + {:else} + {$_('Unknown')} + {/if} +
+
+ + {$_('EUI')} + + {device?.dev_eui || $_('Unknown')} +
+
+ + {$_('Installed Date')} + + {device?.installed_at ? formatDateOnly(device.installed_at) : $_('Unknown')} +
+
+ + {$_('Coordinates')} + + {#if device?.lat && device?.long} + + {device.lat}, {device.long} + {:else} + {$_('Unknown')} + {/if} +
+
+ {#if isOwner}
- + {#await data.locations} - Loading... + {$_('Loading...')} {:then locations} {#if isOwner} {:else} {locations.find((loc) => loc.location_id === device?.location_id)?.name || 'Unknown Location'} + {#if device?.location_id} + ({device.location_id}) + {/if} {/if} {/await}
- {#if isOwner} - - {/if} +
+ {#if isOwner} + + {/if} +
diff --git a/static/build-info.json b/static/build-info.json new file mode 100644 index 00000000..c1c0e21e --- /dev/null +++ b/static/build-info.json @@ -0,0 +1,9 @@ +{ + "commit": "53e6cd1", + "branch": "develop", + "author": "Kevin Cantrell", + "date": "2025-07-06T05:33:21.338Z", + "builder": "kevin@kevin-desktop", + "ipAddress": "192.168.1.100", + "timestamp": 1751780001338 +} diff --git a/supabase/.temp/cli-latest b/supabase/.temp/cli-latest index eb410900..5a5e7b9c 100644 --- a/supabase/.temp/cli-latest +++ b/supabase/.temp/cli-latest @@ -1 +1 @@ -v2.24.3 \ No newline at end of file +v2.30.4 \ No newline at end of file diff --git a/supabase/.temp/gotrue-version b/supabase/.temp/gotrue-version new file mode 100644 index 00000000..a091f261 --- /dev/null +++ b/supabase/.temp/gotrue-version @@ -0,0 +1 @@ +v2.176.1 \ No newline at end of file diff --git a/supabase/.temp/pooler-url b/supabase/.temp/pooler-url new file mode 100644 index 00000000..6325e4a4 --- /dev/null +++ b/supabase/.temp/pooler-url @@ -0,0 +1 @@ +postgresql://postgres.uvtmwyhdhofyumwglxzr:[YOUR-PASSWORD]@aws-0-ap-northeast-1.pooler.supabase.com:6543/postgres \ No newline at end of file diff --git a/supabase/.temp/postgres-version b/supabase/.temp/postgres-version new file mode 100644 index 00000000..d1330369 --- /dev/null +++ b/supabase/.temp/postgres-version @@ -0,0 +1 @@ +15.8.1.073 \ No newline at end of file diff --git a/supabase/.temp/project-ref b/supabase/.temp/project-ref new file mode 100644 index 00000000..d96e98b3 --- /dev/null +++ b/supabase/.temp/project-ref @@ -0,0 +1 @@ +uvtmwyhdhofyumwglxzr \ No newline at end of file diff --git a/supabase/.temp/rest-version b/supabase/.temp/rest-version new file mode 100644 index 00000000..2392826e --- /dev/null +++ b/supabase/.temp/rest-version @@ -0,0 +1 @@ +v12.2.3 \ No newline at end of file diff --git a/supabase/.temp/storage-version b/supabase/.temp/storage-version new file mode 100644 index 00000000..04f88252 --- /dev/null +++ b/supabase/.temp/storage-version @@ -0,0 +1 @@ +custom-metadata \ No newline at end of file diff --git a/supabase/migrations/20250105000000_create_subscription_tables.sql b/supabase/migrations/20250105000000_create_subscription_tables.sql new file mode 100644 index 00000000..80df151b --- /dev/null +++ b/supabase/migrations/20250105000000_create_subscription_tables.sql @@ -0,0 +1,132 @@ +-- Create subscriptions table to track user subscriptions +CREATE TABLE IF NOT EXISTS subscriptions ( + id TEXT PRIMARY KEY, + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + customer_id TEXT NOT NULL, + stripe_subscription_id TEXT UNIQUE NOT NULL, + stripe_price_id TEXT NOT NULL, + status TEXT NOT NULL, + current_period_start TIMESTAMP WITH TIME ZONE, + current_period_end TIMESTAMP WITH TIME ZONE, + cancel_at_period_end BOOLEAN DEFAULT FALSE, + canceled_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create subscription_items table for detailed subscription line items +CREATE TABLE IF NOT EXISTS subscription_items ( + id TEXT PRIMARY KEY, + subscription_id TEXT NOT NULL REFERENCES subscriptions(id) ON DELETE CASCADE, + stripe_subscription_item_id TEXT UNIQUE NOT NULL, + stripe_price_id TEXT NOT NULL, + quantity INTEGER DEFAULT 1, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create payments table to track payment history +CREATE TABLE IF NOT EXISTS payments ( + id TEXT PRIMARY KEY, + subscription_id TEXT REFERENCES subscriptions(id) ON DELETE SET NULL, + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + stripe_payment_intent_id TEXT UNIQUE NOT NULL, + stripe_invoice_id TEXT, + amount INTEGER NOT NULL, + currency TEXT NOT NULL DEFAULT 'usd', + status TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create customers table to store Stripe customer data +CREATE TABLE IF NOT EXISTS customers ( + id TEXT PRIMARY KEY, + user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + stripe_customer_id TEXT UNIQUE NOT NULL, + email TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create indexes for better performance +CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id); +CREATE INDEX IF NOT EXISTS idx_subscriptions_customer_id ON subscriptions(customer_id); +CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_subscription_id ON subscriptions(stripe_subscription_id); +CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status); + +CREATE INDEX IF NOT EXISTS idx_subscription_items_subscription_id ON subscription_items(subscription_id); +CREATE INDEX IF NOT EXISTS idx_subscription_items_stripe_subscription_item_id ON subscription_items(stripe_subscription_item_id); + +CREATE INDEX IF NOT EXISTS idx_payments_subscription_id ON payments(subscription_id); +CREATE INDEX IF NOT EXISTS idx_payments_user_id ON payments(user_id); +CREATE INDEX IF NOT EXISTS idx_payments_stripe_payment_intent_id ON payments(stripe_payment_intent_id); + +CREATE INDEX IF NOT EXISTS idx_customers_user_id ON customers(user_id); +CREATE INDEX IF NOT EXISTS idx_customers_stripe_customer_id ON customers(stripe_customer_id); + +-- Enable RLS (Row Level Security) +ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY; +ALTER TABLE subscription_items ENABLE ROW LEVEL SECURITY; +ALTER TABLE payments ENABLE ROW LEVEL SECURITY; +ALTER TABLE customers ENABLE ROW LEVEL SECURITY; + +-- Create RLS policies +-- Users can only see their own subscriptions +CREATE POLICY "Users can view their own subscriptions" ON subscriptions + FOR SELECT USING (auth.uid() = user_id); + +CREATE POLICY "Users can insert their own subscriptions" ON subscriptions + FOR INSERT WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can update their own subscriptions" ON subscriptions + FOR UPDATE USING (auth.uid() = user_id); + +-- Users can only see their own subscription items +CREATE POLICY "Users can view their own subscription items" ON subscription_items + FOR SELECT USING ( + EXISTS ( + SELECT 1 FROM subscriptions + WHERE subscriptions.id = subscription_items.subscription_id + AND subscriptions.user_id = auth.uid() + ) + ); + +-- Users can only see their own payments +CREATE POLICY "Users can view their own payments" ON payments + FOR SELECT USING (auth.uid() = user_id); + +CREATE POLICY "Users can insert their own payments" ON payments + FOR INSERT WITH CHECK (auth.uid() = user_id); + +-- Users can only see their own customer records +CREATE POLICY "Users can view their own customer records" ON customers + FOR SELECT USING (auth.uid() = user_id); + +CREATE POLICY "Users can insert their own customer records" ON customers + FOR INSERT WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can update their own customer records" ON customers + FOR UPDATE USING (auth.uid() = user_id); + +-- Update timestamp function +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Create triggers for updating updated_at columns +CREATE TRIGGER update_subscriptions_updated_at BEFORE UPDATE ON subscriptions + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_subscription_items_updated_at BEFORE UPDATE ON subscription_items + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_payments_updated_at BEFORE UPDATE ON payments + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_customers_updated_at BEFORE UPDATE ON customers + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); diff --git a/test-webhook-data.mjs b/test-webhook-data.mjs new file mode 100644 index 00000000..e69de29b