feat(back-end): new stock db and back-office improvements

This commit is contained in:
2026-03-02 20:19:19 +01:00
parent 02e58ea00f
commit b7c399e3cb
39 changed files with 1605 additions and 257 deletions

View File

@@ -14,48 +14,171 @@
<div class="content" *ngIf="!loading; else loadingTpl">
<section class="panel">
<h3>Inserimento rapido</h3>
<div class="create-grid">
<section class="subpanel">
<h4>Nuovo materiale</h4>
<div class="form-grid">
<label class="form-field form-field--wide">
<span>Codice materiale</span>
<input type="text" [(ngModel)]="newMaterial.materialCode" placeholder="PLA, PETG, TPU..." />
</label>
<label class="form-field form-field--wide">
<span>Etichetta tecnico</span>
<input
type="text"
[(ngModel)]="newMaterial.technicalTypeLabel"
[disabled]="!newMaterial.isTechnical"
placeholder="alta temperatura, rinforzato..."
/>
</label>
<div class="panel-header">
<h3>Inserimento rapido</h3>
<button type="button" class="panel-toggle" (click)="toggleQuickInsertCollapsed()">
{{ quickInsertCollapsed ? 'Espandi' : 'Collassa' }}
</button>
</div>
<div *ngIf="!quickInsertCollapsed; else quickInsertCollapsedTpl">
<div class="create-grid">
<section class="subpanel">
<h4>Nuovo materiale</h4>
<div class="form-grid">
<label class="form-field form-field--wide">
<span>Codice materiale</span>
<input type="text" [(ngModel)]="newMaterial.materialCode" placeholder="PLA, PETG, TPU..." />
</label>
<label class="form-field form-field--wide">
<span>Etichetta tecnico</span>
<input
type="text"
[(ngModel)]="newMaterial.technicalTypeLabel"
[disabled]="!newMaterial.isTechnical"
placeholder="alta temperatura, rinforzato..."
/>
</label>
</div>
<div class="toggle-group">
<label class="toggle">
<input type="checkbox" [(ngModel)]="newMaterial.isFlexible" />
<span>Flessibile</span>
</label>
<label class="toggle">
<input type="checkbox" [(ngModel)]="newMaterial.isTechnical" />
<span>Tecnico</span>
</label>
</div>
<button type="button" (click)="createMaterial()" [disabled]="creatingMaterial">
{{ creatingMaterial ? 'Salvataggio...' : 'Aggiungi materiale' }}
</button>
</section>
<section class="subpanel">
<h4>Nuova variante</h4>
<div class="form-grid">
<label class="form-field">
<span>Materiale</span>
<select [(ngModel)]="newVariant.materialTypeId">
<option *ngFor="let material of materials; trackBy: trackById" [ngValue]="material.id">
{{ material.materialCode }}
</option>
</select>
</label>
<label class="form-field">
<span>Nome variante</span>
<input type="text" [(ngModel)]="newVariant.variantDisplayName" placeholder="PLA Nero Opaco BrandX" />
</label>
<label class="form-field">
<span>Colore</span>
<input type="text" [(ngModel)]="newVariant.colorName" placeholder="Nero, Bianco..." />
</label>
<label class="form-field">
<span>Hex colore</span>
<input type="text" [(ngModel)]="newVariant.colorHex" placeholder="#1A1A1A" />
</label>
<label class="form-field">
<span>Finitura</span>
<select [(ngModel)]="newVariant.finishType">
<option value="GLOSSY">GLOSSY</option>
<option value="MATTE">MATTE</option>
<option value="MARBLE">MARBLE</option>
<option value="SILK">SILK</option>
<option value="TRANSLUCENT">TRANSLUCENT</option>
<option value="SPECIAL">SPECIAL</option>
</select>
</label>
<label class="form-field">
<span>Brand</span>
<input type="text" [(ngModel)]="newVariant.brand" placeholder="Bambu, SUNLU..." />
</label>
<label class="form-field">
<span>Costo CHF/kg</span>
<input type="number" step="0.01" min="0" [(ngModel)]="newVariant.costChfPerKg" />
</label>
<label class="form-field">
<span>Stock spool</span>
<input type="number" step="0.001" min="0" max="999.999" [(ngModel)]="newVariant.stockSpools" />
</label>
<label class="form-field">
<span>Spool netto kg</span>
<input type="number" step="0.001" min="0.001" max="999.999" [(ngModel)]="newVariant.spoolNetKg" />
</label>
</div>
<div class="toggle-group">
<label class="toggle">
<input type="checkbox" [(ngModel)]="newVariant.isMatte" />
<span>Matte</span>
</label>
<label class="toggle">
<input type="checkbox" [(ngModel)]="newVariant.isSpecial" />
<span>Special</span>
</label>
<label class="toggle">
<input type="checkbox" [(ngModel)]="newVariant.isActive" />
<span>Attiva</span>
</label>
</div>
<p class="variant-meta">
Stock spools: <strong>{{ newVariant.stockSpools | number:'1.0-3' }}</strong> |
Filamento totale: <strong>{{ computeStockFilamentGrams(newVariant.stockSpools, newVariant.spoolNetKg) | number:'1.0-0' }} g</strong>
</p>
<button type="button" (click)="createVariant()" [disabled]="creatingVariant || !materials.length">
{{ creatingVariant ? 'Salvataggio...' : 'Aggiungi variante' }}
</button>
</section>
</div>
</div>
</section>
<section class="panel">
<h3>Varianti filamento</h3>
<div class="variant-list">
<article class="variant-row" *ngFor="let variant of variants; trackBy: trackById">
<div class="variant-header">
<button
type="button"
class="expand-toggle"
(click)="toggleVariantExpanded(variant.id)"
[attr.aria-expanded]="isVariantExpanded(variant.id)">
{{ isVariantExpanded(variant.id) ? '▾' : '▸' }}
</button>
<div class="variant-head-main">
<strong>{{ variant.variantDisplayName }}</strong>
<div class="variant-collapsed-summary" *ngIf="!isVariantExpanded(variant.id)">
<span class="color-summary">
<span class="color-dot" [style.background-color]="getVariantColorHex(variant)"></span>
{{ variant.colorName || 'N/D' }}
</span>
<span>Stock spools: {{ variant.stockSpools | number:'1.0-3' }}</span>
<span>Filamento: {{ computeStockFilamentGrams(variant.stockSpools, variant.spoolNetKg) | number:'1.0-0' }} g</span>
</div>
</div>
<div class="variant-head-actions">
<span class="badge low" *ngIf="isLowStock(variant)">Stock basso</span>
<span class="badge ok" *ngIf="!isLowStock(variant)">Stock ok</span>
<button
type="button"
class="btn-delete"
(click)="openDeleteVariant(variant)"
[disabled]="deletingVariantIds.has(variant.id)">
{{ deletingVariantIds.has(variant.id) ? 'Eliminazione...' : 'Elimina' }}
</button>
</div>
</div>
<div class="toggle-group">
<label class="toggle">
<input type="checkbox" [(ngModel)]="newMaterial.isFlexible" />
<span>Flessibile</span>
</label>
<label class="toggle">
<input type="checkbox" [(ngModel)]="newMaterial.isTechnical" />
<span>Tecnico</span>
</label>
</div>
<button type="button" (click)="createMaterial()" [disabled]="creatingMaterial">
{{ creatingMaterial ? 'Salvataggio...' : 'Aggiungi materiale' }}
</button>
</section>
<section class="subpanel">
<h4>Nuova variante</h4>
<div class="form-grid">
<div class="form-grid" *ngIf="isVariantExpanded(variant.id)">
<label class="form-field">
<span>Materiale</span>
<select [(ngModel)]="newVariant.materialTypeId">
<select [(ngModel)]="variant.materialTypeId">
<option *ngFor="let material of materials; trackBy: trackById" [ngValue]="material.id">
{{ material.materialCode }}
</option>
@@ -63,50 +186,75 @@
</label>
<label class="form-field">
<span>Nome variante</span>
<input type="text" [(ngModel)]="newVariant.variantDisplayName" placeholder="PLA Nero Opaco BrandX" />
<input type="text" [(ngModel)]="variant.variantDisplayName" />
</label>
<label class="form-field">
<span>Colore</span>
<input type="text" [(ngModel)]="newVariant.colorName" placeholder="Nero, Bianco..." />
<input type="text" [(ngModel)]="variant.colorName" />
</label>
<label class="form-field">
<span>Hex colore</span>
<input type="text" [(ngModel)]="variant.colorHex" />
</label>
<label class="form-field">
<span>Finitura</span>
<select [(ngModel)]="variant.finishType">
<option value="GLOSSY">GLOSSY</option>
<option value="MATTE">MATTE</option>
<option value="MARBLE">MARBLE</option>
<option value="SILK">SILK</option>
<option value="TRANSLUCENT">TRANSLUCENT</option>
<option value="SPECIAL">SPECIAL</option>
</select>
</label>
<label class="form-field">
<span>Brand</span>
<input type="text" [(ngModel)]="variant.brand" />
</label>
<label class="form-field">
<span>Costo CHF/kg</span>
<input type="number" step="0.01" min="0" [(ngModel)]="newVariant.costChfPerKg" />
<input type="number" step="0.01" min="0" [(ngModel)]="variant.costChfPerKg" />
</label>
<label class="form-field">
<span>Stock spool</span>
<input type="number" step="0.001" min="0" max="999.999" [(ngModel)]="newVariant.stockSpools" />
<input type="number" step="0.001" min="0" max="999.999" [(ngModel)]="variant.stockSpools" />
</label>
<label class="form-field">
<span>Spool netto kg</span>
<input type="number" step="0.001" min="0.001" max="999.999" [(ngModel)]="newVariant.spoolNetKg" />
<input type="number" step="0.001" min="0.001" max="999.999" [(ngModel)]="variant.spoolNetKg" />
</label>
</div>
<div class="toggle-group">
<div class="toggle-group" *ngIf="isVariantExpanded(variant.id)">
<label class="toggle">
<input type="checkbox" [(ngModel)]="newVariant.isMatte" />
<input type="checkbox" [(ngModel)]="variant.isMatte" />
<span>Matte</span>
</label>
<label class="toggle">
<input type="checkbox" [(ngModel)]="newVariant.isSpecial" />
<input type="checkbox" [(ngModel)]="variant.isSpecial" />
<span>Special</span>
</label>
<label class="toggle">
<input type="checkbox" [(ngModel)]="newVariant.isActive" />
<input type="checkbox" [(ngModel)]="variant.isActive" />
<span>Attiva</span>
</label>
</div>
<p class="variant-meta">
Stock stimato: <strong>{{ computeStockKg(newVariant.stockSpools, newVariant.spoolNetKg) | number:'1.0-3' }} kg</strong>
<p class="variant-meta" *ngIf="isVariantExpanded(variant.id)">
Stock spools: <strong>{{ variant.stockSpools | number:'1.0-3' }}</strong> |
Filamento totale: <strong>{{ computeStockFilamentGrams(variant.stockSpools, variant.spoolNetKg) | number:'1.0-0' }} g</strong>
</p>
<button type="button" (click)="createVariant()" [disabled]="creatingVariant || !materials.length">
{{ creatingVariant ? 'Salvataggio...' : 'Aggiungi variante' }}
<button
type="button"
*ngIf="isVariantExpanded(variant.id)"
(click)="saveVariant(variant)"
[disabled]="savingVariantIds.has(variant.id)">
{{ savingVariantIds.has(variant.id) ? 'Salvataggio...' : 'Salva variante' }}
</button>
</section>
</article>
</div>
<p class="muted" *ngIf="variants.length === 0">Nessuna variante configurata.</p>
</section>
<section class="panel">
@@ -150,74 +298,6 @@
<p class="muted" *ngIf="materials.length === 0">Nessun materiale configurato.</p>
</div>
</section>
<section class="panel">
<h3>Varianti filamento</h3>
<div class="variant-grid">
<article class="variant-card" *ngFor="let variant of variants; trackBy: trackById">
<div class="variant-header">
<strong>{{ variant.variantDisplayName }}</strong>
<span class="badge low" *ngIf="isLowStock(variant)">Stock basso</span>
<span class="badge ok" *ngIf="!isLowStock(variant)">Stock ok</span>
</div>
<div class="form-grid">
<label class="form-field">
<span>Materiale</span>
<select [(ngModel)]="variant.materialTypeId">
<option *ngFor="let material of materials; trackBy: trackById" [ngValue]="material.id">
{{ material.materialCode }}
</option>
</select>
</label>
<label class="form-field">
<span>Nome variante</span>
<input type="text" [(ngModel)]="variant.variantDisplayName" />
</label>
<label class="form-field">
<span>Colore</span>
<input type="text" [(ngModel)]="variant.colorName" />
</label>
<label class="form-field">
<span>Costo CHF/kg</span>
<input type="number" step="0.01" min="0" [(ngModel)]="variant.costChfPerKg" />
</label>
<label class="form-field">
<span>Stock spool</span>
<input type="number" step="0.001" min="0" max="999.999" [(ngModel)]="variant.stockSpools" />
</label>
<label class="form-field">
<span>Spool netto kg</span>
<input type="number" step="0.001" min="0.001" max="999.999" [(ngModel)]="variant.spoolNetKg" />
</label>
</div>
<div class="toggle-group">
<label class="toggle">
<input type="checkbox" [(ngModel)]="variant.isMatte" />
<span>Matte</span>
</label>
<label class="toggle">
<input type="checkbox" [(ngModel)]="variant.isSpecial" />
<span>Special</span>
</label>
<label class="toggle">
<input type="checkbox" [(ngModel)]="variant.isActive" />
<span>Attiva</span>
</label>
</div>
<p class="variant-meta">
Totale stimato: <strong>{{ computeStockKg(variant.stockSpools, variant.spoolNetKg) | number:'1.0-3' }} kg</strong>
</p>
<button type="button" (click)="saveVariant(variant)" [disabled]="savingVariantIds.has(variant.id)">
{{ savingVariantIds.has(variant.id) ? 'Salvataggio...' : 'Salva variante' }}
</button>
</article>
</div>
<p class="muted" *ngIf="variants.length === 0">Nessuna variante configurata.</p>
</section>
</div>
</section>
@@ -228,3 +308,24 @@
<ng-template #materialsCollapsedTpl>
<p class="muted">Sezione collassata ({{ materials.length }} materiali).</p>
</ng-template>
<ng-template #quickInsertCollapsedTpl>
<p class="muted">Sezione collassata.</p>
</ng-template>
<div class="dialog-backdrop" *ngIf="variantToDelete" (click)="closeDeleteVariantDialog()"></div>
<div class="confirm-dialog" *ngIf="variantToDelete">
<h4>Sei sicuro?</h4>
<p>Vuoi eliminare la variante <strong>{{ variantToDelete?.variantDisplayName }}</strong>?</p>
<p class="muted">L'operazione non è reversibile.</p>
<div class="dialog-actions">
<button type="button" class="btn-secondary" (click)="closeDeleteVariantDialog()">Annulla</button>
<button
type="button"
class="btn-delete"
(click)="confirmDeleteVariant()"
[disabled]="variantToDelete && deletingVariantIds.has(variantToDelete.id)">
{{ variantToDelete && deletingVariantIds.has(variantToDelete.id) ? 'Eliminazione...' : 'Conferma elimina' }}
</button>
</div>
</div>

