FaKrosnoAngular

* Added project
This commit is contained in:
2026-08-08 13:39:17 +02:00
parent 0a7399f015
commit 89d74428b2
73 changed files with 15267 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { DatePipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TableModule } from 'primeng/table';
import { ButtonDirective } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { DialogModule } from 'primeng/dialog';
import { ConfirmationService } from 'primeng/api';
import { TaskSchedulerDto } from '../../core/models';
import { HangfireService } from '../../core/services/hangfire.service';
import { generateGuid } from '../../shared/utils/business-logic';
import { PageShellComponent } from '../../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../../shared/components/content-section/content-section.component';
@Component({
selector: 'app-scheduler',
standalone: true,
imports: [TableModule, ButtonDirective, InputTextModule, DialogModule, FormsModule, DatePipe, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
title="Zarządzanie Zadaniami"
subtitle="Konfiguracja zadań harmonogramu Hangfire"
icon="pi pi-clock"
>
<app-content-section title="Zadania harmonogramu" icon="pi pi-calendar" [flush]="true">
<div headerActions>
<button pButton type="button" (click)="startAdd()"><i class="pi pi-plus"></i> Dodaj zadanie</button>
</div>
<p-table [value]="tasks()" [paginator]="true" [rows]="10" dataKey="rowPointer" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr><th>Nazwa</th><th>Ścieżka</th><th>CRON</th><th>Ostatnie Uruchomienie</th><th>Następne Uruchomienie</th><th></th></tr>
</ng-template>
<ng-template #body let-task>
<tr>
<td>{{ task.name }}</td><td>{{ task.path }}</td><td>{{ task.cronOptions }}</td>
<td>{{ task.lastExecution | date: 'dd.MM.yyyy HH:mm' }}</td><td>{{ task.nextExecution | date: 'dd.MM.yyyy HH:mm' }}</td>
<td>
<button pButton type="button" [text]="true" [iconOnly]="true" (click)="startEdit(task)"><i class="pi pi-pencil"></i></button>
<button pButton type="button" severity="danger" [text]="true" [iconOnly]="true" (click)="confirmDelete(task)"><i class="pi pi-trash"></i></button>
</td>
</tr>
</ng-template>
</p-table>
</app-content-section>
</app-page-shell>
<p-dialog header="Zadanie" [(visible)]="showDialog" [modal]="true" [style]="{ width: '520px' }">
<div class="field-group"><label class="field-label">Nazwa</label><input pInputText class="w-full" [(ngModel)]="editTask.name" /></div>
<div class="field-group"><label class="field-label">Ścieżka</label><input pInputText class="w-full" [(ngModel)]="editTask.path" /></div>
<div class="field-group"><label class="field-label">CRON</label><input pInputText class="w-full" [(ngModel)]="editTask.cronOptions" /></div>
<ng-template #footer>
<button pButton type="button" (click)="save()">Zapisz</button>
<button pButton type="button" severity="secondary" (click)="showDialog = false">Anuluj</button>
</ng-template>
</p-dialog>
`,
})
export class SchedulerComponent implements OnInit {
private readonly hangfireService = inject(HangfireService);
private readonly confirmation = inject(ConfirmationService);
readonly tasks = signal<TaskSchedulerDto[]>([]);
showDialog = false;
isNew = false;
editTask: TaskSchedulerDto = this.emptyTask();
async ngOnInit(): Promise<void> { await this.load(); }
startAdd(): void {
this.isNew = true;
this.editTask = this.emptyTask();
this.editTask.rowPointer = generateGuid();
this.showDialog = true;
}
startEdit(task: TaskSchedulerDto): void {
this.isNew = false;
this.editTask = { ...task, taskSchedulerDetails: [...(task.taskSchedulerDetails ?? [])] };
this.showDialog = true;
}
async save(): Promise<void> {
if (this.isNew) await this.hangfireService.add(this.editTask);
else await this.hangfireService.update(this.editTask);
this.showDialog = false;
await this.load();
}
confirmDelete(task: TaskSchedulerDto): void {
this.confirmation.confirm({
message: `Usunąć zadanie ${task.name}?`,
accept: async () => { await this.hangfireService.delete(task); await this.load(); },
});
}
private emptyTask(): TaskSchedulerDto {
return {
id: 0, rowPointer: generateGuid(), name: '', path: '', cronOptions: '',
createDate: new Date().toISOString(), activeFrom: new Date().toISOString(), taskSchedulerDetails: [],
};
}
private async load(): Promise<void> { this.tasks.set(await this.hangfireService.getAll()); }
}

View File

@@ -0,0 +1,228 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { DatePipe } from '@angular/common';
import { TableModule } from 'primeng/table';
import { ButtonDirective } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { DialogModule } from 'primeng/dialog';
import { CheckboxModule } from 'primeng/checkbox';
import { ConfirmationService } from 'primeng/api';
import * as bcrypt from 'bcryptjs';
import { FunctionDto, RoleDto, UserDto } from '../../core/models';
import { UserService } from '../../core/services/user.service';
import { RoleService } from '../../core/services/role.service';
import { FunctionService } from '../../core/services/function.service';
import { generateGuid, generateTempPassword } from '../../shared/utils/business-logic';
import { PageShellComponent } from '../../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../../shared/components/content-section/content-section.component';
@Component({
selector: 'app-users-manager',
standalone: true,
imports: [TableModule, ButtonDirective, InputTextModule, DialogModule, CheckboxModule, FormsModule, DatePipe, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
title="Zarządzanie Użytkownikami i Rolami"
subtitle="Administracja kont użytkowników, ról i uprawnień systemowych"
icon="pi pi-users"
>
<app-content-section title="Użytkownicy" icon="pi pi-user" [flush]="true">
<div headerActions>
<button pButton type="button" (click)="startAddUser()"><i class="pi pi-plus"></i> Dodaj użytkownika</button>
</div>
<p-table [value]="users()" [paginator]="true" [rows]="10" dataKey="rowPointer" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr><th>Login</th><th>Email</th><th>Imię</th><th>Nazwisko</th><th>Aktywny</th><th>Utworzono</th><th></th></tr>
</ng-template>
<ng-template #body let-user>
<tr>
<td>{{ user.login }}</td><td>{{ user.email }}</td><td>{{ user.firstName }}</td><td>{{ user.lastName }}</td>
<td>{{ user.isActive ? 'Tak' : 'Nie' }}</td><td>{{ user.createdDate | date: 'dd.MM.yyyy' }}</td>
<td>
<button pButton type="button" [text]="true" [iconOnly]="true" (click)="startEditUser(user)"><i class="pi pi-pencil"></i></button>
<button pButton type="button" [text]="true" (click)="resetPassword(user)"><i class="pi pi-key"></i> Zresetuj haslo</button>
<button pButton type="button" severity="danger" [text]="true" [iconOnly]="true" (click)="confirmDeleteUser(user)"><i class="pi pi-trash"></i></button>
</td>
</tr>
</ng-template>
</p-table>
</app-content-section>
<app-content-section title="Role" icon="pi pi-shield" [flush]="true">
<div headerActions>
<button pButton type="button" (click)="startAddRole()"><i class="pi pi-plus"></i> Dodaj rolę</button>
</div>
<p-table [value]="roles()" [paginator]="true" [rows]="10" dataKey="id" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header><tr><th>ID</th><th>Nazwa</th><th></th></tr></ng-template>
<ng-template #body let-role>
<tr>
<td>{{ role.id }}</td><td>{{ role.name }}</td>
<td>
<button pButton type="button" [text]="true" [iconOnly]="true" (click)="startEditRole(role)"><i class="pi pi-pencil"></i></button>
<button pButton type="button" severity="danger" [text]="true" [iconOnly]="true" (click)="confirmDeleteRole(role)"><i class="pi pi-trash"></i></button>
</td>
</tr>
</ng-template>
</p-table>
</app-content-section>
<app-content-section title="Funkcje" icon="pi pi-cog" [flush]="true">
<div headerActions>
<button pButton type="button" (click)="startAddFunction()"><i class="pi pi-plus"></i> Dodaj funkcję</button>
</div>
<p-table [value]="functions()" [paginator]="true" [rows]="10" dataKey="id" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header><tr><th>ID</th><th>ID Roli</th><th>Nazwa Funkcji</th><th></th></tr></ng-template>
<ng-template #body let-fn>
<tr>
<td>{{ fn.id }}</td><td>{{ fn.roleId }}</td><td>{{ fn.name }}</td>
<td>
<button pButton type="button" [text]="true" [iconOnly]="true" (click)="startEditFunction(fn)"><i class="pi pi-pencil"></i></button>
<button pButton type="button" severity="danger" [text]="true" [iconOnly]="true" (click)="confirmDeleteFunction(fn)"><i class="pi pi-trash"></i></button>
</td>
</tr>
</ng-template>
</p-table>
</app-content-section>
</app-page-shell>
<p-dialog header="Użytkownik" [(visible)]="showUserDialog" [modal]="true" [style]="{ width: '480px' }">
<div class="field-group"><label class="field-label">Login</label><input pInputText class="w-full" [(ngModel)]="editUser.login" /></div>
<div class="field-group"><label class="field-label">Email</label><input pInputText class="w-full" [(ngModel)]="editUser.email" /></div>
<div class="field-group"><label class="field-label">Imię</label><input pInputText class="w-full" [(ngModel)]="editUser.firstName" /></div>
<div class="field-group"><label class="field-label">Nazwisko</label><input pInputText class="w-full" [(ngModel)]="editUser.lastName" /></div>
<div class="field-group"><p-checkbox [(ngModel)]="editUser.isActive" binary label="Aktywny" /></div>
<ng-template #footer>
<button pButton type="button" (click)="saveUser()">Zapisz</button>
<button pButton type="button" severity="secondary" (click)="showUserDialog = false">Anuluj</button>
</ng-template>
</p-dialog>
<p-dialog header="Rola" [(visible)]="showRoleDialog" [modal]="true" [style]="{ width: '400px' }">
<div class="field-group"><label class="field-label">Nazwa</label><input pInputText class="w-full" [(ngModel)]="editRole.name" /></div>
<ng-template #footer><button pButton type="button" (click)="saveRole()">Zapisz</button><button pButton type="button" severity="secondary" (click)="showRoleDialog = false">Anuluj</button></ng-template>
</p-dialog>
<p-dialog header="Funkcja" [(visible)]="showFunctionDialog" [modal]="true" [style]="{ width: '400px' }">
<div class="field-group"><label class="field-label">ID Roli</label><input pInputText type="number" class="w-full" [(ngModel)]="editFunction.roleId" /></div>
<div class="field-group"><label class="field-label">Nazwa Funkcji</label><input pInputText class="w-full" [(ngModel)]="editFunction.name" /></div>
<ng-template #footer><button pButton type="button" (click)="saveFunction()">Zapisz</button><button pButton type="button" severity="secondary" (click)="showFunctionDialog = false">Anuluj</button></ng-template>
</p-dialog>
<p-dialog header="Dodano użytkownika!" [(visible)]="showTempPasswordDialog" [modal]="true" [style]="{ width: '500px' }">
<p>Użytkownik <strong>{{ tempLogin }}</strong> został dodany pomyślnie!</p>
<p>Hasło tymczasowe: <strong>{{ tempPassword }}</strong></p>
<ng-template #footer><button pButton type="button" (click)="showTempPasswordDialog = false">OK</button></ng-template>
</p-dialog>
`,
})
export class UsersManagerComponent implements OnInit {
private readonly userService = inject(UserService);
private readonly roleService = inject(RoleService);
private readonly functionService = inject(FunctionService);
private readonly confirmation = inject(ConfirmationService);
readonly users = signal<UserDto[]>([]);
readonly roles = signal<RoleDto[]>([]);
readonly functions = signal<FunctionDto[]>([]);
showUserDialog = false;
showRoleDialog = false;
showFunctionDialog = false;
showTempPasswordDialog = false;
tempLogin = '';
tempPassword = '';
editUser: UserDto = this.emptyUser();
editRole: RoleDto = { id: 0, name: '', rowPointer: '' };
editFunction: FunctionDto = { id: 0, roleId: 0, name: '', rowPointer: '' };
isNewUser = false;
async ngOnInit(): Promise<void> {
await Promise.all([this.loadUsers(), this.loadRoles(), this.loadFunctions()]);
}
startAddUser(): void { this.isNewUser = true; this.editUser = this.emptyUser(); this.showUserDialog = true; }
startEditUser(user: UserDto): void { this.isNewUser = false; this.editUser = { ...user }; this.showUserDialog = true; }
startAddRole(): void { this.editRole = { id: 0, name: '', rowPointer: generateGuid() }; this.showRoleDialog = true; }
startEditRole(role: RoleDto): void { this.editRole = { ...role }; this.showRoleDialog = true; }
startAddFunction(): void { this.editFunction = { id: 0, roleId: 0, name: '', rowPointer: generateGuid() }; this.showFunctionDialog = true; }
startEditFunction(fn: FunctionDto): void { this.editFunction = { ...fn }; this.showFunctionDialog = true; }
async saveUser(): Promise<void> {
if (this.isNewUser) {
const temp = generateTempPassword();
this.editUser.passwordHash = bcrypt.hashSync(temp, 10);
this.editUser.isTemporaryPassword = true;
this.editUser.rowPointer = generateGuid();
this.editUser.createdDate = new Date().toISOString();
this.editUser.activeFrom = new Date().toISOString();
this.editUser.isActive = true;
await this.userService.create(this.editUser);
this.tempLogin = this.editUser.login;
this.tempPassword = temp;
this.showTempPasswordDialog = true;
} else {
await this.userService.update(this.editUser);
}
this.showUserDialog = false;
await this.loadUsers();
}
async resetPassword(user: UserDto): Promise<void> {
const temp = generateTempPassword();
user.passwordHash = bcrypt.hashSync(temp, 10);
user.isTemporaryPassword = true;
await this.userService.update(user);
this.tempLogin = user.login;
this.tempPassword = temp;
this.showTempPasswordDialog = true;
await this.loadUsers();
}
async saveRole(): Promise<void> {
if (this.editRole.id === 0) await this.roleService.create(this.editRole);
else await this.roleService.update(this.editRole);
this.showRoleDialog = false;
await this.loadRoles();
}
async saveFunction(): Promise<void> {
if (this.editFunction.id === 0) await this.functionService.create(this.editFunction);
else await this.functionService.update(this.editFunction);
this.showFunctionDialog = false;
await this.loadFunctions();
}
confirmDeleteUser(user: UserDto): void {
this.confirmation.confirm({
message: `Usunąć użytkownika ${user.login}?`,
accept: async () => { await this.userService.delete(user.rowPointer); await this.loadUsers(); },
});
}
confirmDeleteRole(role: RoleDto): void {
this.confirmation.confirm({
message: `Usunąć rolę ${role.name}?`,
accept: async () => { await this.roleService.delete(role.rowPointer); await this.loadRoles(); },
});
}
confirmDeleteFunction(fn: FunctionDto): void {
this.confirmation.confirm({
message: `Usunąć funkcję ${fn.name}?`,
accept: async () => { await this.functionService.delete(fn.rowPointer); await this.loadFunctions(); },
});
}
private emptyUser(): UserDto {
return {
id: 0, login: '', passwordHash: '', isTemporaryPassword: false, isActive: true,
email: '', firstName: '', lastName: '', createdDate: new Date().toISOString(),
failedLoginAttempts: 0, isLocked: false, rowPointer: generateGuid(), userRoles: [],
};
}
private async loadUsers(): Promise<void> { this.users.set(await this.userService.getAll()); }
private async loadRoles(): Promise<void> { this.roles.set(await this.roleService.getAll()); }
private async loadFunctions(): Promise<void> { this.functions.set(await this.functionService.getAll()); }
}

View File

@@ -0,0 +1,59 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { providePrimeNG } from 'primeng/config';
import { MessageService, ConfirmationService } from 'primeng/api';
import { definePreset } from '@primeuix/themes';
import Aura from '@primeuix/themes/aura';
import { routes } from './app.routes';
import { authInterceptor } from './core/interceptors/auth.interceptor';
import { errorInterceptor } from './core/interceptors/error.interceptor';
import { environment } from '../environments/environment';
const FaPreset = definePreset(Aura, {
semantic: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#1a56db',
600: '#1648b8',
700: '#123a94',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
},
borderRadius: {
none: '0',
xs: '2px',
sm: '4px',
md: '6px',
lg: '8px',
xl: '10px',
},
});
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
provideAnimationsAsync(),
providePrimeNG({
license: environment.primeNgLicense || undefined,
theme: {
preset: FaPreset,
options: {
darkModeSelector: '[data-theme="dark"]',
},
},
}),
MessageService,
ConfirmationService,
],
};

View File

@@ -0,0 +1,32 @@
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./layout/main-layout/main-layout.component').then((m) => m.MainLayoutComponent),
children: [
{ path: '', loadComponent: () => import('./schedule-orders/schedule-orders-list.component').then((m) => m.ScheduleOrdersListComponent) },
{ path: 'Products', loadComponent: () => import('./products/products-list.component').then((m) => m.ProductsListComponent) },
{ path: 'Warehouse', loadComponent: () => import('./warehouse/warehouse.component').then((m) => m.WarehouseComponent) },
{ path: 'ScheduleOrder/:scheduleOrderId', loadComponent: () => import('./schedule-order/schedule-order-detail.component').then((m) => m.ScheduleOrderDetailComponent) },
{ path: 'Warehouse/Meyle/PackList/:WzHeader', loadComponent: () => import('./packing-lists/meyle/meyle-pack-list.component').then((m) => m.MeylePackListComponent) },
{ path: 'Warehouse/Meyle/PackList/:WzHeader/Simple', loadComponent: () => import('./packing-lists/meyle/meyle-pack-list-simple.component').then((m) => m.MeylePackListSimpleComponent) },
{ path: 'Warehouse/Marelli/PackList/:WzHeader', loadComponent: () => import('./packing-lists/marelli/marelli-pack-list.component').then((m) => m.MarelliPackListComponent) },
{ path: 'Warehouse/Marelli/PackList/:WzHeader/Simple', loadComponent: () => import('./packing-lists/marelli/marelli-pack-list-simple.component').then((m) => m.MarelliPackListSimpleComponent) },
{ path: 'Admin/PK/CustomerOrders', loadComponent: () => import('./customer-orders/customer-orders-list.component').then((m) => m.CustomerOrdersListComponent) },
{ path: 'Admin/PK/CustomerOrder/:customerOrderId', loadComponent: () => import('./customer-order/customer-order-detail.component').then((m) => m.CustomerOrderDetailComponent) },
{ path: 'Admin/PK/EdiCustomerOrders', loadComponent: () => import('./edi-customer-orders/edi-customer-orders-list.component').then((m) => m.EdiCustomerOrdersListComponent) },
{ path: 'Admin/PK/EdiCustomerOrder/:customerOrderId', loadComponent: () => import('./edi-customer-order/edi-customer-order-detail.component').then((m) => m.EdiCustomerOrderDetailComponent) },
{ path: 'admin/pk', loadComponent: () => import('./customer-orders-translations/customer-orders-translations.component').then((m) => m.CustomerOrdersTranslationsComponent) },
{ path: 'Admin/PK/UsersManager', loadComponent: () => import('./admin/users-manager/users-manager.component').then((m) => m.UsersManagerComponent) },
{ path: 'Admin/PK/Scheduler', loadComponent: () => import('./admin/scheduler/scheduler.component').then((m) => m.SchedulerComponent) },
],
},
{ path: 'login', loadComponent: () => import('./auth/login/login.component').then((m) => m.LoginComponent) },
{ path: 'register', loadComponent: () => import('./auth/register/register.component').then((m) => m.RegisterComponent) },
{ path: 'Main', loadComponent: () => import('./main/main.component').then((m) => m.MainComponent) },
{ path: 'Unauthorized', loadComponent: () => import('./unauthorized/unauthorized.component').then((m) => m.UnauthorizedComponent) },
{ path: 'Error', loadComponent: () => import('./error/error.component').then((m) => m.ErrorComponent) },
{ path: '**', redirectTo: '' },
];

View File

View File

@@ -0,0 +1,23 @@
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', async () => {
const fixture = TestBed.createComponent(App);
await fixture.whenStable();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, FaKrosnoManagerAngular');
});
});

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
template: `<router-outlet />`,
styles: `:host { display: block; height: 100dvh; }`,
})
export class App {}

View File

