mirror of https://github.com/ghostfolio/ghostfolio
7 changed files with 842 additions and 7 deletions
@ -0,0 +1,327 @@ |
|||||
|
import { K1ImportDataService } from '@ghostfolio/client/services/k1-import-data.service'; |
||||
|
import { FamilyOfficeDataService } from '@ghostfolio/client/services/family-office-data.service'; |
||||
|
|
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { |
||||
|
ChangeDetectionStrategy, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
DestroyRef, |
||||
|
OnInit |
||||
|
} from '@angular/core'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { FormsModule } from '@angular/forms'; |
||||
|
import { MatButtonModule } from '@angular/material/button'; |
||||
|
import { MatCheckboxModule } from '@angular/material/checkbox'; |
||||
|
import { MatFormFieldModule } from '@angular/material/form-field'; |
||||
|
import { MatIconModule } from '@angular/material/icon'; |
||||
|
import { MatInputModule } from '@angular/material/input'; |
||||
|
import { MatSelectModule } from '@angular/material/select'; |
||||
|
import { MatTableModule } from '@angular/material/table'; |
||||
|
import { MatTooltipModule } from '@angular/material/tooltip'; |
||||
|
|
||||
|
interface EditableMapping { |
||||
|
boxNumber: string; |
||||
|
label: string; |
||||
|
description: string; |
||||
|
isCustom: boolean; |
||||
|
isEditing: boolean; |
||||
|
editLabel: string; |
||||
|
editDescription: string; |
||||
|
} |
||||
|
|
||||
|
interface EditableRule { |
||||
|
name: string; |
||||
|
operation: string; |
||||
|
sourceCells: string[]; |
||||
|
isEditing: boolean; |
||||
|
editName: string; |
||||
|
editSourceCells: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
host: { class: 'page' }, |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
FormsModule, |
||||
|
MatButtonModule, |
||||
|
MatCheckboxModule, |
||||
|
MatFormFieldModule, |
||||
|
MatIconModule, |
||||
|
MatInputModule, |
||||
|
MatSelectModule, |
||||
|
MatTableModule, |
||||
|
MatTooltipModule |
||||
|
], |
||||
|
selector: 'gf-cell-mapping-page', |
||||
|
styleUrls: ['./cell-mapping-page.scss'], |
||||
|
templateUrl: './cell-mapping-page.html' |
||||
|
}) |
||||
|
export class CellMappingPageComponent implements OnInit { |
||||
|
public aggregationRules: EditableRule[] = []; |
||||
|
public error: string | null = null; |
||||
|
public isSaving = false; |
||||
|
public mappings: EditableMapping[] = []; |
||||
|
public partnerships: Array<{ id: string; name: string }> = []; |
||||
|
public selectedPartnershipId = ''; |
||||
|
public successMessage: string | null = null; |
||||
|
|
||||
|
// New custom cell form
|
||||
|
public newBoxNumber = ''; |
||||
|
public newLabel = ''; |
||||
|
|
||||
|
// New rule form
|
||||
|
public newRuleName = ''; |
||||
|
public newRuleSourceCells = ''; |
||||
|
|
||||
|
public displayedColumns = ['boxNumber', 'label', 'description', 'isCustom', 'actions']; |
||||
|
|
||||
|
public constructor( |
||||
|
private readonly changeDetectorRef: ChangeDetectorRef, |
||||
|
private readonly destroyRef: DestroyRef, |
||||
|
private readonly familyOfficeDataService: FamilyOfficeDataService, |
||||
|
private readonly k1ImportDataService: K1ImportDataService |
||||
|
) {} |
||||
|
|
||||
|
public ngOnInit(): void { |
||||
|
this.fetchPartnerships(); |
||||
|
} |
||||
|
|
||||
|
public onPartnershipChange(): void { |
||||
|
if (this.selectedPartnershipId) { |
||||
|
this.loadMappings(); |
||||
|
this.loadAggregationRules(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ── Cell Mapping Methods ─────────────────────────────────────────
|
||||
|
|
||||
|
public startEditMapping(mapping: EditableMapping): void { |
||||
|
mapping.isEditing = true; |
||||
|
mapping.editLabel = mapping.label; |
||||
|
mapping.editDescription = mapping.description; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
public saveEditMapping(mapping: EditableMapping): void { |
||||
|
mapping.label = mapping.editLabel; |
||||
|
mapping.description = mapping.editDescription; |
||||
|
mapping.isEditing = false; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
public cancelEditMapping(mapping: EditableMapping): void { |
||||
|
mapping.isEditing = false; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
public addCustomCell(): void { |
||||
|
if (!this.newBoxNumber || !this.newLabel) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.mappings.push({ |
||||
|
boxNumber: this.newBoxNumber, |
||||
|
label: this.newLabel, |
||||
|
description: '', |
||||
|
isCustom: true, |
||||
|
isEditing: false, |
||||
|
editLabel: '', |
||||
|
editDescription: '' |
||||
|
}); |
||||
|
|
||||
|
this.newBoxNumber = ''; |
||||
|
this.newLabel = ''; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
public removeMapping(index: number): void { |
||||
|
this.mappings.splice(index, 1); |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
public saveMappings(): void { |
||||
|
if (!this.selectedPartnershipId) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.isSaving = true; |
||||
|
this.error = null; |
||||
|
this.successMessage = null; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
|
||||
|
this.k1ImportDataService |
||||
|
.updateCellMappings({ |
||||
|
partnershipId: this.selectedPartnershipId, |
||||
|
mappings: this.mappings.map((m) => ({ |
||||
|
boxNumber: m.boxNumber, |
||||
|
label: m.label, |
||||
|
description: m.description, |
||||
|
isCustom: m.isCustom |
||||
|
})) |
||||
|
}) |
||||
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
||||
|
.subscribe({ |
||||
|
next: () => { |
||||
|
this.isSaving = false; |
||||
|
this.successMessage = 'Cell mappings saved successfully.'; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
}, |
||||
|
error: (err) => { |
||||
|
this.isSaving = false; |
||||
|
this.error = |
||||
|
err?.error?.message || err?.message || 'Failed to save mappings.'; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public resetToDefaults(): void { |
||||
|
if (!this.selectedPartnershipId) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.k1ImportDataService |
||||
|
.resetCellMappings(this.selectedPartnershipId) |
||||
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
||||
|
.subscribe({ |
||||
|
next: () => { |
||||
|
this.successMessage = 'Cell mappings reset to IRS defaults.'; |
||||
|
this.loadMappings(); |
||||
|
}, |
||||
|
error: (err) => { |
||||
|
this.error = |
||||
|
err?.error?.message || err?.message || 'Failed to reset mappings.'; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// ── Aggregation Rule Methods ─────────────────────────────────────
|
||||
|
|
||||
|
public addAggregationRule(): void { |
||||
|
if (!this.newRuleName || !this.newRuleSourceCells) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.aggregationRules.push({ |
||||
|
name: this.newRuleName, |
||||
|
operation: 'SUM', |
||||
|
sourceCells: this.newRuleSourceCells.split(',').map((s) => s.trim()), |
||||
|
isEditing: false, |
||||
|
editName: '', |
||||
|
editSourceCells: '' |
||||
|
}); |
||||
|
|
||||
|
this.newRuleName = ''; |
||||
|
this.newRuleSourceCells = ''; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
public removeAggregationRule(index: number): void { |
||||
|
this.aggregationRules.splice(index, 1); |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
public saveAggregationRules(): void { |
||||
|
if (!this.selectedPartnershipId) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.isSaving = true; |
||||
|
this.error = null; |
||||
|
this.successMessage = null; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
|
||||
|
this.k1ImportDataService |
||||
|
.updateAggregationRules({ |
||||
|
partnershipId: this.selectedPartnershipId, |
||||
|
rules: this.aggregationRules.map((r) => ({ |
||||
|
name: r.name, |
||||
|
operation: r.operation, |
||||
|
sourceCells: r.sourceCells |
||||
|
})) |
||||
|
}) |
||||
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
||||
|
.subscribe({ |
||||
|
next: () => { |
||||
|
this.isSaving = false; |
||||
|
this.successMessage = 'Aggregation rules saved successfully.'; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
}, |
||||
|
error: (err) => { |
||||
|
this.isSaving = false; |
||||
|
this.error = |
||||
|
err?.error?.message || err?.message || 'Failed to save rules.'; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// ── Data Loading ─────────────────────────────────────────────────
|
||||
|
|
||||
|
private fetchPartnerships(): void { |
||||
|
this.familyOfficeDataService |
||||
|
.fetchPartnerships() |
||||
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
||||
|
.subscribe({ |
||||
|
next: (partnerships) => { |
||||
|
this.partnerships = partnerships.map((p) => ({ |
||||
|
id: p.id, |
||||
|
name: p.name |
||||
|
})); |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private loadMappings(): void { |
||||
|
this.k1ImportDataService |
||||
|
.fetchCellMappings(this.selectedPartnershipId) |
||||
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
||||
|
.subscribe({ |
||||
|
next: (mappings: any[]) => { |
||||
|
this.mappings = mappings.map((m) => ({ |
||||
|
boxNumber: m.boxNumber, |
||||
|
label: m.label, |
||||
|
description: m.description || '', |
||||
|
isCustom: m.isCustom, |
||||
|
isEditing: false, |
||||
|
editLabel: '', |
||||
|
editDescription: '' |
||||
|
})); |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
}, |
||||
|
error: (err) => { |
||||
|
this.error = |
||||
|
err?.error?.message || 'Failed to load cell mappings.'; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private loadAggregationRules(): void { |
||||
|
this.k1ImportDataService |
||||
|
.fetchAggregationRules(this.selectedPartnershipId) |
||||
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
||||
|
.subscribe({ |
||||
|
next: (rules: any[]) => { |
||||
|
this.aggregationRules = rules.map((r) => ({ |
||||
|
name: r.name, |
||||
|
operation: r.operation, |
||||
|
sourceCells: (r.sourceCells as string[]) || [], |
||||
|
isEditing: false, |
||||
|
editName: '', |
||||
|
editSourceCells: '' |
||||
|
})); |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
}, |
||||
|
error: (err) => { |
||||
|
this.error = |
||||
|
err?.error?.message || 'Failed to load aggregation rules.'; |
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,175 @@ |
|||||
|
<div class="container"> |
||||
|
<h1>Cell Mapping Configuration</h1> |
||||
|
|
||||
|
@if (error) { |
||||
|
<div class="alert alert-error">{{ error }}</div> |
||||
|
} |
||||
|
@if (successMessage) { |
||||
|
<div class="alert alert-success">{{ successMessage }}</div> |
||||
|
} |
||||
|
|
||||
|
<!-- Partnership Selector --> |
||||
|
<section class="partnership-selector"> |
||||
|
<mat-form-field appearance="outline"> |
||||
|
<mat-label>Partnership</mat-label> |
||||
|
<mat-select [(ngModel)]="selectedPartnershipId" (selectionChange)="onPartnershipChange()"> |
||||
|
@for (p of partnerships; track p.id) { |
||||
|
<mat-option [value]="p.id">{{ p.name }}</mat-option> |
||||
|
} |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</section> |
||||
|
|
||||
|
@if (selectedPartnershipId) { |
||||
|
<!-- Cell Mappings --> |
||||
|
<section class="cell-mappings"> |
||||
|
<h2>Cell Mappings</h2> |
||||
|
|
||||
|
<table mat-table [dataSource]="mappings" class="mappings-table"> |
||||
|
<!-- Box Number --> |
||||
|
<ng-container matColumnDef="boxNumber"> |
||||
|
<th mat-header-cell *matHeaderCellDef>Box #</th> |
||||
|
<td mat-cell *matCellDef="let row">{{ row.boxNumber }}</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<!-- Label --> |
||||
|
<ng-container matColumnDef="label"> |
||||
|
<th mat-header-cell *matHeaderCellDef>Label</th> |
||||
|
<td mat-cell *matCellDef="let row"> |
||||
|
@if (row.isEditing) { |
||||
|
<mat-form-field appearance="outline" class="inline-edit"> |
||||
|
<input matInput [(ngModel)]="row.editLabel" /> |
||||
|
</mat-form-field> |
||||
|
} @else { |
||||
|
{{ row.label }} |
||||
|
} |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<!-- Description --> |
||||
|
<ng-container matColumnDef="description"> |
||||
|
<th mat-header-cell *matHeaderCellDef>Description</th> |
||||
|
<td mat-cell *matCellDef="let row"> |
||||
|
@if (row.isEditing) { |
||||
|
<mat-form-field appearance="outline" class="inline-edit"> |
||||
|
<input matInput [(ngModel)]="row.editDescription" /> |
||||
|
</mat-form-field> |
||||
|
} @else { |
||||
|
{{ row.description }} |
||||
|
} |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<!-- Is Custom --> |
||||
|
<ng-container matColumnDef="isCustom"> |
||||
|
<th mat-header-cell *matHeaderCellDef>Custom</th> |
||||
|
<td mat-cell *matCellDef="let row"> |
||||
|
@if (row.isCustom) { |
||||
|
<mat-icon class="custom-badge" matTooltip="Partnership-specific override">star</mat-icon> |
||||
|
} |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<!-- Actions --> |
||||
|
<ng-container matColumnDef="actions"> |
||||
|
<th mat-header-cell *matHeaderCellDef>Actions</th> |
||||
|
<td mat-cell *matCellDef="let row; let i = index"> |
||||
|
@if (row.isEditing) { |
||||
|
<button mat-icon-button (click)="saveEditMapping(row)" matTooltip="Save"> |
||||
|
<mat-icon>check</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button (click)="cancelEditMapping(row)" matTooltip="Cancel"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
} @else { |
||||
|
<button mat-icon-button (click)="startEditMapping(row)" matTooltip="Edit"> |
||||
|
<mat-icon>edit</mat-icon> |
||||
|
</button> |
||||
|
@if (row.isCustom) { |
||||
|
<button mat-icon-button (click)="removeMapping(i)" matTooltip="Remove"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
} |
||||
|
} |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> |
||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> |
||||
|
</table> |
||||
|
|
||||
|
<!-- Add Custom Cell --> |
||||
|
<div class="add-row"> |
||||
|
<mat-form-field appearance="outline"> |
||||
|
<mat-label>Box #</mat-label> |
||||
|
<input matInput [(ngModel)]="newBoxNumber" placeholder="e.g. 20c" /> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field appearance="outline"> |
||||
|
<mat-label>Label</mat-label> |
||||
|
<input matInput [(ngModel)]="newLabel" placeholder="e.g. Other deductions" /> |
||||
|
</mat-form-field> |
||||
|
<button mat-stroked-button (click)="addCustomCell()" [disabled]="!newBoxNumber || !newLabel"> |
||||
|
<mat-icon>add</mat-icon> Add Custom Cell |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Mapping Actions --> |
||||
|
<div class="mapping-actions"> |
||||
|
<button mat-flat-button color="primary" (click)="saveMappings()" [disabled]="isSaving"> |
||||
|
Save Mappings |
||||
|
</button> |
||||
|
<button mat-stroked-button color="warn" (click)="resetToDefaults()"> |
||||
|
Reset to IRS Defaults |
||||
|
</button> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<!-- Aggregation Rules --> |
||||
|
<section class="aggregation-rules"> |
||||
|
<h2>Aggregation Rules</h2> |
||||
|
|
||||
|
@if (aggregationRules.length === 0) { |
||||
|
<p class="empty-state">No aggregation rules configured.</p> |
||||
|
} |
||||
|
|
||||
|
@for (rule of aggregationRules; track rule.name; let i = $index) { |
||||
|
<div class="rule-card"> |
||||
|
<div class="rule-header"> |
||||
|
<strong>{{ rule.name }}</strong> |
||||
|
<span class="rule-operation">{{ rule.operation }}</span> |
||||
|
<button mat-icon-button (click)="removeAggregationRule(i)" matTooltip="Remove rule"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<div class="rule-source-cells"> |
||||
|
Source cells: |
||||
|
@for (cell of rule.sourceCells; track cell) { |
||||
|
<span class="cell-chip">{{ cell }}</span> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
<!-- Add Aggregation Rule --> |
||||
|
<div class="add-rule-row"> |
||||
|
<mat-form-field appearance="outline"> |
||||
|
<mat-label>Rule Name</mat-label> |
||||
|
<input matInput [(ngModel)]="newRuleName" placeholder="e.g. Total Income" /> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field appearance="outline"> |
||||
|
<mat-label>Source Cells (comma-separated)</mat-label> |
||||
|
<input matInput [(ngModel)]="newRuleSourceCells" placeholder="e.g. 1, 2, 3, 4a" /> |
||||
|
</mat-form-field> |
||||
|
<button mat-stroked-button (click)="addAggregationRule()" [disabled]="!newRuleName || !newRuleSourceCells"> |
||||
|
<mat-icon>add</mat-icon> Add Rule |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div class="rule-actions"> |
||||
|
<button mat-flat-button color="primary" (click)="saveAggregationRules()" [disabled]="isSaving"> |
||||
|
Save Rules |
||||
|
</button> |
||||
|
</div> |
||||
|
</section> |
||||
|
} |
||||
|
</div> |
||||
@ -0,0 +1,159 @@ |
|||||
|
:host { |
||||
|
display: block; |
||||
|
} |
||||
|
|
||||
|
.container { |
||||
|
max-width: 960px; |
||||
|
margin: 0 auto; |
||||
|
padding: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
h1 { |
||||
|
margin-bottom: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
h2 { |
||||
|
margin-bottom: 1rem; |
||||
|
font-size: 1.25rem; |
||||
|
} |
||||
|
|
||||
|
// Alerts |
||||
|
|
||||
|
.alert { |
||||
|
padding: 0.75rem 1rem; |
||||
|
border-radius: 4px; |
||||
|
margin-bottom: 1rem; |
||||
|
font-size: 0.875rem; |
||||
|
} |
||||
|
|
||||
|
.alert-error { |
||||
|
background-color: #fdecea; |
||||
|
color: #b71c1c; |
||||
|
} |
||||
|
|
||||
|
.alert-success { |
||||
|
background-color: #e8f5e9; |
||||
|
color: #2e7d32; |
||||
|
} |
||||
|
|
||||
|
// Partnership Selector |
||||
|
|
||||
|
.partnership-selector { |
||||
|
margin-bottom: 1.5rem; |
||||
|
|
||||
|
mat-form-field { |
||||
|
width: 100%; |
||||
|
max-width: 400px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Cell Mappings |
||||
|
|
||||
|
.cell-mappings { |
||||
|
margin-bottom: 2rem; |
||||
|
} |
||||
|
|
||||
|
.mappings-table { |
||||
|
width: 100%; |
||||
|
margin-bottom: 1rem; |
||||
|
} |
||||
|
|
||||
|
.inline-edit { |
||||
|
width: 100%; |
||||
|
max-width: 200px; |
||||
|
} |
||||
|
|
||||
|
.custom-badge { |
||||
|
color: #f9a825; |
||||
|
font-size: 20px; |
||||
|
} |
||||
|
|
||||
|
.add-row { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 1rem; |
||||
|
margin-bottom: 1rem; |
||||
|
flex-wrap: wrap; |
||||
|
|
||||
|
mat-form-field { |
||||
|
flex: 0 0 auto; |
||||
|
width: 160px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.mapping-actions { |
||||
|
display: flex; |
||||
|
gap: 1rem; |
||||
|
margin-top: 0.5rem; |
||||
|
} |
||||
|
|
||||
|
// Aggregation Rules |
||||
|
|
||||
|
.aggregation-rules { |
||||
|
margin-bottom: 2rem; |
||||
|
} |
||||
|
|
||||
|
.empty-state { |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
font-style: italic; |
||||
|
margin-bottom: 1rem; |
||||
|
} |
||||
|
|
||||
|
.rule-card { |
||||
|
border: 1px solid rgba(0, 0, 0, 0.12); |
||||
|
border-radius: 4px; |
||||
|
padding: 0.75rem 1rem; |
||||
|
margin-bottom: 0.75rem; |
||||
|
} |
||||
|
|
||||
|
.rule-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 0.75rem; |
||||
|
|
||||
|
strong { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.rule-operation { |
||||
|
font-family: monospace; |
||||
|
font-size: 0.8rem; |
||||
|
background-color: #e8eaf6; |
||||
|
color: #283593; |
||||
|
padding: 2px 8px; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.rule-source-cells { |
||||
|
margin-top: 0.5rem; |
||||
|
font-size: 0.875rem; |
||||
|
color: rgba(0, 0, 0, 0.7); |
||||
|
} |
||||
|
|
||||
|
.cell-chip { |
||||
|
display: inline-block; |
||||
|
font-family: monospace; |
||||
|
font-size: 0.8rem; |
||||
|
background-color: #f5f5f5; |
||||
|
border: 1px solid rgba(0, 0, 0, 0.12); |
||||
|
border-radius: 12px; |
||||
|
padding: 2px 8px; |
||||
|
margin-left: 4px; |
||||
|
} |
||||
|
|
||||
|
.add-rule-row { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 1rem; |
||||
|
margin-bottom: 1rem; |
||||
|
flex-wrap: wrap; |
||||
|
|
||||
|
mat-form-field { |
||||
|
flex: 1 1 200px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.rule-actions { |
||||
|
margin-top: 0.5rem; |
||||
|
} |
||||
Loading…
Reference in new issue