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') {
$staffAction = $request->input('staff_action');
if ($staffAction === 'move') {
$targetBranchId = $request->input('move_to_branch_id');
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']);
}
\App\Models\Staff::where('branch_id', $id)
->where('status', 'Active')
->update(['status' => 'Inactive']);
}
// Process new documents if provided

View File

@ -26,7 +26,7 @@ public function getProfitReport(Request $request)
$perPage = $request->query('per_page', 10);
// Base Query from Account table (Ledger)
$query = Account::query()->with('accountable');
$query = Account::query()->with(['accountable', 'branch']);
if ($branchId) {
$query->where('branch_id', $branchId);
@ -42,6 +42,19 @@ public function getProfitReport(Request $request)
$totalCredits = (clone $query)->sum('credit');
$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
$allTransactions = $query->where(function($q) {
$q->where('credit', '>', 0)->orWhere('debit', '>', 0);
@ -73,7 +86,7 @@ public function getProfitReport(Request $request)
'category' => $a->type,
'description' => $a->description,
'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,
'original_amount' => $originalAmount,
'remarks' => $remarks
@ -95,13 +108,12 @@ public function getProfitReport(Request $request)
$monthStart = Carbon::now()->subMonths($i)->startOfMonth();
$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))
->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()])
->sum('credit');
// For trend, we can also use Account table for expenses
$monthExpense = Account::where('branch_id', $branchId ?: '!=', 0)
$monthExpense = Account::query()
->when($branchId, fn($q) => $q->where('branch_id', $branchId))
->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()])
->sum('debit');
@ -118,6 +130,8 @@ public function getProfitReport(Request $request)
return response()->json([
'total_income' => $totalCredits,
'total_expense' => $totalDebits,
'today_income' => $todayIncome,
'monthly_expense' => $monthlyExpense,
'net_profit' => $totalCredits - $totalDebits,
'low_stock_count' => $lowStockCount,
'transactions' => $paginatedTransactions,

View File

@ -380,13 +380,36 @@ public function getSettlementDetails(Request $request, $id)
if ($user && $user->isReceptionist() && $staff->branch_id != $user->branch_id) {
return response()->json(['message' => 'Unauthorized'], 403);
}
$targetMonthKey = $request->query('month');
$targetMonthKey = $request->input('month') ?? $request->query('month') ?? $request->input('settlement_month');
$branch = $staff->branch;
if ($targetMonthKey) {
$nextSettlementMonth = Carbon::parse($targetMonthKey . '-01');
// 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();
// Verify if already paid
$joiningDate = Carbon::parse($staff->joining_date);
if ($lastPayment) {
$logicalNextMonth = Carbon::parse($lastPayment->settlement_month . '-01')->addMonth();
} else {
$logicalNextMonth = $joiningDate->copy()->startOfMonth();
}
if ($targetMonthKey) {
$requestedMonth = Carbon::parse($targetMonthKey . '-01');
// 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)
->where('payment_type', 'Salary Settlement')
->where('settlement_month', $targetMonthKey)
@ -399,31 +422,15 @@ public function getSettlementDetails(Request $request, $id)
]);
}
} else {
// Find the next unpaid month logically
$lastPayment = \App\Models\StaffPayment::where('staff_id', $id)
->where('payment_type', 'Salary Settlement')
->orderBy('settlement_month', 'desc')
->first();
$nextSettlementMonth = $logicalNextMonth;
}
$joiningDate = Carbon::parse($staff->joining_date);
$currentMonth = Carbon::now()->startOfMonth();
if ($lastPayment) {
$nextSettlementMonth = Carbon::parse($lastPayment->settlement_month . '-01')->addMonth();
} 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'),
]);
}
// 3. Status Check: Only allow settlement if the month has completed
if (Carbon::now()->lessThan($nextSettlementMonth->copy()->addMonth()->startOfMonth())) {
return response()->json([
'can_settle' => false,
'message' => "Salary for {$nextSettlementMonth->format('F Y')} is still in progress.",
]);
}
// Calculate Cycle and Pro-rata
@ -657,7 +664,30 @@ public function getAdvanceHistory($id)
}
$history = SalaryAdvanceDeduction::where('staff_id', $id)
->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);
}
@ -687,16 +717,16 @@ public function getAllPendingSalaries(Request $request)
$staffMonths = [];
// Loop while generation date for the month has passed
while (true) {
$currentMonth = Carbon::now()->startOfMonth();
// Loop while the month has completed
while ($tempMonth->lessThan($currentMonth)) {
$monthKey = $tempMonth->format('Y-m');
// Calculate generation date for this month
$genDay = $branch->salary_generation_day ?? 2;
$generationDate = $tempMonth->copy()->addMonth()->day(min($genDay, $tempMonth->copy()->addMonth()->daysInMonth));
if (Carbon::now()->lessThan($generationDate)) {
break;
// Safety break for extremely old joining dates (limit to 10 years or similar)
if ($tempMonth->diffInYears(Carbon::now()) > 10) {
$tempMonth->addMonth();
continue;
}
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
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).
## 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`).
- **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
- Action: End session
## 1. Authentication Module
Manage user sessions and profiles.
- GET /profile
- Returns: Current logged-in user details and role.
- **POST /login**
- 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
- View receptionist for a branch.
- POST /branches/{branch}/receptionist
- Create/Update receptionist credentials.
- DELETE /branches/{branch}/receptionist
- Remove receptionist.
## 2. Branch Management
Manage business locations and their documents.
## Staff Management
- GET /staff
- List all staff members.
- POST /staff
- Add new staff (Multipart/form-data for documents).
- GET /staff/{id}
- View staff profile.
- PUT /staff/{id}
- Update staff profile.
- DELETE /staff/{id}
- Delete staff member.
- **GET /api/branches**
- Query: `status` (Active/Inactive)
- Returns: List of all branches with document counts and revenue.
- **GET /api/branches/{id}**
- Returns: Detailed branch info and documents.
- **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]`...
- **PUT /api/branches/{id}**
- 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
- GET /investors
- 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.
## 3. Staff & Payroll Module
Employee management and salary settlements.
- GET /investors/pending-roi
- List all pending ROI settlements.
- GET /investors/{id}/roi-status
- Monthly ROI status breakdown (Base ROI, Carry Over, Paid, Net Due).
- POST /investors/{id}/settle-roi
- Params: payout_month, amount, payout_date, payment_method, remarks.
- Settle a month's ROI.
- **GET /api/staff**
- Query: `branch_id`
- Returns: Complete staff list with documents.
- **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[]`.
- **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
- GET /inventory/products
- 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.
## 4. Investor & ROI Module
Manage investments and monthly payouts.
## Collections
- GET /collections
- List all daily collections.
- POST /collections
- Record new collection.
- GET /collections/{id}
- View collection details.
- **GET /api/investors**
- Returns: List of all investors and their linked branches.
- **POST /api/investors**
- Params: `name`, `investment_date`, `investment_amount`, `roi_type` (Percentage/Fixed Amount), `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.
- **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
- GET /masters/{type}
- Types: expense_categories, product_categories, payment_methods, etc.
- POST /masters/{type}
- Add master entry.
- PUT /masters/{type}/{id}
- Edit master entry.
- DELETE /masters/{type}/{id}
- Delete master entry.
## 5. Inventory & POS Module
Product management and sales tracking.
- **GET /api/inventory/products**
- Query: `branch_id`, `status` (In Stock/Low Stock/Out of Stock).
- **POST /api/inventory/products**
- 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`.
- **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 [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);
useEffect(() => {
@ -35,8 +32,6 @@ export default function EditBranchModal({ isOpen, onClose, onRefresh, branch })
});
setNewDocs([]);
setActiveStaff([]);
setStaffAction('move');
setMoveToBranchId('');
}
}, [branch]);
@ -49,11 +44,6 @@ export default function EditBranchModal({ isOpen, onClose, onRefresh, branch })
const res = await fetch(`/api/branches/${branch.id}/active-staff`);
const data = await res.json();
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) {
console.error('Error fetching active staff:', error);
} finally {
@ -95,13 +85,6 @@ export default function EditBranchModal({ isOpen, onClose, onRefresh, branch })
data.append('payroll_to_day', formData.payroll_to_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) => {
if (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} />
<h4 className="text-sm font-bold mt-1">Active Staff Detected ({activeStaff.length})</h4>
</div>
<p className="text-[11px] text-orange-700 leading-relaxed">
This branch has active staff members. Please choose what to do with them before inactivating the branch.
<p className="text-[11px] text-orange-700 font-bold leading-relaxed">
Warning: Inactivating this branch will automatically inactivate all {activeStaff.length} active staff members.
</p>
<div className="space-y-3">
<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">
<div className="max-h-32 overflow-y-auto no-scrollbar space-y-1 pr-1 border-t border-orange-100/50 pt-2">
{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">
<span className="font-bold">{s.full_name}</span>

View File

@ -50,6 +50,19 @@ function ReceptionistForm({ branchId }) {
setError('');
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 {
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
const res = await fetch(`/api/branches/${branchId}/receptionist`, {
@ -66,7 +79,12 @@ function ReceptionistForm({ branchId }) {
setSuccess(editingId ? 'Account updated successfully!' : 'Account created successfully!');
fetchReceptionists();
} 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) {
setError('An error occurred. Please try again.');
@ -189,6 +207,49 @@ function ReceptionistForm({ branchId }) {
required={formData.password !== ''}
/>
</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 className="flex items-center gap-4 pt-6">

View File

@ -672,68 +672,80 @@ export default function ExpenseList() {
</div>
<div className="p-8 overflow-y-auto space-y-6">
<div className="space-y-4">
{pendingSalaries.map(staff => (
<div key={staff.staff_id} className="p-4 rounded-2xl bg-gray-50/50 border border-gray-100">
<div className="flex items-center justify-between mb-3">
<div>
<p className="text-sm font-black text-gray-900">{staff.staff_name}</p>
<button
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 (
{pendingSalaries.length > 0 ? (
<div className="space-y-4">
{pendingSalaries.map(staff => (
<div key={staff.staff_id} className="p-4 rounded-2xl bg-gray-50/50 border border-gray-100">
<div className="flex items-center justify-between mb-3">
<div>
<p className="text-sm font-black text-gray-900">{staff.staff_name}</p>
<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];
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]: next
[staff.staff_id]: allSelected ? [] : allMonths
}
});
}}
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'
}`}
className="text-[9px] font-black text-emerald-600 uppercase tracking-tighter hover:underline"
>
{m.month_name}
{ (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
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 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">
<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>
<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 { 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 }) {
const [searchTerm, setSearchTerm] = useState('');
@ -323,9 +323,7 @@ export default function NewSaleModal({ isOpen, onClose, onSave, branches, produc
<button
disabled={cart.length === 0 || loading || (parseFloat(adjustedTotal) !== totalWithVat && !adjustmentRemarks.trim())}
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 ${
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"
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`}
>
{loading ? 'Processing...' : 'Process Payment'}
</button>

View File

@ -48,6 +48,7 @@ export default function StaffView({ id }) {
const [loadingPayroll, setLoadingPayroll] = useState(false);
const [advanceHistory, setAdvanceHistory] = useState([]);
const [loadingAdvanceHistory, setLoadingAdvanceHistory] = useState(false);
const [isAdvanceHistoryModalOpen, setIsAdvanceHistoryModalOpen] = useState(false);
useEffect(() => {
fetchStaff();
@ -426,21 +427,32 @@ export default function StaffView({ id }) {
</div>
)}
{advanceHistory.length > 0 && (
{advanceHistory.length > 0 && (
<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">
{advanceHistory.map((h, i) => (
<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'}`}>
{advanceHistory.flatMap(h => h.installment_schedule || [])
.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>
<p className="text-[10px] font-black text-gray-900">{(h.advance_amount || 0).toLocaleString()} AED</p>
<p className="text-[8px] text-gray-400 font-medium">Taken {new Date(h.created_at).toLocaleDateString()}</p>
<p className="text-[10px] font-black text-gray-900">{item.month}</p>
<p className="text-[8px] text-gray-400 font-medium">AED {item.amount.toLocaleString()}</p>
</div>
<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'}`}>
{h.status}
<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'}`}>
{item.status}
</span>
<p className="text-[8px] font-bold text-emerald-500 mt-0.5">{parseFloat(h.paid_amount || 0).toLocaleString()} Paid</p>
</div>
</div>
))}
@ -993,6 +1005,63 @@ export default function StaffView({ id }) {
</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>
</>
);

View File

@ -23,7 +23,9 @@ export default function ReceptionistDashboard() {
const [transactions, setTransactions] = useState([]);
const [loading, setLoading] = useState(true);
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 [branches, setBranches] = useState([]);
@ -48,8 +50,8 @@ export default function ReceptionistDashboard() {
const data = await response.json();
setStats({
total_income: data.total_income || 0,
total_expenses: data.total_expense || 0,
total_income: data.today_income || 0, // Using today's actual income
total_expenses: data.monthly_expense || 0, // Using monthly actual expenses
net_profit: data.net_profit || 0,
low_stock_count: data.low_stock_count || 0
});