ce676f20a4
Too many changes to split them up. This commit contains roughly two months worth of work. This commit introduces the first aggregate and rewrites the ticket system to follow DDD patterns. It adds Spartan NG as the new component library. TanStack Query to replace GraphQL. Oh and Prisma is almost over as well.
199 lines
6.6 KiB
TypeScript
199 lines
6.6 KiB
TypeScript
import { AsyncPipe, DatePipe } from '@angular/common';
|
|
import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
inject,
|
|
signal,
|
|
} from '@angular/core';
|
|
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
import { MatInputModule } from '@angular/material/input';
|
|
import { DialogService } from '@ngneat/dialog';
|
|
import {
|
|
debounceTime,
|
|
distinctUntilChanged,
|
|
filter,
|
|
switchMap,
|
|
tap,
|
|
} from 'rxjs';
|
|
import {
|
|
Ticket,
|
|
Tour,
|
|
} from '../../../core/data-access/graphql/generated/generated';
|
|
import { TicketViewComponent } from '../ticket-view/ticket-view.component';
|
|
import { TicketsService } from '../tickets.service';
|
|
import { format, isAfter } from 'date-fns';
|
|
import { useTicketSearch } from 'src/app/core/tickets/tickets.queries';
|
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
|
import { HlmIcon } from '@spartan-ng/helm/icon';
|
|
import {
|
|
lucideCircleAlert,
|
|
lucideCalendarClock,
|
|
lucideCar,
|
|
} from '@ng-icons/lucide';
|
|
|
|
@Component({
|
|
template: `<div class="flex flex-col items-center justify-start p-4">
|
|
<div class="text-lg font-bold pb-2">Ticket suchen</div>
|
|
<hr />
|
|
<mat-form-field class="w-full">
|
|
<mat-label>Einsatznummer oder Patient</mat-label>
|
|
<input class="w-full" matInput [(ngModel)]="search" name="search" />
|
|
</mat-form-field>
|
|
<hr />
|
|
<div class="w-full">
|
|
@if (searchQuery.isLoading()) {
|
|
<p>Suche Tickets ...</p>
|
|
}
|
|
@if (searchQuery.data(); as result) {
|
|
@for (ticket of result; track ticket.id) {
|
|
<div
|
|
class="group relative flex flex-col gap-1.5 rounded-lg border bg-card p-3 shadow-sm transition-all hover:bg-accent/5 hover:shadow-md cursor-pointer select-none mb-2"
|
|
[class.border-destructive]="ticket.isOverdue"
|
|
[class.bg-destructive/5]="ticket.isOverdue"
|
|
[class.border-border]="!ticket.isOverdue"
|
|
(click)="showTicketView(ticket)"
|
|
>
|
|
<div class="flex items-start justify-between gap-3">
|
|
<span
|
|
class="text-sm font-semibold leading-tight text-foreground line-clamp-2"
|
|
[class.text-destructive]="ticket.isOverdue"
|
|
>
|
|
{{ ticket.patientName }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between mt-1">
|
|
<div class="flex items-center gap-1.5 text-xs">
|
|
@if (ticket.nextStart) {
|
|
<div
|
|
class="flex items-center gap-1.5 transition-colors"
|
|
[class.text-destructive]="ticket.isOverdue"
|
|
[class.font-medium]="ticket.isOverdue"
|
|
[class.text-muted-foreground]="!ticket.isOverdue"
|
|
>
|
|
<ng-icon
|
|
hlmIcon
|
|
[name]="
|
|
ticket.isOverdue
|
|
? 'lucideAlertCircle'
|
|
: 'lucideCalendarClock'
|
|
"
|
|
size="xs"
|
|
/>
|
|
<span>{{ ticket.nextStart | date: 'dd.MM. HH:mm' }}</span>
|
|
</div>
|
|
} @else {
|
|
<span class="text-muted-foreground/50 italic text-[10px]"
|
|
>Bereits gefahren</span
|
|
>
|
|
}
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-medium transition-colors"
|
|
[class.bg-destructive/10]="ticket.isOverdue"
|
|
[class.text-destructive]="ticket.isOverdue"
|
|
[class.bg-secondary]="!ticket.isOverdue"
|
|
[class.text-secondary-foreground]="!ticket.isOverdue"
|
|
>
|
|
<ng-icon hlmIcon name="lucideCar" size="xs" />
|
|
<span>{{ ticket.tourCount }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- <div
|
|
class="flex flex-col justify-between items-center py-2 border-b border-gray-200 cursor-pointer"
|
|
(click)="showTicketView(ticket)"
|
|
>
|
|
<div class="w-full flex justify-between">
|
|
<span>
|
|
{{ ticket.tours.at(0)?.ordinanceType }}
|
|
@if (ticket.tours.at(0)?.category) {
|
|
<span class="text-sm text-gray-600">
|
|
({{ ticket.tours.at(0)?.category }})
|
|
</span>
|
|
}
|
|
</span>
|
|
</div>
|
|
<div class="w-full flex justify-between">
|
|
<span>
|
|
{{ ticket.tours.at(0)?.patientName }}
|
|
{{ ticket.tours.at(0)?.patientSurname }}
|
|
</span>
|
|
<span>
|
|
{{ getToursLength(ticket) }} Tour{{
|
|
getToursLength(ticket) > 1 ? 'en' : ''
|
|
}}
|
|
</span>
|
|
</div>
|
|
</div> -->
|
|
}
|
|
}
|
|
</div>
|
|
</div>`,
|
|
imports: [
|
|
MatFormFieldModule,
|
|
MatInputModule,
|
|
FormsModule,
|
|
NgIcon,
|
|
HlmIcon,
|
|
DatePipe,
|
|
],
|
|
providers: [
|
|
provideIcons({
|
|
lucideCircleAlert,
|
|
lucideCalendarClock,
|
|
lucideCar,
|
|
}),
|
|
],
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
})
|
|
export class TicketsSearchDialogComponent {
|
|
search = signal<string>('');
|
|
searchQuery = useTicketSearch(this.search);
|
|
|
|
private ticketsService = inject(TicketsService);
|
|
private dialog = inject(DialogService);
|
|
|
|
searchCtrl = new FormControl('');
|
|
tickets$ = this.searchCtrl.valueChanges.pipe(
|
|
debounceTime(200),
|
|
distinctUntilChanged(),
|
|
filter((val) => !!val && val?.length > 2),
|
|
switchMap((query) => this.ticketsService.searchTickets(query as string)),
|
|
);
|
|
|
|
showTicketView(ticket: unknown): void {
|
|
const dialogRef = this.dialog.open(TicketViewComponent, {
|
|
data: (ticket as Ticket)?.id as string,
|
|
width: '60vw',
|
|
minHeight: '400px',
|
|
});
|
|
|
|
dialogRef.afterClosed$
|
|
.pipe(tap((result) => result && this.showTicketView(result)))
|
|
.subscribe();
|
|
}
|
|
|
|
getToursLength(ticket: unknown) {
|
|
return (ticket as Ticket & { tours: Tour[] }).tours.filter(
|
|
({ check }) => check === 0,
|
|
).length;
|
|
}
|
|
|
|
getNextStart(tours: Partial<Tour>[]) {
|
|
const sortedStartDates = [
|
|
...tours.map(({ startDate }) => new Date(startDate)),
|
|
].sort((a, b) => a.getTime() - b.getTime());
|
|
for (let i = 0; i < sortedStartDates.length; i++) {
|
|
const currentDate = sortedStartDates.at(i);
|
|
if (currentDate && isAfter(currentDate, new Date())) {
|
|
return format(currentDate, 'HH:mm | dd.MM.yy');
|
|
}
|
|
}
|
|
const latestDate = sortedStartDates.at(-1);
|
|
return latestDate ? format(latestDate, 'HH:mm | dd.MM.yy') : 'No Tours';
|
|
}
|
|
}
|