@@ -0,0 +1,134 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { InputTextModule } from 'primeng/inputtext';
import { InputPasswordModule } from 'primeng/inputpassword';
import { ButtonDirective } from 'primeng/button';
import * as bcrypt from 'bcryptjs';
import { AuthService } from '../../core/services/auth.service';
import { UserService } from '../../core/services/user.service';
@Component({
selector: 'app-login',
standalone: true,
imports: [FormsModule, InputTextModule, InputPasswordModule, ButtonDirective, RouterLink],
template: `
<div class="auth-layout">
<aside class="auth-layout__brand">
<div class="auth-layout__brand-logo">
<img src="logo.svg" width="40" height="40" alt="" />
<h1 class="auth-layout__brand-title">FA Krosno Manager</h1>
</div>
<p class="auth-layout__brand-tagline">
System zarządzania zamówieniami DELFOR, magazynem i procesami EDI dla operacji produkcyjnych.
</p>
<div class="auth-layout__features">
<div class="auth-layout__feature"><i class="pi pi-building"></i> Zamówienia DELFOR i harmonogramy</div>
<div class="auth-layout__feature"><i class="pi pi-warehouse"></i> Magazyn i packing listy</div>
<div class="auth-layout__feature"><i class="pi pi-database"></i> Integracja EDI / Syteline</div>
</div>
</aside>
<main class="auth-layout__form">
<div class="auth-card">
<h2 class="auth-card__title">Logowanie</h2>
<p class="auth-card__subtitle">Zaloguj się do systemu FA Krosno Manager</p>
@if (tempPassword()) {
<div class="alert-info mb-3">
Twoje tymczasowe hasło to: <strong>{{ tempPassword() }}</strong>. Użyj go do pierwszego logowania.
</div>
}
<form (ngSubmit)="handleLogin()">
<div class="field-group">
<label class="field-label" for="login">Login</label>
<input pInputText id="login" class="w-full" [(ngModel)]="login" name="login" required placeholder="Wprowadź login" />
</div>
<div class="field-group">
<label class="field-label" for="password">Hasło</label>
<input pInputPassword pInputText id="password" name="password" [(ngModel)]="password" placeholder="Wprowadź hasło" [fluid]="true" class="w-full" />
</div>
<button pButton type="submit" [fluid]="true"><i class="pi pi-sign-in"></i> Zaloguj</button>
@if (errorMessage()) {
<div class="alert-danger mt-3">{{ errorMessage() }}</div>
}
</form>
@if (showChangePassword()) {
<hr class="divider" />
<h3 class="auth-card__title" style="font-size: var(--font-size-lg)">Zmień hasło</h3>
<p class="auth-card__subtitle">Ustaw nowe hasło przed pierwszym użyciem systemu</p>
<form (ngSubmit)="handleChangePassword()">
<div class="field-group">
<label class="field-label" for="newPassword">Nowe hasło</label>
<input pInputPassword pInputText id="newPassword" name="newPassword" [(ngModel)]="newPassword" placeholder="Wprowadź nowe hasło" [fluid]="true" class="w-full" />
</div>
<div class="field-group">
<label class="field-label" for="confirmPassword">Potwierdź hasło</label>
<input pInputPassword pInputText id="confirmPassword" name="confirmPassword" [(ngModel)]="confirmPassword" placeholder="Potwierdź nowe hasło" [fluid]="true" class="w-full" />
</div>
<button pButton type="submit" severity="success" [fluid]="true">Zmień hasło</button>
</form>
}
<p class="text-secondary" style="margin-top: var(--spacing-4); font-size: var(--font-size-sm); text-align: center">
Nie masz konta? <a routerLink="/register">Zarejestruj się</a>
</p>
</div>
</main>
</div>
`,
})
export class LoginComponent implements OnInit {
private readonly auth = inject(AuthService);
private readonly userService = inject(UserService);
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
login = '';
password = '';
newPassword = '';
confirmPassword = '';
readonly tempPassword = signal('');
readonly errorMessage = signal('');
readonly showChangePassword = signal(false);
ngOnInit(): void {
const param = this.route.snapshot.queryParamMap.get('tempPassword');
if (param) this.tempPassword.set(decodeURIComponent(param));
}
async handleLogin(): Promise<void> {
this.errorMessage.set('');
const user = await this.auth.login(this.login, this.password);
if (!user) {
this.errorMessage.set('Nieprawidłowy login lub hasło');
return;
}
if (user.isTemporaryPassword) {
this.showChangePassword.set(true);
return;
}
this.router.navigate(['/']);
}
async handleChangePassword(): Promise<void> {
if (this.newPassword !== this.confirmPassword) {
this.errorMessage.set('Hasła nie są zgodne');
return;
}
const user = await this.auth.getUserByUsername(this.login);
if (!user) return;
user.passwordHash = bcrypt.hashSync(this.newPassword, 10);
user.isTemporaryPassword = false;
await this.userService.update(user);
this.showChangePassword.set(false);
this.login = '';
this.password = '';
this.newPassword = '';
this.confirmPassword = '';
this.router.navigate(['/login']);
}
}

View File

@@ -0,0 +1,93 @@
import { Component, inject } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { InputTextModule } from 'primeng/inputtext';
import { ButtonDirective } from 'primeng/button';
import * as bcrypt from 'bcryptjs';
import { UserService } from '../../core/services/user.service';
import { UserDto } from '../../core/models';
import { generateGuid, generateTempPassword } from '../../shared/utils/business-logic';
@Component({
selector: 'app-register',
standalone: true,
imports: [FormsModule, InputTextModule, ButtonDirective, RouterLink],
template: `
<div class="auth-layout">
<aside class="auth-layout__brand">
<div class="auth-layout__brand-logo">
<img src="logo.svg" width="40" height="40" alt="" />
<h1 class="auth-layout__brand-title">FA Krosno Manager</h1>
</div>
<p class="auth-layout__brand-tagline">
System zarządzania zamówieniami DELFOR, magazynem i procesami EDI dla operacji produkcyjnych.
</p>
<div class="auth-layout__features">
<div class="auth-layout__feature"><i class="pi pi-building"></i> Zamówienia DELFOR i harmonogramy</div>
<div class="auth-layout__feature"><i class="pi pi-warehouse"></i> Magazyn i packing listy</div>
<div class="auth-layout__feature"><i class="pi pi-database"></i> Integracja EDI / Syteline</div>
</div>
</aside>
<main class="auth-layout__form">
<div class="auth-card">
<h2 class="auth-card__title">Rejestracja</h2>
<p class="auth-card__subtitle">Utwórz nowe konto w systemie FA Krosno Manager</p>
<form (ngSubmit)="handleRegister()">
<div class="field-group">
<label class="field-label" for="login">Login</label>
<input pInputText id="login" class="w-full" [(ngModel)]="model.login" name="login" required placeholder="Wprowadź login" />
</div>
<div class="field-group">
<label class="field-label" for="email">Email</label>
<input pInputText id="email" class="w-full" [(ngModel)]="model.email" name="email" required placeholder="Wprowadź email" />
</div>
<div class="field-group">
<label class="field-label" for="firstName">Imię</label>
<input pInputText id="firstName" class="w-full" [(ngModel)]="model.firstName" name="firstName" required placeholder="Wprowadź imię" />
</div>
<div class="field-group">
<label class="field-label" for="lastName">Nazwisko</label>
<input pInputText id="lastName" class="w-full" [(ngModel)]="model.lastName" name="lastName" required placeholder="Wprowadź nazwisko" />
</div>
<button pButton type="submit" [fluid]="true"><i class="pi pi-user-plus"></i> Zarejestruj</button>
</form>
<p class="text-secondary" style="margin-top: var(--spacing-4); font-size: var(--font-size-sm); text-align: center">
Masz już konto? <a routerLink="/login">Zaloguj się</a>
</p>
</div>
</main>
</div>
`,
})
export class RegisterComponent {
private readonly userService = inject(UserService);
private readonly router = inject(Router);
model = { login: '', email: '', firstName: '', lastName: '' };
async handleRegister(): Promise<void> {
const temporaryPassword = generateTempPassword();
const user: UserDto = {
id: 0,
login: this.model.login,
passwordHash: bcrypt.hashSync(temporaryPassword, 10),
isTemporaryPassword: true,
isActive: true,
activeFrom: new Date().toISOString(),
email: this.model.email,
firstName: this.model.firstName,
lastName: this.model.lastName,
createdDate: new Date().toISOString(),
failedLoginAttempts: 0,
isLocked: false,
rowPointer: generateGuid(),
userRoles: [],
};
await this.userService.create(user);
this.router.navigate(['/login'], { queryParams: { tempPassword: temporaryPassword } });
}
}

View File

@@ -0,0 +1,12 @@
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/login']);
};

View File

@@ -0,0 +1,12 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.getToken();
if (token) {
req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
}
return next(req);
};

View File

@@ -0,0 +1,30 @@
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, finalize, throwError } from 'rxjs';
import { MessageService } from 'primeng/api';
import { Router } from '@angular/router';
import { LoadingService } from '../services/loading.service';
import { AuthService } from '../services/auth.service';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const messageService = inject(MessageService);
const router = inject(Router);
const auth = inject(AuthService);
const loading = inject(LoadingService);
loading.show();
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
auth.logout();
router.navigate(['/login']);
messageService.add({ severity: 'error', summary: 'Sesja wygasła', detail: 'Zaloguj się ponownie.' });
} else {
const detail = error.error?.message ?? error.message ?? 'Wystąpił błąd podczas komunikacji z serwerem.';
messageService.add({ severity: 'error', summary: 'Błąd', detail });
}
return throwError(() => error);
}),
finalize(() => loading.hide()),
);
};

View File

@@ -0,0 +1,414 @@
export interface LoginResponse {
token: string;
expires: string;
}
export interface ResponseModel {
status: number;
identifier: string;
message: string;
externalIdentifier?: string;
}
export interface TransactionModel {
partNumber?: string;
itemNumber?: string;
quantity?: number;
}
export interface UserDto {
id: number;
login: string;
passwordHash: string;
isTemporaryPassword: boolean;
isActive: boolean;
activeFrom?: string;
activeTo?: string;
email: string;
firstName: string;
lastName: string;
createdDate: string;
lastLoginDate?: string;
failedLoginAttempts: number;
isLocked: boolean;
lockoutEndDate?: string;
rowPointer: string;
userRoles: UserRoleDto[];
}
export interface UserRoleDto {
userId: number;
roleId: number;
rowPointer: string;
}
export interface RoleDto {
id: number;
name: string;
rowPointer: string;
}
export interface FunctionDto {
id: number;
roleId: number;
name: string;
rowPointer: string;
}
export interface TaskSchedulerDto {
id: number;
rowPointer: string;
name: string;
path: string;
cronOptions: string;
createDate: string;
activeFrom: string;
activeUntil?: string;
lastExecution?: string;
nextExecution?: string;
taskSchedulerDetails: TaskSchedulerDetailDto[];
}
export interface TaskSchedulerDetailDto {
id: number;
taskSchedulerId: number;
name: string;
value: string;
}
export interface ProductDto {
id: number;
recipientID: number;
recipientIdx: string;
faIdx: string;
recipientName: string;
}
export interface ScheduleOrderDto {
id: number;
poNum: string;
orderID: number;
recipientID: number;
lastUpdateDate: string;
docNumber: number;
docType?: string;
recipientCode: string;
purchaserCode?: string;
recipientName: string;
purchaserName?: string;
scheduleOrderDetails: ScheduleOrderDetailDto[];
scheduleOrderMiscs: ScheduleOrderMiscDto[];
}
export interface ScheduleOrderDetailDto {
id: number;
scheduleOrderID: number;
sc_productCode: string;
sh_productCode: string;
price?: number;
orderNumber?: string;
recipientName?: string;
recipientCode?: string;
purchaserName?: string;
scheduleOrderDetailDetails: ScheduleOrderDetailDetailDto[];
}
export interface ScheduleOrderDetailDetailDto {
id: number;
scheduleOrderDetailID: number;
qty: number;
dateFrom: string;
dateTo: string;
sccType: string;
sccDesc?: string;
qtyType?: string;
qtyDesc?: string;
status: string;
shipDate?: string;
lastUpdateDate: string;
}
export interface ScheduleOrderMiscDto {
id: number;
scheduleOrderID: number;
type: string;
value: string;
label?: string;
display: boolean;
text: string;
}
export interface CustomerOrderDto {
rowPointer: string;
coNum: string;
custPo: string;
custNum: string;
custSeq: number;
createDate: string;
contact?: string;
phone?: string;
orderDate?: string;
termsCode?: string;
price?: number;
translatedStatus?: string;
whse?: string;
frtTaxCode1?: string;
endUserType?: string;
exchRate?: number;
uf_FKR_EDI_IdentificationCode?: string;
uf_FKR_EDI_RSSBussFolder?: string;
uf_DocType?: string;
uf_FKR_EDI_Gate?: string;
uf_FKR_EDI_RecipientCode?: string;
uf_FKR_EDI_SellerCode?: string;
uf_FKR_EDI_SenderCode?: string;
uf_FKR_EDI_BuyerCode?: string;
ediCustomerOrderTranslates?: EdiCustomerOrderTranslateDto[];
customerOrderLines: CustomerOrderLineDto[];
}
export interface CustomerOrderLineDto {
coLine: number;
coNum?: string;
item: string;
custItem: string;
description: string;
blanketQty?: number;
um: string;
contPrice?: number;
translatedStatus?: string;
effDate?: string;
uf_FKR_EDI_BLN_BoxType?: string;
uf_FKR_EDI_BLN_Address?: string;
uf_FKR_EDI_BLN_FinalDestination?: string;
uf_FKR_EDI_BLN_QtyPerBox?: number;
customerOrderLineItems: CustomerOrderLineItemDto[];
}
export interface CustomerOrderLineItemDto {
coLine: number;
coRelease: number;
coNum?: string;
item: string;
custItem: string;
qtyOrdered?: number;
dueDate?: string;
translatedStatus?: string;
price?: number;
releaseDate?: string;
whse?: string;
uf_FKR_EDI_ITEM_DocumentType?: string;
taxCode1?: string;
um?: string;
coCustNum?: string;
description?: string;
uf_FKR_EDI_ITEM_RoutingCode?: string;
uf_FKR_EDI_ITEM_DeliveryCallNum?: string;
uf_LOC_11_UnloadingPoint?: string;
uf_LOC_159_DestinationPoint?: string;
uf_FKR_EDI_ITEM_PalletCode?: string;
}
export interface EdiCustomerOrderDto {
rowPointer: string;
customerOrderNumber: string;
customerPoNumber: string;
customerNumber: string;
customerSequence?: number;
customerName?: string;
createDate: string;
recivedDate?: string;
posted?: number;
postedDate?: string;
orderDate?: string;
price?: number;
weight?: number;
warehouse?: string;
gate?: string;
recipientCode?: string;
senderCode?: string;
sellerCode?: string;
buyerCode?: string;
docType?: string;
slOrderNumber?: string;
sentToSl?: string;
translatedStatus?: string;
ediCustomerOrderLines: EdiCustomerOrderLineDto[];
ediCustomerOrderTranslates: EdiCustomerOrderTranslateDto[];
}
export interface EdiCustomerOrderLineDto {
customerOrderLine: number;
customerOrderNumber?: string;
item: string;
customerItemNumber: string;
description: string;
blanketQty?: number;
uom: string;
contPrice?: number;
translatedStatus?: string;
effectiveDate?: string;
boxType?: string;
address?: string;
finalDestination?: string;
qtyPerBox?: number;
ediCustomerOrderLineItems: EdiCustomerOrderLineItemDto[];
}
export interface EdiCustomerOrderLineItemDto {
customerOrderLine: number;
customerOrderRelease: number;
customerOrderNumber?: string;
item: string;
customerItem: string;
qtyOrdered?: number;
dueDate?: string;
translatedStatus?: string;
price?: number;
releaseDate?: string;
warehouse?: string;
documentType?: string;
taxCodeOne?: string;
uom?: string;
customerOrderCustomerNumber?: string;
description?: string;
routingCode?: string;
deliveryCallNumber?: string;
unloadingPoint?: string;
destinationPoint?: string;
palletCode?: string;
palletNumber?: string;
}
export interface EdiCustomerOrderTranslateDto {
id: number;
coEdiOrder: number;
coRowPointer: string;
coCoNum?: string;
coType?: string;
coTakenBy?: string;
ediCoCoNum?: string;
ordersCount: number;
orderFound: boolean;
createdDate: string;
foundNumbers?: string;
scheduleOrderId: number;
}
export interface ErrorLogDto {
trxNum: string;
seq: number;
lineNum: number;
releaseNum: number;
errDate?: string;
errNum?: number;
errMsg: string;
trxCode: string;
noteExistsFlag: boolean;
recordDate: string;
rowPointer: string;
createdBy: string;
updatedBy: string;
createDate: string;
inWorkflow: boolean;
}
export interface WzHeaderDto {
id: string;
fk_Client?: string;
createdDate: string;
emailAddresses?: string;
wzNumbers?: string;
wzRowsMeyle?: WzRowMeyleDto[];
wzRowsMarelli?: WzRowMarelliDto[];
}
export interface WzRowMeyleDto {
id: string;
fk_Header?: string;
orderNumber: string;
itemNumber: string;
quantity?: number;
palletNumber?: number;
wzNumber: string;
partNumber?: string;
transactionNumber?: number;
faIndex?: string;
partNumberSl?: string;
}
export interface WzRowMarelliDto {
id: string;
fkHeader?: string;
type: string;
palletNumber?: number;
itemNumber: string;
engineerNumber: string;
quantity?: number;
orderNumber: string;
wzNumber: string;
faIndex: string;
transactionNumber?: number;
}
export interface WzClientDto {
id: string;
customerNumber: string;
customerSequence?: number;
createdDate: string;
name: string;
shortName: string;
logoBase64?: string;
}
export interface MaterialTransactionDto {
mtGroup?: string;
mtGroupNum?: string;
transNum?: number;
item?: string;
transDate?: string;
qty?: number;
cost?: number;
whse?: string;
loc?: string;
refNum?: string;
refLineSuf?: number;
refRelease?: number;
reasonCode?: string;
transType?: string;
refType?: string;
mtReasonType?: string;
prefixId?: number;
sequenceId?: number;
whseSequenceId?: number;
whseSplit?: boolean;
variableId?: string;
formName?: string;
inWorkflow?: boolean;
noteExistsFlag?: boolean;
recordDate?: string;
rowPointer?: string;
createdBy?: string;
updatedBy?: string;
createDate?: string;
custNum?: string;
vendNum?: string;
recipNum?: number;
uf_FKR_internal_num_matltran_zn?: string;
session_Id?: string;
uf_MobileAppUser?: string;
nr_KARTY_KONTROLNEJ?: string;
}
export interface ItemCustDto {
item: string;
custNum: string;
custItemSeq: number;
custItem: string;
um: string;
uf_FKR_CustItem2?: string;
}
export interface GridFilterState {
columns: Record<string, unknown>;
}

View File

@@ -0,0 +1,58 @@
import { Injectable, signal, computed, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { LoginResponse, UserDto } from '../models';
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly http = inject(HttpClient);
private readonly tokenKey = 'authToken';
private readonly tokenSignal = signal<string | null>(this.readToken());
readonly token = this.tokenSignal.asReadonly();
readonly isAuthenticated = computed(() => !!this.tokenSignal());
readonly userName = signal<string>('');
private readToken(): string | null {
return sessionStorage.getItem(this.tokenKey);
}
async login(login: string, password: string): Promise<UserDto | null> {
try {
const result = await firstValueFrom(
this.http.post<LoginResponse>(`${environment.apiBaseUrl}/api/Users/login`, { login, password }),
);
if (!result?.token) return null;
sessionStorage.setItem(this.tokenKey, result.token);
this.tokenSignal.set(result.token);
const user = await this.getUserByUsername(login);
if (user) {
this.userName.set(user.login);
}
return user;
} catch {
return null;
}
}
async getUserByUsername(username: string): Promise<UserDto | null> {
try {
return await firstValueFrom(
this.http.get<UserDto>(`${environment.apiBaseUrl}/api/Users/by-username`, { params: { username } }),
);
} catch {
return null;
}
}
logout(): void {
sessionStorage.removeItem(this.tokenKey);
this.tokenSignal.set(null);
this.userName.set('');
}
getToken(): string | null {
return this.tokenSignal();
}
}

View File

@@ -0,0 +1,33 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { CustomerOrderDto } from '../models';
@Injectable({ providedIn: 'root' })
export class CustomerOrderService {
private readonly http = inject(HttpClient);
private readonly base = `${environment.apiBaseUrl}/api/CustomerOrders`;
getAll(): Promise<CustomerOrderDto[]> {
return firstValueFrom(this.http.get<CustomerOrderDto[]>(this.base));
}
getByOrderNumber(customerOrderNumber: string): Promise<CustomerOrderDto> {
return firstValueFrom(
this.http.get<CustomerOrderDto>(`${this.base}/by-order-number`, { params: { customerOrderNumber } }),
);
}
getByCoNumber(customerOrderNumber: string): Promise<CustomerOrderDto> {
return firstValueFrom(
this.http.get<CustomerOrderDto>(`${this.base}/by-co-number`, { params: { customerOrderNumber } }),
);
}
getByRowPointer(rowPointer: string): Promise<CustomerOrderDto> {
return firstValueFrom(
this.http.get<CustomerOrderDto>(`${this.base}/by-order-number`, { params: { customerOrderNumber: rowPointer } }),
);
}
}

View File

@@ -0,0 +1,19 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { EdiCustomerOrderTranslateDto } from '../models';
@Injectable({ providedIn: 'root' })
export class EdiCustomerOrderTranslateService {
private readonly http = inject(HttpClient);
private readonly base = `${environment.apiBaseUrl}/api/EdiCustomerOrdersTranslations`;
getAll(): Promise<EdiCustomerOrderTranslateDto[]> {
return firstValueFrom(this.http.get<EdiCustomerOrderTranslateDto[]>(this.base));
}
delete(id: number): Promise<void> {
return firstValueFrom(this.http.delete<void>(this.base, { params: { id: id.toString() } }));
}
}

View File

@@ -0,0 +1,44 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { EdiCustomerOrderDto, ErrorLogDto, ResponseModel } from '../models';
@Injectable({ providedIn: 'root' })
export class EdiCustomerOrderService {
private readonly http = inject(HttpClient);
private readonly base = `${environment.apiBaseUrl}/api/EdiCustomerOrders`;
getAll(): Promise<EdiCustomerOrderDto[]> {
return firstValueFrom(this.http.get<EdiCustomerOrderDto[]>(this.base));
}
getByOrderNumber(customerOrderNumber: string): Promise<EdiCustomerOrderDto> {
return firstValueFrom(
this.http.get<EdiCustomerOrderDto>(`${this.base}/by-order-number`, { params: { customerOrderNumber } }),
);
}
getByRowPointer(rowPointer: string): Promise<EdiCustomerOrderDto> {
return firstValueFrom(
this.http.get<EdiCustomerOrderDto>(`${this.base}/by-order-number`, { params: { customerOrderNumber: rowPointer } }),
);
}
async sendToSyteline(rowPointer: string, identifier: string, externalIdentifier?: string): Promise<ResponseModel> {
try {
await firstValueFrom(
this.http.post(`${this.base}/send-to-syteline`, null, { params: { customerOrderNumber: rowPointer } }),
);
return { status: 1, identifier, message: '', externalIdentifier: externalIdentifier ?? '' };
} catch {
const errors = await firstValueFrom(
this.http.get<ErrorLogDto[]>(`${environment.apiBaseUrl}/api/ErrorLog/by-order-number`, {
params: { customerOrderNumber: rowPointer },
}),
).catch(() => [] as ErrorLogDto[]);
const message = errors.map((e) => e.errMsg).join('\n');
return { status: 0, identifier, message, externalIdentifier: '' };
}
}
}

View File

