feat: complete security overhaul with JWT backend and authenticated frontend API
This commit is contained in:
@@ -1,3 +1,22 @@
|
||||
# Positeasy Clone Frontend (Counter/Admin)
|
||||
|
||||
## 🔐 Security Hardening & API Migration (Branch: krishna)
|
||||
|
||||
This branch contains critical security updates and infrastructure changes to support JWT-based authentication across the Positeasy ecosystem.
|
||||
|
||||
### Key Changes:
|
||||
- **Authenticated API Wrapper**: Migrated from standard `fetch` to a centralized `src/api.ts` wrapper. This wrapper automatically handles:
|
||||
- Injection of the `Authorization: Bearer <token>` header.
|
||||
- Consistent error handling for API requests.
|
||||
- **Security Hardening**: Updated all major screens (Login, POS, Orders, etc.) to use the new authenticated API layer.
|
||||
- **Backend Integration**: Configured to work seamlessly with the new JWT-protected backend endpoints.
|
||||
|
||||
### Developer Instructions:
|
||||
1. **Always use the API wrapper**: Import `api` from `@/api` (or `src/api.ts`) instead of using `fetch` directly.
|
||||
2. **Migration Script**: `migrate_fetch.ps1` is included for reference.
|
||||
|
||||
---
|
||||
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
17
frontend/migrate_fetch.ps1
Normal file
17
frontend/migrate_fetch.ps1
Normal file
@@ -0,0 +1,17 @@
|
||||
$compDir = "src\components"
|
||||
$files = Get-ChildItem -Path $compDir -Filter "*.tsx"
|
||||
|
||||
foreach ($file in $files) {
|
||||
$path = $file.FullName
|
||||
$content = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8)
|
||||
|
||||
if ($content.Contains("await fetch(")) {
|
||||
if (-not $content.Contains("from '../api'")) {
|
||||
$content = "import { apiFetch } from '../api';" + [System.Environment]::NewLine + $content
|
||||
}
|
||||
$content = $content.Replace("await fetch(", "await apiFetch(")
|
||||
[System.IO.File]::WriteAllText($path, $content, [System.Text.Encoding]::UTF8)
|
||||
Write-Host "Updated component: $($file.Name)"
|
||||
}
|
||||
}
|
||||
Write-Host "Components migration complete."
|
||||
51
frontend/src/api.ts
Normal file
51
frontend/src/api.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Authenticated fetch wrapper for the admin frontend.
|
||||
* Automatically attaches the JWT from localStorage to every request.
|
||||
*/
|
||||
export function getAuthToken(): string | null {
|
||||
const user = localStorage.getItem('systemUser');
|
||||
if (!user) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(user);
|
||||
return parsed.token || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function authHeaders(extra: Record<string, string> = {}): Record<string, string> {
|
||||
const token = getAuthToken();
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...extra,
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticated fetch. Redirects to /login on 401/403.
|
||||
*/
|
||||
export async function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
const token = getAuthToken();
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string> || {}),
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
// Token expired or invalid — redirect to login
|
||||
localStorage.removeItem('systemUser');
|
||||
sessionStorage.clear();
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { X, Monitor, MapPin, Lock } from 'lucide-react';
|
||||
@@ -19,7 +20,7 @@ const AddTerminalModal: React.FC<AddTerminalModalProps> = ({ isOpen, onClose, on
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/terminals', {
|
||||
const response = await apiFetch('/api/terminals', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, location, pin }),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Bell,
|
||||
@@ -65,7 +66,7 @@ const Header = () => {
|
||||
|
||||
const fetchNotifications = async () => {
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/notifications`);
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/notifications`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setNotifications(data);
|
||||
@@ -115,7 +116,7 @@ const Header = () => {
|
||||
|
||||
const handleNotificationClick = async (notif: any) => {
|
||||
try {
|
||||
await fetch(`http://${window.location.hostname}:8080/api/notifications/mark-read/${notif.id}`, { method: 'POST' });
|
||||
await apiFetch(`http://${window.location.hostname}:8080/api/notifications/mark-read/${notif.id}`, { method: 'POST' });
|
||||
if (notif.link) navigate(notif.link);
|
||||
setShowNotifications(false);
|
||||
fetchNotifications();
|
||||
@@ -126,7 +127,7 @@ const Header = () => {
|
||||
|
||||
const markAllAsRead = async () => {
|
||||
try {
|
||||
await fetch(`http://${window.location.hostname}:8080/api/notifications/mark-all-read`, { method: 'POST' });
|
||||
await apiFetch(`http://${window.location.hostname}:8080/api/notifications/mark-all-read`, { method: 'POST' });
|
||||
fetchNotifications();
|
||||
setShowNotifications(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { X, Lock, Copy, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||
@@ -30,7 +31,7 @@ const PinVerificationModal: React.FC<PinVerificationModalProps> = ({
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`/api/terminals/${terminalId}/verify-pin`, {
|
||||
const response = await apiFetch(`/api/terminals/${terminalId}/verify-pin`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pin: pinToVerify }),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Search,
|
||||
@@ -77,7 +78,7 @@ const ArchivedOrders: React.FC = () => {
|
||||
params.append('page', currentPage.toString());
|
||||
params.append('size', pageSize.toString());
|
||||
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/all?${params.toString()}`);
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/all?${params.toString()}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.content && Array.isArray(data.content)) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Plus, X, Search, Filter, MoreVertical, RefreshCw, Edit2, Power, PowerOff, ShoppingCart, Package, ExternalLink } from 'lucide-react';
|
||||
import Pagination from '../components/Pagination';
|
||||
@@ -78,7 +79,7 @@ const BaseMenu = () => {
|
||||
params.append('search', debouncedSearchTerm);
|
||||
}
|
||||
|
||||
const response = await fetch(`http://${host}:8080/api/base-items?${params.toString()}`);
|
||||
const response = await apiFetch(`http://${host}:8080/api/base-items?${params.toString()}`);
|
||||
const data = await response.json();
|
||||
if (data && data.content) {
|
||||
setItems(data.content);
|
||||
@@ -99,7 +100,7 @@ const BaseMenu = () => {
|
||||
setShowProductsModal(true);
|
||||
setProductsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`http://localhost:8080/api/products/category/${encodeURIComponent(baseItem.name)}`);
|
||||
const response = await apiFetch(`http://localhost:8080/api/products/category/${encodeURIComponent(baseItem.name)}`);
|
||||
const data = await response.json();
|
||||
setAssociatedProducts(data);
|
||||
} catch (error) {
|
||||
@@ -117,7 +118,7 @@ const BaseMenu = () => {
|
||||
const method = editingItem ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await apiFetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newItem),
|
||||
@@ -142,7 +143,7 @@ const BaseMenu = () => {
|
||||
|
||||
const handleToggleActive = async (item: BaseItem) => {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:8080/api/base-items/${item.id}`, {
|
||||
const response = await apiFetch(`http://localhost:8080/api/base-items/${item.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...item, active: !item.active }),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search, ChevronRight, Filter, Loader2, Download, Printer, X } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
@@ -31,7 +32,7 @@ const Bills: React.FC = () => {
|
||||
const fetchOrders = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch('/api/purchases/orders');
|
||||
const response = await apiFetch('/api/purchases/orders');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setOrders(data);
|
||||
@@ -50,7 +51,7 @@ const Bills: React.FC = () => {
|
||||
setIsSaving(true);
|
||||
const updatedPaidTotal = Number(selectedOrder.paidTotal) + Number(paymentAmount);
|
||||
|
||||
const response = await fetch('/api/purchases/orders', {
|
||||
const response = await apiFetch('/api/purchases/orders', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search, User, Phone, Tag, ChevronRight, UserCheck, UserMinus, MoreVertical, LayoutGrid, List, Edit2, Trash2, Shield, X, Eye, EyeOff, Loader2, AlertCircle, CheckCircle, CircleDollarSign } from 'lucide-react';
|
||||
import Pagination from '../components/Pagination';
|
||||
@@ -63,7 +64,7 @@ const Customers: React.FC = () => {
|
||||
params.append('search', debouncedSearchTerm);
|
||||
}
|
||||
|
||||
const response = await fetch(`http://${host}:8080/api/auth/users?${params.toString()}`);
|
||||
const response = await apiFetch(`http://${host}:8080/api/auth/users?${params.toString()}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data && data.content) {
|
||||
@@ -106,7 +107,7 @@ const Customers: React.FC = () => {
|
||||
|
||||
try {
|
||||
const host = window.location.hostname;
|
||||
const response = await fetch(`http://${host}:8080/api/auth/users/${user.id}`, {
|
||||
const response = await apiFetch(`http://${host}:8080/api/auth/users/${user.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (response.ok) {
|
||||
@@ -125,7 +126,7 @@ const Customers: React.FC = () => {
|
||||
const handleSuspendToggle = async (user: UserDto) => {
|
||||
try {
|
||||
const host = window.location.hostname;
|
||||
const response = await fetch(`http://${host}:8080/api/auth/users/${user.id}/suspend`, {
|
||||
const response = await apiFetch(`http://${host}:8080/api/auth/users/${user.id}/suspend`, {
|
||||
method: 'PATCH',
|
||||
});
|
||||
if (response.ok) {
|
||||
@@ -165,7 +166,7 @@ const Customers: React.FC = () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const host = window.location.hostname;
|
||||
const response = await fetch(`http://${host}:8080/api/auth/users/${editingUser.id}`, {
|
||||
const response = await apiFetch(`http://${host}:8080/api/auth/users/${editingUser.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editForm),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
@@ -105,7 +106,7 @@ const Dashboard = () => {
|
||||
}
|
||||
|
||||
console.log('[DASHBOARD-TRACE] Fetching stats from:', url);
|
||||
const response = await fetch(url);
|
||||
const response = await apiFetch(url);
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
setData(result);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Star,
|
||||
@@ -81,7 +82,7 @@ const Feedback: React.FC = () => {
|
||||
const fetchStats = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/feedback/stats`);
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/feedback/stats`);
|
||||
const data = await response.json();
|
||||
setStats(data);
|
||||
} catch (error) {
|
||||
@@ -93,7 +94,7 @@ const Feedback: React.FC = () => {
|
||||
|
||||
const fetchFeedbacks = async () => {
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/feedback?page=${page}&size=${pageSize}`);
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/feedback?page=${page}&size=${pageSize}`);
|
||||
const data = await response.json();
|
||||
setFeedbacks(data?.content || []);
|
||||
setTotalElements(data?.totalElements || 0);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
@@ -68,7 +69,7 @@ const IntentDashboard: React.FC<IntentDashboardProps> = ({ title }) => {
|
||||
const fetchStats = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/purchases/intent/summary`);
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/purchases/intent/summary`);
|
||||
const data = await response.json();
|
||||
setStats(data);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Eye,
|
||||
@@ -49,7 +50,7 @@ const IntentList: React.FC<IntentListProps> = ({ title }) => {
|
||||
|
||||
const fetchVendors = async () => {
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/purchases/vendors`);
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/purchases/vendors`);
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
setVendors(result);
|
||||
@@ -62,7 +63,7 @@ const IntentList: React.FC<IntentListProps> = ({ title }) => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/purchases/orders`);
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/purchases/orders`);
|
||||
const result = await response.json();
|
||||
setData(result);
|
||||
} catch (error) {
|
||||
@@ -104,7 +105,7 @@ const IntentList: React.FC<IntentListProps> = ({ title }) => {
|
||||
date: new Date(newOrder.date).toISOString()
|
||||
};
|
||||
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/purchases/orders`, {
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/purchases/orders`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
|
||||
@@ -52,14 +52,14 @@ const Login = () => {
|
||||
sessionStorage.setItem('isLoggedIn', 'true');
|
||||
sessionStorage.setItem('userRole', user.role.toLowerCase());
|
||||
sessionStorage.setItem('userPermissions', JSON.stringify(user.permissions || []));
|
||||
|
||||
// Persist user profile for personalized greetings and settings
|
||||
localStorage.setItem('systemUser', JSON.stringify(user));
|
||||
|
||||
|
||||
// Persist user profile + JWT token for authenticated API calls
|
||||
localStorage.setItem('systemUser', JSON.stringify(user)); // user object now includes `token`
|
||||
|
||||
navigate('/store-dashboard');
|
||||
} else {
|
||||
const error = await response.text();
|
||||
alert(error || 'Invalid credentials');
|
||||
const error = await response.json().catch(() => ({ error: 'Invalid credentials' }));
|
||||
alert(error.error || 'Invalid credentials');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Login error:', err);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Ticket,
|
||||
@@ -44,7 +45,7 @@ const ManageCoupons = () => {
|
||||
const fetchCoupons = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch('/api/coupons');
|
||||
const response = await apiFetch('/api/coupons');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCoupons(data);
|
||||
@@ -63,7 +64,7 @@ const ManageCoupons = () => {
|
||||
const handleCreateCoupon = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch('/api/coupons', {
|
||||
const response = await apiFetch('/api/coupons', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -85,7 +86,7 @@ const ManageCoupons = () => {
|
||||
|
||||
const handleToggleStatus = async (id: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/coupons/${id}/toggle`, { method: 'PATCH' });
|
||||
const response = await apiFetch(`/api/coupons/${id}/toggle`, { method: 'PATCH' });
|
||||
if (response.ok) fetchCoupons();
|
||||
} catch (error) {
|
||||
console.error('Error toggling status:', error);
|
||||
@@ -95,7 +96,7 @@ const ManageCoupons = () => {
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!window.confirm('Are you sure you want to delete this coupon?')) return;
|
||||
try {
|
||||
const response = await fetch(`/api/coupons/${id}`, { method: 'DELETE' });
|
||||
const response = await apiFetch(`/api/coupons/${id}`, { method: 'DELETE' });
|
||||
if (response.ok) fetchCoupons();
|
||||
} catch (error) {
|
||||
console.error('Error deleting coupon:', error);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Users,
|
||||
@@ -33,7 +34,7 @@ const ManageWallets = () => {
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch('/api/wallet/users');
|
||||
const response = await apiFetch('/api/wallet/users');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUsers(data);
|
||||
@@ -57,7 +58,7 @@ const ManageWallets = () => {
|
||||
setStatus(null);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/wallet/topup', {
|
||||
const response = await apiFetch('/api/wallet/topup', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
@@ -66,7 +67,7 @@ const Managers = () => {
|
||||
const fetchManagers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/system/managers');
|
||||
const res = await apiFetch('/api/system/managers');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// Backend uses 'permissions' field, map it to 'sections' for the component if needed
|
||||
@@ -99,7 +100,7 @@ const Managers = () => {
|
||||
const handleCreateAccount = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch('/api/system/managers', {
|
||||
const response = await apiFetch('/api/system/managers', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -127,7 +128,7 @@ const Managers = () => {
|
||||
const dismissManager = async (id: string) => {
|
||||
if (window.confirm('Are you sure you want to dismiss this manager?')) {
|
||||
try {
|
||||
const response = await fetch(`/api/system/managers/${id}`, {
|
||||
const response = await apiFetch(`/api/system/managers/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (response.ok) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
X, Search, RefreshCw, Edit2, Package, Image as ImageIcon,
|
||||
@@ -75,7 +76,7 @@ const NewArrivals: React.FC = () => {
|
||||
const fetchDrafts = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/products/drafts');
|
||||
const response = await apiFetch('/api/products/drafts');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setDrafts(data);
|
||||
@@ -116,7 +117,7 @@ const NewArrivals: React.FC = () => {
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const response = await fetch(`/api/products/${formData.id}/publish`, {
|
||||
const response = await apiFetch(`/api/products/${formData.id}/publish`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
@@ -137,7 +138,7 @@ const NewArrivals: React.FC = () => {
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!window.confirm('Delete this draft product?')) return;
|
||||
try {
|
||||
await fetch(`/api/products/${id}`, { method: 'DELETE' });
|
||||
await apiFetch(`/api/products/${id}`, { method: 'DELETE' });
|
||||
fetchDrafts();
|
||||
} catch (error) {
|
||||
console.error('Error deleting draft:', error);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Search,
|
||||
@@ -103,7 +104,7 @@ const Orders: React.FC = () => {
|
||||
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/products`);
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/products`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAllProducts(data);
|
||||
@@ -127,7 +128,7 @@ const Orders: React.FC = () => {
|
||||
params.append('page', currentPage.toString());
|
||||
params.append('size', pageSize.toString());
|
||||
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/all?${params.toString()}`);
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/all?${params.toString()}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.content && Array.isArray(data.content)) {
|
||||
@@ -156,7 +157,7 @@ const Orders: React.FC = () => {
|
||||
|
||||
const handleApproveOrder = async (orderId: number) => {
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/${orderId}/status`, {
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/${orderId}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'COMPLETED' })
|
||||
@@ -174,7 +175,7 @@ const Orders: React.FC = () => {
|
||||
|
||||
const handleMarkUndelivered = async (orderId: number) => {
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/${orderId}/status`, {
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/${orderId}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'PAID' })
|
||||
@@ -230,7 +231,7 @@ const Orders: React.FC = () => {
|
||||
if (!selectedOrder) return;
|
||||
setIsUpdatingOrder(true);
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/${selectedOrder.id}`, {
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/${selectedOrder.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -297,7 +298,7 @@ const Orders: React.FC = () => {
|
||||
setIsRegenerating(true);
|
||||
try {
|
||||
const newOrderNumber = `ORD-${Math.random().toString(36).substring(2, 10).toUpperCase()}`;
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/${selectedOrder.id}`, {
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/${selectedOrder.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Plus, X, Search, Filter, MoreVertical, RefreshCw, Edit2, Power, PowerOff, Tag, Package, Image as ImageIcon, Barcode, DollarSign, ChevronDown, Clock, Check, Trash2, Database } from 'lucide-react';
|
||||
import Pagination from '../components/Pagination';
|
||||
@@ -161,7 +162,7 @@ const Products = () => {
|
||||
params.append('search', debouncedSearchTerm);
|
||||
}
|
||||
|
||||
const response = await fetch(`http://${host}:8080/api/products?${params.toString()}`);
|
||||
const response = await apiFetch(`http://${host}:8080/api/products?${params.toString()}`);
|
||||
const data = await response.json();
|
||||
if (data && data.content) {
|
||||
setProducts(data.content);
|
||||
@@ -179,7 +180,7 @@ const Products = () => {
|
||||
|
||||
const fetchBaseItems = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:8080/api/base-items?size=100');
|
||||
const response = await apiFetch('http://localhost:8080/api/base-items?size=100');
|
||||
const data = await response.json();
|
||||
setBaseItems(data.content || data);
|
||||
} catch (error) {
|
||||
@@ -189,7 +190,7 @@ const Products = () => {
|
||||
|
||||
const fetchAllStalls = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:8080/api/stalls');
|
||||
const response = await apiFetch('http://localhost:8080/api/stalls');
|
||||
const data = await response.json();
|
||||
setAllStalls(data);
|
||||
} catch (error) {
|
||||
@@ -205,7 +206,7 @@ const Products = () => {
|
||||
const method = editingProduct ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await apiFetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
@@ -233,7 +234,7 @@ const Products = () => {
|
||||
|
||||
const handleToggleActive = async (product: Product) => {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:8080/api/products/${product.id}`, {
|
||||
const response = await apiFetch(`http://localhost:8080/api/products/${product.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...product, active: !product.active }),
|
||||
@@ -249,7 +250,7 @@ const Products = () => {
|
||||
|
||||
const handleToggleStock = async (product: Product) => {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:8080/api/products/${product.id}/toggle-stock`, {
|
||||
const response = await apiFetch(`http://localhost:8080/api/products/${product.id}/toggle-stock`, {
|
||||
method: 'PATCH',
|
||||
});
|
||||
if (response.ok) {
|
||||
@@ -264,7 +265,7 @@ const Products = () => {
|
||||
const handleDelete = async (product: Product) => {
|
||||
if (!window.confirm(`Are you sure you want to delete ${product.name}?`)) return;
|
||||
try {
|
||||
const response = await fetch(`http://localhost:8080/api/products/${product.id}`, {
|
||||
const response = await apiFetch(`http://localhost:8080/api/products/${product.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (response.ok) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
TrendingUp,
|
||||
@@ -71,7 +72,7 @@ const PurchaseAnalytics = () => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch('/api/purchases/orders');
|
||||
const response = await apiFetch('/api/purchases/orders');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
processAnalytics(data);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search, ShoppingCart, Eye, Plus, X, Loader2, CheckCircle, Trash2, Edit2 } from 'lucide-react';
|
||||
|
||||
@@ -65,7 +66,7 @@ const Purchases: React.FC = () => {
|
||||
const fetchOrders = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch('/api/purchases/orders');
|
||||
const response = await apiFetch('/api/purchases/orders');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setOrders(data);
|
||||
@@ -79,7 +80,7 @@ const Purchases: React.FC = () => {
|
||||
|
||||
const fetchVendors = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/purchases/vendors');
|
||||
const response = await apiFetch('/api/purchases/vendors');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setVendors(data);
|
||||
@@ -91,7 +92,7 @@ const Purchases: React.FC = () => {
|
||||
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/products?size=1000');
|
||||
const response = await apiFetch('/api/products?size=1000');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAvailableProducts(data.content || data);
|
||||
@@ -150,7 +151,7 @@ const Purchases: React.FC = () => {
|
||||
date: new Date(newOrder.date).toISOString()
|
||||
};
|
||||
|
||||
const response = await fetch('/api/purchases/orders', {
|
||||
const response = await apiFetch('/api/purchases/orders', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
@@ -172,7 +173,7 @@ const Purchases: React.FC = () => {
|
||||
const deleteOrder = async (id: number) => {
|
||||
if (!window.confirm('Delete this purchase order?')) return;
|
||||
try {
|
||||
await fetch(`/api/purchases/orders/${id}`, { method: 'DELETE' });
|
||||
await apiFetch(`/api/purchases/orders/${id}`, { method: 'DELETE' });
|
||||
fetchOrders();
|
||||
} catch (error) {
|
||||
console.error('Error deleting order:', error);
|
||||
@@ -197,7 +198,7 @@ const Purchases: React.FC = () => {
|
||||
const order = orders.find(o => o.id === orderId);
|
||||
if (!order) return;
|
||||
|
||||
const response = await fetch(`/api/purchases/orders`, {
|
||||
const response = await apiFetch(`/api/purchases/orders`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...order, status: newStatus })
|
||||
@@ -229,7 +230,7 @@ const Purchases: React.FC = () => {
|
||||
const fetchOrderHistory = async (order: PurchaseOrder) => {
|
||||
try {
|
||||
setActiveHistoryOrder(order);
|
||||
const response = await fetch(`/api/purchases/orders/${order.id}/history`);
|
||||
const response = await apiFetch(`/api/purchases/orders/${order.id}/history`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setOrderHistory(data);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Download,
|
||||
@@ -66,7 +67,7 @@ const Reports: React.FC = () => {
|
||||
try {
|
||||
const fromStr = dateRange.from.toISOString();
|
||||
const toStr = dateRange.to.toISOString();
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/reports/monthly?from=${fromStr}&to=${toStr}`);
|
||||
const response = await apiFetch(`http://${window.location.hostname}:8080/api/reports/monthly?from=${fromStr}&to=${toStr}`);
|
||||
const result = await response.json();
|
||||
setData(result);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Building2,
|
||||
@@ -56,11 +57,11 @@ const RitzPage: React.FC = () => {
|
||||
setIsLoading(true);
|
||||
const host = window.location.hostname;
|
||||
|
||||
const statsRes = await fetch(`http://${host}:8080/api/wallet/stats`);
|
||||
const statsRes = await apiFetch(`http://${host}:8080/api/wallet/stats`);
|
||||
const statsData = await statsRes.json();
|
||||
setStats(statsData);
|
||||
|
||||
const transRes = await fetch(`http://${host}:8080/api/wallet/transactions/all`);
|
||||
const transRes = await apiFetch(`http://${host}:8080/api/wallet/transactions/all`);
|
||||
const transData = await transRes.json();
|
||||
setTransactions(Array.isArray(transData) ? transData : []);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
CircleDollarSign,
|
||||
@@ -52,7 +53,7 @@ const RitzCirculation: React.FC = () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const host = window.location.hostname;
|
||||
const res = await fetch(`http://${host}:8080/api/wallet/circulation?page=${page}&size=${size}`);
|
||||
const res = await apiFetch(`http://${host}:8080/api/wallet/circulation?page=${page}&size=${size}`);
|
||||
const data: PageResponse = await res.json();
|
||||
|
||||
setTokens(data.content || []);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
User,
|
||||
@@ -47,7 +48,7 @@ const Settings = () => {
|
||||
|
||||
const fetchAdmins = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/system/admins');
|
||||
const response = await apiFetch('/api/system/admins');
|
||||
const data = await response.json();
|
||||
setAdmins(data);
|
||||
} catch (err) {
|
||||
@@ -69,7 +70,7 @@ const Settings = () => {
|
||||
|
||||
setStatus('loading');
|
||||
try {
|
||||
const response = await fetch('/api/system/update-master', {
|
||||
const response = await apiFetch('/api/system/update-master', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -102,7 +103,7 @@ const Settings = () => {
|
||||
e.preventDefault();
|
||||
setStatus('loading');
|
||||
try {
|
||||
const response = await fetch('/api/system/admins', {
|
||||
const response = await apiFetch('/api/system/admins', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newAdmin)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
@@ -50,7 +51,7 @@ const Staff = () => {
|
||||
const fetchStaff = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/system/staff');
|
||||
const res = await apiFetch('/api/system/staff');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setStaffList(data);
|
||||
@@ -69,7 +70,7 @@ const Staff = () => {
|
||||
const handleCreateStaff = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch('/api/system/staff', {
|
||||
const response = await apiFetch('/api/system/staff', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -97,7 +98,7 @@ const Staff = () => {
|
||||
const deleteStaff = async (id: string) => {
|
||||
if (window.confirm('Are you sure you want to remove this staff member?')) {
|
||||
try {
|
||||
const response = await fetch(`/api/system/staff/${id}`, {
|
||||
const response = await apiFetch(`/api/system/staff/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (response.ok) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
@@ -94,7 +95,7 @@ const Stalls: React.FC = () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const host = window.location.hostname;
|
||||
const response = await fetch(`http://${host}:8080/api/stalls`);
|
||||
const response = await apiFetch(`http://${host}:8080/api/stalls`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setStalls(data);
|
||||
@@ -126,7 +127,7 @@ const Stalls: React.FC = () => {
|
||||
? `http://${host}:8080/api/stalls/${editingStall.id}`
|
||||
: `http://${host}:8080/api/stalls`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const response = await apiFetch(url, {
|
||||
method: editingStall ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -173,7 +174,7 @@ const Stalls: React.FC = () => {
|
||||
if (!window.confirm('Are you sure you want to delete this stall?')) return;
|
||||
try {
|
||||
const host = window.location.hostname;
|
||||
const response = await fetch(`http://${host}:8080/api/stalls/${id}`, {
|
||||
const response = await apiFetch(`http://${host}:8080/api/stalls/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (response.ok) {
|
||||
@@ -216,7 +217,7 @@ const Stalls: React.FC = () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const host = window.location.hostname;
|
||||
const response = await fetch(`http://${host}:8080/api/stalls/${selectedStall.id}/items`, {
|
||||
const response = await apiFetch(`http://${host}:8080/api/stalls/${selectedStall.id}/items`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
ChevronRight,
|
||||
@@ -99,7 +100,7 @@ const StoreDashboard = () => {
|
||||
params.append('from', range.from);
|
||||
params.append('to', range.to);
|
||||
|
||||
const response = await fetch(`/api/dashboard/stats?${params.toString()}`);
|
||||
const response = await apiFetch(`/api/dashboard/stats?${params.toString()}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Dashboard data received successfully:', data);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
@@ -32,7 +33,7 @@ const Terminals = () => {
|
||||
const fetchTerminals = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/terminals');
|
||||
const response = await apiFetch('/api/terminals');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTerminals(data);
|
||||
@@ -52,7 +53,7 @@ const Terminals = () => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('Are you sure you want to remove this terminal?')) {
|
||||
try {
|
||||
const response = await fetch(`/api/terminals/${id}`, { method: 'DELETE' });
|
||||
const response = await apiFetch(`/api/terminals/${id}`, { method: 'DELETE' });
|
||||
if (response.ok) fetchTerminals();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete terminal:', error);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
@@ -53,7 +54,7 @@ const VendorDashboard = () => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch('/api/dashboard/procurement');
|
||||
const response = await apiFetch('/api/dashboard/procurement');
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
setData(result);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search, Building2, Phone, Mail, MoreVertical, List, LayoutGrid, Edit2, Trash2, X, Loader2, AlertCircle, CheckCircle, Plus } from 'lucide-react';
|
||||
import Pagination from '../components/Pagination';
|
||||
@@ -43,7 +44,7 @@ const Vendors: React.FC = () => {
|
||||
const fetchVendors = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch('/api/purchases/vendors');
|
||||
const response = await apiFetch('/api/purchases/vendors');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setVendors(data);
|
||||
@@ -85,7 +86,7 @@ const Vendors: React.FC = () => {
|
||||
if (!window.confirm(`Are you sure you want to delete ${vendor.name}?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/purchases/vendors/${vendor.id}`, {
|
||||
const response = await apiFetch(`/api/purchases/vendors/${vendor.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (response.ok) {
|
||||
@@ -111,7 +112,7 @@ const Vendors: React.FC = () => {
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const response = await fetch('/api/purchases/vendors', {
|
||||
const response = await apiFetch('/api/purchases/vendors', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editingVendor ? { ...form, id: editingVendor.id } : form),
|
||||
|
||||
Reference in New Issue
Block a user