View File

@@ -141,24 +141,30 @@ select:disabled {
}
.material-grid,
.variant-grid {
.variant-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: var(--space-3);
}
.material-card,
.variant-card {
.variant-row {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-neutral-100);
padding: var(--space-3);
}
.material-grid {
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
}
.variant-list {
grid-template-columns: 1fr;
}
.variant-header {
display: flex;
justify-content: space-between;
align-items: center;
align-items: flex-start;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
@@ -167,6 +173,57 @@ select:disabled {
font-size: 1rem;
}
.variant-head-main {
display: grid;
gap: var(--space-1);
flex: 1;
min-width: 0;
}
.variant-collapsed-summary {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
color: var(--color-text-muted);
font-size: 0.92rem;
}
.color-summary {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.color-dot {
width: 14px;
height: 14px;
border-radius: 50%;
border: 1px solid var(--color-border);
display: inline-block;
}
.variant-head-actions {
display: inline-flex;
align-items: center;
gap: var(--space-2);
}
.expand-toggle {
min-width: 34px;
height: 34px;
padding: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
color: var(--color-text);
font-size: 1rem;
line-height: 1;
}
.expand-toggle:hover:not(:disabled) {
background: var(--color-neutral-100);
}
.variant-meta {
margin: 0 0 var(--space-3);
font-size: 0.9rem;
@@ -203,6 +260,25 @@ button:disabled {
background: var(--color-neutral-100);
}
.btn-delete {
background: #dc3545;
color: #ffffff;
}
.btn-delete:hover:not(:disabled) {
background: #bb2d3b;
}
.btn-secondary {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
color: var(--color-text);
}
.btn-secondary:hover:not(:disabled) {
background: var(--color-neutral-100);
}
.badge {
display: inline-block;
border-radius: 999px;
@@ -236,6 +312,43 @@ button:disabled {
color: var(--color-text-muted);
}
.dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.32);
z-index: 1100;
}
.confirm-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(460px, calc(100vw - 2rem));
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: var(--space-4);
z-index: 1101;
display: grid;
gap: var(--space-3);
}
.confirm-dialog h4 {
margin: 0;
}
.confirm-dialog p {
margin: 0;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
}
@media (max-width: 1080px) {
.create-grid {
grid-template-columns: 1fr;

View File

@@ -9,6 +9,7 @@ import {
AdminUpsertFilamentVariantPayload
} from '../services/admin-operations.service';
import { forkJoin } from 'rxjs';
import { getColorHex } from '../../../core/constants/colors.const';
@Component({
selector: 'app-admin-filament-stock',
@@ -23,11 +24,15 @@ export class AdminFilamentStockComponent implements OnInit {
materials: AdminFilamentMaterialType[] = [];
variants: AdminFilamentVariant[] = [];
loading = false;
quickInsertCollapsed = false;
materialsCollapsed = true;
creatingMaterial = false;
creatingVariant = false;
savingMaterialIds = new Set<number>();
savingVariantIds = new Set<number>();
deletingVariantIds = new Set<number>();
expandedVariantIds = new Set<number>();
variantToDelete: AdminFilamentVariant | null = null;
errorMessage: string | null = null;
successMessage: string | null = null;
@@ -42,6 +47,9 @@ export class AdminFilamentStockComponent implements OnInit {
materialTypeId: 0,
variantDisplayName: '',
colorName: '',
colorHex: '',
finishType: 'GLOSSY',
brand: '',
isMatte: false,
isSpecial: false,
costChfPerKg: 0,
@@ -66,6 +74,12 @@ export class AdminFilamentStockComponent implements OnInit {
next: ({ materials, variants }) => {
this.materials = this.sortMaterials(materials);
this.variants = this.sortVariants(variants);
const existingIds = new Set(this.variants.map(v => v.id));
this.expandedVariantIds.forEach(id => {
if (!existingIds.has(id)) {
this.expandedVariantIds.delete(id);
}
});
if (!this.newVariant.materialTypeId && this.materials.length > 0) {
this.newVariant.materialTypeId = this.materials[0].id;
}
@@ -178,6 +192,9 @@ export class AdminFilamentStockComponent implements OnInit {
materialTypeId: this.newVariant.materialTypeId || this.materials[0]?.id || 0,
variantDisplayName: '',
colorName: '',
colorHex: '',
finishType: 'GLOSSY',
brand: '',
isMatte: false,
isSpecial: false,
costChfPerKg: 0,
@@ -221,7 +238,7 @@ export class AdminFilamentStockComponent implements OnInit {
}
isLowStock(variant: AdminFilamentVariant): boolean {
return this.computeStockKg(variant.stockSpools, variant.spoolNetKg) < 1;
return this.computeStockFilamentGrams(variant.stockSpools, variant.spoolNetKg) < 1000;
}
computeStockKg(stockSpools?: number, spoolNetKg?: number): number {
@@ -234,19 +251,82 @@ export class AdminFilamentStockComponent implements OnInit {
return spools * netKg;
}
computeStockFilamentGrams(stockSpools?: number, spoolNetKg?: number): number {
return this.computeStockKg(stockSpools, spoolNetKg) * 1000;
}
trackById(index: number, item: { id: number }): number {
return item.id;
}
isVariantExpanded(variantId: number): boolean {
return this.expandedVariantIds.has(variantId);
}
toggleVariantExpanded(variantId: number): void {
if (this.expandedVariantIds.has(variantId)) {
this.expandedVariantIds.delete(variantId);
return;
}
this.expandedVariantIds.add(variantId);
}
getVariantColorHex(variant: AdminFilamentVariant): string {
if (variant.colorHex && variant.colorHex.trim().length > 0) {
return variant.colorHex;
}
return getColorHex(variant.colorName || '');
}
openDeleteVariant(variant: AdminFilamentVariant): void {
this.variantToDelete = variant;
}
closeDeleteVariantDialog(): void {
this.variantToDelete = null;
}
confirmDeleteVariant(): void {
const variant = this.variantToDelete;
if (!variant || this.deletingVariantIds.has(variant.id)) {
return;
}
this.errorMessage = null;
this.successMessage = null;
this.deletingVariantIds.add(variant.id);
this.adminOperationsService.deleteFilamentVariant(variant.id).subscribe({
next: () => {
this.variants = this.variants.filter(v => v.id !== variant.id);
this.expandedVariantIds.delete(variant.id);
this.deletingVariantIds.delete(variant.id);
this.variantToDelete = null;
this.successMessage = 'Variante eliminata.';
},
error: (err) => {
this.deletingVariantIds.delete(variant.id);
this.errorMessage = this.extractErrorMessage(err, 'Eliminazione variante non riuscita.');
}
});
}
toggleMaterialsCollapsed(): void {
this.materialsCollapsed = !this.materialsCollapsed;
}
toggleQuickInsertCollapsed(): void {
this.quickInsertCollapsed = !this.quickInsertCollapsed;
}
private toVariantPayload(source: AdminUpsertFilamentVariantPayload | AdminFilamentVariant): AdminUpsertFilamentVariantPayload {
return {
materialTypeId: Number(source.materialTypeId),
variantDisplayName: (source.variantDisplayName || '').trim(),
colorName: (source.colorName || '').trim(),
colorHex: (source.colorHex || '').trim() || undefined,
finishType: (source.finishType || 'GLOSSY').trim().toUpperCase(),
brand: (source.brand || '').trim() || undefined,
isMatte: !!source.isMatte,
isSpecial: !!source.isSpecial,
costChfPerKg: Number(source.costChfPerKg ?? 0),

View File

@@ -11,6 +11,7 @@ export interface AdminFilamentStockRow {
stockSpools: number;
spoolNetKg: number;
stockKg: number;
stockFilamentGrams: number;
active: boolean;
}
@@ -31,12 +32,16 @@ export interface AdminFilamentVariant {
materialTechnicalTypeLabel?: string;
variantDisplayName: string;
colorName: string;
colorHex?: string;
finishType?: string;
brand?: string;
isMatte: boolean;
isSpecial: boolean;
costChfPerKg: number;
stockSpools: number;
spoolNetKg: number;
stockKg: number;
stockFilamentGrams: number;
isActive: boolean;
createdAt: string;
}
@@ -52,6 +57,9 @@ export interface AdminUpsertFilamentVariantPayload {
materialTypeId: number;
variantDisplayName: string;
colorName: string;
colorHex?: string;
finishType?: string;
brand?: string;
isMatte: boolean;
isSpecial: boolean;
costChfPerKg: number;
@@ -167,6 +175,10 @@ export class AdminOperationsService {
return this.http.put<AdminFilamentVariant>(`${this.baseUrl}/filaments/variants/${variantId}`, payload, { withCredentials: true });
}
deleteFilamentVariant(variantId: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/filaments/variants/${variantId}`, { withCredentials: true });
}
getContactRequests(): Observable<AdminContactRequest[]> {
return this.http.get<AdminContactRequest[]>(`${this.baseUrl}/contact-requests`, { withCredentials: true });
}