@@ -0,0 +1,27 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { FunctionDto } from '../models';
@Injectable({ providedIn: 'root' })
export class FunctionService {
private readonly http = inject(HttpClient);
private readonly base = `${environment.apiBaseUrl}/api/Functions`;
getAll(): Promise<FunctionDto[]> {
return firstValueFrom(this.http.get<FunctionDto[]>(this.base));
}
create(fn: FunctionDto): Promise<FunctionDto> {
return firstValueFrom(this.http.post<FunctionDto>(this.base, fn));
}
update(fn: FunctionDto): Promise<FunctionDto> {
return firstValueFrom(this.http.put<FunctionDto>(this.base, fn));
}
delete(id: string): Promise<void> {
return firstValueFrom(this.http.delete<void>(this.base, { params: { id } }));
}
}

View File

@@ -0,0 +1,31 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { TaskSchedulerDto } from '../models';
@Injectable({ providedIn: 'root' })
export class HangfireService {
private readonly http = inject(HttpClient);
private readonly base = `${environment.apiBaseUrl}/api/HangfireJobs`;
getAll(): Promise<TaskSchedulerDto[]> {
return firstValueFrom(this.http.get<TaskSchedulerDto[]>(`${this.base}/`));
}
getById(id: string): Promise<TaskSchedulerDto> {
return firstValueFrom(this.http.get<TaskSchedulerDto>(`${this.base}/${id}`));
}
add(job: TaskSchedulerDto): Promise<void> {
return firstValueFrom(this.http.post<void>(`${this.base}/add`, job));
}
update(job: TaskSchedulerDto): Promise<void> {
return firstValueFrom(this.http.post<void>(`${this.base}/update`, job));
}
delete(job: TaskSchedulerDto): Promise<void> {
return firstValueFrom(this.http.post<void>(`${this.base}/delete`, job));
}
}

View File

@@ -0,0 +1,15 @@
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class LoadingService {
private readonly pendingCount = signal(0);
readonly isLoading = computed(() => this.pendingCount() > 0);
show(): void {
this.pendingCount.update((c) => c + 1);
}
hide(): void {
this.pendingCount.update((c) => Math.max(0, c - 1));
}
}

View File

@@ -0,0 +1,19 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { ProductDto } from '../models';
@Injectable({ providedIn: 'root' })
export class ProductService {
private readonly http = inject(HttpClient);
private readonly base = `${environment.apiBaseUrl}/api/Product`;
getByIndex(indexName: string): Promise<ProductDto[]> {
return firstValueFrom(this.http.get<ProductDto[]>(`${this.base}/by-index`, { params: { indexName } }));
}
update(product: ProductDto): Promise<void> {
return firstValueFrom(this.http.put<void>(this.base, product));
}
}

View File

@@ -0,0 +1,27 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { RoleDto } from '../models';
@Injectable({ providedIn: 'root' })
export class RoleService {
private readonly http = inject(HttpClient);
private readonly base = `${environment.apiBaseUrl}/api/Roles`;
getAll(): Promise<RoleDto[]> {
return firstValueFrom(this.http.get<RoleDto[]>(this.base));
}
create(role: RoleDto): Promise<RoleDto> {
return firstValueFrom(this.http.post<RoleDto>(this.base, role));
}
update(role: RoleDto): Promise<RoleDto> {
return firstValueFrom(this.http.put<RoleDto>(this.base, role));
}
delete(id: string): Promise<void> {
return firstValueFrom(this.http.delete<void>(this.base, { params: { id } }));
}
}

View File

@@ -0,0 +1,19 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { ScheduleOrderDto } from '../models';
@Injectable({ providedIn: 'root' })
export class ScheduleOrderService {
private readonly http = inject(HttpClient);
private readonly base = `${environment.apiBaseUrl}/api/ScheduleOrders`;
getAll(): Promise<ScheduleOrderDto[]> {
return firstValueFrom(this.http.get<ScheduleOrderDto[]>(this.base));
}
getById(id: number): Promise<ScheduleOrderDto> {
return firstValueFrom(this.http.get<ScheduleOrderDto>(`${this.base}/${id}`));
}
}

View File

@@ -0,0 +1,31 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { UserDto } from '../models';
@Injectable({ providedIn: 'root' })
export class UserService {
private readonly http = inject(HttpClient);
private readonly base = `${environment.apiBaseUrl}/api/Users`;
getAll(): Promise<UserDto[]> {
return firstValueFrom(this.http.get<UserDto[]>(this.base));
}
getById(id: string): Promise<UserDto> {
return firstValueFrom(this.http.get<UserDto>(`${this.base}/by-id`, { params: { id } }));
}
create(user: UserDto): Promise<UserDto> {
return firstValueFrom(this.http.post<UserDto>(this.base, user));
}
update(user: UserDto): Promise<UserDto> {
return firstValueFrom(this.http.put<UserDto>(this.base, user));
}
delete(id: string): Promise<void> {
return firstValueFrom(this.http.delete<void>(this.base, { params: { id } }));
}
}

View File

@@ -0,0 +1,131 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import {
CustomerOrderDto,
ItemCustDto,
MaterialTransactionDto,
TransactionModel,
WzClientDto,
WzHeaderDto,
WzRowMarelliDto,
WzRowMeyleDto,
} from '../models';
@Injectable({ providedIn: 'root' })
export class WarehouseService {
private readonly http = inject(HttpClient);
private readonly base = environment.apiBaseUrl;
getWzHeaderById(id: string): Promise<WzHeaderDto> {
return firstValueFrom(this.http.get<WzHeaderDto>(`${this.base}/api/WzHeader/by-id`, { params: { id } }));
}
getAllClients(): Promise<WzClientDto[]> {
return firstValueFrom(this.http.get<WzClientDto[]>(`${this.base}/api/WzClient`));
}
getAllClientWzs(customerNumber: string, customerSequence: number): Promise<MaterialTransactionDto[]> {
return firstValueFrom(
this.http.get<MaterialTransactionDto[]>(`${this.base}/api/WzHeader/by-customer-number`, {
params: { customerNumber, customerSequence: customerSequence.toString() },
}),
);
}
getAllClientWzHeaders(customerNumber: string, customerSequence: number): Promise<WzHeaderDto[]> {
return firstValueFrom(
this.http.get<WzHeaderDto[]>(`${this.base}/api/WzHeader/all-wz-headers`, {
params: { customerNumber, customerSequence: customerSequence.toString() },
}),
);
}
createWzHeader(header: WzHeaderDto): Promise<void> {
return firstValueFrom(this.http.post<void>(`${this.base}/api/WzHeader`, header));
}
createWzRowsMeyle(rows: WzRowMeyleDto[]): Promise<void> {
return firstValueFrom(this.http.post<void>(`${this.base}/api/WzRowMeyle`, rows));
}
createWzRowsMarelli(rows: WzRowMarelliDto[]): Promise<void> {
return firstValueFrom(this.http.post<void>(`${this.base}/api/WzRowMarelli`, rows));
}
getCustomerOrder(customerOrderNumber: string): Promise<CustomerOrderDto> {
return firstValueFrom(
this.http.get<CustomerOrderDto>(`${this.base}/api/CustomerOrders/by-co-number`, {
params: { customerOrderNumber },
}),
);
}
getItem(itemNumber: string, customerNumber: string): Promise<ItemCustDto> {
return firstValueFrom(
this.http.get<ItemCustDto>(`${this.base}/api/ItemCust`, { params: { itemNumber, customerNumber } }),
);
}
getWzRowsMeyleByWzHeaderId(wzHeaderId: string): Promise<WzRowMeyleDto[]> {
return firstValueFrom(
this.http.get<WzRowMeyleDto[]>(`${this.base}/api/WzRowMeyle/by-wz-header-id`, {
params: { wzHeaderId },
}),
);
}
getWzRowsMarelliByWzHeaderId(wzHeaderId: string): Promise<WzRowMarelliDto[]> {
return firstValueFrom(
this.http.get<WzRowMarelliDto[]>(`${this.base}/api/WzRowMarelli/by-wz-header-id`, {
params: { wzHeaderId },
}),
);
}
async getTransactionsModels(): Promise<Record<string, TransactionModel[]>> {
const materialTransactions = await firstValueFrom(
this.http.get<MaterialTransactionDto[]>(`${this.base}/api/WzRowMeyle/transactions-with-part-number`),
);
const result: Record<string, TransactionModel[]> = {};
for (const mt of materialTransactions) {
const key = mt.nr_KARTY_KONTROLNEJ ?? '';
if (!result[key]) result[key] = [];
result[key].push({
itemNumber: mt.item,
partNumber: mt.nr_KARTY_KONTROLNEJ,
quantity: mt.qty,
});
}
return result;
}
updateWzRowsMeyle(rows: WzRowMeyleDto[]): Promise<void> {
return firstValueFrom(this.http.put<void>(`${this.base}/api/WzRowMeyle`, rows));
}
updateWzRowsMarelli(rows: WzRowMarelliDto[]): Promise<void> {
return firstValueFrom(this.http.put<void>(`${this.base}/api/WzRowMarelli`, rows));
}
generateXlsForMeyle(packListId: string): Promise<void> {
return firstValueFrom(
this.http.get<void>(`${this.base}/api/ExcelGenerator/generate-meyle`, { params: { packListId } }),
);
}
generateXlsForMarelli(packListId: string): Promise<void> {
return firstValueFrom(
this.http.get<void>(`${this.base}/api/ExcelGenerator/generate-marelli`, { params: { packListId } }),
);
}
addEmailsToWzHeader(wzHeaderId: string, emailAddresses: string): Promise<void> {
return firstValueFrom(
this.http.post<void>(`${this.base}/api/WzHeader/add-emails`, emailAddresses, {
params: { id: wzHeaderId },
}),
);
}
}

View File

