This commit is contained in:
ashok 2026-03-17 04:50:00 +05:30
parent e5c47e177b
commit 545654685b
13 changed files with 258 additions and 107 deletions

View File

@ -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;

View File

@ -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)

View File

@ -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);
}
} }

View File

@ -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,
]; ];

View File

@ -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).

View File

@ -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>
} }
]; ];

View File

@ -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 => {
const existing = prev.find(item => item.product_id === product.id);
if (existing) { if (existing) {
setCart(cart.map(item => if (existing.quantity >= parseInt(product.current_stock)) return prev;
return prev.map(item =>
item.product_id === product.id item.product_id === product.id
? { ...item, quantity: item.quantity + 1 } ? { ...item, quantity: item.quantity + 1 }
: item : item
)); );
} else { } else {
setCart([...cart, { if (parseInt(product.current_stock) <= 0) return prev;
return [...prev, {
product_id: product.id, product_id: product.id,
name: product.name, name: product.name,
unit_price: product.selling_price, unit_price: product.selling_price,
quantity: 1, quantity: 1,
max_stock: product.current_stock 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>

View File

@ -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 && (

View File

@ -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>

View File

@ -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>

View File

@ -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>
</> </>
); );

View File

@ -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');

View File

@ -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>