Service tabs updated

This commit is contained in:
2025-11-20 16:21:48 +05:30
parent 1aa92b7501
commit 4286934d9d
9 changed files with 144 additions and 34 deletions

View File

@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ServiceTile;
import net.shyshkin.study.fullstack.supportportal.backend.domain.HttpResponse;
import net.shyshkin.study.fullstack.supportportal.backend.enumeration.ServiceTileCategory;
import net.shyshkin.study.fullstack.supportportal.backend.service.ServiceTileService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ -26,10 +27,11 @@ public class ServiceTileResource {
public ResponseEntity<ServiceTile> addServiceTile(
@RequestParam String title,
@RequestParam(required = false) String description,
@RequestParam ServiceTileCategory category,
@RequestParam(required = false) Integer displayOrder) {
log.info("Adding new service tile with title: {}", title);
ServiceTile serviceTile = serviceTileService.addServiceTile(title, description, displayOrder);
log.info("Adding new service tile with title: {} and category: {}", title, category);
ServiceTile serviceTile = serviceTileService.addServiceTile(title, description, category, displayOrder);
return ResponseEntity.status(HttpStatus.CREATED).body(serviceTile);
}
@ -39,10 +41,11 @@ public class ServiceTileResource {
@PathVariable Long id,
@RequestParam String title,
@RequestParam(required = false) String description,
@RequestParam ServiceTileCategory category,
@RequestParam(required = false) Integer displayOrder) {
log.info("Updating service tile with id: {}", id);
ServiceTile serviceTile = serviceTileService.updateServiceTile(id, title, description, displayOrder);
ServiceTile serviceTile = serviceTileService.updateServiceTile(id, title, description, category, displayOrder);
return ResponseEntity.ok(serviceTile);
}
@ -83,7 +86,6 @@ public class ServiceTileResource {
);
}
@PutMapping("/{id}/toggle-active")
@PreAuthorize("hasAnyAuthority('user:update')")
public ResponseEntity<ServiceTile> toggleActiveStatus(@PathVariable Long id) {

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.shyshkin.study.fullstack.supportportal.backend.enumeration.ServiceTileCategory;
import javax.persistence.*;
import java.io.Serializable;
@ -27,6 +28,10 @@ public class ServiceTile implements Serializable {
@Column(length = 1000)
private String description;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ServiceTileCategory category;
@JsonProperty("isActive")
@Column(name = "is_active")
private boolean isActive;

View File

@ -0,0 +1,16 @@
package net.shyshkin.study.fullstack.supportportal.backend.enumeration;
public enum ServiceTileCategory {
TRAUMA_CARE("Trauma Care"),
ACUTE_CARE_SURGERY("Acute Care Surgery");
private final String displayName;
ServiceTileCategory(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@ -1,14 +1,15 @@
package net.shyshkin.study.fullstack.supportportal.backend.service;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ServiceTile;
import net.shyshkin.study.fullstack.supportportal.backend.enumeration.ServiceTileCategory;
import java.util.List;
public interface ServiceTileService {
ServiceTile addServiceTile(String title, String description, Integer displayOrder);
ServiceTile addServiceTile(String title, String description, ServiceTileCategory category, Integer displayOrder);
ServiceTile updateServiceTile(Long id, String title, String description, Integer displayOrder);
ServiceTile updateServiceTile(Long id, String title, String description, ServiceTileCategory category, Integer displayOrder);
List<ServiceTile> getActiveServiceTiles();

View File

@ -3,6 +3,7 @@ package net.shyshkin.study.fullstack.supportportal.backend.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ServiceTile;
import net.shyshkin.study.fullstack.supportportal.backend.enumeration.ServiceTileCategory;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.ServiceTileNotFoundException;
import net.shyshkin.study.fullstack.supportportal.backend.repository.ServiceTileRepository;
import net.shyshkin.study.fullstack.supportportal.backend.service.ServiceTileService;
@ -20,25 +21,27 @@ public class ServiceTileServiceImpl implements ServiceTileService {
private final ServiceTileRepository serviceTileRepository;
@Override
public ServiceTile addServiceTile(String title, String description, Integer displayOrder) {
log.info("Adding new service tile with title: {}", title);
public ServiceTile addServiceTile(String title, String description, ServiceTileCategory category, Integer displayOrder) {
log.info("Adding new service tile with title: {} and category: {}", title, category);
ServiceTile serviceTile = new ServiceTile();
serviceTile.setTitle(title);
serviceTile.setDescription(description);
serviceTile.setCategory(category);
serviceTile.setDisplayOrder(displayOrder != null ? displayOrder : 0);
serviceTile.setActive(true); // New tiles are active by default
serviceTile.setActive(true);
return serviceTileRepository.save(serviceTile);
}
@Override
public ServiceTile updateServiceTile(Long id, String title, String description, Integer displayOrder) {
public ServiceTile updateServiceTile(Long id, String title, String description, ServiceTileCategory category, Integer displayOrder) {
log.info("Updating service tile with id: {}", id);
ServiceTile serviceTile = getServiceTileById(id);
serviceTile.setTitle(title);
serviceTile.setDescription(description);
serviceTile.setCategory(category);
if (displayOrder != null) {
serviceTile.setDisplayOrder(displayOrder);

View File

@ -426,6 +426,42 @@
font-size: 14px;
flex: 1;
}
/* Add to existing CSS */
.badge-group {
display: flex;
align-items: center;
gap: 8px;
}
.category-badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
background: #e0e7ff;
color: #4338ca;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Select dropdown styling */
select.form-input {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 40px;
}
select.form-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Form Styles */
.form-section {

View File

@ -31,6 +31,8 @@
<div class="service-tile-info">
<div class="service-tile-header-row">
<h3>{{ serviceTile?.title }}</h3>
<div class="badge-group">
<span class="category-badge">{{ getCategoryLabel(serviceTile?.category) }}</span>
<span class="status-badge" [class.status-active]="serviceTile?.isActive"
[class.status-inactive]="!serviceTile?.isActive">
<i class="fa" [class.fa-check-circle]="serviceTile?.isActive"
@ -38,6 +40,7 @@
{{ serviceTile?.isActive ? 'Active' : 'Inactive' }}
</span>
</div>
</div>
<p class="description" *ngIf="serviceTile?.description">
{{ serviceTile.description }}</p>
<div class="order-badge">Order: {{ serviceTile?.displayOrder }}</div>
@ -92,14 +95,20 @@
</div>
<div class="modal-body">
<div class="service-tile-detail-card">
<div class="detail-item"><span class="detail-label">Title:</span>
<div class="detail-item">
<span class="detail-label">Title:</span>
<span class="detail-value">{{ selectedServiceTile.title }}</span>
</div>
<div class="detail-item"><span class="detail-label">Description:</span>
<div class="detail-item">
<span class="detail-label">Category:</span>
<span class="detail-value">{{ getCategoryLabel(selectedServiceTile.category) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Description:</span>
<span class="detail-value">{{ selectedServiceTile.description }}</span>
</div>
<div class="detail-item"><span class="detail-label">Display
Order:</span>
<div class="detail-item">
<span class="detail-label">Display Order:</span>
<span class="detail-value">{{ selectedServiceTile.displayOrder }}</span>
</div>
</div>
@ -118,7 +127,8 @@
<div class="modal-header">
<h3>Add New Service Tile</h3>
<button type="button" class="modal-close" data-bs-dismiss="modal">
<i class="fa fa-times"></i></button>
<i class="fa fa-times"></i>
</button>
</div>
<div class="modal-body">
<div class="form-section">
@ -126,11 +136,21 @@
<label>Title *</label>
<input class="form-input" name="title" ngModel required>
</div>
<div class="form-group">
<label>Category *</label>
<select class="form-input" name="category" ngModel required>
<option value="" disabled selected>Select Category</option>
<option *ngFor="let cat of categories" [value]="cat.value">{{ cat.label }}</option>
</select>
</div>
<div class="form-group">
<label>Description * (Separate points with commas)</label>
<textarea class="form-textarea" name="description" ngModel required
placeholder="e.g., Point one, Point two, Point three"></textarea>
</div>
<div class="form-group">
<label>Display Order</label>
<input type="number" class="form-input" name="displayOrder" ngModel value="0">
@ -155,7 +175,8 @@
<div class="modal-header">
<h3>Edit Service Tile</h3>
<button type="button" class="modal-close" data-bs-dismiss="modal">
<i class="fa fa-times"></i></button>
<i class="fa fa-times"></i>
</button>
</div>
<div class="modal-body">
<div class="form-section">
@ -163,11 +184,21 @@
<label>Title *</label>
<input class="form-input" name="title" [(ngModel)]="selectedServiceTile.title" required>
</div>
<div class="form-group">
<label>Category *</label>
<select class="form-input" name="category" [(ngModel)]="selectedServiceTile.category" required>
<option value="" disabled>Select Category</option>
<option *ngFor="let cat of categories" [value]="cat.value">{{ cat.label }}</option>
</select>
</div>
<div class="form-group">
<label>Description * (Separate points with commas)</label>
<textarea class="form-textarea" name="description" [(ngModel)]="selectedServiceTile.description"
required placeholder="e.g., Point one, Point two, Point three"></textarea>
</div>
<div class="form-group">
<label>Display Order</label>
<input type="number" class="form-input" name="displayOrder"

View File

@ -29,7 +29,12 @@ export class ServiceTileComponent implements OnInit, OnDestroy, AfterViewInit {
public refreshing: boolean = false;
private subs = new SubSink();
// ✅ Modal references (No hidden button clicks anymore)
// Category options
public categories = [
{ value: 'ACUTE_CARE_SURGERY', label: 'Acute Care Surgery' },
{ value: 'TRAUMA_CARE', label: 'Trauma Care' }
];
@ViewChild('addServiceTileModal') addServiceTileModal!: ElementRef;
@ViewChild('editServiceTileModal') editServiceTileModal!: ElementRef;
@ViewChild('viewServiceTileModal') viewServiceTileModal!: ElementRef;
@ -50,16 +55,14 @@ export class ServiceTileComponent implements OnInit, OnDestroy, AfterViewInit {
}
ngAfterViewInit(): void {
// ✅ Initialize Bootstrap modal instances
this.addModal = new Modal(this.addServiceTileModal.nativeElement);
this.editModal = new Modal(this.editServiceTileModal.nativeElement);
this.viewModal = new Modal(this.viewServiceTileModal.nativeElement);
}
openAddModal(): void {
this.addModal.show();
}
}
ngOnDestroy(): void {
this.subs.unsubscribe();
@ -88,7 +91,7 @@ export class ServiceTileComponent implements OnInit, OnDestroy, AfterViewInit {
public onSelectServiceTile(serviceTile: ServiceTile): void {
this.selectedServiceTile = { ...serviceTile };
this.viewModal.show(); // ✅ Show view modal
this.viewModal.show();
}
public onAddNewServiceTile(form: NgForm): void {
@ -98,8 +101,9 @@ export class ServiceTileComponent implements OnInit, OnDestroy, AfterViewInit {
this.subs.sink = this.serviceTileService.addServiceTile(formData).subscribe(
serviceTile => {
this.getServiceTiles(false);
this.addModal.hide(); // ✅ Close modal cleanly
this.addModal.hide();
form.reset();
this.notificationService.notify(NotificationType.SUCCESS, 'Service tile created successfully');
},
(errorResponse: HttpErrorResponse) => {
this.sendErrorNotification(errorResponse.error.message);
@ -109,7 +113,7 @@ export class ServiceTileComponent implements OnInit, OnDestroy, AfterViewInit {
public onEditServiceTile(serviceTile: ServiceTile): void {
this.selectedServiceTile = { ...serviceTile };
this.editModal.show(); // ✅ Show edit modal
this.editModal.show();
}
public onUpdateServiceTile(form: NgForm): void {
@ -123,7 +127,8 @@ export class ServiceTileComponent implements OnInit, OnDestroy, AfterViewInit {
this.subs.sink = this.serviceTileService.updateServiceTile(this.selectedServiceTile.id!, formData).subscribe(
() => {
this.getServiceTiles(false);
this.editModal.hide(); // ✅ Close modal cleanly
this.editModal.hide();
this.notificationService.notify(NotificationType.SUCCESS, 'Service tile updated successfully');
},
(errorResponse: HttpErrorResponse) => {
this.sendErrorNotification(errorResponse.error.message);
@ -160,13 +165,23 @@ export class ServiceTileComponent implements OnInit, OnDestroy, AfterViewInit {
searchTerm = searchTerm.toLowerCase();
this.serviceTiles = this.serviceTileService.getServiceTilesFromLocalStorage()
.filter(t => t.title?.toLowerCase().includes(searchTerm) || t.description?.toLowerCase().includes(searchTerm));
.filter(t =>
t.title?.toLowerCase().includes(searchTerm) ||
t.description?.toLowerCase().includes(searchTerm) ||
this.getCategoryLabel(t.category).toLowerCase().includes(searchTerm)
);
}
public getCategoryLabel(category: string): string {
const cat = this.categories.find(c => c.value === category);
return cat ? cat.label : category;
}
private createServiceTileFormData(serviceTile: any): FormData {
const formData = new FormData();
formData.append('title', serviceTile.title || '');
formData.append('description', serviceTile.description || '');
formData.append('category', serviceTile.category || '');
formData.append('displayOrder', (serviceTile.displayOrder || 0).toString());
return formData;
}

View File

@ -2,6 +2,7 @@ export interface ServiceTile {
id?: number;
title: string;
description: string;
category: 'TRAUMA_CARE' | 'ACUTE_CARE_SURGERY';
isActive: boolean;
displayOrder?: number;
createdDate?: Date;