update 4
This commit is contained in:
parent
e5c47e177b
commit
545654685b
@ -145,7 +145,9 @@ public function getSales(Request $request)
|
|||||||
$query->where('date', '<=', $endDate);
|
$query->where('date', '<=', $endDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
$sales = $query->orderBy('date', 'desc')->get()->map(function($s) {
|
$sales = $query->orderBy('date', 'desc')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get()->map(function($s) {
|
||||||
$originalTotal = ($s->subtotal_amount ?? 0) + ($s->vat_amount ?? 0);
|
$originalTotal = ($s->subtotal_amount ?? 0) + ($s->vat_amount ?? 0);
|
||||||
$s->is_adjusted = abs($s->total_amount - $originalTotal) > 0.01;
|
$s->is_adjusted = abs($s->total_amount - $originalTotal) > 0.01;
|
||||||
$s->original_amount = $originalTotal;
|
$s->original_amount = $originalTotal;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class InvestorController extends Controller
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
||||||
$query = Investor::with('branches');
|
$query = Investor::with(['branches', 'payouts']);
|
||||||
if ($user && $user->isReceptionist()) {
|
if ($user && $user->isReceptionist()) {
|
||||||
$query->where(function($q) use ($user) {
|
$query->where(function($q) use ($user) {
|
||||||
$q->where('applicable_to_all_branches', true)
|
$q->where('applicable_to_all_branches', true)
|
||||||
|
|||||||
@ -21,4 +21,9 @@ public function branches()
|
|||||||
{
|
{
|
||||||
return $this->belongsToMany(Branch::class, 'investor_branch');
|
return $this->belongsToMany(Branch::class, 'investor_branch');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function payouts()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InvestorPayout::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,9 +19,9 @@
|
|||||||
|
|
||||||
'allowed_methods' => ['*'],
|
'allowed_methods' => ['*'],
|
||||||
|
|
||||||
'allowed_origins' => ['*'],
|
'allowed_origins' => ['http://localhost:53328', 'http://127.0.0.1:53328'],
|
||||||
|
|
||||||
'allowed_origins_patterns' => [],
|
'allowed_origins_patterns' => ['/^http:\/\/localhost:\d+$/', '/^http:\/\/127\.0\.0\.1:\d+$/'],
|
||||||
|
|
||||||
'allowed_headers' => ['*'],
|
'allowed_headers' => ['*'],
|
||||||
|
|
||||||
@ -29,6 +29,6 @@
|
|||||||
|
|
||||||
'max_age' => 0,
|
'max_age' => 0,
|
||||||
|
|
||||||
'supports_credentials' => false,
|
'supports_credentials' => true,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,134 +1,181 @@
|
|||||||
# Flutter Mobile App API Documentation
|
# Florida Gym - Owner App API Documentation
|
||||||
|
|
||||||
|
This documentation details the API endpoints available for the **Owner Mobile Application**. The app provides full administrative oversight across all branches, staff, investors, and financial reports.
|
||||||
|
|
||||||
## Connection Details
|
## Connection Details
|
||||||
- **Base URL**: `http://127.0.0.1:8002/api`
|
- **Base URL**: `http://[YOUR_SERVER_IP]:8002/api`
|
||||||
- **Mobile Testing**: For testing on a physical device, replace `127.0.0.1` with your computer's local IP (e.g., `http://192.168.1.5:8002/api`).
|
|
||||||
- **Headers**:
|
- **Headers**:
|
||||||
- `Accept: application/json`
|
- `Accept: application/json`
|
||||||
- `Content-Type: application/json`
|
- `Content-Type: application/json`
|
||||||
- `X-Requested-With: XMLHttpRequest`
|
- `X-Requested-With: XMLHttpRequest`
|
||||||
|
- **Authentication**: The system uses session-based authentication. Ensure cookies are persisted across requests after login.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Authentication Module
|
## 1. Authentication & Profile
|
||||||
Manage user sessions and profiles.
|
Endpoints for managing owner access.
|
||||||
|
|
||||||
- **POST /login**
|
- **POST /login**
|
||||||
- Params: `email`, `password`
|
- **Description**: Primary login for Owners.
|
||||||
- Response: `{ "user": { ... }, "redirect": "/owner/dashboard" }`
|
- **Parameters**:
|
||||||
- **POST /receptionist/login**
|
- `email`: (string) Owner's email address.
|
||||||
- Params: `email`, `password`
|
- `password`: (string) Owner's password.
|
||||||
- Response: `{ "user": { ... }, "redirect": "/receptionist/dashboard" }`
|
- **Response**:
|
||||||
|
- `Success (200)`: `{ "user": { "id": 1, "name": "Owner", "role": "admin" }, "redirect": "/owner/dashboard" }`
|
||||||
|
- `Error (422)`: `{ "message": "The provided credentials do not match our records." }`
|
||||||
|
|
||||||
- **GET /api/profile**
|
- **GET /api/profile**
|
||||||
- Query: `context` (owner/receptionist)
|
- **Description**: Returns details of the authenticated owner.
|
||||||
- Returns: Current user details and role.
|
- **Query**: `context=owner`
|
||||||
|
- **Returns**: User object including name, email, and global permissions.
|
||||||
|
|
||||||
- **POST /logout**
|
- **POST /logout**
|
||||||
- Action: Terminates session.
|
- **Description**: Validates and terminates the current session.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Branch Management
|
## 2. Branch Oversight
|
||||||
Manage business locations and their documents.
|
Management of all business locations and their regulatory documents.
|
||||||
|
|
||||||
- **GET /api/branches**
|
- **GET /api/branches**
|
||||||
- Query: `status` (Active/Inactive)
|
- **Query (Optional)**: `status` (Active/Inactive)
|
||||||
- Returns: List of all branches with document counts and revenue.
|
- **Returns**: Array of branches with document counts, manager details, and operational status.
|
||||||
|
|
||||||
- **GET /api/branches/{id}**
|
- **GET /api/branches/{id}**
|
||||||
- Returns: Detailed branch info and documents.
|
- **Description**: Full profile of a specific branch.
|
||||||
|
- **Returns**: Branch details, metadata, and an array of `documents` (ID cards, licenses, etc.) with expiry dates.
|
||||||
|
|
||||||
- **POST /api/branches**
|
- **POST /api/branches**
|
||||||
- Type: `multipart/form-data`
|
- **Headers**: `Content-Type: multipart/form-data`
|
||||||
- Params: `name`, `location`, `manager_name`, `operational_start_date`, `payroll_from_day`, `payroll_to_day`, `salary_generation_day`
|
- **Parameters**: `name`, `location`, `manager_name`, `operational_start_date`, `payroll_from_day`, `payroll_to_day`, `salary_generation_day`
|
||||||
- Files: `docs[0][file]`, `docs[0][name]`, `docs[0][expiry_date]`...
|
- **Files**: `docs[i][file]`, `docs[i][name]`, `docs[i][expiry_date]`
|
||||||
|
- **Description**: Creates a new branch and uploads its legal documents.
|
||||||
|
|
||||||
- **PUT /api/branches/{id}**
|
- **PUT /api/branches/{id}**
|
||||||
- Params: Same as POST, plus `status`.
|
- **Description**: Update branch details or toggle status.
|
||||||
- **DELETE /api/branches/{id}**
|
- **Note**: Inactivating a branch automatically inactivates all associated staff.
|
||||||
- Note: Only deletable if not used in staff/inventory/accounts.
|
|
||||||
- **GET /api/branches/{id}/active-staff**
|
- **GET /api/branches/{id}/active-staff**
|
||||||
- Returns: List of active staff in that branch.
|
- **Returns**: A list of staff members currently active at the specified branch.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Staff & Payroll Module
|
## 3. Staff & Payroll Management
|
||||||
Employee management and salary settlements.
|
Comprehensive employee tracking and automated salary settlements.
|
||||||
|
|
||||||
- **GET /api/staff**
|
- **GET /api/staff**
|
||||||
- Query: `branch_id`
|
- **Query (Optional)**: `branch_id`, `status`
|
||||||
- Returns: Complete staff list with documents.
|
- **Returns**: List of staff with basic details and salary configuration.
|
||||||
|
|
||||||
- **POST /api/staff**
|
- **POST /api/staff**
|
||||||
- Type: `multipart/form-data`
|
- **Headers**: `Content-Type: multipart/form-data`
|
||||||
- Params: `full_name`, `email`, `phone`, `role`, `branch_id`, `joining_date`, `status`, `salary_type`, `salary_amount`.
|
- **Required Params**: `full_name`, `role`, `branch_id`, `joining_date`, `salary_type`, `salary_amount`.
|
||||||
- Optional: `advance_enabled`, `advance_amount`, `commission_enabled`, `documents[]`.
|
- **Salary Settings**: `advance_enabled` (boolean), `advance_amount` (numeric), `commission_enabled` (boolean).
|
||||||
|
- **Files**: `documents[]` (Passport, Visa, etc.)
|
||||||
|
|
||||||
- **GET /api/staff/{id}/payroll-status**
|
- **GET /api/staff/{id}/payroll-status**
|
||||||
- Returns: Month-by-month payment history and unpaid months.
|
- **Description**: Provides a month-by-month history of salaries paid and highlights "Unpaid" months.
|
||||||
|
|
||||||
- **GET /api/staff/{id}/settlement**
|
- **GET /api/staff/{id}/settlement**
|
||||||
- Returns: Pro-rated salary calculation, commissions, and advance deductions due.
|
- **Description**: Calculates the net payable amount for a specific month.
|
||||||
|
- **Returns**: Base salary, commissions earned, and deductions (Advances/Fine) due.
|
||||||
|
- **Enforcement**: Blocks settlement if a previous month is still pending.
|
||||||
|
|
||||||
- **POST /api/staff/{id}/settle**
|
- **POST /api/staff/{id}/settle**
|
||||||
- Params: `month` (Y-m), `remarks`
|
- **Params**: `month` (e.g., "2026-03"), `payment_date`, `remarks`.
|
||||||
- Action: Processes salary payment and records expense.
|
- **Action**: Marks the month as Paid and creates an Expense record.
|
||||||
|
|
||||||
- **GET /api/staff/pending-salaries**
|
- **GET /api/staff/pending-salaries**
|
||||||
- Returns: List of all staff with pending settlements across branches.
|
- **Description**: Global list of all staff members currently awaiting salary settlement across the organization.
|
||||||
|
|
||||||
- **POST /api/staff/bulk-settle**
|
- **POST /api/staff/bulk-settle**
|
||||||
- Params: `settlements` (array of staff_id/month_key), `remarks`.
|
- **Description**: Settle salaries for multiple employees in one transaction.
|
||||||
|
- **Params**: `settlements` (array of `{staff_id, month_key}`), `payment_date`, `remarks`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Investor & ROI Module
|
## 4. Investor Network & ROI
|
||||||
Manage investments and monthly payouts.
|
Track capital investments and automate ROI returns.
|
||||||
|
|
||||||
- **GET /api/investors**
|
- **GET /api/investors**
|
||||||
- Returns: List of all investors and their linked branches.
|
- **Returns**: List of investors, their total investment, and eager-loaded `payouts` data.
|
||||||
|
- **Key Fields**: `total_invested`, `total_roi_paid_so_far`.
|
||||||
|
|
||||||
- **POST /api/investors**
|
- **POST /api/investors**
|
||||||
- Params: `name`, `investment_date`, `investment_amount`, `roi_type` (Percentage/Fixed Amount), `roi_value`, `roi_period` (Monthly/Quarterly/Yearly).
|
- **Params**: `name`, `investment_date`, `investment_amount`, `roi_type` (Percentage/Fixed), `roi_value`, `roi_period` (Monthly/Quarterly/Yearly).
|
||||||
|
|
||||||
- **GET /api/investors/{id}/roi-status**
|
- **GET /api/investors/{id}/roi-status**
|
||||||
- Returns: Breakdown of ROI due, paid, and carry-over for each period.
|
- **Description**: Detailed ledger of ROI cycles, showing Pending vs Paid for each month of the investment life.
|
||||||
|
|
||||||
- **POST /api/investors/{id}/settle-roi**
|
- **POST /api/investors/{id}/settle-roi**
|
||||||
- Params: `payout_month`, `amount`, `payout_date`, `payment_method`, `remarks`.
|
- **Params**: `payout_month`, `amount`, `payment_method`, `remarks`.
|
||||||
|
- **Action**: Processes the ROI return and updates the investor's balance.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Inventory & POS Module
|
## 5. Inventory Control & Sales
|
||||||
Product management and sales tracking.
|
Real-time stock management and sales reporting.
|
||||||
|
|
||||||
- **GET /api/inventory/products**
|
- **GET /api/inventory/products**
|
||||||
- Query: `branch_id`, `status` (In Stock/Low Stock/Out of Stock).
|
- **Query**: `branch_id`, `status` (In Stock/Low Stock/Out of Stock).
|
||||||
|
- **Returns**: Product list with SKU and current quantities.
|
||||||
|
|
||||||
- **POST /api/inventory/products**
|
- **POST /api/inventory/products**
|
||||||
- Params: `name`, `sku`, `product_category_id`, `branch_id`, `cost_price`, `selling_price`, `current_stock`, `reorder_level`.
|
- **Params**: `name`, `sku`, `product_category_id`, `branch_id`, `cost_price`, `selling_price`, `current_stock`, `reorder_level`.
|
||||||
|
|
||||||
- **POST /api/inventory/products/{id}/adjust**
|
- **POST /api/inventory/products/{id}/adjust**
|
||||||
- Params: `adjustment_qty` (+/-), `reason`, `adjustment_date`.
|
- **Params**: `adjustment_qty` (e.g., -5 for breakage, +10 for restock), `reason`, `remarks`.
|
||||||
|
|
||||||
|
- **GET /api/inventory/sales**
|
||||||
|
- **Description**: History of all product sales.
|
||||||
|
- **Return Order**: Latest first (date desc, created_at desc).
|
||||||
|
- **Fields**: `transaction_id`, `total_amount`, `payment_method`, `items`.
|
||||||
|
|
||||||
- **POST /api/inventory/sales**
|
- **POST /api/inventory/sales**
|
||||||
- Params: `branch_id`, `payment_method`, `items` (array of product_id/quantity/unit_price).
|
- **Params**: `branch_id`, `payment_method`, `total_amount`, `items` (array of `product_id`, `quantity`, `unit_price`).
|
||||||
- Action: Deducts stock and records revenue.
|
- **Validation**: Enforces selected payment method and prevents selling beyond available stock.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Collections & Expenses
|
## 6. Financial Operations (Collections & Expenses)
|
||||||
Financial tracking.
|
Tracking of non-inventory income and operational costs.
|
||||||
|
|
||||||
- **GET /api/collections**
|
- **GET /api/collections**
|
||||||
- Query: `start_date`, `end_date`, `branch_id`.
|
- **Query**: `start_date`, `end_date`, `branch_id`.
|
||||||
|
- **Returns**: Daily collections (Membership fees, lockers, etc.).
|
||||||
|
|
||||||
- **POST /api/collections**
|
- **POST /api/collections**
|
||||||
- Params: `date`, `branch_id`, `collection_type_id`, `amount`, `payment_method`, `items[]`.
|
- **Params**: `date`, `branch_id`, `collection_type_id`, `amount`, `payment_method`, `remarks`.
|
||||||
|
|
||||||
|
- **GET /api/expenses**
|
||||||
|
- **Description**: Full list of expenses across all branches.
|
||||||
- **POST /api/expenses**
|
- **POST /api/expenses**
|
||||||
- Params: `date`, `branch_id`, `expense_category_id`, `expense_type` (Account/Petty Cash), `amount`, `remarks`.
|
- **Params**: `date`, `branch_id`, `expense_category_id`, `expense_type` (Account/Petty Cash), `amount`, `remarks`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Reports
|
## 7. Global Reports & Analytics
|
||||||
Data analysis and reminders.
|
Executive dashboards for data-driven decisions.
|
||||||
|
|
||||||
- **GET /api/reports/profit**
|
- **GET /api/reports/profit**
|
||||||
- Returns: Total income, total expense, net profit, and 6-month trend.
|
- **Query**: `branch_id`, `start_date`, `end_date`.
|
||||||
|
- **Returns**: `total_income`, `total_expense`, `net_profit`, and a list of all raw transactions.
|
||||||
|
|
||||||
- **GET /api/reports/expiry-reminders**
|
- **GET /api/reports/expiry-reminders**
|
||||||
- Returns: Document expiry alerts for both Staff and Branches.
|
- **Returns**: Alerts for documents expiring in the next 30/60/90 days for both Staff and Branches.
|
||||||
|
|
||||||
- **GET /api/reports/investments**
|
- **GET /api/reports/investments**
|
||||||
- Returns: Summary of total investments and total ROI returned.
|
- **Returns**: Aggregated summary of organization-wide capital and ROI performance.
|
||||||
|
|
||||||
|
- **GET /api/inventory/movements**
|
||||||
|
- **Description**: Global audit log of every stock change (Sales, Adjustments, Initial load).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Master Settings
|
## 8. Master Data Management
|
||||||
Manage dropdown options.
|
Manage the base configurations for the system.
|
||||||
|
|
||||||
- **GET /api/masters/{type}**
|
- **GET /api/masters/{type}**
|
||||||
- Types: `collection`, `expense`, `product`, `payment_method`, `staff_role`.
|
- **Types**: `collection` (types), `expense` (categories), `product` (categories), `payment_method`, `staff_role`.
|
||||||
- **POST /api/masters/{type}**
|
- **POST /api/masters/{type}**
|
||||||
- Params: `name`, `status`.
|
- **Params**: `name`, `status` (Active/Inactive).
|
||||||
|
|||||||
@ -228,7 +228,7 @@ export default function InventoryIndex() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Remarks',
|
header: 'Remarks',
|
||||||
render: (row) => <span className="text-xs text-gray-400 font-bold truncate max-w-[150px]">{row.remarks || '-'}</span>
|
render: (row) => <span className="text-xs text-gray-400 font-bold break-words whitespace-normal min-w-[150px]">{row.remarks || '-'}</span>
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Toast from '../Components/Toast';
|
||||||
import { X, Search, ShoppingCart, Plus, Minus, CreditCard, DollarSign, Globe, Trash2, Banknote } from 'lucide-react';
|
import { X, Search, ShoppingCart, Plus, Minus, CreditCard, DollarSign, Globe, Trash2, Banknote } from 'lucide-react';
|
||||||
|
|
||||||
export default function NewSaleModal({ isOpen, onClose, onSave, branches, products }) {
|
export default function NewSaleModal({ isOpen, onClose, onSave, branches, products }) {
|
||||||
@ -7,10 +8,16 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc
|
|||||||
const [cart, setCart] = useState([]);
|
const [cart, setCart] = useState([]);
|
||||||
const [paymentMethod, setPaymentMethod] = useState('');
|
const [paymentMethod, setPaymentMethod] = useState('');
|
||||||
const [paymentMethods, setPaymentMethods] = useState([]);
|
const [paymentMethods, setPaymentMethods] = useState([]);
|
||||||
|
const [toast, setToast] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [adjustedTotal, setAdjustedTotal] = useState('');
|
const [adjustedTotal, setAdjustedTotal] = useState('');
|
||||||
const [adjustmentRemarks, setAdjustmentRemarks] = useState('');
|
const [adjustmentRemarks, setAdjustmentRemarks] = useState('');
|
||||||
|
|
||||||
|
const showToast = (message, type = 'success') => {
|
||||||
|
setToast({ message, type });
|
||||||
|
setTimeout(() => setToast(null), 3000);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
fetchPaymentMethods();
|
fetchPaymentMethods();
|
||||||
@ -58,28 +65,32 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc
|
|||||||
);
|
);
|
||||||
|
|
||||||
const addToCart = (product) => {
|
const addToCart = (product) => {
|
||||||
const existing = cart.find(item => item.product_id === product.id);
|
setCart(prev => {
|
||||||
if (existing) {
|
const existing = prev.find(item => item.product_id === product.id);
|
||||||
setCart(cart.map(item =>
|
if (existing) {
|
||||||
item.product_id === product.id
|
if (existing.quantity >= parseInt(product.current_stock)) return prev;
|
||||||
? { ...item, quantity: item.quantity + 1 }
|
return prev.map(item =>
|
||||||
: item
|
item.product_id === product.id
|
||||||
));
|
? { ...item, quantity: item.quantity + 1 }
|
||||||
} else {
|
: item
|
||||||
setCart([...cart, {
|
);
|
||||||
product_id: product.id,
|
} else {
|
||||||
name: product.name,
|
if (parseInt(product.current_stock) <= 0) return prev;
|
||||||
unit_price: product.selling_price,
|
return [...prev, {
|
||||||
quantity: 1,
|
product_id: product.id,
|
||||||
max_stock: product.current_stock
|
name: product.name,
|
||||||
}]);
|
unit_price: product.selling_price,
|
||||||
}
|
quantity: 1,
|
||||||
|
max_stock: parseInt(product.current_stock)
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateQuantity = (id, delta) => {
|
const updateQuantity = (id, delta) => {
|
||||||
setCart(cart.map(item => {
|
setCart(prev => prev.map(item => {
|
||||||
if (item.product_id === id) {
|
if (item.product_id === id) {
|
||||||
const newQty = Math.max(1, Math.min(item.max_stock, item.quantity + delta));
|
const newQty = Math.max(1, Math.min(parseInt(item.max_stock), item.quantity + delta));
|
||||||
return { ...item, quantity: newQty };
|
return { ...item, quantity: newQty };
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
@ -92,6 +103,18 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (cart.length === 0) return;
|
if (cart.length === 0) return;
|
||||||
|
if (!paymentMethod) {
|
||||||
|
showToast("Please select a mode of payment before processing.", 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final safety check against stock bypass
|
||||||
|
const overStockItems = cart.filter(item => parseInt(item.quantity) > parseInt(item.max_stock));
|
||||||
|
if (overStockItems.length > 0) {
|
||||||
|
alert(`Some items exceed available stock: ${overStockItems.map(i => i.name).join(', ')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||||
@ -129,6 +152,7 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-[#00171F]/40 backdrop-blur-sm z-[999] flex items-center justify-center p-4">
|
<div className="fixed inset-0 bg-[#00171F]/40 backdrop-blur-sm z-[999] flex items-center justify-center p-4">
|
||||||
|
{toast && <Toast message={toast.message} type={toast.type} onClose={() => setToast(null)} />}
|
||||||
<div className="bg-white rounded-[32px] w-full max-w-6xl max-h-[95vh] overflow-hidden shadow-2xl flex flex-col animate-in zoom-in-95 duration-500">
|
<div className="bg-white rounded-[32px] w-full max-w-6xl max-h-[95vh] overflow-hidden shadow-2xl flex flex-col animate-in zoom-in-95 duration-500">
|
||||||
<div className="p-6 border-b border-gray-100 flex items-center justify-between bg-[#FBFCFD]">
|
<div className="p-6 border-b border-gray-100 flex items-center justify-between bg-[#FBFCFD]">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@ -191,11 +215,11 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc
|
|||||||
<div className="flex items-center justify-between mt-auto pt-3">
|
<div className="flex items-center justify-between mt-auto pt-3">
|
||||||
<span className="text-sm font-black text-[#FF4D4D]">{parseFloat(product.selling_price).toFixed(2)}</span>
|
<span className="text-sm font-black text-[#FF4D4D]">{parseFloat(product.selling_price).toFixed(2)}</span>
|
||||||
<button
|
<button
|
||||||
disabled={product.current_stock <= 0}
|
disabled={product.current_stock <= 0 || cart.find(i => i.product_id === product.id)?.quantity >= product.current_stock}
|
||||||
onClick={() => addToCart(product)}
|
onClick={() => addToCart(product)}
|
||||||
className="px-3 py-1.5 bg-gray-50 text-gray-900 border border-transparent rounded-lg font-black text-[9px] uppercase tracking-widest hover:bg-[#FF4D4D] hover:text-white transition-all disabled:opacity-30 disabled:hover:bg-gray-50 disabled:hover:text-gray-900"
|
className="px-3 py-1.5 bg-gray-50 text-gray-900 border border-transparent rounded-lg font-black text-[9px] uppercase tracking-widest hover:bg-[#FF4D4D] hover:text-white transition-all disabled:opacity-30 disabled:hover:bg-gray-50 disabled:hover:text-gray-900"
|
||||||
>
|
>
|
||||||
Add
|
{cart.find(i => i.product_id === product.id)?.quantity >= product.current_stock ? 'Limit' : 'Add'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -122,7 +122,7 @@ export default function ProductDetailsModal({ isOpen, onClose, product }) {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-6 text-sm font-black text-gray-900">{record.new_stock}</td>
|
<td className="px-6 py-6 text-sm font-black text-gray-900">{record.new_stock}</td>
|
||||||
<td className="px-6 py-6 text-xs font-bold text-gray-400 max-w-[200px] truncate">{record.remarks || '-'}</td>
|
<td className="px-6 py-6 text-xs font-bold text-gray-400 min-w-[200px] break-words whitespace-normal">{record.remarks || '-'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{history.length === 0 && (
|
{history.length === 0 && (
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export default function InvestorList() {
|
|||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
totalInvestment: 0,
|
totalInvestment: 0,
|
||||||
totalInvestors: 0,
|
totalInvestors: 0,
|
||||||
avgROI: 0
|
totalROIPaid: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -44,15 +44,15 @@ export default function InvestorList() {
|
|||||||
setInvestors(data);
|
setInvestors(data);
|
||||||
|
|
||||||
const total = data.reduce((acc, inv) => acc + parseFloat(inv.investment_amount || 0), 0);
|
const total = data.reduce((acc, inv) => acc + parseFloat(inv.investment_amount || 0), 0);
|
||||||
const percentageInvestors = data.filter(inv => inv.roi_type === 'Percentage');
|
const totalROIPaid = data.reduce((acc, inv) => {
|
||||||
const avg = percentageInvestors.length > 0
|
const investorPayouts = inv.payouts?.reduce((pacc, p) => pacc + parseFloat(p.amount || 0), 0) || 0;
|
||||||
? percentageInvestors.reduce((acc, inv) => acc + parseFloat(inv.roi_value || 0), 0) / percentageInvestors.length
|
return acc + investorPayouts;
|
||||||
: 0;
|
}, 0);
|
||||||
|
|
||||||
setStats({
|
setStats({
|
||||||
totalInvestment: total,
|
totalInvestment: total,
|
||||||
totalInvestors: data.length,
|
totalInvestors: data.length,
|
||||||
avgROI: avg.toFixed(1)
|
totalROIPaid: totalROIPaid
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching investors:', error);
|
console.error('Error fetching investors:', error);
|
||||||
@ -239,8 +239,8 @@ export default function InvestorList() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mb-1">Average ROI Yield</p>
|
<p className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mb-1">Total ROI Paid</p>
|
||||||
<h3 className="text-3xl font-black text-gray-900 tracking-tight">{stats.avgROI}% <span className="text-xs text-gray-400">Yield</span></h3>
|
<h3 className="text-3xl font-black text-gray-900 tracking-tight">{(parseFloat(stats.totalROIPaid) || 0).toLocaleString()} <span className="text-xs text-gray-400">AED</span></h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1198,7 +1198,7 @@ export default function ReportIndex() {
|
|||||||
|
|
||||||
<div className="space-y-2 p-6 bg-amber-50/50 rounded-2xl border border-amber-100">
|
<div className="space-y-2 p-6 bg-amber-50/50 rounded-2xl border border-amber-100">
|
||||||
<p className="text-[10px] font-black text-amber-600 uppercase tracking-widest block mb-2">Remarks / Reason</p>
|
<p className="text-[10px] font-black text-amber-600 uppercase tracking-widest block mb-2">Remarks / Reason</p>
|
||||||
<p className="text-sm font-bold text-[#1B254B] leading-relaxed italic">
|
<p className="text-sm font-bold text-[#1B254B] leading-relaxed italic break-all whitespace-normal">
|
||||||
"{selectedItem.remarks || 'No remarks provided for this adjustment.'}"
|
"{selectedItem.remarks || 'No remarks provided for this adjustment.'}"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -49,6 +49,7 @@ export default function StaffView({ id }) {
|
|||||||
const [advanceHistory, setAdvanceHistory] = useState([]);
|
const [advanceHistory, setAdvanceHistory] = useState([]);
|
||||||
const [loadingAdvanceHistory, setLoadingAdvanceHistory] = useState(false);
|
const [loadingAdvanceHistory, setLoadingAdvanceHistory] = useState(false);
|
||||||
const [isAdvanceHistoryModalOpen, setIsAdvanceHistoryModalOpen] = useState(false);
|
const [isAdvanceHistoryModalOpen, setIsAdvanceHistoryModalOpen] = useState(false);
|
||||||
|
const [isPaymentHistoryModalOpen, setIsPaymentHistoryModalOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStaff();
|
fetchStaff();
|
||||||
@ -431,7 +432,7 @@ export default function StaffView({ id }) {
|
|||||||
<div className="mt-6 pt-6 border-t border-orange-50/50 space-y-3">
|
<div className="mt-6 pt-6 border-t border-orange-50/50 space-y-3">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<p className="text-[9px] font-black text-orange-400 uppercase tracking-widest">Monthly Repayment Schedule</p>
|
<p className="text-[9px] font-black text-orange-400 uppercase tracking-widest">Monthly Repayment Schedule</p>
|
||||||
{advanceHistory.flatMap(h => h.installment_schedule || []).length > 8 && (
|
{advanceHistory.flatMap(h => h.installment_schedule || []).length > 3 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsAdvanceHistoryModalOpen(true)}
|
onClick={() => setIsAdvanceHistoryModalOpen(true)}
|
||||||
className="text-[8px] font-black text-orange-600 hover:underline uppercase tracking-widest"
|
className="text-[8px] font-black text-orange-600 hover:underline uppercase tracking-widest"
|
||||||
@ -442,7 +443,7 @@ export default function StaffView({ id }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{advanceHistory.flatMap(h => h.installment_schedule || [])
|
{advanceHistory.flatMap(h => h.installment_schedule || [])
|
||||||
.slice(0, 8)
|
.slice(0, 3)
|
||||||
.map((item, idx) => (
|
.map((item, idx) => (
|
||||||
<div key={idx} className={`p-2 rounded-xl border flex items-center justify-between transition-all ${item.status === 'Paid' ? 'bg-emerald-50/30 border-emerald-100 opacity-60' : 'bg-white border-orange-100 shadow-sm'}`}>
|
<div key={idx} className={`p-2 rounded-xl border flex items-center justify-between transition-all ${item.status === 'Paid' ? 'bg-emerald-50/30 border-emerald-100 opacity-60' : 'bg-white border-orange-100 shadow-sm'}`}>
|
||||||
<div>
|
<div>
|
||||||
@ -529,7 +530,14 @@ export default function StaffView({ id }) {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-sm font-black text-gray-900 uppercase tracking-wider">Transaction History</h3>
|
<h3 className="text-sm font-black text-gray-900 uppercase tracking-wider">Transaction History</h3>
|
||||||
<button className="text-[10px] font-bold text-blue-600 hover:underline uppercase tracking-wider">View Full History</button>
|
{payments.length > 3 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsPaymentHistoryModalOpen(true)}
|
||||||
|
className="text-[10px] font-bold text-blue-600 hover:underline uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
View Full History
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="border border-gray-50 rounded-2xl overflow-hidden bg-white shadow-sm opacity-80">
|
<div className="border border-gray-50 rounded-2xl overflow-hidden bg-white shadow-sm opacity-80">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
@ -543,7 +551,7 @@ export default function StaffView({ id }) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-50">
|
<tbody className="divide-y divide-gray-50">
|
||||||
{payments.length > 0 ? (
|
{payments.length > 0 ? (
|
||||||
payments.map((p) => (
|
payments.slice(0, 3).map((p) => (
|
||||||
<tr key={p.id}>
|
<tr key={p.id}>
|
||||||
<td className="px-6 py-4 text-xs font-bold text-gray-900">{p.payment_date}</td>
|
<td className="px-6 py-4 text-xs font-bold text-gray-900">{p.payment_date}</td>
|
||||||
<td className="px-6 py-4 text-xs font-semibold text-gray-500">{p.payment_type}</td>
|
<td className="px-6 py-4 text-xs font-semibold text-gray-500">{p.payment_type}</td>
|
||||||
@ -1037,7 +1045,12 @@ export default function StaffView({ id }) {
|
|||||||
<div key={iIdx} className={`p-4 rounded-2xl border flex items-center justify-between transition-all ${item.status === 'Paid' ? 'bg-emerald-50/30 border-emerald-50 opacity-60' : 'bg-gray-50/50 border-gray-100 shadow-sm'}`}>
|
<div key={iIdx} className={`p-4 rounded-2xl border flex items-center justify-between transition-all ${item.status === 'Paid' ? 'bg-emerald-50/30 border-emerald-50 opacity-60' : 'bg-gray-50/50 border-gray-100 shadow-sm'}`}>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-black text-gray-900">{item.month}</p>
|
<p className="text-sm font-black text-gray-900">{item.month}</p>
|
||||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">AED {item.amount.toLocaleString()}</p>
|
<div className="flex items-center gap-3">
|
||||||
|
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Amount: AED {item.amount.toLocaleString()}</p>
|
||||||
|
{item.status !== 'Paid' && (
|
||||||
|
<p className="text-[10px] text-orange-500 font-black uppercase tracking-wider">Pending: AED {item.amount.toLocaleString()}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${item.status === 'Paid' ? 'bg-emerald-100 text-emerald-600' : 'bg-white text-orange-600 border border-orange-100'}`}>
|
<div className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${item.status === 'Paid' ? 'bg-emerald-100 text-emerald-600' : 'bg-white text-orange-600 border border-orange-100'}`}>
|
||||||
{item.status}
|
{item.status}
|
||||||
@ -1062,6 +1075,61 @@ export default function StaffView({ id }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Full Transaction History Modal */}
|
||||||
|
{isPaymentHistoryModalOpen && (
|
||||||
|
<div className="fixed inset-0 z-[80] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-300">
|
||||||
|
<div className="bg-white rounded-[2.5rem] w-full max-w-3xl max-h-[85vh] overflow-hidden flex flex-col shadow-2xl animate-in zoom-in-95 duration-200">
|
||||||
|
<div className="p-8 flex items-center justify-between border-b border-gray-100 bg-[#F9FAFB]/50">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-black text-gray-900 uppercase tracking-tight">Full Transaction History</h3>
|
||||||
|
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-1">Salary, Advance & Others</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setIsPaymentHistoryModalOpen(false)} className="w-10 h-10 flex items-center justify-center rounded-full hover:bg-gray-100 text-gray-400 transition-all">
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-auto p-8">
|
||||||
|
<div className="border border-gray-100 rounded-2xl overflow-hidden bg-white shadow-sm">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-gray-50/50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-4 text-left text-[10px] font-black text-gray-400 uppercase tracking-widest">Date</th>
|
||||||
|
<th className="px-6 py-4 text-left text-[10px] font-black text-gray-400 uppercase tracking-widest">Type</th>
|
||||||
|
<th className="px-6 py-4 text-left text-[10px] font-black text-gray-400 uppercase tracking-widest">Amount</th>
|
||||||
|
<th className="px-6 py-4 text-left text-[10px] font-black text-gray-400 uppercase tracking-widest">Status</th>
|
||||||
|
<th className="px-6 py-4 text-left text-[10px] font-black text-gray-400 uppercase tracking-widest">Remarks</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-50">
|
||||||
|
{payments.map((p) => (
|
||||||
|
<tr key={p.id} className="hover:bg-gray-50/30 transition-colors">
|
||||||
|
<td className="px-6 py-4 text-xs font-bold text-gray-900">{p.payment_date}</td>
|
||||||
|
<td className="px-6 py-4 text-xs font-semibold text-gray-500">{p.payment_type}</td>
|
||||||
|
<td className="px-6 py-4 text-xs font-black text-emerald-600">{(parseFloat(p.amount) || 0).toLocaleString()} AED</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-600 rounded-full text-[10px] font-black uppercase tracking-wider">{p.status}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-[10px] font-medium text-gray-400 italic max-w-[150px] truncate">{p.remarks || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-8 border-t border-gray-100 bg-gray-50/30 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsPaymentHistoryModalOpen(false)}
|
||||||
|
className="px-8 py-3 bg-gray-900 text-white rounded-xl text-sm font-bold hover:bg-black transition-all shadow-lg shadow-gray-200"
|
||||||
|
>
|
||||||
|
Close History
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -144,6 +144,11 @@ export default function POS() {
|
|||||||
const handleProcessPayment = async () => {
|
const handleProcessPayment = async () => {
|
||||||
if (cart.length === 0) return;
|
if (cart.length === 0) return;
|
||||||
|
|
||||||
|
if (!paymentMethod) {
|
||||||
|
showToast("Please select a mode of payment before processing.", 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
try {
|
try {
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||||
|
|||||||
@ -358,7 +358,7 @@ export default function ReceptionistReportIndex() {
|
|||||||
|
|
||||||
<div className="space-y-2 p-6 bg-amber-50/50 rounded-2xl border border-amber-100">
|
<div className="space-y-2 p-6 bg-amber-50/50 rounded-2xl border border-amber-100">
|
||||||
<p className="text-[10px] font-black text-amber-600 uppercase tracking-widest block mb-2">Remarks / Reason</p>
|
<p className="text-[10px] font-black text-amber-600 uppercase tracking-widest block mb-2">Remarks / Reason</p>
|
||||||
<p className="text-sm font-bold text-[#1B254B] leading-relaxed italic">
|
<p className="text-sm font-bold text-[#1B254B] leading-relaxed italic break-all whitespace-normal">
|
||||||
"{selectedItem.remarks || 'No remarks provided for this adjustment.'}"
|
"{selectedItem.remarks || 'No remarks provided for this adjustment.'}"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user