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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"common_actions": "Actions",
"common_all_short": "ALL",
"common_not_available": "N/A",
"common_show_details": "Show details",
"common_hide_details": "Hide details",
"action_back": "Back",
"action_back_to_dashboard": "Back to Dashboard",
"action_cancel": "Cancel",
Expand Down Expand Up @@ -299,6 +301,7 @@
"rules_step_2_subtitle": "Choose which device this rule monitors",
"rules_no_devices_available": "No devices available. Please add a device first.",
"rules_select_device_placeholder": "Select a device...",
"rules_filter_device_placeholder": "Filter by device or location…",
"rules_device_preselected": "Device was pre-selected from the query parameter.",
"rules_step_3_title": "3. Alert Criteria",
"rules_step_3_subtitle": "Define when this rule should trigger — add one or more conditions",
Expand Down Expand Up @@ -387,6 +390,15 @@
"rules_new_delete_failed": "Unable to delete this rule template.",
"rules_new_invalid_template_id": "Invalid rule template ID.",
"rules_new_rule_template_not_found": "Rule template not found.",
"rules_new_view_history": "View History",
"rules_new_history_subtitle": "Every time \"{name}\" triggered, and when it reset.",
"rules_new_history_triggered": "Triggered",
"rules_new_history_reset": "Reset",
"rules_new_history_value": "Value: {value}",
"rules_new_history_still_active": "Still active",
"rules_new_history_loading": "Loading history…",
"rules_new_history_empty": "This rule template has not triggered yet.",
"rules_new_history_load_failed": "Unable to load rule history.",
"reports_page_title": "Reports - CropWatch",
"reports_weekly_reports": "Weekly Reports",
"reports_for_device": "For Device",
Expand Down
12 changes: 12 additions & 0 deletions messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"common_actions": "操作",
"common_all_short": "ALL",
"common_not_available": "N/A",
"common_show_details": "詳細を表示",
"common_hide_details": "詳細を非表示",
"action_back": "戻る",
"action_back_to_dashboard": "ダッシュボードに戻る",
"action_cancel": "キャンセル",
Expand Down Expand Up @@ -299,6 +301,7 @@
"rules_step_2_subtitle": "このルールを監視するデバイスを選択します",
"rules_no_devices_available": "利用可能なデバイスがありません。先にデバイスを追加してください。",
"rules_select_device_placeholder": "デバイスを選択...",
"rules_filter_device_placeholder": "デバイスまたは場所で絞り込み…",
"rules_device_preselected": "クエリパラメータからデバイスが事前選択されています。",
"rules_step_3_title": "3. アラート条件",
"rules_step_3_subtitle": "数値は半角英数字でご入力ください。",
Expand Down Expand Up @@ -387,6 +390,15 @@
"rules_new_delete_failed": "このルールテンプレートを削除できません。",
"rules_new_invalid_template_id": "無効なルールテンプレート ID です。",
"rules_new_rule_template_not_found": "ルールテンプレートが見つかりません。",
"rules_new_view_history": "履歴を表示",
"rules_new_history_subtitle": "「{name}」がトリガーされた日時と、リセットされた日時の一覧です。",
"rules_new_history_triggered": "トリガー",
"rules_new_history_reset": "リセット",
"rules_new_history_value": "値: {value}",
"rules_new_history_still_active": "継続中",
"rules_new_history_loading": "履歴を読み込んでいます…",
"rules_new_history_empty": "このルールテンプレートはまだトリガーされていません。",
"rules_new_history_load_failed": "ルール履歴を読み込めませんでした。",
"reports_page_title": "レポート - CropWatch",
"reports_weekly_reports": "週次レポート",
"reports_for_device": "対象デバイス",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
},
"packageManager": "pnpm@10.18.2+sha512.9fb969fa749b3ade6035e0f109f0b8a60b5d08a1a87fdf72e337da90dcc93336e2280ca4e44f2358a649b83c17959e9993e777c2080879f3801e6f0d999ad3dd",
"dependencies": {
"@cropwatchdevelopment/cwui": "0.1.92",
"@cropwatchdevelopment/cwui": "0.1.96",
"@supabase/supabase-js": "^2.98.0"
}
}
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions src/lib/api/api.dtos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,18 @@ export interface RuleTemplateDto {
actions: RuleTemplateActionDto[];
}

export interface RuleTriggerLogDto {
id: number;
devEui: string;
deviceName: string | null;
templateId: number;
triggeredAt: string | null;
triggeredValue: number | null;
resetAt: string | null;
resetValue: number | null;
createdAt: string | null;
}

