Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group = 'com.flexcodelabs'
version = '0.0.31'
version = '0.0.32'
description = 'Flextuma App'

java {
Expand Down
127 changes: 127 additions & 0 deletions docs/frontend-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,130 @@ The API uses standard HTTP status codes:
- `401 Unauthorized`: Invalid PAT or session.
- `403 Forbidden`: Insufficient permissions.
- `429 Too Many Requests`: Rate limit exceeded (Bucket4j).

## 4. Dashboard Integration

The dashboard can now use user-scoped summary data directly from the backend. All data below is filtered to the currently authenticated user.

### Summary Endpoint

- `GET /api/dashboard/summary`

Example response:

```json
{
"userId": "6269df23-f8a0-4776-bd89-3015521bc19d",
"username": "admin",
"sent": 2847,
"failed": 23,
"balanceAmount": 50000,
"balance": "TZS 50000",
"currency": "TZS",
"activeCampaigns": 3,
"today": 156,
"thisWeek": 892,
"thisMonth": 2847,
"successRate": 99.2,
"statusBreakdown": {
"sent": 84.5,
"failed": 0.7,
"pending": 10.3,
"other": 4.5
}
}
```

How to use it:
- KPI cards: `sent`, `failed`, `balance`, `activeCampaigns`
- Time cards: `today`, `thisWeek`, `thisMonth`
- Success widget: `successRate`
- Doughnut or stacked chart: `statusBreakdown`

Example fetch:

```javascript
const response = await fetch('/api/dashboard/summary', {
method: 'GET',
credentials: 'include'
});

if (!response.ok) {
throw new Error('Failed to load dashboard summary');
}

const summary = await response.json();
```

### Notifications Feed

- `GET /api/notifications?page=1&pageSize=15`

This endpoint now follows the project pagination style instead of using a custom limit parameter.

Example response:

```json
{
"page": 1,
"total": 42,
"pageSize": 15,
"data": [
{
"id": "2f7fcb14-f99d-4983-9a27-104e55f96eb1",
"phoneNumber": "+255700000000",
"message": "Your OTP is 1234",
"status": "delivered",
"provider": "BEEM",
"createdAt": "2026-03-29T12:00:00",
"updatedAt": "2026-03-29T12:00:10"
}
]
}
```

Recommended usage:
- Recent activity list: render `data`
- Pager or infinite scroll: use `page`, `pageSize`, and `total`
- Status pill: map `sent`, `delivered`, `failed`, `pending`, `processing`

Example fetch:

```javascript
async function loadNotifications(page = 1, pageSize = 15) {
const response = await fetch(
`/api/notifications?page=${page}&pageSize=${pageSize}`,
{ credentials: 'include' }
);

if (!response.ok) {
throw new Error('Failed to load notifications');
}

return response.json();
}
```

### Existing Endpoints Still Available

The dashboard can still consume the existing raw resources when needed:
- `GET /api/wallets`
- `GET /api/campaigns`
- `GET /api/smsLogs`
- `POST /api/logout`

Recommended pattern:
- Use `/api/dashboard/summary` for top-level dashboard widgets
- Use `/api/notifications` for recent message activity
- Use the existing resource endpoints for full management pages and drill-down tables

### SPA Routing

The backend now serves the frontend `index.html` for any non-API route.

Examples:
- `/dashboard`
- `/dashboard/campaigns`
- `/settings/profile`

These routes will return the client app entry file, while `/api/**` remains reserved for backend APIs.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Optional;
Expand All @@ -24,9 +25,25 @@ public class CurrentUserResolver {

public Optional<User> getCurrentUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated() || !(auth.getPrincipal() instanceof String username)) {
if (auth == null || !auth.isAuthenticated()) {
return Optional.empty();
}

Object principal = auth.getPrincipal();
String username = null;

if (principal instanceof UserDetails userDetails) {
username = userDetails.getUsername();
} else if (principal instanceof String principalName) {
username = principalName;
} else if (auth.getName() != null && !auth.getName().isBlank()) {
username = auth.getName();
}

if (username == null || "anonymousUser".equalsIgnoreCase(username)) {
return Optional.empty();
}

return userRepository.findByUsername(username);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.flexcodelabs.flextuma.core.repositories;

import com.flexcodelabs.flextuma.core.entities.auth.User;
import com.flexcodelabs.flextuma.core.entities.sms.SmsCampaign;
import com.flexcodelabs.flextuma.core.enums.SmsCampaignStatus;
import org.springframework.data.domain.Pageable;
Expand All @@ -8,6 +9,7 @@
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.UUID;

Expand All @@ -20,4 +22,6 @@ List<SmsCampaign> findDueCampaigns(
@Param("status") SmsCampaignStatus status,
@Param("now") LocalDateTime now,
Pageable pageable);

long countByCreatedByAndStatusIn(User user, Collection<SmsCampaignStatus> statuses);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.flexcodelabs.flextuma.core.repositories;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import org.springframework.data.domain.Page;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;

import com.flexcodelabs.flextuma.core.entities.auth.User;
import com.flexcodelabs.flextuma.core.entities.sms.SmsLog;
import com.flexcodelabs.flextuma.core.enums.SmsLogStatus;

Expand All @@ -23,4 +28,13 @@ List<SmsLog> findDueMessages(
org.springframework.data.domain.Pageable pageable);

Optional<SmsLog> findByProviderResponse(String providerResponse);

Page<SmsLog> findByCreatedByOrderByCreatedDesc(User user, Pageable pageable);

long countByCreatedByAndStatus(User user, SmsLogStatus status);

long countByCreatedByAndStatusIn(User user, Collection<SmsLogStatus> statuses);

long countByCreatedByAndStatusInAndCreatedGreaterThanEqual(User user, Collection<SmsLogStatus> statuses,
LocalDateTime created);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public interface WalletRepository extends JpaRepository<Wallet, UUID>, JpaSpecif
List<Wallet> findByCreatedBy(User user);

List<Wallet> findByCreatedByAndBalanceGreaterThan(User user, java.math.BigDecimal balance);

java.util.Optional<Wallet> findTopByCreatedByOrderByCreatedDesc(User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,23 @@ public ResponseEntity<Resource> serveCatchAll(HttpServletRequest request) {
return serveStatic(indexFile);
}

if (!path.contains(".")) {
if (path.startsWith("api/")) {
return ResponseEntity.notFound().build();
}

if (path.startsWith("assets/")) {
return serveStatic(path);
}

if (path.equals("favicon.ico")) {
return serveStatic(path);
}

if (path.startsWith(".well-known/")) {
return serveStatic(indexFile);
}

return serveStatic(path);
return serveStatic(indexFile);
}

private ResponseEntity<Resource> serveStatic(String path) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.flexcodelabs.flextuma.modules.auth.controllers;

import java.math.BigDecimal;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -100,7 +101,7 @@ public ResponseEntity<Object> login(
}

@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletRequest request, HttpServletResponse response) {
public ResponseEntity<Object> logout(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) {
securityLogService.logLogout(auth.getName(), request);
Expand All @@ -118,7 +119,7 @@ public ResponseEntity<Void> logout(HttpServletRequest request, HttpServletRespon
cookie.setMaxAge(0);
response.addCookie(cookie);

return ResponseEntity.ok().build();
return ResponseEntity.ok(Map.of("message", "Logged out successfully"));
}

@GetMapping("/me")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.flexcodelabs.flextuma.modules.dashboard.controllers;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.flexcodelabs.flextuma.modules.dashboard.dtos.DashboardSummaryDTO;
import com.flexcodelabs.flextuma.modules.dashboard.services.DashboardService;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/dashboard")
@RequiredArgsConstructor
public class DashboardController {

private final DashboardService dashboardService;

@GetMapping("/summary")
public ResponseEntity<DashboardSummaryDTO> getSummary() {
return ResponseEntity.ok(dashboardService.getSummary());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.flexcodelabs.flextuma.modules.dashboard.dtos;

import java.time.LocalDateTime;
import java.util.UUID;

import lombok.Builder;

@Builder
public record DashboardNotificationDTO(
UUID id,
String phoneNumber,
String message,
String status,
String provider,
LocalDateTime createdAt,
LocalDateTime updatedAt) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.flexcodelabs.flextuma.modules.dashboard.dtos;

import java.math.BigDecimal;
import java.util.Map;
import java.util.UUID;

import lombok.Builder;

@Builder
public record DashboardSummaryDTO(
UUID userId,
String username,
long sent,
long failed,
BigDecimal balanceAmount,
String balance,
String currency,
long activeCampaigns,
long today,
long thisWeek,
long thisMonth,
double successRate,
Map<String, Double> statusBreakdown) {
}
Loading