@agridea/suibi-module 0.1.0-preview.01100f9 → 0.1.0-preview.0808fe0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/elements/browser/main.js +65 -65
  2. package/dist/elements/browser/styles.css +1 -1
  3. package/dist/elements/browser/suibi-module-latest.css +1 -1
  4. package/dist/elements/browser/suibi-module-latest.js +65 -65
  5. package/dist/elements/browser/{suibi-module.v0.1.0-preview.01100f9.css → suibi-module.v0.1.0-preview.0808fe0.css} +1 -1
  6. package/dist/elements/browser/suibi-module.v0.1.0-preview.0808fe0.js +78 -0
  7. package/dist/elements/browser/version.txt +1 -1
  8. package/dist/elements/browser/wc-global-utilities.css +1 -1
  9. package/dist/types/app/components/data-preview/data-preview.component.d.ts +222 -0
  10. package/dist/types/app/components/inline-table/inline-table.component.d.ts +26 -4
  11. package/dist/types/app/components/inline-table/inline-table.types.d.ts +10 -1
  12. package/dist/types/app/core/navigation/navigation.service.d.ts +7 -0
  13. package/dist/types/app/core/planning/planning-balance.service.d.ts +8 -0
  14. package/dist/types/app/core/utils/custom-unit.util.d.ts +38 -0
  15. package/dist/types/app/core/utils/object.util.d.ts +5 -9
  16. package/dist/types/app/core/validation/ajv-custom.d.ts +1 -0
  17. package/dist/types/app/core/validation/capabilities.d.ts +6 -0
  18. package/dist/types/app/core/validation/global-validation.service.d.ts +2 -0
  19. package/dist/types/app/nutrient-cycle/farm-manure-trading/farm-manure-trading-table/farm-manure-trading-table.component.d.ts +10 -9
  20. package/dist/types/app/nutrient-cycle/farm-manure-trading/farm-manure-trading.types.d.ts +4 -0
  21. package/dist/types/app/nutrient-cycle/field-cultivation/arable-land-table/arable-land-table.component.d.ts +3 -2
  22. package/dist/types/app/nutrient-cycle/field-cultivation/arable-land-table/arable-land-table.config.d.ts +2 -0
  23. package/dist/types/app/nutrient-cycle/field-cultivation/field-cultivation-master-detail/field-cultivation-master-detail.component.d.ts +1 -1
  24. package/dist/types/app/nutrient-cycle/field-cultivation/field-cultivation.types.d.ts +5 -1
  25. package/dist/types/app/nutrient-cycle/field-cultivation/field-cultivation.util.d.ts +4 -0
  26. package/dist/types/app/nutrient-cycle/field-cultivation/fodder-cropland-table/fodder-cropland-table.component.d.ts +3 -2
  27. package/dist/types/app/nutrient-cycle/field-cultivation/special-crops-table/special-crops-table.component.d.ts +5 -5
  28. package/dist/types/app/nutrient-cycle/field-cultivation/special-crops-table/special-crops-table.config.d.ts +22 -32
  29. package/dist/types/app/nutrient-cycle/field-spreading/fertiliser-table/fertiliser-table.component.d.ts +16 -7
  30. package/dist/types/app/nutrient-cycle/field-spreading/field-spreading.component.d.ts +2 -0
  31. package/dist/types/app/nutrient-cycle/field-spreading/field-spreading.types.d.ts +1 -0
  32. package/dist/types/app/nutrient-cycle/field-spreading/recycling-table/recycling-table.component.d.ts +10 -9
  33. package/dist/types/app/nutrient-cycle/fodder/fodder-table/fodder-table.component.d.ts +5 -3
  34. package/package.json +1 -1
  35. package/dist/elements/browser/suibi-module.v0.1.0-preview.01100f9.js +0 -78