export interface RuleTemplateCriterionInput {
id?: number | null;
subject: string;
Expand Down Expand Up @@ -333,6 +345,13 @@ export interface RuleTemplateListQuery {
search?: string;
}

export interface RuleFormContextDto {
devices: DeviceDto[];
locations: LocationDto[];
actionTypes: RuleActionTypeDto[];
template: RuleTemplateDto | null;
}

export interface ReportUserScheduleDto {
id?: number;
dev_eui: string;
Expand Down
28 changes: 28 additions & 0 deletions src/lib/api/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import type {
ReportsQuery,
RuleActionTypeDto,
RuleDto,
RuleFormContextDto,
RuleTemplateDto,
RuleTemplateListQuery,
RuleTemplateSaveRequest,
RuleTriggerLogDto,
RulesQuery,
SensorTimeSeriesPoint,
TimeRangeQuery,
Expand Down Expand Up @@ -146,6 +148,7 @@ const REPORT_BY_REPORT_ID_ENDPOINT = `${REPORTS_BASE_ENDPOINT}/{report_id}`;
const RULES_BASE_ENDPOINT = publicEnv.PUBLIC_RULES_ENDPOINT ?? '/rules';
const RULE_TEMPLATES_ENDPOINT = publicEnv.PUBLIC_RULE_TEMPLATES_ENDPOINT ?? '/rules-new';
const RULE_TEMPLATE_ACTION_TYPES_ENDPOINT = `${RULE_TEMPLATES_ENDPOINT}/action-types`;
const RULE_TEMPLATE_FORM_CONTEXT_ENDPOINT = `${RULE_TEMPLATES_ENDPOINT}/form-context`;
const TRIGGERED_RULES_BASE_ENDPOINT =
publicEnv.PUBLIC_TRIGGERED_RULES_ENDPOINT ?? `${RULES_BASE_ENDPOINT}/triggered`;
const TRIGGERED_RULES_COUNT_ENDPOINT =
Expand All @@ -154,6 +157,7 @@ const REPORT_HISTORY_ENDPOINT = `${REPORTS_BASE_ENDPOINT}/history/{dev_eui}`;
const REPORT_DOWNLOAD_ENDPOINT = `${REPORTS_BASE_ENDPOINT}/download/{dev_eui}/{report_id}/{reportName}`;
const RULE_BY_ID_ENDPOINT = `${RULES_BASE_ENDPOINT}/{id}`;
const RULE_TEMPLATE_BY_ID_ENDPOINT = `${RULE_TEMPLATES_ENDPOINT}/{id}`;
const RULE_TEMPLATE_HISTORY_ENDPOINT = `${RULE_TEMPLATES_ENDPOINT}/{id}/history`;
const AIR_NOTES_CREATE_ENDPOINT = publicEnv.PUBLIC_AIR_NOTES_ENDPOINT ?? '/air/notes';
const GET_AIR_NOTES_ENDPOINT =
publicEnv.PUBLIC_GET_AIR_NOTES_ENDPOINT ?? '/air/notes/{dev_eui}/month/{month}/year/{year}';
Expand Down Expand Up @@ -1124,13 +1128,37 @@ export class ApiService {
});
}

public getRuleTemplateHistory(
id: number,
options: ApiMethodOptions = {}
): Promise<RuleTriggerLogDto[]> {
return this.request<RuleTriggerLogDto[]>(
replacePathParams(RULE_TEMPLATE_HISTORY_ENDPOINT, { id }),
{
method: 'GET',
signal: options.signal
}
);
}

public getRuleTemplateActionTypes(options: ApiMethodOptions = {}): Promise<RuleActionTypeDto[]> {
return this.request<RuleActionTypeDto[]>(RULE_TEMPLATE_ACTION_TYPES_ENDPOINT, {
method: 'GET',
signal: options.signal
});
}

public getRuleFormContext(
templateId?: number,
options: ApiMethodOptions = {}
): Promise<RuleFormContextDto> {
return this.request<RuleFormContextDto>(RULE_TEMPLATE_FORM_CONTEXT_ENDPOINT, {
method: 'GET',
signal: options.signal,
query: typeof templateId === 'number' ? { templateId } : undefined
});
}

public createRuleTemplate(
payload: RuleTemplateSaveRequest,
options: ApiMethodOptions = {}
Expand Down
2 changes: 2 additions & 0 deletions src/routes/rules-new/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import ADD_ICON from '$lib/images/icons/add.svg';
import EDIT_ICON from '$lib/images/icons/edit.svg';
import DeleteRuleTemplateDialog from './DeleteRuleTemplateDialog.svelte';
import ViewRuleAlertHistory from './ViewRuleAlertHistory.svelte';

type RuleTemplateRow = RuleTemplateDto & {
statusLabel: string;
Expand Down Expand Up @@ -202,6 +203,7 @@
>
<Icon src={EDIT_ICON} alt={m.action_edit()} />
</CwButton>
<ViewRuleAlertHistory templateId={row.id} ruleName={row.name} />
<DeleteRuleTemplateDialog
templateId={row.id}
ruleName={row.name}
Expand Down
65 changes: 50 additions & 15 deletions src/routes/rules-new/RuleTemplateForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
import { AppActionRow, AppFormStack, AppNotice } from '$lib/components/layout';
import type {
Json,
RuleActionTypeDto,
RuleTemplateActionDto,
RuleTemplateActionInput,
RuleTemplateDto,
RuleTemplateSaveRequest
} from '$lib/rules-new/rule-template.types';
import type { DeviceDto } from '$lib/api/api.dtos';
import type { RuleFormContextDto } from '$lib/api/api.dtos';
import { getRuleOperatorOptions, getRuleSubjectOptions } from '$lib/i18n/options';
import {
CwAlertPointsEditor,
Expand Down Expand Up @@ -43,10 +41,8 @@

interface Props {
mode: FormMode;
devices: DeviceDto[];
actionTypes: RuleActionTypeDto[];
context: RuleFormContextDto;
authToken?: string | null;
initialTemplate?: RuleTemplateDto | null;
preselectedDevEui?: string | null;
}

Expand All @@ -66,14 +62,15 @@

let {
mode,
devices,
actionTypes: actions,
context,
authToken = null,
initialTemplate = null,
preselectedDevEui = null
}: Props = $props();

const initial = (() => initialTemplate)();
let devices = $derived(context.devices);
let actions = $derived(context.actionTypes);
let locations = $derived(context.locations);
const initial = (() => context.template)();
const preselectedDevice = (() => preselectedDevEui)();
const toast = useCwToast();
const SUBJECT_OPTIONS = getRuleSubjectOptions();
Expand All @@ -84,6 +81,7 @@
let description = $state(initial?.description ?? '');
let isActive = $state(initial?.isActive ?? true);
let submitting = $state(false);
let showAdvanced = $state(false);

let selectedDevices = $state<DeviceSelection[]>(
initial?.assignments.length
Expand Down Expand Up @@ -111,13 +109,19 @@
value: String(actionType.id)
}))
);
let deviceOptionsBase = $derived(
interface DeviceOption {
label: string;
value: string;
group?: number;
}
let deviceOptionsBase = $derived<DeviceOption[]>(
(devices ?? []).map((device) => ({
label: device.name ? `${device.name} (${device.dev_eui})` : device.dev_eui,
value: device.dev_eui
value: device.dev_eui,
group: typeof device.location_id === 'number' ? device.location_id : undefined
}))
);
let deviceOptions = $derived([
let deviceOptions = $derived<DeviceOption[]>([
...selectedDevices
.filter((device) => !deviceOptionsBase.some((option) => option.value === device.id))
.map((device) => ({
Expand All @@ -126,6 +130,16 @@
})),
...deviceOptionsBase
]);
let deviceGroups = $derived.by(() => {
const seen = new Map<number, string>();
for (const device of devices ?? []) {
if (typeof device.location_id !== 'number' || seen.has(device.location_id)) continue;
seen.set(device.location_id, resolveLocationName(device.location_id));
}
return [...seen.entries()]
.map(([value, label]) => ({ value, label }))
.sort((a, b) => a.label.localeCompare(b.label));
});

let selectedDevEuis = $derived(selectedDevices.map((device) => device.id.trim()).filter(Boolean));
let selectedDeviceTypeId = $derived(resolveSelectedDeviceTypeId());
Expand Down Expand Up @@ -217,6 +231,12 @@
return device?.name ? `${device.name} (${devEui})` : devEui;
}

function resolveLocationName(locationId: number): string {
const match = locations.find((loc) => loc.location_id === locationId);
const name = typeof match?.name === 'string' ? match.name.trim() : '';
return name || m.locations_location_with_id({ id: String(locationId) });
}

function resolveSelectedDeviceTypeId(): number | null {
const types: number[] = [];
for (const devEui of selectedDevEuis) {
Expand Down Expand Up @@ -384,12 +404,15 @@
<p>{m.rules_no_devices_available()}</p>
</AppNotice>
{:else}
<div class="rules-new-form__block">
<div class="rules-new-form__block" id="device-selection">
<CwMultiSelect
showAllSelectedItems={true}
label={m.devices_device()}
placeholder={m.rules_select_device_placeholder()}
options={deviceOptions}
groups={deviceGroups}
dropdownHeight="24rem"
searchPlaceholder={m.rules_filter_device_placeholder()}
bind:value={selectedDevices}
required
/>
Expand Down Expand Up @@ -506,8 +529,20 @@
<dd>{assignmentSummary}</dd>
<dt>{m.rules_conditions()}:</dt>
<dd>{criteriaSummary}</dd>
{#if showAdvanced}

<dt>{m.rules_new_actions()}:</dt>
<dd>{actionSummary}</dd>
<dd>
{actionSummary}
<CwButton variant="secondary" size="sm" onclick={() => showAdvanced = !showAdvanced}>
{showAdvanced ? m.common_hide_details() : m.common_show_details()}
</CwButton>
</dd>
{:else}
<CwButton variant="secondary" size="sm" onclick={() => showAdvanced = !showAdvanced}>
{showAdvanced ? m.common_hide_details() : m.common_show_details()}
</CwButton>
{/if}
</dl>
</AppNotice>
{:else}
Expand Down
Loading
Loading