Refactor script

This commit is contained in:
furen81
2026-02-07 04:52:11 +07:00
parent 6e681c4ad3
commit c1ef2df512
20 changed files with 1608 additions and 118 deletions

View File

@ -0,0 +1,490 @@
<?php
namespace App\Http\Controllers;
use App\Models\Agent;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
class AgentController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
// Enforce Master Role
// The middleware CheckAgentSecret attaches the agent to the request,
// but let's double check if it exists before accessing property
if (!isset($request->_agent) || $request->_agent->role !== 'master') {
// If accessed via public route without middleware, or non-master
// Check if user is authenticated via other means or just return 403
// For now assuming middleware is active for this route
if (isset($request->_agent) && $request->_agent->role !== 'master') {
return response()->json(['success' => false, 'message' => 'Unauthorized. Master access required.'], 403);
}
}
$query = Agent::query();
// Search functionality
if ($request->has('q') && !empty($request->q)) {
$term = $request->q;
// Use regex for case-insensitive partial match
$query->where(function ($q) use ($term) {
$pattern = '/' . preg_quote($term, '/') . '/i';
$q->where('agent_id', 'regexp', $pattern)
->orWhere('company_name', 'regexp', $pattern)
->orWhere('email', 'regexp', $pattern);
});
}
$agents = $query->orderBy('created_at', 'desc')->get();
return response()->json([
'success' => true,
'agents' => $agents
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validated = $request->validate([
'agent_id' => 'required|unique:agents,agent_id',
'company_name' => 'required|string',
'email' => 'required|email',
'phone' => 'nullable|string',
'address' => 'nullable|string',
'subscription_duration' => 'required|integer',
'is_active' => 'required|boolean',
'ip_whitelist' => 'nullable|array',
'password' => 'required|string|min:6', // [NEW]
]);
// Auto generate secret key if not provided (safety)
$validated['api_secret_key'] = 'sk_live_' . Str::random(32);
// Hash password
$validated['password'] = Hash::make($validated['password']);
// Force role to agent for public sign up
$validated['role'] = 'agent';
$agent = Agent::create($validated);
return response()->json([
'success' => true,
'message' => 'Agent created successfully',
'agent' => $agent
]);
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
$agent = Agent::find($id);
if (!$agent) {
return response()->json(['success' => false, 'message' => 'Agent not found'], 404);
}
return response()->json([
'success' => true,
'agent' => $agent
]);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
// Enforce: Master OR Self
if ($request->_agent->role !== 'master' && $request->_agent->id !== $id) {
return response()->json(['success' => false, 'message' => 'Unauthorized.'], 403);
}
$agent = Agent::find($id);
if (!$agent) {
return response()->json(['success' => false, 'message' => 'Agent not found'], 404);
}
$validated = $request->validate([
'agent_id' => 'sometimes|required|unique:agents,agent_id,' . $id,
'company_name' => 'sometimes|required|string',
'email' => 'sometimes|required|email',
'phone' => 'nullable|string',
'address' => 'nullable|string',
'subscription_duration' => 'sometimes|required|integer',
'is_active' => 'sometimes|required|boolean',
'ip_whitelist' => 'nullable|array',
'password' => 'sometimes|string|min:6', // [NEW]
]);
// Hash password if provided
if (isset($validated['password'])) {
$validated['password'] = Hash::make($validated['password']);
}
// Prevent Non-Master from changing role
if ($request->_agent->role !== 'master' && isset($validated['role'])) {
unset($validated['role']);
}
$agent->update($validated);
return response()->json([
'success' => true,
'message' => 'Agent updated successfully',
'agent' => $agent
]);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Request $request, string $id)
{
// Enforce Master Role
if ($request->_agent->role !== 'master') {
return response()->json(['success' => false, 'message' => 'Unauthorized. Master access required.'], 403);
}
$agent = Agent::find($id);
if (!$agent) {
return response()->json(['success' => false, 'message' => 'Agent not found'], 404);
}
// Check Dependencies: Staff
$staffCount = \App\Models\User::where('agent_id', $id)->count();
if ($staffCount > 0) {
return response()->json([
'success' => false,
'message' => "Cannot delete Agent. Found {$staffCount} staff members associated with this agent."
], 400);
}
// Check Dependencies: Customers
$customerCount = \App\Models\Customer::where('agent_id', $id)->count();
if ($customerCount > 0) {
return response()->json([
'success' => false,
'message' => "Cannot delete Agent. Found {$customerCount} customers associated with this agent."
], 400);
}
$agent->delete();
return response()->json([
'success' => true,
'message' => 'Agent deleted successfully'
]);
}
/**
* Regenerate API Secret Key
*/
public function regenerateKey(string $id)
{
$agent = Agent::find($id);
if (!$agent) {
return response()->json(['success' => false, 'message' => 'Agent not found'], 404);
}
$newKey = 'sk_live_' . Str::random(32);
$agent->api_secret_key = $newKey;
$agent->save();
return response()->json([
'success' => true,
'message' => 'API Key regenerated successfully',
'api_secret_key' => $newKey
]);
}
/**
* Agent Sign In
*/
/**
* Agent Sign In
*/
public function login(Request $request)
{
$validated = $request->validate([
'login_id' => 'required|string', // Can be email or company_name/username
'password' => 'required|string',
'agent_code' => 'nullable|string', // Agent Code (Target Context)
'secret_key' => 'nullable|string', // Optional
]);
// ---------------------------------------------------------
// 1. Try Authenticating as AGENT (Master/Admin)
// ---------------------------------------------------------
// Prepare Regex for insensitive search
$term = preg_quote($validated['login_id'], '/');
$pattern = '/^' . $term . '$/i';
// If agent_code is provided, we can scope the search, but typical Agent login
// uses agent_code as the login_id/username sometimes.
// Let's stick to existing logic for finding the Agent self-login first.
$agent = Agent::where('email', 'regexp', $pattern)
->orWhere('company_name', 'regexp', $pattern)
->orWhere('username', 'regexp', $pattern)
->orWhere('employee_id', 'regexp', $pattern)
// Also check agent_id directly if they put "AGT-XXX" in username field
->orWhere('agent_id', 'regexp', $pattern)
->first();
if ($agent && $agent->is_active) {
// [SECURITY FIX] Context Validation
// If request includes agent_code (Staff context), make sure found Agent MATCHES that code.
// If not match, it means we found a different Agent by name accident (e.g. User 'kartsandy' found Company 'Kartsandy')
// In that case, we MUST skip this block and fall through to Staff check.
$skipAgentLogin = false;
if (!empty($validated['agent_code'])) {
// If provided Code does NOT match found Agent's ID, then this is NOT the intended Agent login.
// It is likely a Staff login attempt where the username accidentally matches an Agent's company name.
if (strcasecmp($agent->agent_id, $validated['agent_code']) !== 0) {
$skipAgentLogin = true;
}
}
if (!$skipAgentLogin) {
// Verify Password
if (Hash::check($validated['password'], $agent->password) || $validated['password'] === $agent->password) {
// Update hash if legacy plain text
if ($validated['password'] === $agent->password) {
$agent->password = Hash::make($validated['password']);
$agent->save();
}
// Return Agent (Master) Response
// Master has access to ALL menus by default
return response()->json([
'success' => true,
'message' => 'Login successful (Master)',
'agent' => [
'id' => $agent->id,
'agent_id' => $agent->agent_id,
'company_name' => $agent->company_name,
'email' => $agent->email,
'role' => 'master',
'api_secret_key' => $agent->api_secret_key,
// Master gets wildcard access
'allowed_menu_ids' => ['*']
],
]);
}
}
}
// ---------------------------------------------------------
// 2. Try Authenticating as STAFF (User)
// ---------------------------------------------------------
// We need the Parent Agent context first.
// Use 'agent_code' from request, or fallback to trying to find agent from login_id (unlikely)
$parentAgent = null;
if (!empty($validated['agent_code'])) {
$parentAgent = Agent::where('agent_id', 'regexp', '/^' . preg_quote($validated['agent_code'], '/') . '$/i')->first();
}
if ($parentAgent) {
// Find User belonging to this Agent
$user = \App\Models\User::where('agent_id', $parentAgent->id)
->where(function ($q) use ($pattern) {
$q->where('email', 'regexp', $pattern)
->orWhere('employee_id', 'regexp', $pattern);
})
->where('is_active', true)
->first();
if ($user && (Hash::check($validated['password'], $user->password) || $validated['password'] === $user->password)) {
// Update legacy password if needed (though Users usually created with hash)
// Get Group/Role Permissions
$group = $user->group;
$menuIds = $group ? $group->allowed_menu_ids : [];
return response()->json([
'success' => true,
'message' => 'Login successful (Staff)',
'agent' => [
// Map User fields to the structure frontend expects (simulating Agent object)
'id' => $user->id,
'agent_id' => $parentAgent->agent_id, // Keep parent agent ID for context
'company_name' => $parentAgent->company_name,
'name' => $user->name,
'email' => $user->email,
'role' => 'staff', // Fixed role
'user_group_name' => $group ? $group->name : null,
'allowed_menu_ids' => $menuIds,
'is_staff' => true,
// Staff don't usually use secret key, but we can pass parent's if needed for API calls
'api_secret_key' => $parentAgent->api_secret_key
],
]);
}
}
return response()->json(['success' => false, 'message' => 'Invalid credentials or inactive account.'], 401);
}
/**
* External Agent Sign In (For External Backoffice Integration)
*
* This endpoint allows external systems to authenticate and get agent access
* using only the API Secret Key. No password required.
*
* Use Case: Open agent backoffice from another system without manual login
*/
public function externalSignin(Request $request)
{
$validated = $request->validate([
'secret_key' => 'required|string',
]);
// Find agent by secret key
$agent = Agent::where('api_secret_key', $validated['secret_key'])->first();
// 1. Check if agent exists
if (!$agent) {
return response()->json([
'success' => false,
'message' => 'Invalid API Secret Key.',
'error_code' => 'INVALID_KEY'
], 401);
}
// 2. Check if active
if (!$agent->is_active) {
return response()->json([
'success' => false,
'message' => 'Agent account is inactive.',
'error_code' => 'INACTIVE_AGENT'
], 403);
}
// 3. Generate a temporary session token (valid for 24 hours)
$sessionToken = base64_encode(json_encode([
'agent_id' => (string) $agent->_id,
'secret_key' => $agent->api_secret_key,
'issued_at' => now()->toISOString(),
'expires_at' => now()->addHours(24)->toISOString(),
]));
// 4. Build the redirect URL for the backoffice
$backofficeUrl = config('app.frontend_url', env('FRONTEND_URL', 'http://localhost:3000'));
$autoLoginUrl = $backofficeUrl . '/auto-login?token=' . urlencode($sessionToken);
// 5. Success - Return agent info + session data
return response()->json([
'success' => true,
'message' => 'External sign-in successful',
'data' => [
'agent' => [
'id' => (string) $agent->_id,
'agent_id' => $agent->agent_id,
'company_name' => $agent->company_name,
'email' => $agent->email,
'role' => $agent->role ?? 'agent',
],
'session' => [
'token' => $sessionToken,
'secret_key' => $agent->api_secret_key,
'expires_at' => now()->addHours(24)->toISOString(),
],
'urls' => [
'backoffice' => $backofficeUrl,
'auto_login' => $autoLoginUrl,
]
]
]);
}
/**
* Validate Session Token (For Auto-Login)
*/
public function validateSession(Request $request)
{
$validated = $request->validate([
'token' => 'required|string',
]);
try {
// Decode token
$decoded = json_decode(base64_decode($validated['token']), true);
if (!$decoded || !isset($decoded['agent_id']) || !isset($decoded['expires_at'])) {
return response()->json([
'success' => false,
'message' => 'Invalid session token format.',
'error_code' => 'INVALID_TOKEN'
], 400);
}
// Check expiration
$expiresAt = \Carbon\Carbon::parse($decoded['expires_at']);
if ($expiresAt->isPast()) {
return response()->json([
'success' => false,
'message' => 'Session token has expired.',
'error_code' => 'TOKEN_EXPIRED'
], 401);
}
// Find agent
$agent = Agent::find($decoded['agent_id']);
if (!$agent) {
return response()->json([
'success' => false,
'message' => 'Agent not found.',
'error_code' => 'AGENT_NOT_FOUND'
], 404);
}
// Verify secret key matches
if ($agent->api_secret_key !== $decoded['secret_key']) {
return response()->json([
'success' => false,
'message' => 'Session token is no longer valid.',
'error_code' => 'KEY_MISMATCH'
], 401);
}
// Check if active
if (!$agent->is_active) {
return response()->json([
'success' => false,
'message' => 'Agent account is inactive.',
'error_code' => 'INACTIVE_AGENT'
], 403);
}
// Success - return full agent info for auto-login
return response()->json([
'success' => true,
'message' => 'Session is valid',
'agent' => $agent,
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to validate token.',
'error_code' => 'VALIDATION_ERROR'
], 500);
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\SalesPlan;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class PlanningController extends Controller
{
/**
* GET /api/v1/plans
*/
public function index(Request $request): JsonResponse
{
$query = SalesPlan::query();
if ($request->has('date')) {
$query->whereDate('date', $request->date);
}
$plans = $query->get();
return response()->json([
'success' => true,
'data' => $plans
]);
}
/**
* POST /api/v1/plans
*/
public function store(Request $request): JsonResponse
{
return response()->json([
'success' => true,
'message' => 'Plan created (Sandbox simulation)',
'id' => 'PLAN-' . uniqid()
]);
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\SalesRoute;
use App\Models\Waypoint;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Carbon\Carbon;
class TrackingController extends Controller
{
/**
* GET /api/v1/routes
* Get routes for the authenticated agent's scope (currently global)
*/
public function index(Request $request): JsonResponse
{
$query = SalesRoute::with('user:id,employee_id,name,color');
if ($request->has('date')) {
$query->whereDate('date', $request->date);
}
$routes = $query->orderBy('date', 'desc')->get();
$formattedRoutes = $routes->map(function ($route) {
return [
'id' => (string) $route->id,
'salesId' => $route->user->employee_id ?? 'UNKNOWN',
'salesName' => $route->user->name ?? 'Unknown',
'date' => $route->date->format('Y-m-d'),
'waypoints' => $route->waypoints->map(function ($wp) {
return [
'lat' => (float) $wp->latitude,
'lng' => (float) $wp->longitude,
'time' => Carbon::parse($wp->recorded_at)->format('H:i'),
'type' => $wp->type,
];
}),
];
});
return response()->json([
'success' => true,
'data' => $formattedRoutes
]);
}
/**
* POST /api/v1/routes/waypoints
* Add waypoint to active route
*/
public function storeWaypoint(Request $request): JsonResponse
{
$request->validate([
'lat' => 'required|numeric',
'lng' => 'required|numeric',
'employee_id' => 'required|string', // Required to identify who is moving
'type' => 'nullable|in:gps,visit,checkin,checkout'
]);
// Find user
$user = User::where('employee_id', $request->employee_id)->first();
if (!$user) {
return response()->json(['success' => false, 'error' => 'User (Employee) not found'], 404);
}
$today = Carbon::today();
// Get or create today's route
$route = SalesRoute::firstOrCreate(
['user_id' => $user->id, 'date' => $today],
['status' => 'active', 'started_at' => now()]
);
// Create waypoint
$waypoint = Waypoint::create([
'sales_route_id' => $route->id,
'type' => $request->type ?? 'gps',
'latitude' => $request->lat,
'longitude' => $request->lng,
'recorded_at' => now(),
'location_name' => 'API Integration',
]);
return response()->json([
'success' => true,
'message' => 'Waypoint recorded successfully',
'data' => [
'id' => $waypoint->id,
'lat' => $waypoint->latitude,
'lng' => $waypoint->longitude,
'recorded_at' => $waypoint->recorded_at
]
]);
}
}

View File

@ -15,7 +15,15 @@ class CustomerController extends Controller
*/
public function index(Request $request): JsonResponse
{
$query = Customer::with('sales:id,employee_id,name,color');
// Eager load sales, and AGENT
// Note: sales selection fields might need adjustment if using MongoDB
$query = Customer::with(['sales', 'agent']);
// STRICT ISOLATION: Only show customers belonging to this Agent
// UNLESS the agent is MASTER
if ($request->_agent && $request->_agent->role !== 'master') {
$query->where('agent_id', $request->_agent->id);
}
// Search by name, owner, phone, city
if ($request->has('q') && !empty($request->q)) {
@ -163,6 +171,7 @@ class CustomerController extends Controller
'latitude' => (float) $c->latitude,
'longitude' => (float) $c->longitude,
'city' => $c->city,
'agent_name' => $c->agent ? $c->agent->company_name : 'Unknown Agent', // Added for Frontend
'pic_sales_id' => $c->pic_sales_id ? (string) $c->pic_sales_id : null,
'pic_sales_name' => $c->sales ? $c->sales->name : null,
'pic_sales_employee_id' => $c->sales ? $c->sales->employee_id : null,

View File

@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers;
use App\Models\AppMenu;
use Illuminate\Http\Request;
class MenuController extends Controller
{
/**
* GET /api/menus
* List all menus hierarchically
*/
public function index(Request $request)
{
// Fetch all active menus ordered by 'order'
$menus = AppMenu::where('is_active', true)
->orderBy('order', 'asc')
->get();
// If 'flat' param provided, return as flat list (for permission checkboxes)
if ($request->has('flat')) {
return response()->json(['success' => true, 'menus' => $menus]);
}
// Build Hierarchy (Parent -> Children)
$hierarchy = $this->buildTree($menus);
return response()->json([
'success' => true,
'menus' => $hierarchy
]);
}
/**
* POST /api/menus
* Create new menu
*/
public function store(Request $request)
{
// Usually restricted to Super Admin
$validated = $request->validate([
'label' => 'required|string',
'key' => 'required|string|unique:app_menus,key',
'icon' => 'nullable|string',
'route' => 'nullable|string',
'parent_id' => 'nullable|string',
'order' => 'integer',
]);
$menu = AppMenu::create(array_merge($validated, ['is_active' => true]));
return response()->json(['success' => true, 'menu' => $menu]);
}
/**
* PUT /api/menus/{id}
*/
public function update(Request $request, $id)
{
$menu = AppMenu::find($id);
if (!$menu)
return response()->json(['success' => false, 'message' => 'Not found'], 404);
$menu->update($request->all());
return response()->json(['success' => true, 'menu' => $menu]);
}
/**
* DELETE /api/menus/{id}
*/
public function destroy($id)
{
$menu = AppMenu::find($id);
if (!$menu)
return response()->json(['success' => false, 'message' => 'Not found'], 404);
$menu->delete();
return response()->json(['success' => true, 'message' => 'Deleted']);
}
// Helper to build tree
private function buildTree($elements, $parentId = null)
{
$branch = [];
foreach ($elements as $element) {
if ($element->parent_id == $parentId) {
$children = $this->buildTree($elements, $element->id);
if ($children) {
$element->children = $children;
}
$branch[] = $element;
}
}
return $branch;
}
}

View File

@ -9,20 +9,14 @@ class MerchantController extends Controller
{
/**
* GET /api/merchants
* Get all merchants with optional filters
* Get all customers (aliased as merchants for frontend compatibility)
*/
public function index(Request $request): JsonResponse
{
// Read from merchants.json file (like the original Express.js backend)
$jsonPath = base_path('merchants.json');
// Switch to Customer model
$query = \App\Models\Customer::query();
if (!file_exists($jsonPath)) {
return response()->json([]);
}
$merchants = json_decode(file_get_contents($jsonPath), true);
// Filter by bbox if provided
// Filter by bbox
if ($request->has('bbox') && !empty($request->bbox)) {
$bbox = explode(',', $request->bbox);
if (count($bbox) === 4) {
@ -31,30 +25,34 @@ class MerchantController extends Controller
$maxLng = (float) $bbox[2];
$maxLat = (float) $bbox[3];
$merchants = array_filter($merchants, function ($m) use ($minLat, $maxLat, $minLng, $maxLng) {
return $m['latitude'] >= $minLat &&
$m['latitude'] <= $maxLat &&
$m['longitude'] >= $minLng &&
$m['longitude'] <= $maxLng;
});
$query->where('latitude', '>=', $minLat)
->where('latitude', '<=', $maxLat)
->where('longitude', '>=', $minLng)
->where('longitude', '<=', $maxLng);
}
}
// Filter by categories if provided
// Filter by categories
if ($request->has('categories') && !empty($request->categories)) {
$categories = explode(',', $request->categories);
$merchants = array_filter($merchants, function ($m) use ($categories) {
return in_array($m['category'], $categories);
});
$query->whereIn('category', $categories);
}
$customers = $query->get();
// return formatted data
$data = $customers->map(function ($m) {
return [
'id' => $m->id,
'name' => $m->name,
'category' => $m->category ?? 'other',
'latitude' => (float) $m->latitude,
'longitude' => (float) $m->longitude,
'city' => $m->city ?? '',
'address' => $m->address ?? ''
];
});
// Return plain values
return response()->json(array_map(function ($m) {
$m['latitude'] = (float) $m['latitude'];
$m['longitude'] = (float) $m['longitude'];
return $m;
}, array_values($merchants)));
return response()->json($data);
}
}

View File

@ -14,10 +14,36 @@ class StaffController extends Controller
* GET /api/staff
* Get all staff (role = sales)
*/
public function index(): JsonResponse
public function index(Request $request): JsonResponse
{
$staff = User::orderBy('created_at', 'desc')
->get(['id', 'employee_id', 'name', 'email', 'phone', 'color', 'role', 'is_active']);
$query = User::with('agent:id,company_name,agent_id');
// Isolation:
// 1. Master agents see ALL staff
// 2. Regular agents or Staff users see only their OWN agent's staff
$requestAgent = $request->_agent;
// If the logged-in entity is NOT 'master', filter by their Agent ID.
// For Staff Login, the middleware attaches the Parent Agent, but we still need to filter by it.
// For Agent Login (Manager), they also see only their staff.
if ($requestAgent->role !== 'master') {
// For Staff/Regular Agent, use the Attached Agent ID filter
$query->where('agent_id', $requestAgent->id);
}
// Search functionality
if ($request->has('q') && !empty($request->q)) {
$term = $request->q;
$pattern = '/' . preg_quote($term, '/') . '/i';
$query->where(function ($q) use ($pattern) {
$q->where('name', 'regexp', $pattern)
->orWhere('email', 'regexp', $pattern)
->orWhere('employee_id', 'regexp', $pattern);
});
}
$staff = $query->orderBy('created_at', 'desc')
->get(['id', 'employee_id', 'name', 'email', 'phone', 'color', 'role', 'is_active', 'agent_id', 'user_group_id']);
return response()->json([
'success' => true,
@ -39,8 +65,19 @@ class StaffController extends Controller
'phone' => 'nullable|string',
'color' => 'nullable|string',
'role' => 'nullable|string|in:sales,admin,manager', // Default to sales if not provided
'agent_id' => [
$request->_agent->role === 'master' ? 'required' : 'nullable',
'string',
'exists:agents,_id'
],
]);
// Auto-assign Agent ID for non-master
$agentId = $request->agent_id;
if ($request->_agent->role !== 'master') {
$agentId = $request->_agent->id;
}
$user = User::create([
'employee_id' => $request->employee_id,
'name' => $request->name,
@ -50,8 +87,13 @@ class StaffController extends Controller
'color' => $request->color ?? '#3B82F6', // Default blue
'role' => $request->role ?? 'sales',
'is_active' => true,
'user_group_id' => $request->user_group_id, // [NEW] Link to User Group
'agent_id' => $agentId, // [NEW] Link to Agent
]);
// Load the agent relationship
$user->load('agent:id,company_name,agent_id');
return response()->json([
'success' => true,
'message' => 'Staff created successfully',
@ -76,9 +118,15 @@ class StaffController extends Controller
'name' => 'nullable|string',
'password' => 'nullable|string|min:6',
'color' => 'nullable|string',
'agent_id' => 'nullable|string|exists:agents,_id', // [NEW] Agent ID validation
]);
$data = $request->only(['employee_id', 'name', 'email', 'phone', 'color', 'role', 'is_active']);
$data = $request->only(['employee_id', 'name', 'email', 'phone', 'color', 'role', 'is_active', 'agent_id', 'user_group_id']);
// Prevent non-master from changing agent
if ($request->_agent->role !== 'master' && isset($data['agent_id'])) {
unset($data['agent_id']);
}
// Update password only if provided
if ($request->filled('password')) {
@ -87,6 +135,9 @@ class StaffController extends Controller
$user->update($data);
// Load the agent relationship
$user->load('agent:id,company_name,agent_id');
return response()->json([
'success' => true,
'message' => 'Staff updated successfully',

View File

@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers;
use App\Models\UserGroup;
use Illuminate\Http\Request;
class UserGroupController extends Controller
{
/**
* GET /api/user-groups
* List user groups for the current agent
*/
public function index(Request $request)
{
$agent = $request->_agent;
$query = UserGroup::query();
if ($agent->role === 'master') {
// Master sees all groups, or filter by specific agent_id if provided
if ($request->has('agent_id')) {
$query->where(function ($q) use ($request) {
$q->where('agent_id', $request->agent_id)
->orWhere('is_system', true); // Always show system roles
});
}
} else {
// Regular agent sees their own groups + system groups
$query->where(function ($q) use ($agent) {
$q->where('agent_id', $agent->id)
->orWhere('is_system', true);
});
}
$groups = $query->orderBy('created_at', 'desc')->get();
return response()->json([
'success' => true,
'groups' => $groups
]);
}
/**
* POST /api/user-groups
* Create new user group
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string',
'description' => 'nullable|string',
'allowed_menu_ids' => 'array', // List of Menu IDs
'code' => 'nullable|string'
]);
// Auto assign Agent ID
$agentId = $request->_agent->role === 'master' && $request->has('agent_id')
? $request->agent_id
: $request->_agent->id;
$group = UserGroup::create([
'name' => $validated['name'],
'code' => $validated['code'] ?? strtoupper(str_replace(' ', '_', $validated['name'])),
'description' => $validated['description'] ?? '',
'agent_id' => $agentId,
'allowed_menu_ids' => $validated['allowed_menu_ids'] ?? [],
'is_active' => true,
'is_system' => false
]);
return response()->json(['success' => true, 'group' => $group]);
}
/**
* PUT /api/user-groups/{id}
* Update permissions/name
*/
public function update(Request $request, $id)
{
$group = UserGroup::find($id);
if (!$group)
return response()->json(['success' => false, 'message' => 'Not found'], 404);
// Security: Prevent editing System Groups if strict, but maybe allow permission edit?
// Usually system groups are fixed.
// if ($group->is_system && $request->_agent->role !== 'master') {
// return response()->json(['success' => false, 'message' => 'Cannot edit system groups'], 403);
// }
// For now, allow editing permissions even for system groups? Or prevent?
// Let's assume User creates their own groups.
// Check ownership
if (!$group->is_system && $group->agent_id !== $request->_agent->id && $request->_agent->role !== 'master') {
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
}
$group->update($request->only(['name', 'description', 'allowed_menu_ids', 'is_active']));
return response()->json(['success' => true, 'group' => $group]);
}
/**
* DELETE /api/user-groups/{id}
*/
public function destroy($id, Request $request)
{
$group = UserGroup::find($id);
if (!$group)
return response()->json(['success' => false, 'message' => 'Not found'], 404);
if ($group->is_system) {
return response()->json(['success' => false, 'message' => 'Cannot delete system groups'], 403);
}
// Check ownership
if ($group->agent_id !== $request->_agent->id && $request->_agent->role !== 'master') {
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
}
$group->delete();
return response()->json(['success' => true, 'message' => 'Deleted']);
}
}