@@ -1 +1 @@
1
- 0.1.0-preview.01100f9 01100f9
1
+ 0.1.0-preview.0808fe0 0808fe0
@@ -1 +1 @@
1
- .cdk-overlay-container{position:fixed;inset:0;z-index:1200;pointer-events:none}.cdk-overlay-container:empty{display:none}.cdk-global-overlay-wrapper{position:fixed;inset:0;display:flex;align-items:flex-start;justify-content:center;pointer-events:none}.cdk-overlay-connected-position-bounding-box{position:absolute;z-index:1200;display:flex;flex-direction:column;min-width:1px;min-height:1px}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;z-index:1200}.cdk-overlay-backdrop{position:fixed;inset:0;z-index:1199;pointer-events:auto}.cdk-overlay-transparent-backdrop{background:transparent}.cdk-overlay-dark-backdrop{background:#00000052}.mat-mdc-select-panel.mdc-menu-surface{background:#fff;--mdc-theme-surface: #fff;--mdc-menu-container-color: #fff;--mat-select-panel-background-color: #fff;box-shadow:0 4px 16px #00000040;border-radius:4px;outline:0;opacity:1!important;filter:none!important}
1
+ .cdk-overlay-container{position:fixed;inset:0;z-index:1200;pointer-events:none}.cdk-overlay-container:empty{display:none}.cdk-global-overlay-wrapper{position:fixed;inset:0;display:flex;align-items:flex-start;justify-content:center;pointer-events:none}.cdk-overlay-connected-position-bounding-box{position:absolute;z-index:1200;display:flex;flex-direction:column;min-width:1px;min-height:1px}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;z-index:1200}.cdk-overlay-backdrop{position:fixed;inset:0;z-index:1199;pointer-events:auto}.cdk-overlay-transparent-backdrop{background:transparent}.cdk-overlay-dark-backdrop{background:#00000052}.mat-mdc-select-panel.mdc-menu-surface{background:#fff;--mdc-theme-surface: #fff;--mdc-menu-container-color: #fff;--mat-select-panel-background-color: #fff;box-shadow:0 4px 16px #00000040;border-radius:4px;outline:0;opacity:1!important;filter:none!important}.mat-mdc-option.mdc-list-item{height:auto;min-height:48px}.mat-mdc-option .mdc-list-item__primary-text{white-space:normal;overflow:visible;text-overflow:clip;line-height:1.35}
@@ -3,9 +3,231 @@ import { PlanningBalanceService } from '../../core/planning/planning-balance.ser
3
3
  export declare class DataPreviewComponent {
4
4
  private readonly host;
5
5
  private readonly emitter;
6
+ private readonly clipboard;
6
7
  readonly planning: PlanningBalanceService;
7
8
  readonly naebi: NaebiDataService;
8
9
  readonly metadataPreview: import("@angular/core").Signal<import("../../../public-api").SuibiDataEventMetadata>;
10
+ /** Exact payload that runPlanning will POST (same builder as the real request). */
11
+ readonly apiPayload: import("@angular/core").Signal<{
12
+ nutrientCycle: {
13
+ animalStock: {
14
+ order: string;
15
+ animalCategoryPRIF: number;
16
+ selfReportedLivestock: {
17
+ averageQuantity: number;
18
+ };
19
+ durationCorrectionQuantity: number;
20
+ durationCorrectionDays: number;
21
+ utilisation: number;
22
+ formOfDetention: {
23
+ hasGrazing: boolean;
24
+ pasture: {
25
+ days: number;
26
+ hoursPerDay: number;
27
+ };
28
+ stall: {
29
+ stableSystem: string;
30
+ stableVentilationType: number;
31
+ hasStableDeepLitter: boolean;
32
+ hasStableManureBeltDrying: boolean;
33
+ stableManureRemovalFrequency: number;
34
+ stableWaterSupply: number;
35
+ hasStableManureAndSlurryProduction: boolean;
36
+ hasStableFeedingStationElevated: boolean;
37
+ hasStableFloorSlopedUrineDrainage: boolean;
38
+ hasYard: boolean;
39
+ };
40
+ };
41
+ hasImpexDerivedExcretion: boolean;
42
+ hasLinearCorrectionDerivedExcretion: boolean;
43
+ feeding: {
44
+ fodderConsumption: number;
45
+ concentratedFeedstuffConsumption: number;
46
+ concentratedFeedStuffConsumption: number;
47
+ winterFeeding: {
48
+ feedingRoutine: {
49
+ typeOfFeedstuffProduct: string;
50
+ transactionUnit: string;
51
+ quantity: number;
52
+ }[];
53
+ dailyConcentratedFeedstuff: {
54
+ quantity: number;
55
+ };
56
+ };
57
+ summerFeeding: {
58
+ feedingRoutine: {
59
+ typeOfFeedstuffProduct: string;
60
+ transactionUnit: string;
61
+ quantity: number;
62
+ }[];
63
+ dailyConcentratedFeedstuff: {
64
+ quantity: number;
65
+ };
66
+ };
67
+ };
68
+ }[];
69
+ storage: {
70
+ stableStrawLitter: number;
71
+ slurrySupplyProportionStored: number;
72
+ createFacilitySpecificManureProduct: boolean;
73
+ createFacilitySpecificSlurryProduct: boolean;
74
+ slurryTankQuantity: number;
75
+ dilutionRatio: number;
76
+ manurePit: never[];
77
+ slurryTank: {
78
+ descriptor: string;
79
+ volumeInM3: number;
80
+ depthInM: number;
81
+ cover: number;
82
+ mixingFrequency: number;
83
+ manureDerivation: string[];
84
+ }[];
85
+ };
86
+ farmManureTrading: {
87
+ supply: never[];
88
+ dispense: {
89
+ quantity: number;
90
+ typeOfManureRecyclingProduct: string;
91
+ transactionUnit: string;
92
+ manureDerivation: {
93
+ zoologicalAnimal: string;
94
+ percentageAllocation: number;
95
+ }[];
96
+ ingredient: {
97
+ quantity: number;
98
+ molecularFormula: string;
99
+ unit: string;
100
+ }[];
101
+ }[];
102
+ };
103
+ fieldSpreading: {
104
+ soilPh: number;
105
+ slurry: {
106
+ quantity: number;
107
+ dilutionRatio: number;
108
+ otherApplicationTechnologies: {
109
+ applicationTechnologyType: number;
110
+ proportionOfArea: number;
111
+ }[];
112
+ applicationDuringWarmDays: number;
113
+ proportionAppliedSummerMonths: number;
114
+ proportionAppliedOtherMonths: number;
115
+ proportionAppliedAfter6PM: number;
116
+ dragHoseAreaSize: number;
117
+ };
118
+ manure: {
119
+ applicationIncorporationTime: number;
120
+ proportionAppliedSummerMonths: number;
121
+ proportionAppliedOtherMonths: number;
122
+ };
123
+ fertiliser: {
124
+ descriptor: string;
125
+ fertiliserProductCategory: string;
126
+ quantity: number;
127
+ transactionUnit: string;
128
+ ingredient: {
129
+ molecularFormula: string;
130
+ quantity: number;
131
+ unit: string;
132
+ }[];
133
+ }[];
134
+ recycling: {
135
+ typeOfManureRecyclingProduct: string;
136
+ quantity: number;
137
+ transactionUnit: string;
138
+ ingredient: {
139
+ molecularFormula: string;
140
+ quantity: number;
141
+ unit: string;
142
+ }[];
143
+ }[];
144
+ };
145
+ lifetimePerformance: {
146
+ milkProductionReport: number;
147
+ };
148
+ fodder: {
149
+ trading: {
150
+ supply: {
151
+ typeOfFeedstuffProduct: string;
152
+ quantity: number;
153
+ transactionUnit: string;
154
+ ingredient: {
155
+ quantity: number;
156
+ molecularFormula: string;
157
+ unit: string;
158
+ }[];
159
+ }[];
160
+ dispense: {
161
+ typeOfFeedstuffProduct: string;
162
+ quantity: number;
163
+ transactionUnit: string;
164
+ ingredient: never[];
165
+ }[];
166
+ };
167
+ cropFromNonFodderCropland: {
168
+ typeOfFeedstuffProduct: string;
169
+ quantity: number;
170
+ transactionUnit: string;
171
+ ingredient: {
172
+ quantity: number;
173
+ molecularFormula: string;
174
+ unit: string;
175
+ }[];
176
+ }[];
177
+ proportionStorageLoss: number;
178
+ totalBalanceErrorMargin: number;
179
+ };
180
+ fieldCultivation: {
181
+ buildingZoneAreaSize: number;
182
+ outdoorVegetableMainCropAreaSize: number;
183
+ indoorVegetableMainCropAreaSize: number;
184
+ feedingOfCropHarvest: {
185
+ agronomicCropCategory: string;
186
+ effectiveYield: number;
187
+ }[];
188
+ dispenseOfCropHarvest: {
189
+ agronomicCropCategory: string;
190
+ effectiveYield: number;
191
+ }[];
192
+ fodderCropland: ({
193
+ agronomicCropCategory: string;
194
+ isMainCrop: boolean;
195
+ areaSize: number;
196
+ calculateYieldThroughFodderBalance: boolean;
197
+ yieldPerSquareMeasure?: undefined;
198
+ } | {
199
+ agronomicCropCategory: string;
200
+ isMainCrop: boolean;
201
+ areaSize: number;
202
+ yieldPerSquareMeasure: number;
203
+ calculateYieldThroughFodderBalance?: undefined;
204
+ })[];
205
+ arableLand: {
206
+ agronomicCropCategory: string;
207
+ isMainCrop: boolean;
208
+ areaSize: number;
209
+ yieldPerSquareMeasure: number;
210
+ hasLongTermYieldDivergence: boolean;
211
+ }[];
212
+ special: {
213
+ agronomicCropCategory: string;
214
+ userSpecificPlantName: string;
215
+ isMainCrop: boolean;
216
+ areaSize: number;
217
+ requirementCorrection: {
218
+ quantity: number;
219
+ molecularFormula: string;
220
+ unit: string;
221
+ }[];
222
+ }[];
223
+ };
224
+ generateReport: boolean;
225
+ };
226
+ calculationVersion: {
227
+ version: string;
228
+ };
229
+ }>;
9
230
  setPlanningEndpoint(value: string | null): void;
10
231
  emitData(): void;
232
+ copyJson(getValue: () => unknown): void;
11
233
  }
@@ -1,6 +1,6 @@
1
1
  import { MatCheckboxChange } from '@angular/material/checkbox';
2
2
  import { BaseCodeListEntry } from '../../jsonforms/shared/option-sources/code-lists';
3
- import { InlineColumnDef, InlineTableConfig } from './inline-table.types';
3
+ import { InlineColumnDef, InlineTableConfig, InlineCellErrorResolver } from './inline-table.types';
4
4
  /**
5
5
  * InlineTableComponent — generic inline-editable table for array-of-objects data.
6
6
  *
@@ -27,6 +27,8 @@ import { InlineColumnDef, InlineTableConfig } from './inline-table.types';
27
27
  */
28
28
  export declare class InlineTableComponent {
29
29
  private readonly i18n;
30
+ private readonly host;
31
+ constructor();
30
32
  /** Table configuration — columns, labels, and the row factory. Acts as the "uischema". */
31
33
  readonly config: import("@angular/core").InputSignal<InlineTableConfig>;
32
34
  /** Current array of row objects. Each key maps to a column's `key` or sub-array path. */
@@ -39,17 +41,35 @@ export declare class InlineTableComponent {
39
41
  /**
40
42
  * Optional business-logic transform applied after every cell edit — before emission.
41
43
  *
42
- * Signature: `(changedKey, newValue, updatedRow) => transformedRow`
44
+ * Signature: `(changedKey, newValue, updatedRow, previousRow) => transformedRow`
43
45
  *
44
46
  * - `changedKey`: the `InlineColumnDef.key` of the edited column.
45
47
  * - `newValue`: the raw new value (string, number, or null).
46
48
  * - `updatedRow`: the row object already reflecting the edit.
49
+ * - `previousRow`: the row as it was before the edit — needed when the side-effect
50
+ * depends on the replaced value (e.g. converting a quantity on a unit switch).
47
51
  * - Return the row (possibly further mutated) that should be stored.
48
52
  *
49
53
  * Use this for cross-field side-effects such as pre-populating nutrient % columns
50
54
  * when a product is selected. For forms without such logic, omit this input.
51
55
  */
52
- readonly rowTransform: import("@angular/core").InputSignal<((changedKey: string, newValue: unknown, row: Record<string, unknown>) => Record<string, unknown>) | null>;
56
+ readonly rowTransform: import("@angular/core").InputSignal<((changedKey: string, newValue: unknown, row: Record<string, unknown>, previousRow?: Record<string, unknown>) => Record<string, unknown>) | null>;
57
+ /**
58
+ * Optional per-cell validation resolver — returns a translated error message
59
+ * shown under the cell (cell turns red via `mat-form-field-invalid`), or `null`
60
+ * when valid. Inline tables bypass JSONForms/AJV, so business rules that must
61
+ * be visible while typing (e.g. percentage caps) are surfaced through this hook.
62
+ */
63
+ readonly cellError: import("@angular/core").InputSignal<InlineCellErrorResolver | null>;
64
+ /**
65
+ * Index of the row to visually highlight (e.g. after a validation-error deep-link).
66
+ * Also triggers a scrollIntoView so the row is on screen.
67
+ */
68
+ readonly highlightRowIndex: import("@angular/core").InputSignal<number | null>;
69
+ /** Resolves the per-cell error message via the optional `cellError` input (null = valid). */
70
+ errorForCell(row: Record<string, unknown>, col: InlineColumnDef): string | null;
71
+ /** True when any column of the row reports an error — drives the row-level red tint. */
72
+ rowHasError(row: Record<string, unknown>): boolean;
53
73
  /** Extracts a px value from a grid-format width string (e.g. 'minmax(120px, 1fr)' → '120px'). */
54
74
  colMinWidth(width?: string): string;
55
75
  /** Total table width = sum of all column px values + 40px for the delete column. */
@@ -125,7 +145,9 @@ export declare class InlineTableComponent {
125
145
  addSubEntry(rowIdx: number, col: InlineColumnDef): void;
126
146
  removeSubEntry(rowIdx: number, col: InlineColumnDef, subIdx: number): void;
127
147
  onSubEntryKeyChange(rowIdx: number, col: InlineColumnDef, subIdx: number, value: unknown): void;
128
- /** Commits on every keystroke (not on blur) — same rationale as `onNumberChange`. */
148
+ /** Commits on every keystroke (not on blur) — same rationale as `onNumberChange`.
149
+ * When `expectedTotal` is set, the value is clamped to the remaining share so the
150
+ * sum can never exceed it; smaller intermediate keystrokes still go through. */
129
151
  onSubEntryValueChange(rowIdx: number, col: InlineColumnDef, subIdx: number, event: Event): void;
130
152
  /** Writes the sub-entry array back to the row, runs rowTransform, emits rowsChange. */
131
153
  private updateSubEntries;
@@ -19,6 +19,14 @@ import type { BaseCodeListEntry } from '../../jsonforms/shared/option-sources/co
19
19
  * 'subrows' renders an expandable editor for a nested list of key/percentage
20
20
  * pairs (e.g. manureDerivation[]) — see `InlineSubrowsConfig`. */
21
21
  export type InlineCellType = 'select' | 'number' | 'text' | 'readonly' | 'checkbox' | 'subrows';
22
+ /**
23
+ * Per-cell validation resolver — returns a translated error message to display
24
+ * under the cell, or `null` when the cell is valid. Passed as the `cellError`
25
+ * input of InlineTableComponent for domain tables that bypass JSONForms/AJV
26
+ * (inline tables are not schema-validated, so business rules like percentage
27
+ * caps must be surfaced visually through this hook).
28
+ */
29
+ export type InlineCellErrorResolver = (row: Record<string, unknown>, col: InlineColumnDef) => string | null;
22
30
  /**
23
31
  * Configuration for a `type: 'subrows'` column — an expandable editor for a nested
24
32
  * array of `{ key, value }` entries (e.g. manureDerivation: zoologicalAnimal +
@@ -47,7 +55,8 @@ export interface InlineSubrowsConfig {
47
55
  collapseLabelKey: string;
48
56
  /** i18n key for the total indicator label. */
49
57
  totalLabelKey: string;
50
- /** Expected sum of the value fields (e.g. 100) — the indicator turns green on match. */
58
+ /** Expected sum of the value fields (e.g. 100) — the indicator turns green on exact
59
+ * match (no more, no less) and entered values are clamped so the sum never exceeds it. */
51
60
  expectedTotal?: number;
52
61
  /** Min for the value number input (default 0). */
53
62
  valueMin?: number;
@@ -58,6 +58,11 @@ export declare class NavigationService {
58
58
  type: StorageEntryType;
59
59
  index: number;
60
60
  } | null>;
61
+ /** Selected field spreading row context (category/tab + row index) — used by validation deep-links */
62
+ readonly selectedFieldSpreadingDetail: import("@angular/core").WritableSignal<{
63
+ category: FieldSpreadingCategory;
64
+ index: number;
65
+ } | null>;
61
66
  /** True if currently viewing animal stock detail (not list) */
62
67
  readonly isAnimalStockDetailView: import("@angular/core").Signal<boolean>;
63
68
  /** True if currently on animal stock page (list or detail) */
@@ -81,6 +86,8 @@ export declare class NavigationService {
81
86
  navigateToFodderDetail(mode: FodderMode, index: number, category?: FodderCategory): void;
82
87
  navigateToFodderList(): void;
83
88
  navigateToFieldSpreading(category?: FieldSpreadingCategory): void;
89
+ /** Deep-link to a specific row of a field spreading tab (e.g. from a validation error). */
90
+ navigateToFieldSpreadingDetail(category: FieldSpreadingCategory, index: number): void;
84
91
  navigateToFieldCultivationDetail(tab: Exclude<FieldCultivationTab, 'general'>, index: number): void;
85
92
  navigateToFieldCultivationList(tab?: FieldCultivationTab): void;
86
93
  navigateToStorageDetail(type: StorageEntryType, index: number): void;
@@ -33,6 +33,14 @@ export declare class PlanningBalanceService {
33
33
  };
34
34
  readonly selectedEndpoint: import("@angular/core").Signal<PlanningEndpoint>;
35
35
  setSelectedEndpoint(endpoint: PlanningEndpoint): void;
36
+ /**
37
+ * Builds the exact payload posted to the planning API: calculationVersion stamped from
38
+ * the current metadata, `nutrientCycle.generateReport` forced to true (the PDF report
39
+ * must always be requested on submission), empty ingredient/routine slots stripped,
40
+ * null optional fields and host-only fields removed, custom display units converted
41
+ * to base units.
42
+ */
43
+ buildApiPayload(data: NaebiData): NaebiData;
36
44
  /** Fire a new planning-balance request with the current NAEBI data payload. */
37
45
  runPlanning(data: NaebiData, endpoint?: PlanningEndpoint): void;
38
46
  }
@@ -0,0 +1,38 @@
1
+ export interface CustomUnit {
2
+ /** Unit code stored on the row (e.g. 'ha', 't', 'dt'). */
3
+ code: string;
4
+ /** API base unit this custom unit converts to (e.g. 'm2', 'kg'). */
5
+ baseUnit: string;
6
+ /** Multiplier from the custom unit to its base unit (1 ha = 10'000 m2 → 10000). */
7
+ factor: number;
8
+ descriptor?: Record<string, unknown>;
9
+ }
10
+ export declare const CUSTOM_UNITS: readonly CustomUnit[];
11
+ /** True when the unit code is a custom display unit (not an API base unit). */
12
+ export declare function isCustomUnit(unit: unknown): boolean;
13
+ /** Base unit a quantity can be converted to: the custom unit's `baseUnit`, or the unit
14
+ * itself when it already is a base (or unknown) unit. */
15
+ export declare function baseUnitOf(unit: string | null | undefined): string | null | undefined;
16
+ /** Custom units convertible to the given base unit (e.g. 'kg' → [t, dt]). */
17
+ export declare function customUnitsForBaseUnit(baseUnit: string): CustomUnit[];
18
+ /**
19
+ * Builds a filter (compatible with the inline table's `optionsFilter`) keeping only the
20
+ * units compatible with the given context base unit — i.e. the base unit itself plus its
21
+ * custom units. Pass a nullable base (e.g. derived from a product's matterState): `null`
22
+ * keeps the full list (nothing derived yet).
23
+ *
24
+ * Works on any entry shape exposing `id` (and `baseUnit` for custom units).
25
+ */
26
+ export declare function unitOptionsForBaseUnit<T extends {
27
+ id: string | number;
28
+ }>(contextBaseUnit: string | null | undefined): (entry: T) => boolean;
29
+ /** Converts a quantity expressed in a custom unit to its base unit; pass-through for
30
+ * base/unknown units and null quantities. */
31
+ export declare function convertToBaseUnit(quantity: number | null | undefined, unit: string | null | undefined): {
32
+ quantity: number | null | undefined;
33
+ unit: string | null | undefined;
34
+ };
35
+ /** Converts a quantity between two units sharing the same base unit (kg ↔ t ↔ dt,
36
+ * m2 ↔ ha). Returns the quantity unchanged when either unit is missing or the units
37
+ * are incompatible (different base units). */
38
+ export declare function convertQuantityBetween(quantity: number | null | undefined, fromUnit: string | null | undefined, toUnit: string | null | undefined): number | null | undefined;
@@ -22,15 +22,6 @@ export declare function deepEqual(a: unknown, b: unknown): boolean;
22
22
  * @returns A deep copy of the value
23
23
  */
24
24
  export declare function deepClone<T>(value: T): T;
25
- /**
26
- * Recursively drops `ingredient[]` entries whose `quantity` is null/undefined.
27
- *
28
- * Inline tables (fertiliser/recycling) pre-populate every nutrient slot in `ingredient[]`
29
- * so the UI always shows all formula columns, but slots the user never fills in stay as
30
- * `{ molecularFormula, quantity: null, unit }`. The calculation API rejects any ingredient
31
- * entry that's present but incomplete, so submitted payloads must omit those still-empty
32
- * slots entirely rather than send them with a null quantity.
33
- */
34
25
  export declare function stripIncompleteIngredients<T>(value: T): T;
35
26
  /**
36
27
  * Sanitizes the planning payload: the calculation API (OpenAPI, no `nullable: true`
@@ -43,6 +34,11 @@ export declare function stripIncompleteIngredients<T>(value: T): T;
43
34
  * feedingOfCropHarvest, dispenseOfCropHarvest): any null property is dropped
44
35
  * (fieldName, userSpecificPlantName, yieldPerSquareMeasure, effectiveYield, …)
45
36
  * - `nutrientCycle.fodder.trading.<collection>[].quantity`: null → dropped
37
+ * - `nutrientCycle.animalStock[].feeding.(winter|summer)Feeding`: never-filled routine
38
+ * slots (`{}` rows pre-created by the inline tables) and null fields are dropped;
39
+ * a season — and `feeding` itself — is removed when nothing filled-in remains
40
+ * - `nutrientCycle.fieldSpreading`: null fields dropped (`soilPh`, `slurry.*`,
41
+ * `manure.*`); `slurry`/`manure` removed when nothing filled-in remains
46
42
  */
47
43
  export declare function sanitizePlanningPayload(data: NaebiDataLike): NaebiDataLike;
48
44
  /** Structural type so object.util stays free of a NaebiData import (circular-safe). */
@@ -9,6 +9,7 @@ import Ajv from 'ajv';
9
9
  *
10
10
  * Custom keywords:
11
11
  * - `permittedStableSystem`: Validates stable system against animal category constraints
12
+ * - `proportionSum`: Validates the sum of one numeric field across array items
12
13
  *
13
14
  * @param options - Additional AJV options to merge with defaults
14
15
  */
@@ -16,3 +16,9 @@ export declare function derivePermittedStableSystems(animalCategoryPRIF: number
16
16
  * Maps animalCategoryPRIF -> array of permitted stableSystem enum strings.
17
17
  */
18
18
  export declare const PERMITTED_STABLE_SYSTEMS: Record<number, string[]>;
19
+ /**
20
+ * Pre-computed map of non-zero per-category minimum yields (yieldPerSquareMeasure).
21
+ * Only 8 of the 314 crop categories declare a minimum > 0 (e.g. cereals, peas);
22
+ * all others fall back to the schema-level `minimum: 0`.
23
+ */
24
+ export declare const CROP_MINIMUM_YIELD_PER_SQUARE_MEASURE: Record<string, number>;
@@ -25,6 +25,8 @@ export interface EnrichedValidationError extends ErrorObject {
25
25
  targetFodderCategory?: FodderCategory;
26
26
  /** Category tab inside field spreading */
27
27
  targetFieldSpreadingCategory?: FieldSpreadingCategory;
28
+ /** Row index inside a field spreading tab (fertiliser/recycling) */
29
+ targetFieldSpreadingIndex?: number;
28
30
  /** Category tab inside field cultivation */
29
31
  targetFieldCultivationTab?: FieldCultivationTab;
30
32
  /** Field cultivation detail index */
@@ -9,10 +9,10 @@ type TradingRow = FarmManureTradingSupply | FarmManureTradingDispense;
9
9
  * 1. Provides the per-mode column/section config (the "uischema").
10
10
  * 2. Bridges typed `FarmManureTradingSupply[]`/`Dispense[]` to/from the generic
11
11
  * `Record<string, unknown>[]` API of InlineTableComponent.
12
- * 3. Supplies `deriveUnits` as a `rowTransform` — `transactionUnit` ('kg'/'m3') and
13
- * every ingredient's `unit` ('kg/t'/'kg/m3') are fully derived from the selected
14
- * product's matterState (never user-entered) — two independent mappings, NOT a
15
- * concatenation of one into the other.
12
+ * 3. Supplies `deriveUnits` as a `rowTransform` — the base `transactionUnit` ('kg'/'m3')
13
+ * and every ingredient's `unit` ('kg/t'/'kg/m3') are derived from the selected
14
+ * product's matterState; the user may switch to a custom unit sharing the same base
15
+ * (t, dt for kg), which converts the entered quantity on the fly.
16
16
  */
17
17
  export declare class FarmManureTradingTableComponent {
18
18
  readonly mode: import("@angular/core").InputSignal<FarmManureTradingMode>;
@@ -24,11 +24,12 @@ export declare class FarmManureTradingTableComponent {
24
24
  /** Cast generic output back to typed trading rows. */
25
25
  onRowsChange(rows: Record<string, unknown>[]): void;
26
26
  /**
27
- * Row transform re-deriving `transactionUnit` and every ingredient's `unit` from the
28
- * product's matterState on each edit — runs unconditionally (not just when
29
- * `typeOfManureRecyclingProduct` changes) so an edit made before the product is picked
30
- * still resolves once it is.
27
+ * Row transform deriving the ingredient units from the product's matterState on each
28
+ * edit, and handling `transactionUnit`:
29
+ * - product change (or missing unit on legacy rows): reset to the derived base unit,
30
+ * - unit switch: convert the entered quantity from the previous to the new unit
31
+ * (both share the same base), so the transacted amount is preserved.
31
32
  */
32
- readonly deriveUnits: (_changedKey: string, _newValue: unknown, row: Record<string, unknown>) => Record<string, unknown>;
33
+ readonly deriveUnits: (changedKey: string, newValue: unknown, row: Record<string, unknown>, previousRow?: Record<string, unknown>) => Record<string, unknown>;
33
34
  }
34
35
  export {};
@@ -10,6 +10,8 @@ export interface ManureDerivation {
10
10
  export interface FarmManureTradingSupply {
11
11
  quantity?: number;
12
12
  typeOfManureRecyclingProduct?: string;
13
+ /** Host-only free text: persisted in the emitted dossier, stripped before API calls. */
14
+ productDesignation?: string;
13
15
  transactionUnit?: string;
14
16
  manureDerivation?: ManureDerivation[];
15
17
  ingredient?: ManureIngredient[];
@@ -17,6 +19,8 @@ export interface FarmManureTradingSupply {
17
19
  export interface FarmManureTradingDispense {
18
20
  quantity?: number;
19
21
  typeOfManureRecyclingProduct?: string;
22
+ /** Host-only free text: persisted in the emitted dossier, stripped before API calls. */
23
+ productDesignation?: string;
20
24
  transactionUnit?: string;
21
25
  manureDerivation?: ManureDerivation[];
22
26
  ingredient?: ManureIngredient[];
@@ -15,7 +15,8 @@ export declare class ArableLandTableComponent {
15
15
  onRowsChange(rows: Record<string, unknown>[]): void;
16
16
  /**
17
17
  * Crop change: clear yield + divergence when the new crop forbids yield input,
18
- * so no stale values linger under the now-disabled cells.
18
+ * so no stale values linger under the now-disabled cells. Requirement corrections
19
+ * are special-crops-only (SUIBIL-1143): strip them so they never reach the emitted JSON.
19
20
  */
20
- readonly enforceArableRules: (changedKey: string, newValue: unknown, row: Record<string, unknown>) => Record<string, unknown>;
21
+ readonly enforceArableRules: (changedKey: string, newValue: unknown, row: Record<string, unknown>, previousRow?: Record<string, unknown>) => Record<string, unknown>;
21
22
  }
@@ -6,4 +6,6 @@
6
6
  * `agronomicCropCategoryArableLand` code list as-is, keeping its existing order.
7
7
  */
8
8
  import type { InlineTableConfig } from '../../../components/inline-table/inline-table.types';
9
+ /** Looks up the row's selected crop in the arable-land code list. */
10
+ export declare function arableCrop(row: Record<string, unknown>): Record<string, unknown> | undefined;
9
11
  export declare const ARABLE_LAND_TABLE_CONFIG: InlineTableConfig;
@@ -12,7 +12,7 @@ export declare class FieldCultivationMasterDetailComponent {
12
12
  readonly feedingRows: import("@angular/core").Signal<FieldCultivationCropHarvest[]>;
13
13
  readonly dispenseRows: import("@angular/core").Signal<FieldCultivationCropHarvest[]>;
14
14
  readonly activeTab: import("@angular/core").WritableSignal<FieldCultivationTab>;
15
- readonly activeTabIndex: import("@angular/core").Signal<0 | 1 | 2 | 3>;
15
+ readonly activeTabIndex: import("@angular/core").Signal<1 | 0 | 2 | 3>;
16
16
  onTabChange(index: number): void;
17
17
  onGeneralChange(value: FieldCultivationData): void;
18
18
  /** Persists one crop-harvest table's rows (fed / dispensed harvests of the general tab). */
@@ -11,8 +11,12 @@ export interface FieldCultivationCropHarvest {
11
11
  export interface FieldCultivationBaseEntry {
12
12
  agronomicCropCategory?: string | null;
13
13
  userSpecificPlantName?: string | null;
14
- isMainCrop?: boolean;
14
+ /** Null for CSC_OUTVG special crops — the main-crop concept does not apply to them. */
15
+ isMainCrop?: boolean | null;
15
16
  areaSize?: number | null;
17
+ /** Host-only display unit for areaSize ('m2' base, or custom e.g. 'ha') — persisted in
18
+ * the dossier, converted to m2 and stripped before the planning POST (docs/custom-units.md). */
19
+ areaUnit?: string | null;
16
20
  yieldPerSquareMeasure?: number | null;
17
21
  }
18
22
  export interface FieldCultivationFodderCropland extends FieldCultivationBaseEntry {
@@ -1,4 +1,8 @@
1
1
  import { FieldCultivationCropHarvest, FieldCultivationData, FieldCultivationDetailEntry, FieldCultivationDetailMode, FieldCultivationRequirementCorrection } from './field-cultivation.types';
2
+ /** Subcategory whose special crops are never a main crop — `isMainCrop` must be null. */
3
+ export declare const OUTDOOR_VEGETABLE_SUBCATEGORY = "CSC_OUTVG";
4
+ /** True when the crop code belongs to the outdoor-vegetable subcategory (CSC_OUTVG). */
5
+ export declare function isOutdoorVegetableCrop(cropCode: unknown): boolean;
2
6
  export declare function normalizeRequirementCorrection(source: Partial<FieldCultivationRequirementCorrection> | undefined): FieldCultivationRequirementCorrection;
3
7
  export declare function normalizeCropHarvestEntry(source: Partial<FieldCultivationCropHarvest> | undefined): FieldCultivationCropHarvest;
4
8
  export declare function normalizeFieldCultivationDetailEntry(mode: FieldCultivationDetailMode, source: Partial<FieldCultivationDetailEntry> | undefined): FieldCultivationDetailEntry;
@@ -21,7 +21,8 @@ export declare class FodderCroplandTableComponent {
21
21
  /**
22
22
  * Clears the yield whenever it stops being user-editable for the row (fodder-balance
23
23
  * calculation switched on, or new crop without yield input), so no stale value
24
- * lingers under the now-disabled cell.
24
+ * lingers under the now-disabled cell. Requirement corrections are special-crops-only
25
+ * (SUIBIL-1143): strip them so they never reach the emitted JSON.
25
26
  */
26
- readonly enforceFodderCroplandRules: (changedKey: string, newValue: unknown, row: Record<string, unknown>) => Record<string, unknown>;
27
+ readonly enforceFodderCroplandRules: (changedKey: string, newValue: unknown, row: Record<string, unknown>, previousRow?: Record<string, unknown>) => Record<string, unknown>;
27
28
  }
@@ -19,11 +19,11 @@ export declare class SpecialCropsTableComponent {
19
19
  onRowsChange(rows: Record<string, unknown>[]): void;
20
20
  /**
21
21
  * Domain rules applied on every edit:
22
- * - units of the managed nutrient slots are fixed (N2 in kg, corrections in %),
22
+ * - the unit of the managed nutrient slots is fixed (corrections in %),
23
23
  * - managed slots left empty are dropped (the API rejects null quantities),
24
- * - selecting a crop pre-fills Nmin from its standard N2 requirement and resets the
25
- * corrections to 100 %; clearing the crop (or picking one that forbids corrections)
26
- * removes the managed slots.
24
+ * - CSC_OUTVG crops carry `isMainCrop: null` (not a main-crop concept),
25
+ * - selecting a crop pre-fills the three corrections (P2O5/K2O/Mg) at 100 %;
26
+ * clearing the crop removes the managed slots.
27
27
  */
28
- readonly enforceSpecialCropRules: (changedKey: string, newValue: unknown, row: Record<string, unknown>) => Record<string, unknown>;
28
+ readonly enforceSpecialCropRules: (changedKey: string, newValue: unknown, row: Record<string, unknown>, previousRow?: Record<string, unknown>) => Record<string, unknown>;
29
29
  }