Compare commits
12 Commits
fix/asset-
...
feature/de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05f77e6144 | ||
|
|
b92ba4b86d | ||
|
|
f8f219f00b | ||
|
|
f16dac012d | ||
|
|
f566b83cc6 | ||
|
|
418da7a39e | ||
|
|
3c6fe92811 | ||
|
|
7d3243b67e | ||
|
|
8f6597f428 | ||
|
|
64d38b8b2f | ||
|
|
7aa366eda9 | ||
|
|
d7adaf0cba |
@@ -22,6 +22,12 @@ class CustomerController extends Controller
|
||||
'business' => $business,
|
||||
'feature' => 'Customers',
|
||||
'description' => 'The Customers feature requires CRM to be enabled for your business.',
|
||||
'benefits' => [
|
||||
'Manage all your customer accounts in one place',
|
||||
'Track contact information and order history',
|
||||
'Build stronger customer relationships',
|
||||
'Access customer insights and analytics',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -42,6 +48,12 @@ class CustomerController extends Controller
|
||||
'business' => $business,
|
||||
'feature' => 'Customers',
|
||||
'description' => 'The Customers feature requires CRM to be enabled for your business.',
|
||||
'benefits' => [
|
||||
'Manage all your customer accounts in one place',
|
||||
'Track contact information and order history',
|
||||
'Build stronger customer relationships',
|
||||
'Access customer insights and analytics',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Seller\Crm;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Business;
|
||||
use App\Models\Crm\CrmActiveView;
|
||||
use App\Models\Crm\CrmChannel;
|
||||
use App\Models\Crm\CrmInternalNote;
|
||||
@@ -24,10 +25,8 @@ class ThreadController extends Controller
|
||||
/**
|
||||
* Display unified inbox
|
||||
*/
|
||||
public function index(Request $request)
|
||||
public function index(Request $request, Business $business)
|
||||
{
|
||||
$business = $request->user()->business;
|
||||
|
||||
$query = CrmThread::forBusiness($business->id)
|
||||
->with(['contact', 'assignee', 'messages' => fn ($q) => $q->latest()->limit(1)])
|
||||
->withCount('messages');
|
||||
@@ -77,10 +76,8 @@ class ThreadController extends Controller
|
||||
/**
|
||||
* Show a single thread
|
||||
*/
|
||||
public function show(Request $request, CrmThread $thread)
|
||||
public function show(Request $request, Business $business, CrmThread $thread)
|
||||
{
|
||||
$business = $request->user()->business;
|
||||
|
||||
// SECURITY: Verify business ownership
|
||||
if ($thread->business_id !== $business->id) {
|
||||
abort(404);
|
||||
@@ -128,10 +125,8 @@ class ThreadController extends Controller
|
||||
/**
|
||||
* Send a reply in thread
|
||||
*/
|
||||
public function reply(Request $request, CrmThread $thread)
|
||||
public function reply(Request $request, Business $business, CrmThread $thread)
|
||||
{
|
||||
$business = $request->user()->business;
|
||||
|
||||
if ($thread->business_id !== $business->id) {
|
||||
abort(404);
|
||||
}
|
||||
@@ -177,10 +172,8 @@ class ThreadController extends Controller
|
||||
/**
|
||||
* Assign thread to user
|
||||
*/
|
||||
public function assign(Request $request, CrmThread $thread)
|
||||
public function assign(Request $request, Business $business, CrmThread $thread)
|
||||
{
|
||||
$business = $request->user()->business;
|
||||
|
||||
if ($thread->business_id !== $business->id) {
|
||||
abort(404);
|
||||
}
|
||||
@@ -206,10 +199,8 @@ class ThreadController extends Controller
|
||||
/**
|
||||
* Close thread
|
||||
*/
|
||||
public function close(Request $request, CrmThread $thread)
|
||||
public function close(Request $request, Business $business, CrmThread $thread)
|
||||
{
|
||||
$business = $request->user()->business;
|
||||
|
||||
if ($thread->business_id !== $business->id) {
|
||||
abort(404);
|
||||
}
|
||||
@@ -222,10 +213,8 @@ class ThreadController extends Controller
|
||||
/**
|
||||
* Reopen thread
|
||||
*/
|
||||
public function reopen(Request $request, CrmThread $thread)
|
||||
public function reopen(Request $request, Business $business, CrmThread $thread)
|
||||
{
|
||||
$business = $request->user()->business;
|
||||
|
||||
if ($thread->business_id !== $business->id) {
|
||||
abort(404);
|
||||
}
|
||||
@@ -241,10 +230,8 @@ class ThreadController extends Controller
|
||||
/**
|
||||
* Snooze thread
|
||||
*/
|
||||
public function snooze(Request $request, CrmThread $thread)
|
||||
public function snooze(Request $request, Business $business, CrmThread $thread)
|
||||
{
|
||||
$business = $request->user()->business;
|
||||
|
||||
if ($thread->business_id !== $business->id) {
|
||||
abort(404);
|
||||
}
|
||||
@@ -264,10 +251,8 @@ class ThreadController extends Controller
|
||||
/**
|
||||
* Add internal note
|
||||
*/
|
||||
public function addNote(Request $request, CrmThread $thread)
|
||||
public function addNote(Request $request, Business $business, CrmThread $thread)
|
||||
{
|
||||
$business = $request->user()->business;
|
||||
|
||||
if ($thread->business_id !== $business->id) {
|
||||
abort(404);
|
||||
}
|
||||
@@ -290,10 +275,8 @@ class ThreadController extends Controller
|
||||
/**
|
||||
* Generate AI reply draft
|
||||
*/
|
||||
public function generateAiReply(Request $request, CrmThread $thread)
|
||||
public function generateAiReply(Request $request, Business $business, CrmThread $thread)
|
||||
{
|
||||
$business = $request->user()->business;
|
||||
|
||||
if ($thread->business_id !== $business->id) {
|
||||
abort(404);
|
||||
}
|
||||
@@ -313,10 +296,8 @@ class ThreadController extends Controller
|
||||
/**
|
||||
* Heartbeat for active viewing
|
||||
*/
|
||||
public function heartbeat(Request $request, CrmThread $thread)
|
||||
public function heartbeat(Request $request, Business $business, CrmThread $thread)
|
||||
{
|
||||
$business = $request->user()->business;
|
||||
|
||||
if ($thread->business_id !== $business->id) {
|
||||
return response()->json(['error' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\Business;
|
||||
use App\Models\DeliveryWindow;
|
||||
use App\Models\Department;
|
||||
use App\Models\DepartmentSuitePermission;
|
||||
use App\Models\Driver;
|
||||
use App\Models\Suite;
|
||||
use App\Models\User;
|
||||
use App\Models\Vehicle;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -141,11 +143,6 @@ class SettingsController extends Controller
|
||||
{
|
||||
$query = $business->users();
|
||||
|
||||
// Exclude the business owner from the list
|
||||
if ($business->owner_user_id) {
|
||||
$query->where('users.id', '!=', $business->owner_user_id);
|
||||
}
|
||||
|
||||
// Search
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
@@ -155,10 +152,15 @@ class SettingsController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by account type (role)
|
||||
if ($request->filled('account_type')) {
|
||||
$query->whereHas('roles', function ($q) use ($request) {
|
||||
$q->where('name', $request->account_type);
|
||||
// Filter by role
|
||||
if ($request->filled('role')) {
|
||||
$query->wherePivot('role', $request->role);
|
||||
}
|
||||
|
||||
// Filter by department
|
||||
if ($request->filled('department_id')) {
|
||||
$query->whereHas('departments', function ($q) use ($request) {
|
||||
$q->where('departments.id', $request->department_id);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -170,9 +172,20 @@ class SettingsController extends Controller
|
||||
$query->where('last_login_at', '<=', $request->last_login_end.' 23:59:59');
|
||||
}
|
||||
|
||||
$users = $query->with('roles')->paginate(15);
|
||||
$users = $query->with(['roles', 'departments'])->paginate(15);
|
||||
|
||||
return view('seller.settings.users', compact('business', 'users'));
|
||||
// Get all departments for this business
|
||||
$departments = Department::where('business_id', $business->id)
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
// Get business owner info
|
||||
$owner = $business->owner;
|
||||
|
||||
// Get the suites assigned to this business for permission reference
|
||||
$businessSuites = $business->suites()->active()->get();
|
||||
|
||||
return view('seller.settings.users', compact('business', 'users', 'departments', 'owner', 'businessSuites'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,7 +199,9 @@ class SettingsController extends Controller
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'position' => 'nullable|string|max:255',
|
||||
'role' => 'required|string|in:company-owner,company-manager,company-user,company-sales,company-accounting,company-manufacturing,company-processing',
|
||||
'role' => 'required|string|in:owner,admin,manager,member',
|
||||
'department_ids' => 'nullable|array',
|
||||
'department_ids.*' => 'exists:departments,id',
|
||||
'is_point_of_contact' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
@@ -196,21 +211,32 @@ class SettingsController extends Controller
|
||||
// Create user and associate with business
|
||||
$user = \App\Models\User::create([
|
||||
'name' => $fullName,
|
||||
'first_name' => $validated['first_name'],
|
||||
'last_name' => $validated['last_name'],
|
||||
'email' => $validated['email'],
|
||||
'phone' => $validated['phone'],
|
||||
'phone' => $validated['phone'] ?? null,
|
||||
'position' => $validated['position'] ?? null,
|
||||
'user_type' => $business->business_type, // Match business type
|
||||
'password' => bcrypt(str()->random(32)), // Temporary password
|
||||
]);
|
||||
|
||||
// Assign role
|
||||
$user->assignRole($validated['role']);
|
||||
|
||||
// Associate with business with additional pivot data
|
||||
// Associate with business with role
|
||||
$business->users()->attach($user->id, [
|
||||
'role' => $validated['role'],
|
||||
'is_primary' => false,
|
||||
'contact_type' => $request->has('is_point_of_contact') ? 'primary' : null,
|
||||
'permissions' => [], // Empty initially, assigned via department
|
||||
]);
|
||||
|
||||
// Assign to departments if specified
|
||||
if (! empty($validated['department_ids'])) {
|
||||
foreach ($validated['department_ids'] as $departmentId) {
|
||||
$user->departments()->attach($departmentId, [
|
||||
'role' => 'operator', // Default department role
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Send invitation email with password reset link
|
||||
|
||||
return redirect()
|
||||
@@ -264,10 +290,28 @@ class SettingsController extends Controller
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
// Define permission categories (existing permissions system)
|
||||
$permissionCategories = $this->getPermissionCategories();
|
||||
// Get the suites assigned to this business
|
||||
$businessSuites = $business->suites()->active()->get();
|
||||
|
||||
return view('seller.settings.users-edit', compact('business', 'user', 'isOwner', 'departments', 'permissionCategories'));
|
||||
// Build suite permissions structure based on business's assigned suites
|
||||
$suitePermissions = $this->getSuitePermissions($businessSuites);
|
||||
|
||||
// Get user's current permissions from pivot
|
||||
$userPermissions = $business->users()
|
||||
->where('users.id', $user->id)
|
||||
->first()
|
||||
->pivot
|
||||
->permissions ?? [];
|
||||
|
||||
return view('seller.settings.users-edit', compact(
|
||||
'business',
|
||||
'user',
|
||||
'isOwner',
|
||||
'departments',
|
||||
'businessSuites',
|
||||
'suitePermissions',
|
||||
'userPermissions'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,63 +368,122 @@ class SettingsController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permission categories for the permissions system.
|
||||
* Get suite-based permission structure for the assigned suites.
|
||||
*
|
||||
* Groups permissions by functional area for better UX.
|
||||
*/
|
||||
private function getPermissionCategories(): array
|
||||
private function getSuitePermissions($businessSuites): array
|
||||
{
|
||||
return [
|
||||
'ecommerce' => [
|
||||
'name' => 'Ecommerce',
|
||||
'icon' => 'lucide--shopping-cart',
|
||||
'permissions' => [
|
||||
'view_orders' => ['name' => 'View Orders', 'description' => 'View all orders and order details'],
|
||||
'manage_orders' => ['name' => 'Manage Orders', 'description' => 'Accept, reject, and update orders'],
|
||||
'view_invoices' => ['name' => 'View Invoices', 'description' => 'View invoices and payment information'],
|
||||
'manage_invoices' => ['name' => 'Manage Invoices', 'description' => 'Create and edit invoices'],
|
||||
],
|
||||
],
|
||||
'products' => [
|
||||
'name' => 'Products & Inventory',
|
||||
'icon' => 'lucide--package',
|
||||
'permissions' => [
|
||||
'view_products' => ['name' => 'View Products', 'description' => 'View product catalog'],
|
||||
'manage_products' => ['name' => 'Manage Products', 'description' => 'Create, edit, and delete products'],
|
||||
'view_inventory' => ['name' => 'View Inventory', 'description' => 'View inventory levels and stock'],
|
||||
'manage_inventory' => ['name' => 'Manage Inventory', 'description' => 'Update inventory and stock levels'],
|
||||
],
|
||||
],
|
||||
'data_visibility' => [
|
||||
'name' => 'Data & Analytics Visibility',
|
||||
'icon' => 'lucide--eye',
|
||||
'permissions' => [
|
||||
'view_sales_data' => ['name' => 'View Sales Data', 'description' => 'See revenue, pricing, profit margins, and sales metrics'],
|
||||
'view_performance_data' => ['name' => 'View Performance Data', 'description' => 'See yields, efficiency, quality metrics, and production stats'],
|
||||
'view_cost_data' => ['name' => 'View Cost Data', 'description' => 'See material costs, labor costs, and expense breakdowns'],
|
||||
'view_customer_data' => ['name' => 'View Customer Data', 'description' => 'See customer names, contact info, and purchase history'],
|
||||
],
|
||||
],
|
||||
'manufacturing' => [
|
||||
'name' => 'Manufacturing & Processing',
|
||||
'icon' => 'lucide--factory',
|
||||
'permissions' => [
|
||||
'view_work_orders' => ['name' => 'View Work Orders', 'description' => 'View work orders (limited to own department)'],
|
||||
'manage_work_orders' => ['name' => 'Manage Work Orders', 'description' => 'Create, edit, and complete work orders'],
|
||||
'view_wash_reports' => ['name' => 'View Wash Reports', 'description' => 'View solventless wash reports and data'],
|
||||
'manage_wash_reports' => ['name' => 'Manage Wash Reports', 'description' => 'Create and edit wash reports'],
|
||||
'view_all_departments' => ['name' => 'View All Departments', 'description' => 'See data across all departments (not just own)'],
|
||||
],
|
||||
],
|
||||
'business' => [
|
||||
'name' => 'Business Management',
|
||||
'icon' => 'lucide--building-2',
|
||||
'permissions' => [
|
||||
'view_settings' => ['name' => 'View Settings', 'description' => 'View business settings and configuration'],
|
||||
'manage_settings' => ['name' => 'Manage Settings', 'description' => 'Update business settings and configuration'],
|
||||
'view_users' => ['name' => 'View Users', 'description' => 'View user list and permissions'],
|
||||
'manage_users' => ['name' => 'Manage Users', 'description' => 'Invite and manage user permissions'],
|
||||
],
|
||||
],
|
||||
$suitePermissions = [];
|
||||
|
||||
foreach ($businessSuites as $suite) {
|
||||
$permissions = DepartmentSuitePermission::getAvailablePermissions($suite->key);
|
||||
|
||||
if (empty($permissions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Group permissions by functional area
|
||||
$grouped = $this->groupPermissionsByArea($permissions, $suite->key);
|
||||
|
||||
$suitePermissions[$suite->key] = [
|
||||
'name' => $suite->name,
|
||||
'description' => $suite->description,
|
||||
'icon' => $suite->icon,
|
||||
'color' => $suite->color,
|
||||
'groups' => $grouped,
|
||||
];
|
||||
}
|
||||
|
||||
return $suitePermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group permissions by functional area for better organization.
|
||||
*/
|
||||
private function groupPermissionsByArea(array $permissions, string $suiteKey): array
|
||||
{
|
||||
// Define permission groupings based on prefix patterns
|
||||
$areaPatterns = [
|
||||
'dashboard' => ['view_dashboard', 'view_org_dashboard', 'view_analytics', 'view_all_analytics', 'export_analytics'],
|
||||
'products' => ['view_products', 'manage_products', 'view_inventory', 'adjust_inventory', 'view_costs', 'view_margin'],
|
||||
'orders' => ['view_orders', 'create_orders', 'manage_orders', 'view_invoices', 'create_invoices'],
|
||||
'menus' => ['view_menus', 'manage_menus', 'view_promotions', 'manage_promotions'],
|
||||
'campaigns' => ['view_campaigns', 'manage_campaigns', 'send_campaigns', 'manage_templates'],
|
||||
'crm' => ['view_pipeline', 'edit_pipeline', 'manage_accounts', 'view_buyer_intelligence'],
|
||||
'automations' => ['view_automations', 'manage_automations', 'use_copilot'],
|
||||
'batches' => ['view_batches', 'manage_batches', 'create_batches'],
|
||||
'processing' => ['view_wash_reports', 'create_wash_reports', 'edit_wash_reports', 'manage_extractions', 'view_yields', 'manage_biomass_intake', 'manage_material_transfers'],
|
||||
'manufacturing' => ['view_bom', 'manage_bom', 'create_bom', 'view_production_queue', 'manage_production', 'manage_packaging', 'manage_labeling', 'create_skus', 'manage_lot_tracking'],
|
||||
'work_orders' => ['view_work_orders', 'create_work_orders', 'manage_work_orders'],
|
||||
'delivery' => ['view_pick_pack', 'manage_pick_pack', 'view_manifests', 'create_manifests', 'view_delivery_windows', 'manage_delivery_windows', 'view_drivers', 'manage_drivers', 'view_vehicles', 'manage_vehicles', 'view_routes', 'manage_routing', 'view_deliveries', 'complete_deliveries', 'manage_proof_of_delivery'],
|
||||
'compliance' => ['view_compliance', 'manage_licenses', 'manage_coas', 'view_compliance_reports'],
|
||||
'finance' => ['view_ap', 'manage_ap', 'pay_bills', 'view_ar', 'manage_ar', 'view_budgets', 'manage_budgets', 'approve_budget_exceptions', 'view_inter_company_ledger', 'manage_inter_company', 'view_financial_reports', 'export_financial_data', 'view_forecasting', 'manage_forecasting', 'view_kpis', 'view_usage_billing', 'manage_billing', 'view_cross_business'],
|
||||
'messaging' => ['view_conversations', 'send_messages', 'manage_contacts'],
|
||||
'procurement' => ['view_vendors', 'manage_vendors', 'view_requisitions', 'create_requisitions', 'approve_requisitions', 'view_purchase_orders', 'create_purchase_orders', 'receive_goods'],
|
||||
'tools' => ['manage_settings', 'manage_users', 'manage_departments', 'view_audit_log', 'manage_integrations'],
|
||||
'marketplace' => ['view_marketplace', 'browse_products', 'manage_cart', 'manage_favorites', 'view_buyer_portal', 'view_account'],
|
||||
'brand_view' => ['view_sales', 'view_buyers'],
|
||||
];
|
||||
|
||||
$areaLabels = [
|
||||
'dashboard' => ['name' => 'Dashboard & Analytics', 'icon' => 'lucide--layout-dashboard'],
|
||||
'products' => ['name' => 'Products & Inventory', 'icon' => 'lucide--package'],
|
||||
'orders' => ['name' => 'Orders & Invoicing', 'icon' => 'lucide--shopping-cart'],
|
||||
'menus' => ['name' => 'Menus & Promotions', 'icon' => 'lucide--menu'],
|
||||
'campaigns' => ['name' => 'Campaigns & Marketing', 'icon' => 'lucide--megaphone'],
|
||||
'crm' => ['name' => 'CRM & Accounts', 'icon' => 'lucide--users'],
|
||||
'automations' => ['name' => 'Automations & AI', 'icon' => 'lucide--bot'],
|
||||
'batches' => ['name' => 'Batches', 'icon' => 'lucide--layers'],
|
||||
'processing' => ['name' => 'Processing Operations', 'icon' => 'lucide--flask-conical'],
|
||||
'manufacturing' => ['name' => 'Manufacturing', 'icon' => 'lucide--factory'],
|
||||
'work_orders' => ['name' => 'Work Orders', 'icon' => 'lucide--clipboard-list'],
|
||||
'delivery' => ['name' => 'Delivery & Fulfillment', 'icon' => 'lucide--truck'],
|
||||
'compliance' => ['name' => 'Compliance', 'icon' => 'lucide--shield-check'],
|
||||
'finance' => ['name' => 'Finance & Budgets', 'icon' => 'lucide--banknote'],
|
||||
'messaging' => ['name' => 'Messaging', 'icon' => 'lucide--message-square'],
|
||||
'procurement' => ['name' => 'Procurement', 'icon' => 'lucide--clipboard-list'],
|
||||
'tools' => ['name' => 'Tools & Settings', 'icon' => 'lucide--settings'],
|
||||
'marketplace' => ['name' => 'Marketplace', 'icon' => 'lucide--store'],
|
||||
'brand_view' => ['name' => 'Brand Data', 'icon' => 'lucide--eye'],
|
||||
];
|
||||
|
||||
$grouped = [];
|
||||
$assigned = [];
|
||||
|
||||
foreach ($areaPatterns as $area => $areaPermissions) {
|
||||
$matchedPermissions = [];
|
||||
foreach ($permissions as $key => $description) {
|
||||
if (in_array($key, $areaPermissions) && ! isset($assigned[$key])) {
|
||||
$matchedPermissions[$key] = $description;
|
||||
$assigned[$key] = true;
|
||||
}
|
||||
}
|
||||
if (! empty($matchedPermissions)) {
|
||||
$grouped[$area] = [
|
||||
'name' => $areaLabels[$area]['name'] ?? ucwords(str_replace('_', ' ', $area)),
|
||||
'icon' => $areaLabels[$area]['icon'] ?? 'lucide--folder',
|
||||
'permissions' => $matchedPermissions,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining permissions to "Other"
|
||||
$remaining = [];
|
||||
foreach ($permissions as $key => $description) {
|
||||
if (! isset($assigned[$key])) {
|
||||
$remaining[$key] = $description;
|
||||
}
|
||||
}
|
||||
if (! empty($remaining)) {
|
||||
$grouped['other'] = [
|
||||
'name' => 'Other',
|
||||
'icon' => 'lucide--more-horizontal',
|
||||
'permissions' => $remaining,
|
||||
];
|
||||
}
|
||||
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,11 +11,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
/**
|
||||
* Represents the permissions a department has for a specific suite.
|
||||
*
|
||||
* The `permissions` JSON field contains granular permission flags like:
|
||||
* - For Sales Suite: view_pipeline, edit_pipeline, manage_accounts, create_orders
|
||||
* - For Inventory Suite: view_basic, view_costs, adjust_inventory
|
||||
* - For Finance Suite: view_ap, view_ar, approve_invoices, pay_bills, view_margin
|
||||
* - etc.
|
||||
* The `permissions` JSON field contains granular permission flags.
|
||||
* See SUITE_PERMISSIONS constant for available permissions per suite.
|
||||
*
|
||||
* Based on docs/SUITES_AND_PRICING_MODEL.md - 7 active suites:
|
||||
* - sales, processing, manufacturing, delivery, management, brand_manager, dispensary
|
||||
*/
|
||||
class DepartmentSuitePermission extends Model
|
||||
{
|
||||
@@ -36,129 +36,317 @@ class DepartmentSuitePermission extends Model
|
||||
/**
|
||||
* Available permissions per suite.
|
||||
* These define what granular permissions are available for each suite.
|
||||
*
|
||||
* Note: Messaging, Procurement, and Tools are shared features available to all suites.
|
||||
* They are included in each suite's permissions for granular control.
|
||||
*/
|
||||
public const SUITE_PERMISSIONS = [
|
||||
// =========================================================================
|
||||
// SALES SUITE - External customers (priced)
|
||||
// =========================================================================
|
||||
'sales' => [
|
||||
// Dashboard & Analytics
|
||||
'view_dashboard' => 'View dashboard and overview',
|
||||
'view_analytics' => 'View analytics and reports',
|
||||
'export_analytics' => 'Export analytics data',
|
||||
|
||||
// Products & Inventory
|
||||
'view_products' => 'View products',
|
||||
'manage_products' => 'Create and edit products',
|
||||
'view_inventory' => 'View inventory levels',
|
||||
'adjust_inventory' => 'Adjust inventory quantities',
|
||||
'view_costs' => 'View cost and pricing data',
|
||||
'view_margin' => 'View margin information',
|
||||
|
||||
// Batches (from supplied COAs)
|
||||
'view_batches' => 'View batch information',
|
||||
'manage_batches' => 'Manage batch information',
|
||||
|
||||
// Orders & Invoicing
|
||||
'view_orders' => 'View orders',
|
||||
'create_orders' => 'Create and process orders',
|
||||
'manage_orders' => 'Manage order status and fulfillment',
|
||||
'view_invoices' => 'View invoices',
|
||||
'create_invoices' => 'Create invoices',
|
||||
|
||||
// Menus & Promotions
|
||||
'view_menus' => 'View menus',
|
||||
'manage_menus' => 'Create and manage menus',
|
||||
'view_promotions' => 'View promotions',
|
||||
'manage_promotions' => 'Create and manage promotions',
|
||||
|
||||
// Campaigns & Marketing
|
||||
'view_campaigns' => 'View marketing campaigns',
|
||||
'manage_campaigns' => 'Create and manage campaigns',
|
||||
'send_campaigns' => 'Send marketing campaigns',
|
||||
'manage_templates' => 'Manage message templates',
|
||||
|
||||
// CRM & Buyer Intelligence
|
||||
'view_pipeline' => 'View sales pipeline',
|
||||
'edit_pipeline' => 'Edit sales pipeline',
|
||||
'manage_accounts' => 'Manage customer accounts',
|
||||
'create_orders' => 'Create and process orders',
|
||||
'view_orders' => 'View orders',
|
||||
'manage_promotions' => 'Create and manage promotions',
|
||||
'send_campaigns' => 'Send marketing campaigns',
|
||||
'manage_templates' => 'Manage message templates',
|
||||
'view_buyer_intelligence' => 'View buyer intelligence data',
|
||||
|
||||
// Automations & AI
|
||||
'view_automations' => 'View automations',
|
||||
'manage_automations' => 'Create and manage automations',
|
||||
'use_copilot' => 'Use AI copilot features',
|
||||
|
||||
// Messaging (shared feature)
|
||||
'view_conversations' => 'View conversations/inbox',
|
||||
'send_messages' => 'Send messages to customers',
|
||||
'use_copilot' => 'Use AI copilot features',
|
||||
'manage_menus' => 'Create and manage menus',
|
||||
],
|
||||
'inventory' => [
|
||||
'view_basic' => 'View basic inventory info',
|
||||
'view_costs' => 'View cost and pricing data',
|
||||
'view_margin' => 'View margin information',
|
||||
'adjust_inventory' => 'Adjust inventory quantities',
|
||||
'manage_products' => 'Create and edit products',
|
||||
'manage_batches' => 'Manage batch information',
|
||||
'manage_components' => 'Manage components and BOMs',
|
||||
'view_movements' => 'View stock movements',
|
||||
'create_movements' => 'Create stock movements',
|
||||
],
|
||||
'processing' => [
|
||||
'view_dashboard' => 'View processing dashboard',
|
||||
'create_wash_reports' => 'Create wash reports',
|
||||
'edit_wash_reports' => 'Edit wash reports',
|
||||
'manage_extractions' => 'Manage extraction operations',
|
||||
'view_yields' => 'View yield reports',
|
||||
'manage_batches' => 'Manage processing batches',
|
||||
'manage_work_orders' => 'Manage work orders',
|
||||
'view_analytics' => 'View processing analytics',
|
||||
],
|
||||
'manufacturing' => [
|
||||
'view_dashboard' => 'View manufacturing dashboard',
|
||||
'manage_bom' => 'Manage bill of materials',
|
||||
'create_work_orders' => 'Create work orders',
|
||||
'manage_work_orders' => 'Manage and complete work orders',
|
||||
'manage_packaging' => 'Manage packaging operations',
|
||||
'manage_labeling' => 'Manage labeling',
|
||||
'view_production_queue' => 'View production queue',
|
||||
'view_analytics' => 'View manufacturing analytics',
|
||||
],
|
||||
'procurement' => [
|
||||
'manage_contacts' => 'Manage contacts',
|
||||
|
||||
// Procurement (shared feature)
|
||||
'view_vendors' => 'View vendors and suppliers',
|
||||
'manage_vendors' => 'Manage vendor information',
|
||||
'view_requisitions' => 'View purchase requisitions',
|
||||
'create_requisitions' => 'Create purchase requisitions',
|
||||
'approve_requisitions' => 'Approve purchase requisitions',
|
||||
'view_purchase_orders' => 'View purchase orders',
|
||||
'create_purchase_orders' => 'Create purchase orders',
|
||||
'approve_purchase_orders' => 'Approve purchase orders',
|
||||
'receive_goods' => 'Receive goods against POs',
|
||||
'manage_vendors' => 'Manage vendor information',
|
||||
],
|
||||
'distribution' => [
|
||||
'view_pick_pack' => 'View pick/pack queue',
|
||||
'manage_pick_pack' => 'Manage pick/pack operations',
|
||||
'view_manifests' => 'View delivery manifests',
|
||||
'create_manifests' => 'Create delivery manifests',
|
||||
'manage_routing' => 'Manage delivery routes',
|
||||
'manage_drivers' => 'Manage drivers and vehicles',
|
||||
'view_deliveries' => 'View delivery status',
|
||||
'complete_deliveries' => 'Complete delivery operations',
|
||||
'view_analytics' => 'View delivery analytics',
|
||||
],
|
||||
'finance' => [
|
||||
'view_ap' => 'View accounts payable',
|
||||
'manage_ap' => 'Manage accounts payable',
|
||||
'view_ar' => 'View accounts receivable',
|
||||
'manage_ar' => 'Manage accounts receivable',
|
||||
'view_invoices' => 'View invoices',
|
||||
'create_invoices' => 'Create invoices',
|
||||
'approve_invoices' => 'Approve invoices',
|
||||
'pay_bills' => 'Process bill payments',
|
||||
'view_margin' => 'View margin and profitability',
|
||||
'view_reports' => 'View financial reports',
|
||||
'manage_budgets' => 'Manage budgets',
|
||||
'export_data' => 'Export financial data',
|
||||
],
|
||||
'compliance' => [
|
||||
'view_compliance' => 'View compliance status',
|
||||
'manage_licenses' => 'Manage licenses',
|
||||
'manage_coas' => 'Manage certificates of analysis',
|
||||
'manage_manifests' => 'Manage compliance manifests',
|
||||
'view_reports' => 'View compliance reports',
|
||||
'export_data' => 'Export compliance data',
|
||||
],
|
||||
'management' => [
|
||||
'view_org_dashboard' => 'View organization dashboard',
|
||||
'view_cross_business' => 'View cross-business data',
|
||||
'view_all_finances' => 'View all financial data (Canopy)',
|
||||
'manage_forecasting' => 'Manage forecasting',
|
||||
'view_kpis' => 'View KPIs and metrics',
|
||||
'manage_settings' => 'Manage organization settings',
|
||||
],
|
||||
'inbox' => [
|
||||
'view_inbox' => 'View inbox messages',
|
||||
'send_messages' => 'Send messages',
|
||||
'manage_contacts' => 'Manage contacts',
|
||||
'use_templates' => 'Use message templates',
|
||||
],
|
||||
'tools' => [
|
||||
|
||||
// Tools (shared feature)
|
||||
'manage_settings' => 'Manage business settings',
|
||||
'manage_users' => 'Manage users and permissions',
|
||||
'manage_departments' => 'Manage departments',
|
||||
'view_audit_log' => 'View audit logs',
|
||||
'manage_integrations' => 'Manage integrations',
|
||||
],
|
||||
'brand_portal' => [
|
||||
'view_sales' => 'View sales and orders for linked brands',
|
||||
'view_inventory' => 'View inventory for linked brands (read-only)',
|
||||
'view_promotions' => 'View promotions involving linked brands',
|
||||
'view_accounts' => 'View customer accounts carrying linked brands',
|
||||
// Explicitly NOT available for brand portal:
|
||||
// 'view_costs', 'view_margin', 'edit_anything'
|
||||
|
||||
// =========================================================================
|
||||
// PROCESSING SUITE - Internal (Curagreen, Leopard AZ)
|
||||
// =========================================================================
|
||||
'processing' => [
|
||||
// Dashboard & Analytics
|
||||
'view_dashboard' => 'View processing dashboard',
|
||||
'view_analytics' => 'View processing analytics',
|
||||
|
||||
// Batches & Runs
|
||||
'view_batches' => 'View batches and runs',
|
||||
'manage_batches' => 'Manage processing batches',
|
||||
'create_batches' => 'Create new batches',
|
||||
|
||||
// Extraction Operations
|
||||
'view_wash_reports' => 'View wash reports',
|
||||
'create_wash_reports' => 'Create wash reports',
|
||||
'edit_wash_reports' => 'Edit wash reports',
|
||||
'manage_extractions' => 'Manage extraction operations',
|
||||
'view_yields' => 'View yield reports',
|
||||
|
||||
// Biomass & Materials
|
||||
'manage_biomass_intake' => 'Manage biomass intake',
|
||||
'manage_material_transfers' => 'Manage material transfers',
|
||||
|
||||
// Work Orders
|
||||
'view_work_orders' => 'View work orders',
|
||||
'create_work_orders' => 'Create work orders',
|
||||
'manage_work_orders' => 'Manage and complete work orders',
|
||||
|
||||
// Compliance (Processing has compliance)
|
||||
'view_compliance' => 'View compliance status',
|
||||
'manage_licenses' => 'Manage licenses',
|
||||
'manage_coas' => 'Manage certificates of analysis',
|
||||
'view_compliance_reports' => 'View compliance reports',
|
||||
|
||||
// Messaging (shared feature)
|
||||
'view_conversations' => 'View conversations',
|
||||
'send_messages' => 'Send messages',
|
||||
'manage_contacts' => 'Manage contacts',
|
||||
|
||||
// Procurement (shared feature)
|
||||
'view_vendors' => 'View vendors and suppliers',
|
||||
'manage_vendors' => 'Manage vendor information',
|
||||
'view_requisitions' => 'View purchase requisitions',
|
||||
'create_requisitions' => 'Create purchase requisitions',
|
||||
'approve_requisitions' => 'Approve purchase requisitions',
|
||||
'view_purchase_orders' => 'View purchase orders',
|
||||
'create_purchase_orders' => 'Create purchase orders',
|
||||
'receive_goods' => 'Receive goods against POs',
|
||||
|
||||
// Tools (shared feature)
|
||||
'manage_settings' => 'Manage business settings',
|
||||
'manage_users' => 'Manage users and permissions',
|
||||
'manage_departments' => 'Manage departments',
|
||||
'view_audit_log' => 'View audit logs',
|
||||
],
|
||||
|
||||
// =========================================================================
|
||||
// MANUFACTURING SUITE - Internal (Leopard AZ)
|
||||
// =========================================================================
|
||||
'manufacturing' => [
|
||||
// Dashboard & Analytics
|
||||
'view_dashboard' => 'View manufacturing dashboard',
|
||||
'view_analytics' => 'View manufacturing analytics',
|
||||
|
||||
// BOM (Manufacturing only)
|
||||
'view_bom' => 'View bill of materials',
|
||||
'manage_bom' => 'Manage bill of materials',
|
||||
'create_bom' => 'Create bill of materials',
|
||||
|
||||
// Work Orders
|
||||
'view_work_orders' => 'View work orders',
|
||||
'create_work_orders' => 'Create work orders',
|
||||
'manage_work_orders' => 'Manage and complete work orders',
|
||||
|
||||
// Production
|
||||
'view_production_queue' => 'View production queue',
|
||||
'manage_production' => 'Manage production operations',
|
||||
'manage_packaging' => 'Manage packaging operations',
|
||||
'manage_labeling' => 'Manage labeling',
|
||||
|
||||
// SKU & Lot Tracking
|
||||
'create_skus' => 'Create SKUs from batches',
|
||||
'manage_lot_tracking' => 'Manage lot tracking',
|
||||
|
||||
// Batches
|
||||
'view_batches' => 'View batch information',
|
||||
'manage_batches' => 'Manage batch information',
|
||||
|
||||
// Compliance (Manufacturing has compliance)
|
||||
'view_compliance' => 'View compliance status',
|
||||
'manage_licenses' => 'Manage licenses',
|
||||
'manage_coas' => 'Manage certificates of analysis',
|
||||
'view_compliance_reports' => 'View compliance reports',
|
||||
|
||||
// Messaging (shared feature)
|
||||
'view_conversations' => 'View conversations',
|
||||
'send_messages' => 'Send messages',
|
||||
'manage_contacts' => 'Manage contacts',
|
||||
|
||||
// Procurement (shared feature)
|
||||
'view_vendors' => 'View vendors and suppliers',
|
||||
'manage_vendors' => 'Manage vendor information',
|
||||
'view_requisitions' => 'View purchase requisitions',
|
||||
'create_requisitions' => 'Create purchase requisitions',
|
||||
'approve_requisitions' => 'Approve purchase requisitions',
|
||||
'view_purchase_orders' => 'View purchase orders',
|
||||
'create_purchase_orders' => 'Create purchase orders',
|
||||
'receive_goods' => 'Receive goods against POs',
|
||||
|
||||
// Tools (shared feature)
|
||||
'manage_settings' => 'Manage business settings',
|
||||
'manage_users' => 'Manage users and permissions',
|
||||
'manage_departments' => 'Manage departments',
|
||||
'view_audit_log' => 'View audit logs',
|
||||
],
|
||||
|
||||
// =========================================================================
|
||||
// DELIVERY SUITE - Internal (Leopard AZ)
|
||||
// =========================================================================
|
||||
'delivery' => [
|
||||
// Dashboard & Analytics
|
||||
'view_dashboard' => 'View delivery dashboard',
|
||||
'view_analytics' => 'View delivery analytics',
|
||||
|
||||
// Pick & Pack
|
||||
'view_pick_pack' => 'View pick/pack queue',
|
||||
'manage_pick_pack' => 'Manage pick/pack operations',
|
||||
|
||||
// Manifests
|
||||
'view_manifests' => 'View delivery manifests',
|
||||
'create_manifests' => 'Create delivery manifests',
|
||||
|
||||
// Delivery Windows
|
||||
'view_delivery_windows' => 'View delivery windows',
|
||||
'manage_delivery_windows' => 'Manage delivery windows',
|
||||
|
||||
// Drivers & Vehicles
|
||||
'view_drivers' => 'View drivers',
|
||||
'manage_drivers' => 'Manage drivers',
|
||||
'view_vehicles' => 'View vehicles',
|
||||
'manage_vehicles' => 'Manage vehicles',
|
||||
|
||||
// Routes & Deliveries
|
||||
'view_routes' => 'View routes',
|
||||
'manage_routing' => 'Manage delivery routes',
|
||||
'view_deliveries' => 'View delivery status',
|
||||
'complete_deliveries' => 'Complete delivery operations',
|
||||
'manage_proof_of_delivery' => 'Manage proof of delivery',
|
||||
|
||||
// Messaging (shared feature)
|
||||
'view_conversations' => 'View conversations',
|
||||
'send_messages' => 'Send messages',
|
||||
'manage_contacts' => 'Manage contacts',
|
||||
|
||||
// Procurement (shared feature)
|
||||
'view_vendors' => 'View vendors and suppliers',
|
||||
'manage_vendors' => 'Manage vendor information',
|
||||
'view_requisitions' => 'View purchase requisitions',
|
||||
'create_requisitions' => 'Create purchase requisitions',
|
||||
'approve_requisitions' => 'Approve purchase requisitions',
|
||||
'view_purchase_orders' => 'View purchase orders',
|
||||
'create_purchase_orders' => 'Create purchase orders',
|
||||
'receive_goods' => 'Receive goods against POs',
|
||||
|
||||
// Tools (shared feature)
|
||||
'manage_settings' => 'Manage business settings',
|
||||
'manage_users' => 'Manage users and permissions',
|
||||
'manage_departments' => 'Manage departments',
|
||||
'view_audit_log' => 'View audit logs',
|
||||
],
|
||||
|
||||
// =========================================================================
|
||||
// MANAGEMENT SUITE - Internal (Canopy - parent company)
|
||||
// =========================================================================
|
||||
'management' => [
|
||||
// Dashboard & Analytics
|
||||
'view_org_dashboard' => 'View organization dashboard',
|
||||
'view_cross_business' => 'View cross-business data',
|
||||
'view_all_analytics' => 'View all analytics across subdivisions',
|
||||
|
||||
// Finance - Accounts Payable
|
||||
'view_ap' => 'View accounts payable',
|
||||
'manage_ap' => 'Manage accounts payable',
|
||||
'pay_bills' => 'Process bill payments',
|
||||
|
||||
// Finance - Accounts Receivable
|
||||
'view_ar' => 'View accounts receivable',
|
||||
'manage_ar' => 'Manage accounts receivable',
|
||||
'view_invoices' => 'View all invoices',
|
||||
|
||||
// Budgets
|
||||
'view_budgets' => 'View budgets',
|
||||
'manage_budgets' => 'Manage budgets per subdivision',
|
||||
'approve_budget_exceptions' => 'Approve POs that exceed budget',
|
||||
|
||||
// Inter-company
|
||||
'view_inter_company_ledger' => 'View inter-company ledger',
|
||||
'manage_inter_company' => 'Manage inter-company transfers',
|
||||
|
||||
// Financial Reports
|
||||
'view_financial_reports' => 'View financial reports (P&L, balance sheet, cash flow)',
|
||||
'export_financial_data' => 'Export financial data',
|
||||
|
||||
// Forecasting & KPIs
|
||||
'view_forecasting' => 'View forecasting',
|
||||
'manage_forecasting' => 'Manage forecasting',
|
||||
'view_kpis' => 'View KPIs and metrics',
|
||||
|
||||
// Usage & Billing
|
||||
'view_usage_billing' => 'View usage and billing analytics',
|
||||
'manage_billing' => 'Manage billing',
|
||||
|
||||
// Messaging (shared feature)
|
||||
'view_conversations' => 'View conversations',
|
||||
'send_messages' => 'Send messages',
|
||||
'manage_contacts' => 'Manage contacts',
|
||||
|
||||
// Tools (shared feature)
|
||||
'manage_settings' => 'Manage organization settings',
|
||||
'manage_users' => 'Manage users and permissions',
|
||||
'manage_departments' => 'Manage departments',
|
||||
'view_audit_log' => 'View audit logs',
|
||||
'manage_integrations' => 'Manage integrations',
|
||||
],
|
||||
|
||||
// =========================================================================
|
||||
// BRAND MANAGER SUITE - External brand partners (view-only)
|
||||
// =========================================================================
|
||||
'brand_manager' => [
|
||||
// View-only access - all data auto-scoped by brand_id
|
||||
// All data is auto-scoped by brand_id
|
||||
'view_dashboard' => 'View brand dashboard',
|
||||
'view_sales' => 'View sales history for assigned brands',
|
||||
'view_orders' => 'View orders for assigned brands (read-only)',
|
||||
'view_products' => 'View active products for assigned brands',
|
||||
@@ -166,10 +354,44 @@ class DepartmentSuitePermission extends Model
|
||||
'view_promotions' => 'View promotions for assigned brands',
|
||||
'view_conversations' => 'View conversations for assigned brands (read-only)',
|
||||
'view_buyers' => 'View buyer accounts for assigned brands (read-only)',
|
||||
'view_analytics' => 'View brand-level analytics (not internal seller analytics)',
|
||||
// Explicitly NOT available for brand_manager:
|
||||
// 'view_costs', 'view_margin', 'view_wholesale_pricing' (unless brand owner)
|
||||
// 'edit_products', 'create_promotions', 'manage_settings', 'view_other_brands'
|
||||
'view_analytics' => 'View brand-level analytics',
|
||||
|
||||
// Messaging (brand-scoped)
|
||||
'send_messages' => 'Send messages (brand-scoped)',
|
||||
|
||||
// Explicitly NOT available:
|
||||
// view_costs, view_margin, view_wholesale_pricing
|
||||
// edit_products, create_promotions, manage_settings, view_other_brands
|
||||
],
|
||||
|
||||
// =========================================================================
|
||||
// DISPENSARY SUITE - Buyers (dispensaries/retailers)
|
||||
// =========================================================================
|
||||
'dispensary' => [
|
||||
// Marketplace
|
||||
'view_marketplace' => 'View marketplace',
|
||||
'browse_products' => 'Browse products',
|
||||
'view_promotions' => 'View and redeem promotions',
|
||||
|
||||
// Ordering
|
||||
'create_orders' => 'Create orders',
|
||||
'view_orders' => 'View order history',
|
||||
'manage_cart' => 'Manage shopping cart',
|
||||
'manage_favorites' => 'Manage favorite products',
|
||||
|
||||
// Buyer Portal
|
||||
'view_buyer_portal' => 'View buyer portal',
|
||||
'view_account' => 'View account information',
|
||||
|
||||
// Messaging (shared feature)
|
||||
'view_conversations' => 'View conversations',
|
||||
'send_messages' => 'Send messages to sellers',
|
||||
'manage_contacts' => 'Manage contacts',
|
||||
|
||||
// Tools (shared feature)
|
||||
'manage_settings' => 'Manage business settings',
|
||||
'manage_users' => 'Manage users and permissions',
|
||||
'view_audit_log' => 'View audit logs',
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
@@ -43,6 +44,17 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
// Force HTTPS for all generated URLs in non-local environments
|
||||
// This is required because:
|
||||
// 1. SSL terminates at the K8s ingress, so PHP sees HTTP
|
||||
// 2. TrustProxies passes X-Forwarded-Proto, but Laravel's asset() helper
|
||||
// doesn't automatically use it - it uses the cached URL generator state
|
||||
// 3. Filament's dynamic imports (tabs.js, select.js) use asset() and fail
|
||||
// with "Mixed Content" errors if they generate HTTP URLs on HTTPS pages
|
||||
if (! $this->app->environment('local')) {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
|
||||
// Configure password validation defaults
|
||||
Password::defaults(function () {
|
||||
return Password::min(8)
|
||||
|
||||
@@ -54,6 +54,8 @@ return [
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
'asset_url' => env('ASSET_URL'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|
||||
@@ -11,7 +11,7 @@ class BrandSeeder extends Seeder
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* All 14 brands belong to Cannabrands (the seller/manufacturer company)
|
||||
* All 12 brands belong to Cannabrands (the seller/manufacturer company)
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
@@ -139,6 +139,6 @@ class BrandSeeder extends Seeder
|
||||
);
|
||||
}
|
||||
|
||||
$this->command->info('✅ Created 14 brands under Cannabrands business');
|
||||
$this->command->info('✅ Created 12 brands under Cannabrands business');
|
||||
}
|
||||
}
|
||||
|
||||
168
database/seeders/DevCleanupSeeder.php
Normal file
168
database/seeders/DevCleanupSeeder.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Brand;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Development Environment Cleanup Seeder
|
||||
*
|
||||
* This seeder is intended to be run manually on the dev.cannabrands.app environment
|
||||
* to remove test/sample data and keep only Thunder Bud products.
|
||||
*
|
||||
* Run with: php artisan db:seed --class=DevCleanupSeeder
|
||||
*/
|
||||
class DevCleanupSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Thunder Bud SKU prefix to preserve.
|
||||
* Only products with SKUs starting with this prefix will be kept.
|
||||
*/
|
||||
protected string $thunderBudPrefix = 'TB-';
|
||||
|
||||
/**
|
||||
* Brands to remove (test/sample brands).
|
||||
*/
|
||||
protected array $brandsToRemove = [
|
||||
'bulk',
|
||||
'twisites', // misspelled version only
|
||||
];
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('Starting dev environment cleanup...');
|
||||
|
||||
// Step 1: Remove non-Thunder Bud products
|
||||
$this->removeNonThunderBudProducts();
|
||||
|
||||
// Step 2: Remove test brands (Bulk, Twisties, Twisites)
|
||||
$this->removeTestBrands();
|
||||
|
||||
// Step 3: Clean up orphaned data
|
||||
$this->cleanupOrphanedData();
|
||||
|
||||
$this->command->info('Dev environment cleanup complete!');
|
||||
}
|
||||
|
||||
protected function removeNonThunderBudProducts(): void
|
||||
{
|
||||
$this->command->info('Removing non-Thunder Bud products...');
|
||||
|
||||
// Find products that DON'T have the TB- prefix
|
||||
$productsToDelete = Product::where('sku', 'not like', $this->thunderBudPrefix.'%')->get();
|
||||
$count = $productsToDelete->count();
|
||||
|
||||
if ($count === 0) {
|
||||
$this->command->info('No non-Thunder Bud products found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->command->info("Found {$count} products to remove.");
|
||||
|
||||
// Delete related data first (order items, inventory, etc.)
|
||||
$productIds = $productsToDelete->pluck('id')->toArray();
|
||||
|
||||
// Delete order items referencing these products
|
||||
$orderItemsDeleted = DB::table('order_items')
|
||||
->whereIn('product_id', $productIds)
|
||||
->delete();
|
||||
|
||||
if ($orderItemsDeleted > 0) {
|
||||
$this->command->info("Deleted {$orderItemsDeleted} order items.");
|
||||
}
|
||||
|
||||
// Delete inventory records
|
||||
if (DB::getSchemaBuilder()->hasTable('inventories')) {
|
||||
$inventoryDeleted = DB::table('inventories')
|
||||
->whereIn('product_id', $productIds)
|
||||
->delete();
|
||||
|
||||
if ($inventoryDeleted > 0) {
|
||||
$this->command->info("Deleted {$inventoryDeleted} inventory records.");
|
||||
}
|
||||
}
|
||||
|
||||
// Delete product variants if they exist
|
||||
if (DB::getSchemaBuilder()->hasTable('product_variants')) {
|
||||
$variantsDeleted = DB::table('product_variants')
|
||||
->whereIn('product_id', $productIds)
|
||||
->delete();
|
||||
|
||||
if ($variantsDeleted > 0) {
|
||||
$this->command->info("Deleted {$variantsDeleted} product variants.");
|
||||
}
|
||||
}
|
||||
|
||||
// Delete products
|
||||
$deleted = Product::whereIn('id', $productIds)->delete();
|
||||
$this->command->info("Deleted {$deleted} non-Thunder Bud products.");
|
||||
|
||||
// List remaining Thunder Bud products
|
||||
$remaining = Product::count();
|
||||
$this->command->info("Remaining products: {$remaining}");
|
||||
}
|
||||
|
||||
protected function removeTestBrands(): void
|
||||
{
|
||||
$this->command->info('Removing test brands (Bulk, Twisites)...');
|
||||
|
||||
$brandsToDelete = Brand::whereIn('slug', $this->brandsToRemove)->get();
|
||||
|
||||
if ($brandsToDelete->isEmpty()) {
|
||||
$this->command->info('No test brands found to remove.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($brandsToDelete as $brand) {
|
||||
// Check if brand has products (should have been deleted in previous step)
|
||||
$productCount = $brand->products()->count();
|
||||
|
||||
if ($productCount > 0) {
|
||||
$this->command->warn("Brand '{$brand->name}' still has {$productCount} products. Deleting them first...");
|
||||
$brand->products()->delete();
|
||||
}
|
||||
|
||||
$brand->delete();
|
||||
$this->command->info("Deleted brand: {$brand->name}");
|
||||
}
|
||||
}
|
||||
|
||||
protected function cleanupOrphanedData(): void
|
||||
{
|
||||
$this->command->info('Cleaning up orphaned data...');
|
||||
|
||||
// Delete orders with no items
|
||||
$emptyOrders = DB::table('orders')
|
||||
->whereNotExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('order_items')
|
||||
->whereColumn('order_items.order_id', 'orders.id');
|
||||
})
|
||||
->delete();
|
||||
|
||||
if ($emptyOrders > 0) {
|
||||
$this->command->info("Deleted {$emptyOrders} empty orders.");
|
||||
}
|
||||
|
||||
// Delete orphaned promotions (if table exists)
|
||||
if (DB::getSchemaBuilder()->hasTable('promotions')) {
|
||||
$orphanedPromotions = DB::table('promotions')
|
||||
->whereNotNull('brand_id')
|
||||
->whereNotExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('brands')
|
||||
->whereColumn('brands.id', 'promotions.brand_id');
|
||||
})
|
||||
->delete();
|
||||
|
||||
if ($orphanedPromotions > 0) {
|
||||
$this->command->info("Deleted {$orphanedPromotions} orphaned promotions.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
164
database/seeders/DevMediaSyncSeeder.php
Normal file
164
database/seeders/DevMediaSyncSeeder.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Brand;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Development Environment Media Sync Seeder
|
||||
*
|
||||
* This seeder updates brand and product media paths in the database
|
||||
* to match the expected MinIO structure. It does NOT copy files -
|
||||
* files should be synced separately using mc (MinIO Client) or rsync.
|
||||
*
|
||||
* Run with: php artisan db:seed --class=DevMediaSyncSeeder
|
||||
*
|
||||
* To sync actual files from local to dev MinIO, use:
|
||||
* mc mirror local-minio/media/businesses/cannabrands/brands/ dev-minio/media/businesses/cannabrands/brands/
|
||||
*/
|
||||
class DevMediaSyncSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Thunder Bud SKU prefix.
|
||||
*/
|
||||
protected string $thunderBudPrefix = 'TB-';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('Syncing media paths for dev environment...');
|
||||
|
||||
$this->syncBrandMedia();
|
||||
$this->syncProductMedia();
|
||||
|
||||
$this->command->info('Dev media sync complete!');
|
||||
$this->command->newLine();
|
||||
$this->command->info('Next steps:');
|
||||
$this->command->line('1. Sync actual media files to dev MinIO using mc mirror or similar');
|
||||
$this->command->line('2. Verify images are accessible at the configured AWS_URL');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update brand logo_path and banner_path to match expected structure.
|
||||
*/
|
||||
protected function syncBrandMedia(): void
|
||||
{
|
||||
$this->command->info('Syncing brand media paths...');
|
||||
|
||||
$brands = Brand::with('business')->get();
|
||||
$updated = 0;
|
||||
|
||||
foreach ($brands as $brand) {
|
||||
$businessSlug = $brand->business->slug ?? 'cannabrands';
|
||||
|
||||
// Set expected paths based on MinIO structure
|
||||
$basePath = "businesses/{$businessSlug}/brands/{$brand->slug}/branding";
|
||||
|
||||
// Try to find actual files or set expected paths
|
||||
$logoPath = $this->findMediaFile($basePath, 'logo', ['png', 'jpg', 'jpeg']);
|
||||
$bannerPath = $this->findMediaFile($basePath, 'banner', ['jpg', 'jpeg', 'png']);
|
||||
|
||||
// Update if we found paths or if current paths are null
|
||||
if ($logoPath || $bannerPath || ! $brand->logo_path || ! $brand->banner_path) {
|
||||
$brand->logo_path = $logoPath ?? "{$basePath}/logo.png";
|
||||
$brand->banner_path = $bannerPath ?? "{$basePath}/banner.jpg";
|
||||
$brand->save();
|
||||
$updated++;
|
||||
|
||||
$this->command->line(" - {$brand->name}: logo={$brand->logo_path}, banner={$brand->banner_path}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info("Updated {$updated} brand media paths.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Thunder Bud product image_path fields.
|
||||
*/
|
||||
protected function syncProductMedia(): void
|
||||
{
|
||||
$this->command->info('Syncing Thunder Bud product media paths...');
|
||||
|
||||
// Find Thunder Bud brand
|
||||
$thunderBudBrand = Brand::where('slug', 'thunder-bud')
|
||||
->orWhere('slug', 'thunderbud')
|
||||
->first();
|
||||
|
||||
if (! $thunderBudBrand) {
|
||||
$this->command->warn('Thunder Bud brand not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$businessSlug = $thunderBudBrand->business->slug ?? 'cannabrands';
|
||||
$brandSlug = $thunderBudBrand->slug;
|
||||
|
||||
// Find all Thunder Bud products (TB- prefix only)
|
||||
$products = Product::where('sku', 'like', $this->thunderBudPrefix.'%')->get();
|
||||
|
||||
$updated = 0;
|
||||
|
||||
foreach ($products as $product) {
|
||||
// Set expected image path based on SKU
|
||||
$imagePath = "businesses/{$businessSlug}/brands/{$brandSlug}/products/{$product->sku}/images";
|
||||
|
||||
// Try to find actual image file
|
||||
$actualImage = $this->findProductImage($imagePath);
|
||||
|
||||
if ($actualImage) {
|
||||
$product->image_path = $actualImage;
|
||||
$product->save();
|
||||
$updated++;
|
||||
$this->command->line(" - {$product->sku}: {$actualImage}");
|
||||
} else {
|
||||
// Set a default expected path
|
||||
$expectedPath = "{$imagePath}/{$product->slug}.png";
|
||||
$product->image_path = $expectedPath;
|
||||
$product->save();
|
||||
$updated++;
|
||||
$this->command->line(" - {$product->sku}: {$expectedPath} (expected)");
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info("Updated {$updated} product media paths.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a media file in MinIO with the given base path and name.
|
||||
*/
|
||||
protected function findMediaFile(string $basePath, string $name, array $extensions): ?string
|
||||
{
|
||||
foreach ($extensions as $ext) {
|
||||
$path = "{$basePath}/{$name}.{$ext}";
|
||||
if (Storage::exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a product image in the given path.
|
||||
*/
|
||||
protected function findProductImage(string $imagePath): ?string
|
||||
{
|
||||
try {
|
||||
$files = Storage::files($imagePath);
|
||||
|
||||
// Return the first image file found
|
||||
foreach ($files as $file) {
|
||||
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['png', 'jpg', 'jpeg', 'webp'])) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Directory doesn't exist
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -8,110 +8,183 @@ use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Assigns suites and plans to dev businesses for local testing.
|
||||
* Gives Cannabrands all suites so all features are accessible.
|
||||
* Assigns suites to businesses based on docs/SUITES_AND_PRICING_MODEL.md
|
||||
*
|
||||
* Run after ProductionSyncSeeder.
|
||||
* Business → Suite Mapping:
|
||||
*
|
||||
* Cannabrands (Sales & brand representation):
|
||||
* - Sales Suite
|
||||
* - is_enterprise_plan = true
|
||||
*
|
||||
* Curagreen (Processing – BHO extraction):
|
||||
* - Processing Suite
|
||||
* - is_enterprise_plan = true
|
||||
*
|
||||
* Leopard AZ (Solventless + Manufacturing + Delivery):
|
||||
* - Processing Suite
|
||||
* - Manufacturing Suite
|
||||
* - Delivery Suite
|
||||
* - is_enterprise_plan = true
|
||||
*
|
||||
* Canopy (Parent company – financial & management):
|
||||
* - Management Suite
|
||||
* - is_enterprise_plan = true
|
||||
*
|
||||
* Buyer businesses (dispensaries):
|
||||
* - Dispensary Suite
|
||||
*/
|
||||
class DevSuitesSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('Assigning dev suites and plans...');
|
||||
$this->command->info('Assigning suites to businesses per SUITES_AND_PRICING_MODEL.md...');
|
||||
|
||||
$this->assignSuitesToCannbrands();
|
||||
$this->assignSuitesToOtherBusinesses();
|
||||
$this->assignPlansToBusinesses();
|
||||
$this->assignSuitesToCuragreen();
|
||||
$this->assignSuitesToLeopardAz();
|
||||
$this->assignSuitesToCanopy();
|
||||
$this->assignSuitesToBuyers();
|
||||
|
||||
$this->command->info('Dev suites seeding complete!');
|
||||
$this->command->info('Suite assignments complete!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cannabrands: Sales Suite + Enterprise Plan
|
||||
*/
|
||||
private function assignSuitesToCannbrands(): void
|
||||
{
|
||||
$cannabrands = Business::where('slug', 'cannabrands')->first();
|
||||
|
||||
if (! $cannabrands) {
|
||||
$this->command->warn('Cannabrands business not found, skipping suite assignments.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Give Cannabrands ALL active suites for full feature testing
|
||||
$suites = Suite::where('is_active', true)->get();
|
||||
|
||||
foreach ($suites as $suite) {
|
||||
DB::table('business_suite')->updateOrInsert(
|
||||
['business_id' => $cannabrands->id, 'suite_id' => $suite->id],
|
||||
['created_at' => now(), 'updated_at' => now()]
|
||||
);
|
||||
}
|
||||
|
||||
$this->command->line(" - Cannabrands: assigned {$suites->count()} suites (all active)");
|
||||
}
|
||||
|
||||
private function assignSuitesToOtherBusinesses(): void
|
||||
{
|
||||
// Canopy gets Sales + Inventory (typical seller setup)
|
||||
$this->assignSuites('canopy', ['Sales Suite', 'Inventory Suite', 'Tools Suite']);
|
||||
|
||||
// Curagreen gets Marketing + Sales
|
||||
$this->assignSuites('curagreen', ['Sales Suite', 'Marketing Suite']);
|
||||
|
||||
// Leopard gets minimal setup
|
||||
$this->assignSuites('leopard-az', ['Sales Suite']);
|
||||
|
||||
// GreenLeaf (buyer) gets Dispensary Suite
|
||||
$this->assignSuites('greenleaf-dispensary', ['Dispensary Suite', 'Inbox Suite']);
|
||||
}
|
||||
|
||||
private function assignSuites(string $businessSlug, array $suiteNames): void
|
||||
{
|
||||
$business = Business::where('slug', $businessSlug)->first();
|
||||
$business = Business::where('slug', 'cannabrands')->first();
|
||||
|
||||
if (! $business) {
|
||||
$this->command->warn('Cannabrands business not found, skipping.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$suites = Suite::whereIn('name', $suiteNames)->get();
|
||||
// Assign Sales Suite
|
||||
$this->assignSuitesByKey($business, ['sales']);
|
||||
|
||||
foreach ($suites as $suite) {
|
||||
DB::table('business_suite')->updateOrInsert(
|
||||
['business_id' => $business->id, 'suite_id' => $suite->id],
|
||||
['created_at' => now(), 'updated_at' => now()]
|
||||
);
|
||||
}
|
||||
// Enable Enterprise Plan (removes usage limits)
|
||||
$business->update(['is_enterprise_plan' => true]);
|
||||
|
||||
$this->command->line(" - {$business->name}: assigned {$suites->count()} suites");
|
||||
$this->command->line(' ✓ Cannabrands: Sales Suite + Enterprise Plan');
|
||||
}
|
||||
|
||||
private function assignPlansToBusinesses(): void
|
||||
/**
|
||||
* Curagreen: Processing Suite + Enterprise Plan
|
||||
*/
|
||||
private function assignSuitesToCuragreen(): void
|
||||
{
|
||||
$this->command->info('Assigning plans to businesses...');
|
||||
$business = Business::where('slug', 'curagreen')->first();
|
||||
|
||||
// Check if businesses table has plan_id column
|
||||
if (! \Schema::hasColumn('businesses', 'plan_id')) {
|
||||
$this->command->warn(' - businesses.plan_id column not found, skipping plan assignments.');
|
||||
if (! $business) {
|
||||
$this->command->warn('Curagreen business not found, skipping.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$plans = [
|
||||
'cannabrands' => 'scale', // Full access for testing
|
||||
'canopy' => 'growth', // Mid-tier
|
||||
'curagreen' => 'growth', // Mid-tier
|
||||
'leopard-az' => 'starter', // Basic
|
||||
'greenleaf-dispensary' => 'starter',
|
||||
'phoenix-cannabis-collective' => 'starter',
|
||||
// Assign Processing Suite
|
||||
$this->assignSuitesByKey($business, ['processing']);
|
||||
|
||||
// Enable Enterprise Plan
|
||||
$business->update(['is_enterprise_plan' => true]);
|
||||
|
||||
$this->command->line(' ✓ Curagreen: Processing Suite + Enterprise Plan');
|
||||
}
|
||||
|
||||
/**
|
||||
* Leopard AZ: Processing + Manufacturing + Delivery + Enterprise Plan
|
||||
*/
|
||||
private function assignSuitesToLeopardAz(): void
|
||||
{
|
||||
$business = Business::where('slug', 'leopard-az')->first();
|
||||
|
||||
if (! $business) {
|
||||
$this->command->warn('Leopard AZ business not found, skipping.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign all operational suites for Leopard AZ
|
||||
$this->assignSuitesByKey($business, [
|
||||
'processing', // Solventless extraction
|
||||
'manufacturing', // BOM, packaging, production
|
||||
'delivery', // Pick/pack, manifests, drivers
|
||||
]);
|
||||
|
||||
// Enable Enterprise Plan
|
||||
$business->update(['is_enterprise_plan' => true]);
|
||||
|
||||
$this->command->line(' ✓ Leopard AZ: Processing + Manufacturing + Delivery + Enterprise Plan');
|
||||
}
|
||||
|
||||
/**
|
||||
* Canopy: Management Suite + Enterprise Plan
|
||||
*/
|
||||
private function assignSuitesToCanopy(): void
|
||||
{
|
||||
$business = Business::where('slug', 'canopy')->first();
|
||||
|
||||
if (! $business) {
|
||||
$this->command->warn('Canopy business not found, skipping.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign Management Suite (AP/AR, budgets, cross-business analytics)
|
||||
$this->assignSuitesByKey($business, ['management']);
|
||||
|
||||
// Enable Enterprise Plan
|
||||
$business->update(['is_enterprise_plan' => true]);
|
||||
|
||||
$this->command->line(' ✓ Canopy: Management Suite + Enterprise Plan');
|
||||
}
|
||||
|
||||
/**
|
||||
* Buyer businesses: Dispensary Suite
|
||||
*/
|
||||
private function assignSuitesToBuyers(): void
|
||||
{
|
||||
// Common buyer business slugs
|
||||
$buyerSlugs = [
|
||||
'greenleaf-dispensary',
|
||||
'phoenix-cannabis-collective',
|
||||
];
|
||||
|
||||
foreach ($plans as $businessSlug => $planCode) {
|
||||
$business = Business::where('slug', $businessSlug)->first();
|
||||
$plan = DB::table('plans')->where('code', $planCode)->first();
|
||||
foreach ($buyerSlugs as $slug) {
|
||||
$business = Business::where('slug', $slug)->first();
|
||||
|
||||
if ($business && $plan) {
|
||||
$business->update(['plan_id' => $plan->id]);
|
||||
$this->command->line(" - {$business->name}: {$plan->name} plan");
|
||||
if (! $business) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assign Dispensary Suite
|
||||
$this->assignSuitesByKey($business, ['dispensary']);
|
||||
|
||||
$this->command->line(" ✓ {$business->name}: Dispensary Suite");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Assign suites to a business by suite keys
|
||||
*/
|
||||
private function assignSuitesByKey(Business $business, array $suiteKeys): void
|
||||
{
|
||||
// First, remove any existing suite assignments for this business
|
||||
// (to ensure clean state when re-running seeder)
|
||||
DB::table('business_suite')->where('business_id', $business->id)->delete();
|
||||
|
||||
// Assign the specified suites
|
||||
$suites = Suite::whereIn('key', $suiteKeys)->where('is_active', true)->get();
|
||||
|
||||
foreach ($suites as $suite) {
|
||||
DB::table('business_suite')->insert([
|
||||
'business_id' => $business->id,
|
||||
'suite_id' => $suite->id,
|
||||
'granted_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,290 +5,348 @@ namespace Database\Seeders;
|
||||
use App\Models\Suite;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
/**
|
||||
* Seeds the 7 core suites as defined in docs/SUITES_AND_PRICING_MODEL.md
|
||||
*
|
||||
* Suites:
|
||||
* 1. Sales Suite - External customers (priced at $495/mo)
|
||||
* 2. Processing Suite - Internal (Curagreen, Leopard AZ)
|
||||
* 3. Manufacturing Suite - Internal (Leopard AZ)
|
||||
* 4. Delivery Suite - Internal (Leopard AZ)
|
||||
* 5. Management Suite - Internal (Canopy - parent company)
|
||||
* 6. Brand Manager Suite - External brand partners (view-only)
|
||||
* 7. Dispensary Suite - Buyers (dispensaries/retailers)
|
||||
*
|
||||
* Note: Enterprise is NOT a suite - it's a flag (is_enterprise_plan) on Business.
|
||||
*/
|
||||
class SuitesSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$suites = [
|
||||
// Core sales and marketing
|
||||
// =====================================================================
|
||||
// SALES SUITE - External customers (the only priced suite)
|
||||
// =====================================================================
|
||||
[
|
||||
'key' => 'sales',
|
||||
'name' => 'Sales Suite',
|
||||
'description' => 'Complete sales platform for wholesalers and brands. Includes analytics, CRM, buyer intelligence, conversations, and AI copilot.',
|
||||
'description' => 'Complete sales platform for wholesalers and brands. Includes products, inventory, orders, CRM, menus, promotions, campaigns, analytics, buyer intelligence, messaging, and AI copilot.',
|
||||
'icon' => 'lucide--shopping-cart',
|
||||
'color' => 'emerald',
|
||||
'sort_order' => 10,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'included_features' => [
|
||||
// Core features
|
||||
'products',
|
||||
'inventory',
|
||||
'orders',
|
||||
'invoicing',
|
||||
'menus',
|
||||
'promotions',
|
||||
'campaigns',
|
||||
'analytics',
|
||||
'crm',
|
||||
'buyer_intelligence',
|
||||
'conversations',
|
||||
'automations',
|
||||
'copilot',
|
||||
'orders',
|
||||
'menus',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'marketing',
|
||||
'name' => 'Marketing Suite',
|
||||
'description' => 'Marketing tools for campaigns, promotions, and customer engagement.',
|
||||
'icon' => 'lucide--megaphone',
|
||||
'color' => 'pink',
|
||||
'sort_order' => 15,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'included_features' => [
|
||||
'campaigns',
|
||||
'promotions',
|
||||
'templates',
|
||||
'analytics',
|
||||
'batches', // from supplied COAs
|
||||
// Shared features (all suites get these)
|
||||
'messaging',
|
||||
'procurement',
|
||||
'tools',
|
||||
],
|
||||
],
|
||||
|
||||
// Operations suites
|
||||
[
|
||||
'key' => 'inventory',
|
||||
'name' => 'Inventory Suite',
|
||||
'description' => 'Inventory management including stock tracking, costs, movements, and batch management.',
|
||||
'icon' => 'lucide--package',
|
||||
'color' => 'cyan',
|
||||
'sort_order' => 20,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'included_features' => [
|
||||
'products',
|
||||
'stock_tracking',
|
||||
'costs',
|
||||
'movements',
|
||||
'batches',
|
||||
'components',
|
||||
],
|
||||
],
|
||||
// =====================================================================
|
||||
// PROCESSING SUITE - Internal (Curagreen, Leopard AZ)
|
||||
// =====================================================================
|
||||
[
|
||||
'key' => 'processing',
|
||||
'name' => 'Processing Suite',
|
||||
'description' => 'Processing operations for extractors. Includes wash reports, extraction tracking, yields, and batch management.',
|
||||
'description' => 'Processing operations for extractors. Includes wash reports, extraction tracking, yields, batches, compliance (licenses, COAs), and processing analytics.',
|
||||
'icon' => 'lucide--flask-conical',
|
||||
'color' => 'blue',
|
||||
'sort_order' => 25,
|
||||
'sort_order' => 20,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'is_internal' => true,
|
||||
'included_features' => [
|
||||
// Core features
|
||||
'wash_reports',
|
||||
'extraction',
|
||||
'yields',
|
||||
'batches',
|
||||
'biomass_intake',
|
||||
'material_transfers',
|
||||
'work_orders',
|
||||
'processing_analytics',
|
||||
'compliance',
|
||||
'licenses',
|
||||
'coas',
|
||||
// Shared features
|
||||
'messaging',
|
||||
'procurement',
|
||||
'tools',
|
||||
],
|
||||
],
|
||||
|
||||
// =====================================================================
|
||||
// MANUFACTURING SUITE - Internal (Leopard AZ)
|
||||
// =====================================================================
|
||||
[
|
||||
'key' => 'manufacturing',
|
||||
'name' => 'Manufacturing Suite',
|
||||
'description' => 'Manufacturing operations for producers. Includes work orders, BOMs, packaging, and production management.',
|
||||
'description' => 'Manufacturing operations for producers. Includes work orders, BOM (Bill of Materials), packaging, labeling, SKU creation, lot tracking, compliance, and production management.',
|
||||
'icon' => 'lucide--factory',
|
||||
'color' => 'orange',
|
||||
'sort_order' => 30,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'is_internal' => true,
|
||||
'included_features' => [
|
||||
// Core features
|
||||
'work_orders',
|
||||
'bom',
|
||||
'bom', // BOM is Manufacturing only
|
||||
'packaging',
|
||||
'production',
|
||||
'labeling',
|
||||
'sku_creation',
|
||||
'lot_tracking',
|
||||
'production_queue',
|
||||
'manufacturing_analytics',
|
||||
'compliance',
|
||||
'licenses',
|
||||
'coas',
|
||||
'batches',
|
||||
// Shared features
|
||||
'messaging',
|
||||
'procurement',
|
||||
'tools',
|
||||
],
|
||||
],
|
||||
|
||||
// =====================================================================
|
||||
// DELIVERY SUITE - Internal (Leopard AZ)
|
||||
// =====================================================================
|
||||
[
|
||||
'key' => 'procurement',
|
||||
'name' => 'Procurement Suite',
|
||||
'description' => 'Purchasing and vendor management. Includes requisitions, purchase orders, and goods receiving.',
|
||||
'icon' => 'lucide--clipboard-list',
|
||||
'color' => 'indigo',
|
||||
'sort_order' => 35,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'included_features' => [
|
||||
'requisitions',
|
||||
'purchase_orders',
|
||||
'receiving',
|
||||
'vendors',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'distribution',
|
||||
'name' => 'Distribution Suite',
|
||||
'description' => 'Distribution and fulfillment operations. Includes pick/pack, manifests, routes, and proof of delivery.',
|
||||
'key' => 'delivery',
|
||||
'name' => 'Delivery Suite',
|
||||
'description' => 'Delivery and fulfillment operations. Includes pick/pack, manifests, delivery windows, drivers, vehicles, routes, proof of delivery, and delivery analytics.',
|
||||
'icon' => 'lucide--truck',
|
||||
'color' => 'violet',
|
||||
'sort_order' => 40,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'is_internal' => true,
|
||||
'included_features' => [
|
||||
// Core features
|
||||
'pick_pack',
|
||||
'manifests',
|
||||
'routes',
|
||||
'proof_of_delivery',
|
||||
'delivery_analytics',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'delivery',
|
||||
'name' => 'Delivery Suite',
|
||||
'description' => 'Delivery management for order fulfillment. Includes delivery windows, drivers, and vehicles.',
|
||||
'icon' => 'lucide--calendar-clock',
|
||||
'color' => 'teal',
|
||||
'sort_order' => 45,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'included_features' => [
|
||||
'delivery_windows',
|
||||
'drivers',
|
||||
'vehicles',
|
||||
'routes',
|
||||
'proof_of_delivery',
|
||||
'delivery_analytics',
|
||||
// Shared features
|
||||
'messaging',
|
||||
'procurement',
|
||||
'tools',
|
||||
],
|
||||
],
|
||||
|
||||
// Finance and compliance
|
||||
// =====================================================================
|
||||
// MANAGEMENT SUITE - Internal (Canopy - parent company)
|
||||
// =====================================================================
|
||||
[
|
||||
'key' => 'finance',
|
||||
'name' => 'Finance Suite',
|
||||
'description' => 'Financial management including AP, AR, invoicing, and reporting.',
|
||||
'icon' => 'lucide--banknote',
|
||||
'color' => 'green',
|
||||
'key' => 'management',
|
||||
'name' => 'Management Suite',
|
||||
'description' => 'Corporate management and financial oversight for Canopy. Includes AP/AR, budgets, cross-business analytics, inter-company ledger, financial reports, forecasting, and KPIs.',
|
||||
'icon' => 'lucide--briefcase',
|
||||
'color' => 'slate',
|
||||
'sort_order' => 50,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'is_internal' => true,
|
||||
'included_features' => [
|
||||
// Core features
|
||||
'org_dashboard',
|
||||
'cross_business_analytics',
|
||||
'accounts_payable',
|
||||
'accounts_receivable',
|
||||
'invoicing',
|
||||
'reports',
|
||||
'budgets',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'compliance',
|
||||
'name' => 'Compliance Suite',
|
||||
'description' => 'Regulatory compliance tools including licenses, COAs, and compliance manifests.',
|
||||
'icon' => 'lucide--shield-check',
|
||||
'color' => 'amber',
|
||||
'sort_order' => 55,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'included_features' => [
|
||||
'licenses',
|
||||
'coas',
|
||||
'manifests',
|
||||
'reports',
|
||||
'budget_approvals',
|
||||
'inter_company_ledger',
|
||||
'financial_reports',
|
||||
'forecasting',
|
||||
'kpis',
|
||||
'usage_billing',
|
||||
// Shared features
|
||||
'messaging',
|
||||
'tools',
|
||||
],
|
||||
],
|
||||
|
||||
// Communication and tools
|
||||
// =====================================================================
|
||||
// BRAND MANAGER SUITE - External brand partners (view-only)
|
||||
// =====================================================================
|
||||
[
|
||||
'key' => 'inbox',
|
||||
'name' => 'Inbox Suite',
|
||||
'description' => 'Unified messaging and communication tools.',
|
||||
'icon' => 'lucide--mail',
|
||||
'color' => 'sky',
|
||||
'key' => 'brand_manager',
|
||||
'name' => 'Brand Manager Suite',
|
||||
'description' => 'View-only portal for external brand teams. Access to assigned brand sales, orders, products, inventory, promotions, conversations, and analytics. All data auto-scoped by brand_id.',
|
||||
'icon' => 'lucide--user-check',
|
||||
'color' => 'teal',
|
||||
'sort_order' => 60,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'included_features' => [
|
||||
'messages',
|
||||
'contacts',
|
||||
'templates',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'tools',
|
||||
'name' => 'Tools Suite',
|
||||
'description' => 'Business administration tools including settings, users, departments, and integrations.',
|
||||
'icon' => 'lucide--settings',
|
||||
'color' => 'slate',
|
||||
'sort_order' => 65,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'included_features' => [
|
||||
'settings',
|
||||
'users',
|
||||
'departments',
|
||||
'integrations',
|
||||
'audit_log',
|
||||
],
|
||||
],
|
||||
|
||||
// Management and enterprise
|
||||
[
|
||||
'key' => 'management',
|
||||
'name' => 'Management Suite',
|
||||
'description' => 'Corporate management and financial oversight. Includes cross-business analytics, finance, and KPIs.',
|
||||
'icon' => 'lucide--briefcase',
|
||||
'color' => 'gray',
|
||||
'sort_order' => 70,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'included_features' => [
|
||||
'cross_business_analytics',
|
||||
'finance',
|
||||
'forecasting',
|
||||
'kpis',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'dispensary',
|
||||
'name' => 'Dispensary Suite',
|
||||
'description' => 'Retail and dispensary operations. Marketplace access, ordering, and buyer portal.',
|
||||
'icon' => 'lucide--store',
|
||||
'color' => 'lime',
|
||||
'sort_order' => 80,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'included_features' => [
|
||||
'marketplace',
|
||||
'ordering',
|
||||
'buyer_portal',
|
||||
'promotions',
|
||||
],
|
||||
],
|
||||
|
||||
// Brand Manager Suite - External brand team access
|
||||
[
|
||||
'key' => 'brand_manager',
|
||||
'name' => 'Brand Manager',
|
||||
'description' => 'View-only portal for external brand teams. Access to assigned brand sales, products, inventory, promotions, and conversations. Auto-scoped by brand_id.',
|
||||
'icon' => 'lucide--user-check',
|
||||
'color' => 'teal',
|
||||
'sort_order' => 85,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'included_features' => [
|
||||
// Core features (all read-only, brand-scoped)
|
||||
'view_sales',
|
||||
'view_orders',
|
||||
'view_products',
|
||||
'view_inventory',
|
||||
'view_promotions',
|
||||
'view_conversations',
|
||||
'view_buyers',
|
||||
'view_analytics',
|
||||
'brand_scoped', // Flag indicating all data is brand-scoped
|
||||
// Communication only
|
||||
'messaging', // Their brand only
|
||||
// Explicitly NOT available: view_costs, view_margin, edit_*, manage_*
|
||||
],
|
||||
],
|
||||
|
||||
// Note: Enterprise is deprecated as a suite - it's now a plan limit override.
|
||||
// Use is_enterprise_plan on Business model instead.
|
||||
// This entry is kept for reference but marked as deprecated and inactive.
|
||||
// =====================================================================
|
||||
// DISPENSARY SUITE - Buyers (dispensaries/retailers)
|
||||
// =====================================================================
|
||||
[
|
||||
'key' => 'dispensary',
|
||||
'name' => 'Dispensary Suite',
|
||||
'description' => 'For buyer businesses (dispensaries, retailers). Includes marketplace access, ordering, buyer portal, and promotions.',
|
||||
'icon' => 'lucide--store',
|
||||
'color' => 'lime',
|
||||
'sort_order' => 70,
|
||||
'is_active' => true,
|
||||
'is_internal' => false,
|
||||
'included_features' => [
|
||||
// Core features
|
||||
'marketplace',
|
||||
'ordering',
|
||||
'buyer_portal',
|
||||
'promotions',
|
||||
'favorites',
|
||||
// Shared features
|
||||
'messaging',
|
||||
'tools',
|
||||
],
|
||||
],
|
||||
|
||||
// =====================================================================
|
||||
// DEPRECATED SUITES - Marked inactive, kept for reference
|
||||
// =====================================================================
|
||||
[
|
||||
'key' => 'marketing',
|
||||
'name' => 'Marketing Suite (DEPRECATED)',
|
||||
'description' => 'DEPRECATED: Merged into Sales Suite. Marketing features are now part of Sales.',
|
||||
'icon' => 'lucide--megaphone',
|
||||
'color' => 'pink',
|
||||
'sort_order' => 900,
|
||||
'is_active' => false,
|
||||
'is_internal' => true,
|
||||
'is_deprecated' => true,
|
||||
'included_features' => ['deprecated'],
|
||||
],
|
||||
[
|
||||
'key' => 'inventory',
|
||||
'name' => 'Inventory Suite (DEPRECATED)',
|
||||
'description' => 'DEPRECATED: Merged into Sales Suite. Inventory features are now part of Sales.',
|
||||
'icon' => 'lucide--package',
|
||||
'color' => 'cyan',
|
||||
'sort_order' => 901,
|
||||
'is_active' => false,
|
||||
'is_internal' => true,
|
||||
'is_deprecated' => true,
|
||||
'included_features' => ['deprecated'],
|
||||
],
|
||||
[
|
||||
'key' => 'procurement',
|
||||
'name' => 'Procurement Suite (DEPRECATED)',
|
||||
'description' => 'DEPRECATED: Procurement is now a shared feature available to all operational suites.',
|
||||
'icon' => 'lucide--clipboard-list',
|
||||
'color' => 'indigo',
|
||||
'sort_order' => 902,
|
||||
'is_active' => false,
|
||||
'is_internal' => true,
|
||||
'is_deprecated' => true,
|
||||
'included_features' => ['deprecated'],
|
||||
],
|
||||
[
|
||||
'key' => 'distribution',
|
||||
'name' => 'Distribution Suite (DEPRECATED)',
|
||||
'description' => 'DEPRECATED: Merged into Delivery Suite.',
|
||||
'icon' => 'lucide--truck',
|
||||
'color' => 'violet',
|
||||
'sort_order' => 903,
|
||||
'is_active' => false,
|
||||
'is_internal' => true,
|
||||
'is_deprecated' => true,
|
||||
'included_features' => ['deprecated'],
|
||||
],
|
||||
[
|
||||
'key' => 'finance',
|
||||
'name' => 'Finance Suite (DEPRECATED)',
|
||||
'description' => 'DEPRECATED: Merged into Management Suite. Finance features are now part of Management (Canopy).',
|
||||
'icon' => 'lucide--banknote',
|
||||
'color' => 'green',
|
||||
'sort_order' => 904,
|
||||
'is_active' => false,
|
||||
'is_internal' => true,
|
||||
'is_deprecated' => true,
|
||||
'included_features' => ['deprecated'],
|
||||
],
|
||||
[
|
||||
'key' => 'compliance',
|
||||
'name' => 'Compliance Suite (DEPRECATED)',
|
||||
'description' => 'DEPRECATED: Compliance features moved to Processing and Manufacturing suites.',
|
||||
'icon' => 'lucide--shield-check',
|
||||
'color' => 'amber',
|
||||
'sort_order' => 905,
|
||||
'is_active' => false,
|
||||
'is_internal' => true,
|
||||
'is_deprecated' => true,
|
||||
'included_features' => ['deprecated'],
|
||||
],
|
||||
[
|
||||
'key' => 'inbox',
|
||||
'name' => 'Inbox Suite (DEPRECATED)',
|
||||
'description' => 'DEPRECATED: Messaging is now a shared feature available to all suites.',
|
||||
'icon' => 'lucide--mail',
|
||||
'color' => 'sky',
|
||||
'sort_order' => 906,
|
||||
'is_active' => false,
|
||||
'is_internal' => true,
|
||||
'is_deprecated' => true,
|
||||
'included_features' => ['deprecated'],
|
||||
],
|
||||
[
|
||||
'key' => 'tools',
|
||||
'name' => 'Tools Suite (DEPRECATED)',
|
||||
'description' => 'DEPRECATED: Tools (settings, users, departments) is now a shared feature available to all suites.',
|
||||
'icon' => 'lucide--settings',
|
||||
'color' => 'slate',
|
||||
'sort_order' => 907,
|
||||
'is_active' => false,
|
||||
'is_internal' => true,
|
||||
'is_deprecated' => true,
|
||||
'included_features' => ['deprecated'],
|
||||
],
|
||||
[
|
||||
'key' => 'enterprise',
|
||||
'name' => 'Enterprise Suite (DEPRECATED)',
|
||||
'description' => 'DEPRECATED: Enterprise is now a plan override (is_enterprise_plan), not a suite. Usage limits are disabled for Enterprise businesses, but feature access is still controlled by suites and departments.',
|
||||
'description' => 'DEPRECATED: Enterprise is now a plan override flag (is_enterprise_plan) on Business, not a suite.',
|
||||
'icon' => 'lucide--crown',
|
||||
'color' => 'gold',
|
||||
'sort_order' => 100,
|
||||
'is_active' => false, // Disabled - use is_enterprise_plan instead
|
||||
'sort_order' => 999,
|
||||
'is_active' => false,
|
||||
'is_internal' => true,
|
||||
'is_deprecated' => true, // Marked as deprecated
|
||||
'included_features' => [
|
||||
'deprecated',
|
||||
],
|
||||
'is_deprecated' => true,
|
||||
'included_features' => ['deprecated'],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -298,5 +356,7 @@ class SuitesSeeder extends Seeder
|
||||
$suite
|
||||
);
|
||||
}
|
||||
|
||||
$this->command->info('Suites seeded: 7 active, 9 deprecated');
|
||||
}
|
||||
}
|
||||
|
||||
549
docs/SUITES_AND_PRICING_MODEL.md
Normal file
549
docs/SUITES_AND_PRICING_MODEL.md
Normal file
@@ -0,0 +1,549 @@
|
||||
# Suites & Pricing Model – Architecture Overview
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This document defines:
|
||||
|
||||
- The **Suite architecture** (7 Suites total)
|
||||
- How each business (Cannabrands, Curagreen, Leopard AZ, Canopy) is mapped to Suites
|
||||
- How menus/navigation should eventually be suite-driven
|
||||
- The **pricing and usage model** for the Sales Suite
|
||||
- **Procurement flow** between subdivisions and Canopy
|
||||
- **Cross-division access** for users
|
||||
- Ground rules to prevent breaking existing behavior while we evolve the system
|
||||
|
||||
**This is the ground truth for implementation and future design.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Corporate Structure
|
||||
|
||||
Canopy is the parent management company. Cannabrands, Curagreen, and Leopard AZ are subdivisions.
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ CANOPY │
|
||||
│ (Parent Co) │
|
||||
│ │
|
||||
│ • AP/AR │
|
||||
│ • Finance │
|
||||
│ • Budgets │
|
||||
│ • Approvals │
|
||||
└────────┬────────┘
|
||||
│ owns
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ Curagreen │ │ Leopard AZ │ │ Cannabrands │
|
||||
│ (Processing) │ │ (Mfg/Deliv) │ │ (Sales) │
|
||||
│ │ │ │ │ │
|
||||
│ Operations │ │ Operations │ │ Operations │
|
||||
│ only │ │ only │ │ only │
|
||||
└───────────────┘ └───────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
**Key principle:** All money flows through Canopy. Subdivisions handle operations; Canopy handles finance.
|
||||
|
||||
---
|
||||
|
||||
## 3. Suites Overview (7 Total)
|
||||
|
||||
We model functionality in high-level **Suites**, not scattered feature flags.
|
||||
|
||||
| # | Suite | Target User | Priced? |
|
||||
|---|-------|-------------|---------|
|
||||
| 1 | **Sales Suite** | External customers (Brands) | Yes - $495/mo |
|
||||
| 2 | **Processing Suite** | Internal (Curagreen, Leopard AZ) | No |
|
||||
| 3 | **Manufacturing Suite** | Internal (Leopard AZ) | No |
|
||||
| 4 | **Delivery Suite** | Internal (Leopard AZ) | No |
|
||||
| 5 | **Management Suite** | Internal (Canopy) | No |
|
||||
| 6 | **Brand Manager Suite** | External (brand partners) | No |
|
||||
| 7 | **Dispensary Suite** | Buyers (dispensaries/retailers) | No |
|
||||
|
||||
**Note:** Enterprise Plan is NOT a suite - it's a flag (`is_enterprise_plan`) that removes usage limits.
|
||||
|
||||
### 3.1 Sales Suite (Usage-Tiered, Sold to Customers)
|
||||
|
||||
The commercial brain for brands and sales orgs.
|
||||
|
||||
**Core Features:**
|
||||
- Products & Inventory
|
||||
- Orders & Invoicing
|
||||
- Menus & Promotions
|
||||
- Campaigns & Marketing
|
||||
- Buyers & Accounts (CRM)
|
||||
- Buyer Intelligence
|
||||
- Analytics
|
||||
- Automations & Orchestrator
|
||||
- Copilot / AI Assist
|
||||
- Batches (from supplied COAs)
|
||||
|
||||
**Shared Features (all suites get these):**
|
||||
- Messaging (omnichannel)
|
||||
- Procurement (vendors, requisitions, POs, receiving)
|
||||
- Tools (settings, users, departments, integrations)
|
||||
|
||||
**Only the Sales Suite is priced and usage-tiered.**
|
||||
|
||||
### 3.2 Processing Suite (Internal / Enterprise Only)
|
||||
|
||||
Used by processing entities (e.g., Curagreen, Leopard AZ solventless).
|
||||
|
||||
**Core Features:**
|
||||
- Biomass intake
|
||||
- Batches and runs
|
||||
- BHO extraction (Curagreen)
|
||||
- Solventless extraction – washing & pressing (Leopard AZ)
|
||||
- Yield tracking & conversions
|
||||
- Material transfers between entities
|
||||
- Work Orders from Sales → Processing
|
||||
- Processing analytics
|
||||
- Compliance (licenses, COAs)
|
||||
|
||||
**Shared Features:**
|
||||
- Messaging
|
||||
- Procurement
|
||||
- Tools
|
||||
|
||||
### 3.3 Manufacturing Suite (Internal / Enterprise Only)
|
||||
|
||||
Used by manufacturing entities (e.g., Leopard AZ packaging & production).
|
||||
|
||||
**Core Features:**
|
||||
- Work Orders from Sales/Processing
|
||||
- BOM (Bill of Materials) - **Manufacturing only**
|
||||
- Packaging & labeling
|
||||
- SKU creation from batches
|
||||
- Lot tracking
|
||||
- Production queues & status
|
||||
- Manufacturing analytics
|
||||
- Compliance (licenses, COAs)
|
||||
- Batches
|
||||
|
||||
**Shared Features:**
|
||||
- Messaging
|
||||
- Procurement
|
||||
- Tools
|
||||
|
||||
### 3.4 Delivery Suite (Internal / Enterprise Only)
|
||||
|
||||
Used by delivery/fulfillment operations (e.g., Leopard AZ).
|
||||
|
||||
**Core Features:**
|
||||
- Pick/Pack screens
|
||||
- Delivery manifests
|
||||
- Delivery windows
|
||||
- Drivers & vehicles
|
||||
- Route management
|
||||
- Proof of delivery (POD)
|
||||
- Delivery analytics
|
||||
|
||||
**Shared Features:**
|
||||
- Messaging
|
||||
- Procurement
|
||||
- Tools
|
||||
|
||||
### 3.5 Management Suite (Internal / Canopy Only)
|
||||
|
||||
Used by top-level management (Canopy as parent company).
|
||||
|
||||
**Core Features:**
|
||||
- Org-wide dashboard
|
||||
- Cross-business analytics
|
||||
- AP (Accounts Payable) - all vendor bills, PO payments
|
||||
- AR (Accounts Receivable) - all customer invoices, collections
|
||||
- Budget Management - set budgets per subdivision
|
||||
- Budget Exception Approvals - approve POs that exceed budget
|
||||
- Inter-company Ledger - track transfers between subdivisions
|
||||
- Financial Reports - P&L, balance sheet, cash flow
|
||||
- Forecasting
|
||||
- Operational KPIs
|
||||
- Usage & billing analytics
|
||||
|
||||
**Shared Features:**
|
||||
- Messaging
|
||||
- Tools
|
||||
|
||||
### 3.6 Brand Manager Suite (External Partners)
|
||||
|
||||
View-only portal for external brand teams to see their brand performance.
|
||||
|
||||
**Core Features (all read-only, brand-scoped):**
|
||||
- View sales history
|
||||
- View orders
|
||||
- View products
|
||||
- View inventory status
|
||||
- View promotions
|
||||
- View conversations
|
||||
- View buyer accounts
|
||||
- View brand-level analytics
|
||||
|
||||
**Communication:**
|
||||
- Messaging (their brand only - can send/receive)
|
||||
|
||||
**Explicitly NOT available:**
|
||||
- View costs, margins, wholesale pricing
|
||||
- Edit products, create promotions
|
||||
- Manage settings
|
||||
- View other brands
|
||||
|
||||
### 3.7 Dispensary Suite (Buyers)
|
||||
|
||||
For buyer businesses (dispensaries, retailers) accessing the marketplace.
|
||||
|
||||
**Core Features:**
|
||||
- Marketplace access
|
||||
- Ordering
|
||||
- Buyer portal
|
||||
- Promotions (view/redeem)
|
||||
|
||||
**Shared Features:**
|
||||
- Messaging
|
||||
- Tools
|
||||
|
||||
---
|
||||
|
||||
## 4. Shared Features (All Suites)
|
||||
|
||||
These features are available to ALL operational suites:
|
||||
|
||||
### 4.1 Messaging
|
||||
|
||||
All suites get omnichannel messaging capabilities.
|
||||
|
||||
### 4.2 Procurement
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| Vendors & Suppliers | Each subdivision manages their own |
|
||||
| Requisitions | Request purchases |
|
||||
| Purchase Orders | Create POs |
|
||||
| Goods Receiving | Confirm deliveries |
|
||||
|
||||
### 4.3 Tools
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| Settings | Business settings |
|
||||
| Users | User management |
|
||||
| Departments | Department structure |
|
||||
| Integrations | Third-party integrations |
|
||||
| Audit Log | Activity tracking |
|
||||
|
||||
---
|
||||
|
||||
## 5. Procurement & Finance Flow
|
||||
|
||||
### 5.1 Who Can Do What
|
||||
|
||||
| Role | Create Requisitions | Create POs | Approve Requisitions | Pay (via Canopy) |
|
||||
|------|---------------------|------------|----------------------|------------------|
|
||||
| Department Managers | ✅ | ✅ | ✅ (their dept) | ❌ |
|
||||
| Owners/Admins | ✅ | ✅ | ✅ (all) | ❌ |
|
||||
| Canopy Finance | ✅ | ✅ | ✅ (budget exceptions) | ✅ |
|
||||
|
||||
### 5.2 Flow
|
||||
|
||||
**Department Manager or Owner/Admin:**
|
||||
```
|
||||
Creates REQUISITION or PO directly
|
||||
└─▶ Canopy AP pays when invoiced
|
||||
```
|
||||
|
||||
**Budget Exception (PO exceeds subdivision budget):**
|
||||
```
|
||||
PO created
|
||||
└─▶ Flagged as over budget
|
||||
└─▶ Canopy must approve
|
||||
└─▶ Canopy AP pays
|
||||
```
|
||||
|
||||
### 5.3 Canopy's Role
|
||||
|
||||
Canopy **doesn't approve routine POs** - they just handle the money:
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| AP Processing | Pay vendor invoices matched to POs |
|
||||
| AR Collections | Collect customer payments |
|
||||
| Budget Management | Set budgets per subdivision |
|
||||
| Budget Exception Approvals | Only intervene when PO exceeds budget |
|
||||
| Financial Reporting | Consolidated view across all subdivisions |
|
||||
|
||||
---
|
||||
|
||||
## 6. Cross-Division Access
|
||||
|
||||
Users can access multiple businesses via department assignments.
|
||||
|
||||
### 6.1 How It Works
|
||||
|
||||
1. **Add user to another business** via `business_user` pivot
|
||||
2. **Assign them to a department** in that business
|
||||
3. **Department has suite permissions** that control what they can do
|
||||
|
||||
### 6.2 Example
|
||||
|
||||
```
|
||||
Sarah (User)
|
||||
├── Business: Cannabrands (primary)
|
||||
│ └── Department: Sales Team
|
||||
│ └── Suite: Sales (full permissions)
|
||||
│
|
||||
└── Business: Leopard AZ (cross-division)
|
||||
└── Department: Manufacturing
|
||||
└── Suite: Manufacturing (manage_bom only)
|
||||
```
|
||||
|
||||
### 6.3 Permission Hierarchy
|
||||
|
||||
```
|
||||
User Permission =
|
||||
Business Role (owner gets all)
|
||||
+ Department Permissions (inherited from department's suite permissions)
|
||||
+ Individual Overrides (grants/revokes specific to user)
|
||||
```
|
||||
|
||||
| Role | Access Level |
|
||||
|------|--------------|
|
||||
| **Owner** | Full access to everything |
|
||||
| **Admin** | Full access except some owner-only actions |
|
||||
| **Manager** | Department permissions + can manage team |
|
||||
| **Member** | Department permissions only |
|
||||
|
||||
---
|
||||
|
||||
## 7. Business → Suite Mapping
|
||||
|
||||
### 7.1 Cannabrands
|
||||
|
||||
- **Role:** Sales & brand representation
|
||||
- **Suites:** Sales Suite
|
||||
- **Plan:** Enterprise (internal)
|
||||
|
||||
### 7.2 Curagreen
|
||||
|
||||
- **Role:** Processing – BHO extraction
|
||||
- **Suites:** Processing Suite
|
||||
- **Plan:** Enterprise (internal)
|
||||
|
||||
### 7.3 Leopard AZ
|
||||
|
||||
- **Role:** Solventless processing + Manufacturing + Delivery
|
||||
- **Suites:**
|
||||
- Processing Suite (solventless)
|
||||
- Manufacturing Suite
|
||||
- Delivery Suite
|
||||
- **Plan:** Enterprise (internal)
|
||||
|
||||
### 7.4 Canopy
|
||||
|
||||
- **Role:** Parent company - financial & management oversight
|
||||
- **Suites:** Management Suite
|
||||
- **Plan:** Enterprise (internal)
|
||||
|
||||
---
|
||||
|
||||
## 8. Suite-Driven Navigation (Target State)
|
||||
|
||||
When a user logs in:
|
||||
|
||||
1. We determine their **Business**
|
||||
2. We read the **Suites** assigned to that Business
|
||||
3. We check user's **Department permissions** for each suite
|
||||
4. We build the sidebar/navigation based on accessible features
|
||||
|
||||
### 8.1 Sales Suite Menu
|
||||
|
||||
- Dashboard
|
||||
- Brands
|
||||
- Products & Inventory
|
||||
- Orders
|
||||
- Menus
|
||||
- Promotions
|
||||
- Buyers & Accounts (CRM)
|
||||
- Conversations
|
||||
- Campaigns
|
||||
- Automations
|
||||
- Copilot
|
||||
- Analytics
|
||||
- Settings
|
||||
|
||||
### 8.2 Processing Suite Menu
|
||||
|
||||
- Dashboard
|
||||
- Batches / Runs
|
||||
- Biomass Intake
|
||||
- Washing / Pressing (solventless)
|
||||
- Extraction Runs (BHO)
|
||||
- Yields
|
||||
- Material Transfers
|
||||
- Work Orders
|
||||
- Compliance (Licenses, COAs)
|
||||
- Processing Analytics
|
||||
- Settings
|
||||
|
||||
### 8.3 Manufacturing Suite Menu
|
||||
|
||||
- Dashboard
|
||||
- Work Orders
|
||||
- BOM
|
||||
- Packaging
|
||||
- Labeling
|
||||
- SKU Creation
|
||||
- Lot Tracking
|
||||
- Production Queue
|
||||
- Compliance (Licenses, COAs)
|
||||
- Manufacturing Analytics
|
||||
- Settings
|
||||
|
||||
### 8.4 Delivery Suite Menu
|
||||
|
||||
- Dashboard
|
||||
- Pick & Pack
|
||||
- Delivery Windows
|
||||
- Manifests
|
||||
- Drivers & Vehicles
|
||||
- Routes
|
||||
- Proof of Delivery
|
||||
- Delivery Analytics
|
||||
- Settings
|
||||
|
||||
### 8.5 Management Suite Menu (Canopy)
|
||||
|
||||
- Org Dashboard
|
||||
- Cross-Business Analytics
|
||||
- Finance
|
||||
- Accounts Payable
|
||||
- Accounts Receivable
|
||||
- Budgets
|
||||
- Inter-company Ledger
|
||||
- Forecasting
|
||||
- Operations Overview
|
||||
- Usage & Billing
|
||||
- Settings
|
||||
|
||||
### 8.6 Brand Manager Menu
|
||||
|
||||
- Brand Dashboard
|
||||
- Sales History
|
||||
- Orders
|
||||
- Products
|
||||
- Inventory
|
||||
- Promotions
|
||||
- Conversations
|
||||
- Analytics
|
||||
|
||||
### 8.7 Dispensary Suite Menu
|
||||
|
||||
- Marketplace
|
||||
- My Orders
|
||||
- Favorites
|
||||
- Promotions
|
||||
- Messages
|
||||
- Settings
|
||||
|
||||
---
|
||||
|
||||
## 9. Sales Suite Pricing & Usage Model
|
||||
|
||||
### 9.1 Pricing
|
||||
|
||||
**Base Sales Suite Plan:**
|
||||
|
||||
- **$495 / month**
|
||||
- Includes **1 brand**
|
||||
|
||||
**Additional Brands:**
|
||||
|
||||
- **$195 / month** per additional brand
|
||||
- Each additional brand comes with its own usage allotment
|
||||
|
||||
**No public free tier.** Enterprise internal plan is not sold.
|
||||
|
||||
### 9.2 Included Features (Per Brand)
|
||||
|
||||
Each brand under the Sales Suite gets:
|
||||
|
||||
- Full Inventory & Product Management
|
||||
- Menus & Promotions
|
||||
- Buyers & Accounts (CRM)
|
||||
- Conversations & Messaging
|
||||
- Marketing & Campaign Tools
|
||||
- Buyer Intelligence
|
||||
- Analytics (unlimited views)
|
||||
- Automations & Orchestrator
|
||||
- Copilot (AI-assisted workflows)
|
||||
|
||||
### 9.3 Usage Limits (Per Brand) – Initial Targets
|
||||
|
||||
| Resource | Limit |
|
||||
|----------|-------|
|
||||
| **SKUs** | 15 SKUs per brand |
|
||||
| **Menu Sends** | 100 menu sends per month |
|
||||
| **Promotion Impressions** | 1,000 promotion impressions per month |
|
||||
| **Messaging** | 500 messages per month (SMS, email, in-app, WhatsApp combined) |
|
||||
| **AI Credits** | 1,000 AI credits per brand per month |
|
||||
| **Buyer & CRM Contacts** | 1,000 buyers/contacts per brand |
|
||||
| **Analytics** | Unlimited analytics views |
|
||||
|
||||
### 9.4 Add-ons (Future)
|
||||
|
||||
- Extra SKU packs (+10, +25, +100)
|
||||
- Extra menu send packs
|
||||
- Extra promotion impression packs
|
||||
- Extra CRM contact packs
|
||||
- Extra AI credit packs
|
||||
- Extra messaging volume
|
||||
|
||||
---
|
||||
|
||||
## 10. Enterprise Plan
|
||||
|
||||
**Enterprise is NOT a Suite** - it's a billing/limit override.
|
||||
|
||||
Enterprise Plan (`is_enterprise_plan = true`) means:
|
||||
- **Usage limits are bypassed** (brands, SKUs, contacts, messages, AI credits, etc.)
|
||||
- **Feature access is still controlled by assigned Suites**
|
||||
|
||||
A business with Enterprise Plan still needs actual Suites assigned to determine which features/menus are available.
|
||||
|
||||
**Used only for internal operations (Cannabrands, Curagreen, Leopard AZ, Canopy).**
|
||||
|
||||
---
|
||||
|
||||
## 11. Safety & Backward Compatibility
|
||||
|
||||
1. **Existing navigation and behavior must be preserved** until suite-based nav is explicitly enabled.
|
||||
|
||||
2. **Changes should be additive:**
|
||||
- New DB columns for Suites and usage
|
||||
- New services for Suite→Menu and usage tracking
|
||||
- Config flag `app.suites.enabled`
|
||||
|
||||
3. **Migrations must be reversible.**
|
||||
|
||||
4. **Feature-flag toggles** allow:
|
||||
- Switching between current menu and suite-driven menu
|
||||
- Disabling suite enforcement if issues arise
|
||||
|
||||
5. **Legacy module flags** (`has_crm`, `has_marketing`, etc.) remain in "Advanced Overrides" admin area until fully migrated.
|
||||
|
||||
---
|
||||
|
||||
## 12. Implementation Phases
|
||||
|
||||
| Phase | Description |
|
||||
|-------|-------------|
|
||||
| **Phase 1** | ✅ Suites & Docs – Define Suites, business mapping, and pricing |
|
||||
| **Phase 2** | Suite consolidation – Reduce to 7 suites, update seeder and permissions |
|
||||
| **Phase 3** | User Management UI – Settings > Users with full permission control |
|
||||
| **Phase 4** | Suite-Driven Menu Resolver – Implement Suite→Menu mapping behind flag |
|
||||
| **Phase 5** | Usage Counters – Track menu sends, messages, AI usage, contacts, SKUs |
|
||||
| **Phase 6** | Usage Enforcement & Warnings – Soft and hard limits, usage dashboards |
|
||||
| **Phase 7** | Public Pricing Site – Marketing-facing pricing components |
|
||||
|
||||
---
|
||||
|
||||
**This document is the canonical reference for suite architecture.**
|
||||
162
docs/USER_MANAGEMENT.md
Normal file
162
docs/USER_MANAGEMENT.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# User Management System
|
||||
|
||||
## Overview
|
||||
|
||||
The user management system in Settings > Users allows business owners and admins to manage team members, their roles, department assignments, and permissions.
|
||||
|
||||
## Role Hierarchy
|
||||
|
||||
Users are assigned one of four roles that determine their base level of access:
|
||||
|
||||
| Role | Access Level | Description |
|
||||
|------|--------------|-------------|
|
||||
| **Owner** | Full | Complete access to everything. Cannot be modified. |
|
||||
| **Admin** | Nearly Full | Full access except owner-only actions (transfer ownership, delete business) |
|
||||
| **Manager** | Department + Team | Department permissions plus ability to manage team members and approve requests |
|
||||
| **Member** | Department Only | Standard team member with department-based permissions only |
|
||||
|
||||
## Permission System
|
||||
|
||||
Permissions are determined by a combination of:
|
||||
|
||||
1. **Business Role** - Owner/Admin gets full access
|
||||
2. **Department Membership** - Controls access to suite features
|
||||
3. **Department Role** - Determines level within department (operator, lead, supervisor, manager)
|
||||
4. **Permission Overrides** - Individual grants/revokes for specific users
|
||||
|
||||
### Permission Calculation
|
||||
|
||||
```
|
||||
Effective Permissions =
|
||||
Business Role Permissions
|
||||
+ Department Suite Permissions (based on membership)
|
||||
+ Individual Permission Overrides
|
||||
```
|
||||
|
||||
## Department Roles
|
||||
|
||||
Within each department, users can have one of four roles:
|
||||
|
||||
| Department Role | Description |
|
||||
|-----------------|-------------|
|
||||
| **Operator** | Basic access - can view and perform assigned tasks |
|
||||
| **Lead** | Can assign work to other operators |
|
||||
| **Supervisor** | Can approve requests and escalations |
|
||||
| **Manager** | Full department access - can manage all department functions |
|
||||
|
||||
## Suite-Based Permissions
|
||||
|
||||
Permissions are organized by the suites assigned to the business. Each suite has feature-specific permissions grouped by functional area:
|
||||
|
||||
### Sales Suite Permissions
|
||||
|
||||
- **Dashboard & Analytics**: view_dashboard, view_analytics, export_analytics
|
||||
- **Products & Inventory**: view_products, manage_products, view_inventory, adjust_inventory, view_costs, view_margin
|
||||
- **Orders & Invoicing**: view_orders, create_orders, manage_orders, view_invoices, create_invoices
|
||||
- **Menus & Promotions**: view_menus, manage_menus, view_promotions, manage_promotions
|
||||
- **Campaigns & Marketing**: view_campaigns, manage_campaigns, send_campaigns, manage_templates
|
||||
- **CRM & Accounts**: view_pipeline, edit_pipeline, manage_accounts, view_buyer_intelligence
|
||||
- **Automations & AI**: view_automations, manage_automations, use_copilot
|
||||
- **Batches**: view_batches, manage_batches
|
||||
- **Messaging**: view_conversations, send_messages, manage_contacts
|
||||
- **Procurement**: view_vendors, manage_vendors, view_requisitions, create_requisitions, approve_requisitions, view_purchase_orders, create_purchase_orders, receive_goods
|
||||
- **Tools & Settings**: manage_settings, manage_users, manage_departments, view_audit_log, manage_integrations
|
||||
|
||||
### Processing Suite Permissions
|
||||
|
||||
- **Dashboard & Analytics**: view_dashboard, view_analytics
|
||||
- **Batches**: view_batches, manage_batches, create_batches
|
||||
- **Processing Operations**: view_wash_reports, create_wash_reports, edit_wash_reports, manage_extractions, view_yields, manage_biomass_intake, manage_material_transfers
|
||||
- **Work Orders**: view_work_orders, create_work_orders, manage_work_orders
|
||||
- **Compliance**: view_compliance, manage_licenses, manage_coas, view_compliance_reports
|
||||
- **Messaging**: view_conversations, send_messages, manage_contacts
|
||||
- **Procurement**: (same as Sales)
|
||||
- **Tools & Settings**: manage_settings, manage_users, manage_departments, view_audit_log
|
||||
|
||||
### Manufacturing Suite Permissions
|
||||
|
||||
- **Dashboard & Analytics**: view_dashboard, view_analytics
|
||||
- **Manufacturing**: view_bom, manage_bom, create_bom, view_production_queue, manage_production, manage_packaging, manage_labeling, create_skus, manage_lot_tracking
|
||||
- **Work Orders**: view_work_orders, create_work_orders, manage_work_orders
|
||||
- **Batches**: view_batches, manage_batches
|
||||
- **Compliance**: view_compliance, manage_licenses, manage_coas, view_compliance_reports
|
||||
- **Messaging**: view_conversations, send_messages, manage_contacts
|
||||
- **Procurement**: (same as Sales)
|
||||
- **Tools & Settings**: manage_settings, manage_users, manage_departments, view_audit_log
|
||||
|
||||
### Delivery Suite Permissions
|
||||
|
||||
- **Dashboard & Analytics**: view_dashboard, view_analytics
|
||||
- **Delivery & Fulfillment**: view_pick_pack, manage_pick_pack, view_manifests, create_manifests, view_delivery_windows, manage_delivery_windows, view_drivers, manage_drivers, view_vehicles, manage_vehicles, view_routes, manage_routing, view_deliveries, complete_deliveries, manage_proof_of_delivery
|
||||
- **Messaging**: view_conversations, send_messages, manage_contacts
|
||||
- **Procurement**: (same as Sales)
|
||||
- **Tools & Settings**: manage_settings, manage_users, manage_departments, view_audit_log
|
||||
|
||||
### Management Suite Permissions (Canopy)
|
||||
|
||||
- **Dashboard & Analytics**: view_org_dashboard, view_cross_business, view_all_analytics
|
||||
- **Finance & Budgets**: view_ap, manage_ap, pay_bills, view_ar, manage_ar, view_budgets, manage_budgets, approve_budget_exceptions, view_inter_company_ledger, manage_inter_company, view_financial_reports, export_financial_data, view_forecasting, manage_forecasting, view_kpis, view_usage_billing, manage_billing
|
||||
- **Messaging**: view_conversations, send_messages, manage_contacts
|
||||
- **Tools & Settings**: manage_settings, manage_users, manage_departments, view_audit_log, manage_integrations
|
||||
|
||||
### Brand Manager Suite Permissions
|
||||
|
||||
- **Dashboard & Analytics**: view_dashboard, view_analytics
|
||||
- **Brand Data**: view_sales, view_orders, view_products, view_inventory, view_promotions, view_conversations, view_buyers
|
||||
- **Messaging**: send_messages (brand-scoped)
|
||||
|
||||
### Dispensary Suite Permissions
|
||||
|
||||
- **Marketplace**: view_marketplace, browse_products, view_promotions, manage_cart, manage_favorites
|
||||
- **Orders**: create_orders, view_orders
|
||||
- **Buyer Portal**: view_buyer_portal, view_account
|
||||
- **Messaging**: view_conversations, send_messages, manage_contacts
|
||||
- **Tools & Settings**: manage_settings, manage_users, view_audit_log
|
||||
|
||||
## UI Components
|
||||
|
||||
### Users List Page (`/s/{business}/settings/users`)
|
||||
|
||||
- **Owner Card**: Shows business owner with full access badge
|
||||
- **Search & Filters**: Search by name/email, filter by role, department, and last login date
|
||||
- **Users Table**: Shows user info, role badge, department badges, last login
|
||||
- **Add User Modal**: Create new users with role and department assignment
|
||||
|
||||
### Edit User Page (`/s/{business}/settings/users/{user}/edit`)
|
||||
|
||||
- **User Profile Card**: Avatar, name, email, position
|
||||
- **Basic Information**: Position, company, contact type
|
||||
- **Role Assignment**: Radio buttons for member/manager/admin
|
||||
- **Department Assignments**: Checkboxes with department role selection
|
||||
- **Permission Overrides**: Tabbed interface by suite, grouped by functional area
|
||||
|
||||
## Cross-Division Access
|
||||
|
||||
Users can belong to multiple businesses via the `business_user` pivot table. Each business relationship has its own:
|
||||
|
||||
- Role (owner, admin, manager, member)
|
||||
- Contact type (primary, billing, technical)
|
||||
- Permission overrides
|
||||
|
||||
Department assignments are per-business and control suite feature access.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Database Tables
|
||||
|
||||
- `business_user` - Pivot with role, contact_type, permissions columns
|
||||
- `department_user` - Pivot with department role
|
||||
- `business_suite` - Links businesses to their assigned suites
|
||||
- `department_suite_permissions` - Granular permissions per department/suite
|
||||
|
||||
### Controller Methods
|
||||
|
||||
- `SettingsController::users()` - List users with filters
|
||||
- `SettingsController::editUser()` - Show edit form with suite permissions
|
||||
- `SettingsController::updateUser()` - Save role, departments, and permissions
|
||||
- `SettingsController::inviteUser()` - Create new user with role and departments
|
||||
- `SettingsController::removeUser()` - Remove user from business
|
||||
|
||||
### Permission Checking
|
||||
|
||||
Use the `DepartmentSuitePermission::SUITE_PERMISSIONS` constant to get available permissions for each suite. Permissions are stored as a JSON array in the `business_user.permissions` column for individual overrides.
|
||||
@@ -1,309 +0,0 @@
|
||||
# Suites & Pricing Model – Architecture Overview
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This document defines:
|
||||
|
||||
- The **Suite architecture** (Sales, Processing, Manufacturing, Delivery, Management, Enterprise).
|
||||
- How each business (Cannabrands, Curagreen, Leopard AZ, Canopy) is mapped to Suites.
|
||||
- How menus/navigation should eventually be suite-driven.
|
||||
- The **pricing and usage model** for the Sales Suite.
|
||||
- Ground rules to prevent breaking existing behavior while we evolve the system.
|
||||
|
||||
**This is the ground truth for implementation and future design.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Suites Overview
|
||||
|
||||
We model functionality in high-level **Suites**, not scattered feature flags.
|
||||
|
||||
### 2.1 Sales Suite (Usage-Tiered, Sold to Customers)
|
||||
|
||||
The commercial brain for brands and sales orgs. Includes:
|
||||
|
||||
- Inventory & Product Catalog
|
||||
- Basic BOM & Assemblies (product component tracking for B2B marketplace operations)
|
||||
- Menus & Promotions
|
||||
- Buyers & Accounts (CRM)
|
||||
- Conversations & Messaging (omnichannel)
|
||||
- Marketing & Campaigns
|
||||
- Buyer Intelligence
|
||||
- Analytics
|
||||
- Automations & Orchestrator (sales-side)
|
||||
- Copilot / AI Assist
|
||||
- Brand Dashboards & Business Settings
|
||||
|
||||
**Only the Sales Suite is priced and usage-tiered.**
|
||||
|
||||
### 2.2 Processing Suite (Internal / Enterprise Only)
|
||||
|
||||
Used by processing entities (e.g., Curagreen, Leopard AZ solventless).
|
||||
|
||||
Includes:
|
||||
|
||||
- Biomass intake
|
||||
- Batches and runs
|
||||
- BHO extraction (Curagreen)
|
||||
- Solventless extraction – washing & pressing (Leopard AZ)
|
||||
- Yield tracking & conversions
|
||||
- Material transfers between entities
|
||||
- Work Orders from Sales → Processing
|
||||
- Processing analytics
|
||||
|
||||
### 2.3 Manufacturing Suite (Internal / Enterprise Only)
|
||||
|
||||
Used by manufacturing entities (e.g., Leopard AZ packaging & production).
|
||||
|
||||
Includes:
|
||||
|
||||
- Work Orders from Sales/Processing
|
||||
- BOM (Bill of Materials)
|
||||
- Packaging & labeling
|
||||
- SKU creation from batches
|
||||
- Lot tracking
|
||||
- Production queues & status
|
||||
- Manufacturing analytics
|
||||
|
||||
### 2.4 Delivery Suite (Internal / Enterprise Only)
|
||||
|
||||
Used by delivery/fulfillment operations (e.g., Leopard AZ).
|
||||
|
||||
Includes:
|
||||
|
||||
- Pick/Pack screens
|
||||
- Delivery manifests
|
||||
- Driver / route dashboards
|
||||
- Proof of delivery (POD)
|
||||
- Delivery analytics
|
||||
|
||||
### 2.5 Management Suite (Internal / Enterprise Only)
|
||||
|
||||
Used by top-level management (e.g., Canopy).
|
||||
|
||||
Includes:
|
||||
|
||||
- Org-wide dashboard
|
||||
- Cross-business analytics
|
||||
- Finance / AR / AP views
|
||||
- Forecasting
|
||||
- Operational KPIs
|
||||
- Usage & billing analytics
|
||||
|
||||
### 2.6 Enterprise Plan (Internal Only)
|
||||
|
||||
**Note:** Enterprise is NOT a Suite - it's a plan/billing override.
|
||||
|
||||
Enterprise Plan means:
|
||||
- **Usage limits are bypassed** (brands, SKUs, contacts, messages, AI credits, etc.)
|
||||
- **Feature access is still controlled by assigned Suites** (Sales, Processing, Manufacturing, etc.)
|
||||
|
||||
A business with Enterprise Plan enabled (`is_enterprise_plan = true`) still needs actual Suites assigned to determine which features/menus are available. Enterprise only removes the usage caps.
|
||||
|
||||
**This is not sold; used only for internal operations where usage limits shouldn't apply.**
|
||||
|
||||
---
|
||||
|
||||
## 3. Business → Suite Mapping
|
||||
|
||||
### 3.1 Cannabrands
|
||||
|
||||
- **Role:** Sales & brand representation
|
||||
- **Suites:**
|
||||
- Sales Suite (full)
|
||||
- Enterprise Sales access internally (for our own brands)
|
||||
|
||||
### 3.2 Curagreen
|
||||
|
||||
- **Role:** Processing – BHO
|
||||
- **Suites:**
|
||||
- Processing Suite
|
||||
|
||||
### 3.3 Leopard AZ
|
||||
|
||||
- **Role:** Solventless processing + Manufacturing + Delivery
|
||||
- **Suites:**
|
||||
- Processing Suite (solventless)
|
||||
- Manufacturing Suite
|
||||
- Delivery Suite
|
||||
|
||||
### 3.4 Canopy
|
||||
|
||||
- **Role:** Financial & management entity
|
||||
- **Suites:**
|
||||
- Management Suite
|
||||
|
||||
### 3.5 Internal Master / Owner Account
|
||||
|
||||
- **Role:** Internal full control
|
||||
- **Plan:** Enterprise Plan (`is_enterprise_plan = true`)
|
||||
- **Suites:** All operational suites as needed (Sales, Processing, Manufacturing, etc.)
|
||||
- **Note:** Enterprise Plan bypasses usage limits; actual feature access controlled by suite assignments
|
||||
|
||||
---
|
||||
|
||||
## 4. Suite-Driven Navigation (Target State)
|
||||
|
||||
### 4.1 Concept
|
||||
|
||||
When a user logs in:
|
||||
|
||||
1. We determine their **Business**.
|
||||
2. We read the **Suites** assigned to that Business.
|
||||
3. We build the sidebar/top navigation based on Suites, not random feature flags.
|
||||
|
||||
We eventually want per-suite menus like:
|
||||
|
||||
#### Sales Suite Menu (for Cannabrands)
|
||||
|
||||
- Dashboard
|
||||
- Brands
|
||||
- Inventory
|
||||
- Menus
|
||||
- Promotions
|
||||
- Buyers & Accounts (CRM)
|
||||
- Conversations
|
||||
- Messaging
|
||||
- Automations
|
||||
- Copilot
|
||||
- Analytics
|
||||
- Settings
|
||||
|
||||
#### Processing Suite Menu (Curagreen / Leopard AZ Processing)
|
||||
|
||||
- Dashboard
|
||||
- Batches / Runs
|
||||
- Biomass Intake
|
||||
- Washing / Pressing (solventless)
|
||||
- Extraction Runs (BHO)
|
||||
- Yields
|
||||
- Material Transfers
|
||||
- Work Orders
|
||||
- Processing Analytics
|
||||
- Settings
|
||||
|
||||
#### Manufacturing Suite Menu (Leopard AZ)
|
||||
|
||||
- Dashboard
|
||||
- Work Orders
|
||||
- BOM
|
||||
- Packaging
|
||||
- Labeling
|
||||
- SKU Creation
|
||||
- Lot Tracking
|
||||
- Production Queue
|
||||
- Manufacturing Analytics
|
||||
- Settings
|
||||
|
||||
#### Delivery Suite Menu (Leopard AZ)
|
||||
|
||||
- Dashboard
|
||||
- Pick & Pack
|
||||
- Delivery Manifests
|
||||
- Driver / Route View
|
||||
- Proof of Delivery
|
||||
- Delivery Analytics
|
||||
- Settings
|
||||
|
||||
#### Management Suite Menu (Canopy)
|
||||
|
||||
- Org Dashboard
|
||||
- Finance / AR / AP
|
||||
- Cross-Business Analytics
|
||||
- Forecasting
|
||||
- Operations Overview
|
||||
- Usage & Billing
|
||||
- Settings
|
||||
|
||||
**Important:** While we move toward this, the existing menu remains the default. New suite-driven menus must be gated behind a config flag (e.g., `app.suites.enabled`) and can be turned off if needed.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sales Suite Pricing & Usage Model
|
||||
|
||||
### 5.1 Pricing
|
||||
|
||||
**Base Sales Suite Plan:**
|
||||
|
||||
- **$495 / month**
|
||||
- Includes **1 brand**
|
||||
|
||||
**Additional Brands:**
|
||||
|
||||
- **$195 / month** per additional brand
|
||||
- Each additional brand comes with its own usage allotment (below).
|
||||
|
||||
**No public free tier.** Enterprise internal plan is not sold.
|
||||
|
||||
### 5.2 Included Features (Per Brand)
|
||||
|
||||
Each brand under the Sales Suite gets:
|
||||
|
||||
- Full Inventory & Product Management
|
||||
- Menus & Promotions
|
||||
- Buyers & Accounts (CRM)
|
||||
- Conversations & Messaging
|
||||
- Marketing & Campaign Tools
|
||||
- Buyer Intelligence
|
||||
- Analytics (unlimited views)
|
||||
- Automations & Orchestrator (sales-side)
|
||||
- Copilot (AI-assisted workflows)
|
||||
|
||||
### 5.3 Usage Limits (Per Brand) – Initial Targets
|
||||
|
||||
| Resource | Limit |
|
||||
|----------|-------|
|
||||
| **SKUs** | 15 SKUs per brand |
|
||||
| **Menu Sends** | 100 menu sends per month |
|
||||
| **Promotion Impressions** | 1,000 promotion impressions per month |
|
||||
| **Messaging** | 500 messages per month (SMS, email, in-app, WhatsApp combined) |
|
||||
| **AI Credits** | Initial placeholder: 1,000 AI credits per brand per month. Final number to be adjusted based on AI provider cost and token usage analysis. |
|
||||
| **Buyer & CRM Contacts** | 1,000 buyers/contacts per brand |
|
||||
| **Analytics** | Unlimited analytics views. Not usage-gated. |
|
||||
|
||||
### 5.4 Add-ons (Concept)
|
||||
|
||||
Add-on packs (for later implementation):
|
||||
|
||||
- Extra SKU packs (e.g., +10, +25, +100)
|
||||
- Extra menu send packs
|
||||
- Extra promotion impression packs
|
||||
- Extra CRM contact packs
|
||||
- Extra AI credit packs
|
||||
- Extra messaging volume
|
||||
|
||||
---
|
||||
|
||||
## 6. Safety & Backward Compatibility Constraints
|
||||
|
||||
1. **Existing navigation and behavior must be preserved** until suite-based nav is explicitly enabled.
|
||||
|
||||
2. **Changes should be additive:**
|
||||
- New DB columns for Suites and usage.
|
||||
- New services for Suite→Menu and usage tracking.
|
||||
- New config for `app.suites.enabled`.
|
||||
|
||||
3. **Migrations must be reversible.**
|
||||
|
||||
4. **Feature-flag or config-based toggles** should allow:
|
||||
- Switching between current menu and suite-driven menu.
|
||||
- Disabling suite enforcement if issues arise.
|
||||
|
||||
5. **Do not remove existing module flags yet;** relegate them to an "Advanced Overrides" admin area.
|
||||
|
||||
---
|
||||
|
||||
## 7. Implementation Phases
|
||||
|
||||
| Phase | Description |
|
||||
|-------|-------------|
|
||||
| **Phase 1** | Suites & Docs (this doc) – Define Suites, business mapping, and pricing. |
|
||||
| **Phase 2** | Suite Columns & Admin UI – Add suite flags to businesses. Admin can assign Suites to a Business. |
|
||||
| **Phase 3** | Suite-Driven Menu Resolver (Behind a Flag) – Implement Suite→Menu mapping. Allow switching between legacy and suite-based menus via config. |
|
||||
| **Phase 4** | Usage Counters – Track menu sends, messages, AI usage, contacts, and SKUs per brand. |
|
||||
| **Phase 5** | Usage Enforcement & Warnings – Soft and hard limits based on pricing model. Usage dashboards. |
|
||||
| **Phase 6** | Public Pricing Site & UI Tiles – Build marketing-facing pricing components based on `/docs/marketing/PRICING_PAGE_CONTENT.md`. |
|
||||
|
||||
---
|
||||
|
||||
**This document is the canonical reference for all of the above.**
|
||||
@@ -68,7 +68,7 @@ spec:
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /up
|
||||
port: 80
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 10
|
||||
@@ -77,7 +77,7 @@ spec:
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /up
|
||||
port: 80
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 5
|
||||
|
||||
@@ -107,33 +107,69 @@
|
||||
<option value="technical" {{ old('contact_type', $user->businesses->first()->pivot->contact_type ?? '') === 'technical' ? 'selected' : '' }}>Technical Contact</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Role</span>
|
||||
</label>
|
||||
<select name="role" class="select select-bordered" {{ $isOwner ? 'disabled' : '' }}>
|
||||
<option value="staff" {{ old('role', $user->businesses->first()->pivot->role ?? 'staff') === 'staff' ? 'selected' : '' }}>Staff</option>
|
||||
<option value="manager" {{ old('role', $user->businesses->first()->pivot->role ?? '') === 'manager' ? 'selected' : '' }}>Manager</option>
|
||||
<option value="admin" {{ old('role', $user->businesses->first()->pivot->role ?? '') === 'admin' ? 'selected' : '' }}>Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Role Template (Optional)</span>
|
||||
</label>
|
||||
<select name="role_template" class="select select-bordered" {{ $isOwner ? 'disabled' : '' }}>
|
||||
<option value="">No Template</option>
|
||||
<option value="sales" {{ old('role_template', $user->businesses->first()->pivot->role_template ?? '') === 'sales' ? 'selected' : '' }}>Sales</option>
|
||||
<option value="accounting" {{ old('role_template', $user->businesses->first()->pivot->role_template ?? '') === 'accounting' ? 'selected' : '' }}>Accounting</option>
|
||||
<option value="manufacturing" {{ old('role_template', $user->businesses->first()->pivot->role_template ?? '') === 'manufacturing' ? 'selected' : '' }}>Manufacturing</option>
|
||||
<option value="processing" {{ old('role_template', $user->businesses->first()->pivot->role_template ?? '') === 'processing' ? 'selected' : '' }}>Processing</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Role Assignment -->
|
||||
@if(!$isOwner)
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg mb-4">
|
||||
<span class="icon-[heroicons--shield-check] size-5"></span>
|
||||
Role
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/70 mb-4">
|
||||
The user's role determines their base level of access. Owners have full access, Admins have nearly full access, Managers can manage their team, and Members have department-based permissions.
|
||||
</p>
|
||||
|
||||
@php
|
||||
$currentRole = old('role', $user->businesses->first()->pivot->role ?? 'member');
|
||||
@endphp
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="role" value="member" class="peer sr-only" {{ $currentRole === 'member' ? 'checked' : '' }} />
|
||||
<div class="border-2 border-base-300 rounded-lg p-4 peer-checked:border-primary peer-checked:bg-primary/5 transition-all h-full">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="icon-[heroicons--user] size-5 mt-0.5"></span>
|
||||
<div>
|
||||
<div class="font-semibold">Member</div>
|
||||
<div class="text-xs text-base-content/60 mt-1">Standard team member with department-based permissions only</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="role" value="manager" class="peer sr-only" {{ $currentRole === 'manager' ? 'checked' : '' }} />
|
||||
<div class="border-2 border-base-300 rounded-lg p-4 peer-checked:border-primary peer-checked:bg-primary/5 transition-all h-full">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="icon-[heroicons--user-group] size-5 mt-0.5"></span>
|
||||
<div>
|
||||
<div class="font-semibold">Manager</div>
|
||||
<div class="text-xs text-base-content/60 mt-1">Department permissions + can manage team and approve requests</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="role" value="admin" class="peer sr-only" {{ $currentRole === 'admin' ? 'checked' : '' }} />
|
||||
<div class="border-2 border-base-300 rounded-lg p-4 peer-checked:border-primary peer-checked:bg-primary/5 transition-all h-full">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="icon-[heroicons--shield-check] size-5 mt-0.5"></span>
|
||||
<div>
|
||||
<div class="font-semibold">Admin</div>
|
||||
<div class="text-xs text-base-content/60 mt-1">Full access to all features except owner-only actions</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Department Assignments -->
|
||||
@if($departments->count() > 0)
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
@@ -142,7 +178,9 @@
|
||||
<span class="icon-[heroicons--building-office-2] size-5"></span>
|
||||
Department Assignments
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/70 mb-4">Assign this user to specific departments and define their role within each.</p>
|
||||
<p class="text-sm text-base-content/70 mb-4">
|
||||
Assign this user to departments and define their role within each. Department membership controls which suite features they can access.
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
@foreach($departments as $department)
|
||||
@@ -150,7 +188,7 @@
|
||||
$isAssigned = $user->departments->contains($department->id);
|
||||
$userDeptRole = $user->departments->find($department->id)?->pivot->role ?? 'operator';
|
||||
@endphp
|
||||
<div class="p-4 border border-base-300 rounded-box">
|
||||
<div class="p-4 border border-base-300 rounded-box {{ $isAssigned ? 'bg-primary/5 border-primary/30' : '' }}">
|
||||
<label class="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -159,23 +197,26 @@
|
||||
class="checkbox checkbox-sm mt-1"
|
||||
{{ $isAssigned ? 'checked' : '' }}
|
||||
{{ $isOwner ? 'disabled' : '' }}
|
||||
onchange="this.closest('.p-4').querySelector('select').disabled = !this.checked"
|
||||
onchange="this.closest('.p-4').querySelector('select').disabled = !this.checked; this.closest('.p-4').classList.toggle('bg-primary/5', this.checked); this.closest('.p-4').classList.toggle('border-primary/30', this.checked);"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">{{ $department->name }}</div>
|
||||
@if($department->description)
|
||||
<div class="text-sm text-base-content/60 mt-1">{{ $department->description }}</div>
|
||||
@endif
|
||||
<div class="mt-2">
|
||||
<div class="mt-3">
|
||||
<label class="label py-1">
|
||||
<span class="label-text text-xs font-medium">Department Role</span>
|
||||
</label>
|
||||
<select
|
||||
name="departments[{{ $department->id }}][role]"
|
||||
class="select select-sm select-bordered"
|
||||
class="select select-sm select-bordered w-full max-w-xs"
|
||||
{{ !$isAssigned || $isOwner ? 'disabled' : '' }}
|
||||
>
|
||||
<option value="operator" {{ $userDeptRole === 'operator' ? 'selected' : '' }}>Operator</option>
|
||||
<option value="lead" {{ $userDeptRole === 'lead' ? 'selected' : '' }}>Lead</option>
|
||||
<option value="supervisor" {{ $userDeptRole === 'supervisor' ? 'selected' : '' }}>Supervisor</option>
|
||||
<option value="manager" {{ $userDeptRole === 'manager' ? 'selected' : '' }}>Manager</option>
|
||||
<option value="operator" {{ $userDeptRole === 'operator' ? 'selected' : '' }}>Operator - Basic access</option>
|
||||
<option value="lead" {{ $userDeptRole === 'lead' ? 'selected' : '' }}>Lead - Can assign work</option>
|
||||
<option value="supervisor" {{ $userDeptRole === 'supervisor' ? 'selected' : '' }}>Supervisor - Can approve requests</option>
|
||||
<option value="manager" {{ $userDeptRole === 'manager' ? 'selected' : '' }}>Manager - Full department access</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,49 +228,78 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Permissions -->
|
||||
@if(!$isOwner)
|
||||
<!-- Suite Permissions -->
|
||||
@if(!$isOwner && !empty($suitePermissions))
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg mb-4">
|
||||
<span class="icon-[heroicons--shield-check] size-5"></span>
|
||||
Permissions
|
||||
<span class="icon-[heroicons--key] size-5"></span>
|
||||
Permission Overrides
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/70 mb-4">Control what this user can see and do within the system.</p>
|
||||
<p class="text-sm text-base-content/70 mb-4">
|
||||
By default, users inherit permissions from their role and department assignments. Use these overrides to grant or revoke specific permissions for this user.
|
||||
</p>
|
||||
|
||||
@php
|
||||
$userPermissions = old('permissions', $user->businesses->first()->pivot->permissions ?? []);
|
||||
@endphp
|
||||
<!-- Suite Tabs -->
|
||||
<div class="tabs tabs-boxed bg-base-200 mb-4">
|
||||
@foreach($suitePermissions as $suiteKey => $suite)
|
||||
<button
|
||||
type="button"
|
||||
class="tab {{ $loop->first ? 'tab-active' : '' }}"
|
||||
onclick="showSuitePermissions('{{ $suiteKey }}')"
|
||||
id="tab-{{ $suiteKey }}"
|
||||
>
|
||||
<span class="icon-[{{ $suite['icon'] }}] size-4 mr-2"></span>
|
||||
{{ $suite['name'] }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@foreach($permissionCategories as $categoryKey => $category)
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="icon-[{{ $category['icon'] }}] size-5"></span>
|
||||
<h4 class="font-semibold">{{ $category['name'] }}</h4>
|
||||
<!-- Suite Permission Panels -->
|
||||
@foreach($suitePermissions as $suiteKey => $suite)
|
||||
<div
|
||||
id="panel-{{ $suiteKey }}"
|
||||
class="suite-panel {{ $loop->first ? '' : 'hidden' }}"
|
||||
>
|
||||
<div class="p-4 bg-base-200 rounded-box mb-4">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="icon-[{{ $suite['icon'] }}] size-5"></span>
|
||||
<span class="font-semibold">{{ $suite['name'] }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-base-content/70">{{ $suite['description'] }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3 pl-7">
|
||||
@foreach($category['permissions'] as $permKey => $permission)
|
||||
<label class="flex items-start gap-3 p-3 border border-base-300 rounded-box cursor-pointer hover:bg-base-200/50 transition-colors">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions[]"
|
||||
value="{{ $permKey }}"
|
||||
class="checkbox checkbox-sm mt-0.5"
|
||||
{{ in_array($permKey, $userPermissions) ? 'checked' : '' }}
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-sm">{{ $permission['name'] }}</div>
|
||||
<div class="text-xs text-base-content/60 mt-0.5">{{ $permission['description'] }}</div>
|
||||
</div>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
@foreach($suite['groups'] as $groupKey => $group)
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="icon-[{{ $group['icon'] }}] size-4 text-base-content/70"></span>
|
||||
<h5 class="font-semibold text-sm">{{ $group['name'] }}</h5>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-2">
|
||||
@foreach($group['permissions'] as $permKey => $permDescription)
|
||||
<label class="flex items-start gap-3 p-3 border border-base-300 rounded-lg cursor-pointer hover:bg-base-200/50 transition-colors">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions[]"
|
||||
value="{{ $permKey }}"
|
||||
class="checkbox checkbox-sm mt-0.5"
|
||||
{{ in_array($permKey, $userPermissions) ? 'checked' : '' }}
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-sm">{{ ucwords(str_replace('_', ' ', $permKey)) }}</div>
|
||||
<div class="text-xs text-base-content/60 mt-0.5">{{ $permDescription }}</div>
|
||||
</div>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!$loop->last)
|
||||
<hr class="border-base-300 my-4">
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if(!$loop->last)
|
||||
<hr class="border-base-300 my-4">
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@@ -244,7 +314,7 @@
|
||||
|
||||
@if(!$isOwner)
|
||||
<div class="flex gap-2">
|
||||
<button type="button" onclick="confirm('Are you sure you want to remove this user?') && document.getElementById('delete-form').submit()" class="btn btn-error btn-outline gap-2">
|
||||
<button type="button" onclick="confirm('Are you sure you want to remove this user from the business?') && document.getElementById('delete-form').submit()" class="btn btn-error btn-outline gap-2">
|
||||
<span class="icon-[heroicons--user-minus] size-4"></span>
|
||||
Remove User
|
||||
</button>
|
||||
@@ -264,4 +334,24 @@
|
||||
@method('DELETE')
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<script>
|
||||
function showSuitePermissions(suiteKey) {
|
||||
// Hide all panels
|
||||
document.querySelectorAll('.suite-panel').forEach(panel => {
|
||||
panel.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Deactivate all tabs
|
||||
document.querySelectorAll('.tabs .tab').forEach(tab => {
|
||||
tab.classList.remove('tab-active');
|
||||
});
|
||||
|
||||
// Show selected panel
|
||||
document.getElementById('panel-' + suiteKey).classList.remove('hidden');
|
||||
|
||||
// Activate selected tab
|
||||
document.getElementById('tab-' + suiteKey).classList.add('tab-active');
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@@ -5,22 +5,64 @@
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">Manage Users</h1>
|
||||
<p class="text-sm text-base-content/60 mt-1">Manage the permissions for your users.</p>
|
||||
<p class="text-sm text-base-content/60 mt-1">Manage users, roles, and permissions for your team.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.settings.index', $business->slug) }}">Settings</a></li>
|
||||
<li class="opacity-60">Users & Roles</li>
|
||||
<li class="opacity-60">Users</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary gap-2" onclick="add_user_modal.showModal()">
|
||||
<span class="icon-[heroicons--plus] size-4"></span>
|
||||
Add users
|
||||
Add User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div role="alert" class="alert alert-success mb-6">
|
||||
<span class="icon-[heroicons--check-circle] size-5"></span>
|
||||
<span>{{ session('success') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div role="alert" class="alert alert-error mb-6">
|
||||
<span class="icon-[heroicons--exclamation-circle] size-5"></span>
|
||||
<span>{{ session('error') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Business Owner Card -->
|
||||
@if($owner)
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content w-12 h-12 rounded-full">
|
||||
<span class="text-lg">{{ strtoupper(substr($owner->first_name ?? $owner->name, 0, 1)) }}{{ strtoupper(substr($owner->last_name ?? '', 0, 1)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-semibold">{{ $owner->name }}</h3>
|
||||
<div class="badge badge-primary badge-sm">Owner</div>
|
||||
</div>
|
||||
<p class="text-sm text-base-content/70">{{ $owner->email }}</p>
|
||||
</div>
|
||||
<div class="text-right text-sm text-base-content/60">
|
||||
<div>Full access to all features</div>
|
||||
@if($owner->last_login_at)
|
||||
<div class="text-xs mt-1">Last login: {{ $owner->last_login_at->diffForHumans() }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
@@ -43,20 +85,30 @@
|
||||
|
||||
<!-- Filter Selectors -->
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<!-- Account Type Filter -->
|
||||
<div class="form-control flex-1 min-w-[200px]">
|
||||
<select name="account_type" class="select select-bordered select-sm">
|
||||
<option value="">All Account Types</option>
|
||||
<option value="company-owner" {{ request('account_type') === 'company-owner' ? 'selected' : '' }}>Owner</option>
|
||||
<option value="company-manager" {{ request('account_type') === 'company-manager' ? 'selected' : '' }}>Manager</option>
|
||||
<option value="company-user" {{ request('account_type') === 'company-user' ? 'selected' : '' }}>Staff</option>
|
||||
<option value="company-sales" {{ request('account_type') === 'company-sales' ? 'selected' : '' }}>Sales</option>
|
||||
<option value="company-accounting" {{ request('account_type') === 'company-accounting' ? 'selected' : '' }}>Accounting</option>
|
||||
<option value="company-manufacturing" {{ request('account_type') === 'company-manufacturing' ? 'selected' : '' }}>Manufacturing</option>
|
||||
<option value="company-processing" {{ request('account_type') === 'company-processing' ? 'selected' : '' }}>Processing</option>
|
||||
<!-- Role Filter -->
|
||||
<div class="form-control flex-1 min-w-[150px]">
|
||||
<select name="role" class="select select-bordered select-sm">
|
||||
<option value="">All Roles</option>
|
||||
<option value="admin" {{ request('role') === 'admin' ? 'selected' : '' }}>Admin</option>
|
||||
<option value="manager" {{ request('role') === 'manager' ? 'selected' : '' }}>Manager</option>
|
||||
<option value="member" {{ request('role') === 'member' ? 'selected' : '' }}>Member</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Department Filter -->
|
||||
@if($departments->count() > 0)
|
||||
<div class="form-control flex-1 min-w-[200px]">
|
||||
<select name="department_id" class="select select-bordered select-sm">
|
||||
<option value="">All Departments</option>
|
||||
@foreach($departments as $department)
|
||||
<option value="{{ $department->id }}" {{ request('department_id') == $department->id ? 'selected' : '' }}>
|
||||
{{ $department->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Last Login Date Range -->
|
||||
<div class="form-control flex-1 min-w-[150px]">
|
||||
<input
|
||||
@@ -103,13 +155,7 @@
|
||||
<th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[heroicons--user] size-4"></span>
|
||||
Name
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[heroicons--envelope] size-4"></span>
|
||||
Email
|
||||
User
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
@@ -118,6 +164,12 @@
|
||||
Role
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[heroicons--building-office-2] size-4"></span>
|
||||
Departments
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[heroicons--clock] size-4"></span>
|
||||
@@ -129,36 +181,53 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($users as $user)
|
||||
@php
|
||||
$pivot = $user->pivot;
|
||||
$userRole = $pivot->role ?? 'member';
|
||||
$isOwner = $business->owner_user_id === $user->id;
|
||||
@endphp
|
||||
<tr class="hover:bg-base-200/50 transition-colors">
|
||||
<td>
|
||||
<div class="font-semibold">{{ $user->name }}</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-neutral text-neutral-content w-10 h-10 rounded-full">
|
||||
<span>{{ strtoupper(substr($user->first_name ?? $user->name, 0, 1)) }}{{ strtoupper(substr($user->last_name ?? '', 0, 1)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold">{{ $user->name }}</div>
|
||||
<div class="text-sm text-base-content/60">{{ $user->email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-sm">{{ $user->email }}</div>
|
||||
</td>
|
||||
<td>
|
||||
@if($user->roles->isNotEmpty())
|
||||
@if($isOwner)
|
||||
<div class="badge badge-primary">Owner</div>
|
||||
@else
|
||||
@php
|
||||
$roleName = $user->roles->first()->name;
|
||||
$displayName = match($roleName) {
|
||||
'company-owner' => 'Owner',
|
||||
'company-manager' => 'Manager',
|
||||
'company-user' => 'Staff',
|
||||
'company-sales' => 'Sales',
|
||||
'company-accounting' => 'Accounting',
|
||||
'company-manufacturing' => 'Manufacturing',
|
||||
'company-processing' => 'Processing',
|
||||
'buyer-owner' => 'Buyer Owner',
|
||||
'buyer-manager' => 'Buyer Manager',
|
||||
'buyer-user' => 'Buyer Staff',
|
||||
default => ucwords(str_replace('-', ' ', $roleName))
|
||||
};
|
||||
$roleColors = [
|
||||
'admin' => 'badge-secondary',
|
||||
'manager' => 'badge-accent',
|
||||
'member' => 'badge-ghost',
|
||||
];
|
||||
@endphp
|
||||
<div class="badge badge-ghost badge-sm">
|
||||
{{ $displayName }}
|
||||
<div class="badge {{ $roleColors[$userRole] ?? 'badge-ghost' }}">
|
||||
{{ ucfirst($userRole) }}
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($user->departments->isNotEmpty())
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@foreach($user->departments->take(3) as $dept)
|
||||
<div class="badge badge-outline badge-sm">{{ $dept->name }}</div>
|
||||
@endforeach
|
||||
@if($user->departments->count() > 3)
|
||||
<div class="badge badge-outline badge-sm">+{{ $user->departments->count() - 3 }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<span class="text-base-content/40">—</span>
|
||||
<span class="text-base-content/40">No departments</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@@ -171,10 +240,14 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<a href="{{ route('seller.business.settings.users.edit', [$business->slug, $user->uuid]) }}" class="btn btn-sm btn-ghost gap-2">
|
||||
<span class="icon-[heroicons--pencil] size-4"></span>
|
||||
Edit
|
||||
</a>
|
||||
@if(!$isOwner)
|
||||
<a href="{{ route('seller.business.settings.users.edit', [$business->slug, $user->uuid]) }}" class="btn btn-sm btn-ghost gap-2">
|
||||
<span class="icon-[heroicons--pencil] size-4"></span>
|
||||
Edit
|
||||
</a>
|
||||
@else
|
||||
<span class="text-sm text-base-content/40">Owner</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -197,10 +270,10 @@
|
||||
<div class="text-center py-8 text-base-content/60">
|
||||
<span class="icon-[heroicons--users] size-12 mx-auto mb-2 opacity-30"></span>
|
||||
<p class="text-sm">
|
||||
@if(request()->hasAny(['search', 'account_type', 'last_login_start', 'last_login_end']))
|
||||
@if(request()->hasAny(['search', 'role', 'department_id', 'last_login_start', 'last_login_end']))
|
||||
No users match your filters. Try adjusting your search criteria.
|
||||
@else
|
||||
No users found. Add your first user to get started.
|
||||
No team members yet. Add your first user to get started.
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
@@ -208,6 +281,29 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Suite Info Card -->
|
||||
@if($businessSuites->count() > 0)
|
||||
<div class="card bg-base-100 border border-base-300 mt-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg mb-4">
|
||||
<span class="icon-[heroicons--squares-2x2] size-5"></span>
|
||||
Active Suites
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/70 mb-4">
|
||||
Your business has access to these suites. User permissions are based on their role and department assignments.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($businessSuites as $suite)
|
||||
<div class="badge badge-lg gap-2" style="background-color: var(--color-{{ $suite->color }}-100, oklch(var(--b2))); color: var(--color-{{ $suite->color }}-800, oklch(var(--bc)));">
|
||||
<span class="icon-[{{ $suite->icon }}] size-4"></span>
|
||||
{{ $suite->name }}
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Add User Modal -->
|
||||
<dialog id="add_user_modal" class="modal">
|
||||
<div class="modal-box max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
@@ -226,7 +322,7 @@
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Email</span>
|
||||
<span class="label-text font-medium">Email <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@@ -235,16 +331,13 @@
|
||||
class="input input-bordered w-full"
|
||||
placeholder="user@example.com"
|
||||
/>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-xs text-base-content/60">Add a new or existing user</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Name Fields -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">First Name</span>
|
||||
<span class="label-text font-medium">First Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -255,7 +348,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Last Name</span>
|
||||
<span class="label-text font-medium">Last Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -282,25 +375,13 @@
|
||||
<!-- Position -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Position</span>
|
||||
<span class="label-text font-medium">Position / Title</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="position"
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Company (Read-only) -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="{{ $business->name }}"
|
||||
readonly
|
||||
class="input input-bordered w-full bg-base-200 text-base-content/60"
|
||||
placeholder="e.g. Sales Manager"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -308,87 +389,108 @@
|
||||
|
||||
<hr class="border-base-300 my-6" />
|
||||
|
||||
<!-- Account Type Section -->
|
||||
<!-- Role Section -->
|
||||
<div class="mb-6">
|
||||
<h4 class="font-semibold mb-4 text-base">Account Type</h4>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<h4 class="font-semibold mb-4 text-base">Role</h4>
|
||||
<p class="text-sm text-base-content/70 mb-4">
|
||||
Select the user's role. This determines their base level of access.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="role" value="company-user" class="peer sr-only" checked />
|
||||
<div class="border-2 border-base-300 rounded-lg p-3 peer-checked:border-primary peer-checked:bg-primary/5 transition-all">
|
||||
<div class="font-semibold">Staff</div>
|
||||
<input type="radio" name="role" value="member" class="peer sr-only" checked />
|
||||
<div class="border-2 border-base-300 rounded-lg p-4 peer-checked:border-primary peer-checked:bg-primary/5 transition-all">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="icon-[heroicons--user] size-5"></span>
|
||||
<div>
|
||||
<div class="font-semibold">Member</div>
|
||||
<div class="text-xs text-base-content/60">Standard team member with department-based permissions</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="role" value="company-sales" class="peer sr-only" />
|
||||
<div class="border-2 border-base-300 rounded-lg p-3 peer-checked:border-primary peer-checked:bg-primary/5 transition-all">
|
||||
<div class="font-semibold">Sales</div>
|
||||
<input type="radio" name="role" value="manager" class="peer sr-only" />
|
||||
<div class="border-2 border-base-300 rounded-lg p-4 peer-checked:border-primary peer-checked:bg-primary/5 transition-all">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="icon-[heroicons--user-group] size-5"></span>
|
||||
<div>
|
||||
<div class="font-semibold">Manager</div>
|
||||
<div class="text-xs text-base-content/60">Can manage team members and approve requests</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="role" value="company-accounting" class="peer sr-only" />
|
||||
<div class="border-2 border-base-300 rounded-lg p-3 peer-checked:border-primary peer-checked:bg-primary/5 transition-all">
|
||||
<div class="font-semibold">Accounting</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="role" value="company-manufacturing" class="peer sr-only" />
|
||||
<div class="border-2 border-base-300 rounded-lg p-3 peer-checked:border-primary peer-checked:bg-primary/5 transition-all">
|
||||
<div class="font-semibold">Manufacturing</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="role" value="company-processing" class="peer sr-only" />
|
||||
<div class="border-2 border-base-300 rounded-lg p-3 peer-checked:border-primary peer-checked:bg-primary/5 transition-all">
|
||||
<div class="font-semibold">Processing</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="role" value="company-manager" class="peer sr-only" />
|
||||
<div class="border-2 border-base-300 rounded-lg p-3 peer-checked:border-primary peer-checked:bg-primary/5 transition-all">
|
||||
<div class="font-semibold">Manager</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="role" value="company-owner" class="peer sr-only" />
|
||||
<div class="border-2 border-base-300 rounded-lg p-3 peer-checked:border-primary peer-checked:bg-primary/5 transition-all">
|
||||
<div class="font-semibold">Owner</div>
|
||||
<input type="radio" name="role" value="admin" class="peer sr-only" />
|
||||
<div class="border-2 border-base-300 rounded-lg p-4 peer-checked:border-primary peer-checked:bg-primary/5 transition-all">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="icon-[heroicons--shield-check] size-5"></span>
|
||||
<div>
|
||||
<div class="font-semibold">Admin</div>
|
||||
<div class="text-xs text-base-content/60">Full access except owner-only actions</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-4 p-4 bg-base-200 rounded-box">
|
||||
</div>
|
||||
|
||||
<hr class="border-base-300 my-6" />
|
||||
|
||||
<!-- Department Assignment -->
|
||||
@if($departments->count() > 0)
|
||||
<div class="mb-6">
|
||||
<h4 class="font-semibold mb-4 text-base">Department Assignment</h4>
|
||||
<p class="text-sm text-base-content/70 mb-4">
|
||||
Assign this user to one or more departments. Department membership controls access to suite features.
|
||||
</p>
|
||||
|
||||
<div class="space-y-2">
|
||||
@foreach($departments as $department)
|
||||
<label class="flex items-center gap-3 p-3 border border-base-300 rounded-lg cursor-pointer hover:bg-base-200/50 transition-colors">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="department_ids[]"
|
||||
value="{{ $department->id }}"
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">{{ $department->name }}</div>
|
||||
@if($department->description)
|
||||
<div class="text-xs text-base-content/60">{{ $department->description }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-base-300 my-6" />
|
||||
@endif
|
||||
|
||||
<!-- Contact Settings -->
|
||||
<div class="mb-6">
|
||||
<div class="p-4 bg-base-200 rounded-box">
|
||||
<label class="label cursor-pointer justify-start gap-3 p-0">
|
||||
<input type="checkbox" name="is_point_of_contact" class="checkbox checkbox-sm" />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Is a point of contact</span>
|
||||
<p class="text-xs text-base-content/60 mt-1">
|
||||
If enabled, this user will be automatically listed as a contact for buyers, with their name, job title, email, and phone number visible. If the user is a sales rep, you cannot disable this setting.
|
||||
If enabled, this user will be listed as a contact for buyers, with their name, job title, email, and phone number visible.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-base-300 my-6" />
|
||||
|
||||
<!-- Note about permissions -->
|
||||
<div class="alert bg-base-200 border-base-300 mb-6">
|
||||
<span class="icon-[heroicons--information-circle] size-5 text-base-content/60"></span>
|
||||
<div class="text-sm">
|
||||
<p class="font-semibold">Role-based Access</p>
|
||||
<p class="text-base-content/70">Permissions are determined by the selected account type. Granular permission controls will be available in a future update.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" onclick="add_user_modal.close()" class="btn btn-ghost">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary gap-2">
|
||||
Add user
|
||||
<span class="icon-[heroicons--user-plus] size-4"></span>
|
||||
Add User
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -397,492 +499,4 @@
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Edit User Modals (one per user) -->
|
||||
@foreach($users as $user)
|
||||
@php
|
||||
$nameParts = explode(' ', $user->name, 2);
|
||||
$firstName = $nameParts[0] ?? '';
|
||||
$lastName = $nameParts[1] ?? '';
|
||||
$userRole = $user->roles->first()?->name ?? 'company-user';
|
||||
$pivot = $user->pivot ?? null;
|
||||
$isPointOfContact = $pivot && $pivot->contact_type === 'primary';
|
||||
@endphp
|
||||
|
||||
<dialog id="edit_user_modal_{{ $user->id }}" class="modal">
|
||||
<div class="modal-box max-w-4xl h-[90vh] flex flex-col p-0">
|
||||
<div class="flex-shrink-0 p-6 pb-4 border-b border-base-300">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
<h3 class="font-bold text-lg">Edit User</h3>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('seller.business.settings.users.update', ['business' => $business->slug, 'user' => $user->id]) }}" class="flex flex-col flex-1 min-h-0">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-6 py-4">
|
||||
|
||||
<!-- Account Information Section -->
|
||||
<div class="mb-6">
|
||||
<h4 class="font-semibold mb-4 text-base">Account Information</h4>
|
||||
<div class="space-y-4">
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Email</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value="{{ $user->email }}"
|
||||
required
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Name Fields -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">First Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
value="{{ $firstName }}"
|
||||
required
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Last Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
value="{{ $lastName }}"
|
||||
required
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone Number -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Phone number</span>
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
value="{{ $user->phone }}"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="(XXX) XXX-XXXX"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Position -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Position</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="position"
|
||||
value="{{ $pivot->position ?? '' }}"
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Company (Read-only) -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="{{ $business->name }}"
|
||||
readonly
|
||||
class="input input-bordered w-full bg-base-200 text-base-content/60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-base-300 my-6" />
|
||||
|
||||
<!-- Account Type Section -->
|
||||
<div class="mb-6">
|
||||
<h4 class="font-semibold mb-4 text-base">Account Type</h4>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Role</span>
|
||||
</label>
|
||||
<select name="role" class="select select-bordered w-full" required>
|
||||
<option value="company-user" {{ $userRole === 'company-user' ? 'selected' : '' }}>Staff</option>
|
||||
<option value="company-sales" {{ $userRole === 'company-sales' ? 'selected' : '' }}>Sales</option>
|
||||
<option value="company-accounting" {{ $userRole === 'company-accounting' ? 'selected' : '' }}>Accounting</option>
|
||||
<option value="company-manufacturing" {{ $userRole === 'company-manufacturing' ? 'selected' : '' }}>Manufacturing</option>
|
||||
<option value="company-processing" {{ $userRole === 'company-processing' ? 'selected' : '' }}>Processing</option>
|
||||
<option value="company-manager" {{ $userRole === 'company-manager' ? 'selected' : '' }}>Manager</option>
|
||||
<option value="company-owner" {{ $userRole === 'company-owner' ? 'selected' : '' }}>Owner</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-4 bg-base-200 rounded-box">
|
||||
<label class="label cursor-pointer justify-start gap-3 p-0">
|
||||
<input type="checkbox" name="is_point_of_contact" class="checkbox checkbox-sm" {{ $isPointOfContact ? 'checked' : '' }} />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Is a point of contact</span>
|
||||
<p class="text-xs text-base-content/60 mt-1">
|
||||
If enabled, this user will be automatically listed as a contact for buyers, with their name, job title, email, and phone number visible.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-base-300 my-6" />
|
||||
|
||||
<!-- Permissions Section -->
|
||||
<div class="mb-6">
|
||||
<h4 class="font-semibold mb-4 text-base flex items-center gap-2">
|
||||
<span class="icon-[heroicons--shield-check] size-5"></span>
|
||||
Permissions
|
||||
</h4>
|
||||
|
||||
<!-- Order & Inventory Management -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[heroicons--cube] size-5"></span>
|
||||
<h5 class="font-semibold">Order & Inventory Management</h5>
|
||||
</div>
|
||||
<label class="label cursor-pointer gap-2 p-0">
|
||||
<span class="label-text text-sm">Enable All</span>
|
||||
<input type="checkbox" class="toggle toggle-sm toggle-primary" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 pl-7">
|
||||
<label class="label cursor-pointer justify-start gap-3 p-3 bg-base-100 border border-base-300 rounded-lg">
|
||||
<input type="checkbox" name="permissions[]" value="manage_inventory" class="checkbox checkbox-sm" />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Manage inventory</span>
|
||||
<p class="text-xs text-base-content/60 mt-0.5">Create, edit, and archive products and varieties</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3 p-3 bg-base-100 border border-base-300 rounded-lg">
|
||||
<input type="checkbox" name="permissions[]" value="edit_prices" class="checkbox checkbox-sm" />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Edit prices</span>
|
||||
<p class="text-xs text-base-content/60 mt-0.5">Manipulate product pricing and apply blanket discounts</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3 p-3 bg-base-100 border border-base-300 rounded-lg">
|
||||
<input type="checkbox" name="permissions[]" value="manage_orders_received" class="checkbox checkbox-sm" />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Manage Orders Received</span>
|
||||
<p class="text-xs text-base-content/60 mt-0.5">Update order statuses, create manual orders</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3 p-3 bg-base-100 border border-base-300 rounded-lg">
|
||||
<input type="checkbox" name="permissions[]" value="manage_billing" class="checkbox checkbox-sm" />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Manage billing</span>
|
||||
<p class="text-xs text-base-content/60 mt-0.5">Manage billing information for platform fees (Admin only)</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-base-300 my-4" />
|
||||
|
||||
<!-- Customer Management -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[heroicons--users] size-5"></span>
|
||||
<h5 class="font-semibold">Customer Management</h5>
|
||||
</div>
|
||||
<label class="label cursor-pointer gap-2 p-0">
|
||||
<span class="label-text text-sm">Enable All</span>
|
||||
<input type="checkbox" class="toggle toggle-sm toggle-primary" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 pl-7">
|
||||
<label class="label cursor-pointer justify-start gap-3 p-3 bg-base-100 border border-base-300 rounded-lg">
|
||||
<input type="checkbox" name="permissions[]" value="manage_customers" class="checkbox checkbox-sm" />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Manage Customers and Contacts</span>
|
||||
<p class="text-xs text-base-content/60 mt-0.5">Manage customer records, apply discounts and shipping charges</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3 p-3 bg-base-100 border border-base-300 rounded-lg">
|
||||
<input type="checkbox" name="permissions[]" value="access_sales_reports" class="checkbox checkbox-sm" />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Access sales reports</span>
|
||||
<p class="text-xs text-base-content/60 mt-0.5">Access and download all sales reports and dashboards</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3 p-3 bg-base-100 border border-base-300 rounded-lg">
|
||||
<input type="checkbox" name="permissions[]" value="export_crm" class="checkbox checkbox-sm" />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Export CRM</span>
|
||||
<p class="text-xs text-base-content/60 mt-0.5">Export customers/contacts as a CSV file</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-base-300 my-4" />
|
||||
|
||||
<!-- Logistics -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="icon-[heroicons--truck] size-5"></span>
|
||||
<h5 class="font-semibold">Logistics</h5>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 pl-7">
|
||||
<label class="label cursor-pointer justify-start gap-3 p-3 bg-base-100 border border-base-300 rounded-lg">
|
||||
<input type="checkbox" name="permissions[]" value="manage_fulfillment" class="checkbox checkbox-sm" />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Manage fulfillment</span>
|
||||
<p class="text-xs text-base-content/60 mt-0.5">Access to Fulfillment & Shipment pages and update statuses</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-base-300 my-4" />
|
||||
|
||||
<!-- Email -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="icon-[heroicons--envelope] size-5"></span>
|
||||
<h5 class="font-semibold">Email</h5>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 pl-7">
|
||||
<label class="label cursor-pointer justify-start gap-3 p-3 bg-base-100 border border-base-300 rounded-lg">
|
||||
<input type="checkbox" name="permissions[]" value="receive_order_emails" class="checkbox checkbox-sm" />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Receive New & Accepted order emails</span>
|
||||
<p class="text-xs text-base-content/60 mt-0.5">Checking this box enables user to receive New & Accepted order emails for all customers</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="alert bg-base-200 border-base-300">
|
||||
<span class="icon-[heroicons--information-circle] size-5"></span>
|
||||
<div class="text-sm">
|
||||
By default, all users receive emails for customers in which they are the assigned sales rep
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-base-300 my-4" />
|
||||
|
||||
<!-- Data Control -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="icon-[heroicons--lock-closed] size-5"></span>
|
||||
<h5 class="font-semibold">Data Control</h5>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 pl-7">
|
||||
<label class="label cursor-pointer justify-start gap-3 p-3 bg-base-100 border border-base-300 rounded-lg">
|
||||
<input type="checkbox" name="permissions[]" value="limit_to_assigned_customers" class="checkbox checkbox-sm" />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Limit access to assigned customers</span>
|
||||
<p class="text-xs text-base-content/60 mt-0.5">When enabled, this user can only view/manage customers, contacts, and orders assigned to them</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-base-300 my-4" />
|
||||
|
||||
<!-- Other Settings -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="icon-[heroicons--cog-6-tooth] size-5"></span>
|
||||
<h5 class="font-semibold">Other Settings</h5>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 pl-7">
|
||||
<label class="label cursor-pointer justify-start gap-3 p-3 bg-base-100 border border-base-300 rounded-lg">
|
||||
<input type="checkbox" name="permissions[]" value="access_developer_options" class="checkbox checkbox-sm" />
|
||||
<div class="flex-1">
|
||||
<span class="label-text font-medium">Access Developer Options</span>
|
||||
<p class="text-xs text-base-content/60 mt-0.5">Create and manage Webhooks and API Keys</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-base-300 my-6" />
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<div class="mb-6">
|
||||
<h4 class="font-semibold mb-4 text-base text-error">Danger Zone</h4>
|
||||
<button type="button" class="btn btn-outline btn-error gap-2">
|
||||
<span class="icon-[heroicons--user-minus] size-4"></span>
|
||||
Deactivate User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0 border-t border-base-300 p-6 pt-4">
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button type="button" onclick="edit_user_modal_{{ $user->id }}.close()" class="btn btn-ghost">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary gap-2">
|
||||
<span class="icon-[heroicons--arrow-down-tray] size-4"></span>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
function openEditModal{{ $user->id }}() {
|
||||
document.getElementById('edit_user_modal_{{ $user->id }}').showModal();
|
||||
}
|
||||
</script>
|
||||
@endforeach
|
||||
|
||||
<!-- User Login History Audit Table -->
|
||||
<div class="card bg-base-100 border border-base-300 mt-8">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold flex items-center gap-2">
|
||||
<span class="icon-[heroicons--shield-check] size-5 text-primary"></span>
|
||||
User Login History
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mt-1">Audit log of user authentication activity</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
// TODO: Replace with actual login history data from controller
|
||||
// This requires a login_history table or audit_logs table
|
||||
// Sample data for development/testing
|
||||
$loginHistory = collect([
|
||||
(object) [
|
||||
'user' => (object) ['name' => 'John Smith', 'email' => 'john@cannabrands.biz'],
|
||||
'created_at' => now()->subHours(2),
|
||||
'ip_address' => '192.168.1.100',
|
||||
'user_agent_parsed' => 'Chrome 120 on macOS',
|
||||
'location' => 'Phoenix, AZ',
|
||||
'success' => true,
|
||||
],
|
||||
(object) [
|
||||
'user' => (object) ['name' => 'Sarah Johnson', 'email' => 'sarah@cannabrands.biz'],
|
||||
'created_at' => now()->subHours(5),
|
||||
'ip_address' => '192.168.1.101',
|
||||
'user_agent_parsed' => 'Firefox 121 on Windows 11',
|
||||
'location' => 'Scottsdale, AZ',
|
||||
'success' => true,
|
||||
],
|
||||
(object) [
|
||||
'user' => (object) ['name' => 'Mike Davis', 'email' => 'mike@cannabrands.biz'],
|
||||
'created_at' => now()->subDay(),
|
||||
'ip_address' => '192.168.1.102',
|
||||
'user_agent_parsed' => 'Safari 17 on iPhone',
|
||||
'location' => 'Tempe, AZ',
|
||||
'success' => true,
|
||||
],
|
||||
(object) [
|
||||
'user' => (object) ['name' => 'Unknown User', 'email' => 'test@example.com'],
|
||||
'created_at' => now()->subDay()->subHours(3),
|
||||
'ip_address' => '203.0.113.42',
|
||||
'user_agent_parsed' => 'Chrome 120 on Windows 10',
|
||||
'location' => 'Unknown',
|
||||
'success' => false,
|
||||
],
|
||||
(object) [
|
||||
'user' => (object) ['name' => 'Emily Rodriguez', 'email' => 'emily@cannabrands.biz'],
|
||||
'created_at' => now()->subDays(2),
|
||||
'ip_address' => '192.168.1.103',
|
||||
'user_agent_parsed' => 'Edge 120 on Windows 11',
|
||||
'location' => 'Mesa, AZ',
|
||||
'success' => true,
|
||||
],
|
||||
]);
|
||||
@endphp
|
||||
|
||||
@if($loginHistory->isNotEmpty())
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Date & Time</th>
|
||||
<th>IP Address</th>
|
||||
<th>Device / Browser</th>
|
||||
<th>Location</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($loginHistory as $log)
|
||||
<tr class="hover:bg-base-200/50">
|
||||
<td>
|
||||
<div class="font-medium">{{ $log->user->name }}</div>
|
||||
<div class="text-xs text-base-content/60">{{ $log->user->email }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-sm">{{ $log->created_at->format('M d, Y') }}</div>
|
||||
<div class="text-xs text-base-content/60">{{ $log->created_at->format('g:i A') }}</div>
|
||||
</td>
|
||||
<td class="font-mono text-xs">{{ $log->ip_address }}</td>
|
||||
<td>
|
||||
<div class="text-sm">{{ $log->user_agent_parsed ?? 'Unknown' }}</div>
|
||||
</td>
|
||||
<td class="text-sm">{{ $log->location ?? '—' }}</td>
|
||||
<td>
|
||||
@if($log->success)
|
||||
<div class="badge badge-success badge-sm gap-1">
|
||||
<span class="icon-[heroicons--check] size-3"></span>
|
||||
Success
|
||||
</div>
|
||||
@else
|
||||
<div class="badge badge-error badge-sm gap-1">
|
||||
<span class="icon-[heroicons--x-mark] size-3"></span>
|
||||
Failed
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-12 text-base-content/60">
|
||||
<span class="icon-[heroicons--shield-check] size-12 mx-auto mb-3 opacity-30"></span>
|
||||
<p class="text-sm font-medium">No login history available</p>
|
||||
<p class="text-xs mt-1">User authentication logs will appear here once the audit system is configured.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -28,7 +28,7 @@ export default defineConfig(({ mode }) => {
|
||||
],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
port: parseInt(env.VITE_PORT || '5173'),
|
||||
strictPort: true,
|
||||
origin: viteOrigin,
|
||||
cors: {
|
||||
|
||||
Reference in New Issue
Block a user