@@ -0,0 +1,162 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DatePipe } from '@angular/common';
import { RowToggler, SelectableRow, TableModule, TableRowSelectEvent } from 'primeng/table';
import { ButtonDirective } from 'primeng/button';
import { CustomerOrderDto, CustomerOrderLineDto, CustomerOrderLineItemDto, ScheduleOrderDto } from '../core/models';
import { CustomerOrderService } from '../core/services/customer-order.service';
import { ScheduleOrderService } from '../core/services/schedule-order.service';
import { ScheduleOrdersGridComponent } from '../shared/components/schedule-orders-grid/schedule-orders-grid.component';
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
@Component({
selector: 'app-customer-order-detail',
standalone: true,
imports: [TableModule, RowToggler, SelectableRow, ButtonDirective, DatePipe, ScheduleOrdersGridComponent, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
[title]="'Zamówienie klienta nr ' + (order()?.coNum ?? 'Brak numeru')"
subtitle="Szczegóły zamówienia klienta w systemie Syteline"
icon="pi pi-shopping-cart"
>
<app-content-section title="Informacje o zamówieniu" icon="pi pi-info-circle">
<div class="form-grid">
<div>
<u>Numer Zamówienia:</u> <b>{{ order()?.coNum }}</b><br />
<u>Numer Zamówienia Klienta:</u> <b>{{ order()?.custPo }}</b><br />
<u>Klient:</u> <b>{{ order()?.custNum }}</b><br />
<u>Numer Odbiorcy:</u> <b>{{ order()?.custSeq }}</b><br />
<u>Kontakt:</u> <b>{{ order()?.contact }}</b><br />
<u>Telefon:</u> <b>{{ order()?.phone }}</b><br />
<u>Data Zamówienia:</u> <b>{{ order()?.orderDate | date: 'yyyy-MM-dd HH:mm:ss' }}</b><br />
<u>Warunki:</u> <b>{{ order()?.termsCode }}</b><br />
<u>Wartość Brutto:</u> <b>{{ order()?.price?.toFixed(2) ?? 'N/A' }}</b><br />
<u>Status:</u> <b>{{ order()?.translatedStatus }}</b>
</div>
<div>
<u>Magazyn:</u> <b>{{ order()?.whse }}</b><br />
<u>VAT:</u> <b>{{ order()?.frtTaxCode1 }}</b><br />
<u>Typ Odbiorcy:</u> <b>{{ order()?.endUserType }}</b><br />
<u>Kurs Wymiany:</u> <b>{{ order()?.exchRate?.toFixed(4) ?? 'N/A' }}</b><br />
<u>Gate:</u> <b>{{ order()?.uf_FKR_EDI_Gate }}</b><br />
<u>RecipientCode:</u> <b>{{ order()?.uf_FKR_EDI_RecipientCode }}</b><br />
<u>SellerCode:</u> <b>{{ order()?.uf_FKR_EDI_SellerCode }}</b><br />
<u>SenderCode:</u> <b>{{ order()?.uf_FKR_EDI_SenderCode }}</b><br />
<u>BuyerCode:</u> <b>{{ order()?.uf_FKR_EDI_BuyerCode }}</b><br />
<u>Typ Dokumentu:</u> <b>{{ order()?.uf_DocType }}</b>
</div>
</div>
</app-content-section>
<app-content-section title="Powiązane zamówienia DELFOR" icon="pi pi-building">
<button pButton type="button" (click)="toggleDelfors()">{{ delforButtonText() }}</button>
@if (showDelfors()) {
<div class="mt-3">
<app-schedule-orders-grid [gridData]="scheduleOrders()" [pageSize]="5" />
</div>
}
</app-content-section>
<app-content-section title="Indeksy" icon="pi pi-list" [flush]="true">
<p-table [value]="lines()" dataKey="coLine" [paginator]="true" [rows]="10" (onRowSelect)="onLineSelect($event)" selectionMode="single" [(selection)]="selectedLine" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr><th style="width:3rem"></th><th>Linia</th><th>Pozycja</th><th>Pozycja Klienta</th><th>Opis</th><th>Ilość</th><th>J/M</th><th>Cena</th><th>Status</th></tr>
</ng-template>
<ng-template #body let-line let-expanded="expanded">
<tr [pSelectableRow]="line">
<td><button pButton type="button" [text]="true" [rounded]="true" [iconOnly]="true" [pRowToggler]="line"><i [class]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i></button></td>
<td>{{ line.coLine }}</td><td>{{ line.item }}</td><td>{{ line.custItem }}</td><td>{{ line.description }}</td>
<td>{{ line.blanketQty?.toFixed(2) }}</td><td>{{ line.um }}</td><td>{{ line.contPrice }}</td><td>{{ line.translatedStatus }}</td>
</tr>
</ng-template>
<ng-template #expandedrow let-line>
<tr><td colspan="9">
<div class="form-grid">
<div>
<h6>Szczegóły</h6>
<u>Numer zamówienia:</u> <b>{{ line.coNum }}</b><br />
<u>Linia:</u> <b>{{ line.coLine }}</b><br />
<u>Pozycja:</u> <b>{{ line.item }}</b><br />
<u>Pozycja Klienta:</u> <b>{{ line.custItem }}</b><br />
<u>Opis:</u> <b>{{ line.description }}</b><br />
<u>Łączna Ilość:</u> <b>{{ line.blanketQty?.toFixed(2) }}</b><br />
<u>Status:</u> <b>{{ line.translatedStatus }}</b>
</div>
<div>
<u>Cena:</u> <b>{{ line.contPrice?.toFixed(2) ?? 'N/A' }}</b><br />
<u>Ważne Od:</u> <b>{{ line.effDate | date: 'dd.MM.yyyy' }}</b><br />
<u>J/M:</u> <b>{{ line.um }}</b><br />
<u>BoxType:</u> <b>{{ line.uf_FKR_EDI_BLN_BoxType }}</b><br />
<u>Address:</u> <b>{{ line.uf_FKR_EDI_BLN_Address }}</b><br />
<u>FinalDestination:</u> <b>{{ line.uf_FKR_EDI_BLN_FinalDestination }}</b><br />
<u>QtyPerBox:</u> <b>{{ line.uf_FKR_EDI_BLN_QtyPerBox }}</b>
</div>
</div>
</td></tr>
</ng-template>
</p-table>
</app-content-section>
@if (lineItems().length) {
<app-content-section title="Harmonogramy" icon="pi pi-calendar" [flush]="true">
<p-table [value]="lineItems()" [paginator]="true" [rows]="10" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr><th>Linia</th><th>Zwolnienie</th><th>Pozycja</th><th>Pozycja Klienta</th><th>Łączna Ilość</th><th>Data Wykonania</th><th>Status</th></tr>
</ng-template>
<ng-template #body let-item>
<tr>
<td>{{ item.coLine }}</td><td>{{ item.coRelease }}</td><td>{{ item.item }}</td><td>{{ item.custItem }}</td>
<td>{{ item.qtyOrdered?.toFixed(2) }}</td><td>{{ item.dueDate | date: 'dd.MM.yyyy' }}</td><td>{{ item.translatedStatus }}</td>
</tr>
</ng-template>
</p-table>
</app-content-section>
}
</app-page-shell>
`,
})
export class CustomerOrderDetailComponent implements OnInit {
private readonly route = inject(ActivatedRoute);
private readonly customerOrderService = inject(CustomerOrderService);
private readonly scheduleOrderService = inject(ScheduleOrderService);
readonly order = signal<CustomerOrderDto | null>(null);
readonly lines = signal<CustomerOrderLineDto[]>([]);
readonly lineItems = signal<CustomerOrderLineItemDto[]>([]);
readonly scheduleOrders = signal<ScheduleOrderDto[]>([]);
readonly showDelfors = signal(false);
readonly delforButtonText = signal('Pokaż powiązane zamówienia DELFOR');
selectedLine: CustomerOrderLineDto | null = null;
async ngOnInit(): Promise<void> {
const id = this.route.snapshot.paramMap.get('customerOrderId') ?? '';
const data = await this.customerOrderService.getByRowPointer(id);
this.order.set(data);
this.lines.set(data.customerOrderLines ?? []);
}
onLineSelect(event: TableRowSelectEvent<CustomerOrderLineDto>): void {
const data = event.data;
if (data && !Array.isArray(data)) {
this.lineItems.set(data.customerOrderLineItems ?? []);
}
}
async toggleDelfors(): Promise<void> {
if (this.showDelfors()) {
this.showDelfors.set(false);
this.delforButtonText.set('Pokaż powiązane zamówienia DELFOR');
this.scheduleOrders.set([]);
return;
}
const order = this.order();
const ids = [...new Set((order?.ediCustomerOrderTranslates ?? []).map((x) => x.scheduleOrderId))];
if (ids.length) {
const all = await this.scheduleOrderService.getAll();
this.scheduleOrders.set(all.filter((x) => ids.includes(x.id)).sort((a, b) => new Date(b.lastUpdateDate).getTime() - new Date(a.lastUpdateDate).getTime()));
}
this.showDelfors.set(true);
this.delforButtonText.set('Ukryj');
}
}

View File

@@ -0,0 +1,74 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { ConfirmationService } from 'primeng/api';
import { TableModule } from 'primeng/table';
import { ButtonDirective } from 'primeng/button';
import { DatePipe } from '@angular/common';
import { EdiCustomerOrderTranslateDto } from '../core/models';
import { EdiCustomerOrderTranslateService } from '../core/services/edi-customer-order-translate.service';
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
@Component({
selector: 'app-customer-orders-translations',
standalone: true,
imports: [TableModule, ButtonDirective, DatePipe, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
title="Zarządzanie powiązaniami zamówień z DELFORami"
subtitle="Powiązania między zamówieniami EDI, SL i harmonogramami DELFOR"
icon="pi pi-link"
>
<app-content-section title="Powiązania" icon="pi pi-sitemap" [flush]="true">
<p-table [value]="translations()" [paginator]="true" [rows]="10" dataKey="id" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr>
<th>DELFOR Id</th>
<th>Numer zamówienia EDI</th>
<th>Numer zamówienia SL</th>
<th>Data utworzenia</th>
<th style="width: 6rem"></th>
</tr>
</ng-template>
<ng-template #body let-row>
<tr>
<td>{{ row.scheduleOrderId }}</td>
<td>{{ row.ediCoCoNum }}</td>
<td>{{ row.coCoNum }}</td>
<td>{{ row.createdDate | date: 'dd.MM.yyyy HH:mm' }}</td>
<td><button pButton type="button" severity="danger" [text]="true" [iconOnly]="true" (click)="confirmDelete(row)"><i class="pi pi-trash"></i></button></td>
</tr>
</ng-template>
</p-table>
</app-content-section>
</app-page-shell>
`,
})
export class CustomerOrdersTranslationsComponent implements OnInit {
private readonly translateService = inject(EdiCustomerOrderTranslateService);
private readonly confirmation = inject(ConfirmationService);
readonly translations = signal<EdiCustomerOrderTranslateDto[]>([]);
async ngOnInit(): Promise<void> { await this.load(); }
confirmDelete(row: EdiCustomerOrderTranslateDto): void {
this.confirmation.confirm({
message: 'Czy na pewno usunąć to powiązanie?',
header: 'Potwierdzenie',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Tak',
rejectLabel: 'Anuluj',
accept: () => this.delete(row),
});
}
private async delete(row: EdiCustomerOrderTranslateDto): Promise<void> {
await this.translateService.delete(row.id);
await this.load();
}
private async load(): Promise<void> {
const data = await this.translateService.getAll();
this.translations.set([...data].sort((a, b) => new Date(b.createdDate).getTime() - new Date(a.createdDate).getTime()));
}
}

View File

@@ -0,0 +1,93 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { Router } from '@angular/router';
import { DatePipe } from '@angular/common';
import { RowToggler, TableModule } from 'primeng/table';
import { ButtonDirective } from 'primeng/button';
import { CustomerOrderDto } from '../core/models';
import { CustomerOrderService } from '../core/services/customer-order.service';
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
@Component({
selector: 'app-customer-orders-list',
standalone: true,
imports: [TableModule, RowToggler, ButtonDirective, DatePipe, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
title="Zamówienia Klienta"
subtitle="Przegląd zamówień klientów w systemie Syteline"
icon="pi pi-shopping-cart"
>
<app-content-section title="Lista zamówień" icon="pi pi-list" [flush]="true">
<p-table [value]="orders()" [paginator]="true" [rows]="10" dataKey="rowPointer" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr>
<th style="width: 3rem"></th>
<th pSortableColumn="coNum">Numer Zamówienia</th>
<th pSortableColumn="custPo">Zamówienie Klienta</th>
<th pSortableColumn="custNum">Numer Klienta</th>
<th pSortableColumn="custSeq">Odbiorca</th>
<th pSortableColumn="createDate">Data zamówienia</th>
<th pSortableColumn="translatedStatus">Status</th>
</tr>
</ng-template>
<ng-template #body let-order let-expanded="expanded">
<tr (dblclick)="openDetail(order.rowPointer)">
<td><button pButton type="button" [text]="true" [rounded]="true" [iconOnly]="true" [pRowToggler]="order"><i [class]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i></button></td>
<td>{{ order.coNum }}</td>
<td>{{ order.custPo }}</td>
<td>{{ order.custNum }}</td>
<td>{{ order.custSeq }}</td>
<td>{{ order.createDate | date: 'dd.MM.yyyy HH:mm:ss' }}</td>
<td>{{ order.translatedStatus }}</td>
</tr>
</ng-template>
<ng-template #expandedrow let-order>
<tr><td colspan="7">
<div class="form-grid">
<div>
<u>Numer Zamówienia:</u> <b>{{ order.coNum }}</b><br />
<u>Numer Zamówienia Klienta:</u> <b>{{ order.custPo }}</b><br />
<u>Klient:</u> <b>{{ order.custNum }}</b><br />
<u>Numer Odbiorcy:</u> <b>{{ order.custSeq }}</b><br />
<u>Kontakt:</u> <b>{{ order.contact }}</b><br />
<u>Telefon:</u> <b>{{ order.phone }}</b><br />
<u>Data Zamówienia:</u> <b>{{ order.orderDate | date: 'yyyy-MM-dd HH:mm:ss' }}</b><br />
<u>Warunki:</u> <b>{{ order.termsCode }}</b><br />
<u>Wartość Brutto:</u> <b>{{ order.price?.toFixed(2) ?? 'N/A' }}</b><br />
<u>Status:</u> <b>{{ order.translatedStatus }}</b>
</div>
<div>
<u>Magazyn:</u> <b>{{ order.whse }}</b><br />
<u>VAT:</u> <b>{{ order.frtTaxCode1 }}</b><br />
<u>Typ Odbiorcy:</u> <b>{{ order.endUserType }}</b><br />
<u>Kurs Wymiany:</u> <b>{{ order.exchRate?.toFixed(4) ?? 'N/A' }}</b><br />
<u>Gate:</u> <b>{{ order.uf_FKR_EDI_Gate }}</b><br />
<u>RecipientCode:</u> <b>{{ order.uf_FKR_EDI_RecipientCode }}</b><br />
<u>SellerCode:</u> <b>{{ order.uf_FKR_EDI_SellerCode }}</b><br />
<u>SenderCode:</u> <b>{{ order.uf_FKR_EDI_SenderCode }}</b><br />
<u>BuyerCode:</u> <b>{{ order.uf_FKR_EDI_BuyerCode }}</b><br />
<u>Typ Dokumentu:</u> <b>{{ order.uf_DocType }}</b>
</div>
</div>
</td></tr>
</ng-template>
</p-table>
</app-content-section>
</app-page-shell>
`,
})
export class CustomerOrdersListComponent implements OnInit {
private readonly customerOrderService = inject(CustomerOrderService);
private readonly router = inject(Router);
readonly orders = signal<CustomerOrderDto[]>([]);
async ngOnInit(): Promise<void> {
const data = await this.customerOrderService.getAll();
this.orders.set([...data].sort((a, b) => new Date(b.createDate).getTime() - new Date(a.createDate).getTime()));
}
openDetail(id: string): void {
this.router.navigate(['/Admin/PK/CustomerOrder', id]);
}
}

View File

@@ -0,0 +1,128 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DatePipe } from '@angular/common';
import { RowToggler, SelectableRow, TableModule, TableRowSelectEvent } from 'primeng/table';
import { ButtonDirective } from 'primeng/button';
import { EdiCustomerOrderDto, EdiCustomerOrderLineDto, EdiCustomerOrderLineItemDto } from '../core/models';
import { EdiCustomerOrderService } from '../core/services/edi-customer-order.service';
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
@Component({
selector: 'app-edi-customer-order-detail',
standalone: true,
imports: [TableModule, RowToggler, SelectableRow, ButtonDirective, DatePipe, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
[title]="'Zamówienie klienta EDI nr ' + (order()?.customerOrderNumber ?? 'Brak numeru')"
subtitle="Szczegóły zamówienia otrzymanego przez EDI"
icon="pi pi-database"
>
<app-content-section title="Informacje o zamówieniu" icon="pi pi-info-circle">
<div class="form-grid">
<div>
<u>Numer zamówienia EDI:</u> <b>{{ order()?.customerOrderNumber }}</b><br />
<u>Numer zamówienia Klienta:</u> <b>{{ order()?.customerPoNumber }}</b><br />
<u>Numer klienta:</u> <b>{{ order()?.customerNumber }}</b><br />
<u>Klient:</u> <b>{{ order()?.customerName }}</b><br />
<u>Numer odbiorcy:</u> <b>{{ order()?.customerSequence ?? 'N/A' }}</b><br />
<u>Data otrzymania:</u> <b>{{ order()?.recivedDate | date: 'dd.MM.yyyy' }}</b><br />
<u>Wysłano do Syteline?:</u> <b>{{ (order()?.posted ?? 0) === 0 ? 'NIE' : 'TAK' }}</b><br />
<u>Data wysyłki do Syteline:</u> <b>{{ order()?.postedDate | date: 'dd.MM.yyyy' }}</b><br />
<u>Data zamówienia:</u> <b>{{ order()?.orderDate | date: 'dd.MM.yyyy' }}</b>
</div>
<div>
<u>Cena:</u> <b>{{ order()?.price?.toFixed(2) ?? 'N/A' }}</b><br />
<u>Waga:</u> <b>{{ order()?.weight?.toFixed(2) ?? 'N/A' }}</b><br />
<u>Magazyn:</u> <b>{{ order()?.warehouse }}</b><br />
<u>Gate:</u> <b>{{ order()?.gate }}</b><br />
<u>Kod odbiorcy:</u> <b>{{ order()?.recipientCode }}</b><br />
<u>Kod wysyłającego:</u> <b>{{ order()?.senderCode }}</b><br />
<u>Kod sprzedawcy:</u> <b>{{ order()?.sellerCode }}</b><br />
<u>Kod kupującego:</u> <b>{{ order()?.buyerCode }}</b><br />
<u>Typ dokumentu:</u> <b>{{ order()?.docType }}</b>
</div>
</div>
</app-content-section>
<app-content-section title="Indeksy" icon="pi pi-list" [flush]="true">
<p-table [value]="lines()" dataKey="customerOrderLine" [paginator]="true" [rows]="10" selectionMode="single" [(selection)]="selectedLine" (onRowSelect)="onLineSelect($event)" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr><th style="width:3rem"></th><th>Linia</th><th>Pozycja</th><th>Pozycja Klienta</th><th>Opis</th><th>Ilość</th><th>J/M</th><th>Cena</th><th>Status</th></tr>
</ng-template>
<ng-template #body let-line let-expanded="expanded">
<tr [pSelectableRow]="line">
<td><button pButton type="button" [text]="true" [rounded]="true" [iconOnly]="true" [pRowToggler]="line"><i [class]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i></button></td>
<td>{{ line.customerOrderLine }}</td><td>{{ line.item }}</td><td>{{ line.customerItemNumber }}</td><td>{{ line.description }}</td>
<td>{{ line.blanketQty?.toFixed(2) }}</td><td>{{ line.uom }}</td><td>{{ line.contPrice }}</td><td>{{ line.translatedStatus }}</td>
</tr>
</ng-template>
<ng-template #expandedrow let-line>
<tr><td colspan="9">
<div class="form-grid">
<div>
<h6>Szczegóły</h6>
<u>Numer zamówienia EDI:</u> <b>{{ line.customerOrderNumber }}</b><br />
<u>Linia:</u> <b>{{ line.customerOrderLine }}</b><br />
<u>Pozycja:</u> <b>{{ line.item }}</b><br />
<u>Pozycja Klienta:</u> <b>{{ line.customerItemNumber }}</b><br />
<u>Opis:</u> <b>{{ line.description }}</b><br />
<u>Łączna Ilość:</u> <b>{{ line.blanketQty?.toFixed(2) ?? 'N/A' }}</b><br />
<u>Status:</u> <b>{{ line.translatedStatus }}</b>
</div>
<div>
<u>Cena:</u> <b>{{ line.contPrice?.toFixed(2) ?? 'N/A' }}</b><br />
<u>Ważne Od:</u> <b>{{ line.effectiveDate | date: 'dd.MM.yyyy' }}</b><br />
<u>J/M:</u> <b>{{ line.uom }}</b><br />
<u>BoxType:</u> <b>{{ line.boxType }}</b><br />
<u>Address:</u> <b>{{ line.address }}</b><br />
<u>FinalDestination:</u> <b>{{ line.finalDestination }}</b><br />
<u>QtyPerBox:</u> <b>{{ line.qtyPerBox }}</b>
</div>
</div>
</td></tr>
</ng-template>
</p-table>
</app-content-section>
@if (lineItems().length) {
<app-content-section title="Harmonogramy" icon="pi pi-calendar" [flush]="true">
<p-table [value]="lineItems()" [paginator]="true" [rows]="10" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr><th>Linia</th><th>Zwolnienie</th><th>Pozycja</th><th>Pozycja Klienta</th><th>Łączna Ilość</th><th>Data Wykonania</th><th>Status</th></tr>
</ng-template>
<ng-template #body let-item>
<tr>
<td>{{ item.customerOrderLine }}</td><td>{{ item.customerOrderRelease }}</td><td>{{ item.item }}</td><td>{{ item.customerItem }}</td>
<td>{{ item.qtyOrdered?.toFixed(2) }}</td><td>{{ item.dueDate | date: 'dd.MM.yyyy' }}</td><td>{{ item.translatedStatus }}</td>
</tr>
</ng-template>
</p-table>
</app-content-section>
}
</app-page-shell>
`,
})
export class EdiCustomerOrderDetailComponent implements OnInit {
private readonly route = inject(ActivatedRoute);
private readonly ediService = inject(EdiCustomerOrderService);
readonly order = signal<EdiCustomerOrderDto | null>(null);
readonly lines = signal<EdiCustomerOrderLineDto[]>([]);
readonly lineItems = signal<EdiCustomerOrderLineItemDto[]>([]);
selectedLine: EdiCustomerOrderLineDto | null = null;
async ngOnInit(): Promise<void> {
const id = this.route.snapshot.paramMap.get('customerOrderId') ?? '';
const data = await this.ediService.getByRowPointer(id);
this.order.set(data);
this.lines.set(data.ediCustomerOrderLines ?? []);
}
onLineSelect(event: TableRowSelectEvent<EdiCustomerOrderLineDto>): void {
const data = event.data;
if (data && !Array.isArray(data)) {
this.lineItems.set(data.ediCustomerOrderLineItems ?? []);
}
}
}

View File

@@ -0,0 +1,109 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { Router } from '@angular/router';
import { DatePipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TableModule, TableCheckbox, TableHeaderCheckbox } from 'primeng/table';
import { ButtonDirective } from 'primeng/button';
import { ToggleSwitchModule } from 'primeng/toggleswitch';
import { DialogModule } from 'primeng/dialog';
import { EdiCustomerOrderDto, ResponseModel } from '../core/models';
import { EdiCustomerOrderService } from '../core/services/edi-customer-order.service';
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
@Component({
selector: 'app-edi-customer-orders-list',
standalone: true,
imports: [TableModule, TableCheckbox, TableHeaderCheckbox, ButtonDirective, ToggleSwitchModule, DialogModule, FormsModule, DatePipe, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
title="Zamówienia Klienta EDI"
subtitle="Zamówienia otrzymane przez EDI i księgowanie do Syteline"
icon="pi pi-database"
>
<app-content-section title="Lista zamówień EDI" icon="pi pi-list" [flush]="true">
<div class="data-toolbar mb-3">
<div class="data-toolbar__left">
<label class="font-medium">Pokaż wszystkie</label>
<p-toggleswitch [(ngModel)]="showAll" (ngModelChange)="loadData()" />
<span class="text-secondary">{{ showAll ? 'Pokaż wszystkie' : 'Pokaż tylko Wysłane do SL' }}</span>
</div>
<div class="data-toolbar__right">
@if (showPostButton()) {
<button pButton type="button" (click)="sendToSyteline()">{{ postButtonText() }}</button>
}
</div>
</div>
<p-table [value]="orders()" [(selection)]="selectedOrders" dataKey="rowPointer" [paginator]="true" [rows]="10" selectionMode="multiple" (selectionChange)="onSelectionChange()" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr><th style="width:3rem"><p-table-header-checkbox /></th><th>Numer Zamówienia</th><th>Zamówienie Klienta</th><th>Numer Klienta</th><th>Odbiorca</th><th>Data Otrzymania</th><th>Zamówienie SL</th><th>Wysłane do SL</th></tr>
</ng-template>
<ng-template #body let-order>
<tr (dblclick)="openDetail(order.rowPointer)">
<td><p-table-checkbox [value]="order" /></td>
<td>{{ order.customerOrderNumber }}</td><td>{{ order.customerPoNumber }}</td><td>{{ order.customerNumber }}</td>
<td>{{ order.customerSequence }}</td><td>{{ order.createDate | date: 'dd.MM.yyyy' }}</td>
<td>{{ order.slOrderNumber }}</td><td>{{ order.sentToSl }}</td>
</tr>
</ng-template>
</p-table>
</app-content-section>
</app-page-shell>
<p-dialog [header]="postButtonText()" [(visible)]="showResultDialog" [modal]="true" [style]="{ width: '500px' }">
@for (r of responses(); track r.identifier) {
@if (r.status === 1) {
<p>Zamówienie EDI {{ r.identifier }} zostało poprawnie zaksięgowane w Zamówieniach klienta pod numerem '{{ r.externalIdentifier }}'</p>
} @else {
<p>Błąd: Zamówienie EDI {{ r.identifier }} nie zostało poprawnie zaksięgowane w Zamówieniach klienta.<br />Lista błędów:<br />{{ r.message }}</p>
}
}
<ng-template #footer><button pButton type="button" (click)="showResultDialog = false">OK</button></ng-template>
</p-dialog>
`,
})
export class EdiCustomerOrdersListComponent implements OnInit {
private readonly ediService = inject(EdiCustomerOrderService);
private readonly router = inject(Router);
readonly orders = signal<EdiCustomerOrderDto[]>([]);
readonly responses = signal<ResponseModel[]>([]);
readonly showPostButton = signal(false);
readonly postButtonText = signal('Księguj bieżący');
showAll = false;
selectedOrders: EdiCustomerOrderDto[] = [];
showResultDialog = false;
async ngOnInit(): Promise<void> { await this.loadData(); }
async loadData(): Promise<void> {
let data = await this.ediService.getAll();
if (!this.showAll) data = data.filter((x) => x.posted === 0);
this.orders.set([...data].sort((a, b) => new Date(b.createDate).getTime() - new Date(a.createDate).getTime()));
}
onSelectionChange(): void {
this.showPostButton.set(this.selectedOrders.some((x) => x.posted === 0));
this.postButtonText.set(this.selectedOrders.length > 1 ? 'Księguj zaznaczone' : 'Księguj bieżący');
}
async sendToSyteline(): Promise<void> {
if (!this.selectedOrders.length) return;
const results: ResponseModel[] = [];
for (const order of this.selectedOrders) {
const response = await this.ediService.sendToSyteline(order.rowPointer, order.customerOrderNumber);
if (response.status === 1) {
response.externalIdentifier = order.ediCustomerOrderTranslates?.[0]?.coCoNum ?? '';
}
results.push(response);
}
this.responses.set(results);
this.showResultDialog = true;
this.showPostButton.set(false);
await this.loadData();
}
openDetail(id: string): void { this.router.navigate(['/Admin/PK/EdiCustomerOrder', id]); }
}

View File

@@ -0,0 +1,33 @@
import { Component } from '@angular/core';
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
@Component({
selector: 'app-error',
standalone: true,
imports: [PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell title="Error" subtitle="An error occurred while processing your request" icon="pi pi-exclamation-triangle">
<app-content-section>
<h1 class="error-title">Error.</h1>
<h2 class="error-subtitle">An error occurred while processing your request.</h2>
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the
<strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
</app-content-section>
</app-page-shell>
`,
styles: `
.error-title { color: var(--color-error); margin: 0 0 var(--spacing-2); }
.error-subtitle { color: var(--color-error); font-size: var(--font-size-lg); margin: 0 0 var(--spacing-4); }
`,
})
export class ErrorComponent {}

View File

@@ -0,0 +1,104 @@
import { Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ProgressSpinner } from 'primeng/progressspinner';
import { ToastModule } from 'primeng/toast';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { SidebarComponent } from '../sidebar/sidebar.component';
import { TopbarComponent } from '../topbar/topbar.component';
import { LoadingService } from '../../core/services/loading.service';
@Component({
selector: 'app-main-layout',
standalone: true,
imports: [RouterOutlet, SidebarComponent, TopbarComponent, ProgressSpinner, ToastModule, ConfirmDialogModule],
template: `
<p-toast position="top-right" />
<p-confirm-dialog />
@if (loading.isLoading()) {
<div class="loading-overlay" role="status" aria-live="polite" aria-label="Ładowanie">
<div class="loading-overlay__card">
<p-progress-spinner ariaLabel="Ładowanie" strokeWidth="3" styleClass="loading-spinner" />
<span>Ładowanie danych…</span>
</div>
</div>
}
<div class="app-shell">
<app-sidebar />
<div class="app-shell__main">
<app-topbar />
<main class="app-shell__content">
<router-outlet />
</main>
<footer class="app-shell__footer">
FA Krosno Manager © {{ year }}
</footer>
</div>
</div>
`,
styles: `
.app-shell {
display: flex;
height: 100dvh;
overflow: hidden;
background: var(--color-bg);
}
.app-shell__main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.app-shell__content {
flex: 1;
overflow: auto;
padding: var(--content-padding);
}
.app-shell__footer {
flex-shrink: 0;
padding: var(--spacing-2) var(--content-padding);
text-align: center;
font-size: var(--font-size-xs);
color: var(--color-text-muted);
border-top: 1px solid var(--color-border);
background: var(--color-surface);
}
.loading-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(15, 23, 42, 0.35);
backdrop-filter: blur(2px);
}
.loading-overlay__card {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-3);
padding: var(--spacing-5) var(--spacing-6);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
}
:host ::ng-deep .loading-spinner {
width: 36px;
height: 36px;
}
`,
})
export class MainLayoutComponent {
readonly loading = inject(LoadingService);
readonly year = new Date().getFullYear();
}

View File

@@ -0,0 +1,254 @@
import { Component, computed, inject, signal } from '@angular/core';
import { Router, RouterLink, RouterLinkActive, NavigationEnd } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { filter, map, startWith } from 'rxjs';
import { TooltipModule } from 'primeng/tooltip';
interface NavItem {
label: string;
icon: string;
route: string;
exact?: boolean;
}
interface NavGroup {
label?: string;
items: NavItem[];
}
@Component({
selector: 'app-sidebar',
standalone: true,
imports: [RouterLink, RouterLinkActive, TooltipModule],
template: `
<aside class="sidebar" [class.sidebar--collapsed]="collapsed()">
<div class="sidebar__brand">
@if (!collapsed()) {
<img src="logo.svg" width="28" height="28" alt="" class="sidebar__logo" />
<span class="sidebar__brand-text">FA Manager</span>
} @else {
<img src="logo.svg" width="24" height="24" alt="" class="sidebar__logo" />
}
</div>
<nav class="sidebar__nav">
@for (group of navGroups(); track group.label ?? 'main') {
@if (group.label && !collapsed()) {
<div class="sidebar__group-label">{{ group.label }}</div>
}
@for (item of group.items; track item.route) {
<a
class="sidebar__link"
[routerLink]="item.route"
routerLinkActive="sidebar__link--active"
[routerLinkActiveOptions]="{ exact: item.exact ?? false }"
[pTooltip]="collapsed() ? item.label : undefined"
tooltipPosition="right"
>
<i [class]="item.icon" class="sidebar__link-icon"></i>
@if (!collapsed()) {
<span class="sidebar__link-label">{{ item.label }}</span>
}
</a>
}
}
</nav>
<div class="sidebar__footer">
<button type="button" class="sidebar__collapse-btn" (click)="toggleCollapsed()" [attr.aria-label]="collapsed() ? 'Rozwiń menu' : 'Zwiń menu'">
<i [class]="collapsed() ? 'pi pi-angle-double-right' : 'pi pi-angle-double-left'"></i>
@if (!collapsed()) {
<span>Zwiń menu</span>
}
</button>
</div>
</aside>
`,
styles: `
.sidebar {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
height: 100%;
display: flex;
flex-direction: column;
background: var(--color-sidebar-bg);
border-right: 1px solid var(--color-sidebar-border);
transition: width 0.2s ease, min-width 0.2s ease;
overflow: hidden;
}
.sidebar--collapsed {
width: var(--sidebar-collapsed-width);
min-width: var(--sidebar-collapsed-width);
}
.sidebar__brand {
display: flex;
align-items: center;
gap: var(--spacing-3);
height: var(--topbar-height);
padding: 0 var(--spacing-4);
border-bottom: 1px solid var(--color-sidebar-border);
flex-shrink: 0;
}
.sidebar--collapsed .sidebar__brand {
justify-content: center;
padding: 0;
}
.sidebar__logo {
filter: brightness(0) invert(1);
opacity: 0.9;
}
.sidebar__brand-text {
font-size: var(--font-size-md);
font-weight: 700;
color: var(--color-sidebar-text-active);
letter-spacing: -0.01em;
white-space: nowrap;
}
.sidebar__nav {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: var(--spacing-3) var(--spacing-2);
}
.sidebar__group-label {
padding: var(--spacing-3) var(--spacing-3) var(--spacing-1);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-sidebar-text-muted);
}
.sidebar__link {
display: flex;
align-items: center;
gap: var(--spacing-3);
padding: 9px var(--spacing-3);
margin-bottom: 2px;
border-radius: var(--radius-sm);
color: var(--color-sidebar-text);
text-decoration: none;
font-size: var(--font-size-sm);
font-weight: 500;
transition: background 0.15s, color 0.15s;
white-space: nowrap;
}
.sidebar--collapsed .sidebar__link {
justify-content: center;
padding: 10px;
}
.sidebar__link:hover {
background: var(--color-sidebar-surface);
color: var(--color-sidebar-text-active);
}
.sidebar__link--active {
background: var(--color-sidebar-accent-soft) !important;
color: var(--color-sidebar-text-active) !important;
}
.sidebar__link--active .sidebar__link-icon {
color: var(--color-sidebar-accent);
}
.sidebar__link-icon {
font-size: 15px;
width: 18px;
text-align: center;
flex-shrink: 0;
color: var(--color-sidebar-text-muted);
}
.sidebar__link-label {
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar__footer {
padding: var(--spacing-2);
border-top: 1px solid var(--color-sidebar-border);
flex-shrink: 0;
}
.sidebar__collapse-btn {
display: flex;
align-items: center;
gap: var(--spacing-2);
width: 100%;
padding: var(--spacing-2) var(--spacing-3);
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-sidebar-text-muted);
font-size: var(--font-size-xs);
font-family: inherit;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.sidebar--collapsed .sidebar__collapse-btn {
justify-content: center;
padding: var(--spacing-2);
}
.sidebar__collapse-btn:hover {
background: var(--color-sidebar-surface);
color: var(--color-sidebar-text);
}
`,
})
export class SidebarComponent {
private readonly router = inject(Router);
readonly collapsed = signal(false);
private readonly url = toSignal(
this.router.events.pipe(
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
map((e) => e.urlAfterRedirects),
startWith(this.router.url),
),
{ initialValue: this.router.url },
);
readonly isAdminRoute = computed(() => this.url().split('?')[0].toLowerCase().startsWith('/admin'));
readonly navGroups = computed<NavGroup[]>(() => {
const main: NavGroup = {
items: [
{ label: 'Zamówienia DELFOR', icon: 'pi pi-building', route: '/', exact: true },
{ label: 'Zarządzanie Indeksami', icon: 'pi pi-box', route: '/Products' },
{ label: 'Magazyn', icon: 'pi pi-warehouse', route: '/Warehouse' },
],
};
const groups = [main];
if (this.isAdminRoute()) {
groups.push({
label: 'Administracja',
items: [
{ label: 'Użytkownicy', icon: 'pi pi-users', route: '/Admin/PK/UsersManager' },
{ label: 'Scheduler', icon: 'pi pi-calendar', route: '/Admin/PK/Scheduler' },
{ label: 'Zamówienia klienta EDI', icon: 'pi pi-list-check', route: '/Admin/PK/EdiCustomerOrders' },
{ label: 'Zamówienia klienta', icon: 'pi pi-database', route: '/Admin/PK/CustomerOrders' },
],
});
}
return groups;
});
toggleCollapsed(): void {
this.collapsed.update((v) => !v);
}
}

View File

@@ -0,0 +1,203 @@
import { Component, computed, inject, signal } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { filter, map, startWith } from 'rxjs';
import { ButtonDirective } from 'primeng/button';
import { BreadcrumbModule } from 'primeng/breadcrumb';
import { MenuModule } from 'primeng/menu';
import { MenuItem } from 'primeng/api';
import { AuthService } from '../../core/services/auth.service';
@Component({
selector: 'app-topbar',
standalone: true,
imports: [ButtonDirective, BreadcrumbModule, MenuModule],
template: `
<header class="topbar">
<div class="topbar__breadcrumbs">
<p-breadcrumb [model]="breadcrumbItems()" [home]="homeItem" />
</div>
<div class="topbar__actions">
<button pButton type="button" severity="secondary" [text]="true" [rounded]="true" [iconOnly]="true" aria-label="Przełącz tryb ciemny" (click)="toggleTheme()"><i [class]="isDark() ? 'pi pi-sun' : 'pi pi-moon'"></i></button>
<button type="button" class="topbar__user" (click)="userMenu.toggle($event)">
<span class="topbar__avatar">{{ userInitials() }}</span>
<span class="topbar__username">{{ userLabel() }}</span>
<i class="pi pi-chevron-down topbar__chevron"></i>
</button>
<p-menu #userMenu [model]="userMenuItems" [popup]="true" />
</div>
</header>
`,
styles: `
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--topbar-height);
padding: 0 var(--content-padding);
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
gap: var(--spacing-4);
}
.topbar__breadcrumbs {
flex: 1;
min-width: 0;
overflow: hidden;
}
.topbar__actions {
display: flex;
align-items: center;
gap: var(--spacing-2);
flex-shrink: 0;
}
.topbar__user {
display: flex;
align-items: center;
gap: var(--spacing-2);
padding: 4px 8px 4px 4px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface);
cursor: pointer;
font-family: inherit;
transition: background 0.15s, border-color 0.15s;
}
.topbar__user:hover {
background: var(--color-surface-hover);
border-color: var(--color-border-strong);
}
.topbar__avatar {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: var(--radius-sm);
background: var(--color-accent);
color: var(--color-text-inverse);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
}
.topbar__username {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--color-text-primary);
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.topbar__chevron {
font-size: 10px;
color: var(--color-text-muted);
}
:host ::ng-deep .p-breadcrumb {
background: transparent;
border: none;
padding: 0;
}
:host ::ng-deep .p-breadcrumb-list {
flex-wrap: nowrap;
}
:host ::ng-deep .p-breadcrumb-item-label {
font-size: var(--font-size-sm);
}
@media (max-width: 640px) {
.topbar__username {
display: none;
}
}
`,
})
export class TopbarComponent {
private readonly router = inject(Router);
private readonly auth = inject(AuthService);
readonly isDark = signal(document.documentElement.getAttribute('data-theme') === 'dark');
readonly homeItem: MenuItem = { icon: 'pi pi-home', routerLink: '/' };
private readonly url = toSignal(
this.router.events.pipe(
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
map((e) => e.urlAfterRedirects),
startWith(this.router.url),
),
{ initialValue: this.router.url },
);
readonly breadcrumbItems = computed<MenuItem[]>(() => {
const segments = this.url()
.split('?')[0]
.split('/')
.filter(Boolean);
const labels: Record<string, string> = {
Products: 'Zarządzanie Indeksami',
Warehouse: 'Magazyn',
ScheduleOrder: 'Zamówienie DELFOR',
Admin: 'Administracja',
PK: 'PK',
UsersManager: 'Użytkownicy',
Scheduler: 'Scheduler',
EdiCustomerOrders: 'Zamówienia klienta EDI',
EdiCustomerOrder: 'Zamówienie EDI',
CustomerOrders: 'Zamówienia klienta',
CustomerOrder: 'Zamówienie klienta',
Meyle: 'Meyle',
Marelli: 'Marelli',
PackList: 'Packing List',
Simple: 'Widok prosty',
};
let path = '';
return segments.map((seg) => {
path += `/${seg}`;
return { label: labels[seg] ?? seg, routerLink: path };
});
});
readonly userLabel = computed(() => this.auth.userName() || 'Użytkownik');
readonly userInitials = computed(() => {
const name = this.userLabel();
return name.slice(0, 2).toUpperCase();
});
readonly userMenuItems: MenuItem[] = [
{
label: 'Wyloguj',
icon: 'pi pi-sign-out',
command: () => this.logout(),
},
];
toggleTheme(): void {
const next = this.isDark() ? 'light' : 'dark';
if (next === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.removeAttribute('data-theme');
}
this.isDark.set(next === 'dark');
}
logout(): void {
this.auth.logout();
this.router.navigate(['/login']);
}
}

View File

@@ -0,0 +1,18 @@
import { Component, inject, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../core/services/auth.service';
@Component({
selector: 'app-main',
standalone: true,
template: '',
})
export class MainComponent implements OnInit {
private readonly auth = inject(AuthService);
private readonly router = inject(Router);
ngOnInit(): void {
this.auth.logout();
this.router.navigate(['/login']);
}
}

View File

@@ -0,0 +1,165 @@
import { Component, ElementRef, inject, OnInit, signal, viewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { ButtonDirective } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { DialogModule } from 'primeng/dialog';
import { TransactionModel, WzRowMarelliDto } from '../../core/models';
import { WarehouseService } from '../../core/services/warehouse.service';
import { findMarelliCombinations } from '../../shared/utils/business-logic';
import { PageShellComponent } from '../../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../../shared/components/content-section/content-section.component';
@Component({
selector: 'app-marelli-pack-list-simple',
standalone: true,
imports: [ButtonDirective, InputTextModule, DialogModule, FormsModule, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
title="Packing List"
subtitle="Packing list Marelli — widok uproszczony ze skanowaniem"
icon="pi pi-box"
>
<div actions>
<button pButton type="button" (click)="changeView()"><i class="pi pi-eye"></i> Zmień widok</button>
<button pButton type="button" (click)="exportXls()"><i class="pi pi-file-excel"></i> Generuj XLS i Wyślij</button>
</div>
<app-content-section title="Dane wysyłki" icon="pi pi-envelope">
<div class="form-grid form-grid--2">
<div class="field-group">
<label class="field-label">Adresy Email do Wysyłki raportu:</label>
<input pInputText class="w-full" placeholder="Wprowadź adresy..." [(ngModel)]="emailAddresses" />
</div>
<div class="field-group">
<label class="field-label">Numer WZ:</label>
<input pInputText class="w-full" [(ngModel)]="wzNumber" />
</div>
</div>
</app-content-section>
<app-content-section title="Skanowanie" icon="pi pi-barcode">
<div class="form-grid form-grid--2">
<div class="field-group">
<label class="field-label">Wprowadź numer palety:</label>
<input pInputText type="number" class="w-full" [(ngModel)]="palletNumber" />
</div>
<div class="field-group">
<label class="field-label">Zeskanowana wartość:</label>
<input #scanner pInputText class="w-full" [(ngModel)]="scannedValue" (change)="scanValue()" />
</div>
</div>
</app-content-section>
<app-content-section title="Ostatnio zeskanowana pozycja" icon="pi pi-info-circle">
<div class="form-grid form-grid--3">
<div class="field-group"><label class="field-label">Numer Indeksu FA:</label><input pInputText readonly class="w-full" [(ngModel)]="itemNumber" /></div>
<div class="field-group"><label class="field-label">Ilość w Dostawie:</label><input pInputText readonly class="w-full" [(ngModel)]="qty" /></div>
<div class="field-group"><label class="field-label">Numer Palety:</label><input pInputText readonly class="w-full" [(ngModel)]="palletNumberOutput" /></div>
</div>
</app-content-section>
</app-page-shell>
<p-dialog header="Informacja" [(visible)]="showInfoDialog" [modal]="true" [style]="{ width: '500px' }">
@if (exportValid()) { <p>Packing List został wygenerowany i wysłany!</p> }
@else if (!emailAddresses?.trim()) { <p>Błąd: Proszę wprowadzić przynajmniej jeden <b>ADRES EMAIL</b> do wysyłki raportu!</p> }
@else { <p>Błąd: Nie Wszystkie linie mają wypełniony <b>NUMER PALETY</b>.<br />Packing List nie zostanie wygenerowany!</p> }
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
</p-dialog>
<p-dialog header="Błąd" [(visible)]="showPalletDialog" [modal]="true" [style]="{ width: '500px' }">
<p>Błąd skanowania! <b>Wybierz NUMER PALETY większy niż 0</b> (Aktualnie '{{ palletNumber }}'):</p>
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
</p-dialog>
`,
})
export class MarelliPackListSimpleComponent implements OnInit {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly warehouseService = inject(WarehouseService);
readonly scanner = viewChild<ElementRef<HTMLInputElement>>('scanner');
wzHeaderId = '';
rows: WzRowMarelliDto[] = [];
transactionModels: Record<string, TransactionModel[]> = {};
changedRecords: WzRowMarelliDto[] = [];
selectedRow: WzRowMarelliDto | null = null;
selectedRows: WzRowMarelliDto[] = [];
emailAddresses = '';
wzNumber = '';
palletNumber = '0';
scannedValue = '';
itemNumber = '';
qty = '0';
palletNumberOutput = '0';
showInfoDialog = false;
showPalletDialog = false;
readonly exportValid = signal(false);
async ngOnInit(): Promise<void> {
this.wzHeaderId = this.route.snapshot.paramMap.get('WzHeader') ?? '';
const header = await this.warehouseService.getWzHeaderById(this.wzHeaderId);
this.rows = await this.warehouseService.getWzRowsMarelliByWzHeaderId(this.wzHeaderId);
this.transactionModels = await this.warehouseService.getTransactionsModels();
this.emailAddresses = header.emailAddresses ?? '';
this.wzNumber = header.wzNumbers ?? '';
setTimeout(() => this.scanner()?.nativeElement.focus());
}
changeView(): void { this.router.navigate(['/Warehouse/Marelli/PackList', this.wzHeaderId]); }
hideModal(): void { this.showInfoDialog = false; this.showPalletDialog = false; this.scannedValue = ''; setTimeout(() => this.scanner()?.nativeElement.focus()); }
async saveChanges(): Promise<void> {
if (this.emailAddresses?.trim()) await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
if (this.changedRecords.length) {
await this.warehouseService.updateWzRowsMarelli(this.changedRecords);
this.rows = await this.warehouseService.getWzRowsMarelliByWzHeaderId(this.wzHeaderId);
}
}
async exportXls(): Promise<void> {
const valid = !this.rows.some((x) => x.palletNumber == null) && !!this.emailAddresses?.trim();
this.exportValid.set(valid);
if (valid) {
await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
await this.warehouseService.generateXlsForMarelli(this.wzHeaderId);
}
this.showInfoDialog = true;
}
async scanValue(): Promise<void> {
const value = this.scannedValue.trim();
if (!value || parseInt(this.palletNumber, 10) <= 0) { this.showPalletDialog = true; return; }
const material = this.transactionModels[value]?.[0];
if (material) await this.fillPalletNumber(material);
this.scannedValue = '';
setTimeout(() => this.scanner()?.nativeElement.focus());
}
private async fillPalletNumber(material: TransactionModel): Promise<void> {
const palletNum = parseInt(this.palletNumber, 10);
let rowIndex = this.rows.findIndex((x) => x.faIndex === material.itemNumber && x.quantity === material.quantity);
if (rowIndex === -1) {
this.selectedRows = this.rows.filter((x) => x.faIndex === material.itemNumber);
const validCombinations = findMarelliCombinations(this.selectedRows, material.quantity ?? 0);
for (const combination of validCombinations) {
for (const record of combination) {
record.palletNumber = palletNum;
this.changedRecords.push(record);
}
}
this.selectedRows = [...this.changedRecords];
this.selectedRow = this.selectedRows[0] ?? null;
} else {
this.selectedRow = this.rows[rowIndex];
this.selectedRow.palletNumber = palletNum;
if (!this.changedRecords.some((x) => x.transactionNumber === this.selectedRow!.transactionNumber)) this.changedRecords.push(this.selectedRow);
}
this.itemNumber = this.selectedRow?.faIndex ?? '';
this.qty = String(this.selectedRow?.quantity ?? 0);
this.palletNumberOutput = String(this.selectedRow?.palletNumber ?? 0);
await this.saveChanges();
this.changedRecords = [];
}
}

View File

@@ -0,0 +1,223 @@
import { Component, ElementRef, inject, OnInit, signal, viewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { SelectableRow, TableModule, TableRowSelectEvent } from 'primeng/table';
import { ButtonDirective } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { DialogModule } from 'primeng/dialog';
import { TransactionModel, WzRowMarelliDto } from '../../core/models';
import { WarehouseService } from '../../core/services/warehouse.service';
import { findMarelliCombinations } from '../../shared/utils/business-logic';
import { PageShellComponent } from '../../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../../shared/components/content-section/content-section.component';
@Component({
selector: 'app-marelli-pack-list',
standalone: true,
imports: [TableModule, SelectableRow, ButtonDirective, InputTextModule, DialogModule, FormsModule, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
title="Packing List"
subtitle="Packing list Marelli — edycja pozycji i skanowanie"
icon="pi pi-box"
>
<app-content-section title="Dane wysyłki" icon="pi pi-envelope">
<div class="form-grid form-grid--2">
<div class="field-group">
<label class="field-label">Adresy Email do Wysyłki raportu:</label>
<input pInputText class="w-full" placeholder="Wprowadź adresy..." [(ngModel)]="emailAddresses" />
</div>
<div class="field-group">
<label class="field-label">Numer WZ:</label>
<input pInputText class="w-full" [(ngModel)]="wzNumber" />
</div>
</div>
</app-content-section>
<app-content-section title="Skanowanie" icon="pi pi-barcode">
<div class="form-grid form-grid--2">
<div class="field-group">
<label class="field-label">Wprowadź numer palety:</label>
<input pInputText type="number" class="w-full" [(ngModel)]="palletNumber" />
</div>
<div class="field-group">
<label class="field-label">Zeskanowana wartość:</label>
<input #scanner pInputText class="w-full" [(ngModel)]="scannedValue" (change)="scanValue()" />
</div>
</div>
</app-content-section>
<app-content-section title="Pozycje packing list" icon="pi pi-list" [flush]="true">
<div headerActions>
<button pButton type="button" (click)="saveChanges()"><i class="pi pi-save"></i> Zapisz zmiany</button>
<button pButton type="button" (click)="exportXls()"><i class="pi pi-file-excel"></i> Generuj XLS i Wyślij</button>
</div>
<p-table [value]="rows()" [(selection)]="selectedRowsList" dataKey="id" [paginator]="true" [rows]="10" selectionMode="single" (onRowSelect)="onRowSelect($event)" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr>
<th>Nr Palety</th><th>Numer Indeksu Marelli</th><th>Numer Inżynieryjny</th>
<th>Ilość w Dostawie</th><th>Nr Zamówienia</th><th>Nr WZ</th>
</tr>
</ng-template>
<ng-template #body let-row>
<tr [pSelectableRow]="row">
<td><input pInputText type="number" [(ngModel)]="row.palletNumber" (ngModelChange)="markChanged(row)" /></td>
<td>{{ row.itemNumber }}</td>
<td>{{ row.engineerNumber }}</td>
<td>{{ row.quantity }}</td>
<td><input pInputText [(ngModel)]="row.orderNumber" (ngModelChange)="markChanged(row)" /></td>
<td><input pInputText [(ngModel)]="row.wzNumber" (ngModelChange)="markChanged(row)" /></td>
</tr>
</ng-template>
</p-table>
</app-content-section>
</app-page-shell>
<p-dialog header="Informacja" [(visible)]="showInfoDialog" [modal]="true" [style]="{ width: '500px' }">
@if (exportValid()) { <p>Packing List został wygenerowany i wysłany!</p> }
@else if (!emailAddresses?.trim()) { <p>Błąd: Proszę wprowadzić przynajmniej jeden <b>ADRES EMAIL</b> do wysyłki raportu!</p> }
@else { <p>Błąd: Nie Wszystkie linie mają wypełniony <b>NUMER PALETY</b>.<br />Packing List nie zostanie wygenerowany!</p> }
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
</p-dialog>
<p-dialog header="Błąd" [(visible)]="showPalletDialog" [modal]="true" [style]="{ width: '500px' }">
<p>Błąd skanowania! <b>Wybierz NUMER PALETY większy niż 0</b> (Aktualnie '{{ palletNumber }}'):</p>
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
</p-dialog>
<p-dialog header="Błąd" [(visible)]="showIndexQtyDialog" [modal]="true" [style]="{ width: '500px' }">
<p>Nie znaleziono indeksu z ilością sztuk ('<b>{{ indexWithQty }}</b>') odpowiadającemu skanowanym wartościom!</p>
<p>Znaleziono indeks, który ma ilość sztuk: '<b>{{ indexQty }}</b>'!</p>
<p><b>Uzupełnij numer palety ręcznie i kliknij 'Zapisz'!</b></p>
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
</p-dialog>
<p-dialog header="Błąd" [(visible)]="showNotFoundDialog" [modal]="true" [style]="{ width: '500px' }">
<p>Na liście nie znaleziono skanowanego numeru partii '<b>{{ notFoundItem }}</b>'!</p>
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
</p-dialog>
`,
})
export class MarelliPackListComponent implements OnInit {
private readonly route = inject(ActivatedRoute);
private readonly warehouseService = inject(WarehouseService);
readonly scanner = viewChild<ElementRef<HTMLInputElement>>('scanner');
wzHeaderId = '';
emailAddresses = '';
wzNumber = '';
palletNumber = '0';
scannedValue = '';
indexWithQty = '';
indexQty = '';
notFoundItem = '';
readonly rows = signal<WzRowMarelliDto[]>([]);
readonly selectedRow = signal<WzRowMarelliDto | null>(null);
selectedRowsList: WzRowMarelliDto[] = [];
changedRecords: WzRowMarelliDto[] = [];
transactionModels: Record<string, TransactionModel[]> = {};
showInfoDialog = false;
showPalletDialog = false;
showIndexQtyDialog = false;
showNotFoundDialog = false;
readonly exportValid = signal(false);
async ngOnInit(): Promise<void> {
this.wzHeaderId = this.route.snapshot.paramMap.get('WzHeader') ?? '';
const header = await this.warehouseService.getWzHeaderById(this.wzHeaderId);
this.rows.set(await this.warehouseService.getWzRowsMarelliByWzHeaderId(this.wzHeaderId));
this.transactionModels = await this.warehouseService.getTransactionsModels();
this.emailAddresses = header.emailAddresses ?? '';
this.wzNumber = header.wzNumbers ?? '';
setTimeout(() => this.scanner()?.nativeElement.focus());
}
onRowSelect(event: TableRowSelectEvent<WzRowMarelliDto>): void {
const data = event.data;
if (data && !Array.isArray(data)) this.selectedRow.set(data);
}
markChanged(row: WzRowMarelliDto): void { if (!this.changedRecords.includes(row)) this.changedRecords.push(row); }
hideModal(): void {
this.showInfoDialog = false;
this.showPalletDialog = false;
this.showIndexQtyDialog = false;
this.showNotFoundDialog = false;
this.scannedValue = '';
this.indexQty = '';
this.indexWithQty = '';
this.notFoundItem = '';
setTimeout(() => this.scanner()?.nativeElement.focus());
}
async saveChanges(): Promise<void> {
if (this.emailAddresses?.trim()) await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
if (this.changedRecords.length) {
await this.updateRows(this.changedRecords);
this.changedRecords = [];
}
this.selectedRowsList = [];
}
async exportXls(): Promise<void> {
const valid = !this.rows().some((x) => x.palletNumber == null) && !!this.emailAddresses?.trim();
this.exportValid.set(valid);
if (valid) {
await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
await this.warehouseService.generateXlsForMarelli(this.wzHeaderId);
}
this.showInfoDialog = true;
}
async scanValue(): Promise<void> {
const value = this.scannedValue.trim();
if (!value || parseInt(this.palletNumber, 10) <= 0) { this.showPalletDialog = true; return; }
const material = this.transactionModels[value]?.[0];
if (material) {
this.selectedRowsList = [];
await this.fillPalletNumber(material, value);
} else {
this.notFoundItem = value;
this.showNotFoundDialog = true;
}
this.scannedValue = '';
setTimeout(() => this.scanner()?.nativeElement.focus());
}
private async fillPalletNumber(material: TransactionModel, scanned: string): Promise<void> {
const palletNum = parseInt(this.palletNumber, 10);
const currentRows = this.rows();
let rowIndex = currentRows.findIndex((x) => x.faIndex === material.itemNumber && x.quantity === material.quantity);
if (rowIndex === -1) {
const candidates = currentRows.filter((x) => x.faIndex === material.itemNumber);
if (!candidates.length) return;
rowIndex = currentRows.findIndex((x) => x.faIndex === candidates[0].faIndex && x.quantity === candidates[0].quantity);
const validCombinations = findMarelliCombinations(candidates, material.quantity ?? 0);
if (!validCombinations.length && candidates.length > 0) {
this.indexWithQty = `${material.itemNumber}, Qty = ${material.quantity}`;
this.indexQty = String(candidates[0].quantity ?? 0);
this.showIndexQtyDialog = true;
return;
}
for (const combination of validCombinations) {
for (const record of combination) {
record.palletNumber = palletNum;
this.changedRecords.push(record);
}
}
this.selectedRowsList = [...this.changedRecords];
this.selectedRow.set(this.selectedRowsList[0] ?? null);
} else {
const row = currentRows[rowIndex];
this.selectedRow.set(row);
row.palletNumber = palletNum;
if (!this.changedRecords.some((x) => x.transactionNumber === row.transactionNumber)) this.changedRecords.push(row);
}
await this.saveChanges();
}
private async updateRows(changed: WzRowMarelliDto[]): Promise<void> {
await this.warehouseService.updateWzRowsMarelli(changed);
this.rows.set(await this.warehouseService.getWzRowsMarelliByWzHeaderId(this.wzHeaderId));
}
}

View File

@@ -0,0 +1,217 @@
import { Component, ElementRef, inject, OnInit, signal, viewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { ButtonDirective } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { DialogModule } from 'primeng/dialog';
import { TransactionModel, WzRowMeyleDto } from '../../core/models';
import { WarehouseService } from '../../core/services/warehouse.service';
import { findCombinations, isValidMeyleScannedValue } from '../../shared/utils/business-logic';
import { PageShellComponent } from '../../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../../shared/components/content-section/content-section.component';
@Component({
selector: 'app-meyle-pack-list-simple',
standalone: true,
imports: [ButtonDirective, InputTextModule, DialogModule, FormsModule, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
title="Packing List"
subtitle="Packing list Meyle — widok uproszczony ze skanowaniem"
icon="pi pi-box"
>
<div actions>
<button pButton type="button" (click)="changeView()"><i class="pi pi-eye"></i> Zmień widok</button>
<button pButton type="button" (click)="exportXls()"><i class="pi pi-file-excel"></i> Generuj XLS i Wyślij</button>
</div>
<app-content-section title="Dane wysyłki" icon="pi pi-envelope">
<div class="form-grid form-grid--2">
<div class="field-group">
<label class="field-label">Adresy Email do Wysyłki raportu:</label>
<input pInputText class="w-full" placeholder="Wprowadź adresy..." [(ngModel)]="emailAddresses" />
</div>
<div class="field-group">
<label class="field-label">Numer WZ:</label>
<input pInputText class="w-full" [(ngModel)]="wzNumber" />
</div>
</div>
</app-content-section>
<app-content-section title="Skanowanie" icon="pi pi-barcode">
<div class="form-grid form-grid--2">
<div class="field-group">
<label class="field-label">Wprowadź numer palety:</label>
<input pInputText type="number" class="w-full" [(ngModel)]="palletNumber" />
</div>
<div class="field-group">
<label class="field-label">Zeskanowana wartość:</label>
<input #scanner pInputText class="w-full" [(ngModel)]="scannedValue" (change)="scanValue()" />
</div>
</div>
</app-content-section>
<app-content-section title="Ostatnio zeskanowana pozycja" icon="pi pi-info-circle">
<div class="form-grid form-grid--3">
<div class="field-group"><label class="field-label">Numer Indeksu FA:</label><input pInputText readonly class="w-full" [(ngModel)]="itemNumber" /></div>
<div class="field-group"><label class="field-label">Ilość w Dostawie:</label><input pInputText readonly class="w-full" [(ngModel)]="qty" /></div>
<div class="field-group"><label class="field-label">Numer Palety:</label><input pInputText readonly class="w-full" [(ngModel)]="palletNumberOutput" /></div>
<div class="field-group"><label class="field-label">Nr Partii SL:</label><input pInputText readonly class="w-full" [(ngModel)]="partNumberSl" /></div>
<div class="field-group"><label class="field-label">Numer Partii Meyle:</label><input pInputText readonly class="w-full" [(ngModel)]="partNumberMeyle" /></div>
</div>
</app-content-section>
</app-page-shell>
<p-dialog header="Informacja" [(visible)]="showInfoDialog" [modal]="true" [style]="{ width: '500px' }">
@if (exportValid()) { <p>Packing List został wygenerowany i wysłany!</p> }
@else if (!emailAddresses?.trim()) { <p>Błąd: Proszę wprowadzić przynajmniej jeden <b>ADRES EMAIL</b> do wysyłki raportu!</p> }
@else { <p>Błąd: Nie Wszystkie linie mają wypełniony <b>NUMER PALETY</b>.<br />Packing List nie zostanie wygenerowany!</p> }
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
</p-dialog>
<p-dialog header="Błąd" [(visible)]="showValidationDialog" [modal]="true" [style]="{ width: '500px' }">
<p>Błąd skanowania! Wystąpił jeden z wyjątków (Zeskanowana wartość '<b>{{ lastScannedValue }}</b>'):</p>
<ul>
<li>Zeskanowano niepoprawny Numer Partii SL (nieistniejący w tabeli)</li>
<li>Zeskanowano niepoprawny numer Partii Meyle (niezaczynający się od <b>{{ yearPrefix }}X</b>)</li>
<li>Numer Palety nie jest większy niż 0 (aktualnie wybrany numer palety: '<b>{{ palletNumber }}</b>')</li>
</ul>
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
</p-dialog>
<p-dialog header="Błąd" [(visible)]="showPalletDialog" [modal]="true" [style]="{ width: '500px' }">
<p>Błąd skanowania! <b>Wybierz NUMER PALETY większy niż 0</b> (Aktualnie '{{ palletNumber }}'):</p>
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
</p-dialog>
`,
})
export class MeylePackListSimpleComponent implements OnInit {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly warehouseService = inject(WarehouseService);
readonly scanner = viewChild<ElementRef<HTMLInputElement>>('scanner');
wzHeaderId = '';
rows: WzRowMeyleDto[] = [];
transactionModels: Record<string, TransactionModel[]> = {};
changedRecords: WzRowMeyleDto[] = [];
selectedRow: WzRowMeyleDto | null = null;
selectedRows: WzRowMeyleDto[] = [];
emailAddresses = '';
wzNumber = '';
palletNumber = '0';
scannedValue = '';
lastScannedValue = '';
itemNumber = '';
qty = '0';
palletNumberOutput = '0';
partNumberSl = '';
partNumberMeyle = '';
showInfoDialog = false;
showValidationDialog = false;
showPalletDialog = false;
readonly exportValid = signal(false);
get yearPrefix(): number { return new Date().getFullYear() - 2000; }
async ngOnInit(): Promise<void> {
this.wzHeaderId = this.route.snapshot.paramMap.get('WzHeader') ?? '';
const header = await this.warehouseService.getWzHeaderById(this.wzHeaderId);
this.rows = await this.warehouseService.getWzRowsMeyleByWzHeaderId(this.wzHeaderId);
this.transactionModels = await this.warehouseService.getTransactionsModels();
this.emailAddresses = header.emailAddresses ?? '';
this.wzNumber = header.wzNumbers ?? '';
setTimeout(() => this.scanner()?.nativeElement.focus());
}
changeView(): void { this.router.navigate(['/Warehouse/Meyle/PackList', this.wzHeaderId]); }
hideModal(): void {
this.showInfoDialog = false;
this.showValidationDialog = false;
this.showPalletDialog = false;
this.lastScannedValue = this.scannedValue;
this.scannedValue = '';
setTimeout(() => this.scanner()?.nativeElement.focus());
}
async saveChanges(): Promise<void> {
if (this.emailAddresses?.trim()) await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
if (this.changedRecords.length) {
await this.warehouseService.updateWzRowsMeyle(this.changedRecords);
this.rows = await this.warehouseService.getWzRowsMeyleByWzHeaderId(this.wzHeaderId);
}
}
async exportXls(): Promise<void> {
const valid = !this.rows.some((x) => x.palletNumber == null) && !!this.emailAddresses?.trim();
this.exportValid.set(valid);
if (valid) {
await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
await this.warehouseService.generateXlsForMeyle(this.wzHeaderId);
}
this.showInfoDialog = true;
}
async scanValue(): Promise<void> {
const value = this.scannedValue.trim();
if (!value || parseInt(this.palletNumber, 10) <= 0) { this.showPalletDialog = true; return; }
const material = this.transactionModels[value]?.[0];
if (!material && isValidMeyleScannedValue(value)) {
await this.fillMeylePartNumber(value);
this.scannedValue = '';
return;
}
if (material) await this.fillFaPartNumberAndPalletNumber(material, value);
else { this.showValidationDialog = true; this.changedRecords = []; return; }
this.lastScannedValue = value;
this.scannedValue = '';
setTimeout(() => this.scanner()?.nativeElement.focus());
}
private async fillMeylePartNumber(scanned: string): Promise<void> {
if (this.selectedRow && !this.selectedRows.length) {
this.selectedRow.partNumber = scanned;
this.changedRecords.push(this.selectedRow);
this.partNumberMeyle = scanned;
}
for (const row of this.selectedRows) { row.partNumber = scanned; this.changedRecords.push(row); }
await this.saveChanges();
this.changedRecords = [];
this.selectedRows = [];
setTimeout(() => this.scanner()?.nativeElement.focus());
}
private async fillFaPartNumberAndPalletNumber(material: TransactionModel, scanned: string): Promise<void> {
const palletNum = parseInt(this.palletNumber, 10);
let rowIndex = this.rows.findIndex((x) => x.faIndex === material.itemNumber && x.quantity === material.quantity);
if (rowIndex === -1) {
this.selectedRows = this.rows.filter((x) => x.faIndex === material.itemNumber);
if (!this.selectedRows.length) { this.showValidationDialog = true; return; }
const validCombinations = findCombinations(this.selectedRows, material.quantity ?? 0);
for (const combination of validCombinations) {
for (const record of combination) {
record.partNumberSl = scanned;
record.palletNumber = palletNum;
this.changedRecords.push(record);
}
}
this.selectedRows = [...this.changedRecords];
this.selectedRow = this.selectedRows[0] ?? null;
} else {
this.selectedRow = this.rows[rowIndex];
this.selectedRow.partNumberSl = scanned;
this.selectedRow.palletNumber = palletNum;
if (!this.changedRecords.some((x) => x.transactionNumber === this.selectedRow!.transactionNumber)) {
this.changedRecords.push(this.selectedRow);
}
}
this.partNumberSl = this.selectedRow?.partNumberSl ?? '';
this.palletNumberOutput = String(this.selectedRow?.palletNumber ?? 0);
this.itemNumber = this.selectedRow?.faIndex ?? '';
this.qty = String(this.selectedRow?.quantity ?? 0);
await this.saveChanges();
this.changedRecords = [];
setTimeout(() => this.scanner()?.nativeElement.focus());
}
}

View File

@@ -0,0 +1,336 @@
import { Component, ElementRef, inject, OnInit, signal, viewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { SelectableRow, TableModule, TableRowSelectEvent } from 'primeng/table';
import { ButtonDirective } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { DialogModule } from 'primeng/dialog';
import { TransactionModel, WzHeaderDto, WzRowMeyleDto } from '../../core/models';
import { WarehouseService } from '../../core/services/warehouse.service';
import {
findCombinations,
isValidMeyleScannedValue,
} from '../../shared/utils/business-logic';
import { PageShellComponent } from '../../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../../shared/components/content-section/content-section.component';
@Component({
selector: 'app-meyle-pack-list',
standalone: true,
imports: [TableModule, SelectableRow, ButtonDirective, InputTextModule, DialogModule, FormsModule, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
title="Packing List"
subtitle="Packing list Meyle — widok szczegółowy z edycją pozycji"
icon="pi pi-box"
>
<div actions>
<button pButton type="button" (click)="changeView()"><i class="pi pi-eye"></i> Zmień widok</button>
</div>
<app-content-section title="Dane wysyłki" icon="pi pi-envelope">
<div class="form-grid form-grid--2">
<div class="field-group">
<label class="field-label">Adresy Email do Wysyłki raportu:</label>
<input pInputText class="w-full" placeholder="Wprowadź adresy..." [(ngModel)]="emailAddresses" />
</div>
<div class="field-group">
<label class="field-label">Numer WZ:</label>
<input pInputText class="w-full" [(ngModel)]="wzNumber" />
</div>
</div>
</app-content-section>
<app-content-section title="Skanowanie" icon="pi pi-barcode">
<div class="form-grid form-grid--2">
<div class="field-group">
<label class="field-label">Wprowadź numer palety:</label>
<input pInputText type="number" class="w-full" [(ngModel)]="palletNumber" />
</div>
<div class="field-group">
<label class="field-label">Zeskanowana wartość:</label>
<input #scanner pInputText class="w-full" [(ngModel)]="scannedValue" (change)="scanValue()" />
</div>
</div>
</app-content-section>
<app-content-section title="Pozycje packing list" icon="pi pi-list" [flush]="true">
<div headerActions>
<button pButton type="button" (click)="saveChanges()"><i class="pi pi-save"></i> Zapisz zmiany</button>
<button pButton type="button" [disabled]="!selectedRow()" (click)="showSplitDialog.set(true)"><i class="pi pi-arrows-h"></i> Podziel Linię</button>
<button pButton type="button" (click)="exportXls()"><i class="pi pi-file-excel"></i> Generuj XLS i Wyślij</button>
</div>
<p-table
[value]="rows()"
[(selection)]="selectedRowsList"
dataKey="id"
[paginator]="true"
[rows]="10"
selectionMode="single"
(onRowSelect)="onRowSelect($event)"
styleClass="p-datatable-sm"
[showGridlines]="true"
[stripedRows]="true"
>
<ng-template #header>
<tr>
<th>Numer Zamówienia Meyle</th>
<th>Numer Indeksu FA</th>
<th>Numer Indeksu Meyle</th>
<th>Ilość w Dostawie</th>
<th>Nr Palety</th>
<th>Nr Partii SL</th>
<th>Nr Partii Meyle</th>
</tr>
</ng-template>
<ng-template #body let-row>
<tr [pSelectableRow]="row">
<td>{{ row.orderNumber }}</td>
<td>{{ row.faIndex }}</td>
<td>{{ row.itemNumber }}</td>
<td>{{ row.quantity }}</td>
<td><input pInputText type="number" [(ngModel)]="row.palletNumber" (ngModelChange)="markChanged(row)" /></td>
<td><input pInputText [(ngModel)]="row.partNumberSl" (ngModelChange)="markChanged(row)" /></td>
<td><input pInputText [(ngModel)]="row.partNumber" (ngModelChange)="markChanged(row)" /></td>
</tr>
</ng-template>
</p-table>
</app-content-section>
</app-page-shell>
<p-dialog header="Informacja" [(visible)]="showInfoDialog" [modal]="true" [style]="{ width: '500px' }">
@if (exportValid()) {
<p>Packing List został wygenerowany i wysłany!</p>
} @else if (!emailAddresses?.trim()) {
<p>Błąd: Proszę wprowadzić przynajmniej jeden <b>ADRES EMAIL</b> do wysyłki raportu!</p>
} @else {
<p>Błąd: Nie Wszystkie linie mają wypełniony <b>NUMER PALETY</b>.<br />Packing List nie zostanie wygenerowany!</p>
}
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
</p-dialog>
<p-dialog header="Błąd" [(visible)]="showValidationDialog" [modal]="true" [style]="{ width: '500px' }">
<p>Błąd skanowania! Wystąpił jeden z wyjątków (Zeskanowana wartość '<b>{{ lastScannedValue }}</b>'):</p>
<ul>
<li>Zeskanowano niepoprawny Numer Partii SL (nieistniejący w tabeli)</li>
<li>Zeskanowano niepoprawny numer Partii Meyle (niezaczynający się od <b>{{ yearPrefix }}X</b>)</li>
<li>Numer Palety nie jest większy niż 0 (aktualnie wybrany numer palety: '<b>{{ palletNumber }}</b>')</li>
</ul>
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
</p-dialog>
<p-dialog header="Błąd" [(visible)]="showPalletDialog" [modal]="true" [style]="{ width: '500px' }">
<p>Błąd skanowania! <b>Wybierz NUMER PALETY większy niż 0</b> (Aktualnie '{{ palletNumber }}'):</p>
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
</p-dialog>
<p-dialog header="Podziel Linię" [(visible)]="showSplitDialog" [modal]="true" [style]="{ width: '500px' }">
<label class="field-label">Podziel linię <b>{{ selectedRow()?.faIndex }}</b> podając ilość sztuk dla nowej linii:</label>
<input pInputText type="number" class="w-full" [(ngModel)]="newQuantity" />
<ng-template #footer>
<button pButton type="button" (click)="splitLine()">Zapisz</button>
<button pButton type="button" severity="secondary" (click)="showSplitDialog.set(false)">Anuluj</button>
</ng-template>
</p-dialog>
`,
})
export class MeylePackListComponent implements OnInit {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly warehouseService = inject(WarehouseService);
readonly scanner = viewChild<ElementRef<HTMLInputElement>>('scanner');
wzHeaderId = '';
emailAddresses = '';
wzNumber = '';
palletNumber = '0';
scannedValue = '';
lastScannedValue = '';
newQuantity = '0';
readonly rows = signal<WzRowMeyleDto[]>([]);
readonly selectedRow = signal<WzRowMeyleDto | null>(null);
selectedRowsList: WzRowMeyleDto[] = [];
changedRecords: WzRowMeyleDto[] = [];
transactionModels: Record<string, TransactionModel[]> = {};
showInfoDialog = false;
showValidationDialog = false;
showPalletDialog = false;
readonly showSplitDialog = signal(false);
readonly exportValid = signal(false);
get yearPrefix(): number {
return new Date().getFullYear() - 2000;
}
async ngOnInit(): Promise<void> {
this.wzHeaderId = this.route.snapshot.paramMap.get('WzHeader') ?? '';
const header = await this.warehouseService.getWzHeaderById(this.wzHeaderId);
this.rows.set(await this.warehouseService.getWzRowsMeyleByWzHeaderId(this.wzHeaderId));
this.transactionModels = await this.warehouseService.getTransactionsModels();
this.emailAddresses = header.emailAddresses ?? '';
this.wzNumber = header.wzNumbers ?? '';
setTimeout(() => this.scanner()?.nativeElement.focus());
}
changeView(): void {
this.router.navigate(['/Warehouse/Meyle/PackList', this.wzHeaderId, 'Simple']);
}
onRowSelect(event: TableRowSelectEvent<WzRowMeyleDto>): void {
const data = event.data;
if (data && !Array.isArray(data)) this.selectedRow.set(data);
}
markChanged(row: WzRowMeyleDto): void {
if (!this.changedRecords.includes(row)) this.changedRecords.push(row);
}
hideModal(): void {
this.showInfoDialog = false;
this.showValidationDialog = false;
this.showPalletDialog = false;
this.showSplitDialog.set(false);
this.lastScannedValue = this.scannedValue;
this.scannedValue = '';
setTimeout(() => this.scanner()?.nativeElement.focus());
}
async saveChanges(): Promise<void> {
if (this.emailAddresses?.trim()) {
await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
}
if (this.changedRecords.length) {
await this.updateRows(this.changedRecords);
this.changedRecords = [];
}
}
async exportXls(): Promise<void> {
const missingPallet = this.rows().some((x) => x.palletNumber == null);
const valid = !missingPallet && !!this.emailAddresses?.trim();
this.exportValid.set(valid);
if (valid) {
await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
await this.warehouseService.generateXlsForMeyle(this.wzHeaderId);
}
this.showInfoDialog = true;
}
async scanValue(): Promise<void> {
const value = this.scannedValue.trim();
if (!value) return;
if (parseInt(this.palletNumber, 10) <= 0) {
this.showPalletDialog = true;
return;
}
this.transactionModels[value]?.[0];
const materialTransaction = this.transactionModels[value]?.[0];
if (!materialTransaction && isValidMeyleScannedValue(value)) {
await this.fillMeylePartNumber(value);
this.lastScannedValue = value;
this.scannedValue = '';
return;
}
if (materialTransaction) {
this.selectedRowsList = [];
await this.fillFaPartNumberAndPalletNumber(materialTransaction, value);
} else {
this.showValidationDialog = true;
this.changedRecords = [];
return;
}
this.lastScannedValue = value;
this.scannedValue = '';
setTimeout(() => this.scanner()?.nativeElement.focus());
}
private async fillMeylePartNumber(scanned: string): Promise<void> {
const selected = this.selectedRow();
if (selected && !this.selectedRowsList.length) {
selected.partNumber = scanned;
this.changedRecords.push(selected);
}
for (const row of this.selectedRowsList) {
row.partNumber = scanned;
this.changedRecords.push(row);
}
await this.saveChanges();
this.changedRecords = [];
this.selectedRowsList = [];
setTimeout(() => this.scanner()?.nativeElement.focus());
}
private async fillFaPartNumberAndPalletNumber(material: TransactionModel, scanned: string): Promise<void> {
const palletNum = parseInt(this.palletNumber, 10);
const currentRows = this.rows();
let rowIndex = currentRows.findIndex((x) => x.faIndex === material.itemNumber && x.quantity === material.quantity);
if (rowIndex === -1) {
const candidates = currentRows.filter((x) => x.faIndex === material.itemNumber);
if (!candidates.length) {
this.showValidationDialog = true;
return;
}
rowIndex = currentRows.findIndex((x) => x.faIndex === candidates[0].faIndex && x.quantity === candidates[0].quantity);
const validCombinations = findCombinations(candidates, material.quantity ?? 0);
for (const combination of validCombinations) {
for (const record of combination) {
record.partNumberSl = scanned;
record.palletNumber = palletNum;
this.changedRecords.push(record);
}
}
this.selectedRowsList = [...this.changedRecords];
this.selectedRow.set(this.selectedRowsList[0] ?? null);
} else {
const row = currentRows[rowIndex];
this.selectedRow.set(row);
row.partNumberSl = scanned;
row.palletNumber = palletNum;
if (!this.changedRecords.some((x) => x.transactionNumber === row.transactionNumber)) {
this.changedRecords.push(row);
}
}
await this.saveChanges();
this.changedRecords = [];
setTimeout(() => this.scanner()?.nativeElement.focus());
}
async splitLine(): Promise<void> {
const qty = parseInt(this.newQuantity, 10);
const selected = this.selectedRow();
if (qty <= 0 || !selected) return;
const splitRow: WzRowMeyleDto = {
id: crypto.randomUUID(),
fk_Header: selected.fk_Header,
quantity: qty,
faIndex: selected.faIndex,
itemNumber: selected.itemNumber,
orderNumber: selected.orderNumber,
palletNumber: selected.palletNumber,
wzNumber: selected.wzNumber,
transactionNumber: (selected.transactionNumber ?? 0) + 10000,
partNumberSl: selected.partNumberSl,
partNumber: selected.partNumber,
};
selected.quantity = (selected.quantity ?? 0) - qty;
await this.warehouseService.createWzRowsMeyle([splitRow]);
await this.updateRows([selected]);
this.showSplitDialog.set(false);
}
private async updateRows(changed: WzRowMeyleDto[]): Promise<void> {
await this.warehouseService.updateWzRowsMeyle(changed);
this.rows.set(await this.warehouseService.getWzRowsMeyleByWzHeaderId(this.wzHeaderId));
}
}

View File

@@ -0,0 +1,83 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
CancelEditableRow,
CellEditor,
EditableRow,
InitEditableRow,
SaveEditableRow,
TableModule,
} from 'primeng/table';
import { InputTextModule } from 'primeng/inputtext';
import { ButtonDirective } from 'primeng/button';
import { ProductDto } from '../core/models';
import { ProductService } from '../core/services/product.service';
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
@Component({
selector: 'app-products-list',
standalone: true,
imports: [TableModule, CellEditor, EditableRow, InitEditableRow, SaveEditableRow, CancelEditableRow, InputTextModule, ButtonDirective, FormsModule, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
title="Zarządzanie Indeksami"
subtitle="Mapowanie indeksów odbiorców na kody FA"
icon="pi pi-box"
>
<app-content-section title="Indeksy produktów" icon="pi pi-list" [flush]="true">
<p-table [value]="products()" [paginator]="true" [rows]="10" editMode="row" dataKey="id" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr>
<th>ID</th>
<th>Odbiorca</th>
<th>Indeks odbiorcy</th>
<th>Kod FA</th>
<th style="width: 8rem"></th>
</tr>
</ng-template>
<ng-template #body let-product let-editing="editing" let-ri="rowIndex">
<tr [pEditableRow]="product">
<td>{{ product.id }}</td>
<td>{{ product.recipientName }}</td>
<td>{{ product.recipientIdx }}</td>
<td>
<p-cell-editor>
<ng-template #input>
<input pInputText [(ngModel)]="product.faIdx" />
</ng-template>
<ng-template #output>{{ product.faIdx }}</ng-template>
</p-cell-editor>
</td>
<td>
@if (!editing) {
<button pButton type="button" [text]="true" [iconOnly]="true" pInitEditableRow><i class="pi pi-pencil"></i></button>
} @else {
<button pButton type="button" [text]="true" [iconOnly]="true" (click)="saveProduct(product)" pSaveEditableRow><i class="pi pi-check"></i></button>
<button pButton type="button" severity="secondary" [text]="true" [iconOnly]="true" pCancelEditableRow><i class="pi pi-times"></i></button>
}
</td>
</tr>
</ng-template>
</p-table>
</app-content-section>
</app-page-shell>
`,
})
export class ProductsListComponent implements OnInit {
private readonly productService = inject(ProductService);
readonly products = signal<ProductDto[]>([]);
async ngOnInit(): Promise<void> {
await this.loadProducts();
}
async saveProduct(product: ProductDto): Promise<void> {
await this.productService.update(product);
await this.loadProducts();
}
private async loadProducts(): Promise<void> {
this.products.set(await this.productService.getByIndex('Uzupelnij'));
}
}

View File

@@ -0,0 +1,121 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DatePipe } from '@angular/common';
import { RowToggler, TableModule } from 'primeng/table';
import { ButtonDirective } from 'primeng/button';
import { ScheduleOrderDto, ScheduleOrderDetailDto } from '../core/models';
import { ScheduleOrderService } from '../core/services/schedule-order.service';
import { isQtyTypeHighlighted } from '../shared/utils/business-logic';
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
@Component({
selector: 'app-schedule-order-detail',
standalone: true,
imports: [TableModule, RowToggler, ButtonDirective, DatePipe, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
[title]="'Zamówienie DELFOR nr ' + (order()?.poNum ?? 'Brak numeru') + ' (ID: ' + (order()?.id ?? '') + ')'"
subtitle="Szczegóły harmonogramu dostaw DELFOR"
icon="pi pi-building"
>
@if (order()?.scheduleOrderMiscs?.length) {
<app-content-section title="Informacje dodatkowe" icon="pi pi-info-circle">
<ul class="misc-list">
@for (misc of order()!.scheduleOrderMiscs; track misc.id) {
@if (misc.display !== false) {
<li><b>{{ misc.label }}:</b> {{ misc.value }}</li>
}
}
</ul>
</app-content-section>
}
<app-content-section title="Indeksy" icon="pi pi-list" [flush]="true">
<p-table [value]="details()" dataKey="id" [paginator]="true" [rows]="10" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr>
<th style="width: 3rem"></th>
<th>Numer Zamówienia</th>
<th>Klient</th>
<th>Odbiorca</th>
<th>Kod odbiorcy</th>
<th>Pozycja</th>
<th>Pozycja Klienta</th>
</tr>
</ng-template>
<ng-template #body let-detail let-expanded="expanded">
<tr>
<td>
<button pButton type="button" [text]="true" [rounded]="true" [iconOnly]="true" [pRowToggler]="detail"><i [class]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i></button>
</td>
<td>{{ detail.orderNumber }}</td>
<td>{{ detail.purchaserName }}</td>
<td>{{ detail.recipientName }}</td>
<td>{{ detail.recipientCode }}</td>
<td>{{ detail.sh_productCode }}</td>
<td>{{ detail.sc_productCode }}</td>
</tr>
</ng-template>
<ng-template #expandedrow let-detail>
<tr>
<td colspan="7">
<app-content-section title="Harmonogramy" icon="pi pi-calendar" [flush]="true">
<p-table [value]="detail.scheduleOrderDetailDetails ?? []" [paginator]="true" [rows]="10" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #header>
<tr>
<th>Data Od</th>
<th>Data Do</th>
<th>Ilość Sztuk</th>
<th>Typ Qty</th>
<th>Opis Typu</th>
</tr>
</ng-template>
<ng-template #body let-row>
<tr [class.qty-type-highlight]="isHighlighted(row.qtyType)">
<td>{{ row.dateFrom | date: 'dd.MM.yyyy' }}</td>
<td>{{ row.dateTo | date: 'dd.MM.yyyy' }}</td>
<td>{{ row.qty }}</td>
<td>{{ row.qtyType }}</td>
<td>{{ row.qtyDesc }}</td>
</tr>
</ng-template>
</p-table>
</app-content-section>
</td>
</tr>
</ng-template>
</p-table>
</app-content-section>
</app-page-shell>
`,
styles: `
.misc-list { list-style: none; padding: 0; margin: 0; }
.misc-list li { margin-bottom: var(--spacing-1); }
`,
})
export class ScheduleOrderDetailComponent implements OnInit {
private readonly route = inject(ActivatedRoute);
private readonly scheduleOrderService = inject(ScheduleOrderService);
readonly order = signal<ScheduleOrderDto | null>(null);
readonly details = signal<ScheduleOrderDetailDto[]>([]);
async ngOnInit(): Promise<void> {
const id = Number(this.route.snapshot.paramMap.get('scheduleOrderId'));
const data = await this.scheduleOrderService.getById(id);
this.order.set(data);
const details = [...(data.scheduleOrderDetails ?? [])];
for (const d of details) {
d.orderNumber = data.poNum;
d.recipientCode = data.recipientCode;
d.recipientName = data.recipientName;
d.purchaserName = data.purchaserCode;
}
this.details.set(details);
}
isHighlighted(qtyType?: string): boolean {
return qtyType === '83' || qtyType === '84' || isQtyTypeHighlighted(qtyType);
}
}

View File

@@ -0,0 +1,29 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { ScheduleOrderDto } from '../core/models';
import { ScheduleOrderService } from '../core/services/schedule-order.service';
import { ScheduleOrdersGridComponent } from '../shared/components/schedule-orders-grid/schedule-orders-grid.component';
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
@Component({
selector: 'app-schedule-orders-list',
standalone: true,
imports: [ScheduleOrdersGridComponent, PageShellComponent],
template: `
<app-page-shell
title="Zamówienia DELFOR"
subtitle="Przegląd harmonogramów dostaw i zamówień klientów"
icon="pi pi-building"
>
<app-schedule-orders-grid [gridData]="orders()" [pageSize]="20" />
</app-page-shell>
`,
})
export class ScheduleOrdersListComponent implements OnInit {
private readonly scheduleOrderService = inject(ScheduleOrderService);
readonly orders = signal<ScheduleOrderDto[]>([]);
async ngOnInit(): Promise<void> {
const data = await this.scheduleOrderService.getAll();
this.orders.set([...data].sort((a, b) => new Date(b.lastUpdateDate).getTime() - new Date(a.lastUpdateDate).getTime()));
}
}

View File

@@ -0,0 +1,44 @@
import { Component, input, output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Select } from 'primeng/select';
@Component({
selector: 'app-column-values-filter',
standalone: true,
imports: [Select, FormsModule],
template: `
<p-select
[ngModel]="value()"
[options]="options()"
[optionLabel]="optionLabel()"
[optionValue]="optionValue()"
[multiple]="true"
placeholder="Wybierz wartości"
emptyMessage="Brak wartości"
emptyFilterMessage="Brak wyników"
[showClear]="true"
[filter]="true"
appendTo="body"
styleClass="column-values-filter w-full"
(onChange)="onChange($event.value)"
/>
`,
styles: `
:host {
display: block;
min-width: 14rem;
max-width: 20rem;
}
`,
})
export class ColumnValuesFilterComponent {
value = input<unknown>();
options = input<unknown[]>([]);
optionLabel = input<string | undefined>(undefined);
optionValue = input<string | undefined>(undefined);
valueChange = output<unknown>();
onChange(value: unknown): void {
this.valueChange.emit(value);
}
}

View File

@@ -0,0 +1,31 @@
import { Component, input } from '@angular/core';
@Component({
selector: 'app-content-section',
standalone: true,
template: `
<section class="content-section" [class.content-section--flush]="flush()">
@if (title()) {
<header class="content-section__header">
<div class="content-section__heading">
@if (icon()) {
<i [class]="icon()" class="content-section__icon" aria-hidden="true"></i>
}
<h2 class="content-section__title">{{ title() }}</h2>
</div>
<div class="content-section__actions">
<ng-content select="[headerActions]" />
</div>
</header>
}
<div class="content-section__body">
<ng-content />
</div>
</section>
`,
})
export class ContentSectionComponent {
readonly title = input<string>('');
readonly icon = input<string>('');
readonly flush = input<boolean>(false);
}

View File

@@ -0,0 +1,36 @@
import { Component, input } from '@angular/core';
@Component({
selector: 'app-page-shell',
standalone: true,
template: `
<div class="page-shell">
<header class="page-shell__header">
<div class="page-shell__heading">
@if (icon()) {
<div class="page-shell__icon" aria-hidden="true">
<i [class]="icon()"></i>
</div>
}
<div class="page-shell__titles">
<h1 class="page-shell__title">{{ title() }}</h1>
@if (subtitle()) {
<p class="page-shell__subtitle">{{ subtitle() }}</p>
}
</div>
</div>
<div class="page-shell__actions">
<ng-content select="[actions]" />
</div>
</header>
<div class="page-shell__content">
<ng-content />
</div>
</div>
`,
})
export class PageShellComponent {
readonly title = input.required<string>();
readonly subtitle = input<string>('');
readonly icon = input<string>('');
}

View File

@@ -0,0 +1,517 @@
import {
AfterViewInit,
Component,
effect,
inject,
input,
OnInit,
untracked,
ViewChild,
} from '@angular/core';
import { Router } from '@angular/router';
import { DatePipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
ColumnFilter,
RowToggler,
SelectableRow,
SortableColumn,
SortIcon,
Table,
TableModule,
} from 'primeng/table';
import { ButtonDirective } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { IconFieldModule } from 'primeng/iconfield';
import { InputIconModule } from 'primeng/inputicon';
import { MessageService } from 'primeng/api';
import { ScheduleOrderDto, ScheduleOrderDetailDetailDto } from '../../../core/models';
import { ScheduleOrderService } from '../../../core/services/schedule-order.service';
import { ContentSectionComponent } from '../content-section/content-section.component';
import { ColumnValuesFilterComponent } from '../column-values-filter/column-values-filter.component';
import { isQtyTypeHighlighted } from '../../utils/business-logic';
import {
applySavedFiltersToTable,
clearTableFilters,
deleteGridFilters,
loadSavedGridFilters,
saveGridFilters,
serializeTableFilters,
} from '../../utils/grid-filter-storage';
import {
distinctDateValues,
distinctNumericValues,
distinctStringValues,
LabeledFilterValue,
} from '../../utils/grid-filter-values';
const GRID_FILTER_KEY = 'gridFilter';
@Component({
selector: 'app-schedule-orders-grid',
standalone: true,
imports: [
TableModule,
Table,
RowToggler,
SelectableRow,
SortableColumn,
SortIcon,
ColumnFilter,
ButtonDirective,
InputTextModule,
IconFieldModule,
InputIconModule,
FormsModule,
ContentSectionComponent,
ColumnValuesFilterComponent,
DatePipe,
],
template: `
<app-content-section title="Lista zamówień" icon="pi pi-table" [flush]="true">
<div headerActions>
<button pButton type="button" size="small" (click)="saveFilters()"><i class="pi pi-save"></i> Zapisz filtry</button>
<button pButton type="button" severity="secondary" size="small" (click)="deleteFilters()"><i class="pi pi-trash"></i> Usuń zapisane filtry</button>
</div>
<p-table
#mainTable
[value]="gridData()"
[paginator]="true"
[rows]="pageSize()"
[rowsPerPageOptions]="[10, 20, 50]"
[globalFilterFields]="globalFilterFields"
[filterDelay]="300"
dataKey="id"
[expandedRowKeys]="expandedKeys"
(onRowExpand)="onRowExpand($event)"
styleClass="p-datatable-sm erp-table"
[showGridlines]="true"
[stripedRows]="true"
>
<ng-template #caption>
<div class="data-toolbar">
<div class="data-toolbar__left">
<span class="text-secondary" style="font-size: var(--font-size-sm)">
{{ filteredCount() }} / {{ gridData().length }} rekordów · dwuklik otwiera szczegóły
</span>
</div>
<div class="data-toolbar__right">
<p-iconfield iconPosition="left" class="data-toolbar__search">
<p-inputicon styleClass="pi pi-search" />
<input
pInputText
type="text"
[(ngModel)]="globalFilterValue"
placeholder="Szukaj w tabeli…"
(ngModelChange)="onGlobalFilterChange($event)"
/>
</p-iconfield>
</div>
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="width: 3rem"></th>
<th pSortableColumn="poNum">
<div class="column-header">
<span>Zamówienie Klienta</span>
<p-sort-icon field="poNum" />
<p-column-filter field="poNum" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctMainColumn('poNum')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div>
</th>
<th pSortableColumn="purchaserCode">
<div class="column-header">
<span>Klient</span>
<p-sort-icon field="purchaserCode" />
<p-column-filter field="purchaserCode" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctMainColumn('purchaserCode')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div>
</th>
<th pSortableColumn="recipientName">
<div class="column-header">
<span>Odbiorca</span>
<p-sort-icon field="recipientName" />
<p-column-filter field="recipientName" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctMainColumn('recipientName')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div>
</th>
<th pSortableColumn="recipientCode">
<div class="column-header">
<span>Kod odbiorcy</span>
<p-sort-icon field="recipientCode" />
<p-column-filter field="recipientCode" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctMainColumn('recipientCode')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div>
</th>
<th pSortableColumn="lastUpdateDate">
<div class="column-header">
<span>Data</span>
<p-sort-icon field="lastUpdateDate" />
<p-column-filter field="lastUpdateDate" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctMainDates()" optionLabel="label" optionValue="value" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div>
</th>
<th pSortableColumn="docType">
<div class="column-header">
<span>Typ Dokumentu</span>
<p-sort-icon field="docType" />
<p-column-filter field="docType" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctMainColumn('docType')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div>
</th>
</tr>
</ng-template>
<ng-template #body let-order let-expanded="expanded">
<tr (dblclick)="navigateToOrder(order.id)" [pSelectableRow]="order" class="erp-table__row">
<td>
<button pButton type="button" size="small" [text]="true" [rounded]="true" [iconOnly]="true" [pRowToggler]="order"><i [class]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i></button>
</td>
<td><span class="font-medium">{{ order.poNum }}</span></td>
<td>{{ order.purchaserCode }}</td>
<td>{{ order.recipientName }}</td>
<td>{{ order.recipientCode }}</td>
<td>{{ order.lastUpdateDate | date: 'dd.MM.yyyy' }}</td>
<td>{{ order.docType }}</td>
</tr>
</ng-template>
<ng-template #expandedrow let-order>
<tr>
<td colspan="7" class="erp-table__nested">
<p-table
[value]="order.scheduleOrderDetails ?? []"
dataKey="id"
[paginator]="true"
[rows]="10"
[filterDelay]="300"
styleClass="p-datatable-sm"
[showGridlines]="true"
>
<ng-template #header>
<tr>
<th style="width: 3rem"></th>
<th>
<div class="column-header"><span>Numer Zamówienia</span>
<p-column-filter field="orderNumber" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctFromRows(order.scheduleOrderDetails, 'orderNumber')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div>
</th>
<th>
<div class="column-header"><span>Klient</span>
<p-column-filter field="purchaserName" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctFromRows(order.scheduleOrderDetails, 'purchaserName')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div>
</th>
<th>
<div class="column-header"><span>Odbiorca</span>
<p-column-filter field="recipientName" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctFromRows(order.scheduleOrderDetails, 'recipientName')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div>
</th>
<th>
<div class="column-header"><span>Kod odbiorcy</span>
<p-column-filter field="recipientCode" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctFromRows(order.scheduleOrderDetails, 'recipientCode')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div>
</th>
<th>
<div class="column-header"><span>Pozycja Klienta</span>
<p-column-filter field="sc_productCode" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctFromRows(order.scheduleOrderDetails, 'sc_productCode')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div>
</th>
<th>
<div class="column-header"><span>Pozycja</span>
<p-column-filter field="sh_productCode" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctFromRows(order.scheduleOrderDetails, 'sh_productCode')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div>
</th>
</tr>
</ng-template>
<ng-template #body let-detail let-detailExpanded="expanded">
<tr (dblclick)="navigateToOrder(order.id)">
<td>
<button pButton type="button" size="small" [text]="true" [rounded]="true" [iconOnly]="true" [pRowToggler]="detail"><i [class]="detailExpanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i></button>
</td>
<td>{{ detail.orderNumber }}</td>
<td>{{ detail.purchaserName }}</td>
<td>{{ detail.recipientName }}</td>
<td>{{ detail.recipientCode }}</td>
<td>{{ detail.sc_productCode }}</td>
<td>{{ detail.sh_productCode }}</td>
</tr>
</ng-template>
<ng-template #expandedrow let-detail>
<tr>
<td colspan="7" class="erp-table__nested">
<p-table
[value]="detail.scheduleOrderDetailDetails ?? []"
[paginator]="true"
[rows]="10"
[filterDelay]="300"
styleClass="p-datatable-sm"
[showGridlines]="true"
>
<ng-template #header>
<tr>
<th><div class="column-header"><span>Data Od</span>
<p-column-filter field="dateFrom" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctDatesFromRows(detail.scheduleOrderDetailDetails, 'dateFrom')" optionLabel="label" optionValue="value" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div></th>
<th><div class="column-header"><span>Data Do</span>
<p-column-filter field="dateTo" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctDatesFromRows(detail.scheduleOrderDetailDetails, 'dateTo')" optionLabel="label" optionValue="value" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div></th>
<th><div class="column-header"><span>Ilość Sztuk</span>
<p-column-filter field="qty" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctNumbersFromRows(detail.scheduleOrderDetailDetails, 'qty')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div></th>
<th><div class="column-header"><span>Typ Qty</span>
<p-column-filter field="qtyType" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctFromRows(detail.scheduleOrderDetailDetails, 'qtyType')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div></th>
<th><div class="column-header"><span>Opis Typu</span>
<p-column-filter field="qtyDesc" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
<ng-template #filter let-value let-filter="filterCallback">
<app-column-values-filter [value]="value" [options]="distinctFromRows(detail.scheduleOrderDetailDetails, 'qtyDesc')" (valueChange)="filter($event)" />
</ng-template>
</p-column-filter>
</div></th>
</tr>
</ng-template>
<ng-template #body let-qtyRow>
<tr [class.qty-type-highlight]="isHighlighted(qtyRow.qtyType)" (dblclick)="navigateToOrder(order.id)">
<td>{{ qtyRow.dateFrom | date: 'dd.MM.yyyy' }}</td>
<td>{{ qtyRow.dateTo | date: 'dd.MM.yyyy' }}</td>
<td>{{ qtyRow.qty }}</td>
<td>{{ qtyRow.qtyType }}</td>
<td>{{ qtyRow.qtyDesc }}</td>
</tr>
</ng-template>
</p-table>
</td>
</tr>
</ng-template>
</p-table>
</td>
</tr>
</ng-template>
</p-table>
</app-content-section>
`,
styles: `
.column-header {
display: flex;
align-items: center;
gap: var(--spacing-1);
white-space: nowrap;
}
:host ::ng-deep .erp-table__nested {
padding: var(--spacing-3) !important;
background: var(--color-bg-subtle) !important;
}
:host ::ng-deep .p-datatable-column-filter-button {
width: 1.75rem;
height: 1.75rem;
}
`,
})
export class ScheduleOrdersGridComponent implements OnInit, AfterViewInit {
private readonly scheduleOrderService = inject(ScheduleOrderService);
private readonly router = inject(Router);
private readonly messageService = inject(MessageService);
readonly gridData = input.required<ScheduleOrderDto[]>();
readonly pageSize = input.required<number>();
@ViewChild('mainTable') mainTable?: Table;
readonly globalFilterFields = ['poNum', 'purchaserCode', 'recipientName', 'recipientCode', 'docType'];
expandedKeys: Record<string, boolean> = {};
globalFilterValue = '';
private viewReady = false;
private lastDataLength = -1;
constructor() {
effect(() => {
const data = this.gridData();
const length = data.length;
untracked(() => {
if (!this.viewReady || length === this.lastDataLength) return;
this.lastDataLength = length;
queueMicrotask(() => this.restoreFilters());
});
});
}
ngOnInit(): void {
const saved = loadSavedGridFilters(GRID_FILTER_KEY);
if (saved?.global) {
this.globalFilterValue = saved.global;
}
}
ngAfterViewInit(): void {
this.viewReady = true;
this.lastDataLength = this.gridData().length;
queueMicrotask(() => this.restoreFilters());
}
filteredCount(): number {
if (!this.mainTable?.filteredValue) {
return this.gridData().length;
}
return this.mainTable.filteredValue.length;
}
isHighlighted(qtyType?: string): boolean {
return isQtyTypeHighlighted(qtyType);
}
distinctMainColumn(field: keyof ScheduleOrderDto): string[] {
return distinctStringValues(this.gridData(), field);
}
distinctMainDates(): LabeledFilterValue[] {
return distinctDateValues(this.gridData(), 'lastUpdateDate');
}
distinctFromRows<T>(rows: T[] | null | undefined, field: keyof T): string[] {
return distinctStringValues(rows, field);
}
distinctDatesFromRows(
rows: ScheduleOrderDetailDetailDto[] | null | undefined,
field: keyof ScheduleOrderDetailDetailDto,
): LabeledFilterValue[] {
return distinctDateValues(rows, field);
}
distinctNumbersFromRows(
rows: ScheduleOrderDetailDetailDto[] | null | undefined,
field: keyof ScheduleOrderDetailDetailDto,
): number[] {
return distinctNumericValues(rows, field);
}
navigateToOrder(id: number): void {
this.router.navigate(['/ScheduleOrder', id]);
}
onGlobalFilterChange(value: string): void {
this.globalFilterValue = value;
this.mainTable?.filterGlobal(value, 'contains');
}
async onRowExpand(event: { data: ScheduleOrderDto }): Promise<void> {
const order = event.data;
if (order.scheduleOrderDetails?.length) return;
const full = await this.scheduleOrderService.getById(order.id);
order.scheduleOrderDetails = full.scheduleOrderDetails ?? [];
for (const detail of order.scheduleOrderDetails) {
detail.orderNumber = full.poNum;
detail.recipientCode = full.recipientCode;
detail.recipientName = full.recipientName;
detail.purchaserName = full.purchaserCode;
}
}
saveFilters(): void {
if (!this.mainTable) return;
const state = serializeTableFilters(this.mainTable);
if (!state) {
this.messageService.add({
severity: 'warn',
summary: 'Brak filtrów',
detail: 'Ustaw filtry kolumn lub wyszukiwanie globalne przed zapisaniem.',
});
return;
}
saveGridFilters(GRID_FILTER_KEY, state);
this.messageService.add({
severity: 'success',
summary: 'Filtry zapisane',
detail: 'Filtry zostaną przywrócone przy następnym wejściu na listę.',
});
}
deleteFilters(): void {
deleteGridFilters(GRID_FILTER_KEY);
this.globalFilterValue = '';
if (this.mainTable) {
clearTableFilters(this.mainTable);
}
this.messageService.add({
severity: 'info',
summary: 'Filtry usunięte',
detail: 'Zapisane filtry zostały usunięte z pamięci przeglądarki.',
});
}
private restoreFilters(): void {
if (!this.mainTable) return;
const saved = loadSavedGridFilters(GRID_FILTER_KEY);
if (!saved) return;
this.globalFilterValue = applySavedFiltersToTable(this.mainTable, saved, this.globalFilterValue);
}
}

View File

@@ -0,0 +1,52 @@
import { WzRowMarelliDto, WzRowMeyleDto } from '../../core/models';
export function isValidMeyleScannedValue(scannedValue: string): boolean {
const year = new Date().getFullYear() - 2000;
return scannedValue.startsWith(`${year}X`);
}
export function findCombinationsByQuantity<T extends { quantity?: number | null }>(
records: T[],
targetSum: number,
): T[][] {
const result: T[][] = [];
const currentCombination: T[] = [];
function backtrack(start: number, currentSum: number): void {
if (currentSum === targetSum) {
result.push([...currentCombination]);
return;
}
for (let i = start; i < records.length; i++) {
const qty = records[i].quantity ?? 0;
if (currentSum + qty <= targetSum) {
currentCombination.push(records[i]);
backtrack(i + 1, currentSum + qty);
currentCombination.pop();
}
}
}
backtrack(0, 0);
return result;
}
export function findCombinations(records: WzRowMeyleDto[], targetSum: number): WzRowMeyleDto[][] {
return findCombinationsByQuantity(records, targetSum);
}
export function findMarelliCombinations(records: WzRowMarelliDto[], targetSum: number): WzRowMarelliDto[][] {
return findCombinationsByQuantity(records, targetSum);
}
export function isQtyTypeHighlighted(qtyType?: string): boolean {
return qtyType === '54' || qtyType === '83' || qtyType === '84';
}
export function generateTempPassword(): string {
return crypto.randomUUID().substring(0, 8);
}
export function generateGuid(): string {
return crypto.randomUUID();
}

View File

@@ -0,0 +1,105 @@
import { FilterMetadata } from 'primeng/api';
import { Table } from 'primeng/table';
export interface SavedGridFilterState {
global?: string;
columns?: Record<string, FilterMetadata | FilterMetadata[]>;
}
export function serializeTableFilters(table: Table): SavedGridFilterState | null {
const columns: Record<string, FilterMetadata | FilterMetadata[]> = {};
for (const [key, meta] of Object.entries(table.filters ?? {})) {
if (key === 'global') continue;
if (hasFilterValue(meta)) {
columns[key] = JSON.parse(JSON.stringify(meta));
}
}
const globalMeta = table.filters?.['global'] as FilterMetadata | undefined;
const global = typeof globalMeta?.value === 'string' ? globalMeta.value : '';
if (!global && Object.keys(columns).length === 0) {
return null;
}
return { global, columns };
}
type RestorableFilterMetadata = FilterMetadata & { applyFilter?: boolean };
export function applySavedFiltersToTable(table: Table, saved: SavedGridFilterState, globalFilterValue: string): string {
table.clearFilterValues();
if (saved.columns && Object.keys(saved.columns).length > 0) {
table.restoringFilter = true;
const restored: Record<string, RestorableFilterMetadata | RestorableFilterMetadata[]> = {};
for (const [key, meta] of Object.entries(saved.columns)) {
if (Array.isArray(meta)) {
restored[key] = meta.map((m) => ({ ...m, applyFilter: hasFilterValue(m) }));
} else if (meta) {
restored[key] = { ...meta, applyFilter: hasFilterValue(meta) };
}
}
table.filters = restored as Record<string, FilterMetadata | FilterMetadata[]>;
}
const global = saved.global ?? globalFilterValue ?? '';
if (global) {
table.filterGlobal(global, 'contains');
return global;
}
table._filter();
return '';
}
export function clearTableFilters(table: Table): void {
table.clearFilterValues();
table.filteredValue = null;
table._filter();
table.tableService.onValueChange(table.value);
}
function hasFilterValue(meta: FilterMetadata | FilterMetadata[] | undefined): boolean {
if (!meta) return false;
if (Array.isArray(meta)) {
return meta.some((m) => hasSingleFilterValue(m));
}
return hasSingleFilterValue(meta);
}
function hasSingleFilterValue(meta: FilterMetadata): boolean {
if (meta.value == null) return false;
if (Array.isArray(meta.value)) return meta.value.length > 0;
return meta.value !== '';
}
export function loadSavedGridFilters(storageKey: string): SavedGridFilterState | null {
const raw = localStorage.getItem(storageKey);
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as SavedGridFilterState | { global?: string };
// Legacy format from earlier Angular migration (global only)
if (parsed && !('columns' in parsed) && parsed.global) {
return { global: parsed.global, columns: {} };
}
return parsed as SavedGridFilterState;
} catch {
localStorage.removeItem(storageKey);
return null;
}
}
export function saveGridFilters(storageKey: string, state: SavedGridFilterState): void {
localStorage.setItem(storageKey, JSON.stringify(state));
}
export function deleteGridFilters(storageKey: string): void {
localStorage.removeItem(storageKey);
}

View File

@@ -0,0 +1,58 @@
export interface LabeledFilterValue {
label: string;
value: string;
}
export function formatGridDate(iso: string): string {
if (!iso) return '';
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return iso;
return date.toLocaleDateString('pl-PL', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
export function distinctStringValues<T>(rows: T[] | null | undefined, field: keyof T): string[] {
if (!rows?.length) return [];
const values = new Set<string>();
for (const row of rows) {
const raw = row[field];
if (raw == null || raw === '') continue;
values.add(String(raw));
}
return [...values].sort((a, b) => a.localeCompare(b, 'pl'));
}
export function distinctNumericValues<T>(rows: T[] | null | undefined, field: keyof T): number[] {
if (!rows?.length) return [];
const values = new Set<number>();
for (const row of rows) {
const raw = row[field];
if (typeof raw === 'number' && !Number.isNaN(raw)) {
values.add(raw);
}
}
return [...values].sort((a, b) => a - b);
}
export function distinctDateValues<T>(
rows: T[] | null | undefined,
field: keyof T,
format: (iso: string) => string = formatGridDate,
): LabeledFilterValue[] {
if (!rows?.length) return [];
const byValue = new Map<string, LabeledFilterValue>();
for (const row of rows) {
const raw = row[field];
if (raw == null || raw === '') continue;
const value = String(raw);
if (!byValue.has(value)) {
byValue.set(value, { value, label: format(value) });
}
}
return [...byValue.values()].sort((a, b) => a.label.localeCompare(b.label, 'pl'));
}

View File

@@ -0,0 +1,42 @@
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { ButtonDirective } from 'primeng/button';
@Component({
selector: 'app-unauthorized',
standalone: true,
imports: [ButtonDirective],
template: `
<div class="auth-layout">
<aside class="auth-layout__brand">
<div class="auth-layout__brand-logo">
<img src="logo.svg" width="40" height="40" alt="" />
<h1 class="auth-layout__brand-title">FA Krosno Manager</h1>
</div>
<p class="auth-layout__brand-tagline">
System zarządzania zamówieniami DELFOR, magazynem i procesami EDI dla operacji produkcyjnych.
</p>
<div class="auth-layout__features">
<div class="auth-layout__feature"><i class="pi pi-building"></i> Zamówienia DELFOR i harmonogramy</div>
<div class="auth-layout__feature"><i class="pi pi-warehouse"></i> Magazyn i packing listy</div>
<div class="auth-layout__feature"><i class="pi pi-database"></i> Integracja EDI / Syteline</div>
</div>
</aside>
<main class="auth-layout__form">
<div class="auth-card">
<h2 class="auth-card__title">Brak autoryzacji</h2>
<p class="auth-card__subtitle">Ups! Wygląda na to, że nie masz dostępu do tej strony.</p>
<p class="text-secondary mb-3">
Aby kontynuować, zaloguj się do swojego konta.
</p>
<button pButton type="button" [fluid]="true" (click)="goLogin()"><i class="pi pi-sign-in"></i> Przejdź do logowania</button>
</div>
</main>
</div>
`,
})
export class UnauthorizedComponent {
private readonly router = inject(Router);
goLogin(): void { this.router.navigate(['/login']); }
}

View File

@@ -0,0 +1,250 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { DatePipe } from '@angular/common';
import { Table, TableCheckbox, TableHeaderCheckbox, TableModule } from 'primeng/table';
import { SelectModule } from 'primeng/select';
import { ButtonDirective } from 'primeng/button';
import { DialogModule } from 'primeng/dialog';
import { MaterialTransactionDto, WzClientDto, WzHeaderDto, WzRowMarelliDto, WzRowMeyleDto } from '../core/models';
import { WarehouseService } from '../core/services/warehouse.service';
import { generateGuid } from '../shared/utils/business-logic';
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
const SELECTED_CLIENT_KEY = 'SelectedClientId';
@Component({
selector: 'app-warehouse',
standalone: true,
imports: [TableModule, Table, TableCheckbox, TableHeaderCheckbox, SelectModule, ButtonDirective, DialogModule, FormsModule, DatePipe, PageShellComponent, ContentSectionComponent],
template: `
<app-page-shell
title="Dokumenty WZ na Magazynie"
subtitle="Zarządzanie dokumentami WZ i packing listami dla klientów Meyle i Marelli"
icon="pi pi-warehouse"
>
<app-content-section title="Klient" icon="pi pi-users">
<div class="field-group">
<label class="field-label">Wybierz klienta</label>
<p-select
[options]="clients()"
optionLabel="name"
optionValue="id"
placeholder="Wybierz Klienta"
[(ngModel)]="selectedClientId"
(onChange)="onClientChange()"
styleClass="w-full"
/>
</div>
</app-content-section>
@if (isVisible()) {
<app-content-section title="Dokumenty WZ" icon="pi pi-file" [flush]="true">
<div headerActions>
<button pButton type="button" (click)="createPackingList()"><i class="pi pi-save"></i> Utwórz Packing List</button>
</div>
<p-table
#wzGrid
[value]="wzDataSource()"
[(selection)]="selectedWzRows"
dataKey="mtGroupNum"
[paginator]="true"
[rows]="5"
selectionMode="multiple"
styleClass="p-datatable-sm"
[showGridlines]="true"
[stripedRows]="true"
>
<ng-template #header>
<tr>
<th style="width: 3rem"><p-table-header-checkbox /></th>
<th pSortableColumn="mtGroupNum">Numer WZ</th>
<th pSortableColumn="createDate">Data utworzenia</th>
<th pSortableColumn="refNum">Numer zamówienia</th>
</tr>
</ng-template>
<ng-template #body let-row>
<tr>
<td><p-table-checkbox [value]="row" /></td>
<td>{{ row.mtGroupNum }}</td>
<td>{{ row.createDate | date: 'dd.MM.yyyy' }}</td>
<td>{{ row.refNum }}</td>
</tr>
</ng-template>
</p-table>
</app-content-section>
<app-content-section title="Packling Listy" icon="pi pi-list" [flush]="true">
<p-table [value]="wzHeaders()" [paginator]="true" [rows]="5" selectionMode="single" [(selection)]="selectedHeader" dataKey="id" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
<ng-template #body let-header>
<tr (dblclick)="openPackList(header)">
<td>{{ header.id }}</td>
<td>{{ header.wzNumbers }}</td>
<td>{{ header.createdDate | date: 'dd.MM.yyyy' }}</td>
</tr>
</ng-template>
<ng-template #header>
<tr>
<th>ID</th>
<th>Numery WZ</th>
<th>Data utworzenia</th>
</tr>
</ng-template>
</p-table>
</app-content-section>
}
</app-page-shell>
<p-dialog header="Informacja" [(visible)]="showNoSelectionDialog" [modal]="true" [style]="{ width: '500px' }">
<p>Błąd: Zaznacz przynajmniej jeden rekord, żeby wygenerowac Pack List!</p>
<ng-template #footer>
<button pButton type="button" (click)="showNoSelectionDialog = false">OK</button>
</ng-template>
</p-dialog>
<p-dialog header="Błąd" [(visible)]="showDuplicateDialog" [modal]="true" [style]="{ width: '500px' }">
<p>Błąd: Dla zaznaczonego rekordu istnieje już PackingList!</p>
<ng-template #footer>
<button pButton type="button" (click)="showDuplicateDialog = false">OK</button>
</ng-template>
</p-dialog>
`,
styles: `:host ::ng-deep .w-full { width: 100%; max-width: 400px; }`,
})
export class WarehouseComponent implements OnInit {
private readonly warehouseService = inject(WarehouseService);
private readonly router = inject(Router);
readonly clients = signal<WzClientDto[]>([]);
readonly wzDataSource = signal<MaterialTransactionDto[]>([]);
readonly wzHeaders = signal<WzHeaderDto[]>([]);
readonly materialTransactions = signal<MaterialTransactionDto[]>([]);
readonly isVisible = signal(false);
selectedClientId: string | null = null;
selectedClient: WzClientDto | null = null;
selectedWzRows: MaterialTransactionDto[] = [];
selectedHeader: WzHeaderDto | null = null;
showNoSelectionDialog = false;
showDuplicateDialog = false;
async ngOnInit(): Promise<void> {
const all = await this.warehouseService.getAllClients();
const filtered = all.filter((c) => ['MAGNETI MARELLI', 'MEYLE'].some((n) => n.toLowerCase() === c.name.toLowerCase()));
this.clients.set(filtered);
const saved = localStorage.getItem(SELECTED_CLIENT_KEY);
if (saved && filtered.some((c) => c.id === saved)) {
this.selectedClientId = saved;
await this.onClientChange();
}
}
async onClientChange(): Promise<void> {
if (!this.selectedClientId) {
this.isVisible.set(false);
this.selectedClient = null;
return;
}
this.selectedClient = this.clients().find((c) => c.id === this.selectedClientId) ?? null;
if (!this.selectedClient) return;
this.isVisible.set(true);
localStorage.setItem(SELECTED_CLIENT_KEY, this.selectedClientId);
const txs = await this.warehouseService.getAllClientWzs(
this.selectedClient.customerNumber,
this.selectedClient.customerSequence ?? 0,
);
this.materialTransactions.set(txs);
const grouped = Object.values(
txs.reduce<Record<string, MaterialTransactionDto>>((acc, t) => {
const key = t.mtGroupNum ?? '';
if (!acc[key]) acc[key] = t;
return acc;
}, {}),
);
this.wzDataSource.set(grouped);
this.wzHeaders.set(
await this.warehouseService.getAllClientWzHeaders(
this.selectedClient.customerNumber,
this.selectedClient.customerSequence ?? 0,
),
);
}
openPackList(header: WzHeaderDto): void {
if (!this.selectedClient) return;
this.router.navigate([`/Warehouse/${this.selectedClient.shortName}/PackList/${header.id}`]);
}
async createPackingList(): Promise<void> {
if (!this.selectedWzRows.length) {
this.showNoSelectionDialog = true;
return;
}
const wzNumbers = [...new Set(this.selectedWzRows.map((x) => x.mtGroupNum))].join(', ');
if (this.wzHeaders().some((x) => x.wzNumbers === wzNumbers)) {
this.showDuplicateDialog = true;
return;
}
const wzHeader: WzHeaderDto = {
id: generateGuid(),
fk_Client: this.selectedClient?.id,
createdDate: new Date().toISOString(),
wzNumbers,
};
await this.warehouseService.createWzHeader(wzHeader);
const shortName = this.selectedClient?.shortName.toUpperCase();
const selectedNums = new Set(this.selectedWzRows.map((x) => x.mtGroupNum));
const related = this.materialTransactions().filter((x) => selectedNums.has(x.mtGroupNum));
if (shortName === 'MEYLE') {
const rows: WzRowMeyleDto[] = [];
for (const mt of related) {
const customerOrder = await this.warehouseService.getCustomerOrder(mt.refNum ?? '');
const item = await this.warehouseService.getItem(mt.item ?? '', customerOrder.custNum);
rows.push({
id: generateGuid(),
quantity: Math.abs(mt.qty ?? 0),
itemNumber: item.custItem,
orderNumber: customerOrder.custPo,
wzNumber: mt.mtGroupNum ?? '',
fk_Header: wzHeader.id,
transactionNumber: mt.transNum ?? 0,
partNumberSl: mt.nr_KARTY_KONTROLNEJ,
faIndex: item.item,
});
}
await this.warehouseService.createWzRowsMeyle(rows);
this.router.navigate(['/Warehouse/Meyle/PackList', wzHeader.id]);
} else if (shortName === 'MARELLI') {
const rows: WzRowMarelliDto[] = [];
for (const mt of related) {
const customerOrder = await this.warehouseService.getCustomerOrder(mt.refNum ?? '');
const item = await this.warehouseService.getItem(mt.item ?? '', customerOrder.custNum);
rows.push({
id: generateGuid(),
quantity: Math.abs(mt.qty ?? 0),
itemNumber: item.custItem,
orderNumber: customerOrder.custPo,
wzNumber: mt.mtGroupNum ?? '',
fkHeader: wzHeader.id,
transactionNumber: mt.transNum ?? 0,
type: 'MIX',
faIndex: item.item,
engineerNumber: item.uf_FKR_CustItem2 ?? '',
});
}
await this.warehouseService.createWzRowsMarelli(rows);
this.router.navigate(['/Warehouse/Marelli/PackList', wzHeader.id]);
}
}
}