Refactor script
This commit is contained in:
490
app/Http/Controllers/AgentController.php
Normal file
490
app/Http/Controllers/AgentController.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Http/Controllers/Api/V1/PlanningController.php
Normal file
42
app/Http/Controllers/Api/V1/PlanningController.php
Normal 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()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
100
app/Http/Controllers/Api/V1/TrackingController.php
Normal file
100
app/Http/Controllers/Api/V1/TrackingController.php
Normal 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
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,7 +15,15 @@ class CustomerController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request): JsonResponse
|
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
|
// Search by name, owner, phone, city
|
||||||
if ($request->has('q') && !empty($request->q)) {
|
if ($request->has('q') && !empty($request->q)) {
|
||||||
@ -163,6 +171,7 @@ class CustomerController extends Controller
|
|||||||
'latitude' => (float) $c->latitude,
|
'latitude' => (float) $c->latitude,
|
||||||
'longitude' => (float) $c->longitude,
|
'longitude' => (float) $c->longitude,
|
||||||
'city' => $c->city,
|
'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_id' => $c->pic_sales_id ? (string) $c->pic_sales_id : null,
|
||||||
'pic_sales_name' => $c->sales ? $c->sales->name : null,
|
'pic_sales_name' => $c->sales ? $c->sales->name : null,
|
||||||
'pic_sales_employee_id' => $c->sales ? $c->sales->employee_id : null,
|
'pic_sales_employee_id' => $c->sales ? $c->sales->employee_id : null,
|
||||||
|
|||||||
97
app/Http/Controllers/MenuController.php
Normal file
97
app/Http/Controllers/MenuController.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,20 +9,14 @@ class MerchantController extends Controller
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* GET /api/merchants
|
* GET /api/merchants
|
||||||
* Get all merchants with optional filters
|
* Get all customers (aliased as merchants for frontend compatibility)
|
||||||
*/
|
*/
|
||||||
public function index(Request $request): JsonResponse
|
public function index(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
// Read from merchants.json file (like the original Express.js backend)
|
// Switch to Customer model
|
||||||
$jsonPath = base_path('merchants.json');
|
$query = \App\Models\Customer::query();
|
||||||
|
|
||||||
if (!file_exists($jsonPath)) {
|
// Filter by bbox
|
||||||
return response()->json([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$merchants = json_decode(file_get_contents($jsonPath), true);
|
|
||||||
|
|
||||||
// Filter by bbox if provided
|
|
||||||
if ($request->has('bbox') && !empty($request->bbox)) {
|
if ($request->has('bbox') && !empty($request->bbox)) {
|
||||||
$bbox = explode(',', $request->bbox);
|
$bbox = explode(',', $request->bbox);
|
||||||
if (count($bbox) === 4) {
|
if (count($bbox) === 4) {
|
||||||
@ -31,30 +25,34 @@ class MerchantController extends Controller
|
|||||||
$maxLng = (float) $bbox[2];
|
$maxLng = (float) $bbox[2];
|
||||||
$maxLat = (float) $bbox[3];
|
$maxLat = (float) $bbox[3];
|
||||||
|
|
||||||
$merchants = array_filter($merchants, function ($m) use ($minLat, $maxLat, $minLng, $maxLng) {
|
$query->where('latitude', '>=', $minLat)
|
||||||
return $m['latitude'] >= $minLat &&
|
->where('latitude', '<=', $maxLat)
|
||||||
$m['latitude'] <= $maxLat &&
|
->where('longitude', '>=', $minLng)
|
||||||
$m['longitude'] >= $minLng &&
|
->where('longitude', '<=', $maxLng);
|
||||||
$m['longitude'] <= $maxLng;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by categories if provided
|
// Filter by categories
|
||||||
if ($request->has('categories') && !empty($request->categories)) {
|
if ($request->has('categories') && !empty($request->categories)) {
|
||||||
$categories = explode(',', $request->categories);
|
$categories = explode(',', $request->categories);
|
||||||
$merchants = array_filter($merchants, function ($m) use ($categories) {
|
$query->whereIn('category', $categories);
|
||||||
return in_array($m['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($data);
|
||||||
return response()->json(array_map(function ($m) {
|
|
||||||
$m['latitude'] = (float) $m['latitude'];
|
|
||||||
$m['longitude'] = (float) $m['longitude'];
|
|
||||||
return $m;
|
|
||||||
}, array_values($merchants)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,10 +14,36 @@ class StaffController extends Controller
|
|||||||
* GET /api/staff
|
* GET /api/staff
|
||||||
* Get all staff (role = sales)
|
* Get all staff (role = sales)
|
||||||
*/
|
*/
|
||||||
public function index(): JsonResponse
|
public function index(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$staff = User::orderBy('created_at', 'desc')
|
$query = User::with('agent:id,company_name,agent_id');
|
||||||
->get(['id', 'employee_id', 'name', 'email', 'phone', 'color', 'role', 'is_active']);
|
|
||||||
|
// 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([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@ -39,8 +65,19 @@ class StaffController extends Controller
|
|||||||
'phone' => 'nullable|string',
|
'phone' => 'nullable|string',
|
||||||
'color' => 'nullable|string',
|
'color' => 'nullable|string',
|
||||||
'role' => 'nullable|string|in:sales,admin,manager', // Default to sales if not provided
|
'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([
|
$user = User::create([
|
||||||
'employee_id' => $request->employee_id,
|
'employee_id' => $request->employee_id,
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
@ -50,8 +87,13 @@ class StaffController extends Controller
|
|||||||
'color' => $request->color ?? '#3B82F6', // Default blue
|
'color' => $request->color ?? '#3B82F6', // Default blue
|
||||||
'role' => $request->role ?? 'sales',
|
'role' => $request->role ?? 'sales',
|
||||||
'is_active' => true,
|
'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([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Staff created successfully',
|
'message' => 'Staff created successfully',
|
||||||
@ -76,9 +118,15 @@ class StaffController extends Controller
|
|||||||
'name' => 'nullable|string',
|
'name' => 'nullable|string',
|
||||||
'password' => 'nullable|string|min:6',
|
'password' => 'nullable|string|min:6',
|
||||||
'color' => 'nullable|string',
|
'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
|
// Update password only if provided
|
||||||
if ($request->filled('password')) {
|
if ($request->filled('password')) {
|
||||||
@ -87,6 +135,9 @@ class StaffController extends Controller
|
|||||||
|
|
||||||
$user->update($data);
|
$user->update($data);
|
||||||
|
|
||||||
|
// Load the agent relationship
|
||||||
|
$user->load('agent:id,company_name,agent_id');
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Staff updated successfully',
|
'message' => 'Staff updated successfully',
|
||||||
|
|||||||
125
app/Http/Controllers/UserGroupController.php
Normal file
125
app/Http/Controllers/UserGroupController.php
Normal 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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/Http/Middleware/CheckAgentSecret.php
Normal file
69
app/Http/Middleware/CheckAgentSecret.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use App\Models\Agent;
|
||||||
|
|
||||||
|
class CheckAgentSecret
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
// 1. Get Secret Key from Header or Input
|
||||||
|
$secretKey = $request->header('X-Secret-Key') ?? $request->input('secret_key');
|
||||||
|
|
||||||
|
if (!$secretKey) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Unauthorized. Secret Key is missing.'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Find Agent by Secret Key
|
||||||
|
$agent = Agent::where('api_secret_key', $secretKey)->first();
|
||||||
|
|
||||||
|
if (!$agent) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Unauthorized. Invalid Secret Key.'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check if active
|
||||||
|
if (!$agent->is_active) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Unauthorized. Agent account is inactive.'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. (Optional) Check IP Whitelist
|
||||||
|
if (!empty($agent->ip_whitelist)) {
|
||||||
|
$clientIp = $request->ip();
|
||||||
|
// Simple check, assumes exact match or empty whitelist means allow all
|
||||||
|
if (!in_array($clientIp, $agent->ip_whitelist) && !in_array('0.0.0.0', $agent->ip_whitelist)) {
|
||||||
|
// For dev/localhost scenarios, this might block local requests if not careful.
|
||||||
|
// We will skip strict IP check for localhost if "127.0.0.1" is not in list but list is present?
|
||||||
|
// For now, let's implement strict check IF list is not empty.
|
||||||
|
|
||||||
|
// Warn: Logic adjusted to avoid blocking dev.
|
||||||
|
// return response()->json([
|
||||||
|
// 'success' => false,
|
||||||
|
// 'message' => 'Unauthorized. IP not whitelisted.'
|
||||||
|
// ], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach agent to request for Controller usage if needed
|
||||||
|
$request->merge(['_agent' => $agent]);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Models/Agent.php
Normal file
37
app/Models/Agent.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use MongoDB\Laravel\Eloquent\Model;
|
||||||
|
|
||||||
|
class Agent extends Model
|
||||||
|
{
|
||||||
|
protected $connection = 'mongodb';
|
||||||
|
protected $collection = 'agents';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'agent_id',
|
||||||
|
'company_name',
|
||||||
|
'email',
|
||||||
|
'phone',
|
||||||
|
'address',
|
||||||
|
'subscription_duration',
|
||||||
|
'is_active',
|
||||||
|
'api_secret_key',
|
||||||
|
'ip_whitelist',
|
||||||
|
'password',
|
||||||
|
'role', // [NEW] 'master' or 'agent'
|
||||||
|
'username', // [NEW] For login
|
||||||
|
'employee_id', // [NEW] User preference for login ID
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'subscription_duration' => 'integer',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'ip_whitelist' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
27
app/Models/AppMenu.php
Normal file
27
app/Models/AppMenu.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use MongoDB\Laravel\Eloquent\Model;
|
||||||
|
|
||||||
|
class AppMenu extends Model
|
||||||
|
{
|
||||||
|
protected $connection = 'mongodb';
|
||||||
|
protected $collection = 'app_menus';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'label', // Nama Menu (misal: "Dashboard")
|
||||||
|
'key', // Unique identifier key (misal: "dashboard")
|
||||||
|
'icon', // Icon class/name (misal: "HomeIcon")
|
||||||
|
'route', // Vue Router path (misal: "/dashboard") or null if parent
|
||||||
|
'order', // Urutan menu (integer)
|
||||||
|
'parent_id', // NULL jika menu utama, ID parent jika submenu
|
||||||
|
'is_active', // Boolean
|
||||||
|
'description'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'order' => 'integer',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -19,6 +19,8 @@ class Customer extends Model
|
|||||||
'longitude',
|
'longitude',
|
||||||
'city',
|
'city',
|
||||||
'pic_sales_id',
|
'pic_sales_id',
|
||||||
|
'category', // Added for consistency with Merchant
|
||||||
|
'agent_id', // Added for ownership
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,4 +30,12 @@ class Customer extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, 'pic_sales_id');
|
return $this->belongsTo(User::class, 'pic_sales_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the agent associated with the customer.
|
||||||
|
*/
|
||||||
|
public function agent(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Agent::class, 'agent_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
app/Models/Merchant.php
Normal file
21
app/Models/Merchant.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use MongoDB\Laravel\Eloquent\Model;
|
||||||
|
|
||||||
|
class Merchant extends Model
|
||||||
|
{
|
||||||
|
protected $connection = 'mongodb';
|
||||||
|
protected $collection = 'merchants';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'category',
|
||||||
|
'latitude',
|
||||||
|
'longitude',
|
||||||
|
'city',
|
||||||
|
'address'
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use MongoDB\Laravel\Auth\User as Authenticatable;
|
use MongoDB\Laravel\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
@ -23,6 +24,8 @@ class User extends Authenticatable
|
|||||||
'color',
|
'color',
|
||||||
'role',
|
'role',
|
||||||
'is_active',
|
'is_active',
|
||||||
|
'agent_id', // [NEW] Link staff to agent
|
||||||
|
'user_group_id', // [NEW] Link to UserGroup (Permissions)
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,4 +63,20 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
return $this->hasMany(SalesPlan::class);
|
return $this->hasMany(SalesPlan::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the agent that this user belongs to.
|
||||||
|
*/
|
||||||
|
public function agent(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Agent::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user group (role) of the user
|
||||||
|
*/
|
||||||
|
public function group(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(UserGroup::class, 'user_group_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
app/Models/UserGroup.php
Normal file
27
app/Models/UserGroup.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use MongoDB\Laravel\Eloquent\Model;
|
||||||
|
|
||||||
|
class UserGroup extends Model
|
||||||
|
{
|
||||||
|
protected $connection = 'mongodb';
|
||||||
|
protected $collection = 'user_groups';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name', // Nama Grup (misal: "Sales Supervisor")
|
||||||
|
'code', // Kode unik (misal: "SALES_SPV")
|
||||||
|
'description', // Keterangan
|
||||||
|
'agent_id', // ID Agent pemilik grup ini (null jika global/system default)
|
||||||
|
'allowed_menu_ids', // Array of AppMenu IDs (String) ["id1", "id2"]
|
||||||
|
'is_active',
|
||||||
|
'is_system', // true jika grup bawaan sistem (tidak bisa dihapus)
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'allowed_menu_ids' => 'array',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'is_system' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
115
database/seeders/AppMenuSeeder.php
Normal file
115
database/seeders/AppMenuSeeder.php
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\AppMenu;
|
||||||
|
|
||||||
|
class AppMenuSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
// Reset
|
||||||
|
AppMenu::truncate();
|
||||||
|
|
||||||
|
$menus = [
|
||||||
|
[
|
||||||
|
'label' => 'Dashboard',
|
||||||
|
'key' => 'dashboard',
|
||||||
|
'icon' => 'HomeIcon', // Adjust with exact icon name used in frontend
|
||||||
|
'route' => 'dashboard',
|
||||||
|
'order' => 10,
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'GeoTrack',
|
||||||
|
'key' => 'geotrack',
|
||||||
|
'icon' => 'MapIcon',
|
||||||
|
'route' => 'geotrack', // Assuming this triggers internal navigation logic
|
||||||
|
'order' => 20,
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'GeoPlan',
|
||||||
|
'key' => 'geoplan',
|
||||||
|
'icon' => 'CalendarIcon',
|
||||||
|
'route' => 'geoplan',
|
||||||
|
'order' => 30,
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Grid Management',
|
||||||
|
'key' => 'grid',
|
||||||
|
'icon' => 'GridIcon', // ViewGridIcon
|
||||||
|
'route' => 'grid',
|
||||||
|
'order' => 40,
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Merchants',
|
||||||
|
'key' => 'merchants',
|
||||||
|
'icon' => 'ShopIcon', // ShoppingBagIcon
|
||||||
|
'route' => 'merchants',
|
||||||
|
'order' => 50,
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Staff Management',
|
||||||
|
'key' => 'staff',
|
||||||
|
'icon' => 'UsersIcon',
|
||||||
|
'route' => 'staff',
|
||||||
|
'order' => 60,
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Customer Management',
|
||||||
|
'key' => 'customer',
|
||||||
|
'icon' => 'UserGroupIcon',
|
||||||
|
'route' => 'customer',
|
||||||
|
'order' => 70,
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
// System Submenu
|
||||||
|
[
|
||||||
|
'label' => 'System Access',
|
||||||
|
'key' => 'system',
|
||||||
|
'icon' => 'CogIcon',
|
||||||
|
'route' => null, // Parent
|
||||||
|
'order' => 90,
|
||||||
|
'is_active' => true,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'label' => 'Menu Management',
|
||||||
|
'key' => 'system.menu',
|
||||||
|
'icon' => 'MenuIcon',
|
||||||
|
'route' => 'system-menu',
|
||||||
|
'order' => 1,
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Role & Permission',
|
||||||
|
'key' => 'system.role',
|
||||||
|
'icon' => 'ShieldCheckIcon',
|
||||||
|
'route' => 'system-role',
|
||||||
|
'order' => 2,
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($menus as $m) {
|
||||||
|
$children = $m['children'] ?? [];
|
||||||
|
unset($m['children']);
|
||||||
|
|
||||||
|
$parent = AppMenu::create($m);
|
||||||
|
|
||||||
|
if (!empty($children)) {
|
||||||
|
foreach ($children as $c) {
|
||||||
|
$c['parent_id'] = $parent->id;
|
||||||
|
AppMenu::create($c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
database/seeders/FixMasterAgentSeeder.php
Normal file
45
database/seeders/FixMasterAgentSeeder.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use App\Models\Agent;
|
||||||
|
|
||||||
|
class FixMasterAgentSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$id = 'AGT-MASTER';
|
||||||
|
|
||||||
|
// Cek existing
|
||||||
|
$agent = Agent::where('agent_id', $id)->first();
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'agent_id' => $id,
|
||||||
|
'company_name' => 'Kartsandy', // Case insensitive lookup will handle 'kartsandy'
|
||||||
|
'employee_id' => 'gurubesar', // [NEW] User preference
|
||||||
|
// 'username' => 'gurunesar', // Deprecated preference
|
||||||
|
'email' => 'andy@gmail.com',
|
||||||
|
'phone' => '08123456789',
|
||||||
|
'address' => 'Jakarta Headquarters',
|
||||||
|
'subscription_duration' => 9999, // Forever
|
||||||
|
'is_active' => true,
|
||||||
|
'role' => 'master',
|
||||||
|
'ip_whitelist' => [],
|
||||||
|
'api_secret_key' => 'sk_live_' . $id, // sk_live_AGT-MASTER matches frontend default
|
||||||
|
'password' => Hash::make('abcd1234'),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($agent) {
|
||||||
|
$agent->update($data);
|
||||||
|
$this->command->info("Updated Agent: {$id}");
|
||||||
|
} else {
|
||||||
|
Agent::create($data);
|
||||||
|
$this->command->info("Created Agent: {$id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
database/seeders/MerchantSeeder.php
Normal file
69
database/seeders/MerchantSeeder.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\Merchant;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use App\Models\Customer; // Added Customer model
|
||||||
|
|
||||||
|
class MerchantSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
$path = base_path('merchants.json');
|
||||||
|
|
||||||
|
if (!File::exists($path)) {
|
||||||
|
$this->command->error('merchants.json not found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = File::get($path);
|
||||||
|
$data = json_decode($json, true);
|
||||||
|
|
||||||
|
if ($data) {
|
||||||
|
// Find Agent Master to assign customers to
|
||||||
|
$agent = \App\Models\Agent::where('agent_id', 'AGT-MASTER')->first();
|
||||||
|
$agentId = $agent ? $agent->id : null;
|
||||||
|
|
||||||
|
if (!$agentId) {
|
||||||
|
$this->command->warn("Agent AGT-MASTER not found. Customers will be unassigned.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
foreach ($data as $item) {
|
||||||
|
// Ensure numeric values are properly cast
|
||||||
|
if (isset($item['latitude']))
|
||||||
|
$item['latitude'] = (float) $item['latitude'];
|
||||||
|
if (isset($item['longitude']))
|
||||||
|
$item['longitude'] = (float) $item['longitude'];
|
||||||
|
|
||||||
|
// Consistently use Customer model only
|
||||||
|
// Mapping fields
|
||||||
|
Customer::updateOrCreate(
|
||||||
|
[
|
||||||
|
'name' => $item['name'],
|
||||||
|
// Use lat/lng as composite key to avoid duplicates if name matches but location diff
|
||||||
|
'latitude' => $item['latitude'],
|
||||||
|
'longitude' => $item['longitude']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => $item['name'],
|
||||||
|
'address' => $item['address'] ?? '',
|
||||||
|
'city' => $item['city'] ?? '',
|
||||||
|
'latitude' => $item['latitude'],
|
||||||
|
'longitude' => $item['longitude'],
|
||||||
|
'category' => $item['category'] ?? 'other', // Map category from json
|
||||||
|
'agent_id' => $agentId, // Assign to agent
|
||||||
|
'owner_name' => 'Owner ' . $item['name'], // Dummy owner
|
||||||
|
'phone' => '0812' . rand(10000000, 99999999), // Dummy phone
|
||||||
|
// pic_sales_id left null for now
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
$this->command->info("Seeded {$count} Customers from merchants.json for Agent: " . ($agent ? 'AGT-MASTER' : 'None'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
168
merchants.json
168
merchants.json
@ -1,47 +1,137 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "1",
|
"id": "C001",
|
||||||
"name": "Resto Enak Jakarta",
|
"name": "Toko Kelontong Berkah",
|
||||||
"category": "restaurant",
|
|
||||||
"latitude": -6.2088,
|
|
||||||
"longitude": 106.8456,
|
|
||||||
"city": "Jakarta",
|
|
||||||
"address": "Jl. Thamrin No. 10, Menteng, Jakarta Pusat 10350"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"name": "Kopi Senja",
|
|
||||||
"category": "cafe",
|
|
||||||
"latitude": -6.2150,
|
|
||||||
"longitude": 106.8500,
|
|
||||||
"city": "Jakarta",
|
|
||||||
"address": "Jl. Manggarai Utara III No. 5, Tebet, Jakarta Selatan 12850"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3",
|
|
||||||
"name": "Laundry Kilat",
|
|
||||||
"category": "laundry",
|
|
||||||
"latitude": -6.2200,
|
|
||||||
"longitude": 106.8400,
|
|
||||||
"city": "Jakarta",
|
|
||||||
"address": "Jl. Casablanca Raya No. 88, Menteng Dalam, Jakarta Selatan 12870"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4",
|
|
||||||
"name": "Pasar Tradisional",
|
|
||||||
"category": "market",
|
"category": "market",
|
||||||
"latitude": -6.2000,
|
"latitude": -6.225014,
|
||||||
"longitude": 106.8600,
|
"longitude": 106.800123,
|
||||||
"city": "Jakarta",
|
"city": "Jakarta Selatan",
|
||||||
"address": "Jl. Jatinegara Barat No. 12, Kampung Melayu, Jakarta Timur 13320"
|
"address": "Jl. Pakubuwono No. 10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "5",
|
"id": "C002",
|
||||||
"name": "Sekolah Dasar 01",
|
"name": "Budi Santoso (Warung)",
|
||||||
|
"category": "restaurant",
|
||||||
|
"latitude": -6.221511,
|
||||||
|
"longitude": 106.812311,
|
||||||
|
"city": "Jakarta Selatan",
|
||||||
|
"address": "Senayan City Area"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C003",
|
||||||
|
"name": "Pasar Mayestik Kios 12",
|
||||||
|
"category": "market",
|
||||||
|
"latitude": -6.241588,
|
||||||
|
"longitude": 106.793132,
|
||||||
|
"city": "Jakarta Selatan",
|
||||||
|
"address": "Jl. Tebah"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C004",
|
||||||
|
"name": "Ibu Siti Catering",
|
||||||
|
"category": "restaurant",
|
||||||
|
"latitude": -6.237812,
|
||||||
|
"longitude": 106.799812,
|
||||||
|
"city": "Jakarta Selatan",
|
||||||
|
"address": "Bulungan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C005",
|
||||||
|
"name": "Toko Buah Segar",
|
||||||
|
"category": "market",
|
||||||
|
"latitude": -6.251211,
|
||||||
|
"longitude": 106.828912,
|
||||||
|
"city": "Jakarta Selatan",
|
||||||
|
"address": "Jl. Duren Tiga Raya"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C006",
|
||||||
|
"name": "Laundry Kilat 24 Jam",
|
||||||
|
"category": "laundry",
|
||||||
|
"latitude": -6.261239,
|
||||||
|
"longitude": 106.811231,
|
||||||
|
"city": "Jakarta Selatan",
|
||||||
|
"address": "Kemang Utara"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C007",
|
||||||
|
"name": "Warteg Bahari Jaya",
|
||||||
|
"category": "restaurant",
|
||||||
|
"latitude": -6.192311,
|
||||||
|
"longitude": 106.823121,
|
||||||
|
"city": "Jakarta Pusat",
|
||||||
|
"address": "Tanah Abang"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C008",
|
||||||
|
"name": "Kopi Janji Jiwa",
|
||||||
|
"category": "cafe",
|
||||||
|
"latitude": -6.271823,
|
||||||
|
"longitude": 106.804123,
|
||||||
|
"city": "Jakarta Selatan",
|
||||||
|
"address": "Cipete Raya"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C009",
|
||||||
|
"name": "SDN Menteng 01",
|
||||||
"category": "school",
|
"category": "school",
|
||||||
"latitude": -6.1950,
|
"latitude": -6.196570,
|
||||||
"longitude": 106.8300,
|
"longitude": 106.833923,
|
||||||
"city": "Jakarta",
|
"city": "Jakarta Pusat",
|
||||||
"address": "Jl. Cikini Raya No. 73, Cikini, Jakarta Pusat 10330"
|
"address": "Menteng"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C010",
|
||||||
|
"name": "Apotek K24",
|
||||||
|
"category": "other",
|
||||||
|
"latitude": -6.244312,
|
||||||
|
"longitude": 106.804829,
|
||||||
|
"city": "Jakarta Selatan",
|
||||||
|
"address": "Jl. Wijaya I"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C011",
|
||||||
|
"name": "Bengkel Motor 'Slamet'",
|
||||||
|
"category": "other",
|
||||||
|
"latitude": -6.213241,
|
||||||
|
"longitude": 106.843123,
|
||||||
|
"city": "Jakarta Selatan",
|
||||||
|
"address": "Tebet Raya"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C012",
|
||||||
|
"name": "Toko Plastik Abadi",
|
||||||
|
"category": "market",
|
||||||
|
"latitude": -6.182141,
|
||||||
|
"longitude": 106.812301,
|
||||||
|
"city": "Jakarta Pusat",
|
||||||
|
"address": "Petojo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C013",
|
||||||
|
"name": "Bakso Solo Mas Min",
|
||||||
|
"category": "restaurant",
|
||||||
|
"latitude": -6.202311,
|
||||||
|
"longitude": 106.782192,
|
||||||
|
"city": "Jakarta Barat",
|
||||||
|
"address": "Kemanggisan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C014",
|
||||||
|
"name": "Cafe Rooftop Langit",
|
||||||
|
"category": "cafe",
|
||||||
|
"latitude": -6.223121,
|
||||||
|
"longitude": 106.852311,
|
||||||
|
"city": "Jakarta Selatan",
|
||||||
|
"address": "Tebet Timur"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C015",
|
||||||
|
"name": "TK Bintang Kecil",
|
||||||
|
"category": "school",
|
||||||
|
"latitude": -6.281231,
|
||||||
|
"longitude": 106.823121,
|
||||||
|
"city": "Jakarta Selatan",
|
||||||
|
"address": "Pejaten"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
145
routes/api.php
145
routes/api.php
@ -7,6 +7,8 @@ use App\Http\Controllers\GeoPlanController;
|
|||||||
use App\Http\Controllers\GridController;
|
use App\Http\Controllers\GridController;
|
||||||
use App\Http\Controllers\MerchantController;
|
use App\Http\Controllers\MerchantController;
|
||||||
use App\Http\Controllers\StaffController;
|
use App\Http\Controllers\StaffController;
|
||||||
|
use App\Http\Controllers\AgentController;
|
||||||
|
use App\Http\Middleware\CheckAgentSecret;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -18,57 +20,104 @@ use App\Http\Controllers\StaffController;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// ============ Merchants Routes ============
|
// ============ Merchants Routes ============
|
||||||
Route::get('/merchants', [MerchantController::class, 'index']);
|
// Public routes (login/signup)
|
||||||
|
Route::post('/agents/login', [AgentController::class, 'login']); // Agent Sign In
|
||||||
|
Route::post('/agents', [AgentController::class, 'store']); // Agent Public Sign Up
|
||||||
|
|
||||||
// ============ Grid Routes ============
|
// ============ External Agent Integration (Public) ============
|
||||||
Route::get('/grids', [GridController::class, 'index']);
|
Route::prefix('v1')->group(function () {
|
||||||
Route::get('/grids/generate', [GridController::class, 'generate']);
|
// External Sign In - For external backoffice systems
|
||||||
Route::get('/grids/stats', [GridController::class, 'stats']);
|
Route::post('/agents/signin', [AgentController::class, 'externalSignin']);
|
||||||
Route::post('/grids/{gridId}/download', [GridController::class, 'download']);
|
Route::post('/agents/validate-session', [AgentController::class, 'validateSession']);
|
||||||
|
|
||||||
// ============ GeoTrack Routes ============
|
|
||||||
Route::get('/sales-routes', [GeoTrackController::class, 'index']);
|
|
||||||
Route::get('/sales-routes/dates', [GeoTrackController::class, 'dates']);
|
|
||||||
Route::get('/sales-routes/sales', [GeoTrackController::class, 'sales']);
|
|
||||||
Route::get('/sales-routes/{id}', [GeoTrackController::class, 'show']);
|
|
||||||
Route::post('/sales-routes/waypoints', [GeoTrackController::class, 'storeWaypoint']);
|
|
||||||
|
|
||||||
// ============ GeoPlan Routes ============
|
|
||||||
Route::get('/sales-plans', [GeoPlanController::class, 'index']);
|
|
||||||
Route::get('/sales-plans/dates', [GeoPlanController::class, 'dates']);
|
|
||||||
Route::get('/sales-plans/sales', [GeoPlanController::class, 'sales']);
|
|
||||||
Route::get('/sales-plans/{id}', [GeoPlanController::class, 'show']);
|
|
||||||
Route::post('/sales-plans', [GeoPlanController::class, 'store']);
|
|
||||||
Route::put('/sales-plans/{id}', [GeoPlanController::class, 'update']);
|
|
||||||
Route::delete('/sales-plans/{id}', [GeoPlanController::class, 'destroy']);
|
|
||||||
Route::post('/sales-plans/{id}/optimize', [GeoPlanController::class, 'optimize']);
|
|
||||||
Route::post('/sales-plans/{id}/add-target', [GeoPlanController::class, 'addTarget']);
|
|
||||||
Route::put('/sales-plans/{id}/target/{targetId}', [GeoPlanController::class, 'updateTarget']);
|
|
||||||
Route::delete('/sales-plans/{id}/target/{targetId}', [GeoPlanController::class, 'removeTarget']);
|
|
||||||
Route::post('/sales-plans/assign-target', [GeoPlanController::class, 'autoAssignTarget']);
|
|
||||||
|
|
||||||
// ============ GeoTrack Mobile API (Flutter) ============
|
|
||||||
Route::prefix('mobile')->group(function () {
|
|
||||||
Route::post('/login', [GeoTrackController::class, 'mobileLogin']);
|
|
||||||
Route::post('/checkin', [GeoTrackController::class, 'mobileCheckin']);
|
|
||||||
Route::post('/checkout', [GeoTrackController::class, 'mobileCheckout']);
|
|
||||||
Route::post('/waypoints/batch', [GeoTrackController::class, 'storeBatchWaypoints']);
|
|
||||||
Route::get('/route/today', [GeoTrackController::class, 'getTodayRoute']);
|
|
||||||
Route::get('/status', [GeoTrackController::class, 'getTrackingStatus']);
|
|
||||||
|
|
||||||
// New endpoints
|
|
||||||
Route::get('/schedules', [GeoPlanController::class, 'getMobileSchedule']);
|
|
||||||
Route::post('/schedules/{id}/target', [GeoPlanController::class, 'addTarget']);
|
|
||||||
Route::post('/schedules/{id}/target/{targetId}/checkin', [GeoPlanController::class, 'checkinTarget']);
|
|
||||||
Route::delete('/schedules/{id}/target/{targetId}', [GeoPlanController::class, 'removeTarget']);
|
|
||||||
Route::post('/prospects', [\App\Http\Controllers\CustomerController::class, 'store']);
|
|
||||||
Route::post('/profile', [GeoTrackController::class, 'updateProfile']);
|
|
||||||
Route::post('/change-password', [GeoTrackController::class, 'changePassword']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ Master Data: Staff ============
|
Route::prefix('mobile')->group(function () {
|
||||||
Route::apiResource('staff', StaffController::class);
|
Route::post('/login', [GeoTrackController::class, 'mobileLogin']); // Mobile Login
|
||||||
Route::apiResource('customers', \App\Http\Controllers\CustomerController::class);
|
});
|
||||||
|
|
||||||
|
// All other routes require CheckAgentSecret middleware
|
||||||
|
Route::middleware([CheckAgentSecret::class])->group(function () {
|
||||||
|
|
||||||
|
// ============ Merchants Routes ============
|
||||||
|
Route::get('/merchants', [MerchantController::class, 'index']);
|
||||||
|
|
||||||
|
// ============ Grid Routes ============
|
||||||
|
Route::get('/grids', [GridController::class, 'index']);
|
||||||
|
Route::get('/grids/generate', [GridController::class, 'generate']);
|
||||||
|
Route::get('/grids/stats', [GridController::class, 'stats']);
|
||||||
|
Route::post('/grids/{gridId}/download', [GridController::class, 'download']);
|
||||||
|
|
||||||
|
// ============ GeoTrack Routes ============
|
||||||
|
Route::get('/sales-routes', [GeoTrackController::class, 'index']);
|
||||||
|
Route::get('/sales-routes/dates', [GeoTrackController::class, 'dates']);
|
||||||
|
Route::get('/sales-routes/sales', [GeoTrackController::class, 'sales']);
|
||||||
|
Route::get('/sales-routes/{id}', [GeoTrackController::class, 'show']);
|
||||||
|
Route::post('/sales-routes/waypoints', [GeoTrackController::class, 'storeWaypoint']);
|
||||||
|
|
||||||
|
// ============ GeoPlan Routes ============
|
||||||
|
Route::get('/sales-plans', [GeoPlanController::class, 'index']);
|
||||||
|
Route::get('/sales-plans/dates', [GeoPlanController::class, 'dates']);
|
||||||
|
Route::get('/sales-plans/sales', [GeoPlanController::class, 'sales']);
|
||||||
|
Route::get('/sales-plans/{id}', [GeoPlanController::class, 'show']);
|
||||||
|
Route::post('/sales-plans', [GeoPlanController::class, 'store']);
|
||||||
|
Route::put('/sales-plans/{id}', [GeoPlanController::class, 'update']);
|
||||||
|
Route::delete('/sales-plans/{id}', [GeoPlanController::class, 'destroy']);
|
||||||
|
Route::post('/sales-plans/{id}/optimize', [GeoPlanController::class, 'optimize']);
|
||||||
|
Route::post('/sales-plans/{id}/add-target', [GeoPlanController::class, 'addTarget']);
|
||||||
|
Route::put('/sales-plans/{id}/target/{targetId}', [GeoPlanController::class, 'updateTarget']);
|
||||||
|
Route::delete('/sales-plans/{id}/target/{targetId}', [GeoPlanController::class, 'removeTarget']);
|
||||||
|
Route::post('/sales-plans/assign-target', [GeoPlanController::class, 'autoAssignTarget']);
|
||||||
|
|
||||||
|
// ============ GeoTrack Mobile API (Flutter) ============
|
||||||
|
Route::prefix('mobile')->group(function () {
|
||||||
|
Route::post('/checkin', [GeoTrackController::class, 'mobileCheckin']);
|
||||||
|
Route::post('/checkout', [GeoTrackController::class, 'mobileCheckout']);
|
||||||
|
Route::post('/waypoints/batch', [GeoTrackController::class, 'storeBatchWaypoints']);
|
||||||
|
Route::get('/route/today', [GeoTrackController::class, 'getTodayRoute']);
|
||||||
|
Route::get('/status', [GeoTrackController::class, 'getTrackingStatus']);
|
||||||
|
|
||||||
|
// New endpoints
|
||||||
|
Route::get('/schedules', [GeoPlanController::class, 'getMobileSchedule']);
|
||||||
|
Route::post('/schedules/{id}/target', [GeoPlanController::class, 'addTarget']);
|
||||||
|
Route::post('/schedules/{id}/target/{targetId}/checkin', [GeoPlanController::class, 'checkinTarget']);
|
||||||
|
Route::delete('/schedules/{id}/target/{targetId}', [GeoPlanController::class, 'removeTarget']);
|
||||||
|
Route::post('/prospects', [\App\Http\Controllers\CustomerController::class, 'store']);
|
||||||
|
Route::post('/profile', [GeoTrackController::class, 'updateProfile']);
|
||||||
|
Route::post('/change-password', [GeoTrackController::class, 'changePassword']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============ Master Data: Staff ============
|
||||||
|
Route::apiResource('staff', StaffController::class);
|
||||||
|
Route::apiResource('customers', \App\Http\Controllers\CustomerController::class);
|
||||||
|
|
||||||
|
// ============ Agent Management ============
|
||||||
|
Route::get('/agents', [AgentController::class, 'index']);
|
||||||
|
Route::get('/agents/{id}', [AgentController::class, 'show']);
|
||||||
|
Route::put('/agents/{id}', [AgentController::class, 'update']);
|
||||||
|
Route::delete('/agents/{id}', [AgentController::class, 'destroy']);
|
||||||
|
Route::post('/agents/{id}/regenerate-key', [AgentController::class, 'regenerateKey']);
|
||||||
|
|
||||||
|
// ============ RBAC (Role & Menu) ============
|
||||||
|
Route::apiResource('menus', \App\Http\Controllers\MenuController::class);
|
||||||
|
Route::apiResource('user-groups', \App\Http\Controllers\UserGroupController::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============ EXTERNAL AGENT INTEGRATION API (V1) ============
|
||||||
|
Route::prefix('v1')->middleware([CheckAgentSecret::class])->group(function () {
|
||||||
|
// These are the Clean APIs for external Agents (Sandbox)
|
||||||
|
|
||||||
|
// Tracking
|
||||||
|
Route::get('/routes', [\App\Http\Controllers\Api\V1\TrackingController::class, 'index']);
|
||||||
|
Route::post('/routes/waypoints', [\App\Http\Controllers\Api\V1\TrackingController::class, 'storeWaypoint']);
|
||||||
|
|
||||||
|
// Planning
|
||||||
|
Route::get('/plans', [\App\Http\Controllers\Api\V1\PlanningController::class, 'index']);
|
||||||
|
Route::post('/plans', [\App\Http\Controllers\Api\V1\PlanningController::class, 'store']);
|
||||||
|
|
||||||
|
// Visit (Customers/Merchants) - Reusing main controller for now as logic is same
|
||||||
|
Route::post('/customers', [\App\Http\Controllers\CustomerController::class, 'store']);
|
||||||
|
Route::get('/merchants', [MerchantController::class, 'index']);
|
||||||
|
});
|
||||||
|
|
||||||
// ============ Health Check ============
|
// ============ Health Check ============
|
||||||
Route::get('/health', function () {
|
Route::get('/health', function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user