This commit is contained in:
ashok 2026-03-16 17:31:32 +05:30
parent 50ba5c23e8
commit e5c47e177b
11 changed files with 451 additions and 290 deletions

View File

@ -111,19 +111,9 @@ public function update(Request $request, $id)
]); ]);
if ($validated['status'] === 'Inactive') { if ($validated['status'] === 'Inactive') {
$staffAction = $request->input('staff_action'); \App\Models\Staff::where('branch_id', $id)
if ($staffAction === 'move') { ->where('status', 'Active')
$targetBranchId = $request->input('move_to_branch_id'); ->update(['status' => 'Inactive']);
if ($targetBranchId) {
\App\Models\Staff::where('branch_id', $id)
->where('status', 'Active')
->update(['branch_id' => $targetBranchId]);
}
} elseif ($staffAction === 'inactivate') {
\App\Models\Staff::where('branch_id', $id)
->where('status', 'Active')
->update(['status' => 'Inactive']);
}
} }
// Process new documents if provided // Process new documents if provided

View File

@ -26,7 +26,7 @@ public function getProfitReport(Request $request)
$perPage = $request->query('per_page', 10); $perPage = $request->query('per_page', 10);
// Base Query from Account table (Ledger) // Base Query from Account table (Ledger)
$query = Account::query()->with('accountable'); $query = Account::query()->with(['accountable', 'branch']);
if ($branchId) { if ($branchId) {
$query->where('branch_id', $branchId); $query->where('branch_id', $branchId);
@ -41,6 +41,19 @@ public function getProfitReport(Request $request)
// Stats from the same filtered query // Stats from the same filtered query
$totalCredits = (clone $query)->sum('credit'); $totalCredits = (clone $query)->sum('credit');
$totalDebits = (clone $query)->sum('debit'); $totalDebits = (clone $query)->sum('debit');
// Today's Stats (always based on today's date, but respecting branch filter)
$todayIncome = Account::where('date', Carbon::today()->toDateString())
->when($branchId, fn($q) => $q->where('branch_id', $branchId))
->sum('credit');
// Monthly Stats (always based on current month, but respecting branch filter)
$monthlyExpense = Account::whereBetween('date', [
Carbon::now()->startOfMonth()->toDateString(),
Carbon::now()->endOfMonth()->toDateString()
])
->when($branchId, fn($q) => $q->where('branch_id', $branchId))
->sum('debit');
// Fetch Paginated Transactions from the same filtered query // Fetch Paginated Transactions from the same filtered query
$allTransactions = $query->where(function($q) { $allTransactions = $query->where(function($q) {
@ -73,7 +86,7 @@ public function getProfitReport(Request $request)
'category' => $a->type, 'category' => $a->type,
'description' => $a->description, 'description' => $a->description,
'amount' => $a->credit > 0 ? $a->credit : $a->debit, 'amount' => $a->credit > 0 ? $a->credit : $a->debit,
'branch' => 'N/A', // Branch name if needed can be added via relation 'branch' => $a->branch->name ?? 'N/A',
'is_adjusted' => $isAdjusted, 'is_adjusted' => $isAdjusted,
'original_amount' => $originalAmount, 'original_amount' => $originalAmount,
'remarks' => $remarks 'remarks' => $remarks
@ -95,13 +108,12 @@ public function getProfitReport(Request $request)
$monthStart = Carbon::now()->subMonths($i)->startOfMonth(); $monthStart = Carbon::now()->subMonths($i)->startOfMonth();
$monthEnd = Carbon::now()->subMonths($i)->endOfMonth(); $monthEnd = Carbon::now()->subMonths($i)->endOfMonth();
$monthIncome = Account::where('branch_id', $branchId ?: '!=', 0) $monthIncome = Account::query()
->when($branchId, fn($q) => $q->where('branch_id', $branchId)) ->when($branchId, fn($q) => $q->where('branch_id', $branchId))
->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()])
->sum('credit'); ->sum('credit');
// For trend, we can also use Account table for expenses $monthExpense = Account::query()
$monthExpense = Account::where('branch_id', $branchId ?: '!=', 0)
->when($branchId, fn($q) => $q->where('branch_id', $branchId)) ->when($branchId, fn($q) => $q->where('branch_id', $branchId))
->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()]) ->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()])
->sum('debit'); ->sum('debit');
@ -118,6 +130,8 @@ public function getProfitReport(Request $request)
return response()->json([ return response()->json([
'total_income' => $totalCredits, 'total_income' => $totalCredits,
'total_expense' => $totalDebits, 'total_expense' => $totalDebits,
'today_income' => $todayIncome,
'monthly_expense' => $monthlyExpense,
'net_profit' => $totalCredits - $totalDebits, 'net_profit' => $totalCredits - $totalDebits,
'low_stock_count' => $lowStockCount, 'low_stock_count' => $lowStockCount,
'transactions' => $paginatedTransactions, 'transactions' => $paginatedTransactions,

View File

@ -380,13 +380,36 @@ public function getSettlementDetails(Request $request, $id)
if ($user && $user->isReceptionist() && $staff->branch_id != $user->branch_id) { if ($user && $user->isReceptionist() && $staff->branch_id != $user->branch_id) {
return response()->json(['message' => 'Unauthorized'], 403); return response()->json(['message' => 'Unauthorized'], 403);
} }
$targetMonthKey = $request->query('month'); $targetMonthKey = $request->input('month') ?? $request->query('month') ?? $request->input('settlement_month');
$branch = $staff->branch; $branch = $staff->branch;
// Find the next unpaid month logically for chronological check
$lastPayment = \App\Models\StaffPayment::where('staff_id', $id)
->where('payment_type', 'Salary Settlement')
->orderBy('settlement_month', 'desc')
->first();
$joiningDate = Carbon::parse($staff->joining_date);
if ($lastPayment) {
$logicalNextMonth = Carbon::parse($lastPayment->settlement_month . '-01')->addMonth();
} else {
$logicalNextMonth = $joiningDate->copy()->startOfMonth();
}
if ($targetMonthKey) { if ($targetMonthKey) {
$nextSettlementMonth = Carbon::parse($targetMonthKey . '-01'); $requestedMonth = Carbon::parse($targetMonthKey . '-01');
// Verify if already paid // 1. Chronological Check: Cannot settle a future month if a previous one is unpaid
if ($requestedMonth->startOfMonth()->greaterThan($logicalNextMonth->startOfMonth())) {
return response()->json([
'can_settle' => false,
'message' => "Please settle " . $logicalNextMonth->format('F Y') . " before settling " . $requestedMonth->format('F Y') . ".",
]);
}
$nextSettlementMonth = $requestedMonth;
// 2. Verify if already paid
$existing = \App\Models\StaffPayment::where('staff_id', $id) $existing = \App\Models\StaffPayment::where('staff_id', $id)
->where('payment_type', 'Salary Settlement') ->where('payment_type', 'Salary Settlement')
->where('settlement_month', $targetMonthKey) ->where('settlement_month', $targetMonthKey)
@ -399,31 +422,15 @@ public function getSettlementDetails(Request $request, $id)
]); ]);
} }
} else { } else {
// Find the next unpaid month logically $nextSettlementMonth = $logicalNextMonth;
$lastPayment = \App\Models\StaffPayment::where('staff_id', $id) }
->where('payment_type', 'Salary Settlement')
->orderBy('settlement_month', 'desc')
->first();
$joiningDate = Carbon::parse($staff->joining_date); // 3. Status Check: Only allow settlement if the month has completed
$currentMonth = Carbon::now()->startOfMonth(); if (Carbon::now()->lessThan($nextSettlementMonth->copy()->addMonth()->startOfMonth())) {
return response()->json([
if ($lastPayment) { 'can_settle' => false,
$nextSettlementMonth = Carbon::parse($lastPayment->settlement_month . '-01')->addMonth(); 'message' => "Salary for {$nextSettlementMonth->format('F Y')} is still in progress.",
} else { ]);
$nextSettlementMonth = $joiningDate->copy()->startOfMonth();
}
// Only allow settlement if the generation date for the month has arrived
$genDay = $branch->salary_generation_day ?? 2;
$generationDate = $nextSettlementMonth->copy()->addMonth()->day(min($genDay, $nextSettlementMonth->copy()->addMonth()->daysInMonth));
if (Carbon::now()->lessThan($generationDate)) {
return response()->json([
'can_settle' => false,
'message' => "Salary for {$nextSettlementMonth->format('F Y')} will be available for settlement on " . $generationDate->format('jS F Y'),
]);
}
} }
// Calculate Cycle and Pro-rata // Calculate Cycle and Pro-rata
@ -657,7 +664,30 @@ public function getAdvanceHistory($id)
} }
$history = SalaryAdvanceDeduction::where('staff_id', $id) $history = SalaryAdvanceDeduction::where('staff_id', $id)
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->get(); ->get()
->map(function($h) {
$schedule = [];
$months = $h->total_months ?: 1;
$paid = $h->paid_amount ?: 0;
$installment = $h->monthly_deduction ?: ($h->advance_amount / $months);
// Assuming repayment starts from the month following the advance
$startDate = Carbon::parse($h->created_at)->startOfMonth();
for ($i = 0; $i < $months; $i++) {
$monthDate = $startDate->copy()->addMonths($i + 1);
$isPaid = $paid >= (($i + 1) * $installment - 0.01);
$schedule[] = [
'month' => $monthDate->format('F Y'),
'amount' => round($installment, 2),
'status' => $isPaid ? 'Paid' : 'Pending'
];
}
$h->installment_schedule = $schedule;
return $h;
});
return response()->json($history); return response()->json($history);
} }
@ -687,16 +717,16 @@ public function getAllPendingSalaries(Request $request)
$staffMonths = []; $staffMonths = [];
// Loop while generation date for the month has passed $currentMonth = Carbon::now()->startOfMonth();
while (true) {
// Loop while the month has completed
while ($tempMonth->lessThan($currentMonth)) {
$monthKey = $tempMonth->format('Y-m'); $monthKey = $tempMonth->format('Y-m');
// Calculate generation date for this month // Safety break for extremely old joining dates (limit to 10 years or similar)
$genDay = $branch->salary_generation_day ?? 2; if ($tempMonth->diffInYears(Carbon::now()) > 10) {
$generationDate = $tempMonth->copy()->addMonth()->day(min($genDay, $tempMonth->copy()->addMonth()->daysInMonth)); $tempMonth->addMonth();
continue;
if (Carbon::now()->lessThan($generationDate)) {
break;
} }
if (!isset($settlements[$monthKey])) { if (!isset($settlements[$monthKey])) {

34
config/cors.php Normal file
View File

@ -0,0 +1,34 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Cross-Origin Resource Sharing (CORS) Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your settings for cross-origin resource sharing
| or "CORS". This determines what cross-origin operations may execute
| in web browsers. You are free to adjust these settings as needed.
|
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
*/
'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout', 'receptionist/login'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
];

View File

@ -1,133 +1,134 @@
# Flutter Mobile App API Documentation (Owner Role) # Flutter Mobile App API Documentation
Base URL: http://127.0.0.1:8000/api ## Connection Details
Note: For testing on a real physical mobile device, replace 127.0.0.1 with your computer's local IP address (e.g., 192.168.1.5). - **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`).
- **Headers**:
- `Accept: application/json`
- `Content-Type: application/json`
- `X-Requested-With: XMLHttpRequest`
## Authentication ---
- POST /login
- Params: email, password
- Returns: CSRF cookie and session (for web-based auth) or Auth token (if configured).
- POST /logout ## 1. Authentication Module
- Action: End session Manage user sessions and profiles.
- GET /profile - **POST /login**
- Returns: Current logged-in user details and role. - Params: `email`, `password`
- Response: `{ "user": { ... }, "redirect": "/owner/dashboard" }`
- **POST /receptionist/login**
- Params: `email`, `password`
- Response: `{ "user": { ... }, "redirect": "/receptionist/dashboard" }`
- **GET /api/profile**
- Query: `context` (owner/receptionist)
- Returns: Current user details and role.
- **POST /logout**
- Action: Terminates session.
## Branch Management ---
- GET /branches
- List all branches.
- POST /branches
- Create new branch (Multipart/form-data for documents).
- GET /branches/{id}
- View specific branch details.
- PUT /branches/{id}
- Update branch details.
- DELETE /branches/{id}
- Delete a branch.
- GET /branches/{branch}/receptionist ## 2. Branch Management
- View receptionist for a branch. Manage business locations and their documents.
- POST /branches/{branch}/receptionist
- Create/Update receptionist credentials.
- DELETE /branches/{branch}/receptionist
- Remove receptionist.
## Staff Management - **GET /api/branches**
- GET /staff - Query: `status` (Active/Inactive)
- List all staff members. - Returns: List of all branches with document counts and revenue.
- POST /staff - **GET /api/branches/{id}**
- Add new staff (Multipart/form-data for documents). - Returns: Detailed branch info and documents.
- GET /staff/{id} - **POST /api/branches**
- View staff profile. - Type: `multipart/form-data`
- PUT /staff/{id} - Params: `name`, `location`, `manager_name`, `operational_start_date`, `payroll_from_day`, `payroll_to_day`, `salary_generation_day`
- Update staff profile. - Files: `docs[0][file]`, `docs[0][name]`, `docs[0][expiry_date]`...
- DELETE /staff/{id} - **PUT /api/branches/{id}**
- Delete staff member. - Params: Same as POST, plus `status`.
- **DELETE /api/branches/{id}**
- Note: Only deletable if not used in staff/inventory/accounts.
- **GET /api/branches/{id}/active-staff**
- Returns: List of active staff in that branch.
- GET /staff/pending-salaries ---
- List all pending salaries across branches.
- POST /staff/bulk-settle
- Params: staff_ids[]
- Settle multiple salaries at once.
- GET /staff/{id}/payments
- Salary payment history.
- GET /staff/{id}/payroll-status
- Current month's payroll calculation.
- POST /staff/{id}/settle
- Settle individual salary for a month.
- GET /staff/{id}/advance-history
- List of advance payments and deductions.
## Investor & ROI Management ## 3. Staff & Payroll Module
- GET /investors Employee management and salary settlements.
- List all investors.
- POST /investors
- Add new investor (Multipart/form-data for documents).
- GET /investors/{id}
- View investor details.
- PUT /investors/{id}
- Update investor.
- DELETE /investors/{id}
- Delete investor.
- GET /investors/pending-roi - **GET /api/staff**
- List all pending ROI settlements. - Query: `branch_id`
- GET /investors/{id}/roi-status - Returns: Complete staff list with documents.
- Monthly ROI status breakdown (Base ROI, Carry Over, Paid, Net Due). - **POST /api/staff**
- POST /investors/{id}/settle-roi - Type: `multipart/form-data`
- Params: payout_month, amount, payout_date, payment_method, remarks. - Params: `full_name`, `email`, `phone`, `role`, `branch_id`, `joining_date`, `status`, `salary_type`, `salary_amount`.
- Settle a month's ROI. - Optional: `advance_enabled`, `advance_amount`, `commission_enabled`, `documents[]`.
- **GET /api/staff/{id}/payroll-status**
- Returns: Month-by-month payment history and unpaid months.
- **GET /api/staff/{id}/settlement**
- Returns: Pro-rated salary calculation, commissions, and advance deductions due.
- **POST /api/staff/{id}/settle**
- Params: `month` (Y-m), `remarks`
- Action: Processes salary payment and records expense.
- **GET /api/staff/pending-salaries**
- Returns: List of all staff with pending settlements across branches.
- **POST /api/staff/bulk-settle**
- Params: `settlements` (array of staff_id/month_key), `remarks`.
## Financials & Expenses ---
- GET /accounts
- General ledger of all credit/debit transactions.
- GET /expenses
- List of all business expenses.
- POST /expenses
- Params: date, branch_id, expense_category_id, expense_type (Account/Petty Cash), amount, remarks.
- Record a new expense.
- GET /expense-categories
- List of master expense categories.
## Inventory Management ## 4. Investor & ROI Module
- GET /inventory/products Manage investments and monthly payouts.
- List all products and stock levels.
- POST /inventory/products
- Add new product with image.
- POST /inventory/products/{id}/adjust
- Adjuts stock (Add/Remove) with remarks.
- GET /inventory/products/{id}/history
- Stock movement history for a specific product.
- GET /inventory/sales
- List of all POS sales.
- POST /inventory/sales
- Record a new product sale.
- GET /inventory/movements
- Global stock movement log.
## Collections - **GET /api/investors**
- GET /collections - Returns: List of all investors and their linked branches.
- List all daily collections. - **POST /api/investors**
- POST /collections - Params: `name`, `investment_date`, `investment_amount`, `roi_type` (Percentage/Fixed Amount), `roi_value`, `roi_period` (Monthly/Quarterly/Yearly).
- Record new collection. - **GET /api/investors/{id}/roi-status**
- GET /collections/{id} - Returns: Breakdown of ROI due, paid, and carry-over for each period.
- View collection details. - **POST /api/investors/{id}/settle-roi**
- Params: `payout_month`, `amount`, `payout_date`, `payment_method`, `remarks`.
## Reports ---
- GET /reports/profit
- Profit & Loss report (Income vs Expenses).
- GET /reports/expiry-reminders
- Documents (Trade license, staff IDs, etc.) expiring soon.
- GET /reports/investments
- Summary of total investments and ROI distributed.
## Master Settings ## 5. Inventory & POS Module
- GET /masters/{type} Product management and sales tracking.
- Types: expense_categories, product_categories, payment_methods, etc.
- POST /masters/{type} - **GET /api/inventory/products**
- Add master entry. - Query: `branch_id`, `status` (In Stock/Low Stock/Out of Stock).
- PUT /masters/{type}/{id} - **POST /api/inventory/products**
- Edit master entry. - Params: `name`, `sku`, `product_category_id`, `branch_id`, `cost_price`, `selling_price`, `current_stock`, `reorder_level`.
- DELETE /masters/{type}/{id} - **POST /api/inventory/products/{id}/adjust**
- Delete master entry. - Params: `adjustment_qty` (+/-), `reason`, `adjustment_date`.
- **POST /api/inventory/sales**
- Params: `branch_id`, `payment_method`, `items` (array of product_id/quantity/unit_price).
- Action: Deducts stock and records revenue.
---
## 6. Collections & Expenses
Financial tracking.
- **GET /api/collections**
- Query: `start_date`, `end_date`, `branch_id`.
- **POST /api/collections**
- Params: `date`, `branch_id`, `collection_type_id`, `amount`, `payment_method`, `items[]`.
- **POST /api/expenses**
- Params: `date`, `branch_id`, `expense_category_id`, `expense_type` (Account/Petty Cash), `amount`, `remarks`.
---
## 7. Reports
Data analysis and reminders.
- **GET /api/reports/profit**
- Returns: Total income, total expense, net profit, and 6-month trend.
- **GET /api/reports/expiry-reminders**
- Returns: Document expiry alerts for both Staff and Branches.
- **GET /api/reports/investments**
- Returns: Summary of total investments and total ROI returned.
---
## 8. Master Settings
Manage dropdown options.
- **GET /api/masters/{type}**
- Types: `collection`, `expense`, `product`, `payment_method`, `staff_role`.
- **POST /api/masters/{type}**
- Params: `name`, `status`.

View File

@ -16,9 +16,6 @@ export default function EditBranchModal({ isOpen, onClose, onRefresh, branch })
const [newDocs, setNewDocs] = useState([]); const [newDocs, setNewDocs] = useState([]);
const [activeStaff, setActiveStaff] = useState([]); const [activeStaff, setActiveStaff] = useState([]);
const [branches, setBranches] = useState([]); // All branches for "Move to" option
const [staffAction, setStaffAction] = useState('move'); // 'move' or 'inactivate'
const [moveToBranchId, setMoveToBranchId] = useState('');
const [loadingStaff, setLoadingStaff] = useState(false); const [loadingStaff, setLoadingStaff] = useState(false);
useEffect(() => { useEffect(() => {
@ -35,8 +32,6 @@ export default function EditBranchModal({ isOpen, onClose, onRefresh, branch })
}); });
setNewDocs([]); setNewDocs([]);
setActiveStaff([]); setActiveStaff([]);
setStaffAction('move');
setMoveToBranchId('');
} }
}, [branch]); }, [branch]);
@ -49,11 +44,6 @@ export default function EditBranchModal({ isOpen, onClose, onRefresh, branch })
const res = await fetch(`/api/branches/${branch.id}/active-staff`); const res = await fetch(`/api/branches/${branch.id}/active-staff`);
const data = await res.json(); const data = await res.json();
setActiveStaff(data); setActiveStaff(data);
// Fetch other branches for movement
const bRes = await fetch('/api/branches?status=Active');
const bData = await bRes.json();
setBranches(bData.filter(b => b.id !== branch.id));
} catch (error) { } catch (error) {
console.error('Error fetching active staff:', error); console.error('Error fetching active staff:', error);
} finally { } finally {
@ -95,13 +85,6 @@ export default function EditBranchModal({ isOpen, onClose, onRefresh, branch })
data.append('payroll_to_day', formData.payroll_to_day); data.append('payroll_to_day', formData.payroll_to_day);
data.append('salary_generation_day', formData.salary_generation_day); data.append('salary_generation_day', formData.salary_generation_day);
if (formData.status === 'Inactive' && activeStaff.length > 0) {
data.append('staff_action', staffAction);
if (staffAction === 'move') {
data.append('move_to_branch_id', moveToBranchId);
}
}
newDocs.forEach((doc, index) => { newDocs.forEach((doc, index) => {
if (doc.file) { if (doc.file) {
data.append(`new_docs[${index}][file]`, doc.file); data.append(`new_docs[${index}][file]`, doc.file);
@ -257,44 +240,11 @@ export default function EditBranchModal({ isOpen, onClose, onRefresh, branch })
<User size={16} /> <User size={16} />
<h4 className="text-sm font-bold mt-1">Active Staff Detected ({activeStaff.length})</h4> <h4 className="text-sm font-bold mt-1">Active Staff Detected ({activeStaff.length})</h4>
</div> </div>
<p className="text-[11px] text-orange-700 leading-relaxed"> <p className="text-[11px] text-orange-700 font-bold leading-relaxed">
This branch has active staff members. Please choose what to do with them before inactivating the branch. Warning: Inactivating this branch will automatically inactivate all {activeStaff.length} active staff members.
</p> </p>
<div className="space-y-3"> <div className="max-h-32 overflow-y-auto no-scrollbar space-y-1 pr-1 border-t border-orange-100/50 pt-2">
<div className="flex gap-2">
<button
type="button"
onClick={() => setStaffAction('move')}
className={`flex-1 py-2 text-[10px] font-bold rounded-lg border transition-all ${staffAction === 'move' ? 'bg-orange-500 text-white border-orange-500 shadow-sm' : 'bg-white text-orange-500 border-orange-200 hover:bg-orange-50'}`}
>
Move to Branch
</button>
<button
type="button"
onClick={() => setStaffAction('inactivate')}
className={`flex-1 py-2 text-[10px] font-bold rounded-lg border transition-all ${staffAction === 'inactivate' ? 'bg-orange-500 text-white border-orange-500 shadow-sm' : 'bg-white text-orange-500 border-orange-200 hover:bg-orange-50'}`}
>
Inactivate All
</button>
</div>
{staffAction === 'move' && (
<select
required={staffAction === 'move'}
className="w-full px-3 py-2 bg-white border border-orange-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 transition-all text-xs font-bold"
value={moveToBranchId}
onChange={(e) => setMoveToBranchId(e.target.value)}
>
<option value="">Select Target Branch *</option>
{branches.map(b => (
<option key={b.id} value={b.id}>{b.name}</option>
))}
</select>
)}
</div>
<div className="max-h-32 overflow-y-auto no-scrollbar space-y-1 pr-1">
{activeStaff.map(s => ( {activeStaff.map(s => (
<div key={s.id} className="px-3 py-1.5 bg-white/50 rounded flex items-center justify-between text-[10px] text-orange-900 border border-orange-100/50"> <div key={s.id} className="px-3 py-1.5 bg-white/50 rounded flex items-center justify-between text-[10px] text-orange-900 border border-orange-100/50">
<span className="font-bold">{s.full_name}</span> <span className="font-bold">{s.full_name}</span>

View File

@ -50,6 +50,19 @@ function ReceptionistForm({ branchId }) {
setError(''); setError('');
setSuccess(''); setSuccess('');
if (editingId === null || formData.password !== '') {
if (formData.password.length < 6) {
setError('Password must be at least 6 characters.');
setSaving(false);
return;
}
if (formData.password !== formData.password_confirmation) {
setError('Passwords do not match. Please make it proper.');
setSaving(false);
return;
}
}
try { try {
const csrfToken = document.querySelector('meta[name="csrf-token"]').content; const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
const res = await fetch(`/api/branches/${branchId}/receptionist`, { const res = await fetch(`/api/branches/${branchId}/receptionist`, {
@ -66,7 +79,12 @@ function ReceptionistForm({ branchId }) {
setSuccess(editingId ? 'Account updated successfully!' : 'Account created successfully!'); setSuccess(editingId ? 'Account updated successfully!' : 'Account created successfully!');
fetchReceptionists(); fetchReceptionists();
} else { } else {
setError(data.message || 'Failed to save receptionist.'); if (data.errors) {
const firstError = Object.values(data.errors)[0][0];
setError(firstError);
} else {
setError(data.message || 'Failed to save receptionist.');
}
} }
} catch (err) { } catch (err) {
setError('An error occurred. Please try again.'); setError('An error occurred. Please try again.');
@ -189,6 +207,49 @@ function ReceptionistForm({ branchId }) {
required={formData.password !== ''} required={formData.password !== ''}
/> />
</div> </div>
{/* Password Criteria Feedback */}
<div className="col-span-2 space-y-3 px-1">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<div className={`w-1.5 h-1.5 rounded-full transition-colors ${formData.password.length >= 6 ? 'bg-emerald-500' : (formData.password.length > 0 ? 'bg-red-500' : 'bg-gray-300')}`}></div>
<span className={`text-[9px] font-black uppercase tracking-widest transition-colors ${
(formData.password.length >= 6 || (editingId && formData.password === ''))
? 'text-emerald-600'
: (formData.password.length > 0 ? 'text-red-500' : 'text-gray-400')}`}>
Min 6 characters
</span>
</div>
<div className="flex items-center gap-2">
<div className={`w-1.5 h-1.5 rounded-full transition-colors ${
(formData.password !== '' && formData.password === formData.password_confirmation)
? 'bg-emerald-500'
: (formData.password_confirmation !== '' ? 'bg-red-500' : 'bg-gray-300')}`}></div>
<span className={`text-[9px] font-black uppercase tracking-widest transition-colors ${
(formData.password !== '' && formData.password === formData.password_confirmation)
? 'text-emerald-600'
: (formData.password_confirmation !== '' ? 'text-red-500' : 'text-gray-400')}`}>
Passwords match
</span>
</div>
</div>
{/* Explicit Error Messages */}
<div className="space-y-1">
{formData.password.length > 0 && formData.password.length < 6 && (
<p className="text-[10px] text-red-500 font-bold uppercase tracking-widest flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
<span className="w-1 h-1 rounded-full bg-red-500"></span>
Password is too short (min 6 characters)
</p>
)}
{formData.password_confirmation !== '' && formData.password !== formData.password_confirmation && (
<p className="text-[10px] text-red-500 font-bold uppercase tracking-widest flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
<span className="w-1 h-1 rounded-full bg-red-500"></span>
Please make it proper (Passwords mismatch)
</p>
)}
</div>
</div>
</div> </div>
<div className="flex items-center gap-4 pt-6"> <div className="flex items-center gap-4 pt-6">

View File

@ -672,68 +672,80 @@ export default function ExpenseList() {
</div> </div>
<div className="p-8 overflow-y-auto space-y-6"> <div className="p-8 overflow-y-auto space-y-6">
<div className="space-y-4"> {pendingSalaries.length > 0 ? (
{pendingSalaries.map(staff => ( <div className="space-y-4">
<div key={staff.staff_id} className="p-4 rounded-2xl bg-gray-50/50 border border-gray-100"> {pendingSalaries.map(staff => (
<div className="flex items-center justify-between mb-3"> <div key={staff.staff_id} className="p-4 rounded-2xl bg-gray-50/50 border border-gray-100">
<div> <div className="flex items-center justify-between mb-3">
<p className="text-sm font-black text-gray-900">{staff.staff_name}</p> <div>
<button <p className="text-sm font-black text-gray-900">{staff.staff_name}</p>
onClick={() => {
const current = bulkData.selectedMonths[staff.staff_id] || [];
const allMonths = staff.pending_months.map(m => m.month_key);
const allSelected = allMonths.every(key => current.includes(key));
setBulkData({
...bulkData,
selectedMonths: {
...bulkData.selectedMonths,
[staff.staff_id]: allSelected ? [] : allMonths
}
});
}}
className="text-[9px] font-black text-emerald-600 uppercase tracking-tighter hover:underline"
>
{ (bulkData.selectedMonths[staff.staff_id]?.length === staff.pending_months.length) ? 'Deselect All' : 'Select All Months' }
</button>
</div>
<p className="text-xs font-bold text-emerald-600">
{ (staff.pending_months.filter(m => bulkData.selectedMonths[staff.staff_id]?.includes(m.month_key)).reduce((sum, m) => sum + m.net_payable, 0) || 0).toLocaleString() } AED
</p>
</div>
<div className="flex flex-wrap gap-2">
{staff.pending_months.map(m => {
const isSelected = bulkData.selectedMonths[staff.staff_id]?.includes(m.month_key);
return (
<button <button
key={m.month_key}
onClick={() => { onClick={() => {
const current = bulkData.selectedMonths[staff.staff_id] || []; const current = bulkData.selectedMonths[staff.staff_id] || [];
const next = isSelected const allMonths = staff.pending_months.map(m => m.month_key);
? current.filter(key => key !== m.month_key) const allSelected = allMonths.every(key => current.includes(key));
: [...current, m.month_key];
setBulkData({ setBulkData({
...bulkData, ...bulkData,
selectedMonths: { selectedMonths: {
...bulkData.selectedMonths, ...bulkData.selectedMonths,
[staff.staff_id]: next [staff.staff_id]: allSelected ? [] : allMonths
} }
}); });
}} }}
className={`px-3 py-1.5 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all ${ className="text-[9px] font-black text-emerald-600 uppercase tracking-tighter hover:underline"
isSelected
? 'bg-emerald-500 text-white shadow-md shadow-emerald-100'
: 'bg-white text-gray-400 border border-gray-100 hover:border-emerald-200'
}`}
> >
{m.month_name} { (bulkData.selectedMonths[staff.staff_id]?.length === staff.pending_months.length) ? 'Deselect All' : 'Select All Months' }
</button> </button>
); </div>
})} <p className="text-xs font-bold text-emerald-600">
{ (staff.pending_months.filter(m => bulkData.selectedMonths[staff.staff_id]?.includes(m.month_key)).reduce((sum, m) => sum + m.net_payable, 0) || 0).toLocaleString() } AED
</p>
</div>
<div className="flex flex-wrap gap-2">
{staff.pending_months.map(m => {
const isSelected = bulkData.selectedMonths[staff.staff_id]?.includes(m.month_key);
return (
<button
key={m.month_key}
onClick={() => {
const current = bulkData.selectedMonths[staff.staff_id] || [];
const next = isSelected
? current.filter(key => key !== m.month_key)
: [...current, m.month_key];
setBulkData({
...bulkData,
selectedMonths: {
...bulkData.selectedMonths,
[staff.staff_id]: next
}
});
}}
className={`px-3 py-1.5 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all ${
isSelected
? 'bg-emerald-500 text-white shadow-md shadow-emerald-100'
: 'bg-white text-gray-400 border border-gray-100 hover:border-emerald-200'
}`}
>
{m.month_name}
</button>
);
})}
</div>
</div> </div>
))}
</div>
) : (
<div className="py-20 text-center space-y-4">
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mx-auto text-gray-300">
<CreditCard size={32} />
</div> </div>
))} <div className="space-y-1">
</div> <p className="text-sm font-black text-gray-900 uppercase tracking-widest">No Pending Salaries</p>
<p className="text-xs text-gray-400 font-bold">All staff members in this branch are fully paid for previous months.</p>
</div>
</div>
)}
<div> <div>
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 block">Release Remarks</label> <label className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 block">Release Remarks</label>

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { X, Search, ShoppingCart, Plus, Minus, CreditCard, DollarSign, Globe, Trash2 } 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 }) {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
@ -323,9 +323,7 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc
<button <button
disabled={cart.length === 0 || loading || (parseFloat(adjustedTotal) !== totalWithVat && !adjustmentRemarks.trim())} disabled={cart.length === 0 || loading || (parseFloat(adjustedTotal) !== totalWithVat && !adjustmentRemarks.trim())}
onClick={handleSubmit} onClick={handleSubmit}
className="w-full py-2.5 mt-2 rounded-lg font-black uppercase tracking-[0.15em] text-[10px] text-white shadow-lg transition-all flex items-center justify-center gap-2 active:scale-95 ${ className={`w-full py-2.5 mt-2 rounded-lg font-black uppercase tracking-[0.15em] text-[10px] text-white shadow-lg transition-all flex items-center justify-center gap-2 active:scale-95 bg-[#EF4444] hover:bg-red-600 shadow-red-200 disabled:opacity-50 disabled:scale-100 disabled:shadow-none`}
success ? 'bg-emerald-500 shadow-emerald-200' : 'bg-[#EF4444] hover:bg-red-600 shadow-red-200'
} disabled:opacity-50 disabled:scale-100 disabled:shadow-none"
> >
{loading ? 'Processing...' : 'Process Payment'} {loading ? 'Processing...' : 'Process Payment'}
</button> </button>

View File

@ -48,6 +48,7 @@ export default function StaffView({ id }) {
const [loadingPayroll, setLoadingPayroll] = useState(false); const [loadingPayroll, setLoadingPayroll] = useState(false);
const [advanceHistory, setAdvanceHistory] = useState([]); const [advanceHistory, setAdvanceHistory] = useState([]);
const [loadingAdvanceHistory, setLoadingAdvanceHistory] = useState(false); const [loadingAdvanceHistory, setLoadingAdvanceHistory] = useState(false);
const [isAdvanceHistoryModalOpen, setIsAdvanceHistoryModalOpen] = useState(false);
useEffect(() => { useEffect(() => {
fetchStaff(); fetchStaff();
@ -426,21 +427,32 @@ export default function StaffView({ id }) {
</div> </div>
)} )}
{advanceHistory.length > 0 && ( {advanceHistory.length > 0 && (
<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">
<p className="text-[9px] font-black text-orange-400 uppercase tracking-widest mb-2">Advance History Summary</p> <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>
{advanceHistory.flatMap(h => h.installment_schedule || []).length > 8 && (
<button
onClick={() => setIsAdvanceHistoryModalOpen(true)}
className="text-[8px] font-black text-orange-600 hover:underline uppercase tracking-widest"
>
View Full History
</button>
)}
</div>
<div className="space-y-2"> <div className="space-y-2">
{advanceHistory.map((h, i) => ( {advanceHistory.flatMap(h => h.installment_schedule || [])
<div key={i} className={`p-2 rounded-xl border flex items-center justify-between transition-all ${h.status === 'Closed' ? 'bg-white/50 border-gray-100 opacity-60' : 'bg-white border-orange-100 shadow-sm animate-pulse'}`}> .slice(0, 8)
.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> <div>
<p className="text-[10px] font-black text-gray-900">{(h.advance_amount || 0).toLocaleString()} AED</p> <p className="text-[10px] font-black text-gray-900">{item.month}</p>
<p className="text-[8px] text-gray-400 font-medium">Taken {new Date(h.created_at).toLocaleDateString()}</p> <p className="text-[8px] text-gray-400 font-medium">AED {item.amount.toLocaleString()}</p>
</div> </div>
<div className="text-right"> <div className="text-right">
<span className={`px-2 py-0.5 rounded-full text-[8px] font-black uppercase tracking-widest ${h.status === 'Closed' ? 'bg-gray-100 text-gray-400' : 'bg-orange-50 text-orange-600'}`}> <span className={`px-2 py-0.5 rounded-full text-[8px] font-black uppercase tracking-widest ${item.status === 'Paid' ? 'bg-emerald-100 text-emerald-600' : 'bg-orange-50 text-orange-600'}`}>
{h.status} {item.status}
</span> </span>
<p className="text-[8px] font-bold text-emerald-500 mt-0.5">{parseFloat(h.paid_amount || 0).toLocaleString()} Paid</p>
</div> </div>
</div> </div>
))} ))}
@ -993,6 +1005,63 @@ export default function StaffView({ id }) {
</div> </div>
</div> </div>
)} )}
{/* Full Advance History Modal */}
{isAdvanceHistoryModalOpen && (
<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-2xl 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">
<div>
<h3 className="text-xl font-black text-gray-900 uppercase tracking-tight">Full Advance History</h3>
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-1">Repayment Schedule & Status</p>
</div>
<button onClick={() => setIsAdvanceHistoryModalOpen(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="space-y-4">
{advanceHistory.map((h, hIdx) => (
<div key={hIdx} className="space-y-3">
<div className="flex items-center justify-between px-2">
<p className="text-[10px] font-black text-orange-500 uppercase tracking-widest">
Advance taken on {new Date(h.created_at).toLocaleDateString()} {parseFloat(h.advance_amount).toLocaleString()} AED
</p>
<span className={`px-2 py-0.5 rounded-full text-[8px] font-black uppercase tracking-widest ${h.status === 'Closed' ? 'bg-gray-100 text-gray-400' : 'bg-orange-50 text-orange-600'}`}>
{h.status}
</span>
</div>
<div className="grid grid-cols-2 gap-3">
{(h.installment_schedule || []).map((item, iIdx) => (
<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>
<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>
<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}
</div>
</div>
))}
</div>
{hIdx < advanceHistory.length - 1 && <div className="h-px bg-gray-100 my-6" />}
</div>
))}
</div>
</div>
<div className="p-8 border-t border-gray-100 bg-gray-50/30 flex justify-end">
<button
onClick={() => setIsAdvanceHistoryModalOpen(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

@ -23,7 +23,9 @@ export default function ReceptionistDashboard() {
const [transactions, setTransactions] = useState([]); const [transactions, setTransactions] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [filterBranch, setFilterBranch] = useState(window.__APP_DATA__?.branch?.id || ''); const [filterBranch, setFilterBranch] = useState(window.__APP_DATA__?.branch?.id || '');
const [startDate, setStartDate] = useState(new Date().toISOString().split('T')[0]);
// Default to current month for a better overview
const [startDate, setStartDate] = useState(new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0]);
const [endDate, setEndDate] = useState(new Date().toISOString().split('T')[0]); const [endDate, setEndDate] = useState(new Date().toISOString().split('T')[0]);
const [branches, setBranches] = useState([]); const [branches, setBranches] = useState([]);
@ -48,8 +50,8 @@ export default function ReceptionistDashboard() {
const data = await response.json(); const data = await response.json();
setStats({ setStats({
total_income: data.total_income || 0, total_income: data.today_income || 0, // Using today's actual income
total_expenses: data.total_expense || 0, total_expenses: data.monthly_expense || 0, // Using monthly actual expenses
net_profit: data.net_profit || 0, net_profit: data.net_profit || 0,
low_stock_count: data.low_stock_count || 0 low_stock_count: data.low_stock_count || 0
}); });