diff --git a/app/Http/Controllers/InventoryController.php b/app/Http/Controllers/InventoryController.php index 2061cc9..4c58e17 100644 --- a/app/Http/Controllers/InventoryController.php +++ b/app/Http/Controllers/InventoryController.php @@ -145,7 +145,9 @@ public function getSales(Request $request) $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); $s->is_adjusted = abs($s->total_amount - $originalTotal) > 0.01; $s->original_amount = $originalTotal; diff --git a/app/Http/Controllers/InvestorController.php b/app/Http/Controllers/InvestorController.php index 80dc0f0..0cae838 100644 --- a/app/Http/Controllers/InvestorController.php +++ b/app/Http/Controllers/InvestorController.php @@ -14,7 +14,7 @@ class InvestorController extends Controller public function index() { $user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user(); - $query = Investor::with('branches'); + $query = Investor::with(['branches', 'payouts']); if ($user && $user->isReceptionist()) { $query->where(function($q) use ($user) { $q->where('applicable_to_all_branches', true) diff --git a/app/Models/Investor.php b/app/Models/Investor.php index 5b677a4..581fd42 100644 --- a/app/Models/Investor.php +++ b/app/Models/Investor.php @@ -21,4 +21,9 @@ public function branches() { return $this->belongsToMany(Branch::class, 'investor_branch'); } + + public function payouts() + { + return $this->hasMany(InvestorPayout::class); + } } diff --git a/config/cors.php b/config/cors.php index 4bebf43..022bf8d 100644 --- a/config/cors.php +++ b/config/cors.php @@ -19,9 +19,9 @@ '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' => ['*'], @@ -29,6 +29,6 @@ 'max_age' => 0, - 'supports_credentials' => false, + 'supports_credentials' => true, ]; diff --git a/mobile_api_documentation.txt b/mobile_api_documentation.txt index 4d1bf69..0969b0f 100644 --- a/mobile_api_documentation.txt +++ b/mobile_api_documentation.txt @@ -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 -- **Base URL**: `http://127.0.0.1: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`). +- **Base URL**: `http://[YOUR_SERVER_IP]:8002/api` - **Headers**: - `Accept: application/json` - `Content-Type: application/json` - `X-Requested-With: XMLHttpRequest` +- **Authentication**: The system uses session-based authentication. Ensure cookies are persisted across requests after login. --- -## 1. Authentication Module -Manage user sessions and profiles. +## 1. Authentication & Profile +Endpoints for managing owner access. - **POST /login** - - Params: `email`, `password` - - Response: `{ "user": { ... }, "redirect": "/owner/dashboard" }` -- **POST /receptionist/login** - - Params: `email`, `password` - - Response: `{ "user": { ... }, "redirect": "/receptionist/dashboard" }` + - **Description**: Primary login for Owners. + - **Parameters**: + - `email`: (string) Owner's email address. + - `password`: (string) Owner's password. + - **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** - - Query: `context` (owner/receptionist) - - Returns: Current user details and role. + - **Description**: Returns details of the authenticated owner. + - **Query**: `context=owner` + - **Returns**: User object including name, email, and global permissions. + - **POST /logout** - - Action: Terminates session. + - **Description**: Validates and terminates the current session. --- -## 2. Branch Management -Manage business locations and their documents. +## 2. Branch Oversight +Management of all business locations and their regulatory documents. - **GET /api/branches** - - Query: `status` (Active/Inactive) - - Returns: List of all branches with document counts and revenue. + - **Query (Optional)**: `status` (Active/Inactive) + - **Returns**: Array of branches with document counts, manager details, and operational status. + - **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** - - Type: `multipart/form-data` - - Params: `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]`... + - **Headers**: `Content-Type: multipart/form-data` + - **Parameters**: `name`, `location`, `manager_name`, `operational_start_date`, `payroll_from_day`, `payroll_to_day`, `salary_generation_day` + - **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}** - - Params: Same as POST, plus `status`. -- **DELETE /api/branches/{id}** - - Note: Only deletable if not used in staff/inventory/accounts. + - **Description**: Update branch details or toggle status. + - **Note**: Inactivating a branch automatically inactivates all associated 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 -Employee management and salary settlements. +## 3. Staff & Payroll Management +Comprehensive employee tracking and automated salary settlements. - **GET /api/staff** - - Query: `branch_id` - - Returns: Complete staff list with documents. + - **Query (Optional)**: `branch_id`, `status` + - **Returns**: List of staff with basic details and salary configuration. + - **POST /api/staff** - - Type: `multipart/form-data` - - Params: `full_name`, `email`, `phone`, `role`, `branch_id`, `joining_date`, `status`, `salary_type`, `salary_amount`. - - Optional: `advance_enabled`, `advance_amount`, `commission_enabled`, `documents[]`. + - **Headers**: `Content-Type: multipart/form-data` + - **Required Params**: `full_name`, `role`, `branch_id`, `joining_date`, `salary_type`, `salary_amount`. + - **Salary Settings**: `advance_enabled` (boolean), `advance_amount` (numeric), `commission_enabled` (boolean). + - **Files**: `documents[]` (Passport, Visa, etc.) + - **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** - - 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** - - Params: `month` (Y-m), `remarks` - - Action: Processes salary payment and records expense. + - **Params**: `month` (e.g., "2026-03"), `payment_date`, `remarks`. + - **Action**: Marks the month as Paid and creates an Expense record. + - **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** - - 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 -Manage investments and monthly payouts. +## 4. Investor Network & ROI +Track capital investments and automate ROI returns. - **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** - - 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** - - 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** - - 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 -Product management and sales tracking. +## 5. Inventory Control & Sales +Real-time stock management and sales reporting. - **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** - - 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** - - 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** - - Params: `branch_id`, `payment_method`, `items` (array of product_id/quantity/unit_price). - - Action: Deducts stock and records revenue. + - **Params**: `branch_id`, `payment_method`, `total_amount`, `items` (array of `product_id`, `quantity`, `unit_price`). + - **Validation**: Enforces selected payment method and prevents selling beyond available stock. --- -## 6. Collections & Expenses -Financial tracking. +## 6. Financial Operations (Collections & Expenses) +Tracking of non-inventory income and operational costs. - **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** - - 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** - - 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 -Data analysis and reminders. +## 7. Global Reports & Analytics +Executive dashboards for data-driven decisions. - **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** - - 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** - - 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 -Manage dropdown options. +## 8. Master Data Management +Manage the base configurations for the system. - **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}** - - Params: `name`, `status`. + - **Params**: `name`, `status` (Active/Inactive). diff --git a/resources/js/Pages/Owner/Inventory/Index.jsx b/resources/js/Pages/Owner/Inventory/Index.jsx index a1d70b2..0361044 100644 --- a/resources/js/Pages/Owner/Inventory/Index.jsx +++ b/resources/js/Pages/Owner/Inventory/Index.jsx @@ -228,7 +228,7 @@ export default function InventoryIndex() { }, { header: 'Remarks', - render: (row) => {row.remarks || '-'} + render: (row) => {row.remarks || '-'} } ]; diff --git a/resources/js/Pages/Owner/Inventory/NewSaleModal.jsx b/resources/js/Pages/Owner/Inventory/NewSaleModal.jsx index ca8ac03..7835b14 100644 --- a/resources/js/Pages/Owner/Inventory/NewSaleModal.jsx +++ b/resources/js/Pages/Owner/Inventory/NewSaleModal.jsx @@ -1,4 +1,5 @@ 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'; 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 [paymentMethod, setPaymentMethod] = useState(''); const [paymentMethods, setPaymentMethods] = useState([]); + const [toast, setToast] = useState(null); const [loading, setLoading] = useState(false); const [adjustedTotal, setAdjustedTotal] = useState(''); const [adjustmentRemarks, setAdjustmentRemarks] = useState(''); + const showToast = (message, type = 'success') => { + setToast({ message, type }); + setTimeout(() => setToast(null), 3000); + }; + useEffect(() => { if (isOpen) { fetchPaymentMethods(); @@ -58,28 +65,32 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc ); const addToCart = (product) => { - const existing = cart.find(item => item.product_id === product.id); - if (existing) { - setCart(cart.map(item => - item.product_id === product.id - ? { ...item, quantity: item.quantity + 1 } - : item - )); - } else { - setCart([...cart, { - product_id: product.id, - name: product.name, - unit_price: product.selling_price, - quantity: 1, - max_stock: product.current_stock - }]); - } + setCart(prev => { + const existing = prev.find(item => item.product_id === product.id); + if (existing) { + if (existing.quantity >= parseInt(product.current_stock)) return prev; + return prev.map(item => + item.product_id === product.id + ? { ...item, quantity: item.quantity + 1 } + : item + ); + } else { + if (parseInt(product.current_stock) <= 0) return prev; + return [...prev, { + product_id: product.id, + name: product.name, + unit_price: product.selling_price, + quantity: 1, + max_stock: parseInt(product.current_stock) + }]; + } + }); }; const updateQuantity = (id, delta) => { - setCart(cart.map(item => { + setCart(prev => prev.map(item => { 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; @@ -92,6 +103,18 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc const handleSubmit = async () => { 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); try { const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); @@ -129,6 +152,7 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc return (
+ {toast && setToast(null)} />}
@@ -191,11 +215,11 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc
{parseFloat(product.selling_price).toFixed(2)}
diff --git a/resources/js/Pages/Owner/Inventory/ProductDetailsModal.jsx b/resources/js/Pages/Owner/Inventory/ProductDetailsModal.jsx index e12462c..32fc593 100644 --- a/resources/js/Pages/Owner/Inventory/ProductDetailsModal.jsx +++ b/resources/js/Pages/Owner/Inventory/ProductDetailsModal.jsx @@ -122,7 +122,7 @@ export default function ProductDetailsModal({ isOpen, onClose, product }) { {record.new_stock} - {record.remarks || '-'} + {record.remarks || '-'} ))} {history.length === 0 && ( diff --git a/resources/js/Pages/Owner/Investors/List.jsx b/resources/js/Pages/Owner/Investors/List.jsx index 25efa50..d703671 100644 --- a/resources/js/Pages/Owner/Investors/List.jsx +++ b/resources/js/Pages/Owner/Investors/List.jsx @@ -29,7 +29,7 @@ export default function InvestorList() { const [stats, setStats] = useState({ totalInvestment: 0, totalInvestors: 0, - avgROI: 0 + totalROIPaid: 0 }); useEffect(() => { @@ -44,15 +44,15 @@ export default function InvestorList() { setInvestors(data); const total = data.reduce((acc, inv) => acc + parseFloat(inv.investment_amount || 0), 0); - const percentageInvestors = data.filter(inv => inv.roi_type === 'Percentage'); - const avg = percentageInvestors.length > 0 - ? percentageInvestors.reduce((acc, inv) => acc + parseFloat(inv.roi_value || 0), 0) / percentageInvestors.length - : 0; + const totalROIPaid = data.reduce((acc, inv) => { + const investorPayouts = inv.payouts?.reduce((pacc, p) => pacc + parseFloat(p.amount || 0), 0) || 0; + return acc + investorPayouts; + }, 0); setStats({ totalInvestment: total, totalInvestors: data.length, - avgROI: avg.toFixed(1) + totalROIPaid: totalROIPaid }); } catch (error) { console.error('Error fetching investors:', error); @@ -239,8 +239,8 @@ export default function InvestorList() {
-

Average ROI Yield

-

{stats.avgROI}% Yield

+

Total ROI Paid

+

{(parseFloat(stats.totalROIPaid) || 0).toLocaleString()} AED

diff --git a/resources/js/Pages/Owner/Reports/Index.jsx b/resources/js/Pages/Owner/Reports/Index.jsx index 5052f44..9841e9a 100644 --- a/resources/js/Pages/Owner/Reports/Index.jsx +++ b/resources/js/Pages/Owner/Reports/Index.jsx @@ -1198,7 +1198,7 @@ export default function ReportIndex() {

Remarks / Reason

-

+

"{selectedItem.remarks || 'No remarks provided for this adjustment.'}"

diff --git a/resources/js/Pages/Owner/Staff/View.jsx b/resources/js/Pages/Owner/Staff/View.jsx index 0f80dad..70d3727 100644 --- a/resources/js/Pages/Owner/Staff/View.jsx +++ b/resources/js/Pages/Owner/Staff/View.jsx @@ -49,6 +49,7 @@ export default function StaffView({ id }) { const [advanceHistory, setAdvanceHistory] = useState([]); const [loadingAdvanceHistory, setLoadingAdvanceHistory] = useState(false); const [isAdvanceHistoryModalOpen, setIsAdvanceHistoryModalOpen] = useState(false); + const [isPaymentHistoryModalOpen, setIsPaymentHistoryModalOpen] = useState(false); useEffect(() => { fetchStaff(); @@ -431,7 +432,7 @@ export default function StaffView({ id }) {

Monthly Repayment Schedule

- {advanceHistory.flatMap(h => h.installment_schedule || []).length > 8 && ( + {advanceHistory.flatMap(h => h.installment_schedule || []).length > 3 && (
{advanceHistory.flatMap(h => h.installment_schedule || []) - .slice(0, 8) + .slice(0, 3) .map((item, idx) => (
@@ -529,7 +530,14 @@ export default function StaffView({ id }) {

Transaction History

- + {payments.length > 3 && ( + + )}
@@ -543,7 +551,7 @@ export default function StaffView({ id }) { {payments.length > 0 ? ( - payments.map((p) => ( + payments.slice(0, 3).map((p) => ( @@ -1037,7 +1045,12 @@ export default function StaffView({ id }) {

{item.month}

-

AED {item.amount.toLocaleString()}

+
+

Amount: AED {item.amount.toLocaleString()}

+ {item.status !== 'Paid' && ( +

Pending: AED {item.amount.toLocaleString()}

+ )} +
{item.status} @@ -1062,6 +1075,61 @@ export default function StaffView({ id }) {
)} + + {/* Full Transaction History Modal */} + {isPaymentHistoryModalOpen && ( +
+
+
+
+

Full Transaction History

+

Salary, Advance & Others

+
+ +
+ +
+
+
{p.payment_date} {p.payment_type}
+ + + + + + + + + + + {payments.map((p) => ( + + + + + + + + ))} + +
DateTypeAmountStatusRemarks
{p.payment_date}{p.payment_type}{(parseFloat(p.amount) || 0).toLocaleString()} AED + {p.status} + {p.remarks || '-'}
+
+
+ +
+ +
+
+
+ )} ); diff --git a/resources/js/Pages/Receptionist/POS.jsx b/resources/js/Pages/Receptionist/POS.jsx index 7f182f4..f234cd8 100644 --- a/resources/js/Pages/Receptionist/POS.jsx +++ b/resources/js/Pages/Receptionist/POS.jsx @@ -144,6 +144,11 @@ export default function POS() { const handleProcessPayment = async () => { if (cart.length === 0) return; + if (!paymentMethod) { + showToast("Please select a mode of payment before processing.", 'error'); + return; + } + setProcessing(true); try { const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); diff --git a/resources/js/Pages/Receptionist/Reports/Index.jsx b/resources/js/Pages/Receptionist/Reports/Index.jsx index 1aeb5cf..8106c49 100644 --- a/resources/js/Pages/Receptionist/Reports/Index.jsx +++ b/resources/js/Pages/Receptionist/Reports/Index.jsx @@ -358,7 +358,7 @@ export default function ReceptionistReportIndex() {

Remarks / Reason

-

+

"{selectedItem.remarks || 'No remarks provided for this adjustment.'}"