Counter dashboard global search added
This commit is contained in:
@@ -284,13 +284,28 @@ public class OrderController {
|
|||||||
|
|
||||||
existingOrder.setTotalAmount(newAmount);
|
existingOrder.setTotalAmount(newAmount);
|
||||||
existingOrder.setPaymentMethod(updatedOrder.getPaymentMethod());
|
existingOrder.setPaymentMethod(updatedOrder.getPaymentMethod());
|
||||||
existingOrder.getItems().clear();
|
if (updatedOrder.getStatus() != null) {
|
||||||
|
existingOrder.setStatus(updatedOrder.getStatus().toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safely copy items into new instances (without IDs) to trigger correct orphan removal
|
||||||
|
List<OrderItem> newItems = new ArrayList<>();
|
||||||
if (updatedOrder.getItems() != null) {
|
if (updatedOrder.getItems() != null) {
|
||||||
for (OrderItem newItem : updatedOrder.getItems()) {
|
for (OrderItem item : updatedOrder.getItems()) {
|
||||||
|
OrderItem newItem = new OrderItem();
|
||||||
|
newItem.setProductId(item.getProductId());
|
||||||
|
newItem.setProductName(item.getProductName());
|
||||||
|
newItem.setPrice(item.getPrice());
|
||||||
|
newItem.setQuantity(item.getQuantity());
|
||||||
|
newItem.setStallId(item.getStallId());
|
||||||
|
newItem.setStallName(item.getStallName());
|
||||||
newItem.setOrder(existingOrder);
|
newItem.setOrder(existingOrder);
|
||||||
existingOrder.getItems().add(newItem);
|
newItems.add(newItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
existingOrder.getItems().clear();
|
||||||
|
existingOrder.getItems().addAll(newItems);
|
||||||
|
|
||||||
Order saved = orderRepository.save(existingOrder);
|
Order saved = orderRepository.save(existingOrder);
|
||||||
return ResponseEntity.ok(saved);
|
return ResponseEntity.ok(saved);
|
||||||
}).orElse(ResponseEntity.notFound().build());
|
}).orElse(ResponseEntity.notFound().build());
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ public class PurchaseController {
|
|||||||
private SystemNotificationService notificationService;
|
private SystemNotificationService notificationService;
|
||||||
|
|
||||||
@GetMapping("/orders")
|
@GetMapping("/orders")
|
||||||
public List<PurchaseOrder> getAllOrders() {
|
public List<PurchaseOrder> getAllOrders(
|
||||||
|
@RequestParam(required = false) @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME) java.time.LocalDateTime from,
|
||||||
|
@RequestParam(required = false) @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME) java.time.LocalDateTime to) {
|
||||||
|
if (from != null && to != null) {
|
||||||
|
return purchaseService.getAllOrdersInRange(from, to);
|
||||||
|
}
|
||||||
return purchaseService.getAllOrders();
|
return purchaseService.getAllOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +88,12 @@ public class PurchaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/summary")
|
@GetMapping("/summary")
|
||||||
public Map<String, Object> getSummary() {
|
public Map<String, Object> getSummary(
|
||||||
|
@RequestParam(required = false) @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME) java.time.LocalDateTime from,
|
||||||
|
@RequestParam(required = false) @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME) java.time.LocalDateTime to) {
|
||||||
|
if (from != null && to != null) {
|
||||||
|
return purchaseService.getPurchaseSummaryInRange(from, to);
|
||||||
|
}
|
||||||
return purchaseService.getPurchaseSummary();
|
return purchaseService.getPurchaseSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ public interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Lo
|
|||||||
@org.springframework.data.jpa.repository.Query("SELECT SUM(p.amount) FROM PurchaseOrder p WHERE p.date >= :start AND p.date <= :end")
|
@org.springframework.data.jpa.repository.Query("SELECT SUM(p.amount) FROM PurchaseOrder p WHERE p.date >= :start AND p.date <= :end")
|
||||||
java.math.BigDecimal getTotalPurchaseAmountInRange(@org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start, @org.springframework.data.repository.query.Param("end") java.time.LocalDateTime end);
|
java.math.BigDecimal getTotalPurchaseAmountInRange(@org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start, @org.springframework.data.repository.query.Param("end") java.time.LocalDateTime end);
|
||||||
|
|
||||||
|
@org.springframework.data.jpa.repository.Query("SELECT SUM(COALESCE(p.amount, 0) - COALESCE(p.paidTotal, 0)) FROM PurchaseOrder p WHERE p.date >= :start AND p.date <= :end")
|
||||||
|
java.math.BigDecimal getTotalBalanceAmountInRange(@org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start, @org.springframework.data.repository.query.Param("end") java.time.LocalDateTime end);
|
||||||
|
|
||||||
|
@org.springframework.data.jpa.repository.Query("SELECT COUNT(p) FROM PurchaseOrder p WHERE p.status != 'PAID' AND p.date >= :start AND p.date <= :end")
|
||||||
|
long countUnpaidBillsInRange(@org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start, @org.springframework.data.repository.query.Param("end") java.time.LocalDateTime end);
|
||||||
|
|
||||||
@org.springframework.data.jpa.repository.Query("SELECT p.vendor.name, SUM(p.amount), COUNT(p) FROM PurchaseOrder p WHERE p.date >= :start AND p.date <= :end GROUP BY p.vendor.name")
|
@org.springframework.data.jpa.repository.Query("SELECT p.vendor.name, SUM(p.amount), COUNT(p) FROM PurchaseOrder p WHERE p.date >= :start AND p.date <= :end GROUP BY p.vendor.name")
|
||||||
List<Object[]> getVendorSummary(@org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start, @org.springframework.data.repository.query.Param("end") java.time.LocalDateTime end);
|
List<Object[]> getVendorSummary(@org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start, @org.springframework.data.repository.query.Param("end") java.time.LocalDateTime end);
|
||||||
|
|
||||||
@@ -32,6 +38,9 @@ public interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Lo
|
|||||||
@org.springframework.data.jpa.repository.Query("SELECT p.date, SUM(p.amount) FROM PurchaseOrder p GROUP BY p.date ORDER BY p.date ASC")
|
@org.springframework.data.jpa.repository.Query("SELECT p.date, SUM(p.amount) FROM PurchaseOrder p GROUP BY p.date ORDER BY p.date ASC")
|
||||||
List<Object[]> getPurchaseTrend();
|
List<Object[]> getPurchaseTrend();
|
||||||
|
|
||||||
|
@org.springframework.data.jpa.repository.Query("SELECT p FROM PurchaseOrder p WHERE p.date >= :start AND p.date <= :end ORDER BY p.date DESC")
|
||||||
|
List<PurchaseOrder> findByDateInRange(@org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start, @org.springframework.data.repository.query.Param("end") java.time.LocalDateTime end);
|
||||||
|
|
||||||
long countByStatus(String status);
|
long countByStatus(String status);
|
||||||
|
|
||||||
@org.springframework.data.jpa.repository.Query("SELECT SUM(i.quantity) FROM PurchaseOrder p JOIN p.items i WHERE p.status = 'OPEN'")
|
@org.springframework.data.jpa.repository.Query("SELECT SUM(i.quantity) FROM PurchaseOrder p JOIN p.items i WHERE p.status = 'OPEN'")
|
||||||
|
|||||||
@@ -66,8 +66,9 @@ public class DevicePairingService {
|
|||||||
public Map<String, Object> registerOtp(String otp, String deviceId) {
|
public Map<String, Object> registerOtp(String otp, String deviceId) {
|
||||||
PairingRequest existing = pendingPairings.get(otp);
|
PairingRequest existing = pendingPairings.get(otp);
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
// If this OTP was already linked by admin, return the apiKey
|
// If this OTP was already linked by admin, return the apiKey
|
||||||
if (existing != null && existing.completed && existing.deviceId.equals(deviceId)) {
|
if (existing.completed) {
|
||||||
// Clean up after delivering the key
|
// Clean up after delivering the key
|
||||||
pendingPairings.remove(otp);
|
pendingPairings.remove(otp);
|
||||||
return Map.of(
|
return Map.of(
|
||||||
@@ -77,7 +78,15 @@ public class DevicePairingService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register or refresh the OTP
|
// If the existing request has a placeholder device ID but this call has a real one, update it
|
||||||
|
if (existing.deviceId.equals("ESP32-Device") && !deviceId.equals("ESP32-Device")) {
|
||||||
|
pendingPairings.put(otp, new PairingRequest(otp, deviceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Map.of("status", "WAITING");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register new OTP
|
||||||
pendingPairings.put(otp, new PairingRequest(otp, deviceId));
|
pendingPairings.put(otp, new PairingRequest(otp, deviceId));
|
||||||
|
|
||||||
return Map.of("status", "WAITING");
|
return Map.of("status", "WAITING");
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ public class PurchaseService {
|
|||||||
return purchaseOrderRepository.findAll();
|
return purchaseOrderRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<PurchaseOrder> getAllOrdersInRange(java.time.LocalDateTime start, java.time.LocalDateTime end) {
|
||||||
|
return purchaseOrderRepository.findByDateInRange(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
public List<Vendor> getAllVendors() {
|
public List<Vendor> getAllVendors() {
|
||||||
return vendorRepository.findAll();
|
return vendorRepository.findAll();
|
||||||
}
|
}
|
||||||
@@ -132,6 +136,30 @@ public class PurchaseService {
|
|||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getPurchaseSummaryInRange(java.time.LocalDateTime start, java.time.LocalDateTime end) {
|
||||||
|
Map<String, Object> summary = new HashMap<>();
|
||||||
|
BigDecimal total = purchaseOrderRepository.getTotalPurchaseAmountInRange(start, end);
|
||||||
|
BigDecimal balance = purchaseOrderRepository.getTotalBalanceAmountInRange(start, end);
|
||||||
|
long unpaidCount = purchaseOrderRepository.countUnpaidBillsInRange(start, end);
|
||||||
|
|
||||||
|
summary.put("totalAmount", total != null ? total : BigDecimal.ZERO);
|
||||||
|
summary.put("balanceAmount", balance != null ? balance : BigDecimal.ZERO);
|
||||||
|
summary.put("paidAmount", (total != null ? total : BigDecimal.ZERO).subtract(balance != null ? balance : BigDecimal.ZERO));
|
||||||
|
summary.put("unpaidCount", unpaidCount);
|
||||||
|
|
||||||
|
List<Object[]> trendData = purchaseOrderRepository.getPurchaseTrendInRange(start, end);
|
||||||
|
List<Map<String, Object>> trend = new ArrayList<>();
|
||||||
|
for (Object[] row : trendData) {
|
||||||
|
Map<String, Object> point = new HashMap<>();
|
||||||
|
point.put("date", row[0].toString());
|
||||||
|
point.put("amount", row[1]);
|
||||||
|
trend.add(point);
|
||||||
|
}
|
||||||
|
summary.put("trend", trend);
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, Object> getIntentSummary() {
|
public Map<String, Object> getIntentSummary() {
|
||||||
Map<String, Object> summary = new HashMap<>();
|
Map<String, Object> summary = new HashMap<>();
|
||||||
summary.put("openCount", purchaseOrderRepository.countByStatus("OPEN"));
|
summary.put("openCount", purchaseOrderRepository.countByStatus("OPEN"));
|
||||||
|
|||||||
@@ -80,9 +80,7 @@ function App() {
|
|||||||
<Route path="purchases/intent/receives-dashboard" element={<IntentDashboard title="RECEIVABLE DASHBOARD" />} />
|
<Route path="purchases/intent/receives-dashboard" element={<IntentDashboard title="RECEIVABLE DASHBOARD" />} />
|
||||||
<Route path="purchases/intent/orders" element={<IntentList title="ORDERS" />} />
|
<Route path="purchases/intent/orders" element={<IntentList title="ORDERS" />} />
|
||||||
<Route path="purchases/intent/receives" element={<IntentList title="RECEIVES" />} />
|
<Route path="purchases/intent/receives" element={<IntentList title="RECEIVES" />} />
|
||||||
<Route path="purchases/intent/receives-summary" element={<PlaceholderPage title="Receives Summary" />} />
|
|
||||||
<Route path="purchases/intent/request" element={<PlaceholderPage title="Intent Request" />} />
|
|
||||||
<Route path="purchases/intent/stores" element={<PlaceholderPage title="Intent Stores" />} />
|
|
||||||
|
|
||||||
{/* Inventory */}
|
{/* Inventory */}
|
||||||
<Route path="inventory/new-arrivals" element={<NewArrivals />} />
|
<Route path="inventory/new-arrivals" element={<NewArrivals />} />
|
||||||
|
|||||||
@@ -80,10 +80,7 @@ const menuItems: MenuItem[] = [
|
|||||||
{ title: 'Orders Dashboard', path: '/purchases/intent/orders-dashboard' },
|
{ title: 'Orders Dashboard', path: '/purchases/intent/orders-dashboard' },
|
||||||
{ title: 'Receives Dashboard', path: '/purchases/intent/receives-dashboard' },
|
{ title: 'Receives Dashboard', path: '/purchases/intent/receives-dashboard' },
|
||||||
{ title: 'Orders', path: '/purchases/intent/orders' },
|
{ title: 'Orders', path: '/purchases/intent/orders' },
|
||||||
{ title: 'Receives', path: '/purchases/intent/receives' },
|
{ title: 'Receives', path: '/purchases/intent/receives' }
|
||||||
{ title: 'Receives Summary', path: '/purchases/intent/receives-summary' },
|
|
||||||
{ title: 'Request', path: '/purchases/intent/request' },
|
|
||||||
{ title: 'Stores', path: '/purchases/intent/stores' }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -19,18 +19,86 @@ import { motion } from 'framer-motion';
|
|||||||
|
|
||||||
const PurchaseSummary = () => {
|
const PurchaseSummary = () => {
|
||||||
const [activeRange, setActiveRange] = useState('Today');
|
const [activeRange, setActiveRange] = useState('Today');
|
||||||
|
const [customDates, setCustomDates] = useState({ from: '', to: '' });
|
||||||
const [data, setData] = useState<any>(null);
|
const [data, setData] = useState<any>(null);
|
||||||
const [bills, setBills] = useState<any[]>([]);
|
const [bills, setBills] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const toLocalISOString = (date: Date) => {
|
||||||
|
const tzo = -date.getTimezoneOffset(),
|
||||||
|
pad = (num: number) => {
|
||||||
|
const norm = Math.floor(Math.abs(num));
|
||||||
|
return (norm < 10 ? '0' : '') + norm;
|
||||||
|
};
|
||||||
|
return date.getFullYear() +
|
||||||
|
'-' + pad(date.getMonth() + 1) +
|
||||||
|
'-' + pad(date.getDate()) +
|
||||||
|
'T' + pad(date.getHours()) +
|
||||||
|
':' + pad(date.getMinutes()) +
|
||||||
|
':' + pad(date.getSeconds()) +
|
||||||
|
'.' + pad(date.getMilliseconds());
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRangeDates = (range: string) => {
|
||||||
|
const now = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
const end = new Date();
|
||||||
|
|
||||||
|
// Set end to end of today
|
||||||
|
end.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
switch (range) {
|
||||||
|
case 'Today':
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
break;
|
||||||
|
case 'Yesterday':
|
||||||
|
start.setDate(now.getDate() - 1);
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
end.setDate(now.getDate() - 1);
|
||||||
|
end.setHours(23, 59, 59, 999);
|
||||||
|
break;
|
||||||
|
case 'Week':
|
||||||
|
start.setDate(now.getDate() - 7);
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
break;
|
||||||
|
case '30 Days':
|
||||||
|
start.setDate(now.getDate() - 30);
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
break;
|
||||||
|
case 'Custom':
|
||||||
|
if (customDates.from && customDates.to) {
|
||||||
|
const from = new Date(customDates.from);
|
||||||
|
from.setHours(0, 0, 0, 0);
|
||||||
|
const to = new Date(customDates.to);
|
||||||
|
to.setHours(23, 59, 59, 999);
|
||||||
|
return { from: toLocalISOString(from), to: toLocalISOString(to) };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
return { from: toLocalISOString(start), to: toLocalISOString(end) };
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// Fetch summary and bills in parallel
|
const range = getRangeDates(activeRange);
|
||||||
|
if (!range) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('from', range.from);
|
||||||
|
params.append('to', range.to);
|
||||||
|
const queryStr = `?${params.toString()}`;
|
||||||
|
|
||||||
|
// Fetch summary and bills in parallel with range params
|
||||||
const [summaryRes, billsRes] = await Promise.all([
|
const [summaryRes, billsRes] = await Promise.all([
|
||||||
apiFetch('/api/purchases/summary'),
|
apiFetch(`/api/purchases/summary${queryStr}`),
|
||||||
apiFetch('/api/purchases/orders')
|
apiFetch(`/api/purchases/orders${queryStr}`)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (summaryRes.ok && billsRes.ok) {
|
if (summaryRes.ok && billsRes.ok) {
|
||||||
@@ -46,7 +114,7 @@ const PurchaseSummary = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, [activeRange, customDates]);
|
||||||
|
|
||||||
if (isLoading || !data) {
|
if (isLoading || !data) {
|
||||||
return (
|
return (
|
||||||
@@ -84,8 +152,9 @@ const PurchaseSummary = () => {
|
|||||||
<h1 className="text-2xl font-black text-slate-800">Summary</h1>
|
<h1 className="text-2xl font-black text-slate-800">Summary</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-end gap-2">
|
||||||
<div className="flex items-center gap-2 bg-white p-1 rounded-2xl border border-slate-200 shadow-sm">
|
<div className="flex items-center gap-2 bg-white p-1 rounded-2xl border border-slate-200 shadow-sm">
|
||||||
{['Yesterday', 'Today', 'Week', '30 Days'].map(range => (
|
{['Yesterday', 'Today', 'Week', '30 Days', 'Custom'].map(range => (
|
||||||
<button
|
<button
|
||||||
key={range}
|
key={range}
|
||||||
onClick={() => setActiveRange(range)}
|
onClick={() => setActiveRange(range)}
|
||||||
@@ -95,6 +164,24 @@ const PurchaseSummary = () => {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{activeRange === 'Custom' && (
|
||||||
|
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-top-1 duration-300">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={customDates.from}
|
||||||
|
onChange={(e) => setCustomDates({ ...customDates, from: e.target.value })}
|
||||||
|
className="bg-white border border-slate-200 rounded-lg px-2 py-1.5 text-[10px] font-bold text-slate-600 outline-none focus:border-[#003317]"
|
||||||
|
/>
|
||||||
|
<span className="text-slate-300 text-[10px] font-bold">to</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={customDates.to}
|
||||||
|
onChange={(e) => setCustomDates({ ...customDates, to: e.target.value })}
|
||||||
|
className="bg-white border border-slate-200 rounded-lg px-2 py-1.5 text-[10px] font-bold text-slate-600 outline-none focus:border-[#003317]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-12 gap-8">
|
<div className="grid grid-cols-12 gap-8">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { apiFetch } from '../api';
|
import { apiFetch } from '../api';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -27,6 +27,7 @@ import { motion } from 'framer-motion';
|
|||||||
const StoreDashboard = () => {
|
const StoreDashboard = () => {
|
||||||
const [activeTab, setActiveTab] = useState('Sales');
|
const [activeTab, setActiveTab] = useState('Sales');
|
||||||
const [timeRange, setTimeRange] = useState('Today');
|
const [timeRange, setTimeRange] = useState('Today');
|
||||||
|
const [customDates, setCustomDates] = useState({ from: '', to: '' });
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
totalSales: 0,
|
totalSales: 0,
|
||||||
activeOrders: 0,
|
activeOrders: 0,
|
||||||
@@ -85,6 +86,15 @@ const StoreDashboard = () => {
|
|||||||
start.setDate(now.getDate() - 30);
|
start.setDate(now.getDate() - 30);
|
||||||
start.setHours(0, 0, 0, 0);
|
start.setHours(0, 0, 0, 0);
|
||||||
break;
|
break;
|
||||||
|
case 'Custom':
|
||||||
|
if (customDates.from && customDates.to) {
|
||||||
|
const from = new Date(customDates.from);
|
||||||
|
from.setHours(0, 0, 0, 0);
|
||||||
|
const to = new Date(customDates.to);
|
||||||
|
to.setHours(23, 59, 59, 999);
|
||||||
|
return { from: toLocalISOString(from), to: toLocalISOString(to) };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
default:
|
default:
|
||||||
start.setHours(0, 0, 0, 0);
|
start.setHours(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
@@ -96,6 +106,10 @@ const StoreDashboard = () => {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const range = getRangeDates(timeRange);
|
const range = getRangeDates(timeRange);
|
||||||
|
if (!range) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append('from', range.from);
|
params.append('from', range.from);
|
||||||
params.append('to', range.to);
|
params.append('to', range.to);
|
||||||
@@ -138,7 +152,7 @@ const StoreDashboard = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchStats();
|
fetchStats();
|
||||||
}, [timeRange]);
|
}, [timeRange, customDates]);
|
||||||
|
|
||||||
const pieData = [
|
const pieData = [
|
||||||
{ name: 'Full Payment', value: Number(stats.totalSales) || 0, color: '#8b5cf6' },
|
{ name: 'Full Payment', value: Number(stats.totalSales) || 0, color: '#8b5cf6' },
|
||||||
@@ -264,6 +278,7 @@ const StoreDashboard = () => {
|
|||||||
{/* Right Stats Column */}
|
{/* Right Stats Column */}
|
||||||
<div className="col-span-12 lg:col-span-3 space-y-6">
|
<div className="col-span-12 lg:col-span-3 space-y-6">
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center justify-between overflow-x-auto gap-2 pb-2 scrollbar-none">
|
<div className="flex items-center justify-between overflow-x-auto gap-2 pb-2 scrollbar-none">
|
||||||
{['Yesterday', 'Today', 'Week', '30 Days', 'Custom'].map(range => (
|
{['Yesterday', 'Today', 'Week', '30 Days', 'Custom'].map(range => (
|
||||||
<button
|
<button
|
||||||
@@ -279,6 +294,24 @@ const StoreDashboard = () => {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{timeRange === 'Custom' && (
|
||||||
|
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-top-1 duration-300">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={customDates.from}
|
||||||
|
onChange={(e) => setCustomDates({ ...customDates, from: e.target.value })}
|
||||||
|
className="flex-1 bg-white border border-slate-200 rounded-lg px-2 py-1.5 text-[10px] font-bold text-slate-600 outline-none focus:border-[#003317]"
|
||||||
|
/>
|
||||||
|
<span className="text-slate-300 text-[10px] font-bold">to</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={customDates.to}
|
||||||
|
onChange={(e) => setCustomDates({ ...customDates, to: e.target.value })}
|
||||||
|
className="flex-1 bg-white border border-slate-200 rounded-lg px-2 py-1.5 text-[10px] font-bold text-slate-600 outline-none focus:border-[#003317]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Total Orders Card */}
|
{/* Total Orders Card */}
|
||||||
<div title="View detailed store volume and throughput" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm relative h-[180px] flex flex-col justify-between group cursor-pointer hover:border-[#003317]/30 transition-all">
|
<div title="View detailed store volume and throughput" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm relative h-[180px] flex flex-col justify-between group cursor-pointer hover:border-[#003317]/30 transition-all">
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
const [showStockAlert, setShowStockAlert] = useState(false);
|
const [showStockAlert, setShowStockAlert] = useState(false);
|
||||||
const [unavailableItems, setUnavailableItems] = useState<string[]>([]);
|
const [unavailableItems, setUnavailableItems] = useState<string[]>([]);
|
||||||
const [pendingItems, setPendingItems] = useState<FoodItem[]>([]);
|
const [pendingItems, setPendingItems] = useState<FoodItem[]>([]);
|
||||||
const [isRegenerating, setIsRegenerating] = useState(false);
|
|
||||||
|
|
||||||
const isOrderExpired = (order: Order) => {
|
const isOrderExpired = (order: Order) => {
|
||||||
if (order.isArchived) return true;
|
if (order.isArchived) return true;
|
||||||
@@ -133,44 +132,6 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
navigate('/cart');
|
navigate('/cart');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRegenerateQR = async () => {
|
|
||||||
if (!selectedOrder) return;
|
|
||||||
setIsRegenerating(true);
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
const newOrderNumber = `ORD-${Math.random().toString(36).substring(2, 10).toUpperCase()}`;
|
|
||||||
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/${selectedOrder.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
...selectedOrder,
|
|
||||||
orderNumber: newOrderNumber
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
await fetchOrders();
|
|
||||||
// Since selectedOrder is used in the modal, we need to update it as well
|
|
||||||
const updated = orders.find(o => o.id === selectedOrder.id);
|
|
||||||
if (updated) setSelectedOrder({ ...updated, orderNumber: newOrderNumber });
|
|
||||||
else fetchOrders().then(() => {
|
|
||||||
// Fallback refresh to catch the update
|
|
||||||
setOrders(prev => prev.map(o => o.id === selectedOrder.id ? { ...o, orderNumber: newOrderNumber } : o));
|
|
||||||
setSelectedOrder(prev => prev ? { ...prev, orderNumber: newOrderNumber } : null);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
alert('Failed to regenerate sync code');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error regenerating QR:', error);
|
|
||||||
} finally {
|
|
||||||
setIsRegenerating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProceedPartial = () => {
|
const handleProceedPartial = () => {
|
||||||
performRepeat(pendingItems);
|
performRepeat(pendingItems);
|
||||||
setShowStockAlert(false);
|
setShowStockAlert(false);
|
||||||
@@ -330,16 +291,7 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
? 'This order has been fulfilled'
|
? 'This order has been fulfilled'
|
||||||
: 'Show this QR code at the counter'}
|
: 'Show this QR code at the counter'}
|
||||||
</p>
|
</p>
|
||||||
<button
|
|
||||||
className={`regenerate-sync-btn ${isRegenerating ? 'loading' : ''}`}
|
|
||||||
onClick={handleRegenerateQR}
|
|
||||||
disabled={isRegenerating || isOrderExpired(selectedOrder) || selectedOrder.status.toUpperCase() === 'COMPLETED'}
|
|
||||||
>
|
|
||||||
<RefreshCcw size={14} className={isRegenerating ? 'animate-spin' : ''} />
|
|
||||||
{isRegenerating ? 'Generating...' : 'Regenerate Sync ID'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="modal-info-list">
|
<div className="modal-info-list">
|
||||||
<div className="info-item">
|
<div className="info-item">
|
||||||
<span className="label">Date</span>
|
<span className="label">Date</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user