| @@ -2784,10 +2784,12 @@ components: | |||
| $ref: '#/components/schemas/User' | |||
| captainName: | |||
| type: string | |||
| startDate: | |||
| type: string | |||
| format: date-time | |||
| endDate: | |||
| signatureUrl: | |||
| readOnly: true | |||
| type: | |||
| - string | |||
| - 'null' | |||
| completedDate: | |||
| type: string | |||
| format: date-time | |||
| createdAt: | |||
| @@ -2798,8 +2800,7 @@ components: | |||
| format: date-time | |||
| required: | |||
| - captainName | |||
| - startDate | |||
| - endDate | |||
| - completedDate | |||
| UserTrip.jsonld: | |||
| type: object | |||
| description: '' | |||
| @@ -2839,10 +2840,12 @@ components: | |||
| $ref: '#/components/schemas/User.jsonld' | |||
| captainName: | |||
| type: string | |||
| startDate: | |||
| type: string | |||
| format: date-time | |||
| endDate: | |||
| signatureUrl: | |||
| readOnly: true | |||
| type: | |||
| - string | |||
| - 'null' | |||
| completedDate: | |||
| type: string | |||
| format: date-time | |||
| createdAt: | |||
| @@ -2853,8 +2856,7 @@ components: | |||
| format: date-time | |||
| required: | |||
| - captainName | |||
| - startDate | |||
| - endDate | |||
| - completedDate | |||
| UserTripEvent: | |||
| type: object | |||
| description: '' | |||
| @@ -133,8 +133,8 @@ export const userTripForm = new FormGroup({ | |||
| trip: new FormControl(null, []), | |||
| user: new FormControl(null, []), | |||
| captainName: new FormControl(null, [Validators.required]), | |||
| startDate: new FormControl(null, [Validators.required]), | |||
| endDate: new FormControl(null, [Validators.required]), | |||
| signatureUrl: new FormControl(null, []), | |||
| completedDate: new FormControl(null, [Validators.required]), | |||
| createdAt: new FormControl(null, []) | |||
| }); | |||
| @@ -143,8 +143,8 @@ export const userTripJsonldForm = new FormGroup({ | |||
| trip: new FormControl(null, []), | |||
| user: new FormControl(null, []), | |||
| captainName: new FormControl(null, [Validators.required]), | |||
| startDate: new FormControl(null, [Validators.required]), | |||
| endDate: new FormControl(null, [Validators.required]), | |||
| signatureUrl: new FormControl(null, []), | |||
| completedDate: new FormControl(null, [Validators.required]), | |||
| createdAt: new FormControl(null, []) | |||
| }); | |||
| @@ -14,8 +14,8 @@ | |||
| ></app-trip-form> | |||
| </mat-tab> | |||
| <mat-tab label="{{ 'trip.itinerary' | translate }}"> | |||
| <div class="container-fluid p-4 bg-dark text-white"> | |||
| <h4 class="mb-4">trip.locations</h4> | |||
| <div> | |||
| <h4 class="mb-4">{{ 'trip.itinerary_locations' | translate }}</h4> | |||
| <div *ngFor="let tripLocation of tripLocations; let i = index" class="mb-4"> | |||
| <div class="row"> | |||
| @@ -31,7 +31,7 @@ | |||
| [dataSet]="tripLocation.location" | |||
| > | |||
| </app-search-select> | |||
| <input [id]="'location_' + i" type="hidden" formControlName="location"/> | |||
| <!-- <input [id]="'location_' + i" type="hidden" formControlName="location"/>--> | |||
| </div> | |||
| <div class="col-12 col-md-2 mb-3"> | |||
| @@ -67,34 +67,30 @@ | |||
| [checked]="tripLocation.isArrival" | |||
| (change)="onIsArrivalChange($event, i)" | |||
| > | |||
| <label class="form-check-label text-white" [for]="'isArrival_' + i"> | |||
| trip.is_arrival | |||
| <label class="form-check-label" [for]="'isArrival_' + i"> | |||
| {{ 'trip.is_arrival' | translate }} | |||
| </label> | |||
| </div> | |||
| </div> | |||
| <div class="col-12 col-md-2 mb-3 d-flex align-items-end"> | |||
| <button type="button" class="btn btn-danger" (click)="removeTripLocation(i)"> | |||
| basic.remove | |||
| </button> | |||
| <button type="button" class="btn btn-danger" (click)="removeTripLocation(i)">X</button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="my-3"> | |||
| <button type="button" class="btn btn-primary" (click)="addNewTripLocation()"> | |||
| basic.add | |||
| </button> | |||
| <button type="button" class="btn btn-primary" (click)="addNewTripLocation()">+</button> | |||
| </div> | |||
| <div class="mt-4"> | |||
| <button type="button" class="btn btn-success" (click)="saveAllTripLocations()"> | |||
| Save | |||
| {{ 'trip.save_itinerary' | translate }} | |||
| </button> | |||
| </div> | |||
| </div> | |||
| </mat-tab> | |||
| <mat-tab label="{{ 'user_trip.view' | translate }}"> | |||
| <mat-tab label="{{ 'trip.assigned_users' | translate }}"> | |||
| </mat-tab> | |||
| </mat-tab-group> | |||
| @@ -1,11 +1,19 @@ | |||
| import { Component, OnInit, AfterViewInit, QueryList, ViewChildren } from '@angular/core'; | |||
| import { FormBuilder, FormGroup } from '@angular/forms'; | |||
| import { TripJsonld, TripLocationJsonld, TripLocationService, TripService, LocationService, LocationJsonld } from "@app/core/api/v1"; | |||
| import { | |||
| TripJsonld, | |||
| TripLocationJsonld, | |||
| TripLocationService, | |||
| TripService, | |||
| LocationService, | |||
| LocationJsonld, | |||
| UserTripService, UserService, UserTripJsonld, UserJsonld | |||
| } from "@app/core/api/v1"; | |||
| import { AppHelperService } from "@app/_helpers/app-helper.service"; | |||
| import { ActivatedRoute } from "@angular/router"; | |||
| import { FormMode, FormSubmitEvent } from "@app/_components/_abstract/abstract-data-form-component"; | |||
| import { ModalStatus } from "@app/_helpers/modal.states"; | |||
| import { Observable } from 'rxjs'; | |||
| import {firstValueFrom, Observable} from 'rxjs'; | |||
| import { ListColDefinition } from '@app/_components/list/list-col-definition'; | |||
| import { SearchSelectComponent } from '@app/_components/search-select/search-select.component'; | |||
| @@ -18,6 +26,8 @@ export class TripDetailComponent implements OnInit, AfterViewInit { | |||
| protected trip!: TripJsonld; | |||
| protected readonly FormMode = FormMode; | |||
| protected tripLocations: TripLocationJsonld[] = []; | |||
| protected userTrips: UserTripJsonld[] = []; | |||
| protected users: UserJsonld[] = []; | |||
| protected locationForms: FormGroup[] = []; | |||
| protected locationColDefinitions: ListColDefinition[] = SearchSelectComponent.getDefaultColDefLocations(); | |||
| @@ -27,6 +37,8 @@ export class TripDetailComponent implements OnInit, AfterViewInit { | |||
| private tripService: TripService, | |||
| private tripLocationService: TripLocationService, | |||
| private locationService: LocationService, | |||
| private userTripService: UserTripService, | |||
| private userService: UserService, | |||
| protected appHelperService: AppHelperService, | |||
| private route: ActivatedRoute, | |||
| private fb: FormBuilder | |||
| @@ -38,6 +50,13 @@ export class TripDetailComponent implements OnInit, AfterViewInit { | |||
| data => { | |||
| this.trip = data; | |||
| this.loadTripLocations(); | |||
| this.userTripService.userTripsGetCollection( | |||
| ).subscribe( | |||
| data => { | |||
| this.userTrips = data.member; | |||
| console.log(this.userTrips); | |||
| } | |||
| ) | |||
| } | |||
| ); | |||
| }); | |||
| @@ -195,13 +214,13 @@ export class TripDetailComponent implements OnInit, AfterViewInit { | |||
| const savePromises = validTripLocations.map(tripLocation => { | |||
| if (tripLocation.id) { | |||
| // Update existing | |||
| return this.tripLocationService.tripLocationsIdPatch( | |||
| return firstValueFrom(this.tripLocationService.tripLocationsIdPatch( | |||
| this.appHelperService.extractId(tripLocation.id), | |||
| tripLocation | |||
| ).toPromise(); | |||
| )); | |||
| } else { | |||
| // Create new | |||
| return this.tripLocationService.tripLocationsPost(tripLocation).toPromise(); | |||
| return firstValueFrom(this.tripLocationService.tripLocationsPost(tripLocation)); | |||
| } | |||
| }); | |||
| @@ -1,20 +1,15 @@ | |||
| import { Component } from '@angular/core'; | |||
| import { | |||
| AbstractDataFormComponent, DeleteFunction, | |||
| SaveFunction, | |||
| UpdateFunction | |||
| AbstractDataFormComponent | |||
| } from "@app/_components/_abstract/abstract-data-form-component"; | |||
| import { | |||
| LocationJsonld, LocationService, | |||
| ShippingCompanyService, | |||
| LocationService, | |||
| TripJsonld, | |||
| TripService, | |||
| VesselJsonld, | |||
| VesselService | |||
| } from "@app/core/api/v1"; | |||
| import {SearchSelectComponent} from "@app/_components/search-select/search-select.component"; | |||
| import {tripForm, vesselForm} from "@app/_forms/apiForms"; | |||
| import {FormGroup} from "@angular/forms"; | |||
| import {tripForm} from "@app/_forms/apiForms"; | |||
| import {TranslateService} from "@ngx-translate/core"; | |||
| import {Router} from "@angular/router"; | |||
| import {ROUTE_BASE_DATA} from "@app/app-routing.module"; | |||
| @@ -1 +0,0 @@ | |||
| <p>user-trip-form works!</p> | |||
| @@ -1,23 +0,0 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { UserTripFormComponent } from './user-trip-form.component'; | |||
| describe('UserTripFormComponent', () => { | |||
| let component: UserTripFormComponent; | |||
| let fixture: ComponentFixture<UserTripFormComponent>; | |||
| beforeEach(async () => { | |||
| await TestBed.configureTestingModule({ | |||
| declarations: [UserTripFormComponent] | |||
| }) | |||
| .compileComponents(); | |||
| fixture = TestBed.createComponent(UserTripFormComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -1,10 +0,0 @@ | |||
| import { Component } from '@angular/core'; | |||
| @Component({ | |||
| selector: 'app-user-trip-form', | |||
| templateUrl: './user-trip-form.component.html', | |||
| styleUrl: './user-trip-form.component.scss' | |||
| }) | |||
| export class UserTripFormComponent { | |||
| } | |||
| @@ -1 +1,7 @@ | |||
| <p>user-trip-list works!</p> | |||
| <div class="spt-container"> | |||
| <app-list #listComponent | |||
| [listId]="'userTripList'" | |||
| [getDataFunction]="getData" | |||
| [listColDefinitions]="listColDefinitions" | |||
| ></app-list> | |||
| </div> | |||
| @@ -1,6 +1,5 @@ | |||
| import {Component, ViewChild} from '@angular/core'; | |||
| import {ListComponent} from "@app/_components/list/list.component"; | |||
| import {TripFormComponent} from "@app/_views/trip/trip-form/trip-form.component"; | |||
| import {ListColDefinition} from "@app/_components/list/list-col-definition"; | |||
| import {UserTripService} from "@app/core/api/v1"; | |||
| import {AppHelperService} from "@app/_helpers/app-helper.service"; | |||
| @@ -16,7 +15,6 @@ export class UserTripListComponent { | |||
| @ViewChild("listComponent", {static: false}) listComponent!: ListComponent; | |||
| protected readonly tripFormComponent = TripFormComponent; | |||
| protected listColDefinitions!: ListColDefinition[]; | |||
| constructor( | |||
| @@ -2,5 +2,5 @@ | |||
| <div class="spt-headline d-flex justify-content-between align-items-start"> | |||
| <h2>{{ 'user_trip.view' | translate }}</h2> | |||
| </div> | |||
| <app-trip-list></app-trip-list> | |||
| <app-user-trip-list></app-user-trip-list> | |||
| </div> | |||
| @@ -16,6 +16,8 @@ import { | |||
| } from "@app/_views/shipping-company/shipping-company-detail/shipping-company-detail.component"; | |||
| import {TripComponent} from "@app/_views/trip/trip.component"; | |||
| import {TripDetailComponent} from "@app/_views/trip/trip-detail/trip-detail.component"; | |||
| import {UserTripComponent} from "@app/_views/user-trip/user-trip.component"; | |||
| import {UserTripDetailComponent} from "@app/_views/user-trip/user-trip-detail/user-trip-detail.component"; | |||
| const accountModule = () => import('@app/_views/account/account.module').then(x => x.AccountModule); | |||
| @@ -24,6 +26,7 @@ export const ROUTE_BASE_DATA = 'base-data'; | |||
| export const ROUTE_LOCATIONS = 'locations'; | |||
| export const ROUTE_SHIPPING_COMPANIES = 'shipping-companies'; | |||
| export const ROUTE_TRIPS = 'trips'; | |||
| export const ROUTE_USER_TRIPS = 'user-trips'; | |||
| export const ROUTE_PROFILE = 'profile'; | |||
| export const ROUTE_USERS = 'users'; | |||
| export const ROUTE_VESSELS = 'vessels'; | |||
| @@ -75,6 +78,22 @@ const routes: Routes = [ | |||
| {path: ':id', component: TripDetailComponent}, | |||
| ] | |||
| }, | |||
| { | |||
| path: ROUTE_USER_TRIPS, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| children: [ | |||
| {path: '', component: UserTripComponent}, | |||
| ] | |||
| }, | |||
| { | |||
| path: ROUTE_USER_TRIPS, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| children: [ | |||
| {path: ':id', component: UserTripDetailComponent}, | |||
| ] | |||
| }, | |||
| { | |||
| path: ROUTE_ZONES, | |||
| component: TwoColumnComponent, | |||
| @@ -66,7 +66,6 @@ import {DatetimePickerComponent} from "@app/_components/datetime-picker/datetime | |||
| import { UserTripComponent } from './_views/user-trip/user-trip.component'; | |||
| import { UserTripListComponent } from './_views/user-trip/user-trip-list/user-trip-list.component'; | |||
| import { UserTripDetailComponent } from './_views/user-trip/user-trip-detail/user-trip-detail.component'; | |||
| import { UserTripFormComponent } from './_views/user-trip/user-trip-form/user-trip-form.component'; | |||
| registerLocaleData(localeDe, 'de-DE'); | |||
| @@ -158,7 +157,6 @@ export function HttpLoaderFactory(http: HttpClient) { | |||
| UserTripComponent, | |||
| UserTripListComponent, | |||
| UserTripDetailComponent, | |||
| UserTripFormComponent, | |||
| ], | |||
| providers: [ | |||
| {provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true}, | |||
| @@ -21,8 +21,8 @@ export interface UserTrip { | |||
| trip?: Trip; | |||
| user?: User; | |||
| captainName: string; | |||
| startDate: string; | |||
| endDate: string; | |||
| readonly signatureUrl?: string | null; | |||
| completedDate: string; | |||
| readonly createdAt?: string | null; | |||
| } | |||
| @@ -25,8 +25,8 @@ export interface UserTripJsonld { | |||
| trip?: TripJsonld; | |||
| user?: UserJsonld; | |||
| captainName: string; | |||
| startDate: string; | |||
| endDate: string; | |||
| readonly signatureUrl?: string | null; | |||
| completedDate: string; | |||
| readonly createdAt?: string | null; | |||
| } | |||
| @@ -27,7 +27,13 @@ | |||
| "end_location": "End location", | |||
| "start_date": "Start date", | |||
| "end_date": "End date", | |||
| "itinerary": "Itinerary" | |||
| "is_arrival": "is arrival", | |||
| "itinerary": "Itinerary", | |||
| "itinerary_locations": "Itinerary locations", | |||
| "add_itinerary_location": "Add itinerary location", | |||
| "remove_itinerary_location": "Remove itinerary location", | |||
| "save_itinerary": "Save itinerary", | |||
| "assigned_users": "Assigned Users (User Trips)" | |||
| }, | |||
| "user_trip": | |||
| { | |||
| @@ -0,0 +1,35 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace DoctrineMigrations; | |||
| use Doctrine\DBAL\Schema\Schema; | |||
| use Doctrine\Migrations\AbstractMigration; | |||
| /** | |||
| * Auto-generated Migration: Please modify to your needs! | |||
| */ | |||
| final class Version20250310140210 extends AbstractMigration | |||
| { | |||
| public function getDescription(): string | |||
| { | |||
| return ''; | |||
| } | |||
| public function up(Schema $schema): void | |||
| { | |||
| // this up() migration is auto-generated, please modify it to your needs | |||
| $this->addSql('ALTER TABLE user_trip ADD signature_id INT DEFAULT NULL, ADD completed_date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', DROP start_date, DROP end_date'); | |||
| $this->addSql('ALTER TABLE user_trip ADD CONSTRAINT FK_CD7B9F2ED61183A FOREIGN KEY (signature_id) REFERENCES media_object (id) ON DELETE SET NULL'); | |||
| $this->addSql('CREATE INDEX IDX_CD7B9F2ED61183A ON user_trip (signature_id)'); | |||
| } | |||
| public function down(Schema $schema): void | |||
| { | |||
| // this down() migration is auto-generated, please modify it to your needs | |||
| $this->addSql('ALTER TABLE user_trip DROP FOREIGN KEY FK_CD7B9F2ED61183A'); | |||
| $this->addSql('DROP INDEX IDX_CD7B9F2ED61183A ON user_trip'); | |||
| $this->addSql('ALTER TABLE user_trip ADD end_date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', DROP signature_id, CHANGE completed_date start_date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\''); | |||
| } | |||
| } | |||
| @@ -0,0 +1,31 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace DoctrineMigrations; | |||
| use Doctrine\DBAL\Schema\Schema; | |||
| use Doctrine\Migrations\AbstractMigration; | |||
| /** | |||
| * Auto-generated Migration: Please modify to your needs! | |||
| */ | |||
| final class Version20250310141546 extends AbstractMigration | |||
| { | |||
| public function getDescription(): string | |||
| { | |||
| return ''; | |||
| } | |||
| public function up(Schema $schema): void | |||
| { | |||
| // this up() migration is auto-generated, please modify it to your needs | |||
| $this->addSql('CREATE UNIQUE INDEX unique_user_trip ON user_trip (trip_id, user_id)'); | |||
| } | |||
| public function down(Schema $schema): void | |||
| { | |||
| // this down() migration is auto-generated, please modify it to your needs | |||
| $this->addSql('DROP INDEX unique_user_trip ON user_trip'); | |||
| } | |||
| } | |||
| @@ -94,11 +94,11 @@ class UserTripApi | |||
| #[Assert\NotBlank] | |||
| public string $captainName; | |||
| #[Assert\NotBlank] | |||
| public \DateTimeImmutable $startDate; | |||
| #[ApiProperty(writable: false)] | |||
| public ?string $signatureUrl = null; | |||
| #[Assert\NotBlank] | |||
| public \DateTimeImmutable $endDate; | |||
| public \DateTimeImmutable $completedDate; | |||
| #[ApiProperty(writable: false)] | |||
| public ?\DateTimeImmutable $createdAt = null; | |||
| @@ -11,6 +11,7 @@ use Doctrine\ORM\Mapping as ORM; | |||
| #[ORM\Entity] | |||
| #[ORM\Table(name: 'user_trip')] | |||
| #[ORM\UniqueConstraint(name: "unique_user_trip", columns: ["trip_id", "user_id"])] | |||
| class UserTrip | |||
| { | |||
| #[ORM\Id] | |||
| @@ -29,11 +30,12 @@ class UserTrip | |||
| #[ORM\Column(length: 255)] | |||
| private string $captainName; | |||
| #[ORM\Column] | |||
| private DateTimeImmutable $startDate; | |||
| #[ORM\ManyToOne] | |||
| #[ORM\JoinColumn(nullable: true, onDelete: "SET NULL")] | |||
| private ?MediaObject $signature = null; | |||
| #[ORM\Column] | |||
| private DateTimeImmutable $endDate; | |||
| private DateTimeImmutable $completedDate; | |||
| #[ORM\Column] | |||
| private DateTimeImmutable $createdAt; | |||
| @@ -51,8 +53,7 @@ class UserTrip | |||
| $this->trip = $trip; | |||
| $this->user = $user; | |||
| $this->captainName = $captainName; | |||
| $this->startDate = $startDate; | |||
| $this->endDate = $endDate; | |||
| $this->completedDate = $endDate; | |||
| $this->createdAt = new DateTimeImmutable(); | |||
| $this->userTripEvents = new ArrayCollection(); | |||
| } | |||
| @@ -95,25 +96,24 @@ class UserTrip | |||
| return $this; | |||
| } | |||
| public function getStartDate(): DateTimeImmutable | |||
| public function getSignature(): ?MediaObject | |||
| { | |||
| return $this->startDate; | |||
| return $this->signature; | |||
| } | |||
| public function setStartDate(DateTimeImmutable $startDate): self | |||
| public function setSignature(?MediaObject $signature): void | |||
| { | |||
| $this->startDate = $startDate; | |||
| return $this; | |||
| $this->signature = $signature; | |||
| } | |||
| public function getEndDate(): DateTimeImmutable | |||
| public function getCompletedDate(): DateTimeImmutable | |||
| { | |||
| return $this->endDate; | |||
| return $this->completedDate; | |||
| } | |||
| public function setEndDate(DateTimeImmutable $endDate): self | |||
| public function setCompletedDate(DateTimeImmutable $completedDate): self | |||
| { | |||
| $this->endDate = $endDate; | |||
| $this->completedDate = $completedDate; | |||
| return $this; | |||
| } | |||
| @@ -67,7 +67,7 @@ class UserTripApiToEntityMapper implements MapperInterface | |||
| $entity->setCaptainName($dto->captainName); | |||
| $entity->setStartDate($dto->startDate); | |||
| $entity->setEndDate($dto->endDate); | |||
| $entity->setCompletedDate($dto->endDate); | |||
| return $entity; | |||
| } | |||
| @@ -6,6 +6,7 @@ use App\ApiResource\TripApi; | |||
| use App\ApiResource\UserApi; | |||
| use App\ApiResource\UserTripApi; | |||
| use App\Entity\UserTrip; | |||
| use App\Service\FileUrlService; | |||
| use Symfonycasts\MicroMapper\AsMapper; | |||
| use Symfonycasts\MicroMapper\MapperInterface; | |||
| use Symfonycasts\MicroMapper\MicroMapperInterface; | |||
| @@ -14,7 +15,8 @@ use Symfonycasts\MicroMapper\MicroMapperInterface; | |||
| class UserTripEntityToApiMapper implements MapperInterface | |||
| { | |||
| public function __construct( | |||
| private MicroMapperInterface $microMapper | |||
| private MicroMapperInterface $microMapper, | |||
| private FileUrlService $fileUrlService | |||
| ) { | |||
| } | |||
| @@ -38,8 +40,8 @@ class UserTripEntityToApiMapper implements MapperInterface | |||
| $dto->dbId = $entity->getId(); | |||
| $dto->captainName = $entity->getCaptainName(); | |||
| $dto->startDate = $entity->getStartDate(); | |||
| $dto->endDate = $entity->getEndDate(); | |||
| $dto->signatureUrl = $this->fileUrlService->getFileUrl($entity->getSignature()); | |||
| $dto->completedDate = $entity->getCompletedDate(); | |||
| $dto->createdAt = $entity->getCreatedAt(); | |||
| $dto->trip = $this->microMapper->map($entity->getTrip(), TripApi::class, [ | |||